14.8 KB
controllers_test.go
package controllers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/readysite/readysite/website/models"
)
// Test database setup is handled by testhelper_test.go
// --- Health Controller Tests ---
func TestHealthController_Check(t *testing.T) {
_, c := Health()
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
handler := c.Handle(req)
handler.(*HealthController).Check(w, req)
if w.Code != http.StatusOK {
t.Errorf("Check() status = %d, want %d", w.Code, http.StatusOK)
}
// Verify response is valid JSON with expected fields
var status HealthStatus
if err := json.Unmarshal(w.Body.Bytes(), &status); err != nil {
t.Fatalf("Check() returned invalid JSON: %v", err)
}
if status.Status != "ok" {
t.Errorf("Check() status field = %q, want %q", status.Status, "ok")
}
if status.Database != "ok" {
t.Errorf("Check() database field = %q, want %q", status.Database, "ok")
}
}
func TestHealthController_Check_ContentType(t *testing.T) {
_, c := Health()
req := httptest.NewRequest("GET", "/healthz", nil)
w := httptest.NewRecorder()
handler := c.Handle(req)
handler.(*HealthController).Check(w, req)
contentType := w.Header().Get("Content-Type")
if contentType != "application/json" {
t.Errorf("Check() Content-Type = %q, want %q", contentType, "application/json")
}
}
func TestHealthController_Check_JSONStructure(t *testing.T) {
_, c := Health()
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
handler := c.Handle(req)
handler.(*HealthController).Check(w, req)
// Verify the JSON contains exactly the expected keys
var raw map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &raw); err != nil {
t.Fatalf("Check() returned invalid JSON: %v", err)
}
if _, ok := raw["status"]; !ok {
t.Error("Check() JSON missing 'status' field")
}
if _, ok := raw["database"]; !ok {
t.Error("Check() JSON missing 'database' field")
}
}
// --- Admin Controller Tests ---
func TestAdminController_Handle(t *testing.T) {
_, c := Admin()
req := httptest.NewRequest("GET", "/admin", nil)
handler := c.Handle(req)
if handler == nil {
t.Fatal("Handle() returned nil")
}
// Verify the returned controller is of the correct type
if _, ok := handler.(*AdminController); !ok {
t.Errorf("Handle() returned %T, want *AdminController", handler)
}
}
func TestAdminController_TourCompleted_NoUser(t *testing.T) {
_, c := Admin()
// Request without authentication - no user in JWT
req := httptest.NewRequest("GET", "/admin", nil)
handler := c.Handle(req)
result := handler.(*AdminController).TourCompleted()
if !result {
t.Error("TourCompleted() = false for unauthenticated user, want true (default when no user)")
}
}
func TestAdminController_TourCompleted_AuthenticatedUser(t *testing.T) {
_, c := Admin()
req := httptest.NewRequest("GET", "/admin", nil)
authenticateRequest(req)
handler := c.Handle(req)
// For a new user without the setting, tour should not be completed
result := handler.(*AdminController).TourCompleted()
if result {
t.Error("TourCompleted() = true for new authenticated user, want false (tour not yet completed)")
}
}
// --- Partials Controller Tests ---
func TestPartialsController_Create(t *testing.T) {
_, c := Partials()
tests := []struct {
name string
formData url.Values
wantStatus int
}{
{
name: "valid partial creation",
formData: url.Values{
"name": {"Header"},
"slug": {"test-header"},
"description": {"Site header partial"},
"html": {"<header><h1>My Site</h1></header>"},
"published": {"on"},
},
wantStatus: http.StatusSeeOther, // redirect on success
},
{
name: "missing name",
formData: url.Values{
"slug": {"no-name-partial"},
"description": {"Missing name"},
"html": {"<div>Content</div>"},
},
wantStatus: http.StatusOK, // error rendered as HTML
},
{
name: "valid partial without slug",
formData: url.Values{
"name": {"Auto ID Partial"},
"description": {"Partial with auto-generated ID"},
"html": {"<div>Auto</div>"},
},
wantStatus: http.StatusSeeOther, // redirect on success
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("POST", "/admin/partials", strings.NewReader(tt.formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
authenticateRequest(req)
w := httptest.NewRecorder()
handler := c.Handle(req)
handler.(*PartialsController).Create(w, req)
if w.Code != tt.wantStatus {
t.Errorf("Create() status = %d, want %d, body: %s", w.Code, tt.wantStatus, w.Body.String())
}
})
}
}
func TestPartialsController_Create_DuplicateSlug(t *testing.T) {
// Create a partial with a specific slug
partial := &models.Partial{
Name: "Existing Partial",
Description: "Already exists",
HTML: "<div>Existing</div>",
}
partial.ID = "duplicate-slug-test"
models.Partials.Insert(partial)
// Try to create another partial with the same slug
formData := url.Values{
"name": {"Duplicate"},
"slug": {"duplicate-slug-test"},
"description": {"Should fail"},
"html": {"<div>Duplicate</div>"},
}
req := httptest.NewRequest("POST", "/admin/partials", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
authenticateRequest(req)
w := httptest.NewRecorder()
_, c := Partials()
handler := c.Handle(req)
handler.(*PartialsController).Create(w, req)
if w.Code != http.StatusOK {
t.Errorf("Create() with duplicate slug status = %d, want %d (validation error)", w.Code, http.StatusOK)
}
}
func TestPartialsController_Create_InvalidSlug(t *testing.T) {
formData := url.Values{
"name": {"Bad Slug Partial"},
"slug": {"bad slug with spaces"},
"description": {"Invalid slug"},
"html": {"<div>Content</div>"},
}
req := httptest.NewRequest("POST", "/admin/partials", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
authenticateRequest(req)
w := httptest.NewRecorder()
_, c := Partials()
handler := c.Handle(req)
handler.(*PartialsController).Create(w, req)
if w.Code != http.StatusOK {
t.Errorf("Create() with invalid slug status = %d, want %d (validation error)", w.Code, http.StatusOK)
}
}
func TestPartialsController_Update(t *testing.T) {
// Create a partial to update
partial := &models.Partial{
Name: "Original Name",
Description: "Original description",
HTML: "<div>Original</div>",
Published: false,
}
partial.ID = "update-partial-test"
models.Partials.Insert(partial)
formData := url.Values{
"name": {"Updated Name"},
"description": {"Updated description"},
"html": {"<div>Updated</div>"},
"published": {"on"},
}
req := httptest.NewRequest("PUT", "/admin/partials/update-partial-test", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetPathValue("id", "update-partial-test")
authenticateRequest(req)
w := httptest.NewRecorder()
_, c := Partials()
handler := c.Handle(req)
handler.(*PartialsController).Update(w, req)
// Update calls c.Refresh which redirects for non-HTMX requests
if w.Code != http.StatusSeeOther {
t.Errorf("Update() status = %d, want %d, body: %s", w.Code, http.StatusSeeOther, w.Body.String())
}
// Verify the partial was updated
updated, err := models.Partials.Get("update-partial-test")
if err != nil {
t.Fatalf("Failed to get updated partial: %v", err)
}
if updated.Name != "Updated Name" {
t.Errorf("Partial name = %q, want %q", updated.Name, "Updated Name")
}
if updated.Description != "Updated description" {
t.Errorf("Partial description = %q, want %q", updated.Description, "Updated description")
}
if updated.HTML != "<div>Updated</div>" {
t.Errorf("Partial HTML = %q, want %q", updated.HTML, "<div>Updated</div>")
}
if !updated.Published {
t.Error("Partial should be published after update")
}
}
func TestPartialsController_Update_NotFound(t *testing.T) {
formData := url.Values{
"name": {"Does Not Matter"},
"description": {"Does Not Matter"},
"html": {"<div>Does Not Matter</div>"},
}
req := httptest.NewRequest("PUT", "/admin/partials/nonexistent-partial", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetPathValue("id", "nonexistent-partial")
authenticateRequest(req)
w := httptest.NewRecorder()
_, c := Partials()
handler := c.Handle(req)
handler.(*PartialsController).Update(w, req)
if w.Code != http.StatusOK {
t.Errorf("Update() not found status = %d, want %d (error rendered as HTML)", w.Code, http.StatusOK)
}
}
func TestPartialsController_Update_MissingName(t *testing.T) {
// Create a partial to attempt update on
partial := &models.Partial{
Name: "Has A Name",
Description: "Description",
HTML: "<div>Content</div>",
}
partial.ID = "update-no-name-test"
models.Partials.Insert(partial)
formData := url.Values{
"description": {"Updated description"},
"html": {"<div>Updated</div>"},
}
req := httptest.NewRequest("PUT", "/admin/partials/update-no-name-test", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetPathValue("id", "update-no-name-test")
authenticateRequest(req)
w := httptest.NewRecorder()
_, c := Partials()
handler := c.Handle(req)
handler.(*PartialsController).Update(w, req)
// Should return validation error (200 OK with error HTML)
if w.Code != http.StatusOK {
t.Errorf("Update() missing name status = %d, want %d", w.Code, http.StatusOK)
}
}
func TestPartialsController_Delete(t *testing.T) {
// Create a partial to delete
partial := &models.Partial{
Name: "Delete Me",
Description: "To be deleted",
HTML: "<div>Delete</div>",
}
partial.ID = "delete-partial-test"
models.Partials.Insert(partial)
req := httptest.NewRequest("DELETE", "/admin/partials/delete-partial-test", nil)
req.SetPathValue("id", "delete-partial-test")
authenticateRequest(req)
w := httptest.NewRecorder()
_, c := Partials()
handler := c.Handle(req)
handler.(*PartialsController).Delete(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("Delete() status = %d, want %d", w.Code, http.StatusSeeOther)
}
// Verify deletion
_, err := models.Partials.Get("delete-partial-test")
if err == nil {
t.Error("Partial should be deleted")
}
}
func TestPartialsController_Delete_NotFound(t *testing.T) {
req := httptest.NewRequest("DELETE", "/admin/partials/nonexistent-delete", nil)
req.SetPathValue("id", "nonexistent-delete")
authenticateRequest(req)
w := httptest.NewRecorder()
_, c := Partials()
handler := c.Handle(req)
handler.(*PartialsController).Delete(w, req)
// Should return error rendered as HTML (200 OK)
if w.Code != http.StatusOK {
t.Errorf("Delete() not found status = %d, want %d (error rendered as HTML)", w.Code, http.StatusOK)
}
}
func TestPartialsController_Partial_ByID(t *testing.T) {
// Create a partial to retrieve
partial := &models.Partial{
Name: "Lookup Partial",
Description: "For lookup test",
HTML: "<div>Lookup</div>",
}
partial.ID = "lookup-partial-test"
models.Partials.Insert(partial)
req := httptest.NewRequest("GET", "/admin/partials/lookup-partial-test", nil)
req.SetPathValue("id", "lookup-partial-test")
_, c := Partials()
handler := c.Handle(req)
result := handler.(*PartialsController).Partial()
if result == nil {
t.Fatal("Partial() returned nil for existing partial")
}
if result.Name != "Lookup Partial" {
t.Errorf("Partial().Name = %q, want %q", result.Name, "Lookup Partial")
}
}
func TestPartialsController_Partial_NoRequest(t *testing.T) {
_, c := Partials()
// Call Partial() without setting up a request (nil request)
result := c.Partial()
if result != nil {
t.Error("Partial() should return nil when request is nil")
}
}
func TestPartialsController_Partial_NotFound(t *testing.T) {
req := httptest.NewRequest("GET", "/admin/partials/does-not-exist", nil)
req.SetPathValue("id", "does-not-exist")
_, c := Partials()
handler := c.Handle(req)
result := handler.(*PartialsController).Partial()
if result != nil {
t.Error("Partial() should return nil for nonexistent partial")
}
}
func TestPartialsController_Partials_List(t *testing.T) {
// Clear any existing partials and create fresh ones
existing, _ := models.Partials.All()
for _, p := range existing {
models.Partials.Delete(p)
}
partials := []*models.Partial{
{Name: "Alpha", HTML: "<div>A</div>"},
{Name: "Beta", HTML: "<div>B</div>"},
{Name: "Gamma", HTML: "<div>G</div>"},
}
for _, p := range partials {
models.Partials.Insert(p)
}
_, c := Partials()
req := httptest.NewRequest("GET", "/admin/partials", nil)
handler := c.Handle(req)
result := handler.(*PartialsController).Partials()
if len(result) != 3 {
t.Errorf("Partials() returned %d items, want 3", len(result))
}
// Verify ordering by Name (ORDER BY Name)
if len(result) >= 3 {
if result[0].Name != "Alpha" {
t.Errorf("Partials()[0].Name = %q, want %q", result[0].Name, "Alpha")
}
if result[1].Name != "Beta" {
t.Errorf("Partials()[1].Name = %q, want %q", result[1].Name, "Beta")
}
if result[2].Name != "Gamma" {
t.Errorf("Partials()[2].Name = %q, want %q", result[2].Name, "Gamma")
}
}
}
func TestPartialsController_Create_PublishedFlag(t *testing.T) {
_, c := Partials()
tests := []struct {
name string
publishedVal string
wantPublished bool
}{
{"published on", "on", true},
{"published true", "true", true},
{"published empty", "", false},
{"published false", "false", false},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
formData := url.Values{
"name": {tt.name},
"slug": {strings.ReplaceAll(tt.name, " ", "-") + "-pub-test"},
"description": {"Testing published flag"},
"html": {"<div>Test</div>"},
}
if tt.publishedVal != "" {
formData.Set("published", tt.publishedVal)
}
req := httptest.NewRequest("POST", "/admin/partials", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
authenticateRequest(req)
w := httptest.NewRecorder()
handler := c.Handle(req)
handler.(*PartialsController).Create(w, req)
if w.Code != http.StatusSeeOther {
t.Fatalf("Create() status = %d, want %d, body: %s", w.Code, http.StatusSeeOther, w.Body.String())
}
// Get the created partial by slug
slug := formData.Get("slug")
created, err := models.Partials.Get(slug)
if err != nil {
t.Fatalf("Failed to get created partial (index %d, slug %q): %v", i, slug, err)
}
if created.Published != tt.wantPublished {
t.Errorf("Published = %v, want %v", created.Published, tt.wantPublished)
}
})
}
}