test(payment): cover ErrOrderNotFound sentinel contract
Service layer (payment_fulfillment_order_not_found_test.go): - TestHandlePaymentNotification_UnknownOrder_ReturnsSentinel: in-memory sqlite ent client, query for a non-existent out_trade_no → errors.Is must recognise ErrOrderNotFound (handler relies on this to ack 200). - TestHandlePaymentNotification_NonSuccessStatus_Skips: non-success notification short-circuits before DB lookup → nil error. - TestErrOrderNotFound_DistinctFromOtherErrors: generic errors must not match the sentinel (prevents silently swallowing DB failures). Handler layer (payment_webhook_handler_test.go): - TestUnknownOrderWebhookAcksWithSuccess: locks the two ingredients the handleNotify ack path depends on — fmt.Errorf %w wrapping preserves errors.Is recognition, and writeSuccessResponse(stripe) returns an empty 200 body that Stripe treats as acknowledged.
This commit is contained in:
@@ -6,11 +6,13 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/payment"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -91,6 +93,43 @@ func TestWriteSuccessResponse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnknownOrderWebhookAcksWithSuccess exercises the response contract that
|
||||
// handleNotify relies on when HandlePaymentNotification returns ErrOrderNotFound:
|
||||
// we still need to emit the provider-specific 2xx so the provider stops
|
||||
// retrying. We can't easily drive handleNotify end-to-end without mocking the
|
||||
// concrete *service.PaymentService, so this test locks down the two ingredients
|
||||
// the fix depends on:
|
||||
// 1. errors.Is recognises the sentinel through fmt.Errorf %w wrapping (which
|
||||
// is how service layer wraps it with the out_trade_no context).
|
||||
// 2. writeSuccessResponse produces the provider-specific body for Stripe
|
||||
// (empty 200) — matching what handleNotify calls on the ack path.
|
||||
//
|
||||
// If either contract breaks, the Stripe "unknown order → 500 loop" regresses.
|
||||
func TestUnknownOrderWebhookAcksWithSuccess(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
// 1) Sentinel recognition through wrapping.
|
||||
wrapped := fmt.Errorf("%w: out_trade_no=sub2_missing_42", service.ErrOrderNotFound)
|
||||
require.True(t, errors.Is(wrapped, service.ErrOrderNotFound),
|
||||
"handleNotify uses errors.Is on the wrapped service error; regression here "+
|
||||
"would mean unknown-order webhooks go back to returning 500 and looping forever")
|
||||
|
||||
// A distinct error must NOT match — otherwise a DB failure would be silently
|
||||
// swallowed as an ack.
|
||||
other := errors.New("lookup order failed: connection refused")
|
||||
require.False(t, errors.Is(other, service.ErrOrderNotFound))
|
||||
|
||||
// 2) Provider-specific success body is what handleNotify emits on the
|
||||
// ack path. Asserted again here because this is the shape Stripe expects
|
||||
// to consider the webhook acknowledged.
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
writeSuccessResponse(c, payment.TypeStripe)
|
||||
require.Equal(t, http.StatusOK, w.Code,
|
||||
"Stripe requires 2xx to stop retrying; anything else restarts the retry loop")
|
||||
require.Empty(t, w.Body.String(), "Stripe expects an empty body on the ack path")
|
||||
}
|
||||
|
||||
func TestWebhookConstants(t *testing.T) {
|
||||
t.Run("maxWebhookBodySize is 1MB", func(t *testing.T) {
|
||||
assert.Equal(t, int64(1<<20), int64(maxWebhookBodySize))
|
||||
|
||||
Reference in New Issue
Block a user