BillTracker/tests/seedDemoData.test.js

130 lines
9.0 KiB
JavaScript

'use strict';
// Demo seed + surgical removal. seedDemoData() must populate every user-facing
// surface, and clearSeededDemoData() must remove EXACTLY the seeded rows — never a
// user's own bills/categories/planning, and leave no orphans. This is the contract
// the "Seed / Clear Demo Data" buttons depend on.
const test = require('node:test');
const assert = require('node:assert/strict');
const os = require('node:os');
const path = require('node:path');
const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-seed-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database.cts');
const { seedDemoData } = require('../scripts/seedDemoData.cts');
const { clearSeededDemoData, eraseUserData } = require('../services/userDataService.cts');
const mkUser = (db, name) =>
db.prepare("INSERT INTO users (username, password_hash, role, active) VALUES (?, 'hash', 'user', 1)").run(name).lastInsertRowid;
const num = (db, sql, ...p) => db.prepare(sql).get(...p).n;
let db, userA, userB, userC;
test.before(() => {
db = getDb();
db.pragma('foreign_keys = ON');
userA = mkUser(db, 'seed-a');
userB = mkUser(db, 'seed-b');
userC = mkUser(db, 'seed-c');
seedDemoData(userA);
seedDemoData(userB);
seedDemoData(userC);
});
test.after(() => {
closeDb();
for (const s of ['', '-wal', '-shm']) { try { fs.unlinkSync(dbPath + s); } catch {} }
});
test('seed populates every user-facing surface', () => {
assert.equal(num(db, 'SELECT COUNT(*) n FROM bills WHERE user_id=? AND is_seeded=1', userA), 23);
// 8 of 9 bill categories reuse the user's defaults (not flagged); Healthcare + 5 spending are created.
assert.equal(num(db, 'SELECT COUNT(*) n FROM categories WHERE user_id=? AND is_seeded=1', userA), 6);
assert.equal(num(db, 'SELECT COUNT(*) n FROM categories WHERE user_id=? AND is_seeded=1 AND spending_enabled=1', userA), 5);
assert.ok(num(db, 'SELECT COUNT(*) n FROM payments p JOIN bills b ON b.id=p.bill_id WHERE b.user_id=?', userA) > 40, 'payment history');
assert.ok(num(db, 'SELECT COUNT(*) n FROM transactions WHERE user_id=? AND is_seeded=1', userA) > 10, 'bank transactions');
assert.ok(num(db, "SELECT COUNT(*) n FROM transactions WHERE user_id=? AND match_status='matched'", userA) >= 3, 'matched txns');
assert.ok(num(db, "SELECT COUNT(*) n FROM transactions WHERE user_id=? AND match_status='unmatched'", userA) >= 5, 'unmatched txns → suggestions/recommendations');
assert.ok(num(db, 'SELECT COUNT(*) n FROM transactions WHERE user_id=? AND spending_category_id IS NOT NULL', userA) > 5, 'categorised spending');
assert.ok(num(db, 'SELECT COUNT(*) n FROM monthly_bill_state m JOIN bills b ON b.id=m.bill_id WHERE b.user_id=?', userA) >= 4, 'skip/override/note/snooze');
assert.equal(num(db, 'SELECT COUNT(*) n FROM category_groups WHERE user_id=? AND is_seeded=1', userA), 2);
assert.equal(num(db, 'SELECT COUNT(*) n FROM snowball_plans WHERE user_id=? AND is_seeded=1', userA), 1);
assert.equal(num(db, 'SELECT COUNT(*) n FROM spending_budgets WHERE user_id=? AND is_seeded=1', userA), 5);
assert.ok(num(db, 'SELECT COUNT(*) n FROM spending_category_rules WHERE user_id=? AND is_seeded=1', userA) >= 3);
assert.ok(num(db, 'SELECT COUNT(*) n FROM monthly_income WHERE user_id=? AND is_seeded=1', userA) >= 1);
assert.ok(num(db, 'SELECT COUNT(*) n FROM monthly_starting_amounts WHERE user_id=? AND is_seeded=1', userA) >= 1);
assert.ok(num(db, 'SELECT COUNT(*) n FROM bill_merchant_rules r JOIN bills b ON b.id=r.bill_id WHERE b.user_id=?', userA) >= 1);
assert.equal(num(db, 'SELECT COUNT(*) n FROM data_sources WHERE user_id=? AND is_seeded=1', userA), 1);
assert.equal(num(db, 'SELECT COUNT(*) n FROM financial_accounts WHERE user_id=? AND is_seeded=1', userA), 1);
assert.equal(num(db, 'SELECT COUNT(*) n FROM calendar_tokens WHERE user_id=? AND is_seeded=1', userA), 1);
assert.ok(db.prepare('SELECT snowball_extra_payment e FROM users WHERE id=?').get(userA).e > 0, 'snowball extra-payment set');
// Matched payments link a real seeded transaction; money is integer cents.
assert.ok(num(db, "SELECT COUNT(*) n FROM payments WHERE payment_source='transaction_match' AND transaction_id IN (SELECT id FROM transactions)") >= 3);
assert.equal(num(db, 'SELECT COUNT(*) n FROM payments WHERE amount <> CAST(amount AS INT)'), 0, 'cents-clean');
});
test('seed refuses to double-seed (idempotency guard)', () => {
assert.throws(() => seedDemoData(userA), (e) => e.code === 'ALREADY_SEEDED' && e.status === 409);
});
test('clear removes ONLY seeded data — user bills/categories & no orphans', () => {
// A user's OWN bill in a seed-created category (Healthcare) + one in a default category.
const healthcareId = db.prepare("SELECT id FROM categories WHERE user_id=? AND name='Healthcare'").get(userA).id;
assert.equal(db.prepare('SELECT is_seeded s FROM categories WHERE id=?').get(healthcareId).s, 1, 'Healthcare is a seeded category');
const utilitiesId = db.prepare("SELECT id FROM categories WHERE user_id=? AND name='Utilities'").get(userA).id;
const ownBill1 = db.prepare("INSERT INTO bills (user_id, name, category_id, due_day, expected_amount, active, is_seeded) VALUES (?, 'My Doctor', ?, 9, 5000, 1, 0)").run(userA, healthcareId).lastInsertRowid;
const ownBill2 = db.prepare("INSERT INTO bills (user_id, name, category_id, due_day, expected_amount, active, is_seeded) VALUES (?, 'My Power', ?, 9, 6000, 1, 0)").run(userA, utilitiesId).lastInsertRowid;
const result = clearSeededDemoData(db, userA);
assert.ok(result.removed > 0);
// Every seeded surface gone.
assert.equal(num(db, 'SELECT COUNT(*) n FROM bills WHERE user_id=? AND is_seeded=1', userA), 0);
assert.equal(num(db, 'SELECT COUNT(*) n FROM transactions WHERE user_id=? AND is_seeded=1', userA), 0);
assert.equal(num(db, 'SELECT COUNT(*) n FROM data_sources WHERE user_id=? AND is_seeded=1', userA), 0);
assert.equal(num(db, 'SELECT COUNT(*) n FROM financial_accounts WHERE user_id=? AND is_seeded=1', userA), 0);
assert.equal(num(db, 'SELECT COUNT(*) n FROM snowball_plans WHERE user_id=? AND is_seeded=1', userA), 0);
assert.equal(num(db, 'SELECT COUNT(*) n FROM spending_budgets WHERE user_id=? AND is_seeded=1', userA), 0);
assert.equal(num(db, 'SELECT COUNT(*) n FROM monthly_income WHERE user_id=? AND is_seeded=1', userA), 0);
assert.equal(num(db, 'SELECT COUNT(*) n FROM category_groups WHERE user_id=? AND is_seeded=1', userA), 0);
// No orphans: payments/transactions/state for deleted bills are gone.
assert.equal(num(db, 'SELECT COUNT(*) n FROM payments p LEFT JOIN bills b ON b.id=p.bill_id WHERE b.id IS NULL'), 0, 'no orphan payments');
assert.equal(num(db, 'SELECT COUNT(*) n FROM monthly_bill_state m LEFT JOIN bills b ON b.id=m.bill_id WHERE b.id IS NULL'), 0, 'no orphan monthly_bill_state');
// User's own data preserved.
assert.ok(db.prepare('SELECT 1 FROM bills WHERE id=?').get(ownBill1), 'user bill 1 survives');
assert.ok(db.prepare('SELECT 1 FROM bills WHERE id=?').get(ownBill2), 'user bill 2 survives');
assert.equal(db.prepare('SELECT category_id c FROM bills WHERE id=?').get(ownBill1).c, healthcareId, 'still categorised (not SET NULL)');
// Healthcare seeded category SURVIVES because a user bill still references it.
assert.ok(db.prepare('SELECT 1 FROM categories WHERE id=?').get(healthcareId), 'referenced seeded category kept');
// A seeded spending category with no user bill IS removed.
assert.equal(num(db, "SELECT COUNT(*) n FROM categories WHERE user_id=? AND name='Groceries'", userA), 0, 'unreferenced seeded category removed');
// Default categories untouched.
assert.ok(db.prepare('SELECT 1 FROM categories WHERE id=?').get(utilitiesId), 'default category kept');
// Snowball extra-payment reset.
assert.equal(db.prepare('SELECT snowball_extra_payment e FROM users WHERE id=?').get(userA).e, 0);
});
test('clear leaves other users\' seeded data untouched', () => {
assert.equal(num(db, 'SELECT COUNT(*) n FROM bills WHERE user_id=? AND is_seeded=1', userB), 23, 'user B intact after clearing A');
assert.ok(num(db, 'SELECT COUNT(*) n FROM transactions WHERE user_id=? AND is_seeded=1', userB) > 0);
});
test('clear is audited to import_history', () => {
const row = db.prepare("SELECT file_type, source_filename FROM import_history WHERE user_id=? AND file_type='clear-demo' ORDER BY id DESC LIMIT 1").get(userA);
assert.equal(row.file_type, 'clear-demo');
assert.equal(row.source_filename, 'clear-demo-data');
});
test('eraseUserData still wipes seeded demo data wholesale', () => {
assert.ok(num(db, 'SELECT COUNT(*) n FROM bills WHERE user_id=?', userC) > 0);
eraseUserData(db, userC);
assert.equal(num(db, 'SELECT COUNT(*) n FROM bills WHERE user_id=?', userC), 0);
assert.equal(num(db, 'SELECT COUNT(*) n FROM transactions WHERE user_id=?', userC), 0);
assert.equal(num(db, 'SELECT COUNT(*) n FROM snowball_plans WHERE user_id=?', userC), 0);
assert.equal(num(db, 'SELECT COUNT(*) n FROM payments p LEFT JOIN bills b ON b.id=p.bill_id WHERE b.id IS NULL'), 0, 'no orphans after erase');
});