Skip to content

Commit 0044caa

Browse files
committed
feat: add new E2E test command and update tests README
- Introduced a new command in package.json for running end-to-end tests: `test:e2e`. - Updated the tests README to provide a comprehensive overview of included tests and their execution instructions. - Consolidated authentication test details and added notes on the new E2E flow for better clarity and usability.
1 parent 4c7b614 commit 0044caa

File tree

4 files changed

+470
-83
lines changed

4 files changed

+470
-83
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"test": "vitest",
1717
"test:auth": "vitest tests/auth-integration.test.ts",
1818
"test:auth:watch": "vitest tests/auth-integration.test.ts --watch",
19+
"test:e2e": "vitest tests/e2e-auth-billing.test.ts",
1920
"db:pull": "npx drizzle-kit pull",
2021
"db:generate": "npx drizzle-kit generate",
2122
"db:migrate": "npx drizzle-kit migrate",

tests/README.md

Lines changed: 25 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,42 @@
1-
# Authentication Tests
1+
# Tests
22

3-
This directory contains end-to-end tests for the authentication system.
3+
## What's included
44

5-
## Running Tests
5+
- `auth-integration.test.ts` — basic Better‑Auth HTTP integration tests
6+
- `email-security.test.ts` — email provider security/log redaction tests
7+
- `e2e-auth-billing.test.ts`**new** end‑to‑end auth + billing flow with Polar webhook simulation
8+
- `helpers/e2e-helpers.ts`**new** shared test helper (HTTP wrapper, DB verification toggle, webhook simulators)
69

7-
### Prerequisites
10+
## Running tests
811

9-
1. **Dev server must be running**: The integration tests make real HTTP requests to `http://localhost:3000`
10-
```bash
11-
pnpm dev
12-
```
12+
Make sure your dev server is running locally:
1313

14-
2. **Database should be set up**: Make sure your PostgreSQL database is running and migrations are applied
15-
```bash
16-
docker compose up -d db
17-
pnpm db:migrate
18-
```
19-
20-
### Running the Tests
21-
22-
Run all authentication tests:
2314
```bash
24-
pnpm test:auth
25-
```
15+
pnpm dev
16+
````
2617

27-
Run tests in watch mode (useful during development):
28-
```bash
29-
pnpm test:auth:watch
30-
```
18+
Confirm the environment variables in `.env.test` or `.env` (at minimum `DATABASE_URL` and `TEST_BASE_URL`, defaults to `http://localhost:3000`).
19+
20+
Run the full test suite:
3121

32-
Run all tests in the project:
3322
```bash
3423
pnpm test
3524
```
3625

37-
## Test Coverage
38-
39-
The authentication tests cover:
40-
41-
- **Sign Up Flow**
42-
- Successful user registration
43-
- Email validation
44-
- Password strength validation
45-
- Duplicate email prevention
46-
47-
- **Email Verification**
48-
- Sending verification emails
49-
- Verifying email with valid token
50-
- Rejecting invalid tokens
51-
- Auto sign-in after verification
26+
Run only the new E2E spec:
5227

53-
- **Sign In Flow**
54-
- Sign in with verified email
55-
- Blocking unverified emails
56-
- Password validation
57-
- Session creation
58-
59-
- **Session Management**
60-
- Creating sessions on sign-in
61-
- Getting current session
62-
- Sign out functionality
63-
64-
- **Password Reset**
65-
- Sending reset emails
66-
- Reset token validation
67-
68-
- **Email Provider Testing**
69-
- Testing email sending functionality
70-
- Provider switching (console, mailhog, smtp, resend)
71-
72-
## How Tests Work
73-
74-
1. **Email Mocking**: The tests mock the `sendEmail` function to capture emails instead of actually sending them
75-
2. **Unique Test Users**: Each test uses a unique email address to avoid conflicts
76-
3. **Real API Calls**: Tests make actual HTTP requests to the auth endpoints
77-
4. **Session Testing**: Tests verify cookie-based session management
78-
79-
## Debugging Tests
80-
81-
If tests are failing:
28+
```bash
29+
pnpm run test:e2e
30+
```
8231

83-
1. **Check the dev server is running** at `http://localhost:3000`
84-
2. **Check environment variables** - ensure `.env` is properly configured
85-
3. **Check the console output** - the tests log captured emails
86-
4. **Run individual tests** by using `.only`:
87-
```typescript
88-
it.only("should verify email with valid token", async () => {
89-
// test code
90-
})
91-
```
32+
### Notes on the E2E flow
9233

93-
## Adding New Tests
34+
* **Email verification**: The E2E test marks the user as verified **directly in the database** for determinism and to avoid email parsing.
35+
* **Polar sandbox**: The test **does not** hit Polar's network. Instead, it **simulates** Polar webhook events by invoking `polarWebhookHandlers` directly, and **mocks** `~/server/polar` to avoid SDK calls (e.g., invoice generation).
36+
* **Checkout URL test (optional)**: If `PLANS.pro.polarProductId` (derived from your Polar product env vars) is set, the test attempts to start checkout and asserts that a URL is returned. If not configured, it is skipped gracefully.
9437
95-
When adding new auth features, add corresponding tests:
38+
If you prefer real end‑to‑end payment interactions with Polar sandbox:
9639
97-
1. Create a new `describe` block for the feature
98-
2. Test both success and failure cases
99-
3. Mock external dependencies (like email sending)
100-
4. Use unique test data to avoid conflicts
40+
1. Set your `POLAR_*` env vars (access token, org, product IDs).
41+
2. Disable the `vi.mock('~/server/polar', ...)` in the E2E test and point webhooks to your local app (e.g., via `ngrok` or the included Cloudflare tunnel profile).
42+
3. Ensure webhook signatures are properly configured.

tests/e2e-auth-billing.test.ts

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/**
2+
* E2E: Auth + Billing flow
3+
*
4+
* REQUIREMENTS:
5+
* - Dev server running at TEST_BASE_URL (default http://localhost:3000)
6+
* - DATABASE_URL configured and reachable
7+
* - These tests do NOT hit real Polar endpoints; we simulate webhooks and mock invoice generation
8+
*/
9+
10+
import { beforeAll, describe, expect, it, vi } from 'vitest';
11+
import {
12+
BASE_URL,
13+
apiRequest,
14+
getBillingInfo,
15+
getSession,
16+
saveBillingProfile,
17+
setEmailVerified,
18+
signInUser,
19+
signUpUser,
20+
simulateOrderPaidCredits,
21+
simulateProSubscription,
22+
startCheckout,
23+
uniqueEmail,
24+
} from './helpers/e2e-helpers';
25+
import { PLANS } from '~/config/plans';
26+
27+
// --- Mock Polar server functions BEFORE importing webhook handlers ---
28+
vi.mock('~/server/polar', () => {
29+
return {
30+
// Avoid any network to Polar during tests
31+
ensureOrderInvoice: vi.fn(async (orderId: string) => ({
32+
invoice_pdf: `https://example.com/invoices/${orderId}.pdf`,
33+
hosted_invoice_url: `https://example.com/invoices/${orderId}`,
34+
})),
35+
upsertPolarCustomerByExternalId: vi.fn(async () => {
36+
return { id: 'cust_mock' };
37+
}),
38+
// The rest of the exports aren't required by our invocation path here.
39+
};
40+
});
41+
42+
// Import after mock so our handlers use the mocked functions
43+
import { polarWebhookHandlers } from '~/server/polar-webhooks';
44+
45+
describe('E2E • Auth + Billing', () => {
46+
beforeAll(() => {
47+
// Sanity notice for the developer running tests
48+
// eslint-disable-next-line no-console
49+
console.log(`Running E2E tests against ${BASE_URL}`);
50+
});
51+
52+
it('1) signs up a user (free, unverified)', async () => {
53+
const email = uniqueEmail('free');
54+
const password = 'StrongPassw0rd!';
55+
const name = 'Free User';
56+
57+
const { response, user } = await signUpUser({ email, password, name });
58+
59+
expect(response.ok).toBe(true);
60+
expect(response.status).toBe(200);
61+
expect(user).toBeDefined();
62+
expect(user?.email).toBe(email);
63+
expect(user?.name).toBe(name);
64+
// In most configs, user starts unverified
65+
expect(user?.emailVerified ?? false).toBe(false);
66+
});
67+
68+
it('2) signs up a user and verifies email (free account), then can log in and fetch session', async () => {
69+
const email = uniqueEmail('verified');
70+
const password = 'StrongPassw0rd!';
71+
const name = 'Verified User';
72+
73+
const { response, user } = await signUpUser({ email, password, name });
74+
expect(response.ok).toBe(true);
75+
expect(user?.id).toBeDefined();
76+
77+
// Mark verified directly in DB for E2E determinism
78+
await setEmailVerified(user!.id);
79+
80+
const { response: signInRes, cookies } = await signInUser({ email, password });
81+
expect(signInRes.ok).toBe(true);
82+
83+
const sessionRes = await getSession(cookies);
84+
expect(sessionRes.ok).toBe(true);
85+
expect(sessionRes.data?.user?.email).toBe(email);
86+
expect(sessionRes.data?.user?.emailVerified).toBe(true);
87+
});
88+
89+
it('3) logs in a user (basic sign-in flow)', async () => {
90+
const email = uniqueEmail('signin');
91+
const password = 'SignInPass123!';
92+
const name = 'Sign In User';
93+
94+
const { response, user } = await signUpUser({ email, password, name });
95+
expect(response.ok).toBe(true);
96+
97+
await setEmailVerified(user!.id);
98+
99+
const { response: signInRes } = await signInUser({ email, password });
100+
expect(signInRes.ok).toBe(true);
101+
});
102+
103+
it('4) signs up a user and (via Polar webhook simulation) upgrades to Pro; billing reflects Pro + monthly credits', async () => {
104+
const email = uniqueEmail('pro');
105+
const password = 'ProUserPass123!';
106+
const name = 'Pro User';
107+
108+
const { response, user } = await signUpUser({ email, password, name });
109+
expect(response.ok).toBe(true);
110+
await setEmailVerified(user!.id);
111+
112+
const { response: signInRes, cookies } = await signInUser({ email, password });
113+
expect(signInRes.ok).toBe(true);
114+
115+
// Baseline: should be free
116+
const beforeBilling = await getBillingInfo(cookies);
117+
expect(beforeBilling.ok).toBe(true);
118+
expect(beforeBilling.data?.planId).toBe('free');
119+
120+
// Simulate Polar subscription active webhook => Pro
121+
await simulateProSubscription(polarWebhookHandlers, { userId: user!.id, email });
122+
123+
const afterBilling = await getBillingInfo(cookies);
124+
expect(afterBilling.ok).toBe(true);
125+
expect(afterBilling.data?.planId).toBe('pro');
126+
expect(afterBilling.data?.status).toBeTypeOf('string');
127+
// Monthly credits should reflect Pro plan config
128+
expect(afterBilling.data?.credits?.monthlyAllotment).toBe(PLANS.pro.monthlyCredits);
129+
130+
// Simulate an order paid that grants extra credits (e.g., 50)
131+
await simulateOrderPaidCredits(polarWebhookHandlers, { userId: user!.id, email, credits: 50 });
132+
133+
const afterCredits = await getBillingInfo(cookies);
134+
expect(afterCredits.ok).toBe(true);
135+
expect(afterCredits.data?.credits?.extraCredits).toBeGreaterThanOrEqual(50);
136+
});
137+
138+
it('5) other smoke tests: billing settings (GET, PATCH) & daily refill job', async () => {
139+
const email = uniqueEmail('billing');
140+
const password = 'BillingPass123!';
141+
const name = 'Billing User';
142+
143+
const { response, user } = await signUpUser({ email, password, name });
144+
expect(response.ok).toBe(true);
145+
await setEmailVerified(user!.id);
146+
147+
const { response: signInRes, cookies } = await signInUser({ email, password });
148+
expect(signInRes.ok).toBe(true);
149+
150+
// Load default billing profile
151+
const getProfile = await apiRequest('/api/settings/billing', {
152+
method: 'GET',
153+
headers: cookies ? { Cookie: cookies } : {},
154+
});
155+
expect(getProfile.ok).toBe(true);
156+
expect(getProfile.data?.profile?.billingEmail).toBe(email);
157+
158+
// Update billing profile
159+
const patchProfile = await saveBillingProfile(cookies, {
160+
billingEmail: email,
161+
company: 'ACME Inc.',
162+
line1: '123 Main St',
163+
city: 'Metropolis',
164+
state: 'CA',
165+
postalCode: '94000',
166+
country: 'US',
167+
vat: 'US123456789',
168+
});
169+
expect(patchProfile.ok).toBe(true);
170+
171+
// Verify update succeeds on subsequent GET
172+
const verifyProfile = await apiRequest('/api/settings/billing', {
173+
method: 'GET',
174+
headers: cookies ? { Cookie: cookies } : {},
175+
});
176+
expect(verifyProfile.ok).toBe(true);
177+
expect(verifyProfile.data?.profile?.company).toBe('ACME Inc.');
178+
179+
// Daily credit refill job endpoint (noop for free users, refills for active subs)
180+
const jobRes = await apiRequest('/api/jobs/daily-credit-refill', { method: 'POST' });
181+
expect(jobRes.ok).toBe(true);
182+
expect(jobRes.text).toContain('ok');
183+
});
184+
185+
it('Optional) Checkout start returns a URL when Polar product IDs are configured', async () => {
186+
// If product is not configured in env, skip gracefully
187+
if (!PLANS.pro.polarProductId) {
188+
// eslint-disable-next-line no-console
189+
console.log('Skipping checkout URL test: PLANS.pro.polarProductId is not set');
190+
return;
191+
}
192+
193+
const email = uniqueEmail('checkout');
194+
const password = 'CheckoutPass123!';
195+
const name = 'Checkout User';
196+
197+
const { response, user } = await signUpUser({ email, password, name });
198+
expect(response.ok).toBe(true);
199+
await setEmailVerified(user!.id);
200+
201+
const { response: signInRes, cookies } = await signInUser({ email, password });
202+
expect(signInRes.ok).toBe(true);
203+
204+
const checkoutRes = await startCheckout(cookies, PLANS.pro.polarProductId!);
205+
expect(checkoutRes.ok).toBe(true);
206+
207+
// Be tolerant to response shape: { url } OR { data: { url } } OR { data: { data: { url } } }
208+
const directUrl = checkoutRes.data?.url;
209+
const nestedUrl = checkoutRes.data?.data?.url;
210+
const deepUrl = checkoutRes.data?.data?.data?.url;
211+
const anyUrl = directUrl || nestedUrl || deepUrl;
212+
213+
expect(typeof anyUrl).toBe('string');
214+
expect(String(anyUrl)).toMatch(/^https?:\/\//);
215+
});
216+
});

0 commit comments

Comments
 (0)