package httpapi import ( "context" "io" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "git.vakhrushev.me/av/jellybit/internal/store" "git.vakhrushev.me/av/jellybit/internal/worker" ) // stubReader — минимальный Reader для проверки рендера списка. type stubReader struct { list []store.Download one *store.Download } func (s stubReader) ListDownloads(context.Context) ([]store.Download, error) { return s.list, nil } func (s stubReader) ListDownloadsPage(context.Context, store.ListFilter) ([]store.Download, int, error) { return s.list, len(s.list), nil } func (s stubReader) GetDownload(context.Context, int64) (*store.Download, error) { if s.one == nil { return nil, store.ErrNotFound } return s.one, nil } // stubReviewer — Reviewer-заглушка (нужна для /download/{id}). type stubReviewer struct{ data *worker.ReviewData } func (s stubReviewer) ReviewData(context.Context, int64) (*worker.ReviewData, error) { if s.data == nil { return nil, store.ErrNotFound } return s.data, nil } func (stubReviewer) Apply(context.Context, int64) error { return nil } func (stubReviewer) Refine(context.Context, int64, string) error { return nil } func (stubReviewer) SetType(context.Context, int64, string) error { return nil } func (stubReviewer) IgnoreFile(context.Context, int64, string) error { return nil } func (stubReviewer) Defer(context.Context, int64) error { return nil } func (stubReviewer) Undo(context.Context, int64) error { return nil } func (stubReviewer) Relink(context.Context, int64) error { return nil } func (stubReviewer) Rerecognize(context.Context, int64) error { return nil } func (stubReviewer) ChooseCandidate(context.Context, int64, int64) error { return nil } func (stubReviewer) SetProviderID(context.Context, int64, string, string) error { return nil } func (stubReviewer) ClearProvider(context.Context, int64) error { return nil } // stubLive — заглушка источника живой телеметрии. type stubLive struct{ m map[string]worker.Live } func (s stubLive) Live(infohash string) (worker.Live, bool) { l, ok := s.m[infohash] return l, ok } func testRouter(t *testing.T, r stubReader, rv stubReviewer) http.Handler { t.Helper() return testRouterLive(t, r, rv, stubLive{}) } func testRouterLive(t *testing.T, r stubReader, rv stubReviewer, lv stubLive) http.Handler { t.Helper() h, err := NewRouter(Deps{ Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), Reader: r, Reviewer: rv, Live: lv, }) if err != nil { t.Fatalf("NewRouter: %v", err) } return h } func get(t *testing.T, h http.Handler, path string) *httptest.ResponseRecorder { t.Helper() rr := httptest.NewRecorder() h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, path, nil)) return rr } // TestRouterRendersPages проверяет, что шаблоны парсятся и страницы рендерятся. func TestRouterRendersPages(t *testing.T) { dl := store.Download{ID: 7, SourceRef: "Fargo.S02", Infohash: store.NullString("a1b2c3d4e5f6a7b8"), State: store.StateReview} h := testRouter(t, stubReader{list: []store.Download{dl}, one: &dl}, stubReviewer{data: &worker.ReviewData{Download: dl}}, ) if rr := get(t, h, "/"); rr.Code != http.StatusOK { t.Fatalf("GET / = %d, want 200", rr.Code) } else if !strings.Contains(rr.Body.String(), "st-review") { t.Errorf("index не содержит бейдж статуса") } if rr := get(t, h, "/download/7"); rr.Code != http.StatusOK { t.Fatalf("GET /download/7 = %d, want 200", rr.Code) } } // TestStaticServed проверяет отдачу встроенной статики с кэш-заголовком. func TestStaticServed(t *testing.T) { h := testRouter(t, stubReader{}, stubReviewer{}) rr := get(t, h, "/static/css/jellybit.css") if rr.Code != http.StatusOK { t.Fatalf("GET css = %d, want 200", rr.Code) } if cc := rr.Header().Get("Cache-Control"); cc == "" { t.Errorf("нет Cache-Control на статике") } if !strings.Contains(rr.Body.String(), "@font-face") { t.Errorf("css без @font-face (шрифты не self-hosted?)") } } // TestDownloadNotFound — несуществующая загрузка → 404. func TestDownloadNotFound(t *testing.T) { h := testRouter(t, stubReader{}, stubReviewer{}) if rr := get(t, h, "/download/999"); rr.Code != http.StatusNotFound { t.Fatalf("GET /download/999 = %d, want 404", rr.Code) } }