PSA/server/test-utils/dbConfig.ts
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

169 lines
5.3 KiB
TypeScript

import { Knex, knex } from 'knex';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import dotenv from 'dotenv';
import { getSecret } from '../src/lib/utils/getSecret';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const serverRoot = path.resolve(__dirname, '..');
const PRODUCTION_DB_NAMES = ['sebastian_prod', 'production', 'prod', 'server'];
const TEST_DB_NAME = 'test_database';
export interface CreateTestDbConnectionOptions {
databaseName?: string;
migrationsDir?: string;
seedsDir?: string;
runSeeds?: boolean;
}
export function verifyTestDatabase(dbName: string): void {
if (PRODUCTION_DB_NAMES.includes(dbName.toLowerCase())) {
throw new Error(`Attempting to use production database (${dbName}) for testing`);
}
}
export async function createTestDbConnection(
options: CreateTestDbConnectionOptions = {}
): Promise<Knex> {
const databaseName = options.databaseName || TEST_DB_NAME;
const migrationsDir = options.migrationsDir || path.join(serverRoot, 'migrations');
const seedsDir = options.seedsDir || path.join(serverRoot, 'seeds', 'dev');
const runSeeds = options.runSeeds ?? true;
verifyTestDatabase(databaseName);
const dbHost = process.env.DB_HOST || 'localhost';
const dbPort = parseInt(process.env.DB_PORT || '5432', 10);
const adminUser = process.env.DB_USER_ADMIN || 'postgres';
const adminPassword = await getSecret('postgres_password', 'DB_PASSWORD_ADMIN', 'postpass123');
const appUser = process.env.DB_USER_SERVER || 'app_user';
const appPassword = await getSecret('db_password_server', 'DB_PASSWORD_SERVER', 'postpass123');
await recreateDatabase(databaseName, dbHost, dbPort, adminUser, adminPassword, appUser, appPassword);
process.env.DB_HOST = dbHost;
process.env.DB_PORT = String(dbPort);
process.env.DB_NAME_SERVER = databaseName;
process.env.DB_USER_SERVER = appUser;
process.env.DB_USER_ADMIN = adminUser;
const adminKnex = knex({
client: 'pg',
connection: {
host: dbHost,
port: dbPort,
user: adminUser,
password: adminPassword,
database: databaseName,
},
migrations: {
directory: migrationsDir,
},
seeds: {
directory: seedsDir,
},
});
// Citus-distribution probes (SELECT ... FROM pg_dist_partition) run inside
// dozens of migrations; on plain Postgres each one ERRORs server-side before
// its try/catch concludes "not Citus". An empty stand-in catalog makes every
// probe succeed with is_distributed=false — same behavior, silent logs.
await adminKnex.raw('CREATE TABLE IF NOT EXISTS public.pg_dist_partition (logicalrelid regclass)');
await adminKnex.migrate.latest();
if (runSeeds) {
await adminKnex.seed.run();
}
// The DB-guardrail migration sets cluster-wide role GUCs
// (idle_in_transaction_session_timeout, lock_timeout) on the app role.
// They are production insurance; in tests they turn legitimate lock waits
// and slow in-transaction work into spurious timeouts. Reset them after
// every bootstrap (the migration re-sets them each run).
const safeAppUser = appUser.replace(/[^a-zA-Z0-9_]/g, '');
await adminKnex.raw(`ALTER ROLE ${safeAppUser} RESET idle_in_transaction_session_timeout`);
await adminKnex.raw(`ALTER ROLE ${safeAppUser} RESET lock_timeout`);
await adminKnex.destroy();
const db = knex({
client: 'pg',
connection: {
host: dbHost,
port: dbPort,
user: appUser,
password: appPassword,
database: databaseName,
},
asyncStackTraces: true,
pool: {
min: 2,
max: 20,
},
});
return db;
}
async function recreateDatabase(
databaseName: string,
dbHost: string,
dbPort: number,
adminUser: string,
adminPassword: string,
appUser: string,
appPassword: string
): Promise<void> {
const adminConnection = knex({
client: 'pg',
connection: {
host: dbHost,
port: dbPort,
user: adminUser,
password: adminPassword,
database: 'postgres',
},
pool: {
min: 1,
max: 2,
},
});
try {
const safeDbName = databaseName.replace(/"/g, '""');
await adminConnection.raw(
'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = ? AND pid <> pg_backend_pid()',
[databaseName]
);
await adminConnection.raw(`DROP DATABASE IF EXISTS "${safeDbName}"`);
await adminConnection.raw(`CREATE DATABASE "${safeDbName}"`);
await adminConnection.raw(`DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${appUser}') THEN
CREATE ROLE ${appUser} WITH LOGIN PASSWORD '${appPassword}';
ELSE
ALTER ROLE ${appUser} WITH LOGIN PASSWORD '${appPassword}';
END IF;
END;
$$;`);
await adminConnection.raw(`ALTER DATABASE "${safeDbName}" OWNER TO ${appUser}`);
await adminConnection.raw(`GRANT ALL PRIVILEGES ON DATABASE "${safeDbName}" TO ${appUser}`);
} finally {
await adminConnection.destroy().catch(() => undefined);
}
}
export async function createTestDbConnectionWithTenant(tenant: string): Promise<Knex> {
return createTestDbConnection();
}
export function isValidTenantId(tenantId: string): boolean {
if (!tenantId) return true;
if (tenantId === 'default') return true;
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(tenantId);
}