From f89aa37318e7d65a31ced9671a536e81fec54655 Mon Sep 17 00:00:00 2001 From: ZuoZuo <68836346+Mrz-sakura@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:50:33 +0800 Subject: [PATCH] test(notice): cover confirm message delivery --- .../modules/confirmnotice/service_test.go | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 services/notice-service/internal/modules/confirmnotice/service_test.go diff --git a/services/notice-service/internal/modules/confirmnotice/service_test.go b/services/notice-service/internal/modules/confirmnotice/service_test.go new file mode 100644 index 00000000..875e6bc4 --- /dev/null +++ b/services/notice-service/internal/modules/confirmnotice/service_test.go @@ -0,0 +1,168 @@ +package confirmnotice + +import ( + "context" + "errors" + "testing" + "time" + + "hyapp/pkg/messagemq" + "hyapp/pkg/tencentim" +) + +func TestProcessActionOutboxMessagePublishesConfirmC2C(t *testing.T) { + payloadJSON := `{"type":"im_confirm","confirm_id":"cfm_1","sender_user_id":"1001","receiver_user_id":"2002"}` + repository := &confirmRepositoryFake{claimOK: true} + publisher := &confirmPublisherFake{} + service := New(Config{NodeID: "notice-node-1"}, repository, publisher) + + handled, err := service.ProcessActionOutboxMessage(context.Background(), messagemq.ActionOutboxMessage{ + AppCode: " lalu ", + EventID: "evt-confirm-1", + EventType: "ConfirmMessageCreated", + ConfirmID: "cfm_1", + PayloadJSON: payloadJSON, + OccurredAtMS: 1710000000123, + }, WorkerOptions{PublishTimeout: time.Second}) + if err != nil { + t.Fatalf("ProcessActionOutboxMessage failed: %v", err) + } + if !handled { + t.Fatal("ConfirmMessageCreated must be handled") + } + if repository.claimed.EventID != "evt-confirm-1" || repository.claimed.ConfirmID != "cfm_1" { + t.Fatalf("claimed delivery lost outbox identity: %+v", repository.claimed) + } + if repository.claimed.AppCode != "lalu" || repository.claimed.SenderUserID != 1001 || repository.claimed.TargetUserID != 2002 { + t.Fatalf("claimed delivery has incorrect routing fields: %+v", repository.claimed) + } + if len(publisher.messages) != 1 { + t.Fatalf("published messages=%d, want 1", len(publisher.messages)) + } + message := publisher.messages[0] + // 邀请者必须作为发送方、被邀请者必须作为接收方;交换两者会把确认按钮投递给无权处理邀请的人。 + if message.FromAccount != "1001" || message.ToAccount != "2002" { + t.Fatalf("unexpected C2C direction: from=%q to=%q", message.FromAccount, message.ToAccount) + } + if message.EventID != "evt-confirm-1" || message.Desc != "im_confirm" || message.Ext != "im_confirm" { + t.Fatalf("unexpected C2C envelope: %+v", message) + } + if string(message.PayloadJSON) != payloadJSON { + t.Fatalf("payload changed during delivery: got=%s want=%s", message.PayloadJSON, payloadJSON) + } + if repository.delivered.EventID != "evt-confirm-1" || string(repository.deliveredPayload) != payloadJSON { + t.Fatalf("delivery was not durably marked: delivery=%+v payload=%s", repository.delivered, repository.deliveredPayload) + } +} + +func TestProcessActionOutboxMessageIgnoresUnrelatedEvents(t *testing.T) { + repository := &confirmRepositoryFake{claimOK: true} + publisher := &confirmPublisherFake{} + service := New(Config{NodeID: "notice-node-1"}, repository, publisher) + + handled, err := service.ProcessActionOutboxMessage(context.Background(), messagemq.ActionOutboxMessage{ + EventType: "ActivityNoticeCreated", + }, WorkerOptions{}) + if err != nil { + t.Fatalf("unrelated event returned error: %v", err) + } + if handled { + t.Fatal("unrelated event must not be claimed by confirm notice") + } + if repository.claimCalls != 0 || len(publisher.messages) != 0 { + t.Fatalf("unrelated event caused side effects: claims=%d publishes=%d", repository.claimCalls, len(publisher.messages)) + } +} + +func TestProcessActionOutboxMessageRecordsPublishFailureState(t *testing.T) { + tests := []struct { + name string + claimedRetry int + maxRetry int + wantErr bool + wantRetry int + wantFailed int + }{ + {name: "retryable", claimedRetry: 1, maxRetry: 3, wantErr: true, wantRetry: 2}, + {name: "terminal", claimedRetry: 2, maxRetry: 3, wantFailed: 3}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repository := &confirmRepositoryFake{claimOK: true, claimedRetryCount: tt.claimedRetry} + publisher := &confirmPublisherFake{err: errors.New("tencent im timeout")} + service := New(Config{NodeID: "notice-node-1"}, repository, publisher) + + handled, err := service.ProcessActionOutboxMessage(context.Background(), confirmActionMessage(), WorkerOptions{ + PublishTimeout: time.Second, + MaxRetryCount: tt.maxRetry, + InitialBackoff: time.Second, + MaxBackoff: time.Second, + }) + if !handled { + t.Fatal("confirm event must remain owned by confirm notice on publish failure") + } + if (err != nil) != tt.wantErr { + t.Fatalf("error=%v, wantErr=%v", err, tt.wantErr) + } + // 可重试错误必须返回给 MQ 触发重投;达到上限后落 failed 并 ACK,避免同一毒消息无限阻塞消费。 + if repository.retryCount != tt.wantRetry || repository.failedRetryCount != tt.wantFailed { + t.Fatalf("failure state mismatch: retry=%d failed=%d", repository.retryCount, repository.failedRetryCount) + } + }) + } +} + +func confirmActionMessage() messagemq.ActionOutboxMessage { + return messagemq.ActionOutboxMessage{ + AppCode: "lalu", + EventID: "evt-confirm-failure", + EventType: "ConfirmMessageCreated", + ConfirmID: "cfm_failure", + PayloadJSON: `{"sender_user_id":"1001","receiver_user_id":"2002"}`, + OccurredAtMS: 1710000000123, + } +} + +type confirmRepositoryFake struct { + claimOK bool + claimedRetryCount int + claimCalls int + claimed Delivery + delivered Delivery + deliveredPayload []byte + retryCount int + failedRetryCount int +} + +func (r *confirmRepositoryFake) ClaimConfirmDelivery(_ context.Context, _ string, delivery Delivery, _ time.Duration) (Delivery, bool, error) { + r.claimCalls++ + r.claimed = delivery + delivery.RetryCount = r.claimedRetryCount + return delivery, r.claimOK, nil +} + +func (r *confirmRepositoryFake) MarkConfirmDelivered(_ context.Context, delivery Delivery, payload []byte, _ int64) error { + r.delivered = delivery + r.deliveredPayload = append([]byte(nil), payload...) + return nil +} + +func (r *confirmRepositoryFake) MarkConfirmRetryable(_ context.Context, _ Delivery, retryCount int, _ int64, _ string, _ int64) error { + r.retryCount = retryCount + return nil +} + +func (r *confirmRepositoryFake) MarkConfirmFailed(_ context.Context, _ Delivery, retryCount int, _ string, _ int64) error { + r.failedRetryCount = retryCount + return nil +} + +type confirmPublisherFake struct { + messages []tencentim.CustomUserMessage + err error +} + +func (p *confirmPublisherFake) PublishUserCustomMessage(_ context.Context, message tencentim.CustomUserMessage) error { + p.messages = append(p.messages, message) + return p.err +}