Files
Crussell/backend/handlers/user/customer_relationship_test.go
T
popertots e73c96b653 refactor: optimize test DB setup — TestMain per package, truncate-only between tests
- Add TestMain to all 10 test packages (schema DROP+CREATE runs once per package)
- Convert per-test setupTestDB to resetTestData (TRUNCATE only, ~60% faster)
- Add 3 missing tables to TruncateTables (booking_edit_requests, exceptional_group_applications, business_settings)
- Remove dead truncateDiscountTables helper
- Consolidate discount_test.go into package bookings (was external test package)
- Update testutils.SetupTestDB to truncate-only
- Fix unused imports across user, bookings, and handlers packages
- Verify: 286 passing, 2 skipped, 0 failures with -count=2 (no state leakage)
2026-05-10 17:27:51 +01:00

321 lines
9.0 KiB
Go

//go:build test
// +build test
package user
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"crussell/db"
"crussell/testutils/fixtures"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestCustomerRelationship_Success(t *testing.T) {
resetTestData(t)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
svc1, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create service 1: %v", err)
}
svc2ID, err := createService(db.DB, "Gel Manicure", 35.00)
if err != nil {
t.Fatalf("failed to create service 2: %v", err)
}
booking1 := createCompletedBooking(t, db.DB, userID, svc1, "2024-01-15 10:00:00+00", 50.00)
booking2 := createCompletedBooking(t, db.DB, userID, svc1, "2024-06-20 14:00:00+00", 50.00)
booking3 := createCompletedBooking(t, db.DB, userID, svc2ID, "2024-12-01 11:00:00+00", 35.00)
createPayment(t, db.DB, booking1, "full", 50.00)
createPayment(t, db.DB, booking2, "full", 50.00)
createPayment(t, db.DB, booking3, "full", 35.00)
req := newAdminRequest("GET", "/api/admin/users/"+userID+"/relationship", userID)
rr := httptest.NewRecorder()
GetCustomerRelationshipHandler(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d. body: %s", rr.Code, rr.Body.String())
}
var result CustomerRelationship
if err := json.Unmarshal(rr.Body.Bytes(), &result); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if result.TotalSpend != 135.00 {
t.Errorf("expected total spend 135.00, got %.2f", result.TotalSpend)
}
if result.TotalTips != 0 {
t.Errorf("expected total tips 0, got %.2f", result.TotalTips)
}
if result.TotalVisits != 3 {
t.Errorf("expected total visits 3, got %d", result.TotalVisits)
}
if result.CustomerFor == "" {
t.Error("expected customerFor to be set")
}
if result.FirstVisitDate == nil {
t.Fatal("expected firstVisitDate to be set")
}
if !strings.Contains(*result.FirstVisitDate, "2024-01-15") {
t.Errorf("expected first visit date to contain 2024-01-15, got %s", *result.FirstVisitDate)
}
if result.LastVisitDate == nil {
t.Fatal("expected lastVisitDate to be set")
}
if !strings.Contains(*result.LastVisitDate, "2024-12-01") {
t.Errorf("expected last visit date to contain 2024-12-01, got %s", *result.LastVisitDate)
}
if len(result.TopServices) != 2 {
t.Fatalf("expected 2 top services, got %d", len(result.TopServices))
}
if result.TopServices[0].Count != 2 {
t.Errorf("expected top service count 2, got %d", result.TopServices[0].Count)
}
if result.TopServices[1].Count != 1 {
t.Errorf("expected second service count 1, got %d", result.TopServices[1].Count)
}
}
func TestCustomerRelationship_NoBookings(t *testing.T) {
resetTestData(t)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
req := newAdminRequest("GET", "/api/admin/users/"+userID+"/relationship", userID)
rr := httptest.NewRecorder()
GetCustomerRelationshipHandler(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d. body: %s", rr.Code, rr.Body.String())
}
var result CustomerRelationship
if err := json.Unmarshal(rr.Body.Bytes(), &result); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if result.TotalSpend != 0 {
t.Errorf("expected total spend 0, got %.2f", result.TotalSpend)
}
if result.TotalTips != 0 {
t.Errorf("expected total tips 0, got %.2f", result.TotalTips)
}
if result.TotalVisits != 0 {
t.Errorf("expected total visits 0, got %d", result.TotalVisits)
}
if result.CustomerFor != "" {
t.Errorf("expected empty customerFor, got %s", result.CustomerFor)
}
if result.FirstVisitDate != nil {
t.Errorf("expected firstVisitDate to be nil, got %s", *result.FirstVisitDate)
}
if result.LastVisitDate != nil {
t.Errorf("expected lastVisitDate to be nil, got %s", *result.LastVisitDate)
}
if len(result.TopServices) != 0 {
t.Errorf("expected empty top services, got %d items", len(result.TopServices))
}
}
func TestCustomerRelationship_UserNotFound(t *testing.T) {
resetTestData(t)
req := newAdminRequest("GET", "/api/admin/users/000000000000/relationship", "000000000000")
rr := httptest.NewRecorder()
GetCustomerRelationshipHandler(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("expected status 404, got %d", rr.Code)
}
}
func TestCustomerRelationship_InvalidID(t *testing.T) {
resetTestData(t)
tests := []struct {
name string
id string
}{
{"too_short", "abc"},
{"too_long", "abcdef1234567"},
{"empty", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := newAdminRequest("GET", "/api/admin/users/"+tt.id+"/relationship", tt.id)
rr := httptest.NewRecorder()
GetCustomerRelationshipHandler(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("expected status 404 for id %q, got %d", tt.id, rr.Code)
}
})
}
}
func TestCustomerRelationship_OnlyPendingBookings(t *testing.T) {
resetTestData(t)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
svcID, err := fixtures.CreateTestService(db.DB)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, svcID)
if err != nil {
t.Fatalf("failed to create booking: %v", err)
}
_ = bookingID
req := newAdminRequest("GET", "/api/admin/users/"+userID+"/relationship", userID)
rr := httptest.NewRecorder()
GetCustomerRelationshipHandler(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d. body: %s", rr.Code, rr.Body.String())
}
var result CustomerRelationship
if err := json.Unmarshal(rr.Body.Bytes(), &result); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if result.TotalVisits != 0 {
t.Errorf("expected 0 visits (pending booking), got %d", result.TotalVisits)
}
if result.TotalSpend != 0 {
t.Errorf("expected 0 spend (no completed payments), got %.2f", result.TotalSpend)
}
if result.TotalTips != 0 {
t.Errorf("expected 0 tips, got %.2f", result.TotalTips)
}
}
func TestCustomerRelationship_PartialPayments(t *testing.T) {
resetTestData(t)
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
svcID, err := createService(db.DB, "Test Service", 100.00)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
bookingID := createCompletedBooking(t, db.DB, userID, svcID, "2024-03-01 10:00:00+00", 100.00)
createPayment(t, db.DB, bookingID, "full", 80.00)
createPayment(t, db.DB, bookingID, "tip", 10.00)
createPayment(t, db.DB, bookingID, "deposit", 20.00)
req := newAdminRequest("GET", "/api/admin/users/"+userID+"/relationship", userID)
rr := httptest.NewRecorder()
GetCustomerRelationshipHandler(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d. body: %s", rr.Code, rr.Body.String())
}
var result CustomerRelationship
if err := json.Unmarshal(rr.Body.Bytes(), &result); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if result.TotalSpend != 100.00 {
t.Errorf("expected total spend 100.00 (full + deposit), got %.2f", result.TotalSpend)
}
if result.TotalTips != 10.00 {
t.Errorf("expected total tips 10.00, got %.2f", result.TotalTips)
}
}
func newAdminRequest(method, path, userID string) *http.Request {
req := httptest.NewRequest(method, path, nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", userID)
ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
return req.WithContext(ctx)
}
func createService(pool *pgxpool.Pool, name string, price float64) (string, error) {
ctx := context.Background()
var id string
err := pool.QueryRow(ctx, `
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id
`, name, "Test service", price, 60, true, 16).Scan(&id)
return id, err
}
func createCompletedBooking(t *testing.T, pool *pgxpool.Pool, userID, serviceID, startTime string, price float64) string {
t.Helper()
ctx := context.Background()
var bookingID string
err := pool.QueryRow(ctx, `
INSERT INTO bookings (user_id, start_time, status)
VALUES ($1, $2, 'completed')
RETURNING id
`, userID, startTime).Scan(&bookingID)
if err != nil {
t.Fatalf("failed to create completed booking: %v", err)
}
_, err = pool.Exec(ctx, `
INSERT INTO booking_services (booking_id, service_id, override_price)
VALUES ($1, $2, $3)
`, bookingID, serviceID, price)
if err != nil {
t.Fatalf("failed to link service to booking: %v", err)
}
return bookingID
}
func createPayment(t *testing.T, pool *pgxpool.Pool, bookingID, paymentType string, amount float64) {
t.Helper()
ctx := context.Background()
_, err := pool.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status)
VALUES ($1, $2, 'in_person_card', $3, 'completed')
`, bookingID, paymentType, amount)
if err != nil {
t.Fatalf("failed to create payment: %v", err)
}
}