2026-07-04 01:53:10 +08:00

260 lines
11 KiB
Go

package cpnotice
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
"hyapp/pkg/tencentim"
"hyapp/pkg/usermq"
)
func TestProcessApplicationCreatedPublishesC2CAndBothRoomIMs(t *testing.T) {
repo := &fakeRepository{}
publisher := &fakePublisher{}
service := New(Config{NodeID: "notice-test"}, repo, publisher)
handled, err := service.ProcessUserOutboxMessage(context.Background(), usermq.UserOutboxMessage{
AppCode: "lalu",
EventID: "cp_application:created:1002:1",
EventType: eventApplicationCreated,
AggregateType: "cp_application",
AggregateID: 1002,
PayloadJSON: applicationPayloadJSON(),
OccurredAtMS: 1700000000000,
}, CPNoticeWorkerOptions{PublishTimeout: time.Second})
if err != nil || !handled {
t.Fatalf("ProcessUserOutboxMessage failed handled=%v err=%v", handled, err)
}
if len(publisher.userMessages) != 1 || publisher.userMessages[0].FromAccount != "1001" || publisher.userMessages[0].ToAccount != "1002" {
t.Fatalf("expected target c2c message, got %+v", publisher.userMessages)
}
if len(publisher.groupMessages) != 2 || publisher.groupMessages[0].GroupID != "room-100" || publisher.groupMessages[1].GroupID != "room-100" {
t.Fatalf("expected target and requester room group messages, got %+v", publisher.groupMessages)
}
targetRoomPayload := mustObject(string(publisher.groupMessages[0].PayloadJSON))
if targetRoomPayload["recipient_user_id"] != "1002" {
t.Fatalf("target room payload mismatch: %+v", targetRoomPayload)
}
requesterRoomPayload := mustObject(string(publisher.groupMessages[1].PayloadJSON))
if requesterRoomPayload["recipient_user_id"] != "1001" {
t.Fatalf("requester room payload mismatch: %+v", requesterRoomPayload)
}
if len(repo.delivered) != 3 || repo.delivered[0].Channel != channelTencentIMC2C || repo.delivered[1].Channel != channelTencentIMRoom || repo.delivered[2].Channel != channelTencentIMRoomSender {
t.Fatalf("delivery channels mismatch: %+v", repo.delivered)
}
}
func TestProcessApplicationAcceptedPublishesMutualC2CAndRoomIM(t *testing.T) {
repo := &fakeRepository{}
publisher := &fakePublisher{}
service := New(Config{NodeID: "notice-test"}, repo, publisher)
handled, err := service.ProcessUserOutboxMessage(context.Background(), usermq.UserOutboxMessage{
AppCode: "lalu",
EventID: "cp_application:accepted:1002:1",
EventType: eventApplicationAccepted,
AggregateType: "cp_application",
AggregateID: 1002,
PayloadJSON: applicationPayloadJSON(),
OccurredAtMS: 1700000000001,
}, CPNoticeWorkerOptions{PublishTimeout: time.Second})
if err != nil || !handled {
t.Fatalf("ProcessUserOutboxMessage failed handled=%v err=%v", handled, err)
}
if len(publisher.userMessages) != 2 {
t.Fatalf("expected mutual c2c messages, got %+v", publisher.userMessages)
}
if publisher.userMessages[0].FromAccount != "1002" || publisher.userMessages[0].ToAccount != "1001" {
t.Fatalf("expected acceptor -> requester c2c message, got %+v", publisher.userMessages[0])
}
requesterPayload := mustObject(string(publisher.userMessages[0].PayloadJSON))
if requesterPayload["c2c_notice_role"] != "accepted_to_requester" || requesterPayload["message_text"] != "我同意了你" {
t.Fatalf("requester c2c payload mismatch: %+v", requesterPayload)
}
if publisher.userMessages[1].FromAccount != "1001" || publisher.userMessages[1].ToAccount != "1002" {
t.Fatalf("expected requester -> acceptor c2c message, got %+v", publisher.userMessages[1])
}
acceptorPayload := mustObject(string(publisher.userMessages[1].PayloadJSON))
if acceptorPayload["c2c_notice_role"] != "accepted_to_acceptor" || acceptorPayload["message_text"] != "对方已同意你的申请" {
t.Fatalf("acceptor c2c payload mismatch: %+v", acceptorPayload)
}
if len(publisher.groupMessages) != 1 || publisher.groupMessages[0].GroupID != "room-100" {
t.Fatalf("expected requester room group message, got %+v", publisher.groupMessages)
}
if len(repo.delivered) != 3 || repo.delivered[0].Channel != channelTencentIMC2C || repo.delivered[1].Channel != channelTencentIMC2CAcceptor || repo.delivered[2].Channel != channelTencentIMRoom {
t.Fatalf("delivery channels mismatch: %+v", repo.delivered)
}
}
func TestProcessApplicationRejectedPublishesRequesterC2CAndRoomIM(t *testing.T) {
repo := &fakeRepository{}
publisher := &fakePublisher{}
service := New(Config{NodeID: "notice-test"}, repo, publisher)
body, _ := json.Marshal(map[string]any{
"application": mustObject(applicationPayloadJSON()),
"reason": "not_now",
})
handled, err := service.ProcessUserOutboxMessage(context.Background(), usermq.UserOutboxMessage{
AppCode: "lalu",
EventID: "cp_application:rejected:1002:1",
EventType: eventApplicationRejected,
AggregateType: "cp_application",
AggregateID: 1002,
PayloadJSON: string(body),
OccurredAtMS: 1700000000001,
}, CPNoticeWorkerOptions{PublishTimeout: time.Second})
if err != nil || !handled {
t.Fatalf("ProcessUserOutboxMessage failed handled=%v err=%v", handled, err)
}
if len(publisher.userMessages) != 1 || publisher.userMessages[0].FromAccount != "1002" || publisher.userMessages[0].ToAccount != "1001" {
t.Fatalf("expected requester c2c message, got %+v", publisher.userMessages)
}
if len(publisher.groupMessages) != 1 || publisher.groupMessages[0].GroupID != "room-100" {
t.Fatalf("expected requester room group message, got %+v", publisher.groupMessages)
}
if len(repo.delivered) != 2 || repo.delivered[0].NoticeType != noticeTypeApplicationRejected || repo.delivered[1].Channel != channelTencentIMRoom {
t.Fatalf("delivery channels mismatch: %+v", repo.delivered)
}
}
func TestProcessRelationshipCreatedPublishesRegionIM(t *testing.T) {
repo := &fakeRepository{}
publisher := &fakePublisher{}
service := New(Config{NodeID: "notice-test", GroupIDPrefix: "test_"}, repo, publisher)
payload := map[string]any{
"application": mustObject(applicationPayloadJSON()),
"relationship": map[string]any{
"RelationshipID": "cp_rel:1001:1002",
},
}
body, _ := json.Marshal(payload)
handled, err := service.ProcessUserOutboxMessage(context.Background(), usermq.UserOutboxMessage{
AppCode: "lalu",
EventID: "cp_relationship:created:1002:1",
EventType: eventRelationshipCreated,
AggregateType: "cp_relationship",
AggregateID: 1002,
PayloadJSON: string(body),
OccurredAtMS: 1700000000002,
}, CPNoticeWorkerOptions{PublishTimeout: time.Second})
if err != nil || !handled {
t.Fatalf("ProcessUserOutboxMessage failed handled=%v err=%v", handled, err)
}
if len(publisher.ensuredGroups) != 1 || publisher.ensuredGroups[0].GroupID != "test_hy_lalu_bc_r_v2_86" {
t.Fatalf("expected region group ensure, got %+v", publisher.ensuredGroups)
}
// v2 区域群是 AVChatRoom(不带 ApplyJoinOption),不再受 ChatRoom 套餐成员上限约束。
if publisher.ensuredGroups[0].Type != tencentim.BroadcastGroupType || publisher.ensuredGroups[0].ApplyJoinOption != "" {
t.Fatalf("v2 region group must be AVChatRoom: %+v", publisher.ensuredGroups[0])
}
// v2 群投递成功后过渡期向 v1 旧群补发一条 _legacy 副本。
if len(publisher.groupMessages) != 2 || publisher.groupMessages[0].GroupID != "test_hy_lalu_bc_r_v2_86" {
t.Fatalf("expected region group message, got %+v", publisher.groupMessages)
}
if publisher.groupMessages[1].GroupID != "test_hy_lalu_bc_r_86" || publisher.groupMessages[1].EventID != "cp_relationship:created:1002:1_legacy" {
t.Fatalf("expected legacy region copy, got %+v", publisher.groupMessages[1])
}
if len(publisher.userMessages) != 0 {
t.Fatalf("relationship created should not publish c2c: %+v", publisher.userMessages)
}
}
func TestPublishFailureMarksRetryable(t *testing.T) {
repo := &fakeRepository{}
publisher := &fakePublisher{err: errors.New("im timeout")}
service := New(Config{NodeID: "notice-test"}, repo, publisher)
_, err := service.ProcessUserOutboxMessage(context.Background(), usermq.UserOutboxMessage{
AppCode: "lalu",
EventID: "cp_application:created:1002:retry",
EventType: eventApplicationCreated,
AggregateType: "cp_application",
AggregateID: 1002,
PayloadJSON: applicationPayloadJSON(),
OccurredAtMS: 1700000000003,
}, CPNoticeWorkerOptions{PublishTimeout: time.Second, MaxRetryCount: 3})
if err != nil {
t.Fatalf("retryable publish should be recorded without returning poison error: %v", err)
}
if len(repo.retryable) == 0 || repo.retryable[0].RetryCount != 1 {
t.Fatalf("expected retryable delivery, got %+v", repo.retryable)
}
}
type fakeRepository struct {
delivered []CPDelivery
retryable []CPDelivery
failed []CPDelivery
}
func (r *fakeRepository) ClaimCPDelivery(_ context.Context, _ string, delivery CPDelivery, _ time.Duration) (CPDelivery, bool, error) {
return delivery, true, nil
}
func (r *fakeRepository) MarkCPDelivered(_ context.Context, delivery CPDelivery, _ []byte, _ int64) error {
r.delivered = append(r.delivered, delivery)
return nil
}
func (r *fakeRepository) MarkCPRetryable(_ context.Context, delivery CPDelivery, retryCount int, _ int64, _ string, _ int64) error {
delivery.RetryCount = retryCount
r.retryable = append(r.retryable, delivery)
return nil
}
func (r *fakeRepository) MarkCPFailed(_ context.Context, delivery CPDelivery, retryCount int, _ string, _ int64) error {
delivery.RetryCount = retryCount
r.failed = append(r.failed, delivery)
return nil
}
type fakePublisher struct {
userMessages []tencentim.CustomUserMessage
groupMessages []tencentim.CustomGroupMessage
ensuredGroups []tencentim.GroupSpec
err error
}
func (p *fakePublisher) PublishUserCustomMessage(_ context.Context, message tencentim.CustomUserMessage) error {
p.userMessages = append(p.userMessages, message)
return p.err
}
func (p *fakePublisher) PublishGroupCustomMessage(_ context.Context, message tencentim.CustomGroupMessage) error {
p.groupMessages = append(p.groupMessages, message)
return p.err
}
func (p *fakePublisher) EnsureGroup(_ context.Context, spec tencentim.GroupSpec) error {
p.ensuredGroups = append(p.ensuredGroups, spec)
return p.err
}
func applicationPayloadJSON() string {
return `{
"ApplicationID":"cp_app:event-1",
"RelationType":"cp",
"Status":"pending",
"Requester":{"UserID":1001,"DisplayUserID":"163001","Username":"Alice","Avatar":"https://cdn.example/a.png"},
"Target":{"UserID":1002,"DisplayUserID":"163002","Username":"Bob","Avatar":"https://cdn.example/b.png"},
"RoomID":"room-100",
"RoomRegionID":86,
"Gift":{"GiftID":"cp_ring","GiftName":"CP Ring","GiftIconURL":"https://cdn.example/gift.png","GiftAnimationURL":"","GiftCount":1,"GiftValue":100,"BillingReceiptID":"tx-1"},
"ExpiresAtMS":1700003600000,
"DecidedAtMS":0
}`
}
func mustObject(value string) map[string]any {
var out map[string]any
if err := json.Unmarshal([]byte(value), &out); err != nil {
panic(err)
}
return out
}