mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:31:26 -05:00
Expand approvalPipelineTests with 12 tests: park/ride updates, composites, photo galleries, edge cases (locks, invalid refs, banned users), and versioning. Include helpers for photo submissions and direct entity creation, plus photo gallery support and invalid temp-ref handling.
697 lines
18 KiB
TypeScript
697 lines
18 KiB
TypeScript
/**
|
|
* Approval Pipeline Test Helpers
|
|
*
|
|
* Reusable helper functions for approval pipeline integration tests.
|
|
* These helpers abstract common patterns for submission creation, approval,
|
|
* and verification across all entity types.
|
|
*/
|
|
|
|
import { supabase } from '@/lib/supabaseClient';
|
|
import { TestDataTracker } from '../TestDataTracker';
|
|
import {
|
|
submitParkCreation,
|
|
submitRideCreation,
|
|
submitManufacturerCreation,
|
|
submitOperatorCreation,
|
|
submitDesignerCreation,
|
|
submitPropertyOwnerCreation,
|
|
submitRideModelCreation
|
|
} from '@/lib/entitySubmissionHelpers';
|
|
|
|
// ============================================
|
|
// AUTHENTICATION
|
|
// ============================================
|
|
|
|
/**
|
|
* Get current user auth token for edge function calls
|
|
*/
|
|
export async function getAuthToken(): Promise<string> {
|
|
const { data: { session }, error } = await supabase.auth.getSession();
|
|
if (error || !session) {
|
|
throw new Error('Not authenticated - cannot run approval tests');
|
|
}
|
|
return session.access_token;
|
|
}
|
|
|
|
/**
|
|
* Get current user ID
|
|
*/
|
|
export async function getCurrentUserId(): Promise<string> {
|
|
const { data: { user }, error } = await supabase.auth.getUser();
|
|
if (error || !user) {
|
|
throw new Error('Not authenticated - cannot get user ID');
|
|
}
|
|
return user.id;
|
|
}
|
|
|
|
// ============================================
|
|
// EDGE FUNCTION CONFIGURATION
|
|
// ============================================
|
|
|
|
/**
|
|
* Get edge function base URL (hardcoded per project requirements)
|
|
*/
|
|
export function getEdgeFunctionUrl(): string {
|
|
return 'https://api.thrillwiki.com/functions/v1';
|
|
}
|
|
|
|
/**
|
|
* Get Supabase anon key (hardcoded per project requirements)
|
|
*/
|
|
export function getSupabaseAnonKey(): string {
|
|
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRka2VueWdwcHlzZ3NlcmJ5aW9hIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Mjg0ODY0MTIsImV4cCI6MjA0NDA2MjQxMn0.0qfDbOvh-Hs5n7HHZ0cRQzH5oEL_1D7kj7v6nh4PqgI';
|
|
}
|
|
|
|
// ============================================
|
|
// TEST DATA GENERATORS
|
|
// ============================================
|
|
|
|
/**
|
|
* Generate unique park submission data
|
|
*/
|
|
export function generateUniqueParkData(testId: string): any {
|
|
const timestamp = Date.now();
|
|
const slug = `test-park-${testId}-${timestamp}`;
|
|
|
|
return {
|
|
name: `Test Park ${testId} ${timestamp}`,
|
|
slug,
|
|
description: `Test park for ${testId}`,
|
|
park_type: 'theme_park',
|
|
status: 'operating',
|
|
opening_date: '2000-01-01',
|
|
opening_date_precision: 'year',
|
|
location: {
|
|
name: 'Test Location',
|
|
city: 'Test City',
|
|
country: 'US',
|
|
latitude: 40.7128,
|
|
longitude: -74.0060,
|
|
display_name: 'Test City, US',
|
|
},
|
|
is_test_data: true,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate unique ride submission data
|
|
*/
|
|
export function generateUniqueRideData(parkId: string, testId: string): any {
|
|
const timestamp = Date.now();
|
|
const slug = `test-ride-${testId}-${timestamp}`;
|
|
|
|
return {
|
|
name: `Test Ride ${testId} ${timestamp}`,
|
|
slug,
|
|
description: `Test ride for ${testId}`,
|
|
category: 'roller_coaster',
|
|
status: 'operating',
|
|
park_id: parkId,
|
|
opening_date: '2005-01-01',
|
|
opening_date_precision: 'year',
|
|
max_speed_kmh: 100,
|
|
max_height_meters: 50,
|
|
length_meters: 1000,
|
|
is_test_data: true,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate unique company submission data
|
|
*/
|
|
export function generateUniqueCompanyData(companyType: string, testId: string): any {
|
|
const timestamp = Date.now();
|
|
const slug = `test-${companyType}-${testId}-${timestamp}`;
|
|
|
|
return {
|
|
name: `Test ${companyType} ${testId} ${timestamp}`,
|
|
slug,
|
|
description: `Test ${companyType} for ${testId}`,
|
|
person_type: 'company',
|
|
founded_year: 1990,
|
|
is_test_data: true,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate unique ride model submission data
|
|
*/
|
|
export function generateUniqueRideModelData(manufacturerId: string, testId: string): any {
|
|
const timestamp = Date.now();
|
|
const slug = `test-model-${testId}-${timestamp}`;
|
|
|
|
return {
|
|
name: `Test Model ${testId} ${timestamp}`,
|
|
slug,
|
|
manufacturer_id: manufacturerId,
|
|
category: 'roller_coaster',
|
|
ride_type: 'steel',
|
|
description: `Test ride model for ${testId}`,
|
|
is_test_data: true,
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// SUBMISSION CREATION HELPERS
|
|
// ============================================
|
|
|
|
/**
|
|
* Create a test park submission
|
|
*/
|
|
export async function createTestParkSubmission(
|
|
data: any,
|
|
userId: string,
|
|
tracker: TestDataTracker
|
|
): Promise<{ submissionId: string; itemId: string }> {
|
|
const result = await submitParkCreation(data, userId);
|
|
|
|
if (!result.submissionId) {
|
|
throw new Error('Park submission creation failed - no submission ID returned');
|
|
}
|
|
|
|
// Track submission for cleanup
|
|
tracker.track('content_submissions', result.submissionId);
|
|
|
|
// Get the submission item ID
|
|
const { data: items } = await supabase
|
|
.from('submission_items')
|
|
.select('id')
|
|
.eq('submission_id', result.submissionId)
|
|
.single();
|
|
|
|
if (!items?.id) {
|
|
throw new Error('Failed to get submission item ID');
|
|
}
|
|
|
|
tracker.track('submission_items', items.id);
|
|
|
|
return {
|
|
submissionId: result.submissionId,
|
|
itemId: items.id,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a test ride submission
|
|
*/
|
|
export async function createTestRideSubmission(
|
|
data: any,
|
|
userId: string,
|
|
tracker: TestDataTracker
|
|
): Promise<{ submissionId: string; itemId: string }> {
|
|
const result = await submitRideCreation(data, userId);
|
|
|
|
if (!result.submissionId) {
|
|
throw new Error('Ride submission creation failed - no submission ID returned');
|
|
}
|
|
|
|
tracker.track('content_submissions', result.submissionId);
|
|
|
|
const { data: items } = await supabase
|
|
.from('submission_items')
|
|
.select('id')
|
|
.eq('submission_id', result.submissionId)
|
|
.single();
|
|
|
|
if (!items?.id) {
|
|
throw new Error('Failed to get submission item ID');
|
|
}
|
|
|
|
tracker.track('submission_items', items.id);
|
|
|
|
return {
|
|
submissionId: result.submissionId,
|
|
itemId: items.id,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a test company submission
|
|
*/
|
|
export async function createTestCompanySubmission(
|
|
companyType: 'manufacturer' | 'operator' | 'designer' | 'property_owner',
|
|
data: any,
|
|
userId: string,
|
|
tracker: TestDataTracker
|
|
): Promise<{ submissionId: string; itemId: string }> {
|
|
// Call the appropriate company type-specific submission function
|
|
let result: { submitted: boolean; submissionId: string };
|
|
|
|
switch (companyType) {
|
|
case 'manufacturer':
|
|
result = await submitManufacturerCreation(data, userId);
|
|
break;
|
|
case 'operator':
|
|
result = await submitOperatorCreation(data, userId);
|
|
break;
|
|
case 'designer':
|
|
result = await submitDesignerCreation(data, userId);
|
|
break;
|
|
case 'property_owner':
|
|
result = await submitPropertyOwnerCreation(data, userId);
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown company type: ${companyType}`);
|
|
}
|
|
|
|
if (!result.submissionId) {
|
|
throw new Error('Company submission creation failed - no submission ID returned');
|
|
}
|
|
|
|
tracker.track('content_submissions', result.submissionId);
|
|
|
|
const { data: items } = await supabase
|
|
.from('submission_items')
|
|
.select('id')
|
|
.eq('submission_id', result.submissionId)
|
|
.single();
|
|
|
|
if (!items?.id) {
|
|
throw new Error('Failed to get submission item ID');
|
|
}
|
|
|
|
tracker.track('submission_items', items.id);
|
|
|
|
return {
|
|
submissionId: result.submissionId,
|
|
itemId: items.id,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a test ride model submission
|
|
*/
|
|
export async function createTestRideModelSubmission(
|
|
data: any,
|
|
userId: string,
|
|
tracker: TestDataTracker
|
|
): Promise<{ submissionId: string; itemId: string }> {
|
|
const result = await submitRideModelCreation(data, userId);
|
|
|
|
if (!result.submissionId) {
|
|
throw new Error('Ride model submission creation failed - no submission ID returned');
|
|
}
|
|
|
|
tracker.track('content_submissions', result.submissionId);
|
|
|
|
const { data: items } = await supabase
|
|
.from('submission_items')
|
|
.select('id')
|
|
.eq('submission_id', result.submissionId)
|
|
.single();
|
|
|
|
if (!items?.id) {
|
|
throw new Error('Failed to get submission item ID');
|
|
}
|
|
|
|
tracker.track('submission_items', items.id);
|
|
|
|
return {
|
|
submissionId: result.submissionId,
|
|
itemId: items.id,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a composite submission with dependencies
|
|
*/
|
|
export async function createCompositeSubmission(
|
|
primaryEntity: { type: 'park' | 'ride'; data: any },
|
|
dependencies: Array<{ type: string; data: any; tempId: string; companyType?: string }>,
|
|
userId: string,
|
|
tracker: TestDataTracker
|
|
): Promise<{ submissionId: string; itemIds: string[] }> {
|
|
// Create main submission
|
|
const { data: submission, error: submissionError } = await supabase
|
|
.from('content_submissions')
|
|
.insert({
|
|
user_id: userId,
|
|
submission_type: primaryEntity.type === 'park' ? 'park_create' : 'ride_create',
|
|
status: 'pending',
|
|
is_test_data: true,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (submissionError || !submission) {
|
|
throw new Error(`Failed to create submission: ${submissionError?.message}`);
|
|
}
|
|
|
|
tracker.track('content_submissions', submission.id);
|
|
|
|
const itemIds: string[] = [];
|
|
|
|
// Note: This is a simplified composite submission creation
|
|
// In reality, the actual implementation uses specialized submission tables
|
|
// (park_submissions, company_submissions, etc.) which are more complex
|
|
// For testing purposes, we'll track items but note this is incomplete
|
|
|
|
// Track submission for cleanup
|
|
itemIds.push(submission.id);
|
|
|
|
return {
|
|
submissionId: submission.id,
|
|
itemIds,
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// APPROVAL INVOCATION
|
|
// ============================================
|
|
|
|
/**
|
|
* Approve submission via edge function
|
|
*/
|
|
export async function approveSubmission(
|
|
submissionId: string,
|
|
itemIds: string[],
|
|
authToken: string,
|
|
idempotencyKey?: string
|
|
): Promise<{
|
|
success: boolean;
|
|
status?: string;
|
|
error?: string;
|
|
duration: number;
|
|
}> {
|
|
const startTime = performance.now();
|
|
|
|
const key = idempotencyKey || `test-${Date.now()}-${Math.random()}`;
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`${getEdgeFunctionUrl()}/process-selective-approval`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${authToken}`,
|
|
'Content-Type': 'application/json',
|
|
'apikey': getSupabaseAnonKey(),
|
|
},
|
|
body: JSON.stringify({
|
|
submissionId,
|
|
itemIds,
|
|
idempotencyKey: key,
|
|
}),
|
|
}
|
|
);
|
|
|
|
const duration = performance.now() - startTime;
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
return {
|
|
success: false,
|
|
error: `HTTP ${response.status}: ${errorText}`,
|
|
duration,
|
|
};
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
return {
|
|
success: true,
|
|
status: result.status || 'approved',
|
|
duration,
|
|
};
|
|
} catch (error) {
|
|
const duration = performance.now() - startTime;
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
duration,
|
|
};
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// POLLING & VERIFICATION
|
|
// ============================================
|
|
|
|
/**
|
|
* Poll for entity creation
|
|
*/
|
|
export async function pollForEntity(
|
|
table: 'parks' | 'rides' | 'companies' | 'ride_models',
|
|
id: string,
|
|
maxWaitMs: number = 10000
|
|
): Promise<any | null> {
|
|
const pollInterval = 200;
|
|
const startTime = Date.now();
|
|
|
|
while (Date.now() - startTime < maxWaitMs) {
|
|
const { data, error } = await supabase
|
|
.from(table)
|
|
.select('*')
|
|
.eq('id', id)
|
|
.single();
|
|
|
|
if (data && !error) {
|
|
return data;
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Poll for version creation
|
|
*/
|
|
export async function pollForVersion(
|
|
entityType: 'park' | 'ride' | 'company' | 'ride_model',
|
|
entityId: string,
|
|
expectedVersionNumber: number,
|
|
maxWaitMs: number = 10000
|
|
): Promise<any | null> {
|
|
const versionTable = `${entityType}_versions` as 'park_versions' | 'ride_versions' | 'company_versions' | 'ride_model_versions';
|
|
const pollInterval = 200;
|
|
const startTime = Date.now();
|
|
|
|
while (Date.now() - startTime < maxWaitMs) {
|
|
const { data, error } = await supabase
|
|
.from(versionTable)
|
|
.select('*')
|
|
.eq(`${entityType}_id`, entityId)
|
|
.eq('version_number', expectedVersionNumber)
|
|
.single();
|
|
|
|
if (data && !error) {
|
|
return data;
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Verify submission item is approved
|
|
*/
|
|
export async function verifySubmissionItemApproved(
|
|
itemId: string
|
|
): Promise<{ approved: boolean; entityId: string | null; error?: string }> {
|
|
const { data, error } = await supabase
|
|
.from('submission_items')
|
|
.select('status, approved_entity_id')
|
|
.eq('id', itemId)
|
|
.single();
|
|
|
|
if (error) {
|
|
return { approved: false, entityId: null, error: error.message };
|
|
}
|
|
|
|
return {
|
|
approved: data.status === 'approved' && !!data.approved_entity_id,
|
|
entityId: data.approved_entity_id,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Verify submission status
|
|
*/
|
|
export async function verifySubmissionStatus(
|
|
submissionId: string,
|
|
expectedStatus: 'approved' | 'partially_approved' | 'pending'
|
|
): Promise<boolean> {
|
|
const { data, error } = await supabase
|
|
.from('content_submissions')
|
|
.select('status')
|
|
.eq('id', submissionId)
|
|
.single();
|
|
|
|
if (error || !data) {
|
|
return false;
|
|
}
|
|
|
|
return data.status === expectedStatus;
|
|
}
|
|
|
|
/**
|
|
* Create entity directly (bypass moderation for setup)
|
|
*/
|
|
export async function createParkDirectly(
|
|
data: any,
|
|
tracker: TestDataTracker
|
|
): Promise<string> {
|
|
// First create location if provided
|
|
let locationId: string | undefined;
|
|
|
|
if (data.location) {
|
|
const { data: location, error: locError } = await supabase
|
|
.from('locations')
|
|
.insert({
|
|
name: data.location.name,
|
|
city: data.location.city,
|
|
country: data.location.country,
|
|
latitude: data.location.latitude,
|
|
longitude: data.location.longitude,
|
|
display_name: data.location.display_name,
|
|
is_test_data: true,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (locError || !location) {
|
|
throw new Error(`Failed to create location: ${locError?.message}`);
|
|
}
|
|
|
|
locationId = location.id;
|
|
tracker.track('locations', locationId);
|
|
}
|
|
|
|
const parkData = { ...data };
|
|
delete parkData.location;
|
|
if (locationId) {
|
|
parkData.location_id = locationId;
|
|
}
|
|
|
|
const { data: park, error } = await supabase
|
|
.from('parks')
|
|
.insert(parkData)
|
|
.select()
|
|
.single();
|
|
|
|
if (error || !park) {
|
|
throw new Error(`Failed to create park directly: ${error?.message}`);
|
|
}
|
|
|
|
tracker.track('parks', park.id);
|
|
return park.id;
|
|
}
|
|
|
|
/**
|
|
* Create ride directly (bypass moderation for setup)
|
|
*/
|
|
export async function createRideDirectly(
|
|
data: any,
|
|
tracker: TestDataTracker
|
|
): Promise<string> {
|
|
const { data: ride, error } = await supabase
|
|
.from('rides')
|
|
.insert(data)
|
|
.select()
|
|
.single();
|
|
|
|
if (error || !ride) {
|
|
throw new Error(`Failed to create ride directly: ${error?.message}`);
|
|
}
|
|
|
|
tracker.track('rides', ride.id);
|
|
return ride.id;
|
|
}
|
|
|
|
/**
|
|
* Create test photo gallery submission
|
|
*/
|
|
export async function createTestPhotoGallerySubmission(
|
|
entityId: string,
|
|
entityType: 'park' | 'ride',
|
|
photoCount: number,
|
|
userId: string,
|
|
tracker: TestDataTracker
|
|
): Promise<{ submissionId: string; itemId: string }> {
|
|
// Create content submission first
|
|
const { data: submission, error: submissionError } = await supabase
|
|
.from('content_submissions')
|
|
.insert({
|
|
user_id: userId,
|
|
submission_type: 'photo_gallery',
|
|
status: 'pending',
|
|
is_test_data: true,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (submissionError || !submission) {
|
|
throw new Error(`Failed to create content submission: ${submissionError?.message}`);
|
|
}
|
|
|
|
tracker.track('content_submissions', submission.id);
|
|
|
|
// Create photo submission
|
|
const { data: photoSubmission, error: photoSubError } = await supabase
|
|
.from('photo_submissions')
|
|
.insert({
|
|
entity_id: entityId,
|
|
entity_type: entityType,
|
|
submission_id: submission.id,
|
|
is_test_data: true,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (photoSubError || !photoSubmission) {
|
|
throw new Error(`Failed to create photo submission: ${photoSubError?.message}`);
|
|
}
|
|
|
|
tracker.track('photo_submissions', photoSubmission.id);
|
|
|
|
// Create submission item linking to photo submission
|
|
const { data: item, error: itemError } = await supabase
|
|
.from('submission_items')
|
|
.insert({
|
|
submission_id: submission.id,
|
|
photo_submission_id: photoSubmission.id,
|
|
item_type: 'photo_gallery',
|
|
status: 'pending',
|
|
is_test_data: true,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (itemError || !item) {
|
|
throw new Error(`Failed to create submission item: ${itemError?.message}`);
|
|
}
|
|
|
|
tracker.track('submission_items', item.id);
|
|
|
|
// Create photo submission items
|
|
for (let i = 0; i < photoCount; i++) {
|
|
const { data: photoItem, error: photoItemError } = await supabase
|
|
.from('photo_submission_items')
|
|
.insert({
|
|
photo_submission_id: photoSubmission.id,
|
|
cloudflare_image_id: `test-image-${Date.now()}-${i}`,
|
|
cloudflare_image_url: `https://test.com/image-${i}.jpg`,
|
|
caption: `Test photo ${i + 1}`,
|
|
order_index: i,
|
|
is_test_data: true,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (photoItemError || !photoItem) {
|
|
throw new Error(`Failed to create photo item ${i}: ${photoItemError?.message}`);
|
|
}
|
|
|
|
tracker.track('photo_submission_items', photoItem.id);
|
|
}
|
|
|
|
return {
|
|
submissionId: submission.id,
|
|
itemId: item.id,
|
|
};
|
|
}
|