|
|
@@ -34,6 +34,7 @@ import (
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
|
|
|
|
"github.com/lib/pq"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// setupTestDB replaces the global db.DB with a test pool and returns a cleanup function
|
|
|
|
// setupTestDB replaces the global db.DB with a test pool and returns a cleanup function
|
|
|
@@ -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
|
|
|
|
// helper function to make JSON request with JWT auth
|
|
|
|
// For authenticated requests, use makeAuthRequest which extracts user from JWT
|
|
|
|
// For authenticated requests, use makeAuthRequest which extracts user from JWT
|
|
|
|
func makeRequest(handler http.Handler, method, path string, body interface{}, token string) *httptest.ResponseRecorder {
|
|
|
|
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)
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
defer cleanup()
|
|
|
|
defer cleanup()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Seed working hours for booking tests
|
|
|
|
|
|
|
|
seedDefaultWorkingHours(t)
|
|
|
|
|
|
|
|
|
|
|
|
// Create test user and service
|
|
|
|
// Create test user and service
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
@@ -232,7 +269,9 @@ func TestBookings_Create(t *testing.T) {
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
|
|
|
|
|
|
|
|
// Create booking request - use future time to avoid 48h deposit requirement
|
|
|
|
// 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.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{
|
|
|
|
req := CreateBookingRequest{
|
|
|
|
StartTime: futureTime,
|
|
|
|
StartTime: futureTime,
|
|
|
|
ServiceIDs: []string{serviceID},
|
|
|
|
ServiceIDs: []string{serviceID},
|
|
|
@@ -1248,6 +1287,9 @@ func TestBookings_Create_Within48HourDepositRequired(t *testing.T) {
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
defer cleanup()
|
|
|
|
defer cleanup()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Seed working hours for booking tests
|
|
|
|
|
|
|
|
seedDefaultWorkingHours(t)
|
|
|
|
|
|
|
|
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
|
@@ -1268,7 +1310,9 @@ func TestBookings_Create_Within48HourDepositRequired(t *testing.T) {
|
|
|
|
|
|
|
|
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
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.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{
|
|
|
|
req := CreateBookingRequest{
|
|
|
|
StartTime: within48h,
|
|
|
|
StartTime: within48h,
|
|
|
|
ServiceIDs: []string{serviceID},
|
|
|
|
ServiceIDs: []string{serviceID},
|
|
|
@@ -1298,6 +1342,9 @@ func TestBookings_Create_MultipleServices(t *testing.T) {
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
defer cleanup()
|
|
|
|
defer cleanup()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Seed working hours for booking tests
|
|
|
|
|
|
|
|
seedDefaultWorkingHours(t)
|
|
|
|
|
|
|
|
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
|
@@ -1324,7 +1371,9 @@ func TestBookings_Create_MultipleServices(t *testing.T) {
|
|
|
|
|
|
|
|
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
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.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{
|
|
|
|
req := CreateBookingRequest{
|
|
|
|
StartTime: futureTime,
|
|
|
|
StartTime: futureTime,
|
|
|
|
ServiceIDs: []string{serviceID1, serviceID2},
|
|
|
|
ServiceIDs: []string{serviceID1, serviceID2},
|
|
|
@@ -1613,9 +1662,8 @@ func TestUserCancelBooking_PendingNoNotification(t *testing.T) {
|
|
|
|
// Transaction and Error Handling Tests
|
|
|
|
// Transaction and Error Handling Tests
|
|
|
|
// =============================================================================
|
|
|
|
// =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
// TestUserCancelBooking_TransactionIntegrity tests that the cancellation
|
|
|
|
// TestUserCancelBooking_TransactionIntegrity verifies that if any part of the
|
|
|
|
// transaction properly commits - verifying the booking status actually changes
|
|
|
|
// cancellation transaction fails, the booking status is NOT changed (rollback behavior)
|
|
|
|
// after a successful cancellation request.
|
|
|
|
|
|
|
|
func TestUserCancelBooking_TransactionIntegrity(t *testing.T) {
|
|
|
|
func TestUserCancelBooking_TransactionIntegrity(t *testing.T) {
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
defer cleanup()
|
|
|
|
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
|
|
|
|
// TestCreateEditRequest verifies that a user can request an edit to their
|
|
|
|
// confirmed booking (e.g., change time). This creates a booking_edit_request record
|
|
|
|
// confirmed booking (e.g., change time). This creates a booking_edit_request record
|
|
|
|
// and generates an admin notification for staff review.
|
|
|
|
// and generates an admin notification for staff review.
|
|
|
@@ -1767,8 +1813,7 @@ func TestCreateEditRequest(t *testing.T) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestDeleteEditRequest tests that user deleting their edit request deletes the admin notification
|
|
|
|
// TestDeleteEditRequest tests that user deleting their edit request deletes the admin notification
|
|
|
|
// TestDeleteEditRequest tests that an admin can delete/remove a pending
|
|
|
|
func TestDeleteEditRequest(t *testing.T) {
|
|
|
|
// edit request from a booking without affecting the original booking data.
|
|
|
|
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
defer cleanup()
|
|
|
|
defer cleanup()
|
|
|
|
|
|
|
|
|
|
|
@@ -1859,9 +1904,7 @@ func TestCreateEditRequest(t *testing.T) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestAdminApproveEditRequest tests that admin approving acknowledges the notification (not deletes)
|
|
|
|
// TestAdminApproveEditRequest tests that admin approving acknowledges the notification (not deletes)
|
|
|
|
// TestAdminApproveEditRequest verifies that an admin can approve a user's
|
|
|
|
func TestAdminApproveEditRequest(t *testing.T) {
|
|
|
|
// edit request. This updates the booking's start time to the requested time and
|
|
|
|
|
|
|
|
// marks the edit request as handled.
|
|
|
|
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
defer cleanup()
|
|
|
|
defer cleanup()
|
|
|
|
|
|
|
|
|
|
|
@@ -1978,8 +2021,7 @@ func TestCreateEditRequest(t *testing.T) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestAdminRejectEditRequest tests that admin rejecting acknowledges the notification (not deletes)
|
|
|
|
// TestAdminRejectEditRequest tests that admin rejecting acknowledges the notification (not deletes)
|
|
|
|
// TestAdminRejectEditRequest tests that an admin can reject an edit request.
|
|
|
|
func TestAdminRejectEditRequest(t *testing.T) {
|
|
|
|
// The original booking remains unchanged and the edit request is deleted.
|
|
|
|
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
defer cleanup()
|
|
|
|
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 tests that requesting an edit for a non-existent booking returns 404
|
|
|
|
// TestBookings_RequestEdit_BookingNotFound verifies that requesting an edit
|
|
|
|
func TestBookings_RequestEdit_BookingNotFound(t *testing.T) {
|
|
|
|
// for a non-existent booking returns HTTP 404 Not Found.
|
|
|
|
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
defer cleanup()
|
|
|
|
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 second edit request while one already exists
|
|
|
|
// TestBookings_RequestEdit_AlreadyHasPending tests that a user cannot create
|
|
|
|
func TestBookings_RequestEdit_AlreadyHasPending(t *testing.T) {
|
|
|
|
// a new edit request if one is already pending for the same booking.
|
|
|
|
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
cleanup := setupTestDB(t)
|
|
|
|
defer cleanup()
|
|
|
|
defer cleanup()
|
|
|
|
|
|
|
|
|
|
|
@@ -2182,20 +2220,19 @@ func TestCreateEditRequest(t *testing.T) {
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
token := jwt.GenerateUserToken(userID)
|
|
|
|
|
|
|
|
|
|
|
|
// Try to create another edit request via API
|
|
|
|
// Try to create another edit request via API
|
|
|
|
|
|
|
|
// Note: The handler actually replaces (upserts) the existing request, not rejects it
|
|
|
|
handler := http.HandlerFunc(RequestEditHandler)
|
|
|
|
handler := http.HandlerFunc(RequestEditHandler)
|
|
|
|
reqBody := map[string]interface{}{
|
|
|
|
reqBody := map[string]interface{}{
|
|
|
|
"notes": "Please change to a different day",
|
|
|
|
"notes": "Please change to a different day",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token)
|
|
|
|
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 {
|
|
|
|
if w.Code != http.StatusCreated {
|
|
|
|
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
|
|
|
|
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 old one was replaced)
|
|
|
|
|
|
|
|
|
|
|
|
// Verify only 1 edit request exists in DB (the original one)
|
|
|
|
|
|
|
|
var erCount int
|
|
|
|
var erCount int
|
|
|
|
err = db.DB.QueryRow(context.Background(),
|
|
|
|
err = db.DB.QueryRow(context.Background(),
|
|
|
|
"SELECT COUNT(*) FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&erCount)
|
|
|
|
"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 {
|
|
|
|
if erCount != 1 {
|
|
|
|
t.Errorf("expected 1 edit request, got %d", erCount)
|
|
|
|
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)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|