package monthlyusage import ( "context" "testing" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model" ) func newScopeProducer(t *testing.T, cards []Card) (*Producer, *producerRedis) { t.Helper() store := testStore(t) scanner := scannerFor(cards, cards[len(cards)-1].ID) for i := range scanner.channels { for _, c := range cards { if c.ChanKey == scanner.channels[i].ChanKey { scanner.channels[i].TempKey = c.TempKey scanner.channels[i].SyncEnabled = c.SyncEnabled break } } } p, err := New(scanner, store, Config{PageSize: 100}) if err != nil { t.Fatal(err) } return p, &producerRedis{enqueueBatches: map[string]int{}} } func scopedMessage(runID string, channels ...string) model.TrafficUsageMonthlyStartMessage { return model.TrafficUsageMonthlyStartMessage{ RunID: runID, Operation: "current_month_usage_full_sync", QueryMonth: "202608", Scope: &model.SyncScope{Channels: channels}, } } func TestProduceScopeEnqueuesOnlySelectedChannels(t *testing.T) { producer, redis := newScopeProducer(t, []Card{ {ID: 1, ChanKey: "217", TempKey: "sim_yd_v5", Operator: 1}, {ID: 2, ChanKey: "173", TempKey: "sim_yd_v5", Operator: 1}, {ID: 3, ChanKey: "217", TempKey: "sim_yd_v5", Operator: 1}, {ID: 4, ChanKey: "189", TempKey: "sim_yd_v5", Operator: 1}, }) if err := producer.Produce(context.Background(), scopedMessage("scoped", "217")); err != nil { t.Fatal(err) } redis.enqueueBatches = map[string]int{} for _, ch := range []string{"217", "173", "189", "ghost-channel", "a", "b", "mobile-a", "mobile-b"} { key, _ := producer.store.RoundActiveKey("scoped", 1, ch) n, _ := producer.store.SizeKey(context.Background(), key) if n > 0 { redis.enqueueBatches["flow:"+ch] = int(n) } } redis.mu.Lock() defer redis.mu.Unlock() if len(redis.enqueueBatches) != 1 { t.Fatalf("enqueue batches = %+v, want only the 217 channel", redis.enqueueBatches) } if redis.enqueueBatches["flow:217"] != 2 { t.Fatalf("enqueue batches = %+v, want 2 tasks on flow:217", redis.enqueueBatches) } } func TestProduceScopeMultipleChannels(t *testing.T) { producer, redis := newScopeProducer(t, []Card{ {ID: 1, ChanKey: "217", TempKey: "sim_yd_v5", Operator: 1}, {ID: 2, ChanKey: "173", TempKey: "sim_yd_v5", Operator: 1}, {ID: 3, ChanKey: "189", TempKey: "sim_yd_v5", Operator: 1}, }) if err := producer.Produce(context.Background(), scopedMessage("scoped", "189", "217")); err != nil { t.Fatal(err) } redis.enqueueBatches = map[string]int{} for _, ch := range []string{"217", "173", "189", "ghost-channel", "a", "b", "mobile-a", "mobile-b"} { key, _ := producer.store.RoundActiveKey("scoped", 1, ch) n, _ := producer.store.SizeKey(context.Background(), key) if n > 0 { redis.enqueueBatches["flow:"+ch] = int(n) } } redis.mu.Lock() defer redis.mu.Unlock() if len(redis.enqueueBatches) != 2 { t.Fatalf("enqueue batches = %+v, want 217 and 189", redis.enqueueBatches) } if _, ok := redis.enqueueBatches["flow:173"]; ok { t.Fatalf("channel 173 was enqueued despite not being selected: %+v", redis.enqueueBatches) } } func TestProduceFullScopeKeepsEveryChannel(t *testing.T) { producer, redis := newScopeProducer(t, []Card{ {ID: 1, ChanKey: "217", TempKey: "sim_yd_v5", Operator: 1}, {ID: 2, ChanKey: "173", TempKey: "sim_yd_v5", Operator: 1}, }) if err := producer.Produce(context.Background(), model.TrafficUsageMonthlyStartMessage{ RunID: "full", Operation: "current_month_usage_full_sync", QueryMonth: "202608", }); err != nil { t.Fatal(err) } redis.enqueueBatches = map[string]int{} for _, ch := range []string{"217", "173", "189", "ghost-channel", "a", "b", "mobile-a", "mobile-b"} { key, _ := producer.store.RoundActiveKey("full", 1, ch) n, _ := producer.store.SizeKey(context.Background(), key) if n > 0 { redis.enqueueBatches["flow:"+ch] = int(n) } } redis.mu.Lock() defer redis.mu.Unlock() if len(redis.enqueueBatches) != 2 { t.Fatalf("enqueue batches = %+v, want both channels for a full sync", redis.enqueueBatches) } } func TestProduceScopeCombinesWithSyncDisabledGuard(t *testing.T) { producer, redis := newScopeProducer(t, []Card{ {ID: 1, ChanKey: "217", TempKey: "sim_yd_v5", Operator: 1}, // Selected but excluded by sync_enabled: scope must not override it. {ID: 2, ChanKey: "173", TempKey: "sim_yd_v5", SyncEnabled: true, Operator: 1}, }) if err := producer.Produce(context.Background(), scopedMessage("scoped", "173")); err != nil { t.Fatal(err) } redis.enqueueBatches = map[string]int{} for _, ch := range []string{"217", "173", "189", "ghost-channel", "a", "b", "mobile-a", "mobile-b"} { key, _ := producer.store.RoundActiveKey("scoped", 1, ch) n, _ := producer.store.SizeKey(context.Background(), key) if n > 0 { redis.enqueueBatches["flow:"+ch] = int(n) } } redis.mu.Lock() defer redis.mu.Unlock() if len(redis.enqueueBatches) != 0 { t.Fatalf("enqueue batches = %+v, want none (selected channel is sync-disabled)", redis.enqueueBatches) } } func TestProduceScopeSelectingUnknownChannelEnqueuesNothing(t *testing.T) { producer, redis := newScopeProducer(t, []Card{ {ID: 1, ChanKey: "217", TempKey: "sim_yd_v5", Operator: 1}, }) if err := producer.Produce(context.Background(), scopedMessage("scoped", "does-not-exist")); err != nil { t.Fatal(err) } redis.enqueueBatches = map[string]int{} for _, ch := range []string{"217", "173", "189", "ghost-channel", "a", "b", "mobile-a", "mobile-b"} { key, _ := producer.store.RoundActiveKey("scoped", 1, ch) n, _ := producer.store.SizeKey(context.Background(), key) if n > 0 { redis.enqueueBatches["flow:"+ch] = int(n) } } redis.mu.Lock() defer redis.mu.Unlock() if len(redis.enqueueBatches) != 0 { t.Fatalf("enqueue batches = %+v, want none", redis.enqueueBatches) } } func TestProduceRejectsInvalidScope(t *testing.T) { producer, redis := newScopeProducer(t, []Card{ {ID: 1, ChanKey: "217", TempKey: "sim_yd_v5", Operator: 1}, }) if err := producer.Produce(context.Background(), scopedMessage("bad", "invalid/chan")); err == nil { t.Fatal("expected invalid chanKey to be rejected") } redis.enqueueBatches = map[string]int{} for _, ch := range []string{"217", "173", "189", "ghost-channel", "a", "b", "mobile-a", "mobile-b"} { key, _ := producer.store.RoundActiveKey("scoped", 1, ch) n, _ := producer.store.SizeKey(context.Background(), key) if n > 0 { redis.enqueueBatches["flow:"+ch] = int(n) } } redis.mu.Lock() defer redis.mu.Unlock() if len(redis.enqueueBatches) != 0 { t.Fatalf("rejected scope must not enqueue: %+v", redis.enqueueBatches) } } func TestProduceScopeDeduplicatesChannels(t *testing.T) { producer, redis := newScopeProducer(t, []Card{ {ID: 1, ChanKey: "217", TempKey: "sim_yd_v5", Operator: 1}, }) if err := producer.Produce(context.Background(), scopedMessage("scoped", "217", "217")); err != nil { t.Fatal(err) } redis.enqueueBatches = map[string]int{} for _, ch := range []string{"217", "173", "189", "ghost-channel", "a", "b", "mobile-a", "mobile-b"} { key, _ := producer.store.RoundActiveKey("scoped", 1, ch) n, _ := producer.store.SizeKey(context.Background(), key) if n > 0 { redis.enqueueBatches["flow:"+ch] = int(n) } } redis.mu.Lock() defer redis.mu.Unlock() if redis.enqueueBatches["flow:217"] != 1 { t.Fatalf("enqueue batches = %+v, want 1 task despite a duplicated chanKey", redis.enqueueBatches) } } func TestScopeDigestIsOrderIndependent(t *testing.T) { first := &model.SyncScope{Channels: []string{"173", "217"}} second := &model.SyncScope{Channels: []string{"217", "173"}} // Normalization sorts the channels, so both requests produce one digest. normalizedFirst, err := model.NormalizeScope(first) if err != nil { t.Fatal(err) } normalizedSecond, err := model.NormalizeScope(second) if err != nil { t.Fatal(err) } if scopeDigest(normalizedFirst) != scopeDigest(normalizedSecond) { t.Fatalf("digest depends on channel order: %s vs %s", scopeDigest(normalizedFirst), scopeDigest(normalizedSecond)) } if scopeDigest(nil) != "" { t.Fatalf("full sync must have an empty digest") } } func TestRunAlreadyFinishedTreatsMissingRunAsFirstDelivery(t *testing.T) { producer, _ := newScopeProducer(t, []Card{ {ID: 1, ChanKey: "217", TempKey: "sim_yd_v5", Operator: 1}, }) // The fake Redis returns no metadata, which must not be treated as finished. finished, err := producer.runAlreadyFinished(context.Background(), "fresh") if err != nil { t.Fatalf("unexpected error: %v", err) } if finished { t.Fatal("a run without stored metadata must not be reported as finished") } }