Implement integration testing system

This commit is contained in:
gpt-engineer-app[bot]
2025-10-30 14:35:09 +00:00
parent c0e59c2758
commit 0c34ae1075
8 changed files with 1523 additions and 2 deletions

View 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';

View File

@@ -0,0 +1,259 @@
/**
* Authentication & Authorization Test Suite
*
* Tests auth flows, MFA enforcement, role checks, and session management.
*/
import { supabase } from '@/integrations/supabase/client';
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', { _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()
};
}
}
}
]
};

View File

@@ -0,0 +1,292 @@
/**
* Data Integrity & Constraints Test Suite
*
* Tests database constraints, RLS policies, and data integrity rules.
*/
import { supabase } from '@/integrations/supabase/client';
import type { TestSuite, TestResult } from '../testRunner';
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'
});
// 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();
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'
})
.select('id')
.single();
if (createError) throw new Error(`Park creation failed: ${createError.message}`);
if (!park) throw new Error('No park returned');
parkId = park.id;
// 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'
});
// 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 {
if (parkId) {
await supabase.from('parks').delete().eq('id', parkId);
}
}
}
},
{
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()
};
}
}
}
]
};

View File

@@ -0,0 +1,22 @@
/**
* 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 type { TestSuite } from '../testRunner';
export const allTestSuites: TestSuite[] = [
authTestSuite,
versioningTestSuite,
dataIntegrityTestSuite,
];
export {
authTestSuite,
versioningTestSuite,
dataIntegrityTestSuite,
};

View File

@@ -0,0 +1,456 @@
/**
* Versioning & Rollback Test Suite
*
* Tests the complete versioning system end-to-end including automatic
* version creation, attribution, and rollback functionality.
*/
import { supabase } from '@/integrations/supabase/client';
import type { TestSuite, TestResult } from '../testRunner';
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();
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;
// Wait a bit for trigger to execute
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();
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 {
if (parkId) {
await supabase.from('parks').delete().eq('id', parkId);
}
}
}
},
{
id: 'version-003',
name: 'Rollback Authorization Check',
description: 'Tests that rollback_to_version requires moderator role',
run: async (): Promise<TestResult> => {
const startTime = Date.now();
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;
await new Promise(resolve => setTimeout(resolve, 100));
// Get version 1 ID
const { data: v1, error: v1Error } = await supabase
.from('park_versions')
.select('version_id')
.eq('park_id', park.id)
.eq('version_number', 1)
.single();
if (v1Error || !v1) throw new Error('Version 1 not found');
// 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 {
if (parkId) {
await supabase.from('parks').delete().eq('id', parkId);
}
}
}
},
{
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();
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 {
if (parkId) {
await supabase.from('parks').delete().eq('id', parkId);
}
}
}
}
]
};

View File

@@ -0,0 +1,193 @@
/**
* Integration Test Runner
*
* Core infrastructure for running comprehensive integration tests.
* Tests run against real database functions, edge functions, and API endpoints.
*/
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;
}
}