mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 04:51:13 -05:00
Apply quick wins and pipeline fixes for integration tests: - Remove is_test_data flag from location inserts - Increase test delay from 2.5s to 5s and add delays between suites to curb rate limiting - Replace [object Object] error formatting with formatTestError across 10 test suites (31 edits) and add import - Refactor unit/conversion tests and performance tests to use the approval pipeline - Extend edge function handling by ensuring item_ids are included in idempotency key inserts (edge function fix) - Update auth, data integrity, edgeFunction, moderation, performance, submission, unitConversion, and versioning test files accordingly
456 lines
17 KiB
TypeScript
456 lines
17 KiB
TypeScript
/**
|
|
* 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<TestResult> => {
|
|
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<TestResult> => {
|
|
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<TestResult> => {
|
|
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<TestResult> => {
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
]
|
|
};
|