/** * Versioning & Rollback Test Suite * * Tests the complete versioning system end-to-end including automatic * version creation, attribution, and rollback functionality. * * All tests follow the sacred pipeline: submitParkCreation → approve → verify versioning */ import { supabase } from '@/lib/supabaseClient'; import type { TestSuite, TestResult } from '../testRunner'; import { TestDataTracker } from '../TestDataTracker'; import { formatTestError } from '../formatTestError'; import { generateUniqueParkData, createTestParkSubmission, approveSubmission, pollForEntity, pollForVersion, getAuthToken, getCurrentUserId, } from '../helpers/approvalTestHelpers'; export const versioningTestSuite: TestSuite = { id: 'versioning', name: 'Versioning & Rollback', description: 'Tests version creation, attribution, rollback, and cleanup via sacred pipeline', tests: [ { id: 'version-001', name: 'Automatic Version Creation on Insert', description: 'Verifies version 1 is created automatically when entity is approved', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { // Follow sacred pipeline: Form → Submission → Approval → Versioning const userId = await getCurrentUserId(); const authToken = await getAuthToken(); const parkData = generateUniqueParkData('version-001'); // Create submission const { submissionId, itemId } = await createTestParkSubmission(parkData, userId, tracker); // Approve submission const approval = await approveSubmission(submissionId, [itemId], authToken); if (!approval.success) { throw new Error(`Approval failed: ${approval.error}`); } // Get approved entity ID const { data: item } = await supabase .from('submission_items') .select('approved_entity_id') .eq('id', itemId) .single(); if (!item?.approved_entity_id) { throw new Error('No entity ID returned after approval'); } const parkId = item.approved_entity_id; tracker.track('parks', parkId); // Poll for park entity const park = await pollForEntity('parks', parkId); if (!park) throw new Error('Park not created after approval'); // Verify version 1 was created automatically const version = await pollForVersion('park', parkId, 1); if (!version) throw new Error('Version 1 not created'); if (version.name !== parkData.name) { throw new Error(`Version has incorrect name: expected "${parkData.name}", got "${version.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, submissionId, versionNumber: version.version_number, changeType: version.change_type, isCurrent: version.is_current, followedPipeline: true } }; } catch (error) { const duration = Date.now() - startTime; return { id: 'version-001', name: 'Automatic Version Creation on Insert', suite: 'Versioning & Rollback', status: 'fail', duration, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString() }; } finally { await tracker.cleanup(); } } }, { id: 'version-002', name: 'Automatic Version Creation on Update', description: 'Verifies version 2 is created when entity is updated via pipeline', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { // Create and approve initial park const userId = await getCurrentUserId(); const authToken = await getAuthToken(); const parkData = generateUniqueParkData('version-002'); const { submissionId, itemId } = await createTestParkSubmission(parkData, userId, tracker); const approval = await approveSubmission(submissionId, [itemId], authToken); if (!approval.success) { throw new Error(`Initial approval failed: ${approval.error}`); } // Get park ID const { data: item } = await supabase .from('submission_items') .select('approved_entity_id') .eq('id', itemId) .single(); const parkId = item?.approved_entity_id; if (!parkId) throw new Error('No park ID after approval'); tracker.track('parks', parkId); // Wait for version 1 const v1 = await pollForVersion('park', parkId, 1); if (!v1) throw new Error('Version 1 not created'); // Update park directly (simulating approved edit) // In production, this would go through edit submission pipeline const { error: updateError } = await supabase .from('parks') .update({ name: 'Updated Name', description: 'Updated Description' }) .eq('id', parkId); if (updateError) throw new Error(`Park update failed: ${updateError.message}`); // Verify version 2 created const v2 = await pollForVersion('park', parkId, 2); if (!v2) throw new Error('Version 2 not created after update'); if (v2.name !== 'Updated Name') { throw new Error(`Version 2 has incorrect name: expected "Updated Name", got "${v2.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'); } // Verify version 1 is no longer current const { data: v1Updated } = await supabase .from('park_versions') .select('is_current') .eq('park_id', parkId) .eq('version_number', 1) .single(); if (v1Updated?.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, v1IsCurrent: v1Updated?.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: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString() }; } finally { await tracker.cleanup(); } } }, { 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(); try { // Create and approve park const userId = await getCurrentUserId(); const authToken = await getAuthToken(); const parkData = generateUniqueParkData('version-003'); const { submissionId, itemId } = await createTestParkSubmission(parkData, userId, tracker); const approval = await approveSubmission(submissionId, [itemId], authToken); if (!approval.success) { throw new Error(`Approval failed: ${approval.error}`); } // Get park ID const { data: item } = await supabase .from('submission_items') .select('approved_entity_id') .eq('id', itemId) .single(); const parkId = item?.approved_entity_id; if (!parkId) throw new Error('No park ID after approval'); tracker.track('parks', parkId); // Wait for version 1 const v1 = await pollForVersion('park', parkId, 1); if (!v1) throw new Error('Version 1 not created'); // Check current user role 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: parkId, p_target_version_id: v1.version_id, p_changed_by: user.id, p_reason: 'Authorization test' }); // Verify authorization enforcement 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: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString() }; } finally { await tracker.cleanup(); } } }, { 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(); 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) { 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 and approve park const userId = await getCurrentUserId(); const authToken = await getAuthToken(); const parkData = { ...generateUniqueParkData('version-004'), description: 'Original Description' }; const { submissionId, itemId } = await createTestParkSubmission(parkData, userId, tracker); const approval = await approveSubmission(submissionId, [itemId], authToken); if (!approval.success) { throw new Error(`Approval failed: ${approval.error}`); } // Get park ID const { data: item } = await supabase .from('submission_items') .select('approved_entity_id') .eq('id', itemId) .single(); const parkId = item?.approved_entity_id; if (!parkId) throw new Error('No park ID after approval'); tracker.track('parks', parkId); // Wait for version 1 const v1 = await pollForVersion('park', parkId, 1); if (!v1) throw new Error('Version 1 not created'); // Update park const { error: updateError } = await supabase .from('parks') .update({ name: 'Modified Name', description: 'Modified Description' }) .eq('id', parkId); if (updateError) throw new Error(`Park update failed: ${updateError.message}`); // Wait for version 2 const v2 = await pollForVersion('park', parkId, 2); 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: parkId, 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}`); // Verify park data restored const restored = await pollForEntity('parks', parkId, 3000); if (!restored) throw new Error('Could not fetch restored park'); if (restored.name !== parkData.name) { throw new Error(`Rollback failed: expected "${parkData.name}", got "${restored.name}"`); } if (restored.description !== 'Original Description') { throw new Error(`Description not restored: got "${restored.description}"`); } // Verify version 3 created with change_type = 'restored' const v3 = await pollForVersion('park', parkId, 3, 3000); if (!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 !== parkData.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, 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: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString() }; } finally { await tracker.cleanup(); } } } ] };