payment_fulfillment_order_not_found_test.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "database/sql"
  6. "errors"
  7. "testing"
  8. "entgo.io/ent/dialect"
  9. entsql "entgo.io/ent/dialect/sql"
  10. _ "modernc.org/sqlite"
  11. dbent "github.com/Wei-Shaw/sub2api/ent"
  12. "github.com/Wei-Shaw/sub2api/ent/enttest"
  13. "github.com/Wei-Shaw/sub2api/internal/payment"
  14. "github.com/stretchr/testify/require"
  15. )
  16. // newOrderNotFoundTestClient wires an in-memory sqlite-backed ent.Client so
  17. // tests can exercise HandlePaymentNotification's real DB lookup path without
  18. // standing up a service stack.
  19. func newOrderNotFoundTestClient(t *testing.T) *dbent.Client {
  20. t.Helper()
  21. db, err := sql.Open("sqlite", "file:payment_order_not_found?mode=memory&cache=shared&_fk=1")
  22. require.NoError(t, err)
  23. t.Cleanup(func() { _ = db.Close() })
  24. _, err = db.Exec("PRAGMA foreign_keys = ON")
  25. require.NoError(t, err)
  26. drv := entsql.OpenDB(dialect.SQLite, db)
  27. client := enttest.NewClient(t, enttest.WithOptions(dbent.Driver(drv)))
  28. t.Cleanup(func() { _ = client.Close() })
  29. return client
  30. }
  31. // TestHandlePaymentNotification_UnknownOrder_ReturnsSentinel exercises the
  32. // happy-path of the webhook 404 fix: when the notification references an
  33. // out_trade_no that does not exist in our DB, HandlePaymentNotification must
  34. // return an error that errors.Is(err, ErrOrderNotFound) recognizes. The
  35. // webhook handler relies on that contract to ack with a 2xx so the provider
  36. // stops retrying.
  37. func TestHandlePaymentNotification_UnknownOrder_ReturnsSentinel(t *testing.T) {
  38. ctx := context.Background()
  39. client := newOrderNotFoundTestClient(t)
  40. svc := &PaymentService{
  41. entClient: client,
  42. providersLoaded: true,
  43. }
  44. notification := &payment.PaymentNotification{
  45. OrderID: "sub2_does_not_exist_12345",
  46. TradeNo: "stripe_evt_test_xyz",
  47. Status: payment.NotificationStatusSuccess,
  48. Amount: 1000,
  49. }
  50. err := svc.HandlePaymentNotification(ctx, notification, payment.TypeStripe)
  51. require.Error(t, err, "unknown out_trade_no should surface an error")
  52. require.ErrorIs(t, err, ErrOrderNotFound,
  53. "webhook handler relies on errors.Is(err, ErrOrderNotFound) to downgrade to 200")
  54. // Sanity: the wrapped error message should still include the out_trade_no
  55. // for operator diagnostics.
  56. require.Contains(t, err.Error(), notification.OrderID)
  57. }
  58. // TestHandlePaymentNotification_NonSuccessStatus_Skips documents the
  59. // short-circuit that precedes the DB lookup: when the notification is not a
  60. // success event (e.g. Stripe non-payment events that reach us via the webhook
  61. // route), we return nil without touching the DB and the handler responds 200.
  62. func TestHandlePaymentNotification_NonSuccessStatus_Skips(t *testing.T) {
  63. ctx := context.Background()
  64. client := newOrderNotFoundTestClient(t)
  65. svc := &PaymentService{
  66. entClient: client,
  67. providersLoaded: true,
  68. }
  69. notification := &payment.PaymentNotification{
  70. OrderID: "sub2_does_not_exist_12345",
  71. Status: "failed", // any value other than NotificationStatusSuccess
  72. }
  73. err := svc.HandlePaymentNotification(ctx, notification, payment.TypeStripe)
  74. require.NoError(t, err,
  75. "non-success notifications must short-circuit before the DB lookup")
  76. }
  77. // TestErrOrderNotFound_DistinctFromOtherErrors guards against an accidental
  78. // collapse where a generic wrapped error would start matching ErrOrderNotFound
  79. // (which would silently mask real DB failures).
  80. func TestErrOrderNotFound_DistinctFromOtherErrors(t *testing.T) {
  81. genericErr := errors.New("some other failure")
  82. require.False(t, errors.Is(genericErr, ErrOrderNotFound))
  83. require.False(t, errors.Is(ErrOrderNotFound, genericErr))
  84. wrappedLookupErr := errors.New("lookup order failed for out_trade_no sub2_42: connection refused")
  85. require.False(t, errors.Is(wrappedLookupErr, ErrOrderNotFound),
  86. "DB connection failures must not masquerade as order-not-found")
  87. }