mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 14:31:12 -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.
420 lines
11 KiB
TypeScript
420 lines
11 KiB
TypeScript
/**
|
|
* Integration Test Runner
|
|
*
|
|
* Core infrastructure for running comprehensive integration tests.
|
|
* Tests run against real database functions, edge functions, and API endpoints.
|
|
*/
|
|
|
|
import { moderationTestSuite } from './suites/moderationTests';
|
|
import { moderationLockTestSuite } from './suites/moderationLockTests';
|
|
import { moderationDependencyTestSuite } from './suites/moderationDependencyTests';
|
|
import { approvalPipelineTestSuite } from './suites/approvalPipelineTests';
|
|
import { cleanupTestData, type CleanupSummary } from './testCleanup';
|
|
|
|
/**
|
|
* Registry of all available test suites
|
|
*/
|
|
export const ALL_TEST_SUITES = [
|
|
moderationTestSuite,
|
|
moderationLockTestSuite,
|
|
moderationDependencyTestSuite,
|
|
approvalPipelineTestSuite,
|
|
];
|
|
|
|
export interface TestResult {
|
|
id: string;
|
|
name: string;
|
|
suite: string;
|
|
status: 'pass' | 'fail' | 'skip' | 'running';
|
|
duration: number; // milliseconds
|
|
error?: string;
|
|
details?: any;
|
|
timestamp: string;
|
|
stack?: string;
|
|
}
|
|
|
|
export interface Test {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
run: () => Promise<TestResult>;
|
|
}
|
|
|
|
export interface TestSuite {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
tests: Test[];
|
|
}
|
|
|
|
export class IntegrationTestRunner {
|
|
private results: TestResult[] = [];
|
|
private isRunning = false;
|
|
private shouldStop = false;
|
|
private onProgress?: (result: TestResult) => void;
|
|
private delayBetweenTests: number;
|
|
private cleanupEnabled: boolean;
|
|
private cleanupSummary?: CleanupSummary;
|
|
|
|
constructor(
|
|
onProgress?: (result: TestResult) => void,
|
|
delayBetweenTests: number = 8000,
|
|
cleanupEnabled: boolean = true
|
|
) {
|
|
this.onProgress = onProgress;
|
|
this.delayBetweenTests = delayBetweenTests; // Default 8 seconds to prevent rate limiting
|
|
this.cleanupEnabled = cleanupEnabled;
|
|
}
|
|
|
|
/**
|
|
* Wait for specified milliseconds (for rate limiting prevention)
|
|
*/
|
|
private async delay(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* Run a single test with error handling
|
|
*/
|
|
async runTest(test: Test, suiteName: string): Promise<TestResult> {
|
|
if (this.shouldStop) {
|
|
return {
|
|
id: test.id,
|
|
name: test.name,
|
|
suite: suiteName,
|
|
status: 'skip',
|
|
duration: 0,
|
|
timestamp: new Date().toISOString(),
|
|
details: { reason: 'Test run stopped by user' }
|
|
};
|
|
}
|
|
|
|
// Mark as running
|
|
const runningResult: TestResult = {
|
|
id: test.id,
|
|
name: test.name,
|
|
suite: suiteName,
|
|
status: 'running',
|
|
duration: 0,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
if (this.onProgress) {
|
|
this.onProgress(runningResult);
|
|
}
|
|
|
|
try {
|
|
const result = await test.run();
|
|
this.results.push(result);
|
|
|
|
if (this.onProgress) {
|
|
this.onProgress(result);
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
const failResult: TestResult = {
|
|
id: test.id,
|
|
name: test.name,
|
|
suite: suiteName,
|
|
status: 'fail',
|
|
duration: 0,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
this.results.push(failResult);
|
|
|
|
if (this.onProgress) {
|
|
this.onProgress(failResult);
|
|
}
|
|
|
|
return failResult;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run all tests in a suite
|
|
*/
|
|
async runSuite(suite: TestSuite): Promise<TestResult[]> {
|
|
const suiteResults: TestResult[] = [];
|
|
|
|
for (let i = 0; i < suite.tests.length; i++) {
|
|
const test = suite.tests[i];
|
|
const result = await this.runTest(test, suite.name);
|
|
suiteResults.push(result);
|
|
|
|
if (this.shouldStop) {
|
|
break;
|
|
}
|
|
|
|
// Add delay between tests to prevent rate limiting (except after the last test)
|
|
if (i < suite.tests.length - 1 && this.delayBetweenTests > 0) {
|
|
// Report delay status with countdown
|
|
const delaySeconds = this.delayBetweenTests / 1000;
|
|
const delayResult: TestResult = {
|
|
id: `delay-${Date.now()}`,
|
|
name: `⏳ Rate limit delay: ${delaySeconds}s`,
|
|
suite: suite.name,
|
|
status: 'running',
|
|
duration: 0,
|
|
timestamp: new Date().toISOString(),
|
|
details: {
|
|
reason: 'Pausing to prevent rate limiting',
|
|
delayMs: this.delayBetweenTests
|
|
}
|
|
};
|
|
|
|
if (this.onProgress) {
|
|
this.onProgress(delayResult);
|
|
}
|
|
|
|
await this.delay(this.delayBetweenTests);
|
|
|
|
// Mark delay as complete
|
|
const delayCompleteResult: TestResult = {
|
|
...delayResult,
|
|
status: 'skip',
|
|
duration: this.delayBetweenTests,
|
|
details: { reason: 'Rate limit delay completed' }
|
|
};
|
|
|
|
if (this.onProgress) {
|
|
this.onProgress(delayCompleteResult);
|
|
}
|
|
}
|
|
}
|
|
|
|
return suiteResults;
|
|
}
|
|
|
|
/**
|
|
* Run all suites sequentially
|
|
*/
|
|
async runAllSuites(suites: TestSuite[]): Promise<TestResult[]> {
|
|
this.results = [];
|
|
this.isRunning = true;
|
|
this.shouldStop = false;
|
|
|
|
// Track submission-heavy suites for adaptive delays
|
|
const submissionHeavySuites = [
|
|
'Entity Submission & Validation',
|
|
'Approval Pipeline',
|
|
'Unit Conversion Tests',
|
|
'Performance & Scalability'
|
|
];
|
|
|
|
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]);
|
|
|
|
if (this.shouldStop) {
|
|
break;
|
|
}
|
|
|
|
// REACTIVE delay AFTER suites complete
|
|
if (i < suites.length - 1 && this.delayBetweenTests > 0) {
|
|
// Longer delay after submission-heavy suites
|
|
const delayMs = isHeavySuite
|
|
? this.delayBetweenTests * 2.25 // 18s delay after heavy suites (increased from 12s)
|
|
: this.delayBetweenTests; // 8s delay after others (increased from 6s)
|
|
|
|
const delaySeconds = delayMs / 1000;
|
|
const delayResult: TestResult = {
|
|
id: `suite-delay-${Date.now()}`,
|
|
name: `⏳ Suite completion delay: ${delaySeconds}s${isHeavySuite ? ' (submission-heavy)' : ''}`,
|
|
suite: 'System',
|
|
status: 'running',
|
|
duration: 0,
|
|
timestamp: new Date().toISOString(),
|
|
details: {
|
|
reason: 'Pausing between suites to prevent rate limiting',
|
|
isSubmissionHeavy: isHeavySuite
|
|
}
|
|
};
|
|
|
|
if (this.onProgress) {
|
|
this.onProgress(delayResult);
|
|
}
|
|
|
|
await this.delay(delayMs);
|
|
|
|
if (this.onProgress) {
|
|
this.onProgress({
|
|
...delayResult,
|
|
status: 'skip',
|
|
duration: delayMs,
|
|
details: { reason: 'Suite delay completed' }
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
return this.results;
|
|
}
|
|
|
|
/**
|
|
* Stop the test run
|
|
*/
|
|
stop(): void {
|
|
this.shouldStop = true;
|
|
}
|
|
|
|
/**
|
|
* Get all results
|
|
*/
|
|
getResults(): TestResult[] {
|
|
return this.results;
|
|
}
|
|
|
|
/**
|
|
* Get summary statistics
|
|
*/
|
|
getSummary(): {
|
|
total: number;
|
|
passed: number;
|
|
failed: number;
|
|
skipped: number;
|
|
running: number;
|
|
totalDuration: number;
|
|
cleanup?: CleanupSummary;
|
|
} {
|
|
const total = this.results.length;
|
|
const passed = this.results.filter(r => r.status === 'pass').length;
|
|
const failed = this.results.filter(r => r.status === 'fail').length;
|
|
const skipped = this.results.filter(r => r.status === 'skip').length;
|
|
const running = this.results.filter(r => r.status === 'running').length;
|
|
const totalDuration = this.results.reduce((sum, r) => sum + r.duration, 0);
|
|
|
|
return {
|
|
total,
|
|
passed,
|
|
failed,
|
|
skipped,
|
|
running,
|
|
totalDuration,
|
|
cleanup: this.cleanupSummary
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if runner is currently running
|
|
*/
|
|
getIsRunning(): boolean {
|
|
return this.isRunning;
|
|
}
|
|
|
|
/**
|
|
* Reset runner state
|
|
*/
|
|
reset(): void {
|
|
this.results = [];
|
|
this.isRunning = 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;
|
|
}
|
|
}
|