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