test(backend): +31 integration tests for permissions/overview/search/sessions/features
Coverage: - Permissions: role/user toggle + audit + token_version bump, /me, 403 non-admin (10 tests) - Admin overview: shape, all fields, types, auth guard, empty DB zeros (4 tests) - Cmd+K search: shape, min-query empty, SQL injection sanity, user lookup (5 tests) - Session delete: CASCADE, audit entry, 404 missing, 403 non-admin (4 tests) - Feature gates: disabled flag returns 404, enabled returns 401/200, admin API toggle (5 tests) - setup.js: add /api/permissions, /api/pet, /api/biochem routes for test coverage tests 66 (was 35) · pass 63 (was 32) · fail 3 (baseline auth.test.js, unchanged) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Integration tests: /api/permissions
|
||||
* Covers: role toggle + audit + token_version bump, user override, reset, /me, 403 for non-admin.
|
||||
*/
|
||||
const { describe, it, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { db, inject, getToken, cleanup } = require('./setup');
|
||||
|
||||
after(() => cleanup());
|
||||
|
||||
describe('Permissions', () => {
|
||||
let adminToken, teacherUser, studentUser;
|
||||
|
||||
before(async () => {
|
||||
const a = await getToken('admin');
|
||||
adminToken = a.token;
|
||||
|
||||
teacherUser = await getToken('teacher');
|
||||
studentUser = await getToken('student');
|
||||
});
|
||||
|
||||
// ── 1. Non-admin gets 403 on POST /api/permissions ────────────────────────
|
||||
it('non-admin (teacher) gets 403 on POST /api/permissions', async () => {
|
||||
const res = await inject('POST', '/api/permissions', {
|
||||
role: 'student', permission: 'tests.free', enabled: false,
|
||||
}, teacherUser.token);
|
||||
assert.equal(res.status, 403, `expected 403, got ${res.status}`);
|
||||
});
|
||||
|
||||
it('non-admin (student) gets 403 on POST /api/permissions', async () => {
|
||||
const res = await inject('POST', '/api/permissions', {
|
||||
role: 'student', permission: 'tests.free', enabled: false,
|
||||
}, studentUser.token);
|
||||
assert.equal(res.status, 403, `expected 403, got ${res.status}`);
|
||||
});
|
||||
|
||||
// ── 2. No token gets 401 on GET /api/permissions ──────────────────────────
|
||||
it('unauthenticated request gets 401 on GET /api/permissions', async () => {
|
||||
const res = await inject('GET', '/api/permissions', null, null);
|
||||
assert.equal(res.status, 401, `expected 401, got ${res.status}`);
|
||||
});
|
||||
|
||||
// ── 3. Admin can toggle role-level permission + token_version bumped ───────
|
||||
it('admin toggles role permission and student token_version is bumped', async () => {
|
||||
const tvBefore = db.prepare('SELECT token_version FROM users WHERE id = ?')
|
||||
.get(studentUser.userId).token_version;
|
||||
|
||||
const res = await inject('POST', '/api/permissions', {
|
||||
role: 'student', permission: 'tests.free', enabled: false,
|
||||
}, adminToken);
|
||||
assert.equal(res.status, 200, `expected 200, got ${res.status}: ${JSON.stringify(res.body)}`);
|
||||
assert.equal(res.body.ok, true);
|
||||
|
||||
const tvAfter = db.prepare('SELECT token_version FROM users WHERE id = ?')
|
||||
.get(studentUser.userId).token_version;
|
||||
assert.ok(tvAfter > tvBefore, `token_version should increase (was ${tvBefore}, got ${tvAfter})`);
|
||||
|
||||
// Restore
|
||||
await inject('POST', '/api/permissions', {
|
||||
role: 'student', permission: 'tests.free', enabled: true,
|
||||
}, adminToken);
|
||||
});
|
||||
|
||||
// ── 4. Audit entry created for role permission toggle ─────────────────────
|
||||
it('audit entry created when role permission is toggled', async () => {
|
||||
// Clear audit log first to count new entries
|
||||
const countBefore = db.prepare(
|
||||
"SELECT COUNT(*) AS n FROM admin_audit_log WHERE action = 'permission.set'"
|
||||
).get().n;
|
||||
|
||||
await inject('POST', '/api/permissions', {
|
||||
role: 'teacher', permission: 'questions.manage', enabled: true,
|
||||
}, adminToken);
|
||||
|
||||
const countAfter = db.prepare(
|
||||
"SELECT COUNT(*) AS n FROM admin_audit_log WHERE action = 'permission.set'"
|
||||
).get().n;
|
||||
|
||||
assert.ok(countAfter > countBefore, 'Expected new permission.set audit entry');
|
||||
});
|
||||
|
||||
// ── 5. POST /api/permissions/users/:id sets user override + bumps token_version
|
||||
it('admin sets user override and target token_version bumped', async () => {
|
||||
const tvBefore = db.prepare('SELECT token_version FROM users WHERE id = ?')
|
||||
.get(studentUser.userId).token_version;
|
||||
|
||||
const res = await inject('POST', `/api/permissions/users/${studentUser.userId}`, {
|
||||
permission: 'shop.purchase', enabled: false,
|
||||
}, adminToken);
|
||||
assert.equal(res.status, 200, `expected 200, got ${res.status}: ${JSON.stringify(res.body)}`);
|
||||
assert.equal(res.body.ok, true);
|
||||
|
||||
const tvAfter = db.prepare('SELECT token_version FROM users WHERE id = ?')
|
||||
.get(studentUser.userId).token_version;
|
||||
assert.ok(tvAfter > tvBefore, `token_version should increase for targeted user`);
|
||||
|
||||
// Verify the override was persisted
|
||||
const row = db.prepare(
|
||||
'SELECT enabled FROM user_permissions WHERE user_id = ? AND permission = ?'
|
||||
).get(studentUser.userId, 'shop.purchase');
|
||||
assert.ok(row, 'user_permissions row should exist');
|
||||
assert.equal(row.enabled, 0, 'override should be disabled');
|
||||
});
|
||||
|
||||
// ── 6. Audit entry created for user override ─────────────────────────────
|
||||
it('audit entry created when user override is set', async () => {
|
||||
const countBefore = db.prepare(
|
||||
"SELECT COUNT(*) AS n FROM admin_audit_log WHERE action = 'permission.user_set'"
|
||||
).get().n;
|
||||
|
||||
await inject('POST', `/api/permissions/users/${teacherUser.userId}`, {
|
||||
permission: 'results.export', enabled: false,
|
||||
}, adminToken);
|
||||
|
||||
const countAfter = db.prepare(
|
||||
"SELECT COUNT(*) AS n FROM admin_audit_log WHERE action = 'permission.user_set'"
|
||||
).get().n;
|
||||
assert.ok(countAfter > countBefore, 'Expected new permission.user_set audit entry');
|
||||
});
|
||||
|
||||
// ── 7. DELETE /api/permissions/users/:id/reset clears overrides, bumps tv ─
|
||||
it('reset user permissions clears overrides and bumps token_version', async () => {
|
||||
// Ensure there's at least one override
|
||||
await inject('POST', `/api/permissions/users/${studentUser.userId}`, {
|
||||
permission: 'board.post', enabled: false,
|
||||
}, adminToken);
|
||||
|
||||
const tvBefore = db.prepare('SELECT token_version FROM users WHERE id = ?')
|
||||
.get(studentUser.userId).token_version;
|
||||
|
||||
const res = await inject('DELETE', `/api/permissions/users/${studentUser.userId}/reset`, {}, adminToken);
|
||||
assert.equal(res.status, 200, `expected 200, got ${res.status}: ${JSON.stringify(res.body)}`);
|
||||
assert.equal(res.body.ok, true);
|
||||
|
||||
const tvAfter = db.prepare('SELECT token_version FROM users WHERE id = ?')
|
||||
.get(studentUser.userId).token_version;
|
||||
assert.ok(tvAfter > tvBefore, 'token_version should increase after reset');
|
||||
|
||||
// No more overrides in user_permissions
|
||||
const rows = db.prepare('SELECT * FROM user_permissions WHERE user_id = ?')
|
||||
.all(studentUser.userId);
|
||||
assert.equal(rows.length, 0, 'user_permissions should be empty after reset');
|
||||
});
|
||||
|
||||
// ── 8. GET /api/permissions/me returns effective permissions ──────────────
|
||||
it('GET /api/permissions/me returns role and permissions array for student', async () => {
|
||||
// Use a fresh student whose token_version has not been bumped by earlier tests
|
||||
const freshStudent = await getToken('student');
|
||||
const res = await inject('GET', '/api/permissions/me', null, freshStudent.token);
|
||||
assert.equal(res.status, 200, `expected 200, got ${res.status}: ${JSON.stringify(res.body)}`);
|
||||
assert.equal(res.body.role, 'student');
|
||||
assert.ok(Array.isArray(res.body.permissions), 'permissions should be an array');
|
||||
assert.ok(res.body.permissions.length > 0, 'permissions array should not be empty');
|
||||
// Each entry must have key and effective
|
||||
for (const p of res.body.permissions) {
|
||||
assert.ok('key' in p, 'permission entry missing key');
|
||||
assert.ok('effective' in p, 'permission entry missing effective');
|
||||
}
|
||||
});
|
||||
|
||||
it('GET /api/permissions/me for admin returns empty permissions (admin bypasses all)', async () => {
|
||||
const res = await inject('GET', '/api/permissions/me', null, adminToken);
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.role, 'admin');
|
||||
assert.ok(Array.isArray(res.body.permissions), 'admin permissions should be an array');
|
||||
assert.equal(res.body.permissions.length, 0, 'admin should have empty permissions list');
|
||||
});
|
||||
|
||||
// ── 9. Invalid role rejected ───────────────────────────────────────────────
|
||||
it('POST /api/permissions with invalid role returns 400', async () => {
|
||||
const res = await inject('POST', '/api/permissions', {
|
||||
role: 'superadmin', permission: 'tests.free', enabled: true,
|
||||
}, adminToken);
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user