85 lines
3.4 KiB
JavaScript
85 lines
3.4 KiB
JavaScript
|
|
#!/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'}`);
|