215 lines
7.0 KiB
JavaScript
215 lines
7.0 KiB
JavaScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { afterEach, describe, it } from "vitest";
|
|
|
|
import { checkEnvDrift, formatEnvDriftResult } from "./check-env-drift.mjs";
|
|
|
|
const fixtures = [];
|
|
|
|
const ENV_FIXTURES = {
|
|
".env.example": `# Core
|
|
APP_NAME=ToolCTRL
|
|
DATABASE_URL="postgres://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}"
|
|
SESSION_SECRET=
|
|
CRYPTO_SECRET=
|
|
APP_BASE_DOMAIN=localhost
|
|
WEB_APP_HOST=app.localhost
|
|
ADMIN_APP_HOST=admin.localhost
|
|
API_HOST=api.localhost
|
|
# APP_PROTOCOL=http
|
|
API_ACCESS_TOKEN=
|
|
JWT_SECRET=
|
|
# REDIS_URL is a fallback only when BULLMQ_REDIS_URL is absent; keep it commented if a platform injects it.
|
|
# REDIS_URL=redis://localhost:6379/0
|
|
SITE_KEY_EXAMPLE_SITE=
|
|
`,
|
|
".env.local.example": `APP_NAME=ToolCTRL Local
|
|
DATABASE_URL="file:./dev.db?connection_limit=1&connect_timeout=30"
|
|
SESSION_SECRET=replace-with-local-session-secret
|
|
CRYPTO_SECRET=replace-with-local-crypto-secret
|
|
APP_BASE_DOMAIN=localhost:3000
|
|
WEB_APP_HOST=app.localhost:3000
|
|
ADMIN_APP_HOST=admin.localhost:3000
|
|
API_HOST=api.localhost:3001
|
|
APP_PROTOCOL=http
|
|
API_ACCESS_TOKEN=replace-with-local-api-token
|
|
JWT_SECRET=replace-with-local-jwt-secret
|
|
SITE_KEY_LOCAL=replace-with-local-site-key
|
|
`,
|
|
".env.production.example": `APP_NAME=ToolCTRL
|
|
DATABASE_URL="postgres://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE}"
|
|
SESSION_SECRET=
|
|
CRYPTO_SECRET=
|
|
APP_BASE_DOMAIN=your-domain.invalid
|
|
WEB_APP_HOST=app.your-domain.invalid
|
|
ADMIN_APP_HOST=admin.your-domain.invalid
|
|
API_HOST=api.your-domain.invalid
|
|
APP_PROTOCOL=https
|
|
API_ACCESS_TOKEN=
|
|
JWT_SECRET=
|
|
BULLMQ_REDIS_URL=redis://localhost:6379/0
|
|
# REDIS_URL=redis://localhost:6379/0
|
|
`,
|
|
".env.fly.example": `flyctl secrets set APP_NAME=YOUR_APP_NAME \\
|
|
DATABASE_URL=postgres://{USER}:{PASSWORD}@{HOST}:{PORT}/{DATABASE} \\
|
|
SESSION_SECRET=<session-secret> \\
|
|
CRYPTO_SECRET=<crypto-secret> \\
|
|
API_ACCESS_TOKEN=<api-access-token> \\
|
|
JWT_SECRET=<jwt-secret> \\
|
|
APP_BASE_DOMAIN=your-domain.invalid \\
|
|
WEB_APP_HOST=app.your-domain.invalid \\
|
|
ADMIN_APP_HOST=admin.your-domain.invalid \\
|
|
API_HOST=api.your-domain.invalid \\
|
|
APP_PROTOCOL=https \\
|
|
BULLMQ_REDIS_URL=redis://default:<redis-password>@redis.your-domain.invalid:6379/0 \\
|
|
--app YOUR_APP_NAME
|
|
# REDIS_URL=redis://default:<redis-password>@redis.your-domain.invalid:6379/0
|
|
`,
|
|
".env.test": `APP_NAME=ToolCTRL Test
|
|
DATABASE_URL="file:./test.db?connection_limit=1&connect_timeout=30"
|
|
SESSION_SECRET=test-session-secret-placeholder
|
|
CRYPTO_SECRET=test-crypto-secret-placeholder
|
|
APP_BASE_DOMAIN=localhost.test
|
|
WEB_APP_HOST=app.localhost.test
|
|
ADMIN_APP_HOST=admin.localhost.test
|
|
API_HOST=api.localhost.test
|
|
APP_PROTOCOL=http
|
|
API_ACCESS_TOKEN=test-api-access-token-placeholder
|
|
JWT_SECRET=test-jwt-secret-placeholder
|
|
SITE_KEY_EXAMPLE_SITE=test-site-key-placeholder
|
|
`,
|
|
};
|
|
|
|
function writeFile(rootDir, relativePath, content) {
|
|
const absolutePath = path.join(rootDir, relativePath);
|
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
fs.writeFileSync(absolutePath, content);
|
|
}
|
|
|
|
function createFixture(overrides = {}) {
|
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "env-drift-"));
|
|
fixtures.push(rootDir);
|
|
|
|
for (const [relativePath, content] of Object.entries(ENV_FIXTURES)) {
|
|
writeFile(rootDir, relativePath, overrides[relativePath] ?? content);
|
|
}
|
|
|
|
return rootDir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
while (fixtures.length > 0) {
|
|
fs.rmSync(fixtures.pop(), { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("check-env-drift", () => {
|
|
it("prints PASS output for cataloged env fixtures", () => {
|
|
const rootDir = createFixture();
|
|
|
|
const result = checkEnvDrift(rootDir);
|
|
const output = formatEnvDriftResult(result);
|
|
|
|
assert.deepEqual(result.failures, []);
|
|
assert.match(output, /^PASS env drift verification/);
|
|
assert.match(output, /PASS \.env\.test: \d+ active keys match catalog/);
|
|
assert.match(output, /Summary: \d+ passed, 0 observed, 0 failed/);
|
|
});
|
|
|
|
it("fails for unknown keys", () => {
|
|
const rootDir = createFixture({
|
|
".env.test": `${ENV_FIXTURES[".env.test"]}UNTRACKED_SECRET=super-secret-value
|
|
`,
|
|
});
|
|
|
|
const result = checkEnvDrift(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /\.env\.test:\d+: unknown-key UNTRACKED_SECRET/);
|
|
});
|
|
|
|
it("fails for duplicate keys within one file", () => {
|
|
const rootDir = createFixture({
|
|
".env.local.example": `${ENV_FIXTURES[".env.local.example"]}APP_NAME=Duplicate
|
|
`,
|
|
});
|
|
|
|
const result = checkEnvDrift(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /\.env\.local\.example:\d+: duplicate-key APP_NAME/);
|
|
});
|
|
|
|
it("allows dynamic SITE_KEY-prefixed variables", () => {
|
|
const rootDir = createFixture({
|
|
".env.test": `${ENV_FIXTURES[".env.test"]}SITE_KEY_CUSTOMER_PORTAL=test-site-key-placeholder
|
|
`,
|
|
});
|
|
|
|
const result = checkEnvDrift(rootDir);
|
|
|
|
assert.deepEqual(result.failures, []);
|
|
});
|
|
|
|
it("fails when production example files keep example.com placeholders", () => {
|
|
const rootDir = createFixture({
|
|
".env.production.example": ENV_FIXTURES[".env.production.example"].replaceAll("your-domain.invalid", "example.com"),
|
|
});
|
|
|
|
const result = checkEnvDrift(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /\.env\.production\.example:\d+: production-domain-placeholder APP_BASE_DOMAIN/);
|
|
});
|
|
|
|
it("requires production keys in fly secrets examples", () => {
|
|
const rootDir = createFixture({
|
|
".env.fly.example": ENV_FIXTURES[".env.fly.example"].replace(/JWT_SECRET=<jwt-secret> \\\n/, ""),
|
|
});
|
|
|
|
const result = checkEnvDrift(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /\.env\.fly\.example: missing-required-key JWT_SECRET/);
|
|
});
|
|
|
|
it("redacts values from diagnostics", () => {
|
|
const leakedValue = "top-secret-token-value";
|
|
const rootDir = createFixture({
|
|
".env.test": `${ENV_FIXTURES[".env.test"]}UNTRACKED_SECRET=${leakedValue}
|
|
`,
|
|
});
|
|
|
|
const result = checkEnvDrift(rootDir);
|
|
const output = formatEnvDriftResult(result);
|
|
|
|
assert.match(output, /unknown-key UNTRACKED_SECRET/);
|
|
assert.doesNotMatch(output, new RegExp(leakedValue));
|
|
});
|
|
|
|
it("CLI exits non-zero without leaking values", () => {
|
|
const leakedValue = "super-secret-cli-value";
|
|
const rootDir = createFixture({
|
|
".env.test": `${ENV_FIXTURES[".env.test"]}UNTRACKED_SECRET=${leakedValue}
|
|
`,
|
|
});
|
|
const scriptPath = path.resolve("scripts/check-env-drift.mjs");
|
|
const nodePath = path.resolve("node_modules");
|
|
|
|
assert.throws(
|
|
() => execFileSync(process.execPath, ["--import", path.join(nodePath, "tsx/dist/loader.mjs"), scriptPath], {
|
|
cwd: rootDir,
|
|
encoding: "utf8",
|
|
stdio: "pipe",
|
|
}),
|
|
(error) => {
|
|
assert.equal(error.status, 1);
|
|
const output = `${error.stdout}${error.stderr}`;
|
|
assert.match(output, /FAIL env drift verification/);
|
|
assert.match(output, /unknown-key UNTRACKED_SECRET/);
|
|
assert.doesNotMatch(output, new RegExp(leakedValue));
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
});
|