readysite / website / controllers / controllers_test.go
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)
			}
		})
	}
}
← Back