diff --git a/apps/api/src/persistence/PersistenceModeVerification.test.ts b/apps/api/src/persistence/PersistenceModeVerification.test.ts new file mode 100644 index 000000000..a43245dbf --- /dev/null +++ b/apps/api/src/persistence/PersistenceModeVerification.test.ts @@ -0,0 +1,144 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; + +/** + * Unit test to verify persistence mode configuration + * This test ensures that the correct persistence modules are loaded + * based on the GRIDPILOT_API_PERSISTENCE environment variable + */ +describe('Persistence Mode Verification', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + // Reset modules before each test + vi.resetModules(); + }); + + afterEach(() => { + // Restore original environment + process.env = { ...originalEnv }; + }); + + it('should load postgres persistence when GRIDPILOT_API_PERSISTENCE=postgres', async () => { + process.env.GRIDPILOT_API_PERSISTENCE = 'postgres'; + process.env.DATABASE_URL = 'postgres://user:pass@localhost:5432/test'; + + const { getApiPersistence } = await import('../env'); + const persistence = getApiPersistence(); + + expect(persistence).toBe('postgres'); + }); + + it('should load inmemory persistence when GRIDPILOT_API_PERSISTENCE=inmemory', async () => { + process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory'; + delete process.env.DATABASE_URL; + + const { getApiPersistence } = await import('../env'); + const persistence = getApiPersistence(); + + expect(persistence).toBe('inmemory'); + }); + + it('should default to inmemory in test environment without DATABASE_URL', async () => { + process.env.NODE_ENV = 'test'; + delete process.env.GRIDPILOT_API_PERSISTENCE; + delete process.env.DATABASE_URL; + + const { getApiPersistence } = await import('../env'); + const persistence = getApiPersistence(); + + expect(persistence).toBe('inmemory'); + }); + + it('should verify all persistence modules have both inmemory and postgres alternatives', async () => { + const fs = require('fs'); + const path = require('path'); + + // Dynamically discover all persistence modules by scanning directories + const inMemoryDir = path.join(__dirname, 'inmemory'); + const postgresDir = path.join(__dirname, 'postgres'); + + // Get all module files from inmemory directory + const inMemoryModules = fs.readdirSync(inMemoryDir) + .filter((file: string) => file.endsWith('PersistenceModule.ts')) + .map((file: string) => file.replace('.ts', '')); + + // Get all module files from postgres directory + const postgresModules = fs.readdirSync(postgresDir) + .filter((file: string) => file.endsWith('PersistenceModule.ts')) + .map((file: string) => file.replace('.ts', '')); + + // Verify we have the same number of modules in both directories + expect(inMemoryModules.length).toBe(postgresModules.length); + expect(inMemoryModules.length).toBeGreaterThan(0); + + // Verify each in-memory module has a corresponding postgres module + for (const inMemoryModule of inMemoryModules) { + // Convert InMemoryX to PostgresX + const postgresModule = inMemoryModule.replace('InMemory', 'Postgres'); + expect(postgresModules).toContain(postgresModule); + + // Verify both files exist + expect(fs.existsSync(path.join(inMemoryDir, `${inMemoryModule}.ts`))).toBe(true); + expect(fs.existsSync(path.join(postgresDir, `${postgresModule}.ts`))).toBe(true); + } + + // Log the discovered modules for verification + console.log('Discovered persistence modules:', { + count: inMemoryModules.length, + modules: inMemoryModules.map((m: string) => m.replace('InMemory', '')) + }); + }); + + it('should verify that postgres persistence modules exist and are properly structured', async () => { + const fs = require('fs'); + const path = require('path'); + + // Verify that PostgresRacingPersistenceModule exists and exports TypeORM repositories + const postgresRacingModulePath = path.join(__dirname, 'postgres', 'PostgresRacingPersistenceModule.ts'); + expect(fs.existsSync(postgresRacingModulePath)).toBe(true); + + // Read the module to verify it uses TypeORM + const moduleContent = fs.readFileSync(postgresRacingModulePath, 'utf-8'); + expect(moduleContent).toContain('TypeOrmModule'); + expect(moduleContent).toContain('TypeOrmDriverRepository'); + expect(moduleContent).toContain('TypeOrmLeagueRepository'); + }); + + it('should verify that in-memory modules exist for comparison', async () => { + const fs = require('fs'); + const path = require('path'); + + // Verify that InMemoryRacingPersistenceModule exists + const inMemoryRacingModulePath = path.join(__dirname, 'inmemory', 'InMemoryRacingPersistenceModule.ts'); + expect(fs.existsSync(inMemoryRacingModulePath)).toBe(true); + + // Read the module to verify it uses InMemory repositories + const moduleContent = fs.readFileSync(inMemoryRacingModulePath, 'utf-8'); + expect(moduleContent).toContain('InMemoryDriverRepository'); + expect(moduleContent).toContain('InMemoryLeagueRepository'); + expect(moduleContent).not.toContain('TypeOrmModule'); + }); + + it('should verify docker-compose.test.yml is configured for PostgreSQL', async () => { + const fs = require('fs'); + const path = require('path'); + + const dockerComposePath = path.join(__dirname, '..', '..', '..', '..', 'docker-compose.test.yml'); + expect(fs.existsSync(dockerComposePath)).toBe(true); + + const content = fs.readFileSync(dockerComposePath, 'utf-8'); + + // Verify PostgreSQL service exists + expect(content).toContain('db:'); + expect(content).toContain('postgres:15-alpine'); + + // Verify API service uses postgres persistence + expect(content).toContain('GRIDPILOT_API_PERSISTENCE=postgres'); + expect(content).toContain('DATABASE_URL='); + + // Verify database connection environment variables + expect(content).toContain('POSTGRES_DB=gridpilot_test'); + expect(content).toContain('POSTGRES_USER=gridpilot_test_user'); + expect(content).toContain('POSTGRES_PASSWORD=gridpilot_test_pass'); + }); +}); \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 5c1c2f8e1..f4fc02636 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,4 +1,25 @@ services: + # PostgreSQL database for real-world testing + db: + image: postgres:15-alpine + environment: + - POSTGRES_DB=gridpilot_test + - POSTGRES_USER=gridpilot_test_user + - POSTGRES_PASSWORD=gridpilot_test_pass + ports: + - "5433:5432" # Use different port to avoid conflicts with dev + volumes: + - test_db_data:/var/lib/postgresql/data + networks: + - gridpilot-test-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U gridpilot_test_user -d gridpilot_test"] + interval: 2s + timeout: 2s + retries: 10 + start_period: 5s + restart: "no" + # Ready check - simple service that verifies dependencies are available ready: image: node:20-alpine @@ -15,26 +36,32 @@ services: - gridpilot-test-network restart: "no" - # Real API server (not mock) + # Real API server with TypeORM/PostgreSQL api: image: node:20-alpine working_dir: /app/apps/api environment: - NODE_ENV=test - PORT=3000 - - GRIDPILOT_API_PERSISTENCE=inmemory + - GRIDPILOT_API_PERSISTENCE=postgres - GRIDPILOT_API_BOOTSTRAP=true - GRIDPILOT_API_FORCE_RESEED=true - GRIDPILOT_FEATURES_JSON={"sponsors.portal":"enabled","admin.dashboard":"enabled"} + - DATABASE_URL=postgres://gridpilot_test_user:gridpilot_test_pass@db:5432/gridpilot_test + - POSTGRES_DB=gridpilot_test + - POSTGRES_USER=gridpilot_test_user + - POSTGRES_PASSWORD=gridpilot_test_pass ports: - "3101:3000" volumes: - ./:/app - /Users/marcmintel/Projects/gridpilot/node_modules:/app/node_modules:ro - command: ["sh", "-lc", "echo '[api] Starting real API...'; npm run start:dev"] + command: ["sh", "-lc", "echo '[api] Starting real API with TypeORM...'; npm run start:dev"] depends_on: ready: condition: service_completed_successfully + db: + condition: service_healthy networks: - gridpilot-test-network restart: unless-stopped @@ -88,4 +115,7 @@ services: networks: gridpilot-test-network: - driver: bridge \ No newline at end of file + driver: bridge + +volumes: + test_db_data: \ No newline at end of file