若干更新迭代

This commit is contained in:
andy committed 2026-10-02 19:56:32 +08:00
1 parent deb9cc5bcf
commit 4ac786aafb
64 files changed
+3251 -291

No files matched your search

+96
View File
@@ -28,6 +28,96 @@ func TestBillingMemberRoutesUseRefreshedSessionScope(t *testing.T) {
}
}
func TestBillingBalanceUsesOnlyRefreshedSessionOrganization(t *testing.T) {
service := &billingHTTPServiceStub{}
h := billingTestHandler(t, identity.Session{AuthMode: identity.AuthModeUser, User: identity.User{ID: "member", ClientID: "platform", OrganizationID: "db-org", OrganizationName: "DB Org", Role: "user"}}, service, nil)
response := serveJSON(t, h, http.MethodGet, "/api/billing/balance?organizationId=attacker&orgID=attacker", nil)
if response.Code != 200 || response.Header().Get("Cache-Control") != "no-store" || service.walletOrganization != "db-org" {
t.Fatalf("status=%d cache=%q scope=%q body=%s", response.Code, response.Header().Get("Cache-Control"), service.walletOrganization, response.Body.String())
}
var payload map[string]json.RawMessage
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if len(payload) != 2 || string(payload["organization"]) != `{"id":"db-org","name":"DB Org"}` {
t.Fatalf("unexpected organization or extra fields: %s", response.Body.String())
}
var wallet billing.Wallet
if err := json.Unmarshal(payload["wallet"], &wallet); err != nil || wallet.OrganizationID != "db-org" || wallet.BalanceFen != 0 {
t.Fatalf("zero wallet=%+v err=%v body=%s", wallet, err, response.Body.String())
}
}
func TestBillingBalanceRejectsUnboundAndUnauthenticatedSessions(t *testing.T) {
service := &billingHTTPServiceStub{}
h := billingTestHandler(t, identity.Session{AuthMode: identity.AuthModeUser, User: identity.User{ID: "member", ClientID: "platform", Role: "user"}}, service, nil)
if got := serveJSON(t, h, http.MethodGet, "/api/billing/balance?organizationId=other", nil); got.Code != 422 || service.walletOrganization != "" {
t.Fatalf("unbound status=%d scope=%q body=%s", got.Code, service.walletOrganization, got.Body.String())
}
for _, credential := range []struct{ name, header, value string }{
{"missing", "", ""}, {"bearer", "Authorization", "Bearer other"}, {"api-key", "X-API-Key", "other"},
} {
t.Run(credential.name, func(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/api/billing/balance", nil)
if credential.header != "" {
request.Header.Set(credential.header, credential.value)
}
response := httptest.NewRecorder()
h.ServeHTTP(response, request)
if response.Code != 401 || service.walletOrganization != "" {
t.Fatalf("status=%d scope=%q body=%s", response.Code, service.walletOrganization, response.Body.String())
}
})
}
authorizer, err := NewPlatformAuthorizer(AuthState{Required: true, Configured: true}, &platformSessionResolverStub{outcome: "unauthenticated"})
if err != nil {
t.Fatal(err)
}
invalidSession := NewBillingHandler(authorizer, service, nil)
if got := serveJSON(t, invalidSession, http.MethodGet, "/api/billing/balance", nil); got.Code != 401 || service.walletOrganization != "" {
t.Fatalf("invalid session status=%d scope=%q body=%s", got.Code, service.walletOrganization, got.Body.String())
}
}
func TestBillingBalanceHidesStoreErrors(t *testing.T) {
service := &billingHTTPServiceStub{err: errors.New("database secret")}
h := billingTestHandler(t, identity.Session{AuthMode: identity.AuthModeUser, User: identity.User{ID: "member", ClientID: "platform", OrganizationID: "org", Role: "user"}}, service, nil)
got := serveJSON(t, h, http.MethodGet, "/api/billing/balance", nil)
if got.Code != 500 || bytes.Contains(got.Body.Bytes(), []byte("database secret")) || got.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("status=%d body=%s", got.Code, got.Body.String())
}
}
func TestBillingBalanceReadsOnlyWallet(t *testing.T) {
store := &balanceOnlyStore{}
h := billingTestHandler(t, identity.Session{AuthMode: identity.AuthModeUser, User: identity.User{ID: "member", ClientID: "platform", OrganizationID: "org", Role: "user"}}, billing.NewService(store, nil), nil)
got := serveJSON(t, h, http.MethodGet, "/api/billing/balance", nil)
if got.Code != 200 || store.walletCalls != 1 || store.organizationID != "org" {
t.Fatalf("status=%d walletCalls=%d org=%q body=%s", got.Code, store.walletCalls, store.organizationID, got.Body.String())
}
}
type balanceOnlyStore struct {
emptyBillingStore
walletCalls int
organizationID string
}
func (s *balanceOnlyStore) BillingWallet(_ context.Context, organizationID string) (billing.Wallet, error) {
s.walletCalls++
s.organizationID = organizationID
return billing.Wallet{OrganizationID: organizationID, BalanceFen: 0, Currency: billing.CurrencyCNY}, nil
}
func (*balanceOnlyStore) BillingLedger(context.Context, string, string, int) ([]billing.LedgerEntry, error) {
panic("balance endpoint accessed ledger")
}
func (*balanceOnlyStore) ListBillingPriceRules(context.Context, bool) ([]billing.PriceRule, error) {
panic("balance endpoint accessed catalog")
}
func (*balanceOnlyStore) BillingWallets(context.Context) ([]billing.Wallet, error) {
panic("balance endpoint accessed all wallets")
}
func TestBillingOverviewResponsesEncodeEmptyCollectionsAsArrays(t *testing.T) {
service := billing.NewService(&emptyBillingStore{}, nil)
member := billingTestHandler(t, identity.Session{AuthMode: identity.AuthModeUser, User: identity.User{ID: "user", ClientID: "platform", OrganizationID: "org", Role: "user"}}, service, nil)
@@ -135,6 +225,7 @@ func TestBillingHandlerRejectsWrongMethods(t *testing.T) {
type billingHTTPServiceStub struct {
overviewOrganization, overviewAccount string
walletOrganization string
quote billing.QuoteCommand
adjustment billing.AdjustmentCommand
pricePatch billing.PricePatch
@@ -143,6 +234,11 @@ type billingHTTPServiceStub struct {
err error
}
func (s *billingHTTPServiceStub) Wallet(_ context.Context, organizationID string) (billing.Wallet, error) {
s.walletOrganization = organizationID
return billing.Wallet{OrganizationID: organizationID, Currency: billing.CurrencyCNY}, s.err
}
func (s *billingHTTPServiceStub) Overview(_ context.Context, organizationID, accountID string) (billing.Overview, error) {
s.overviewOrganization, s.overviewAccount = organizationID, accountID
return billing.Overview{Wallet: billing.Wallet{OrganizationID: organizationID, Currency: billing.CurrencyCNY}}, s.err