Files
zeekayandHanzo Dev 8685f2074b bank: cover the issuer CaaS surface + webhook HMAC auth (58% -> 63%)
- Issuer card routes (/v1/bank/cards/*) driven end to end against the
  sandbox simulation: create account, account state, KYC, virtual
  create, hosted KYC/consent URLs, order — registerIssuerRoutes 100%,
  issuerJSON/issuerURL 80-85%, every simIssuer method 100%.
- The live sfprivate client against a mock upstream: Bearer auth, the
  per-endpoint paths, URL-field extraction, the unconfigured refusal,
  and that an upstream error stays opaque (no body/URL/key leak).
- RequireHMACAuth to 94%: valid signature passes, bad/missing/
  unconfigured all 401.

Total statement coverage across ./... rises to 63.1%; the CaaS surface
and the security-critical webhook gate are now exercised.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-22 15:04:33 -07:00

87 lines
2.2 KiB
Go

package hooks
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/hanzoai/base/core"
)
func sign(secret, body string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(body))
return hex.EncodeToString(mac.Sum(nil))
}
// drive runs the HMAC middleware against a request and returns the status the
// caller would see (200 means it called e.Next, i.e. passed the gate).
func drive(t *testing.T, secret, sigHeader, body string) int {
t.Helper()
if secret != "" {
t.Setenv("WEBHOOK_HMAC_SECRET", secret)
} else {
t.Setenv("WEBHOOK_HMAC_SECRET", "")
}
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/v1/bank/webhooks/x", io.NopCloser(stringReader(body)))
if sigHeader != "" {
req.Header.Set("X-Signature", sigHeader)
}
e := &core.RequestEvent{}
e.Request = req
e.Response = rec
// e.Next is nil; on a pass the middleware calls it. Guard by recovering:
// a nil-Next panic still proves the gate passed. Simpler: assert on the
// recorder status the middleware itself writes on rejection.
h := RequireHMACAuth()
func() {
defer func() { _ = recover() }()
_ = h.Func(e)
}()
if rec.Code == 200 && rec.Body.Len() == 0 {
// middleware wrote nothing → it passed to (nil) Next.
return 200
}
return rec.Code
}
type sr struct {
s string
i int
}
func stringReader(s string) *sr { return &sr{s: s} }
func (r *sr) Read(p []byte) (int, error) {
if r.i >= len(r.s) {
return 0, io.EOF
}
n := copy(p, r.s[r.i:])
r.i += n
return n, nil
}
func TestHMACValidSignaturePasses(t *testing.T) {
body := `{"event":"payment.settled"}`
if code := drive(t, "sekret", sign("sekret", body), body); code != 200 {
t.Errorf("valid signature: gate returned %d, want pass(200)", code)
}
}
func TestHMACRejects(t *testing.T) {
body := `{"event":"x"}`
if code := drive(t, "sekret", "deadbeef", body); code != 401 {
t.Errorf("bad signature = %d, want 401", code)
}
if code := drive(t, "sekret", "", body); code != 401 {
t.Errorf("missing header = %d, want 401", code)
}
if code := drive(t, "", sign("x", body), body); code != 401 {
t.Errorf("unconfigured secret = %d, want 401", code)
}
}