mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 08:31:12 -05:00
- 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
1896 lines
59 KiB
TypeScript
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,
|
|
],
|
|
};
|