Files
photog/manage_test.go
T
foxandClaude Fable 5.1 b7fdc8a3bc feat(generator): add -default-view to choose timeline or grid as site root
The chosen view is published at /, the other under /timeline/ or
/grid/. Page paths, pager links and the cross-links between the views
are derived from the setting. The directory named after the default
view is removed on build, since it can only be a leftover from before
a switch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 07:10:29 +02:00

196 lines
6.0 KiB
Go

package main
import (
"bytes"
"image"
"image/jpeg"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"github.com/BurntSushi/toml"
)
// newTestServer returns a server over a content directory holding one photo,
// "sunrise", with a sidecar.
func newTestServer(t *testing.T) *Server {
t.Helper()
// Not t.TempDir(): an edit kicks off a background rebuild that can still
// be writing when the test ends, and TempDir fails the test if its
// cleanup finds the directory busy.
root, err := os.MkdirTemp("", "photog-test-")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.RemoveAll(root) })
content := filepath.Join(root, "content")
output := filepath.Join(root, "public")
for _, d := range []string{content, output} {
if err := os.MkdirAll(d, 0755); err != nil {
t.Fatal(err)
}
}
var img bytes.Buffer
if err := jpeg.Encode(&img, image.NewRGBA(image.Rect(0, 0, 8, 8)), nil); err != nil {
t.Fatal(err)
}
writeFile(t, filepath.Join(content, "sunrise.jpg"), img.Bytes())
writeFile(t, filepath.Join(content, "sunrise.toml"),
[]byte("caption = \"Tempelhof at sunrise\"\ndate = \"2026-06-14\"\n"))
g := NewGenerator(content, output, "https://photos.example.com",
"Ada", "", "", "", "", "Crimson Pro", "Manjari", 60, viewTimeline)
return NewServer("0", content, output, false, g)
}
func writeFile(t *testing.T, path string, data []byte) {
t.Helper()
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
}
func readMeta(t *testing.T, s *Server, slug string) PhotoMeta {
t.Helper()
var meta PhotoMeta
if _, err := toml.DecodeFile(filepath.Join(s.contentDir, slug+".toml"), &meta); err != nil {
t.Fatal(err)
}
return meta
}
// postEdit submits the edit form for slug and returns the response.
func postEdit(s *Server, slug string, form url.Values, headers map[string]string) *httptest.ResponseRecorder {
r := httptest.NewRequest("POST", "/manage/edit/"+slug, strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
for k, v := range headers {
r.Header.Set(k, v)
}
r.SetPathValue("slug", slug)
w := httptest.NewRecorder()
s.handleEdit(w, r)
return w
}
func TestManagePageListsPhotos(t *testing.T) {
s := newTestServer(t)
w := httptest.NewRecorder()
s.handleManagePage(w, httptest.NewRequest("GET", "/manage", nil))
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", w.Code)
}
body := w.Body.String()
for _, want := range []string{"Tempelhof at sunrise", "2026-06-14", `href="/manage/edit/sunrise"`} {
if !strings.Contains(body, want) {
t.Errorf("manage page is missing %q", want)
}
}
}
func TestEditPagePrefillsForm(t *testing.T) {
s := newTestServer(t)
r := httptest.NewRequest("GET", "/manage/edit/sunrise", nil)
r.SetPathValue("slug", "sunrise")
w := httptest.NewRecorder()
s.handleEditPage(w, r)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", w.Code)
}
body := w.Body.String()
for _, want := range []string{`value="Tempelhof at sunrise"`, `value="2026-06-14"`} {
if !strings.Contains(body, want) {
t.Errorf("edit page is missing %q", want)
}
}
}
func TestEditRewritesSidecar(t *testing.T) {
s := newTestServer(t)
form := url.Values{"caption": {`Tempelhof, "golden hour"`}, "date": {"2026-06-15"}}
w := postEdit(s, "sunrise", form, nil)
if w.Code != http.StatusSeeOther {
t.Fatalf("status = %d, want 303: %s", w.Code, w.Body)
}
if loc := w.Header().Get("Location"); loc != "/manage?saved=sunrise" {
t.Errorf("Location = %q", loc)
}
meta := readMeta(t, s, "sunrise")
if meta.Caption != `Tempelhof, "golden hour"` || meta.Date != "2026-06-15" {
t.Errorf("sidecar = %+v", meta)
}
}
func TestEditEmptyCaptionFallsBackToSlug(t *testing.T) {
s := newTestServer(t)
w := postEdit(s, "sunrise", url.Values{"caption": {" "}, "date": {"2026-06-14"}}, nil)
if w.Code != http.StatusSeeOther {
t.Fatalf("status = %d, want 303", w.Code)
}
if meta := readMeta(t, s, "sunrise"); meta.Caption != "sunrise" {
t.Errorf("caption = %q, want the slug", meta.Caption)
}
}
func TestEditRejectsBadInput(t *testing.T) {
valid := url.Values{"caption": {"changed"}, "date": {"2026-06-15"}}
tests := []struct {
name string
slug string
form url.Values
headers map[string]string
want int
}{
{"unknown photo", "nope", valid, nil, http.StatusNotFound},
{"path traversal", "../sunrise", valid, nil, http.StatusNotFound},
{"malformed date", "sunrise", url.Values{"caption": {"changed"}, "date": {"15.06.2026"}}, nil, http.StatusBadRequest},
{"missing date", "sunrise", url.Values{"caption": {"changed"}}, nil, http.StatusBadRequest},
{"cross-site fetch", "sunrise", valid, map[string]string{"Sec-Fetch-Site": "cross-site"}, http.StatusForbidden},
{"foreign origin", "sunrise", valid, map[string]string{"Origin": "https://evil.example"}, http.StatusForbidden},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := newTestServer(t)
w := postEdit(s, tt.slug, tt.form, tt.headers)
if w.Code != tt.want {
t.Errorf("status = %d, want %d", w.Code, tt.want)
}
if meta := readMeta(t, s, "sunrise"); meta.Caption != "Tempelhof at sunrise" || meta.Date != "2026-06-14" {
t.Errorf("sidecar was changed: %+v", meta)
}
})
}
}
func TestEditAcceptsOwnOrigin(t *testing.T) {
form := url.Values{"caption": {"changed"}, "date": {"2026-06-15"}}
tests := []struct {
name string
headers map[string]string
}{
{"same-origin fetch metadata", map[string]string{"Sec-Fetch-Site": "same-origin"}},
// Behind nginx the Host header is the upstream address, so only
// -base-url can vouch for the public origin.
{"origin equal to base URL", map[string]string{"Origin": "https://photos.example.com"}},
{"origin equal to host", map[string]string{"Origin": "http://example.com"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := newTestServer(t)
if w := postEdit(s, "sunrise", form, tt.headers); w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want 303: %s", w.Code, w.Body)
}
})
}
}