//go:build test && dev package payments import ( "strings" "testing" "github.com/stretchr/testify/require" ) // TestValidateAmount covers the amount validator used by every payment // handler: positive and within the £10,000 (1,000,000 pence) cap. Amounts are // integer pence, so sub-penny "precision" is impossible by construction. func TestValidateAmount(t *testing.T) { t.Parallel() valid := []int64{1, 500, 10000, 999999, 1000000} for _, amount := range valid { require.NoErrorf(t, ValidateAmount(amount), "expected %d to be a valid amount", amount) } invalid := []int64{0, -1, -500, 1000001} for _, amount := range invalid { require.Errorf(t, ValidateAmount(amount), "expected %d to be rejected", amount) } // The cap is exclusive: exactly £10,000 (1,000,000 pence) is allowed, one // penny more is rejected. if err := ValidateAmount(1000001); err == nil { t.Error("expected amount above £10,000 cap to be rejected") } if err := ValidateAmount(1000000); err != nil { t.Errorf("expected exactly £10,000 to be allowed, got %v", err) } } // TestValidateCardInfo_SCACoexistence pins the SCA saved-card wire contract on // the card-source validator: a saved-card reference (card_id or saved_card_id) // riding along WITH new_card_token is now VALID — the tokenize-result token is // the one-time charge source and the saved-card row supplies the customer // (resolveChargeSource implements the coexistence). A card reference alone // (legacy saved-card), a token alone (new-card), empty references, and neither // present keep their prior semantics. func TestValidateCardInfo_SCACoexistence(t *testing.T) { t.Parallel() cardID := "card-1" savedCardID := "card-2" token := "cnon:sca-tokenize" empty := "" // card_id + new_card_token — now valid (SCA tokenize-result source). require.NoError(t, ValidateCardInfo(&cardID, nil, &token), "card_id + new_card_token must be valid (SCA tokenize-result)") // saved_card_id + new_card_token — valid (SCA tokenize-result). require.NoError(t, ValidateCardInfo(nil, &savedCardID, &token), "saved_card_id + new_card_token must be valid (SCA tokenize-result)") // card_id only — valid legacy saved-card charge. require.NoError(t, ValidateCardInfo(&cardID, nil, nil), "card_id alone must remain valid") // new_card_token only — valid new-card charge. require.NoError(t, ValidateCardInfo(nil, nil, &token), "a bare new_card_token must remain valid (new-card path)") // Neither present — invalid. require.Error(t, ValidateCardInfo(nil, nil, nil), "neither a card reference nor new_card_token must be rejected") // Empty strings are treated as absent — invalid when nothing is present. require.Error(t, ValidateCardInfo(&empty, &empty, &empty), "empty-string values must be rejected as absent") require.Error(t, ValidateCardInfo(&empty, nil, &empty), "an empty card_id with an empty token must be rejected") // An empty card reference with a real token is still the valid new-card path. require.NoError(t, ValidateCardInfo(&empty, nil, &token), "an empty card_id with a real new_card_token is the new-card path and must be valid") } // TestValidateCardInfo_Table covers the full card-source shape space as a table: // the three legal wire shapes (saved-card reference only, new-card token only, // and both together as the SCA tokenize-result source) plus the both-absent and // empty-string rejections. This is the drift guard for the SCA wire contract — // every shape the frontend can send is enumerated here. func TestValidateCardInfo_Table(t *testing.T) { t.Parallel() cardID := "card-1" savedCardID := "saved-2" token := "cnon:sca-tokenize-3" empty := "" tests := []struct { name string cardID *string savedCard *string token *string wantValid bool }{ {"saved card reference (card_id) only — legacy saved-card charge", &cardID, nil, nil, true}, {"saved card reference (saved_card_id) only", nil, &savedCardID, nil, true}, {"new card token only — new-card charge", nil, nil, &token, true}, {"saved-card reference + new card token — SCA tokenize-result source", &cardID, nil, &token, true}, {"saved_card_id + new card token — SCA tokenize-result source", nil, &savedCardID, &token, true}, {"neither present — invalid", nil, nil, nil, false}, {"empty card_id + empty saved_card_id + empty token — all absent", &empty, &empty, &empty, false}, {"empty card_id + empty token — absent card ref", &empty, nil, &empty, false}, {"empty saved_card_id + empty token — absent card ref", nil, &empty, &empty, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateCardInfo(tt.cardID, tt.savedCard, tt.token) if tt.wantValid { require.NoErrorf(t, err, "expected shape to be valid: %v", tt) } else { require.Errorf(t, err, "expected shape to be rejected: %v", tt) } }) } } // TestValidatePartialAmount_PenceSemantics pins the pence comparison: amounts // are integer pence, so the boundary is exact — equal pence passes, one penny // over fails, and non-positive amounts are rejected regardless of the balance. func TestValidatePartialAmount_PenceSemantics(t *testing.T) { t.Parallel() tests := []struct { name string amountPence int64 remaining int64 wantValid bool }{ {"partial equal to the remaining balance passes", 2500, 2500, true}, {"partial below the remaining balance passes", 1000, 2500, true}, {"one penny over the remaining balance fails", 2501, 2500, false}, {"large partial over the balance fails", 5000, 2500, false}, {"zero amount rejected even with balance", 0, 2500, false}, {"negative amount rejected", -100, 2500, false}, {"zero remaining rejects any positive partial", 1, 0, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePartialAmount(tt.amountPence, tt.remaining) if tt.wantValid { require.NoErrorf(t, err, "expected %d pence against %d remaining to be valid", tt.amountPence, tt.remaining) } else { require.Errorf(t, err, "expected %d pence against %d remaining to be rejected", tt.amountPence, tt.remaining) } }) } } // TestValidateVerificationToken_Bound pins the 512-char bound on Square's // verification token: empty/nil is fine (token-less charges are valid input), // anything at or under 512 chars passes, and 513+ is rejected. func TestValidateVerificationToken_Bound(t *testing.T) { t.Parallel() ok := strings.Repeat("t", 512) tooLong := strings.Repeat("t", 513) tests := []struct { name string token *string wantErr bool }{ {"nil token passes (token-less is valid)", nil, false}, {"empty token passes", strPtr(""), false}, {"exactly 512 chars passes", &ok, false}, {"513 chars rejected", &tooLong, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateVerificationToken(tt.token) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } }) } }