payment_config_providers_test.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "crypto/rand"
  6. "crypto/rsa"
  7. "crypto/x509"
  8. "encoding/pem"
  9. "strconv"
  10. "testing"
  11. "time"
  12. dbent "github.com/Wei-Shaw/sub2api/ent"
  13. "github.com/Wei-Shaw/sub2api/internal/payment"
  14. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/require"
  17. )
  18. func TestValidateProviderRequest(t *testing.T) {
  19. t.Parallel()
  20. tests := []struct {
  21. name string
  22. providerKey string
  23. providerName string
  24. supportedTypes string
  25. wantErr bool
  26. errContains string
  27. }{
  28. {
  29. name: "valid easypay with types",
  30. providerKey: "easypay",
  31. providerName: "MyProvider",
  32. supportedTypes: "alipay,wxpay",
  33. wantErr: false,
  34. },
  35. {
  36. name: "valid stripe with empty types",
  37. providerKey: "stripe",
  38. providerName: "Stripe Provider",
  39. supportedTypes: "",
  40. wantErr: false,
  41. },
  42. {
  43. name: "valid airwallex provider",
  44. providerKey: payment.TypeAirwallex,
  45. providerName: "Airwallex Provider",
  46. supportedTypes: payment.TypeAirwallex,
  47. wantErr: false,
  48. },
  49. {
  50. name: "valid alipay provider",
  51. providerKey: "alipay",
  52. providerName: "Alipay Direct",
  53. supportedTypes: "alipay",
  54. wantErr: false,
  55. },
  56. {
  57. name: "valid wxpay provider",
  58. providerKey: "wxpay",
  59. providerName: "WeChat Pay",
  60. supportedTypes: "wxpay",
  61. wantErr: false,
  62. },
  63. {
  64. name: "invalid provider key",
  65. providerKey: "invalid",
  66. providerName: "Name",
  67. supportedTypes: "alipay",
  68. wantErr: true,
  69. errContains: "invalid provider key",
  70. },
  71. {
  72. name: "empty name",
  73. providerKey: "easypay",
  74. providerName: "",
  75. supportedTypes: "alipay",
  76. wantErr: true,
  77. errContains: "provider name is required",
  78. },
  79. {
  80. name: "whitespace-only name",
  81. providerKey: "easypay",
  82. providerName: " ",
  83. supportedTypes: "alipay",
  84. wantErr: true,
  85. errContains: "provider name is required",
  86. },
  87. {
  88. name: "tab-only name",
  89. providerKey: "easypay",
  90. providerName: "\t",
  91. supportedTypes: "alipay",
  92. wantErr: true,
  93. errContains: "provider name is required",
  94. },
  95. }
  96. for _, tc := range tests {
  97. t.Run(tc.name, func(t *testing.T) {
  98. t.Parallel()
  99. err := validateProviderRequest(tc.providerKey, tc.providerName, tc.supportedTypes)
  100. if tc.wantErr {
  101. require.Error(t, err)
  102. assert.Contains(t, err.Error(), tc.errContains)
  103. } else {
  104. require.NoError(t, err)
  105. }
  106. })
  107. }
  108. }
  109. func TestValidateEasyPayCustomMethods(t *testing.T) {
  110. t.Parallel()
  111. tests := []struct {
  112. name string
  113. config map[string]string
  114. supportedTypes string
  115. wantErr string
  116. }{
  117. {
  118. name: "valid custom methods",
  119. config: map[string]string{"customMethods": `[{"type":"ldc","upstreamType":"epay","displayName":"LDC"}]`},
  120. supportedTypes: "alipay,wxpay,ldc",
  121. },
  122. {
  123. name: "malformed custom methods json",
  124. config: map[string]string{"customMethods": `not-json`},
  125. supportedTypes: "alipay,wxpay,ldc",
  126. wantErr: "customMethods must be a JSON array",
  127. },
  128. {
  129. name: "missing upstream type",
  130. config: map[string]string{"customMethods": `[{"type":"ldc","displayName":"LDC"}]`},
  131. supportedTypes: "alipay,wxpay,ldc",
  132. wantErr: "customMethods upstreamType is required",
  133. },
  134. {
  135. name: "duplicate custom type",
  136. config: map[string]string{"customMethods": `[{"type":"ldc","upstreamType":"epay"},{"type":"ldc","upstreamType":"epay2"}]`},
  137. supportedTypes: "alipay,wxpay,ldc",
  138. wantErr: "duplicate customMethods type",
  139. },
  140. {
  141. name: "custom type must already be lowercase",
  142. config: map[string]string{"customMethods": `[{"type":"LDC","upstreamType":"epay"}]`},
  143. supportedTypes: "alipay,wxpay,ldc",
  144. wantErr: "customMethods type may only contain lowercase letters",
  145. },
  146. {
  147. name: "upstream type must already be lowercase",
  148. config: map[string]string{"customMethods": `[{"type":"ldc","upstreamType":"ALIPAY"}]`},
  149. supportedTypes: "alipay,wxpay,ldc",
  150. wantErr: "customMethods upstreamType may only contain lowercase letters",
  151. },
  152. {
  153. name: "custom type uses alipay prefix",
  154. config: map[string]string{"customMethods": `[{"type":"alipay_hk","upstreamType":"hkpay"}]`},
  155. supportedTypes: "alipay,wxpay,alipay_hk",
  156. wantErr: "customMethods type cannot start with alipay or wxpay",
  157. },
  158. {
  159. name: "custom type uses wxpay prefix",
  160. config: map[string]string{"customMethods": `[{"type":"wxpay_usdt","upstreamType":"usdt"}]`},
  161. supportedTypes: "alipay,wxpay,wxpay_usdt",
  162. wantErr: "customMethods type cannot start with alipay or wxpay",
  163. },
  164. {
  165. name: "supported custom type missing mapping",
  166. config: map[string]string{"customMethods": `[{"type":"ldc","upstreamType":"epay"}]`},
  167. supportedTypes: "alipay,wxpay,ldc,usdt_trc20",
  168. wantErr: "supported EasyPay custom type usdt_trc20 has no customMethods mapping",
  169. },
  170. {
  171. name: "supported custom type must already be lowercase",
  172. config: map[string]string{"customMethods": `[{"type":"ldc","upstreamType":"epay"}]`},
  173. supportedTypes: "alipay,wxpay,LDC",
  174. wantErr: "supported EasyPay custom type LDC may only contain lowercase letters",
  175. },
  176. }
  177. for _, tc := range tests {
  178. tc := tc
  179. t.Run(tc.name, func(t *testing.T) {
  180. t.Parallel()
  181. err := validateEasyPayCustomMethods(tc.config, tc.supportedTypes)
  182. if tc.wantErr == "" {
  183. require.NoError(t, err)
  184. return
  185. }
  186. require.Error(t, err)
  187. require.Contains(t, err.Error(), tc.wantErr)
  188. })
  189. }
  190. }
  191. func TestIsSensitiveProviderConfigField(t *testing.T) {
  192. t.Parallel()
  193. tests := []struct {
  194. providerKey string
  195. field string
  196. wantSen bool
  197. }{
  198. // Stripe: publishableKey is public, only secretKey/webhookSecret are secrets
  199. {"stripe", "secretKey", true},
  200. {"stripe", "webhookSecret", true},
  201. {"stripe", "SecretKey", true}, // case-insensitive
  202. {"stripe", "publishableKey", false},
  203. {"stripe", "currency", false},
  204. {"stripe", "appId", false},
  205. // Alipay
  206. {"alipay", "privateKey", true},
  207. {"alipay", "publicKey", true},
  208. {"alipay", "alipayPublicKey", true},
  209. {"alipay", "appId", false},
  210. {"alipay", "notifyUrl", false},
  211. // Wxpay
  212. {"wxpay", "privateKey", true},
  213. {"wxpay", "apiV3Key", true},
  214. {"wxpay", "publicKey", true},
  215. {"wxpay", "publicKeyId", false},
  216. {"wxpay", "certSerial", false},
  217. {"wxpay", "mchId", false},
  218. // EasyPay
  219. {"easypay", "pkey", true},
  220. {"easypay", "pid", false},
  221. {"easypay", "apiBase", false},
  222. // Airwallex
  223. {payment.TypeAirwallex, "apiKey", true},
  224. {payment.TypeAirwallex, "webhookSecret", true},
  225. {payment.TypeAirwallex, "clientId", false},
  226. {payment.TypeAirwallex, "apiBase", false},
  227. {payment.TypeAirwallex, "accountId", false},
  228. {payment.TypeAirwallex, "currency", false},
  229. // Unknown provider: never sensitive
  230. {"unknown", "secretKey", false},
  231. }
  232. for _, tc := range tests {
  233. tc := tc
  234. t.Run(tc.providerKey+"/"+tc.field, func(t *testing.T) {
  235. t.Parallel()
  236. got := isSensitiveProviderConfigField(tc.providerKey, tc.field)
  237. assert.Equal(t, tc.wantSen, got, "isSensitiveProviderConfigField(%q, %q)", tc.providerKey, tc.field)
  238. })
  239. }
  240. }
  241. func TestJoinTypes(t *testing.T) {
  242. t.Parallel()
  243. tests := []struct {
  244. name string
  245. input []string
  246. want string
  247. }{
  248. {
  249. name: "multiple types",
  250. input: []string{"alipay", "wxpay"},
  251. want: "alipay,wxpay",
  252. },
  253. {
  254. name: "single type",
  255. input: []string{"stripe"},
  256. want: "stripe",
  257. },
  258. {
  259. name: "empty slice",
  260. input: []string{},
  261. want: "",
  262. },
  263. {
  264. name: "nil slice",
  265. input: nil,
  266. want: "",
  267. },
  268. {
  269. name: "three types",
  270. input: []string{"alipay", "wxpay", "stripe"},
  271. want: "alipay,wxpay,stripe",
  272. },
  273. {
  274. name: "types with spaces are not trimmed",
  275. input: []string{" alipay ", " wxpay "},
  276. want: " alipay , wxpay ",
  277. },
  278. }
  279. for _, tc := range tests {
  280. t.Run(tc.name, func(t *testing.T) {
  281. t.Parallel()
  282. got := joinTypes(tc.input)
  283. assert.Equal(t, tc.want, got)
  284. })
  285. }
  286. }
  287. func TestCreateProviderInstanceAllowsVisibleMethodProvidersFromDifferentSources(t *testing.T) {
  288. t.Parallel()
  289. ctx := context.Background()
  290. client := newPaymentConfigServiceTestClient(t)
  291. svc := &PaymentConfigService{
  292. entClient: client,
  293. encryptionKey: []byte("0123456789abcdef0123456789abcdef"),
  294. }
  295. _, err := svc.CreateProviderInstance(ctx, CreateProviderInstanceRequest{
  296. ProviderKey: "easypay",
  297. Name: "EasyPay Alipay",
  298. Config: map[string]string{
  299. "pid": "1001",
  300. "pkey": "pkey-1001",
  301. "apiBase": "https://pay.example.com",
  302. "notifyUrl": "https://merchant.example.com/notify",
  303. "returnUrl": "https://merchant.example.com/return",
  304. },
  305. SupportedTypes: []string{"alipay"},
  306. Enabled: true,
  307. })
  308. require.NoError(t, err)
  309. _, err = svc.CreateProviderInstance(ctx, CreateProviderInstanceRequest{
  310. ProviderKey: "alipay",
  311. Name: "Official Alipay",
  312. Config: map[string]string{"appId": "app-1", "privateKey": "private-key"},
  313. SupportedTypes: []string{"alipay"},
  314. Enabled: true,
  315. })
  316. require.NoError(t, err)
  317. }
  318. func TestUpdateProviderInstanceAllowsEnablingVisibleMethodProviderFromDifferentSource(t *testing.T) {
  319. t.Parallel()
  320. ctx := context.Background()
  321. client := newPaymentConfigServiceTestClient(t)
  322. svc := &PaymentConfigService{
  323. entClient: client,
  324. encryptionKey: []byte("0123456789abcdef0123456789abcdef"),
  325. }
  326. existing, err := svc.CreateProviderInstance(ctx, CreateProviderInstanceRequest{
  327. ProviderKey: "easypay",
  328. Name: "EasyPay WeChat",
  329. Config: map[string]string{
  330. "pid": "2001",
  331. "pkey": "pkey-2001",
  332. "apiBase": "https://pay.example.com",
  333. "notifyUrl": "https://merchant.example.com/notify",
  334. "returnUrl": "https://merchant.example.com/return",
  335. },
  336. SupportedTypes: []string{"wxpay"},
  337. Enabled: true,
  338. })
  339. require.NoError(t, err)
  340. require.NotNil(t, existing)
  341. candidate, err := svc.CreateProviderInstance(ctx, CreateProviderInstanceRequest{
  342. ProviderKey: "wxpay",
  343. Name: "Official WeChat",
  344. Config: validWxpayProviderConfig(t),
  345. SupportedTypes: []string{"wxpay"},
  346. Enabled: false,
  347. })
  348. require.NoError(t, err)
  349. _, err = svc.UpdateProviderInstance(ctx, candidate.ID, UpdateProviderInstanceRequest{
  350. Enabled: boolPtrValue(true),
  351. })
  352. require.NoError(t, err)
  353. }
  354. func TestUpdateProviderInstancePersistsEnabledAndSupportedTypes(t *testing.T) {
  355. t.Parallel()
  356. ctx := context.Background()
  357. client := newPaymentConfigServiceTestClient(t)
  358. svc := &PaymentConfigService{
  359. entClient: client,
  360. encryptionKey: []byte("0123456789abcdef0123456789abcdef"),
  361. }
  362. instance, err := svc.CreateProviderInstance(ctx, CreateProviderInstanceRequest{
  363. ProviderKey: "easypay",
  364. Name: "EasyPay",
  365. Config: map[string]string{
  366. "pid": "3001",
  367. "pkey": "pkey-3001",
  368. "apiBase": "https://pay.example.com",
  369. "notifyUrl": "https://merchant.example.com/notify",
  370. "returnUrl": "https://merchant.example.com/return",
  371. },
  372. SupportedTypes: []string{"alipay"},
  373. Enabled: false,
  374. })
  375. require.NoError(t, err)
  376. _, err = svc.UpdateProviderInstance(ctx, instance.ID, UpdateProviderInstanceRequest{
  377. Enabled: boolPtrValue(true),
  378. SupportedTypes: []string{"alipay", "wxpay"},
  379. })
  380. require.NoError(t, err)
  381. saved, err := client.PaymentProviderInstance.Get(ctx, instance.ID)
  382. require.NoError(t, err)
  383. require.True(t, saved.Enabled)
  384. require.Equal(t, "alipay,wxpay", saved.SupportedTypes)
  385. }
  386. func TestUpdateProviderInstanceRejectsProtectedConfigChangesWhilePendingOrders(t *testing.T) {
  387. t.Parallel()
  388. tests := []struct {
  389. name string
  390. providerKey string
  391. createConfig func(*testing.T) map[string]string
  392. supportedType []string
  393. updateConfig map[string]string
  394. fieldName string
  395. wantValue string
  396. }{
  397. {
  398. name: "wxpay appId",
  399. providerKey: payment.TypeWxpay,
  400. createConfig: validWxpayProviderConfig,
  401. supportedType: []string{payment.TypeWxpay},
  402. updateConfig: map[string]string{"appId": "wx-app-updated"},
  403. fieldName: "appId",
  404. wantValue: "wx-app-test",
  405. },
  406. {
  407. name: "wxpay mpAppId",
  408. providerKey: payment.TypeWxpay,
  409. createConfig: validWxpayProviderConfigWithJSAPIAppID,
  410. supportedType: []string{payment.TypeWxpay},
  411. updateConfig: map[string]string{"mpAppId": "wx-mp-app-updated"},
  412. fieldName: "mpAppId",
  413. wantValue: "wx-mp-app-test",
  414. },
  415. {
  416. name: "wxpay mchId",
  417. providerKey: payment.TypeWxpay,
  418. createConfig: validWxpayProviderConfig,
  419. supportedType: []string{payment.TypeWxpay},
  420. updateConfig: map[string]string{"mchId": "mch-updated"},
  421. fieldName: "mchId",
  422. wantValue: "mch-test",
  423. },
  424. {
  425. name: "wxpay publicKeyId",
  426. providerKey: payment.TypeWxpay,
  427. createConfig: validWxpayProviderConfig,
  428. supportedType: []string{payment.TypeWxpay},
  429. updateConfig: map[string]string{"publicKeyId": "public-key-id-updated"},
  430. fieldName: "publicKeyId",
  431. wantValue: "public-key-id-test",
  432. },
  433. {
  434. name: "wxpay certSerial",
  435. providerKey: payment.TypeWxpay,
  436. createConfig: validWxpayProviderConfig,
  437. supportedType: []string{payment.TypeWxpay},
  438. updateConfig: map[string]string{"certSerial": "cert-serial-updated"},
  439. fieldName: "certSerial",
  440. wantValue: "cert-serial-test",
  441. },
  442. {
  443. name: "alipay appId",
  444. providerKey: payment.TypeAlipay,
  445. createConfig: validAlipayProviderConfig,
  446. supportedType: []string{payment.TypeAlipay},
  447. updateConfig: map[string]string{"appId": "alipay-app-updated"},
  448. fieldName: "appId",
  449. wantValue: "alipay-app-test",
  450. },
  451. {
  452. name: "easypay pid",
  453. providerKey: payment.TypeEasyPay,
  454. createConfig: validEasyPayProviderConfig,
  455. supportedType: []string{payment.TypeAlipay},
  456. updateConfig: map[string]string{"pid": "pid-updated"},
  457. fieldName: "pid",
  458. wantValue: "pid-test",
  459. },
  460. {
  461. name: "stripe currency",
  462. providerKey: payment.TypeStripe,
  463. createConfig: validStripeProviderConfig,
  464. supportedType: []string{payment.TypeStripe},
  465. updateConfig: map[string]string{"currency": "HKD"},
  466. fieldName: "currency",
  467. wantValue: "CNY",
  468. },
  469. {
  470. name: "airwallex accountId",
  471. providerKey: payment.TypeAirwallex,
  472. createConfig: validAirwallexProviderConfig,
  473. supportedType: []string{payment.TypeAirwallex},
  474. updateConfig: map[string]string{"accountId": "acct-updated"},
  475. fieldName: "accountId",
  476. wantValue: "acct-test",
  477. },
  478. {
  479. name: "airwallex currency",
  480. providerKey: payment.TypeAirwallex,
  481. createConfig: validAirwallexProviderConfig,
  482. supportedType: []string{payment.TypeAirwallex},
  483. updateConfig: map[string]string{"currency": "HKD"},
  484. fieldName: "currency",
  485. wantValue: "CNY",
  486. },
  487. {
  488. name: "airwallex webhookSecret",
  489. providerKey: payment.TypeAirwallex,
  490. createConfig: validAirwallexProviderConfig,
  491. supportedType: []string{payment.TypeAirwallex},
  492. updateConfig: map[string]string{"webhookSecret": "whsec-updated"},
  493. fieldName: "webhookSecret",
  494. wantValue: "whsec-test",
  495. },
  496. }
  497. for _, tc := range tests {
  498. tc := tc
  499. t.Run(tc.name, func(t *testing.T) {
  500. t.Parallel()
  501. ctx := context.Background()
  502. client := newPaymentConfigServiceTestClient(t)
  503. svc := &PaymentConfigService{
  504. entClient: client,
  505. encryptionKey: []byte("0123456789abcdef0123456789abcdef"),
  506. }
  507. instance, err := svc.CreateProviderInstance(ctx, CreateProviderInstanceRequest{
  508. ProviderKey: tc.providerKey,
  509. Name: "protected-config-instance",
  510. Config: tc.createConfig(t),
  511. SupportedTypes: tc.supportedType,
  512. Enabled: true,
  513. })
  514. require.NoError(t, err)
  515. createPendingProviderConfigOrder(t, ctx, client, instance)
  516. updated, err := svc.UpdateProviderInstance(ctx, instance.ID, UpdateProviderInstanceRequest{
  517. Config: tc.updateConfig,
  518. })
  519. require.Nil(t, updated)
  520. require.Error(t, err)
  521. require.Equal(t, "PENDING_ORDERS", infraerrors.Reason(err))
  522. saved, err := client.PaymentProviderInstance.Get(ctx, instance.ID)
  523. require.NoError(t, err)
  524. cfg, err := svc.decryptConfig(saved.Config)
  525. require.NoError(t, err)
  526. require.Equal(t, tc.wantValue, cfg[tc.fieldName])
  527. })
  528. }
  529. }
  530. func TestUpdateProviderInstanceAllowsSafeConfigChangesWhilePendingOrders(t *testing.T) {
  531. t.Parallel()
  532. tests := []struct {
  533. name string
  534. providerKey string
  535. createConfig func(*testing.T) map[string]string
  536. supportedType []string
  537. updateConfig map[string]string
  538. fieldName string
  539. wantValue string
  540. }{
  541. {
  542. name: "wxpay notifyUrl",
  543. providerKey: payment.TypeWxpay,
  544. createConfig: validWxpayProviderConfig,
  545. supportedType: []string{payment.TypeWxpay},
  546. updateConfig: map[string]string{"notifyUrl": "https://merchant.example.com/wxpay/notify-v2"},
  547. fieldName: "notifyUrl",
  548. wantValue: "https://merchant.example.com/wxpay/notify-v2",
  549. },
  550. {
  551. name: "alipay same appId",
  552. providerKey: payment.TypeAlipay,
  553. createConfig: validAlipayProviderConfig,
  554. supportedType: []string{payment.TypeAlipay},
  555. updateConfig: map[string]string{"appId": "alipay-app-test"},
  556. fieldName: "appId",
  557. wantValue: "alipay-app-test",
  558. },
  559. }
  560. for _, tc := range tests {
  561. tc := tc
  562. t.Run(tc.name, func(t *testing.T) {
  563. t.Parallel()
  564. ctx := context.Background()
  565. client := newPaymentConfigServiceTestClient(t)
  566. svc := &PaymentConfigService{
  567. entClient: client,
  568. encryptionKey: []byte("0123456789abcdef0123456789abcdef"),
  569. }
  570. instance, err := svc.CreateProviderInstance(ctx, CreateProviderInstanceRequest{
  571. ProviderKey: tc.providerKey,
  572. Name: "safe-config-instance",
  573. Config: tc.createConfig(t),
  574. SupportedTypes: tc.supportedType,
  575. Enabled: true,
  576. })
  577. require.NoError(t, err)
  578. createPendingProviderConfigOrder(t, ctx, client, instance)
  579. updated, err := svc.UpdateProviderInstance(ctx, instance.ID, UpdateProviderInstanceRequest{
  580. Config: tc.updateConfig,
  581. })
  582. require.NoError(t, err)
  583. require.NotNil(t, updated)
  584. saved, err := client.PaymentProviderInstance.Get(ctx, instance.ID)
  585. require.NoError(t, err)
  586. cfg, err := svc.decryptConfig(saved.Config)
  587. require.NoError(t, err)
  588. require.Equal(t, tc.wantValue, cfg[tc.fieldName])
  589. })
  590. }
  591. }
  592. func TestUpdateProviderInstanceClearsAirwallexAccountID(t *testing.T) {
  593. t.Parallel()
  594. ctx := context.Background()
  595. client := newPaymentConfigServiceTestClient(t)
  596. svc := &PaymentConfigService{
  597. entClient: client,
  598. encryptionKey: []byte("0123456789abcdef0123456789abcdef"),
  599. }
  600. instance, err := svc.CreateProviderInstance(ctx, CreateProviderInstanceRequest{
  601. ProviderKey: payment.TypeAirwallex,
  602. Name: "airwallex-clear-account",
  603. Config: validAirwallexProviderConfig(t),
  604. SupportedTypes: []string{payment.TypeAirwallex},
  605. Enabled: true,
  606. })
  607. require.NoError(t, err)
  608. updated, err := svc.UpdateProviderInstance(ctx, instance.ID, UpdateProviderInstanceRequest{
  609. Config: map[string]string{"accountId": ""},
  610. })
  611. require.NoError(t, err)
  612. require.NotNil(t, updated)
  613. saved, err := client.PaymentProviderInstance.Get(ctx, instance.ID)
  614. require.NoError(t, err)
  615. cfg, err := svc.decryptConfig(saved.Config)
  616. require.NoError(t, err)
  617. require.Empty(t, cfg["accountId"])
  618. require.Equal(t, "client-id-test", cfg["clientId"])
  619. }
  620. func createPendingProviderConfigOrder(t *testing.T, ctx context.Context, client *dbent.Client, instance *dbent.PaymentProviderInstance) {
  621. t.Helper()
  622. user, err := client.User.Create().
  623. SetEmail("provider-config-pending@example.com").
  624. SetPasswordHash("hash").
  625. SetUsername("provider-config-pending-user").
  626. Save(ctx)
  627. require.NoError(t, err)
  628. instanceID := strconv.FormatInt(instance.ID, 10)
  629. _, err = client.PaymentOrder.Create().
  630. SetUserID(user.ID).
  631. SetUserEmail(user.Email).
  632. SetUserName(user.Username).
  633. SetAmount(88).
  634. SetPayAmount(88).
  635. SetFeeRate(0).
  636. SetRechargeCode("PENDING-PROVIDER-CONFIG-" + instanceID).
  637. SetOutTradeNo("sub2_pending_provider_config_" + instanceID).
  638. SetPaymentType(providerPendingOrderPaymentType(instance.ProviderKey)).
  639. SetPaymentTradeNo("").
  640. SetOrderType(payment.OrderTypeBalance).
  641. SetStatus(OrderStatusPending).
  642. SetExpiresAt(time.Now().Add(time.Hour)).
  643. SetClientIP("127.0.0.1").
  644. SetSrcHost("api.example.com").
  645. SetProviderInstanceID(instanceID).
  646. SetProviderKey(instance.ProviderKey).
  647. Save(ctx)
  648. require.NoError(t, err)
  649. }
  650. func providerPendingOrderPaymentType(providerKey string) string {
  651. switch providerKey {
  652. case payment.TypeWxpay:
  653. return payment.TypeWxpay
  654. case payment.TypeAlipay:
  655. return payment.TypeAlipay
  656. case payment.TypeAirwallex:
  657. return payment.TypeAirwallex
  658. case payment.TypeStripe:
  659. return payment.TypeStripe
  660. default:
  661. return payment.TypeAlipay
  662. }
  663. }
  664. func validStripeProviderConfig(t *testing.T) map[string]string {
  665. t.Helper()
  666. return map[string]string{
  667. "secretKey": "sk_test_123",
  668. "publishableKey": "pk_test_123",
  669. "webhookSecret": "whsec-test",
  670. "currency": "CNY",
  671. }
  672. }
  673. func boolPtrValue(v bool) *bool {
  674. return &v
  675. }
  676. func validAlipayProviderConfig(t *testing.T) map[string]string {
  677. t.Helper()
  678. return map[string]string{
  679. "appId": "alipay-app-test",
  680. "privateKey": "alipay-private-key-test",
  681. "notifyUrl": "https://merchant.example.com/alipay/notify",
  682. "returnUrl": "https://merchant.example.com/alipay/return",
  683. }
  684. }
  685. func validEasyPayProviderConfig(t *testing.T) map[string]string {
  686. t.Helper()
  687. return map[string]string{
  688. "pid": "pid-test",
  689. "pkey": "pkey-test",
  690. "apiBase": "https://pay.example.com",
  691. "notifyUrl": "https://merchant.example.com/easypay/notify",
  692. "returnUrl": "https://merchant.example.com/easypay/return",
  693. }
  694. }
  695. func validAirwallexProviderConfig(t *testing.T) map[string]string {
  696. t.Helper()
  697. return map[string]string{
  698. "clientId": "client-id-test",
  699. "apiKey": "api-key-test",
  700. "webhookSecret": "whsec-test",
  701. "apiBase": "https://api-demo.airwallex.com/api/v1",
  702. "accountId": "acct-test",
  703. "currency": "CNY",
  704. }
  705. }
  706. func validWxpayProviderConfig(t *testing.T) map[string]string {
  707. t.Helper()
  708. key, err := rsa.GenerateKey(rand.Reader, 2048)
  709. require.NoError(t, err)
  710. privDER, err := x509.MarshalPKCS8PrivateKey(key)
  711. require.NoError(t, err)
  712. pubDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
  713. require.NoError(t, err)
  714. return map[string]string{
  715. "appId": "wx-app-test",
  716. "mchId": "mch-test",
  717. "privateKey": string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER})),
  718. "apiV3Key": "12345678901234567890123456789012",
  719. "publicKey": string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})),
  720. "publicKeyId": "public-key-id-test",
  721. "certSerial": "cert-serial-test",
  722. }
  723. }
  724. func validWxpayProviderConfigWithJSAPIAppID(t *testing.T) map[string]string {
  725. t.Helper()
  726. cfg := validWxpayProviderConfig(t)
  727. cfg["mpAppId"] = "wx-mp-app-test"
  728. return cfg
  729. }