Files
Crussell/backend/handlers/admin/services_test.go
T
popertotsandSisyphus 220a0ef6e8 refactor(backend): update test files for PoolProxy and per-test transactions
Migrate all test files from SetupTestDB/db.DB pattern to per-test transactions:

- Replace SetupTestDB(t) with SetupTestTx(t) for context + transaction
- Replace db.DB.Query/QueryRow/Exec with tx.Query/QueryRow/Exec
- Replace context.Background() with context from SetupTestTx
- Replace defer rows.Close() pattern with explicit rows.Close()
- Add testdb.SeedBaseline(pool) to all TestMain functions
- Wire db.Conn = db.NewPoolProxy(pool) in all TestMain functions

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-21 19:29:24 +01:00

273 lines
9.1 KiB
Go

//go:build test
// +build test
package admin
// Package admin contains tests for admin service management endpoints.
//
// Test Coverage:
// - CreateServiceHandler: POST /api/admin/services - Create new service
// - AllServicesHandler: GET /api/admin/services - List all services (incl. inactive)
// - ToggleService: PUT /api/admin/services/{id}/toggle - Toggle service active status
// - DeleteServiceHandler: DELETE /api/admin/services/{id} - Soft delete service
//
// Authentication: All endpoints require admin role (403 for non-admins).
import (
"context"
"encoding/json"
"net/http"
"testing"
"crussell/testutils"
"crussell/handlers/services"
"crussell/mw"
)
// TestAdminServices_Create verifies that an admin can create a new service
// with name, description, price, duration, and minimum age requirements. The new
// service is active by default and stored in the database.
func TestAdminServices_Create(t *testing.T) {
ctx, tx := testutils.SetupTestTx(t)
// Create admin user in DB first
_, err := tx.Exec(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Admin', 'User', 'admin@test.com', '+447123456789', '1990-01-01', 'hash', 'admin', 'email')
`)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
handler := http.HandlerFunc(services.CreateServiceHandler)
createReq := services.CreateServiceRequest{
Name: "Test Manicure",
Description: stringPtr("A test manicure service"),
Price: 35.00,
DurationMinutes: 45,
MinimumAgeRequired: 16,
}
w := makeAdminRequest(handler, "POST", "/api/admin/services", createReq, ctx)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
var response services.Service
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if response.Name != "Test Manicure" {
t.Errorf("expected name 'Test Manicure', got %s", response.Name)
}
if response.Price != 35.00 {
t.Errorf("expected price 35.00, got %f", response.Price)
}
if !response.IsActive {
t.Error("expected new service to be active by default")
}
}
// TestAdminServices_List tests that an admin can retrieve all services,
// including inactive ones. This is useful for managing the full service catalog.
func TestAdminServices_List(t *testing.T) {
ctx, tx := testutils.SetupTestTx(t)
// Insert test services
_, err := tx.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES
('Manicure', 'Basic manicure', 25.00, 30, true, 0),
('Pedicure', 'Basic pedicure', 30.00, 45, false, 0),
('Gel Polish', 'Gel polish service', 40.00, 60, true, 16)
`)
if err != nil {
t.Fatalf("failed to create services: %v", err)
}
handler := http.HandlerFunc(services.AllServicesHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/services", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response []services.Service
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if len(response) != 3 {
t.Errorf("expected 3 services, got %d", len(response))
}
// Verify all services including inactive are returned
found := map[string]bool{}
for _, s := range response {
found[s.Name] = true
}
if !found["Manicure"] {
t.Error("expected Manicure in response")
}
if !found["Pedicure"] {
t.Error("expected Pedicure in response (including inactive)")
}
if !found["Gel Polish"] {
t.Error("expected Gel Polish in response")
}
}
// TestAdminServices_Toggle verifies that an admin can toggle a service's
// active status on/off. This is used to temporarily disable a service without
// deleting it from the system.
func TestAdminServices_Toggle(t *testing.T) {
ctx, tx := testutils.SetupTestTx(t)
// Create a service
var serviceID string
err := tx.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 16)
RETURNING id
`).Scan(&serviceID)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
handler := http.HandlerFunc(services.ToggleService)
w := makeAdminRequest(handler, "PUT", "/api/admin/services/"+serviceID+"/toggle", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
// Verify service is now inactive
var isActive bool
err = tx.QueryRow(context.Background(), "SELECT is_active FROM services WHERE id = $1", serviceID).Scan(&isActive)
if err != nil {
t.Fatalf("failed to check service: %v", err)
}
if isActive {
t.Error("expected service to be inactive after toggle")
}
// Toggle again
w = makeAdminRequest(handler, "PUT", "/api/admin/services/"+serviceID+"/toggle", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 on second toggle, got %d", w.Code)
}
// Verify service is active again
err = tx.QueryRow(context.Background(), "SELECT is_active FROM services WHERE id = $1", serviceID).Scan(&isActive)
if err != nil {
t.Fatalf("failed to check service: %v", err)
}
if !isActive {
t.Error("expected service to be active after second toggle")
}
}
// TestAdminServices_Delete tests that an admin can soft-delete a service
// by setting is_active to false. The service record remains but is hidden from
// customers.
func TestAdminServices_Delete(t *testing.T) {
ctx, tx := testutils.SetupTestTx(t)
// Create a service
var serviceID string
err := tx.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 16)
RETURNING id
`).Scan(&serviceID)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
handler := http.HandlerFunc(services.DeleteServiceHandler)
w := makeAdminRequest(handler, "DELETE", "/api/admin/services/"+serviceID, nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
// Verify service is soft deleted (is_active = false)
var isActive bool
err = tx.QueryRow(context.Background(), "SELECT is_active FROM services WHERE id = $1", serviceID).Scan(&isActive)
if err != nil {
t.Fatalf("failed to check service: %v", err)
}
if isActive {
t.Error("expected service to be soft deleted (is_active = false)")
}
}
// TestAdminServices_NonAdmin verifies that non-admin users receive HTTP 403
// Forbidden when attempting to create, list, toggle, or delete services. This
// ensures proper role-based access control.
func TestAdminServices_NonAdmin(t *testing.T) {
ctx, tx := testutils.SetupTestTx(t)
// Create regular user in DB
_, err := tx.Exec(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Regular', 'User', 'user@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
`)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Test CREATE - should get 403 when using middleware
createHandler := mw.RequireAdmin(http.HandlerFunc(services.CreateServiceHandler))
createReq := services.CreateServiceRequest{
Name: "Test Service",
Description: stringPtr("Test"),
Price: 50.00,
DurationMinutes: 60,
MinimumAgeRequired: 16,
}
w := makeUserRequest(createHandler, "POST", "/api/admin/services", createReq, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("CREATE: expected status 403, got %d", w.Code)
}
// Test LIST - should get 403 when using middleware
listHandler := mw.RequireAdmin(http.HandlerFunc(services.AllServicesHandler))
w = makeUserRequest(listHandler, "GET", "/api/admin/services", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("LIST: expected status 403, got %d", w.Code)
}
// Test TOGGLE - should get 403 when using middleware
var serviceID string
err = tx.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 16)
RETURNING id
`).Scan(&serviceID)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
toggleHandler := mw.RequireAdmin(http.HandlerFunc(services.ToggleService))
w = makeUserRequest(toggleHandler, "PUT", "/api/admin/services/"+serviceID+"/toggle", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("TOGGLE: expected status 403, got %d", w.Code)
}
// Test DELETE - should get 403 when using middleware
deleteHandler := mw.RequireAdmin(http.HandlerFunc(services.DeleteServiceHandler))
w = makeUserRequest(deleteHandler, "DELETE", "/api/admin/services/"+serviceID, nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("DELETE: expected status 403, got %d", w.Code)
}
}
func stringPtr(s string) *string {
return &s
}