Production-ready test infrastructure with Page Object Model pattern, reusable fixtures for auth/DB/test-data, and example tests covering login flow, dashboard, accounts CRUD API, and visual regression. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
125 lines
3.8 KiB
TypeScript
125 lines
3.8 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<PoolClient> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
if (this.pool) {
|
|
await this.pool.end();
|
|
this.pool = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const testDb = new TestDatabase();
|