Seven suites read .env with a bare `process.env[key] = value`, which overwrites whatever the shell already set. That made the DATABASE_URL from the production .env authoritative, so a single environment variable was enough to aim them at the live hotel database: RUN_CATALOG_AUDIT_LIVE=1 pnpm vitest run src/lib/services/catalog-audit-repair-live.test.ts Three of those suites then repair the catalog in place: catalog-audit-repair-live and catalog-repair-direct-live rewrite catalog_items and delete duplicate classnames, and clone-bulk-import-live bulk-imports every cloneable item. None of that is undoable, and nothing in their output said the target was production rather than a sandbox. Added src/test/live-env.ts with one shared loader, and pointed all seven suites at it: - Values already in the real environment win, so an explicit DATABASE_URL on the command line is always respected. - DATABASE_URL defaults to the sandbox on port 3307 rather than inheriting the production one from .env. - Anything that is not loopback is treated as production and redirected. - Reaching production requires ALLOW_PRODUCTION_LIVE_DB=1 and logs a warning saying the suite repairs the catalog. Tests in src/test/live-env.test.ts run the loader against a temporary .env so the real project file is never read, and cover the redirect, the shell override, non-loopback detection, the opt-in and quote stripping. A second block asserts each of the seven suites no longer contains an inline `process.env[...] =` assignment. Verified four of them fail against the old loader. This does not enable the suites; they stay gated behind their RUN_* flags. It only removes the possibility of them silently hitting production. Unit suite: 3330 passed, 12 skipped. Typecheck and lint clean.
76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
/**
|
|
* Sandbox database for the live suites. Production runs on port 3306; the
|
|
* throwaway MariaDB used by the rehearsal suites listens on 3307.
|
|
*/
|
|
export const SANDBOX_DATABASE_URL =
|
|
"mysql://[email protected]:3307/habbo_sandbox?charset=utf8mb4";
|
|
|
|
/**
|
|
* Load .env for the live suites without ever pointing them at production.
|
|
*
|
|
* Every live suite used to read .env with a bare
|
|
* `process.env[key] = value`, which overwrites whatever the shell already set.
|
|
* That made the DATABASE_URL from the production .env authoritative: setting
|
|
* RUN_CATALOG_AUDIT_LIVE=1 pointed three suites — catalog-audit-repair-live,
|
|
* catalog-repair-direct-live and clone-bulk-import-live — at the live hotel
|
|
* database, where they rewrite the catalog, delete duplicate classnames and
|
|
* bulk-import thousands of rows. The failure mode was silent: the gate is a
|
|
* single environment variable, and nothing in the suite said the target was
|
|
* production.
|
|
*
|
|
* Rules now:
|
|
* 1. Values already present in the real environment win, so an explicit
|
|
* DATABASE_URL on the command line is always respected.
|
|
* 2. DATABASE_URL defaults to the sandbox, never to production.
|
|
* 3. Targeting production requires ALLOW_PRODUCTION_LIVE_DB=1 and says so
|
|
* loudly, because it is destructive and not undoable.
|
|
*/
|
|
export function loadEnvForLiveTests(): void {
|
|
const allowProduction = process.env.ALLOW_PRODUCTION_LIVE_DB === "1";
|
|
const envFile = resolve(process.cwd(), ".env");
|
|
|
|
if (existsSync(envFile)) {
|
|
for (const line of readFileSync(envFile, "utf8").split(/\r?\n/)) {
|
|
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
if (!match) continue;
|
|
let value = match[2].trim();
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
(value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
// Already-set values win: the shell is a deliberate override.
|
|
if (process.env[match[1]] === undefined) {
|
|
process.env[match[1]] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
const databaseUrl = process.env.DATABASE_URL ?? "";
|
|
const targetsProduction =
|
|
databaseUrl.includes(":3306") ||
|
|
(databaseUrl.includes("@") && !databaseUrl.includes("127.0.0.1"));
|
|
|
|
if (allowProduction) {
|
|
if (targetsProduction) {
|
|
console.warn(
|
|
"[live-test] ALLOW_PRODUCTION_LIVE_DB=1 and DATABASE_URL points at " +
|
|
"a remote database. This suite repairs the catalog in place.",
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (targetsProduction || !databaseUrl) {
|
|
process.env.DATABASE_URL = SANDBOX_DATABASE_URL;
|
|
console.warn(
|
|
`[live-test] redirected DATABASE_URL to the sandbox at ${SANDBOX_DATABASE_URL}. ` +
|
|
"Set ALLOW_PRODUCTION_LIVE_DB=1 to override.",
|
|
);
|
|
}
|
|
}
|