mirror of
https://github.com/luxfi/treasury.git
synced 2026-10-30 15:15:00 +00:00
CreatePayment now sets the JSON body field idempotency_key (Square's required shape — per developer.squareup.com/docs/working-with-apis/ idempotency, NOT a header) from req.IdempotencyKey. Drops the previous wall-clock-derived "lux_<UnixNano>" value, which was worse than nothing because retries got fresh keys and Square would happily create duplicate payments. Empty IdempotencyKey now falls back to provider.DeriveIdempotencyKey, computed over the body BEFORE the idempotency_key field is added to avoid self-feedback. Hermetic httptest covers caller-supplied (body-field) and deterministic-fallback paths.
65 lines
2.0 KiB
Go
65 lines
2.0 KiB
Go
package square
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/luxfi/treasury/pkg/types"
|
|
)
|
|
|
|
func newTestProvider(t *testing.T, h http.HandlerFunc) *Provider {
|
|
t.Helper()
|
|
srv := httptest.NewServer(h)
|
|
t.Cleanup(srv.Close)
|
|
return &Provider{cfg: Config{BaseURL: srv.URL, AccessToken: "k"}, client: srv.Client()}
|
|
}
|
|
|
|
// Square uses a JSON body field, not a header.
|
|
func TestCreatePayment_CallerIdempotencyKey_BodyField(t *testing.T) {
|
|
var seen string
|
|
p := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
var got map[string]interface{}
|
|
_ = json.Unmarshal(b, &got)
|
|
seen, _ = got["idempotency_key"].(string)
|
|
_, _ = w.Write([]byte(`{"payment":{"id":"sq_1","status":"COMPLETED","amount_money":{"amount":100,"currency":"USD"}}}`))
|
|
})
|
|
req := &types.CreatePaymentRequest{
|
|
Amount: "100", Currency: "USD", SourceAccountID: "src",
|
|
IdempotencyKey: "caller-sq",
|
|
}
|
|
if _, err := p.CreatePayment(context.Background(), req); err != nil {
|
|
t.Fatalf("CreatePayment: %v", err)
|
|
}
|
|
if seen != "caller-sq" {
|
|
t.Fatalf("idempotency_key body field: %q", seen)
|
|
}
|
|
}
|
|
|
|
func TestCreatePayment_DeterministicFallback_StableAcrossRetries(t *testing.T) {
|
|
var seen []string
|
|
p := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
var got map[string]interface{}
|
|
_ = json.Unmarshal(b, &got)
|
|
k, _ := got["idempotency_key"].(string)
|
|
seen = append(seen, k)
|
|
_, _ = w.Write([]byte(`{"payment":{"id":"sq_1","status":"COMPLETED","amount_money":{"amount":100,"currency":"USD"}}}`))
|
|
})
|
|
req := &types.CreatePaymentRequest{
|
|
Amount: "100", Currency: "USD", SourceAccountID: "src", OrgID: "org-A",
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
if _, err := p.CreatePayment(context.Background(), req); err != nil {
|
|
t.Fatalf("CreatePayment: %v", err)
|
|
}
|
|
}
|
|
if seen[0] == "" || seen[0] != seen[1] || seen[1] != seen[2] {
|
|
t.Fatalf("expected identical deterministic body-field keys, got %v", seen)
|
|
}
|
|
}
|