package simstatus import ( "context" "errors" "testing" "time" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model" queue "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/queue/zset" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/testutil" ) func newScopeTestProducer(t *testing.T, cards []Card) (*Producer, *queue.Store) { t.Helper() store, err := queue.New(testutil.Redis(t), "sim-status:") if err != nil { t.Fatal(err) } producer, err := New(cardScanner{cards: cards}, store, Config{PageSize: 10}) if err != nil { t.Fatal(err) } return producer, store } func scopeMessage(runID string, channels ...string) model.SimStatusQueueStartMessage { return model.SimStatusQueueStartMessage{ RunID: runID, Scope: &model.SyncScope{Channels: channels}, } } func TestProduceScopeEnqueuesOnlySelectedChannels(t *testing.T) { cards := []Card{ {ID: 1, ICCID: "a", ChanKey: "217", TempKey: "sim_yd_v5"}, {ID: 2, ICCID: "b", ChanKey: "173", TempKey: "sim_yd_v5"}, {ID: 3, ICCID: "c", ChanKey: "217", TempKey: "sim_yd_v5"}, {ID: 4, ICCID: "d", ChanKey: "189", TempKey: "sim_yd_v5"}, } producer, store := newScopeTestProducer(t, cards) ctx := context.Background() if err := producer.Produce(ctx, scopeMessage("scoped", "217")); err != nil { t.Fatal(err) } channels, err := store.RunChannels(ctx, "scoped") if err != nil { t.Fatal(err) } if len(channels) != 1 || channels[0] != "217" { t.Fatalf("run channels = %v, want only 217", channels) } meta, err := store.GetRunMeta(ctx, "scoped") if err != nil { t.Fatal(err) } if meta.ScopeChannels != 1 { t.Fatalf("scopeChannels = %d, want 1", meta.ScopeChannels) } if meta.ScopeDigest == "" { t.Fatalf("scopeDigest must be recorded for a partial run") } if meta.ScopeChannelsSample != "217" { t.Fatalf("scopeChannelsSample = %q, want 217", meta.ScopeChannelsSample) } } func TestProduceFullScopeRecordsNoScopeMetadata(t *testing.T) { cards := []Card{ {ID: 1, ICCID: "a", ChanKey: "217", TempKey: "sim_yd_v5"}, {ID: 2, ICCID: "b", ChanKey: "173", TempKey: "sim_yd_v5"}, } producer, store := newScopeTestProducer(t, cards) ctx := context.Background() if err := producer.Produce(ctx, model.SimStatusQueueStartMessage{RunID: "full"}); err != nil { t.Fatal(err) } meta, err := store.GetRunMeta(ctx, "full") if err != nil { t.Fatal(err) } if meta.ScopeChannels != 0 || meta.ScopeDigest != "" || meta.ScopeChannelsSample != "" { t.Fatalf("full run must not record scope metadata: %+v", meta) } channels, err := store.RunChannels(ctx, "full") if err != nil { t.Fatal(err) } if len(channels) != 2 { t.Fatalf("run channels = %v, want both channels", channels) } } func TestProduceScopeCombinesWithSyncDisabledGuard(t *testing.T) { cards := []Card{ {ID: 1, ICCID: "a", ChanKey: "217", TempKey: "sim_yd_v5"}, // Selected but excluded by the sync_enabled guard: scope must not // override it, and the run must still complete cleanly. {ID: 2, ICCID: "b", ChanKey: "173", TempKey: "sim_yd_v5", SyncEnabled: true}, {ID: 3, ICCID: "c", ChanKey: "189", TempKey: "sim_yd_v5"}, } producer, store := newScopeTestProducer(t, cards) ctx := context.Background() if err := producer.Produce(ctx, scopeMessage("scoped", "173")); err != nil { t.Fatal(err) } channels, err := store.RunChannels(ctx, "scoped") if err != nil { t.Fatal(err) } if len(channels) != 0 { t.Fatalf("run channels = %v, want none (only selected channel is sync-disabled)", channels) } meta, err := store.GetRunMeta(ctx, "scoped") if err != nil { t.Fatal(err) } // The producer never enqueues work for a sync-disabled channel. The run is // closed by the consumer's idle check, which is simulated here. if !meta.ProducerDone || meta.ActiveCount != 0 || meta.EnqueuedCount != 0 { t.Fatalf("empty scoped run metadata = %+v", meta) } if completed, err := store.CompleteRunIfIdle(ctx, "scoped"); err != nil || !completed { t.Fatalf("empty scoped run must be closable: completed=%v err=%v", completed, err) } meta, err = store.GetRunMeta(ctx, "scoped") if err != nil { t.Fatal(err) } if meta.Status != "completed" { t.Fatalf("status = %q, want completed", meta.Status) } } func TestProduceScopeSelectingUnknownChannelProducesEmptyRun(t *testing.T) { cards := []Card{ {ID: 1, ICCID: "a", ChanKey: "217", TempKey: "sim_yd_v5"}, } producer, store := newScopeTestProducer(t, cards) ctx := context.Background() // An unknown channel is not an error; it simply matches no card. if err := producer.Produce(ctx, scopeMessage("scoped", "does-not-exist")); err != nil { t.Fatal(err) } meta, err := store.GetRunMeta(ctx, "scoped") if err != nil { t.Fatal(err) } if !meta.ProducerDone || meta.EnqueuedCount != 0 || meta.ActiveCount != 0 { t.Fatalf("unknown-channel run metadata = %+v", meta) } if meta.ScopeChannels != 1 { t.Fatalf("scopeChannels = %d, want the requested 1", meta.ScopeChannels) } if completed, err := store.CompleteRunIfIdle(ctx, "scoped"); err != nil || !completed { t.Fatalf("unknown-channel run must be closable: completed=%v err=%v", completed, err) } } func TestProduceRejectsInvalidScope(t *testing.T) { producer, store := newScopeTestProducer(t, []Card{{ID: 1, ICCID: "a", ChanKey: "217", TempKey: "sim_yd_v5"}}) ctx := context.Background() err := producer.Produce(ctx, scopeMessage("bad", "invalid/chan")) if err == nil { t.Fatal("expected invalid chanKey to be rejected") } // A rejected request must not leave a run behind. if _, metaErr := store.GetRunMeta(ctx, "bad"); metaErr == nil { t.Fatal("rejected scope created a run") } } func TestProduceScopeDeduplicatesChannels(t *testing.T) { producer, store := newScopeTestProducer(t, []Card{ {ID: 1, ICCID: "a", ChanKey: "217", TempKey: "sim_yd_v5"}, }) ctx := context.Background() if err := producer.Produce(ctx, scopeMessage("scoped", "217", "217", "217")); err != nil { t.Fatal(err) } meta, err := store.GetRunMeta(ctx, "scoped") if err != nil { t.Fatal(err) } if meta.ScopeChannels != 1 { t.Fatalf("scopeChannels = %d, want 1 after deduplication", meta.ScopeChannels) } } func TestProduceRejectsRedeliveryOfFinishedRun(t *testing.T) { producer, store := newScopeTestProducer(t, []Card{ {ID: 1, ICCID: "a", ChanKey: "217", TempKey: "sim_yd_v5"}, }) ctx := context.Background() if err := producer.Produce(ctx, model.SimStatusQueueStartMessage{RunID: "done"}); err != nil { t.Fatal(err) } // Drain the queued work so the run reaches a terminal state. claims, err := store.ClaimRun(ctx, "done", "217", time.Now(), time.Now().Add(time.Minute), 10) if err != nil || len(claims) != 1 { t.Fatalf("claims=%v err=%v", claims, err) } if _, err = store.FinalizeRunTasks(ctx, claims, []queue.TaskOutcome{{Success: true}}); err != nil { t.Fatal(err) } if completed, err := store.CompleteRunIfIdle(ctx, "done"); err != nil || !completed { t.Fatalf("complete run: completed=%v err=%v", completed, err) } // A redelivered start message must not resurrect the finished run. err = producer.Produce(ctx, model.SimStatusQueueStartMessage{RunID: "done"}) if !errors.Is(err, ErrRunFinished) { t.Fatalf("redelivery error = %v, want ErrRunFinished", err) } }