mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 07:31:12 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
188
src-old/lib/integrationTests/TestDataTracker.ts
Normal file
188
src-old/lib/integrationTests/TestDataTracker.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { Database } from '@/integrations/supabase/types';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
type TableName = keyof Database['public']['Tables'];
|
||||
|
||||
/**
|
||||
* TestDataTracker - Manages test data lifecycle for integration tests
|
||||
*
|
||||
* Tracks all created test entities and ensures proper cleanup in dependency order.
|
||||
* All tracked entities are marked with is_test_data=true for easy identification.
|
||||
*/
|
||||
export class TestDataTracker {
|
||||
private entities = new Map<string, Set<string>>();
|
||||
|
||||
/**
|
||||
* Track an entity for cleanup
|
||||
* @param table - Database table name
|
||||
* @param id - Entity ID
|
||||
*/
|
||||
track(table: string, id: string): void {
|
||||
if (!this.entities.has(table)) {
|
||||
this.entities.set(table, new Set());
|
||||
}
|
||||
this.entities.get(table)!.add(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track multiple entities at once
|
||||
* @param table - Database table name
|
||||
* @param ids - Array of entity IDs
|
||||
*/
|
||||
trackMany(table: string, ids: string[]): void {
|
||||
ids.forEach(id => this.track(table, id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tracked entity IDs for a specific table
|
||||
* @param table - Database table name
|
||||
* @returns Array of tracked IDs
|
||||
*/
|
||||
getTracked(table: string): string[] {
|
||||
return Array.from(this.entities.get(table) || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup all tracked test data in proper dependency order
|
||||
* Deletes children first, then parents to avoid foreign key violations
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
// Define deletion order (children first, parents last)
|
||||
const deletionOrder: TableName[] = [
|
||||
'reviews',
|
||||
'photos',
|
||||
'submission_items',
|
||||
'content_submissions',
|
||||
'ride_versions',
|
||||
'park_versions',
|
||||
'company_versions',
|
||||
'ride_model_versions',
|
||||
'rides',
|
||||
'parks',
|
||||
'ride_models',
|
||||
'companies',
|
||||
'test_data_registry'
|
||||
];
|
||||
|
||||
const errors: Array<{ table: string; error: any }> = [];
|
||||
|
||||
for (const table of deletionOrder) {
|
||||
const ids = this.getTracked(table);
|
||||
if (ids.length === 0) continue;
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from(table as any)
|
||||
.delete()
|
||||
.in('id', ids);
|
||||
|
||||
if (error) {
|
||||
errors.push({ table, error });
|
||||
logger.warn('Failed to cleanup test data table', { table, error });
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ table, error: err });
|
||||
logger.warn('Exception cleaning up test data table', { table, error: err });
|
||||
}
|
||||
}
|
||||
|
||||
// Clear tracking after cleanup attempt
|
||||
this.entities.clear();
|
||||
|
||||
if (errors.length > 0) {
|
||||
logger.warn('Cleanup completed with errors', { errorCount: errors.length, errors });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that all tracked test data has been cleaned up
|
||||
* @returns Array of remaining test data items
|
||||
*/
|
||||
async verifyCleanup(): Promise<Array<{ table: string; count: number }>> {
|
||||
const tables: TableName[] = [
|
||||
'parks', 'rides', 'companies', 'ride_models',
|
||||
'content_submissions', 'submission_items',
|
||||
'park_versions', 'ride_versions', 'company_versions', 'ride_model_versions',
|
||||
'photos', 'reviews', 'test_data_registry'
|
||||
];
|
||||
|
||||
const remaining: Array<{ table: string; count: number }> = [];
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
const { count, error } = await supabase
|
||||
.from(table as any)
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('is_test_data', true);
|
||||
|
||||
if (error) {
|
||||
logger.warn('Failed to check test data table', { table, error });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count && count > 0) {
|
||||
remaining.push({ table, count });
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Exception checking test data table', { table, error: err });
|
||||
}
|
||||
}
|
||||
|
||||
return remaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk delete all test data from the database (emergency cleanup)
|
||||
* WARNING: This deletes ALL data marked with is_test_data=true
|
||||
*/
|
||||
static async bulkCleanupAllTestData(): Promise<{ deleted: number; errors: number }> {
|
||||
const tables: TableName[] = [
|
||||
'reviews', 'photos', 'submission_items', 'content_submissions',
|
||||
'ride_versions', 'park_versions', 'company_versions', 'ride_model_versions',
|
||||
'rides', 'parks', 'ride_models', 'companies', 'test_data_registry'
|
||||
];
|
||||
|
||||
let totalDeleted = 0;
|
||||
let totalErrors = 0;
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
// First count how many will be deleted
|
||||
const { count: countToDelete } = await supabase
|
||||
.from(table as any)
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('is_test_data', true);
|
||||
|
||||
// Then delete without selecting (avoids needing SELECT permission on deleted rows)
|
||||
const { error } = await supabase
|
||||
.from(table as any)
|
||||
.delete()
|
||||
.eq('is_test_data', true);
|
||||
|
||||
if (error) {
|
||||
logger.warn('Failed to bulk delete test data', { table, error });
|
||||
totalErrors++;
|
||||
} else if (countToDelete) {
|
||||
totalDeleted += countToDelete;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Exception bulk deleting test data', { table, error: err });
|
||||
totalErrors++;
|
||||
}
|
||||
}
|
||||
|
||||
return { deleted: totalDeleted, errors: totalErrors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get summary of tracked entities
|
||||
*/
|
||||
getSummary(): Record<string, number> {
|
||||
const summary: Record<string, number> = {};
|
||||
this.entities.forEach((ids, table) => {
|
||||
summary[table] = ids.size;
|
||||
});
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
10
src-old/lib/integrationTests/index.ts
Normal file
10
src-old/lib/integrationTests/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Integration Testing System
|
||||
*
|
||||
* Main exports for the comprehensive integration testing framework.
|
||||
*/
|
||||
|
||||
export { IntegrationTestRunner } from './testRunner';
|
||||
export { allTestSuites } from './suites';
|
||||
|
||||
export type { TestResult, Test, TestSuite } from './testRunner';
|
||||
259
src-old/lib/integrationTests/suites/authTests.ts
Normal file
259
src-old/lib/integrationTests/suites/authTests.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Authentication & Authorization Test Suite
|
||||
*
|
||||
* Tests auth flows, MFA enforcement, role checks, and session management.
|
||||
*/
|
||||
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { TestSuite, TestResult } from '../testRunner';
|
||||
|
||||
export const authTestSuite: TestSuite = {
|
||||
id: 'auth',
|
||||
name: 'Authentication & Authorization',
|
||||
description: 'Tests for auth flows, MFA, roles, and permissions',
|
||||
tests: [
|
||||
{
|
||||
id: 'auth-001',
|
||||
name: 'User Session Validation',
|
||||
description: 'Validates current user session is valid with proper JWT structure',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Get current session
|
||||
const { data: { session }, error } = await supabase.auth.getSession();
|
||||
|
||||
if (error) throw new Error(`Session fetch failed: ${error.message}`);
|
||||
if (!session) throw new Error('No active session found');
|
||||
if (!session.access_token) throw new Error('No access token in session');
|
||||
if (!session.user) throw new Error('No user in session');
|
||||
if (!session.user.id) throw new Error('No user ID in session');
|
||||
|
||||
// Validate token structure (JWT has 3 parts separated by dots)
|
||||
const tokenParts = session.access_token.split('.');
|
||||
if (tokenParts.length !== 3) {
|
||||
throw new Error(`Invalid JWT structure: expected 3 parts, got ${tokenParts.length}`);
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if (session.expires_at && session.expires_at < Date.now() / 1000) {
|
||||
throw new Error('Session token is expired');
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'auth-001',
|
||||
name: 'User Session Validation',
|
||||
suite: 'Authentication & Authorization',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
userId: session.user.id,
|
||||
email: session.user.email,
|
||||
expiresAt: session.expires_at,
|
||||
aal: (session.user as any).aal || 'aal1'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'auth-001',
|
||||
name: 'User Session Validation',
|
||||
suite: 'Authentication & Authorization',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'auth-002',
|
||||
name: 'Role-Based Access Control (RBAC)',
|
||||
description: 'Tests role checks are consistent across hooks and database functions',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error('No authenticated user');
|
||||
|
||||
// Query user_roles table
|
||||
const { data: roles, error: rolesError } = await supabase
|
||||
.from('user_roles')
|
||||
.select('role')
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (rolesError) throw new Error(`Failed to fetch roles: ${rolesError.message}`);
|
||||
|
||||
// Test is_moderator() database function
|
||||
const { data: isMod, error: modError } = await supabase
|
||||
.rpc('is_moderator', { _user_id: user.id });
|
||||
|
||||
if (modError) throw new Error(`is_moderator() failed: ${modError.message}`);
|
||||
|
||||
// Test is_superuser() database function
|
||||
const { data: isSuper, error: superError } = await supabase
|
||||
.rpc('is_superuser', { _user_id: user.id });
|
||||
|
||||
if (superError) throw new Error(`is_superuser() failed: ${superError.message}`);
|
||||
|
||||
// Validate consistency
|
||||
const hasModRole = roles?.some(r => ['moderator', 'admin', 'superuser'].includes(r.role));
|
||||
if (hasModRole !== isMod) {
|
||||
throw new Error(`Inconsistent moderator check: has role=${hasModRole}, is_moderator()=${isMod}`);
|
||||
}
|
||||
|
||||
const hasSuperRole = roles?.some(r => r.role === 'superuser');
|
||||
if (hasSuperRole !== isSuper) {
|
||||
throw new Error(`Inconsistent superuser check: has role=${hasSuperRole}, is_superuser()=${isSuper}`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'auth-002',
|
||||
name: 'Role-Based Access Control (RBAC)',
|
||||
suite: 'Authentication & Authorization',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
roles: roles?.map(r => r.role) || [],
|
||||
isModerator: isMod,
|
||||
isSuperuser: isSuper,
|
||||
consistent: true
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'auth-002',
|
||||
name: 'Role-Based Access Control (RBAC)',
|
||||
suite: 'Authentication & Authorization',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'auth-003',
|
||||
name: 'MFA Factor Detection',
|
||||
description: 'Tests MFA enrollment detection and AAL level',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error('No authenticated user');
|
||||
|
||||
// Get MFA factors
|
||||
const { data: factors, error: factorsError } = await supabase.auth.mfa.listFactors();
|
||||
|
||||
if (factorsError) throw new Error(`Failed to list MFA factors: ${factorsError.message}`);
|
||||
|
||||
const hasVerifiedFactor = factors?.totp?.some(f => f.status === 'verified') || false;
|
||||
const currentAAL = (user as any).aal || 'aal1';
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'auth-003',
|
||||
name: 'MFA Factor Detection',
|
||||
suite: 'Authentication & Authorization',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
hasVerifiedMFA: hasVerifiedFactor,
|
||||
currentAAL: currentAAL,
|
||||
totpFactorCount: factors?.totp?.length || 0,
|
||||
verifiedFactorCount: factors?.totp?.filter(f => f.status === 'verified').length || 0
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'auth-003',
|
||||
name: 'MFA Factor Detection',
|
||||
suite: 'Authentication & Authorization',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'auth-004',
|
||||
name: 'Banned User Detection',
|
||||
description: 'Tests banned user detection in profiles table',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error('No authenticated user');
|
||||
|
||||
// Query profile banned status
|
||||
const { data: profile, error: profileError } = await supabase
|
||||
.from('profiles')
|
||||
.select('banned')
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
|
||||
if (profileError) throw new Error(`Failed to fetch profile: ${profileError.message}`);
|
||||
if (!profile) throw new Error('No profile found');
|
||||
|
||||
// Test is_user_banned() database function
|
||||
const { data: isBanned, error: bannedError } = await supabase
|
||||
.rpc('is_user_banned', { p_user_id: user.id });
|
||||
|
||||
if (bannedError) throw new Error(`is_user_banned() failed: ${bannedError.message}`);
|
||||
|
||||
// Validate consistency
|
||||
if (profile.banned !== isBanned) {
|
||||
throw new Error(`Inconsistent banned check: profile=${profile.banned}, is_user_banned()=${isBanned}`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'auth-004',
|
||||
name: 'Banned User Detection',
|
||||
suite: 'Authentication & Authorization',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
isBanned: profile.banned,
|
||||
consistent: true
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'auth-004',
|
||||
name: 'Banned User Detection',
|
||||
suite: 'Authentication & Authorization',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
300
src-old/lib/integrationTests/suites/dataIntegrityTests.ts
Normal file
300
src-old/lib/integrationTests/suites/dataIntegrityTests.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Data Integrity & Constraints Test Suite
|
||||
*
|
||||
* Tests database constraints, RLS policies, and data integrity rules.
|
||||
*/
|
||||
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { TestSuite, TestResult } from '../testRunner';
|
||||
import { TestDataTracker } from '../TestDataTracker';
|
||||
|
||||
export const dataIntegrityTestSuite: TestSuite = {
|
||||
id: 'data-integrity',
|
||||
name: 'Data Integrity & Constraints',
|
||||
description: 'Tests database constraints, RLS policies, and data integrity',
|
||||
tests: [
|
||||
{
|
||||
id: 'integrity-001',
|
||||
name: 'RLS Policy Enforcement - Public Read',
|
||||
description: 'Validates public read access to entity tables',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Test public read access to parks
|
||||
const { data: parks, error: parksError } = await supabase
|
||||
.from('parks')
|
||||
.select('id, name, slug')
|
||||
.limit(5);
|
||||
|
||||
if (parksError) throw new Error(`Parks read failed: ${parksError.message}`);
|
||||
|
||||
// Test public read access to rides
|
||||
const { data: rides, error: ridesError } = await supabase
|
||||
.from('rides')
|
||||
.select('id, name, slug')
|
||||
.limit(5);
|
||||
|
||||
if (ridesError) throw new Error(`Rides read failed: ${ridesError.message}`);
|
||||
|
||||
// Test public read access to companies
|
||||
const { data: companies, error: companiesError } = await supabase
|
||||
.from('companies')
|
||||
.select('id, name, slug')
|
||||
.limit(5);
|
||||
|
||||
if (companiesError) throw new Error(`Companies read failed: ${companiesError.message}`);
|
||||
|
||||
// Test public read access to ride_models
|
||||
const { data: models, error: modelsError } = await supabase
|
||||
.from('ride_models')
|
||||
.select('id, name, slug')
|
||||
.limit(5);
|
||||
|
||||
if (modelsError) throw new Error(`Ride models read failed: ${modelsError.message}`);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'integrity-001',
|
||||
name: 'RLS Policy Enforcement - Public Read',
|
||||
suite: 'Data Integrity & Constraints',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
parksReadable: Array.isArray(parks),
|
||||
ridesReadable: Array.isArray(rides),
|
||||
companiesReadable: Array.isArray(companies),
|
||||
rideModelsReadable: Array.isArray(models)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'integrity-001',
|
||||
name: 'RLS Policy Enforcement - Public Read',
|
||||
suite: 'Data Integrity & Constraints',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'integrity-002',
|
||||
name: 'Foreign Key Constraint Enforcement',
|
||||
description: 'Tests foreign key constraints prevent invalid references',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Try to create a ride with non-existent park_id
|
||||
const invalidParkId = '00000000-0000-0000-0000-000000000000';
|
||||
const slug = `test-ride-${Date.now()}`;
|
||||
|
||||
const { error } = await supabase
|
||||
.from('rides')
|
||||
.insert({
|
||||
name: 'Invalid Ride',
|
||||
slug,
|
||||
park_id: invalidParkId,
|
||||
category: 'roller_coaster',
|
||||
status: 'operating',
|
||||
is_test_data: true
|
||||
});
|
||||
|
||||
// This SHOULD fail with foreign key violation
|
||||
if (!error) {
|
||||
throw new Error('Foreign key constraint not enforced - invalid park_id was accepted');
|
||||
}
|
||||
|
||||
// Verify it's a foreign key violation
|
||||
if (!error.message.includes('foreign key') && !error.message.includes('violates')) {
|
||||
throw new Error(`Expected foreign key error, got: ${error.message}`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'integrity-002',
|
||||
name: 'Foreign Key Constraint Enforcement',
|
||||
suite: 'Data Integrity & Constraints',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
constraintEnforced: true,
|
||||
errorMessage: error.message
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'integrity-002',
|
||||
name: 'Foreign Key Constraint Enforcement',
|
||||
suite: 'Data Integrity & Constraints',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'integrity-003',
|
||||
name: 'Unique Constraint Enforcement',
|
||||
description: 'Tests unique constraints prevent duplicate slugs',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
const tracker = new TestDataTracker();
|
||||
let parkId: string | null = null;
|
||||
|
||||
try {
|
||||
// Create a park
|
||||
const slug = `unique-test-${Date.now()}`;
|
||||
const { data: park, error: createError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Unique Test Park',
|
||||
slug,
|
||||
park_type: 'theme_park',
|
||||
status: 'operating',
|
||||
is_test_data: true
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (createError) throw new Error(`Park creation failed: ${createError.message}`);
|
||||
if (!park) throw new Error('No park returned');
|
||||
|
||||
parkId = park.id;
|
||||
tracker.track('parks', parkId);
|
||||
|
||||
// Try to create another park with same slug
|
||||
const { error: duplicateError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Duplicate Park',
|
||||
slug, // Same slug
|
||||
park_type: 'theme_park',
|
||||
status: 'operating',
|
||||
is_test_data: true
|
||||
});
|
||||
|
||||
// This SHOULD fail with unique violation
|
||||
if (!duplicateError) {
|
||||
throw new Error('Unique constraint not enforced - duplicate slug was accepted');
|
||||
}
|
||||
|
||||
// Verify it's a unique violation
|
||||
if (!duplicateError.message.includes('unique') && !duplicateError.message.includes('duplicate')) {
|
||||
throw new Error(`Expected unique constraint error, got: ${duplicateError.message}`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'integrity-003',
|
||||
name: 'Unique Constraint Enforcement',
|
||||
suite: 'Data Integrity & Constraints',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
constraintEnforced: true,
|
||||
errorMessage: duplicateError.message
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'integrity-003',
|
||||
name: 'Unique Constraint Enforcement',
|
||||
suite: 'Data Integrity & Constraints',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
await tracker.cleanup();
|
||||
const remaining = await tracker.verifyCleanup();
|
||||
if (remaining.length > 0) {
|
||||
console.warn('integrity-003 cleanup incomplete:', remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'integrity-004',
|
||||
name: 'No JSONB in Entity Tables',
|
||||
description: 'Validates no JSONB columns exist in entity tables (per requirements)',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Sample actual data and check structure (information_schema not accessible via RLS)
|
||||
const { data: parks } = await supabase.from('parks').select('*').limit(1);
|
||||
const { data: rides } = await supabase.from('rides').select('*').limit(1);
|
||||
const { data: companies } = await supabase.from('companies').select('*').limit(1);
|
||||
const { data: models } = await supabase.from('ride_models').select('*').limit(1);
|
||||
|
||||
// Check if any fields appear to be JSONB objects
|
||||
const hasJsonbFields = [parks, rides, companies, models].some(dataset => {
|
||||
if (!dataset || dataset.length === 0) return false;
|
||||
const record = dataset[0] as any;
|
||||
return Object.keys(record).some(key => {
|
||||
const val = record[key];
|
||||
// Check if value is a plain object (not Date, not Array, not null)
|
||||
if (val === null || val === undefined) return false;
|
||||
if (typeof val !== 'object') return false;
|
||||
if (Array.isArray(val)) return false;
|
||||
// Check if it's a Date by checking if it has getTime method
|
||||
if (val && typeof val.getTime === 'function') return false;
|
||||
// If we get here, it's likely a JSONB object
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
if (hasJsonbFields) {
|
||||
throw new Error('Found JSONB-like fields in entity tables');
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'integrity-004',
|
||||
name: 'No JSONB in Entity Tables',
|
||||
suite: 'Data Integrity & Constraints',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
noJsonbColumns: true,
|
||||
validation: 'Entity tables use relational structure only'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'integrity-004',
|
||||
name: 'No JSONB in Entity Tables',
|
||||
suite: 'Data Integrity & Constraints',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
197
src-old/lib/integrationTests/suites/edgeFunctionTests.ts
Normal file
197
src-old/lib/integrationTests/suites/edgeFunctionTests.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Edge Function Integration Tests
|
||||
*
|
||||
* Tests for edge function authentication, authorization, and functionality.
|
||||
*/
|
||||
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { TestSuite, TestResult } from '../testRunner';
|
||||
|
||||
export const edgeFunctionTestSuite: TestSuite = {
|
||||
id: 'edge-functions',
|
||||
name: 'Edge Function Tests',
|
||||
description: 'Tests for edge function authentication and business logic',
|
||||
tests: [
|
||||
{
|
||||
id: 'edge-001',
|
||||
name: 'Edge Function Authentication',
|
||||
description: 'Validates edge functions require authentication',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Get current session
|
||||
const { data: session } = await supabase.auth.getSession();
|
||||
|
||||
if (!session.session) {
|
||||
throw new Error('No active session for test');
|
||||
}
|
||||
|
||||
// Verify we have a valid JWT token
|
||||
const token = session.session.access_token;
|
||||
if (!token || token.length < 50) {
|
||||
throw new Error('Invalid access token');
|
||||
}
|
||||
|
||||
// Decode JWT to check structure (basic validation)
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('JWT token has invalid structure');
|
||||
}
|
||||
|
||||
// Test that auth is working by calling a protected endpoint
|
||||
const { data: user } = await supabase.auth.getUser();
|
||||
if (!user.user) {
|
||||
throw new Error('Cannot retrieve authenticated user');
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'edge-001',
|
||||
name: 'Edge Function Authentication',
|
||||
suite: 'Edge Function Tests',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
hasToken: true,
|
||||
userId: user.user.id,
|
||||
tokenLength: token.length
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'edge-001',
|
||||
name: 'Edge Function Authentication',
|
||||
suite: 'Edge Function Tests',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'edge-002',
|
||||
name: 'User Ban Check Function',
|
||||
description: 'Tests is_user_banned database function',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { data: userData } = await supabase.auth.getUser();
|
||||
if (!userData.user) throw new Error('No authenticated user');
|
||||
|
||||
// Call the ban check function
|
||||
const { data: isBanned, error: banError } = await supabase
|
||||
.rpc('is_user_banned', {
|
||||
p_user_id: userData.user.id
|
||||
});
|
||||
|
||||
if (banError) throw new Error(`Ban check failed: ${banError.message}`);
|
||||
|
||||
// Superuser running tests should not be banned
|
||||
if (isBanned === true) {
|
||||
throw new Error('Test user is banned (superuser should not be banned)');
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'edge-002',
|
||||
name: 'User Ban Check Function',
|
||||
suite: 'Edge Function Tests',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
userId: userData.user.id,
|
||||
isBanned: isBanned,
|
||||
functionWorks: true
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'edge-002',
|
||||
name: 'User Ban Check Function',
|
||||
suite: 'Edge Function Tests',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'edge-003',
|
||||
name: 'Moderator Permissions Function',
|
||||
description: 'Tests is_moderator and permission checking',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { data: userData } = await supabase.auth.getUser();
|
||||
if (!userData.user) throw new Error('No authenticated user');
|
||||
|
||||
// Test is_moderator function
|
||||
const { data: isMod, error: modError } = await supabase
|
||||
.rpc('is_moderator', {
|
||||
_user_id: userData.user.id
|
||||
});
|
||||
|
||||
if (modError) throw new Error(`Moderator check failed: ${modError.message}`);
|
||||
|
||||
// Test user running tests should be a moderator (superuser)
|
||||
if (!isMod) {
|
||||
throw new Error('Test user is not a moderator (superuser should be moderator)');
|
||||
}
|
||||
|
||||
// Test is_superuser function
|
||||
const { data: isSuperuser, error: superError } = await supabase
|
||||
.rpc('is_superuser', {
|
||||
_user_id: userData.user.id
|
||||
});
|
||||
|
||||
if (superError) throw new Error(`Superuser check failed: ${superError.message}`);
|
||||
|
||||
if (!isSuperuser) {
|
||||
throw new Error('Test user is not a superuser');
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'edge-003',
|
||||
name: 'Moderator Permissions Function',
|
||||
suite: 'Edge Function Tests',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
userId: userData.user.id,
|
||||
isModerator: isMod,
|
||||
isSuperuser: isSuperuser,
|
||||
functionsWork: true
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'edge-003',
|
||||
name: 'Moderator Permissions Function',
|
||||
suite: 'Edge Function Tests',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
37
src-old/lib/integrationTests/suites/index.ts
Normal file
37
src-old/lib/integrationTests/suites/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Integration Test Suites Export
|
||||
*
|
||||
* Exports all test suites for the integration testing system.
|
||||
*/
|
||||
|
||||
import { authTestSuite } from './authTests';
|
||||
import { versioningTestSuite } from './versioningTests';
|
||||
import { dataIntegrityTestSuite } from './dataIntegrityTests';
|
||||
import { submissionTestSuite } from './submissionTests';
|
||||
import { moderationTestSuite } from './moderationTests';
|
||||
import { edgeFunctionTestSuite } from './edgeFunctionTests';
|
||||
import { unitConversionTestSuite } from './unitConversionTests';
|
||||
import { performanceTestSuite } from './performanceTests';
|
||||
import type { TestSuite } from '../testRunner';
|
||||
|
||||
export const allTestSuites: TestSuite[] = [
|
||||
authTestSuite,
|
||||
versioningTestSuite,
|
||||
dataIntegrityTestSuite,
|
||||
submissionTestSuite,
|
||||
moderationTestSuite,
|
||||
edgeFunctionTestSuite,
|
||||
unitConversionTestSuite,
|
||||
performanceTestSuite,
|
||||
];
|
||||
|
||||
export {
|
||||
authTestSuite,
|
||||
versioningTestSuite,
|
||||
dataIntegrityTestSuite,
|
||||
submissionTestSuite,
|
||||
moderationTestSuite,
|
||||
edgeFunctionTestSuite,
|
||||
unitConversionTestSuite,
|
||||
performanceTestSuite,
|
||||
};
|
||||
154
src-old/lib/integrationTests/suites/moderationDependencyTests.ts
Normal file
154
src-old/lib/integrationTests/suites/moderationDependencyTests.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Multi-Item Dependency Resolution Integration Tests
|
||||
*
|
||||
* Tests for handling complex submission dependencies
|
||||
*/
|
||||
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { TestSuite, TestResult } from '../testRunner';
|
||||
|
||||
export const moderationDependencyTestSuite: TestSuite = {
|
||||
id: 'moderation-dependencies',
|
||||
name: 'Multi-Item Dependency Resolution',
|
||||
description: 'Tests for handling complex submission dependencies',
|
||||
tests: [
|
||||
{
|
||||
id: 'dep-001',
|
||||
name: 'Approve Independent Items in Any Order',
|
||||
description: 'Verifies that items without dependencies can be approved in any order',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { data: userData } = await supabase.auth.getUser();
|
||||
if (!userData.user) throw new Error('No authenticated user');
|
||||
|
||||
// Create submission with 2 independent park items
|
||||
const { data: submission, error: createError } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert({
|
||||
user_id: userData.user.id,
|
||||
submission_type: 'park',
|
||||
status: 'pending',
|
||||
content: { test: true }
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (createError) throw createError;
|
||||
|
||||
// Create two park submission items (independent)
|
||||
const { error: items1Error } = await supabase
|
||||
.from('submission_items')
|
||||
.insert([
|
||||
{
|
||||
submission_id: submission.id,
|
||||
item_type: 'park',
|
||||
item_data: { name: 'Test Park 1', slug: 'test-park-1', country: 'US' },
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
submission_id: submission.id,
|
||||
item_type: 'park',
|
||||
item_data: { name: 'Test Park 2', slug: 'test-park-2', country: 'US' },
|
||||
status: 'pending'
|
||||
}
|
||||
]);
|
||||
|
||||
if (items1Error) throw items1Error;
|
||||
|
||||
// Get items
|
||||
const { data: items } = await supabase
|
||||
.from('submission_items')
|
||||
.select('id')
|
||||
.eq('submission_id', submission.id)
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (!items || items.length !== 2) {
|
||||
throw new Error('Failed to create submission items');
|
||||
}
|
||||
|
||||
// Approve second item first (should work - no dependencies)
|
||||
const { error: approve2Error } = await supabase
|
||||
.from('submission_items')
|
||||
.update({ status: 'approved' })
|
||||
.eq('id', items[1].id);
|
||||
|
||||
if (approve2Error) throw new Error('Failed to approve second item first');
|
||||
|
||||
// Approve first item second (should also work)
|
||||
const { error: approve1Error } = await supabase
|
||||
.from('submission_items')
|
||||
.update({ status: 'approved' })
|
||||
.eq('id', items[0].id);
|
||||
|
||||
if (approve1Error) throw new Error('Failed to approve first item second');
|
||||
|
||||
// Cleanup
|
||||
await supabase.from('content_submissions').delete().eq('id', submission.id);
|
||||
|
||||
return {
|
||||
id: 'dep-001',
|
||||
name: 'Approve Independent Items in Any Order',
|
||||
suite: 'Multi-Item Dependency Resolution',
|
||||
status: 'pass',
|
||||
duration: Date.now() - startTime,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'dep-001',
|
||||
name: 'Approve Independent Items in Any Order',
|
||||
suite: 'Multi-Item Dependency Resolution',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: 'dep-002',
|
||||
name: 'Verify Submission Item Dependencies Exist',
|
||||
description: 'Verifies that submission items have proper dependency tracking',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Verify submission_items table has dependency columns
|
||||
const { data: testItem } = await supabase
|
||||
.from('submission_items')
|
||||
.select('id, status')
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
// If query succeeds, table exists and is accessible
|
||||
return {
|
||||
id: 'dep-002',
|
||||
name: 'Verify Submission Item Dependencies Exist',
|
||||
suite: 'Multi-Item Dependency Resolution',
|
||||
status: 'pass',
|
||||
duration: Date.now() - startTime,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
tableAccessible: true,
|
||||
testQuery: 'submission_items table verified'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'dep-002',
|
||||
name: 'Verify Submission Item Dependencies Exist',
|
||||
suite: 'Multi-Item Dependency Resolution',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
294
src-old/lib/integrationTests/suites/moderationLockTests.ts
Normal file
294
src-old/lib/integrationTests/suites/moderationLockTests.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Moderation Lock Management Integration Tests
|
||||
*
|
||||
* Tests for submission locking, claiming, extending, and release mechanisms
|
||||
*/
|
||||
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { TestSuite, TestResult } from '../testRunner';
|
||||
|
||||
export const moderationLockTestSuite: TestSuite = {
|
||||
id: 'moderation-locks',
|
||||
name: 'Moderation Lock Management',
|
||||
description: 'Tests for submission locking, claiming, and release mechanisms',
|
||||
tests: [
|
||||
{
|
||||
id: 'lock-001',
|
||||
name: 'Claim Submission Creates Active Lock',
|
||||
description: 'Verifies that claiming a submission creates a lock with correct expiry',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { data: userData } = await supabase.auth.getUser();
|
||||
if (!userData.user) throw new Error('No authenticated user');
|
||||
|
||||
// 1. Create test submission
|
||||
const { data: submission, error: createError } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert({
|
||||
user_id: userData.user.id,
|
||||
submission_type: 'park',
|
||||
status: 'pending',
|
||||
content: { test: true }
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (createError) throw createError;
|
||||
|
||||
// 2. Claim the submission (manual update for testing)
|
||||
const { error: lockError } = await supabase
|
||||
.from('content_submissions')
|
||||
.update({
|
||||
assigned_to: userData.user.id,
|
||||
locked_until: new Date(Date.now() + 15 * 60 * 1000).toISOString()
|
||||
})
|
||||
.eq('id', submission.id);
|
||||
|
||||
if (lockError) throw new Error(`Claim failed: ${lockError.message}`);
|
||||
|
||||
// 3. Verify lock exists
|
||||
const { data: lockedSubmission, error: fetchError } = await supabase
|
||||
.from('content_submissions')
|
||||
.select('assigned_to, locked_until')
|
||||
.eq('id', submission.id)
|
||||
.single();
|
||||
|
||||
if (fetchError) throw fetchError;
|
||||
|
||||
// 4. Assertions
|
||||
if (lockedSubmission.assigned_to !== userData.user.id) {
|
||||
throw new Error('Submission not assigned to current user');
|
||||
}
|
||||
|
||||
if (!lockedSubmission.locked_until) {
|
||||
throw new Error('locked_until not set');
|
||||
}
|
||||
|
||||
const lockedUntil = new Date(lockedSubmission.locked_until);
|
||||
const now = new Date();
|
||||
const diffMinutes = (lockedUntil.getTime() - now.getTime()) / (1000 * 60);
|
||||
|
||||
if (diffMinutes < 14 || diffMinutes > 16) {
|
||||
throw new Error(`Lock duration incorrect: ${diffMinutes} minutes`);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
await supabase.from('content_submissions').delete().eq('id', submission.id);
|
||||
|
||||
return {
|
||||
id: 'lock-001',
|
||||
name: 'Claim Submission Creates Active Lock',
|
||||
suite: 'Moderation Lock Management',
|
||||
status: 'pass',
|
||||
duration: Date.now() - startTime,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
submissionId: submission.id,
|
||||
lockDurationMinutes: diffMinutes,
|
||||
assignedTo: lockedSubmission.assigned_to
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'lock-001',
|
||||
name: 'Claim Submission Creates Active Lock',
|
||||
suite: 'Moderation Lock Management',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: 'lock-002',
|
||||
name: 'Release Lock Clears Assignment',
|
||||
description: 'Verifies that releasing a lock clears assigned_to and locked_until',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { data: userData } = await supabase.auth.getUser();
|
||||
if (!userData.user) throw new Error('No authenticated user');
|
||||
|
||||
// Create and claim submission
|
||||
const { data: submission, error: createError } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert({
|
||||
user_id: userData.user.id,
|
||||
submission_type: 'park',
|
||||
status: 'pending',
|
||||
content: { test: true }
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (createError) throw createError;
|
||||
|
||||
await supabase
|
||||
.from('content_submissions')
|
||||
.update({
|
||||
assigned_to: userData.user.id,
|
||||
locked_until: new Date(Date.now() + 15 * 60 * 1000).toISOString()
|
||||
})
|
||||
.eq('id', submission.id);
|
||||
|
||||
// Release lock
|
||||
const { error: releaseError } = await supabase
|
||||
.from('content_submissions')
|
||||
.update({
|
||||
assigned_to: null,
|
||||
locked_until: null
|
||||
})
|
||||
.eq('id', submission.id);
|
||||
|
||||
if (releaseError) throw new Error(`release_lock failed: ${releaseError.message}`);
|
||||
|
||||
// Verify lock cleared
|
||||
const { data: releasedSubmission, error: fetchError } = await supabase
|
||||
.from('content_submissions')
|
||||
.select('assigned_to, locked_until')
|
||||
.eq('id', submission.id)
|
||||
.single();
|
||||
|
||||
if (fetchError) throw fetchError;
|
||||
|
||||
if (releasedSubmission.assigned_to !== null) {
|
||||
throw new Error('assigned_to not cleared');
|
||||
}
|
||||
|
||||
if (releasedSubmission.locked_until !== null) {
|
||||
throw new Error('locked_until not cleared');
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
await supabase.from('content_submissions').delete().eq('id', submission.id);
|
||||
|
||||
return {
|
||||
id: 'lock-002',
|
||||
name: 'Release Lock Clears Assignment',
|
||||
suite: 'Moderation Lock Management',
|
||||
status: 'pass',
|
||||
duration: Date.now() - startTime,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'lock-002',
|
||||
name: 'Release Lock Clears Assignment',
|
||||
suite: 'Moderation Lock Management',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: 'lock-003',
|
||||
name: 'Extend Lock Adds 15 Minutes',
|
||||
description: 'Verifies that extending a lock adds correct duration',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { data: userData } = await supabase.auth.getUser();
|
||||
if (!userData.user) throw new Error('No authenticated user');
|
||||
|
||||
// Create and claim submission
|
||||
const { data: submission, error: createError } = await supabase
|
||||
.from('content_submissions')
|
||||
.insert({
|
||||
user_id: userData.user.id,
|
||||
submission_type: 'park',
|
||||
status: 'pending',
|
||||
content: { test: true }
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (createError) throw createError;
|
||||
|
||||
const initialLockTime = new Date(Date.now() + 15 * 60 * 1000);
|
||||
await supabase
|
||||
.from('content_submissions')
|
||||
.update({
|
||||
assigned_to: userData.user.id,
|
||||
locked_until: initialLockTime.toISOString()
|
||||
})
|
||||
.eq('id', submission.id);
|
||||
|
||||
// Get initial lock time
|
||||
const { data: initialLock } = await supabase
|
||||
.from('content_submissions')
|
||||
.select('locked_until')
|
||||
.eq('id', submission.id)
|
||||
.single();
|
||||
|
||||
// Extend lock (add 15 more minutes)
|
||||
const extendedLockTime = new Date(initialLockTime.getTime() + 15 * 60 * 1000);
|
||||
const { error: extendError } = await supabase
|
||||
.from('content_submissions')
|
||||
.update({
|
||||
locked_until: extendedLockTime.toISOString()
|
||||
})
|
||||
.eq('id', submission.id);
|
||||
|
||||
if (extendError) throw new Error(`extend_lock failed: ${extendError.message}`);
|
||||
|
||||
// Verify extended lock
|
||||
const { data: extendedLock, error: fetchError } = await supabase
|
||||
.from('content_submissions')
|
||||
.select('locked_until')
|
||||
.eq('id', submission.id)
|
||||
.single();
|
||||
|
||||
if (fetchError) throw fetchError;
|
||||
|
||||
if (!initialLock?.locked_until || !extendedLock.locked_until) {
|
||||
throw new Error('Lock times not found');
|
||||
}
|
||||
|
||||
const initialTime = new Date(initialLock.locked_until);
|
||||
const extendedTime = new Date(extendedLock.locked_until);
|
||||
const diffMinutes = (extendedTime.getTime() - initialTime.getTime()) / (1000 * 60);
|
||||
|
||||
if (diffMinutes < 14 || diffMinutes > 16) {
|
||||
throw new Error(`Extension duration incorrect: ${diffMinutes} minutes`);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
await supabase.from('content_submissions').delete().eq('id', submission.id);
|
||||
|
||||
return {
|
||||
id: 'lock-003',
|
||||
name: 'Extend Lock Adds 15 Minutes',
|
||||
suite: 'Moderation Lock Management',
|
||||
status: 'pass',
|
||||
duration: Date.now() - startTime,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
extensionMinutes: diffMinutes
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'lock-003',
|
||||
name: 'Extend Lock Adds 15 Minutes',
|
||||
suite: 'Moderation Lock Management',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
63
src-old/lib/integrationTests/suites/moderationTests.ts
Normal file
63
src-old/lib/integrationTests/suites/moderationTests.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Moderation Queue & Workflow Integration Tests
|
||||
*
|
||||
* Tests for moderation queue operations, locking, and state management.
|
||||
*/
|
||||
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { TestSuite, TestResult } from '../testRunner';
|
||||
|
||||
export const moderationTestSuite: TestSuite = {
|
||||
id: 'moderation',
|
||||
name: 'Moderation Queue & Workflow',
|
||||
description: 'Tests for moderation queue operations and submission workflows',
|
||||
tests: [
|
||||
{
|
||||
id: 'moderation-001',
|
||||
name: 'Moderation Functions Available',
|
||||
description: 'Validates moderation database functions are accessible',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { data: userData } = await supabase.auth.getUser();
|
||||
if (!userData.user) throw new Error('No authenticated user');
|
||||
|
||||
// Test that moderation functions exist and are callable
|
||||
const { data: isMod, error: modError } = await supabase
|
||||
.rpc('is_moderator', {
|
||||
_user_id: userData.user.id
|
||||
});
|
||||
|
||||
if (modError) throw new Error(`is_moderator function failed: ${modError.message}`);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'moderation-001',
|
||||
name: 'Moderation Functions Available',
|
||||
suite: 'Moderation Queue & Workflow',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
isModerator: isMod,
|
||||
functionsAccessible: true
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'moderation-001',
|
||||
name: 'Moderation Functions Available',
|
||||
suite: 'Moderation Queue & Workflow',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
275
src-old/lib/integrationTests/suites/performanceTests.ts
Normal file
275
src-old/lib/integrationTests/suites/performanceTests.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Performance & Scalability Integration Tests
|
||||
*
|
||||
* Tests for system performance under various conditions.
|
||||
*/
|
||||
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { TestSuite, TestResult } from '../testRunner';
|
||||
import { TestDataTracker } from '../TestDataTracker';
|
||||
|
||||
export const performanceTestSuite: TestSuite = {
|
||||
id: 'performance',
|
||||
name: 'Performance & Scalability',
|
||||
description: 'Tests for system performance and query efficiency',
|
||||
tests: [
|
||||
{
|
||||
id: 'perf-001',
|
||||
name: 'Entity Query Performance',
|
||||
description: 'Measures query performance for entity lists',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Test parks query performance
|
||||
const parksStart = Date.now();
|
||||
const { data: parks, error: parksError } = await supabase
|
||||
.from('parks')
|
||||
.select('id, name, slug, park_type, status')
|
||||
.limit(50);
|
||||
|
||||
const parksDuration = Date.now() - parksStart;
|
||||
|
||||
if (parksError) throw new Error(`Parks query failed: ${parksError.message}`);
|
||||
|
||||
// Test rides query performance
|
||||
const ridesStart = Date.now();
|
||||
const { data: rides, error: ridesError } = await supabase
|
||||
.from('rides')
|
||||
.select('id, name, slug, category, status, park_id')
|
||||
.limit(50);
|
||||
|
||||
const ridesDuration = Date.now() - ridesStart;
|
||||
|
||||
if (ridesError) throw new Error(`Rides query failed: ${ridesError.message}`);
|
||||
|
||||
// Test companies query performance
|
||||
const companiesStart = Date.now();
|
||||
const { data: companies, error: companiesError } = await supabase
|
||||
.from('companies')
|
||||
.select('id, name, slug, company_type')
|
||||
.limit(50);
|
||||
|
||||
const companiesDuration = Date.now() - companiesStart;
|
||||
|
||||
if (companiesError) throw new Error(`Companies query failed: ${companiesError.message}`);
|
||||
|
||||
// Set performance thresholds (in milliseconds)
|
||||
const threshold = 1000; // 1 second
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (parksDuration > threshold) {
|
||||
warnings.push(`Parks query slow: ${parksDuration}ms`);
|
||||
}
|
||||
if (ridesDuration > threshold) {
|
||||
warnings.push(`Rides query slow: ${ridesDuration}ms`);
|
||||
}
|
||||
if (companiesDuration > threshold) {
|
||||
warnings.push(`Companies query slow: ${companiesDuration}ms`);
|
||||
}
|
||||
|
||||
const totalDuration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'perf-001',
|
||||
name: 'Entity Query Performance',
|
||||
suite: 'Performance & Scalability',
|
||||
status: warnings.length === 0 ? 'pass' : 'fail',
|
||||
duration: totalDuration,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: warnings.length > 0 ? warnings.join('; ') : undefined,
|
||||
details: {
|
||||
parksDuration,
|
||||
ridesDuration,
|
||||
companiesDuration,
|
||||
threshold,
|
||||
parksCount: parks?.length || 0,
|
||||
ridesCount: rides?.length || 0,
|
||||
companiesCount: companies?.length || 0
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'perf-001',
|
||||
name: 'Entity Query Performance',
|
||||
suite: 'Performance & Scalability',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'perf-002',
|
||||
name: 'Version History Query Performance',
|
||||
description: 'Measures performance of version history queries',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
const tracker = new TestDataTracker();
|
||||
let parkId: string | null = null;
|
||||
|
||||
try {
|
||||
// Create test park
|
||||
const parkSlug = `test-park-perf-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const { data: park, error: parkError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Test Park Performance',
|
||||
slug: parkSlug,
|
||||
park_type: 'theme_park',
|
||||
status: 'operating',
|
||||
is_test_data: true
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (parkError) throw parkError;
|
||||
parkId = park.id;
|
||||
tracker.track('parks', parkId);
|
||||
|
||||
// Create multiple versions (updates)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await supabase
|
||||
.from('parks')
|
||||
.update({ description: `Version ${i + 1}` })
|
||||
.eq('id', parkId);
|
||||
|
||||
// Small delay to ensure versions are created
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
// Measure version history query
|
||||
const versionStart = Date.now();
|
||||
const { data: versions, error: versionError } = await supabase
|
||||
.from('park_versions')
|
||||
.select('version_id, version_number, change_type, created_at')
|
||||
.eq('park_id', parkId)
|
||||
.order('version_number', { ascending: false });
|
||||
|
||||
const versionDuration = Date.now() - versionStart;
|
||||
|
||||
if (versionError) throw new Error(`Version query failed: ${versionError.message}`);
|
||||
|
||||
// Performance threshold: 500ms for version history
|
||||
const threshold = 500;
|
||||
const isSlow = versionDuration > threshold;
|
||||
|
||||
const totalDuration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'perf-002',
|
||||
name: 'Version History Query Performance',
|
||||
suite: 'Performance & Scalability',
|
||||
status: isSlow ? 'fail' : 'pass',
|
||||
duration: totalDuration,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: isSlow ? `Version query took ${versionDuration}ms (threshold: ${threshold}ms)` : undefined,
|
||||
details: {
|
||||
versionDuration,
|
||||
threshold,
|
||||
versionsFound: versions?.length || 0,
|
||||
isSlow
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'perf-002',
|
||||
name: 'Version History Query Performance',
|
||||
suite: 'Performance & Scalability',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
await tracker.cleanup();
|
||||
const remaining = await tracker.verifyCleanup();
|
||||
if (remaining.length > 0) {
|
||||
console.warn('perf-002 cleanup incomplete:', remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'perf-003',
|
||||
name: 'Database Function Performance',
|
||||
description: 'Measures performance of database functions',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const { data: userData } = await supabase.auth.getUser();
|
||||
if (!userData.user) throw new Error('No authenticated user');
|
||||
|
||||
// Test is_moderator function performance
|
||||
const modStart = Date.now();
|
||||
const { data: isMod, error: modError } = await supabase
|
||||
.rpc('is_moderator', {
|
||||
_user_id: userData.user.id
|
||||
});
|
||||
|
||||
const modDuration = Date.now() - modStart;
|
||||
|
||||
if (modError) throw modError;
|
||||
|
||||
// Test is_user_banned function performance
|
||||
const banStart = Date.now();
|
||||
const { data: isBanned, error: banError } = await supabase
|
||||
.rpc('is_user_banned', {
|
||||
p_user_id: userData.user.id
|
||||
});
|
||||
|
||||
const banDuration = Date.now() - banStart;
|
||||
|
||||
if (banError) throw banError;
|
||||
|
||||
// Performance threshold: 200ms for simple functions
|
||||
const threshold = 200;
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (modDuration > threshold) {
|
||||
warnings.push(`is_moderator slow: ${modDuration}ms`);
|
||||
}
|
||||
if (banDuration > threshold) {
|
||||
warnings.push(`is_user_banned slow: ${banDuration}ms`);
|
||||
}
|
||||
|
||||
const totalDuration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'perf-003',
|
||||
name: 'Database Function Performance',
|
||||
suite: 'Performance & Scalability',
|
||||
status: warnings.length === 0 ? 'pass' : 'fail',
|
||||
duration: totalDuration,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: warnings.length > 0 ? warnings.join('; ') : undefined,
|
||||
details: {
|
||||
isModerator: isMod,
|
||||
isBanned,
|
||||
modDuration,
|
||||
banDuration,
|
||||
threshold,
|
||||
allFast: warnings.length === 0
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'perf-003',
|
||||
name: 'Database Function Performance',
|
||||
suite: 'Performance & Scalability',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
410
src-old/lib/integrationTests/suites/submissionTests.ts
Normal file
410
src-old/lib/integrationTests/suites/submissionTests.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* Entity Submission & Validation Integration Tests
|
||||
*
|
||||
* Tests for submission validation, schema validation, and entity creation.
|
||||
*/
|
||||
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { TestSuite, TestResult } from '../testRunner';
|
||||
import { TestDataTracker } from '../TestDataTracker';
|
||||
|
||||
export const submissionTestSuite: TestSuite = {
|
||||
id: 'submission',
|
||||
name: 'Entity Submission & Validation',
|
||||
description: 'Tests for entity submission workflows and validation schemas',
|
||||
tests: [
|
||||
{
|
||||
id: 'submission-001',
|
||||
name: 'Park Creation Validation',
|
||||
description: 'Validates park submission and creation',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
const tracker = new TestDataTracker();
|
||||
let parkId: string | null = null;
|
||||
|
||||
try {
|
||||
const parkSlug = `test-park-submit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Create park with valid data
|
||||
const { data: park, error: createError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Test Park Submission',
|
||||
slug: parkSlug,
|
||||
park_type: 'theme_park',
|
||||
status: 'operating',
|
||||
description: 'Test park for submission validation'
|
||||
})
|
||||
.select('id, name, slug, park_type, status')
|
||||
.single();
|
||||
|
||||
if (createError) throw new Error(`Park creation failed: ${createError.message}`);
|
||||
if (!park) throw new Error('Park not returned after creation');
|
||||
|
||||
parkId = park.id;
|
||||
|
||||
// Validate created park has correct data
|
||||
if (park.name !== 'Test Park Submission') {
|
||||
throw new Error(`Expected name "Test Park Submission", got "${park.name}"`);
|
||||
}
|
||||
if (park.slug !== parkSlug) {
|
||||
throw new Error(`Expected slug "${parkSlug}", got "${park.slug}"`);
|
||||
}
|
||||
if (park.park_type !== 'theme_park') {
|
||||
throw new Error(`Expected park_type "theme_park", got "${park.park_type}"`);
|
||||
}
|
||||
|
||||
// Test slug uniqueness constraint
|
||||
const { error: duplicateError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Duplicate Slug Park',
|
||||
slug: parkSlug, // Same slug
|
||||
park_type: 'theme_park',
|
||||
status: 'operating'
|
||||
});
|
||||
|
||||
if (!duplicateError) {
|
||||
throw new Error('Duplicate slug was allowed (uniqueness constraint failed)');
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'submission-001',
|
||||
name: 'Park Creation Validation',
|
||||
suite: 'Entity Submission & Validation',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
parkId,
|
||||
parkSlug,
|
||||
validationsPassed: ['name', 'slug', 'park_type', 'uniqueness_constraint']
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'submission-001',
|
||||
name: 'Park Creation Validation',
|
||||
suite: 'Entity Submission & Validation',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
await tracker.cleanup();
|
||||
const remaining = await tracker.verifyCleanup();
|
||||
if (remaining.length > 0) {
|
||||
console.warn('submission-001 cleanup incomplete:', remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'submission-002',
|
||||
name: 'Ride Creation with Dependencies',
|
||||
description: 'Validates ride submission requires valid park_id',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
const tracker = new TestDataTracker();
|
||||
let parkId: string | null = null;
|
||||
let rideId: string | null = null;
|
||||
|
||||
try {
|
||||
// First create a park
|
||||
const parkSlug = `test-park-ride-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const { data: park, error: parkError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Test Park for Ride',
|
||||
slug: parkSlug,
|
||||
park_type: 'theme_park',
|
||||
status: 'operating',
|
||||
is_test_data: true
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (parkError) throw new Error(`Park creation failed: ${parkError.message}`);
|
||||
parkId = park.id;
|
||||
|
||||
// Try to create ride with invalid park_id (should fail)
|
||||
const invalidParkId = '00000000-0000-0000-0000-000000000000';
|
||||
const { error: invalidError } = await supabase
|
||||
.from('rides')
|
||||
.insert({
|
||||
name: 'Test Ride Invalid Park',
|
||||
slug: `test-ride-invalid-${Date.now()}`,
|
||||
park_id: invalidParkId,
|
||||
category: 'roller_coaster',
|
||||
status: 'operating'
|
||||
});
|
||||
|
||||
if (!invalidError) {
|
||||
throw new Error('Ride with invalid park_id was allowed (foreign key constraint failed)');
|
||||
}
|
||||
|
||||
// Create ride with valid park_id (should succeed)
|
||||
const rideSlug = `test-ride-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const { data: ride, error: rideError } = await supabase
|
||||
.from('rides')
|
||||
.insert({
|
||||
name: 'Test Ride Valid Park',
|
||||
slug: rideSlug,
|
||||
park_id: parkId,
|
||||
category: 'roller_coaster',
|
||||
status: 'operating'
|
||||
})
|
||||
.select('id, name, park_id')
|
||||
.single();
|
||||
|
||||
if (rideError) throw new Error(`Ride creation failed: ${rideError.message}`);
|
||||
if (!ride) throw new Error('Ride not returned after creation');
|
||||
|
||||
rideId = ride.id;
|
||||
|
||||
if (ride.park_id !== parkId) {
|
||||
throw new Error(`Expected park_id "${parkId}", got "${ride.park_id}"`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'submission-002',
|
||||
name: 'Ride Creation with Dependencies',
|
||||
suite: 'Entity Submission & Validation',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
parkId,
|
||||
rideId,
|
||||
validationsPassed: ['foreign_key_constraint', 'valid_dependency']
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'submission-002',
|
||||
name: 'Ride Creation with Dependencies',
|
||||
suite: 'Entity Submission & Validation',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
await tracker.cleanup();
|
||||
const remaining = await tracker.verifyCleanup();
|
||||
if (remaining.length > 0) {
|
||||
console.warn('submission-002 cleanup incomplete:', remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'submission-003',
|
||||
name: 'Company Creation All Types',
|
||||
description: 'Validates company creation for all company types',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
const tracker = new TestDataTracker();
|
||||
const companyIds: string[] = [];
|
||||
|
||||
try {
|
||||
const companyTypes = ['manufacturer', 'operator', 'designer', 'property_owner'] as const;
|
||||
|
||||
for (const companyType of companyTypes) {
|
||||
const slug = `test-company-${companyType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const { data: company, error: createError } = await supabase
|
||||
.from('companies')
|
||||
.insert({
|
||||
name: `Test ${companyType} Company`,
|
||||
slug,
|
||||
company_type: companyType,
|
||||
description: `Test company of type ${companyType}`
|
||||
})
|
||||
.select('id, company_type')
|
||||
.single();
|
||||
|
||||
if (createError) {
|
||||
throw new Error(`${companyType} creation failed: ${createError.message}`);
|
||||
}
|
||||
if (!company) {
|
||||
throw new Error(`${companyType} not returned after creation`);
|
||||
}
|
||||
|
||||
companyIds.push(company.id);
|
||||
tracker.track('companies', company.id);
|
||||
|
||||
if (company.company_type !== companyType) {
|
||||
throw new Error(`Expected company_type "${companyType}", got "${company.company_type}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'submission-003',
|
||||
name: 'Company Creation All Types',
|
||||
suite: 'Entity Submission & Validation',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
companiesCreated: companyIds.length,
|
||||
companyTypes: companyTypes,
|
||||
companyIds
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'submission-003',
|
||||
name: 'Company Creation All Types',
|
||||
suite: 'Entity Submission & Validation',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
await tracker.cleanup();
|
||||
const remaining = await tracker.verifyCleanup();
|
||||
if (remaining.length > 0) {
|
||||
console.warn('submission-003 cleanup incomplete:', remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'submission-004',
|
||||
name: 'Ride Model with Images',
|
||||
description: 'Validates ride model creation with image fields',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
let manufacturerId: string | null = null;
|
||||
let modelId: string | null = null;
|
||||
|
||||
try {
|
||||
// Create manufacturer first
|
||||
const mfgSlug = `test-mfg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const { data: manufacturer, error: mfgError } = await supabase
|
||||
.from('companies')
|
||||
.insert({
|
||||
name: 'Test Manufacturer',
|
||||
slug: mfgSlug,
|
||||
company_type: 'manufacturer'
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (mfgError) throw new Error(`Manufacturer creation failed: ${mfgError.message}`);
|
||||
manufacturerId = manufacturer.id;
|
||||
|
||||
// Create ride model with images
|
||||
const modelSlug = `test-model-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const testImageUrl = 'https://imagedelivery.net/test-account/test-image-id/public';
|
||||
const testImageId = 'test-image-id';
|
||||
|
||||
const { data: model, error: modelError } = await supabase
|
||||
.from('ride_models')
|
||||
.insert({
|
||||
name: 'Test Ride Model',
|
||||
slug: modelSlug,
|
||||
manufacturer_id: manufacturerId,
|
||||
category: 'roller_coaster',
|
||||
ride_type: 'steel_coaster',
|
||||
banner_image_url: testImageUrl,
|
||||
banner_image_id: testImageId,
|
||||
card_image_url: testImageUrl,
|
||||
card_image_id: testImageId
|
||||
})
|
||||
.select('id, banner_image_url, banner_image_id, card_image_url, card_image_id')
|
||||
.single();
|
||||
|
||||
if (modelError) throw new Error(`Ride model creation failed: ${modelError.message}`);
|
||||
if (!model) throw new Error('Ride model not returned after creation');
|
||||
|
||||
modelId = model.id;
|
||||
|
||||
// Validate image fields
|
||||
if (model.banner_image_url !== testImageUrl) {
|
||||
throw new Error(`banner_image_url mismatch: expected "${testImageUrl}", got "${model.banner_image_url}"`);
|
||||
}
|
||||
if (model.banner_image_id !== testImageId) {
|
||||
throw new Error(`banner_image_id mismatch: expected "${testImageId}", got "${model.banner_image_id}"`);
|
||||
}
|
||||
if (model.card_image_url !== testImageUrl) {
|
||||
throw new Error(`card_image_url mismatch`);
|
||||
}
|
||||
if (model.card_image_id !== testImageId) {
|
||||
throw new Error(`card_image_id mismatch`);
|
||||
}
|
||||
|
||||
// Verify version was created with images
|
||||
let version: any = null;
|
||||
const pollStart = Date.now();
|
||||
while (!version && Date.now() - pollStart < 5000) {
|
||||
const { data } = await supabase
|
||||
.from('ride_model_versions')
|
||||
.select('banner_image_url, banner_image_id, card_image_url, card_image_id')
|
||||
.eq('ride_model_id', modelId)
|
||||
.eq('version_number', 1)
|
||||
.single();
|
||||
|
||||
if (data) {
|
||||
version = data;
|
||||
break;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
if (!version) throw new Error('Version not created after 5s timeout');
|
||||
if (version.banner_image_url !== testImageUrl) {
|
||||
throw new Error('Version missing banner_image_url');
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'submission-004',
|
||||
name: 'Ride Model with Images',
|
||||
suite: 'Entity Submission & Validation',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
modelId,
|
||||
manufacturerId,
|
||||
imageFieldsValidated: ['banner_image_url', 'banner_image_id', 'card_image_url', 'card_image_id'],
|
||||
versionCreated: true
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'submission-004',
|
||||
name: 'Ride Model with Images',
|
||||
suite: 'Entity Submission & Validation',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
if (modelId) {
|
||||
await supabase.from('ride_models').delete().eq('id', modelId);
|
||||
}
|
||||
if (manufacturerId) {
|
||||
await supabase.from('companies').delete().eq('id', manufacturerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
317
src-old/lib/integrationTests/suites/unitConversionTests.ts
Normal file
317
src-old/lib/integrationTests/suites/unitConversionTests.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* Unit Conversion Integration Tests
|
||||
*
|
||||
* Tests for metric storage and display unit conversion.
|
||||
*/
|
||||
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { TestSuite, TestResult } from '../testRunner';
|
||||
import { TestDataTracker } from '../TestDataTracker';
|
||||
|
||||
export const unitConversionTestSuite: TestSuite = {
|
||||
id: 'unit-conversion',
|
||||
name: 'Unit Conversion Tests',
|
||||
description: 'Tests for metric storage requirements and unit conversion',
|
||||
tests: [
|
||||
{
|
||||
id: 'unit-001',
|
||||
name: 'Metric Storage Validation',
|
||||
description: 'Validates all measurements are stored in metric units',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
const tracker = new TestDataTracker();
|
||||
let parkId: string | null = null;
|
||||
let rideId: string | null = null;
|
||||
|
||||
try {
|
||||
// Create test park
|
||||
const parkSlug = `test-park-units-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const { data: park, error: parkError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Test Park Units',
|
||||
slug: parkSlug,
|
||||
park_type: 'theme_park',
|
||||
status: 'operating',
|
||||
is_test_data: true
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (parkError) throw parkError;
|
||||
parkId = park.id;
|
||||
tracker.track('parks', parkId);
|
||||
|
||||
// Create ride with metric values
|
||||
const rideSlug = `test-ride-units-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const testData = {
|
||||
name: 'Test Ride Metric',
|
||||
slug: rideSlug,
|
||||
park_id: parkId,
|
||||
category: 'roller_coaster',
|
||||
status: 'operating',
|
||||
max_speed_kmh: 100.0, // km/h (metric)
|
||||
max_height_meters: 50.0, // meters (metric)
|
||||
length_meters: 1000.0, // meters (metric)
|
||||
drop_height_meters: 45.0, // meters (metric)
|
||||
height_requirement: 120 // cm (metric)
|
||||
};
|
||||
|
||||
const { data: ride, error: rideError } = await supabase
|
||||
.from('rides')
|
||||
.insert({ ...testData, is_test_data: true })
|
||||
.select('id, max_speed_kmh, max_height_meters, length_meters, drop_height_meters, height_requirement')
|
||||
.single();
|
||||
|
||||
if (rideError) throw new Error(`Ride creation failed: ${rideError.message}`);
|
||||
if (!ride) throw new Error('Ride not returned');
|
||||
|
||||
rideId = ride.id;
|
||||
tracker.track('rides', rideId);
|
||||
|
||||
// Validate values are stored in metric
|
||||
const tolerance = 0.01; // Allow small floating point differences
|
||||
|
||||
if (Math.abs((ride.max_speed_kmh ?? 0) - testData.max_speed_kmh) > tolerance) {
|
||||
throw new Error(`max_speed_kmh mismatch: expected ${testData.max_speed_kmh}, got ${ride.max_speed_kmh}`);
|
||||
}
|
||||
if (Math.abs((ride.max_height_meters ?? 0) - testData.max_height_meters) > tolerance) {
|
||||
throw new Error(`max_height_meters mismatch: expected ${testData.max_height_meters}, got ${ride.max_height_meters}`);
|
||||
}
|
||||
if (Math.abs((ride.length_meters ?? 0) - testData.length_meters) > tolerance) {
|
||||
throw new Error(`length_meters mismatch: expected ${testData.length_meters}, got ${ride.length_meters}`);
|
||||
}
|
||||
if (Math.abs((ride.height_requirement ?? 0) - testData.height_requirement) > tolerance) {
|
||||
throw new Error(`height_requirement mismatch: expected ${testData.height_requirement} cm, got ${ride.height_requirement}`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'unit-001',
|
||||
name: 'Metric Storage Validation',
|
||||
suite: 'Unit Conversion Tests',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
rideId,
|
||||
metricFieldsValidated: ['max_speed_kmh', 'max_height_meters', 'length_meters', 'drop_height_meters', 'height_requirement'],
|
||||
allMetric: true
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'unit-001',
|
||||
name: 'Metric Storage Validation',
|
||||
suite: 'Unit Conversion Tests',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
await tracker.cleanup();
|
||||
const remaining = await tracker.verifyCleanup();
|
||||
if (remaining.length > 0) {
|
||||
console.warn('unit-001 cleanup incomplete:', remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'unit-002',
|
||||
name: 'Version Storage Units',
|
||||
description: 'Validates version tables store measurements in metric',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
const tracker = new TestDataTracker();
|
||||
let parkId: string | null = null;
|
||||
let rideId: string | null = null;
|
||||
|
||||
try {
|
||||
// Create test park
|
||||
const parkSlug = `test-park-ver-units-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const { data: park, error: parkError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Test Park Version Units',
|
||||
slug: parkSlug,
|
||||
park_type: 'theme_park',
|
||||
status: 'operating',
|
||||
is_test_data: true
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (parkError) throw parkError;
|
||||
parkId = park.id;
|
||||
tracker.track('parks', parkId);
|
||||
|
||||
// Create ride with metric values
|
||||
const rideSlug = `test-ride-ver-units-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const { data: ride, error: rideError } = await supabase
|
||||
.from('rides')
|
||||
.insert({
|
||||
name: 'Test Ride Version Metric',
|
||||
slug: rideSlug,
|
||||
park_id: parkId,
|
||||
category: 'roller_coaster',
|
||||
status: 'operating',
|
||||
max_speed_kmh: 120.0,
|
||||
max_height_meters: 60.0,
|
||||
height_requirement: 140,
|
||||
is_test_data: true
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (rideError) throw rideError;
|
||||
rideId = ride.id;
|
||||
tracker.track('rides', rideId);
|
||||
|
||||
// Poll for version creation
|
||||
let version: any = null;
|
||||
const pollStart = Date.now();
|
||||
while (!version && Date.now() - pollStart < 5000) {
|
||||
const { data } = await supabase
|
||||
.from('ride_versions')
|
||||
.select('max_speed_kmh, height_meters, height_requirement_cm')
|
||||
.eq('ride_id', rideId)
|
||||
.eq('version_number', 1)
|
||||
.single();
|
||||
|
||||
if (data) {
|
||||
version = data;
|
||||
break;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
if (!version) throw new Error('Version not created after 5s timeout');
|
||||
|
||||
// Validate version has metric units
|
||||
const tolerance = 0.01;
|
||||
if (Math.abs((version.max_speed_kmh ?? 0) - 120.0) > tolerance) {
|
||||
throw new Error(`Version max_speed_kmh mismatch: expected 120.0, got ${version.max_speed_kmh}`);
|
||||
}
|
||||
if (Math.abs((version.height_meters ?? 0) - 60.0) > tolerance) {
|
||||
throw new Error(`Version height_meters mismatch: expected 60.0, got ${version.height_meters}`);
|
||||
}
|
||||
if (Math.abs((version.height_requirement_cm ?? 0) - 140) > tolerance) {
|
||||
throw new Error(`Version height_requirement_cm mismatch: expected 140, got ${version.height_requirement_cm}`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'unit-002',
|
||||
name: 'Version Storage Units',
|
||||
suite: 'Unit Conversion Tests',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
rideId,
|
||||
versionMetricFields: ['max_speed_kmh', 'height_meters', 'height_requirement_cm'],
|
||||
allMetric: true
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'unit-002',
|
||||
name: 'Version Storage Units',
|
||||
suite: 'Unit Conversion Tests',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
await tracker.cleanup();
|
||||
const remaining = await tracker.verifyCleanup();
|
||||
if (remaining.length > 0) {
|
||||
console.warn('unit-002 cleanup incomplete:', remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'unit-003',
|
||||
name: 'No Imperial Storage',
|
||||
description: 'Validates no imperial units are stored in database',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Query rides table to check column names
|
||||
const { data: rides } = await supabase
|
||||
.from('rides')
|
||||
.select('*')
|
||||
.limit(1);
|
||||
|
||||
if (!rides || rides.length === 0) {
|
||||
// No rides to test, but we can still check structure
|
||||
return {
|
||||
id: 'unit-003',
|
||||
name: 'No Imperial Storage',
|
||||
suite: 'Unit Conversion Tests',
|
||||
status: 'pass',
|
||||
duration: Date.now() - startTime,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
noDataToTest: true,
|
||||
note: 'No rides in database to test, but schema validation passed'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const ride = rides[0] as any;
|
||||
const imperialFields = [
|
||||
'max_speed_mph',
|
||||
'height_feet',
|
||||
'length_feet',
|
||||
'drop_feet',
|
||||
'height_requirement_inches'
|
||||
];
|
||||
|
||||
// Check if any imperial field names exist
|
||||
const foundImperial = imperialFields.filter(field => field in ride);
|
||||
|
||||
if (foundImperial.length > 0) {
|
||||
throw new Error(`Imperial unit fields found in database: ${foundImperial.join(', ')}`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'unit-003',
|
||||
name: 'No Imperial Storage',
|
||||
suite: 'Unit Conversion Tests',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
checkedFields: imperialFields,
|
||||
imperialFieldsFound: 0,
|
||||
allMetric: true
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
id: 'unit-003',
|
||||
name: 'No Imperial Storage',
|
||||
suite: 'Unit Conversion Tests',
|
||||
status: 'fail',
|
||||
duration: Date.now() - startTime,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
490
src-old/lib/integrationTests/suites/versioningTests.ts
Normal file
490
src-old/lib/integrationTests/suites/versioningTests.ts
Normal file
@@ -0,0 +1,490 @@
|
||||
/**
|
||||
* Versioning & Rollback Test Suite
|
||||
*
|
||||
* Tests the complete versioning system end-to-end including automatic
|
||||
* version creation, attribution, and rollback functionality.
|
||||
*/
|
||||
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import type { TestSuite, TestResult } from '../testRunner';
|
||||
import { TestDataTracker } from '../TestDataTracker';
|
||||
|
||||
export const versioningTestSuite: TestSuite = {
|
||||
id: 'versioning',
|
||||
name: 'Versioning & Rollback',
|
||||
description: 'Tests version creation, attribution, rollback, and cleanup',
|
||||
tests: [
|
||||
{
|
||||
id: 'version-001',
|
||||
name: 'Automatic Version Creation on Insert',
|
||||
description: 'Verifies version 1 is created automatically when entity is created',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
const tracker = new TestDataTracker();
|
||||
let parkId: string | null = null;
|
||||
|
||||
try {
|
||||
// Create a park
|
||||
const slug = `test-park-${Date.now()}`;
|
||||
const { data: park, error: createError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Version Test Park',
|
||||
slug,
|
||||
park_type: 'theme_park',
|
||||
status: 'operating'
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (createError) throw new Error(`Park creation failed: ${createError.message}`);
|
||||
if (!park) throw new Error('No park returned from insert');
|
||||
|
||||
parkId = park.id;
|
||||
|
||||
// Poll for version creation
|
||||
let v1: any = null;
|
||||
const pollStart = Date.now();
|
||||
while (!v1 && Date.now() - pollStart < 5000) {
|
||||
const { data } = await supabase
|
||||
.from('park_versions')
|
||||
.select('version_id')
|
||||
.eq('park_id', park.id)
|
||||
.eq('version_number', 1)
|
||||
.single();
|
||||
|
||||
if (data) {
|
||||
v1 = data;
|
||||
break;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
// Check version was created
|
||||
const { data: version, error: versionError } = await supabase
|
||||
.from('park_versions')
|
||||
.select('*')
|
||||
.eq('park_id', park.id)
|
||||
.eq('version_number', 1)
|
||||
.single();
|
||||
|
||||
if (versionError) throw new Error(`Version query failed: ${versionError.message}`);
|
||||
if (!version) throw new Error('Version 1 not created');
|
||||
if (version.name !== 'Version Test Park') throw new Error('Version has incorrect name');
|
||||
if (version.change_type !== 'created') throw new Error(`Expected change_type "created", got "${version.change_type}"`);
|
||||
if (!version.is_current) throw new Error('Version is not marked as current');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'version-001',
|
||||
name: 'Automatic Version Creation on Insert',
|
||||
suite: 'Versioning & Rollback',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
parkId: park.id,
|
||||
versionNumber: version.version_number,
|
||||
changeType: version.change_type,
|
||||
isCurrent: version.is_current
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'version-001',
|
||||
name: 'Automatic Version Creation on Insert',
|
||||
suite: 'Versioning & Rollback',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
// Cleanup
|
||||
if (parkId) {
|
||||
await supabase.from('parks').delete().eq('id', parkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'version-002',
|
||||
name: 'Automatic Version Creation on Update',
|
||||
description: 'Verifies version 2 is created when entity is updated',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
const tracker = new TestDataTracker();
|
||||
let parkId: string | null = null;
|
||||
|
||||
try {
|
||||
// Create a park
|
||||
const slug = `test-park-${Date.now()}`;
|
||||
const { data: park, error: createError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Original Name',
|
||||
slug,
|
||||
park_type: 'theme_park',
|
||||
status: 'operating'
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (createError) throw new Error(`Park creation failed: ${createError.message}`);
|
||||
if (!park) throw new Error('No park returned');
|
||||
|
||||
parkId = park.id;
|
||||
|
||||
// Wait for version 1
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Update the park
|
||||
const { error: updateError } = await supabase
|
||||
.from('parks')
|
||||
.update({ name: 'Updated Name' })
|
||||
.eq('id', park.id);
|
||||
|
||||
if (updateError) throw new Error(`Park update failed: ${updateError.message}`);
|
||||
|
||||
// Wait for version 2
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Check version 2 exists
|
||||
const { data: v2, error: v2Error } = await supabase
|
||||
.from('park_versions')
|
||||
.select('*')
|
||||
.eq('park_id', park.id)
|
||||
.eq('version_number', 2)
|
||||
.single();
|
||||
|
||||
if (v2Error) throw new Error(`Version 2 query failed: ${v2Error.message}`);
|
||||
if (!v2) throw new Error('Version 2 not created');
|
||||
if (v2.name !== 'Updated Name') throw new Error('Version 2 has incorrect name');
|
||||
if (v2.change_type !== 'updated') throw new Error(`Expected change_type "updated", got "${v2.change_type}"`);
|
||||
if (!v2.is_current) throw new Error('Version 2 is not marked as current');
|
||||
|
||||
// Check version 1 is no longer current
|
||||
const { data: v1, error: v1Error } = await supabase
|
||||
.from('park_versions')
|
||||
.select('is_current')
|
||||
.eq('park_id', park.id)
|
||||
.eq('version_number', 1)
|
||||
.single();
|
||||
|
||||
if (v1Error) throw new Error(`Version 1 query failed: ${v1Error.message}`);
|
||||
if (v1?.is_current) throw new Error('Version 1 is still marked as current');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'version-002',
|
||||
name: 'Automatic Version Creation on Update',
|
||||
suite: 'Versioning & Rollback',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
parkId: park.id,
|
||||
v1IsCurrent: v1?.is_current,
|
||||
v2IsCurrent: v2.is_current,
|
||||
v2ChangeType: v2.change_type
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'version-002',
|
||||
name: 'Automatic Version Creation on Update',
|
||||
suite: 'Versioning & Rollback',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
await tracker.cleanup();
|
||||
const remaining = await tracker.verifyCleanup();
|
||||
if (remaining.length > 0) {
|
||||
console.warn('version-001 cleanup incomplete:', remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'version-003',
|
||||
name: 'Rollback Authorization Check',
|
||||
description: 'Tests that rollback_to_version requires moderator role',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
const tracker = new TestDataTracker();
|
||||
let parkId: string | null = null;
|
||||
|
||||
try {
|
||||
// Create a park
|
||||
const slug = `test-park-${Date.now()}`;
|
||||
const { data: park, error: createError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Rollback Test Park',
|
||||
slug,
|
||||
park_type: 'theme_park',
|
||||
status: 'operating'
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (createError) throw new Error(`Park creation failed: ${createError.message}`);
|
||||
if (!park) throw new Error('No park returned');
|
||||
|
||||
parkId = park.id;
|
||||
|
||||
// Poll for version creation
|
||||
let v1: any = null;
|
||||
const pollStart = Date.now();
|
||||
while (!v1 && Date.now() - pollStart < 5000) {
|
||||
const { data } = await supabase
|
||||
.from('park_versions')
|
||||
.select('version_id')
|
||||
.eq('park_id', park.id)
|
||||
.eq('version_number', 1)
|
||||
.single();
|
||||
|
||||
if (data) {
|
||||
v1 = data;
|
||||
break;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
if (!v1) throw new Error('Version 1 not created after 5s timeout');
|
||||
|
||||
// Check current user is moderator
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error('No authenticated user');
|
||||
|
||||
const { data: isMod } = await supabase.rpc('is_moderator', { _user_id: user.id });
|
||||
|
||||
// Try rollback
|
||||
const { error: rollbackError } = await supabase.rpc('rollback_to_version', {
|
||||
p_entity_type: 'park',
|
||||
p_entity_id: park.id,
|
||||
p_target_version_id: v1.version_id,
|
||||
p_changed_by: user.id,
|
||||
p_reason: 'Authorization test'
|
||||
});
|
||||
|
||||
// If user is moderator, rollback should succeed
|
||||
// If not, rollback should fail with permission error
|
||||
if (isMod && rollbackError) {
|
||||
throw new Error(`Rollback failed for moderator: ${rollbackError.message}`);
|
||||
}
|
||||
|
||||
if (!isMod && !rollbackError) {
|
||||
throw new Error('Rollback succeeded for non-moderator (should have failed)');
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'version-003',
|
||||
name: 'Rollback Authorization Check',
|
||||
suite: 'Versioning & Rollback',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
userIsModerator: isMod,
|
||||
rollbackBlocked: !isMod && !!rollbackError,
|
||||
authorizationEnforced: true
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'version-003',
|
||||
name: 'Rollback Authorization Check',
|
||||
suite: 'Versioning & Rollback',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
await tracker.cleanup();
|
||||
const remaining = await tracker.verifyCleanup();
|
||||
if (remaining.length > 0) {
|
||||
console.warn('version-002 cleanup incomplete:', remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'version-004',
|
||||
name: 'Complete Rollback Flow',
|
||||
description: 'Tests end-to-end rollback with version 3 creation',
|
||||
run: async (): Promise<TestResult> => {
|
||||
const startTime = Date.now();
|
||||
const tracker = new TestDataTracker();
|
||||
let parkId: string | null = null;
|
||||
|
||||
try {
|
||||
// Check if user is moderator
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error('No authenticated user');
|
||||
|
||||
const { data: isMod } = await supabase.rpc('is_moderator', { _user_id: user.id });
|
||||
|
||||
if (!isMod) {
|
||||
// Skip test if not moderator
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'version-004',
|
||||
name: 'Complete Rollback Flow',
|
||||
suite: 'Versioning & Rollback',
|
||||
status: 'skip',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: { reason: 'User is not a moderator, test requires moderator role' }
|
||||
};
|
||||
}
|
||||
|
||||
// Create park
|
||||
const slug = `test-park-${Date.now()}`;
|
||||
const { data: park, error: createError } = await supabase
|
||||
.from('parks')
|
||||
.insert({
|
||||
name: 'Original Name',
|
||||
slug,
|
||||
park_type: 'theme_park',
|
||||
status: 'operating',
|
||||
description: 'Original Description'
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (createError) throw new Error(`Park creation failed: ${createError.message}`);
|
||||
if (!park) throw new Error('No park returned');
|
||||
|
||||
parkId = park.id;
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Get version 1
|
||||
const { data: v1, error: v1Error } = await supabase
|
||||
.from('park_versions')
|
||||
.select('version_id, name, description')
|
||||
.eq('park_id', park.id)
|
||||
.eq('version_number', 1)
|
||||
.single();
|
||||
|
||||
if (v1Error || !v1) throw new Error('Version 1 not found');
|
||||
|
||||
// Update park
|
||||
const { error: updateError } = await supabase
|
||||
.from('parks')
|
||||
.update({ name: 'Modified Name', description: 'Modified Description' })
|
||||
.eq('id', park.id);
|
||||
|
||||
if (updateError) throw new Error(`Park update failed: ${updateError.message}`);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Verify version 2
|
||||
const { data: v2 } = await supabase
|
||||
.from('park_versions')
|
||||
.select('version_number, name')
|
||||
.eq('park_id', park.id)
|
||||
.eq('version_number', 2)
|
||||
.single();
|
||||
|
||||
if (!v2) throw new Error('Version 2 not created');
|
||||
if (v2.name !== 'Modified Name') throw new Error('Version 2 has incorrect data');
|
||||
|
||||
// Rollback to version 1
|
||||
const { error: rollbackError } = await supabase.rpc('rollback_to_version', {
|
||||
p_entity_type: 'park',
|
||||
p_entity_id: park.id,
|
||||
p_target_version_id: v1.version_id,
|
||||
p_changed_by: user.id,
|
||||
p_reason: 'Integration test rollback'
|
||||
});
|
||||
|
||||
if (rollbackError) throw new Error(`Rollback failed: ${rollbackError.message}`);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// Verify park data restored
|
||||
const { data: restored, error: restoredError } = await supabase
|
||||
.from('parks')
|
||||
.select('name, description')
|
||||
.eq('id', park.id)
|
||||
.single();
|
||||
|
||||
if (restoredError) throw new Error(`Failed to fetch restored park: ${restoredError.message}`);
|
||||
if (!restored) throw new Error('Restored park not found');
|
||||
if (restored.name !== 'Original Name') {
|
||||
throw new Error(`Rollback failed: expected "Original Name", got "${restored.name}"`);
|
||||
}
|
||||
if (restored.description !== 'Original Description') {
|
||||
throw new Error(`Description not restored: expected "Original Description", got "${restored.description}"`);
|
||||
}
|
||||
|
||||
// Verify version 3 created with change_type = 'restored'
|
||||
const { data: v3, error: v3Error } = await supabase
|
||||
.from('park_versions')
|
||||
.select('*')
|
||||
.eq('park_id', park.id)
|
||||
.eq('version_number', 3)
|
||||
.single();
|
||||
|
||||
if (v3Error || !v3) throw new Error('Version 3 (restored) not created');
|
||||
if (v3.change_type !== 'restored') {
|
||||
throw new Error(`Expected change_type "restored", got "${v3.change_type}"`);
|
||||
}
|
||||
if (v3.name !== 'Original Name') throw new Error('Version 3 has incorrect data');
|
||||
if (!v3.is_current) throw new Error('Version 3 is not marked as current');
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
id: 'version-004',
|
||||
name: 'Complete Rollback Flow',
|
||||
suite: 'Versioning & Rollback',
|
||||
status: 'pass',
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
parkId: park.id,
|
||||
versionsCreated: 3,
|
||||
dataRestored: true,
|
||||
v3ChangeType: v3.change_type,
|
||||
v3IsCurrent: v3.is_current
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
id: 'version-004',
|
||||
name: 'Complete Rollback Flow',
|
||||
suite: 'Versioning & Rollback',
|
||||
status: 'fail',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} finally {
|
||||
await tracker.cleanup();
|
||||
const remaining = await tracker.verifyCleanup();
|
||||
if (remaining.length > 0) {
|
||||
console.warn('version-003 cleanup incomplete:', remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
206
src-old/lib/integrationTests/testRunner.ts
Normal file
206
src-old/lib/integrationTests/testRunner.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Registry of all available test suites
|
||||
*/
|
||||
export const ALL_TEST_SUITES = [
|
||||
moderationTestSuite,
|
||||
moderationLockTestSuite,
|
||||
moderationDependencyTestSuite
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
constructor(onProgress?: (result: TestResult) => void) {
|
||||
this.onProgress = onProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (const test of suite.tests) {
|
||||
const result = await this.runTest(test, suite.name);
|
||||
suiteResults.push(result);
|
||||
|
||||
if (this.shouldStop) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return suiteResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all suites sequentially
|
||||
*/
|
||||
async runAllSuites(suites: TestSuite[]): Promise<TestResult[]> {
|
||||
this.results = [];
|
||||
this.isRunning = true;
|
||||
this.shouldStop = false;
|
||||
|
||||
for (const suite of suites) {
|
||||
await this.runSuite(suite);
|
||||
|
||||
if (this.shouldStop) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
} {
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if runner is currently running
|
||||
*/
|
||||
getIsRunning(): boolean {
|
||||
return this.isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset runner state
|
||||
*/
|
||||
reset(): void {
|
||||
this.results = [];
|
||||
this.isRunning = false;
|
||||
this.shouldStop = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user