Files
NianAIGC/backend/internal/httpapi/payments_test.go
T
2026-10-12 13:57:23 +08:00

130 lines
5.0 KiB
Go

package httpapi
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/payments"
)
type paymentStub struct {
org, account, requestKey string
amount int64
notifications int
err error
}
func (s *paymentStub) Options() payments.Options {
return payments.Options{Enabled: true, PresetAmountsFen: []int64{10000}, MinAmountFen: 100, MaxAmountFen: 1000000}
}
func (s *paymentStub) Create(_ context.Context, org, account, key string, amount int64) (payments.Order, error) {
s.org, s.account, s.requestKey, s.amount = org, account, key, amount
return payments.Order{ID: "order1", OrganizationID: org, AmountFen: amount, Status: payments.StatusPending}, s.err
}
func (s *paymentStub) Get(_ context.Context, org, id string) (payments.Order, error) {
s.org = org
return payments.Order{ID: id, Status: payments.StatusPaid, CodeURL: "weixin://stale"}, s.err
}
func (s *paymentStub) List(_ context.Context, org string) ([]payments.Order, error) {
s.org = org
return nil, s.err
}
func (s *paymentStub) Notify(_ context.Context, _ *http.Request) error {
s.notifications++
return s.err
}
func paymentTestHandler(t *testing.T, role string, service PaymentService) http.Handler {
t.Helper()
auth, err := NewPlatformAuthorizer(AuthState{Required: true, Configured: true}, &fixedSessionResolver{session: identity.Session{AuthMode: identity.AuthModeAdmin, User: identity.User{ID: "account1", OrganizationID: "org1", Role: role}}})
if err != nil {
t.Fatal(err)
}
return NewPaymentHandler(auth, service, "https://pay.example.test")
}
func paymentRequest(h http.Handler, method, path, body, origin string, authenticated bool) *httptest.ResponseRecorder {
r := httptest.NewRequest(method, path, strings.NewReader(body))
r.Header.Set("Content-Type", "application/json")
if origin != "" {
r.Header.Set("Origin", origin)
}
if authenticated {
r.AddCookie(&http.Cookie{Name: identity.SessionCookieName, Value: "valid"})
}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
return w
}
func TestPaymentCreateUsesSessionAndRejectsMoneyTampering(t *testing.T) {
s := &paymentStub{}
h := paymentTestHandler(t, "organization_admin", s)
got := paymentRequest(h, "POST", "/api/payments/orders", `{"amountFen":12345,"requestKey":"retry-1"}`, "https://pay.example.test", true)
if got.Code != 200 || s.org != "org1" || s.account != "account1" || s.amount != 12345 {
t.Fatalf("status=%d scope=%s/%s amount=%d", got.Code, s.org, s.account, s.amount)
}
for _, body := range []string{
`{"amountFen":1.01,"requestKey":"x"}`, `{"amountFen":1e4,"requestKey":"x"}`,
`{"amountFen":100,"requestKey":"x","organizationId":"victim"}`,
`{"amountFen":100,"requestKey":"x"} {}`, `{"amountFen":100,"requestKey":"a/b"}`,
} {
if got := paymentRequest(h, "POST", "/api/payments/orders", body, "", true); got.Code != 400 {
t.Fatalf("body=%s status=%d", body, got.Code)
}
}
}
func TestPaymentAuthorizationAndOrigin(t *testing.T) {
for _, test := range []struct {
role, origin string
authenticated bool
want int
}{
{"user", "", true, 403}, {"organization_admin", "https://evil.example", true, 403},
{"organization_admin", "null", true, 403}, {"organization_admin", "", false, 401},
} {
s := &paymentStub{}
h := paymentTestHandler(t, test.role, s)
got := paymentRequest(h, "POST", "/api/payments/orders", `{"amountFen":100,"requestKey":"x"}`, test.origin, test.authenticated)
if got.Code != test.want || s.account != "" {
t.Fatalf("test=%+v status=%d", test, got.Code)
}
}
}
func TestPaymentCallbackUsesVerifierWithoutBrowserSessionAndRetriesFailures(t *testing.T) {
for _, test := range []struct {
err error
want int
}{
{nil, 204}, {payments.ErrVerification, 400}, {errors.New("secret error detail"), 500},
} {
s := &paymentStub{err: test.err}
h := paymentTestHandler(t, "user", s)
got := paymentRequest(h, "POST", "/api/payments/wechat/notify", `{}`, "", false)
if got.Code != test.want || s.notifications != 1 || strings.Contains(got.Body.String(), "secret") {
t.Fatalf("status=%d body=%s", got.Code, got.Body.String())
}
}
}
func TestPaymentDisabledOptionsAndScopedReads(t *testing.T) {
h := paymentTestHandler(t, "organization_admin", nil)
got := paymentRequest(h, "GET", "/api/payments/options", "", "", true)
if got.Code != 200 || !strings.Contains(got.Body.String(), `"enabled":false`) || got.Header().Get("Cache-Control") != "no-store" {
t.Fatal(got.Body.String())
}
s := &paymentStub{}
h = paymentTestHandler(t, "organization_admin", s)
got = paymentRequest(h, "GET", "/api/payments/orders/order1?organizationId=victim", "", "", true)
if got.Code != 200 || s.org != "org1" || strings.Contains(got.Body.String(), "weixin") {
t.Fatalf("status=%d body=%s", got.Code, got.Body.String())
}
h = paymentTestHandler(t, "user", s)
got = paymentRequest(h, "GET", "/api/payments/options", "", "", true)
if !strings.Contains(got.Body.String(), `"canRecharge":false`) {
t.Fatal(got.Body.String())
}
}