mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-29 03:47:05 -05:00
Compare commits
3 Commits
73e847015d
...
8259096c3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8259096c3f | ||
|
|
f51d9dcba2 | ||
|
|
ea22ab199f |
@@ -18,6 +18,7 @@ import { IntegrationTestRunner as TestRunner, allTestSuites, type TestResult, fo
|
|||||||
import { Play, Square, Download, ChevronDown, CheckCircle2, XCircle, Clock, SkipForward, Copy, ClipboardX } from 'lucide-react';
|
import { Play, Square, Download, ChevronDown, CheckCircle2, XCircle, Clock, SkipForward, Copy, ClipboardX } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { handleError } from '@/lib/errorHandler';
|
import { handleError } from '@/lib/errorHandler';
|
||||||
|
import { CleanupReport } from '@/components/ui/cleanup-report';
|
||||||
|
|
||||||
export function IntegrationTestRunner() {
|
export function IntegrationTestRunner() {
|
||||||
const superuserGuard = useSuperuserGuard();
|
const superuserGuard = useSuperuserGuard();
|
||||||
@@ -252,6 +253,11 @@ export function IntegrationTestRunner() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Cleanup Report */}
|
||||||
|
{!isRunning && summary.cleanup && (
|
||||||
|
<CleanupReport summary={summary.cleanup} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Results */}
|
{/* Results */}
|
||||||
{results.length > 0 && (
|
{results.length > 0 && (
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
221
src/components/ui/cleanup-report.tsx
Normal file
221
src/components/ui/cleanup-report.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* Cleanup Verification Report Component
|
||||||
|
*
|
||||||
|
* Displays detailed results of test data cleanup after integration tests complete.
|
||||||
|
* Shows tables cleaned, records deleted, errors, and verification status.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CheckCircle2, XCircle, AlertCircle, Database, Trash2, Clock } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import type { CleanupSummary } from '@/lib/integrationTests/testCleanup';
|
||||||
|
|
||||||
|
interface CleanupReportProps {
|
||||||
|
summary: CleanupSummary;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CleanupReport({ summary, className = '' }: CleanupReportProps) {
|
||||||
|
const successCount = summary.results.filter(r => !r.error).length;
|
||||||
|
const errorCount = summary.results.filter(r => r.error).length;
|
||||||
|
const successRate = summary.results.length > 0
|
||||||
|
? (successCount / summary.results.length) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={`border-border ${className}`}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Trash2 className="h-5 w-5 text-muted-foreground" />
|
||||||
|
Test Data Cleanup Report
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Summary Stats */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">Total Deleted</p>
|
||||||
|
<p className="text-2xl font-bold text-foreground">
|
||||||
|
{summary.totalDeleted.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">Tables Cleaned</p>
|
||||||
|
<p className="text-2xl font-bold text-foreground">
|
||||||
|
{successCount}/{summary.results.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">Duration</p>
|
||||||
|
<p className="text-2xl font-bold text-foreground flex items-center gap-1">
|
||||||
|
<Clock className="h-4 w-4" />
|
||||||
|
{(summary.totalDuration / 1000).toFixed(1)}s
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">Status</p>
|
||||||
|
<Badge
|
||||||
|
variant={summary.success ? "default" : "destructive"}
|
||||||
|
className="text-base font-semibold"
|
||||||
|
>
|
||||||
|
{summary.success ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
|
Complete
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<XCircle className="h-4 w-4" />
|
||||||
|
Failed
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Success Rate Progress */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Success Rate</span>
|
||||||
|
<span className="font-medium text-foreground">{successRate.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={successRate} className="h-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table-by-Table Results */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
Cleanup Details
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-1 max-h-64 overflow-y-auto border border-border rounded-md">
|
||||||
|
{summary.results.map((result, index) => (
|
||||||
|
<div
|
||||||
|
key={`${result.table}-${index}`}
|
||||||
|
className="flex items-center justify-between p-3 hover:bg-accent/50 transition-colors border-b border-border last:border-b-0"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
|
{result.error ? (
|
||||||
|
<XCircle className="h-4 w-4 text-destructive flex-shrink-0" />
|
||||||
|
) : result.deleted > 0 ? (
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-mono text-sm text-foreground truncate">
|
||||||
|
{result.table}
|
||||||
|
</p>
|
||||||
|
{result.error && (
|
||||||
|
<p className="text-xs text-destructive truncate">
|
||||||
|
{result.error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 flex-shrink-0">
|
||||||
|
<Badge
|
||||||
|
variant={result.deleted > 0 ? "default" : "secondary"}
|
||||||
|
className="font-mono"
|
||||||
|
>
|
||||||
|
{result.deleted} deleted
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-muted-foreground font-mono w-16 text-right">
|
||||||
|
{result.duration}ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Summary (if any) */}
|
||||||
|
{errorCount > 0 && (
|
||||||
|
<div className="p-3 bg-destructive/10 border border-destructive/20 rounded-md">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-destructive">
|
||||||
|
{errorCount} {errorCount === 1 ? 'table' : 'tables'} failed to clean
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-destructive/80 mt-1">
|
||||||
|
Check error messages above for details. Test data may remain in database.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success Message */}
|
||||||
|
{summary.success && summary.totalDeleted > 0 && (
|
||||||
|
<div className="p-3 bg-green-500/10 border border-green-500/20 rounded-md">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-green-700 dark:text-green-300">
|
||||||
|
Cleanup completed successfully
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-green-600 dark:text-green-400 mt-1">
|
||||||
|
All test data has been removed from the database.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No Data Message */}
|
||||||
|
{summary.success && summary.totalDeleted === 0 && (
|
||||||
|
<div className="p-3 bg-muted border border-border rounded-md">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-muted-foreground">
|
||||||
|
No test data found
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Database is already clean or no test data was created during this run.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compact version for inline display in test results
|
||||||
|
*/
|
||||||
|
export function CleanupReportCompact({ summary }: CleanupReportProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-accent/50 rounded-md border border-border">
|
||||||
|
<Trash2 className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
Cleanup: {summary.totalDeleted} records deleted
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{summary.results.filter(r => !r.error).length}/{summary.results.length} tables cleaned
|
||||||
|
{' • '}
|
||||||
|
{(summary.totalDuration / 1000).toFixed(1)}s
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{summary.success ? (
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-5 w-5 text-destructive flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
441
src/lib/integrationTests/testCleanup.ts
Normal file
441
src/lib/integrationTests/testCleanup.ts
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { moderationTestSuite } from './suites/moderationTests';
|
|||||||
import { moderationLockTestSuite } from './suites/moderationLockTests';
|
import { moderationLockTestSuite } from './suites/moderationLockTests';
|
||||||
import { moderationDependencyTestSuite } from './suites/moderationDependencyTests';
|
import { moderationDependencyTestSuite } from './suites/moderationDependencyTests';
|
||||||
import { approvalPipelineTestSuite } from './suites/approvalPipelineTests';
|
import { approvalPipelineTestSuite } from './suites/approvalPipelineTests';
|
||||||
|
import { cleanupTestData, type CleanupSummary } from './testCleanup';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry of all available test suites
|
* Registry of all available test suites
|
||||||
@@ -52,10 +53,17 @@ export class IntegrationTestRunner {
|
|||||||
private shouldStop = false;
|
private shouldStop = false;
|
||||||
private onProgress?: (result: TestResult) => void;
|
private onProgress?: (result: TestResult) => void;
|
||||||
private delayBetweenTests: number;
|
private delayBetweenTests: number;
|
||||||
|
private cleanupEnabled: boolean;
|
||||||
|
private cleanupSummary?: CleanupSummary;
|
||||||
|
|
||||||
constructor(onProgress?: (result: TestResult) => void, delayBetweenTests: number = 6000) {
|
constructor(
|
||||||
|
onProgress?: (result: TestResult) => void,
|
||||||
|
delayBetweenTests: number = 8000,
|
||||||
|
cleanupEnabled: boolean = true
|
||||||
|
) {
|
||||||
this.onProgress = onProgress;
|
this.onProgress = onProgress;
|
||||||
this.delayBetweenTests = delayBetweenTests; // Default 6 seconds to prevent rate limiting
|
this.delayBetweenTests = delayBetweenTests; // Default 8 seconds to prevent rate limiting
|
||||||
|
this.cleanupEnabled = cleanupEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -198,19 +206,53 @@ export class IntegrationTestRunner {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < suites.length; i++) {
|
for (let i = 0; i < suites.length; i++) {
|
||||||
|
const isHeavySuite = submissionHeavySuites.includes(suites[i].name);
|
||||||
|
|
||||||
|
// PREEMPTIVE delay BEFORE heavy suites start (prevents rate limit buildup)
|
||||||
|
if (isHeavySuite && i > 0) {
|
||||||
|
const preemptiveDelayMs = 8000; // 8s "cooldown" before heavy suite
|
||||||
|
const delaySeconds = preemptiveDelayMs / 1000;
|
||||||
|
const delayResult: TestResult = {
|
||||||
|
id: `preemptive-delay-${Date.now()}`,
|
||||||
|
name: `⏳ Pre-suite cooldown: ${delaySeconds}s (preparing for ${suites[i].name})`,
|
||||||
|
suite: 'System',
|
||||||
|
status: 'running',
|
||||||
|
duration: 0,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
details: {
|
||||||
|
reason: 'Preemptive rate limit prevention before submission-heavy suite',
|
||||||
|
nextSuite: suites[i].name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.onProgress) {
|
||||||
|
this.onProgress(delayResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.delay(preemptiveDelayMs);
|
||||||
|
|
||||||
|
if (this.onProgress) {
|
||||||
|
this.onProgress({
|
||||||
|
...delayResult,
|
||||||
|
status: 'skip',
|
||||||
|
duration: preemptiveDelayMs,
|
||||||
|
details: { reason: 'Cooldown completed' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.runSuite(suites[i]);
|
await this.runSuite(suites[i]);
|
||||||
|
|
||||||
if (this.shouldStop) {
|
if (this.shouldStop) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add delay between suites with adaptive timing
|
// REACTIVE delay AFTER suites complete
|
||||||
if (i < suites.length - 1 && this.delayBetweenTests > 0) {
|
if (i < suites.length - 1 && this.delayBetweenTests > 0) {
|
||||||
// Longer delay after submission-heavy suites
|
// Longer delay after submission-heavy suites
|
||||||
const isHeavySuite = submissionHeavySuites.includes(suites[i].name);
|
|
||||||
const delayMs = isHeavySuite
|
const delayMs = isHeavySuite
|
||||||
? this.delayBetweenTests * 2 // 12s delay after heavy suites
|
? this.delayBetweenTests * 2.25 // 18s delay after heavy suites (increased from 12s)
|
||||||
: this.delayBetweenTests; // 6s delay after others
|
: this.delayBetweenTests; // 8s delay after others (increased from 6s)
|
||||||
|
|
||||||
const delaySeconds = delayMs / 1000;
|
const delaySeconds = delayMs / 1000;
|
||||||
const delayResult: TestResult = {
|
const delayResult: TestResult = {
|
||||||
@@ -243,6 +285,59 @@ export class IntegrationTestRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run cleanup after all tests complete (if enabled)
|
||||||
|
if (this.cleanupEnabled && !this.shouldStop) {
|
||||||
|
const cleanupStartResult: TestResult = {
|
||||||
|
id: `cleanup-start-${Date.now()}`,
|
||||||
|
name: '🧹 Starting test data cleanup...',
|
||||||
|
suite: 'System',
|
||||||
|
status: 'running',
|
||||||
|
duration: 0,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
details: { reason: 'Removing test fixtures to prevent database bloat' }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.onProgress) {
|
||||||
|
this.onProgress(cleanupStartResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.cleanupSummary = await cleanupTestData();
|
||||||
|
|
||||||
|
const cleanupCompleteResult: TestResult = {
|
||||||
|
id: `cleanup-complete-${Date.now()}`,
|
||||||
|
name: `✅ Cleanup complete: ${this.cleanupSummary.totalDeleted} records deleted`,
|
||||||
|
suite: 'System',
|
||||||
|
status: this.cleanupSummary.success ? 'pass' : 'fail',
|
||||||
|
duration: this.cleanupSummary.totalDuration,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
details: {
|
||||||
|
totalDeleted: this.cleanupSummary.totalDeleted,
|
||||||
|
results: this.cleanupSummary.results,
|
||||||
|
success: this.cleanupSummary.success
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.onProgress) {
|
||||||
|
this.onProgress(cleanupCompleteResult);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const cleanupErrorResult: TestResult = {
|
||||||
|
id: `cleanup-error-${Date.now()}`,
|
||||||
|
name: '❌ Cleanup failed',
|
||||||
|
suite: 'System',
|
||||||
|
status: 'fail',
|
||||||
|
duration: 0,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.onProgress) {
|
||||||
|
this.onProgress(cleanupErrorResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
return this.results;
|
return this.results;
|
||||||
}
|
}
|
||||||
@@ -271,6 +366,7 @@ export class IntegrationTestRunner {
|
|||||||
skipped: number;
|
skipped: number;
|
||||||
running: number;
|
running: number;
|
||||||
totalDuration: number;
|
totalDuration: number;
|
||||||
|
cleanup?: CleanupSummary;
|
||||||
} {
|
} {
|
||||||
const total = this.results.length;
|
const total = this.results.length;
|
||||||
const passed = this.results.filter(r => r.status === 'pass').length;
|
const passed = this.results.filter(r => r.status === 'pass').length;
|
||||||
@@ -279,7 +375,15 @@ export class IntegrationTestRunner {
|
|||||||
const running = this.results.filter(r => r.status === 'running').length;
|
const running = this.results.filter(r => r.status === 'running').length;
|
||||||
const totalDuration = this.results.reduce((sum, r) => sum + r.duration, 0);
|
const totalDuration = this.results.reduce((sum, r) => sum + r.duration, 0);
|
||||||
|
|
||||||
return { total, passed, failed, skipped, running, totalDuration };
|
return {
|
||||||
|
total,
|
||||||
|
passed,
|
||||||
|
failed,
|
||||||
|
skipped,
|
||||||
|
running,
|
||||||
|
totalDuration,
|
||||||
|
cleanup: this.cleanupSummary
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -296,5 +400,20 @@ export class IntegrationTestRunner {
|
|||||||
this.results = [];
|
this.results = [];
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
this.shouldStop = false;
|
this.shouldStop = false;
|
||||||
|
this.cleanupSummary = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cleanup summary
|
||||||
|
*/
|
||||||
|
getCleanupSummary(): CleanupSummary | undefined {
|
||||||
|
return this.cleanupSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable automatic cleanup
|
||||||
|
*/
|
||||||
|
setCleanupEnabled(enabled: boolean): void {
|
||||||
|
this.cleanupEnabled = enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user