Files
bank/account_reads_test.go
zeekayandHanzo Dev 428baefa41 one customer cannot read another, and a wallets route carries an address
The three per-account reads are addressed by account id in the path, so
the id is a caller's to write, and one comparison in each handler is what
stands between a customer and another's balances, deposit addresses and
payment history. All three were right and none was exercised. An account
that does not exist is answered not-found rather than forbidden, since
"forbidden" for an id nobody holds tells a caller which ids are real.

The wallets one answered with a shape of its own that carried the
custodian's internal handle and left out the address — the one thing a
wallet is for — so the route could not be used for the thing it names.
Nothing consumed it; /v1/bank/wallet is what the app reads. It answers
with that same view now, which is one shape rather than two.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-27 14:35:22 -07:00

80 lines
2.6 KiB
Go

package bank
import (
"fmt"
"net/http"
"testing"
"github.com/hanzoai/base/tests"
)
// The three per-account reads are addressed by account id in the path, so the
// id is a caller's to write. What stops one customer reading another's balances,
// deposit addresses and payment history is a single comparison in each handler,
// and none of them was exercised.
func TestOneCustomerCannotReadAnother(t *testing.T) {
app := newBankApp(t)
owner, token := seedPrincipal(t, app)
if primaryAccount(app, owner) == nil {
t.Fatal("no account provisioned")
}
theirs := secondAccount(t, app, "another-owner", "approved")
for _, path := range []string{"balances", "wallets", "transactions"} {
run(t, app, tests.ApiScenario{
Name: "reading another account's " + path + " is refused",
Method: http.MethodGet,
URL: fmt.Sprintf("/v1/bank/accounts/%s/%s", theirs, path),
Headers: map[string]string{"Authorization": token},
ExpectedStatus: http.StatusForbidden,
ExpectedContent: []string{"account"},
})
// An account that does not exist is not found rather than forbidden:
// answering "forbidden" for an id nobody holds tells a caller which
// ids are real.
run(t, app, tests.ApiScenario{
Name: "reading a " + path + " for an account that does not exist",
Method: http.MethodGet,
URL: "/v1/bank/accounts/nosuchaccount/" + path,
Headers: map[string]string{"Authorization": token},
ExpectedStatus: http.StatusNotFound,
ExpectedContent: []string{"not found"},
})
run(t, app, tests.ApiScenario{
Name: path + " rejects the anonymous caller",
Method: http.MethodGet,
URL: fmt.Sprintf("/v1/bank/accounts/%s/%s", theirs, path),
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{"message"},
})
}
}
// And a customer reads their own.
func TestACustomerReadsTheirOwnAccount(t *testing.T) {
app := newBankApp(t)
owner, token := seedPrincipal(t, app)
mine := primaryAccount(app, owner)
if mine == nil {
t.Fatal("no account provisioned")
}
h := map[string]string{"Authorization": token}
for path, expect := range map[string]string{
"balances": `"currency"`,
"wallets": `"address"`,
"transactions": `[`,
} {
run(t, app, tests.ApiScenario{
Name: "reading my own " + path,
Method: http.MethodGet,
URL: fmt.Sprintf("/v1/bank/accounts/%s/%s", mine.Id, path),
Headers: h,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{expect},
})
}
}