package trafficusage import ( "context" "errors" "testing" "time" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" ) func stubDailyTrigger(t *testing.T) { t.Helper() originalNow := dailyUsageNow originalRunID := dailyUsageRunID originalEnabled := dailyUsageRabbitEnabled originalPublish := dailyUsagePublish t.Cleanup(func() { dailyUsageNow = originalNow dailyUsageRunID = originalRunID dailyUsageRabbitEnabled = originalEnabled dailyUsagePublish = originalPublish }) } // TestStartDailyPublishesPreviousShanghaiDay pins the period: the run must cover // the previous calendar day in Asia/Shanghai, matching what the legacy admin // scan wrote, so the migration cannot shift the day by one. func TestStartDailyPublishesPreviousShanghaiDay(t *testing.T) { stubDailyTrigger(t) dailyUsageNow = func() time.Time { // 2026-10-10 06:00 +08:00 -> the previous day is 20261009. return time.Date(2026, time.October, 10, 6, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60)) } dailyUsageRunID = func() string { return "test-run" } dailyUsageRabbitEnabled = func(context.Context) bool { return true } var publishedQueue string var published model.TrafficUsageDailyStartMessage dailyUsagePublish = func(_ context.Context, queue string, data any) error { publishedQueue = queue published = data.(model.TrafficUsageDailyStartMessage) return nil } if err := NewTrafficUsageDailySync().StartDaily(context.Background()); err != nil { t.Fatalf("StartDaily() error = %v", err) } if publishedQueue != consts.QueueTrafficUsageDailyStart { t.Fatalf("queue = %q, want %q", publishedQueue, consts.QueueTrafficUsageDailyStart) } if published.QueryDate != "20261009" { t.Fatalf("queryDate = %q, want 20261009", published.QueryDate) } if published.RunID != "daily-usage-20261009-test-run" { t.Fatalf("runId = %q", published.RunID) } if published.Operation != model.OperationPreviousDayUsageFullSync { t.Fatalf("operation = %q", published.Operation) } } // TestStartDailyUsesShanghaiDayAcrossUTCMidnight guards the timezone bug that // the legacy code carried: a UTC instant just after midnight is still the // previous day in Shanghai, so the period must not be computed in UTC. func TestStartDailyUsesShanghaiDayAcrossUTCMidnight(t *testing.T) { stubDailyTrigger(t) // 2026-10-09 17:30 UTC == 2026-10-10 01:30 +08:00 -> previous day 20261009. dailyUsageNow = func() time.Time { return time.Date(2026, time.October, 9, 17, 30, 0, 0, time.UTC) } dailyUsageRunID = func() string { return "tz-run" } dailyUsageRabbitEnabled = func(context.Context) bool { return true } var published model.TrafficUsageDailyStartMessage dailyUsagePublish = func(_ context.Context, _ string, data any) error { published = data.(model.TrafficUsageDailyStartMessage) return nil } if err := NewTrafficUsageDailySync().StartDaily(context.Background()); err != nil { t.Fatalf("StartDaily() error = %v", err) } if published.QueryDate != "20261009" { t.Fatalf("queryDate = %q, want 20261009 (Shanghai, not UTC)", published.QueryDate) } if published.TriggeredAt != "2026-10-10T01:30:00+08:00" { t.Fatalf("triggeredAt = %q, want the Shanghai instant", published.TriggeredAt) } } func TestStartDailyRunScopedForwardsScope(t *testing.T) { stubDailyTrigger(t) dailyUsageNow = func() time.Time { return time.Date(2026, time.October, 10, 6, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60)) } dailyUsageRunID = func() string { return "scoped-run" } dailyUsageRabbitEnabled = func(context.Context) bool { return true } var published model.TrafficUsageDailyStartMessage dailyUsagePublish = func(_ context.Context, _ string, data any) error { published = data.(model.TrafficUsageDailyStartMessage) return nil } runID, err := NewTrafficUsageDailySync().StartDailyRunScoped(context.Background(), &model.SyncScope{Channels: []string{"217"}}) if err != nil { t.Fatalf("StartDailyRunScoped() error = %v", err) } if runID != "daily-usage-20261009-scoped-run" { t.Fatalf("runId = %q", runID) } if published.Scope == nil || published.Scope.ChannelCount() != 1 || !published.Scope.Allows("217") { t.Fatalf("scope = %+v, want channel 217 only", published.Scope) } } func TestStartDailyRejectsInvalidScope(t *testing.T) { stubDailyTrigger(t) dailyUsageRabbitEnabled = func(context.Context) bool { return true } called := false dailyUsagePublish = func(context.Context, string, any) error { called = true return nil } if _, err := NewTrafficUsageDailySync().StartDailyRunScoped(context.Background(), &model.SyncScope{Channels: []string{"invalid/chan"}}); err == nil { t.Fatal("expected an invalid chanKey to be rejected") } if called { t.Fatal("publisher called for a rejected scope") } } func TestStartDailyReturnsPublishError(t *testing.T) { stubDailyTrigger(t) dailyUsageNow = func() time.Time { return time.Date(2026, time.October, 10, 6, 0, 0, 0, time.UTC) } dailyUsageRunID = func() string { return "err-run" } dailyUsageRabbitEnabled = func(context.Context) bool { return true } dailyUsagePublish = func(context.Context, string, any) error { return errors.New("broker unavailable") } if err := NewTrafficUsageDailySync().StartDaily(context.Background()); err == nil { t.Fatal("StartDaily() error = nil, want publish error") } } func TestStartDailySkipsWhenRabbitMQDisabled(t *testing.T) { stubDailyTrigger(t) dailyUsageRabbitEnabled = func(context.Context) bool { return false } called := false dailyUsagePublish = func(context.Context, string, any) error { called = true return nil } if err := NewTrafficUsageDailySync().StartDaily(context.Background()); err == nil { t.Fatal("StartDaily() error = nil, want disabled error") } if called { t.Fatal("publisher called while RabbitMQ is disabled") } } // dailyBackfillClock pins "now" for the period-aware daily tests: 2026-10-20 // 06:00 +08:00, so the default day is 20261019 and nothing later is valid. func dailyBackfillClock(t *testing.T) { t.Helper() stubDailyTrigger(t) dailyUsageNow = func() time.Time { return time.Date(2026, time.October, 20, 6, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60)) } dailyUsageRunID = func() string { return "backfill-run" } dailyUsageRabbitEnabled = func(context.Context) bool { return true } } func captureDailyStart(t *testing.T) *model.TrafficUsageDailyStartMessage { t.Helper() published := &model.TrafficUsageDailyStartMessage{} dailyUsagePublish = func(_ context.Context, _ string, data any) error { *published = data.(model.TrafficUsageDailyStartMessage) return nil } return published } func TestStartDailyRunScopedAtUsesGivenDate(t *testing.T) { dailyBackfillClock(t) published := captureDailyStart(t) runID, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, "20261014") if err != nil { t.Fatalf("StartDailyRunScopedAt() error = %v", err) } if published.QueryDate != "20261014" { t.Fatalf("queryDate = %q, want 20261014", published.QueryDate) } if runID != "daily-usage-20261014-backfill-run" { t.Fatalf("runId = %q", runID) } if published.Operation != model.OperationPreviousDayUsageFullSync { t.Fatalf("operation = %q", published.Operation) } } func TestStartDailyRunScopedAtRejectsFutureDate(t *testing.T) { dailyBackfillClock(t) called := false dailyUsagePublish = func(context.Context, string, any) error { called = true return nil } for _, queryDate := range []string{"20261020", "20261021", "20270101"} { if _, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, queryDate); err == nil { t.Fatalf("queryDate %q accepted, want a future-date error", queryDate) } } if called { t.Fatal("publisher called for a rejected date") } } func TestStartDailyRunScopedAtRejectsMalformedDate(t *testing.T) { dailyBackfillClock(t) called := false dailyUsagePublish = func(context.Context, string, any) error { called = true return nil } for _, queryDate := range []string{"2026-10-14", "abc", "2026101", "202610140", "20261314"} { if _, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, queryDate); err == nil { t.Fatalf("queryDate %q accepted, want a format error", queryDate) } } if called { t.Fatal("publisher called for a malformed date") } } func TestStartDailyRunScopedAtRejectsDateBeforeLowerBound(t *testing.T) { dailyBackfillClock(t) called := false dailyUsagePublish = func(context.Context, string, any) error { called = true return nil } for _, queryDate := range []string{"20191231", "19990101"} { if _, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, queryDate); err == nil { t.Fatalf("queryDate %q accepted, want a lower-bound error", queryDate) } } if called { t.Fatal("publisher called for a date below the lower bound") } } func TestStartDailyRunScopedAtEmptyFallsBackToYesterday(t *testing.T) { dailyBackfillClock(t) published := captureDailyStart(t) runID, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, "") if err != nil { t.Fatalf("StartDailyRunScopedAt() error = %v", err) } if published.QueryDate != "20261019" { t.Fatalf("queryDate = %q, want 20261019 (yesterday)", published.QueryDate) } if runID != "daily-usage-20261019-backfill-run" { t.Fatalf("runId = %q", runID) } } // TestStartDailyRunScopedAtEmptyMatchesLegacyScoped keeps the backward // compatibility promise concrete: the old entry point and an empty period must // publish byte-identical messages. func TestStartDailyRunScopedAtEmptyMatchesLegacyScoped(t *testing.T) { dailyBackfillClock(t) var messages []model.TrafficUsageDailyStartMessage dailyUsagePublish = func(_ context.Context, _ string, data any) error { messages = append(messages, data.(model.TrafficUsageDailyStartMessage)) return nil } legacyID, err := NewTrafficUsageDailySync().StartDailyRunScoped(context.Background(), nil) if err != nil { t.Fatal(err) } atID, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, "") if err != nil { t.Fatal(err) } if len(messages) != 2 { t.Fatalf("published %d messages, want 2", len(messages)) } if legacyID != atID || messages[0] != messages[1] { t.Fatalf("legacy = %+v/%q, at = %+v/%q", messages[0], legacyID, messages[1], atID) } }