/** * DB helpers for Fase 7 E2E tests — direct-to-postgres helpers that bypass RLS * via the admin client. Import from `*.test.ts` only, never from `src/`. * * These helpers exist because Fase 7 exercises collective creation end-to-end: * tests need to reset the "Eva has no collective" precondition between runs * and seed invitation rows in exotic states (expired / already-accepted) that * the UI can't produce on its own. */ import { sql, ANA_ID, COLLECTIVE_ID } from '@colectivo/test-utils'; /** * Scrub all state created by Eva and all memberships she holds. * * Call in `beforeEach` of suites that assume Eva is a non-member at start. * Implementation via raw SQL (bypasses RLS + cascades) instead of admin client * to avoid a two-step delete dance. */ export async function resetEva(): Promise { const EVA_ID = '55555555-5555-5555-5555-555555555555'; // Delete collectives Eva created (CASCADE wipes members + invitations + lists). await sql('DELETE FROM public.collectives WHERE created_by = $1', [EVA_ID]); // Clear any memberships Eva holds in other collectives. await sql('DELETE FROM public.collective_members WHERE user_id = $1', [EVA_ID]); // Clear any invitations that *targeted* Eva (accepted_by). await sql( 'UPDATE public.collective_invitations SET accepted_at = NULL, accepted_by = NULL WHERE accepted_by = $1', [EVA_ID] ); } /** * Insert an invitation row with an expires_at in the past. * Returns the token (uuid). */ export async function seedExpiredInvitation( collectiveId: string = COLLECTIVE_ID, createdBy: string = ANA_ID ): Promise { const res = await sql( `INSERT INTO public.collective_invitations (collective_id, role, created_by, expires_at) VALUES ($1, 'member', $2, now() - interval '1 hour') RETURNING token`, [collectiveId, createdBy] ); return res.rows[0].token as string; } /** * Insert an invitation row already marked accepted (accepted_at + accepted_by). * Returns the token. */ export async function seedUsedInvitation( collectiveId: string = COLLECTIVE_ID, createdBy: string = ANA_ID, acceptedBy: string = ANA_ID ): Promise { const res = await sql( `INSERT INTO public.collective_invitations (collective_id, role, created_by, accepted_at, accepted_by) VALUES ($1, 'member', $2, now(), $3) RETURNING token`, [collectiveId, createdBy, acceptedBy] ); return res.rows[0].token as string; } /** * Reset the seed collective's membership roster back to the post-seed state: * Ana = admin, Borja = member, Carmen = member, David = guest * * Call in `afterAll` of suites that mutate roles / remove members. Critical — * downstream suites (lists, items, tasks, notes) assume this shape. */ export async function restoreSeedMembership(): Promise { // Use raw SQL to bypass RLS without needing the service-role JWT (which // isn't always exported to the Playwright process env). const seed = [ ['11111111-1111-1111-1111-111111111111', 'admin'], ['22222222-2222-2222-2222-222222222222', 'member'], ['33333333-3333-3333-3333-333333333333', 'member'], ['44444444-4444-4444-4444-444444444444', 'guest'] ] as const; for (const [userId, role] of seed) { await sql( `INSERT INTO public.collective_members (collective_id, user_id, role) VALUES ($1, $2, $3::public.member_role) ON CONFLICT (collective_id, user_id) DO UPDATE SET role = EXCLUDED.role`, [COLLECTIVE_ID, userId, role] ); } // Rename the collective back if a test changed it. Emoji stays whatever. await sql('UPDATE public.collectives SET name = $1 WHERE id = $2', [ 'Casa García-López', COLLECTIVE_ID ]); // Nuke any invitations that accumulated. await sql('DELETE FROM public.collective_invitations WHERE collective_id = $1', [COLLECTIVE_ID]); } /** Count rows in collective_members for a given (collective, user). 0 or 1. */ export async function countMembership(collectiveId: string, userId: string): Promise { const res = await sql( 'SELECT count(*)::int AS n FROM public.collective_members WHERE collective_id = $1 AND user_id = $2', [collectiveId, userId] ); return res.rows[0].n as number; }