Harden Square HTTP client and dev mock: status codes, token validation, deadline wire format
Add StatusCode/Category/Field to squareAPIError and an IsNotFound helper so 400/401/404/429/5xx are distinguishable structurally instead of by substring. Validate cnon:/ccof: token prefixes in createPayment/createCardOnFile (PCI parity with the mock). Reject ccof charges without customer_id in the mock so dev parity catches the production bug. Emit Deadline as the RFC 3339 duration (PT5M) and correct the deprecated-comment.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -88,6 +89,28 @@ func TestDevClient_CreateCheckout_PendingThenCompleted(t *testing.T) {
|
||||
assert.NotEmpty(t, completed.EntryMethod)
|
||||
}
|
||||
|
||||
// TestDevClient_CreateCheckout_DeadlineDurationFormat verifies the mock emits
|
||||
// Square's deadline_duration wire format — an RFC 3339 duration ("PT5M"), NOT
|
||||
// an absolute RFC3339 timestamp — so dev parity matches the real API.
|
||||
func TestDevClient_CreateCheckout_DeadlineDurationFormat(t *testing.T) {
|
||||
client := NewDevClient().(*MockClient)
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := client.CreateCheckout(ctx, CreateCheckoutReq{
|
||||
Amount: 5000,
|
||||
Currency: "GBP",
|
||||
IdempotencyKey: "checkout-deadline",
|
||||
ReferenceID: "deadline-ref",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "PT5M", result.Deadline, "deadline_duration must be an RFC 3339 duration, not a timestamp")
|
||||
|
||||
// Round-trip through checkoutFromSquare: the wire value is copied through
|
||||
// unchanged (it is not parsed/reformatted anywhere in the package).
|
||||
res := checkoutFromSquare(&sqTerminalCheckout{Deadline: result.Deadline})
|
||||
assert.Equal(t, "PT5M", res.Deadline)
|
||||
}
|
||||
|
||||
func TestDevClient_CreateCheckout_NoTip(t *testing.T) {
|
||||
client := NewDevClient().(*MockClient)
|
||||
|
||||
@@ -872,20 +895,55 @@ func TestDevClient_CreatePayment_RejectsRawPAN(t *testing.T) {
|
||||
{"amex", "378282246310005"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := client.CreatePayment(ctx, CreatePaymentReq{
|
||||
Amount: 5000,
|
||||
Currency: "GBP",
|
||||
SourceID: tt.pan,
|
||||
IdempotencyKey: "raw-pan-" + tt.name,
|
||||
ReferenceID: "booking-raw",
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := client.CreatePayment(ctx, CreatePaymentReq{
|
||||
Amount: 5000,
|
||||
Currency: "GBP",
|
||||
SourceID: tt.pan,
|
||||
IdempotencyKey: "raw-pan-" + tt.name,
|
||||
ReferenceID: "booking-raw",
|
||||
})
|
||||
require.Error(t, err, "raw PAN must be rejected for production parity")
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "invalid source_id")
|
||||
})
|
||||
require.Error(t, err, "raw PAN must be rejected for production parity")
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "invalid source_id")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDevClient_CreatePayment_CardOnFileRequiresCustomerID verifies the mock
|
||||
// mirrors Square's real enforcement: charging a ccof: (card-on-file) token
|
||||
// without a customer_id is rejected with a structured 400 INVALID_REQUEST_ERROR
|
||||
// (this is the exact production bug the mock must catch in dev), while the same
|
||||
// charge with a customer_id succeeds as ON_FILE.
|
||||
func TestDevClient_CreatePayment_CardOnFileRequiresCustomerID(t *testing.T) {
|
||||
client := NewDevClient().(*MockClient)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.CreatePayment(ctx, CreatePaymentReq{
|
||||
Amount: 5000,
|
||||
Currency: "GBP",
|
||||
SourceID: "ccof:mock_saved",
|
||||
IdempotencyKey: "ccof-no-customer",
|
||||
ReferenceID: "booking-ccof-no-customer",
|
||||
})
|
||||
require.Error(t, err, "ccof charge without customer_id must be rejected")
|
||||
assert.Equal(t, "INVALID_REQUEST_ERROR", ErrorCode(err))
|
||||
assert.Contains(t, ErrorDetail(err), "customer_id required")
|
||||
assert.Equal(t, http.StatusBadRequest, ErrorStatusCode(err))
|
||||
|
||||
result, err := client.CreatePayment(ctx, CreatePaymentReq{
|
||||
Amount: 5000,
|
||||
Currency: "GBP",
|
||||
SourceID: "ccof:mock_saved",
|
||||
IdempotencyKey: "ccof-with-customer",
|
||||
ReferenceID: "booking-ccof-with-customer",
|
||||
CustomerID: "cus_mock_1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "COMPLETED", result.Status)
|
||||
assert.Equal(t, "ON_FILE", result.EntryMethod)
|
||||
assert.Equal(t, "cus_mock_1", result.CustomerID)
|
||||
}
|
||||
|
||||
func TestDevClient_RefundPayment_ForcePending(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user