const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const dbPath = path.join(os.tmpdir(), `bill-tracker-reorder-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); const { getTracker } = require('../services/trackerService'); function createUser(db, suffix) { return db.prepare(` INSERT INTO users (username, password_hash, role, active, email, created_at, updated_at) VALUES (?, 'x', 'user', 1, ?, datetime('now'), datetime('now')) `).run(`reorder-user-${suffix}`, `reorder-user-${suffix}@local`).lastInsertRowid; } function createBill(db, userId, name, dueDay) { return db.prepare(` INSERT INTO bills (user_id, name, due_day, expected_amount) VALUES (?, ?, ?, 2500) `).run(userId, name, dueDay).lastInsertRowid; } function callBillsRoute(routePath, method, { userId, params = {}, query = {}, body = {} }) { const billsRouter = require('../routes/bills'); const layer = billsRouter.stack.find(item => item.route?.path === routePath && item.route.methods[method]); assert.ok(layer, `route ${method.toUpperCase()} ${routePath} should exist`); const handler = layer.route.stack[0].handle; return new Promise((resolve, reject) => { const req = { body, params, query, user: { id: userId, role: 'user' }, }; const res = { statusCode: 200, status(code) { this.statusCode = code; return this; }, json(data) { resolve({ status: this.statusCode, data }); }, }; try { handler(req, res); } catch (err) { reject(err); } }); } test.after(() => { closeDb(); for (const suffix of ['', '-wal', '-shm']) { fs.rmSync(`${dbPath}${suffix}`, { force: true }); } }); test('bill reorder endpoint persists tracker order for the current user', async () => { const db = getDb(); const userId = createUser(db, 'owner'); const water = createBill(db, userId, 'Water', 10); const power = createBill(db, userId, 'Power', 5); const rent = createBill(db, userId, 'Rent', 20); const initial = getTracker(userId, { year: 2026, month: 5 }, new Date('2026-05-01T12:00:00Z')); assert.deepEqual(initial.rows.map(row => row.id), [power, water, rent]); const response = await callBillsRoute('/reorder', 'put', { userId, body: { [rent]: 0, [water]: 1, [power]: 2, }, }); assert.equal(response.status, 200); assert.equal(response.data.success, true); const tracker = getTracker(userId, { year: 2026, month: 5 }, new Date('2026-05-01T12:00:00Z')); assert.deepEqual(tracker.rows.map(row => row.id), [rent, water, power]); }); test('bill reorder rejects bills outside the current user scope', async () => { const db = getDb(); const ownerId = createUser(db, 'scoped-owner'); const otherId = createUser(db, 'scoped-other'); const ownerBill = createBill(db, ownerId, 'Internet', 7); const otherBill = createBill(db, otherId, 'Other Internet', 7); const response = await callBillsRoute('/reorder', 'put', { userId: ownerId, body: { [ownerBill]: 0, [otherBill]: 1, }, }); assert.equal(response.status, 404); }); test('bill archived endpoint toggles tracker visibility without deleting the bill', async () => { const db = getDb(); const userId = createUser(db, 'archive'); const billId = createBill(db, userId, 'Streaming', 12); const archived = await callBillsRoute('/:id/archived', 'put', { userId, params: { id: String(billId) }, body: { archived: true }, }); assert.equal(archived.status, 200); assert.equal(archived.data.archived, true); assert.equal(getTracker(userId, { year: 2026, month: 5 }, new Date('2026-05-01T12:00:00Z')).rows.length, 0); const restored = await callBillsRoute('/:id/archived', 'put', { userId, params: { id: String(billId) }, body: { archived: false }, }); assert.equal(restored.status, 200); assert.equal(restored.data.archived, false); assert.equal(getTracker(userId, { year: 2026, month: 5 }, new Date('2026-05-01T12:00:00Z')).rows.length, 1); });