package web import ( "net/http" "net/http/httptest" "strings" "testing" "testing/fstest" "khosra/internal/content" "khosra/internal/render" ) func assetHandler(t *testing.T) http.Handler { t.Helper() fsys := fstest.MapFS{ // A directory bundle owns the files beside it. "content/art/monsoon/index.md": {Data: []byte("---\ntitle: Monsoon\n---\n{{< gallery >}}\n")}, // A Bengali variant, so /bn/ is a language prefix at all: the engine treats a leading segment as a // language only when some bundle is written in it (content-model.md). "content/art/monsoon/index.bn.md": {Data: []byte("---\ntitle: বর্ষা\n---\nx\n")}, "content/art/monsoon/10-first.jpg": {Data: []byte("\xff\xd8\xff-not-really-a-jpeg")}, "content/art/monsoon/_notes.md": {Data: []byte("a fragment\n")}, "content/art/monsoon/notes.md": {Data: []byte("---\ntitle: Notes\n---\nx\n")}, // A single-file bundle has no directory of its own. "content/posts/plain.md": {Data: []byte("---\ntitle: Plain\n---\nx\n")}, "content/posts/loose.jpg": {Data: []byte("bytes")}, // A series landing page, whose directory also holds child bundles. "content/comics/series/_index.md": {Data: []byte("---\ntitle: Series\n---\nx\n")}, "content/comics/series/cover.png": {Data: []byte("png")}, "content/comics/series/one.md": {Data: []byte("---\ntitle: One\n---\nx\n")}, } bundles, err := content.Scan(fsys) if err != nil { t.Fatal(err) } r, err := render.New(nil, content.Settings{}, nil) if err != nil { t.Fatal(err) } return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{}) } func TestABundlesOwnFilesAreServed(t *testing.T) { h := assetHandler(t) rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/art/monsoon/10-first.jpg", nil)) if rec.Code != http.StatusOK { t.Fatalf("got %d, want 200 — this is what a relative src in a body resolves to", rec.Code) } if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "image/jpeg") { t.Errorf("content-type = %q, want image/jpeg from the extension", ct) } // A landing page's directory works the same way. rec = httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/comics/series/cover.png", nil)) if rec.Code != http.StatusOK { t.Errorf("a series landing page owns its files too: got %d", rec.Code) } // Under a language prefix as well: an image is not translated. rec = httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/bn/art/monsoon/10-first.jpg", nil)) if rec.Code != http.StatusOK { t.Errorf("prefixed request for the same file: got %d", rec.Code) } } func TestMarkdownInsideABundleIsNeverAnAsset(t *testing.T) { // Serving these raw would publish fragments an author never addressed, and hand out the source of a // bundle that has its own rendered URL. h := assetHandler(t) for _, path := range []string{"/art/monsoon/_notes.md", "/art/monsoon/notes.md"} { rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil)) if rec.Code == http.StatusOK { t.Errorf("GET %s = 200, want a miss:\n%s", path, rec.Body.String()) } } } func TestASingleFileBundleOwnsNoDirectory(t *testing.T) { // Its neighbours belong to the section, not to it — so nothing is served under its slash-terminated URL. h := assetHandler(t) rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/plain/loose.jpg", nil)) if rec.Code != http.StatusNotFound { t.Errorf("got %d, want 404", rec.Code) } } func TestAnAssetRequestCannotWanderOffItsBundle(t *testing.T) { h := assetHandler(t) for _, path := range []string{ "/art/monsoon/../../posts/loose.jpg", "/art/nonexistent/10-first.jpg", "/art/monsoon/missing.jpg", } { rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil)) if rec.Code == http.StatusOK { t.Errorf("GET %s = 200, want a miss:\n%s", path, rec.Body.String()) } } }