package web import ( "net/http" "net/http/httptest" "strings" "testing" "testing/fstest" "khosra/internal/content" "khosra/internal/render" ) // unpublishedFS holds a draft, a future-dated bundle and a live one, each a directory bundle with a file. func unpublishedFS() fstest.MapFS { return fstest.MapFS{ "content/art/draft/index.md": {Data: []byte("---\ntitle: Draft\ndraft: true\n---\nx\n")}, "content/art/draft/one.jpg": {Data: []byte("secret bytes")}, "content/art/future/index.md": {Data: []byte("---\ntitle: Future\ndate: 2099-01-01\n---\nx\n")}, "content/art/future/two.jpg": {Data: []byte("not yet")}, "content/art/live/index.md": {Data: []byte("---\ntitle: Live\ndate: 2020-01-01\n---\nx\n")}, "content/art/live/three.jpg": {Data: []byte("fine")}, } } func TestNothingInsideAnUnpublishedBundleIsServed(t *testing.T) { // ADR-0024: an unpublished bundle answers 404 for itself *and* for every file inside it, since 403 would // confirm the work exists. The guard is the bundle lookup — which is why the asset route asks for the // bundle before reading any bytes, and why this needed no second filter. fsys := unpublishedFS() 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) } hidden := Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{}) for path, want := range map[string]int{ "/art/draft/": http.StatusNotFound, "/art/draft/one.jpg": http.StatusNotFound, "/art/future/": http.StatusNotFound, "/art/future/two.jpg": http.StatusNotFound, "/art/live/": http.StatusOK, "/art/live/three.jpg": http.StatusOK, } { rec := httptest.NewRecorder() hidden.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil)) if rec.Code != want { t.Errorf("GET %s = %d, want %d", path, rec.Code, want) } if body := rec.Body.String(); strings.Contains(body, "secret bytes") || strings.Contains(body, "not yet") { t.Fatalf("GET %s served bytes from an unpublished bundle", path) } } // Revealing them is the only thing that changes the answer. site := content.NewSite(bundles) site.Reveal() shown := Handler(Fixed(site), r, fsys, nil, content.Settings{}) for _, path := range []string{"/art/draft/", "/art/draft/one.jpg", "/art/future/", "/art/future/two.jpg"} { rec := httptest.NewRecorder() shown.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil)) if rec.Code != http.StatusOK { t.Errorf("dev mode: GET %s = %d, want 200", path, rec.Code) } } } func TestUnpublishedBundlesAreAbsentFromEverythingThatLists(t *testing.T) { // A listing, a feed and a sitemap all go through the same Query, so hiding a draft in one place hides it // everywhere. That is the property worth testing rather than each surface separately. fsys := unpublishedFS() fsys["content/art/live/index.md"] = &fstest.MapFile{Data: []byte("---\ntitle: Live\ndate: 2020-01-01\n---\nx\n")} bundles, err := content.Scan(fsys) if err != nil { t.Fatal(err) } settings := content.Settings{Base: "https://khosra.example", Title: "Khosra"} r, err := render.New(nil, settings, nil) if err != nil { t.Fatal(err) } h := Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, settings) for _, path := range []string{"/art/", "/feed.xml", "/sitemap.xml"} { rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil)) body := rec.Body.String() if !strings.Contains(body, "live") && !strings.Contains(body, "Live") { t.Errorf("GET %s lost the published bundle:\n%s", path, body) } for _, hidden := range []string{"draft", "future"} { if strings.Contains(body, hidden) { t.Errorf("GET %s leaked the %s bundle:\n%s", path, hidden, body) } } } }