diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index f1ed42b6..86074cb9 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -194,6 +194,7 @@ export type Database = { founded_year: number | null headquarters_location: string | null id: string + is_test_data: boolean | null logo_url: string | null name: string person_type: string | null @@ -219,6 +220,7 @@ export type Database = { founded_year?: number | null headquarters_location?: string | null id?: string + is_test_data?: boolean | null logo_url?: string | null name: string person_type?: string | null @@ -244,6 +246,7 @@ export type Database = { founded_year?: number | null headquarters_location?: string | null id?: string + is_test_data?: boolean | null logo_url?: string | null name?: string person_type?: string | null @@ -349,6 +352,7 @@ export type Database = { founded_year: number | null headquarters_location: string | null is_current: boolean + is_test_data: boolean | null logo_url: string | null name: string person_type: string | null @@ -375,6 +379,7 @@ export type Database = { founded_year?: number | null headquarters_location?: string | null is_current?: boolean + is_test_data?: boolean | null logo_url?: string | null name: string person_type?: string | null @@ -401,6 +406,7 @@ export type Database = { founded_year?: number | null headquarters_location?: string | null is_current?: boolean + is_test_data?: boolean | null logo_url?: string | null name?: string person_type?: string | null @@ -628,6 +634,7 @@ export type Database = { escalation_reason: string | null first_reviewed_at: string | null id: string + is_test_data: boolean | null locked_until: string | null original_submission_id: string | null resolved_at: string | null @@ -653,6 +660,7 @@ export type Database = { escalation_reason?: string | null first_reviewed_at?: string | null id?: string + is_test_data?: boolean | null locked_until?: string | null original_submission_id?: string | null resolved_at?: string | null @@ -678,6 +686,7 @@ export type Database = { escalation_reason?: string | null first_reviewed_at?: string | null id?: string + is_test_data?: boolean | null locked_until?: string | null original_submission_id?: string | null resolved_at?: string | null @@ -1509,6 +1518,7 @@ export type Database = { description: string | null email: string | null is_current: boolean + is_test_data: boolean | null location_id: string | null name: string opening_date: string | null @@ -1539,6 +1549,7 @@ export type Database = { description?: string | null email?: string | null is_current?: boolean + is_test_data?: boolean | null location_id?: string | null name: string opening_date?: string | null @@ -1569,6 +1580,7 @@ export type Database = { description?: string | null email?: string | null is_current?: boolean + is_test_data?: boolean | null location_id?: string | null name?: string opening_date?: string | null @@ -1651,6 +1663,7 @@ export type Database = { description: string | null email: string | null id: string + is_test_data: boolean | null location_id: string | null name: string opening_date: string | null @@ -1682,6 +1695,7 @@ export type Database = { description?: string | null email?: string | null id?: string + is_test_data?: boolean | null location_id?: string | null name: string opening_date?: string | null @@ -1713,6 +1727,7 @@ export type Database = { description?: string | null email?: string | null id?: string + is_test_data?: boolean | null location_id?: string | null name?: string opening_date?: string | null @@ -1866,6 +1881,7 @@ export type Database = { entity_type: string id: string is_featured: boolean | null + is_test_data: boolean | null order_index: number | null photographer_credit: string | null submission_id: string | null @@ -1886,6 +1902,7 @@ export type Database = { entity_type: string id?: string is_featured?: boolean | null + is_test_data?: boolean | null order_index?: number | null photographer_credit?: string | null submission_id?: string | null @@ -1906,6 +1923,7 @@ export type Database = { entity_type?: string id?: string is_featured?: boolean | null + is_test_data?: boolean | null order_index?: number | null photographer_credit?: string | null submission_id?: string | null @@ -2301,6 +2319,7 @@ export type Database = { created_at: string helpful_votes: number | null id: string + is_test_data: boolean | null moderated_at: string | null moderated_by: string | null moderation_status: string @@ -2321,6 +2340,7 @@ export type Database = { created_at?: string helpful_votes?: number | null id?: string + is_test_data?: boolean | null moderated_at?: string | null moderated_by?: string | null moderation_status?: string @@ -2341,6 +2361,7 @@ export type Database = { created_at?: string helpful_votes?: number | null id?: string + is_test_data?: boolean | null moderated_at?: string | null moderated_by?: string | null moderation_status?: string @@ -2788,6 +2809,7 @@ export type Database = { created_by: string | null description: string | null is_current: boolean + is_test_data: boolean | null manufacturer_id: string | null name: string ride_model_id: string @@ -2809,6 +2831,7 @@ export type Database = { created_by?: string | null description?: string | null is_current?: boolean + is_test_data?: boolean | null manufacturer_id?: string | null name: string ride_model_id: string @@ -2830,6 +2853,7 @@ export type Database = { created_by?: string | null description?: string | null is_current?: boolean + is_test_data?: boolean | null manufacturer_id?: string | null name?: string ride_model_id?: string @@ -2887,6 +2911,7 @@ export type Database = { created_at: string description: string | null id: string + is_test_data: boolean | null manufacturer_id: string name: string ride_type: string @@ -2902,6 +2927,7 @@ export type Database = { created_at?: string description?: string | null id?: string + is_test_data?: boolean | null manufacturer_id: string name: string ride_type: string @@ -2917,6 +2943,7 @@ export type Database = { created_at?: string description?: string | null id?: string + is_test_data?: boolean | null manufacturer_id?: string name?: string ride_type?: string @@ -3364,6 +3391,7 @@ export type Database = { intensity_level: string | null inversions_count: number | null is_current: boolean + is_test_data: boolean | null length_meters: number | null manufacturer_id: string | null max_age: number | null @@ -3439,6 +3467,7 @@ export type Database = { intensity_level?: string | null inversions_count?: number | null is_current?: boolean + is_test_data?: boolean | null length_meters?: number | null manufacturer_id?: string | null max_age?: number | null @@ -3514,6 +3543,7 @@ export type Database = { intensity_level?: string | null inversions_count?: number | null is_current?: boolean + is_test_data?: boolean | null length_meters?: number | null manufacturer_id?: string | null max_age?: number | null @@ -3688,6 +3718,7 @@ export type Database = { image_url: string | null intensity_level: string | null inversions: number | null + is_test_data: boolean | null length_meters: number | null manufacturer_id: string | null max_age: number | null @@ -3762,6 +3793,7 @@ export type Database = { image_url?: string | null intensity_level?: string | null inversions?: number | null + is_test_data?: boolean | null length_meters?: number | null manufacturer_id?: string | null max_age?: number | null @@ -3836,6 +3868,7 @@ export type Database = { image_url?: string | null intensity_level?: string | null inversions?: number | null + is_test_data?: boolean | null length_meters?: number | null manufacturer_id?: string | null max_age?: number | null @@ -3950,6 +3983,7 @@ export type Database = { created_at: string depends_on: string | null id: string + is_test_data: boolean | null item_data: Json item_type: string order_index: number | null @@ -3965,6 +3999,7 @@ export type Database = { created_at?: string depends_on?: string | null id?: string + is_test_data?: boolean | null item_data: Json item_type: string order_index?: number | null @@ -3980,6 +4015,7 @@ export type Database = { created_at?: string depends_on?: string | null id?: string + is_test_data?: boolean | null item_data?: Json item_type?: string order_index?: number | null @@ -4013,6 +4049,7 @@ export type Database = { entity_slug: string entity_type: string id: string + is_test_data: boolean | null metadata: Json | null submission_item_id: string | null test_session_id: string | null @@ -4023,6 +4060,7 @@ export type Database = { entity_slug: string entity_type: string id?: string + is_test_data?: boolean | null metadata?: Json | null submission_item_id?: string | null test_session_id?: string | null @@ -4033,6 +4071,7 @@ export type Database = { entity_slug?: string entity_type?: string id?: string + is_test_data?: boolean | null metadata?: Json | null submission_item_id?: string | null test_session_id?: string | null diff --git a/src/lib/entityTransformers.ts b/src/lib/entityTransformers.ts index 626f019a..e7d922b6 100644 --- a/src/lib/entityTransformers.ts +++ b/src/lib/entityTransformers.ts @@ -39,6 +39,7 @@ export function transformParkData(submissionData: ParkSubmissionData): ParkInser review_count: 0, ride_count: 0, coaster_count: 0, + is_test_data: submissionData.is_test_data || false, }; } @@ -120,6 +121,7 @@ export function transformRideData(submissionData: RideSubmissionData): RideInser image_url: submissionData.image_url || null, average_rating: 0, review_count: 0, + is_test_data: submissionData.is_test_data || false, }; } @@ -145,6 +147,7 @@ export function transformCompanyData( logo_url: submissionData.logo_url || null, average_rating: 0, review_count: 0, + is_test_data: submissionData.is_test_data || false, }; } @@ -167,6 +170,7 @@ export function transformRideModelData(submissionData: RideModelSubmissionData): banner_image_id: submissionData.banner_image_id || null, card_image_url: submissionData.card_image_url || null, card_image_id: submissionData.card_image_id || null, + is_test_data: submissionData.is_test_data || false, }; } diff --git a/src/lib/integrationTests/TestDataTracker.ts b/src/lib/integrationTests/TestDataTracker.ts new file mode 100644 index 00000000..75a7b0ff --- /dev/null +++ b/src/lib/integrationTests/TestDataTracker.ts @@ -0,0 +1,181 @@ +import { supabase } from '@/integrations/supabase/client'; +import type { Database } from '@/integrations/supabase/types'; + +type TableName = keyof Database['public']['Tables']; + +/** + * TestDataTracker - Manages test data lifecycle for integration tests + * + * Tracks all created test entities and ensures proper cleanup in dependency order. + * All tracked entities are marked with is_test_data=true for easy identification. + */ +export class TestDataTracker { + private entities = new Map>(); + + /** + * Track an entity for cleanup + * @param table - Database table name + * @param id - Entity ID + */ + track(table: string, id: string): void { + if (!this.entities.has(table)) { + this.entities.set(table, new Set()); + } + this.entities.get(table)!.add(id); + } + + /** + * Track multiple entities at once + * @param table - Database table name + * @param ids - Array of entity IDs + */ + trackMany(table: string, ids: string[]): void { + ids.forEach(id => this.track(table, id)); + } + + /** + * Get all tracked entity IDs for a specific table + * @param table - Database table name + * @returns Array of tracked IDs + */ + getTracked(table: string): string[] { + return Array.from(this.entities.get(table) || []); + } + + /** + * Cleanup all tracked test data in proper dependency order + * Deletes children first, then parents to avoid foreign key violations + */ + async cleanup(): Promise { + // Define deletion order (children first, parents last) + const deletionOrder: TableName[] = [ + 'reviews', + 'photos', + 'submission_items', + 'content_submissions', + 'ride_versions', + 'park_versions', + 'company_versions', + 'ride_model_versions', + 'rides', + 'parks', + 'ride_models', + 'companies', + 'test_data_registry' + ]; + + const errors: Array<{ table: string; error: any }> = []; + + for (const table of deletionOrder) { + const ids = this.getTracked(table); + if (ids.length === 0) continue; + + try { + const { error } = await supabase + .from(table as any) + .delete() + .in('id', ids); + + if (error) { + errors.push({ table, error }); + console.warn(`Failed to cleanup ${table}:`, error); + } + } catch (err) { + errors.push({ table, error: err }); + console.warn(`Exception cleaning up ${table}:`, err); + } + } + + // Clear tracking after cleanup attempt + this.entities.clear(); + + if (errors.length > 0) { + console.warn(`Cleanup completed with ${errors.length} errors:`, errors); + } + } + + /** + * Verify that all tracked test data has been cleaned up + * @returns Array of remaining test data items + */ + async verifyCleanup(): Promise> { + const tables: TableName[] = [ + 'parks', 'rides', 'companies', 'ride_models', + 'content_submissions', 'submission_items', + 'park_versions', 'ride_versions', 'company_versions', 'ride_model_versions', + 'photos', 'reviews', 'test_data_registry' + ]; + + const remaining: Array<{ table: string; count: number }> = []; + + for (const table of tables) { + try { + const { count, error } = await supabase + .from(table as any) + .select('*', { count: 'exact', head: true }) + .eq('is_test_data', true); + + if (error) { + console.warn(`Failed to check ${table}:`, error); + continue; + } + + if (count && count > 0) { + remaining.push({ table, count }); + } + } catch (err) { + console.warn(`Exception checking ${table}:`, err); + } + } + + return remaining; + } + + /** + * Bulk delete all test data from the database (emergency cleanup) + * WARNING: This deletes ALL data marked with is_test_data=true + */ + static async bulkCleanupAllTestData(): Promise<{ deleted: number; errors: number }> { + const tables: TableName[] = [ + 'reviews', 'photos', 'submission_items', 'content_submissions', + 'ride_versions', 'park_versions', 'company_versions', 'ride_model_versions', + 'rides', 'parks', 'ride_models', 'companies', 'test_data_registry' + ]; + + let totalDeleted = 0; + let totalErrors = 0; + + for (const table of tables) { + try { + const { error, data } = await supabase + .from(table as any) + .delete() + .eq('is_test_data', true) + .select('id'); + + if (error) { + console.warn(`Failed to bulk delete from ${table}:`, error); + totalErrors++; + } else if (data) { + totalDeleted += data.length; + } + } catch (err) { + console.warn(`Exception bulk deleting from ${table}:`, err); + totalErrors++; + } + } + + return { deleted: totalDeleted, errors: totalErrors }; + } + + /** + * Get summary of tracked entities + */ + getSummary(): Record { + const summary: Record = {}; + this.entities.forEach((ids, table) => { + summary[table] = ids.size; + }); + return summary; + } +} diff --git a/src/lib/testDataGenerator.ts b/src/lib/testDataGenerator.ts index 49376923..6d162963 100644 --- a/src/lib/testDataGenerator.ts +++ b/src/lib/testDataGenerator.ts @@ -127,7 +127,8 @@ export function generateRandomPark(counter: number): ParkSubmissionData { banner_image_url: null, banner_image_id: null, card_image_url: null, - card_image_id: null + card_image_id: null, + is_test_data: true }; } @@ -166,7 +167,8 @@ export function generateRandomRide(parkId: string, counter: number): RideSubmiss banner_image_id: null, card_image_url: null, card_image_id: null, - image_url: null + image_url: null, + is_test_data: true }; } @@ -187,7 +189,8 @@ export function generateRandomCompany(type: 'manufacturer' | 'operator' | 'desig banner_image_url: null, banner_image_id: null, card_image_url: null, - card_image_id: null + card_image_id: null, + is_test_data: true }; } @@ -206,7 +209,8 @@ export function generateRandomRideModel(manufacturerId: string, counter: number) banner_image_url: null, banner_image_id: null, card_image_url: null, - card_image_id: null + card_image_id: null, + is_test_data: true }; } diff --git a/src/types/submission-data.ts b/src/types/submission-data.ts index c4d09a4f..e1a0ebea 100644 --- a/src/types/submission-data.ts +++ b/src/types/submission-data.ts @@ -25,6 +25,7 @@ export interface ParkSubmissionData { card_image_id?: string | null; source_url?: string; submission_notes?: string; + is_test_data?: boolean; } export interface RideSubmissionData { @@ -100,6 +101,7 @@ export interface RideSubmissionData { image_url?: string | null; source_url?: string; submission_notes?: string; + is_test_data?: boolean; } export interface CompanySubmissionData { @@ -120,6 +122,7 @@ export interface CompanySubmissionData { card_image_id?: string | null; source_url?: string; submission_notes?: string; + is_test_data?: boolean; } export interface RideModelSubmissionData { @@ -135,6 +138,7 @@ export interface RideModelSubmissionData { card_image_id?: string | null; source_url?: string; submission_notes?: string; + is_test_data?: boolean; } export interface TimelineEventItemData { diff --git a/supabase/migrations/20251030145804_5ba1aa05-47f3-4438-8a4d-5b4f55297e24.sql b/supabase/migrations/20251030145804_5ba1aa05-47f3-4438-8a4d-5b4f55297e24.sql new file mode 100644 index 00000000..312a701f --- /dev/null +++ b/supabase/migrations/20251030145804_5ba1aa05-47f3-4438-8a4d-5b4f55297e24.sql @@ -0,0 +1,74 @@ +-- Add is_test_data flag to all entity tables for test data identification +-- This allows safe cleanup and filtering of test data from production queries + +-- Add is_test_data column to parks +ALTER TABLE public.parks +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +CREATE INDEX IF NOT EXISTS idx_parks_is_test_data ON public.parks(is_test_data) WHERE is_test_data = true; + +-- Add is_test_data column to rides +ALTER TABLE public.rides +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +CREATE INDEX IF NOT EXISTS idx_rides_is_test_data ON public.rides(is_test_data) WHERE is_test_data = true; + +-- Add is_test_data column to companies +ALTER TABLE public.companies +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +CREATE INDEX IF NOT EXISTS idx_companies_is_test_data ON public.companies(is_test_data) WHERE is_test_data = true; + +-- Add is_test_data column to ride_models +ALTER TABLE public.ride_models +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +CREATE INDEX IF NOT EXISTS idx_ride_models_is_test_data ON public.ride_models(is_test_data) WHERE is_test_data = true; + +-- Add is_test_data column to content_submissions +ALTER TABLE public.content_submissions +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +CREATE INDEX IF NOT EXISTS idx_content_submissions_is_test_data ON public.content_submissions(is_test_data) WHERE is_test_data = true; + +-- Add is_test_data column to submission_items +ALTER TABLE public.submission_items +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +CREATE INDEX IF NOT EXISTS idx_submission_items_is_test_data ON public.submission_items(is_test_data) WHERE is_test_data = true; + +-- Add is_test_data column to version tables +ALTER TABLE public.park_versions +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +ALTER TABLE public.ride_versions +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +ALTER TABLE public.company_versions +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +ALTER TABLE public.ride_model_versions +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +-- Add is_test_data column to photos +ALTER TABLE public.photos +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +CREATE INDEX IF NOT EXISTS idx_photos_is_test_data ON public.photos(is_test_data) WHERE is_test_data = true; + +-- Add is_test_data column to reviews +ALTER TABLE public.reviews +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT false; + +CREATE INDEX IF NOT EXISTS idx_reviews_is_test_data ON public.reviews(is_test_data) WHERE is_test_data = true; + +-- Add is_test_data column to test_data_registry +ALTER TABLE public.test_data_registry +ADD COLUMN IF NOT EXISTS is_test_data BOOLEAN DEFAULT true; + +COMMENT ON COLUMN public.parks.is_test_data IS 'Flag to identify test data for safe cleanup and filtering'; +COMMENT ON COLUMN public.rides.is_test_data IS 'Flag to identify test data for safe cleanup and filtering'; +COMMENT ON COLUMN public.companies.is_test_data IS 'Flag to identify test data for safe cleanup and filtering'; +COMMENT ON COLUMN public.ride_models.is_test_data IS 'Flag to identify test data for safe cleanup and filtering'; +COMMENT ON COLUMN public.content_submissions.is_test_data IS 'Flag to identify test data for safe cleanup and filtering'; +COMMENT ON COLUMN public.submission_items.is_test_data IS 'Flag to identify test data for safe cleanup and filtering'; \ No newline at end of file