BillTracker/e2e/setup/prepare-db.js

85 lines
3.4 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* Deterministic scratch DB for the E2E suite (docs/QA_PLAN.md §4.4).
*
* Creates a throwaway SQLite DB, a regular `user` with known creds, and seeds
* realistic demo data for that user so `npm run test:e2e` is safe to run:
* it NEVER touches the real db/bills.db. Runs fresh every invocation.
*/
const path = require('path');
const fs = require('fs');
const bcrypt = require('bcryptjs');
const { E2E_USER, E2E_PASS, scratchDbPath } = require('../constants');
const REAL_DB = path.join(__dirname, '..', '..', 'db', 'bills.db');
const dbPath = scratchDbPath();
// Safety rail: refuse to operate on the real database.
if (path.resolve(dbPath) === path.resolve(REAL_DB)) {
console.error(`[e2e] Refusing to use the real DB at ${REAL_DB}. Set E2E_DB_PATH to a scratch path.`);
process.exit(1);
}
// Start from a clean slate so every run is reproducible.
for (const suffix of ['', '-wal', '-shm']) {
const f = dbPath + suffix;
if (fs.existsSync(f)) fs.rmSync(f);
}
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
// db/database.js reads DB_PATH at require-time — set it BEFORE requiring.
process.env.DB_PATH = dbPath;
const { getDb, ensureUserDefaultCategories } = require('../../db/database');
const { seedDemoData } = require('../../scripts/seedDemoData');
const db = getDb(); // initializes schema + runs migrations
// Regular `user` (role 'user', no forced password change) — mirrors the app's
// own INIT_REGULAR_USER seed path in server.js.
let user = db.prepare('SELECT id FROM users WHERE username = ?').get(E2E_USER);
if (!user) {
const hash = bcrypt.hashSync(E2E_PASS, 12);
const res = db
.prepare(
`INSERT INTO users (username, password_hash, role, first_login, must_change_password, is_default_admin)
VALUES (?, ?, 'user', 0, 0, 0)`,
)
.run(E2E_USER, hash);
user = { id: Number(res.lastInsertRowid) };
}
ensureUserDefaultCategories(user.id);
// Seed demo bills/categories for this user (idempotent).
const result = seedDemoData(user.id);
// Second user (role 'user') with their OWN seeded data — the IDOR target for the
// data-isolation probe (e2e/api.probe.spec.js). User A must not be able to
// read/modify any of user B's resources.
const E2E_USER_B = 'e2e_user_b';
let userB = db.prepare('SELECT id FROM users WHERE username = ?').get(E2E_USER_B);
if (!userB) {
const hashB = bcrypt.hashSync(E2E_PASS, 12);
const resB = db
.prepare(
`INSERT INTO users (username, password_hash, role, first_login, must_change_password, is_default_admin)
VALUES (?, ?, 'user', 0, 0, 0)`,
)
.run(E2E_USER_B, hashB);
userB = { id: Number(resB.lastInsertRowid) };
}
ensureUserDefaultCategories(userB.id);
seedDemoData(userB.id);
const billB = db.prepare('SELECT id FROM bills WHERE user_id = ? ORDER BY id LIMIT 1').get(userB.id);
// Fixture consumed by the probe spec (git-ignored under e2e/.auth/).
const fixturePath = path.join(__dirname, '..', '.auth', 'fixture.json');
fs.mkdirSync(path.dirname(fixturePath), { recursive: true });
fs.writeFileSync(
fixturePath,
JSON.stringify({ userAId: user.id, userBId: userB.id, userBBillId: billB ? billB.id : null }, null, 2),
);
console.log(`[e2e] Scratch DB ready at ${dbPath}`);
console.log(`[e2e] User A '${E2E_USER}' (id=${user.id}) — ${JSON.stringify(result)}`);
console.log(`[e2e] User B '${E2E_USER_B}' (id=${userB.id}) — IDOR target bill id=${billB ? billB.id : 'none'}`);