| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- //go:build unit
- package service
- import (
- "context"
- "crypto/rand"
- "crypto/rsa"
- "crypto/x509"
- "encoding/json"
- "encoding/pem"
- "strconv"
- "testing"
- "time"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/internal/payment"
- "github.com/stretchr/testify/require"
- )
- const webhookProviderTestEncryptionKey = "0123456789abcdef0123456789abcdef"
- type webhookProviderTestDouble struct {
- key string
- types []payment.PaymentType
- }
- func (p webhookProviderTestDouble) Name() string { return p.key }
- func (p webhookProviderTestDouble) ProviderKey() string { return p.key }
- func (p webhookProviderTestDouble) SupportedTypes() []payment.PaymentType { return p.types }
- func (p webhookProviderTestDouble) CreatePayment(context.Context, payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
- panic("unexpected call")
- }
- func (p webhookProviderTestDouble) QueryOrder(context.Context, string) (*payment.QueryOrderResponse, error) {
- panic("unexpected call")
- }
- func (p webhookProviderTestDouble) VerifyNotification(context.Context, string, map[string]string) (*payment.PaymentNotification, error) {
- panic("unexpected call")
- }
- func (p webhookProviderTestDouble) Refund(context.Context, payment.RefundRequest) (*payment.RefundResponse, error) {
- panic("unexpected call")
- }
- func encryptWebhookProviderConfig(t *testing.T, config map[string]string) string {
- t.Helper()
- data, err := json.Marshal(config)
- require.NoError(t, err)
- encrypted, err := payment.Encrypt(string(data), []byte(webhookProviderTestEncryptionKey))
- require.NoError(t, err)
- return encrypted
- }
- func newWebhookProviderTestLoadBalancer(client *dbent.Client) payment.LoadBalancer {
- return payment.NewDefaultLoadBalancer(client, []byte(webhookProviderTestEncryptionKey))
- }
- func encryptValidWebhookWxpayConfig(t *testing.T, suffix string) string {
- t.Helper()
- key, err := rsa.GenerateKey(rand.Reader, 2048)
- require.NoError(t, err)
- privDER, err := x509.MarshalPKCS8PrivateKey(key)
- require.NoError(t, err)
- pubDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
- require.NoError(t, err)
- return encryptWebhookProviderConfig(t, map[string]string{
- "appId": "wx-app-" + suffix,
- "mchId": "mch-" + suffix,
- "privateKey": string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER})),
- "apiV3Key": webhookProviderTestEncryptionKey,
- "publicKey": string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})),
- "publicKeyId": "public-key-id-" + suffix,
- "certSerial": "cert-serial-" + suffix,
- })
- }
- func TestGetOrderProviderInstanceResolvesUniqueLegacyProviderKey(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- inst, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeStripe).
- SetName("stripe-a").
- SetConfig(encryptWebhookProviderConfig(t, map[string]string{"secretKey": "sk_test_legacy_provider_key"})).
- SetSupportedTypes("stripe").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- providerKey := payment.TypeStripe
- order := &dbent.PaymentOrder{
- PaymentType: payment.TypeStripe,
- ProviderKey: &providerKey,
- }
- svc := &PaymentService{
- entClient: client,
- loadBalancer: newWebhookProviderTestLoadBalancer(client),
- }
- got, err := svc.getOrderProviderInstance(ctx, order)
- require.NoError(t, err)
- require.NotNil(t, got)
- require.Equal(t, inst.ID, got.ID)
- }
- func TestGetOrderProviderInstanceResolvesUniqueLegacyPaymentType(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- inst, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeWxpay).
- SetName("wxpay-a").
- SetConfig("{}").
- SetSupportedTypes("wxpay").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- order := &dbent.PaymentOrder{
- PaymentType: payment.TypeWxpayDirect,
- }
- svc := &PaymentService{
- entClient: client,
- loadBalancer: newWebhookProviderTestLoadBalancer(client),
- }
- got, err := svc.getOrderProviderInstance(ctx, order)
- require.NoError(t, err)
- require.NotNil(t, got)
- require.Equal(t, inst.ID, got.ID)
- }
- func TestGetOrderProviderInstanceLeavesAmbiguousLegacyOrderUnresolved(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeEasyPay).
- SetName("easypay-a").
- SetConfig("{}").
- SetSupportedTypes("wxpay").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeWxpay).
- SetName("wxpay-a").
- SetConfig("{}").
- SetSupportedTypes("wxpay").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- order := &dbent.PaymentOrder{
- PaymentType: payment.TypeWxpay,
- }
- svc := &PaymentService{
- entClient: client,
- loadBalancer: newWebhookProviderTestLoadBalancer(client),
- }
- got, err := svc.getOrderProviderInstance(ctx, order)
- require.NoError(t, err)
- require.Nil(t, got)
- }
- func TestGetOrderProviderInstanceLeavesLegacyProviderKeyUnresolvedWhenHistoricalInstancesConflict(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeStripe).
- SetName("stripe-disabled-legacy").
- SetConfig("{}").
- SetSupportedTypes("stripe").
- SetEnabled(false).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeStripe).
- SetName("stripe-enabled-current").
- SetConfig("{}").
- SetSupportedTypes("stripe").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- providerKey := payment.TypeStripe
- order := &dbent.PaymentOrder{
- PaymentType: payment.TypeStripe,
- ProviderKey: &providerKey,
- }
- svc := &PaymentService{
- entClient: client,
- loadBalancer: newWebhookProviderTestLoadBalancer(client),
- }
- got, err := svc.getOrderProviderInstance(ctx, order)
- require.NoError(t, err)
- require.Nil(t, got)
- }
- func TestGetOrderProviderInstanceLeavesProviderKeyMatchUnresolvedWhenTypeNotSupported(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeWxpay).
- SetName("wxpay-only").
- SetConfig("{}").
- SetSupportedTypes("wxpay").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- providerKey := payment.TypeWxpay
- order := &dbent.PaymentOrder{
- PaymentType: payment.TypeAlipayDirect,
- ProviderKey: &providerKey,
- }
- svc := &PaymentService{
- entClient: client,
- loadBalancer: newWebhookProviderTestLoadBalancer(client),
- }
- got, err := svc.getOrderProviderInstance(ctx, order)
- require.NoError(t, err)
- require.Nil(t, got)
- }
- func TestGetOrderProviderInstanceUsesProviderSnapshotWhenPinnedColumnMissing(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- inst, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeStripe).
- SetName("stripe-snapshot").
- SetConfig(encryptWebhookProviderConfig(t, map[string]string{"secretKey": "sk_snapshot"})).
- SetSupportedTypes("stripe").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- order := &dbent.PaymentOrder{
- ID: 42,
- PaymentType: payment.TypeStripe,
- ProviderSnapshot: map[string]any{
- "schema_version": 1,
- "provider_instance_id": strconv.FormatInt(inst.ID, 10),
- "provider_key": payment.TypeStripe,
- },
- }
- svc := &PaymentService{
- entClient: client,
- loadBalancer: newWebhookProviderTestLoadBalancer(client),
- }
- got, err := svc.getOrderProviderInstance(ctx, order)
- require.NoError(t, err)
- require.NotNil(t, got)
- require.Equal(t, inst.ID, got.ID)
- }
- func TestGetOrderProviderInstanceRejectsMissingSnapshotInstanceWithoutLegacyFallback(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeStripe).
- SetName("stripe-legacy-fallback").
- SetConfig(encryptWebhookProviderConfig(t, map[string]string{"secretKey": "sk_legacy"})).
- SetSupportedTypes("stripe").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- order := &dbent.PaymentOrder{
- ID: 43,
- PaymentType: payment.TypeStripe,
- ProviderSnapshot: map[string]any{
- "schema_version": 1,
- "provider_instance_id": "999999",
- "provider_key": payment.TypeStripe,
- },
- }
- svc := &PaymentService{
- entClient: client,
- loadBalancer: newWebhookProviderTestLoadBalancer(client),
- }
- got, err := svc.getOrderProviderInstance(ctx, order)
- require.Nil(t, got)
- require.Error(t, err)
- require.Contains(t, err.Error(), "provider snapshot instance 999999 is missing")
- }
- func TestGetWebhookProviderRejectsAmbiguousRegistryFallback(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- wxpayConfigA := encryptValidWebhookWxpayConfig(t, "a")
- wxpayConfigB := encryptValidWebhookWxpayConfig(t, "b")
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeWxpay).
- SetName("wxpay-a").
- SetConfig(wxpayConfigA).
- SetSupportedTypes("wxpay").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeWxpay).
- SetName("wxpay-b").
- SetConfig(wxpayConfigB).
- SetSupportedTypes("wxpay").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- svc := &PaymentService{
- entClient: client,
- loadBalancer: newWebhookProviderTestLoadBalancer(client),
- registry: payment.NewRegistry(),
- providersLoaded: true,
- }
- providers, err := svc.GetWebhookProviders(ctx, payment.TypeWxpay, "")
- require.NoError(t, err)
- require.Len(t, providers, 2)
- }
- func TestGetWebhookProvidersRejectAmbiguousFallbackForNonWxpay(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeAlipay).
- SetName("alipay-a").
- SetConfig("{}").
- SetSupportedTypes("alipay").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeAlipay).
- SetName("alipay-b").
- SetConfig("{}").
- SetSupportedTypes("alipay").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- svc := &PaymentService{
- entClient: client,
- registry: payment.NewRegistry(),
- providersLoaded: true,
- }
- _, err = svc.GetWebhookProviders(ctx, payment.TypeAlipay, "")
- require.Error(t, err)
- require.Contains(t, err.Error(), "ambiguous")
- }
- func TestGetWebhookProviderAllowsSingleInstanceRegistryFallback(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeStripe).
- SetName("stripe-a").
- SetConfig("{}").
- SetSupportedTypes("stripe").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- registry := payment.NewRegistry()
- registry.Register(webhookProviderTestDouble{
- key: payment.TypeStripe,
- types: []payment.PaymentType{payment.TypeStripe},
- })
- svc := &PaymentService{
- entClient: client,
- registry: registry,
- providersLoaded: true,
- }
- providers, err := svc.GetWebhookProviders(ctx, payment.TypeStripe, "")
- require.NoError(t, err)
- require.Len(t, providers, 1)
- prov := providers[0]
- require.Equal(t, payment.TypeStripe, prov.ProviderKey())
- }
- func TestGetWebhookProviderRejectsRegistryFallbackForPinnedOrder(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- user, err := client.User.Create().
- SetEmail("webhook@example.com").
- SetPasswordHash("hash").
- SetUsername("webhook").
- Save(ctx)
- require.NoError(t, err)
- pinnedInstanceID := "999"
- _, err = client.PaymentOrder.Create().
- SetUserID(user.ID).
- SetUserEmail(user.Email).
- SetUserName(user.Username).
- SetAmount(88).
- SetPayAmount(88).
- SetFeeRate(0).
- SetRechargeCode("TEST-RECHARGE").
- SetOutTradeNo("sub2_test_pinned_order").
- SetPaymentType(payment.TypeWxpay).
- SetPaymentTradeNo("").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusPending).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- SetProviderInstanceID(pinnedInstanceID).
- Save(ctx)
- require.NoError(t, err)
- registry := payment.NewRegistry()
- registry.Register(webhookProviderTestDouble{
- key: payment.TypeWxpay,
- types: []payment.PaymentType{payment.TypeWxpay},
- })
- svc := &PaymentService{
- entClient: client,
- registry: registry,
- providersLoaded: true,
- }
- _, err = svc.GetWebhookProviders(ctx, payment.TypeWxpay, "sub2_test_pinned_order")
- require.Error(t, err)
- require.Contains(t, err.Error(), "provider instance")
- }
- func TestGetWebhookProviderUsesProviderSnapshotBeforeWxpayFallback(t *testing.T) {
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- user, err := client.User.Create().
- SetEmail("snapshot-webhook@example.com").
- SetPasswordHash("hash").
- SetUsername("snapshot-webhook").
- Save(ctx)
- require.NoError(t, err)
- wxpayConfigA := encryptValidWebhookWxpayConfig(t, "snapshot-a")
- wxpayConfigB := encryptValidWebhookWxpayConfig(t, "snapshot-b")
- instA, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeWxpay).
- SetName("wxpay-snapshot-a").
- SetConfig(wxpayConfigA).
- SetSupportedTypes("wxpay").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeWxpay).
- SetName("wxpay-snapshot-b").
- SetConfig(wxpayConfigB).
- SetSupportedTypes("wxpay").
- SetEnabled(true).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.PaymentOrder.Create().
- SetUserID(user.ID).
- SetUserEmail(user.Email).
- SetUserName(user.Username).
- SetAmount(66).
- SetPayAmount(66).
- SetFeeRate(0).
- SetRechargeCode("SNAPSHOT-WEBHOOK").
- SetOutTradeNo("sub2_test_snapshot_webhook_order").
- SetPaymentType(payment.TypeWxpay).
- SetPaymentTradeNo("").
- SetOrderType(payment.OrderTypeBalance).
- SetStatus(OrderStatusPending).
- SetExpiresAt(time.Now().Add(time.Hour)).
- SetClientIP("127.0.0.1").
- SetSrcHost("api.example.com").
- SetProviderSnapshot(map[string]any{
- "schema_version": 1,
- "provider_instance_id": strconv.FormatInt(instA.ID, 10),
- "provider_key": payment.TypeWxpay,
- "payment_mode": "native",
- }).
- Save(ctx)
- require.NoError(t, err)
- svc := &PaymentService{
- entClient: client,
- loadBalancer: newWebhookProviderTestLoadBalancer(client),
- registry: payment.NewRegistry(),
- providersLoaded: true,
- }
- providers, err := svc.GetWebhookProviders(ctx, payment.TypeWxpay, "sub2_test_snapshot_webhook_order")
- require.NoError(t, err)
- require.Len(t, providers, 1)
- require.Equal(t, payment.TypeWxpay, providers[0].ProviderKey())
- }
|