mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-25 02:52:20 +02:00
* feat(sca): sweep abandoned 3ds requires_action orders to failed * fix(sca): address pr review on abandoned 3ds sweep - Drop stripe.Key write in ReconcileStale3DSOrders; wire once in api/main.go after cfg.Load so concurrent reconcilers don't race the SDK global. - Add empty-PI short-circuit before paymentintent.Get to bury a future regression in a slog.Warn instead of a Stripe 4xx. - Swap fmt.Printf for slog.Warn with k/v attrs in loadAbandoned3DSThresholdMinutes so Loki sees structured fields instead of literal %q/%d. - Comment the updated_at predicate (RetryAuth bumps it via GORM auto-stamp, intentional grace-window extension) + the conditional-UPDATE/map-form choice. - Tests: processing/requires_capture skip, unknown-status skip, stripe-error skip, empty-PI skip, full env-parse table for loadAbandoned3DSThresholdMinutes. * fix(stripe): wire key once at init drop per-call writes * refactor(stripe): drop dead stripeKey fields and pre-call sdk params
171 lines
4.5 KiB
Go
171 lines
4.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"terminalShop/pkg/database"
|
|
"terminalShop/pkg/models"
|
|
)
|
|
|
|
func setupOrderTestDB(t *testing.T) models.User {
|
|
t.Helper()
|
|
testDB := filepath.Join(t.TempDir(), "orders.db")
|
|
database.ResetForTesting()
|
|
t.Cleanup(database.ResetForTesting)
|
|
|
|
db, err := database.Connect(testDB)
|
|
if err != nil {
|
|
t.Fatalf("failed to connect to test database: %v", err)
|
|
}
|
|
if err := database.Migrate(db); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
if err := database.Seed(db); err != nil {
|
|
t.Fatalf("failed to seed: %v", err)
|
|
}
|
|
|
|
user := models.User{
|
|
SSHKeyFingerprint: "SHA256:ordertestfingerprint",
|
|
SSHPublicKey: "ssh-ed25519 AAAA ordertestkey",
|
|
Name: "Order Test User",
|
|
Email: "orders@example.com",
|
|
}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
func orderRequest(method, url string, body []byte, userID uint, orderID string) *http.Request {
|
|
req := authRequest(method, url, body, userID)
|
|
routeCtx := chi.NewRouteContext()
|
|
routeCtx.URLParams.Add("id", orderID)
|
|
return req.WithContext(contextWithRoute(req.Context(), routeCtx))
|
|
}
|
|
|
|
func contextWithRoute(ctx context.Context, routeCtx *chi.Context) context.Context {
|
|
return context.WithValue(ctx, chi.RouteCtxKey, routeCtx)
|
|
}
|
|
|
|
func TestCreateRefundRequestRequiresMessageForOther(t *testing.T) {
|
|
user := setupOrderTestDB(t)
|
|
|
|
handler := NewOrderHandler("", "", "")
|
|
body := []byte(`{"reason":"Other","message":" "}`)
|
|
req := orderRequest("POST", "/api/v1/orders/1/refund-request", body, user.ID, "1")
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.CreateRefundRequest(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status 400, got %d", w.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Error struct {
|
|
Code string `json:"code"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if resp.Error.Code != "MESSAGE_REQUIRED" {
|
|
t.Errorf("expected MESSAGE_REQUIRED, got %q", resp.Error.Code)
|
|
}
|
|
}
|
|
|
|
func TestCreateRefundRequestRejectsInvalidOrderState(t *testing.T) {
|
|
user := setupOrderTestDB(t)
|
|
|
|
db := database.GetDB()
|
|
order := models.Order{
|
|
UserID: user.ID,
|
|
CardID: 1,
|
|
Status: models.OrderStatusFailed,
|
|
Subtotal: 100,
|
|
ShippingCost: 0,
|
|
Total: 100,
|
|
}
|
|
if err := db.Create(&order).Error; err != nil {
|
|
t.Fatalf("failed to create order: %v", err)
|
|
}
|
|
|
|
handler := NewOrderHandler("", "", "")
|
|
body := []byte(`{"reason":"Quality issue","message":""}`)
|
|
req := orderRequest("POST", "/api/v1/orders/1/refund-request", body, user.ID, fmt.Sprintf("%d", order.ID))
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.CreateRefundRequest(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status 400, got %d", w.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Error struct {
|
|
Code string `json:"code"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if resp.Error.Code != "INVALID_STATE" {
|
|
t.Errorf("expected INVALID_STATE, got %q", resp.Error.Code)
|
|
}
|
|
}
|
|
|
|
func TestCreateRefundRequestEnforcesCooldown(t *testing.T) {
|
|
user := setupOrderTestDB(t)
|
|
|
|
db := database.GetDB()
|
|
recent := time.Now().Add(-1 * time.Minute)
|
|
order := models.Order{
|
|
UserID: user.ID,
|
|
CardID: 1,
|
|
Status: models.OrderStatusPaid,
|
|
Subtotal: 100,
|
|
ShippingCost: 0,
|
|
Total: 100,
|
|
LastRefundRequestAt: &recent,
|
|
}
|
|
if err := db.Create(&order).Error; err != nil {
|
|
t.Fatalf("failed to create order: %v", err)
|
|
}
|
|
|
|
handler := NewOrderHandler("", "", "")
|
|
body := []byte(`{"reason":"Quality issue","message":""}`)
|
|
req := orderRequest("POST", "/api/v1/orders/1/refund-request", body, user.ID, fmt.Sprintf("%d", order.ID))
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.CreateRefundRequest(w, req)
|
|
|
|
if w.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("expected status 429, got %d", w.Code)
|
|
}
|
|
if got := w.Header().Get("Retry-After"); got == "" {
|
|
t.Errorf("expected Retry-After header, got empty")
|
|
}
|
|
|
|
var resp struct {
|
|
Error struct {
|
|
Code string `json:"code"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if resp.Error.Code != "RATE_LIMITED" {
|
|
t.Errorf("expected RATE_LIMITED, got %q", resp.Error.Code)
|
|
}
|
|
}
|