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