Complete tasks 4.1-4.2: Page management service and HTTP endpoints

- Implemented PageService with full CRUD operations
- Added GetPages, CreatePage, UpdatePage, DeletePage, ReorderPages methods
- Cascade deletion of widgets when page is deleted
- Prevention of last page deletion
- Created page HTTP endpoints (GET, POST, PUT, DELETE, reorder)
- HTMX-friendly HTML fragment responses
- Comprehensive unit tests for service and handlers
- Updated dashboard to use PageService and create default pages
This commit is contained in:
2026-02-19 00:08:05 -05:00
parent 9f07b0c6f9
commit 299ac03939
16 changed files with 1572 additions and 31 deletions

View File

@@ -0,0 +1,251 @@
package handlers
import (
"context"
"html/template"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"custom-start-page/internal/middleware"
)
// createMockPageTemplate creates a simple mock template for testing
func createMockPageTemplate() *template.Template {
tmpl := template.New("base")
// Define page-tabs template
template.Must(tmpl.New("page-tabs.html").Parse(`{{define "page-tabs.html"}}{{range .Pages}}<button>{{.Name}}</button>{{end}}{{end}}`))
// Define widget-grid template
template.Must(tmpl.New("widget-grid.html").Parse(`{{define "widget-grid.html"}}<div>Widgets for page {{.PageID}}</div>{{end}}`))
// Define page-tab template
template.Must(tmpl.New("page-tab.html").Parse(`{{define "page-tab.html"}}<button>{{.Page.Name}}</button>{{end}}`))
return tmpl
}
// TestHandleGetPage tests retrieving a page's widget grid
func TestHandleGetPage(t *testing.T) {
// Setup
mockTemplate := createMockPageTemplate()
mockService := &mockPageService{}
handler := &PageHandler{
pageService: mockService,
templates: mockTemplate,
}
// Create request with user ID in context
req := httptest.NewRequest(http.MethodGet, "/pages/page-1", nil)
req.SetPathValue("id", "page-1")
ctx := context.WithValue(req.Context(), middleware.GetUserIDContextKey(), "test-user-123")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
// Execute
handler.HandleGetPage(w, req)
// Assert
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
body := w.Body.String()
if !strings.Contains(body, "page-1") {
t.Errorf("Expected response to contain page ID, got: %s", body)
}
}
// TestHandleCreatePage tests creating a new page
func TestHandleCreatePage(t *testing.T) {
// Setup
mockTemplate := createMockPageTemplate()
mockService := &mockPageService{}
handler := &PageHandler{
pageService: mockService,
templates: mockTemplate,
}
// Create form data
form := url.Values{}
form.Add("name", "New Page")
// Create request with user ID in context
req := httptest.NewRequest(http.MethodPost, "/pages", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
ctx := context.WithValue(req.Context(), middleware.GetUserIDContextKey(), "test-user-123")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
// Execute
handler.HandleCreatePage(w, req)
// Assert
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
// Verify page was created
if len(mockService.pages) != 1 {
t.Errorf("Expected 1 page to be created, got %d", len(mockService.pages))
}
if mockService.pages[0].Name != "New Page" {
t.Errorf("Expected page name 'New Page', got '%s'", mockService.pages[0].Name)
}
}
// TestHandleCreatePage_InvalidName tests creating a page with invalid name
func TestHandleCreatePage_InvalidName(t *testing.T) {
// Setup
mockTemplate := createMockPageTemplate()
mockService := &mockPageService{}
handler := &PageHandler{
pageService: mockService,
templates: mockTemplate,
}
// Test empty name
form := url.Values{}
form.Add("name", "")
req := httptest.NewRequest(http.MethodPost, "/pages", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
ctx := context.WithValue(req.Context(), middleware.GetUserIDContextKey(), "test-user-123")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
handler.HandleCreatePage(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected status 400 for empty name, got %d", w.Code)
}
// Test name too long
form = url.Values{}
form.Add("name", strings.Repeat("a", 51))
req = httptest.NewRequest(http.MethodPost, "/pages", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
ctx = context.WithValue(req.Context(), middleware.GetUserIDContextKey(), "test-user-123")
req = req.WithContext(ctx)
w = httptest.NewRecorder()
handler.HandleCreatePage(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected status 400 for name too long, got %d", w.Code)
}
}
// TestHandleUpdatePage tests updating a page
func TestHandleUpdatePage(t *testing.T) {
// Setup
mockTemplate := createMockPageTemplate()
mockService := &mockPageService{}
// Create initial page
_, _ = mockService.CreatePage(context.Background(), "test-user-123", "Old Name")
handler := &PageHandler{
pageService: mockService,
templates: mockTemplate,
}
// Create form data
form := url.Values{}
form.Add("name", "Updated Name")
// Create request with user ID in context
req := httptest.NewRequest(http.MethodPut, "/pages/new-page-id", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetPathValue("id", "new-page-id")
ctx := context.WithValue(req.Context(), middleware.GetUserIDContextKey(), "test-user-123")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
// Execute
handler.HandleUpdatePage(w, req)
// Assert
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
// Verify page was updated
if mockService.pages[0].Name != "Updated Name" {
t.Errorf("Expected page name 'Updated Name', got '%s'", mockService.pages[0].Name)
}
}
// TestHandleDeletePage tests deleting a page
func TestHandleDeletePage(t *testing.T) {
// Setup
mockTemplate := createMockPageTemplate()
mockService := &mockPageService{}
// Create two pages
_, _ = mockService.CreatePage(context.Background(), "test-user-123", "Page 1")
_, _ = mockService.CreatePage(context.Background(), "test-user-123", "Page 2")
handler := &PageHandler{
pageService: mockService,
templates: mockTemplate,
}
// Create request with user ID in context
req := httptest.NewRequest(http.MethodDelete, "/pages/page-1", nil)
req.SetPathValue("id", "page-1")
ctx := context.WithValue(req.Context(), middleware.GetUserIDContextKey(), "test-user-123")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
// Execute
handler.HandleDeletePage(w, req)
// Assert
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
}
// TestHandleReorderPages tests reordering pages
func TestHandleReorderPages(t *testing.T) {
// Setup
mockTemplate := createMockPageTemplate()
mockService := &mockPageService{}
// Create pages
_, _ = mockService.CreatePage(context.Background(), "test-user-123", "Page 1")
_, _ = mockService.CreatePage(context.Background(), "test-user-123", "Page 2")
handler := &PageHandler{
pageService: mockService,
templates: mockTemplate,
}
// Create form data with JSON array
form := url.Values{}
form.Add("order", `["page-2", "page-1"]`)
// Create request with user ID in context
req := httptest.NewRequest(http.MethodPost, "/pages/reorder", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
ctx := context.WithValue(req.Context(), middleware.GetUserIDContextKey(), "test-user-123")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
// Execute
handler.HandleReorderPages(w, req)
// Assert
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
}