mirror of
https://github.com/luxfi/bank.git
synced 2026-10-30 17:02:07 +00:00
POST /v1/bank/cards issues a simulation: the number is 4242424242 — the test BIN every processor publishes — with four random digits after it, and the CVV is three more generated on the call and belonging to nothing. The route was not gated, so a deployment outside the sandbox handed that to a customer as their card, and the dash calls it. Measured: it returns "4242 4242 4242 8361". Such a deployment reaches its issuer at /v1/bank/cards/virtual, which is provider-backed and correctly gated already, so the refusal says that rather than answering 404. A card that declines everywhere it is presented is worse than a feature that says it is not here. Alongside it, two things that were never exercised: a card is frozen only by the account that holds it, and the number and code the sandbox surfaces once are on no field of the stored record — the assertion walks every field rather than naming the two it expects, since the point is that neither is anywhere. Co-authored-by: Hanzo Dev <[email protected]>
110 lines
3.8 KiB
Go
110 lines
3.8 KiB
Go
package bank
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/base/tests"
|
|
"github.com/luxfi/bank/collections"
|
|
)
|
|
|
|
// The card this route issues is a simulation: the number is the published test
|
|
// BIN with four random digits after it, and the CVV is three more generated on
|
|
// the call and belonging to nothing. In the sandbox that is the demo. Outside
|
|
// it, handing that to a customer is the bank claiming to have issued a card
|
|
// that will decline everywhere it is presented.
|
|
func TestASimulatedCardIsNotIssuedOutsideTheSandbox(t *testing.T) {
|
|
t.Setenv("BANK_SANDBOX", "false")
|
|
t.Setenv("BANK_CHAIN_RPC", "")
|
|
app := newBankApp(t)
|
|
_, token := seedPrincipal(t, app)
|
|
|
|
run(t, app, tests.ApiScenario{
|
|
Name: "issuing a simulated card is refused where the bank is real",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/bank/cards",
|
|
Body: strings.NewReader(`{"currency":"USD"}`),
|
|
Headers: map[string]string{"Authorization": token, "Content-Type": "application/json"},
|
|
ExpectedStatus: http.StatusNotImplemented,
|
|
ExpectedContent: []string{"cards/virtual"},
|
|
})
|
|
}
|
|
|
|
// In the sandbox it is the demo, and it hands the number and CVV over once.
|
|
// They are not stored: only the masked display and the last four persist, so a
|
|
// second read cannot produce them.
|
|
func TestTheSandboxCardSurfacesItsNumberOnceAndStoresNeither(t *testing.T) {
|
|
app := newBankApp(t)
|
|
_, token := seedPrincipal(t, app)
|
|
h := map[string]string{"Authorization": token, "Content-Type": "application/json"}
|
|
|
|
body := post(t, app, h, "/v1/bank/cards", `{"currency":"USD"}`, http.StatusCreated, `"pan"`, `"cvv"`)
|
|
pan, _ := body["pan"].(string)
|
|
cvv, _ := body["cvv"].(string)
|
|
if len(strings.ReplaceAll(pan, " ", "")) != 16 || len(cvv) != 3 {
|
|
t.Fatalf("pan %q / cvv %q are not a card's shape", pan, cvv)
|
|
}
|
|
|
|
last4 := strings.ReplaceAll(pan, " ", "")[12:]
|
|
cards, err := app.FindRecordsByFilter(collections.CardCollectionName, "last4 = {:l}", "", 1, 0,
|
|
map[string]any{"l": last4})
|
|
if err != nil || len(cards) == 0 {
|
|
t.Fatalf("the issued card was not recorded: %v", err)
|
|
}
|
|
stored := cards[0]
|
|
|
|
// Whatever the record holds, it must not be the number or the code.
|
|
for _, field := range stored.Collection().Fields {
|
|
v := stored.GetString(field.GetName())
|
|
if v == "" {
|
|
continue
|
|
}
|
|
if strings.Contains(strings.ReplaceAll(v, " ", ""), strings.ReplaceAll(pan, " ", "")) {
|
|
t.Errorf("the full number is stored on %s", field.GetName())
|
|
}
|
|
if v == cvv {
|
|
t.Errorf("the security code is stored on %s", field.GetName())
|
|
}
|
|
}
|
|
if got := stored.GetString("display"); !strings.Contains(got, "•") {
|
|
t.Errorf("the stored display %q is not masked", got)
|
|
}
|
|
}
|
|
|
|
// A card belongs to one account: freezing is how a customer stops their own
|
|
// card, and it must not reach anybody else's.
|
|
func TestACardIsFrozenOnlyByItsOwnAccount(t *testing.T) {
|
|
app := newBankApp(t)
|
|
owner, token := seedPrincipal(t, app)
|
|
if primaryAccount(app, owner) == nil {
|
|
t.Fatal("no account provisioned")
|
|
}
|
|
h := map[string]string{"Authorization": token, "Content-Type": "application/json"}
|
|
|
|
theirs := secondAccount(t, app, "another-owner", "approved")
|
|
card := issueCardRecord(app, theirs, "Someone Else", "USD")
|
|
if card == nil {
|
|
t.Fatal("could not issue the other account's card")
|
|
}
|
|
|
|
for _, verb := range []string{"freeze", "unfreeze"} {
|
|
run(t, app, tests.ApiScenario{
|
|
Name: verb + " on another account's card is refused",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/bank/cards/" + card.Id + "/" + verb,
|
|
Headers: h,
|
|
ExpectedStatus: http.StatusForbidden,
|
|
ExpectedContent: []string{"card"},
|
|
})
|
|
}
|
|
|
|
after, err := app.FindRecordById(collections.CardCollectionName, card.Id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := after.GetString("status"); got != card.GetString("status") {
|
|
t.Errorf("another account's card changed status to %q", got)
|
|
}
|
|
}
|