fix(e2e): align browser harness with local stack

This commit is contained in:
jjbrunton committed 2026-08-30 21:00:12 +01:00
1 parent db5980c9bd
commit cf19d23d8a
20 files changed
+267 -85

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
import { randomUUID } from 'node:crypto';
import { Pool } from 'pg';
function localDatabaseUrl(): string {
const raw = process.env.E2E_DATABASE_URL;
if (!raw) throw new Error('Run E2E through npm run verify:e2e');
const url = new URL(raw);
if (url.protocol !== 'postgresql:' || !['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)) {
throw new Error(`E2E refuses non-loopback PostgreSQL: ${url.origin}`);
}
return raw;
}
export interface RedemptionFixture {
email: string;
code: string;
userId: string;
codeId: string;
assertRedeemed(): Promise<void>;
cleanup(): Promise<void>;
}
export async function createRedemptionFixture(): Promise<RedemptionFixture> {
const pool = new Pool({ connectionString: localDatabaseUrl() });
const nonce = randomUUID();
const userId = randomUUID();
const email = `redeem-${nonce}@example.test`;
const code = `E2E-${nonce}`.toUpperCase();
let codeId: string | undefined;
try {
const codeResult = await pool.query<{ id: string }>(
`insert into redemption_codes (code, description, max_uses, is_active, expires_at)
values ($1, 'Playwright loopback fixture', 1, true, now() + interval '1 hour')
returning id`,
[code],
);
codeId = codeResult.rows[0]?.id;
if (!codeId) throw new Error('Failed to create redemption code fixture');
await pool.query(
`insert into profiles (id, email, name)
values ($1, $2, 'Playwright Redemption Fixture')`,
[userId, email],
);
} catch (cause) {
if (codeId) await pool.query('delete from redemption_codes where id = $1', [codeId]);
await pool.end();
throw cause;
}
return {
email,
code,
userId,
codeId,
async assertRedeemed() {
const profile = await pool.query<{ is_premium: boolean }>(
'select is_premium from profiles where id = $1',
[userId],
);
if (profile.rows[0]?.is_premium !== true) {
throw new Error('Redemption fixture profile was not marked premium');
}
const redemptions = await pool.query<{ count: number }>(
`select count(*)::int as count from code_redemptions
where code_id = $1 and user_id = $2`,
[codeId, userId],
);
if (redemptions.rows[0]?.count !== 1) {
throw new Error('Redemption fixture did not record exactly one redemption');
}
},
async cleanup() {
await pool.query('delete from redemption_codes where id = $1', [codeId]);
await pool.query('delete from profiles where id = $1', [userId]);
await pool.end();
},
};
}
-36
View File
@@ -1,36 +0,0 @@
import { createClient } from '@supabase/supabase-js';
export function localSupabase() {
const url = process.env.SUPABASE_URL;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!url || !serviceKey) throw new Error('Run E2E through npm run verify:e2e');
const host = new URL(url).hostname;
if (host !== '127.0.0.1' && host !== 'localhost') {
throw new Error(`E2E refuses non-local Supabase host: ${host}`);
}
return createClient(url, serviceKey, {
auth: { autoRefreshToken: false, persistSession: false },
});
}
export async function createRedemptionFixture() {
const client = localSupabase();
const nonce = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const email = `redeem-${nonce}@example.test`;
const code = `E2E-${nonce}`.toUpperCase();
const { data: created, error: userError } = await client.auth.admin.createUser({
email,
password: 'Sauci-e2e-only-2026!',
email_confirm: true,
});
if (userError || !created.user) throw userError ?? new Error('User creation failed');
const { data: redemptionCode, error: codeError } = await client.from('redemption_codes').insert({
code,
description: 'Playwright local fixture',
max_uses: 1,
is_active: true,
expires_at: new Date(Date.now() + 3_600_000).toISOString(),
}).select().single();
if (codeError) throw codeError;
return { client, email, code, userId: created.user.id, codeId: redemptionCode.id };
}