231 lines
9.0 KiB
Go
231 lines
9.0 KiB
Go
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
|
||
}
|