package taskqueue_test import ( "context" "encoding/json" "testing" "time" "github.com/libnovel/backend/internal/domain" "github.com/libnovel/backend/internal/taskqueue" ) // ── Compile-time interface satisfaction ─────────────────────────────────────── // stubStore satisfies all three taskqueue interfaces. // Any method that is called but not expected panics — making accidental // calls immediately visible in tests. type stubStore struct{} func (s *stubStore) CreateScrapeTask(_ context.Context, _, _ string, _, _ int) (string, error) { return "task-1", nil } func (s *stubStore) CreateAudioTask(_ context.Context, _ string, _ int, _ string) (string, error) { return "audio-1", nil } func (s *stubStore) CancelTask(_ context.Context, _ string) error { return nil } func (s *stubStore) ClaimNextScrapeTask(_ context.Context, _ string) (domain.ScrapeTask, bool, error) { return domain.ScrapeTask{ID: "task-1", Status: domain.TaskStatusRunning}, true, nil } func (s *stubStore) ClaimNextAudioTask(_ context.Context, _ string) (domain.AudioTask, bool, error) { return domain.AudioTask{ID: "audio-1", Status: domain.TaskStatusRunning}, true, nil } func (s *stubStore) FinishScrapeTask(_ context.Context, _ string, _ domain.ScrapeResult) error { return nil } func (s *stubStore) FinishAudioTask(_ context.Context, _ string, _ domain.AudioResult) error { return nil } func (s *stubStore) FailTask(_ context.Context, _, _ string) error { return nil } func (s *stubStore) HeartbeatTask(_ context.Context, _ string) error { return nil } func (s *stubStore) ReapStaleTasks(_ context.Context, _ time.Duration) (int, error) { return 0, nil } func (s *stubStore) ListScrapeTasks(_ context.Context) ([]domain.ScrapeTask, error) { return nil, nil } func (s *stubStore) GetScrapeTask(_ context.Context, _ string) (domain.ScrapeTask, bool, error) { return domain.ScrapeTask{}, false, nil } func (s *stubStore) ListAudioTasks(_ context.Context) ([]domain.AudioTask, error) { return nil, nil } func (s *stubStore) GetAudioTask(_ context.Context, _ string) (domain.AudioTask, bool, error) { return domain.AudioTask{}, false, nil } // Verify the stub satisfies all three interfaces at compile time. var _ taskqueue.Producer = (*stubStore)(nil) var _ taskqueue.Consumer = (*stubStore)(nil) var _ taskqueue.Reader = (*stubStore)(nil) // ── Behavioural tests (using stub) ──────────────────────────────────────────── func TestProducer_CreateScrapeTask(t *testing.T) { var p taskqueue.Producer = &stubStore{} id, err := p.CreateScrapeTask(context.Background(), "book", "https://example.com/book/slug", 0, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } if id == "" { t.Error("expected non-empty task ID") } } func TestConsumer_ClaimNextScrapeTask(t *testing.T) { var c taskqueue.Consumer = &stubStore{} task, ok, err := c.ClaimNextScrapeTask(context.Background(), "worker-1") if err != nil { t.Fatalf("unexpected error: %v", err) } if !ok { t.Fatal("expected a task to be claimed") } if task.Status != domain.TaskStatusRunning { t.Errorf("want running, got %q", task.Status) } } func TestConsumer_ClaimNextAudioTask(t *testing.T) { var c taskqueue.Consumer = &stubStore{} task, ok, err := c.ClaimNextAudioTask(context.Background(), "worker-1") if err != nil { t.Fatalf("unexpected error: %v", err) } if !ok { t.Fatal("expected an audio task to be claimed") } if task.ID == "" { t.Error("expected non-empty task ID") } } // ── domain.ScrapeResult / domain.AudioResult JSON shape ────────────────────── func TestScrapeResult_JSONRoundtrip(t *testing.T) { cases := []domain.ScrapeResult{ {BooksFound: 5, ChaptersScraped: 100, ChaptersSkipped: 2, Errors: 0}, {BooksFound: 0, ChaptersScraped: 0, Errors: 1, ErrorMessage: "timeout"}, } for _, orig := range cases { b, err := json.Marshal(orig) if err != nil { t.Fatalf("marshal: %v", err) } var got domain.ScrapeResult if err := json.Unmarshal(b, &got); err != nil { t.Fatalf("unmarshal: %v", err) } if got != orig { t.Errorf("want %+v, got %+v", orig, got) } } } func TestAudioResult_JSONRoundtrip(t *testing.T) { cases := []domain.AudioResult{ {ObjectKey: "audio/slug/1/af_bella.mp3"}, {ErrorMessage: "kokoro unavailable"}, } for _, orig := range cases { b, _ := json.Marshal(orig) var got domain.AudioResult json.Unmarshal(b, &got) if got != orig { t.Errorf("want %+v, got %+v", orig, got) } } }