package payment import ( "context" "encoding/json" "strings" "testing" "kra/internal/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) } }) } }