test+fix(stammbaum): capture script refuses default creds and non-localhost (#361)
@Nora + @Tobias on PR #693: defaulting CAPTURE_EMAIL/PASSWORD to documented admin creds and BACKEND_URL to localhost:8080 means an env-var slip silently auth's against staging/prod. Make both explicit: refuse to run unless CAPTURE_EMAIL and CAPTURE_PASSWORD are set, and unless BACKEND_URL hostname is localhost / 127.0.0.1 / ::1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,45 @@ import { fileURLToPath } from 'node:url';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:8080';
|
||||
const EMAIL = process.env.CAPTURE_EMAIL ?? 'admin@familyarchive.local';
|
||||
const PASSWORD = process.env.CAPTURE_PASSWORD ?? 'admin123';
|
||||
const EMAIL = process.env.CAPTURE_EMAIL;
|
||||
const PASSWORD = process.env.CAPTURE_PASSWORD;
|
||||
|
||||
// Preflight guards: this script writes the canonical Stammbaum fixture from
|
||||
// a *running* backend with admin-shaped credentials. Two slips would be
|
||||
// silent disasters — running with default creds against staging/prod, or
|
||||
// running with a typo'd BACKEND_URL that happens to resolve. Refuse both
|
||||
// before sending a single byte.
|
||||
preflight();
|
||||
|
||||
function preflight() {
|
||||
const failures = [];
|
||||
if (!EMAIL) {
|
||||
failures.push('CAPTURE_EMAIL must be set explicitly (no default).');
|
||||
}
|
||||
if (!PASSWORD) {
|
||||
failures.push('CAPTURE_PASSWORD must be set explicitly (no default).');
|
||||
}
|
||||
if (!isLocalhost(BACKEND_URL)) {
|
||||
failures.push(
|
||||
`BACKEND_URL must point at localhost / 127.0.0.1 (got: ${BACKEND_URL}). ` +
|
||||
'This script is local-only.'
|
||||
);
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
console.error('Preflight failed:');
|
||||
for (const f of failures) console.error(` - ${f}`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalhost(url) {
|
||||
try {
|
||||
const host = new URL(url).hostname;
|
||||
return host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const FIXTURE_PATH = `${HERE}/../src/lib/person/genealogy/__fixtures__/stammbaum.json`;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
// The capture script is a local-only utility (never invoked from CI) but its
|
||||
// failure modes are load-bearing for the canonical fixture lifecycle: a slip
|
||||
// in env vars must not silently authenticate to a non-local backend or
|
||||
// overwrite the canonical snapshot with a vacuous response. These guards are
|
||||
// the public contract.
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const SCRIPT_PATH = resolve(HERE, '../../../../../scripts/capture-network-fixture.mjs');
|
||||
|
||||
function runScript(env: Record<string, string | undefined>) {
|
||||
return spawnSync(process.execPath, [SCRIPT_PATH], {
|
||||
env: { PATH: process.env.PATH, ...env } as NodeJS.ProcessEnv,
|
||||
encoding: 'utf8',
|
||||
timeout: 5_000
|
||||
});
|
||||
}
|
||||
|
||||
describe('capture-network-fixture.mjs — preflight guards', () => {
|
||||
it('refuses_to_run_without_explicit_CAPTURE_EMAIL', () => {
|
||||
const result = runScript({
|
||||
CAPTURE_PASSWORD: 'pw',
|
||||
BACKEND_URL: 'http://localhost:8080'
|
||||
});
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toMatch(/CAPTURE_EMAIL/);
|
||||
});
|
||||
|
||||
it('refuses_to_run_without_explicit_CAPTURE_PASSWORD', () => {
|
||||
const result = runScript({
|
||||
CAPTURE_EMAIL: 'someone@example.test',
|
||||
BACKEND_URL: 'http://localhost:8080'
|
||||
});
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toMatch(/CAPTURE_PASSWORD/);
|
||||
});
|
||||
|
||||
it('refuses_to_run_when_BACKEND_URL_is_not_localhost', () => {
|
||||
const result = runScript({
|
||||
CAPTURE_EMAIL: 'someone@example.test',
|
||||
CAPTURE_PASSWORD: 'pw',
|
||||
BACKEND_URL: 'https://staging.example.com'
|
||||
});
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toMatch(/BACKEND_URL/);
|
||||
expect(result.stderr).toMatch(/localhost|127\.0\.0\.1/);
|
||||
});
|
||||
|
||||
it('accepts_127_0_0_1_as_BACKEND_URL', () => {
|
||||
// With creds + a localhost backend the preflight passes; the fetch
|
||||
// then fails (no server) — but the exit message must NOT mention the
|
||||
// preflight guards, proving they let the run through.
|
||||
const result = runScript({
|
||||
CAPTURE_EMAIL: 'someone@example.test',
|
||||
CAPTURE_PASSWORD: 'pw',
|
||||
BACKEND_URL: 'http://127.0.0.1:65500'
|
||||
});
|
||||
expect(result.stderr).not.toMatch(/BACKEND_URL.*localhost/);
|
||||
expect(result.stderr).not.toMatch(/CAPTURE_EMAIL/);
|
||||
expect(result.stderr).not.toMatch(/CAPTURE_PASSWORD/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user