Fix test setup and middleware chain - Handler tests now passing

- Fix TestRequireRoleMiddleware by chaining RequireAuth before RequireRole (role context requirement)
- Remove unused 'strings' import from testdb.go
- Create crussell_test database in Docker setup
- Tests now properly initialize authentication context for role-based tests

Result: handlers test suite passes (13/13 tests)
Remaining failures in admin/auth/bookings/portfolio/scheduling/services/user packages need further investigation (environment setup, database constraints, endpoint initialization)
This commit is contained in:
2026-02-21 23:50:17 +00:00
parent e858c782a4
commit 44cac94f64
20 changed files with 6725 additions and 7 deletions
File diff suppressed because it is too large Load Diff
+255
View File
@@ -0,0 +1,255 @@
//go:build test
// +build test
package admin
import (
"context"
"encoding/json"
"net/http"
"testing"
"crussell/db"
"crussell/handlers/services"
"crussell/mw"
)
func TestAdminServices_Create(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create admin user in DB first
_, err := db.DB.Exec(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Admin', 'User', 'admin@test.com', '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,
PatchTestDurationHours: 0,
MinimumAgeRequired: 16,
}
w := makeAdminRequest(handler, "POST", "/api/admin/services", createReq)
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")
}
}
func TestAdminServices_List(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Insert test services
_, err := db.DB.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES
('Manicure', 'Basic manicure', 25.00, 30, true, 0, 0),
('Pedicure', 'Basic pedicure', 30.00, 45, false, 0, 0),
('Gel Polish', 'Gel polish service', 40.00, 60, true, 48, 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)
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")
}
}
func TestAdminServices_Toggle(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create a service
var serviceID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 0, 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)
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 = db.DB.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)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 on second toggle, got %d", w.Code)
}
// Verify service is active again
err = db.DB.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")
}
}
func TestAdminServices_Delete(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create a service
var serviceID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 0, 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)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
// Verify service is deleted
var count int
err = db.DB.QueryRow(context.Background(), "SELECT COUNT(*) FROM services WHERE id = $1", serviceID).Scan(&count)
if err != nil {
t.Fatalf("failed to check service: %v", err)
}
if count != 0 {
t.Error("expected service to be deleted")
}
}
func TestAdminServices_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create regular user in DB
_, err := db.DB.Exec(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Regular', 'User', 'user@test.com', '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,
PatchTestDurationHours: 0,
MinimumAgeRequired: 16,
}
w := makeUserRequest(createHandler, "POST", "/api/admin/services", createReq)
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)
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 = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 0, 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)
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)
if w.Code != http.StatusForbidden {
t.Errorf("DELETE: expected status 403, got %d", w.Code)
}
}
func stringPtr(s string) *string {
return &s
}
+71
View File
@@ -0,0 +1,71 @@
//go:build test
// +build test
package admin
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"crussell/db"
"crussell/mw"
"crussell/testutils/jwt"
"crussell/testutils/testdb"
)
// setupTestDB replaces the global db.DB with a test pool and returns a cleanup function
func setupTestDB(t *testing.T) func() {
t.Helper()
pool := testdb.Pool(t)
testdb.Migrate(t, pool)
originalDB := db.DB
db.DB = pool
jwt.Init()
return func() {
db.DB = originalDB
pool.Close()
}
}
// makeAdminRequest creates a request with admin context
func makeAdminRequest(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder {
return makeRequestWithContext(handler, method, path, body, "admin-test-001", "admin")
}
// makeUserRequest creates a request with regular user context
func makeUserRequest(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder {
return makeRequestWithContext(handler, method, path, body, "user-test-001", "verified_email")
}
// makeRequestWithContext creates a request with specific user context
func makeRequestWithContext(handler http.Handler, method, path string, body interface{}, userID, role string) *httptest.ResponseRecorder {
var req *http.Request
if body != nil {
bodyBytes, _ := json.Marshal(body)
req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
} else {
req = httptest.NewRequest(method, path, nil)
}
// Set up context with user ID and role (simulating middleware)
ctx := context.WithValue(req.Context(), mw.UserIDKey, userID)
ctx = context.WithValue(ctx, mw.UserRoleKey, role)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
return w
}
func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
return json.Unmarshal(w.Body.Bytes(), dest)
}
+362
View File
@@ -0,0 +1,362 @@
//go:build test
// +build test
package admin
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"crussell/db"
"crussell/handlers/notifications"
"crussell/handlers/today"
"crussell/mw"
)
func TestAdminToday_CurrentNext(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', 'hash', 'verified_email', 'standard')
RETURNING id
`).Scan(&userID)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Create service
var serviceID string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active)
VALUES ('Manicure', 'Basic manicure', 25.00, 30, true)
RETURNING id
`).Scan(&serviceID)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
// Create booking for today (in_progress)
_, err = db.DB.Exec(context.Background(), `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, NOW(), 'in_progress', NOW())
`, userID)
if err != nil {
t.Fatalf("failed to create booking: %v", err)
}
// Get the booking ID
var bookingID string
err = db.DB.QueryRow(context.Background(), `
SELECT id FROM bookings WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1
`, userID).Scan(&bookingID)
if err != nil {
t.Fatalf("failed to get booking ID: %v", err)
}
// Add service to booking
_, err = db.DB.Exec(context.Background(), `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
if err != nil {
t.Fatalf("failed to add service to booking: %v", err)
}
handler := http.HandlerFunc(today.GetCurrentAndNextHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response today.CurrentNextResponse
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if response.Current == nil {
t.Errorf("expected current appointment, got nil")
}
if response.Current != nil && response.Current.ID != bookingID {
t.Errorf("expected booking ID %s, got %s", bookingID, response.Current.ID)
}
}
func TestAdminToday_Appointments(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', 'hash', 'verified_email', 'standard')
RETURNING id
`).Scan(&userID)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Create service
var serviceID string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active)
VALUES ('Manicure', 'Basic manicure', 25.00, 30, true)
RETURNING id
`).Scan(&serviceID)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
// Create booking for today
_, err = db.DB.Exec(context.Background(), `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, NOW(), 'confirmed', NOW())
`, userID)
if err != nil {
t.Fatalf("failed to create booking: %v", err)
}
// Get the booking ID
var bookingID string
err = db.DB.QueryRow(context.Background(), `
SELECT id FROM bookings WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1
`, userID).Scan(&bookingID)
if err != nil {
t.Fatalf("failed to get booking ID: %v", err)
}
// Add service to booking
_, err = db.DB.Exec(context.Background(), `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
if err != nil {
t.Fatalf("failed to add service to booking: %v", err)
}
handler := http.HandlerFunc(today.GetTodayAppointmentsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/appointments", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response today.TodayAppointmentsResponse
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if len(response.Appointments) != 1 {
t.Errorf("expected 1 appointment, got %d", len(response.Appointments))
}
if len(response.Appointments) > 0 && response.Appointments[0].ID != bookingID {
t.Errorf("expected booking ID %s, got %s", bookingID, response.Appointments[0].ID)
}
}
func TestAdminToday_PendingApprovals(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', 'hash', 'verified_email', 'standard')
RETURNING id
`).Scan(&userID)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Create service
var serviceID string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active)
VALUES ('Manicure', 'Basic manicure', 25.00, 30, true)
RETURNING id
`).Scan(&serviceID)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
// Create pending booking
_, err = db.DB.Exec(context.Background(), `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, NOW() + INTERVAL '1 day', 'pending', NOW())
`, userID)
if err != nil {
t.Fatalf("failed to create booking: %v", err)
}
// Get the booking ID
var bookingID string
err = db.DB.QueryRow(context.Background(), `
SELECT id FROM bookings WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1
`, userID).Scan(&bookingID)
if err != nil {
t.Fatalf("failed to get booking ID: %v", err)
}
// Add service to booking
_, err = db.DB.Exec(context.Background(), `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
if err != nil {
t.Fatalf("failed to add service to booking: %v", err)
}
handler := http.HandlerFunc(today.GetPendingApprovalsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/pending-approvals", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response today.PendingApprovalsResponse
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if len(response.Approvals) != 1 {
t.Errorf("expected 1 pending approval, got %d", len(response.Approvals))
}
if len(response.Approvals) > 0 && response.Approvals[0].ID != bookingID {
t.Errorf("expected booking ID %s, got %s", bookingID, response.Approvals[0].ID)
}
}
func TestAdminNotifications_List(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create a notification
_, err := db.DB.Exec(context.Background(), `
INSERT INTO admin_notifications (reason, booking_id, user_id, created_at)
VALUES ('pending_booking', 1, 1, NOW())
`)
if err != nil {
t.Fatalf("failed to create notification: %v", err)
}
handler := http.HandlerFunc(notifications.GetNotifications)
w := makeAdminRequest(handler, "GET", "/api/admin/notifications", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response notifications.AdminNotificationListResponse
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if response.Total != 1 {
t.Errorf("expected 1 notification, got %d", response.Total)
}
if len(response.Notifications) != 1 {
t.Errorf("expected 1 notification in list, got %d", len(response.Notifications))
}
if len(response.Notifications) > 0 && response.Notifications[0].Reason != "pending_booking" {
t.Errorf("expected reason 'pending_booking', got %s", response.Notifications[0].Reason)
}
}
func TestAdminNotifications_Acknowledge(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create a notification
var notificationID int
err := db.DB.QueryRow(context.Background(), `
INSERT INTO admin_notifications (reason, booking_id, user_id, created_at)
VALUES ('pending_booking', 1, 1, NOW())
RETURNING id
`).Scan(&notificationID)
if err != nil {
t.Fatalf("failed to create notification: %v", err)
}
// Create request to acknowledge
req := httptest.NewRequest("POST", "/api/admin/notifications/"+strconv.Itoa(notificationID)+"/acknowledge", nil)
ctx := req.Context()
ctx = context.WithValue(ctx, mw.UserIDKey, "admin-test-001")
ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
handler := http.HandlerFunc(notifications.AcknowledgeNotification)
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
// Verify it's acknowledged
var acknowledged bool
err = db.DB.QueryRow(context.Background(), `
SELECT acknowledged_at IS NOT NULL FROM admin_notifications WHERE id = $1
`, notificationID).Scan(&acknowledged)
if err != nil {
t.Fatalf("failed to check acknowledgment: %v", err)
}
if !acknowledged {
t.Errorf("expected notification to be acknowledged")
}
}
func TestAdminToday_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Test current-next endpoint
currentNextHandler := mw.RequireAdmin(http.HandlerFunc(today.GetCurrentAndNextHandler))
w := makeUserRequest(currentNextHandler, "GET", "/api/admin/today/current-next", nil)
if w.Code != http.StatusForbidden {
t.Errorf("CurrentNext: expected status 403, got %d", w.Code)
}
// Test appointments endpoint
appointmentsHandler := mw.RequireAdmin(http.HandlerFunc(today.GetTodayAppointmentsHandler))
w = makeUserRequest(appointmentsHandler, "GET", "/api/admin/today/appointments", nil)
if w.Code != http.StatusForbidden {
t.Errorf("Appointments: expected status 403, got %d", w.Code)
}
// Test pending-approvals endpoint
pendingApprovalsHandler := mw.RequireAdmin(http.HandlerFunc(today.GetPendingApprovalsHandler))
w = makeUserRequest(pendingApprovalsHandler, "GET", "/api/admin/today/pending-approvals", nil)
if w.Code != http.StatusForbidden {
t.Errorf("PendingApprovals: expected status 403, got %d", w.Code)
}
// Test notifications list endpoint
notificationsHandler := mw.RequireAdmin(http.HandlerFunc(notifications.GetNotifications))
w = makeUserRequest(notificationsHandler, "GET", "/api/admin/notifications", nil)
if w.Code != http.StatusForbidden {
t.Errorf("Notifications List: expected status 403, got %d", w.Code)
}
// Test notifications acknowledge endpoint
ackHandler := mw.RequireAdmin(http.HandlerFunc(notifications.AcknowledgeNotification))
w = makeUserRequest(ackHandler, "POST", "/api/admin/notifications/1/acknowledge", nil)
if w.Code != http.StatusForbidden {
t.Errorf("Notifications Acknowledge: expected status 403, got %d", w.Code)
}
}
+350
View File
@@ -0,0 +1,350 @@
//go:build test
// +build test
package admin
import (
"context"
"encoding/json"
"net/http"
"testing"
"crussell/db"
"crussell/handlers/user"
"crussell/mw"
)
func TestAdminUsers_List(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create test users
_, err := db.DB.Exec(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES
('Alice', 'Smith', 'alice@test.com', 'hash1', 'admin', 'standard'),
('Bob', 'Jones', 'bob@test.com', 'hash2', 'verified_email', 'standard'),
('Charlie', 'Brown', 'charlie@test.com', 'hash3', 'verified_email', 'vip')
`)
if err != nil {
t.Fatalf("failed to create users: %v", err)
}
handler := http.HandlerFunc(user.ListAdminUsersHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response user.UserListResponse
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if response.Total != 3 {
t.Errorf("expected 3 users, got %d", response.Total)
}
if len(response.Users) != 3 {
t.Errorf("expected 3 users in list, got %d", len(response.Users))
}
}
func TestAdminUsers_Get(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', 'hash', 'verified_email', 'vip')
RETURNING id
`).Scan(&userID)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
handler := http.HandlerFunc(user.GetAdminUserHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID, nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response user.AdminUserDetail
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if response.ID != userID {
t.Errorf("expected user ID %s, got %s", userID, response.ID)
}
if response.AccountType != "vip" {
t.Errorf("expected account type 'vip', got %s", response.AccountType)
}
}
func TestAdminUsers_Get_NotFound(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(user.GetAdminUserHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users/nonexistent-id", nil)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d", w.Code)
}
}
func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', 'hash', 'verified_email', 'standard')
RETURNING id
`).Scan(&userID)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Create services - some with patch test, some without
_, err = db.DB.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES
('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0, 0),
('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 48, 16),
('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 48, 16),
('Inactive Service', 'Inactive', 30.00, 30, false, 48, 16)
`)
if err != nil {
t.Fatalf("failed to create services: %v", err)
}
handler := http.HandlerFunc(user.GetEligiblePatchTestServicesHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID+"/patch-tests/eligible", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response []user.ServiceForPatchTest
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
// Should return 2 services (the two with patch test duration > 0 that are active)
if len(response) != 2 {
t.Errorf("expected 2 eligible services, got %d. body: %s", len(response), w.Body.String())
}
}
func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', 'hash', 'verified_email', 'standard')
RETURNING id
`).Scan(&userID)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Create services with patch test
var serviceID1, serviceID2 string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 48, 16)
RETURNING id
`).Scan(&serviceID1)
if err != nil {
t.Fatalf("failed to create service 1: %v", err)
}
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 48, 16)
RETURNING id
`).Scan(&serviceID2)
if err != nil {
t.Fatalf("failed to create service 2: %v", err)
}
// Add one patch test for the user
_, err = db.DB.Exec(context.Background(), `
INSERT INTO user_service_patch_tests (user_id, service_id, last_time)
VALUES ($1, $2, NOW())
`, userID, serviceID1)
if err != nil {
t.Fatalf("failed to add patch test: %v", err)
}
handler := http.HandlerFunc(user.GetEligiblePatchTestServicesHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID+"/patch-tests/eligible", nil)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var response []user.ServiceForPatchTest
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
// Should return only 1 service (the one not already added)
if len(response) != 1 {
t.Errorf("expected 1 eligible service, got %d. body: %s", len(response), w.Body.String())
}
if len(response) > 0 && response[0].ID != serviceID2 {
t.Errorf("expected service %s, got %s", serviceID2, response[0].ID)
}
}
func TestAdminUsers_AddPatchTest(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', 'hash', 'verified_email', 'standard')
RETURNING id
`).Scan(&userID)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Create service with patch test
var serviceID string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 48, 16)
RETURNING id
`).Scan(&serviceID)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
handler := http.HandlerFunc(user.AddPatchTestHandler)
reqBody := user.AddPatchTestRequest{ServiceID: serviceID}
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
// Verify patch test was added
var count int
err = db.DB.QueryRow(context.Background(), `
SELECT COUNT(*) FROM user_service_patch_tests WHERE user_id = $1 AND service_id = $2
`, userID, serviceID).Scan(&count)
if err != nil {
t.Fatalf("failed to check patch test: %v", err)
}
if count != 1 {
t.Errorf("expected 1 patch test record, got %d", count)
}
}
func TestAdminUsers_AddPatchTest_InvalidService(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', 'hash', 'verified_email', 'standard')
RETURNING id
`).Scan(&userID)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Create service without patch test requirement
var serviceID string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
VALUES ('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0, 0)
RETURNING id
`).Scan(&serviceID)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
handler := http.HandlerFunc(user.AddPatchTestHandler)
reqBody := user.AddPatchTestRequest{ServiceID: serviceID}
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestAdminUsers_NonAdmin(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Create regular user in DB
_, err := db.DB.Exec(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Regular', 'User', 'user@test.com', 'hash', 'verified_email', 'standard')
`)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
// Create test user for GET
var targetUserID string
err = db.DB.QueryRow(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
VALUES ('Target', 'User', 'target@test.com', 'hash', 'verified_email', 'standard')
RETURNING id
`).Scan(&targetUserID)
if err != nil {
t.Fatalf("failed to create target user: %v", err)
}
// Test LIST - should get 403 when using middleware
listHandler := mw.RequireAdmin(http.HandlerFunc(user.ListAdminUsersHandler))
w := makeUserRequest(listHandler, "GET", "/api/admin/users", nil)
if w.Code != http.StatusForbidden {
t.Errorf("LIST: expected status 403, got %d", w.Code)
}
// Test GET - should get 403 when using middleware
getHandler := mw.RequireAdmin(http.HandlerFunc(user.GetAdminUserHandler))
w = makeUserRequest(getHandler, "GET", "/api/admin/users/"+targetUserID, nil)
if w.Code != http.StatusForbidden {
t.Errorf("GET: expected status 403, got %d", w.Code)
}
// Test eligible patch tests - should get 403 when using middleware
eligibleHandler := mw.RequireAdmin(http.HandlerFunc(user.GetEligiblePatchTestServicesHandler))
w = makeUserRequest(eligibleHandler, "GET", "/api/admin/users/"+targetUserID+"/patch-tests/eligible", nil)
if w.Code != http.StatusForbidden {
t.Errorf("ELIGIBLE: expected status 403, got %d", w.Code)
}
// Test add patch test - should get 403 when using middleware
addHandler := mw.RequireAdmin(http.HandlerFunc(user.AddPatchTestHandler))
w = makeUserRequest(addHandler, "POST", "/api/admin/users/"+targetUserID+"/patch-tests", map[string]string{"service_id": "some-service-id"})
if w.Code != http.StatusForbidden {
t.Errorf("ADD: expected status 403, got %d", w.Code)
}
}