SPB Git

Repository Public

test-db.test.ts 4.6 KB 59 lines typescript

1// V8 hardening (C): the shared test-database helper. Names are unique per checkout and per run; a database is created empty2// and dropped again; nothing but spw_t_* databases can ever be dropped by it.3import { readdirSync, readFileSync } from 'node:fs';4import path from 'node:path';5import postgres from 'postgres';6import { describe, expect, it } from 'vitest';7import { createTestDb, dropTestDbs, newRunId, REPO_ROOT, runId, TEST_DB_PREFIX, testDbName } from './test-db.ts';89describe('test databases', () => {10  it('a name carries the run id (start time, checkout hash, random) and the label; two runs never share one', () => {11    expect(process.env.SPW_TEST_RUN, 'vitest global setup sets the run id').toMatch(/^[0-9a-z]+_[0-9a-f]{8}$/);12    expect(runId()).toBe(process.env.SPW_TEST_RUN);13    expect(testDbName('api')).toBe(`${TEST_DB_PREFIX}${process.env.SPW_TEST_RUN}_api`);14    const a = newRunId(), b = newRunId();15    expect(a).not.toBe(b);16    expect(a.split('_')[1]!.slice(0, 4)).toBe(b.split('_')[1]!.slice(0, 4));   // same checkout: same path hash17    expect(testDbName('x'.repeat(30)).length).toBeLessThanOrEqual(63);          // PostgreSQL's identifier limit18    for (const bad of ['', 'UPPER', 'a-b', 'a b', 'x'.repeat(31), 'a;drop']) expect(() => testDbName(bad), bad).toThrow(/label/);19  });2021  it('creates an empty database, drops it, and never matches a database outside the prefix', async () => {22    const db = await createTestDb('helper_selftest'), sql = postgres(db.url, { max: 1, onnotice: () => {} });23    expect((await sql`select count(*)::int as n from information_schema.tables where table_schema = 'public'`)[0]!.n).toBe(0);24    await sql`create table t (x int)`; await sql.end();25    const again = await createTestDb('helper_selftest'), sql2 = postgres(again.url, { max: 1, onnotice: () => {} });   // same label: recreated empty26    expect((await sql2`select count(*)::int as n from information_schema.tables where table_schema = 'public'`)[0]!.n).toBe(0);27    await sql2.end();28    await db.drop();29    const admin = postgres('postgres:///postgres', { max: 1, onnotice: () => {} });30    expect(await admin`select 1 from pg_database where datname = ${db.name}`).toHaveLength(0);31    await admin.end();32    const seen: string[] = [];33    await dropTestDbs((n) => { seen.push(n); return false; });34    for (const n of seen) expect(n.startsWith(TEST_DB_PREFIX)).toBe(true);35  }, 180_000);3637  // v10 integration: the convention is CHECKED, so a test file merged from another branch cannot bring a fixed name back38  // (bridge-sol.test.ts and derivatives-gmx.test.ts arrived from `main` with spw_test_cctp_sol / spw_deriv_gmx_test).39  it('no test file of the vitest suite names a database itself: every one comes from this helper', () => {40    const files: string[] = [];41    const walk = (dir: string): void => { for (const e of readdirSync(dir, { withFileTypes: true })) { if (e.name === 'node_modules' || e.name === 'fixtures') continue; const p = path.join(dir, e.name); if (e.isDirectory()) walk(p); else if (/\.(ts|mts)$/.test(e.name)) files.push(p); } };42    for (const group of ['packages', 'services', 'apps']) for (const pkg of readdirSync(path.join(REPO_ROOT, group), { withFileTypes: true })) {43      const dir = path.join(REPO_ROOT, group, pkg.name, 'test'); try { walk(dir); } catch { /* a package without tests */ }44    }45    expect(files.length).toBeGreaterThan(100);46    const offenders: string[] = [];47    for (const f of files) {48      const rel = path.relative(REPO_ROOT, f); if (rel === 'services/api/test/test-db.ts' || rel === 'services/api/test/test-db.test.ts') continue;49      const code = readFileSync(f, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '$1');   // comments may still tell the history50      // a literal database name in a URL (not the placeholder of files that never query, not a name interpolated from the helper), a database created or dropped by hand, an admin connection, a legacy name51      for (const m of code.matchAll(/postgres(?:ql)?:\/\/\/?([A-Za-z0-9_$\{\}.:@-]*)/g)) if (/^[A-Za-z0-9_]+$/.test(m[1]!) && m[1] !== 'unused') offenders.push(`${rel}: ${m[0]}`);52      if (/create database|drop database|\bcreatedb\b|\bdropdb\b/i.test(code)) offenders.push(`${rel}: creates or drops a database itself`);53      for (const m of code.matchAll(/['"`](spw_[a-z0-9_]*test[a-z0-9_]*)['"`]/g)) offenders.push(`${rel}: fixed name ${m[1]}`);54      for (const m of code.matchAll(/\b(?:useDatabase|useTestDatabase|createTestDb|testDbName|alertsWorld)\(\s*['"`]([^'"`]*)['"`]/g)) if (/^spw_|_test$/.test(m[1]!)) offenders.push(`${rel}: label ${m[1]} looks like a fixed database name`);55    }56    expect(offenders).toEqual([]);57  });58});59
Files main
Menu

Theme