/** * Database fixture for E2E tests. * * Provides helpers to seed and clean up test data in Postgres. * Uses the `pg` driver directly (same driver the backend uses) * to avoid coupling tests to the backend's TypeORM setup. * * Usage in tests: * import { testDb } from '../fixtures/db.fixture'; * test.beforeAll(async () => { await testDb.connect(); }); * test.afterAll(async () => { await testDb.cleanup(); await testDb.disconnect(); }); */ import { Pool, type PoolClient } from 'pg'; import { TEST_PREFIX } from './test-data'; const TEST_DB_URL = process.env.TEST_DB_URL || 'postgresql://hoafinance:change_me@localhost:5432/hoafinance'; class TestDatabase { private pool: Pool | null = null; /** Connect to the test database */ async connect(): Promise { this.pool = new Pool({ connectionString: TEST_DB_URL, max: 5 }); // Verify connectivity const client = await this.pool.connect(); try { await client.query('SELECT 1'); } finally { client.release(); } } /** Get a client from the pool */ async getClient(): Promise { if (!this.pool) throw new Error('TestDatabase not connected'); return this.pool.connect(); } /** Execute a query */ async query(sql: string, params?: unknown[]) { if (!this.pool) throw new Error('TestDatabase not connected'); return this.pool.query(sql, params); } /** * Clean up all test-created data. * Removes rows that match the TEST_PREFIX in name/description/memo fields. * This runs within a transaction so it's atomic. */ async cleanup(): Promise { if (!this.pool) return; const client = await this.pool.connect(); try { await client.query('BEGIN'); // Clean up test data from tenant schemas. // We look for the e2e test org schema if it exists. const schemas = await client.query( `SELECT schema_name FROM shared.organizations WHERE name LIKE '${TEST_PREFIX}%' OR schema_name = 'e2e_test_hoa'`, ); for (const row of schemas.rows) { const schema = row.schema_name; // Delete test journal entries, accounts, etc. in tenant schema await client.query( `DELETE FROM "${schema}".journal_entries WHERE memo LIKE $1`, [`${TEST_PREFIX}%`], ).catch(() => {}); // Table may not exist await client.query( `DELETE FROM "${schema}".accounts WHERE name LIKE $1`, [`${TEST_PREFIX}%`], ).catch(() => {}); } // Clean up test users from shared schema await client.query( `DELETE FROM shared.users WHERE email LIKE 'e2e-%@test.hoaledgeriq.com'`, ).catch(() => {}); await client.query('COMMIT'); } catch (err) { await client.query('ROLLBACK'); console.error('Test cleanup failed:', err); } finally { client.release(); } } /** * Seed the minimum data needed for authenticated test flows: * - A test organization (if not exists) * - A test user with known credentials (if not exists) * - Associates user to org with the given role * * This is idempotent — safe to call multiple times. */ async seedTestUser(user: { email: string; password: string; fullName: string; role: string; }): Promise { if (!this.pool) throw new Error('TestDatabase not connected'); // This is done via API calls in auth.setup.ts instead of direct SQL, // because the backend handles password hashing, schema creation, etc. // This method is available for advanced scenarios that need direct DB access. console.log(`[TestDB] Seed user ${user.email} — use API-based seeding in auth.setup.ts`); } /** Disconnect from the database */ async disconnect(): Promise { if (this.pool) { await this.pool.end(); this.pool = null; } } } export const testDb = new TestDatabase();