Files
thrilltrack-explorer/src/lib/integrationTests/suites/approvalPipelineTests.ts
gpt-engineer-app[bot] ade1810a01 Improve error formatting in tests
- Replace [object Object] errors with readable messages by using robust error formatting across test suites
- Introduce formatTestError helper and apply it in all catch blocks and error throws
- Update approvalPipelineTests and related suites to utilize improved error extraction for better debugging
2025-11-10 17:03:25 +00:00

1896 lines
59 KiB
TypeScript

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