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) } } }