'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', ); });