/** * Approval Pipeline Integration Test Suite * * Comprehensive end-to-end tests for the submission approval workflow. * Tests the complete pipeline: Form → Submission → Moderation → Edge Function → RPC → Entity Creation → Versioning * * Coverage: * - Single entity creation (parks, rides, companies, ride models) * - Entity updates through pipeline * - Composite submissions with temp references * - Photo gallery submissions * - Edge cases (partial approval, idempotency, locks, invalid refs, banned users) * - Versioning integrity */ import type { Test, TestSuite, TestResult } from '../testRunner'; import { TestDataTracker } from '../TestDataTracker'; import { supabase } from '@/lib/supabaseClient'; import { getAuthToken, getCurrentUserId, generateUniqueParkData, generateUniqueRideData, generateUniqueCompanyData, generateUniqueRideModelData, createTestParkSubmission, createTestRideSubmission, createTestCompanySubmission, createTestRideModelSubmission, createCompositeSubmission, createTestPhotoGallerySubmission, approveSubmission, pollForEntity, pollForVersion, verifySubmissionItemApproved, verifySubmissionStatus, createParkDirectly, createRideDirectly, formatTestError, } from '../helpers/approvalTestHelpers'; // ============================================ // CATEGORY 1: SINGLE ENTITY CREATION TESTS // ============================================ /** * APL-001: Park Creation Through Full Pipeline */ const parkCreationTest: Test = { id: 'APL-001', name: 'Park Creation Through Full Pipeline', description: 'Validates park creation from submission to entity with versioning', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { // 1. Setup const userId = await getCurrentUserId(); const authToken = await getAuthToken(); const parkData = generateUniqueParkData('apl-001'); // 2. Create submission const { submissionId, itemId } = await createTestParkSubmission(parkData, userId, tracker); // 3. Verify submission created const { data: submission } = await supabase .from('content_submissions') .select('*') .eq('id', submissionId) .single(); if (!submission || submission.status !== 'pending') { throw new Error('Submission not created with pending status'); } // 4. Approve submission const approvalResult = await approveSubmission(submissionId, [itemId], authToken); if (!approvalResult.success) { throw new Error(`Approval failed: ${approvalResult.error}`); } // 5. Poll for park creation const park = await pollForEntity('parks', parkData.slug, 10000); if (!park) { throw new Error('Park not created within timeout'); } tracker.track('parks', park.id); // 6. Verify park data if (park.name !== parkData.name || park.slug !== parkData.slug) { throw new Error('Park data mismatch'); } // 7. Poll for version const version = await pollForVersion('park', park.id, 1, 10000); if (!version) { throw new Error('Park version not created'); } tracker.track('park_versions', version.id); // 8. Verify submission item approved const itemCheck = await verifySubmissionItemApproved(itemId); if (!itemCheck.approved || itemCheck.entityId !== park.id) { throw new Error('Submission item not properly approved'); } // 9. Verify submission status const statusCheck = await verifySubmissionStatus(submissionId, 'approved'); if (!statusCheck) { throw new Error('Submission status not updated to approved'); } return { id: 'APL-001', name: 'Park Creation Through Full Pipeline', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-001', name: 'Park Creation Through Full Pipeline', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-002: Ride Creation Through Full Pipeline */ const rideCreationTest: Test = { id: 'APL-002', name: 'Ride Creation Through Full Pipeline', description: 'Validates ride creation with park relationship', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create test park first const parkData = generateUniqueParkData('apl-002-park'); const parkId = await createParkDirectly(parkData, tracker); // Wait for park version await pollForVersion('park', parkId, 1, 10000); // Create ride submission const rideData = generateUniqueRideData(parkId, 'apl-002'); const { submissionId, itemId } = await createTestRideSubmission(rideData, userId, tracker); // Approve const approvalResult = await approveSubmission(submissionId, [itemId], authToken); if (!approvalResult.success) { throw new Error(`Approval failed: ${approvalResult.error}`); } // Poll for ride const ride = await pollForEntity('rides', rideData.slug, 10000); if (!ride) { throw new Error('Ride not created within timeout'); } tracker.track('rides', ride.id); // Verify park relationship if (ride.park_id !== parkId) { throw new Error('Ride park_id mismatch'); } // Verify version const version = await pollForVersion('ride', ride.id, 1, 10000); if (!version) { throw new Error('Ride version not created'); } tracker.track('ride_versions', version.id); // Verify approval const itemCheck = await verifySubmissionItemApproved(itemId); if (!itemCheck.approved || itemCheck.entityId !== ride.id) { throw new Error('Submission item not properly approved'); } return { id: 'APL-002', name: 'Ride Creation Through Full Pipeline', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-002', name: 'Ride Creation Through Full Pipeline', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-003: Company Creation (All 4 Types) */ const companyCreationTest: Test = { id: 'APL-003', name: 'Company Creation (All 4 Types)', description: 'Validates all company types: manufacturer, operator, designer, property_owner', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); const companyTypes = ['manufacturer', 'operator', 'designer', 'property_owner'] as const; for (const companyType of companyTypes) { const companyData = generateUniqueCompanyData(companyType, `apl-003-${companyType}`); const { submissionId, itemId } = await createTestCompanySubmission( companyType, companyData, userId, tracker ); const approvalResult = await approveSubmission(submissionId, [itemId], authToken); if (!approvalResult.success) { throw new Error(`${companyType} approval failed: ${approvalResult.error}`); } const company = await pollForEntity('companies', companyData.slug, 10000); if (!company) { throw new Error(`${companyType} not created within timeout`); } tracker.track('companies', company.id); // Note: company_type is stored via RPC during approval process // We verify the company was created successfully const version = await pollForVersion('company', company.id, 1, 10000); if (!version) { throw new Error(`${companyType} version not created`); } tracker.track('company_versions', version.id); } return { id: 'APL-003', name: 'Company Creation (All 4 Types)', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-003', name: 'Company Creation (All 4 Types)', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-004: Ride Model Creation */ const rideModelCreationTest: Test = { id: 'APL-004', name: 'Ride Model Creation', description: 'Validates ride model creation with manufacturer relationship', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create manufacturer first const manufacturerData = generateUniqueCompanyData('manufacturer', 'apl-004-mfg'); const { submissionId: mfgSubId, itemId: mfgItemId } = await createTestCompanySubmission( 'manufacturer', manufacturerData, userId, tracker ); await approveSubmission(mfgSubId, [mfgItemId], authToken); const manufacturer = await pollForEntity('companies', manufacturerData.slug, 10000); if (!manufacturer) { throw new Error('Manufacturer not created'); } tracker.track('companies', manufacturer.id); // Create ride model const modelData = generateUniqueRideModelData(manufacturer.id, 'apl-004'); const { submissionId, itemId } = await createTestRideModelSubmission(modelData, userId, tracker); const approvalResult = await approveSubmission(submissionId, [itemId], authToken); if (!approvalResult.success) { throw new Error(`Approval failed: ${approvalResult.error}`); } const model = await pollForEntity('ride_models', modelData.slug, 10000); if (!model) { throw new Error('Ride model not created within timeout'); } tracker.track('ride_models', model.id); if (model.manufacturer_id !== manufacturer.id) { throw new Error('Ride model manufacturer_id mismatch'); } const version = await pollForVersion('ride_model', model.id, 1, 10000); if (!version) { throw new Error('Ride model version not created'); } tracker.track('ride_model_versions', version.id); return { id: 'APL-004', name: 'Ride Model Creation', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-004', name: 'Ride Model Creation', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; // ============================================ // CATEGORY 2: COMPOSITE SUBMISSION TESTS // ============================================ /** * APL-007: Ride + Manufacturer Composite */ const rideManufacturerCompositeTest: Test = { id: 'APL-007', name: 'Ride + Manufacturer Composite', description: 'Validates composite submission with temp manufacturer reference', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create park first const parkData = generateUniqueParkData('apl-007-park'); const parkId = await createParkDirectly(parkData, tracker); // Create composite submission const manufacturerData = generateUniqueCompanyData('manufacturer', 'apl-007-mfg'); const rideData = generateUniqueRideData(parkId, 'apl-007'); const { submissionId, itemIds } = await createCompositeSubmission( { type: 'ride', data: rideData, }, [ { type: 'company', data: manufacturerData, tempId: 'temp-manufacturer', companyType: 'manufacturer', }, ], userId, tracker ); // Note: Temp refs are stored in specialized submission tables (ride_submissions) // not in submission_items.item_data. This test validates the resolution works. // Approve all const approvalResult = await approveSubmission(submissionId, itemIds, authToken); if (!approvalResult.success) { throw new Error(`Approval failed: ${approvalResult.error}`); } // Poll for manufacturer const manufacturer = await pollForEntity('companies', manufacturerData.slug, 10000); if (!manufacturer) { throw new Error('Manufacturer not created'); } tracker.track('companies', manufacturer.id); // Poll for ride const ride = await pollForEntity('rides', rideData.slug, 10000); if (!ride) { throw new Error('Ride not created'); } tracker.track('rides', ride.id); // Verify temp ref resolved if (ride.manufacturer_id !== manufacturer.id) { throw new Error('Temp manufacturer ref not resolved correctly'); } // Verify both items approved const mfgCheck = await verifySubmissionItemApproved(itemIds[0]); const rideCheck = await verifySubmissionItemApproved(itemIds[1]); if (!mfgCheck.approved || !rideCheck.approved) { throw new Error('Not all items approved'); } return { id: 'APL-007', name: 'Ride + Manufacturer Composite', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-007', name: 'Ride + Manufacturer Composite', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-012: Partial Approval */ const partialApprovalTest: Test = { id: 'APL-012', name: 'Partial Approval', description: 'Validates partial approval workflow', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create composite with 3 items const operatorData = generateUniqueCompanyData('operator', 'apl-012-op'); const ownerData = generateUniqueCompanyData('property_owner', 'apl-012-owner'); const parkData = generateUniqueParkData('apl-012'); const { submissionId, itemIds } = await createCompositeSubmission( { type: 'park', data: parkData, }, [ { type: 'company', data: operatorData, tempId: 'temp-operator', companyType: 'operator', }, { type: 'company', data: ownerData, tempId: 'temp-owner', companyType: 'property_owner', }, ], userId, tracker ); // Approve only first 2 items (companies, not park) const approvalResult = await approveSubmission(submissionId, itemIds.slice(0, 2), authToken); if (!approvalResult.success) { throw new Error(`Partial approval failed: ${approvalResult.error}`); } // Verify companies created const operator = await pollForEntity('companies', operatorData.slug, 10000); const owner = await pollForEntity('companies', ownerData.slug, 10000); if (!operator || !owner) { throw new Error('Companies not created'); } tracker.track('companies', operator.id); tracker.track('companies', owner.id); // Verify park NOT created const park = await pollForEntity('parks', parkData.slug, 2000); if (park) { throw new Error('Park should not be created in partial approval'); } // Verify submission status const statusCheck = await verifySubmissionStatus(submissionId, 'partially_approved'); if (!statusCheck) { throw new Error('Submission should be partially_approved'); } // Now approve the park const secondApprovalResult = await approveSubmission(submissionId, [itemIds[2]], authToken); if (!secondApprovalResult.success) { throw new Error(`Second approval failed: ${secondApprovalResult.error}`); } // Verify park created with correct refs const parkNow = await pollForEntity('parks', parkData.slug, 10000); if (!parkNow) { throw new Error('Park not created after second approval'); } tracker.track('parks', parkNow.id); if (parkNow.operator_id !== operator.id || parkNow.property_owner_id !== owner.id) { throw new Error('Park company refs not resolved correctly'); } // Verify final status const finalStatusCheck = await verifySubmissionStatus(submissionId, 'approved'); if (!finalStatusCheck) { throw new Error('Submission should be fully approved now'); } return { id: 'APL-012', name: 'Partial Approval', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-012', name: 'Partial Approval', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-013: Idempotency Key Handling */ const idempotencyTest: Test = { id: 'APL-013', name: 'Idempotency Key Handling', description: 'Validates idempotency prevents duplicate approvals', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); const parkData = generateUniqueParkData('apl-013'); const { submissionId, itemId } = await createTestParkSubmission(parkData, userId, tracker); // Generate unique idempotency key const idempotencyKey = `test-idempotency-${Date.now()}`; // First approval const firstResult = await approveSubmission(submissionId, [itemId], authToken, idempotencyKey); if (!firstResult.success) { throw new Error(`First approval failed: ${firstResult.error}`); } // Wait for completion const park = await pollForEntity('parks', parkData.slug, 10000); if (!park) { throw new Error('Park not created'); } tracker.track('parks', park.id); // Second approval with same key const secondResult = await approveSubmission(submissionId, [itemId], authToken, idempotencyKey); if (!secondResult.success) { throw new Error(`Second approval should succeed with cached result: ${secondResult.error}`); } // Verify only 1 park exists const { count } = await supabase .from('parks') .select('*', { count: 'exact', head: true }) .eq('slug', parkData.slug); if (count !== 1) { throw new Error(`Expected 1 park, found ${count}`); } // Verify idempotency key record exists const { data: keyRecord } = await supabase .from('submission_idempotency_keys') .select('*') .eq('idempotency_key', idempotencyKey) .single(); if (!keyRecord || keyRecord.status !== 'completed') { throw new Error('Idempotency key not recorded correctly'); } return { id: 'APL-013', name: 'Idempotency Key Handling', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-013', name: 'Idempotency Key Handling', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-005: Park Update Through Pipeline */ const parkUpdateTest: Test = { id: 'APL-005', name: 'Park Update Through Pipeline', description: 'Validates park updates create new versions', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create park directly const parkData = generateUniqueParkData('apl-005'); const parkId = await createParkDirectly(parkData, tracker); // Wait for version 1 const version1 = await pollForVersion('park', parkId, 1, 10000); if (!version1) { throw new Error('Version 1 not created'); } tracker.track('park_versions', version1.id); // Submit update const updateData = { ...parkData, name: `${parkData.name} UPDATED`, description: 'Updated description', }; const { submissionId, itemId } = await createTestParkSubmission(updateData, userId, tracker); // Approve update const approvalResult = await approveSubmission(submissionId, [itemId], authToken); if (!approvalResult.success) { throw new Error(`Update approval failed: ${approvalResult.error}`); } // Wait for version 2 const version2 = await pollForVersion('park', parkId, 2, 10000); if (!version2) { throw new Error('Version 2 not created after update'); } tracker.track('park_versions', version2.id); // Verify park updated const { data: updatedPark } = await supabase .from('parks') .select('*') .eq('id', parkId) .single(); if (!updatedPark || !updatedPark.name.includes('UPDATED')) { throw new Error('Park not updated correctly'); } // Verify version 1 still exists with original data const { data: v1Check } = await supabase .from('park_versions') .select('*') .eq('id', version1.id) .single(); if (!v1Check || v1Check.version_number !== 1) { throw new Error('Version 1 corrupted'); } return { id: 'APL-005', name: 'Park Update Through Pipeline', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-005', name: 'Park Update Through Pipeline', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-006: Ride Update Through Pipeline */ const rideUpdateTest: Test = { id: 'APL-006', name: 'Ride Update Through Pipeline', description: 'Validates ride updates create new versions', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create park first const parkData = generateUniqueParkData('apl-006-park'); const parkId = await createParkDirectly(parkData, tracker); // Create ride directly const rideData = generateUniqueRideData(parkId, 'apl-006'); const rideId = await createRideDirectly(rideData, tracker); // Wait for version 1 const version1 = await pollForVersion('ride', rideId, 1, 10000); if (!version1) { throw new Error('Version 1 not created'); } tracker.track('ride_versions', version1.id); // Submit update const updateData = { ...rideData, name: `${rideData.name} UPDATED`, max_speed_kmh: 120, }; const { submissionId, itemId } = await createTestRideSubmission(updateData, userId, tracker); // Approve update const approvalResult = await approveSubmission(submissionId, [itemId], authToken); if (!approvalResult.success) { throw new Error(`Update approval failed: ${approvalResult.error}`); } // Wait for version 2 const version2 = await pollForVersion('ride', rideId, 2, 10000); if (!version2) { throw new Error('Version 2 not created after update'); } tracker.track('ride_versions', version2.id); // Verify ride updated const { data: updatedRide } = await supabase .from('rides') .select('*') .eq('id', rideId) .single(); if (!updatedRide || !updatedRide.name.includes('UPDATED') || updatedRide.max_speed_kmh !== 120) { throw new Error('Ride not updated correctly'); } return { id: 'APL-006', name: 'Ride Update Through Pipeline', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-006', name: 'Ride Update Through Pipeline', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-008: Ride + Manufacturer + Designer Composite */ const rideManufacturerDesignerCompositeTest: Test = { id: 'APL-008', name: 'Ride + Manufacturer + Designer Composite', description: 'Validates composite with multiple company temp references', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create park const parkData = generateUniqueParkData('apl-008-park'); const parkId = await createParkDirectly(parkData, tracker); // Create composite const manufacturerData = generateUniqueCompanyData('manufacturer', 'apl-008-mfg'); const designerData = generateUniqueCompanyData('designer', 'apl-008-des'); const rideData = generateUniqueRideData(parkId, 'apl-008'); const { submissionId, itemIds } = await createCompositeSubmission( { type: 'ride', data: rideData, }, [ { type: 'company', data: manufacturerData, tempId: 'temp-manufacturer', companyType: 'manufacturer', }, { type: 'company', data: designerData, tempId: 'temp-designer', companyType: 'designer', }, ], userId, tracker ); // Approve all const approvalResult = await approveSubmission(submissionId, itemIds, authToken); if (!approvalResult.success) { throw new Error(`Approval failed: ${approvalResult.error}`); } // Poll for companies const manufacturer = await pollForEntity('companies', manufacturerData.slug, 10000); const designer = await pollForEntity('companies', designerData.slug, 10000); if (!manufacturer || !designer) { throw new Error('Companies not created'); } tracker.track('companies', manufacturer.id); tracker.track('companies', designer.id); // Poll for ride const ride = await pollForEntity('rides', rideData.slug, 10000); if (!ride) { throw new Error('Ride not created'); } tracker.track('rides', ride.id); // Verify both temp refs resolved if (ride.manufacturer_id !== manufacturer.id || ride.designer_id !== designer.id) { throw new Error('Temp refs not resolved correctly'); } return { id: 'APL-008', name: 'Ride + Manufacturer + Designer Composite', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-008', name: 'Ride + Manufacturer + Designer Composite', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-009: Park + Operator + Property Owner Composite */ const parkOperatorOwnerCompositeTest: Test = { id: 'APL-009', name: 'Park + Operator + Property Owner Composite', description: 'Validates park composite with operator and owner temp references', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create composite const operatorData = generateUniqueCompanyData('operator', 'apl-009-op'); const ownerData = generateUniqueCompanyData('property_owner', 'apl-009-owner'); const parkData = generateUniqueParkData('apl-009'); const { submissionId, itemIds } = await createCompositeSubmission( { type: 'park', data: parkData, }, [ { type: 'company', data: operatorData, tempId: 'temp-operator', companyType: 'operator', }, { type: 'company', data: ownerData, tempId: 'temp-owner', companyType: 'property_owner', }, ], userId, tracker ); // Approve all const approvalResult = await approveSubmission(submissionId, itemIds, authToken); if (!approvalResult.success) { throw new Error(`Approval failed: ${approvalResult.error}`); } // Poll for companies const operator = await pollForEntity('companies', operatorData.slug, 10000); const owner = await pollForEntity('companies', ownerData.slug, 10000); if (!operator || !owner) { throw new Error('Companies not created'); } tracker.track('companies', operator.id); tracker.track('companies', owner.id); // Poll for park const park = await pollForEntity('parks', parkData.slug, 10000); if (!park) { throw new Error('Park not created'); } tracker.track('parks', park.id); // Verify temp refs resolved if (park.operator_id !== operator.id || park.property_owner_id !== owner.id) { throw new Error('Park company refs not resolved correctly'); } return { id: 'APL-009', name: 'Park + Operator + Property Owner Composite', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-009', name: 'Park + Operator + Property Owner Composite', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-014: Lock Conflict Prevention */ const lockConflictTest: Test = { id: 'APL-014', name: 'Lock Conflict Prevention', description: 'Validates locked submissions prevent concurrent approval', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create submission 1 - same user lock (should succeed) const parkData1 = generateUniqueParkData('apl-014-a'); const { submissionId: subId1, itemId: itemId1 } = await createTestParkSubmission(parkData1, userId, tracker); // Lock to current user await supabase .from('content_submissions') .update({ assigned_to: userId, locked_until: new Date(Date.now() + 15 * 60 * 1000).toISOString(), }) .eq('id', subId1); // Approve (should succeed - same user) const result1 = await approveSubmission(subId1, [itemId1], authToken); if (!result1.success) { throw new Error(`Same user approval should succeed: ${result1.error}`); } const park1 = await pollForEntity('parks', parkData1.slug, 10000); if (!park1) { throw new Error('Park 1 not created'); } tracker.track('parks', park1.id); // Create submission 2 - different user lock (should fail) const parkData2 = generateUniqueParkData('apl-014-b'); const { submissionId: subId2, itemId: itemId2 } = await createTestParkSubmission(parkData2, userId, tracker); // Lock to fake user const fakeUserId = '00000000-0000-0000-0000-000000000001'; await supabase .from('content_submissions') .update({ assigned_to: fakeUserId, locked_until: new Date(Date.now() + 15 * 60 * 1000).toISOString(), }) .eq('id', subId2); // Try to approve (should fail) const result2 = await approveSubmission(subId2, [itemId2], authToken); if (result2.success) { throw new Error('Approval should fail when locked by different user'); } // Verify park NOT created const park2 = await pollForEntity('parks', parkData2.slug, 2000); if (park2) { throw new Error('Park 2 should not be created due to lock'); } return { id: 'APL-014', name: 'Lock Conflict Prevention', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-014', name: 'Lock Conflict Prevention', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-016: Banned User Rejection */ const bannedUserTest: Test = { id: 'APL-016', name: 'Banned User Rejection', description: 'Validates banned users cannot have submissions approved', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create submission const parkData = generateUniqueParkData('apl-016'); const { submissionId, itemId } = await createTestParkSubmission(parkData, userId, tracker); // Ban user await supabase .from('profiles') .update({ banned: true }) .eq('user_id', userId); // Try to approve (should fail) const result = await approveSubmission(submissionId, [itemId], authToken); if (result.success) { throw new Error('Approval should fail for banned user'); } // Verify park NOT created const park = await pollForEntity('parks', parkData.slug, 2000); if (park) { throw new Error('Park should not be created for banned user'); } // Unban user await supabase .from('profiles') .update({ banned: false }) .eq('user_id', userId); // Try again (should succeed now) const result2 = await approveSubmission(submissionId, [itemId], authToken); if (!result2.success) { throw new Error(`Approval should succeed after unban: ${result2.error}`); } // Verify park created const parkNow = await pollForEntity('parks', parkData.slug, 10000); if (!parkNow) { throw new Error('Park not created after unban'); } tracker.track('parks', parkNow.id); return { id: 'APL-016', name: 'Banned User Rejection', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { // Make sure to unban user even if test fails try { const userId = await getCurrentUserId(); await supabase .from('profiles') .update({ banned: false }) .eq('user_id', userId); } catch (cleanupError) { console.error('Failed to unban user in cleanup:', cleanupError); } return { id: 'APL-016', name: 'Banned User Rejection', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-017: Multiple Edit Chain */ const multipleEditChainTest: Test = { id: 'APL-017', name: 'Multiple Edit Chain', description: 'Validates multiple sequential edits create correct version chain', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create park directly const parkData = generateUniqueParkData('apl-017'); const parkId = await createParkDirectly(parkData, tracker); // Wait for version 1 await pollForVersion('park', parkId, 1, 10000); // Edit 1: Change name const edit1Data = { ...parkData, name: `${parkData.name} V2` }; const { submissionId: sub1, itemId: item1 } = await createTestParkSubmission(edit1Data, userId, tracker); await approveSubmission(sub1, [item1], authToken); const v2 = await pollForVersion('park', parkId, 2, 10000); if (!v2) throw new Error('Version 2 not created'); tracker.track('park_versions', v2.id); // Edit 2: Change description const edit2Data = { ...edit1Data, description: 'Version 3 description' }; const { submissionId: sub2, itemId: item2 } = await createTestParkSubmission(edit2Data, userId, tracker); await approveSubmission(sub2, [item2], authToken); const v3 = await pollForVersion('park', parkId, 3, 10000); if (!v3) throw new Error('Version 3 not created'); tracker.track('park_versions', v3.id); // Edit 3: Change status const edit3Data = { ...edit2Data, status: 'closed' }; const { submissionId: sub3, itemId: item3 } = await createTestParkSubmission(edit3Data, userId, tracker); await approveSubmission(sub3, [item3], authToken); const v4 = await pollForVersion('park', parkId, 4, 10000); if (!v4) throw new Error('Version 4 not created'); tracker.track('park_versions', v4.id); // Verify all 4 versions exist const { count } = await supabase .from('park_versions') .select('*', { count: 'exact', head: true }) .eq('park_id', parkId); if (count !== 4) { throw new Error(`Expected 4 versions, found ${count}`); } // Verify version numbers are correct const { data: versions } = await supabase .from('park_versions') .select('version_number') .eq('park_id', parkId) .order('version_number', { ascending: true }); const versionNumbers = versions?.map(v => v.version_number) || []; if (versionNumbers.join(',') !== '1,2,3,4') { throw new Error(`Version sequence incorrect: ${versionNumbers.join(',')}`); } return { id: 'APL-017', name: 'Multiple Edit Chain', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-017', name: 'Multiple Edit Chain', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-019: Version Snapshot Integrity */ const versionSnapshotIntegrityTest: Test = { id: 'APL-019', name: 'Version Snapshot Integrity', description: 'Validates version snapshots maintain data integrity', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create park with all fields const parkData = generateUniqueParkData('apl-019'); const fullParkData = { ...parkData, description: 'Full description with all fields', website_url: 'https://example.com', phone: '+1234567890', email: 'test@example.com', }; const parkId = await createParkDirectly(fullParkData, tracker); // Wait for version 1 const v1 = await pollForVersion('park', parkId, 1, 10000); if (!v1) throw new Error('Version 1 not created'); tracker.track('park_versions', v1.id); // Update only 2 fields const updateData = { ...fullParkData, name: `${fullParkData.name} CHANGED`, description: 'Updated description only', }; const { submissionId, itemId } = await createTestParkSubmission(updateData, userId, tracker); await approveSubmission(submissionId, [itemId], authToken); // Wait for version 2 const v2 = await pollForVersion('park', parkId, 2, 10000); if (!v2) throw new Error('Version 2 not created'); tracker.track('park_versions', v2.id); // Verify version 1 has original values const { data: version1 } = await supabase .from('park_versions') .select('*') .eq('id', v1.id) .single(); if (!version1 || version1.name !== fullParkData.name) { throw new Error('Version 1 data corrupted'); } if (version1.website_url !== fullParkData.website_url) { throw new Error('Version 1 unchanged fields corrupted'); } // Verify version 2 has updated values const { data: version2 } = await supabase .from('park_versions') .select('*') .eq('id', v2.id) .single(); if (!version2 || !version2.name.includes('CHANGED')) { throw new Error('Version 2 changes not captured'); } // Verify unchanged fields identical in both versions if (version1.website_url !== version2.website_url || version1.phone !== version2.phone || version1.email !== version2.email) { throw new Error('Unchanged fields differ between versions'); } return { id: 'APL-019', name: 'Version Snapshot Integrity', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-019', name: 'Version Snapshot Integrity', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-010: Photo Gallery for Park */ const parkPhotoGalleryTest: Test = { id: 'APL-010', name: 'Photo Gallery for Park', description: 'Validates photo gallery submissions for parks', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create park const parkData = generateUniqueParkData('apl-010'); const parkId = await createParkDirectly(parkData, tracker); // Create photo gallery submission const { submissionId, itemId } = await createTestPhotoGallerySubmission( parkId, 'park', 3, userId, tracker ); // Approve const approvalResult = await approveSubmission(submissionId, [itemId], authToken); if (!approvalResult.success) { throw new Error(`Photo approval failed: ${approvalResult.error}`); } // Wait and verify photos created await new Promise(resolve => setTimeout(resolve, 2000)); const { data: photos, error } = await supabase .from('photos') .select('*') .eq('entity_id', parkId) .eq('entity_type', 'park'); if (error || !photos || photos.length !== 3) { throw new Error(`Expected 3 photos, found ${photos?.length || 0}`); } photos.forEach(photo => { tracker.track('photos', photo.id); if (photo.entity_id !== parkId || photo.entity_type !== 'park') { throw new Error('Photo entity references incorrect'); } }); return { id: 'APL-010', name: 'Photo Gallery for Park', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-010', name: 'Photo Gallery for Park', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-011: Photo Gallery for Ride */ const ridePhotoGalleryTest: Test = { id: 'APL-011', name: 'Photo Gallery for Ride', description: 'Validates photo gallery submissions for rides', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create park and ride const parkData = generateUniqueParkData('apl-011-park'); const parkId = await createParkDirectly(parkData, tracker); const rideData = generateUniqueRideData(parkId, 'apl-011'); const rideId = await createRideDirectly(rideData, tracker); // Create photo gallery submission with 5 photos const { submissionId, itemId } = await createTestPhotoGallerySubmission( rideId, 'ride', 5, userId, tracker ); // Approve const approvalResult = await approveSubmission(submissionId, [itemId], authToken); if (!approvalResult.success) { throw new Error(`Photo approval failed: ${approvalResult.error}`); } // Wait and verify photos created await new Promise(resolve => setTimeout(resolve, 2000)); const { data: photos, error } = await supabase .from('photos') .select('*') .eq('entity_id', rideId) .eq('entity_type', 'ride'); if (error || !photos || photos.length !== 5) { throw new Error(`Expected 5 photos, found ${photos?.length || 0}`); } photos.forEach(photo => { tracker.track('photos', photo.id); if (photo.entity_id !== rideId || photo.entity_type !== 'ride') { throw new Error('Photo entity references incorrect'); } }); return { id: 'APL-011', name: 'Photo Gallery for Ride', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-011', name: 'Photo Gallery for Ride', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-015: Invalid Temp Reference Rejection */ const invalidTempRefTest: Test = { id: 'APL-015', name: 'Invalid Temp Reference Rejection', description: 'Validates submissions with invalid temp references are rejected', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create park const parkData = generateUniqueParkData('apl-015-park'); const parkId = await createParkDirectly(parkData, tracker); // Create ride submission manually with invalid temp ref const rideData = generateUniqueRideData(parkId, 'apl-015'); // Create submission const { data: submission, error: subError } = await supabase .from('content_submissions') .insert({ user_id: userId, submission_type: 'ride_create', status: 'pending', is_test_data: true, }) .select() .single(); if (subError || !submission) { throw new Error('Failed to create test submission'); } tracker.track('content_submissions', submission.id); // Create ride submission with invalid temp ref (99 doesn't exist) const { data: rideSubmission, error: rideSubError } = await supabase .from('ride_submissions') .insert({ ...rideData, _temp_manufacturer_ref: 99, // Invalid - no item with this order_index is_test_data: true, }) .select() .single(); if (rideSubError || !rideSubmission) { throw new Error('Failed to create ride submission'); } tracker.track('ride_submissions', rideSubmission.id); // Create submission item const { data: item, error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submission.id, ride_submission_id: rideSubmission.id, item_type: 'ride_create', status: 'pending', is_test_data: true, }) .select() .single(); if (itemError || !item) { throw new Error('Failed to create submission item'); } tracker.track('submission_items', item.id); // Try to approve (should fail) const approvalResult = await approveSubmission(submission.id, [item.id], authToken); if (approvalResult.success) { throw new Error('Approval should fail with invalid temp ref'); } // Verify ride NOT created const ride = await pollForEntity('rides', rideData.slug, 2000); if (ride) { throw new Error('Ride should not be created with invalid temp ref'); } return { id: 'APL-015', name: 'Invalid Temp Reference Rejection', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-015', name: 'Invalid Temp Reference Rejection', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; /** * APL-018: Concurrent Edit Conflict */ const concurrentEditTest: Test = { id: 'APL-018', name: 'Concurrent Edit Conflict', description: 'Validates concurrent edits are applied in order', run: async (): Promise => { const startTime = Date.now(); const tracker = new TestDataTracker(); try { const userId = await getCurrentUserId(); const authToken = await getAuthToken(); // Create park const parkData = generateUniqueParkData('apl-018'); const parkId = await createParkDirectly(parkData, tracker); // Wait for version 1 await pollForVersion('park', parkId, 1, 10000); // Create two concurrent edits const editA = { ...parkData, name: `${parkData.name} NAME-A` }; const editB = { ...parkData, name: `${parkData.name} NAME-B` }; const { submissionId: subA, itemId: itemA } = await createTestParkSubmission(editA, userId, tracker); const { submissionId: subB, itemId: itemB } = await createTestParkSubmission(editB, userId, tracker); // Approve edit A first await approveSubmission(subA, [itemA], authToken); const v2 = await pollForVersion('park', parkId, 2, 10000); if (!v2) throw new Error('Version 2 not created'); tracker.track('park_versions', v2.id); // Then approve edit B await approveSubmission(subB, [itemB], authToken); const v3 = await pollForVersion('park', parkId, 3, 10000); if (!v3) throw new Error('Version 3 not created'); tracker.track('park_versions', v3.id); // Verify final park name is NAME-B (last edit wins) const { data: finalPark } = await supabase .from('parks') .select('name') .eq('id', parkId) .single(); if (!finalPark || !finalPark.name.includes('NAME-B')) { throw new Error(`Expected NAME-B, got ${finalPark?.name}`); } // Verify version 2 has NAME-A const { data: v2Check } = await supabase .from('park_versions') .select('name') .eq('id', v2.id) .single(); if (!v2Check || !v2Check.name.includes('NAME-A')) { throw new Error('Version 2 should have NAME-A'); } // Verify version 3 has NAME-B const { data: v3Check } = await supabase .from('park_versions') .select('name') .eq('id', v3.id) .single(); if (!v3Check || !v3Check.name.includes('NAME-B')) { throw new Error('Version 3 should have NAME-B'); } return { id: 'APL-018', name: 'Concurrent Edit Conflict', suite: 'Approval Pipeline', status: 'pass', duration: Date.now() - startTime, timestamp: new Date().toISOString(), }; } catch (error) { return { id: 'APL-018', name: 'Concurrent Edit Conflict', suite: 'Approval Pipeline', status: 'fail', duration: Date.now() - startTime, error: formatTestError(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; } finally { await tracker.cleanup(); } }, }; // ============================================ // TEST SUITE EXPORT // ============================================ export const approvalPipelineTestSuite: TestSuite = { id: 'approval-pipeline', name: 'Approval Pipeline Integration', description: 'End-to-end tests for submission approval workflow through edge functions and RPC', tests: [ parkCreationTest, rideCreationTest, companyCreationTest, rideModelCreationTest, parkUpdateTest, rideUpdateTest, rideManufacturerCompositeTest, rideManufacturerDesignerCompositeTest, parkOperatorOwnerCompositeTest, parkPhotoGalleryTest, ridePhotoGalleryTest, partialApprovalTest, idempotencyTest, lockConflictTest, invalidTempRefTest, bannedUserTest, multipleEditChainTest, concurrentEditTest, versionSnapshotIntegrityTest, ], };