import { readFileSync } from 'fs'; import path from 'path'; /** * The guard for #273. * * Three files have to agree about which database the end-to-end suite reads: * `start-local.ps1` chooses it and records it, `run-tests.ps1` reads that record * into the environment, and `frontend/tests/e2e/support/db.ts` opens the * connection from those variables. Nothing compared them, and they disagreed: * with `-E2eDb` the application ran on `redefined_e2e` at 55501 while the helper * kept its `redefined_local` default at 55500, so the app wrote to one database * and the suite read another. * * That is the same shape as #107 and #118, where `envValidation` and a compose * file disagreed and nothing noticed until a deploy refused to boot — and the * same shape again as #287, where the workflow and `start-local.ps1` disagreed * about REMBG_URL and three tests failed on main. The lesson each time is that * a convention shared between two files is not a contract until something * checks it. * * These are text assertions, which is weaker than executing the scripts — a * PowerShell run is not something this suite can do. They are still worth * having: every one of them fails if a name is changed in one file and not the * others, which is the drift that actually happened. */ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..'); function read(relative: string): string { return readFileSync(path.join(REPO_ROOT, relative), 'utf8'); } const START_LOCAL = read('scripts/start-local.ps1'); const RUN_TESTS = read('scripts/run-tests.ps1'); const DB_HELPER = read('frontend/tests/e2e/support/db.ts'); /** The five settings a connection needs, as the JSON record names them. */ const RECORD_KEYS = ['host', 'port', 'user', 'password', 'database'] as const; /** The same five, as the environment variables the helper reads. */ const ENV_NAMES = [ 'TEST_PGHOST', 'TEST_PGPORT', 'TEST_PGUSER', 'TEST_PGPASSWORD', 'TEST_PGDATABASE' ] as const; describe('the record start-local.ps1 writes', () => { it('is written to .local/database.json', () => { expect(START_LOCAL).toContain("Join-Path $StateDir 'database.json'"); }); it.each(RECORD_KEYS)('records %s', (key) => { // The writer is a hashtable literal, so each key appears as `key =`. expect(START_LOCAL).toMatch(new RegExp(`^\\s*${key}\\s*=`, 'm')); }); // Written after the database is up, so the file never names one that failed // to start, and removed on -Stop so a stopped stack does not leave a record // pointing at a container that is gone. it('is written only once the database has started', () => { const startDatabase = START_LOCAL.indexOf(' Start-Database'); const writeChoice = START_LOCAL.indexOf(' Write-DatabaseChoice'); expect(startDatabase).toBeGreaterThan(-1); expect(writeChoice).toBeGreaterThan(startDatabase); }); it('is removed when the stack is stopped', () => { expect(START_LOCAL).toContain('Remove-Item $DatabaseFile'); }); }); describe('what run-tests.ps1 does with it', () => { it('reads the same file start-local.ps1 writes', () => { expect(RUN_TESTS).toContain('.local/database.json'); }); it.each(ENV_NAMES)('sets %s for the end-to-end run', (name) => { expect(RUN_TESTS).toMatch(new RegExp(`\\$env:${name}\\s*=`)); }); it.each(RECORD_KEYS)('takes %s from the record rather than a default', (key) => { expect(RUN_TESTS).toContain(`$db.${key}`); }); /** * The second half of #273, and the reason all five are set rather than the * port alone. Invoke-IntegrationSuite sets TEST_PGPORT and PowerShell keeps * it for the rest of the session, so a `-Suite all` run leaked the * integration port into the e2e run that followed — with none of the matching * credentials. Setting every one of them is what overrides that. */ it('sets every connection variable, so a leaked one cannot survive', () => { const leaks = ENV_NAMES.filter((name) => !RUN_TESTS.includes(`$env:${name} =`)); expect(leaks).toEqual([]); }); // A default is what caused both faults: always plausible, silently wrong, and // it fails in ways that look like application bugs rather than configuration. it('refuses to guess when the record is missing', () => { expect(RUN_TESTS).toMatch(/if \(-not \(Test-Path \$databaseFile\)\) \{\s*throw/); }); }); describe('what the helper actually connects with', () => { it.each(ENV_NAMES)('reads %s', (name) => { expect(DB_HELPER).toContain(`process.env.${name}`); }); // The chain is only closed if the helper reads no connection setting that // run-tests.ps1 does not set. A sixth variable added here and nowhere else // would reintroduce exactly the drift this guards. it('reads no connection variable the runner does not set', () => { const used = [...DB_HELPER.matchAll(/process\.env\.(TEST_PG[A-Z]+)/g)].map((m) => m[1]!); const unset = [...new Set(used)].filter((name) => !RUN_TESTS.includes(`$env:${name} =`)); expect(unset).toEqual([]); }); });