86 lines
4.2 KiB
JavaScript
86 lines
4.2 KiB
JavaScript
|
|
'use strict';
|
||
|
|
|
||
|
|
// Batch 5: "erase my data" wipes the requesting user's financial data only — never
|
||
|
|
// another user's, never the account/auth — and re-seeds default categories.
|
||
|
|
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-erase-${process.pid}.sqlite`);
|
||
|
|
process.env.DB_PATH = dbPath;
|
||
|
|
|
||
|
|
const { getDb, closeDb } = require('../db/database');
|
||
|
|
const { eraseUserData } = require('../services/userDataService');
|
||
|
|
|
||
|
|
function makeUser(db, name) {
|
||
|
|
return db.prepare("INSERT INTO users (username, password_hash, role, active) VALUES (?, 'hash', 'user', 1)").run(name).lastInsertRowid;
|
||
|
|
}
|
||
|
|
function seedFinancialData(db, userId, label) {
|
||
|
|
const catId = db.prepare("INSERT INTO categories (user_id, name) VALUES (?, ?)").run(userId, `${label}-cat`).lastInsertRowid;
|
||
|
|
const billId = db.prepare("INSERT INTO bills (user_id, name, category_id, due_day, expected_amount, active) VALUES (?, ?, ?, 1, 5000, 1)").run(userId, `${label}-bill`, catId).lastInsertRowid;
|
||
|
|
db.prepare("INSERT INTO payments (bill_id, amount, paid_date, payment_source) VALUES (?, 5000, '2026-06-01', 'manual')").run(billId);
|
||
|
|
const dsId = db.prepare("INSERT INTO data_sources (user_id, type, provider, name, status) VALUES (?, 'file_import', 'csv', 'CSV Import', 'active')").run(userId).lastInsertRowid;
|
||
|
|
db.prepare("INSERT INTO transactions (user_id, data_source_id, source_type, provider_transaction_id, amount, match_status, ignored) VALUES (?, ?, 'file_import', ?, -5000, 'unmatched', 0)").run(userId, dsId, `${label}-tx`);
|
||
|
|
return { billId, catId, dsId };
|
||
|
|
}
|
||
|
|
const countFinancial = (db, userId) => ({
|
||
|
|
bills: db.prepare('SELECT COUNT(*) n FROM bills WHERE user_id=?').get(userId).n,
|
||
|
|
payments: db.prepare('SELECT COUNT(*) n FROM payments p JOIN bills b ON b.id=p.bill_id WHERE b.user_id=?').get(userId).n,
|
||
|
|
transactions: db.prepare('SELECT COUNT(*) n FROM transactions WHERE user_id=?').get(userId).n,
|
||
|
|
data_sources: db.prepare('SELECT COUNT(*) n FROM data_sources WHERE user_id=?').get(userId).n,
|
||
|
|
categories: db.prepare('SELECT COUNT(*) n FROM categories WHERE user_id=?').get(userId).n,
|
||
|
|
});
|
||
|
|
|
||
|
|
let db, userA, userB;
|
||
|
|
test.before(() => {
|
||
|
|
db = getDb();
|
||
|
|
db.pragma('foreign_keys = ON');
|
||
|
|
userA = makeUser(db, 'erase-a');
|
||
|
|
userB = makeUser(db, 'erase-b');
|
||
|
|
seedFinancialData(db, userA, 'a');
|
||
|
|
seedFinancialData(db, userB, 'b');
|
||
|
|
// a session + a webauthn credential for user A to prove auth is preserved
|
||
|
|
db.prepare("INSERT INTO sessions (id, user_id, expires_at) VALUES ('sess-a', ?, datetime('now','+1 day'))").run(userA);
|
||
|
|
});
|
||
|
|
test.after(() => {
|
||
|
|
closeDb();
|
||
|
|
for (const s of ['', '-wal', '-shm']) { try { fs.unlinkSync(dbPath + s); } catch {} }
|
||
|
|
});
|
||
|
|
|
||
|
|
test('erase wipes the requesting user\'s financial data', () => {
|
||
|
|
const before = countFinancial(db, userA);
|
||
|
|
assert.ok(before.bills > 0 && before.payments > 0 && before.transactions > 0 && before.data_sources > 0);
|
||
|
|
|
||
|
|
const result = eraseUserData(db, userA);
|
||
|
|
assert.ok(result.erased > 0);
|
||
|
|
|
||
|
|
const after = countFinancial(db, userA);
|
||
|
|
assert.equal(after.bills, 0);
|
||
|
|
assert.equal(after.payments, 0, 'bill-child payments cascade/removed');
|
||
|
|
assert.equal(after.transactions, 0);
|
||
|
|
assert.equal(after.data_sources, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('erase re-seeds default categories (app stays usable)', () => {
|
||
|
|
const cats = db.prepare('SELECT COUNT(*) n FROM categories WHERE user_id=?').get(userA).n;
|
||
|
|
assert.ok(cats > 0, 'default categories re-seeded after wipe');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('erase never touches another user\'s data', () => {
|
||
|
|
const b = countFinancial(db, userB);
|
||
|
|
assert.ok(b.bills > 0 && b.payments > 0 && b.transactions > 0 && b.data_sources > 0, 'user B untouched');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('erase preserves the account and auth (session, user row)', () => {
|
||
|
|
assert.ok(db.prepare('SELECT 1 FROM users WHERE id=?').get(userA), 'account preserved');
|
||
|
|
assert.ok(db.prepare("SELECT 1 FROM sessions WHERE id='sess-a'").get(), 'session preserved');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('erase is audited to import_history', () => {
|
||
|
|
const row = db.prepare("SELECT file_type, source_filename FROM import_history WHERE user_id=? ORDER BY id DESC LIMIT 1").get(userA);
|
||
|
|
assert.equal(row.file_type, 'erase-data');
|
||
|
|
assert.equal(row.source_filename, 'erase-my-data');
|
||
|
|
});
|