payment_resume_lookup_test.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "testing"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/payment"
  8. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  9. "github.com/stretchr/testify/require"
  10. )
  11. type paymentResumeLookupProvider struct {
  12. queryCount int
  13. }
  14. func (p *paymentResumeLookupProvider) Name() string { return "resume-lookup-provider" }
  15. func (p *paymentResumeLookupProvider) ProviderKey() string { return payment.TypeAlipay }
  16. func (p *paymentResumeLookupProvider) SupportedTypes() []payment.PaymentType {
  17. return []payment.PaymentType{payment.TypeAlipay}
  18. }
  19. func (p *paymentResumeLookupProvider) CreatePayment(context.Context, payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
  20. panic("unexpected call")
  21. }
  22. func (p *paymentResumeLookupProvider) QueryOrder(context.Context, string) (*payment.QueryOrderResponse, error) {
  23. p.queryCount++
  24. return &payment.QueryOrderResponse{Status: payment.ProviderStatusPending}, nil
  25. }
  26. func (p *paymentResumeLookupProvider) VerifyNotification(context.Context, string, map[string]string) (*payment.PaymentNotification, error) {
  27. panic("unexpected call")
  28. }
  29. func (p *paymentResumeLookupProvider) Refund(context.Context, payment.RefundRequest) (*payment.RefundResponse, error) {
  30. panic("unexpected call")
  31. }
  32. func TestGetPublicOrderByResumeTokenReturnsMatchingOrder(t *testing.T) {
  33. ctx := context.Background()
  34. client := newPaymentConfigServiceTestClient(t)
  35. user, err := client.User.Create().
  36. SetEmail("resume@example.com").
  37. SetPasswordHash("hash").
  38. SetUsername("resume-user").
  39. Save(ctx)
  40. require.NoError(t, err)
  41. instanceID := "12"
  42. providerKey := payment.TypeEasyPay
  43. order, err := client.PaymentOrder.Create().
  44. SetUserID(user.ID).
  45. SetUserEmail(user.Email).
  46. SetUserName(user.Username).
  47. SetAmount(88).
  48. SetPayAmount(88).
  49. SetFeeRate(0).
  50. SetRechargeCode("RESUME-ORDER").
  51. SetOutTradeNo("sub2_resume_lookup").
  52. SetPaymentType(payment.TypeAlipay).
  53. SetPaymentTradeNo("trade-1").
  54. SetOrderType(payment.OrderTypeBalance).
  55. SetStatus(OrderStatusPending).
  56. SetExpiresAt(time.Now().Add(time.Hour)).
  57. SetClientIP("127.0.0.1").
  58. SetSrcHost("api.example.com").
  59. SetProviderInstanceID(instanceID).
  60. SetProviderKey(providerKey).
  61. Save(ctx)
  62. require.NoError(t, err)
  63. resumeSvc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
  64. token, err := resumeSvc.CreateToken(ResumeTokenClaims{
  65. OrderID: order.ID,
  66. UserID: user.ID,
  67. ProviderInstanceID: instanceID,
  68. ProviderKey: providerKey,
  69. PaymentType: payment.TypeAlipay,
  70. CanonicalReturnURL: "https://app.example.com/payment/result",
  71. })
  72. require.NoError(t, err)
  73. svc := &PaymentService{
  74. entClient: client,
  75. resumeService: resumeSvc,
  76. }
  77. got, err := svc.GetPublicOrderByResumeToken(ctx, token)
  78. require.NoError(t, err)
  79. require.Equal(t, order.ID, got.ID)
  80. }
  81. func TestGetPublicOrderByResumeTokenRejectsSnapshotMismatch(t *testing.T) {
  82. ctx := context.Background()
  83. client := newPaymentConfigServiceTestClient(t)
  84. user, err := client.User.Create().
  85. SetEmail("resume-mismatch@example.com").
  86. SetPasswordHash("hash").
  87. SetUsername("resume-mismatch-user").
  88. Save(ctx)
  89. require.NoError(t, err)
  90. order, err := client.PaymentOrder.Create().
  91. SetUserID(user.ID).
  92. SetUserEmail(user.Email).
  93. SetUserName(user.Username).
  94. SetAmount(88).
  95. SetPayAmount(88).
  96. SetFeeRate(0).
  97. SetRechargeCode("RESUME-MISMATCH").
  98. SetOutTradeNo("sub2_resume_lookup_mismatch").
  99. SetPaymentType(payment.TypeAlipay).
  100. SetPaymentTradeNo("trade-2").
  101. SetOrderType(payment.OrderTypeBalance).
  102. SetStatus(OrderStatusPending).
  103. SetExpiresAt(time.Now().Add(time.Hour)).
  104. SetClientIP("127.0.0.1").
  105. SetSrcHost("api.example.com").
  106. SetProviderInstanceID("12").
  107. SetProviderKey(payment.TypeEasyPay).
  108. Save(ctx)
  109. require.NoError(t, err)
  110. resumeSvc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
  111. token, err := resumeSvc.CreateToken(ResumeTokenClaims{
  112. OrderID: order.ID,
  113. UserID: user.ID,
  114. ProviderInstanceID: "99",
  115. ProviderKey: payment.TypeEasyPay,
  116. PaymentType: payment.TypeAlipay,
  117. CanonicalReturnURL: "https://app.example.com/payment/result",
  118. })
  119. require.NoError(t, err)
  120. svc := &PaymentService{
  121. entClient: client,
  122. resumeService: resumeSvc,
  123. }
  124. _, err = svc.GetPublicOrderByResumeToken(ctx, token)
  125. require.Error(t, err)
  126. require.Equal(t, "INVALID_RESUME_TOKEN", infraerrors.Reason(err))
  127. }
  128. func TestGetPublicOrderByResumeTokenUsesSnapshotAuthorityWhenColumnsDiffer(t *testing.T) {
  129. ctx := context.Background()
  130. client := newPaymentConfigServiceTestClient(t)
  131. user, err := client.User.Create().
  132. SetEmail("resume-snapshot-authority@example.com").
  133. SetPasswordHash("hash").
  134. SetUsername("resume-snapshot-authority-user").
  135. Save(ctx)
  136. require.NoError(t, err)
  137. order, err := client.PaymentOrder.Create().
  138. SetUserID(user.ID).
  139. SetUserEmail(user.Email).
  140. SetUserName(user.Username).
  141. SetAmount(88).
  142. SetPayAmount(88).
  143. SetFeeRate(0).
  144. SetRechargeCode("RESUME-SNAPSHOT-AUTHORITY").
  145. SetOutTradeNo("sub2_resume_snapshot_authority").
  146. SetPaymentType(payment.TypeAlipay).
  147. SetPaymentTradeNo("trade-snapshot-authority").
  148. SetOrderType(payment.OrderTypeBalance).
  149. SetStatus(OrderStatusPending).
  150. SetExpiresAt(time.Now().Add(time.Hour)).
  151. SetClientIP("127.0.0.1").
  152. SetSrcHost("api.example.com").
  153. SetProviderInstanceID("legacy-column-instance").
  154. SetProviderKey(payment.TypeAlipay).
  155. SetProviderSnapshot(map[string]any{
  156. "schema_version": 2,
  157. "provider_instance_id": "snapshot-instance",
  158. "provider_key": payment.TypeEasyPay,
  159. }).
  160. Save(ctx)
  161. require.NoError(t, err)
  162. resumeSvc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
  163. token, err := resumeSvc.CreateToken(ResumeTokenClaims{
  164. OrderID: order.ID,
  165. UserID: user.ID,
  166. ProviderInstanceID: "snapshot-instance",
  167. ProviderKey: payment.TypeEasyPay,
  168. PaymentType: payment.TypeAlipay,
  169. CanonicalReturnURL: "https://app.example.com/payment/result",
  170. })
  171. require.NoError(t, err)
  172. svc := &PaymentService{
  173. entClient: client,
  174. resumeService: resumeSvc,
  175. }
  176. got, err := svc.GetPublicOrderByResumeToken(ctx, token)
  177. require.NoError(t, err)
  178. require.Equal(t, order.ID, got.ID)
  179. }
  180. func TestGetPublicOrderByResumeTokenChecksUpstreamForPendingOrder(t *testing.T) {
  181. ctx := context.Background()
  182. client := newPaymentConfigServiceTestClient(t)
  183. user, err := client.User.Create().
  184. SetEmail("resume-refresh@example.com").
  185. SetPasswordHash("hash").
  186. SetUsername("resume-refresh-user").
  187. Save(ctx)
  188. require.NoError(t, err)
  189. order, err := client.PaymentOrder.Create().
  190. SetUserID(user.ID).
  191. SetUserEmail(user.Email).
  192. SetUserName(user.Username).
  193. SetAmount(88).
  194. SetPayAmount(88).
  195. SetFeeRate(0).
  196. SetRechargeCode("RESUME-PENDING").
  197. SetOutTradeNo("sub2_resume_lookup_pending").
  198. SetPaymentType(payment.TypeAlipay).
  199. SetPaymentTradeNo("trade-pending").
  200. SetOrderType(payment.OrderTypeBalance).
  201. SetStatus(OrderStatusPending).
  202. SetExpiresAt(time.Now().Add(time.Hour)).
  203. SetClientIP("127.0.0.1").
  204. SetSrcHost("api.example.com").
  205. Save(ctx)
  206. require.NoError(t, err)
  207. resumeSvc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
  208. token, err := resumeSvc.CreateToken(ResumeTokenClaims{
  209. OrderID: order.ID,
  210. UserID: user.ID,
  211. PaymentType: payment.TypeAlipay,
  212. CanonicalReturnURL: "https://app.example.com/payment/result",
  213. })
  214. require.NoError(t, err)
  215. registry := payment.NewRegistry()
  216. provider := &paymentResumeLookupProvider{}
  217. registry.Register(provider)
  218. svc := &PaymentService{
  219. entClient: client,
  220. registry: registry,
  221. resumeService: resumeSvc,
  222. providersLoaded: true,
  223. }
  224. got, err := svc.GetPublicOrderByResumeToken(ctx, token)
  225. require.NoError(t, err)
  226. require.Equal(t, order.ID, got.ID)
  227. require.Equal(t, 1, provider.queryCount)
  228. }
  229. func TestVerifyOrderPublicDoesNotCheckUpstreamForPendingOrder(t *testing.T) {
  230. ctx := context.Background()
  231. client := newPaymentConfigServiceTestClient(t)
  232. user, err := client.User.Create().
  233. SetEmail("public-verify@example.com").
  234. SetPasswordHash("hash").
  235. SetUsername("public-verify-user").
  236. Save(ctx)
  237. require.NoError(t, err)
  238. order, err := client.PaymentOrder.Create().
  239. SetUserID(user.ID).
  240. SetUserEmail(user.Email).
  241. SetUserName(user.Username).
  242. SetAmount(88).
  243. SetPayAmount(88).
  244. SetFeeRate(0).
  245. SetRechargeCode("PUBLIC-VERIFY").
  246. SetOutTradeNo("sub2_public_verify_pending").
  247. SetPaymentType(payment.TypeAlipay).
  248. SetPaymentTradeNo("trade-public-verify").
  249. SetOrderType(payment.OrderTypeBalance).
  250. SetStatus(OrderStatusPending).
  251. SetExpiresAt(time.Now().Add(time.Hour)).
  252. SetClientIP("127.0.0.1").
  253. SetSrcHost("api.example.com").
  254. Save(ctx)
  255. require.NoError(t, err)
  256. registry := payment.NewRegistry()
  257. provider := &paymentResumeLookupProvider{}
  258. registry.Register(provider)
  259. svc := &PaymentService{
  260. entClient: client,
  261. registry: registry,
  262. providersLoaded: true,
  263. }
  264. got, err := svc.VerifyOrderPublic(ctx, order.OutTradeNo)
  265. require.NoError(t, err)
  266. require.Equal(t, order.ID, got.ID)
  267. require.Equal(t, 0, provider.queryCount)
  268. }
  269. func TestVerifyOrderPublicRejectsBlankOutTradeNo(t *testing.T) {
  270. svc := &PaymentService{
  271. entClient: newPaymentConfigServiceTestClient(t),
  272. }
  273. _, err := svc.VerifyOrderPublic(context.Background(), " ")
  274. require.Error(t, err)
  275. require.Equal(t, "INVALID_OUT_TRADE_NO", infraerrors.Reason(err))
  276. }