301 lines
11 KiB
Go
301 lines
11 KiB
Go
package payment
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"kra/app/system/biz"
|
|
)
|
|
|
|
func TestSavePaymentConfigRequiresDouyinAppIDWhenEnabled(t *testing.T) {
|
|
db := openIntegrationConfigTestDB(t)
|
|
repo := &paymentRepo{data: &Data{gormDB: newReloadableDB(db, nil)}}
|
|
values := map[string]any{
|
|
"merchant_id": "merchant-douyin",
|
|
"serial_no": "merchant-serial",
|
|
"api_key": "01234567890123456789012345678901",
|
|
"private_key": "merchant-private-key",
|
|
"platform_cert": "platform-public-key",
|
|
"platform_serial_no": "platform-serial",
|
|
}
|
|
encode := func() json.RawMessage {
|
|
raw, err := json.Marshal(values)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
err := repo.SaveConfig(context.Background(), &biz.PaymentConfig{
|
|
Provider: biz.PaymentDouyin,
|
|
Enabled: true,
|
|
Values: encode(),
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "app_id") {
|
|
t.Fatalf("missing app_id error = %v", err)
|
|
}
|
|
var count int64
|
|
if err = db.Model(&integrationConfigPO{}).Where("kind = ? AND provider = ?", integrationKindPayment, biz.PaymentDouyin).Count(&count).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("invalid enabled configuration was persisted: count=%d", count)
|
|
}
|
|
|
|
values["app_id"] = "douyin-app"
|
|
if err = repo.SaveConfig(context.Background(), &biz.PaymentConfig{
|
|
Provider: biz.PaymentDouyin,
|
|
Enabled: true,
|
|
Values: encode(),
|
|
}); err != nil {
|
|
t.Fatalf("save complete Douyin configuration: %v", err)
|
|
}
|
|
var stored integrationConfigPO
|
|
if err = db.Where("kind = ? AND provider = ?", integrationKindPayment, biz.PaymentDouyin).First(&stored).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !stored.Enabled || !strings.Contains(stored.Config, `"app_id":"douyin-app"`) {
|
|
t.Fatalf("stored configuration = %+v", stored)
|
|
}
|
|
}
|
|
|
|
func TestValidateDouyinPaymentConfigAcceptsPlatformCertificateSerialAlias(t *testing.T) {
|
|
err := validatePaymentConfig(biz.PaymentDouyin, map[string]any{
|
|
"app_id": "douyin-app",
|
|
"merchant_id": "merchant-douyin",
|
|
"serial_no": "merchant-serial",
|
|
"api_key": "01234567890123456789012345678901",
|
|
"private_key": "merchant-private-key",
|
|
"platform_cert": "platform-public-key",
|
|
"platform_cert_serial": "platform-serial",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("platform_cert_serial alias rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateAllinPayPaymentConfigRejectsUnknownQueryOrderType(t *testing.T) {
|
|
err := validatePaymentConfig(biz.PaymentAllinPay, map[string]any{
|
|
"cus_id": "customer",
|
|
"app_id": "app",
|
|
"private_key": "private-key",
|
|
"public_key": "public-key",
|
|
"query_order_type": "payinfo",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "reqsn") || !strings.Contains(err.Error(), "trxid") {
|
|
t.Fatalf("unknown query_order_type error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateAlipayV3PaymentConfigRequiresCertificates(t *testing.T) {
|
|
values := map[string]any{
|
|
"app_id": "alipay-v3-app",
|
|
"private_key": "private-key",
|
|
}
|
|
if err := validatePaymentConfig(biz.PaymentAlipayV3, values); err == nil || !strings.Contains(err.Error(), "app_cert") {
|
|
t.Fatalf("missing app certificate error = %v", err)
|
|
}
|
|
|
|
values["app_cert_content"] = "app-certificate"
|
|
if err := validatePaymentConfig(biz.PaymentAlipayV3, values); err == nil || !strings.Contains(err.Error(), "root_cert") {
|
|
t.Fatalf("missing root certificate error = %v", err)
|
|
}
|
|
|
|
values["alipay_root_cert_path"] = "alipayRootCert.crt"
|
|
if err := validatePaymentConfig(biz.PaymentAlipayV3, values); err == nil || !strings.Contains(err.Error(), "public_cert") {
|
|
t.Fatalf("missing public certificate error = %v", err)
|
|
}
|
|
|
|
values["alipay_public_cert_content"] = "alipay-public-certificate"
|
|
if err := validatePaymentConfig(biz.PaymentAlipayV3, values); err != nil {
|
|
t.Fatalf("valid Alipay V3 configuration rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidatePayPalPaymentConfigRequiresWebhookID(t *testing.T) {
|
|
values := map[string]any{
|
|
"client_id": "client-id",
|
|
"client_secret": "client-secret",
|
|
}
|
|
if err := validatePaymentConfig(biz.PaymentPayPal, values); err == nil || !strings.Contains(err.Error(), "webhook_id") {
|
|
t.Fatalf("missing webhook_id error = %v", err)
|
|
}
|
|
|
|
values["webhook_id"] = "webhook-1"
|
|
if err := validatePaymentConfig(biz.PaymentPayPal, values); err != nil {
|
|
t.Fatalf("valid PayPal configuration rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateWechatV2PaymentConfigRequiresRefundCertificatePair(t *testing.T) {
|
|
base := map[string]any{
|
|
"app_id": "wechat-app",
|
|
"merchant_id": "wechat-merchant",
|
|
"mch_key": "merchant-key",
|
|
}
|
|
if err := validatePaymentConfig(biz.PaymentWechatV2, base); err == nil || !strings.Contains(err.Error(), "client_cert") {
|
|
t.Fatalf("missing refund certificate error = %v", err)
|
|
}
|
|
|
|
base["client_cert"] = "certificate-pem"
|
|
if err := validatePaymentConfig(biz.PaymentWechatV2, base); err == nil || !strings.Contains(err.Error(), "client_key") {
|
|
t.Fatalf("missing refund private key error = %v", err)
|
|
}
|
|
|
|
base["client_key"] = "private-key-pem"
|
|
if err := validatePaymentConfig(biz.PaymentWechatV2, base); err != nil {
|
|
t.Fatalf("valid WeChat v2 configuration rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateQQPaymentConfigRequiresRefundCertificate(t *testing.T) {
|
|
base := map[string]any{
|
|
"mch_id": "qq-merchant",
|
|
"api_key": "merchant-key",
|
|
}
|
|
if err := validatePaymentConfig(biz.PaymentQQ, base); err == nil || !strings.Contains(err.Error(), "cert_file") {
|
|
t.Fatalf("missing refund certificate error = %v", err)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
values map[string]any
|
|
}{
|
|
{name: "certificate files", values: map[string]any{"cert_file": "cert.pem", "key_file": "key.pem"}},
|
|
{name: "pkcs12 file", values: map[string]any{"pkcs12_file": "merchant.p12"}},
|
|
{name: "certificate content", values: map[string]any{"cert_content": "certificate-pem", "key_content": "private-key-pem"}},
|
|
{name: "pkcs12 content", values: map[string]any{"pkcs12_content": "base64-pkcs12"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
values := map[string]any{}
|
|
for key, value := range base {
|
|
values[key] = value
|
|
}
|
|
for key, value := range tc.values {
|
|
values[key] = value
|
|
}
|
|
if err := validatePaymentConfig(biz.PaymentQQ, values); err != nil {
|
|
t.Fatalf("valid QQ configuration rejected: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPaymentProviderNotifyURLRequirementMatchesCallbackSupport(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
provider string
|
|
want bool
|
|
}{
|
|
{provider: biz.PaymentApple, want: false},
|
|
{provider: biz.PaymentAllinPay, want: false},
|
|
{provider: biz.PaymentSaobei, want: false},
|
|
{provider: biz.PaymentAlipay, want: true},
|
|
{provider: biz.PaymentAlipayV3, want: true},
|
|
{provider: biz.PaymentWechatV3, want: true},
|
|
} {
|
|
t.Run(tc.provider, func(t *testing.T) {
|
|
if got := paymentProviderRequiresNotifyURL(tc.provider); got != tc.want {
|
|
t.Fatalf("requires notify_url = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPaymentCreateNotifyURLRequirementMatchesSynchronousMethods(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
provider string
|
|
extra map[string]any
|
|
config map[string]any
|
|
want bool
|
|
}{
|
|
{name: "paypal create", provider: biz.PaymentPayPal, want: false},
|
|
{name: "alipay barcode", provider: biz.PaymentAlipay, extra: map[string]any{"method": "barcode"}, want: false},
|
|
{name: "alipay v3 barcode", provider: biz.PaymentAlipayV3, extra: map[string]any{"method": "barcode"}, want: false},
|
|
{name: "alipay ignores unsupported pay type selector", provider: biz.PaymentAlipay, extra: map[string]any{"pay_type": "barcode"}, want: true},
|
|
{name: "wechat v2 micropay", provider: biz.PaymentWechatV2, config: map[string]any{"trade_type": "MICROPAY"}, want: false},
|
|
{name: "wechat v3 codepay", provider: biz.PaymentWechatV3, extra: map[string]any{"method": "codepay"}, want: false},
|
|
{name: "qq micropay", provider: biz.PaymentQQ, config: map[string]any{"trade_type": "MICROPAY"}, want: false},
|
|
{name: "lakala retail", provider: biz.PaymentLakala, extra: map[string]any{"method": "retail"}, want: false},
|
|
{name: "alipay native", provider: biz.PaymentAlipay, config: map[string]any{"method": "native"}, want: true},
|
|
{name: "wechat v3 jsapi", provider: biz.PaymentWechatV3, extra: map[string]any{"trade_type": "jsapi"}, want: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := paymentCreateRequiresNotifyURL(tc.provider, tc.extra, tc.config); got != tc.want {
|
|
t.Fatalf("paymentCreateRequiresNotifyURL() = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPaymentCallbackEventIDPreservesVerifiedAdapterIdentity(t *testing.T) {
|
|
callback := &biz.PaymentCallback{
|
|
Provider: biz.PaymentDouyin,
|
|
Body: []byte(`{"id":"untrusted-flat-id","data":{"event_id":"nested-id"}}`),
|
|
}
|
|
result := &biz.PaymentResult{EventID: " verified-sdk-event "}
|
|
if got := paymentCallbackEventID(callback, result); got != "verified-sdk-event" {
|
|
t.Fatalf("paymentCallbackEventID() = %q, want verified adapter ID", got)
|
|
}
|
|
result.EventID = ""
|
|
if got := paymentCallbackEventID(callback, result); got != "untrusted-flat-id" {
|
|
t.Fatalf("paymentCallbackEventID() fallback = %q, want flat callback ID", got)
|
|
}
|
|
}
|
|
|
|
func TestValidateGenericPaymentConfigRequiresRuntimeEndpointsAndIdentity(t *testing.T) {
|
|
base := map[string]any{
|
|
"protocol_version": "v1",
|
|
"app_id": "app",
|
|
"merchant_id": "merchant",
|
|
"create_url": "https://pay.test/create",
|
|
"query_url": "https://pay.test/query",
|
|
"refund_url": "https://pay.test/refund",
|
|
"app_key": "secret",
|
|
"query_status_field": "data.status",
|
|
"query_success_values": "SUCCESS",
|
|
"query_trade_no_field": "data.trade_no",
|
|
"query_provider_trade_no_field": "data.provider_trade_no",
|
|
"query_amount_field": "data.amount",
|
|
"query_currency_field": "data.currency",
|
|
"query_amount_scale": "100",
|
|
"callback_status_field": "data.status",
|
|
"callback_success_values": "SUCCESS",
|
|
"callback_trade_no_field": "data.trade_no",
|
|
"callback_provider_trade_no_field": "data.provider_trade_no",
|
|
}
|
|
if err := validatePaymentConfig(biz.PaymentChinaums, base); err != nil {
|
|
t.Fatalf("valid generic config rejected: %v", err)
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
key string
|
|
value any
|
|
want string
|
|
}{
|
|
{name: "missing app id", key: "app_id", want: "app_id"},
|
|
{name: "missing merchant id", key: "merchant_id", want: "merchant_id"},
|
|
{name: "missing refund endpoint", key: "refund_url", want: "refund_url"},
|
|
{name: "zero amount scale", key: "query_amount_scale", value: "0", want: "query_amount_scale"},
|
|
{name: "non power amount scale", key: "query_amount_scale", value: "3", want: "query_amount_scale"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
values := map[string]any{}
|
|
for key, value := range base {
|
|
values[key] = value
|
|
}
|
|
if tc.value == nil {
|
|
values[tc.key] = ""
|
|
} else {
|
|
values[tc.key] = tc.value
|
|
}
|
|
err := validatePaymentConfig(biz.PaymentChinaums, values)
|
|
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("validation error = %v, want %q", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|