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