package main import ( "context" "crypto/sha256" "errors" "net/http" "net/http/httptest" "sync/atomic" "testing" "bookmarkmanager/backend/internal/pgtest" "bookmarkmanager/backend/internal/store" ) // fakeCovers stands in for the headless browser. It counts calls so the test // can prove the store spares the browser after the first navigation. type fakeCovers struct { body []byte contentType string err error calls atomic.Int32 lastID atomic.Value } func (f *fakeCovers) Image(_ context.Context, imageID string) ([]byte, string, error) { f.calls.Add(1) f.lastID.Store(imageID) if f.err != nil { return nil, "", f.err } return f.body, f.contentType, nil } const testCoverID = "019fe11a-84c3-7fc3-a84b-88787374b617" func getCover(t *testing.T, srv http.Handler, path string, cookie *http.Cookie) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodGet, path, nil) if cookie != nil { req.AddCookie(cookie) } rr := httptest.NewRecorder() srv.ServeHTTP(rr, req) return rr } // kagane serves its covers behind a Cloudflare challenge and with // cross-origin-resource-policy: same-origin, so the UI can only show one by // re-serving the bytes from its own origin. func TestKaganeCoverPersistsAndReusesStoredBytes(t *testing.T) { cf := &fakeCovers{body: []byte("\x00webp-bytes"), contentType: "image/webp"} cfg := testConfig() cfg.Covers = cf srv, st := newWebTestServer(t, cfg) cookie := sessionCookie(t, st) rr := getCover(t, srv, "/img/kagane/"+testCoverID, cookie) if rr.Code != http.StatusOK { t.Fatalf("first request: status = %d, want 200", rr.Code) } if got := rr.Body.String(); got != string(cf.body) { t.Fatalf("first request: body = %q, want %q", got, cf.body) } // A new Handler has no process-local state from the first request. The same // store must still answer without navigating the browser again. srv = newRouter(st, cfg) rr = getCover(t, srv, "/img/kagane/"+testCoverID, cookie) if rr.Code != http.StatusOK { t.Fatalf("stored request: status = %d, want 200", rr.Code) } if got := rr.Body.String(); got != string(cf.body) { t.Fatalf("stored request: body = %q, want %q", got, cf.body) } if got := cf.calls.Load(); got != 1 { t.Fatalf("fetcher called %d times, want 1 — stored bytes must survive a new handler", got) } if got := cf.lastID.Load(); got != testCoverID { t.Fatalf("fetched image id = %v, want %s", got, testCoverID) } } func TestKaganeCoverServesStoredBytesWithoutBrowser(t *testing.T) { cf := &fakeCovers{err: errors.New("browser must not be called")} cfg := testConfig() cfg.Covers = cf srv, st := newWebTestServer(t, cfg) if err := st.PutKaganeCover(testCoverID, []byte("already-stored"), "image/png"); err != nil { t.Fatalf("PutKaganeCover: %v", err) } rr := getCover(t, srv, "/img/kagane/"+testCoverID, sessionCookie(t, st)) if rr.Code != http.StatusOK || rr.Body.String() != "already-stored" { t.Fatalf("stored request = (%d, %q), want (200, already-stored)", rr.Code, rr.Body.String()) } if got := cf.calls.Load(); got != 0 { t.Fatalf("fetcher called %d times for a stored cover, want 0", got) } } func TestKaganeCoverServesPersistedBytesAfterRestart(t *testing.T) { url := pgtest.URL(t) coverDir := t.TempDir() owner := store.Owner{ DiscordID: "cover-owner", TokenHash: sha256.Sum256([]byte("cover-owner-token")), } first, err := store.Open(url, owner, coverDir) if err != nil { t.Fatalf("Open: %v", err) } if err := first.PutKaganeCover(testCoverID, []byte("survives-restart"), "image/jpeg"); err != nil { first.Close() t.Fatalf("PutKaganeCover: %v", err) } if err := first.Close(); err != nil { t.Fatalf("close first store: %v", err) } second, err := store.Open(url, owner, coverDir) if err != nil { t.Fatalf("reopen: %v", err) } defer second.Close() cf := &fakeCovers{err: errors.New("browser must not be called after restart")} cfg := testConfig() cfg.Covers = cf rr := getCover(t, newRouter(second, cfg), "/img/kagane/"+testCoverID, sessionCookie(t, second)) if rr.Code != http.StatusOK || rr.Body.String() != "survives-restart" { t.Fatalf("restarted request = (%d, %q), want (200, survives-restart)", rr.Code, rr.Body.String()) } if got := cf.calls.Load(); got != 0 { t.Fatalf("fetcher called %d times after restart, want 0", got) } } // The proxy reaches a headless browser, so it is not open to the internet. func TestKaganeCoverRequiresSession(t *testing.T) { cf := &fakeCovers{body: []byte("x"), contentType: "image/webp"} cfg := testConfig() cfg.Covers = cf srv, _ := newWebTestServer(t, cfg) rr := getCover(t, srv, "/img/kagane/"+testCoverID, nil) if rr.Code != http.StatusUnauthorized { t.Fatalf("status = %d, want 401", rr.Code) } if got := cf.calls.Load(); got != 0 { t.Fatalf("fetcher called %d times for an unauthenticated request, want 0", got) } } func TestKaganeCoverRejectsBadInput(t *testing.T) { cases := []struct { name string id string fetch *fakeCovers }{ { "an id that is not a uuid never reaches the browser", "solo-leveling", &fakeCovers{body: []byte("x"), contentType: "image/webp"}, }, { "a uuid-shaped id with a trailing segment is rejected whole", testCoverID + "x", &fakeCovers{body: []byte("x"), contentType: "image/webp"}, }, { "a challenged fetch is a missing cover", testCoverID, &fakeCovers{err: errors.New("challenge held")}, }, { "a content type outside the image set is not echoed back", testCoverID, &fakeCovers{body: []byte("