mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 16:51:13 -05:00
301 lines
11 KiB
TypeScript
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()
|
|
};
|
|
}
|
|
}
|
|
}
|
|
]
|
|
};
|