payment_webhook_provider_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "crypto/rand"
  6. "crypto/rsa"
  7. "crypto/x509"
  8. "encoding/json"
  9. "encoding/pem"
  10. "strconv"
  11. "testing"
  12. "time"
  13. dbent "github.com/Wei-Shaw/sub2api/ent"
  14. "github.com/Wei-Shaw/sub2api/internal/payment"
  15. "github.com/stretchr/testify/require"
  16. )
  17. const webhookProviderTestEncryptionKey = "0123456789abcdef0123456789abcdef"
  18. type webhookProviderTestDouble struct {
  19. key string
  20. types []payment.PaymentType
  21. }
  22. func (p webhookProviderTestDouble) Name() string { return p.key }
  23. func (p webhookProviderTestDouble) ProviderKey() string { return p.key }
  24. func (p webhookProviderTestDouble) SupportedTypes() []payment.PaymentType { return p.types }
  25. func (p webhookProviderTestDouble) CreatePayment(context.Context, payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
  26. panic("unexpected call")
  27. }
  28. func (p webhookProviderTestDouble) QueryOrder(context.Context, string) (*payment.QueryOrderResponse, error) {
  29. panic("unexpected call")
  30. }
  31. func (p webhookProviderTestDouble) VerifyNotification(context.Context, string, map[string]string) (*payment.PaymentNotification, error) {
  32. panic("unexpected call")
  33. }
  34. func (p webhookProviderTestDouble) Refund(context.Context, payment.RefundRequest) (*payment.RefundResponse, error) {
  35. panic("unexpected call")
  36. }
  37. func encryptWebhookProviderConfig(t *testing.T, config map[string]string) string {
  38. t.Helper()
  39. data, err := json.Marshal(config)
  40. require.NoError(t, err)
  41. encrypted, err := payment.Encrypt(string(data), []byte(webhookProviderTestEncryptionKey))
  42. require.NoError(t, err)
  43. return encrypted
  44. }
  45. func newWebhookProviderTestLoadBalancer(client *dbent.Client) payment.LoadBalancer {
  46. return payment.NewDefaultLoadBalancer(client, []byte(webhookProviderTestEncryptionKey))
  47. }
  48. func encryptValidWebhookWxpayConfig(t *testing.T, suffix string) string {
  49. t.Helper()
  50. key, err := rsa.GenerateKey(rand.Reader, 2048)
  51. require.NoError(t, err)
  52. privDER, err := x509.MarshalPKCS8PrivateKey(key)
  53. require.NoError(t, err)
  54. pubDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
  55. require.NoError(t, err)
  56. return encryptWebhookProviderConfig(t, map[string]string{
  57. "appId": "wx-app-" + suffix,
  58. "mchId": "mch-" + suffix,
  59. "privateKey": string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER})),
  60. "apiV3Key": webhookProviderTestEncryptionKey,
  61. "publicKey": string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})),
  62. "publicKeyId": "public-key-id-" + suffix,
  63. "certSerial": "cert-serial-" + suffix,
  64. })
  65. }
  66. func TestGetOrderProviderInstanceResolvesUniqueLegacyProviderKey(t *testing.T) {
  67. ctx := context.Background()
  68. client := newPaymentConfigServiceTestClient(t)
  69. inst, err := client.PaymentProviderInstance.Create().
  70. SetProviderKey(payment.TypeStripe).
  71. SetName("stripe-a").
  72. SetConfig(encryptWebhookProviderConfig(t, map[string]string{"secretKey": "sk_test_legacy_provider_key"})).
  73. SetSupportedTypes("stripe").
  74. SetEnabled(true).
  75. Save(ctx)
  76. require.NoError(t, err)
  77. providerKey := payment.TypeStripe
  78. order := &dbent.PaymentOrder{
  79. PaymentType: payment.TypeStripe,
  80. ProviderKey: &providerKey,
  81. }
  82. svc := &PaymentService{
  83. entClient: client,
  84. loadBalancer: newWebhookProviderTestLoadBalancer(client),
  85. }
  86. got, err := svc.getOrderProviderInstance(ctx, order)
  87. require.NoError(t, err)
  88. require.NotNil(t, got)
  89. require.Equal(t, inst.ID, got.ID)
  90. }
  91. func TestGetOrderProviderInstanceResolvesUniqueLegacyPaymentType(t *testing.T) {
  92. ctx := context.Background()
  93. client := newPaymentConfigServiceTestClient(t)
  94. inst, err := client.PaymentProviderInstance.Create().
  95. SetProviderKey(payment.TypeWxpay).
  96. SetName("wxpay-a").
  97. SetConfig("{}").
  98. SetSupportedTypes("wxpay").
  99. SetEnabled(true).
  100. Save(ctx)
  101. require.NoError(t, err)
  102. order := &dbent.PaymentOrder{
  103. PaymentType: payment.TypeWxpayDirect,
  104. }
  105. svc := &PaymentService{
  106. entClient: client,
  107. loadBalancer: newWebhookProviderTestLoadBalancer(client),
  108. }
  109. got, err := svc.getOrderProviderInstance(ctx, order)
  110. require.NoError(t, err)
  111. require.NotNil(t, got)
  112. require.Equal(t, inst.ID, got.ID)
  113. }
  114. func TestGetOrderProviderInstanceLeavesAmbiguousLegacyOrderUnresolved(t *testing.T) {
  115. ctx := context.Background()
  116. client := newPaymentConfigServiceTestClient(t)
  117. _, err := client.PaymentProviderInstance.Create().
  118. SetProviderKey(payment.TypeEasyPay).
  119. SetName("easypay-a").
  120. SetConfig("{}").
  121. SetSupportedTypes("wxpay").
  122. SetEnabled(true).
  123. Save(ctx)
  124. require.NoError(t, err)
  125. _, err = client.PaymentProviderInstance.Create().
  126. SetProviderKey(payment.TypeWxpay).
  127. SetName("wxpay-a").
  128. SetConfig("{}").
  129. SetSupportedTypes("wxpay").
  130. SetEnabled(true).
  131. Save(ctx)
  132. require.NoError(t, err)
  133. order := &dbent.PaymentOrder{
  134. PaymentType: payment.TypeWxpay,
  135. }
  136. svc := &PaymentService{
  137. entClient: client,
  138. loadBalancer: newWebhookProviderTestLoadBalancer(client),
  139. }
  140. got, err := svc.getOrderProviderInstance(ctx, order)
  141. require.NoError(t, err)
  142. require.Nil(t, got)
  143. }
  144. func TestGetOrderProviderInstanceLeavesLegacyProviderKeyUnresolvedWhenHistoricalInstancesConflict(t *testing.T) {
  145. ctx := context.Background()
  146. client := newPaymentConfigServiceTestClient(t)
  147. _, err := client.PaymentProviderInstance.Create().
  148. SetProviderKey(payment.TypeStripe).
  149. SetName("stripe-disabled-legacy").
  150. SetConfig("{}").
  151. SetSupportedTypes("stripe").
  152. SetEnabled(false).
  153. Save(ctx)
  154. require.NoError(t, err)
  155. _, err = client.PaymentProviderInstance.Create().
  156. SetProviderKey(payment.TypeStripe).
  157. SetName("stripe-enabled-current").
  158. SetConfig("{}").
  159. SetSupportedTypes("stripe").
  160. SetEnabled(true).
  161. Save(ctx)
  162. require.NoError(t, err)
  163. providerKey := payment.TypeStripe
  164. order := &dbent.PaymentOrder{
  165. PaymentType: payment.TypeStripe,
  166. ProviderKey: &providerKey,
  167. }
  168. svc := &PaymentService{
  169. entClient: client,
  170. loadBalancer: newWebhookProviderTestLoadBalancer(client),
  171. }
  172. got, err := svc.getOrderProviderInstance(ctx, order)
  173. require.NoError(t, err)
  174. require.Nil(t, got)
  175. }
  176. func TestGetOrderProviderInstanceLeavesProviderKeyMatchUnresolvedWhenTypeNotSupported(t *testing.T) {
  177. ctx := context.Background()
  178. client := newPaymentConfigServiceTestClient(t)
  179. _, err := client.PaymentProviderInstance.Create().
  180. SetProviderKey(payment.TypeWxpay).
  181. SetName("wxpay-only").
  182. SetConfig("{}").
  183. SetSupportedTypes("wxpay").
  184. SetEnabled(true).
  185. Save(ctx)
  186. require.NoError(t, err)
  187. providerKey := payment.TypeWxpay
  188. order := &dbent.PaymentOrder{
  189. PaymentType: payment.TypeAlipayDirect,
  190. ProviderKey: &providerKey,
  191. }
  192. svc := &PaymentService{
  193. entClient: client,
  194. loadBalancer: newWebhookProviderTestLoadBalancer(client),
  195. }
  196. got, err := svc.getOrderProviderInstance(ctx, order)
  197. require.NoError(t, err)
  198. require.Nil(t, got)
  199. }
  200. func TestGetOrderProviderInstanceUsesProviderSnapshotWhenPinnedColumnMissing(t *testing.T) {
  201. ctx := context.Background()
  202. client := newPaymentConfigServiceTestClient(t)
  203. inst, err := client.PaymentProviderInstance.Create().
  204. SetProviderKey(payment.TypeStripe).
  205. SetName("stripe-snapshot").
  206. SetConfig(encryptWebhookProviderConfig(t, map[string]string{"secretKey": "sk_snapshot"})).
  207. SetSupportedTypes("stripe").
  208. SetEnabled(true).
  209. Save(ctx)
  210. require.NoError(t, err)
  211. order := &dbent.PaymentOrder{
  212. ID: 42,
  213. PaymentType: payment.TypeStripe,
  214. ProviderSnapshot: map[string]any{
  215. "schema_version": 1,
  216. "provider_instance_id": strconv.FormatInt(inst.ID, 10),
  217. "provider_key": payment.TypeStripe,
  218. },
  219. }
  220. svc := &PaymentService{
  221. entClient: client,
  222. loadBalancer: newWebhookProviderTestLoadBalancer(client),
  223. }
  224. got, err := svc.getOrderProviderInstance(ctx, order)
  225. require.NoError(t, err)
  226. require.NotNil(t, got)
  227. require.Equal(t, inst.ID, got.ID)
  228. }
  229. func TestGetOrderProviderInstanceRejectsMissingSnapshotInstanceWithoutLegacyFallback(t *testing.T) {
  230. ctx := context.Background()
  231. client := newPaymentConfigServiceTestClient(t)
  232. _, err := client.PaymentProviderInstance.Create().
  233. SetProviderKey(payment.TypeStripe).
  234. SetName("stripe-legacy-fallback").
  235. SetConfig(encryptWebhookProviderConfig(t, map[string]string{"secretKey": "sk_legacy"})).
  236. SetSupportedTypes("stripe").
  237. SetEnabled(true).
  238. Save(ctx)
  239. require.NoError(t, err)
  240. order := &dbent.PaymentOrder{
  241. ID: 43,
  242. PaymentType: payment.TypeStripe,
  243. ProviderSnapshot: map[string]any{
  244. "schema_version": 1,
  245. "provider_instance_id": "999999",
  246. "provider_key": payment.TypeStripe,
  247. },
  248. }
  249. svc := &PaymentService{
  250. entClient: client,
  251. loadBalancer: newWebhookProviderTestLoadBalancer(client),
  252. }
  253. got, err := svc.getOrderProviderInstance(ctx, order)
  254. require.Nil(t, got)
  255. require.Error(t, err)
  256. require.Contains(t, err.Error(), "provider snapshot instance 999999 is missing")
  257. }
  258. func TestGetWebhookProviderRejectsAmbiguousRegistryFallback(t *testing.T) {
  259. ctx := context.Background()
  260. client := newPaymentConfigServiceTestClient(t)
  261. wxpayConfigA := encryptValidWebhookWxpayConfig(t, "a")
  262. wxpayConfigB := encryptValidWebhookWxpayConfig(t, "b")
  263. _, err := client.PaymentProviderInstance.Create().
  264. SetProviderKey(payment.TypeWxpay).
  265. SetName("wxpay-a").
  266. SetConfig(wxpayConfigA).
  267. SetSupportedTypes("wxpay").
  268. SetEnabled(true).
  269. Save(ctx)
  270. require.NoError(t, err)
  271. _, err = client.PaymentProviderInstance.Create().
  272. SetProviderKey(payment.TypeWxpay).
  273. SetName("wxpay-b").
  274. SetConfig(wxpayConfigB).
  275. SetSupportedTypes("wxpay").
  276. SetEnabled(true).
  277. Save(ctx)
  278. require.NoError(t, err)
  279. svc := &PaymentService{
  280. entClient: client,
  281. loadBalancer: newWebhookProviderTestLoadBalancer(client),
  282. registry: payment.NewRegistry(),
  283. providersLoaded: true,
  284. }
  285. providers, err := svc.GetWebhookProviders(ctx, payment.TypeWxpay, "")
  286. require.NoError(t, err)
  287. require.Len(t, providers, 2)
  288. }
  289. func TestGetWebhookProvidersRejectAmbiguousFallbackForNonWxpay(t *testing.T) {
  290. ctx := context.Background()
  291. client := newPaymentConfigServiceTestClient(t)
  292. _, err := client.PaymentProviderInstance.Create().
  293. SetProviderKey(payment.TypeAlipay).
  294. SetName("alipay-a").
  295. SetConfig("{}").
  296. SetSupportedTypes("alipay").
  297. SetEnabled(true).
  298. Save(ctx)
  299. require.NoError(t, err)
  300. _, err = client.PaymentProviderInstance.Create().
  301. SetProviderKey(payment.TypeAlipay).
  302. SetName("alipay-b").
  303. SetConfig("{}").
  304. SetSupportedTypes("alipay").
  305. SetEnabled(true).
  306. Save(ctx)
  307. require.NoError(t, err)
  308. svc := &PaymentService{
  309. entClient: client,
  310. registry: payment.NewRegistry(),
  311. providersLoaded: true,
  312. }
  313. _, err = svc.GetWebhookProviders(ctx, payment.TypeAlipay, "")
  314. require.Error(t, err)
  315. require.Contains(t, err.Error(), "ambiguous")
  316. }
  317. func TestGetWebhookProviderAllowsSingleInstanceRegistryFallback(t *testing.T) {
  318. ctx := context.Background()
  319. client := newPaymentConfigServiceTestClient(t)
  320. _, err := client.PaymentProviderInstance.Create().
  321. SetProviderKey(payment.TypeStripe).
  322. SetName("stripe-a").
  323. SetConfig("{}").
  324. SetSupportedTypes("stripe").
  325. SetEnabled(true).
  326. Save(ctx)
  327. require.NoError(t, err)
  328. registry := payment.NewRegistry()
  329. registry.Register(webhookProviderTestDouble{
  330. key: payment.TypeStripe,
  331. types: []payment.PaymentType{payment.TypeStripe},
  332. })
  333. svc := &PaymentService{
  334. entClient: client,
  335. registry: registry,
  336. providersLoaded: true,
  337. }
  338. providers, err := svc.GetWebhookProviders(ctx, payment.TypeStripe, "")
  339. require.NoError(t, err)
  340. require.Len(t, providers, 1)
  341. prov := providers[0]
  342. require.Equal(t, payment.TypeStripe, prov.ProviderKey())
  343. }
  344. func TestGetWebhookProviderRejectsRegistryFallbackForPinnedOrder(t *testing.T) {
  345. ctx := context.Background()
  346. client := newPaymentConfigServiceTestClient(t)
  347. user, err := client.User.Create().
  348. SetEmail("webhook@example.com").
  349. SetPasswordHash("hash").
  350. SetUsername("webhook").
  351. Save(ctx)
  352. require.NoError(t, err)
  353. pinnedInstanceID := "999"
  354. _, err = client.PaymentOrder.Create().
  355. SetUserID(user.ID).
  356. SetUserEmail(user.Email).
  357. SetUserName(user.Username).
  358. SetAmount(88).
  359. SetPayAmount(88).
  360. SetFeeRate(0).
  361. SetRechargeCode("TEST-RECHARGE").
  362. SetOutTradeNo("sub2_test_pinned_order").
  363. SetPaymentType(payment.TypeWxpay).
  364. SetPaymentTradeNo("").
  365. SetOrderType(payment.OrderTypeBalance).
  366. SetStatus(OrderStatusPending).
  367. SetExpiresAt(time.Now().Add(time.Hour)).
  368. SetClientIP("127.0.0.1").
  369. SetSrcHost("api.example.com").
  370. SetProviderInstanceID(pinnedInstanceID).
  371. Save(ctx)
  372. require.NoError(t, err)
  373. registry := payment.NewRegistry()
  374. registry.Register(webhookProviderTestDouble{
  375. key: payment.TypeWxpay,
  376. types: []payment.PaymentType{payment.TypeWxpay},
  377. })
  378. svc := &PaymentService{
  379. entClient: client,
  380. registry: registry,
  381. providersLoaded: true,
  382. }
  383. _, err = svc.GetWebhookProviders(ctx, payment.TypeWxpay, "sub2_test_pinned_order")
  384. require.Error(t, err)
  385. require.Contains(t, err.Error(), "provider instance")
  386. }
  387. func TestGetWebhookProviderUsesProviderSnapshotBeforeWxpayFallback(t *testing.T) {
  388. ctx := context.Background()
  389. client := newPaymentConfigServiceTestClient(t)
  390. user, err := client.User.Create().
  391. SetEmail("snapshot-webhook@example.com").
  392. SetPasswordHash("hash").
  393. SetUsername("snapshot-webhook").
  394. Save(ctx)
  395. require.NoError(t, err)
  396. wxpayConfigA := encryptValidWebhookWxpayConfig(t, "snapshot-a")
  397. wxpayConfigB := encryptValidWebhookWxpayConfig(t, "snapshot-b")
  398. instA, err := client.PaymentProviderInstance.Create().
  399. SetProviderKey(payment.TypeWxpay).
  400. SetName("wxpay-snapshot-a").
  401. SetConfig(wxpayConfigA).
  402. SetSupportedTypes("wxpay").
  403. SetEnabled(true).
  404. Save(ctx)
  405. require.NoError(t, err)
  406. _, err = client.PaymentProviderInstance.Create().
  407. SetProviderKey(payment.TypeWxpay).
  408. SetName("wxpay-snapshot-b").
  409. SetConfig(wxpayConfigB).
  410. SetSupportedTypes("wxpay").
  411. SetEnabled(true).
  412. Save(ctx)
  413. require.NoError(t, err)
  414. _, err = client.PaymentOrder.Create().
  415. SetUserID(user.ID).
  416. SetUserEmail(user.Email).
  417. SetUserName(user.Username).
  418. SetAmount(66).
  419. SetPayAmount(66).
  420. SetFeeRate(0).
  421. SetRechargeCode("SNAPSHOT-WEBHOOK").
  422. SetOutTradeNo("sub2_test_snapshot_webhook_order").
  423. SetPaymentType(payment.TypeWxpay).
  424. SetPaymentTradeNo("").
  425. SetOrderType(payment.OrderTypeBalance).
  426. SetStatus(OrderStatusPending).
  427. SetExpiresAt(time.Now().Add(time.Hour)).
  428. SetClientIP("127.0.0.1").
  429. SetSrcHost("api.example.com").
  430. SetProviderSnapshot(map[string]any{
  431. "schema_version": 1,
  432. "provider_instance_id": strconv.FormatInt(instA.ID, 10),
  433. "provider_key": payment.TypeWxpay,
  434. "payment_mode": "native",
  435. }).
  436. Save(ctx)
  437. require.NoError(t, err)
  438. svc := &PaymentService{
  439. entClient: client,
  440. loadBalancer: newWebhookProviderTestLoadBalancer(client),
  441. registry: payment.NewRegistry(),
  442. providersLoaded: true,
  443. }
  444. providers, err := svc.GetWebhookProviders(ctx, payment.TypeWxpay, "sub2_test_snapshot_webhook_order")
  445. require.NoError(t, err)
  446. require.Len(t, providers, 1)
  447. require.Equal(t, payment.TypeWxpay, providers[0].ProviderKey())
  448. }