diff --git a/backend/go.mod b/backend/go.mod index 1a684cb..b1ade20 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -9,6 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 github.com/go-chi/jwtauth/v5 v5.3.3 github.com/kovidgoyal/imaging v1.8.19 + github.com/lib/pq v1.11.2 golang.org/x/text v0.34.0 ) @@ -33,7 +34,6 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/kovidgoyal/go-parallel v1.1.1 // indirect github.com/kovidgoyal/go-shm v1.0.0 // indirect - github.com/lib/pq v1.11.2 // indirect github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/image v0.36.0 // indirect diff --git a/backend/handlers/auth/auth_test.go b/backend/handlers/auth/auth_test.go index 1164c78..c5c0e07 100644 --- a/backend/handlers/auth/auth_test.go +++ b/backend/handlers/auth/auth_test.go @@ -323,7 +323,7 @@ func TestRegister_InvalidInput_Under16(t *testing.T) { handler := http.HandlerFunc(RegisterHandler) // Calculate a date that makes them under 16 - under16DOB := time.Now().AddDate(-15, NULL, 0).Format("2006-01-02") + under16DOB := time.Now().AddDate(-15, 0, 0).Format("2006-01-02") body := RegisterRequest{ FirstName: "Young", diff --git a/backend/handlers/bookings/bookings_test.go b/backend/handlers/bookings/bookings_test.go index 33b3bab..697b19d 100644 --- a/backend/handlers/bookings/bookings_test.go +++ b/backend/handlers/bookings/bookings_test.go @@ -34,6 +34,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5/pgxpool" + "github.com/lib/pq" ) // setupTestDB replaces the global db.DB with a test pool and returns a cleanup function @@ -57,6 +58,39 @@ func setupTestDB(t *testing.T) func() { } } +// seedDefaultWorkingHours seeds default working hours for tests +func seedDefaultWorkingHours(t *testing.T) { + t.Helper() + + // Seed 7 days of working hours (Monday=0 to Sunday=6) + // Use wide hours to avoid test failures due to business logic time checks + hours := []struct { + weekday int + startTime string + endTime string + isOpen bool + }{ + {0, "08:00", "20:00", true}, // Monday + {1, "08:00", "20:00", true}, // Tuesday + {2, "08:00", "20:00", true}, // Wednesday + {3, "08:00", "20:00", true}, // Thursday + {4, "08:00", "20:00", true}, // Friday + {5, "08:00", "20:00", true}, // Saturday + {6, "08:00", "20:00", true}, // Sunday + } + + for _, h := range hours { + _, err := db.DB.Exec(context.Background(), ` + 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 + `, h.weekday, h.startTime, h.endTime, h.isOpen) + if err != nil { + t.Fatalf("failed to seed working hours: %v", err) + } + } +} + // helper function to make JSON request with JWT auth // For authenticated requests, use makeAuthRequest which extracts user from JWT func makeRequest(handler http.Handler, method, path string, body interface{}, token string) *httptest.ResponseRecorder { @@ -209,6 +243,9 @@ func TestBookings_Create(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() + // Seed working hours for booking tests + seedDefaultWorkingHours(t) + // Create test user and service userID, err := fixtures.CreateTestUser(db.DB) if err != nil { @@ -232,7 +269,9 @@ func TestBookings_Create(t *testing.T) { token := jwt.GenerateUserToken(userID) // Create booking request - use future time to avoid 48h deposit requirement + // 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()) req := CreateBookingRequest{ StartTime: futureTime, ServiceIDs: []string{serviceID}, @@ -1248,6 +1287,9 @@ func TestBookings_Create_Within48HourDepositRequired(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() + // Seed working hours for booking tests + seedDefaultWorkingHours(t) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -1268,7 +1310,9 @@ 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}, @@ -1298,6 +1342,9 @@ func TestBookings_Create_MultipleServices(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() + // Seed working hours for booking tests + seedDefaultWorkingHours(t) + userID, err := fixtures.CreateTestUser(db.DB) if err != nil { t.Fatalf("failed to create test user: %v", err) @@ -1324,7 +1371,9 @@ func TestBookings_Create_MultipleServices(t *testing.T) { token := jwt.GenerateUserToken(userID) + // Use 10:00 to ensure services fit 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()) req := CreateBookingRequest{ StartTime: futureTime, ServiceIDs: []string{serviceID1, serviceID2}, @@ -1613,9 +1662,8 @@ func TestUserCancelBooking_PendingNoNotification(t *testing.T) { // Transaction and Error Handling Tests // ============================================================================= -// TestUserCancelBooking_TransactionIntegrity tests that the cancellation -// transaction properly commits - verifying the booking status actually changes -// after a successful cancellation request. +// TestUserCancelBooking_TransactionIntegrity verifies that if any part of the +// cancellation transaction fails, the booking status is NOT changed (rollback behavior) func TestUserCancelBooking_TransactionIntegrity(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() @@ -1688,8 +1736,6 @@ func TestUserCancelBooking_TransactionIntegrity(t *testing.T) { } } - -// TestCreateEditRequest tests that creating an edit request creates an admin notification // TestCreateEditRequest verifies that a user can request an edit to their // confirmed booking (e.g., change time). This creates a booking_edit_request record // and generates an admin notification for staff review. @@ -1767,8 +1813,7 @@ func TestCreateEditRequest(t *testing.T) { } // TestDeleteEditRequest tests that user deleting their edit request deletes the admin notification -// TestDeleteEditRequest tests that an admin can delete/remove a pending -// edit request from a booking without affecting the original booking data. +func TestDeleteEditRequest(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() @@ -1859,9 +1904,7 @@ func TestCreateEditRequest(t *testing.T) { } // TestAdminApproveEditRequest tests that admin approving acknowledges the notification (not deletes) -// TestAdminApproveEditRequest verifies that an admin can approve a user's -// edit request. This updates the booking's start time to the requested time and -// marks the edit request as handled. +func TestAdminApproveEditRequest(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() @@ -1978,8 +2021,7 @@ func TestCreateEditRequest(t *testing.T) { } // TestAdminRejectEditRequest tests that admin rejecting acknowledges the notification (not deletes) -// TestAdminRejectEditRequest tests that an admin can reject an edit request. -// The original booking remains unchanged and the edit request is deleted. +func TestAdminRejectEditRequest(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() @@ -2090,11 +2132,8 @@ func TestCreateEditRequest(t *testing.T) { } } - - // TestBookings_RequestEdit_BookingNotFound tests that requesting an edit for a non-existent booking returns 404 -// TestBookings_RequestEdit_BookingNotFound verifies that requesting an edit -// for a non-existent booking returns HTTP 404 Not Found. +func TestBookings_RequestEdit_BookingNotFound(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() @@ -2125,8 +2164,7 @@ func TestCreateEditRequest(t *testing.T) { } // TestBookings_RequestEdit_AlreadyHasPending tests that a user cannot create a second edit request while one already exists -// TestBookings_RequestEdit_AlreadyHasPending tests that a user cannot create -// a new edit request if one is already pending for the same booking. +func TestBookings_RequestEdit_AlreadyHasPending(t *testing.T) { cleanup := setupTestDB(t) defer cleanup() @@ -2182,20 +2220,19 @@ func TestCreateEditRequest(t *testing.T) { token := jwt.GenerateUserToken(userID) // Try to create another edit request via API + // Note: The handler actually replaces (upserts) the existing request, not rejects it handler := http.HandlerFunc(RequestEditHandler) reqBody := map[string]interface{}{ "notes": "Please change to a different day", } w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token) - // Expect HTTP 201 Created - handler replaces old edit request with new one + // Expect HTTP 201 Created (handler replaces existing request) if w.Code != http.StatusCreated { t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String()) } - // Verify only 1 edit request exists in DB (the old one was replaced with new one) - - // Verify only 1 edit request exists in DB (the original one) + // Verify only 1 edit request exists in DB (the old one was replaced) var erCount int err = db.DB.QueryRow(context.Background(), "SELECT COUNT(*) FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&erCount) @@ -2205,4 +2242,15 @@ func TestCreateEditRequest(t *testing.T) { if erCount != 1 { t.Errorf("expected 1 edit request, got %d", erCount) } + + // Verify the notes were updated + var notes string + err = db.DB.QueryRow(context.Background(), + "SELECT notes FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(¬es) + if err != nil { + t.Fatalf("failed to query edit request notes: %v", err) + } + if notes != "Please change to a different day" { + t.Errorf("expected notes 'Please change to a different day', got '%s'", notes) + } } diff --git a/backend/handlers/bookings/manage.go b/backend/handlers/bookings/manage.go index 5716180..54effbc 100644 --- a/backend/handlers/bookings/manage.go +++ b/backend/handlers/bookings/manage.go @@ -1008,6 +1008,7 @@ func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) { var origStartTime time.Time var bookingStatus string var userName string + var newServices []string err := rows.Scan( &req.ID, @@ -1071,6 +1072,7 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) { // Get the edit request var bookingID string + var newStartTime *time.Time var newServices []string var notes *string var hasOverrides bool @@ -1081,7 +1083,7 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) { `, requestID).Scan(&bookingID, &newStartTime, pq.Array(&newServices), ¬es, &hasOverrides) if err != nil { if errors.Is(err, sql.ErrNoRows) { - http.Error(w, "Edit request not found or already processed", http.StatusNotFound) + http.Error(w, "Edit request not found", http.StatusNotFound) return } log.Printf("Failed to get edit request %s: %v", requestID, err) @@ -1228,7 +1230,6 @@ func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal server error", http.StatusInternalServerError) return } - if err := tx.Commit(r.Context()); err != nil { log.Printf("Failed to commit: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) diff --git a/backend/handlers/user/profile_test.go b/backend/handlers/user/profile_test.go index 7216ca0..7bed0c9 100644 --- a/backend/handlers/user/profile_test.go +++ b/backend/handlers/user/profile_test.go @@ -32,21 +32,6 @@ import ( "crussell/testutils/testdb" "github.com/jackc/pgx/v5/pgxpool" - - "bytes" - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "crussell/db" - "crussell/mw" - "crussell/testutils/fixtures" - "crussell/testutils/jwt" - "crussell/testutils/testdb" - - "github.com/jackc/pgx/v5/pgxpool" ) func setupTest(t *testing.T) (func(), *pgxpool.Pool) { diff --git a/local-dev-2.sh b/local-dev-2.sh index 6f7e1ec..1704a76 100755 --- a/local-dev-2.sh +++ b/local-dev-2.sh @@ -263,6 +263,9 @@ if api_post "$BASE_URL/register" "{\"firstName\":\"Oscar\",\"lastName\":\"Hughes # Promote Admin docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET account_role = 'admin' WHERE email = '$ADMIN_EMAIL'" > /dev/null 2>&1 + +# Set deposits_required=0 for all users (so they can confirm bookings without deposit issues) +docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET deposits_required = 0" > /dev/null 2>&1 echo "${C_GREEN}✅ Registered $success/$total Users${C_RESET}" # 2. Login