package httpapi_test import ( "context" "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "git.vakhrushev.me/av/jellybit/internal/httpapi" "git.vakhrushev.me/av/jellybit/internal/ingest" "git.vakhrushev.me/av/jellybit/internal/store" ) type fakeIngestor struct { res ingest.Result err error lastReq ingest.Request } func (f *fakeIngestor) Ingest(_ context.Context, req ingest.Request) (ingest.Result, error) { f.lastReq = req return f.res, f.err } type fakeCommander struct { cancelled []int64 retried []int64 err error } func (f *fakeCommander) Cancel(_ context.Context, id int64) error { if f.err != nil { return f.err } f.cancelled = append(f.cancelled, id) return nil } func (f *fakeCommander) Retry(_ context.Context, id int64) error { if f.err != nil { return f.err } f.retried = append(f.retried, id) return nil } type fakeReader struct { list []store.Download get *store.Download } func (f *fakeReader) ListDownloads(_ context.Context) ([]store.Download, error) { return f.list, nil } func (f *fakeReader) GetDownload(_ context.Context, id int64) (*store.Download, error) { if f.get != nil { return f.get, nil } return &store.Download{ID: id, State: store.StateCancelled}, nil } func newServer(t *testing.T, d httpapi.Deps) *httptest.Server { t.Helper() if d.Logger == nil { d.Logger = slog.New(slog.NewTextHandler(io.Discard, nil)) } h, err := httpapi.NewRouter(d) if err != nil { t.Fatalf("NewRouter: %v", err) } srv := httptest.NewServer(h) t.Cleanup(srv.Close) return srv } func TestAPIAdd(t *testing.T) { ing := &fakeIngestor{res: ingest.Result{DownloadID: 1, Infohash: "abc", State: store.StateDownloading}} srv := newServer(t, httpapi.Deps{Ingestor: ing, Commander: &fakeCommander{}, Reader: &fakeReader{}}) resp, err := http.Post(srv.URL+"/api/downloads", "application/json", strings.NewReader(`{"source":"magnet:?xt=urn:btih:abc","context":"Дюна"}`)) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { t.Fatalf("status = %d, want 201", resp.StatusCode) } var got map[string]any _ = json.NewDecoder(resp.Body).Decode(&got) if got["id"].(float64) != 1 || got["state"] != "downloading" { t.Errorf("body = %v", got) } if ing.lastReq.Context != "Дюна" { t.Errorf("контекст не проброшен: %q", ing.lastReq.Context) } } func TestAPIAddBadInput(t *testing.T) { ing := &fakeIngestor{err: ingestErr("bad magnet")} srv := newServer(t, httpapi.Deps{Ingestor: ing, Commander: &fakeCommander{}, Reader: &fakeReader{}}) resp, err := http.Post(srv.URL+"/api/downloads", "application/json", strings.NewReader(`{"source":"x"}`)) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Fatalf("status = %d, want 400", resp.StatusCode) } } func TestAPIList(t *testing.T) { reader := &fakeReader{list: []store.Download{ {ID: 2, SourceType: store.SourceMagnet, State: store.StateCompleted, Infohash: store.NullString("abc")}, {ID: 1, SourceType: store.SourceMagnet, State: store.StateDownloading}, }} srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{}, Reader: reader}) resp, err := http.Get(srv.URL + "/api/downloads") if err != nil { t.Fatal(err) } defer resp.Body.Close() var got []map[string]any _ = json.NewDecoder(resp.Body).Decode(&got) if len(got) != 2 { t.Fatalf("len = %d, want 2", len(got)) } if got[0]["state"] != "completed" || got[0]["infohash"] != "abc" { t.Errorf("first = %v", got[0]) } } func TestAPICancel(t *testing.T) { cmd := &fakeCommander{} srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: cmd, Reader: &fakeReader{}}) resp, err := http.Post(srv.URL+"/api/downloads/5/cancel", "", nil) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } if len(cmd.cancelled) != 1 || cmd.cancelled[0] != 5 { t.Errorf("cancel вызван неверно: %v", cmd.cancelled) } } func TestIndexRenders(t *testing.T) { reader := &fakeReader{list: []store.Download{ {ID: 1, SourceType: store.SourceMagnet, SourceRef: "magnet:?xt=urn:btih:abc", State: store.StateDownloading}, }} srv := newServer(t, httpapi.Deps{Ingestor: &fakeIngestor{}, Commander: &fakeCommander{}, Reader: reader}) resp, err := http.Get(srv.URL + "/") if err != nil { t.Fatal(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d", resp.StatusCode) } if !strings.Contains(string(body), "jellybit") || !strings.Contains(string(body), "downloading") { t.Error("страница не содержит ожидаемого контента") } } type ingestErr string func (e ingestErr) Error() string { return string(e) }