Match dev mock to production: reject raw PAN card creation (PCI-DSS parity)

Previously the dev MockClient was more permissive than production:
- MockClient.CreateCardOnFileRaw processed raw PANs and stored mock cards,
  while ProdClient and devProdClient both block raw PANs. A dev testing the
  raw-card flow saw it succeed, masking a production failure.
- MockClient.CreateCardOnFile accepted raw PANs as source_id via an
  isAllDigits branch. Real Square only accepts cnon:xxx/ccof:xxx tokens.

Now the mock behaves identically to production:
- CreateCardOnFileRaw returns the same PCI error as ProdClient
- CreateCardOnFile validates source_id is token-like (cnon:/ccof:) and
  rejects raw PANs
- Removed dead isAllDigits helper

Tests updated to assert the parity behavior:
- TestDevClient_CreateCardOnFileRaw_Rejected_ProdParity (table-driven,
  replaces 5 brand-specific raw-PAN tests)
- TestDevClient_CreateCardOnFile_RejectsRawPAN (replaces RawNumber)
- TestCreatePaymentMethod_HappyPath / SecondCardNotDefault now expect
  500 instead of 200, documenting the prod block
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent 73dd2c2dea
commit bbb55dae82
3 changed files with 61 additions and 213 deletions
+8 -44
View File
@@ -2153,24 +2153,10 @@ func TestCreatePaymentMethod_HappyPath(t *testing.T) {
w := makePaymentRequest(handler, "POST", "/api/user/payment-methods", reqBody, token, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
return
}
var card SavedCard
if err := json.Unmarshal(w.Body.Bytes(), &card); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if card.Brand != "VISA" {
t.Errorf("expected brand VISA, got %s", card.Brand)
}
if card.Last4 != "1111" {
t.Errorf("expected last4 1111, got %s", card.Last4)
}
if !card.IsDefault {
t.Error("expected first card to be default")
// PCI-DSS parity: raw PAN card creation is blocked in production, so the
// mock must reject it too — otherwise dev testing masks a prod failure.
if w.Code != http.StatusInternalServerError {
t.Errorf("expected status 500 (raw PAN rejected), got %d. body: %s", w.Code, w.Body.String())
}
}
@@ -2296,7 +2282,8 @@ func TestCreatePaymentMethod_SecondCardNotDefault(t *testing.T) {
token := jwt.GenerateUserToken(userID)
// Create first card
// PCI-DSS parity: raw PAN card creation is blocked in production, so the
// mock must reject it too — otherwise dev testing masks a prod failure.
handler := CreatePaymentMethod
reqBody := CreatePaymentMethodRequest{
CardNumber: "4111111111111111",
@@ -2305,31 +2292,8 @@ func TestCreatePaymentMethod_SecondCardNotDefault(t *testing.T) {
}
w := makePaymentRequest(handler, "POST", "/api/user/payment-methods", reqBody, token, ctx)
if w.Code != http.StatusOK {
t.Fatalf("failed to create first card: %d. body: %s", w.Code, w.Body.String())
}
reqBody2 := CreatePaymentMethodRequest{
CardNumber: "5500000000000004",
Expiry: "06/30",
CVC: "456",
}
w = makePaymentRequest(handler, "POST", "/api/user/payment-methods", reqBody2, token, ctx)
if w.Code != http.StatusOK {
t.Fatalf("failed to create second card: %d. body: %s", w.Code, w.Body.String())
}
var card SavedCard
if err := json.Unmarshal(w.Body.Bytes(), &card); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if card.Brand != "MASTERCARD" {
t.Errorf("expected brand MASTERCARD, got %s", card.Brand)
}
if card.IsDefault {
t.Error("expected second card to NOT be default")
if w.Code != http.StatusInternalServerError {
t.Fatalf("expected 500 (raw PAN rejected), got %d. body: %s", w.Code, w.Body.String())
}
}