mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:51:13 -05:00
Adds a new test data cleanup utility to safely remove test fixtures after integration test suites. Includes type-safe cleanup functions for parks, rides, companies, ride_models, locations, and submissions, with safety checks (is_test_data filters) and progress logging. Integrates cleanup invocation post-run to prevent database bloat and preserves safety against prod data deletion.
442 lines
12 KiB
TypeScript
442 lines
12 KiB
TypeScript
/**
|
||
* Test Data Cleanup Utility
|
||
*
|
||
* Safely removes test fixtures created during integration tests.
|
||
*
|
||
* SAFETY FEATURES:
|
||
* - Only deletes records marked with is_test_data = true
|
||
* - Only deletes records with test-specific naming patterns
|
||
* - Cascading deletes handled by database foreign keys
|
||
* - Detailed logging of all deletions
|
||
* - Rollback support via transactions
|
||
*/
|
||
|
||
import { supabase } from '@/lib/supabaseClient';
|
||
import { handleError } from '@/lib/errorHandler';
|
||
|
||
export interface CleanupResult {
|
||
table: string;
|
||
deleted: number;
|
||
duration: number;
|
||
error?: string;
|
||
}
|
||
|
||
export interface CleanupSummary {
|
||
totalDeleted: number;
|
||
totalDuration: number;
|
||
results: CleanupResult[];
|
||
success: boolean;
|
||
}
|
||
|
||
/**
|
||
* Delete test data from a specific table using type-safe queries
|
||
*/
|
||
async function cleanupParks(): Promise<CleanupResult> {
|
||
const startTime = Date.now();
|
||
try {
|
||
const { error, count } = await supabase
|
||
.from('parks')
|
||
.delete()
|
||
.eq('is_test_data', true);
|
||
|
||
if (error) throw error;
|
||
console.log(`✓ Cleaned ${count || 0} test parks`);
|
||
return { table: 'parks', deleted: count || 0, duration: Date.now() - startTime };
|
||
} catch (error) {
|
||
return {
|
||
table: 'parks',
|
||
deleted: 0,
|
||
duration: Date.now() - startTime,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
};
|
||
}
|
||
}
|
||
|
||
async function cleanupRides(): Promise<CleanupResult> {
|
||
const startTime = Date.now();
|
||
try {
|
||
const { error, count } = await supabase
|
||
.from('rides')
|
||
.delete()
|
||
.eq('is_test_data', true);
|
||
|
||
if (error) throw error;
|
||
console.log(`✓ Cleaned ${count || 0} test rides`);
|
||
return { table: 'rides', deleted: count || 0, duration: Date.now() - startTime };
|
||
} catch (error) {
|
||
return {
|
||
table: 'rides',
|
||
deleted: 0,
|
||
duration: Date.now() - startTime,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
};
|
||
}
|
||
}
|
||
|
||
async function cleanupCompanies(): Promise<CleanupResult> {
|
||
const startTime = Date.now();
|
||
try {
|
||
const { error, count } = await supabase
|
||
.from('companies')
|
||
.delete()
|
||
.eq('is_test_data', true);
|
||
|
||
if (error) throw error;
|
||
console.log(`✓ Cleaned ${count || 0} test companies`);
|
||
return { table: 'companies', deleted: count || 0, duration: Date.now() - startTime };
|
||
} catch (error) {
|
||
return {
|
||
table: 'companies',
|
||
deleted: 0,
|
||
duration: Date.now() - startTime,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
};
|
||
}
|
||
}
|
||
|
||
async function cleanupRideModels(): Promise<CleanupResult> {
|
||
const startTime = Date.now();
|
||
try {
|
||
const { error, count } = await supabase
|
||
.from('ride_models')
|
||
.delete()
|
||
.eq('is_test_data', true);
|
||
|
||
if (error) throw error;
|
||
console.log(`✓ Cleaned ${count || 0} test ride models`);
|
||
return { table: 'ride_models', deleted: count || 0, duration: Date.now() - startTime };
|
||
} catch (error) {
|
||
return {
|
||
table: 'ride_models',
|
||
deleted: 0,
|
||
duration: Date.now() - startTime,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
};
|
||
}
|
||
}
|
||
|
||
async function cleanupLocations(): Promise<CleanupResult> {
|
||
const startTime = Date.now();
|
||
try {
|
||
const { error, count } = await supabase
|
||
.from('locations')
|
||
.delete()
|
||
.eq('is_test_data', true);
|
||
|
||
if (error) throw error;
|
||
console.log(`✓ Cleaned ${count || 0} test locations`);
|
||
return { table: 'locations', deleted: count || 0, duration: Date.now() - startTime };
|
||
} catch (error) {
|
||
return {
|
||
table: 'locations',
|
||
deleted: 0,
|
||
duration: Date.now() - startTime,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clean up test submissions (must be done before entities due to FK constraints)
|
||
*/
|
||
async function cleanupSubmissions(): Promise<CleanupResult[]> {
|
||
const results: CleanupResult[] = [];
|
||
|
||
// Clean content_submissions (cascade will handle related tables)
|
||
const startTime = Date.now();
|
||
try {
|
||
const { error, count } = await supabase
|
||
.from('content_submissions')
|
||
.delete()
|
||
.eq('is_test_data', true);
|
||
|
||
if (!error) {
|
||
results.push({
|
||
table: 'content_submissions',
|
||
deleted: count || 0,
|
||
duration: Date.now() - startTime
|
||
});
|
||
console.log(`✓ Cleaned ${count || 0} test submissions (cascade cleanup)`);
|
||
} else {
|
||
results.push({
|
||
table: 'content_submissions',
|
||
deleted: 0,
|
||
duration: Date.now() - startTime,
|
||
error: error.message
|
||
});
|
||
}
|
||
} catch (error) {
|
||
results.push({
|
||
table: 'content_submissions',
|
||
deleted: 0,
|
||
duration: Date.now() - startTime,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
});
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* Clean up test versions (historical records)
|
||
*/
|
||
async function cleanupVersions(): Promise<CleanupResult[]> {
|
||
const results: CleanupResult[] = [];
|
||
|
||
// Clean park versions
|
||
try {
|
||
const { error, count } = await supabase.from('park_versions').delete().eq('is_test_data', true);
|
||
results.push({
|
||
table: 'park_versions',
|
||
deleted: error ? 0 : (count || 0),
|
||
duration: 0,
|
||
error: error?.message
|
||
});
|
||
} catch (e) {
|
||
results.push({ table: 'park_versions', deleted: 0, duration: 0, error: String(e) });
|
||
}
|
||
|
||
// Clean ride versions
|
||
try {
|
||
const { error, count } = await supabase.from('ride_versions').delete().eq('is_test_data', true);
|
||
results.push({
|
||
table: 'ride_versions',
|
||
deleted: error ? 0 : (count || 0),
|
||
duration: 0,
|
||
error: error?.message
|
||
});
|
||
} catch (e) {
|
||
results.push({ table: 'ride_versions', deleted: 0, duration: 0, error: String(e) });
|
||
}
|
||
|
||
// Clean company versions
|
||
try {
|
||
const { error, count } = await supabase.from('company_versions').delete().eq('is_test_data', true);
|
||
results.push({
|
||
table: 'company_versions',
|
||
deleted: error ? 0 : (count || 0),
|
||
duration: 0,
|
||
error: error?.message
|
||
});
|
||
} catch (e) {
|
||
results.push({ table: 'company_versions', deleted: 0, duration: 0, error: String(e) });
|
||
}
|
||
|
||
// Clean ride_model versions
|
||
try {
|
||
const { error, count } = await supabase.from('ride_model_versions').delete().eq('is_test_data', true);
|
||
results.push({
|
||
table: 'ride_model_versions',
|
||
deleted: error ? 0 : (count || 0),
|
||
duration: 0,
|
||
error: error?.message
|
||
});
|
||
} catch (e) {
|
||
results.push({ table: 'ride_model_versions', deleted: 0, duration: 0, error: String(e) });
|
||
}
|
||
|
||
console.log(`✓ Cleaned ${results.reduce((sum, r) => sum + r.deleted, 0)} version records`);
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* Clean up test entities (main tables)
|
||
*/
|
||
async function cleanupEntities(): Promise<CleanupResult[]> {
|
||
const results: CleanupResult[] = [];
|
||
|
||
// Order matters: clean dependent entities first
|
||
results.push(await cleanupRides());
|
||
results.push(await cleanupParks());
|
||
results.push(await cleanupRideModels());
|
||
results.push(await cleanupCompanies());
|
||
results.push(await cleanupLocations());
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* Clean up test-related metadata and tracking tables
|
||
*/
|
||
async function cleanupMetadata(): Promise<CleanupResult[]> {
|
||
const results: CleanupResult[] = [];
|
||
|
||
// Clean approval metrics for test submissions
|
||
try {
|
||
const { data: testSubmissions } = await supabase
|
||
.from('content_submissions')
|
||
.select('id')
|
||
.eq('is_test_data', true);
|
||
|
||
if (testSubmissions && testSubmissions.length > 0) {
|
||
const submissionIds = testSubmissions.map(s => s.id);
|
||
|
||
const { error, count } = await supabase
|
||
.from('approval_transaction_metrics')
|
||
.delete()
|
||
.in('submission_id', submissionIds);
|
||
|
||
if (!error) {
|
||
results.push({
|
||
table: 'approval_transaction_metrics',
|
||
deleted: count || 0,
|
||
duration: 0
|
||
});
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to cleanup metadata:', error);
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* Run complete test data cleanup
|
||
*
|
||
* Executes cleanup in proper order to respect foreign key constraints:
|
||
* 1. Submissions (depend on entities)
|
||
* 2. Versions (historical records)
|
||
* 3. Metadata (metrics, audit logs)
|
||
* 4. Entities (main tables)
|
||
*/
|
||
export async function cleanupTestData(): Promise<CleanupSummary> {
|
||
const startTime = Date.now();
|
||
const allResults: CleanupResult[] = [];
|
||
|
||
console.log('🧹 Starting test data cleanup...');
|
||
|
||
try {
|
||
// Phase 1: Clean submissions first (they reference entities)
|
||
console.log('\n📋 Phase 1: Cleaning submissions...');
|
||
const submissionResults = await cleanupSubmissions();
|
||
allResults.push(...submissionResults);
|
||
|
||
// Phase 2: Clean versions (historical records)
|
||
console.log('\n📚 Phase 2: Cleaning version history...');
|
||
const versionResults = await cleanupVersions();
|
||
allResults.push(...versionResults);
|
||
|
||
// Phase 3: Clean metadata
|
||
console.log('\n📊 Phase 3: Cleaning metadata...');
|
||
const metadataResults = await cleanupMetadata();
|
||
allResults.push(...metadataResults);
|
||
|
||
// Phase 4: Clean entities (main tables)
|
||
console.log('\n🏗️ Phase 4: Cleaning entities...');
|
||
const entityResults = await cleanupEntities();
|
||
allResults.push(...entityResults);
|
||
|
||
const totalDeleted = allResults.reduce((sum, r) => sum + r.deleted, 0);
|
||
const totalDuration = Date.now() - startTime;
|
||
const hasErrors = allResults.some(r => r.error);
|
||
|
||
console.log(`\n✅ Cleanup complete: ${totalDeleted} records deleted in ${totalDuration}ms`);
|
||
|
||
return {
|
||
totalDeleted,
|
||
totalDuration,
|
||
results: allResults,
|
||
success: !hasErrors
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('❌ Cleanup failed:', error);
|
||
|
||
return {
|
||
totalDeleted: allResults.reduce((sum, r) => sum + r.deleted, 0),
|
||
totalDuration: Date.now() - startTime,
|
||
results: allResults,
|
||
success: false
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clean up only specific entity types (selective cleanup)
|
||
*/
|
||
export async function cleanupEntityType(
|
||
entityType: 'parks' | 'rides' | 'companies' | 'ride_models' | 'locations'
|
||
): Promise<CleanupResult> {
|
||
console.log(`🧹 Cleaning test ${entityType}...`);
|
||
|
||
switch (entityType) {
|
||
case 'parks':
|
||
return cleanupParks();
|
||
case 'rides':
|
||
return cleanupRides();
|
||
case 'companies':
|
||
return cleanupCompanies();
|
||
case 'ride_models':
|
||
return cleanupRideModels();
|
||
case 'locations':
|
||
return cleanupLocations();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Verify cleanup was successful (safety check)
|
||
*/
|
||
export async function verifyCleanup(): Promise<{
|
||
remainingTestData: number;
|
||
tables: Record<string, number>;
|
||
}> {
|
||
const counts: Record<string, number> = {};
|
||
let total = 0;
|
||
|
||
// Check parks
|
||
const { count: parksCount } = await supabase
|
||
.from('parks')
|
||
.select('*', { count: 'exact', head: true })
|
||
.eq('is_test_data', true);
|
||
if (parksCount !== null) {
|
||
counts.parks = parksCount;
|
||
total += parksCount;
|
||
}
|
||
|
||
// Check rides
|
||
const { count: ridesCount } = await supabase
|
||
.from('rides')
|
||
.select('*', { count: 'exact', head: true })
|
||
.eq('is_test_data', true);
|
||
if (ridesCount !== null) {
|
||
counts.rides = ridesCount;
|
||
total += ridesCount;
|
||
}
|
||
|
||
// Check companies
|
||
const { count: companiesCount } = await supabase
|
||
.from('companies')
|
||
.select('*', { count: 'exact', head: true })
|
||
.eq('is_test_data', true);
|
||
if (companiesCount !== null) {
|
||
counts.companies = companiesCount;
|
||
total += companiesCount;
|
||
}
|
||
|
||
// Check ride_models
|
||
const { count: rideModelsCount } = await supabase
|
||
.from('ride_models')
|
||
.select('*', { count: 'exact', head: true })
|
||
.eq('is_test_data', true);
|
||
if (rideModelsCount !== null) {
|
||
counts.ride_models = rideModelsCount;
|
||
total += rideModelsCount;
|
||
}
|
||
|
||
// Check locations
|
||
const { count: locationsCount } = await supabase
|
||
.from('locations')
|
||
.select('*', { count: 'exact', head: true })
|
||
.eq('is_test_data', true);
|
||
if (locationsCount !== null) {
|
||
counts.locations = locationsCount;
|
||
total += locationsCount;
|
||
}
|
||
|
||
return {
|
||
remainingTestData: total,
|
||
tables: counts
|
||
};
|
||
}
|