test: add coverage tests across backend + fix mock for PENDING checkout support
CI / Nginx config check (push) Successful in 13s
CI / Env docs check (push) Successful in 15s
CI / Docker compose check (push) Successful in 15s
CI / Frontend major deps (push) Failing after 24s
CI / Frontend deps check (push) Successful in 30s
CI / Secrets scan (push) Successful in 38s
CI / Go build (push) Successful in 39s
CI / Frontend build (push) Successful in 1m3s
CI / Knip (push) Successful in 45s
CI / Go vet (prod) (push) Failing after 1m42s
CI / Frontend a11y check (push) Successful in 2m34s
CI / Go vet (dev) (push) Successful in 2m29s
CI / Staticcheck (prod) (push) Failing after 2m38s
CI / go mod tidy (push) Successful in 1m3s
CI / Staticcheck (dev) (push) Successful in 2m55s
CI / Frontend QC (audit) (push) Successful in 51s
CI / golangci-lint (push) Successful in 3m22s
CI / Go vulnerabilities (push) Successful in 1m26s
CI / Frontend QC (typecheck) (push) Successful in 2m18s
CI / Security scan (prod) (push) Successful in 4m18s
CI / Security scan (dev) (push) Successful in 4m40s
CI / Tests (prod) (push) Has been skipped
CI / Tests (dev) (push) Has been skipped
CI / Race (prod) (push) Has been skipped
CI / Race (dev) (push) Has been skipped
CI / Frontend QC (lint) (push) Successful in 2m18s
CI / Svelte strict check (push) Successful in 43s
CI / Nginx config check (push) Successful in 13s
CI / Env docs check (push) Successful in 15s
CI / Docker compose check (push) Successful in 15s
CI / Frontend major deps (push) Failing after 24s
CI / Frontend deps check (push) Successful in 30s
CI / Secrets scan (push) Successful in 38s
CI / Go build (push) Successful in 39s
CI / Frontend build (push) Successful in 1m3s
CI / Knip (push) Successful in 45s
CI / Go vet (prod) (push) Failing after 1m42s
CI / Frontend a11y check (push) Successful in 2m34s
CI / Go vet (dev) (push) Successful in 2m29s
CI / Staticcheck (prod) (push) Failing after 2m38s
CI / go mod tidy (push) Successful in 1m3s
CI / Staticcheck (dev) (push) Successful in 2m55s
CI / Frontend QC (audit) (push) Successful in 51s
CI / golangci-lint (push) Successful in 3m22s
CI / Go vulnerabilities (push) Successful in 1m26s
CI / Frontend QC (typecheck) (push) Successful in 2m18s
CI / Security scan (prod) (push) Successful in 4m18s
CI / Security scan (dev) (push) Successful in 4m40s
CI / Tests (prod) (push) Has been skipped
CI / Tests (dev) (push) Has been skipped
CI / Race (prod) (push) Has been skipped
CI / Race (dev) (push) Has been skipped
CI / Frontend QC (lint) (push) Successful in 2m18s
CI / Svelte strict check (push) Successful in 43s
New test files cover previously untested paths across DAV, validators, S3, Square, mw, bookings, user, and payments packages. Includes mock fix: HoldCheckouts flag on MockClient allows tests to pause auto-complete goroutine for testing PENDING checkout states. Coverage: 50.4% → 65.0% (+14.6pp)
This commit is contained in:
@@ -1351,6 +1351,732 @@ func TestGetGiftCards_InventoryFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GetGiftCardBalance — GET /api/user/giftcards/balance
|
||||
// =============================================================================
|
||||
|
||||
func TestGetGiftCardBalance_HappyPath(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, `INSERT INTO user_giftcard_balances (user_id, balance) VALUES ($1, 75.50)`, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to insert balance: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/user/giftcards/balance", nil)
|
||||
reqCtx := context.WithValue(req.Context(), mw.UserIDKey, userID)
|
||||
reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "verified_email")
|
||||
reqCtx = db.ContextWithTx(reqCtx, tx.(pgx.Tx))
|
||||
req = req.WithContext(reqCtx)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
GetGiftCardBalance(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]float64
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
if resp["balance"] != 75.50 {
|
||||
t.Errorf("expected balance 75.50, got %.2f", resp["balance"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGiftCardBalance_NoBalance(t *testing.T) {
|
||||
_, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/user/giftcards/balance", nil)
|
||||
reqCtx := context.WithValue(req.Context(), mw.UserIDKey, userID)
|
||||
reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "verified_email")
|
||||
req = req.WithContext(reqCtx)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
GetGiftCardBalance(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]float64
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
if resp["balance"] != 0.00 {
|
||||
t.Errorf("expected balance 0.00, got %.2f", resp["balance"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGiftCardBalance_Unauthenticated(t *testing.T) {
|
||||
_, _ = testutils.SetupTestTx(t)
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/user/giftcards/balance", nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
GetGiftCardBalance(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TransferGiftCard — Additional edge cases
|
||||
// =============================================================================
|
||||
|
||||
func TestTransferGiftCard_SameCardRejected(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
// Create a gift card.
|
||||
var cardID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
|
||||
VALUES (100.00, 100.00, $1)
|
||||
RETURNING id
|
||||
`, adminID).Scan(&cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create gift card: %v", err)
|
||||
}
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"to_card_id": cardID,
|
||||
"amount": 30.00,
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/admin/gift-cards/"+cardID+"/transfer", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/admin/gift-cards/{from}/transfer", TransferGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400 for same-card transfer, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferGiftCard_SourceNotFound(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
// Create a destination card.
|
||||
var card2ID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
|
||||
VALUES (20.00, 20.00, $1)
|
||||
RETURNING id
|
||||
`, adminID).Scan(&card2ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create destination card: %v", err)
|
||||
}
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"to_card_id": card2ID,
|
||||
"amount": 10.00,
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/admin/gift-cards/aaaaaaaaaaaa/transfer", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/admin/gift-cards/{from}/transfer", TransferGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferGiftCard_DestinationNotFound(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
// Create a source card.
|
||||
var card1ID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
|
||||
VALUES (100.00, 100.00, $1)
|
||||
RETURNING id
|
||||
`, adminID).Scan(&card1ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create source card: %v", err)
|
||||
}
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"to_card_id": "bbbbbbbbbbbb",
|
||||
"amount": 10.00,
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/admin/gift-cards/"+card1ID+"/transfer", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/admin/gift-cards/{from}/transfer", TransferGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferGiftCard_InsufficientBalance(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
var card1ID, card2ID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
|
||||
VALUES (10.00, 10.00, $1)
|
||||
RETURNING id
|
||||
`, adminID).Scan(&card1ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create source card: %v", err)
|
||||
}
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
|
||||
VALUES (20.00, 20.00, $1)
|
||||
RETURNING id
|
||||
`, adminID).Scan(&card2ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create destination card: %v", err)
|
||||
}
|
||||
|
||||
// Try to transfer more than available.
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"to_card_id": card2ID,
|
||||
"amount": 50.00,
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/admin/gift-cards/"+card1ID+"/transfer", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/admin/gift-cards/{from}/transfer", TransferGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RedeemGiftCard — Additional edge cases
|
||||
// =============================================================================
|
||||
|
||||
func TestRedeemGiftCard_AlreadyRedeemed(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
token := jwt.GenerateTestToken(userID, "verified_email")
|
||||
|
||||
// Create a gift card that is already redeemed.
|
||||
var cardID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, redeemed_by, redeemed_at)
|
||||
VALUES (100.00, 0, $1, NOW())
|
||||
RETURNING id
|
||||
`, userID).Scan(&cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create redeemed gift card: %v", err)
|
||||
}
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{"code": cardID})
|
||||
req := httptest.NewRequest("POST", "/api/user/giftcards/redeem", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/user/giftcards/redeem", RedeemGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedeemGiftCard_NotFound(t *testing.T) {
|
||||
_, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
token := jwt.GenerateTestToken(userID, "verified_email")
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{"code": "cccccccccccc"})
|
||||
req := httptest.NewRequest("POST", "/api/user/giftcards/redeem", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/user/giftcards/redeem", RedeemGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedeemGiftCard_InvalidCode(t *testing.T) {
|
||||
_, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
token := jwt.GenerateTestToken(userID, "verified_email")
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{"code": "$$$"})
|
||||
req := httptest.NewRequest("POST", "/api/user/giftcards/redeem", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/user/giftcards/redeem", RedeemGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedeemGiftCard_ZeroBalance(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
token := jwt.GenerateTestToken(userID, "verified_email")
|
||||
|
||||
// Create a gift card with zero remaining balance (but not redeemed).
|
||||
var cardID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO gift_cards (total_funds_added, amount_remaining)
|
||||
VALUES (0, 0)
|
||||
RETURNING id
|
||||
`).Scan(&cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create zero-balance gift card: %v", err)
|
||||
}
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{"code": cardID})
|
||||
req := httptest.NewRequest("POST", "/api/user/giftcards/redeem", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/user/giftcards/redeem", RedeemGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TopUpGiftCard — Additional edge cases
|
||||
// =============================================================================
|
||||
|
||||
func TestTopUpGiftCard_NotFound(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"amount": 25.00,
|
||||
"payment_method": "on_the_house",
|
||||
})
|
||||
req := httptest.NewRequest("PUT", "/api/admin/gift-cards/dddddddddddd/topup", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Put("/api/admin/gift-cards/{id}/topup", TopUpGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopUpGiftCard_NegativeAmount(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
var cardID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
|
||||
VALUES (50.00, 50.00, $1)
|
||||
RETURNING id
|
||||
`, adminID).Scan(&cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create gift card: %v", err)
|
||||
}
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"amount": -10.00,
|
||||
"payment_method": "on_the_house",
|
||||
})
|
||||
req := httptest.NewRequest("PUT", "/api/admin/gift-cards/"+cardID+"/topup", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Put("/api/admin/gift-cards/{id}/topup", TopUpGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopUpGiftCard_ZeroAmount(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
var cardID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by)
|
||||
VALUES (50.00, 50.00, $1)
|
||||
RETURNING id
|
||||
`, adminID).Scan(&cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create gift card: %v", err)
|
||||
}
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"amount": 0,
|
||||
"payment_method": "on_the_house",
|
||||
})
|
||||
req := httptest.NewRequest("PUT", "/api/admin/gift-cards/"+cardID+"/topup", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Put("/api/admin/gift-cards/{id}/topup", TopUpGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopUpGiftCard_InventoryCardFirstTopUp(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
// Create an inventory card with zero balance.
|
||||
var cardID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory)
|
||||
VALUES (0, 0, $1, TRUE)
|
||||
RETURNING id
|
||||
`, adminID).Scan(&cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create inventory card: %v", err)
|
||||
}
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"amount": 30.00,
|
||||
"payment_method": "on_the_house",
|
||||
})
|
||||
req := httptest.NewRequest("PUT", "/api/admin/gift-cards/"+cardID+"/topup", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Put("/api/admin/gift-cards/{id}/topup", TopUpGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var gc GiftCard
|
||||
if err := json.NewDecoder(w.Body).Decode(&gc); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if gc.TotalFundsAdded != 30.00 || gc.AmountRemaining != 30.00 {
|
||||
t.Errorf("expected added and remaining 30.00, got added=%.2f remaining=%.2f", gc.TotalFundsAdded, gc.AmountRemaining)
|
||||
}
|
||||
|
||||
// Verify transaction type is 'purchase' for first top-up on inventory card.
|
||||
var txType string
|
||||
err = tx.QueryRow(ctx, "SELECT transaction_type FROM gift_card_transactions WHERE gift_card_id = $1", cardID).Scan(&txType)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to query transaction: %v", err)
|
||||
}
|
||||
if txType != "purchase" {
|
||||
t.Errorf("expected transaction type 'purchase' for first inventory top-up, got %q", txType)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BuyGiftCard — Additional edge cases
|
||||
// =============================================================================
|
||||
|
||||
func TestBuyGiftCard_InvalidAmount(t *testing.T) {
|
||||
_, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
token := jwt.GenerateTestToken(userID, "verified_email")
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"amount": 7500,
|
||||
"recipient_type": "self",
|
||||
"new_card_token": "cnon:card-nonce-ok",
|
||||
"idempotency_key": "idempotency-invalid-amount",
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/user/giftcards/buy", BuyGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuyGiftCard_InvalidRecipientType(t *testing.T) {
|
||||
_, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
token := jwt.GenerateTestToken(userID, "verified_email")
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"amount": 2000,
|
||||
"recipient_type": "invalid",
|
||||
"new_card_token": "cnon:card-nonce-ok",
|
||||
"idempotency_key": "idempotency-invalid-recipient",
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/user/giftcards/buy", BuyGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuyGiftCard_CardNotFound(t *testing.T) {
|
||||
_, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
token := jwt.GenerateTestToken(userID, "verified_email")
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"amount": 2000,
|
||||
"recipient_type": "self",
|
||||
"card_id": "nonexistent-card-id",
|
||||
"idempotency_key": "idempotency-card-not-found",
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/user/giftcards/buy", BuyGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuyGiftCard_NoCardInfo(t *testing.T) {
|
||||
_, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
token := jwt.GenerateTestToken(userID, "verified_email")
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"amount": 2000,
|
||||
"recipient_type": "self",
|
||||
"idempotency_key": "idempotency-no-card",
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/user/giftcards/buy", BuyGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuyGiftCard_Unauthenticated(t *testing.T) {
|
||||
_, _ = testutils.SetupTestTx(t)
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"amount": 2000,
|
||||
"recipient_type": "self",
|
||||
"new_card_token": "cnon:card-nonce-ok",
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/user/giftcards/buy", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Post("/api/user/giftcards/buy", BuyGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TestGetUserGiftCardBalanceAdmin_AuditLog verifies admin balance checks
|
||||
// are recorded in the admin_audit_log table.
|
||||
func TestGetUserGiftCardBalanceAdmin_AuditLog(t *testing.T) {
|
||||
@@ -1419,3 +2145,127 @@ func TestGetUserGiftCardBalanceAdmin_AuditLog(t *testing.T) {
|
||||
t.Errorf("expected 1 audit log entry, got %d", logCount)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TransferGiftCard — Validation gap tests
|
||||
// =============================================================================
|
||||
|
||||
func TestTransferGiftCard_InvalidFromCardID(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"to_card_id": "aaaaaaaaaaaa",
|
||||
"amount": 10.00,
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/admin/gift-cards/$$$/transfer", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/admin/gift-cards/{from}/transfer", TransferGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferGiftCard_InvalidToCardID(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"to_card_id": "$$$",
|
||||
"amount": 10.00,
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/admin/gift-cards/aaaaaaaaaaaa/transfer", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/admin/gift-cards/{from}/transfer", TransferGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferGiftCard_ZeroAmount(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||
"to_card_id": "aaaaaaaaaaaa",
|
||||
"amount": 0,
|
||||
})
|
||||
req := httptest.NewRequest("POST", "/api/admin/gift-cards/aaaaaaaaaaaa/transfer", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/admin/gift-cards/{from}/transfer", TransferGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferGiftCard_JSONDecodeError(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
_, _ = tx.Exec(ctx, "UPDATE users SET account_role = 'admin' WHERE id = $1", adminID)
|
||||
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/admin/gift-cards/aaaaaaaaaaaa/transfer", bytes.NewBuffer([]byte(`{invalid}`)))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(db.ContextWithTx(req.Context(), tx.(pgx.Tx)))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.RequireAuth)
|
||||
r.Post("/api/admin/gift-cards/{from}/transfer", TransferGiftCard)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user