2026-05-30 16:13:37 -05:00
|
|
|
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;
|
|
|
|
|
|
refactor(server): migrate middleware, workers, and db layer to TypeScript (.cts)
- middleware/ (5): requireAuth, securityHeaders, errorFormatter, csrf,
rateLimiter — fully typed against the shared http Req/Res/Next types.
- workers/dailyWorker — fully typed.
- db/ (4): database, subscriptionCatalogSeed, migrations/versionedMigrations,
migrations/legacyReconcileMigrations — large dynamic schema/migration infra,
converted with @ts-nocheck (typing deferred). All requires updated to .cts.
Behavior-preserving. Verified: typecheck:server 0, check:server 0, suite 226/226.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 11:34:08 -05:00
|
|
|
const { getDb, closeDb } = require('../db/database.cts');
|
2026-07-10 17:00:01 -05:00
|
|
|
const { formatError } = require('../utils/apiError.cts');
|
2026-07-06 11:16:14 -05:00
|
|
|
const { getTracker } = require('../services/trackerService.cts');
|
2026-05-30 16:13:37 -05:00
|
|
|
|
|
|
|
|
function createUser(db, suffix) {
|
2026-07-06 14:23:53 -05:00
|
|
|
return db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-30 16:13:37 -05:00
|
|
|
INSERT INTO users (username, password_hash, role, active, email, created_at, updated_at)
|
|
|
|
|
VALUES (?, 'x', 'user', 1, ?, datetime('now'), datetime('now'))
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.run(`reorder-user-${suffix}`, `reorder-user-${suffix}@local`).lastInsertRowid;
|
2026-05-30 16:13:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createBill(db, userId, name, dueDay) {
|
2026-07-06 14:23:53 -05:00
|
|
|
return db
|
|
|
|
|
.prepare(
|
|
|
|
|
`
|
2026-05-30 16:13:37 -05:00
|
|
|
INSERT INTO bills (user_id, name, due_day, expected_amount)
|
2026-06-11 20:12:31 -05:00
|
|
|
VALUES (?, ?, ?, 2500)
|
2026-07-06 14:23:53 -05:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.run(userId, name, dueDay).lastInsertRowid;
|
2026-05-30 16:13:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function callBillsRoute(routePath, method, { userId, params = {}, query = {}, body = {} }) {
|
2026-07-06 11:26:50 -05:00
|
|
|
const billsRouter = require('../routes/bills.cts');
|
2026-07-06 14:23:53 -05:00
|
|
|
const layer = billsRouter.stack.find(
|
|
|
|
|
(item) => item.route?.path === routePath && item.route.methods[method],
|
|
|
|
|
);
|
2026-05-30 16:13:37 -05:00
|
|
|
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) {
|
2026-07-10 17:00:01 -05:00
|
|
|
// Routes follow the throw-pattern: mirror the app's terminal error
|
|
|
|
|
// handler so thrown ApiErrors resolve to the same wire shape.
|
|
|
|
|
if (err && (err.status || err.name === 'ApiError')) {
|
|
|
|
|
resolve({ status: err.status || 500, data: formatError(err) });
|
|
|
|
|
} else {
|
|
|
|
|
reject(err);
|
|
|
|
|
}
|
2026-05-30 16:13:37 -05:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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'));
|
2026-07-06 14:23:53 -05:00
|
|
|
assert.deepEqual(
|
|
|
|
|
initial.rows.map((row) => row.id),
|
|
|
|
|
[power, water, rent],
|
|
|
|
|
);
|
2026-05-30 16:13:37 -05:00
|
|
|
|
|
|
|
|
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'));
|
2026-07-06 14:23:53 -05:00
|
|
|
assert.deepEqual(
|
|
|
|
|
tracker.rows.map((row) => row.id),
|
|
|
|
|
[rent, water, power],
|
|
|
|
|
);
|
2026-05-30 16:13:37 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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);
|
2026-07-06 14:23:53 -05:00
|
|
|
assert.equal(
|
|
|
|
|
getTracker(userId, { year: 2026, month: 5 }, new Date('2026-05-01T12:00:00Z')).rows.length,
|
|
|
|
|
0,
|
|
|
|
|
);
|
2026-05-30 16:13:37 -05:00
|
|
|
|
|
|
|
|
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);
|
2026-07-06 14:23:53 -05:00
|
|
|
assert.equal(
|
|
|
|
|
getTracker(userId, { year: 2026, month: 5 }, new Date('2026-05-01T12:00:00Z')).rows.length,
|
|
|
|
|
1,
|
|
|
|
|
);
|
2026-05-30 16:13:37 -05:00
|
|
|
});
|