Mirror Square's customer_id requirement in the dev mock card-creation gate
Square rejects POST /v2/cards without card.customer_id at runtime (confirmed by Square's SDK maintainer); the production client omits an empty id via omitempty and every caller provisions a customer first, so the mock now enforces the same structured 400 INVALID_REQUEST_ERROR to keep sandbox/dev parity with the gate the production code depends on.
This commit is contained in:
@@ -457,6 +457,21 @@ func (m *MockClient) CreateCardOnFile(ctx context.Context, userID, cardToken, cu
|
||||
return nil, fmt.Errorf("invalid source_id: %s — use a card nonce (cnon:xxx) or card ID (ccof:xxx)", tokenPrefix(cardToken))
|
||||
}
|
||||
|
||||
// Square's POST /v2/cards rejects a card without card.customer_id at
|
||||
// runtime (confirmed by Square's own SDK maintainer). The production client
|
||||
// omits an empty customer_id via omitempty and every production caller
|
||||
// provisions a Square customer first, so the gate is enforced upstream — the
|
||||
// mock must mirror it (same structured INVALID_REQUEST_ERROR as the ccof:
|
||||
// CreatePayment gate above) so sandbox/dev tests exercise the same rejection.
|
||||
if customerID == "" {
|
||||
return nil, &squareAPIError{
|
||||
Code: "INVALID_REQUEST_ERROR",
|
||||
Detail: "customer_id is required to create a card on file",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
err: errors.New("square: customer_id is required to create a card on file"),
|
||||
}
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ func TestDevClient_CardOnFile_CreateAndGet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userID := "user-test-123"
|
||||
|
||||
card, err := client.CreateCardOnFile(ctx, userID, "cnon:test-token", "")
|
||||
card, err := client.CreateCardOnFile(ctx, userID, "cnon:test-token", "cus_test123")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, card.ID)
|
||||
@@ -211,10 +211,10 @@ func TestDevClient_CardOnFile_MultipleCards(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userID := "user-test-multiple"
|
||||
|
||||
card1, err := client.CreateCardOnFile(ctx, userID, "cnon:token-1", "")
|
||||
card1, err := client.CreateCardOnFile(ctx, userID, "cnon:token-1", "cus_test123")
|
||||
require.NoError(t, err)
|
||||
|
||||
card2, err := client.CreateCardOnFile(ctx, userID, "cnon:token-2", "")
|
||||
card2, err := client.CreateCardOnFile(ctx, userID, "cnon:token-2", "cus_test123")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, card1.Enabled)
|
||||
@@ -236,7 +236,7 @@ func TestDevClient_CardOnFile_Delete(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userID := "user-test-delete"
|
||||
|
||||
card, err := client.CreateCardOnFile(ctx, userID, "cnon:token-delete", "")
|
||||
card, err := client.CreateCardOnFile(ctx, userID, "cnon:token-delete", "cus_test123")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.DeleteCardOnFile(ctx, card.ID)
|
||||
@@ -286,7 +286,7 @@ func TestDevClient_CreateCardOnFile_RejectsRawPAN(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
card, err := client.CreateCardOnFile(ctx, "user-raw-"+tt.name, tt.cardNumber, "")
|
||||
card, err := client.CreateCardOnFile(ctx, "user-raw-"+tt.name, tt.cardNumber, "cus_test123")
|
||||
require.Error(t, err, "raw PAN must be rejected for production parity")
|
||||
assert.Nil(t, card)
|
||||
assert.Contains(t, err.Error(), "invalid source_id")
|
||||
@@ -294,6 +294,30 @@ func TestDevClient_CreateCardOnFile_RejectsRawPAN(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDevClient_CreateCardOnFile_RequiresCustomerID verifies the mock mirrors
|
||||
// Square's real enforcement: Square's POST /v2/cards rejects a card without
|
||||
// card.customer_id at runtime (confirmed by Square's own SDK maintainer). The
|
||||
// production client omits an empty customer_id via omitempty and every
|
||||
// production caller provisions a customer first, so the mock must reject it too
|
||||
// — the same structured 400 INVALID_REQUEST_ERROR as the ccof: CreatePayment
|
||||
// gate — so sandbox/dev tests exercise the same rejection as production.
|
||||
func TestDevClient_CreateCardOnFile_RequiresCustomerID(t *testing.T) {
|
||||
client := NewDevClient().(*MockClient)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.CreateCardOnFile(ctx, "user-no-customer", "cnon:test-token", "")
|
||||
require.Error(t, err, "card creation without customer_id must be rejected")
|
||||
assert.Equal(t, "INVALID_REQUEST_ERROR", ErrorCode(err))
|
||||
assert.Contains(t, ErrorDetail(err), "customer_id")
|
||||
assert.Equal(t, http.StatusBadRequest, ErrorStatusCode(err))
|
||||
|
||||
card, err := client.CreateCardOnFile(ctx, "user-with-customer", "cnon:test-token", "cus_test123")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, card.ID)
|
||||
assert.Equal(t, "VISA", card.Brand)
|
||||
assert.Equal(t, "4242", card.Last4)
|
||||
}
|
||||
|
||||
func TestCreatePayment_ShouldFail(t *testing.T) {
|
||||
client := NewDevClient().(*MockClient)
|
||||
client.ShouldFail = true
|
||||
@@ -564,7 +588,7 @@ func TestDevClient_CreateCardOnFile_WithNewFields(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userID := "user-new-fields"
|
||||
|
||||
card, err := client.CreateCardOnFile(ctx, userID, "cnon:test-token", "")
|
||||
card, err := client.CreateCardOnFile(ctx, userID, "cnon:test-token", "cus_test123")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, card.Enabled)
|
||||
@@ -583,7 +607,7 @@ func TestDevClient_DeleteCardOnFile_SoftDelete(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userID := "user-soft-delete"
|
||||
|
||||
card, err := client.CreateCardOnFile(ctx, userID, "cnon:token-soft", "")
|
||||
card, err := client.CreateCardOnFile(ctx, userID, "cnon:token-soft", "cus_test123")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.DeleteCardOnFile(ctx, card.ID)
|
||||
|
||||
Reference in New Issue
Block a user