Files
new-api-custom/controller/payment_webhook_availability_test.go
T

286 lines
10 KiB
Go

package controller
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/Calcium-Ion/go-epay/epay"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/setting/system_setting"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func confirmPaymentComplianceForTest(t *testing.T) {
t.Helper()
paymentSetting := operation_setting.GetPaymentSetting()
originalConfirmed := paymentSetting.ComplianceConfirmed
originalTermsVersion := paymentSetting.ComplianceTermsVersion
t.Cleanup(func() {
paymentSetting.ComplianceConfirmed = originalConfirmed
paymentSetting.ComplianceTermsVersion = originalTermsVersion
})
paymentSetting.ComplianceConfirmed = true
paymentSetting.ComplianceTermsVersion = operation_setting.CurrentComplianceTermsVersion
}
func TestStripeWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
confirmPaymentComplianceForTest(t)
originalAPISecret := setting.StripeApiSecret
originalWebhookSecret := setting.StripeWebhookSecret
originalPriceID := setting.StripePriceId
t.Cleanup(func() {
setting.StripeApiSecret = originalAPISecret
setting.StripeWebhookSecret = originalWebhookSecret
setting.StripePriceId = originalPriceID
})
setting.StripeWebhookSecret = ""
setting.StripeApiSecret = "sk_test_123"
setting.StripePriceId = "price_123"
require.False(t, isStripeWebhookEnabled())
setting.StripeWebhookSecret = "whsec_test"
require.True(t, isStripeWebhookEnabled())
setting.StripePriceId = ""
require.False(t, isStripeWebhookEnabled())
}
func TestCreemWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
confirmPaymentComplianceForTest(t)
originalAPIKey := setting.CreemApiKey
originalProducts := setting.CreemProducts
originalWebhookSecret := setting.CreemWebhookSecret
t.Cleanup(func() {
setting.CreemApiKey = originalAPIKey
setting.CreemProducts = originalProducts
setting.CreemWebhookSecret = originalWebhookSecret
})
setting.CreemWebhookSecret = ""
setting.CreemApiKey = "creem_api_key"
setting.CreemProducts = `[{"productId":"prod_123"}]`
require.False(t, isCreemWebhookEnabled())
setting.CreemWebhookSecret = "creem_secret"
require.True(t, isCreemWebhookEnabled())
setting.CreemProducts = "[]"
require.False(t, isCreemWebhookEnabled())
}
func TestWaffoWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
confirmPaymentComplianceForTest(t)
originalEnabled := setting.WaffoEnabled
originalSandbox := setting.WaffoSandbox
originalAPIKey := setting.WaffoApiKey
originalPrivateKey := setting.WaffoPrivateKey
originalPublicCert := setting.WaffoPublicCert
originalSandboxAPIKey := setting.WaffoSandboxApiKey
originalSandboxPrivateKey := setting.WaffoSandboxPrivateKey
originalSandboxPublicCert := setting.WaffoSandboxPublicCert
t.Cleanup(func() {
setting.WaffoEnabled = originalEnabled
setting.WaffoSandbox = originalSandbox
setting.WaffoApiKey = originalAPIKey
setting.WaffoPrivateKey = originalPrivateKey
setting.WaffoPublicCert = originalPublicCert
setting.WaffoSandboxApiKey = originalSandboxAPIKey
setting.WaffoSandboxPrivateKey = originalSandboxPrivateKey
setting.WaffoSandboxPublicCert = originalSandboxPublicCert
})
setting.WaffoEnabled = true
setting.WaffoSandbox = false
setting.WaffoApiKey = ""
setting.WaffoPrivateKey = "private"
setting.WaffoPublicCert = "public"
require.False(t, isWaffoWebhookEnabled())
setting.WaffoApiKey = "api"
require.True(t, isWaffoWebhookEnabled())
setting.WaffoEnabled = false
require.False(t, isWaffoWebhookEnabled())
setting.WaffoEnabled = true
setting.WaffoSandbox = true
setting.WaffoSandboxApiKey = ""
setting.WaffoSandboxPrivateKey = "sandbox_private"
setting.WaffoSandboxPublicCert = "sandbox_public"
require.False(t, isWaffoWebhookEnabled())
setting.WaffoSandboxApiKey = "sandbox_api"
require.True(t, isWaffoWebhookEnabled())
}
func TestWaffoPancakeWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
confirmPaymentComplianceForTest(t)
originalMerchantID := setting.WaffoPancakeMerchantID
originalPrivateKey := setting.WaffoPancakePrivateKey
originalProductID := setting.WaffoPancakeProductID
t.Cleanup(func() {
setting.WaffoPancakeMerchantID = originalMerchantID
setting.WaffoPancakePrivateKey = originalPrivateKey
setting.WaffoPancakeProductID = originalProductID
})
// Presence of all three credentials enables the gateway. Webhook public
// keys are bundled in the SDK and there is no separate Enabled toggle —
// clear any of the three fields to disable.
setting.WaffoPancakeMerchantID = ""
setting.WaffoPancakePrivateKey = "private"
setting.WaffoPancakeProductID = "product"
require.False(t, isWaffoPancakeWebhookEnabled())
setting.WaffoPancakeMerchantID = "merchant"
require.True(t, isWaffoPancakeWebhookEnabled())
setting.WaffoPancakeProductID = ""
require.False(t, isWaffoPancakeWebhookEnabled())
setting.WaffoPancakeProductID = "product"
setting.WaffoPancakePrivateKey = ""
require.False(t, isWaffoPancakeWebhookEnabled())
}
func TestEpayWebhookEnabledRequiresTopUpAndWebhookConfig(t *testing.T) {
confirmPaymentComplianceForTest(t)
originalPayAddress := operation_setting.PayAddress
originalEpayID := operation_setting.EpayId
originalEpayKey := operation_setting.EpayKey
originalPayMethods := operation_setting.PayMethods
t.Cleanup(func() {
operation_setting.PayAddress = originalPayAddress
operation_setting.EpayId = originalEpayID
operation_setting.EpayKey = originalEpayKey
operation_setting.PayMethods = originalPayMethods
})
operation_setting.PayAddress = "https://pay.example.com"
operation_setting.EpayId = "epay_id"
operation_setting.EpayKey = ""
operation_setting.PayMethods = []map[string]string{{"type": "alipay"}}
require.False(t, isEpayWebhookEnabled())
operation_setting.EpayKey = "epay_key"
require.True(t, isEpayWebhookEnabled())
operation_setting.PayMethods = nil
require.False(t, isEpayWebhookEnabled())
}
func TestEpayCallbacksVerifyBeforeSettlement(t *testing.T) {
confirmPaymentComplianceForTest(t)
originalPayAddress := operation_setting.PayAddress
originalEpayID := operation_setting.EpayId
originalEpayKey := operation_setting.EpayKey
originalPayMethods := operation_setting.PayMethods
originalServerAddress := system_setting.ServerAddress
originalDB := model.DB
t.Cleanup(func() {
operation_setting.PayAddress = originalPayAddress
operation_setting.EpayId = originalEpayID
operation_setting.EpayKey = originalEpayKey
operation_setting.PayMethods = originalPayMethods
system_setting.ServerAddress = originalServerAddress
model.DB = originalDB
})
operation_setting.PayAddress = "https://pay.example.com"
operation_setting.EpayId = "epay_id"
operation_setting.EpayKey = "test-key"
operation_setting.PayMethods = []map[string]string{{"type": "alipay"}}
system_setting.ServerAddress = "https://dashboard.example.com"
// Rejected callbacks and valid non-success events must never reach settlement.
// A nil database makes an accidental attempt to settle fail the test.
model.DB = nil
require.True(t, isEpayWebhookEnabled())
// Both ambiguous maps below had this same valid signing string in v0.0.4.
// This must fail because of parameter ambiguity, not a mismatched signature.
ambiguousSign := epay.MD5String("money=1.00&name=item&out_trade_no=other&out_trade_no=order&pid=epay_id&trade_status=TRADE_SUCCESS&type=alipay", "test-key")
pendingSign := epay.MD5String("money=1.00&name=item%26other&out_trade_no=order&pid=epay_id&trade_status=WAIT_BUYER_PAY&type=alipay", "test-key")
cases := []struct {
name string
productName string
tradeNo string
status string
sign string
validPending bool
}{
{"separator in product name", "item&out_trade_no=other", "order", epay.StatusTradeSuccess, ambiguousSign, false},
{"separator in order number", "item", "other&out_trade_no=order", epay.StatusTradeSuccess, ambiguousSign, false},
{"invalid signature", "item", "order", epay.StatusTradeSuccess, "invalid", false},
{"missing signature", "item", "order", epay.StatusTradeSuccess, "", false},
{"valid pending with literal percent escape", "item%26other", "order", "WAIT_BUYER_PAY", pendingSign, true},
}
endpoints := []struct {
name string
path string
handler gin.HandlerFunc
status int
rejectedBody string
pendingBody string
rejectedLocation string
pendingLocation string
}{
{
name: "topup notify", path: "/api/user/epay/notify", handler: EpayNotify,
status: http.StatusOK, rejectedBody: "fail", pendingBody: "success",
},
{
name: "subscription notify", path: "/api/subscription/epay/notify", handler: SubscriptionEpayNotify,
status: http.StatusOK, rejectedBody: "fail", pendingBody: "fail",
},
{
name: "subscription return", path: "/api/subscription/epay/return", handler: SubscriptionEpayReturn,
status: http.StatusFound,
rejectedLocation: "https://dashboard.example.com/wallet?pay=fail",
pendingLocation: "https://dashboard.example.com/wallet?pay=pending",
},
}
router := gin.New()
for _, endpoint := range endpoints {
router.Any(endpoint.path, endpoint.handler)
}
for _, endpoint := range endpoints {
for _, method := range []string{http.MethodGet, http.MethodPost} {
for _, tc := range cases {
t.Run(endpoint.name+"/"+method+"/"+tc.name, func(t *testing.T) {
params := url.Values{
"pid": {"epay_id"}, "type": {"alipay"}, "money": {"1.00"},
"name": {tc.productName}, "out_trade_no": {tc.tradeNo},
"trade_status": {tc.status}, "sign_type": {"MD5"},
}
if tc.sign != "" {
params.Set("sign", tc.sign)
}
request := httptest.NewRequest(method, endpoint.path+"?"+params.Encode(), nil)
if method == http.MethodPost {
request = httptest.NewRequest(method, endpoint.path, strings.NewReader(params.Encode()))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
recorder := httptest.NewRecorder()
require.NotPanics(t, func() { router.ServeHTTP(recorder, request) })
assert.Equal(t, endpoint.status, recorder.Code)
wantBody, wantLocation := endpoint.rejectedBody, endpoint.rejectedLocation
if tc.validPending {
wantBody, wantLocation = endpoint.pendingBody, endpoint.pendingLocation
}
if wantBody != "" {
assert.Equal(t, wantBody, recorder.Body.String())
}
assert.Equal(t, wantLocation, recorder.Header().Get("Location"))
})
}
}
}
}