Harden Square HTTP client and dev mock: DeleteCustomer, response limits, token redaction

Adds SquareClient.DeleteCustomer for GDPR erasure (DELETE /v2/customers/{id}, NOT_FOUND as no-op), bounds doJSON response reads to 1 MiB with rune-safe 500-byte error snippets, adds validCardID guard to the disable-card URL, makes paymentFromSquare card fields consistent when the card ID is empty, redacts ccof/cnon tokens in all log paths, and fixes the idempotency-key-length comment (45 chars for payments/refunds/cards, 64 only for terminal checkouts).
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent 457f7a452e
commit f0099714ff
4 changed files with 143 additions and 13 deletions
+24 -2
View File
@@ -69,6 +69,10 @@ func (d *devProdClient) GetPayment(ctx context.Context, paymentID string) (*Paym
func (d *devProdClient) CreateCustomer(ctx context.Context, name, email string) (*CustomerResult, error) {
return createCustomerHTTP(ctx, name, email)
}
func (d *devProdClient) DeleteCustomer(ctx context.Context, customerID string) error {
return deleteCustomerHTTP(ctx, customerID)
}
func (d *devProdClient) CancelCheckout(ctx context.Context, checkoutID string) error {
return cancelCheckoutHTTP(ctx, checkoutID)
}
@@ -566,7 +570,7 @@ func (m *MockClient) CreateCustomer(ctx context.Context, name, email string) (*C
// the mock mirrors this by deduping on email so a retry returns the
// original customer rather than creating a duplicate.
if existing, ok := m.customers[email]; ok {
log.Printf("[SQUARE-MOCK] CreateCustomer dedup hit: email=%s → id=%s", redactedEmail(email), existing.ID)
log.Printf("[SQUARE-MOCK] CreateCustomer dedup hit: email=%s → id=%s", redactedEmail(email), tokenPrefix(existing.ID))
return existing, nil
}
@@ -577,10 +581,28 @@ func (m *MockClient) CreateCustomer(ctx context.Context, name, email string) (*C
CreatedAt: clock.Now().UTC().Format(time.RFC3339),
}
m.customers[email] = customer
log.Printf("[SQUARE-MOCK] Customer created: id=%s, email=%s", customer.ID, redactedEmail(email))
log.Printf("[SQUARE-MOCK] Customer created: id=%s, email=%s", tokenPrefix(customer.ID), redactedEmail(email))
return customer, nil
}
func (m *MockClient) DeleteCustomer(ctx context.Context, customerID string) error {
log.Printf("[SQUARE-MOCK] DeleteCustomer: id=%s", tokenPrefix(customerID))
m.mu.Lock()
defer m.mu.Unlock()
for email, customer := range m.customers {
if customer.ID == customerID {
delete(m.customers, email)
log.Printf("[SQUARE-MOCK] Customer deleted: id=%s", tokenPrefix(customerID))
return nil
}
}
// Real Square returns 404 / NOT_FOUND for an already-deleted customer —
// mirror the prod semantics of idempotent re-deletion as a no-op.
return nil
}
func (m *MockClient) CancelCheckout(ctx context.Context, checkoutID string) error {
log.Printf("[SQUARE-MOCK] CancelCheckout: id=%s", checkoutID)