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))
|
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()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ func TestDevClient_CardOnFile_CreateAndGet(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userID := "user-test-123"
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotEmpty(t, card.ID)
|
assert.NotEmpty(t, card.ID)
|
||||||
@@ -211,10 +211,10 @@ func TestDevClient_CardOnFile_MultipleCards(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userID := "user-test-multiple"
|
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)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, card1.Enabled)
|
assert.True(t, card1.Enabled)
|
||||||
@@ -236,7 +236,7 @@ func TestDevClient_CardOnFile_Delete(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userID := "user-test-delete"
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = client.DeleteCardOnFile(ctx, card.ID)
|
err = client.DeleteCardOnFile(ctx, card.ID)
|
||||||
@@ -286,7 +286,7 @@ func TestDevClient_CreateCardOnFile_RejectsRawPAN(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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")
|
require.Error(t, err, "raw PAN must be rejected for production parity")
|
||||||
assert.Nil(t, card)
|
assert.Nil(t, card)
|
||||||
assert.Contains(t, err.Error(), "invalid source_id")
|
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) {
|
func TestCreatePayment_ShouldFail(t *testing.T) {
|
||||||
client := NewDevClient().(*MockClient)
|
client := NewDevClient().(*MockClient)
|
||||||
client.ShouldFail = true
|
client.ShouldFail = true
|
||||||
@@ -564,7 +588,7 @@ func TestDevClient_CreateCardOnFile_WithNewFields(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userID := "user-new-fields"
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, card.Enabled)
|
assert.True(t, card.Enabled)
|
||||||
@@ -583,7 +607,7 @@ func TestDevClient_DeleteCardOnFile_SoftDelete(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userID := "user-soft-delete"
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = client.DeleteCardOnFile(ctx, card.ID)
|
err = client.DeleteCardOnFile(ctx, card.ID)
|
||||||
|
|||||||
Reference in New Issue
Block a user