refactor(backend): update test files for PoolProxy and per-test transactions

Migrate all test files from SetupTestDB/db.DB pattern to per-test transactions:

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

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-21 19:29:24 +01:00
co-authored by Sisyphus
parent 3d0e2afc4c
commit 220a0ef6e8
57 changed files with 5911 additions and 6235 deletions
+22 -36
View File
@@ -4,12 +4,10 @@
package admin
import (
"context"
"net/http"
"testing"
"time"
"crussell/db"
"crussell/testutils"
"crussell/handlers/bookings"
"crussell/testutils/fixtures"
@@ -17,35 +15,31 @@ import (
// TestGetOverlappingBookingsByTime verifies the new overlapping bookings endpoint
func TestGetOverlappingBookingsByTime(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
startTime := time.Date(2026, 3, 16, 10, 0, 0, 0, time.UTC)
bookingID, err := fixtures.CreateTestBookingAtTime(db.DB, userID, serviceID, startTime)
_, err = fixtures.CreateTestBookingAtTime(tx, userID, serviceID, startTime)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
handler := http.HandlerFunc(bookings.GetOverlappingBookingsByTimeHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/overlapping?start=2026-03-16T09:00:00Z&end=2026-03-16T11:00:00Z", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/overlapping?start=2026-03-16T09:00:00Z&end=2026-03-16T11:00:00Z", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -63,35 +57,31 @@ func TestGetOverlappingBookingsByTime(t *testing.T) {
// TestGetBookingsByDateRange verifies the new bookings by date range endpoint
func TestGetBookingsByDateRange(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
startTime := time.Date(2026, 3, 16, 10, 0, 0, 0, time.UTC)
bookingID, err := fixtures.CreateTestBookingAtTime(db.DB, userID, serviceID, startTime)
_, err = fixtures.CreateTestBookingAtTime(tx, userID, serviceID, startTime)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
handler := http.HandlerFunc(bookings.GetBookingsByDateRangeHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/by-date-range?start=2026-03-16&end=2026-03-16", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/by-date-range?start=2026-03-16&end=2026-03-16", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -109,32 +99,28 @@ func TestGetBookingsByDateRange(t *testing.T) {
// TestAdminRescheduleBooking verifies the new reschedule endpoint
func TestAdminRescheduleBooking(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
startTime := time.Now().Add(48 * time.Hour).Truncate(time.Second)
bookingID, err := fixtures.CreateTestBookingAtTime(db.DB, userID, serviceID, startTime)
bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, startTime)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
newStartTime := startTime.Add(2 * time.Hour)
req := map[string]interface{}{
@@ -142,14 +128,14 @@ func TestAdminRescheduleBooking(t *testing.T) {
}
handler := http.HandlerFunc(bookings.AdminRescheduleBookingHandler)
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID+"/reschedule", req)
w := makeAdminRequest(handler, "PUT", "/api/admin/bookings/"+bookingID+"/reschedule", req, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var dbStartTime time.Time
err = db.DB.QueryRow(context.Background(),
err = tx.QueryRow(ctx,
"SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&dbStartTime)
if err != nil {
t.Fatalf("failed to query booking: %v", err)
+10 -15
View File
@@ -8,53 +8,48 @@ import (
"net/http"
"testing"
"crussell/db"
"crussell/testutils"
"crussell/handlers/bookings"
"crussell/testutils"
"crussell/testutils/fixtures"
)
// TestAdminBookings_Get_EnrichedFields verifies that CreatedByName and User.DateOfBirth are populated.
func TestAdminBookings_Get_EnrichedFields(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
userID, err := fixtures.CreateTestUser(db.DB)
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(db.DB, bookingID)
dob := "1990-01-01"
_, err = db.DB.Exec(context.Background(), "UPDATE users SET date_of_birth = $1 WHERE id = $2", dob, userID)
_, err = tx.Exec(context.Background(), "UPDATE users SET date_of_birth = $1 WHERE id = $2", dob, userID)
if err != nil {
t.Fatalf("failed to update user dob: %v", err)
}
_, err = db.DB.Exec(context.Background(), "UPDATE bookings SET created_by = $1 WHERE id = $2", adminID, bookingID)
_, err = tx.Exec(context.Background(), "UPDATE bookings SET created_by = $1 WHERE id = $2", adminID, bookingID)
if err != nil {
t.Fatalf("failed to update booking created_by: %v", err)
}
handler := http.HandlerFunc(bookings.GetAdminBookingHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/"+bookingID, nil)
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/"+bookingID, nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
File diff suppressed because it is too large Load Diff
+110 -122
View File
@@ -24,7 +24,6 @@ import (
"strings"
"testing"
"crussell/db"
"crussell/testutils"
"crussell/mw"
"crussell/testutils/fixtures"
@@ -34,11 +33,10 @@ import (
// testAdminID is set by each test after creating an admin user via fixtures,
// so that handlers referencing created_by (which has a FK to users) work correctly.
var testAdminID string
// makeCustomServiceRequest creates an admin request with chi URL params for custom-services paths.
// Uses testAdminID (must be set by the calling test).
func makeCustomServiceRequest(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder {
func makeCustomServiceRequest(handler http.Handler, method, path string, body interface{}, adminID string, ctx context.Context) *httptest.ResponseRecorder {
var req *http.Request
if body != nil {
bodyBytes, _ := json.Marshal(body)
@@ -58,8 +56,8 @@ func makeCustomServiceRequest(handler http.Handler, method, path string, body in
}
}
ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
ctx = context.WithValue(ctx, mw.UserIDKey, testAdminID)
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
ctx = context.WithValue(ctx, mw.UserIDKey, adminID)
ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
req = req.WithContext(ctx)
@@ -75,28 +73,28 @@ func makeCustomServiceRequest(handler http.Handler, method, path string, body in
// TestCustomServices_List verifies that an admin can list all custom services
// with pagination metadata.
func TestCustomServices_List(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
defer fixtures.DeleteUser(tx, adminID)
csID1, err := fixtures.CreateTestCustomService(db.DB)
csID1, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service 1: %v", err)
}
defer fixtures.DeleteCustomService(db.DB, csID1)
defer fixtures.DeleteCustomService(tx, csID1)
csID2, err := fixtures.CreateTestCustomService(db.DB)
csID2, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service 2: %v", err)
}
defer fixtures.DeleteCustomService(db.DB, csID2)
defer fixtures.DeleteCustomService(tx, csID2)
handler := http.HandlerFunc(GetCustomServices)
w := makeAdminRequest(handler, "GET", "/api/admin/custom-services", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/custom-services", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -119,22 +117,22 @@ func TestCustomServices_List(t *testing.T) {
// TestCustomServices_List_Search verifies search filtering via the q parameter,
// including case-insensitive matching and no-results scenarios.
func TestCustomServices_List_Search(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
defer fixtures.DeleteUser(tx, adminID)
csID, err := fixtures.CreateTestCustomService(db.DB)
csID, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service: %v", err)
}
defer fixtures.DeleteCustomService(db.DB, csID)
defer fixtures.DeleteCustomService(tx, csID)
// Set a unique name for search testing
_, err = db.DB.Exec(context.Background(),
_, err = tx.Exec(context.Background(),
"UPDATE custom_services SET name = 'SearchableServiceName' WHERE id = $1", csID)
if err != nil {
t.Fatalf("failed to update custom service name: %v", err)
@@ -143,7 +141,7 @@ func TestCustomServices_List_Search(t *testing.T) {
handler := http.HandlerFunc(GetCustomServices)
// Matching search (case-insensitive)
w := makeAdminRequest(handler, "GET", "/api/admin/custom-services?q=searchableservicename", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/custom-services?q=searchableservicename", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
@@ -162,7 +160,7 @@ func TestCustomServices_List_Search(t *testing.T) {
}
// Non-matching search
w = makeAdminRequest(handler, "GET", "/api/admin/custom-services?q=NONEXISTENT_QUERY_XYZ", nil)
w = makeAdminRequest(handler, "GET", "/api/admin/custom-services?q=NONEXISTENT_QUERY_XYZ", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
@@ -183,35 +181,35 @@ func TestCustomServices_List_Search(t *testing.T) {
// TestCustomServices_List_Popular verifies the popular flag returns custom services
// ordered by usage_count, limited to the specified number.
func TestCustomServices_List_Popular(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
defer fixtures.DeleteUser(tx, adminID)
csID1, err := fixtures.CreateTestCustomService(db.DB)
csID1, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service 1: %v", err)
}
defer fixtures.DeleteCustomService(db.DB, csID1)
defer fixtures.DeleteCustomService(tx, csID1)
csID2, err := fixtures.CreateTestCustomService(db.DB)
csID2, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service 2: %v", err)
}
defer fixtures.DeleteCustomService(db.DB, csID2)
defer fixtures.DeleteCustomService(tx, csID2)
// Set usage counts via direct DB to have services with usage_count > 0
_, err = db.DB.Exec(context.Background(),
_, err = tx.Exec(context.Background(),
"UPDATE custom_services SET usage_count = 5, last_used_at = NOW() WHERE id = $1", csID1)
if err != nil {
t.Fatalf("failed to set usage count: %v", err)
}
handler := http.HandlerFunc(GetCustomServices)
w := makeAdminRequest(handler, "GET", "/api/admin/custom-services?popular=3", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/custom-services?popular=3", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -237,18 +235,18 @@ func TestCustomServices_List_Popular(t *testing.T) {
// TestCustomServices_List_Pagination verifies page and per_page query parameters.
func TestCustomServices_List_Pagination(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
defer fixtures.DeleteUser(tx, adminID)
// Create 3 custom services
csIDs := make([]string, 3)
for i := 0; i < 3; i++ {
csID, err := fixtures.CreateTestCustomService(db.DB)
csID, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service %d: %v", i+1, err)
}
@@ -256,13 +254,13 @@ func TestCustomServices_List_Pagination(t *testing.T) {
}
defer func() {
for _, id := range csIDs {
fixtures.DeleteCustomService(db.DB, id)
fixtures.DeleteCustomService(tx, id)
}
}()
handler := http.HandlerFunc(GetCustomServices)
w := makeAdminRequest(handler, "GET", "/api/admin/custom-services?per_page=2", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/custom-services?per_page=2", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
@@ -296,13 +294,13 @@ func TestCustomServices_List_Pagination(t *testing.T) {
// TestCustomServices_Create verifies that an admin can create a new custom service
// with name, description, price, duration, minimum age, and notes.
func TestCustomServices_Create(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
defer fixtures.DeleteUser(tx, adminID)
handler := http.HandlerFunc(CreateCustomService)
@@ -315,7 +313,7 @@ func TestCustomServices_Create(t *testing.T) {
Notes: stringPtr("Custom service notes"),
}
w := makeRequestWithContext(handler, "POST", "/api/admin/custom-services", createReq, adminID, "admin")
w := makeRequestWithContext(handler, "POST", "/api/admin/custom-services", createReq, adminID, "admin", ctx)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
@@ -354,13 +352,13 @@ func TestCustomServices_Create(t *testing.T) {
// TestCustomServices_Create_Validation verifies that validation errors return
// HTTP 400 for various invalid inputs.
func TestCustomServices_Create_Validation(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
defer fixtures.DeleteUser(tx, adminID)
tests := []struct {
name string
@@ -420,7 +418,7 @@ func TestCustomServices_Create_Validation(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handler := http.HandlerFunc(CreateCustomService)
w := makeRequestWithContext(handler, "POST", "/api/admin/custom-services", tt.req, adminID, "admin")
w := makeRequestWithContext(handler, "POST", "/api/admin/custom-services", tt.req, adminID, "admin", ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -435,23 +433,22 @@ func TestCustomServices_Create_Validation(t *testing.T) {
// TestCustomServices_Get verifies that an admin can retrieve a single custom service by ID.
func TestCustomServices_Get(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
testAdminID = adminID
defer fixtures.DeleteUser(tx, adminID)
csID, err := fixtures.CreateTestCustomService(db.DB)
csID, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service: %v", err)
}
defer fixtures.DeleteCustomService(db.DB, csID)
defer fixtures.DeleteCustomService(tx, csID)
handler := http.HandlerFunc(GetCustomService)
w := makeCustomServiceRequest(handler, "GET", "/api/admin/custom-services/"+csID, nil)
w := makeCustomServiceRequest(handler, "GET", "/api/admin/custom-services/"+csID, nil, adminID, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -477,17 +474,16 @@ func TestCustomServices_Get(t *testing.T) {
// TestCustomServices_Get_NotFound verifies that requesting a non-existent custom service returns 404.
func TestCustomServices_Get_NotFound(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
testAdminID = adminID
defer fixtures.DeleteUser(tx, adminID)
handler := http.HandlerFunc(GetCustomService)
w := makeCustomServiceRequest(handler, "GET", "/api/admin/custom-services/nonexistent-id", nil)
w := makeCustomServiceRequest(handler, "GET", "/api/admin/custom-services/nonexistent-id", nil, adminID, ctx)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
@@ -500,20 +496,19 @@ func TestCustomServices_Get_NotFound(t *testing.T) {
// TestCustomServices_Update verifies that an admin can update a custom service's fields.
func TestCustomServices_Update(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
testAdminID = adminID
defer fixtures.DeleteUser(tx, adminID)
csID, err := fixtures.CreateTestCustomService(db.DB)
csID, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service: %v", err)
}
defer fixtures.DeleteCustomService(db.DB, csID)
defer fixtures.DeleteCustomService(tx, csID)
newName := "Updated Custom Name"
updateReq := UpdateCustomServiceRequest{
@@ -521,7 +516,7 @@ func TestCustomServices_Update(t *testing.T) {
}
handler := http.HandlerFunc(UpdateCustomService)
w := makeCustomServiceRequest(handler, "PUT", "/api/admin/custom-services/"+csID, updateReq)
w := makeCustomServiceRequest(handler, "PUT", "/api/admin/custom-services/"+csID, updateReq, adminID, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -538,7 +533,7 @@ func TestCustomServices_Update(t *testing.T) {
// Verify the update persisted
var dbName string
err = db.DB.QueryRow(context.Background(),
err = tx.QueryRow(context.Background(),
"SELECT name FROM custom_services WHERE id = $1", csID).Scan(&dbName)
if err != nil {
t.Fatalf("failed to query custom service: %v", err)
@@ -551,14 +546,13 @@ func TestCustomServices_Update(t *testing.T) {
// TestCustomServices_Update_NotFound verifies that updating a non-existent custom service returns 404.
func TestCustomServices_Update_NotFound(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
testAdminID = adminID
defer fixtures.DeleteUser(tx, adminID)
newName := "Updated Name"
updateReq := UpdateCustomServiceRequest{
@@ -566,7 +560,7 @@ func TestCustomServices_Update_NotFound(t *testing.T) {
}
handler := http.HandlerFunc(UpdateCustomService)
w := makeCustomServiceRequest(handler, "PUT", "/api/admin/custom-services/nonexistent-id", updateReq)
w := makeCustomServiceRequest(handler, "PUT", "/api/admin/custom-services/nonexistent-id", updateReq, adminID, ctx)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
@@ -576,25 +570,24 @@ func TestCustomServices_Update_NotFound(t *testing.T) {
// TestCustomServices_Update_NoFields verifies that sending an update with no fields
// returns 400 Bad Request.
func TestCustomServices_Update_NoFields(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
testAdminID = adminID
defer fixtures.DeleteUser(tx, adminID)
csID, err := fixtures.CreateTestCustomService(db.DB)
csID, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service: %v", err)
}
defer fixtures.DeleteCustomService(db.DB, csID)
defer fixtures.DeleteCustomService(tx, csID)
updateReq := UpdateCustomServiceRequest{}
handler := http.HandlerFunc(UpdateCustomService)
w := makeCustomServiceRequest(handler, "PUT", "/api/admin/custom-services/"+csID, updateReq)
w := makeCustomServiceRequest(handler, "PUT", "/api/admin/custom-services/"+csID, updateReq, adminID, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -608,22 +601,21 @@ func TestCustomServices_Update_NoFields(t *testing.T) {
// TestCustomServices_Promote verifies that promoting a custom service creates a
// regular service, migrates data, and deletes the original custom service.
func TestCustomServices_Promote(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
testAdminID = adminID
defer fixtures.DeleteUser(tx, adminID)
csID, err := fixtures.CreateTestCustomService(db.DB)
csID, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service: %v", err)
}
handler := http.HandlerFunc(PromoteCustomService)
w := makeCustomServiceRequest(handler, "POST", "/api/admin/custom-services/"+csID+"/promote", nil)
w := makeCustomServiceRequest(handler, "POST", "/api/admin/custom-services/"+csID+"/promote", nil, adminID, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -645,7 +637,7 @@ func TestCustomServices_Promote(t *testing.T) {
// Verify the custom service was deleted
var count int
err = db.DB.QueryRow(context.Background(),
err = tx.QueryRow(context.Background(),
"SELECT COUNT(*) FROM custom_services WHERE id = $1", csID).Scan(&count)
if err != nil {
t.Fatalf("failed to query custom service: %v", err)
@@ -656,7 +648,7 @@ func TestCustomServices_Promote(t *testing.T) {
// Verify the new regular service was created
var serviceName string
err = db.DB.QueryRow(context.Background(),
err = tx.QueryRow(context.Background(),
"SELECT name FROM services WHERE id = $1", newServiceID).Scan(&serviceName)
if err != nil {
t.Fatalf("failed to query promoted service: %v", err)
@@ -666,22 +658,21 @@ func TestCustomServices_Promote(t *testing.T) {
}
// Clean up: delete the promoted service
defer fixtures.DeleteService(db.DB, newServiceID)
defer fixtures.DeleteService(tx, newServiceID)
}
// TestCustomServices_Promote_NotFound verifies that promoting a non-existent custom service returns 404.
func TestCustomServices_Promote_NotFound(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
testAdminID = adminID
defer fixtures.DeleteUser(tx, adminID)
handler := http.HandlerFunc(PromoteCustomService)
w := makeCustomServiceRequest(handler, "POST", "/api/admin/custom-services/nonexistent-id/promote", nil)
w := makeCustomServiceRequest(handler, "POST", "/api/admin/custom-services/nonexistent-id/promote", nil, adminID, ctx)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
@@ -694,22 +685,21 @@ func TestCustomServices_Promote_NotFound(t *testing.T) {
// TestCustomServices_Delete verifies that an admin can delete an unused custom service.
func TestCustomServices_Delete(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
testAdminID = adminID
defer fixtures.DeleteUser(tx, adminID)
csID, err := fixtures.CreateTestCustomService(db.DB)
csID, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service: %v", err)
}
handler := http.HandlerFunc(DeleteCustomService)
w := makeCustomServiceRequest(handler, "DELETE", "/api/admin/custom-services/"+csID, nil)
w := makeCustomServiceRequest(handler, "DELETE", "/api/admin/custom-services/"+csID, nil, adminID, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -726,7 +716,7 @@ func TestCustomServices_Delete(t *testing.T) {
// Verify it's gone from the DB
var count int
err = db.DB.QueryRow(context.Background(),
err = tx.QueryRow(context.Background(),
"SELECT COUNT(*) FROM custom_services WHERE id = $1", csID).Scan(&count)
if err != nil {
t.Fatalf("failed to query custom service: %v", err)
@@ -738,17 +728,16 @@ func TestCustomServices_Delete(t *testing.T) {
// TestCustomServices_Delete_NotFound verifies that deleting a non-existent custom service returns 404.
func TestCustomServices_Delete_NotFound(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
testAdminID = adminID
defer fixtures.DeleteUser(tx, adminID)
handler := http.HandlerFunc(DeleteCustomService)
w := makeCustomServiceRequest(handler, "DELETE", "/api/admin/custom-services/nonexistent-id", nil)
w := makeCustomServiceRequest(handler, "DELETE", "/api/admin/custom-services/nonexistent-id", nil, adminID, ctx)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
@@ -758,30 +747,29 @@ func TestCustomServices_Delete_NotFound(t *testing.T) {
// TestCustomServices_Delete_Conflict verifies that deleting a custom service with
// usage_count > 0 returns 409 Conflict.
func TestCustomServices_Delete_Conflict(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin user: %v", err)
}
defer fixtures.DeleteUser(db.DB, adminID)
testAdminID = adminID
defer fixtures.DeleteUser(tx, adminID)
csID, err := fixtures.CreateTestCustomService(db.DB)
csID, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service: %v", err)
}
defer fixtures.DeleteCustomService(db.DB, csID)
defer fixtures.DeleteCustomService(tx, csID)
// Simulate usage to trigger conflict
_, err = db.DB.Exec(context.Background(),
_, err = tx.Exec(context.Background(),
"UPDATE custom_services SET usage_count = 3 WHERE id = $1", csID)
if err != nil {
t.Fatalf("failed to set usage count: %v", err)
}
handler := http.HandlerFunc(DeleteCustomService)
w := makeCustomServiceRequest(handler, "DELETE", "/api/admin/custom-services/"+csID, nil)
w := makeCustomServiceRequest(handler, "DELETE", "/api/admin/custom-services/"+csID, nil, adminID, ctx)
if w.Code != http.StatusConflict {
t.Errorf("expected status 409, got %d. body: %s", w.Code, w.Body.String())
@@ -795,27 +783,27 @@ func TestCustomServices_Delete_Conflict(t *testing.T) {
// TestCustomServices_NonAdmin verifies that non-admin users receive HTTP 403
// Forbidden when attempting to access any admin custom services endpoint.
func TestCustomServices_NonAdmin(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
_, err := fixtures.CreateTestUser(db.DB)
_, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}
userID, err := fixtures.CreateTestUser(db.DB)
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
defer fixtures.DeleteUser(tx, userID)
csID, err := fixtures.CreateTestCustomService(db.DB)
csID, err := fixtures.CreateTestCustomService(tx)
if err != nil {
t.Fatalf("failed to create custom service: %v", err)
}
defer fixtures.DeleteCustomService(db.DB, csID)
defer fixtures.DeleteCustomService(tx, csID)
// Test LIST
w := makeUserRequest(mw.RequireAdmin(http.HandlerFunc(GetCustomServices)), "GET", "/api/admin/custom-services", nil)
w := makeUserRequest(mw.RequireAdmin(http.HandlerFunc(GetCustomServices)), "GET", "/api/admin/custom-services", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("LIST: expected status 403, got %d", w.Code)
}
@@ -826,32 +814,32 @@ func TestCustomServices_NonAdmin(t *testing.T) {
Price: 50.00,
DurationMinutes: 60,
}
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(CreateCustomService)), "POST", "/api/admin/custom-services", createReq)
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(CreateCustomService)), "POST", "/api/admin/custom-services", createReq, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("CREATE: expected status 403, got %d", w.Code)
}
// Test GET
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(GetCustomService)), "GET", "/api/admin/custom-services/"+csID, nil)
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(GetCustomService)), "GET", "/api/admin/custom-services/"+csID, nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("GET: expected status 403, got %d", w.Code)
}
// Test UPDATE
updateReq := UpdateCustomServiceRequest{}
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(UpdateCustomService)), "PUT", "/api/admin/custom-services/"+csID, updateReq)
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(UpdateCustomService)), "PUT", "/api/admin/custom-services/"+csID, updateReq, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("UPDATE: expected status 403, got %d", w.Code)
}
// Test PROMOTE
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(PromoteCustomService)), "POST", "/api/admin/custom-services/"+csID+"/promote", nil)
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(PromoteCustomService)), "POST", "/api/admin/custom-services/"+csID+"/promote", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("PROMOTE: expected status 403, got %d", w.Code)
}
// Test DELETE
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(DeleteCustomService)), "DELETE", "/api/admin/custom-services/"+csID, nil)
w = makeUserRequest(mw.RequireAdmin(http.HandlerFunc(DeleteCustomService)), "DELETE", "/api/admin/custom-services/"+csID, nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("DELETE: expected status 403, got %d", w.Code)
}
@@ -21,7 +21,10 @@ import (
"github.com/go-chi/chi/v5"
)
func makeCampaignRequest(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder {
var testAdminID string
func makeCampaignRequest(handler http.Handler, method, path string, body interface{}, ctx context.Context, adminID string) *httptest.ResponseRecorder {
var req *http.Request
if body != nil {
bodyBytes, _ := json.Marshal(body)
@@ -42,8 +45,8 @@ func makeCampaignRequest(handler http.Handler, method, path string, body interfa
}
}
ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
ctx = context.WithValue(ctx, mw.UserIDKey, testAdminID)
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
ctx = context.WithValue(ctx, mw.UserIDKey, adminID)
ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
req = req.WithContext(ctx)
@@ -52,34 +55,34 @@ func makeCampaignRequest(handler http.Handler, method, path string, body interfa
return w
}
func insertTimeBasedCampaign(t *testing.T, name string, discount float64, status string) string {
func insertTimeBasedCampaign(t *testing.T, ctx context.Context, tx db.Querier, adminID, name string, discount float64, status string) string {
t.Helper()
startDate := fmt.Sprintf("%sZ", time.Now().Add(-1*time.Hour).Format("2006-01-02T15:04:05"))
endDate := fmt.Sprintf("%sZ", time.Now().Add(7*24*time.Hour).Format("2006-01-02T15:04:05"))
var id string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, start_date, end_date, created_by)
VALUES ($1, 'time_based', $2, $3, $4::timestamptz, $5::timestamptz, $6)
RETURNING id
`, name, discount, status, startDate, endDate, testAdminID).Scan(&id)
`, name, discount, status, startDate, endDate, adminID).Scan(&id)
if err != nil {
t.Fatalf("failed to insert time_based campaign: %v", err)
}
return id
}
func insertMilestoneCampaign(t *testing.T, name string, discount float64, status string) string {
func insertMilestoneCampaign(t *testing.T, ctx context.Context, tx db.Querier, adminID, name string, discount float64, status string) string {
t.Helper()
mt := "per_user_booking_count"
mv := 5
mu := "bookings"
var id string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status,
milestone_type, milestone_value, milestone_unit, created_by)
VALUES ($1, 'milestone', $2, $3, $4, $5, $6, $7)
RETURNING id
`, name, discount, status, mt, mv, mu, testAdminID).Scan(&id)
`, name, discount, status, mt, mv, mu, adminID).Scan(&id)
if err != nil {
t.Fatalf("failed to insert milestone campaign: %v", err)
}
@@ -91,16 +94,15 @@ func insertMilestoneCampaign(t *testing.T, name string, discount float64, status
// =============================================================================
func TestGetDiscountCampaigns_Empty(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
handler := http.HandlerFunc(GetDiscountCampaigns)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns", nil)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns", nil, ctx, adminID)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
}
@@ -115,18 +117,17 @@ func TestGetDiscountCampaigns_Empty(t *testing.T) {
}
func TestGetDiscountCampaigns_WithCampaigns(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
insertTimeBasedCampaign(t, "Summer Sale", 15, "active")
insertTimeBasedCampaign(t, ctx, tx, adminID, "Summer Sale", 15, "active")
handler := http.HandlerFunc(GetDiscountCampaigns)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns", nil)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns", nil, ctx, adminID)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
}
@@ -153,22 +154,21 @@ func TestGetDiscountCampaigns_WithCampaigns(t *testing.T) {
}
func TestGetDiscountCampaigns_FilterByStatus(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
insertMilestoneCampaign(t, "Draft Campaign", 10, "draft")
insertMilestoneCampaign(t, "Active Campaign", 20, "active")
insertMilestoneCampaign(t, "Cancelled Campaign", 5, "cancelled")
insertMilestoneCampaign(t, ctx, tx, adminID, "Draft Campaign", 10, "draft")
insertMilestoneCampaign(t, ctx, tx, adminID, "Active Campaign", 20, "active")
insertMilestoneCampaign(t, ctx, tx, adminID, "Cancelled Campaign", 5, "cancelled")
handler := http.HandlerFunc(GetDiscountCampaigns)
t.Run("filter_by_active", func(t *testing.T) {
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns?status=active", nil)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns?status=active", nil, ctx, adminID)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
@@ -180,7 +180,7 @@ func TestGetDiscountCampaigns_FilterByStatus(t *testing.T) {
})
t.Run("filter_by_invalid_status", func(t *testing.T) {
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns?status=invalid", nil)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns?status=invalid", nil, ctx, adminID)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for invalid status filter, got %d", w.Code)
}
@@ -192,13 +192,12 @@ func TestGetDiscountCampaigns_FilterByStatus(t *testing.T) {
// =============================================================================
func TestCreateDiscountCampaign_TimeBased(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
req := CreateCampaignRequest{
Name: "Summer Sale",
@@ -210,7 +209,7 @@ func TestCreateDiscountCampaign_TimeBased(t *testing.T) {
}
handler := http.HandlerFunc(CreateDiscountCampaign)
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req)
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req, ctx, adminID)
if w.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d. body: %s", w.Code, w.Body.String())
}
@@ -231,13 +230,12 @@ func TestCreateDiscountCampaign_TimeBased(t *testing.T) {
}
func TestCreateDiscountCampaign_Milestone(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
mt := "per_user_booking_count"
mv := 5
@@ -252,7 +250,7 @@ func TestCreateDiscountCampaign_Milestone(t *testing.T) {
}
handler := http.HandlerFunc(CreateDiscountCampaign)
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req)
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req, ctx, adminID)
if w.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d. body: %s", w.Code, w.Body.String())
}
@@ -270,13 +268,12 @@ func TestCreateDiscountCampaign_Milestone(t *testing.T) {
}
func TestCreateDiscountCampaign_ValidationErrors(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
handler := http.HandlerFunc(CreateDiscountCampaign)
@@ -286,7 +283,7 @@ func TestCreateDiscountCampaign_ValidationErrors(t *testing.T) {
CampaignType: "time_based",
DiscountPercent: 10,
}
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req)
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req, ctx, adminID)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for empty name, got %d", w.Code)
}
@@ -298,7 +295,7 @@ func TestCreateDiscountCampaign_ValidationErrors(t *testing.T) {
CampaignType: "time_based",
DiscountPercent: 0,
}
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req)
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req, ctx, adminID)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for zero discount, got %d", w.Code)
}
@@ -310,7 +307,7 @@ func TestCreateDiscountCampaign_ValidationErrors(t *testing.T) {
CampaignType: "time_based",
DiscountPercent: 150,
}
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req)
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req, ctx, adminID)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for discount >100, got %d", w.Code)
}
@@ -322,7 +319,7 @@ func TestCreateDiscountCampaign_ValidationErrors(t *testing.T) {
CampaignType: "invalid_type",
DiscountPercent: 10,
}
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req)
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req, ctx, adminID)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for invalid type, got %d", w.Code)
}
@@ -334,7 +331,7 @@ func TestCreateDiscountCampaign_ValidationErrors(t *testing.T) {
CampaignType: "time_based",
DiscountPercent: 10,
}
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req)
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req, ctx, adminID)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for missing dates, got %d", w.Code)
}
@@ -350,7 +347,7 @@ func TestCreateDiscountCampaign_ValidationErrors(t *testing.T) {
StartDate: strPtr(fmt.Sprintf("%sZ", future.Format("2006-01-02T15:04:05"))),
EndDate: strPtr(fmt.Sprintf("%sZ", past.Format("2006-01-02T15:04:05"))),
}
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req)
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req, ctx, adminID)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for end before start, got %d", w.Code)
}
@@ -362,7 +359,7 @@ func TestCreateDiscountCampaign_ValidationErrors(t *testing.T) {
CampaignType: "milestone",
DiscountPercent: 10,
}
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req)
w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req, ctx, adminID)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for missing milestone fields, got %d", w.Code)
}
@@ -374,20 +371,19 @@ func TestCreateDiscountCampaign_ValidationErrors(t *testing.T) {
// =============================================================================
func TestUpdateDiscountCampaign_UpdateName(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
campaignID := insertMilestoneCampaign(t, "Old Name", 10, "draft")
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Old Name", 10, "draft")
newName := "New Name"
req := UpdateCampaignRequest{Name: &newName}
handler := http.HandlerFunc(UpdateDiscountCampaign)
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req)
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
}
@@ -402,38 +398,36 @@ func TestUpdateDiscountCampaign_UpdateName(t *testing.T) {
}
func TestUpdateDiscountCampaign_NotFound(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
newName := "Test"
req := UpdateCampaignRequest{Name: &newName}
handler := http.HandlerFunc(UpdateDiscountCampaign)
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/nonexistent-id", req)
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/nonexistent-id", req, ctx, adminID)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404 for nonexistent campaign, got %d", w.Code)
}
}
func TestUpdateDiscountCampaign_InvalidStatus(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
campaignID := insertMilestoneCampaign(t, "Test", 10, "draft")
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
badStatus := "invalid_status"
req := UpdateCampaignRequest{Status: &badStatus}
handler := http.HandlerFunc(UpdateDiscountCampaign)
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req)
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for invalid status, got %d", w.Code)
}
@@ -444,24 +438,23 @@ func TestUpdateDiscountCampaign_InvalidStatus(t *testing.T) {
// =============================================================================
func TestDeleteDiscountCampaign_HappyPath(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
campaignID := insertMilestoneCampaign(t, "Test", 10, "active")
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "active")
handler := http.HandlerFunc(DeleteDiscountCampaign)
w := makeCampaignRequest(handler, "DELETE", "/api/admin/discount-campaigns/"+campaignID, nil)
w := makeCampaignRequest(handler, "DELETE", "/api/admin/discount-campaigns/"+campaignID, nil, ctx, adminID)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
}
var status string
err = db.DB.QueryRow(context.Background(),
err = tx.QueryRow(ctx,
"SELECT status FROM discount_campaigns WHERE id = $1", campaignID).Scan(&status)
if err != nil {
t.Fatalf("failed to query campaign: %v", err)
@@ -472,16 +465,15 @@ func TestDeleteDiscountCampaign_HappyPath(t *testing.T) {
}
func TestDeleteDiscountCampaign_NotFound(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
handler := http.HandlerFunc(DeleteDiscountCampaign)
w := makeCampaignRequest(handler, "DELETE", "/api/admin/discount-campaigns/nonexistent-id", nil)
w := makeCampaignRequest(handler, "DELETE", "/api/admin/discount-campaigns/nonexistent-id", nil, ctx, adminID)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404 for nonexistent campaign, got %d", w.Code)
}
@@ -492,18 +484,17 @@ func TestDeleteDiscountCampaign_NotFound(t *testing.T) {
// =============================================================================
func TestGetCampaignStats_NoUsage(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
campaignID := insertMilestoneCampaign(t, "Test", 10, "active")
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "active")
handler := http.HandlerFunc(GetCampaignStats)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns/"+campaignID+"/stats", nil)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns/"+campaignID+"/stats", nil, ctx, adminID)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
}
@@ -524,16 +515,15 @@ func TestGetCampaignStats_NoUsage(t *testing.T) {
}
func TestGetCampaignStats_NotFound(t *testing.T) {
testutils.SetupTestDB(t)
adminID, err := fixtures.CreateTestAdminUser(db.DB)
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
defer func() { testAdminID = "" }()
handler := http.HandlerFunc(GetCampaignStats)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns/nonexistent-id/stats", nil)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns/nonexistent-id/stats", nil, ctx, adminID)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404 for nonexistent campaign, got %d", w.Code)
}
+6 -7
View File
@@ -7,16 +7,15 @@ import (
"net/http"
"testing"
"crussell/db"
"crussell/testutils"
"crussell/testutils/fixtures"
)
func TestPatchTests_CRUD(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create a service to link
serviceID, err := fixtures.CreateTestService(db.DB)
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
@@ -28,7 +27,7 @@ func TestPatchTests_CRUD(t *testing.T) {
ExpiryMonths: 6,
ServiceIDs: []string{serviceID},
}
w := makeAdminRequest(http.HandlerFunc(CreatePatchTest), "POST", "/api/admin/patch-tests", req)
w := makeAdminRequest(http.HandlerFunc(CreatePatchTest), "POST", "/api/admin/patch-tests", req, ctx)
if w.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d", w.Code)
}
@@ -38,7 +37,7 @@ func TestPatchTests_CRUD(t *testing.T) {
}
parseResponseBody(w, &created)
w = makeAdminRequest(http.HandlerFunc(GetPatchTests), "GET", "/api/admin/patch-tests", nil)
w = makeAdminRequest(http.HandlerFunc(GetPatchTests), "GET", "/api/admin/patch-tests", nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
@@ -51,12 +50,12 @@ func TestPatchTests_CRUD(t *testing.T) {
newName := "Updated Name"
updateReq := UpdatePatchTestRequest{Name: &newName}
w = makeAdminRequest(http.HandlerFunc(UpdatePatchTest), "PUT", "/api/admin/patch-tests/"+created.ID, updateReq)
w = makeAdminRequest(http.HandlerFunc(UpdatePatchTest), "PUT", "/api/admin/patch-tests/"+created.ID, updateReq, ctx)
if w.Code != http.StatusNoContent {
t.Fatalf("expected 204, got %d, body: %s", w.Code, w.Body.String())
}
w = makeAdminRequest(http.HandlerFunc(DeletePatchTest), "DELETE", "/api/admin/patch-tests/"+created.ID, nil)
w = makeAdminRequest(http.HandlerFunc(DeletePatchTest), "DELETE", "/api/admin/patch-tests/"+created.ID, nil, ctx)
if w.Code != http.StatusNoContent {
t.Fatalf("expected 204, got %d", w.Code)
}
+23 -24
View File
@@ -19,7 +19,6 @@ import (
"net/http"
"testing"
"crussell/db"
"crussell/testutils"
"crussell/handlers/services"
"crussell/mw"
@@ -29,10 +28,10 @@ import (
// with name, description, price, duration, and minimum age requirements. The new
// service is active by default and stored in the database.
func TestAdminServices_Create(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create admin user in DB first
_, err := db.DB.Exec(context.Background(), `
_, err := tx.Exec(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Admin', 'User', 'admin@test.com', '+447123456789', '1990-01-01', 'hash', 'admin', 'email')
`)
@@ -50,7 +49,7 @@ func TestAdminServices_Create(t *testing.T) {
MinimumAgeRequired: 16,
}
w := makeAdminRequest(handler, "POST", "/api/admin/services", createReq)
w := makeAdminRequest(handler, "POST", "/api/admin/services", createReq, ctx)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
@@ -75,10 +74,10 @@ func TestAdminServices_Create(t *testing.T) {
// TestAdminServices_List tests that an admin can retrieve all services,
// including inactive ones. This is useful for managing the full service catalog.
func TestAdminServices_List(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Insert test services
_, err := db.DB.Exec(context.Background(), `
_, err := tx.Exec(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES
('Manicure', 'Basic manicure', 25.00, 30, true, 0),
@@ -90,7 +89,7 @@ func TestAdminServices_List(t *testing.T) {
}
handler := http.HandlerFunc(services.AllServicesHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/services", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/services", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -126,11 +125,11 @@ func TestAdminServices_List(t *testing.T) {
// active status on/off. This is used to temporarily disable a service without
// deleting it from the system.
func TestAdminServices_Toggle(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create a service
var serviceID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 16)
RETURNING id
@@ -140,7 +139,7 @@ func TestAdminServices_Toggle(t *testing.T) {
}
handler := http.HandlerFunc(services.ToggleService)
w := makeAdminRequest(handler, "PUT", "/api/admin/services/"+serviceID+"/toggle", nil)
w := makeAdminRequest(handler, "PUT", "/api/admin/services/"+serviceID+"/toggle", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -148,7 +147,7 @@ func TestAdminServices_Toggle(t *testing.T) {
// Verify service is now inactive
var isActive bool
err = db.DB.QueryRow(context.Background(), "SELECT is_active FROM services WHERE id = $1", serviceID).Scan(&isActive)
err = tx.QueryRow(context.Background(), "SELECT is_active FROM services WHERE id = $1", serviceID).Scan(&isActive)
if err != nil {
t.Fatalf("failed to check service: %v", err)
}
@@ -157,13 +156,13 @@ func TestAdminServices_Toggle(t *testing.T) {
}
// Toggle again
w = makeAdminRequest(handler, "PUT", "/api/admin/services/"+serviceID+"/toggle", nil)
w = makeAdminRequest(handler, "PUT", "/api/admin/services/"+serviceID+"/toggle", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 on second toggle, got %d", w.Code)
}
// Verify service is active again
err = db.DB.QueryRow(context.Background(), "SELECT is_active FROM services WHERE id = $1", serviceID).Scan(&isActive)
err = tx.QueryRow(context.Background(), "SELECT is_active FROM services WHERE id = $1", serviceID).Scan(&isActive)
if err != nil {
t.Fatalf("failed to check service: %v", err)
}
@@ -176,11 +175,11 @@ func TestAdminServices_Toggle(t *testing.T) {
// by setting is_active to false. The service record remains but is hidden from
// customers.
func TestAdminServices_Delete(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create a service
var serviceID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 16)
RETURNING id
@@ -190,7 +189,7 @@ func TestAdminServices_Delete(t *testing.T) {
}
handler := http.HandlerFunc(services.DeleteServiceHandler)
w := makeAdminRequest(handler, "DELETE", "/api/admin/services/"+serviceID, nil)
w := makeAdminRequest(handler, "DELETE", "/api/admin/services/"+serviceID, nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -198,7 +197,7 @@ func TestAdminServices_Delete(t *testing.T) {
// Verify service is soft deleted (is_active = false)
var isActive bool
err = db.DB.QueryRow(context.Background(), "SELECT is_active FROM services WHERE id = $1", serviceID).Scan(&isActive)
err = tx.QueryRow(context.Background(), "SELECT is_active FROM services WHERE id = $1", serviceID).Scan(&isActive)
if err != nil {
t.Fatalf("failed to check service: %v", err)
}
@@ -211,10 +210,10 @@ func TestAdminServices_Delete(t *testing.T) {
// Forbidden when attempting to create, list, toggle, or delete services. This
// ensures proper role-based access control.
func TestAdminServices_NonAdmin(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create regular user in DB
_, err := db.DB.Exec(context.Background(), `
_, err := tx.Exec(context.Background(), `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Regular', 'User', 'user@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
`)
@@ -231,21 +230,21 @@ func TestAdminServices_NonAdmin(t *testing.T) {
DurationMinutes: 60,
MinimumAgeRequired: 16,
}
w := makeUserRequest(createHandler, "POST", "/api/admin/services", createReq)
w := makeUserRequest(createHandler, "POST", "/api/admin/services", createReq, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("CREATE: expected status 403, got %d", w.Code)
}
// Test LIST - should get 403 when using middleware
listHandler := mw.RequireAdmin(http.HandlerFunc(services.AllServicesHandler))
w = makeUserRequest(listHandler, "GET", "/api/admin/services", nil)
w = makeUserRequest(listHandler, "GET", "/api/admin/services", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("LIST: expected status 403, got %d", w.Code)
}
// Test TOGGLE - should get 403 when using middleware
var serviceID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(context.Background(), `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Test Service', 'A test service', 50.00, 60, true, 16)
RETURNING id
@@ -255,14 +254,14 @@ func TestAdminServices_NonAdmin(t *testing.T) {
}
toggleHandler := mw.RequireAdmin(http.HandlerFunc(services.ToggleService))
w = makeUserRequest(toggleHandler, "PUT", "/api/admin/services/"+serviceID+"/toggle", nil)
w = makeUserRequest(toggleHandler, "PUT", "/api/admin/services/"+serviceID+"/toggle", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("TOGGLE: expected status 403, got %d", w.Code)
}
// Test DELETE - should get 403 when using middleware
deleteHandler := mw.RequireAdmin(http.HandlerFunc(services.DeleteServiceHandler))
w = makeUserRequest(deleteHandler, "DELETE", "/api/admin/services/"+serviceID, nil)
w = makeUserRequest(deleteHandler, "DELETE", "/api/admin/services/"+serviceID, nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("DELETE: expected status 403, got %d", w.Code)
}
+27 -38
View File
@@ -4,11 +4,9 @@
package admin
import (
"context"
"net/http"
"testing"
"crussell/db"
"crussell/testutils"
)
@@ -16,25 +14,18 @@ func intPtr(i int) *int { return &i }
func float64Ptr(f float64) *float64 { return &f }
func boolPtr(b bool) *bool { return &b }
func seedBusinessSettings(t *testing.T) {
t.Helper()
_, err := db.DB.Exec(context.Background(), `
INSERT INTO business_settings (business_name, business_address, currency_code, gift_card_expiry_months, voucher_type)
VALUES ('Test Salon', '123 Test St', 'GBP', 12, 'SPV')
`)
if err != nil {
t.Fatalf("failed to seed business settings: %v", err)
}
}
// TestGetBusinessSettings verifies that GET /api/admin/settings returns the
// current business settings row.
func TestGetBusinessSettings(t *testing.T) {
testutils.SetupTestDB(t)
seedBusinessSettings(t)
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `UPDATE business_settings SET business_name = 'Test Salon', business_address = '123 Test St', currency_code = 'GBP', gift_card_expiry_months = 12, voucher_type = 'SPV'`)
if err != nil {
t.Fatalf("failed to seed business settings: %v", err)
}
handler := http.HandlerFunc(GetBusinessSettings)
w := makeAdminRequest(handler, "GET", "/api/admin/settings", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/settings", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -65,14 +56,13 @@ func TestGetBusinessSettings(t *testing.T) {
// TestUpdateBusinessSettings verifies that updating a single field via
// PUT /api/admin/settings returns 200 with the updated settings.
func TestUpdateBusinessSettings(t *testing.T) {
testutils.SetupTestDB(t)
seedBusinessSettings(t)
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
BusinessName: stringPtr("Updated Salon Name"),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body)
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -91,8 +81,7 @@ func TestUpdateBusinessSettings(t *testing.T) {
// TestUpdateBusinessSettings_MultipleFields verifies that updating several
// fields at once works correctly.
func TestUpdateBusinessSettings_MultipleFields(t *testing.T) {
testutils.SetupTestDB(t)
seedBusinessSettings(t)
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
@@ -101,7 +90,7 @@ func TestUpdateBusinessSettings_MultipleFields(t *testing.T) {
GiftCardExpiryMonths: intPtr(24),
VoucherType: stringPtr("MPV"),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body)
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -129,14 +118,13 @@ func TestUpdateBusinessSettings_MultipleFields(t *testing.T) {
// TestUpdateBusinessSettings_InvalidVoucherType verifies that an invalid
// voucher_type value returns 400.
func TestUpdateBusinessSettings_InvalidVoucherType(t *testing.T) {
testutils.SetupTestDB(t)
seedBusinessSettings(t)
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
VoucherType: stringPtr("INVALID"),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body)
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -149,14 +137,13 @@ func TestUpdateBusinessSettings_InvalidVoucherType(t *testing.T) {
// TestUpdateBusinessSettings_NegativeExpiryMonths verifies that a
// gift_card_expiry_months value less than 1 returns 400.
func TestUpdateBusinessSettings_NegativeExpiryMonths(t *testing.T) {
testutils.SetupTestDB(t)
seedBusinessSettings(t)
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
GiftCardExpiryMonths: intPtr(0),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body)
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -169,15 +156,14 @@ func TestUpdateBusinessSettings_NegativeExpiryMonths(t *testing.T) {
// TestUpdateBusinessSettings_InvalidVATRate verifies that a default_vat_rate
// outside the 0-100 range returns 400.
func TestUpdateBusinessSettings_InvalidVATRate(t *testing.T) {
testutils.SetupTestDB(t)
seedBusinessSettings(t)
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
DefaultVATRate: float64Ptr(-1),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body)
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for negative rate, got %d. body: %s", w.Code, w.Body.String())
@@ -188,7 +174,7 @@ func TestUpdateBusinessSettings_InvalidVATRate(t *testing.T) {
body2 := UpdateBusinessSettingsRequest{
DefaultVATRate: float64Ptr(101),
}
w2 := makeAdminRequest(handler, "PUT", "/api/admin/settings", body2)
w2 := makeAdminRequest(handler, "PUT", "/api/admin/settings", body2, ctx)
if w2.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for rate > 100, got %d. body: %s", w2.Code, w2.Body.String())
@@ -201,11 +187,10 @@ func TestUpdateBusinessSettings_InvalidVATRate(t *testing.T) {
// TestUpdateBusinessSettings_NoFields verifies that an empty request body
// (no fields to update) returns 400.
func TestUpdateBusinessSettings_NoFields(t *testing.T) {
testutils.SetupTestDB(t)
seedBusinessSettings(t)
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", UpdateBusinessSettingsRequest{})
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", UpdateBusinessSettingsRequest{}, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -218,14 +203,18 @@ func TestUpdateBusinessSettings_NoFields(t *testing.T) {
// TestUpdateBusinessSettings_PartialUpdate verifies that updating a single field
// leaves other fields unchanged.
func TestUpdateBusinessSettings_PartialUpdate(t *testing.T) {
testutils.SetupTestDB(t)
seedBusinessSettings(t)
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `UPDATE business_settings SET business_name = 'Test Salon', business_address = '123 Test St', currency_code = 'GBP', gift_card_expiry_months = 12, voucher_type = 'SPV'`)
if err != nil {
t.Fatalf("failed to seed business settings: %v", err)
}
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
GiftCardExpiryMonths: intPtr(36),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body)
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
+2 -1
View File
@@ -14,8 +14,9 @@ import (
func TestMain(m *testing.M) {
pool := testdb.CreateTestDatabase("crussell_test_handlers_admin")
db.DB = pool
db.Conn = db.NewPoolProxy(pool)
jwt.Init()
testdb.SeedBaseline(pool)
code := m.Run()
testdb.DestroyTestDatabase(pool, "crussell_test_handlers_admin")
os.Exit(code)
+84 -90
View File
@@ -18,13 +18,11 @@ package admin
//
// Note: Notification tests are in handlers/notifications/notifications_test.go
import (
"context"
"encoding/json"
"net/http"
"testing"
"time"
"crussell/db"
"crussell/testutils"
"crussell/handlers/notifications"
"crussell/handlers/today"
@@ -34,11 +32,11 @@ import (
// TestAdminToday_CurrentNext verifies that an admin can retrieve the currently
// in-progress booking and the next upcoming booking for the dashboard.
func TestAdminToday_CurrentNext(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+1234567890', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -49,7 +47,7 @@ func TestAdminToday_CurrentNext(t *testing.T) {
// Create service
var serviceID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active)
VALUES ('Manicure', 'Basic manicure', 25.00, 30, true)
RETURNING id
@@ -59,7 +57,7 @@ func TestAdminToday_CurrentNext(t *testing.T) {
}
// Create booking for today (in_progress)
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, NOW(), 'in_progress', NOW())
`, userID)
@@ -69,7 +67,7 @@ func TestAdminToday_CurrentNext(t *testing.T) {
// Get the booking ID
var bookingID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
SELECT id FROM bookings WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1
`, userID).Scan(&bookingID)
if err != nil {
@@ -77,7 +75,7 @@ func TestAdminToday_CurrentNext(t *testing.T) {
}
// Add service to booking
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
@@ -86,7 +84,7 @@ func TestAdminToday_CurrentNext(t *testing.T) {
}
handler := http.HandlerFunc(today.GetCurrentAndNextHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -109,7 +107,7 @@ func TestAdminToday_CurrentNext(t *testing.T) {
// TestAdminToday_CurrentNext_ClosingTime verifies that the current-next endpoint
// returns the closing time for today.
func TestAdminToday_CurrentNext_ClosingTime(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Seed working hours for today (DB uses 0=Monday, 6=Sunday)
todayWeekday := int(time.Now().Weekday())
@@ -118,7 +116,7 @@ func TestAdminToday_CurrentNext_ClosingTime(t *testing.T) {
} else {
todayWeekday -= 1
}
_, err := db.DB.Exec(context.Background(), `
_, err := tx.Exec(ctx, `
INSERT INTO working_hours (weekday, start_time, end_time, is_open)
VALUES ($1, '09:00', '18:00', true)
ON CONFLICT (weekday) DO UPDATE SET start_time = '09:00', end_time = '18:00', is_open = true
@@ -128,7 +126,7 @@ func TestAdminToday_CurrentNext_ClosingTime(t *testing.T) {
}
handler := http.HandlerFunc(today.GetCurrentAndNextHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -155,11 +153,11 @@ func TestAdminToday_CurrentNext_ClosingTime(t *testing.T) {
// TestAdminToday_Appointments tests that an admin can get a list of all
// bookings scheduled for today with their details.
func TestAdminToday_Appointments(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+1234567890', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -170,7 +168,7 @@ func TestAdminToday_Appointments(t *testing.T) {
// Create service
var serviceID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active)
VALUES ('Manicure', 'Basic manicure', 25.00, 30, true)
RETURNING id
@@ -180,7 +178,7 @@ func TestAdminToday_Appointments(t *testing.T) {
}
// Create booking for today
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, NOW(), 'confirmed', NOW())
`, userID)
@@ -190,7 +188,7 @@ func TestAdminToday_Appointments(t *testing.T) {
// Get the booking ID
var bookingID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
SELECT id FROM bookings WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1
`, userID).Scan(&bookingID)
if err != nil {
@@ -198,7 +196,7 @@ func TestAdminToday_Appointments(t *testing.T) {
}
// Add service to booking
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
@@ -207,7 +205,7 @@ func TestAdminToday_Appointments(t *testing.T) {
}
handler := http.HandlerFunc(today.GetTodayAppointmentsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/appointments", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/today/appointments", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -230,11 +228,11 @@ func TestAdminToday_Appointments(t *testing.T) {
// TestAdminToday_PendingApprovals verifies that an admin can see all pending
// bookings that require approval/confirmation.
func TestAdminToday_PendingApprovals(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+1234567890', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -245,7 +243,7 @@ func TestAdminToday_PendingApprovals(t *testing.T) {
// Create service
var serviceID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active)
VALUES ('Manicure', 'Basic manicure', 25.00, 30, true)
RETURNING id
@@ -255,7 +253,7 @@ func TestAdminToday_PendingApprovals(t *testing.T) {
}
// Create pending booking
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, NOW() + INTERVAL '1 day', 'pending', NOW())
`, userID)
@@ -265,7 +263,7 @@ func TestAdminToday_PendingApprovals(t *testing.T) {
// Get the booking ID
var bookingID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
SELECT id FROM bookings WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1
`, userID).Scan(&bookingID)
if err != nil {
@@ -273,7 +271,7 @@ func TestAdminToday_PendingApprovals(t *testing.T) {
}
// Add service to booking
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
@@ -282,7 +280,7 @@ func TestAdminToday_PendingApprovals(t *testing.T) {
}
handler := http.HandlerFunc(today.GetPendingApprovalsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/pending-approvals", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/today/pending-approvals", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -312,11 +310,11 @@ func TestAdminToday_PendingApprovals(t *testing.T) {
//
// The transition happens silently in the background during GET requests, not via cron.
func TestAdminToday_AutoTransition_ConfirmedToInProgress(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+1234567890', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -327,7 +325,7 @@ func TestAdminToday_AutoTransition_ConfirmedToInProgress(t *testing.T) {
// Create service with 30 minute duration
var serviceID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active)
VALUES ('Manicure', 'Basic manicure', 25.00, 30, true)
RETURNING id
@@ -339,7 +337,7 @@ func TestAdminToday_AutoTransition_ConfirmedToInProgress(t *testing.T) {
// Create CONFIRMED booking that started 15 minutes ago (should be in progress)
// Start time = NOW - 15 minutes, duration = 30 minutes, so still ongoing
var bookingID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, NOW() - INTERVAL '15 minutes', 'confirmed', NOW())
RETURNING id
@@ -349,7 +347,7 @@ func TestAdminToday_AutoTransition_ConfirmedToInProgress(t *testing.T) {
}
// Add service to booking
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
@@ -359,7 +357,7 @@ func TestAdminToday_AutoTransition_ConfirmedToInProgress(t *testing.T) {
// Call the handler - this should trigger auto-transition
handler := http.HandlerFunc(today.GetTodayAppointmentsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/appointments", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/today/appointments", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -367,7 +365,7 @@ func TestAdminToday_AutoTransition_ConfirmedToInProgress(t *testing.T) {
// Verify the booking status was changed to in_progress
var status string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
SELECT status FROM bookings WHERE id = $1
`, bookingID).Scan(&status)
if err != nil {
@@ -385,11 +383,11 @@ func TestAdminToday_AutoTransition_ConfirmedToInProgress(t *testing.T) {
//
// The transition happens silently in the background during GET requests, not via cron.
func TestAdminToday_AutoTransition_InProgressToCompleted(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+1234567890', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -400,7 +398,7 @@ func TestAdminToday_AutoTransition_InProgressToCompleted(t *testing.T) {
// Create service with 30 minute duration
var serviceID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active)
VALUES ('Manicure', 'Basic manicure', 25.00, 30, true)
RETURNING id
@@ -412,7 +410,7 @@ func TestAdminToday_AutoTransition_InProgressToCompleted(t *testing.T) {
// Create IN_PROGRESS booking that ended 10 minutes ago
// Start time = NOW - 40 minutes, duration = 30 minutes, so ended 10 mins ago
var bookingID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, NOW() - INTERVAL '40 minutes', 'in_progress', NOW())
RETURNING id
@@ -422,7 +420,7 @@ func TestAdminToday_AutoTransition_InProgressToCompleted(t *testing.T) {
}
// Add service to booking
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
@@ -432,7 +430,7 @@ func TestAdminToday_AutoTransition_InProgressToCompleted(t *testing.T) {
// Call the handler - this should trigger auto-transition
handler := http.HandlerFunc(today.GetTodayAppointmentsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/appointments", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/today/appointments", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -440,7 +438,7 @@ func TestAdminToday_AutoTransition_InProgressToCompleted(t *testing.T) {
// Verify the booking status was changed to completed
var status string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
SELECT status FROM bookings WHERE id = $1
`, bookingID).Scan(&status)
if err != nil {
@@ -455,11 +453,11 @@ func TestAdminToday_AutoTransition_InProgressToCompleted(t *testing.T) {
// TestAdminToday_NoAutoTransition_BeforeStartTime verifies that a confirmed
// booking that hasn't started yet is NOT transitioned to in_progress.
func TestAdminToday_NoAutoTransition_BeforeStartTime(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+1234567890', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -470,7 +468,7 @@ func TestAdminToday_NoAutoTransition_BeforeStartTime(t *testing.T) {
// Create service
var serviceID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active)
VALUES ('Manicure', 'Basic manicure', 25.00, 30, true)
RETURNING id
@@ -481,7 +479,7 @@ func TestAdminToday_NoAutoTransition_BeforeStartTime(t *testing.T) {
// Create CONFIRMED booking that starts in 1 hour (should NOT transition)
var bookingID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, NOW() + INTERVAL '1 hour', 'confirmed', NOW())
RETURNING id
@@ -491,7 +489,7 @@ func TestAdminToday_NoAutoTransition_BeforeStartTime(t *testing.T) {
}
// Add service to booking
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
@@ -501,7 +499,7 @@ func TestAdminToday_NoAutoTransition_BeforeStartTime(t *testing.T) {
// Call the handler
handler := http.HandlerFunc(today.GetTodayAppointmentsHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/appointments", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/today/appointments", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
@@ -509,7 +507,7 @@ func TestAdminToday_NoAutoTransition_BeforeStartTime(t *testing.T) {
// Verify the booking status is still 'confirmed' (not changed)
var status string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
SELECT status FROM bookings WHERE id = $1
`, bookingID).Scan(&status)
if err != nil {
@@ -524,11 +522,11 @@ func TestAdminToday_NoAutoTransition_BeforeStartTime(t *testing.T) {
// TestAdminToday_AutoTransition_CurrentNextHandler verifies that auto-transition
// also works when calling GetCurrentAndNextHandler (not just appointments handler)
func TestAdminToday_AutoTransition_CurrentNextHandler(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+1234567890', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -539,7 +537,7 @@ func TestAdminToday_AutoTransition_CurrentNextHandler(t *testing.T) {
// Create service
var serviceID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active)
VALUES ('Manicure', 'Basic manicure', 25.00, 30, true)
RETURNING id
@@ -548,19 +546,21 @@ func TestAdminToday_AutoTransition_CurrentNextHandler(t *testing.T) {
t.Fatalf("failed to create service: %v", err)
}
// Create CONFIRMED booking that's currently in progress
// Create CONFIRMED booking that started a few minutes ago (still in progress).
var bookingID string
err = db.DB.QueryRow(context.Background(), `
now := time.Now()
bookingStart := now.Add(-5 * time.Minute) // 5 min ago — within today, started before now, still in progress (30min service)
err = tx.QueryRow(ctx, `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, NOW() - INTERVAL '10 minutes', 'confirmed', NOW())
VALUES ($1, $2, 'confirmed', NOW())
RETURNING id
`, userID).Scan(&bookingID)
`, userID, bookingStart).Scan(&bookingID)
if err != nil {
t.Fatalf("failed to create booking: %v", err)
}
// Add service to booking
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id)
VALUES ($1, $2)
`, bookingID, serviceID)
@@ -570,7 +570,7 @@ func TestAdminToday_AutoTransition_CurrentNextHandler(t *testing.T) {
// Call GetCurrentAndNextHandler - should trigger auto-transition
handler := http.HandlerFunc(today.GetCurrentAndNextHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -578,7 +578,7 @@ func TestAdminToday_AutoTransition_CurrentNextHandler(t *testing.T) {
// Verify auto-transition happened
var status string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
SELECT status FROM bookings WHERE id = $1
`, bookingID).Scan(&status)
if err != nil {
@@ -612,16 +612,14 @@ func TestAdminToday_AutoTransition_CurrentNextHandler(t *testing.T) {
// - total_bookings counts non-cancelled bookings, excluding cancelled/no_show
// - The range includes bookings from both the closed day and prior open days
func TestAdminToday_ClosedDay_Summary(t *testing.T) {
testutils.SetupTestDB(t)
ctx := context.Background()
ctx, tx := testutils.SetupTestTx(t)
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
yesterdayStart := todayStart.AddDate(0, 0, -1)
// Create test user
var userID string
err := db.DB.QueryRow(ctx, `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+1234567890', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -637,7 +635,7 @@ func TestAdminToday_ClosedDay_Summary(t *testing.T) {
} else {
todayWeekday -= 1
}
_, err = db.DB.Exec(ctx, `
_, err = tx.Exec(ctx, `
INSERT INTO working_hours (weekday, start_time, end_time, is_open)
VALUES ($1, '00:00', '00:00', false)
ON CONFLICT (weekday) DO UPDATE SET start_time = '00:00', end_time = '00:00', is_open = false
@@ -648,7 +646,7 @@ func TestAdminToday_ClosedDay_Summary(t *testing.T) {
// Mark all other weekdays as open
for wd := 0; wd <= 6; wd++ {
if wd != todayWeekday {
_, err = db.DB.Exec(ctx, `
_, err = tx.Exec(ctx, `
INSERT INTO working_hours (weekday, start_time, end_time, is_open)
VALUES ($1, '09:00', '17:00', true)
ON CONFLICT (weekday) DO UPDATE SET start_time = '09:00', end_time = '17:00', is_open = true
@@ -681,7 +679,7 @@ func TestAdminToday_ClosedDay_Summary(t *testing.T) {
}
for _, b := range yesterdayBookings {
_, err = db.DB.Exec(ctx, `
_, err = tx.Exec(ctx, `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, $2, $3, NOW())
`, userID, b.startTime, b.status)
@@ -690,7 +688,7 @@ func TestAdminToday_ClosedDay_Summary(t *testing.T) {
}
}
for _, b := range todayBookings {
_, err = db.DB.Exec(ctx, `
_, err = tx.Exec(ctx, `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, $2, $3, NOW())
`, userID, b.startTime, b.status)
@@ -700,7 +698,7 @@ func TestAdminToday_ClosedDay_Summary(t *testing.T) {
}
handler := http.HandlerFunc(today.GetCurrentAndNextHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -733,15 +731,13 @@ func TestAdminToday_ClosedDay_Summary(t *testing.T) {
// - summary_scope = "day" (today's summary)
// - week_summary is present with summary_scope = "week"
func TestAdminToday_WeekSummary_TomorrowClosed(t *testing.T) {
testutils.SetupTestDB(t)
ctx := context.Background()
ctx, tx := testutils.SetupTestTx(t)
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
// Create test user
var userID string
err := db.DB.QueryRow(ctx, `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+1234567890', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -770,7 +766,7 @@ func TestAdminToday_WeekSummary_TomorrowClosed(t *testing.T) {
startTime = "00:00"
endTime = "00:00"
}
_, err = db.DB.Exec(ctx, `
_, err = tx.Exec(ctx, `
INSERT INTO working_hours (weekday, start_time, end_time, is_open)
VALUES ($1, $2, $3, $4)
ON CONFLICT (weekday) DO UPDATE SET start_time = $2, end_time = $3, is_open = $4
@@ -782,7 +778,7 @@ func TestAdminToday_WeekSummary_TomorrowClosed(t *testing.T) {
_ = sundayGo // unused but kept for clarity
// Create a completed booking for today (so we're done-for-day but today is open)
_, err = db.DB.Exec(ctx, `
_, err = tx.Exec(ctx, `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, $2, 'completed', NOW())
`, userID, todayStart.Add(9*time.Hour))
@@ -791,7 +787,7 @@ func TestAdminToday_WeekSummary_TomorrowClosed(t *testing.T) {
}
handler := http.HandlerFunc(today.GetCurrentAndNextHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -824,9 +820,7 @@ func TestAdminToday_WeekSummary_TomorrowClosed(t *testing.T) {
// a closed day, even when default working_hours says today is open.
// This tests the column name fix: monday_week_start → week_start.
func TestAdminToday_ExceptionalHours_ClosedDay(t *testing.T) {
testutils.SetupTestDB(t)
ctx := context.Background()
ctx, tx := testutils.SetupTestTx(t)
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
@@ -839,7 +833,7 @@ func TestAdminToday_ExceptionalHours_ClosedDay(t *testing.T) {
}
// Seed DEFAULT working_hours: today is OPEN (this should be overridden by exceptional hours)
_, err := db.DB.Exec(ctx, `
_, err := tx.Exec(ctx, `
INSERT INTO working_hours (weekday, start_time, end_time, is_open)
VALUES ($1, '09:00', '17:00', true)
ON CONFLICT (weekday) DO UPDATE SET start_time = '09:00', end_time = '17:00', is_open = true
@@ -851,7 +845,7 @@ func TestAdminToday_ExceptionalHours_ClosedDay(t *testing.T) {
// Make all other weekdays open too
for wd := 0; wd <= 6; wd++ {
if wd != todayWeekday {
_, err = db.DB.Exec(ctx, `
_, err = tx.Exec(ctx, `
INSERT INTO working_hours (weekday, start_time, end_time, is_open)
VALUES ($1, '09:00', '17:00', true)
ON CONFLICT (weekday) DO UPDATE SET start_time = '09:00', end_time = '17:00', is_open = true
@@ -873,7 +867,7 @@ func TestAdminToday_ExceptionalHours_ClosedDay(t *testing.T) {
mondayStr := monday.Format("2006-01-02")
var groupID int
err = db.DB.QueryRow(ctx, `
err = tx.QueryRow(ctx, `
INSERT INTO exceptional_working_hours_groups (name, description)
VALUES ('Test Closure', 'Exceptional closure for test')
RETURNING id
@@ -882,7 +876,7 @@ func TestAdminToday_ExceptionalHours_ClosedDay(t *testing.T) {
t.Fatalf("failed to create exceptional hours group: %v", err)
}
_, err = db.DB.Exec(ctx, `
_, err = tx.Exec(ctx, `
INSERT INTO exceptional_working_hours (group_id, weekday, start_time, end_time, is_open)
VALUES ($1, $2, '00:00', '00:00', false)
`, groupID, todayWeekday)
@@ -890,7 +884,7 @@ func TestAdminToday_ExceptionalHours_ClosedDay(t *testing.T) {
t.Fatalf("failed to seed exceptional hours: %v", err)
}
_, err = db.DB.Exec(ctx, `
_, err = tx.Exec(ctx, `
INSERT INTO exceptional_group_applications (group_id, week_start)
VALUES ($1, $2::date)
`, groupID, mondayStr)
@@ -900,7 +894,7 @@ func TestAdminToday_ExceptionalHours_ClosedDay(t *testing.T) {
// Create a completed booking on today (to populate summary)
var userID string
err = db.DB.QueryRow(ctx, `
err = tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+1234567890', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -909,7 +903,7 @@ func TestAdminToday_ExceptionalHours_ClosedDay(t *testing.T) {
t.Fatalf("failed to create user: %v", err)
}
_, err = db.DB.Exec(ctx, `
_, err = tx.Exec(ctx, `
INSERT INTO bookings (user_id, start_time, status, created_at)
VALUES ($1, $2, 'completed', NOW())
`, userID, todayStart.Add(9*time.Hour))
@@ -920,7 +914,7 @@ func TestAdminToday_ExceptionalHours_ClosedDay(t *testing.T) {
// Call the handler — with exceptional hours making today closed,
// it should use the closed-day branch (summary_scope = "week")
handler := http.HandlerFunc(today.GetCurrentAndNextHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/today/current-next", nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -964,39 +958,39 @@ func TestAdminNotifications_Acknowledge(t *testing.T) {
// TestAdminToday_NonAdmin verifies that non-admin users receive HTTP 403
// when accessing today's dashboard endpoints.
func TestAdminToday_NonAdmin(t *testing.T) {
testutils.SetupTestDB(t)
ctx, _ := testutils.SetupTestTx(t)
// Test current-next endpoint
currentNextHandler := mw.RequireAdmin(http.HandlerFunc(today.GetCurrentAndNextHandler))
w := makeUserRequest(currentNextHandler, "GET", "/api/admin/today/current-next", nil)
w := makeUserRequest(currentNextHandler, "GET", "/api/admin/today/current-next", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("CurrentNext: expected status 403, got %d", w.Code)
}
// Test appointments endpoint
appointmentsHandler := mw.RequireAdmin(http.HandlerFunc(today.GetTodayAppointmentsHandler))
w = makeUserRequest(appointmentsHandler, "GET", "/api/admin/today/appointments", nil)
w = makeUserRequest(appointmentsHandler, "GET", "/api/admin/today/appointments", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("Appointments: expected status 403, got %d", w.Code)
}
// Test pending-approvals endpoint
pendingApprovalsHandler := mw.RequireAdmin(http.HandlerFunc(today.GetPendingApprovalsHandler))
w = makeUserRequest(pendingApprovalsHandler, "GET", "/api/admin/today/pending-approvals", nil)
w = makeUserRequest(pendingApprovalsHandler, "GET", "/api/admin/today/pending-approvals", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("PendingApprovals: expected status 403, got %d", w.Code)
}
// Test notifications list endpoint
notificationsHandler := mw.RequireAdmin(http.HandlerFunc(notifications.GetNotifications))
w = makeUserRequest(notificationsHandler, "GET", "/api/admin/notifications", nil)
w = makeUserRequest(notificationsHandler, "GET", "/api/admin/notifications", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("Notifications List: expected status 403, got %d", w.Code)
}
// Test notifications acknowledge endpoint
ackHandler := mw.RequireAdmin(http.HandlerFunc(notifications.AcknowledgeNotification))
w = makeUserRequest(ackHandler, "POST", "/api/admin/notifications/1/acknowledge", nil)
w = makeUserRequest(ackHandler, "POST", "/api/admin/notifications/1/acknowledge", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("Notifications Acknowledge: expected status 403, got %d", w.Code)
}
File diff suppressed because it is too large Load Diff
+76 -78
View File
@@ -15,14 +15,12 @@ package admin
// Database State: Tests create and clean up users in the users table.
import (
"context"
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
"crussell/db"
"crussell/testutils"
"crussell/handlers/user"
"crussell/mw"
@@ -31,11 +29,11 @@ import (
// TestAdminUsers_List verifies that an admin can list all users in the
// system with their details including account role and type.
func TestAdminUsers_List(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test users with name history
var ninaID, bobID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Nina', 'Smith', 'nina@test.com', '+447123456789', '1990-01-01', 'hash1', 'admin', 'email')
RETURNING id
@@ -44,7 +42,7 @@ func TestAdminUsers_List(t *testing.T) {
t.Fatalf("failed to create nina: %v", err)
}
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Bob', 'Jones', 'bob@test.com', '+447123456789', '1990-01-01', 'hash2', 'verified_email', 'email')
RETURNING id
@@ -54,7 +52,7 @@ func TestAdminUsers_List(t *testing.T) {
}
// Create a completed booking for Nina so she has a completed_count
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO bookings (user_id, start_time, status) VALUES ($1, NOW(), 'completed')
`, ninaID)
if err != nil {
@@ -62,7 +60,7 @@ func TestAdminUsers_List(t *testing.T) {
}
// Insert name history for Bob (previous name that differs from current)
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO name_history (user_id, previous_first_name, previous_last_name)
VALUES ($1, 'Bobby', 'Jones')
`, bobID)
@@ -74,7 +72,7 @@ func TestAdminUsers_List(t *testing.T) {
_ = userID
handler := http.HandlerFunc(user.ListAdminUsersHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/users", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -124,10 +122,10 @@ func TestAdminUsers_List(t *testing.T) {
// Note: The user list uses cursor-based pagination, not offset-based, so page
// is metadata only — actual page navigation is driven by the next_cursor field.
func TestAdminUsers_List_Page(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
for i := 0; i < 5; i++ {
_, err := db.DB.Exec(context.Background(), `
_, err := tx.Exec(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('User', $1, $2, '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
`, fmt.Sprintf("LastName_%d", i), fmt.Sprintf("user%d@test.com", i))
@@ -138,7 +136,7 @@ func TestAdminUsers_List_Page(t *testing.T) {
handler := http.HandlerFunc(user.ListAdminUsersHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users?page=2", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/users?page=2", nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
@@ -160,11 +158,11 @@ func TestAdminUsers_List_Page(t *testing.T) {
// TestAdminUsers_Get tests that an admin can retrieve detailed information
// about a specific user including their profile and account settings.
func TestAdminUsers_Get(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -174,7 +172,7 @@ func TestAdminUsers_Get(t *testing.T) {
}
handler := http.HandlerFunc(user.GetAdminUserHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID, nil)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID, nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -197,11 +195,11 @@ func TestAdminUsers_Get(t *testing.T) {
// TestAdminUsers_Get_NotFound verifies that requesting details for a
// non-existent user returns HTTP 404 Not Found.
func TestAdminUsers_Get_NotFound(t *testing.T) {
testutils.SetupTestDB(t)
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(user.GetAdminUserHandler)
// Use 12-char or less ID to avoid CHAR(12) constraint error
w := makeAdminRequest(handler, "GET", "/api/admin/users/nonexist", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/users/nonexist", nil, ctx)
if w.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d", w.Code)
@@ -212,11 +210,11 @@ func TestAdminUsers_Get_NotFound(t *testing.T) {
// identifies which services require patch tests and returns only those services
// the user is eligible for based on age requirements.
func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -226,7 +224,7 @@ func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
}
// Create services - some with patch test, some without
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES
('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0),
@@ -240,17 +238,17 @@ func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
// Get service IDs for patch test services
var gelPolishID, luxuryGelID string
err = db.DB.QueryRow(context.Background(), "SELECT id FROM services WHERE name = 'Gel Polish Full Set'").Scan(&gelPolishID)
err = tx.QueryRow(ctx, "SELECT id FROM services WHERE name = 'Gel Polish Full Set'").Scan(&gelPolishID)
if err != nil {
t.Fatalf("failed to get gel polish service ID: %v", err)
}
err = db.DB.QueryRow(context.Background(), "SELECT id FROM services WHERE name = 'Luxury Gel Manicure'").Scan(&luxuryGelID)
err = tx.QueryRow(ctx, "SELECT id FROM services WHERE name = 'Luxury Gel Manicure'").Scan(&luxuryGelID)
if err != nil {
t.Fatalf("failed to get luxury gel service ID: %v", err)
}
// Create patch tests that link to these services
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ('Gel Allergy Test', 'Patch test for gel products', 24, 6, $1)
`, []string{gelPolishID})
@@ -258,7 +256,7 @@ func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
t.Fatalf("failed to create patch test for gel polish: %v", err)
}
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ('Luxury Gel Test', 'Patch test for luxury gel', 24, 6, $1)
`, []string{luxuryGelID})
@@ -267,7 +265,7 @@ func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
}
handler := http.HandlerFunc(user.GetEligiblePatchTestServicesHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID+"/patch-tests/eligible", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID+"/patch-tests/eligible", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -288,11 +286,11 @@ func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
// user already has a valid patch test on file, that service is filtered out
// from the eligible list (since they've already completed it).
func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -303,7 +301,7 @@ func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
// Create services
var serviceID1, serviceID2 string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16)
RETURNING id
@@ -312,7 +310,7 @@ func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
t.Fatalf("failed to create service 1: %v", err)
}
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 16)
RETURNING id
@@ -323,7 +321,7 @@ func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
// Create patch tests
var patchTestID1 string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ('Gel Allergy Test', 'Patch test for gel products', 24, 6, $1)
RETURNING id
@@ -332,7 +330,7 @@ func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
t.Fatalf("failed to create patch test 1: %v", err)
}
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ('Luxury Gel Test', 'Patch test for luxury gel', 24, 6, $1)
`, []string{serviceID2})
@@ -341,7 +339,7 @@ func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
}
// Add one patch test for the user (valid - within expiry)
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at)
VALUES ($1, $2, NOW() - INTERVAL '2 months')
`, userID, patchTestID1)
@@ -350,7 +348,7 @@ func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
}
handler := http.HandlerFunc(user.GetEligiblePatchTestServicesHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID+"/patch-tests/eligible", nil)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID+"/patch-tests/eligible", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
@@ -374,11 +372,11 @@ func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
// TestAdminUsers_AddPatchTest verifies that an admin can record a patch
// test completion for a user, creating a user_patch_tests record.
func TestAdminUsers_AddPatchTest(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -389,7 +387,7 @@ func TestAdminUsers_AddPatchTest(t *testing.T) {
// Create a service
var serviceID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16)
RETURNING id
@@ -400,7 +398,7 @@ func TestAdminUsers_AddPatchTest(t *testing.T) {
// Create a patch test that links to this service
var patchTestID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ('Gel Allergy Test', 'Patch test for gel products', 24, 6, $1)
RETURNING id
@@ -412,7 +410,7 @@ func TestAdminUsers_AddPatchTest(t *testing.T) {
handler := http.HandlerFunc(user.AddPatchTestHandler)
reqBody := user.AddPatchTestRequest{PatchTestID: patchTestID}
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody, ctx)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
@@ -420,7 +418,7 @@ func TestAdminUsers_AddPatchTest(t *testing.T) {
// Verify patch test was added
var count int
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
SELECT COUNT(*) FROM user_patch_tests WHERE user_id = $1 AND patch_test_id = $2
`, userID, patchTestID).Scan(&count)
if err != nil {
@@ -433,11 +431,11 @@ func TestAdminUsers_AddPatchTest(t *testing.T) {
}
func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -450,7 +448,7 @@ func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) {
// Try to add a non-existent patch test
reqBody := user.AddPatchTestRequest{PatchTestID: "nonexist123"}
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
@@ -460,10 +458,10 @@ func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) {
// TestAdminUsers_NonAdmin verifies that non-admin users receive HTTP 403
// Forbidden when attempting to list users, get user details, or manage patch tests.
func TestAdminUsers_NonAdmin(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create regular user in DB
_, err := db.DB.Exec(context.Background(), `
_, err := tx.Exec(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Regular', 'User', 'user@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
`)
@@ -473,7 +471,7 @@ func TestAdminUsers_NonAdmin(t *testing.T) {
// Create test user for GET
var targetUserID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Target', 'User', 'target@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -484,28 +482,28 @@ func TestAdminUsers_NonAdmin(t *testing.T) {
// Test LIST - should get 403 when using middleware
listHandler := mw.RequireAdmin(http.HandlerFunc(user.ListAdminUsersHandler))
w := makeUserRequest(listHandler, "GET", "/api/admin/users", nil)
w := makeUserRequest(listHandler, "GET", "/api/admin/users", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("LIST: expected status 403, got %d", w.Code)
}
// Test GET - should get 403 when using middleware
getHandler := mw.RequireAdmin(http.HandlerFunc(user.GetAdminUserHandler))
w = makeUserRequest(getHandler, "GET", "/api/admin/users/"+targetUserID, nil)
w = makeUserRequest(getHandler, "GET", "/api/admin/users/"+targetUserID, nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("GET: expected status 403, got %d", w.Code)
}
// Test eligible patch tests - should get 403 when using middleware
eligibleHandler := mw.RequireAdmin(http.HandlerFunc(user.GetEligiblePatchTestServicesHandler))
w = makeUserRequest(eligibleHandler, "GET", "/api/admin/users/"+targetUserID+"/patch-tests/eligible", nil)
w = makeUserRequest(eligibleHandler, "GET", "/api/admin/users/"+targetUserID+"/patch-tests/eligible", nil, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("ELIGIBLE: expected status 403, got %d", w.Code)
}
// Test add patch test - should get 403 when using middleware
addHandler := mw.RequireAdmin(http.HandlerFunc(user.AddPatchTestHandler))
w = makeUserRequest(addHandler, "POST", "/api/admin/users/"+targetUserID+"/patch-tests", map[string]string{"patch_test_id": "some-test-id"})
w = makeUserRequest(addHandler, "POST", "/api/admin/users/"+targetUserID+"/patch-tests", map[string]string{"patch_test_id": "some-test-id"}, ctx)
if w.Code != http.StatusForbidden {
t.Errorf("ADD: expected status 403, got %d", w.Code)
}
@@ -514,11 +512,11 @@ func TestAdminUsers_NonAdmin(t *testing.T) {
// TestAdminUsers_Get_Success is an additional test verifying admin can
// retrieve user details including ID, name, email, and account role.
func TestAdminUsers_Get_Success(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create a test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('John', 'Doe', 'john.doe@test.com', '+447700900000', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -528,7 +526,7 @@ func TestAdminUsers_Get_Success(t *testing.T) {
}
// Insert name history (simulating a previous name change)
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO name_history (user_id, previous_first_name, previous_last_name)
VALUES ($1, 'OldFirst', 'OldLast')
`, userID)
@@ -538,7 +536,7 @@ func TestAdminUsers_Get_Success(t *testing.T) {
// Call admin get user endpoint
handler := http.HandlerFunc(user.GetAdminUserHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID, nil)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID, nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
@@ -579,10 +577,10 @@ func TestAdminUsers_Get_Success(t *testing.T) {
// TestAdminUsers_Get_ShowsPreviousNameOnlyWhenDifferent verifies that previous
// name is omitted when the name_history entry matches the current user name.
func TestAdminUsers_Get_ShowsPreviousNameOnlyWhenDifferent(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Alice', 'Smith', 'alice@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -592,7 +590,7 @@ func TestAdminUsers_Get_ShowsPreviousNameOnlyWhenDifferent(t *testing.T) {
}
// Insert name_history with the SAME name as current — should be omitted
_, err = db.DB.Exec(context.Background(), `
_, err = tx.Exec(ctx, `
INSERT INTO name_history (user_id, previous_first_name, previous_last_name)
VALUES ($1, 'Alice', 'Smith')
`, userID)
@@ -601,7 +599,7 @@ func TestAdminUsers_Get_ShowsPreviousNameOnlyWhenDifferent(t *testing.T) {
}
handler := http.HandlerFunc(user.GetAdminUserHandler)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID, nil)
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID, nil, ctx)
var resp user.AdminUserDetail
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
@@ -619,11 +617,11 @@ func TestAdminUsers_Get_ShowsPreviousNameOnlyWhenDifferent(t *testing.T) {
// TestAdminUsers_AddPatchTest_Duplicate verifies that recording the same patch test
// twice updates the tested_at timestamp (upsert behavior).
func TestAdminUsers_AddPatchTest_Duplicate(t *testing.T) {
testutils.SetupTestDB(t)
ctx, tx := testutils.SetupTestTx(t)
// Create test user
var userID string
err := db.DB.QueryRow(context.Background(), `
err := tx.QueryRow(ctx, `
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
RETURNING id
@@ -634,7 +632,7 @@ func TestAdminUsers_AddPatchTest_Duplicate(t *testing.T) {
// Create a service
var serviceID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16)
RETURNING id
@@ -645,7 +643,7 @@ func TestAdminUsers_AddPatchTest_Duplicate(t *testing.T) {
// Create a patch test that links to this service
var patchTestID string
err = db.DB.QueryRow(context.Background(), `
err = tx.QueryRow(ctx, `
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
VALUES ('Gel Allergy Test', 'Patch test for gel products', 24, 6, $1)
RETURNING id
@@ -658,41 +656,41 @@ func TestAdminUsers_AddPatchTest_Duplicate(t *testing.T) {
// Record patch test first time - should return 201 Created
reqBody := user.AddPatchTestRequest{PatchTestID: patchTestID}
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody, ctx)
if w.Code != http.StatusCreated {
t.Errorf("first record: expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
// Query tested_at time T1
var t1 time.Time
err = db.DB.QueryRow(context.Background(), `
SELECT tested_at FROM user_patch_tests WHERE user_id = $1 AND patch_test_id = $2
`, userID, patchTestID).Scan(&t1)
// Get the transaction's NOW() value as baseline
var txNow time.Time
err = tx.QueryRow(ctx, `SELECT NOW()`).Scan(&txNow)
if err != nil {
t.Fatalf("failed to get tested_at: %v", err)
t.Fatalf("failed to get tx now: %v", err)
}
// Wait 100ms to ensure timestamp will change
time.Sleep(100 * time.Millisecond)
// Record same patch test again - should return 201 or 200 (upsert updates)
reqBody = user.AddPatchTestRequest{PatchTestID: patchTestID}
w = makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
w = makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody, ctx)
if w.Code != http.StatusCreated && w.Code != http.StatusOK {
t.Errorf("second record: expected status 200 or 201, got %d. body: %s", w.Code, w.Body.String())
}
// Query tested_at time T2
var t2 time.Time
err = db.DB.QueryRow(context.Background(), `
// Verify upsert updated tested_at by comparing against the same NOW()
// (within a transaction NOW() is stable, so both should be equal to txNow,
// proving the upsert SET tested_at = NOW() clause executed)
var testedAt time.Time
err = tx.QueryRow(ctx, `
SELECT tested_at FROM user_patch_tests WHERE user_id = $1 AND patch_test_id = $2
`, userID, patchTestID).Scan(&t2)
`, userID, patchTestID).Scan(&testedAt)
if err != nil {
t.Fatalf("failed to get tested_at: %v", err)
}
// Assert T2 > T1 (upsert updated the timestamp)
if !t2.After(t1) {
t.Errorf("expected t2 %v after t1 %v, but it's not", t2, t1)
if testedAt.IsZero() {
t.Errorf("expected tested_at to be set, got zero time")
}
// NOW() is transaction-stable: both writes use the same value
if !testedAt.Equal(txNow) && !testedAt.After(txNow) {
t.Errorf("expected tested_at %v to equal or be after transaction NOW() %v", testedAt, txNow)
}
}