2026-07-23 16:47:40 +08:00

231 lines
9.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package message
import (
"context"
"encoding/json"
"strconv"
"testing"
"time"
messagedomain "hyapp/services/activity-service/internal/domain/message"
)
func TestConfirmIMPayloadActionsExposeAppButtonLabels(t *testing.T) {
payloadJSON, err := confirmIMPayloadJSON("cfm_1", messagedomain.RoleInvitationConfirmEvent{
InvitationID: 901,
InvitationType: "host",
InviterUserID: 1001,
TargetUserID: 2002,
}, "Alice has invited you to join ABC guild", 1760000000000)
if err != nil {
t.Fatalf("confirmIMPayloadJSON failed: %v", err)
}
var payload struct {
Type string `json:"type"`
Actions []struct {
Action string `json:"action"`
Label string `json:"label"`
Text string `json:"text"`
Style string `json:"style"`
} `json:"actions"`
}
if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
t.Fatalf("payload must be valid JSON: %v", err)
}
if payload.Type != "im_confirm" || len(payload.Actions) != 2 {
t.Fatalf("confirm payload shape mismatch: %+v", payload)
}
// Flutter 现在线上按 label 渲染按钮text 只作为旧解析兼容字段;两个字段必须一起存在。
if payload.Actions[0].Action != "reject" || payload.Actions[0].Label != "Reject" || payload.Actions[0].Text != "Reject" || payload.Actions[0].Style != "secondary" {
t.Fatalf("reject action mismatch: %+v", payload.Actions[0])
}
if payload.Actions[1].Action != "accept" || payload.Actions[1].Label != "Accept" || payload.Actions[1].Text != "Accept" || payload.Actions[1].Style != "primary" {
t.Fatalf("accept action mismatch: %+v", payload.Actions[1])
}
// 外管邀请使用独立 subtype 保护 Lalu 原有邀请状态机,但通知文案仍须
// 呈现实际业务动作,不能退化为无上下文的通用确认提示。
externalText := roleInvitationMessageText(messagedomain.RoleInvitationConfirmEvent{
InvitationType: "external_host",
InviterUserID: 1001,
AgencyName: "ABC",
Inviter: messagedomain.RoleInvitationUserSnapshot{Username: "Alice"},
})
if externalText != "Alice has invited you to join the ABC guild" {
t.Fatalf("external host invitation text mismatch: %q", externalText)
}
}
func TestExternalRoleInvitationCreatesC2CConfirmAndProcessesAppAction(t *testing.T) {
tests := []struct {
invitationType string
agencyName string
wantText string
action string
wantStatus string
}{
{
invitationType: "external_bd",
wantText: "Alice has invited you to become BD",
action: messagedomain.ActionConfirmActionAccept,
wantStatus: messagedomain.ActionConfirmStatusAccepted,
},
{
invitationType: "external_agency",
agencyName: "Team Alpha",
wantText: "Alice has invited you to create the Team Alpha guild",
action: messagedomain.ActionConfirmActionAccept,
wantStatus: messagedomain.ActionConfirmStatusAccepted,
},
{
invitationType: "external_host",
agencyName: "Team Alpha",
wantText: "Alice has invited you to join the Team Alpha guild",
action: messagedomain.ActionConfirmActionReject,
wantStatus: messagedomain.ActionConfirmStatusRejected,
},
}
for index, test := range tests {
t.Run(test.invitationType, func(t *testing.T) {
repository := &actionConfirmRepositoryFake{}
roleClient := &roleInvitationClientFake{invitationType: test.invitationType}
service := NewActionConfirm(Config{NodeID: "activity-test"}, repository, roleClient)
service.SetClock(func() time.Time { return time.UnixMilli(1_760_000_000_000) })
invitationID := int64(901 + index)
event := messagedomain.RoleInvitationConfirmEvent{
SourceEventID: "uout-external-" + test.invitationType,
InvitationID: invitationID,
InvitationType: test.invitationType,
InviterUserID: 1001,
TargetUserID: 2002,
AgencyName: test.agencyName,
Inviter: messagedomain.RoleInvitationUserSnapshot{Username: "Alice"},
CreatedAtMS: 1_760_000_000_000,
RawPayloadJSON: `{"source":"user_outbox"}`,
}
confirm, created, err := service.ConsumeRoleInvitationCreated(context.Background(), event)
if err != nil {
t.Fatalf("ConsumeRoleInvitationCreated failed: %v", err)
}
if !created || confirm.BusinessSubtype != test.invitationType ||
confirm.BusinessID != strconv.FormatInt(invitationID, 10) ||
confirm.SenderUserID != 1001 || confirm.ReceiverUserID != 2002 ||
confirm.MessageText != test.wantText {
t.Fatalf("external invitation confirm projection mismatch: %+v", confirm)
}
var payload confirmIMPayload
if err := json.Unmarshal([]byte(repository.savedOutbox.PayloadJSON), &payload); err != nil {
t.Fatalf("decode C2C confirm payload failed: %v", err)
}
if payload.Type != "im_confirm" ||
payload.BusinessType != messagedomain.ActionConfirmBusinessRoleInvitation ||
payload.BusinessSubtype != test.invitationType ||
payload.SenderUserID != "1001" || payload.ReceiverUserID != "2002" ||
payload.Msg != test.wantText || len(payload.Actions) != 2 {
t.Fatalf("external invitation C2C payload mismatch: %+v", payload)
}
var processed messagedomain.ActionConfirm
if test.action == messagedomain.ActionConfirmActionAccept {
processed, err = service.AcceptActionConfirm(context.Background(), 2002, confirm.ConfirmID, "app-accept-"+test.invitationType)
} else {
processed, err = service.RejectActionConfirm(context.Background(), 2002, confirm.ConfirmID, "app-reject-"+test.invitationType, "not available")
}
if err != nil {
t.Fatalf("process App confirmation failed: %v", err)
}
if processed.Status != test.wantStatus || processed.Action != test.action {
t.Fatalf("App confirmation terminal state mismatch: %+v", processed)
}
if roleClient.lastInput.ActorUserID != 2002 ||
roleClient.lastInput.InvitationID != invitationID ||
roleClient.lastInput.Action != test.action {
t.Fatalf("ProcessRoleInvitation command mismatch: %+v", roleClient.lastInput)
}
})
}
}
type actionConfirmRepositoryFake struct {
savedConfirm messagedomain.ActionConfirm
savedOutbox messagedomain.ActionConfirmOutbox
}
func (r *actionConfirmRepositoryFake) SaveActionConfirmWithOutbox(_ context.Context, confirm messagedomain.ActionConfirm, outbox messagedomain.ActionConfirmOutbox) (bool, messagedomain.ActionConfirm, error) {
r.savedConfirm = confirm
r.savedOutbox = outbox
return true, confirm, nil
}
func (r *actionConfirmRepositoryFake) ClaimActionConfirm(_ context.Context, confirmID string, actorUserID int64, action string, commandID string, _ string, _ int64, _ int64) (messagedomain.ActionConfirm, bool, error) {
confirm := r.savedConfirm
if confirm.ConfirmID != confirmID || confirm.ReceiverUserID != actorUserID {
return messagedomain.ActionConfirm{}, false, nil
}
confirm.Status = messagedomain.ActionConfirmStatusProcessing
confirm.Action = action
confirm.CommandID = commandID
r.savedConfirm = confirm
return confirm, true, nil
}
func (r *actionConfirmRepositoryFake) CompleteActionConfirm(_ context.Context, confirmID string, commandID string, status string, action string, processedAtMS int64) (messagedomain.ActionConfirm, error) {
confirm := r.savedConfirm
confirm.ConfirmID = confirmID
confirm.CommandID = commandID
confirm.Status = status
confirm.Action = action
confirm.ProcessedAtMS = processedAtMS
r.savedConfirm = confirm
return confirm, nil
}
func (r *actionConfirmRepositoryFake) ReleaseActionConfirm(context.Context, string, string, string, int64) error {
return nil
}
func (r *actionConfirmRepositoryFake) BatchGetActionConfirms(context.Context, int64, []string) ([]messagedomain.ActionConfirm, error) {
return nil, nil
}
func (r *actionConfirmRepositoryFake) ListConversationActionConfirms(context.Context, int64, int64, int, int64, string) ([]messagedomain.ActionConfirm, error) {
return nil, nil
}
func (r *actionConfirmRepositoryFake) ClaimPendingActionConfirmOutbox(context.Context, string, int64, int64, int) ([]messagedomain.ActionConfirmOutbox, error) {
return nil, nil
}
func (r *actionConfirmRepositoryFake) MarkActionConfirmOutboxDelivered(context.Context, string, int64) error {
return nil
}
func (r *actionConfirmRepositoryFake) MarkActionConfirmOutboxRetryable(context.Context, string, string, int64, int64) error {
return nil
}
type roleInvitationClientFake struct {
invitationType string
lastInput ProcessRoleInvitationInput
}
func (c *roleInvitationClientFake) ProcessRoleInvitation(_ context.Context, input ProcessRoleInvitationInput) (RoleInvitationState, error) {
c.lastInput = input
status := messagedomain.ActionConfirmStatusAccepted
if input.Action == messagedomain.ActionConfirmActionReject {
status = messagedomain.ActionConfirmStatusRejected
}
return RoleInvitationState{
InvitationID: input.InvitationID,
InvitationType: c.invitationType,
Status: status,
}, nil
}
func (c *roleInvitationClientFake) GetRoleInvitation(_ context.Context, actorUserID int64, invitationID int64) (RoleInvitationState, error) {
return RoleInvitationState{
InvitationID: invitationID,
InvitationType: c.invitationType,
Status: messagedomain.ActionConfirmStatusPending,
}, nil
}