refactor(backend): migrate db.DB to db.Conn PoolProxy across all handlers
Replace direct *pgxpool.Pool usage with PoolProxy wrapper across the entire backend: - db.DB renamed to db.Conn (*pgxpool.Pool -> *PoolProxy) - JWT functions now accept context.Context instead of using context.Background() - Handler DB calls route through PoolProxy for per-test transaction support - Fixture/helper/testdb functions accept Querier interface for decoupling - Query ordering fixed in bookings handlers: COUNT after data query to avoid pgx conn busy - Time truncation fixed: time.Date instead of Truncate(24*time.Hour) for week start calc - testmain_test.go files updated with SeedBaseline and NewPoolProxy Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -6,28 +6,29 @@ package fixtures
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"crussell/db"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Global counter for unique emails in tests
|
||||
var testEmailCounter int64
|
||||
var testEmailCounter atomic.Int64
|
||||
|
||||
func CreateTestAdminUser(pool *pgxpool.Pool) (string, error) {
|
||||
return createTestUser(pool, "Admin", "User", "", "admin")
|
||||
func CreateTestAdminUser(q db.Querier) (string, error) {
|
||||
return createTestUser(q, "Admin", "User", "", "admin")
|
||||
}
|
||||
|
||||
func CreateTestUser(pool *pgxpool.Pool) (string, error) {
|
||||
return createTestUser(pool, "Test", "User", "", "verified_email")
|
||||
func CreateTestUser(q db.Querier) (string, error) {
|
||||
return createTestUser(q, "Test", "User", "", "verified_email")
|
||||
}
|
||||
|
||||
func CreateTestUserWithEmail(pool *pgxpool.Pool, email, role string) (string, error) {
|
||||
return createTestUser(pool, "Test", "User", email, role)
|
||||
func CreateTestUserWithEmail(q db.Querier, email, role string) (string, error) {
|
||||
return createTestUser(q, "Test", "User", email, role)
|
||||
}
|
||||
|
||||
func createTestUser(pool *pgxpool.Pool, firstName, lastName, email, role string) (string, error) {
|
||||
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)
|
||||
@@ -35,13 +36,13 @@ func createTestUser(pool *pgxpool.Pool, firstName, lastName, email, role string)
|
||||
|
||||
// Generate unique email if not provided
|
||||
if email == "" {
|
||||
testEmailCounter++
|
||||
email = fmt.Sprintf("%s.%s.%d@test.com", firstName, lastName, testEmailCounter)
|
||||
n := testEmailCounter.Add(1)
|
||||
email = fmt.Sprintf("%s.%s.%d@test.com", firstName, lastName, n)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var userID string
|
||||
err = pool.QueryRow(ctx, `
|
||||
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
|
||||
@@ -54,10 +55,10 @@ func createTestUser(pool *pgxpool.Pool, firstName, lastName, email, role string)
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func CreateTestService(pool *pgxpool.Pool) (string, error) {
|
||||
func CreateTestService(q db.Querier) (string, error) {
|
||||
ctx := context.Background()
|
||||
var serviceID string
|
||||
err := pool.QueryRow(ctx, `
|
||||
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
|
||||
@@ -72,12 +73,12 @@ func CreateTestService(pool *pgxpool.Pool) (string, error) {
|
||||
|
||||
// CreateTestServiceWithPatchTest creates a service and a patch test that links to it
|
||||
// Returns serviceID, patchTestID
|
||||
func CreateTestServiceWithPatchTest(pool *pgxpool.Pool) (string, string, error) {
|
||||
func CreateTestServiceWithPatchTest(q db.Querier) (string, string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// First create the service
|
||||
var serviceID string
|
||||
err := pool.QueryRow(ctx, `
|
||||
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
|
||||
@@ -89,7 +90,7 @@ func CreateTestServiceWithPatchTest(pool *pgxpool.Pool) (string, string, error)
|
||||
|
||||
// Now create a patch test that links to this service
|
||||
var patchTestID string
|
||||
err = pool.QueryRow(ctx, `
|
||||
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
|
||||
@@ -103,10 +104,10 @@ func CreateTestServiceWithPatchTest(pool *pgxpool.Pool) (string, string, error)
|
||||
}
|
||||
|
||||
// CreateTestPatchTest creates a patch test definition
|
||||
func CreateTestPatchTest(pool *pgxpool.Pool, serviceIDs []string) (string, error) {
|
||||
func CreateTestPatchTest(q db.Querier, serviceIDs []string) (string, error) {
|
||||
ctx := context.Background()
|
||||
var patchTestID string
|
||||
err := pool.QueryRow(ctx, `
|
||||
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
|
||||
@@ -120,9 +121,9 @@ func CreateTestPatchTest(pool *pgxpool.Pool, serviceIDs []string) (string, error
|
||||
}
|
||||
|
||||
// CreateUserPatchTest creates a user patch test record
|
||||
func CreateUserPatchTest(pool *pgxpool.Pool, userID, patchTestID string, testedAt string) error {
|
||||
func CreateUserPatchTest(q db.Querier, userID, patchTestID string, testedAt string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, `
|
||||
_, err := q.Exec(ctx, `
|
||||
INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at)
|
||||
VALUES ($1, $2, $3)
|
||||
`, userID, patchTestID, testedAt)
|
||||
@@ -134,14 +135,14 @@ func CreateUserPatchTest(pool *pgxpool.Pool, userID, patchTestID string, testedA
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateTestBooking(pool *pgxpool.Pool, userID, serviceID string) (string, error) {
|
||||
return CreateTestBookingAtTime(pool, userID, serviceID, time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC))
|
||||
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(pool *pgxpool.Pool, userID, serviceID string, startTime time.Time) (string, error) {
|
||||
func CreateTestBookingAtTime(q db.Querier, userID, serviceID string, startTime time.Time) (string, error) {
|
||||
ctx := context.Background()
|
||||
var bookingID string
|
||||
err := pool.QueryRow(ctx, `
|
||||
err := q.QueryRow(ctx, `
|
||||
INSERT INTO bookings (user_id, start_time, status, notes)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id
|
||||
@@ -151,7 +152,7 @@ func CreateTestBookingAtTime(pool *pgxpool.Pool, userID, serviceID string, start
|
||||
return "", fmt.Errorf("failed to create booking: %w", err)
|
||||
}
|
||||
|
||||
_, err = pool.Exec(ctx, `
|
||||
_, err = q.Exec(ctx, `
|
||||
INSERT INTO booking_services (booking_id, service_id)
|
||||
VALUES ($1, $2)
|
||||
`, bookingID, serviceID)
|
||||
@@ -163,40 +164,40 @@ func CreateTestBookingAtTime(pool *pgxpool.Pool, userID, serviceID string, start
|
||||
return bookingID, nil
|
||||
}
|
||||
|
||||
func CreateTestVerifiedUser(pool *pgxpool.Pool) (string, error) {
|
||||
return createTestUser(pool, "Verified", "User", "verified@test.com", "verified_email")
|
||||
func CreateTestVerifiedUser(q db.Querier) (string, error) {
|
||||
return createTestUser(q, "Verified", "User", "verified@test.com", "verified_email")
|
||||
}
|
||||
|
||||
func CreateTestUnverifiedUser(pool *pgxpool.Pool) (string, error) {
|
||||
return createTestUser(pool, "Unverified", "User", "unverified@test.com", "unverified_email")
|
||||
func CreateTestUnverifiedUser(q db.Querier) (string, error) {
|
||||
return createTestUser(q, "Unverified", "User", "unverified@test.com", "unverified_email")
|
||||
}
|
||||
|
||||
func CreateTestGuestUser(pool *pgxpool.Pool) (string, error) {
|
||||
return createTestUser(pool, "Guest", "User", "guest@test.com", "guest")
|
||||
func CreateTestGuestUser(q db.Querier) (string, error) {
|
||||
return createTestUser(q, "Guest", "User", "guest@test.com", "guest")
|
||||
}
|
||||
|
||||
func DeleteUser(pool *pgxpool.Pool, userID string) error {
|
||||
func DeleteUser(q db.Querier, userID string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, "DELETE FROM users WHERE id = $1", userID)
|
||||
_, err := q.Exec(ctx, "DELETE FROM users WHERE id = $1", userID)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteService(pool *pgxpool.Pool, serviceID string) error {
|
||||
func DeleteService(q db.Querier, serviceID string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, "DELETE FROM services WHERE id = $1", serviceID)
|
||||
_, err := q.Exec(ctx, "DELETE FROM services WHERE id = $1", serviceID)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteBooking(pool *pgxpool.Pool, bookingID string) error {
|
||||
func DeleteBooking(q db.Querier, bookingID string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, "DELETE FROM bookings WHERE id = $1", bookingID)
|
||||
_, err := q.Exec(ctx, "DELETE FROM bookings WHERE id = $1", bookingID)
|
||||
return err
|
||||
}
|
||||
|
||||
func CreateTestCustomService(pool *pgxpool.Pool) (string, error) {
|
||||
func CreateTestCustomService(q db.Querier) (string, error) {
|
||||
ctx := context.Background()
|
||||
var serviceID string
|
||||
err := pool.QueryRow(ctx, `
|
||||
err := q.QueryRow(ctx, `
|
||||
INSERT INTO custom_services (name, description, price, duration_minutes, minimum_age_required)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
@@ -209,33 +210,33 @@ func CreateTestCustomService(pool *pgxpool.Pool) (string, error) {
|
||||
return serviceID, nil
|
||||
}
|
||||
|
||||
func DeleteCustomService(pool *pgxpool.Pool, serviceID string) error {
|
||||
func DeleteCustomService(q db.Querier, serviceID string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, "DELETE FROM custom_services WHERE id = $1", serviceID)
|
||||
_, 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(db *pgxpool.Pool, userID string) error {
|
||||
return DeleteUser(db, userID)
|
||||
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(db *pgxpool.Pool, serviceID string) error {
|
||||
return DeleteService(db, serviceID)
|
||||
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(db *pgxpool.Pool, bookingID string) error {
|
||||
return DeleteBooking(db, bookingID)
|
||||
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(pool *pgxpool.Pool, startTime time.Time, durationMinutes int, description string) (string, error) {
|
||||
func CreateTestTimeBlocker(q db.Querier, startTime time.Time, durationMinutes int, description string) (string, error) {
|
||||
ctx := context.Background()
|
||||
var blockerID string
|
||||
err := pool.QueryRow(ctx, `
|
||||
err := q.QueryRow(ctx, `
|
||||
INSERT INTO time_blockers (start_time, duration_minutes, description)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id
|
||||
@@ -249,18 +250,18 @@ func CreateTestTimeBlocker(pool *pgxpool.Pool, startTime time.Time, durationMinu
|
||||
}
|
||||
|
||||
// DeleteTimeBlocker removes a time blocker from the database
|
||||
func DeleteTimeBlocker(pool *pgxpool.Pool, blockerID string) error {
|
||||
func DeleteTimeBlocker(q db.Querier, blockerID string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, "DELETE FROM time_blockers WHERE id = $1", blockerID)
|
||||
_, 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(db *pgxpool.Pool, bookingID string, amount float64, method string, ptype string, status string) (string, error) {
|
||||
func CreateTestPayment(q db.Querier, bookingID string, amount float64, method string, ptype string, status string) (string, error) {
|
||||
ctx := context.Background()
|
||||
var paymentID string
|
||||
err := db.QueryRow(ctx, `
|
||||
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
|
||||
@@ -275,10 +276,10 @@ func CreateTestPayment(db *pgxpool.Pool, bookingID string, amount float64, metho
|
||||
|
||||
// CreateTestRefund creates a refund record for testing
|
||||
// Returns refund ID
|
||||
func CreateTestRefund(db *pgxpool.Pool, paymentID string, bookingID string, amount float64) (string, error) {
|
||||
func CreateTestRefund(q db.Querier, paymentID string, bookingID string, amount float64) (string, error) {
|
||||
ctx := context.Background()
|
||||
var refundID string
|
||||
err := db.QueryRow(ctx, `
|
||||
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
|
||||
@@ -293,10 +294,10 @@ func CreateTestRefund(db *pgxpool.Pool, paymentID string, bookingID string, amou
|
||||
|
||||
// CreateTestPaymentMethod creates a saved card for a user
|
||||
// Returns card ID
|
||||
func CreateTestPaymentMethod(db *pgxpool.Pool, userID string, squareCardID string, brand string, last4 string) (string, error) {
|
||||
func CreateTestPaymentMethod(q db.Querier, userID string, squareCardID string, brand string, last4 string) (string, error) {
|
||||
ctx := context.Background()
|
||||
var cardID string
|
||||
err := db.QueryRow(ctx, `
|
||||
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
|
||||
@@ -310,22 +311,22 @@ func CreateTestPaymentMethod(db *pgxpool.Pool, userID string, squareCardID strin
|
||||
}
|
||||
|
||||
// DeletePayment deletes a payment from the database
|
||||
func DeletePayment(pool *pgxpool.Pool, paymentID string) error {
|
||||
func DeletePayment(q db.Querier, paymentID string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, "DELETE FROM payments WHERE id = $1", paymentID)
|
||||
_, err := q.Exec(ctx, "DELETE FROM payments WHERE id = $1", paymentID)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteRefund deletes a refund from the database
|
||||
func DeleteRefund(pool *pgxpool.Pool, refundID string) error {
|
||||
func DeleteRefund(q db.Querier, refundID string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, "DELETE FROM refunds WHERE id = $1", refundID)
|
||||
_, err := q.Exec(ctx, "DELETE FROM refunds WHERE id = $1", refundID)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeletePaymentMethod deletes a saved card from the database
|
||||
func DeletePaymentMethod(pool *pgxpool.Pool, cardID string) error {
|
||||
func DeletePaymentMethod(q db.Querier, cardID string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, "DELETE FROM user_saved_cards WHERE id = $1", cardID)
|
||||
_, err := q.Exec(ctx, "DELETE FROM user_saved_cards WHERE id = $1", cardID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,27 +11,20 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"crussell/db"
|
||||
"crussell/mw"
|
||||
"crussell/testutils/jwt"
|
||||
"crussell/testutils/testdb"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// SetupTestDB resets test data by truncating tables
|
||||
// Assumes db.DB is already set by TestMain
|
||||
func SetupTestDB(t *testing.T) func() {
|
||||
t.Helper()
|
||||
// contextKey matches mw.UserIDKey to avoid import cycle with auth package.
|
||||
type contextKey string
|
||||
|
||||
testdb.TruncateTables(t, db.DB)
|
||||
const userIDKey contextKey = "user_id"
|
||||
|
||||
return func() {}
|
||||
}
|
||||
|
||||
// MakeRequest makes an HTTP request to a handler with optional JWT token
|
||||
// token can be user token or admin token. Pass empty string for no auth.
|
||||
func MakeRequest(handler http.Handler, method, path string, body interface{}, token string) *httptest.ResponseRecorder {
|
||||
// MakeRequest makes an HTTP request to a handler with optional JWT token.
|
||||
// ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing.
|
||||
func MakeRequest(handler http.Handler, method, path string, body interface{}, token string, ctx context.Context) *httptest.ResponseRecorder {
|
||||
var req *http.Request
|
||||
if body != nil {
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
@@ -41,6 +34,8 @@ func MakeRequest(handler http.Handler, method, path string, body interface{}, to
|
||||
req = httptest.NewRequest(method, path, nil)
|
||||
}
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
@@ -68,25 +63,26 @@ func MakeRequestWithContext(handler http.Handler, method, path string, body inte
|
||||
return w
|
||||
}
|
||||
|
||||
// MakeUserRequest makes a request as an authenticated user
|
||||
// Generates a valid user token and includes it in the Authorization header
|
||||
func MakeUserRequest(handler http.Handler, method, path string, body interface{}, userID string) *httptest.ResponseRecorder {
|
||||
// MakeUserRequest makes a request as an authenticated user.
|
||||
// ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing.
|
||||
func MakeUserRequest(handler http.Handler, method, path string, body interface{}, userID string, ctx context.Context) *httptest.ResponseRecorder {
|
||||
token := jwt.GenerateUserToken(userID)
|
||||
return MakeRequest(handler, method, path, body, token)
|
||||
return MakeRequest(handler, method, path, body, token, ctx)
|
||||
}
|
||||
|
||||
// MakeAdminRequest makes a request as an authenticated admin
|
||||
// Generates a valid admin token and includes it in the Authorization header
|
||||
func MakeAdminRequest(handler http.Handler, method, path string, body interface{}, adminID string) *httptest.ResponseRecorder {
|
||||
// MakeAdminRequest makes a request as an authenticated admin.
|
||||
// ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing.
|
||||
func MakeAdminRequest(handler http.Handler, method, path string, body interface{}, adminID string, ctx context.Context) *httptest.ResponseRecorder {
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
return MakeRequest(handler, method, path, body, token)
|
||||
return MakeRequest(handler, method, path, body, token, ctx)
|
||||
}
|
||||
|
||||
// MakeContextRequest makes a request with user context set
|
||||
// Useful for testing handlers that check context before validating token
|
||||
func MakeContextRequest(handler http.Handler, method, path string, body interface{}, userID string) *httptest.ResponseRecorder {
|
||||
ctx := context.WithValue(context.Background(), mw.UserIDKey, userID)
|
||||
return MakeRequestWithContext(handler, method, path, body, ctx)
|
||||
// MakeContextRequest makes a request with user context set.
|
||||
// ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing.
|
||||
// The user ID is layered on top of the transaction context.
|
||||
func MakeContextRequest(handler http.Handler, method, path string, body interface{}, userID string, ctx context.Context) *httptest.ResponseRecorder {
|
||||
reqCtx := context.WithValue(ctx, userIDKey, userID)
|
||||
return MakeRequestWithContext(handler, method, path, body, reqCtx)
|
||||
}
|
||||
|
||||
// ParseResponseBody unmarshals the response body into dest
|
||||
@@ -156,9 +152,10 @@ func GetBodyAsJSON(w *httptest.ResponseRecorder) (map[string]interface{}, error)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// MakeRequestNoAuth makes an HTTP request without authentication (for testing unauthenticated endpoints)
|
||||
func MakeRequestNoAuth(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder {
|
||||
return MakeRequest(handler, method, path, body, "")
|
||||
// MakeRequestNoAuth makes an HTTP request without authentication.
|
||||
// ctx should carry a per-test transaction (from SetupTestTx) for PoolProxy routing.
|
||||
func MakeRequestNoAuth(handler http.Handler, method, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder {
|
||||
return MakeRequest(handler, method, path, body, "", ctx)
|
||||
}
|
||||
|
||||
// AssertErrorStatusCode checks status code and validates error is in response body
|
||||
|
||||
@@ -14,9 +14,16 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Querier is a minimal interface satisfied by *db.PoolProxy, *pgxpool.Pool,
|
||||
// and pgx.Tx. Defined locally to avoid an import cycle (db → testdb → db).
|
||||
type Querier interface {
|
||||
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTestDSN = "postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable"
|
||||
adminDatabaseDSN = "postgres://myuser:mypassword@localhost:5432/mydb?sslmode=disable"
|
||||
@@ -62,7 +69,13 @@ func CreateTestDatabase(dbName string) *pgxpool.Pool {
|
||||
adminPool.Close()
|
||||
|
||||
testDSN := fmt.Sprintf("postgres://myuser:mypassword@localhost:5432/%s?sslmode=disable", dbName)
|
||||
pool, err := pgxpool.New(ctx, testDSN)
|
||||
poolCfg, err := pgxpool.ParseConfig(testDSN)
|
||||
if err != nil {
|
||||
log.Fatalf("testdb: failed to parse config: %v", err)
|
||||
}
|
||||
poolCfg.MaxConns = 16
|
||||
poolCfg.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol
|
||||
pool, err := pgxpool.NewWithConfig(ctx, poolCfg)
|
||||
if err != nil {
|
||||
log.Fatalf("testdb: failed to connect to %s: %v", dbName, err)
|
||||
}
|
||||
@@ -170,6 +183,93 @@ func NewPool(dsn string) (*pgxpool.Pool, error) {
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// SeedBaseline populates the test database with reference data that is needed
|
||||
// by most tests but never changes across test runs (working hours, business
|
||||
// settings, etc.). Called once per TestMain AFTER migration but BEFORE m.Run().
|
||||
// Data is committed at the pool level and visible inside all per-test transactions
|
||||
// (PostgreSQL Read Committed isolation).
|
||||
func SeedBaseline(pool *pgxpool.Pool) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Working hours: Mon-Sun 08:00-20:00, all open.
|
||||
// Used by bookings, admin, and most handler tests that schedule appointments.
|
||||
hours := []struct {
|
||||
weekday int
|
||||
startTime string
|
||||
endTime string
|
||||
isOpen bool
|
||||
}{
|
||||
{0, "08:00", "20:00", true},
|
||||
{1, "08:00", "20:00", true},
|
||||
{2, "08:00", "20:00", true},
|
||||
{3, "08:00", "20:00", true},
|
||||
{4, "08:00", "20:00", true},
|
||||
{5, "08:00", "20:00", true},
|
||||
{6, "08:00", "20:00", true},
|
||||
}
|
||||
for _, h := range hours {
|
||||
_, err := pool.Exec(ctx, `
|
||||
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 {
|
||||
log.Fatalf("testdb: failed to seed working hours: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Business settings: required by admin/settings tests and booking deposit logic.
|
||||
_, err := pool.Exec(ctx, `
|
||||
INSERT INTO business_settings (business_name, business_address, currency_code, gift_card_expiry_months, voucher_type)
|
||||
VALUES ('Test Salon', '123 Test St', 'GBP', 12, 'SPV')
|
||||
ON CONFLICT DO NOTHING
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("testdb: failed to seed business settings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SeedBaselineScheduling is like SeedBaseline but uses working hours appropriate
|
||||
// for the scheduling package's tests (Mon-Fri 09:00-17:00, Sat 10:00-16:00, Sun closed).
|
||||
func SeedBaselineScheduling(pool *pgxpool.Pool) {
|
||||
ctx := context.Background()
|
||||
|
||||
hours := []struct {
|
||||
weekday int
|
||||
startTime string
|
||||
endTime string
|
||||
isOpen bool
|
||||
}{
|
||||
{0, "09:00", "17:00", true}, // Monday
|
||||
{1, "09:00", "17:00", true}, // Tuesday
|
||||
{2, "09:00", "17:00", true}, // Wednesday
|
||||
{3, "09:00", "17:00", true}, // Thursday
|
||||
{4, "09:00", "17:00", true}, // Friday
|
||||
{5, "10:00", "16:00", true}, // Saturday
|
||||
{6, "09:00", "17:00", false}, // Sunday (closed)
|
||||
}
|
||||
for _, h := range hours {
|
||||
_, err := pool.Exec(ctx, `
|
||||
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 {
|
||||
log.Fatalf("testdb: failed to seed scheduling working hours: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Business settings same as baseline.
|
||||
_, err := pool.Exec(ctx, `
|
||||
INSERT INTO business_settings (business_name, business_address, currency_code, gift_card_expiry_months, voucher_type)
|
||||
VALUES ('Test Salon', '123 Test St', 'GBP', 12, 'SPV')
|
||||
ON CONFLICT DO NOTHING
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("testdb: failed to seed business settings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// migratePool executes the full schema migration on the given pool.
|
||||
// This internal version returns errors directly for use by CreateTestDatabase.
|
||||
func migratePool(pool *pgxpool.Pool) error {
|
||||
@@ -287,41 +387,6 @@ func TxWithRollback(t *testing.T, pool *pgxpool.Pool) (pgx.Tx, func()) {
|
||||
}
|
||||
}
|
||||
|
||||
func TruncateTables(t *testing.T, pool *pgxpool.Pool) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
conn, err := pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to acquire connection for truncation: %v", err)
|
||||
}
|
||||
defer conn.Release()
|
||||
|
||||
// Single TRUNCATE with all tables — one round-trip instead of ~44.
|
||||
// CASCADE handles FK dependencies so order doesn't matter.
|
||||
// Advisory lock removed: each package has its own database now.
|
||||
_, err = conn.Exec(ctx, `
|
||||
TRUNCATE TABLE
|
||||
till_sales, admin_audit_log, gift_card_transactions,
|
||||
gift_card_expired_balances, booking_discounts, loyalty_redemptions,
|
||||
discount_campaigns, forgiven_no_shows, name_history, referral_discounts,
|
||||
user_social_logins, verification_codes, booking_custom_services,
|
||||
booking_services, refunds, payments, user_saved_cards, square_deposits,
|
||||
affiliate_payouts, financial_aggregates, gift_cards, user_giftcard_balances,
|
||||
bookings, booking_edit_requests, user_patch_tests, patch_tests, services,
|
||||
custom_services, admin_notifications, user_referrals,
|
||||
user_notification_preferences, time_blockers, working_hours,
|
||||
exceptional_group_applications, exceptional_working_hours,
|
||||
exceptional_working_hours_groups, business_settings, users, images, tags,
|
||||
login_audit, refresh_tokens, revoked_jtis
|
||||
CASCADE
|
||||
`)
|
||||
if err != nil {
|
||||
t.Logf("Warning: truncation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func FindInitScript() (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user