Large manual tests corruption fix
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
// +build test
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
// Package admin contains tests for admin booking management endpoints.
|
// Package admin contains tests for admin booking management endpoints.
|
||||||
//
|
//
|
||||||
// Test Coverage:
|
// Test Coverage:
|
||||||
@@ -16,11 +18,6 @@
|
|||||||
// - AdminRejectEditRequestHandler: POST /api/admin/bookings/{id}/reject-edit - Reject edit
|
// - AdminRejectEditRequestHandler: POST /api/admin/bookings/{id}/reject-edit - Reject edit
|
||||||
//
|
//
|
||||||
// Authentication: All endpoints require admin role (403 for non-admins).
|
// Authentication: All endpoints require admin role (403 for non-admins).
|
||||||
package admin
|
|
||||||
//go:build test
|
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -35,14 +32,15 @@ import (
|
|||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
|
|
||||||
"github.com/lib/pq"
|
|
||||||
)
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/handlers/bookings"
|
"crussell/handlers/bookings"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
@@ -57,6 +55,7 @@ import (
|
|||||||
|
|
||||||
// TestAdminBookings_List verifies that an admin can list all bookings in the
|
// TestAdminBookings_List verifies that an admin can list all bookings in the
|
||||||
// system with pagination support.
|
// system with pagination support.
|
||||||
|
func TestAdminBookings_List(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -113,6 +112,7 @@ import (
|
|||||||
|
|
||||||
// TestAdminBookings_List_FilterByStatus tests that an admin can filter
|
// TestAdminBookings_List_FilterByStatus tests that an admin can filter
|
||||||
// bookings by status (e.g., pending, confirmed, completed).
|
// bookings by status (e.g., pending, confirmed, completed).
|
||||||
|
func TestAdminBookings_List_FilterByStatus(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -177,6 +177,7 @@ import (
|
|||||||
|
|
||||||
// TestAdminBookings_Create verifies that an admin can create a booking
|
// TestAdminBookings_Create verifies that an admin can create a booking
|
||||||
// on behalf of a user. The booking is created with 'confirmed' status.
|
// on behalf of a user. The booking is created with 'confirmed' status.
|
||||||
|
func TestAdminBookings_Create(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -235,6 +236,7 @@ import (
|
|||||||
// TestAdminBookings_Create_InvalidInput verifies that admin booking
|
// TestAdminBookings_Create_InvalidInput verifies that admin booking
|
||||||
// creation fails with HTTP 400 when required fields (userID, startTime, serviceIDs)
|
// creation fails with HTTP 400 when required fields (userID, startTime, serviceIDs)
|
||||||
// are missing or invalid.
|
// are missing or invalid.
|
||||||
|
func TestAdminBookings_Create_InvalidInput(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -303,6 +305,7 @@ import (
|
|||||||
|
|
||||||
// TestAdminBookings_Search tests that an admin can search bookings by
|
// TestAdminBookings_Search tests that an admin can search bookings by
|
||||||
// notes, customer name, or other text fields.
|
// notes, customer name, or other text fields.
|
||||||
|
func TestAdminBookings_Search(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -355,6 +358,7 @@ import (
|
|||||||
|
|
||||||
// TestAdminBookings_Search_MissingQuery verifies that searching without
|
// TestAdminBookings_Search_MissingQuery verifies that searching without
|
||||||
// a query parameter returns HTTP 400 Bad Request.
|
// a query parameter returns HTTP 400 Bad Request.
|
||||||
|
func TestAdminBookings_Search_MissingQuery(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1167,6 +1171,7 @@ func TestAdminBookings_Search_MultipleResults(t *testing.T) {
|
|||||||
t.Errorf("expected total 2, got %d", resp.Total)
|
t.Errorf("expected total 2, got %d", resp.Total)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Admin List Edit Requests Tests
|
// Admin List Edit Requests Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -1208,7 +1213,7 @@ func TestAdminBookings_ListEditRequests(t *testing.T) {
|
|||||||
t.Fatalf("failed to update booking status: %v", err)
|
t.Fatalf("failed to update booking status: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up ALL existing edit requests in DB to ensure clean state (handler doesn't filter by booking_id)
|
// Clean up ALL existing edit requests in DB to ensure clean state
|
||||||
_, err = db.DB.Exec(context.Background(), "DELETE FROM booking_edit_requests")
|
_, err = db.DB.Exec(context.Background(), "DELETE FROM booking_edit_requests")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to clean up edit requests: %v", err)
|
t.Fatalf("failed to clean up edit requests: %v", err)
|
||||||
@@ -1258,7 +1263,6 @@ func TestAdminBookings_ListEditRequests(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Admin Deny Edit Request Tests
|
// Admin Deny Edit Request Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -1320,14 +1324,12 @@ func TestAdminBookings_DenyEditRequest(t *testing.T) {
|
|||||||
t.Fatalf("failed to create edit request: %v", err)
|
t.Fatalf("failed to create edit request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call admin deny endpoint - need to manually set chi context with both bookingID and request_id
|
// Call admin deny endpoint
|
||||||
handler := http.HandlerFunc(bookings.AdminRejectEditRequestHandler)
|
handler := http.HandlerFunc(bookings.AdminRejectEditRequestHandler)
|
||||||
|
|
||||||
// Build request manually to include both bookingID and request_id in chi context
|
|
||||||
path := fmt.Sprintf("/api/admin/bookings/%s/edit-requests/%s/deny", bookingID, editRequestID)
|
path := fmt.Sprintf("/api/admin/bookings/%s/edit-requests/%s/deny", bookingID, editRequestID)
|
||||||
req := httptest.NewRequest("POST", path, nil)
|
req := httptest.NewRequest("POST", path, nil)
|
||||||
|
|
||||||
// Set up chi routing context with both params
|
|
||||||
rctx := chi.NewRouteContext()
|
rctx := chi.NewRouteContext()
|
||||||
rctx.URLParams.Add("id", bookingID)
|
rctx.URLParams.Add("id", bookingID)
|
||||||
rctx.URLParams.Add("request_id", editRequestID)
|
rctx.URLParams.Add("request_id", editRequestID)
|
||||||
@@ -1339,19 +1341,17 @@ func TestAdminBookings_DenyEditRequest(t *testing.T) {
|
|||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(w, req)
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
// Expect HTTP 200 OK
|
|
||||||
if w.Code != http.StatusOK && w.Code != http.StatusNoContent {
|
if w.Code != http.StatusOK && w.Code != http.StatusNoContent {
|
||||||
t.Errorf("expected status 200/204, got %d. body: %s", w.Code, w.Body.String())
|
t.Errorf("expected status 200/204, got %d. body: %s", w.Code, w.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify edit request is handled (deleted in current implementation)
|
// Verify edit request is deleted
|
||||||
var erCount int
|
var erCount int
|
||||||
err = db.DB.QueryRow(context.Background(),
|
err = db.DB.QueryRow(context.Background(),
|
||||||
"SELECT COUNT(*) FROM booking_edit_requests WHERE id = $1", editRequestID).Scan(&erCount)
|
"SELECT COUNT(*) FROM booking_edit_requests WHERE id = $1", editRequestID).Scan(&erCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to query edit requests: %v", err)
|
t.Fatalf("failed to query edit requests: %v", err)
|
||||||
}
|
}
|
||||||
// Current implementation deletes the edit request
|
|
||||||
if erCount != 0 {
|
if erCount != 0 {
|
||||||
t.Errorf("expected edit request to be deleted after deny, got %d", erCount)
|
t.Errorf("expected edit request to be deleted after deny, got %d", erCount)
|
||||||
}
|
}
|
||||||
@@ -1445,14 +1445,12 @@ func TestAdminBookings_ApproveEditRequest(t *testing.T) {
|
|||||||
t.Fatalf("failed to create admin notification: %v", err)
|
t.Fatalf("failed to create admin notification: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call admin approve endpoint using makeAdminRequest with manual chi context for two params
|
// Call admin approve endpoint
|
||||||
handler := http.HandlerFunc(bookings.AdminApproveEditRequestHandler)
|
handler := http.HandlerFunc(bookings.AdminApproveEditRequestHandler)
|
||||||
|
|
||||||
// Build request manually to include both bookingID and request_id in chi context
|
|
||||||
path := fmt.Sprintf("/api/admin/bookings/%s/edit-requests/%s/approve", bookingID, editRequestID)
|
path := fmt.Sprintf("/api/admin/bookings/%s/edit-requests/%s/approve", bookingID, editRequestID)
|
||||||
req := httptest.NewRequest("POST", path, nil)
|
req := httptest.NewRequest("POST", path, nil)
|
||||||
|
|
||||||
// Set up chi routing context with both params
|
|
||||||
rctx := chi.NewRouteContext()
|
rctx := chi.NewRouteContext()
|
||||||
rctx.URLParams.Add("id", bookingID)
|
rctx.URLParams.Add("id", bookingID)
|
||||||
rctx.URLParams.Add("request_id", editRequestID)
|
rctx.URLParams.Add("request_id", editRequestID)
|
||||||
@@ -1464,7 +1462,6 @@ func TestAdminBookings_ApproveEditRequest(t *testing.T) {
|
|||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(w, req)
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
// Expect 204 NoContent
|
|
||||||
if w.Code != http.StatusNoContent {
|
if w.Code != http.StatusNoContent {
|
||||||
t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String())
|
t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
// +build test
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
// Package admin contains tests for admin service management endpoints.
|
// Package admin contains tests for admin service management endpoints.
|
||||||
//
|
//
|
||||||
// Test Coverage:
|
// Test Coverage:
|
||||||
@@ -10,11 +12,6 @@
|
|||||||
// - DeleteServiceHandler: DELETE /api/admin/services/{id} - Soft delete service
|
// - DeleteServiceHandler: DELETE /api/admin/services/{id} - Soft delete service
|
||||||
//
|
//
|
||||||
// Authentication: All endpoints require admin role (403 for non-admins).
|
// Authentication: All endpoints require admin role (403 for non-admins).
|
||||||
package admin
|
|
||||||
//go:build test
|
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -30,6 +27,7 @@ import (
|
|||||||
// TestAdminServices_Create verifies that an admin can create a new service
|
// TestAdminServices_Create verifies that an admin can create a new service
|
||||||
// with name, description, price, duration, and minimum age requirements. The new
|
// with name, description, price, duration, and minimum age requirements. The new
|
||||||
// service is active by default and stored in the database.
|
// service is active by default and stored in the database.
|
||||||
|
func TestAdminServices_Create(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -76,13 +74,14 @@ import (
|
|||||||
|
|
||||||
// TestAdminServices_List tests that an admin can retrieve all services,
|
// TestAdminServices_List tests that an admin can retrieve all services,
|
||||||
// including inactive ones. This is useful for managing the full service catalog.
|
// including inactive ones. This is useful for managing the full service catalog.
|
||||||
|
func TestAdminServices_List(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// Insert test services
|
// Insert test services
|
||||||
_, err := db.DB.Exec(context.Background(), `
|
_, err := db.DB.Exec(context.Background(), `
|
||||||
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
|
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
|
||||||
VALUES
|
VALUES
|
||||||
('Manicure', 'Basic manicure', 25.00, 30, true, 0),
|
('Manicure', 'Basic manicure', 25.00, 30, true, 0),
|
||||||
('Pedicure', 'Basic pedicure', 30.00, 45, false, 0),
|
('Pedicure', 'Basic pedicure', 30.00, 45, false, 0),
|
||||||
('Gel Polish', 'Gel polish service', 40.00, 60, true, 16)
|
('Gel Polish', 'Gel polish service', 40.00, 60, true, 16)
|
||||||
@@ -127,6 +126,7 @@ import (
|
|||||||
// TestAdminServices_Toggle verifies that an admin can toggle a service's
|
// TestAdminServices_Toggle verifies that an admin can toggle a service's
|
||||||
// active status on/off. This is used to temporarily disable a service without
|
// active status on/off. This is used to temporarily disable a service without
|
||||||
// deleting it from the system.
|
// deleting it from the system.
|
||||||
|
func TestAdminServices_Toggle(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -177,6 +177,7 @@ import (
|
|||||||
// TestAdminServices_Delete tests that an admin can soft-delete a service
|
// 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
|
// by setting is_active to false. The service record remains but is hidden from
|
||||||
// customers.
|
// customers.
|
||||||
|
func TestAdminServices_Delete(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -212,6 +213,7 @@ import (
|
|||||||
// TestAdminServices_NonAdmin verifies that non-admin users receive HTTP 403
|
// TestAdminServices_NonAdmin verifies that non-admin users receive HTTP 403
|
||||||
// Forbidden when attempting to create, list, toggle, or delete services. This
|
// Forbidden when attempting to create, list, toggle, or delete services. This
|
||||||
// ensures proper role-based access control.
|
// ensures proper role-based access control.
|
||||||
|
func TestAdminServices_NonAdmin(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -227,11 +229,11 @@ import (
|
|||||||
// Test CREATE - should get 403 when using middleware
|
// Test CREATE - should get 403 when using middleware
|
||||||
createHandler := mw.RequireAdmin(http.HandlerFunc(services.CreateServiceHandler))
|
createHandler := mw.RequireAdmin(http.HandlerFunc(services.CreateServiceHandler))
|
||||||
createReq := services.CreateServiceRequest{
|
createReq := services.CreateServiceRequest{
|
||||||
Name: "Test Service",
|
Name: "Test Service",
|
||||||
Description: stringPtr("Test"),
|
Description: stringPtr("Test"),
|
||||||
Price: 50.00,
|
Price: 50.00,
|
||||||
DurationMinutes: 60,
|
DurationMinutes: 60,
|
||||||
MinimumAgeRequired: 16,
|
MinimumAgeRequired: 16,
|
||||||
}
|
}
|
||||||
w := makeUserRequest(createHandler, "POST", "/api/admin/services", createReq)
|
w := makeUserRequest(createHandler, "POST", "/api/admin/services", createReq)
|
||||||
if w.Code != http.StatusForbidden {
|
if w.Code != http.StatusForbidden {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
// +build test
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
// Package admin contains tests for admin dashboard "today" endpoints.
|
// Package admin contains tests for admin dashboard "today" endpoints.
|
||||||
//
|
//
|
||||||
// Test Coverage:
|
// Test Coverage:
|
||||||
@@ -11,12 +13,7 @@
|
|||||||
// - AcknowledgeNotification: POST /api/admin/notifications/{id}/ack - Acknowledge (WIP - skipped)
|
// - AcknowledgeNotification: POST /api/admin/notifications/{id}/ack - Acknowledge (WIP - skipped)
|
||||||
//
|
//
|
||||||
// Authentication: All endpoints require admin role (403 for non-admins).
|
// Authentication: All endpoints require admin role (403 for non-admins).
|
||||||
// WIP: Notification tests are skipped pending handler implementation.
|
// WIP: Notification tests are skipped pending handler implementation.package admin
|
||||||
package admin
|
|
||||||
//go:build test
|
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -32,6 +29,7 @@ import (
|
|||||||
|
|
||||||
// TestAdminToday_CurrentNext verifies that an admin can retrieve the currently
|
// TestAdminToday_CurrentNext verifies that an admin can retrieve the currently
|
||||||
// in-progress booking and the next upcoming booking for the dashboard.
|
// in-progress booking and the next upcoming booking for the dashboard.
|
||||||
|
func TestAdminToday_CurrentNext(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -107,6 +105,7 @@ import (
|
|||||||
|
|
||||||
// TestAdminToday_Appointments tests that an admin can get a list of all
|
// TestAdminToday_Appointments tests that an admin can get a list of all
|
||||||
// bookings scheduled for today with their details.
|
// bookings scheduled for today with their details.
|
||||||
|
func TestAdminToday_Appointments(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -182,6 +181,7 @@ import (
|
|||||||
|
|
||||||
// TestAdminToday_PendingApprovals verifies that an admin can see all pending
|
// TestAdminToday_PendingApprovals verifies that an admin can see all pending
|
||||||
// bookings that require approval/confirmation.
|
// bookings that require approval/confirmation.
|
||||||
|
func TestAdminToday_PendingApprovals(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -257,16 +257,19 @@ import (
|
|||||||
|
|
||||||
// TestAdminNotifications_List is skipped (WIP) - tests that an admin
|
// TestAdminNotifications_List is skipped (WIP) - tests that an admin
|
||||||
// can list all their notifications.
|
// can list all their notifications.
|
||||||
|
func TestAdminNotifications_List(t *testing.T) {
|
||||||
t.Skip("Skipping - WIP handler")
|
t.Skip("Skipping - WIP handler")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAdminNotifications_Acknowledge is skipped (WIP) - tests that an
|
// TestAdminNotifications_Acknowledge is skipped (WIP) - tests that an
|
||||||
// admin can acknowledge a notification.
|
// admin can acknowledge a notification.
|
||||||
|
func TestAdminNotifications_Acknowledge(t *testing.T) {
|
||||||
t.Skip("Skipping - WIP handler")
|
t.Skip("Skipping - WIP handler")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAdminToday_NonAdmin verifies that non-admin users receive HTTP 403
|
// TestAdminToday_NonAdmin verifies that non-admin users receive HTTP 403
|
||||||
// when accessing today's dashboard endpoints.
|
// when accessing today's dashboard endpoints.
|
||||||
|
func TestAdminToday_NonAdmin(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
// +build test
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
// Package admin contains tests for admin user management endpoints.
|
// Package admin contains tests for admin user management endpoints.
|
||||||
//
|
//
|
||||||
// Test Coverage:
|
// Test Coverage:
|
||||||
@@ -11,11 +13,6 @@
|
|||||||
// - RequireAdmin middleware: All endpoints require admin role (403 for non-admins)
|
// - RequireAdmin middleware: All endpoints require admin role (403 for non-admins)
|
||||||
//
|
//
|
||||||
// Database State: Tests create and clean up users in the users table.
|
// Database State: Tests create and clean up users in the users table.
|
||||||
package admin
|
|
||||||
//go:build test
|
|
||||||
// +build test
|
|
||||||
|
|
||||||
package admin
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -30,13 +27,14 @@ import (
|
|||||||
|
|
||||||
// TestAdminUsers_List verifies that an admin can list all users in the
|
// TestAdminUsers_List verifies that an admin can list all users in the
|
||||||
// system with their details including account role and type.
|
// system with their details including account role and type.
|
||||||
|
func TestAdminUsers_List(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// Create test users
|
// Create test users
|
||||||
_, err := db.DB.Exec(context.Background(), `
|
_, err := db.DB.Exec(context.Background(), `
|
||||||
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
||||||
VALUES
|
VALUES
|
||||||
('Alice', 'Smith', 'alice@test.com', '+447123456789', '1990-01-01', 'hash1', 'admin', 'email'),
|
('Alice', 'Smith', 'alice@test.com', '+447123456789', '1990-01-01', 'hash1', 'admin', 'email'),
|
||||||
('Bob', 'Jones', 'bob@test.com', '+447123456789', '1990-01-01', 'hash2', 'verified_email', 'email'),
|
('Bob', 'Jones', 'bob@test.com', '+447123456789', '1990-01-01', 'hash2', 'verified_email', 'email'),
|
||||||
('Charlie', 'Brown', 'charlie@test.com', '+447123456789', '1990-01-01', 'hash3', 'verified_email', 'email')
|
('Charlie', 'Brown', 'charlie@test.com', '+447123456789', '1990-01-01', 'hash3', 'verified_email', 'email')
|
||||||
@@ -68,6 +66,7 @@ import (
|
|||||||
|
|
||||||
// TestAdminUsers_Get tests that an admin can retrieve detailed information
|
// TestAdminUsers_Get tests that an admin can retrieve detailed information
|
||||||
// about a specific user including their profile and account settings.
|
// about a specific user including their profile and account settings.
|
||||||
|
func TestAdminUsers_Get(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -105,6 +104,7 @@ import (
|
|||||||
|
|
||||||
// TestAdminUsers_Get_NotFound verifies that requesting details for a
|
// TestAdminUsers_Get_NotFound verifies that requesting details for a
|
||||||
// non-existent user returns HTTP 404 Not Found.
|
// non-existent user returns HTTP 404 Not Found.
|
||||||
|
func TestAdminUsers_Get_NotFound(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -120,6 +120,7 @@ import (
|
|||||||
// TestAdminUsers_PatchTests_Eligible tests that the system correctly
|
// TestAdminUsers_PatchTests_Eligible tests that the system correctly
|
||||||
// identifies which services require patch tests and returns only those services
|
// identifies which services require patch tests and returns only those services
|
||||||
// the user is eligible for based on age requirements.
|
// the user is eligible for based on age requirements.
|
||||||
|
func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ import (
|
|||||||
// Create services - some with patch test, some without
|
// Create services - some with patch test, some without
|
||||||
_, err = db.DB.Exec(context.Background(), `
|
_, err = db.DB.Exec(context.Background(), `
|
||||||
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
|
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
|
||||||
VALUES
|
VALUES
|
||||||
('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0),
|
('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0),
|
||||||
('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16),
|
('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16),
|
||||||
('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 16),
|
('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 16),
|
||||||
@@ -196,6 +197,7 @@ import (
|
|||||||
// TestAdminUsers_PatchTests_Eligible_WithExisting verifies that when a
|
// TestAdminUsers_PatchTests_Eligible_WithExisting verifies that when a
|
||||||
// user already has a valid patch test on file, that service is filtered out
|
// user already has a valid patch test on file, that service is filtered out
|
||||||
// from the eligible list (since they've already completed it).
|
// from the eligible list (since they've already completed it).
|
||||||
|
func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -282,6 +284,7 @@ import (
|
|||||||
|
|
||||||
// TestAdminUsers_AddPatchTest verifies that an admin can record a patch
|
// TestAdminUsers_AddPatchTest verifies that an admin can record a patch
|
||||||
// test completion for a user, creating a user_patch_tests record.
|
// test completion for a user, creating a user_patch_tests record.
|
||||||
|
func TestAdminUsers_AddPatchTest(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -369,6 +372,7 @@ func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) {
|
|||||||
|
|
||||||
// TestAdminUsers_NonAdmin verifies that non-admin users receive HTTP 403
|
// TestAdminUsers_NonAdmin verifies that non-admin users receive HTTP 403
|
||||||
// Forbidden when attempting to list users, get user details, or manage patch tests.
|
// Forbidden when attempting to list users, get user details, or manage patch tests.
|
||||||
|
func TestAdminUsers_NonAdmin(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -421,10 +425,9 @@ func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// TestAdminUsers_Get_Success is an additional test verifying admin can
|
// TestAdminUsers_Get_Success is an additional test verifying admin can
|
||||||
// retrieve user details including ID, name, email, and account role.
|
// retrieve user details including ID, name, email, and account role.
|
||||||
|
func TestAdminUsers_Get_Success(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -470,4 +473,3 @@ func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) {
|
|||||||
t.Errorf("expected account_role 'verified_email', got '%s'", resp.AccountRole)
|
t.Errorf("expected account_role 'verified_email', got '%s'", resp.AccountRole)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
// +build test
|
||||||
|
|
||||||
|
package auth
|
||||||
|
|
||||||
// Package auth contains tests for authentication and verification endpoints.
|
// Package auth contains tests for authentication and verification endpoints.
|
||||||
//
|
//
|
||||||
// Test Coverage:
|
// Test Coverage:
|
||||||
@@ -16,11 +18,6 @@
|
|||||||
// * Updates user role from unverified_email to verified_email on success
|
// * Updates user role from unverified_email to verified_email on success
|
||||||
//
|
//
|
||||||
// Validation: Comprehensive tests for invalid inputs (bad email, bad phone, underage, etc.)
|
// Validation: Comprehensive tests for invalid inputs (bad email, bad phone, underage, etc.)
|
||||||
package auth
|
|
||||||
//go:build test
|
|
||||||
// +build test
|
|
||||||
|
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -34,8 +31,8 @@ import (
|
|||||||
|
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
|
|
||||||
"crussell/mw"
|
|
||||||
"crussell/internal/dav"
|
"crussell/internal/dav"
|
||||||
|
"crussell/mw"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
"crussell/testutils/jwt"
|
"crussell/testutils/jwt"
|
||||||
"crussell/testutils/testdb"
|
"crussell/testutils/testdb"
|
||||||
@@ -96,6 +93,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// valid credentials. It tests the happy path: valid name, email, password,
|
// valid credentials. It tests the happy path: valid name, email, password,
|
||||||
// UK phone number, date of birth, and policy agreement. The test confirms
|
// UK phone number, date of birth, and policy agreement. The test confirms
|
||||||
// the user is created in the database with status 201.
|
// the user is created in the database with status 201.
|
||||||
|
func TestRegister_Success(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -132,6 +130,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestRegister_InvalidInput_MissingFields tests that registration fails with
|
// TestRegister_InvalidInput_MissingFields tests that registration fails with
|
||||||
// HTTP 400 when required fields are missing. It covers missing firstName,
|
// HTTP 400 when required fields are missing. It covers missing firstName,
|
||||||
// lastName, email, phone, dateOfBirth, and when policy agreement is not given.
|
// lastName, email, phone, dateOfBirth, and when policy agreement is not given.
|
||||||
|
func TestRegister_InvalidInput_MissingFields(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -179,6 +178,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestRegister_InvalidInput_InvalidEmail verifies that registration fails
|
// TestRegister_InvalidInput_InvalidEmail verifies that registration fails
|
||||||
// with HTTP 400 when an invalid email format is provided (e.g., "not-an-email").
|
// with HTTP 400 when an invalid email format is provided (e.g., "not-an-email").
|
||||||
|
func TestRegister_InvalidInput_InvalidEmail(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -203,6 +203,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestRegister_InvalidInput_InvalidPhone tests that registration fails
|
// TestRegister_InvalidInput_InvalidPhone tests that registration fails
|
||||||
// with HTTP 400 when an invalid UK phone number is provided (e.g., too short).
|
// with HTTP 400 when an invalid UK phone number is provided (e.g., too short).
|
||||||
|
func TestRegister_InvalidInput_InvalidPhone(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -225,9 +226,9 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRegister_ValidUKPhoneNumbers tests all valid UK mobile phone formats
|
|
||||||
// TestRegister_ValidUKPhoneNumbers verifies that registration accepts all
|
// TestRegister_ValidUKPhoneNumbers verifies that registration accepts all
|
||||||
// valid UK mobile phone formats including 07x numbers and E.164 format (+447...).
|
// valid UK mobile phone formats including 07x numbers and E.164 format (+447...).
|
||||||
|
func TestRegister_ValidUKPhoneNumbers(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -238,14 +239,14 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
name string
|
name string
|
||||||
phone string
|
phone string
|
||||||
}{
|
}{
|
||||||
{"07123456789", "07123456789"}, // Standard mobile
|
{"07123456789", "07123456789"}, // Standard mobile
|
||||||
{"07234567890", "07234567890"}, // 072
|
{"07234567890", "07234567890"}, // 072
|
||||||
{"07345678901", "07345678901"}, // 073
|
{"07345678901", "07345678901"}, // 073
|
||||||
{"07456789012", "07456789012"}, // 074
|
{"07456789012", "07456789012"}, // 074
|
||||||
{"07567890123", "07567890123"}, // 075
|
{"07567890123", "07567890123"}, // 075
|
||||||
{"07712345678", "07712345678"}, // 077
|
{"07712345678", "07712345678"}, // 077
|
||||||
{"07812345678", "07812345678"}, // 078
|
{"07812345678", "07812345678"}, // 078
|
||||||
{"07912345678", "07912345678"}, // 079
|
{"07912345678", "07912345678"}, // 079
|
||||||
{"+447123456789", "+447123456789"}, // E.164 format
|
{"+447123456789", "+447123456789"}, // E.164 format
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,10 +271,10 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRegister_InvalidPhoneNumbers tests various invalid phone formats
|
|
||||||
// TestRegister_InvalidPhoneNumbers verifies that registration rejects
|
// TestRegister_InvalidPhoneNumbers verifies that registration rejects
|
||||||
// invalid phone numbers including too short, invalid formats, US numbers, and
|
// invalid phone numbers including too short, invalid formats, US numbers, and
|
||||||
// numbers with special characters.
|
// numbers with special characters.
|
||||||
|
func TestRegister_InvalidPhoneNumbers(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -286,7 +287,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
}{
|
}{
|
||||||
{"too_short", "12345"},
|
{"too_short", "12345"},
|
||||||
{"invalid_07700900000", "07700900000"}, // Invalid number per libphonenumber
|
{"invalid_07700900000", "07700900000"}, // Invalid number per libphonenumber
|
||||||
{"us_number", "+12025551234"}, // US number - not UK
|
{"us_number", "+12025551234"}, // US number - not UK
|
||||||
{"letters", "ABCDEFGHIJK"},
|
{"letters", "ABCDEFGHIJK"},
|
||||||
{"empty", ""},
|
{"empty", ""},
|
||||||
{"special_chars", "+44!@#$%^&*()"},
|
{"special_chars", "+44!@#$%^&*()"},
|
||||||
@@ -315,6 +316,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestRegister_InvalidInput_Under16 tests that users under 16 years old cannot
|
// TestRegister_InvalidInput_Under16 tests that users under 16 years old cannot
|
||||||
// register. The system enforces a minimum age of 16 for account creation.
|
// register. The system enforces a minimum age of 16 for account creation.
|
||||||
|
func TestRegister_InvalidInput_Under16(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -342,6 +344,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestRegister_DuplicateEmail verifies that attempting to register with
|
// TestRegister_DuplicateEmail verifies that attempting to register with
|
||||||
// an email that already exists returns HTTP 409 Conflict.
|
// an email that already exists returns HTTP 409 Conflict.
|
||||||
|
func TestRegister_DuplicateEmail(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -380,6 +383,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestLogin_Success tests that an existing user can successfully log in
|
// TestLogin_Success tests that an existing user can successfully log in
|
||||||
// with correct email and password, receiving a JWT token in the response.
|
// with correct email and password, receiving a JWT token in the response.
|
||||||
|
func TestLogin_Success(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -417,6 +421,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestLogin_InvalidCredentials_WrongPassword verifies that login fails with
|
// TestLogin_InvalidCredentials_WrongPassword verifies that login fails with
|
||||||
// HTTP 401 when the correct email exists but the password is incorrect.
|
// HTTP 401 when the correct email exists but the password is incorrect.
|
||||||
|
func TestLogin_InvalidCredentials_WrongPassword(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -443,6 +448,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestLogin_InvalidCredentials_NonExistentEmail verifies that login fails
|
// TestLogin_InvalidCredentials_NonExistentEmail verifies that login fails
|
||||||
// with HTTP 401 when the email does not exist in the database.
|
// with HTTP 401 when the email does not exist in the database.
|
||||||
|
func TestLogin_InvalidCredentials_NonExistentEmail(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -466,6 +472,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestRefreshToken_Success tests that a valid JWT token can be refreshed
|
// TestRefreshToken_Success tests that a valid JWT token can be refreshed
|
||||||
// to obtain a new token with extended expiry.
|
// to obtain a new token with extended expiry.
|
||||||
|
func TestRefreshToken_Success(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -511,6 +518,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestRefreshToken_Unauthorized_NoToken verifies that attempting to refresh
|
// TestRefreshToken_Unauthorized_NoToken verifies that attempting to refresh
|
||||||
// a token without providing one results in HTTP 401 Unauthorized.
|
// a token without providing one results in HTTP 401 Unauthorized.
|
||||||
|
func TestRefreshToken_Unauthorized_NoToken(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -535,6 +543,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestVerifyGenerate_ValidEmail tests that a verification code can be
|
// TestVerifyGenerate_ValidEmail tests that a verification code can be
|
||||||
// generated for an existing user email. The code is stored in the database
|
// generated for an existing user email. The code is stored in the database
|
||||||
// for subsequent verification.
|
// for subsequent verification.
|
||||||
|
func TestVerifyGenerate_ValidEmail(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -581,6 +590,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestVerifyGenerate_NonExistentEmail verifies that the verification code
|
// TestVerifyGenerate_NonExistentEmail verifies that the verification code
|
||||||
// generation endpoint returns HTTP 200 even for non-existent emails. This is
|
// generation endpoint returns HTTP 200 even for non-existent emails. This is
|
||||||
// a security measure to prevent email enumeration attacks.
|
// a security measure to prevent email enumeration attacks.
|
||||||
|
func TestVerifyGenerate_NonExistentEmail(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -615,6 +625,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestVerifyCheck_ValidCode tests that a valid, non-expired, unused
|
// TestVerifyCheck_ValidCode tests that a valid, non-expired, unused
|
||||||
// verification code successfully verifies a user's email and updates their
|
// verification code successfully verifies a user's email and updates their
|
||||||
// account role from unverified_email to verified_email.
|
// account role from unverified_email to verified_email.
|
||||||
|
func TestVerifyCheck_ValidCode(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -668,6 +679,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestVerifyCheck_InvalidCode verifies that attempting to verify with
|
// TestVerifyCheck_InvalidCode verifies that attempting to verify with
|
||||||
// a non-existent code returns HTTP 400 Bad Request.
|
// a non-existent code returns HTTP 400 Bad Request.
|
||||||
|
func TestVerifyCheck_InvalidCode(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -686,6 +698,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestVerifyCheck_ExpiredCode tests that verification fails with HTTP 400
|
// TestVerifyCheck_ExpiredCode tests that verification fails with HTTP 400
|
||||||
// when the code has expired (past its expires_at timestamp).
|
// when the code has expired (past its expires_at timestamp).
|
||||||
|
func TestVerifyCheck_ExpiredCode(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -726,6 +739,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestLogin_InvalidRequest verifies that sending malformed JSON to the login
|
// TestLogin_InvalidRequest verifies that sending malformed JSON to the login
|
||||||
// endpoint returns HTTP 400 Bad Request.
|
// endpoint returns HTTP 400 Bad Request.
|
||||||
|
func TestLogin_InvalidRequest(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -744,6 +758,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestRegister_NameTooLong tests that registration fails when the first name
|
// TestRegister_NameTooLong tests that registration fails when the first name
|
||||||
// exceeds 50 characters (the maximum allowed length).
|
// exceeds 50 characters (the maximum allowed length).
|
||||||
|
func TestRegister_NameTooLong(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -770,6 +785,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestRegister_InvalidNameCharacters verifies that registration fails when
|
// TestRegister_InvalidNameCharacters verifies that registration fails when
|
||||||
// names contain invalid characters (e.g., numbers).
|
// names contain invalid characters (e.g., numbers).
|
||||||
|
func TestRegister_InvalidNameCharacters(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -795,6 +811,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestVerifyCheck_AlreadyUsed tests that attempting to verify with a code
|
// TestVerifyCheck_AlreadyUsed tests that attempting to verify with a code
|
||||||
// that has already been used returns HTTP 403 Forbidden.
|
// that has already been used returns HTTP 403 Forbidden.
|
||||||
|
func TestVerifyCheck_AlreadyUsed(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -837,6 +854,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestVerifyCheck_RoleChangeToVerified confirms that after a successful
|
// TestVerifyCheck_RoleChangeToVerified confirms that after a successful
|
||||||
// verification, the user's account_role changes from unverified_email to
|
// verification, the user's account_role changes from unverified_email to
|
||||||
// verified_email, granting them full account access.
|
// verified_email, granting them full account access.
|
||||||
|
func TestVerifyCheck_RoleChangeToVerified(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -891,5 +909,6 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
t.Errorf("expected role to change to 'verified_email', got %s", newRole)
|
t.Errorf("expected role to change to 'verified_email', got %s", newRole)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure test compilation - import pgxpool to avoid unused import
|
// Ensure test compilation - import pgxpool to avoid unused import
|
||||||
var _ = func() *pgxpool.Pool { return nil }
|
var _ = func() *pgxpool.Pool { return nil }
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/handlers/notifications"
|
"crussell/handlers/notifications"
|
||||||
"crussell/internal/dav"
|
"crussell/internal/dav"
|
||||||
"crussell/mw"
|
|
||||||
"crussell/internal/validators"
|
"crussell/internal/validators"
|
||||||
|
"crussell/mw"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -1240,7 +1240,6 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if req.StartTime.Before(time.Now()) {
|
if req.StartTime.Before(time.Now()) {
|
||||||
http.Error(w, "Start time cannot be in the past", http.StatusBadRequest)
|
http.Error(w, "Start time cannot be in the past", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate booking fits within operating hours for regular users
|
// Validate booking fits within operating hours for regular users
|
||||||
@@ -1279,8 +1278,6 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Get created by from context (if available)
|
// Get created by from context (if available)
|
||||||
var createdBy *string
|
var createdBy *string
|
||||||
if creatorID, ok := r.Context().Value(mw.UserIDKey).(string); ok {
|
if creatorID, ok := r.Context().Value(mw.UserIDKey).(string); ok {
|
||||||
@@ -1761,7 +1758,6 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tx, err := db.DB.Begin(r.Context())
|
tx, err := db.DB.Begin(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to start transaction: %v", err)
|
log.Printf("Failed to start transaction: %v", err)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
// +build test
|
||||||
|
|
||||||
|
package bookings
|
||||||
|
|
||||||
// Package bookings contains tests for user-facing booking endpoints.
|
// Package bookings contains tests for user-facing booking endpoints.
|
||||||
//
|
//
|
||||||
// Test Coverage:
|
// Test Coverage:
|
||||||
@@ -13,12 +15,6 @@
|
|||||||
// - GetBookingCalendarHandler: GET /api/bookings/calendar - Export bookings as ICS
|
// - GetBookingCalendarHandler: GET /api/bookings/calendar - Export bookings as ICS
|
||||||
//
|
//
|
||||||
// Validation: Tests cover patch test requirements, deposit rules, time slot conflicts.
|
// Validation: Tests cover patch test requirements, deposit rules, time slot conflicts.
|
||||||
package bookings
|
|
||||||
//go:build test
|
|
||||||
// +build test
|
|
||||||
|
|
||||||
package bookings
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
@@ -38,7 +34,6 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
"github.com/lib/pq"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// setupTestDB replaces the global db.DB with a test pool and returns a cleanup function
|
// setupTestDB replaces the global db.DB with a test pool and returns a cleanup function
|
||||||
@@ -210,6 +205,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestBookings_Create tests that a user can successfully create a new booking
|
// TestBookings_Create tests that a user can successfully create a new booking
|
||||||
// with a valid future time and at least one service. The test verifies the
|
// with a valid future time and at least one service. The test verifies the
|
||||||
// booking is created in the database and associated with the correct user.
|
// booking is created in the database and associated with the correct user.
|
||||||
|
func TestBookings_Create(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -273,6 +269,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestBookings_Create_InvalidInput verifies that booking creation fails
|
// TestBookings_Create_InvalidInput verifies that booking creation fails
|
||||||
// with HTTP 400 when required fields are missing: start time or service IDs.
|
// with HTTP 400 when required fields are missing: start time or service IDs.
|
||||||
|
func TestBookings_Create_InvalidInput(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -336,6 +333,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestBookings_List tests that a user can retrieve their list of bookings.
|
// TestBookings_List tests that a user can retrieve their list of bookings.
|
||||||
// The test verifies the response includes the correct total count and that
|
// The test verifies the response includes the correct total count and that
|
||||||
// bookings are properly returned.
|
// bookings are properly returned.
|
||||||
|
func TestBookings_List(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -391,6 +389,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestBookings_List_FilterByStatus tests that booking list can be filtered
|
// TestBookings_List_FilterByStatus tests that booking list can be filtered
|
||||||
// by status (e.g., pending, completed). It verifies that non-matching statuses
|
// by status (e.g., pending, completed). It verifies that non-matching statuses
|
||||||
// return empty results.
|
// return empty results.
|
||||||
|
func TestBookings_List_FilterByStatus(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -461,6 +460,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestBookings_Get tests that a user can retrieve a single booking by its ID.
|
// TestBookings_Get tests that a user can retrieve a single booking by its ID.
|
||||||
// The test verifies the booking details including services are returned.
|
// The test verifies the booking details including services are returned.
|
||||||
|
func TestBookings_Get(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -515,6 +515,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestBookings_Get_NotFound verifies that requesting a non-existent booking
|
// TestBookings_Get_NotFound verifies that requesting a non-existent booking
|
||||||
// returns HTTP 404 Not Found.
|
// returns HTTP 404 Not Found.
|
||||||
|
func TestBookings_Get_NotFound(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -544,6 +545,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestBookings_Get_AccessDenied tests that a user cannot access another user's
|
// TestBookings_Get_AccessDenied tests that a user cannot access another user's
|
||||||
// booking. The test creates two users, one creates a booking, and the other
|
// booking. The test creates two users, one creates a booking, and the other
|
||||||
// attempts to access it - expecting HTTP 404 (not found/access denied).
|
// attempts to access it - expecting HTTP 404 (not found/access denied).
|
||||||
|
func TestBookings_Get_AccessDenied(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -592,6 +594,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestBookings_GetCalendar tests that a user can export their booking
|
// TestBookings_GetCalendar tests that a user can export their booking
|
||||||
// as an ICS calendar file. It verifies the response has the correct
|
// as an ICS calendar file. It verifies the response has the correct
|
||||||
// text/calendar Content-Type and contains ICS-formatted data.
|
// text/calendar Content-Type and contains ICS-formatted data.
|
||||||
|
func TestBookings_GetCalendar(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -653,6 +656,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestBookings_GetCalendar_NotFound verifies that attempting to export
|
// TestBookings_GetCalendar_NotFound verifies that attempting to export
|
||||||
// a non-existent booking to calendar returns HTTP 404.
|
// a non-existent booking to calendar returns HTTP 404.
|
||||||
|
func TestBookings_GetCalendar_NotFound(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -685,6 +689,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestBookings_Edit tests that a user can modify the start time of
|
// TestBookings_Edit tests that a user can modify the start time of
|
||||||
// their existing booking. The test verifies the time is updated in the DB.
|
// their existing booking. The test verifies the time is updated in the DB.
|
||||||
|
func TestBookings_Edit(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -750,6 +755,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestBookings_Edit_InvalidInput verifies that editing fails with HTTP 400
|
// TestBookings_Edit_InvalidInput verifies that editing fails with HTTP 400
|
||||||
// when the start time is missing or is in the past.
|
// when the start time is missing or is in the past.
|
||||||
|
func TestBookings_Edit_InvalidInput(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -811,6 +817,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestBookings_Edit_NotFound verifies that editing a non-existent
|
// TestBookings_Edit_NotFound verifies that editing a non-existent
|
||||||
// booking returns HTTP 404.
|
// booking returns HTTP 404.
|
||||||
|
func TestBookings_Edit_NotFound(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -848,6 +855,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestBookings_Delete tests that a user can delete (cancel) their booking.
|
// TestBookings_Delete tests that a user can delete (cancel) their booking.
|
||||||
// For bookings without payments, it performs a hard delete. The test verifies
|
// For bookings without payments, it performs a hard delete. The test verifies
|
||||||
// the booking is removed from the database.
|
// the booking is removed from the database.
|
||||||
|
func TestBookings_Delete(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -902,6 +910,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// an associated payment requires a reason (client_cancelled). Without a reason,
|
// an associated payment requires a reason (client_cancelled). Without a reason,
|
||||||
// the request fails with HTTP 400. With a reason, the booking is soft-deleted
|
// the request fails with HTTP 400. With a reason, the booking is soft-deleted
|
||||||
// (status changed to client_cancelled).
|
// (status changed to client_cancelled).
|
||||||
|
func TestBookings_Delete_WithReason(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -971,6 +980,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestBookings_Delete_NotFound verifies that deleting a non-existent
|
// TestBookings_Delete_NotFound verifies that deleting a non-existent
|
||||||
// booking returns HTTP 404.
|
// booking returns HTTP 404.
|
||||||
|
func TestBookings_Delete_NotFound(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1004,6 +1014,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestBookings_Unauthorized tests that all booking endpoints require
|
// TestBookings_Unauthorized tests that all booking endpoints require
|
||||||
// authentication. It verifies that requests without a token are rejected with
|
// authentication. It verifies that requests without a token are rejected with
|
||||||
// HTTP 401 for protected endpoints.
|
// HTTP 401 for protected endpoints.
|
||||||
|
func TestBookings_Unauthorized(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1120,6 +1131,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestBookings_List_Empty tests that listing bookings for a user with no
|
// TestBookings_List_Empty tests that listing bookings for a user with no
|
||||||
// bookings returns an empty list with total 0.
|
// bookings returns an empty list with total 0.
|
||||||
|
func TestBookings_List_Empty(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1161,6 +1173,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestBookings_Get_InvalidBookingID verifies that using an invalid
|
// TestBookings_Get_InvalidBookingID verifies that using an invalid
|
||||||
// booking ID format returns HTTP 404 or 400.
|
// booking ID format returns HTTP 404 or 400.
|
||||||
|
func TestBookings_Get_InvalidBookingID(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1190,6 +1203,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
|
|
||||||
// TestBookings_Create_PastDate verifies that creating a booking with a
|
// TestBookings_Create_PastDate verifies that creating a booking with a
|
||||||
// past start time fails with HTTP 400 Bad Request.
|
// past start time fails with HTTP 400 Bad Request.
|
||||||
|
func TestBookings_Create_PastDate(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1230,6 +1244,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestBookings_Create_Within48HourDepositRequired tests that when a booking
|
// TestBookings_Create_Within48HourDepositRequired tests that when a booking
|
||||||
// is made within 48 hours and the user has deposits_required > 0, the booking
|
// is made within 48 hours and the user has deposits_required > 0, the booking
|
||||||
// should have deposit_required=true. With deposits_required=0, no deposit needed.
|
// should have deposit_required=true. With deposits_required=0, no deposit needed.
|
||||||
|
func TestBookings_Create_Within48HourDepositRequired(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1279,6 +1294,7 @@ func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
|||||||
// TestBookings_Create_MultipleServices verifies that a booking can include
|
// TestBookings_Create_MultipleServices verifies that a booking can include
|
||||||
// multiple services at once, and all services are properly associated with
|
// multiple services at once, and all services are properly associated with
|
||||||
// the booking in the database.
|
// the booking in the database.
|
||||||
|
func TestBookings_Create_MultipleServices(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1343,6 +1359,7 @@ var _ = mw.UserIDKey
|
|||||||
|
|
||||||
// TestBookings_Get_NoAuthHeader confirms that accessing a booking without
|
// TestBookings_Get_NoAuthHeader confirms that accessing a booking without
|
||||||
// an Authorization header returns HTTP 401 Unauthorized.
|
// an Authorization header returns HTTP 401 Unauthorized.
|
||||||
|
func TestBookings_Get_NoAuthHeader(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1386,6 +1403,7 @@ var _ = mw.UserIDKey
|
|||||||
// TestBookings_GetCalendar_ValidICS validates that the ICS calendar export
|
// TestBookings_GetCalendar_ValidICS validates that the ICS calendar export
|
||||||
// contains all required fields: BEGIN:VCALENDAR, END:VCALENDAR, BEGIN:VEVENT,
|
// contains all required fields: BEGIN:VCALENDAR, END:VCALENDAR, BEGIN:VEVENT,
|
||||||
// END:VEVENT, DTSTART, DTEND, and SUMMARY.
|
// END:VEVENT, DTSTART, DTEND, and SUMMARY.
|
||||||
|
func TestBookings_GetCalendar_ValidICS(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1459,6 +1477,7 @@ var _ = mw.UserIDKey
|
|||||||
// TestUserCancelBooking_ConfirmedCreatesNotification verifies that when a
|
// TestUserCancelBooking_ConfirmedCreatesNotification verifies that when a
|
||||||
// user cancels a confirmed booking (one with payments), an admin notification
|
// user cancels a confirmed booking (one with payments), an admin notification
|
||||||
// is created to alert staff of the cancellation.
|
// is created to alert staff of the cancellation.
|
||||||
|
func TestUserCancelBooking_ConfirmedCreatesNotification(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1530,6 +1549,7 @@ var _ = mw.UserIDKey
|
|||||||
// TestUserCancelBooking_PendingNoNotification verifies that cancelling a
|
// TestUserCancelBooking_PendingNoNotification verifies that cancelling a
|
||||||
// pending booking (one without payments) does NOT create an admin notification,
|
// pending booking (one without payments) does NOT create an admin notification,
|
||||||
// as pending cancellations don't require staff attention.
|
// as pending cancellations don't require staff attention.
|
||||||
|
func TestUserCancelBooking_PendingNoNotification(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1593,11 +1613,10 @@ var _ = mw.UserIDKey
|
|||||||
// Transaction and Error Handling Tests
|
// Transaction and Error Handling Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// TestUserCancelBooking_TransactionIntegrity verifies that if any part of the
|
|
||||||
// cancellation transaction fails, the booking status is NOT changed (rollback behavior)
|
|
||||||
// TestUserCancelBooking_TransactionIntegrity tests that the cancellation
|
// TestUserCancelBooking_TransactionIntegrity tests that the cancellation
|
||||||
// transaction properly commits - verifying the booking status actually changes
|
// transaction properly commits - verifying the booking status actually changes
|
||||||
// after a successful cancellation request.
|
// after a successful cancellation request.
|
||||||
|
func TestUserCancelBooking_TransactionIntegrity(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -1674,6 +1693,7 @@ var _ = mw.UserIDKey
|
|||||||
// TestCreateEditRequest verifies that a user can request an edit to their
|
// TestCreateEditRequest verifies that a user can request an edit to their
|
||||||
// confirmed booking (e.g., change time). This creates a booking_edit_request record
|
// confirmed booking (e.g., change time). This creates a booking_edit_request record
|
||||||
// and generates an admin notification for staff review.
|
// and generates an admin notification for staff review.
|
||||||
|
func TestCreateEditRequest(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
|
|||||||
@@ -373,15 +373,19 @@ func AdminEditBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Clear any pending edit requests for this booking (admin edit takes priority)
|
// Clear any pending edit requests for this booking (admin edit takes priority)
|
||||||
_, err = db.DB.Exec(r.Context(), `
|
_, err = db.DB.Exec(r.Context(), `
|
||||||
DELETE FROM booking_edit_requests
|
DELETE FROM booking_edit_requests
|
||||||
WHERE booking_id = $1
|
WHERE booking_id = $1
|
||||||
`, bookingID)
|
`, bookingID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to clear edit requests for booking %s: %v", bookingID, err)
|
||||||
|
// Don't fail the request, just log the error
|
||||||
|
}
|
||||||
|
|
||||||
// Return warnings if any
|
// Return warnings if any
|
||||||
if len(warnings) > 0 {
|
if len(warnings) > 0 {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"message": "Booking updated",
|
"message": "Booking updated",
|
||||||
"warnings": warnings,
|
"warnings": warnings,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -537,7 +541,6 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if isClosed {
|
if isClosed {
|
||||||
http.Error(w, "Cannot book during holiday hours when the salon is closed", http.StatusConflict)
|
http.Error(w, "Cannot book during holiday hours when the salon is closed", http.StatusConflict)
|
||||||
return
|
return
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for overlapping confirmed/in_progress/completed bookings
|
// Check for overlapping confirmed/in_progress/completed bookings
|
||||||
@@ -555,7 +558,6 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tx, err := db.DB.Begin(r.Context())
|
tx, err := db.DB.Begin(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to start transaction: %v", err)
|
log.Printf("Failed to start transaction: %v", err)
|
||||||
@@ -700,18 +702,19 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// BookingEditRequest represents a user's request to edit a booking
|
// BookingEditRequest represents a user's request to edit a booking
|
||||||
type BookingEditRequest struct {
|
type BookingEditRequest struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
BookingID string `json:"booking_id"`
|
BookingID string `json:"booking_id"`
|
||||||
RequestedBy string `json:"requested_by"`
|
RequestedBy string `json:"requested_by"`
|
||||||
NewStartTime *time.Time `json:"new_start_time,omitempty"`
|
NewStartTime *time.Time `json:"new_start_time,omitempty"`
|
||||||
NewServices []string `json:"new_services"`
|
NewServices []string `json:"new_services"`
|
||||||
Notes *string `json:"notes,omitempty"`
|
Notes *string `json:"notes,omitempty"`
|
||||||
HasOverrides bool `json:"has_overrides"`
|
HasOverrides bool `json:"has_overrides"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
// Joined fields
|
// Joined fields
|
||||||
Booking *Booking `json:"booking,omitempty"`
|
Booking *Booking `json:"booking,omitempty"`
|
||||||
User *UserSummary `json:"user,omitempty"`
|
User *UserSummary `json:"user,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteEditRequestHandler allows a user to delete/cancel their pending edit request
|
// DeleteEditRequestHandler allows a user to delete/cancel their pending edit request
|
||||||
func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
bookingID := chi.URLParam(r, "id")
|
bookingID := chi.URLParam(r, "id")
|
||||||
@@ -755,7 +758,7 @@ func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Delete the edit request for this booking
|
// Delete the edit request for this booking
|
||||||
res, err := tx.Exec(r.Context(), `
|
res, err := tx.Exec(r.Context(), `
|
||||||
DELETE FROM booking_edit_requests
|
DELETE FROM booking_edit_requests
|
||||||
WHERE booking_id = $1 AND requested_by = $2
|
WHERE booking_id = $1 AND requested_by = $2
|
||||||
`, bookingID, userID)
|
`, bookingID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -790,7 +793,6 @@ func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// RequestEditHandler allows a user to request an edit to their booking
|
// RequestEditHandler allows a user to request an edit to their booking
|
||||||
func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
bookingID := chi.URLParam(r, "id")
|
bookingID := chi.URLParam(r, "id")
|
||||||
@@ -855,7 +857,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if len(req.NewServices) > 0 {
|
if len(req.NewServices) > 0 {
|
||||||
var overrideCount int
|
var overrideCount int
|
||||||
err = db.DB.QueryRow(r.Context(), `
|
err = db.DB.QueryRow(r.Context(), `
|
||||||
SELECT COUNT(*) FROM booking_services
|
SELECT COUNT(*) FROM booking_services
|
||||||
WHERE booking_id = $1 AND (override_price IS NOT NULL OR override_duration_minutes IS NOT NULL)
|
WHERE booking_id = $1 AND (override_price IS NOT NULL OR override_duration_minutes IS NOT NULL)
|
||||||
`, bookingID).Scan(&overrideCount)
|
`, bookingID).Scan(&overrideCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -879,7 +881,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Delete any existing edit request for this booking (upsert behavior)
|
// Delete any existing edit request for this booking (upsert behavior)
|
||||||
_, err = tx.Exec(r.Context(), `
|
_, err = tx.Exec(r.Context(), `
|
||||||
DELETE FROM booking_edit_requests
|
DELETE FROM booking_edit_requests
|
||||||
WHERE booking_id = $1 AND requested_by = $2
|
WHERE booking_id = $1 AND requested_by = $2
|
||||||
`, bookingID, userID)
|
`, bookingID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -910,7 +912,6 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Delete existing admin notification for edit_request before creating new one (refreshes timestamp)
|
// Delete existing admin notification for edit_request before creating new one (refreshes timestamp)
|
||||||
_, err = tx.Exec(r.Context(), `
|
_, err = tx.Exec(r.Context(), `
|
||||||
DELETE FROM admin_notifications
|
DELETE FROM admin_notifications
|
||||||
@@ -937,8 +938,8 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if currentStatus == "pending" {
|
if currentStatus == "pending" {
|
||||||
// Acknowledge existing pending_booking notification
|
// Acknowledge existing pending_booking notification
|
||||||
_, err = tx.Exec(r.Context(), `
|
_, err = tx.Exec(r.Context(), `
|
||||||
UPDATE admin_notifications
|
UPDATE admin_notifications
|
||||||
SET acknowledged_at = NOW()
|
SET acknowledged_at = NOW()
|
||||||
WHERE booking_id = $1 AND reason = 'pending_booking' AND acknowledged_at IS NULL
|
WHERE booking_id = $1 AND reason = 'pending_booking' AND acknowledged_at IS NULL
|
||||||
`, bookingID)
|
`, bookingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -969,7 +970,7 @@ func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// AdminListEditRequestsHandler returns all edit requests
|
// AdminListEditRequestsHandler returns all edit requests
|
||||||
func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
|
func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
baseQuery := `
|
baseQuery := `
|
||||||
SELECT ber.id, ber.booking_id, ber.requested_by, ber.new_start_time,
|
SELECT ber.id, ber.booking_id, ber.requested_by, ber.new_start_time,
|
||||||
ber.new_services, ber.notes, ber.has_overrides, ber.updated_at,
|
ber.new_services, ber.notes, ber.has_overrides, ber.updated_at,
|
||||||
b.start_time as original_start_time, b.status as booking_status,
|
b.start_time as original_start_time, b.status as booking_status,
|
||||||
u.fn as user_name
|
u.fn as user_name
|
||||||
@@ -1007,7 +1008,6 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var origStartTime time.Time
|
var origStartTime time.Time
|
||||||
var bookingStatus string
|
var bookingStatus string
|
||||||
var userName string
|
var userName string
|
||||||
var newServices []string
|
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&req.ID,
|
&req.ID,
|
||||||
@@ -1071,18 +1071,17 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Get the edit request
|
// Get the edit request
|
||||||
var bookingID string
|
var bookingID string
|
||||||
var newStartTime *time.Time
|
|
||||||
var newServices []string
|
var newServices []string
|
||||||
var notes *string
|
var notes *string
|
||||||
var hasOverrides bool
|
var hasOverrides bool
|
||||||
err = tx.QueryRow(r.Context(), `
|
err = tx.QueryRow(r.Context(), `
|
||||||
SELECT booking_id, new_start_time, new_services, notes, has_overrides
|
SELECT booking_id, new_start_time, new_services, notes, has_overrides
|
||||||
FROM booking_edit_requests
|
FROM booking_edit_requests
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`, requestID).Scan(&bookingID, &newStartTime, pq.Array(&newServices), ¬es, &hasOverrides)
|
`, requestID).Scan(&bookingID, &newStartTime, pq.Array(&newServices), ¬es, &hasOverrides)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
http.Error(w, "Edit request not found", http.StatusNotFound)
|
http.Error(w, "Edit request not found or already processed", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Failed to get edit request %s: %v", requestID, err)
|
log.Printf("Failed to get edit request %s: %v", requestID, err)
|
||||||
@@ -1229,6 +1228,7 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(r.Context()); err != nil {
|
if err := tx.Commit(r.Context()); err != nil {
|
||||||
log.Printf("Failed to commit: %v", err)
|
log.Printf("Failed to commit: %v", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
@@ -1272,7 +1272,7 @@ func AdminRejectEditRequestHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Delete the edit request
|
// Delete the edit request
|
||||||
_, err = tx.Exec(r.Context(), `
|
_, err = tx.Exec(r.Context(), `
|
||||||
DELETE FROM booking_edit_requests
|
DELETE FROM booking_edit_requests
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`, requestID)
|
`, requestID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
// +build test
|
||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
// Package handlers contains tests for core middleware and health checks.
|
// Package handlers contains tests for core middleware and health checks.
|
||||||
//
|
//
|
||||||
// Test Coverage:
|
// Test Coverage:
|
||||||
@@ -10,12 +12,6 @@
|
|||||||
// - Integration test: Full user flow with JWT auth and context propagation
|
// - Integration test: Full user flow with JWT auth and context propagation
|
||||||
//
|
//
|
||||||
// Note: These tests focus on middleware behavior, not specific handler business logic.
|
// Note: These tests focus on middleware behavior, not specific handler business logic.
|
||||||
package handlers
|
|
||||||
//go:build test
|
|
||||||
// +build test
|
|
||||||
|
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
@@ -32,6 +28,7 @@ import (
|
|||||||
// TestHealthCheck verifies the health check endpoint returns HTTP 200 OK.
|
// TestHealthCheck verifies the health check endpoint returns HTTP 200 OK.
|
||||||
// This test ensures the basic HTTP server is responding and the health
|
// This test ensures the basic HTTP server is responding and the health
|
||||||
// check handler is properly wired up to return a status response.
|
// check handler is properly wired up to return a status response.
|
||||||
|
func TestHealthCheck(t *testing.T) {
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(`{"status":"ok"}`))
|
w.Write([]byte(`{"status":"ok"}`))
|
||||||
@@ -55,6 +52,7 @@ import (
|
|||||||
// blocks unauthenticated requests and allows valid JWT tokens through.
|
// blocks unauthenticated requests and allows valid JWT tokens through.
|
||||||
// It tests three scenarios: missing auth header (401), valid token (200 with
|
// It tests three scenarios: missing auth header (401), valid token (200 with
|
||||||
// user context), and invalid token (401).
|
// user context), and invalid token (401).
|
||||||
|
func TestRequireAuthMiddleware(t *testing.T) {
|
||||||
handler := mw.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := mw.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, _ := r.Context().Value(mw.UserIDKey).(string)
|
userID, _ := r.Context().Value(mw.UserIDKey).(string)
|
||||||
role, _ := r.Context().Value(mw.UserRoleKey).(string)
|
role, _ := r.Context().Value(mw.UserRoleKey).(string)
|
||||||
@@ -113,6 +111,7 @@ import (
|
|||||||
// correctly blocks non-admin users (403) and allows admin users (200) to access
|
// correctly blocks non-admin users (403) and allows admin users (200) to access
|
||||||
// protected resources. It chains RequireAuth before RequireRole to populate
|
// protected resources. It chains RequireAuth before RequireRole to populate
|
||||||
// the role in the request context.
|
// the role in the request context.
|
||||||
|
func TestRequireRoleMiddleware(t *testing.T) {
|
||||||
// Chain RequireAuth before RequireRole to set the role in context
|
// Chain RequireAuth before RequireRole to set the role in context
|
||||||
// RequireRole expects role to be in context, but that's only set by RequireAuth
|
// RequireRole expects role to be in context, but that's only set by RequireAuth
|
||||||
adminOnlyHandler := mw.RequireAuth(mw.RequireRole("admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
adminOnlyHandler := mw.RequireAuth(mw.RequireRole("admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -120,7 +119,6 @@ import (
|
|||||||
w.Write([]byte(`{"success":true}`))
|
w.Write([]byte(`{"success":true}`))
|
||||||
})))
|
})))
|
||||||
|
|
||||||
|
|
||||||
t.Run("admin role passes", func(t *testing.T) {
|
t.Run("admin role passes", func(t *testing.T) {
|
||||||
jwt.Init()
|
jwt.Init()
|
||||||
token := jwt.GenerateAdminToken()
|
token := jwt.GenerateAdminToken()
|
||||||
@@ -154,6 +152,7 @@ import (
|
|||||||
// verifying that JWT tokens are properly validated, user ID is extracted from
|
// verifying that JWT tokens are properly validated, user ID is extracted from
|
||||||
// the token, and context values are correctly propagated to handlers.
|
// the token, and context values are correctly propagated to handlers.
|
||||||
// This is an integration test that validates the complete middleware chain.
|
// This is an integration test that validates the complete middleware chain.
|
||||||
|
func TestIntegration_UserFlow(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skipping integration test in short mode")
|
t.Skip("skipping integration test in short mode")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,6 @@
|
|||||||
//
|
//
|
||||||
// Authentication: Upload/Delete require admin role (403 for non-admins, 401 for unauth).
|
// Authentication: Upload/Delete require admin role (403 for non-admins, 401 for unauth).
|
||||||
// Note: Upload/Delete tests verify auth only; S3 operations not fully tested (requires mock).
|
// Note: Upload/Delete tests verify auth only; S3 operations not fully tested (requires mock).
|
||||||
package portfolio
|
|
||||||
//go:build test
|
|
||||||
// +build test
|
|
||||||
|
|
||||||
package portfolio
|
package portfolio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
// +build test
|
||||||
|
|
||||||
|
package scheduling
|
||||||
|
|
||||||
// Package scheduling contains tests for working hours and availability endpoints.
|
// Package scheduling contains tests for working hours and availability endpoints.
|
||||||
//
|
//
|
||||||
// Test Coverage:
|
// Test Coverage:
|
||||||
@@ -14,12 +16,6 @@
|
|||||||
// - UpdateExceptionalApplications: PUT /api/scheduling/exceptional-applications - Apply holidays
|
// - UpdateExceptionalApplications: PUT /api/scheduling/exceptional-applications - Apply holidays
|
||||||
//
|
//
|
||||||
// Authentication: Update/Create/Delete endpoints require admin role (403 for non-admins).
|
// Authentication: Update/Create/Delete endpoints require admin role (403 for non-admins).
|
||||||
package scheduling
|
|
||||||
//go:build test
|
|
||||||
// +build test
|
|
||||||
|
|
||||||
package scheduling
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
@@ -125,6 +121,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
// TestScheduling_GetDefaultHours verifies that the default weekly working hours
|
// TestScheduling_GetDefaultHours verifies that the default weekly working hours
|
||||||
// can be retrieved. The test checks that all 7 days are returned with correct
|
// can be retrieved. The test checks that all 7 days are returned with correct
|
||||||
// opening times, closing times, and is_open status.
|
// opening times, closing times, and is_open status.
|
||||||
|
func TestScheduling_GetDefaultHours(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -172,6 +169,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
// TestScheduling_UpdateDefaultHours_Admin tests that an admin can update
|
// TestScheduling_UpdateDefaultHours_Admin tests that an admin can update
|
||||||
// the default weekly working hours. The new schedule is persisted to the
|
// the default weekly working hours. The new schedule is persisted to the
|
||||||
// database and returned on subsequent requests.
|
// database and returned on subsequent requests.
|
||||||
|
func TestScheduling_UpdateDefaultHours_Admin(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -217,6 +215,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
|
|
||||||
// TestScheduling_UpdateDefaultHours_NonAdmin verifies that non-admin users
|
// TestScheduling_UpdateDefaultHours_NonAdmin verifies that non-admin users
|
||||||
// receive HTTP 403 Forbidden when attempting to update default hours.
|
// receive HTTP 403 Forbidden when attempting to update default hours.
|
||||||
|
func TestScheduling_UpdateDefaultHours_NonAdmin(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -244,6 +243,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
|
|
||||||
// TestScheduling_ListExceptionalGroups verifies that admins can list all
|
// TestScheduling_ListExceptionalGroups verifies that admins can list all
|
||||||
// exceptional working hours groups (holidays, special events).
|
// exceptional working hours groups (holidays, special events).
|
||||||
|
func TestScheduling_ListExceptionalGroups(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -281,6 +281,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
|
|
||||||
// TestScheduling_CreateExceptionalGroup_Admin tests that an admin can
|
// TestScheduling_CreateExceptionalGroup_Admin tests that an admin can
|
||||||
// create a new exceptional working hours group with specific hours for each day.
|
// create a new exceptional working hours group with specific hours for each day.
|
||||||
|
func TestScheduling_CreateExceptionalGroup_Admin(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -323,6 +324,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
|
|
||||||
// TestScheduling_CreateExceptionalGroup_NonAdmin verifies that non-admin
|
// TestScheduling_CreateExceptionalGroup_NonAdmin verifies that non-admin
|
||||||
// users receive HTTP 403 when attempting to create exceptional groups.
|
// users receive HTTP 403 when attempting to create exceptional groups.
|
||||||
|
func TestScheduling_CreateExceptionalGroup_NonAdmin(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -355,6 +357,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
// TestScheduling_DeleteExceptionalGroup_Admin tests that an admin can delete
|
// TestScheduling_DeleteExceptionalGroup_Admin tests that an admin can delete
|
||||||
// an exceptional working hours group. This removes the group and its associated
|
// an exceptional working hours group. This removes the group and its associated
|
||||||
// hours from the system.
|
// hours from the system.
|
||||||
|
func TestScheduling_DeleteExceptionalGroup_Admin(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -395,6 +398,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
|
|
||||||
// TestScheduling_DeleteExceptionalGroup_NonAdmin verifies that non-admin
|
// TestScheduling_DeleteExceptionalGroup_NonAdmin verifies that non-admin
|
||||||
// users receive HTTP 403 when attempting to delete exceptional groups.
|
// users receive HTTP 403 when attempting to delete exceptional groups.
|
||||||
|
func TestScheduling_DeleteExceptionalGroup_NonAdmin(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -416,6 +420,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
// TestScheduling_GetWorkingHours verifies that working hours can be
|
// TestScheduling_GetWorkingHours verifies that working hours can be
|
||||||
// retrieved for a given date range. The response includes whether hours come
|
// retrieved for a given date range. The response includes whether hours come
|
||||||
// from default schedule or exceptional groups.
|
// from default schedule or exceptional groups.
|
||||||
|
func TestScheduling_GetWorkingHours(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -451,6 +456,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
// TestScheduling_GetAvailableHours tests that available appointment
|
// TestScheduling_GetAvailableHours tests that available appointment
|
||||||
// slots can be calculated for a date range based on working hours and service
|
// slots can be calculated for a date range based on working hours and service
|
||||||
// durations.
|
// durations.
|
||||||
|
func TestScheduling_GetAvailableHours(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -488,6 +494,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
// TestScheduling_UpdateExceptionalApplications_Admin verifies that an
|
// TestScheduling_UpdateExceptionalApplications_Admin verifies that an
|
||||||
// admin can apply an exceptional hours group to specific weeks, activating
|
// admin can apply an exceptional hours group to specific weeks, activating
|
||||||
// holiday schedules for those periods.
|
// holiday schedules for those periods.
|
||||||
|
func TestScheduling_UpdateExceptionalApplications_Admin(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -532,6 +539,7 @@ func makeAuthRequest(handler http.Handler, method, path, token string, body inte
|
|||||||
|
|
||||||
// TestScheduling_UpdateExceptionalApplications_NonAdmin verifies that
|
// TestScheduling_UpdateExceptionalApplications_NonAdmin verifies that
|
||||||
// non-admin users receive HTTP 403 when attempting to apply exceptional hours.
|
// non-admin users receive HTTP 403 when attempting to apply exceptional hours.
|
||||||
|
func TestScheduling_UpdateExceptionalApplications_NonAdmin(t *testing.T) {
|
||||||
cleanup := setupTestDB(t)
|
cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
|
|||||||
@@ -348,4 +348,3 @@ func TestContact_ReturnsInfo(t *testing.T) {
|
|||||||
t.Error("expected role in response")
|
t.Error("expected role in response")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -542,7 +542,6 @@ func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if req.NewPassword == req.CurrentPassword {
|
if req.NewPassword == req.CurrentPassword {
|
||||||
http.Error(w, "new password must be different from current password", http.StatusBadRequest)
|
http.Error(w, "new password must be different from current password", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//go:build test
|
//go:build test
|
||||||
// +build test
|
// +build test
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
// Package user contains tests for user profile and account management endpoints.
|
// Package user contains tests for user profile and account management endpoints.
|
||||||
//
|
//
|
||||||
// Test Coverage:
|
// Test Coverage:
|
||||||
@@ -13,31 +15,24 @@
|
|||||||
//
|
//
|
||||||
// Authentication: All endpoints require auth (401 for unauthenticated).
|
// Authentication: All endpoints require auth (401 for unauthenticated).
|
||||||
// Validation: Tests cover invalid inputs (missing fields, invalid phone, weak passwords).
|
// Validation: Tests cover invalid inputs (missing fields, invalid phone, weak passwords).
|
||||||
package user
|
|
||||||
//go:build test
|
|
||||||
// +build test
|
|
||||||
|
|
||||||
package user
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/internal/s3"
|
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
"crussell/testutils/fixtures"
|
"crussell/testutils/fixtures"
|
||||||
"crussell/testutils/jwt"
|
"crussell/testutils/jwt"
|
||||||
"crussell/testutils/testdb"
|
"crussell/testutils/testdb"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -328,7 +323,6 @@ func TestLoyalty_Get(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TestProfile_Update_InvalidInput verifies that profile update validation rejects invalid inputs:
|
// TestProfile_Update_InvalidInput verifies that profile update validation rejects invalid inputs:
|
||||||
// missing first name, missing last name, missing phone, invalid phone format, invalid characters in name, name too long.
|
// missing first name, missing last name, missing phone, invalid phone format, invalid characters in name, name too long.
|
||||||
func TestProfile_Update_InvalidInput(t *testing.T) {
|
func TestProfile_Update_InvalidInput(t *testing.T) {
|
||||||
@@ -343,9 +337,9 @@ func TestProfile_Update_InvalidInput(t *testing.T) {
|
|||||||
token := jwt.GenerateUserToken(userID)
|
token := jwt.GenerateUserToken(userID)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
req UpdateProfileRequest
|
req UpdateProfileRequest
|
||||||
expected int
|
expected int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "missing_first_name",
|
name: "missing_first_name",
|
||||||
@@ -485,7 +479,6 @@ func TestPasswordChange_SameAsOld(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TestProfile_UploadPicture verifies that a user can upload a profile picture. May return 500 if S3 is not configured.
|
// TestProfile_UploadPicture verifies that a user can upload a profile picture. May return 500 if S3 is not configured.
|
||||||
func TestProfile_UploadPicture(t *testing.T) {
|
func TestProfile_UploadPicture(t *testing.T) {
|
||||||
cleanup, pool := setupTest(t)
|
cleanup, pool := setupTest(t)
|
||||||
@@ -549,14 +542,14 @@ func TestProfile_UploadPicture(t *testing.T) {
|
|||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
// Note: This test may return 500 if S3 is not configured
|
// Note: This test may return 500 if S3 is not configured
|
||||||
// In that case, we check for either success or proper error handling
|
// In that case, we check for either success or proper error handling
|
||||||
if rr.Code != http.StatusOK && rr.Code != http.StatusInternalServerError {
|
if rr.Code != http.StatusOK && rr.Code != http.StatusInternalServerError {
|
||||||
t.Errorf("expected status 200 or 500 (if S3 not configured), got %d", rr.Code)
|
t.Errorf("expected status 200 or 500 (if S3 not configured), got %d", rr.Code)
|
||||||
t.Logf("response body: %s", rr.Body.String())
|
t.Logf("response body: %s", rr.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// If S3 is configured, verify the response contains a URL
|
// If S3 is configured, verify the response contains a URL
|
||||||
if rr.Code == http.StatusOK {
|
if rr.Code == http.StatusOK {
|
||||||
var resp map[string]string
|
var resp map[string]string
|
||||||
|
|||||||
Reference in New Issue
Block a user