Files
textbee/web/e2e/mock-api.ts
isra el 0bd5d4c900 fix(billing): send the plan name from route params under Next 16
/checkout/pro and /checkout/scale failed with "Plan cannot be purchased"
for every user since the Next 16 upgrade (ea1b00c).

The checkout page still typed `params` as a plain object and read
`params.planName` synchronously. Next 16 removed the sync compat shim, so
the value was undefined, axios dropped the key, and the API ran
findOne({ name: undefined }). Mongoose 9 returns null for that where
older versions stripped the key, so the guard threw a message about a
misconfigured plan for a request that never named one. Plan data and
Polar product ids were correct throughout.

- unwrap params with React's use() and drop the `as string` cast that
  suppressed the type error
- split the conflated guard into PLAN_NAME_REQUIRED, PLAN_NOT_FOUND and
  the existing unpurchasable case, each logged server side
- stop retrying a rejected checkout twice: a 400 is not transient
- mark the Polar product id indexes sparse, they have been failing to
  build with E11000 on every boot because most plans leave them unset

Checkout now defaults to yearly. A URL that names an interval still
redirects straight through, so the marketing funnel is unchanged; the
in-app CTAs that name none get an interval chooser with yearly
preselected instead of a silent monthly default.

Also redesigns the checkout states, which were the only place in the
dashboard hardcoding gray-100/white and so rendering a light slab in dark
mode, and the plan picker, which painted the accent on every tier.

Tests: a checkout e2e spec asserting the request body carries planName,
which is what would have caught this, plus API cases pinning the three
guard failures apart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:47:17 +03:00

61 lines
2.3 KiB
TypeScript

import type { Page, Route } from '@playwright/test'
import {
mockApiKeys,
mockBillingPlans,
mockDevices,
mockMessages,
mockStats,
mockSubscription,
mockUser,
mockWebhookNotifications,
mockWebhooks,
} from '../test/fixtures'
const json = (route: Route, body: unknown, status = 200) =>
route.fulfill({
status,
contentType: 'application/json',
body: JSON.stringify(body),
})
// Intercept EVERY call to the backend (`/api/v1/**`) and serve fixtures. Because
// the app is pointed at a backend host that nothing listens on, this guarantees
// no request ever reaches a real backend during e2e.
export type MockApiOverrides = {
/** Serve a different subscription payload, e.g. the free-user shape. */
subscription?: unknown
/** Fail POST /billing/checkout with this message, to exercise the error state. */
checkoutError?: string
}
export async function mockApi(page: Page, overrides: MockApiOverrides = {}) {
await page.route('**/api/v1/**', (route) => {
const path = new URL(route.request().url()).pathname.replace('/api/v1', '')
// Kept in-origin so following the redirect does not leave the test app.
if (path === '/billing/checkout') {
if (overrides.checkoutError) {
return json(route, { message: overrides.checkoutError }, 400)
}
return json(route, { redirectUrl: '/dashboard?polar-checkout-mock=1' })
}
if (path === '/auth/who-am-i') return json(route, { data: mockUser })
if (path === '/billing/current-subscription')
return json(route, overrides.subscription ?? mockSubscription)
if (path === '/billing/plans') return json(route, { data: mockBillingPlans })
if (path === '/gateway/devices') return json(route, { data: mockDevices })
if (path === '/gateway/stats') return json(route, { data: mockStats })
if (path === '/webhooks') return json(route, { data: mockWebhooks })
if (path === '/webhooks/notifications')
return json(route, mockWebhookNotifications)
if (path === '/auth/api-keys') return json(route, { data: mockApiKeys })
if (/\/gateway\/devices\/[^/]+\/(messages|get-received-sms)/.test(path))
return json(route, mockMessages)
// Any unmapped backend call still gets a benign mocked response so the test
// cannot fall through to a real backend.
return json(route, { data: [] })
})
}