#!/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'}`);