mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-21 21:15:09 +00:00
fix(test): stop the suite writing live API keys into the operator's real key store
`npm test` wrote real, UNREVOKED rows into ~/.ocp/ocp.db — the SAME sqlite database the
running server reads — two per run, unbounded. On the maintainer's host that had accumulated
**737 test-suite keys against 12 real operator keys** (749 rows total), all revoked=0.
Not a credential leak: createKey() mints `randomBytes(24)` and the suite discards the
plaintext, so nobody holds a usable token. But it is real damage:
- the operator's key store grows by 2 rows on every test run, forever
- `ocp keys list` is unusable (749 rows, 12 of them real)
- the suite is RACY: two concurrent runs (e.g. two review worktrees) share one file, so
listKeys() can miss "test-user-1" and `"quota_daily" in undefined` throws a TypeError
rather than failing cleanly. That is the ~1-in-6 flake in `listKeys includes quota
fields`, reported by a reviewer and initially not reproducible serially — it needs a
concurrent run to surface, which is exactly what four parallel reviewers produced.
Root cause: keys.mjs resolved `OCP_DIR`/`DB_PATH` at MODULE TOP-LEVEL and read no env var.
test-features.mjs carried a comment claiming it could "set env before the first getDb() call"
— it could not, on two counts: nothing in keys.mjs read an env var, and ESM hoists imports, so
the assignment would have run after keys.mjs was already evaluated anyway. The isolation was
never real; it just LOOKED real, which is why it survived.
Fix:
- keys.mjs resolves the dir lazily, inside getDb(), honoring OCP_DIR_OVERRIDE. Deliberately
NOT a generic `OCP_DIR`: pointing a RUNNING server at a different key store silently
changes which credentials authenticate, so this must be awkward to set by accident.
- new test-env.mjs, imported BEFORE keys.mjs, mints a per-run scratch dir. A separate module
is required — ESM hoisting means a statement in the test's own body is too late.
- export getDbPath() so the store's location can be asserted.
- as a side effect, importing keys.mjs no longer creates directories in the operator's home.
Two guards added, both MUTATION-TESTED (revert the override → both fail, 317/2):
- the key store under test is a scratch db, NOT the operator's real ~/.ocp/ocp.db
- listKeys does not depend on rows left behind by an earlier or concurrent run
Proven, not asserted: the real ~/.ocp/ocp.db held at 749 rows across two full test runs
(it previously grew by 2 each run). The 737 existing junk rows are NOT cleaned up here —
that is a destructive change to the maintainer's live database and is his call, not a
side effect of a test fix.
npm test: 319 passed, 0 failed (was 317).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR
This commit is contained in:
@@ -6,26 +6,51 @@ import { join } from "node:path";
|
||||
import { mkdirSync, chmodSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
const OCP_DIR = join(homedir(), ".ocp");
|
||||
mkdirSync(OCP_DIR, { recursive: true, mode: 0o700 });
|
||||
// Tighten the directory mode in case it already existed with broader permissions.
|
||||
try { chmodSync(OCP_DIR, 0o700); } catch { /* ignore EPERM on pre-existing dirs */ }
|
||||
const DB_PATH = join(OCP_DIR, "ocp.db");
|
||||
// Resolved LAZILY, on first getDb() — not at module top-level. Two reasons, and the second is
|
||||
// the bug this fixes:
|
||||
//
|
||||
// 1. Merely IMPORTING keys.mjs should not, as a side effect, create directories in the
|
||||
// operator's home.
|
||||
// 2. OCP_DIR_OVERRIDE exists so the test suite can point the key store at a scratch dir — and
|
||||
// because ESM hoists imports, a top-level `const OCP_DIR = ...` here would be evaluated
|
||||
// BEFORE an importing module's body could set the env var. Eager resolution made the
|
||||
// override unsettable in the one place that needs it. (test-features.mjs carried a comment
|
||||
// claiming it could "set env before the first getDb() call" — it could not, because nothing
|
||||
// here ever read an env var. So `npm test` wrote real, UNREVOKED api_keys rows into the
|
||||
// operator's live ~/.ocp/ocp.db: two per run, unbounded — 737 junk keys against 12 real ones
|
||||
// on the maintainer's host — and two concurrent runs raced one file, which is the ~1-in-6
|
||||
// flake in `listKeys includes quota fields`.)
|
||||
//
|
||||
// Deliberately NOT a generic name like OCP_DIR: this must be awkward to set by accident, because
|
||||
// pointing a RUNNING server at a different key store silently changes which credentials authenticate.
|
||||
function resolveOcpDir() {
|
||||
const dir = process.env.OCP_DIR_OVERRIDE || join(homedir(), ".ocp");
|
||||
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
// Tighten the directory mode in case it already existed with broader permissions.
|
||||
try { chmodSync(dir, 0o700); } catch { /* ignore EPERM on pre-existing dirs */ }
|
||||
return dir;
|
||||
}
|
||||
|
||||
let db;
|
||||
let dbPath; // resolved on first open, alongside the db handle
|
||||
|
||||
export function getDb() {
|
||||
if (!db) {
|
||||
db = new DatabaseSync(DB_PATH);
|
||||
dbPath = join(resolveOcpDir(), "ocp.db");
|
||||
db = new DatabaseSync(dbPath);
|
||||
db.exec("PRAGMA journal_mode = WAL");
|
||||
db.exec("PRAGMA foreign_keys = ON");
|
||||
initSchema();
|
||||
// Tighten mode on the DB file (0600) after creation / first open.
|
||||
try { chmodSync(DB_PATH, 0o600); } catch { /* ignore — same-user access still works */ }
|
||||
try { chmodSync(dbPath, 0o600); } catch { /* ignore — same-user access still works */ }
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
// Which file the key store actually opened. Exported so a test can ASSERT it is not the
|
||||
// operator's real db — the bug this replaced was invisible precisely because nothing checked.
|
||||
export function getDbPath() { return dbPath; }
|
||||
|
||||
function initSchema() {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
|
||||
Reference in New Issue
Block a user