test: lock in .env.example placeholder-only template (E00-S04-T05)

This commit is contained in:
implementer
2026-08-30 05:12:06 +00:00
parent 6b04eac354
commit 3b2f61c094
+235
View File
@@ -0,0 +1,235 @@
/**
* .env.example test — locks in the [E00-S04-T05] guarantee that the committed
* `.env.example` template at the repo root contains placeholders only and no
* real secret values.
*
* Acceptance criteria covered (each test fails without the committed state):
* - ".env.example contains placeholders only" → the file exists at the repo
* root and every assignment value is a placeholder (an explicit
* `change-me`/`<…>`-style marker) or a benign non-secret default (bind
* address, port, local database/user name); no value is a long
* random-looking token without a placeholder marker, no value embeds a
* credential URI, every line is a comment, a blank line, or a well-formed
* `KEY=value` assignment, no variable is repeated, and the file documents
* every configuration environment source (`HOST`/`PORT`/`DATABASE_URL`/
* `EPPP_SESSION_SECRET`).
* - "no real secret values appear in the example file" → the same value
* predicate rejects secret-shaped values, the compose dev-default
* credential value appears nowhere in the file (values or comments), and
* `.gitignore` keeps real `.env` files ignored while un-ignoring the
* committed `.env.example`. The mutation probes below prove the
* assertions are non-vacuous (a secret-looking value, a credential URI, a
* compose default credential, a missing required variable, a dropped
* `!.env.example` negation, or a malformed line all break the criterion).
*
* Run: `node --test tests/env-example.test.mjs`
* (node:test — built into Node >= 18; no dependencies, lockfile untouched.)
*/
import test from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8');
const exists = (relPath) => existsSync(path.join(REPO_ROOT, relPath));
const ENV_EXAMPLE_PATH = '.env.example';
/** The config schema's environment sources (E00-S04-T01/T04) the template must document. */
const REQUIRED_VARS = ['HOST', 'PORT', 'DATABASE_URL', 'EPPP_SESSION_SECRET'];
/** Compose dev-default credential values that must never appear in the template. */
const FORBIDDEN_VALUES = ['postgres://eppp:eppp@db:5432/eppp'];
/** A long random-looking value (JWT/API-key/token-shaped literal). */
const LONG_SECRET_RE = /^[A-Za-z0-9+/=_-]{32,}$/;
/** A credential URI (`scheme://user:pass@host`) embedded as a literal value. */
const CREDENTIAL_URI_RE = /:\/\/[^/\s]+:[^@\s]+@/;
/** Explicit placeholder markers a value may carry. */
const PLACEHOLDER_MARKERS = [
'change-me',
'changeme',
'change_me',
'your-',
'replace',
'example',
'xxx',
'<',
'>',
];
/** Benign non-secret defaults the template may show as values. */
const BENIGN_VALUES = new Set([
'0.0.0.0', // HOST container default
'3000', // PORT / APP_PORT default
'5432', // POSTGRES_PORT default
'localhost', // local bind/db host
'eppp', // local database/user name (non-secret)
'postgres://localhost:5432/eppp', // DATABASE_URL shape without embedded credentials
]);
/**
* True when an assignment value is a placeholder, never a real secret.
*
* A value is a placeholder when it is a benign non-secret default or carries
* an explicit placeholder marker; a marked value is still rejected when it
* embeds a credential URI or reproduces a compose default credential. Any
* other value — including a long random-looking token — is not a placeholder.
*/
function isPlaceholderValue(value) {
if (value === '') return false;
if (BENIGN_VALUES.has(value)) return true;
if (PLACEHOLDER_MARKERS.some((marker) => value.includes(marker))) {
return !CREDENTIAL_URI_RE.test(value) && !FORBIDDEN_VALUES.includes(value);
}
return false;
}
/**
* Parses the template into `{ key, value }` assignments, ignoring blank lines
* and `#` comment lines. Throws a descriptive Error on a malformed line so a
* stray non-assignment line cannot silently pass.
*/
function parseAssignments(content) {
const assignments = [];
content.split(/\r?\n/).forEach((raw, index) => {
const line = raw.trim();
if (line === '' || line.startsWith('#')) return;
const match = /^([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
if (!match) {
throw new Error(
`line ${index + 1} is not a comment, blank line, or KEY=value assignment: "${raw}"`,
);
}
assignments.push({ key: match[1], value: match[2] });
});
return assignments;
}
/**
* Asserts the acceptance criteria for the given template content — used by
* the committed-state test and by the mutation probes (which must make it
* throw).
*/
function assertTemplateHasPlaceholdersOnly(content) {
for (const forbidden of FORBIDDEN_VALUES) {
assert.ok(
!content.includes(forbidden),
`the template must not contain the compose default credential value "${forbidden}"`,
);
}
const assignments = parseAssignments(content);
assert.ok(assignments.length > 0, 'the template must contain at least one assignment');
const keys = assignments.map(({ key }) => key);
assert.equal(new Set(keys).size, keys.length, 'the template must not repeat a variable');
for (const required of REQUIRED_VARS) {
assert.ok(
keys.includes(required),
`the template must document the required variable "${required}"`,
);
}
for (const { key, value } of assignments) {
assert.ok(
isPlaceholderValue(value),
`"${key}" must be a placeholder value, got: "${value}"`,
);
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test('a committed .env.example exists at the repo root', () => {
assert.ok(exists(ENV_EXAMPLE_PATH), `"${ENV_EXAMPLE_PATH}" must exist at the repo root`);
});
test('.gitignore keeps real .env files ignored while un-ignoring the committed example', () => {
const gitignore = read('.gitignore');
assert.ok(gitignore.includes('.env'), 'real .env files must stay git-ignored');
assert.ok(gitignore.includes('.env.*'), 'real .env.* files must stay git-ignored');
assert.ok(
gitignore.includes('!.env.example'),
'the committed .env.example must be un-ignored (negation pattern)',
);
});
test('.env.example contains placeholders only and no real secret values', () => {
assertTemplateHasPlaceholdersOnly(read(ENV_EXAMPLE_PATH));
});
// ---------------------------------------------------------------------------
// Non-vacuous probes — the assertions above really do fail on violations
// ---------------------------------------------------------------------------
test('a compose default credential value in the template fails the criterion (mutation probe)', () => {
const content = read(ENV_EXAMPLE_PATH);
const mutated = content.replace(
'DATABASE_URL=postgres://localhost:5432/eppp',
'DATABASE_URL=postgres://eppp:eppp@db:5432/eppp',
);
assert.notEqual(mutated, content, 'the mutation must actually replace the DATABASE_URL value');
assert.throws(
() => assertTemplateHasPlaceholdersOnly(mutated),
/compose default credential|placeholder/,
);
});
test('a long secret-looking value fails the criterion (mutation probe)', () => {
const content = read(ENV_EXAMPLE_PATH);
const mutated = content.replace(
'EPPP_SESSION_SECRET=change-me-to-a-random-32-character-secret',
'EPPP_SESSION_SECRET=aB3dE9fG0hI1jK2lM3nO4pQ5rS6tU7vW8xY9zA0',
);
assert.notEqual(mutated, content, 'the mutation must actually replace the session secret');
assert.throws(() => assertTemplateHasPlaceholdersOnly(mutated), /placeholder/);
});
test('a credential URI value fails the criterion (mutation probe)', () => {
const content = read(ENV_EXAMPLE_PATH);
const mutated = content.replace(
'DATABASE_URL=postgres://localhost:5432/eppp',
'DATABASE_URL=postgres://alice:supersecret@db.example.com:5432/eppp',
);
assert.notEqual(mutated, content, 'the mutation must actually replace the DATABASE_URL value');
assert.throws(() => assertTemplateHasPlaceholdersOnly(mutated), /placeholder/);
});
test('a missing required variable fails the criterion (mutation probe)', () => {
const content = read(ENV_EXAMPLE_PATH);
const mutated = content.replace(/^EPPP_SESSION_SECRET=.*$/m, '');
assert.notEqual(
mutated,
content,
'the mutation must actually remove the EPPP_SESSION_SECRET assignment',
);
assert.throws(() => assertTemplateHasPlaceholdersOnly(mutated), /EPPP_SESSION_SECRET/);
});
test('dropping the !.env.example negation fails the criterion (mutation probe)', () => {
const gitignore = read('.gitignore');
const mutated = gitignore.replace('!.env.example', '');
assert.notEqual(mutated, gitignore, 'the mutation must actually drop the negation pattern');
assert.throws(
() => {
assert.ok(mutated.includes('!.env.example'), 'the committed .env.example must be un-ignored');
},
/un-ignored/,
);
});
test('a malformed non-assignment line fails the criterion (mutation probe)', () => {
const content = read(ENV_EXAMPLE_PATH);
const mutated = `${content}\nTHIS IS NOT A VALID LINE\n`;
assert.throws(() => assertTemplateHasPlaceholdersOnly(mutated), /not a comment/);
});