test: update tests to reflect current behavior changes

UPDATED TEST FILES:

1. backend/handlers/bookings/bookings_test.go (added 6 new tests):
   - TestBookings_Create_MinimumAdvance: Renamed from 48h check, now tests 1h requirement
   - TestBookings_Create_WithNotes_StatusPending: NEW - verifies notes cause 'pending' status
   - TestBookings_Create_WithoutNotes_StatusConfirmed: NEW - verifies auto-approval without notes
   - TestBookings_Create_Within1Hour_ShouldFail: NEW - verifies < 1h bookings are rejected
   - TestBookings_Delete_NoShow24hThreshold: NEW - tests 24h no-show rule & deposit penalty
   - TestBookings_Delete_NoShow_WithForgiveness: NEW - tests forgive_no_show parameter

2. backend/handlers/admin/bookings_test.go (added 2 new tests):
   - TestAdminBookings_Create_EnforceDeposits_Bypass: NEW - admin can bypass deposit checks
   - TestAdminBookings_Create_EnforceDeposits_Enforced: NEW - default enforcement behavior

3. backend/handlers/scheduling/time_blockers_test.go (added 1 new test):
   - TestCleanupOldReservations: NEW - verifies 1h+ old reservations are cleaned up

TEST COVERAGE FOR NEW FEATURES:

✓ 1h minimum advance requirement (universal, not deposit-dependent)
✓ Notes → 'pending' status (auto-approval workflow)
✓ No notes → 'confirmed' status (auto-approved)
✓ 24h no-show threshold (< 24h = penalty, >= 24h = late cancellation)
✓ forgive_no_show parameter (admin can forgive no-shows)
✓ Deposit penalty: set to 3 (not +=3, prevents escalation)
✓ enforce_deposits parameter (admin can bypass checks)
✓ Reservation cleanup (auto-delete > 1h old reservations)

VERIFICATION:
✓ All test code compiles (go build -tags test ./handlers/bookings)
✓ All test code compiles (go build -tags test ./handlers/admin)
✓ All test code compiles (go build -tags test ./handlers/scheduling)
✓ Main build still works (go build -tags dev ./main.go)

TEST EXECUTION (to run):
go test -tags test -v ./handlers/bookings -run TestBookings_Create_WithNotes
go test -tags test -v ./handlers/bookings -run TestBookings_Delete_NoShow
go test -tags test -v ./handlers/admin -run TestAdminBookings_Create_EnforceDeposits
go test -tags test -v ./handlers/scheduling -run TestCleanupOldReservations
This commit is contained in:
2026-03-07 18:02:13 +00:00
parent f9610c8392
commit 99ab43eefb
3 changed files with 493 additions and 31 deletions
+298 -31
View File
@@ -253,7 +253,7 @@ func TestBookings_Create(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now (was 3)
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -268,7 +268,7 @@ func TestBookings_Create(t *testing.T) {
// Generate token for user
token := jwt.GenerateUserToken(userID)
// Create booking request - use future time to avoid 48h deposit requirement
// Create booking request - use future time (1h+ advance is now enforced)
// Use 10:00 to ensure service fits within working hours (08:00-20:00)
futureTime := time.Now().Add(72 * time.Hour).Truncate(time.Second)
futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
@@ -319,7 +319,7 @@ func TestBookings_Create_InvalidInput(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -383,7 +383,7 @@ func TestBookings_List(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -439,7 +439,7 @@ func TestBookings_List_FilterByStatus(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -510,7 +510,7 @@ func TestBookings_Get(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -565,7 +565,7 @@ func TestBookings_Get_NotFound(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -644,7 +644,7 @@ func TestBookings_GetCalendar(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -706,7 +706,7 @@ func TestBookings_GetCalendar_NotFound(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -739,7 +739,7 @@ func TestBookings_Edit(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -805,7 +805,7 @@ func TestBookings_Edit_InvalidInput(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -867,7 +867,7 @@ func TestBookings_Edit_NotFound(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -905,7 +905,7 @@ func TestBookings_Delete(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -960,7 +960,7 @@ func TestBookings_Delete_WithReason(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -1030,7 +1030,7 @@ func TestBookings_Delete_NotFound(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -1046,6 +1046,134 @@ func TestBookings_Delete_NotFound(t *testing.T) {
}
}
// TestBookings_Delete_NoShow24hThreshold tests the 24-hour no-show rule:
// - Cancellation < 24 hours before appointment: treated as no-show (deposits = 3)
// - Cancellation >= 24 hours before appointment: treated as late_cancellation
func TestBookings_Delete_NoShow24hThreshold(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
seedDefaultWorkingHours(t)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
token := jwt.GenerateUserToken(userID)
// Create booking 23 hours from now (will be < 24h when deleted)
soonTime := time.Now().Add(23 * time.Hour).Truncate(time.Second)
soonTime = time.Date(soonTime.Year(), soonTime.Month(), soonTime.Day(), 10, 0, 0, 0, soonTime.Location())
bookingReq := CreateBookingRequest{
StartTime: soonTime,
ServiceIDs: []string{serviceID},
}
handler := http.HandlerFunc(CreateBookingHandler)
w := makeRequest(handler, "POST", "/api/bookings", bookingReq, token)
var booking Booking
if err := parseResponseBody(w, &booking); err != nil {
t.Fatalf("failed to parse booking response: %v", err)
}
// Add payment so deletion works
_, _ = db.DB.Exec(context.Background(),
"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
booking.ID)
// Delete within 24 hours (no forgiveness) - should result in no-show + deposits penalty
delReq := map[string]interface{}{"reason": "client_cancelled", "forgive_no_show": false}
w = makeRequest(handler, "DELETE", "/api/bookings/"+booking.ID, delReq, token)
if w.Code != http.StatusOK && w.Code != http.StatusNoContent {
t.Errorf("expected status 200/204, got %d. body: %s", w.Code, w.Body.String())
}
// Verify deposits were applied
var deposits int
err = db.DB.QueryRow(context.Background(), "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
if err != nil {
t.Errorf("failed to query deposits: %v", err)
}
if deposits != 3 {
t.Errorf("expected deposits=3 after no-show penalty, got %d", deposits)
}
}
// TestBookings_Delete_NoShow_WithForgiveness tests that admin can forgive a no-show
// by passing forgive_no_show=true, which prevents the deposit penalty.
func TestBookings_Delete_NoShow_WithForgiveness(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
seedDefaultWorkingHours(t)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
token := jwt.GenerateUserToken(userID)
// Create booking 20 hours from now (will be < 24h)
soonTime := time.Now().Add(20 * time.Hour).Truncate(time.Second)
soonTime = time.Date(soonTime.Year(), soonTime.Month(), soonTime.Day(), 10, 0, 0, 0, soonTime.Location())
bookingReq := CreateBookingRequest{
StartTime: soonTime,
ServiceIDs: []string{serviceID},
}
handler := http.HandlerFunc(CreateBookingHandler)
w := makeRequest(handler, "POST", "/api/bookings", bookingReq, token)
var booking Booking
if err := parseResponseBody(w, &booking); err != nil {
t.Fatalf("failed to parse booking response: %v", err)
}
// Add payment
_, _ = db.DB.Exec(context.Background(),
"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
booking.ID)
// Delete within 24 hours WITH forgiveness
trueVal := true
delReq := map[string]interface{}{"reason": "client_cancelled", "forgive_no_show": trueVal}
w = makeRequest(handler, "DELETE", "/api/bookings/"+booking.ID, delReq, token)
if w.Code != http.StatusOK && w.Code != http.StatusNoContent {
t.Errorf("expected status 200/204, got %d. body: %s", w.Code, w.Body.String())
}
// Verify NO deposits were applied (forgiveness worked)
var deposits int
err = db.DB.QueryRow(context.Background(), "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
if err != nil {
t.Errorf("failed to query deposits: %v", err)
}
if deposits != 0 {
t.Errorf("expected deposits=0 after forgiveness, got %d", deposits)
}
}
// =============================================================================
// Unauthorized Tests
// =============================================================================
@@ -1064,7 +1192,7 @@ func TestBookings_Unauthorized(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -1181,7 +1309,7 @@ func TestBookings_List_Empty(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -1223,7 +1351,7 @@ func TestBookings_Get_InvalidBookingID(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -1252,7 +1380,7 @@ func TestBookings_Create_PastDate(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -1280,10 +1408,9 @@ func TestBookings_Create_PastDate(t *testing.T) {
}
}
// TestBookings_Create_Within48HourDepositRequired tests that when a booking
// is made within 48 hours and the user has deposits_required > 0, the booking
// should have deposit_required=true. With deposits_required=0, no deposit needed.
func TestBookings_Create_Within48HourDepositRequired(t *testing.T) {
// TestBookings_Create_MinimumAdvance tests that bookings must be made at least
// 1 hour in advance (changed from 48h deposit requirement to universal 1h rule).
func TestBookings_Create_MinimumAdvance(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
@@ -1296,7 +1423,7 @@ func TestBookings_Create_Within48HourDepositRequired(t *testing.T) {
}
defer fixtures.DeleteUser(db.DB, userID)
// Set deposits_required=0 to avoid 48h advance booking requirement
// Note: deposits_required=0 by default now
_, err = db.DB.Exec(context.Background(), "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
@@ -1310,13 +1437,13 @@ func TestBookings_Create_Within48HourDepositRequired(t *testing.T) {
token := jwt.GenerateUserToken(userID)
// Use 10:00 to ensure service fits within working hours (08:00-20:00)
within48h := time.Now().Add(24 * time.Hour).Truncate(time.Second)
within48h = time.Date(within48h.Year(), within48h.Month(), within48h.Day(), 10, 0, 0, 0, within48h.Location())
req := CreateBookingRequest{
StartTime: within48h,
ServiceIDs: []string{serviceID},
}
// Test booking 1+ hour in advance - should succeed
aheadTime := time.Now().Add(2 * time.Hour).Truncate(time.Second)
aheadTime = time.Date(aheadTime.Year(), aheadTime.Month(), aheadTime.Day(), 10, 0, 0, 0, aheadTime.Location())
req := CreateBookingRequest{
StartTime: aheadTime,
ServiceIDs: []string{serviceID},
}
handler := http.HandlerFunc(CreateBookingHandler)
w := makeRequest(handler, "POST", "/api/bookings", req, token)
@@ -1335,6 +1462,146 @@ func TestBookings_Create_Within48HourDepositRequired(t *testing.T) {
}
}
// TestBookings_Create_WithNotes_StatusPending tests that when a booking is created with notes,
// the booking status is automatically set to 'pending' (requires admin approval).
func TestBookings_Create_WithNotes_StatusPending(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Seed working hours
seedDefaultWorkingHours(t)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
token := jwt.GenerateUserToken(userID)
// Booking 2+ hours ahead with notes
futureTime := time.Now().Add(2 * time.Hour).Truncate(time.Second)
futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
notes := "Special treatment needed"
req := CreateBookingRequest{
StartTime: futureTime,
ServiceIDs: []string{serviceID},
Notes: &notes,
}
handler := http.HandlerFunc(CreateBookingHandler)
w := makeRequest(handler, "POST", "/api/bookings", req, token)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
var booking Booking
if err := parseResponseBody(w, &booking); err != nil {
t.Errorf("failed to parse response: %v", err)
}
if booking.Status != "pending" {
t.Errorf("expected status 'pending' when notes provided, got %q", booking.Status)
}
if booking.Notes == nil || *booking.Notes != notes {
t.Error("notes not preserved in booking")
}
}
// TestBookings_Create_WithoutNotes_StatusConfirmed tests that when a booking is created without notes,
// the booking status is automatically set to 'confirmed' (auto-approved).
func TestBookings_Create_WithoutNotes_StatusConfirmed(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Seed working hours
seedDefaultWorkingHours(t)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
token := jwt.GenerateUserToken(userID)
// Booking 2+ hours ahead without notes
futureTime := time.Now().Add(2 * time.Hour).Truncate(time.Second)
futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
req := CreateBookingRequest{
StartTime: futureTime,
ServiceIDs: []string{serviceID},
Notes: nil,
}
handler := http.HandlerFunc(CreateBookingHandler)
w := makeRequest(handler, "POST", "/api/bookings", req, token)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
var booking Booking
if err := parseResponseBody(w, &booking); err != nil {
t.Errorf("failed to parse response: %v", err)
}
if booking.Status != "confirmed" {
t.Errorf("expected status 'confirmed' without notes, got %q", booking.Status)
}
}
// TestBookings_Create_Within1Hour_ShouldFail tests that bookings less than 1 hour in advance are rejected.
func TestBookings_Create_Within1Hour_ShouldFail(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
// Seed working hours
seedDefaultWorkingHours(t)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
serviceID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(db.DB, serviceID)
token := jwt.GenerateUserToken(userID)
// Booking less than 1 hour in advance (30 minutes)
sooonTime := time.Now().Add(30 * time.Minute).Truncate(time.Second)
req := CreateBookingRequest{
StartTime: sooonTime,
ServiceIDs: []string{serviceID},
}
handler := http.HandlerFunc(CreateBookingHandler)
w := makeRequest(handler, "POST", "/api/bookings", req, token)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
}
// TestBookings_Create_MultipleServices verifies that a booking can include
// multiple services at once, and all services are properly associated with
// the booking in the database.