Files
thrilltrack-explorer/src-old/lib/integrationTests/suites/dataIntegrityTests.ts

301 lines
11 KiB
TypeScript

/**
* 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()
};
}
}
}
]
};