cce3d61799
The web proxy for kagane covers is dead: since #62 every Site's cover bytes land in the content-addressed store at creation or on the poll, and the one public route serves them all. Remove the second way to reach a Cover: - Bookmark.CoverURL() and the templates' use of it: templates render the wire value (.Cover) and nothing else. - GET /img/kagane/{id}, web.CoverFetcher, coverIDRe: the route and its identifier validation are gone, and with them web/cover.go. - store.KaganeImageID, GetKaganeCover, PutKaganeCover, kaganeCoverSourceURL: the proxy's persistence. - Bookmark.CoverSource: dead once CoverURL is gone. Acquisition keeps the browser where kagane genuinely needs it, but the Site name leaves the routing: kaganeImageURLRe lives in browser.go with the rest of the per-Site knowledge, browserCoverURL claims the URLs the sidecar alone can fetch, and fetchCoverBytes routes by URL shape with no Site argument. No plain-TLS fallback for a claimed URL — that would only retrieve a challenge page. Cover tests: kagane route tests removed, the general-route guarantees they pinned kept and re-pinned — unstored and traversal-shaped addresses serve nothing (TestPublicCoverRejectsUnknownAddress), non-image content types are never echoed back (TestPublicCoverNeverEchoesNonImage + TestCoverStoreAcceptsAnySourceURL). Store content-addressing and reopen-persistence tests rewritten against PutCover/GetCover. No Site name remains in a cover code path outside the acquisition module; go test ./... green.
118 lines
4.3 KiB
Go
118 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"bookmarkmanager/backend/internal/store"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// The acquired Cover is served from this deployment's own origin, to any
|
|
// browser rendering a third-party page — no session, no credential (ADR-0007).
|
|
func TestPublicCoverServesStoredBytesUnauthenticated(t *testing.T) {
|
|
const sourceURL = "https://cdn.asurascans.com/covers/solo.webp"
|
|
srv, st := newWebTestServer(t, testConfig())
|
|
if err := st.PutCover(sourceURL, []byte("\x00webp-bytes"), "image/webp"); err != nil {
|
|
t.Fatalf("PutCover: %v", err)
|
|
}
|
|
|
|
// The wire URL is what a client actually requests, so the path under test
|
|
// is taken from it rather than rebuilt by hand.
|
|
wire := st.CoverWireURL(store.CoverAddress(sourceURL))
|
|
path, ok := strings.CutPrefix(wire, testCoverBaseURL)
|
|
if !ok {
|
|
t.Fatalf("wire URL %q is not on the public origin %q", wire, testCoverBaseURL)
|
|
}
|
|
rr := getCover(t, srv, path, nil)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200 without any credential", rr.Code)
|
|
}
|
|
if got := rr.Body.String(); got != "\x00webp-bytes" {
|
|
t.Fatalf("body = %q, want the stored bytes", got)
|
|
}
|
|
if got := rr.Header().Get("Content-Type"); got != "image/webp" {
|
|
t.Fatalf("Content-Type = %q, want the stored one", got)
|
|
}
|
|
// Content-addressed bytes never change, so a client that has them must
|
|
// never need to ask again.
|
|
if got := rr.Header().Get("Cache-Control"); !strings.Contains(got, "immutable") {
|
|
t.Fatalf("Cache-Control = %q, want an immutable cache directive", got)
|
|
}
|
|
}
|
|
|
|
func TestPublicCoverRejectsUnknownAddress(t *testing.T) {
|
|
srv, _ := newWebTestServer(t, testConfig())
|
|
cases := map[string]string{
|
|
"unknown": "/covers/" + store.CoverAddress("https://cdn.example/never-stored.jpg"),
|
|
"malformed": "/covers/not-an-address",
|
|
"traversal": "/covers/../../etc/passwd",
|
|
"empty": "/covers/",
|
|
}
|
|
for name, path := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
if rr := getCover(t, srv, path, nil); rr.Code == http.StatusOK {
|
|
t.Fatalf("%s: status = 200, want anything but a served body", path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A content type outside the image set is never echoed back. The old kagane
|
|
// proxy could fetch text/html from a challenged fetch and had to refuse it;
|
|
// the general route's only input is what the store accepted, and the store
|
|
// refuses to record anything that is not an image, so the address that would
|
|
// name one is a miss, not a served body.
|
|
func TestPublicCoverNeverEchoesNonImage(t *testing.T) {
|
|
const sourceURL = "https://cdn.example/cover"
|
|
srv, st := newWebTestServer(t, testConfig())
|
|
if err := st.PutCover(sourceURL, []byte("<script>"), "text/html"); err == nil {
|
|
t.Fatal("PutCover accepted a non-image content type")
|
|
}
|
|
rr := getCover(t, srv, "/covers/"+store.CoverAddress(sourceURL), nil)
|
|
if rr.Code == http.StatusOK {
|
|
t.Fatalf("status = 200, want nothing served for a cover the store refused")
|
|
}
|
|
}
|
|
|
|
// The whole point of acquiring bytes is that the UI shows them: the card's
|
|
// <img> must carry the public address, not a third-party URL and not a
|
|
// placeholder.
|
|
func TestListRendersAcquiredCover(t *testing.T) {
|
|
const sourceURL = "https://static.comix.to/039d/i/1/34/6a6742bf15736@280.jpg"
|
|
srv, st := newWebTestServer(t, testConfig())
|
|
if _, err := st.Upsert(st.OwnerID(), store.Bookmark{
|
|
Key: "comix:n8we", Site: "comix", SeriesID: "n8we", Title: "Dungeons and Crayons",
|
|
SeriesURL: "https://comix.to/title/n8we", UpdatedAt: 1000,
|
|
}); err != nil {
|
|
t.Fatalf("seed: %v", err)
|
|
}
|
|
if err := st.SetSeriesCover("comix", "n8we", sourceURL, []byte("\xff\xd8jpeg"), "image/jpeg"); err != nil {
|
|
t.Fatalf("SetSeriesCover: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/ui/list", nil)
|
|
req.AddCookie(sessionCookie(t, st))
|
|
rr := httptest.NewRecorder()
|
|
srv.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rr.Code)
|
|
}
|
|
want := `src="` + testCoverBaseURL + "/covers/" + store.CoverAddress(sourceURL) + `"`
|
|
if !strings.Contains(rr.Body.String(), want) {
|
|
t.Fatalf("rendered list does not contain %s", want)
|
|
}
|
|
}
|