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