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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
169 lines
5.3 KiB
TypeScript
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);
|
|
}
|