158 lines
6.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|