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

158 lines
6.8 KiB
Go

package httpapi
import (
"context"
"errors"
"net/http"
"strings"
"testing"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/payments"
)
type refundHTTPStub struct {
paymentStub
refundCalls, notifyCalls, readCalls int
order, operator, key, reason, filter string
refundAmount int64
limit, offset int
refundErr error
}
func (s *refundHTTPStub) AdminList(_ context.Context, org string, limit, offset int) (payments.AdminOrdersPage, error) {
s.readCalls++
s.filter, s.limit, s.offset = org, limit, offset
return payments.AdminOrdersPage{Orders: []payments.AdminOrder{{Order: payments.Order{ID: "cross-org", OrganizationID: "org2", Status: payments.StatusPending, CodeURL: "weixin://private"}}}}, s.refundErr
}
func (s *refundHTTPStub) CreateRefund(_ context.Context, order, operator, key, reason string, amount int64) (payments.Refund, error) {
s.refundCalls++
s.order, s.operator, s.key, s.reason, s.refundAmount = order, operator, key, reason, amount
return payments.Refund{ID: "refund1", Status: payments.RefundPending, AmountFen: amount}, s.refundErr
}
func (s *refundHTTPStub) GetRefund(_ context.Context, id string) (payments.Refund, error) {
s.readCalls++
return payments.Refund{ID: id, Status: payments.RefundSuccess}, s.refundErr
}
func (s *refundHTTPStub) ListRefunds(_ context.Context, id string) ([]payments.Refund, error) {
s.readCalls++
s.order = id
return nil, s.refundErr
}
func (s *refundHTTPStub) RefundNotify(_ context.Context, _ *http.Request) error {
s.notifyCalls++
return s.refundErr
}
func TestAdminRefundRoutesRequireSuperAdmin(t *testing.T) {
for _, role := range []string{"user", "organization_admin", "super_admin"} {
for _, authenticated := range []bool{false, true} {
for _, route := range []struct{ method, path string }{
{"GET", "/api/payments/admin/orders"},
{"GET", "/api/payments/admin/orders/order1/refunds"},
{"POST", "/api/payments/admin/orders/order1/refunds"},
{"GET", "/api/payments/admin/refunds/refund1"},
} {
s := &refundHTTPStub{}
got := paymentRequest(paymentTestHandler(t, role, s), route.method, route.path, `{"amountFen":100,"requestKey":"retry-1","reason":"用户申请"}`, "https://pay.example.test", authenticated)
want := 200
if !authenticated {
want = 401
} else if role != "super_admin" {
want = 403
}
if got.Code != want {
t.Fatalf("%s %s role=%s auth=%t got=%d body=%s", route.method, route.path, role, authenticated, got.Code, got.Body.String())
}
if want != 200 && s.refundCalls+s.readCalls != 0 {
t.Fatal("unauthorized service call")
}
}
}
}
}
func TestAdminWithoutOrganizationCanViewAllOrdersAndRefund(t *testing.T) {
s := &refundHTTPStub{}
auth, err := NewPlatformAuthorizer(AuthState{Required: true, Configured: true}, &fixedSessionResolver{session: identity.Session{AuthMode: identity.AuthModeAdmin, User: identity.User{ID: "global-admin", Role: "super_admin"}}})
if err != nil {
t.Fatal(err)
}
h := NewPaymentHandler(auth, s, "https://pay.example.test")
got := paymentRequest(h, "GET", "/api/payments/admin/orders?organizationId=org2&limit=10&offset=20", "", "", true)
if got.Code != 200 || s.filter != "org2" || s.limit != 10 || s.offset != 20 || strings.Contains(got.Body.String(), "weixin:") {
t.Fatalf("status=%d body=%s", got.Code, got.Body.String())
}
got = paymentRequest(h, "POST", "/api/payments/admin/orders/order2/refunds", `{"amountFen":123,"requestKey":"retry-1","reason":" 用户申请 "}`, "", true)
if got.Code != 200 || s.operator != "global-admin" || s.order != "order2" || s.refundAmount != 123 || s.reason != "用户申请" || s.key != "retry-1" {
t.Fatalf("status=%d stub=%+v", got.Code, s)
}
}
func TestAdminRefundRejectsTamperedBodyAndCrossOrigin(t *testing.T) {
s := &refundHTTPStub{}
h := paymentTestHandler(t, "super_admin", s)
for _, body := range []string{
`{"amountFen":1.1,"requestKey":"x","reason":"ok"}`,
`{"amountFen":0,"requestKey":"x","reason":"ok"}`,
`{"amountFen":-1,"requestKey":"x","reason":"ok"}`,
`{"amountFen":100,"requestKey":"x","reason":"ok","operatorId":"victim"}`,
`{"amountFen":100,"requestKey":"x","reason":"ok","organizationId":"victim"}`,
`{"amountFen":100,"requestKey":"a/b","reason":"ok"}`,
`{"amountFen":100,"requestKey":"x","reason":" "}`,
`{"amountFen":100,"requestKey":"x","reason":"` + strings.Repeat("退", 27) + `"}`,
`{"amountFen":100,"requestKey":"x","reason":"ok"} {}`,
} {
got := paymentRequest(h, "POST", "/api/payments/admin/orders/order1/refunds", body, "", true)
if got.Code != 400 {
t.Fatalf("body=%s status=%d", body, got.Code)
}
}
got := paymentRequest(h, "POST", "/api/payments/admin/orders/order1/refunds", `{"amountFen":100,"requestKey":"x","reason":"ok"}`, "https://evil.test", true)
if got.Code != 403 || s.refundCalls != 0 {
t.Fatalf("status=%d calls=%d", got.Code, s.refundCalls)
}
for _, query := range []string{"limit=0", "limit=101", "limit=abc", "offset=-1", "offset=10000001"} {
if got := paymentRequest(h, "GET", "/api/payments/admin/orders?"+query, "", "", true); got.Code != 400 {
t.Fatalf("query=%s status=%d", query, got.Code)
}
}
}
func TestRefundCallbackHasNoSessionAndOnlyAcknowledgesDurableSuccess(t *testing.T) {
for _, test := range []struct {
err error
want int
}{{nil, 204}, {payments.ErrVerification, 400}, {errors.New("secret database details"), 500}} {
s := &refundHTTPStub{refundErr: test.err}
h := paymentTestHandler(t, "user", s)
got := paymentRequest(h, "POST", "/api/payments/wechat/refund-notify", `{}`, "", false)
if got.Code != test.want || s.notifyCalls != 1 || strings.Contains(got.Body.String(), "secret") {
t.Fatalf("status=%d body=%s", got.Code, got.Body.String())
}
got = paymentRequest(h, "GET", "/api/payments/wechat/refund-notify", "", "", false)
if got.Code != 405 || s.notifyCalls != 1 {
t.Fatal("GET invoked refund callback")
}
}
}
func TestRefundErrorMappingAndStrictRoutes(t *testing.T) {
for _, test := range []struct {
err error
want int
}{{payments.ErrNotFound, 404}, {payments.ErrConflict, 409}, {payments.ErrInvalidAmount, 400}, {payments.ErrUnavailable, 503}, {errors.New("secret"), 500}} {
s := &refundHTTPStub{refundErr: test.err}
got := paymentRequest(paymentTestHandler(t, "super_admin", s), "GET", "/api/payments/admin/refunds/refund1", "", "", true)
if got.Code != test.want || strings.Contains(got.Body.String(), "secret") {
t.Fatalf("status=%d body=%s", got.Code, got.Body.String())
}
}
h := paymentTestHandler(t, "super_admin", &refundHTTPStub{})
for _, path := range []string{"/api/payments/admin/", "/api/payments/admin/orders/a/b/refunds", "/api/payments/admin/orders/order1/refunds/"} {
if got := paymentRequest(h, "GET", path, "", "", true); got.Code != 404 {
t.Fatalf("%s status=%d", path, got.Code)
}
}
}