package rabbitmq import ( "context" "encoding/json" "errors" "io" "os" "reflect" "testing" "time" "github.com/gogf/gf/v2/frame/g" amqp "github.com/rabbitmq/amqp091-go" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/batch" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model" dailyusageproducer "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/producer/dailyusage" monthlyhistoryproducer "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/producer/monthlyusagehistory" packagecustomerservice "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/packagecustomer" ) func TestMain(m *testing.M) { g.Log().SetWriter(io.Discard) os.Exit(m.Run()) } type trafficUsageHandlerStub struct { dailyCalls int monthlyCalls int dailyMessage model.TrafficUsageDailyRetryMessage monthMessage model.TrafficUsageMonthlyRetryMessage dailyErr error monthlyErr error } func (s *trafficUsageHandlerStub) RetryDaily(_ context.Context, message model.TrafficUsageDailyRetryMessage) error { s.dailyCalls++ s.dailyMessage = message return s.dailyErr } func (s *trafficUsageHandlerStub) RetryMonthly(_ context.Context, message model.TrafficUsageMonthlyRetryMessage) error { s.monthlyCalls++ s.monthMessage = message return s.monthlyErr } type workOrderHandlerStub struct { calls int message model.WorkOrderAutoCloseMessage err error } type balanceRechargeHandlerStub struct { calls int outTradeNo string err error } func (s *balanceRechargeHandlerStub) CheckRechargeTimeout(_ context.Context, outTradeNo string) error { s.calls++ s.outTradeNo = outTradeNo return s.err } type batchHandlerStub struct { calls int message batch.Message err error } func (s *batchHandlerStub) Handle(_ context.Context, message batch.Message) error { s.calls++ s.message = message return s.err } type packageCustomerSyncHandlerStub struct { calls int message model.PackageCustomerSyncMessage err error } type dailyStartHandlerStub struct { calls int message model.TrafficUsageDailyStartMessage err error } func (s *dailyStartHandlerStub) Produce(_ context.Context, message model.TrafficUsageDailyStartMessage) error { s.calls++ s.message = message return s.err } type monthlyHistoryStartHandlerStub struct { calls int message model.TrafficUsageMonthlyHistoryStartMessage err error } func (s *monthlyHistoryStartHandlerStub) Produce(_ context.Context, message model.TrafficUsageMonthlyHistoryStartMessage) error { s.calls++ s.message = message return s.err } func (s *packageCustomerSyncHandlerStub) SyncDeptPackages(_ context.Context, message model.PackageCustomerSyncMessage) error { s.calls++ s.message = message return s.err } func (s *workOrderHandlerStub) AutoCloseResolved(_ context.Context, message model.WorkOrderAutoCloseMessage) error { s.calls++ s.message = message return s.err } func TestHandleTrafficUsageDailyRetry(t *testing.T) { stubTrafficPublish(t) t.Run("invalid messages are dead lettered", func(t *testing.T) { messages := []model.TrafficUsageDailyRetryMessage{ {}, {SimKey: &model.SimKey{}, Card: model.TrafficUsageSimCard{Iccid: "card"}}, {SimKey: &model.SimKey{ICCID: "sim"}}, } for _, message := range messages { handler := &trafficUsageHandlerStub{} consumers := New(handler, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}) if err := consumers.handleTrafficUsageDailyRetry(context.Background(), message); err != nil { t.Fatalf("invalid message returned error: %v", err) } if handler.dailyCalls != 0 { t.Fatalf("invalid message called handler %d times", handler.dailyCalls) } } }) t.Run("valid message is delegated", func(t *testing.T) { expectedErr := errors.New("daily failed") expected := model.TrafficUsageDailyRetryMessage{ SimKey: &model.SimKey{ICCID: "sim"}, Card: model.TrafficUsageSimCard{Iccid: "sim", PackageCode: "package"}, QueryDate: "20260714", } handler := &trafficUsageHandlerStub{dailyErr: expectedErr} consumers := New(handler, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}) if err := consumers.handleTrafficUsageDailyRetry(context.Background(), expected); err != nil { t.Fatalf("expected delegated error, got %v", err) } if handler.dailyCalls != 1 || !reflect.DeepEqual(handler.dailyMessage, expected) { t.Fatalf("daily message was not delegated exactly once: %+v", handler.dailyMessage) } }) } func TestHandleTrafficUsageMonthlyRetry(t *testing.T) { stubTrafficPublish(t) t.Run("invalid messages are dead lettered", func(t *testing.T) { messages := []model.TrafficUsageMonthlyRetryMessage{ {}, {SimKey: &model.SimKey{}, Card: model.TrafficUsageSimCard{Iccid: "card"}}, {SimKey: &model.SimKey{ICCID: "sim"}}, } for _, message := range messages { handler := &trafficUsageHandlerStub{} consumers := New(handler, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}) if err := consumers.handleTrafficUsageMonthlyRetry(context.Background(), message); err != nil { t.Fatalf("invalid message returned error: %v", err) } if handler.monthlyCalls != 0 { t.Fatalf("invalid message called handler %d times", handler.monthlyCalls) } } }) t.Run("valid message is delegated", func(t *testing.T) { expectedErr := errors.New("monthly failed") expected := model.TrafficUsageMonthlyRetryMessage{ SimKey: &model.SimKey{ICCID: "sim"}, Card: model.TrafficUsageSimCard{Iccid: "sim", PackageCode: "package"}, QueryDate: "202607", } handler := &trafficUsageHandlerStub{monthlyErr: expectedErr} consumers := New(handler, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}) if err := consumers.handleTrafficUsageMonthlyRetry(context.Background(), expected); err != nil { t.Fatalf("expected delegated error, got %v", err) } if handler.monthlyCalls != 1 || !reflect.DeepEqual(handler.monthMessage, expected) { t.Fatalf("monthly message was not delegated exactly once: %+v", handler.monthMessage) } }) } func stubTrafficPublish(t *testing.T) { t.Helper() original, delayed := trafficUsagePublish, trafficUsagePublishDelayed t.Cleanup(func() { trafficUsagePublish, trafficUsagePublishDelayed = original, delayed }) trafficUsagePublish = func(context.Context, string, []byte, amqp.Table) error { return nil } trafficUsagePublishDelayed = func(context.Context, string, string, time.Duration, []byte, amqp.Table) error { return nil } } func TestHandleWorkOrderAutoClose(t *testing.T) { handler := &workOrderHandlerStub{} consumers := New(&trafficUsageHandlerStub{}, handler, &balanceRechargeHandlerStub{}) if err := consumers.handleWorkOrderAutoClose(context.Background(), model.WorkOrderAutoCloseMessage{}); err != nil { t.Fatalf("invalid message returned error: %v", err) } if handler.calls != 0 { t.Fatalf("invalid message called handler %d times", handler.calls) } expectedErr := errors.New("work order failed") expected := model.WorkOrderAutoCloseMessage{WoNo: "WO-1", ResolveTimeTs: 1} handler.err = expectedErr if err := consumers.handleWorkOrderAutoClose(context.Background(), expected); !errors.Is(err, expectedErr) { t.Fatalf("expected delegated error, got %v", err) } if handler.calls != 1 || !reflect.DeepEqual(handler.message, expected) { t.Fatalf("work order message was not delegated exactly once: %+v", handler.message) } } func TestHandleBalanceRechargeTimeout(t *testing.T) { handler := &balanceRechargeHandlerStub{} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, handler) if err := consumers.handleBalanceRechargeTimeout(context.Background(), model.BalanceRechargeTimeoutMessage{}); err != nil { t.Fatalf("invalid message returned error: %v", err) } if handler.calls != 0 { t.Fatalf("invalid message called handler %d times", handler.calls) } message := model.BalanceRechargeTimeoutMessage{OutTradeNo: "BAL-1"} if err := consumers.handleBalanceRechargeTimeout(context.Background(), message); err != nil { t.Fatalf("valid message returned error: %v", err) } if handler.calls != 1 || handler.outTradeNo != message.OutTradeNo { t.Fatalf("balance recharge message was not delegated exactly once: %+v", handler) } } func TestHandleBatchExecute(t *testing.T) { expectedErr := errors.New("batch failed") expected := batch.Message{BatchNo: "BATCH-1", BatchType: batch.Type103} handler := &batchHandlerStub{err: expectedErr} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}, handler) if err := consumers.handleBatchExecute(context.Background(), expected); !errors.Is(err, expectedErr) { t.Fatalf("expected delegated error, got %v", err) } if handler.calls != 1 || !reflect.DeepEqual(handler.message, expected) { t.Fatalf("batch message was not delegated exactly once: %+v", handler.message) } } func TestHandlePackageCustomerSync(t *testing.T) { t.Run("invalid message is dropped", func(t *testing.T) { handler := &packageCustomerSyncHandlerStub{} consumers := NewWithPackageCustomerSync(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}, handler) if err := consumers.handlePackageCustomerSync(context.Background(), model.PackageCustomerSyncMessage{}); err != nil { t.Fatalf("invalid message returned error: %v", err) } if handler.calls != 0 { t.Fatalf("invalid message called handler %d times", handler.calls) } }) t.Run("missing dept is dropped", func(t *testing.T) { expected := model.PackageCustomerSyncMessage{TenantId: "000000", DeptId: 164, Trigger: "package_lifecycle", SourceBatchNo: "898604017"} handler := &packageCustomerSyncHandlerStub{err: packagecustomerservice.ErrDeptNotFound} consumers := NewWithPackageCustomerSync(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}, handler) if err := consumers.handlePackageCustomerSync(context.Background(), expected); err != nil { t.Fatalf("missing dept returned error: %v", err) } if handler.calls != 1 || !reflect.DeepEqual(handler.message, expected) { t.Fatalf("package customer sync message was not delegated exactly once: %+v", handler.message) } }) t.Run("other errors are retried", func(t *testing.T) { expectedErr := errors.New("database failed") expected := model.PackageCustomerSyncMessage{TenantId: "000000", DeptId: 117} handler := &packageCustomerSyncHandlerStub{err: expectedErr} consumers := NewWithPackageCustomerSync(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}, handler) if err := consumers.handlePackageCustomerSync(context.Background(), expected); !errors.Is(err, expectedErr) { t.Fatalf("expected delegated error, got %v", err) } }) } func TestHandleWorkerDiagnostic(t *testing.T) { consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}) if err := consumers.handleWorkerDiagnostic(context.Background(), model.WorkerDiagnosticMessage{}); err != nil { t.Fatalf("invalid message returned error: %v", err) } message := model.WorkerDiagnosticMessage{ TraceID: "worker-test-1", SentAt: 1784030000000, Message: "worker ping", } if err := consumers.handleWorkerDiagnostic(context.Background(), message); err != nil { t.Fatalf("valid message returned error: %v", err) } } func TestWorkerDiagnosticMessageJSONContract(t *testing.T) { message := model.WorkerDiagnosticMessage{ TraceID: "worker-test-1", SentAt: 1784030000000, Message: "worker ping", } body, err := json.Marshal(message) if err != nil { t.Fatalf("marshal diagnostic message: %v", err) } const expected = `{"traceId":"worker-test-1","sentAt":1784030000000,"message":"worker ping"}` if string(body) != expected { t.Fatalf("unexpected diagnostic JSON: %s", body) } } func TestChannelRefreshMessageJSONContract(t *testing.T) { message := model.ChannelRefreshMessage{ Type: "channel_rename", OldChanKey: "old_channel", NewChanKey: "new_channel", } body, err := json.Marshal(message) if err != nil { t.Fatalf("marshal channel rename message: %v", err) } const expected = `{"type":"channel_rename","oldChanKey":"old_channel","newChanKey":"new_channel"}` if string(body) != expected { t.Fatalf("unexpected channel rename JSON: %s", body) } } func TestHandleChannelRefreshDropsInvalidRename(t *testing.T) { consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}) for _, message := range []model.ChannelRefreshMessage{ {Type: "channel_rename"}, {Type: "channel_rename", OldChanKey: "old_channel"}, {Type: "channel_rename", NewChanKey: "new_channel"}, {Type: "channel_rename", OldChanKey: "same", NewChanKey: "same"}, } { if err := consumers.handleChannelRefresh(context.Background(), message); err != nil { t.Fatalf("invalid channel rename returned error: %v", err) } } } func TestHandleChannelRefreshRenameFailureReturnsError(t *testing.T) { consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}) ctx, cancel := context.WithCancel(context.Background()) cancel() message := model.ChannelRefreshMessage{ Type: "channel_rename", OldChanKey: "old_channel", NewChanKey: "new_channel", } // ctx 已取消时 renameChannelWithRetry 会在首次重试等待处返回 ctx.Err(), // 消费者必须把错误向上抛出,让框架 Nack 重新入队而不是 Ack 丢弃。 if err := consumers.handleChannelRefresh(ctx, message); err == nil { t.Fatalf("expected failed channel rename to return error for Nack+requeue") } } func TestHandleTrafficUsageDailyStart(t *testing.T) { t.Run("invalid message is dropped", func(t *testing.T) { for _, message := range []model.TrafficUsageDailyStartMessage{ {}, {RunID: "run-1"}, {QueryDate: "20261009"}, } { handler := &dailyStartHandlerStub{} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithDailyUsageStart(handler) if err := consumers.handleTrafficUsageDailyStart(context.Background(), message); err != nil { t.Fatalf("invalid message returned error: %v", err) } if handler.calls != 0 { t.Fatalf("invalid message called producer %d times", handler.calls) } } }) t.Run("valid message is delegated", func(t *testing.T) { expected := model.TrafficUsageDailyStartMessage{ RunID: "daily-usage-20261009-abc", Operation: model.OperationPreviousDayUsageFullSync, QueryDate: "20261009", TriggeredAt: "2026-10-10T06:00:00+08:00", Scope: &model.SyncScope{Channels: []string{"217"}}, } handler := &dailyStartHandlerStub{} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithDailyUsageStart(handler) if err := consumers.handleTrafficUsageDailyStart(context.Background(), expected); err != nil { t.Fatalf("valid message returned error: %v", err) } if handler.calls != 1 || !reflect.DeepEqual(handler.message, expected) { t.Fatalf("daily start message was not delegated exactly once: %+v", handler.message) } }) t.Run("an already running producer is acked", func(t *testing.T) { handler := &dailyStartHandlerStub{err: dailyusageproducer.ErrAlreadyRunning} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithDailyUsageStart(handler) if err := consumers.handleTrafficUsageDailyStart(context.Background(), model.TrafficUsageDailyStartMessage{RunID: "r", QueryDate: "20261009"}); err != nil { t.Fatalf("ErrAlreadyRunning must be acked, got %v", err) } }) t.Run("a finished run is acked instead of being resurrected", func(t *testing.T) { handler := &dailyStartHandlerStub{err: dailyusageproducer.ErrRunFinished} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithDailyUsageStart(handler) if err := consumers.handleTrafficUsageDailyStart(context.Background(), model.TrafficUsageDailyStartMessage{RunID: "r", QueryDate: "20261009"}); err != nil { t.Fatalf("ErrRunFinished must be acked, got %v", err) } }) t.Run("other errors are surfaced for requeue", func(t *testing.T) { expectedErr := errors.New("redis unavailable") handler := &dailyStartHandlerStub{err: expectedErr} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithDailyUsageStart(handler) if err := consumers.handleTrafficUsageDailyStart(context.Background(), model.TrafficUsageDailyStartMessage{RunID: "r", QueryDate: "20261009"}); !errors.Is(err, expectedErr) { t.Fatalf("expected the producer error to propagate, got %v", err) } }) } // TestDailyUsageStartConsumerIsOptIn pins the assembly contract: without an // explicit WithDailyUsageStart the initializer list must be unchanged, so a // disabled daily chain cannot alter existing deployments. func TestDailyUsageStartConsumerIsOptIn(t *testing.T) { base := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}) withDaily := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithDailyUsageStart(&dailyStartHandlerStub{}) if got, want := len(withDaily.initializers(false)), len(base.initializers(false))+1; got != want { t.Fatalf("initializers = %d, want %d", got, want) } } func TestHandleTrafficUsageMonthlyHistoryStart(t *testing.T) { t.Run("invalid message is dropped", func(t *testing.T) { for _, message := range []model.TrafficUsageMonthlyHistoryStartMessage{ {}, {RunID: "run-1"}, {QueryMonth: "202609"}, } { handler := &monthlyHistoryStartHandlerStub{} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithMonthlyHistoryStart(handler) if err := consumers.handleTrafficUsageMonthlyHistoryStart(context.Background(), message); err != nil { t.Fatalf("invalid message returned error: %v", err) } if handler.calls != 0 { t.Fatalf("invalid message called producer %d times", handler.calls) } } }) t.Run("valid message is delegated", func(t *testing.T) { expected := model.TrafficUsageMonthlyHistoryStartMessage{ RunID: "monthly-history-202609-abc", Operation: model.OperationPreviousMonthUsageFullSync, QueryMonth: "202609", TriggeredAt: "2026-10-01T06:00:00+08:00", Scope: &model.SyncScope{Channels: []string{"217"}}, } handler := &monthlyHistoryStartHandlerStub{} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithMonthlyHistoryStart(handler) if err := consumers.handleTrafficUsageMonthlyHistoryStart(context.Background(), expected); err != nil { t.Fatalf("valid message returned error: %v", err) } if handler.calls != 1 || !reflect.DeepEqual(handler.message, expected) { t.Fatalf("monthly history start message was not delegated exactly once: %+v", handler.message) } }) t.Run("an already running producer is acked", func(t *testing.T) { handler := &monthlyHistoryStartHandlerStub{err: monthlyhistoryproducer.ErrAlreadyRunning} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithMonthlyHistoryStart(handler) if err := consumers.handleTrafficUsageMonthlyHistoryStart(context.Background(), model.TrafficUsageMonthlyHistoryStartMessage{RunID: "r", QueryMonth: "202609"}); err != nil { t.Fatalf("ErrAlreadyRunning must be acked, got %v", err) } }) t.Run("a finished run is acked instead of being resurrected", func(t *testing.T) { handler := &monthlyHistoryStartHandlerStub{err: monthlyhistoryproducer.ErrRunFinished} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithMonthlyHistoryStart(handler) if err := consumers.handleTrafficUsageMonthlyHistoryStart(context.Background(), model.TrafficUsageMonthlyHistoryStartMessage{RunID: "r", QueryMonth: "202609"}); err != nil { t.Fatalf("ErrRunFinished must be acked, got %v", err) } }) t.Run("other errors are surfaced for requeue", func(t *testing.T) { expectedErr := errors.New("redis unavailable") handler := &monthlyHistoryStartHandlerStub{err: expectedErr} consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithMonthlyHistoryStart(handler) if err := consumers.handleTrafficUsageMonthlyHistoryStart(context.Background(), model.TrafficUsageMonthlyHistoryStartMessage{RunID: "r", QueryMonth: "202609"}); !errors.Is(err, expectedErr) { t.Fatalf("expected the producer error to propagate, got %v", err) } }) } // TestMonthlyHistoryStartConsumerIsOptIn mirrors the daily chain: a disabled // monthly-history chain must not add an initializer. func TestMonthlyHistoryStartConsumerIsOptIn(t *testing.T) { base := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}) withHistory := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}).WithMonthlyHistoryStart(&monthlyHistoryStartHandlerStub{}) if got, want := len(withHistory.initializers(false)), len(base.initializers(false))+1; got != want { t.Fatalf("initializers = %d, want %d", got, want) } } func TestConsumerInitializersRespectWorkerDiagnosticSwitch(t *testing.T) { consumers := New(&trafficUsageHandlerStub{}, &workOrderHandlerStub{}, &balanceRechargeHandlerStub{}) // 手动同步上游结果(initDataReviewQuery)随业务消费者常驻,无处理器时自跳过。 if got := len(consumers.initializers(false)); got != 7 { t.Fatalf("diagnostic disabled: expected 7 business consumers, got %d", got) } if got := len(consumers.initializers(true)); got != 8 { t.Fatalf("diagnostic enabled: expected 8 consumers, got %d", got) } }