package app import ( "bytes" "io" "mime/multipart" "net/http" "net/http/httptest" "net/url" "path/filepath" "strconv" "strings" "testing" "archi_folio/internal/store" ) func newTestServer(t *testing.T) *Server { t.Helper() dir := t.TempDir() st, err := store.Open(filepath.Join(dir, "app.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = st.Close() }) if err := st.Migrate("admin", "changeme"); err != nil { t.Fatal(err) } srv, err := New(Config{ DatabasePath: filepath.Join(dir, "app.db"), SessionSecret: "test-secret", AdminUsername: "admin", AdminPassword: "changeme", UploadDir: filepath.Join(dir, "uploads"), }, st) if err != nil { t.Fatal(err) } return srv } func TestPublicRoutes(t *testing.T) { srv := newTestServer(t) handler := srv.Routes() for _, path := range []string{"/", "/projects", "/about", "/projects/courtyard-house"} { req := httptest.NewRequest(http.MethodGet, path, nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("%s returned %d", path, rec.Code) } } } func TestAdminRequiresLogin(t *testing.T) { srv := newTestServer(t) for _, path := range []string{"/admin", "/admin/main", "/admin/projects", "/admin/contact-details"} { req := httptest.NewRequest(http.MethodGet, path, nil) rec := httptest.NewRecorder() srv.Routes().ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("%s expected redirect, got %d", path, rec.Code) } if location := rec.Header().Get("Location"); location != "/admin/login" { t.Fatalf("%s expected login redirect, got %q", path, location) } } } func TestAdminLogin(t *testing.T) { srv := newTestServer(t) form := url.Values{"username": {"admin"}, "password": {"changeme"}} req := httptest.NewRequest(http.MethodPost, "/admin/login", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() srv.Routes().ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("expected redirect, got %d", rec.Code) } if location := rec.Header().Get("Location"); location != "/admin/main" { t.Fatalf("expected admin main redirect, got %q", location) } if len(rec.Result().Cookies()) == 0 { t.Fatal("expected session cookie") } } func TestAdminTabs(t *testing.T) { srv := newTestServer(t) handler := srv.Routes() cookie := loginCookie(t, handler) req := httptest.NewRequest(http.MethodGet, "/admin", nil) req.AddCookie(cookie) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/admin/main" { t.Fatalf("expected /admin to redirect to /admin/main, got %d %q", rec.Code, rec.Header().Get("Location")) } for _, test := range []struct { path string want string }{ {"/admin/main", "Main Content"}, {"/admin/projects", "Add Project"}, {"/admin/contact-details", "Contact Requests"}, } { req := httptest.NewRequest(http.MethodGet, test.path, nil) req.AddCookie(cookie) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("%s returned %d", test.path, rec.Code) } body, _ := io.ReadAll(rec.Result().Body) if !strings.Contains(string(body), test.want) || !strings.Contains(string(body), "") { t.Fatalf("%s did not render full tab page: %s", test.path, body) } } } func TestAdminHTMXTabRequestReturnsPartial(t *testing.T) { srv := newTestServer(t) handler := srv.Routes() cookie := loginCookie(t, handler) req := httptest.NewRequest(http.MethodGet, "/admin/projects", nil) req.Header.Set("HX-Request", "true") req.AddCookie(cookie) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected ok, got %d", rec.Code) } body, _ := io.ReadAll(rec.Result().Body) text := string(body) if strings.Contains(text, "") { t.Fatalf("expected partial response, got full document: %s", text) } if !strings.Contains(text, `hx-swap-oob="true"`) || !strings.Contains(text, "Add Project") { t.Fatalf("expected partial panel and out-of-band tab update: %s", text) } } func TestAdminMutationsRedirectToOwningTabs(t *testing.T) { srv := newTestServer(t) handler := srv.Routes() cookie := loginCookie(t, handler) for _, test := range []struct { path string form url.Values want string }{ { path: "/admin/contact-details", form: url.Values{"email": {"studio@example.com"}, "phone": {"123"}, "location": {"London"}}, want: "/admin/contact-details?ok=contact+details+saved", }, { path: "/admin/content", form: url.Values{ "hero_title": {"Hero"}, "hero_subtitle": {"Subtitle"}, "intro_title": {"Intro"}, "intro_text": {"Text"}, "about_name": {"Name"}, "about_role": {"Role"}, "about_bio": {"Bio"}, "hero_image_current": {"/static/placeholders/hero.svg"}, "about_image_current": {"/static/placeholders/about.svg"}, }, want: "/admin/main?ok=content+saved", }, } { req := httptest.NewRequest(http.MethodPost, test.path, strings.NewReader(test.form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(cookie) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != test.want { t.Fatalf("%s expected redirect %q, got %d %q", test.path, test.want, rec.Code, rec.Header().Get("Location")) } } } func TestAdminProjectValidation(t *testing.T) { srv := newTestServer(t) handler := srv.Routes() cookie := loginCookie(t, handler) form := url.Values{"title": {" "}, "location": {"London"}, "year": {"2026"}, "category": {"Residential"}, "description": {"Text"}} req := httptest.NewRequest(http.MethodPost, "/admin/projects", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(cookie) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("expected redirect, got %d", rec.Code) } if location := rec.Header().Get("Location"); !strings.Contains(location, "/admin/projects?err=") || !strings.Contains(location, "project+title+is+required") { t.Fatalf("expected validation error redirect, got %q", location) } } func TestAdminRejectsSVGUpload(t *testing.T) { srv := newTestServer(t) handler := srv.Routes() cookie := loginCookie(t, handler) var body bytes.Buffer writer := multipart.NewWriter(&body) fields := map[string]string{ "title": "Upload Test", "location": "London", "year": "2026", "category": "Residential", "description": "A project", } for key, value := range fields { if err := writer.WriteField(key, value); err != nil { t.Fatal(err) } } file, err := writer.CreateFormFile("cover_image", "bad.svg") if err != nil { t.Fatal(err) } if _, err := file.Write([]byte(``)); err != nil { t.Fatal(err) } if err := writer.Close(); err != nil { t.Fatal(err) } req := httptest.NewRequest(http.MethodPost, "/admin/projects", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.AddCookie(cookie) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("expected redirect, got %d", rec.Code) } if location := rec.Header().Get("Location"); !strings.Contains(location, "/admin/projects?err=") || !strings.Contains(location, "unsupported+image+type") { t.Fatalf("expected unsupported image redirect, got %q", location) } } func TestContactSubmissionPersists(t *testing.T) { srv := newTestServer(t) form := url.Values{"name": {"Jane"}, "email": {"jane@example.com"}, "message": {"New project"}} req := httptest.NewRequest(http.MethodPost, "/contact", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() srv.Routes().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected ok, got %d", rec.Code) } body, _ := io.ReadAll(rec.Result().Body) if !strings.Contains(string(body), "saved") { t.Fatalf("expected success message, got %s", body) } requests, err := srv.store.ContactRequests(req.Context()) if err != nil { t.Fatal(err) } if len(requests) != 1 || requests[0].Email != "jane@example.com" { t.Fatalf("unexpected contact requests: %+v", requests) } } func TestProjectImageOverlay(t *testing.T) { srv := newTestServer(t) project, err := srv.store.ProjectBySlug(t.Context(), "courtyard-house") if err != nil { t.Fatal(err) } if len(project.Images) == 0 { t.Fatal("expected seeded project images") } path := "/projects/" + project.Slug + "/images/" + strconv.FormatInt(project.Images[0].ID, 10) + "/overlay" req := httptest.NewRequest(http.MethodGet, path, nil) rec := httptest.NewRecorder() srv.Routes().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected ok, got %d", rec.Code) } body, _ := io.ReadAll(rec.Result().Body) if !strings.Contains(string(body), "data-overlay") || !strings.Contains(string(body), project.Images[0].Path) { t.Fatalf("overlay fragment missing expected content: %s", body) } } func loginCookie(t *testing.T, handler http.Handler) *http.Cookie { t.Helper() form := url.Values{"username": {"admin"}, "password": {"changeme"}} req := httptest.NewRequest(http.MethodPost, "/admin/login", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) cookies := rec.Result().Cookies() if len(cookies) == 0 { t.Fatal("expected login cookie") } return cookies[0] }