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-category-groups-test-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); 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(`category-groups-${suffix}`, `category-groups-${suffix}@local`).lastInsertRowid; } function createCategory(db, userId, name) { return db.prepare(` INSERT INTO categories (user_id, name) VALUES (?, ?) `).run(userId, name).lastInsertRowid; } function callCategoriesRoute(routePath, method, { userId, params = {}, body = {} }) { const categoriesRouter = require('../routes/categories'); const layer = categoriesRouter.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, 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('category groups can be created, listed, renamed, and deleted', async () => { const db = getDb(); const userId = createUser(db, 'crud'); const created = await callCategoriesRoute('/groups', 'post', { userId, body: { name: 'Bills' } }); assert.equal(created.status, 201); assert.equal(created.data.name, 'Bills'); const groupId = created.data.id; const listed = await callCategoriesRoute('/groups', 'get', { userId }); assert.equal(listed.status, 200); assert.deepEqual(listed.data.map(g => g.name), ['Bills']); const renamed = await callCategoriesRoute('/groups/:id', 'put', { userId, params: { id: groupId }, body: { name: 'Fixed Costs' } }); assert.equal(renamed.status, 200); assert.equal(renamed.data.name, 'Fixed Costs'); const deleted = await callCategoriesRoute('/groups/:id', 'delete', { userId, params: { id: groupId } }); assert.equal(deleted.status, 200); assert.equal(deleted.data.success, true); const listedAfter = await callCategoriesRoute('/groups', 'get', { userId }); assert.deepEqual(listedAfter.data, []); }); test('creating a duplicate group name for the same user is rejected', async () => { const db = getDb(); const userId = createUser(db, 'dup'); await callCategoriesRoute('/groups', 'post', { userId, body: { name: 'Everyday' } }); const dup = await callCategoriesRoute('/groups', 'post', { userId, body: { name: 'Everyday' } }); assert.equal(dup.status, 409); }); test('assigning a category to a group requires the group to belong to the user', async () => { const db = getDb(); const ownerId = createUser(db, 'group-owner'); const otherId = createUser(db, 'group-other'); const category = createCategory(db, ownerId, 'Groceries'); const otherGroup = await callCategoriesRoute('/groups', 'post', { userId: otherId, body: { name: 'Not Mine' } }); const result = await callCategoriesRoute('/:id', 'put', { userId: ownerId, params: { id: category }, body: { name: 'Groceries', group_id: otherGroup.data.id }, }); assert.equal(result.status, 404); }); test('assigning a category to an owned group succeeds and is reflected on read', async () => { const db = getDb(); const userId = createUser(db, 'group-assign'); const category = createCategory(db, userId, 'Dining'); const group = await callCategoriesRoute('/groups', 'post', { userId, body: { name: 'Everyday' } }); const updated = await callCategoriesRoute('/:id', 'put', { userId, params: { id: category }, body: { name: 'Dining', group_id: group.data.id }, }); assert.equal(updated.status, 200); assert.equal(updated.data.group_id, group.data.id); });