| import { convexTest } from "convex-test"; |
| import { afterEach, describe, expect, test, vi } from "vitest"; |
|
|
| import { api } from "../_generated/api"; |
| import { checkout } from "../lib/dodo"; |
| import { |
| CHECKOUT_RATE_LIMITED, |
| CHECKOUT_RETRY_AFTER_SECONDS, |
| checkoutRateLimitedOutcomeFromError, |
| isCheckoutRateLimitedOutcome, |
| } from "../payments/checkoutRateLimit"; |
| import schema from "../schema"; |
|
|
| vi.mock("../lib/dodo", () => ({ |
| checkout: vi.fn(), |
| })); |
|
|
| const modules = import.meta.glob("../**/*.ts"); |
| const TEST_SIGNING_SECRET = "checkout-rate-limit-test-signing-secret"; |
| const TEST_RELAY_SECRET = "checkout-rate-limit-test-relay-secret"; |
| const TEST_USER = { |
| subject: "user_checkout_rate_limit", |
| tokenIdentifier: "clerk|user_checkout_rate_limit", |
| email: "rate-limit@example.com", |
| }; |
|
|
| function mockObservedProviderRateLimit() { |
| vi.mocked(checkout).mockRejectedValueOnce( |
| new Error("Failed to create checkout session: 429 status code (no body)"), |
| ); |
| } |
|
|
| afterEach(() => { |
| vi.restoreAllMocks(); |
| delete process.env.DODO_IDENTITY_SIGNING_SECRET; |
| delete process.env.RELAY_SHARED_SECRET; |
| }); |
|
|
| describe("checkout rate-limit outcome", () => { |
| test("recognizes the observed Dodo 429 error and returns a bounded retry hint", () => { |
| const result = checkoutRateLimitedOutcomeFromError( |
| new Error("Failed to create checkout session: 429 status code (no body)"), |
| ); |
|
|
| expect(result).toEqual({ |
| checkoutFailed: true, |
| code: CHECKOUT_RATE_LIMITED, |
| retryAfterSeconds: CHECKOUT_RETRY_AFTER_SECONDS, |
| }); |
| expect(isCheckoutRateLimitedOutcome(result)).toBe(true); |
| }); |
|
|
| test("does not reclassify other upstream failures as rate limiting", () => { |
| expect( |
| checkoutRateLimitedOutcomeFromError( |
| new Error("Failed to create checkout session: 503 no healthy upstream"), |
| ), |
| ).toBeNull(); |
| expect( |
| isCheckoutRateLimitedOutcome({ |
| checkoutFailed: true, |
| code: CHECKOUT_RATE_LIMITED, |
| retryAfterSeconds: 999, |
| }), |
| ).toBe(false); |
| }); |
|
|
| test("the internal relay preserves the real action outcome as HTTP 429", async () => { |
| process.env.DODO_IDENTITY_SIGNING_SECRET = TEST_SIGNING_SECRET; |
| process.env.RELAY_SHARED_SECRET = TEST_RELAY_SECRET; |
| mockObservedProviderRateLimit(); |
| const t = convexTest(schema, modules); |
|
|
| const response = await t.fetch("/relay/create-checkout", { |
| method: "POST", |
| headers: { |
| Authorization: `Bearer ${TEST_RELAY_SECRET}`, |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify({ |
| userId: TEST_USER.subject, |
| productId: "prod_rate_limited", |
| }), |
| }); |
|
|
| expect(response.status).toBe(429); |
| expect(response.headers.get("Retry-After")).toBe( |
| String(CHECKOUT_RETRY_AFTER_SECONDS), |
| ); |
| expect(await response.json()).toEqual({ |
| error: CHECKOUT_RATE_LIMITED, |
| message: "Checkout is temporarily rate limited. Retry shortly.", |
| }); |
| }); |
|
|
| test("the public action keeps provider rate limits on its error channel", async () => { |
| process.env.DODO_IDENTITY_SIGNING_SECRET = TEST_SIGNING_SECRET; |
| mockObservedProviderRateLimit(); |
| const t = convexTest(schema, modules); |
|
|
| const request = t.withIdentity(TEST_USER).action( |
| api.payments.checkout.createCheckout, |
| { |
| productId: "prod_rate_limited", |
| }, |
| ); |
| await expect(request).rejects.toBeInstanceOf(Error); |
| await request.catch((error: unknown) => { |
| const data = JSON.parse(String((error as { data?: unknown }).data)); |
| expect(data).toMatchObject({ |
| code: CHECKOUT_RATE_LIMITED, |
| retryAfterSeconds: CHECKOUT_RETRY_AFTER_SECONDS, |
| }); |
| }); |
| }); |
| }); |
|
|