314 lines
9.6 KiB
Go
314 lines
9.6 KiB
Go
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"),
|
|
Version: "test-version",
|
|
}, 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), "<!doctype html>") || !strings.Contains(string(body), "Version test-version") {
|
|
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, "<!doctype html>") {
|
|
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(`<svg xmlns="http://www.w3.org/2000/svg"></svg>`)); 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]
|
|
}
|