191 lines
6.8 KiB
TypeScript
191 lines
6.8 KiB
TypeScript
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<typeof createClient>,
|
|
table: string,
|
|
column: string,
|
|
prefix: string,
|
|
filterCol?: string,
|
|
filterVal?: string,
|
|
): Promise<number> {
|
|
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<typeof createClient>;
|
|
|
|
// 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');
|
|
}
|
|
}
|