//go:build test // +build test package fixtures import ( "context" "fmt" "sync/atomic" "time" "crussell/clock" "crussell/db" "golang.org/x/crypto/bcrypt" ) // Global counter for unique emails in tests var testEmailCounter atomic.Int64 func CreateTestAdminUser(q db.Querier) (string, error) { return createTestUser(q, "Admin", "User", "", "admin") } func CreateTestUser(q db.Querier) (string, error) { return createTestUser(q, "Test", "User", "", "verified_email") } func CreateTestUserWithEmail(q db.Querier, email, role string) (string, error) { return createTestUser(q, "Test", "User", email, role) } func createTestUser(q db.Querier, firstName, lastName, email, role string) (string, error) { passwordHash, err := bcrypt.GenerateFromPassword([]byte("testpassword123"), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("failed to hash password: %w", err) } // Generate unique email if not provided if email == "" { n := testEmailCounter.Add(1) email = fmt.Sprintf("%s.%s.%d@test.com", firstName, lastName, n) } ctx := context.Background() var userID string err = q.QueryRow(ctx, ` INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type) VALUES ($1, $2, $3, $4, $5, $6, $7, 'email') RETURNING id `, firstName, lastName, email, "+447123456789", "1990-01-01", string(passwordHash), role).Scan(&userID) if err != nil { return "", fmt.Errorf("failed to create user: %w", err) } return userID, nil } func CreateTestService(q db.Querier) (string, error) { ctx := context.Background() var serviceID string err := q.QueryRow(ctx, ` INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id `, "Test Service", "A test service for unit tests", 50.00, 60, true, 16).Scan(&serviceID) if err != nil { return "", fmt.Errorf("failed to create service: %w", err) } return serviceID, nil } func CreateTestServiceWithDuration(q db.Querier, durationMinutes int) (string, error) { ctx := context.Background() var serviceID string err := q.QueryRow(ctx, ` INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id `, fmt.Sprintf("Test Service %dmin", durationMinutes), "A test service for unit tests", 50.00, durationMinutes, true, 16).Scan(&serviceID) if err != nil { return "", fmt.Errorf("failed to create service: %w", err) } return serviceID, nil } // CreateTestServiceWithPatchTest creates a service and a patch test that links to it // Returns serviceID, patchTestID func CreateTestServiceWithPatchTest(q db.Querier) (string, string, error) { ctx := context.Background() // First create the service var serviceID string err := q.QueryRow(ctx, ` INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id `, "Test Patch Test Service", "A test service requiring patch test", 75.00, 90, true, 18).Scan(&serviceID) if err != nil { return "", "", fmt.Errorf("failed to create service: %w", err) } // Now create a patch test that links to this service var patchTestID string err = q.QueryRow(ctx, ` INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids) VALUES ($1, $2, $3, $4, $5) RETURNING id `, "Test Patch Test", "A patch test for testing", 24, 6, []string{serviceID}).Scan(&patchTestID) if err != nil { return "", "", fmt.Errorf("failed to create patch test: %w", err) } return serviceID, patchTestID, nil } // CreateTestPatchTest creates a patch test definition func CreateTestPatchTest(q db.Querier, serviceIDs []string) (string, error) { ctx := context.Background() var patchTestID string err := q.QueryRow(ctx, ` INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids) VALUES ($1, $2, $3, $4, $5) RETURNING id `, "Test Patch Test", "A patch test for testing", 24, 6, serviceIDs).Scan(&patchTestID) if err != nil { return "", fmt.Errorf("failed to create patch test: %w", err) } return patchTestID, nil } // CreateUserPatchTest creates a user patch test record func CreateUserPatchTest(q db.Querier, userID, patchTestID string, testedAt string) error { ctx := context.Background() _, err := q.Exec(ctx, ` INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at) VALUES ($1, $2, $3) `, userID, patchTestID, testedAt) if err != nil { return fmt.Errorf("failed to create user patch test: %w", err) } return nil } func CreateTestBooking(q db.Querier, userID, serviceID string) (string, error) { return CreateTestBookingAtTime(q, userID, serviceID, time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)) } func CreateTestBookingAtTime(q db.Querier, userID, serviceID string, startTime time.Time) (string, error) { ctx := context.Background() var bookingID string err := q.QueryRow(ctx, ` INSERT INTO bookings (user_id, start_time, status, notes) VALUES ($1, $2, $3, $4) RETURNING id `, userID, startTime, "pending", "Test booking").Scan(&bookingID) if err != nil { return "", fmt.Errorf("failed to create booking: %w", err) } _, err = q.Exec(ctx, ` INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2) `, bookingID, serviceID) if err != nil { return "", fmt.Errorf("failed to link service to booking: %w", err) } return bookingID, nil } // NextWorkingDayAt returns the time at `hour` UTC on a day `daysAhead` days from // now. Hours in [8, 18] are guaranteed inside the fixture's Mon-Sun 08:00-20:00 // London working hours at any wall-clock time (London is at most UTC+1), unlike // clock.Now().Add(N * time.Hour), which can land after closing and flake tests. func NextWorkingDayAt(daysAhead, hour int) time.Time { day := clock.Now().AddDate(0, 0, daysAhead) return time.Date(day.Year(), day.Month(), day.Day(), hour, 0, 0, 0, time.UTC) } func CreateTestVerifiedUser(q db.Querier) (string, error) { return createTestUser(q, "Verified", "User", "verified@test.com", "verified_email") } func CreateTestUnverifiedUser(q db.Querier) (string, error) { return createTestUser(q, "Unverified", "User", "unverified@test.com", "unverified_email") } func CreateTestGuestUser(q db.Querier) (string, error) { return createTestUser(q, "Guest", "User", "guest@test.com", "guest") } func DeleteUser(q db.Querier, userID string) error { ctx := context.Background() _, err := q.Exec(ctx, "DELETE FROM users WHERE id = $1", userID) return err } func DeleteService(q db.Querier, serviceID string) error { ctx := context.Background() _, err := q.Exec(ctx, "DELETE FROM services WHERE id = $1", serviceID) return err } func DeleteBooking(q db.Querier, bookingID string) error { ctx := context.Background() _, err := q.Exec(ctx, "DELETE FROM bookings WHERE id = $1", bookingID) return err } func CreateTestCustomService(q db.Querier) (string, error) { ctx := context.Background() var serviceID string err := q.QueryRow(ctx, ` INSERT INTO custom_services (name, description, price, duration_minutes, minimum_age_required) VALUES ($1, $2, $3, $4, $5) RETURNING id `, "Test Custom Service", "A test custom service", 75.00, 45, 16).Scan(&serviceID) if err != nil { return "", fmt.Errorf("failed to create custom service: %w", err) } return serviceID, nil } func DeleteCustomService(q db.Querier, serviceID string) error { ctx := context.Background() _, err := q.Exec(ctx, "DELETE FROM custom_services WHERE id = $1", serviceID) return err } // SafeDeleteUser wraps DeleteUser and returns error (for tests that care about cleanup failure) func SafeDeleteUser(q db.Querier, userID string) error { return DeleteUser(q, userID) } // SafeDeleteService wraps DeleteService and returns error (for tests that care about cleanup failure) func SafeDeleteService(q db.Querier, serviceID string) error { return DeleteService(q, serviceID) } // SafeDeleteBooking wraps DeleteBooking and returns error (for tests that care about cleanup failure) func SafeDeleteBooking(q db.Querier, bookingID string) error { return DeleteBooking(q, bookingID) } // CreateTestTimeBlocker creates a time blocker for testing // Returns the blocker ID func CreateTestTimeBlocker(q db.Querier, startTime time.Time, durationMinutes int, description string) (string, error) { ctx := context.Background() var blockerID string err := q.QueryRow(ctx, ` INSERT INTO time_blockers (start_time, duration_minutes, description) VALUES ($1, $2, $3) RETURNING id `, startTime, durationMinutes, description).Scan(&blockerID) if err != nil { return "", fmt.Errorf("failed to create time blocker: %w", err) } return blockerID, nil } // DeleteTimeBlocker removes a time blocker from the database func DeleteTimeBlocker(q db.Querier, blockerID string) error { ctx := context.Background() _, err := q.Exec(ctx, "DELETE FROM time_blockers WHERE id = $1", blockerID) return err } // CreateTestPayment creates a payment record for testing // Returns payment ID func CreateTestPayment(q db.Querier, bookingID string, amount float64, method string, ptype string, status string) (string, error) { ctx := context.Background() var paymentID string err := q.QueryRow(ctx, ` INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) RETURNING id `, bookingID, ptype, method, status, amount).Scan(&paymentID) if err != nil { return "", fmt.Errorf("failed to create payment: %w", err) } return paymentID, nil } // CreateTestRefund creates a refund record for testing // Returns refund ID func CreateTestRefund(q db.Querier, paymentID string, bookingID string, amount float64) (string, error) { ctx := context.Background() var refundID string err := q.QueryRow(ctx, ` INSERT INTO refunds (payment_id, booking_id, amount, status, reason, created_at) VALUES ($1, $2, $3, 'completed', 'test refund', NOW()) RETURNING id `, paymentID, bookingID, amount).Scan(&refundID) if err != nil { return "", fmt.Errorf("failed to create refund: %w", err) } return refundID, nil } // CreateTestPaymentMethod creates a saved card for a user // Returns card ID func CreateTestPaymentMethod(q db.Querier, userID string, squareCardID string, brand string, last4 string) (string, error) { ctx := context.Background() var cardID string err := q.QueryRow(ctx, ` INSERT INTO user_saved_cards (user_id, square_card_id, brand, last_4, exp_month, exp_year, fingerprint, is_default, created_at) VALUES ($1, $2, $3, $4, 12, 2030, 'test_fp', false, NOW()) RETURNING id `, userID, squareCardID, brand, last4).Scan(&cardID) if err != nil { return "", fmt.Errorf("failed to create payment method: %w", err) } return cardID, nil } // DeletePayment deletes a payment from the database func DeletePayment(q db.Querier, paymentID string) error { ctx := context.Background() _, err := q.Exec(ctx, "DELETE FROM payments WHERE id = $1", paymentID) return err } // DeleteRefund deletes a refund from the database func DeleteRefund(q db.Querier, refundID string) error { ctx := context.Background() _, err := q.Exec(ctx, "DELETE FROM refunds WHERE id = $1", refundID) return err } // DeletePaymentMethod deletes a saved card from the database func DeletePaymentMethod(q db.Querier, cardID string) error { ctx := context.Background() _, err := q.Exec(ctx, "DELETE FROM user_saved_cards WHERE id = $1", cardID) return err }