package store import ( "context" "errors" "sync" "testing" "time" ) func TestEnqueueJobTenantsDoNotShareLock(t *testing.T) { m := NewMemory() ctx := context.Background() unlock := m.lockJobEnqueueTenant("tenant-a") var once sync.Once release := func() { once.Do(unlock) } t.Cleanup(release) otherDone := make(chan error, 1) go func() { _, _, err := m.EnqueueJob(ctx, JobEnqueue{TenantID: "tenant-b", Kind: "test"}) otherDone <- err }() select { case err := <-otherDone: if err != nil { t.Fatal(err) } case <-time.After(2 * time.Second): t.Fatal("other tenant waited on the held tenant lock") } sameDone := make(chan struct{}) go func() { _, _, err := m.EnqueueJob(ctx, JobEnqueue{TenantID: "tenant-a", Kind: "test"}) if err != nil { t.Error(err) } close(sameDone) }() select { case <-sameDone: t.Fatal("same tenant did not wait on its lock") case <-time.After(200 * time.Millisecond): } release() select { case <-sameDone: case <-time.After(2 * time.Second): t.Fatal("same tenant did not finish after unlock") } } func TestEnqueueJobIdempotencyKeyConflicts(t *testing.T) { m := NewMemory() ctx := context.Background() key := "same-key" in := JobEnqueue{TenantID: "tenant-a", Kind: "refresh", IdempotencyKey: &key, Meta: map[string]any{"module": "a"}} first, created, err := m.EnqueueJob(ctx, in) if err != nil || !created || first.ID == "" { t.Fatalf("first=%+v created=%v err=%v", first, created, err) } in.Meta = map[string]any{"module": "b"} if _, _, err = m.EnqueueJob(ctx, in); !errors.Is(err, ErrJobConflict) { t.Fatalf("same key accepted: %v", err) } other := JobEnqueue{TenantID: "tenant-b", Kind: "refresh", IdempotencyKey: &key, Meta: map[string]any{"module": "b"}} if _, created, err = m.EnqueueJob(ctx, other); err != nil || !created { t.Fatalf("other tenant created=%v err=%v", created, err) } } func TestMemoryJobQueueEqualTimestampOrdering(t *testing.T) { m := NewMemory() ctx := context.Background() stamp := time.Date(2026, 10, 26, 0, 0, 0, 0, time.UTC) var ids []string for range 32 { j, _, err := m.EnqueueJob(ctx, JobEnqueue{TenantID: "tenant", Kind: "test"}) if err != nil { t.Fatal(err) } ids = append(ids, j.ID) m.jobMu.Lock() saved := m.jobs[j.ID] saved.CreatedAt = stamp m.jobs[j.ID] = saved m.jobMu.Unlock() } // Repeated offset pages must use the same total order despite map iteration. for range 5 { cursor := "" var listed []string for { page, next, more, err := m.ListJobs(ctx, "tenant", "", "", cursor, 3) if err != nil { t.Fatal(err) } for _, j := range page { listed = append(listed, j.ID) } if !more { break } cursor = next } if len(listed) != len(ids) { t.Fatalf("listed %d jobs", len(listed)) } for i, id := range listed { if id != ids[len(ids)-1-i] { t.Fatalf("list[%d]=%s expected reverse enqueue sequence %s", i, id, ids[len(ids)-1-i]) } } } for i, want := range ids { claimed, err := m.ClaimJobs(ctx, "owner", 1, time.Minute) if err != nil || len(claimed) != 1 || claimed[0].ID != want { t.Fatalf("claim[%d]=%+v %v want %s", i, claimed, err, want) } } }