import { createClient } from '@supabase/supabase-js'; import * as fs from 'fs'; import * as path from 'path'; /** * Global teardown: delete all test-created data from Supabase. * Runs after all Playwright tests complete. * * Uses SUPABASE_SERVICE_ROLE_KEY to bypass RLS for reliable cleanup. * Falls back to anon key + password auth if service role key is unavailable. * * Cleanup targets (by name prefix): * Documents: "Test Folder", "Test Doc", "Test Board", "Nav Folder", "Rename Me", "Renamed" * Boards: "PW Board", "PW Card Board", "PW Detail Board", "Board A", "Board B", "Perf Test" * Events: "PW Event", "PW Detail", "PW Delete", "PW Test Event", "PW Finance" * Invites: "playwright-test-*@example.com" * Roles: "Tester" * Tags: "PW Tag" * Sponsors: "PW Sponsor" * Contacts: "PW Contact", "PW Vendor" * Org events: "PW Test Event", "PW Finance" (cascades to depts, modules, budget, etc.) */ // Load .env manually since we're outside Vite function loadEnv() { try { const envPath = path.resolve(process.cwd(), '.env'); const content = fs.readFileSync(envPath, 'utf-8'); for (const line of content.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eqIdx = trimmed.indexOf('='); if (eqIdx === -1) continue; const key = trimmed.slice(0, eqIdx); const value = trimmed.slice(eqIdx + 1); if (!process.env[key]) process.env[key] = value; } } catch { /* .env not found - rely on process.env */ } } loadEnv(); const SUPABASE_URL = process.env.PUBLIC_SUPABASE_URL || ''; const SUPABASE_ANON_KEY = process.env.PUBLIC_SUPABASE_ANON_KEY || ''; const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || ''; // Name prefixes used by tests when creating data const DOC_PREFIXES = ['Test Folder', 'Test Doc', 'Test Board', 'Nav Folder', 'Rename Me', 'Renamed']; const BOARD_PREFIXES = ['PW Board', 'PW Card Board', 'PW Detail Board', 'Board A', 'Board B', 'Perf Test']; const ROLE_PREFIX = 'Tester'; const TAG_PREFIX = 'PW Tag'; const CALENDAR_EVENT_PREFIXES = ['PW Event', 'PW Detail', 'PW Delete']; const ORG_EVENT_PREFIXES = ['PW Test Event', 'PW Finance']; const SPONSOR_PREFIXES = ['PW Sponsor']; const CONTACT_PREFIXES = ['PW Contact', 'PW Vendor']; const INVITE_EMAIL_PATTERN = 'playwright-test-%@example.com'; /** Helper: delete rows by prefix from a table column */ async function deleteByPrefix( supabase: ReturnType, table: string, column: string, prefix: string, filterCol?: string, filterVal?: string, ): Promise { let query = (supabase as any).from(table).select('id').ilike(column, `${prefix}%`); if (filterCol && filterVal) query = query.eq(filterCol, filterVal); const { data } = await query; if (!data || data.length === 0) return 0; const ids = data.map((r: any) => r.id); const { error } = await (supabase as any).from(table).delete().in('id', ids); if (error) { console.log(`[cleanup] Failed to delete from ${table} (prefix "${prefix}"): ${error.message}`); return 0; } return data.length; } export default async function globalTeardown() { if (!SUPABASE_ANON_KEY && !SUPABASE_SERVICE_ROLE_KEY) { console.log('[cleanup] No Supabase keys found - skipping cleanup'); return; } let supabase: ReturnType; // Prefer service role key (bypasses RLS) for reliable cleanup if (SUPABASE_SERVICE_ROLE_KEY) { console.log('[cleanup] Using service role key (RLS bypassed)'); supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { auth: { autoRefreshToken: false, persistSession: false }, }); } else { console.log('[cleanup] No service role key, falling back to anon key + auth'); supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); const { error: authError } = await supabase.auth.signInWithPassword({ email: 'tipilan@ituk.ee', password: 'gu&u6QTMbJK7nT', }); if (authError) { console.log('[cleanup] Auth failed - skipping cleanup:', authError.message); return; } } // Get the org ID for root-test const { data: org } = await supabase .from('organizations') .select('id') .eq('slug', 'root-test') .single(); if (!org) { console.log('[cleanup] root-test org not found - skipping cleanup'); return; } const orgId = (org as any).id; let totalDeleted = 0; // 1. Delete test documents (folders, docs, kanbans) — cascades to content for (const prefix of DOC_PREFIXES) { totalDeleted += await deleteByPrefix(supabase, 'documents', 'name', prefix, 'org_id', orgId); } // 2. Delete test kanban boards — cascades to columns → cards → assignees for (const prefix of BOARD_PREFIXES) { totalDeleted += await deleteByPrefix(supabase, 'kanban_boards', 'name', prefix, 'org_id', orgId); } // 3. Delete test invites const { data: invites } = await (supabase as any) .from('org_invites') .select('id') .eq('org_id', orgId) .ilike('email', INVITE_EMAIL_PATTERN); if (invites && invites.length > 0) { const { error } = await (supabase as any).from('org_invites').delete().in('id', invites.map((i: any) => i.id)); if (!error) totalDeleted += invites.length; else console.log('[cleanup] Failed to delete invites:', error.message); } // 4. Delete test roles totalDeleted += await deleteByPrefix(supabase, 'org_roles', 'name', ROLE_PREFIX, 'org_id', orgId); // 5. Delete test tags totalDeleted += await deleteByPrefix(supabase, 'tags', 'name', TAG_PREFIX, 'org_id', orgId); // 6. Delete test calendar events (simple calendar events, not org events) for (const prefix of CALENDAR_EVENT_PREFIXES) { totalDeleted += await deleteByPrefix(supabase, 'calendar_events', 'title', prefix, 'org_id', orgId); } // 7. Delete test org events — cascades to departments → modules → budget items, checklists, notes, etc. for (const prefix of ORG_EVENT_PREFIXES) { totalDeleted += await deleteByPrefix(supabase, 'events', 'name', prefix, 'org_id', orgId); } // 8. Delete test sponsors for (const prefix of SPONSOR_PREFIXES) { totalDeleted += await deleteByPrefix(supabase, 'sponsors', 'name', prefix); } // 9. Delete test org contacts for (const prefix of CONTACT_PREFIXES) { totalDeleted += await deleteByPrefix(supabase, 'org_contacts', 'name', prefix, 'org_id', orgId); } // 10. Delete test activity log entries (from test actions) const { data: activityLogs } = await (supabase as any) .from('activity_log') .select('id') .eq('org_id', orgId) .gte('created_at', new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString()); if (activityLogs && activityLogs.length > 0) { const { error } = await (supabase as any) .from('activity_log') .delete() .in('id', activityLogs.map((l: any) => l.id)); if (!error) totalDeleted += activityLogs.length; } if (totalDeleted > 0) { console.log(`[cleanup] Deleted ${totalDeleted} test-created items from root-test org`); } else { console.log('[cleanup] No test data to clean up'); } }