payment_resume_service_test.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "crypto/hmac"
  6. "crypto/sha256"
  7. "encoding/base64"
  8. "encoding/json"
  9. "net/url"
  10. "strconv"
  11. "testing"
  12. "time"
  13. "github.com/Wei-Shaw/sub2api/internal/payment"
  14. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  15. )
  16. func TestNormalizeVisibleMethods(t *testing.T) {
  17. t.Parallel()
  18. got := NormalizeVisibleMethods([]string{
  19. "alipay_direct",
  20. "alipay",
  21. " wxpay_direct ",
  22. "wxpay",
  23. "stripe",
  24. "ldc",
  25. })
  26. want := []string{"alipay", "wxpay", "stripe", "ldc"}
  27. if len(got) != len(want) {
  28. t.Fatalf("NormalizeVisibleMethods len = %d, want %d (%v)", len(got), len(want), got)
  29. }
  30. for i := range want {
  31. if got[i] != want[i] {
  32. t.Fatalf("NormalizeVisibleMethods[%d] = %q, want %q (full=%v)", i, got[i], want[i], got)
  33. }
  34. }
  35. }
  36. func TestEnabledVisibleMethodsForEasyPayIncludesCustomSupportedTypes(t *testing.T) {
  37. t.Parallel()
  38. got := enabledVisibleMethodsForProvider(payment.TypeEasyPay, "alipay,ldc,usdt_trc20")
  39. want := []string{"alipay", "ldc", "usdt_trc20"}
  40. if len(got) != len(want) {
  41. t.Fatalf("enabledVisibleMethodsForProvider len = %d, want %d (%v)", len(got), len(want), got)
  42. }
  43. for i := range want {
  44. if got[i] != want[i] {
  45. t.Fatalf("enabledVisibleMethodsForProvider[%d] = %q, want %q (full=%v)", i, got[i], want[i], got)
  46. }
  47. }
  48. }
  49. func TestNormalizePaymentSource(t *testing.T) {
  50. t.Parallel()
  51. tests := []struct {
  52. name string
  53. input string
  54. expect string
  55. }{
  56. {name: "empty uses default", input: "", expect: PaymentSourceHostedRedirect},
  57. {name: "wechat alias normalized", input: "wechat_in_app", expect: PaymentSourceWechatInAppResume},
  58. {name: "canonical value preserved", input: PaymentSourceWechatInAppResume, expect: PaymentSourceWechatInAppResume},
  59. }
  60. for _, tt := range tests {
  61. t.Run(tt.name, func(t *testing.T) {
  62. t.Parallel()
  63. if got := NormalizePaymentSource(tt.input); got != tt.expect {
  64. t.Fatalf("NormalizePaymentSource(%q) = %q, want %q", tt.input, got, tt.expect)
  65. }
  66. })
  67. }
  68. }
  69. func TestCanonicalizeReturnURL(t *testing.T) {
  70. t.Parallel()
  71. got, err := CanonicalizeReturnURL("https://example.com/payment/result?b=2#a", "example.com", "")
  72. if err != nil {
  73. t.Fatalf("CanonicalizeReturnURL returned error: %v", err)
  74. }
  75. if got != "https://example.com/payment/result?b=2" {
  76. t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://example.com/payment/result?b=2")
  77. }
  78. }
  79. func TestCanonicalizeReturnURLRejectsRelativeURL(t *testing.T) {
  80. t.Parallel()
  81. if _, err := CanonicalizeReturnURL("/payment/result", "example.com", ""); err == nil {
  82. t.Fatal("CanonicalizeReturnURL should reject relative URLs")
  83. }
  84. }
  85. func TestCanonicalizeReturnURLRejectsExternalHost(t *testing.T) {
  86. t.Parallel()
  87. if _, err := CanonicalizeReturnURL("https://evil.example/payment/result", "app.example.com", ""); err == nil {
  88. t.Fatal("CanonicalizeReturnURL should reject external hosts")
  89. }
  90. }
  91. func TestCanonicalizeReturnURLAllowsConfiguredFrontendHost(t *testing.T) {
  92. t.Parallel()
  93. got, err := CanonicalizeReturnURL(
  94. "https://app.example.com/payment/result?from=checkout",
  95. "api.example.com",
  96. "https://app.example.com/purchase",
  97. )
  98. if err != nil {
  99. t.Fatalf("CanonicalizeReturnURL returned error: %v", err)
  100. }
  101. if got != "https://app.example.com/payment/result?from=checkout" {
  102. t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://app.example.com/payment/result?from=checkout")
  103. }
  104. }
  105. func TestCanonicalizeReturnURLRejectsNonCanonicalPath(t *testing.T) {
  106. t.Parallel()
  107. if _, err := CanonicalizeReturnURL("https://app.example.com/orders/42", "app.example.com", ""); err == nil {
  108. t.Fatal("CanonicalizeReturnURL should reject non-canonical result paths")
  109. }
  110. }
  111. func TestBuildPaymentReturnURL(t *testing.T) {
  112. t.Parallel()
  113. got, err := buildPaymentReturnURL("https://example.com/payment/result?from=checkout#fragment", 42, "sub2_42", "resume-token")
  114. if err != nil {
  115. t.Fatalf("buildPaymentReturnURL returned error: %v", err)
  116. }
  117. parsed, err := url.Parse(got)
  118. if err != nil {
  119. t.Fatalf("url.Parse returned error: %v", err)
  120. }
  121. if parsed.Fragment != "" {
  122. t.Fatalf("buildPaymentReturnURL should strip fragments, got %q", parsed.Fragment)
  123. }
  124. query := parsed.Query()
  125. if query.Get("from") != "checkout" {
  126. t.Fatalf("expected original query to be preserved, got %q", query.Get("from"))
  127. }
  128. if query.Get("order_id") != strconv.FormatInt(42, 10) {
  129. t.Fatalf("order_id = %q", query.Get("order_id"))
  130. }
  131. if query.Get("out_trade_no") != "sub2_42" {
  132. t.Fatalf("out_trade_no = %q", query.Get("out_trade_no"))
  133. }
  134. if query.Get("resume_token") != "resume-token" {
  135. t.Fatalf("resume_token = %q", query.Get("resume_token"))
  136. }
  137. if query.Get("status") != "success" {
  138. t.Fatalf("status = %q", query.Get("status"))
  139. }
  140. }
  141. func TestBuildPaymentReturnURLWithoutResumeTokenStillIncludesOutTradeNo(t *testing.T) {
  142. t.Parallel()
  143. got, err := buildPaymentReturnURL("https://example.com/payment/result", 42, "sub2_42", "")
  144. if err != nil {
  145. t.Fatalf("buildPaymentReturnURL returned error: %v", err)
  146. }
  147. parsed, err := url.Parse(got)
  148. if err != nil {
  149. t.Fatalf("url.Parse returned error: %v", err)
  150. }
  151. query := parsed.Query()
  152. if query.Get("order_id") != "42" {
  153. t.Fatalf("order_id = %q", query.Get("order_id"))
  154. }
  155. if query.Get("out_trade_no") != "sub2_42" {
  156. t.Fatalf("out_trade_no = %q", query.Get("out_trade_no"))
  157. }
  158. if query.Get("resume_token") != "" {
  159. t.Fatalf("resume_token = %q, want empty", query.Get("resume_token"))
  160. }
  161. }
  162. func TestBuildPaymentReturnURLEmptyBase(t *testing.T) {
  163. t.Parallel()
  164. got, err := buildPaymentReturnURL("", 42, "sub2_42", "resume-token")
  165. if err != nil {
  166. t.Fatalf("buildPaymentReturnURL returned error: %v", err)
  167. }
  168. if got != "" {
  169. t.Fatalf("buildPaymentReturnURL = %q, want empty string", got)
  170. }
  171. }
  172. func TestPaymentResumeTokenRoundTrip(t *testing.T) {
  173. t.Parallel()
  174. svc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
  175. token, err := svc.CreateToken(ResumeTokenClaims{
  176. OrderID: 42,
  177. UserID: 7,
  178. ProviderInstanceID: "19",
  179. ProviderKey: "easypay",
  180. PaymentType: "wxpay",
  181. CanonicalReturnURL: "https://example.com/payment/result",
  182. IssuedAt: 1234567890,
  183. })
  184. if err != nil {
  185. t.Fatalf("CreateToken returned error: %v", err)
  186. }
  187. claims, err := svc.ParseToken(token)
  188. if err != nil {
  189. t.Fatalf("ParseToken returned error: %v", err)
  190. }
  191. if claims.OrderID != 42 || claims.UserID != 7 {
  192. t.Fatalf("claims mismatch: %+v", claims)
  193. }
  194. if claims.ProviderInstanceID != "19" || claims.ProviderKey != "easypay" || claims.PaymentType != "wxpay" {
  195. t.Fatalf("claims provider snapshot mismatch: %+v", claims)
  196. }
  197. if claims.CanonicalReturnURL != "https://example.com/payment/result" {
  198. t.Fatalf("claims return URL = %q", claims.CanonicalReturnURL)
  199. }
  200. }
  201. func TestCreateTokenRejectsMissingSigningKey(t *testing.T) {
  202. t.Parallel()
  203. svc := NewPaymentResumeService(nil)
  204. _, err := svc.CreateToken(ResumeTokenClaims{OrderID: 42})
  205. if err == nil {
  206. t.Fatal("CreateToken should reject missing signing key")
  207. }
  208. }
  209. func TestParseTokenRejectsFallbackSignedTokenWhenSigningKeyMissing(t *testing.T) {
  210. t.Parallel()
  211. token := mustCreateFallbackSignedToken(t, ResumeTokenClaims{OrderID: 42, UserID: 7})
  212. svc := NewPaymentResumeService(nil)
  213. _, err := svc.ParseToken(token)
  214. if err == nil {
  215. t.Fatal("ParseToken should reject tokens when signing key is missing")
  216. }
  217. }
  218. func TestParseTokenRejectsExpiredToken(t *testing.T) {
  219. t.Parallel()
  220. svc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
  221. token, err := svc.CreateToken(ResumeTokenClaims{
  222. OrderID: 42,
  223. UserID: 7,
  224. IssuedAt: time.Now().Add(-25 * time.Hour).Unix(),
  225. ExpiresAt: time.Now().Add(-1 * time.Hour).Unix(),
  226. })
  227. if err != nil {
  228. t.Fatalf("CreateToken returned error: %v", err)
  229. }
  230. _, err = svc.ParseToken(token)
  231. if err == nil {
  232. t.Fatal("ParseToken should reject expired tokens")
  233. }
  234. }
  235. func TestWeChatPaymentResumeTokenRoundTrip(t *testing.T) {
  236. t.Parallel()
  237. svc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
  238. token, err := svc.CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
  239. OpenID: "openid-123",
  240. PaymentType: payment.TypeWxpay,
  241. Amount: "12.50",
  242. OrderType: payment.OrderTypeSubscription,
  243. PlanID: 7,
  244. RedirectTo: "/purchase?from=wechat",
  245. Scope: "snsapi_base",
  246. IssuedAt: 1234567890,
  247. })
  248. if err != nil {
  249. t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
  250. }
  251. claims, err := svc.ParseWeChatPaymentResumeToken(token)
  252. if err != nil {
  253. t.Fatalf("ParseWeChatPaymentResumeToken returned error: %v", err)
  254. }
  255. if claims.OpenID != "openid-123" || claims.PaymentType != payment.TypeWxpay {
  256. t.Fatalf("claims mismatch: %+v", claims)
  257. }
  258. if claims.Amount != "12.50" || claims.OrderType != payment.OrderTypeSubscription || claims.PlanID != 7 {
  259. t.Fatalf("claims payment context mismatch: %+v", claims)
  260. }
  261. if claims.RedirectTo != "/purchase?from=wechat" || claims.Scope != "snsapi_base" {
  262. t.Fatalf("claims redirect/scope mismatch: %+v", claims)
  263. }
  264. }
  265. func TestCreateWeChatPaymentResumeTokenRejectsMissingSigningKey(t *testing.T) {
  266. t.Parallel()
  267. svc := NewPaymentResumeService(nil)
  268. _, err := svc.CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{OpenID: "openid-123"})
  269. if err == nil {
  270. t.Fatal("CreateWeChatPaymentResumeToken should reject missing signing key")
  271. }
  272. }
  273. func TestParseWeChatPaymentResumeTokenRejectsFallbackSignedTokenWhenSigningKeyMissing(t *testing.T) {
  274. t.Parallel()
  275. token := mustCreateFallbackSignedToken(t, WeChatPaymentResumeClaims{
  276. TokenType: wechatPaymentResumeTokenType,
  277. OpenID: "openid-123",
  278. PaymentType: payment.TypeWxpay,
  279. })
  280. svc := NewPaymentResumeService(nil)
  281. _, err := svc.ParseWeChatPaymentResumeToken(token)
  282. if err == nil {
  283. t.Fatal("ParseWeChatPaymentResumeToken should reject tokens when signing key is missing")
  284. }
  285. }
  286. func TestParseWeChatPaymentResumeTokenRejectsExpiredToken(t *testing.T) {
  287. t.Parallel()
  288. svc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
  289. token, err := svc.CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
  290. OpenID: "openid-123",
  291. PaymentType: payment.TypeWxpay,
  292. IssuedAt: time.Now().Add(-30 * time.Minute).Unix(),
  293. ExpiresAt: time.Now().Add(-1 * time.Minute).Unix(),
  294. })
  295. if err != nil {
  296. t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
  297. }
  298. _, err = svc.ParseWeChatPaymentResumeToken(token)
  299. if err == nil {
  300. t.Fatal("ParseWeChatPaymentResumeToken should reject expired tokens")
  301. }
  302. }
  303. func TestPaymentServiceParseWeChatPaymentResumeTokenUsesExplicitSigningKey(t *testing.T) {
  304. t.Setenv("PAYMENT_RESUME_SIGNING_KEY", "explicit-payment-resume-signing-key")
  305. token, err := NewPaymentResumeService([]byte("explicit-payment-resume-signing-key")).CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
  306. OpenID: "openid-explicit-key",
  307. PaymentType: payment.TypeWxpay,
  308. })
  309. if err != nil {
  310. t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
  311. }
  312. svc := &PaymentService{
  313. configService: &PaymentConfigService{
  314. encryptionKey: []byte("0123456789abcdef0123456789abcdef"),
  315. },
  316. }
  317. claims, err := svc.ParseWeChatPaymentResumeToken(token)
  318. if err != nil {
  319. t.Fatalf("ParseWeChatPaymentResumeToken returned error: %v", err)
  320. }
  321. if claims.OpenID != "openid-explicit-key" {
  322. t.Fatalf("openid = %q, want %q", claims.OpenID, "openid-explicit-key")
  323. }
  324. }
  325. func TestPaymentServiceParseWeChatPaymentResumeTokenAcceptsLegacyEncryptionKeyDuringMigration(t *testing.T) {
  326. t.Setenv("PAYMENT_RESUME_SIGNING_KEY", "explicit-payment-resume-signing-key")
  327. legacyKey := []byte("0123456789abcdef0123456789abcdef")
  328. token, err := NewPaymentResumeService(legacyKey).CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
  329. OpenID: "openid-legacy-key",
  330. PaymentType: payment.TypeWxpay,
  331. })
  332. if err != nil {
  333. t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
  334. }
  335. svc := &PaymentService{
  336. configService: &PaymentConfigService{
  337. encryptionKey: legacyKey,
  338. },
  339. }
  340. claims, err := svc.ParseWeChatPaymentResumeToken(token)
  341. if err != nil {
  342. t.Fatalf("ParseWeChatPaymentResumeToken returned error: %v", err)
  343. }
  344. if claims.OpenID != "openid-legacy-key" {
  345. t.Fatalf("openid = %q, want %q", claims.OpenID, "openid-legacy-key")
  346. }
  347. }
  348. func TestNewConfiguredPaymentResumeServicePrefersExplicitSigningKeyAndKeepsLegacyVerificationFallback(t *testing.T) {
  349. t.Setenv("PAYMENT_RESUME_SIGNING_KEY", "explicit-payment-resume-signing-key")
  350. legacyKey := []byte("0123456789abcdef0123456789abcdef")
  351. svc := newLegacyAwarePaymentResumeService(legacyKey)
  352. explicitToken, err := svc.CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
  353. OpenID: "openid-explicit-key",
  354. PaymentType: payment.TypeWxpay,
  355. })
  356. if err != nil {
  357. t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
  358. }
  359. explicitClaims, err := NewPaymentResumeService([]byte("explicit-payment-resume-signing-key")).ParseWeChatPaymentResumeToken(explicitToken)
  360. if err != nil {
  361. t.Fatalf("ParseWeChatPaymentResumeToken returned error: %v", err)
  362. }
  363. if explicitClaims.OpenID != "openid-explicit-key" {
  364. t.Fatalf("openid = %q, want %q", explicitClaims.OpenID, "openid-explicit-key")
  365. }
  366. legacyToken, err := NewPaymentResumeService(legacyKey).CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
  367. OpenID: "openid-legacy-key",
  368. PaymentType: payment.TypeWxpay,
  369. })
  370. if err != nil {
  371. t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
  372. }
  373. legacyClaims, err := svc.ParseWeChatPaymentResumeToken(legacyToken)
  374. if err != nil {
  375. t.Fatalf("ParseWeChatPaymentResumeToken returned error: %v", err)
  376. }
  377. if legacyClaims.OpenID != "openid-legacy-key" {
  378. t.Fatalf("openid = %q, want %q", legacyClaims.OpenID, "openid-legacy-key")
  379. }
  380. }
  381. func TestNormalizeVisibleMethodSource(t *testing.T) {
  382. t.Parallel()
  383. tests := []struct {
  384. name string
  385. method string
  386. input string
  387. want string
  388. }{
  389. {name: "alipay official alias", method: payment.TypeAlipay, input: "alipay", want: VisibleMethodSourceOfficialAlipay},
  390. {name: "alipay easypay alias", method: payment.TypeAlipay, input: "easypay", want: VisibleMethodSourceEasyPayAlipay},
  391. {name: "wxpay official alias", method: payment.TypeWxpay, input: "wxpay", want: VisibleMethodSourceOfficialWechat},
  392. {name: "wxpay easypay alias", method: payment.TypeWxpay, input: "easypay", want: VisibleMethodSourceEasyPayWechat},
  393. {name: "unsupported source", method: payment.TypeWxpay, input: "stripe", want: ""},
  394. }
  395. for _, tt := range tests {
  396. t.Run(tt.name, func(t *testing.T) {
  397. t.Parallel()
  398. if got := NormalizeVisibleMethodSource(tt.method, tt.input); got != tt.want {
  399. t.Fatalf("NormalizeVisibleMethodSource(%q, %q) = %q, want %q", tt.method, tt.input, got, tt.want)
  400. }
  401. })
  402. }
  403. }
  404. func TestVisibleMethodProviderKeyForSource(t *testing.T) {
  405. t.Parallel()
  406. tests := []struct {
  407. name string
  408. method string
  409. source string
  410. want string
  411. ok bool
  412. }{
  413. {name: "official alipay", method: payment.TypeAlipay, source: VisibleMethodSourceOfficialAlipay, want: payment.TypeAlipay, ok: true},
  414. {name: "easypay alipay", method: payment.TypeAlipay, source: VisibleMethodSourceEasyPayAlipay, want: payment.TypeEasyPay, ok: true},
  415. {name: "official wechat", method: payment.TypeWxpay, source: VisibleMethodSourceOfficialWechat, want: payment.TypeWxpay, ok: true},
  416. {name: "easypay wechat", method: payment.TypeWxpay, source: VisibleMethodSourceEasyPayWechat, want: payment.TypeEasyPay, ok: true},
  417. {name: "mismatched method and source", method: payment.TypeAlipay, source: VisibleMethodSourceOfficialWechat, want: "", ok: false},
  418. }
  419. for _, tt := range tests {
  420. t.Run(tt.name, func(t *testing.T) {
  421. t.Parallel()
  422. got, ok := VisibleMethodProviderKeyForSource(tt.method, tt.source)
  423. if got != tt.want || ok != tt.ok {
  424. t.Fatalf("VisibleMethodProviderKeyForSource(%q, %q) = (%q, %v), want (%q, %v)", tt.method, tt.source, got, ok, tt.want, tt.ok)
  425. }
  426. })
  427. }
  428. }
  429. func TestVisibleMethodLoadBalancerUsesEnabledProviderInstance(t *testing.T) {
  430. t.Parallel()
  431. ctx := context.Background()
  432. client := newPaymentConfigServiceTestClient(t)
  433. _, err := client.PaymentProviderInstance.Create().
  434. SetProviderKey(payment.TypeAlipay).
  435. SetName("Official Alipay").
  436. SetConfig("{}").
  437. SetSupportedTypes("alipay").
  438. SetEnabled(true).
  439. SetSortOrder(1).
  440. Save(ctx)
  441. if err != nil {
  442. t.Fatalf("create alipay provider: %v", err)
  443. }
  444. inner := &captureLoadBalancer{}
  445. configService := &PaymentConfigService{
  446. entClient: client,
  447. }
  448. lb := newVisibleMethodLoadBalancer(inner, configService)
  449. _, err = lb.SelectInstance(ctx, "", payment.TypeAlipay, payment.StrategyRoundRobin, 12.5)
  450. if err != nil {
  451. t.Fatalf("SelectInstance returned error: %v", err)
  452. }
  453. if inner.lastProviderKey != payment.TypeAlipay {
  454. t.Fatalf("lastProviderKey = %q, want %q", inner.lastProviderKey, payment.TypeAlipay)
  455. }
  456. }
  457. func TestVisibleMethodLoadBalancerUsesConfiguredSourceWhenMultipleProvidersEnabled(t *testing.T) {
  458. t.Parallel()
  459. tests := []struct {
  460. name string
  461. method payment.PaymentType
  462. officialName string
  463. officialTypes string
  464. easyPayName string
  465. easyPayTypes string
  466. sourceSetting string
  467. wantProvider string
  468. }{
  469. {
  470. name: "alipay uses official source",
  471. method: payment.TypeAlipay,
  472. officialName: "Official Alipay",
  473. officialTypes: "alipay",
  474. easyPayName: "EasyPay Alipay",
  475. easyPayTypes: "alipay",
  476. sourceSetting: VisibleMethodSourceOfficialAlipay,
  477. wantProvider: payment.TypeAlipay,
  478. },
  479. {
  480. name: "alipay uses easypay source",
  481. method: payment.TypeAlipay,
  482. officialName: "Official Alipay",
  483. officialTypes: "alipay",
  484. easyPayName: "EasyPay Alipay",
  485. easyPayTypes: "alipay",
  486. sourceSetting: VisibleMethodSourceEasyPayAlipay,
  487. wantProvider: payment.TypeEasyPay,
  488. },
  489. {
  490. name: "wxpay uses official source",
  491. method: payment.TypeWxpay,
  492. officialName: "Official WeChat",
  493. officialTypes: "wxpay",
  494. easyPayName: "EasyPay WeChat",
  495. easyPayTypes: "wxpay",
  496. sourceSetting: VisibleMethodSourceOfficialWechat,
  497. wantProvider: payment.TypeWxpay,
  498. },
  499. {
  500. name: "wxpay uses easypay source",
  501. method: payment.TypeWxpay,
  502. officialName: "Official WeChat",
  503. officialTypes: "wxpay",
  504. easyPayName: "EasyPay WeChat",
  505. easyPayTypes: "wxpay",
  506. sourceSetting: VisibleMethodSourceEasyPayWechat,
  507. wantProvider: payment.TypeEasyPay,
  508. },
  509. }
  510. for _, tt := range tests {
  511. t.Run(tt.name, func(t *testing.T) {
  512. t.Parallel()
  513. ctx := context.Background()
  514. client := newPaymentConfigServiceTestClient(t)
  515. officialProviderKey := payment.TypeAlipay
  516. if tt.method == payment.TypeWxpay {
  517. officialProviderKey = payment.TypeWxpay
  518. }
  519. _, err := client.PaymentProviderInstance.Create().
  520. SetProviderKey(officialProviderKey).
  521. SetName(tt.officialName).
  522. SetConfig("{}").
  523. SetSupportedTypes(tt.officialTypes).
  524. SetEnabled(true).
  525. SetSortOrder(1).
  526. Save(ctx)
  527. if err != nil {
  528. t.Fatalf("create official provider: %v", err)
  529. }
  530. _, err = client.PaymentProviderInstance.Create().
  531. SetProviderKey(payment.TypeEasyPay).
  532. SetName(tt.easyPayName).
  533. SetConfig("{}").
  534. SetSupportedTypes(tt.easyPayTypes).
  535. SetEnabled(true).
  536. SetSortOrder(2).
  537. Save(ctx)
  538. if err != nil {
  539. t.Fatalf("create easypay provider: %v", err)
  540. }
  541. inner := &captureLoadBalancer{}
  542. configService := &PaymentConfigService{
  543. entClient: client,
  544. settingRepo: &paymentConfigSettingRepoStub{
  545. values: map[string]string{
  546. visibleMethodSourceSettingKey(tt.method): tt.sourceSetting,
  547. },
  548. },
  549. }
  550. lb := newVisibleMethodLoadBalancer(inner, configService)
  551. _, err = lb.SelectInstance(ctx, "", tt.method, payment.StrategyRoundRobin, 12.5)
  552. if err != nil {
  553. t.Fatalf("SelectInstance returned error: %v", err)
  554. }
  555. if inner.lastProviderKey != tt.wantProvider {
  556. t.Fatalf("lastProviderKey = %q, want %q", inner.lastProviderKey, tt.wantProvider)
  557. }
  558. })
  559. }
  560. }
  561. func TestVisibleMethodLoadBalancerPreservesLegacyCrossProviderRoutingWhenSourceMissing(t *testing.T) {
  562. t.Parallel()
  563. ctx := context.Background()
  564. client := newPaymentConfigServiceTestClient(t)
  565. _, err := client.PaymentProviderInstance.Create().
  566. SetProviderKey(payment.TypeAlipay).
  567. SetName("Official Alipay").
  568. SetConfig("{}").
  569. SetSupportedTypes("alipay").
  570. SetEnabled(true).
  571. SetSortOrder(1).
  572. Save(ctx)
  573. if err != nil {
  574. t.Fatalf("create official provider: %v", err)
  575. }
  576. _, err = client.PaymentProviderInstance.Create().
  577. SetProviderKey(payment.TypeEasyPay).
  578. SetName("EasyPay Alipay").
  579. SetConfig("{}").
  580. SetSupportedTypes("alipay").
  581. SetEnabled(true).
  582. SetSortOrder(2).
  583. Save(ctx)
  584. if err != nil {
  585. t.Fatalf("create easypay provider: %v", err)
  586. }
  587. inner := &captureLoadBalancer{}
  588. configService := &PaymentConfigService{
  589. entClient: client,
  590. settingRepo: &paymentConfigSettingRepoStub{
  591. values: map[string]string{
  592. visibleMethodSourceSettingKey(payment.TypeAlipay): "",
  593. },
  594. },
  595. }
  596. lb := newVisibleMethodLoadBalancer(inner, configService)
  597. _, err = lb.SelectInstance(ctx, "", payment.TypeAlipay, payment.StrategyRoundRobin, 9.9)
  598. if err != nil {
  599. t.Fatalf("SelectInstance returned error: %v", err)
  600. }
  601. if inner.lastProviderKey != "" {
  602. t.Fatalf("lastProviderKey = %q, want legacy cross-provider empty key", inner.lastProviderKey)
  603. }
  604. if inner.lastPaymentType != payment.TypeAlipay {
  605. t.Fatalf("lastPaymentType = %q, want %q", inner.lastPaymentType, payment.TypeAlipay)
  606. }
  607. }
  608. func TestVisibleMethodLoadBalancerRejectsInvalidSourceWhenMultipleProvidersEnabled(t *testing.T) {
  609. t.Parallel()
  610. tests := []struct {
  611. name string
  612. method payment.PaymentType
  613. sourceValue string
  614. wantMessage string
  615. }{
  616. {
  617. name: "invalid wxpay source",
  618. method: payment.TypeWxpay,
  619. sourceValue: "stripe",
  620. wantMessage: "wxpay source must be one of the supported payment providers",
  621. },
  622. }
  623. for _, tt := range tests {
  624. t.Run(tt.name, func(t *testing.T) {
  625. t.Parallel()
  626. ctx := context.Background()
  627. client := newPaymentConfigServiceTestClient(t)
  628. officialProviderKey := payment.TypeAlipay
  629. officialSupportedTypes := "alipay"
  630. officialName := "Official Alipay"
  631. easyPaySupportedTypes := "alipay"
  632. easyPayName := "EasyPay Alipay"
  633. if tt.method == payment.TypeWxpay {
  634. officialProviderKey = payment.TypeWxpay
  635. officialSupportedTypes = "wxpay"
  636. officialName = "Official WeChat"
  637. easyPaySupportedTypes = "wxpay"
  638. easyPayName = "EasyPay WeChat"
  639. }
  640. _, err := client.PaymentProviderInstance.Create().
  641. SetProviderKey(officialProviderKey).
  642. SetName(officialName).
  643. SetConfig("{}").
  644. SetSupportedTypes(officialSupportedTypes).
  645. SetEnabled(true).
  646. SetSortOrder(1).
  647. Save(ctx)
  648. if err != nil {
  649. t.Fatalf("create official provider: %v", err)
  650. }
  651. _, err = client.PaymentProviderInstance.Create().
  652. SetProviderKey(payment.TypeEasyPay).
  653. SetName(easyPayName).
  654. SetConfig("{}").
  655. SetSupportedTypes(easyPaySupportedTypes).
  656. SetEnabled(true).
  657. SetSortOrder(2).
  658. Save(ctx)
  659. if err != nil {
  660. t.Fatalf("create easypay provider: %v", err)
  661. }
  662. inner := &captureLoadBalancer{}
  663. configService := &PaymentConfigService{
  664. entClient: client,
  665. settingRepo: &paymentConfigSettingRepoStub{
  666. values: map[string]string{
  667. visibleMethodSourceSettingKey(tt.method): tt.sourceValue,
  668. },
  669. },
  670. }
  671. lb := newVisibleMethodLoadBalancer(inner, configService)
  672. _, err = lb.SelectInstance(ctx, "", tt.method, payment.StrategyRoundRobin, 9.9)
  673. if err == nil {
  674. t.Fatal("SelectInstance should reject invalid visible method source configuration")
  675. }
  676. if infraerrors.Reason(err) != "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE" {
  677. t.Fatalf("Reason(err) = %q, want %q", infraerrors.Reason(err), "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE")
  678. }
  679. if infraerrors.Message(err) != tt.wantMessage {
  680. t.Fatalf("Message(err) = %q, want %q", infraerrors.Message(err), tt.wantMessage)
  681. }
  682. })
  683. }
  684. }
  685. func TestVisibleMethodLoadBalancerRejectsMissingEnabledVisibleMethodProvider(t *testing.T) {
  686. t.Parallel()
  687. inner := &captureLoadBalancer{}
  688. configService := &PaymentConfigService{
  689. entClient: newPaymentConfigServiceTestClient(t),
  690. }
  691. lb := newVisibleMethodLoadBalancer(inner, configService)
  692. if _, err := lb.SelectInstance(context.Background(), "", payment.TypeWxpay, payment.StrategyRoundRobin, 9.9); err == nil {
  693. t.Fatal("SelectInstance should reject when no enabled provider instance exists")
  694. }
  695. }
  696. type captureLoadBalancer struct {
  697. lastProviderKey string
  698. lastPaymentType string
  699. }
  700. func (c *captureLoadBalancer) GetInstanceConfig(context.Context, int64) (map[string]string, error) {
  701. return map[string]string{}, nil
  702. }
  703. func (c *captureLoadBalancer) SelectInstance(_ context.Context, providerKey string, paymentType payment.PaymentType, _ payment.Strategy, _ float64) (*payment.InstanceSelection, error) {
  704. c.lastProviderKey = providerKey
  705. c.lastPaymentType = paymentType
  706. return &payment.InstanceSelection{ProviderKey: providerKey, SupportedTypes: paymentType}, nil
  707. }
  708. func mustCreateFallbackSignedToken(t *testing.T, claims any) string {
  709. t.Helper()
  710. payload, err := json.Marshal(claims)
  711. if err != nil {
  712. t.Fatalf("marshal claims: %v", err)
  713. }
  714. encodedPayload := base64.RawURLEncoding.EncodeToString(payload)
  715. mac := hmac.New(sha256.New, []byte("sub2api-payment-resume"))
  716. _, _ = mac.Write([]byte(encodedPayload))
  717. signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
  718. return encodedPayload + "." + signature
  719. }