1fdbb9a445
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>
80 lines
3.5 KiB
JavaScript
80 lines
3.5 KiB
JavaScript
'use strict';
|
|
/**
|
|
* Integration tests: GET /api/admin/overview
|
|
* Covers: shape, required fields + types, auth guard, empty DB returns zeros.
|
|
*/
|
|
const { describe, it, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { inject, getToken, cleanup } = require('./setup');
|
|
|
|
after(() => cleanup());
|
|
|
|
describe('Admin Overview', () => {
|
|
let adminToken, teacherToken;
|
|
|
|
before(async () => {
|
|
const a = await getToken('admin');
|
|
adminToken = a.token;
|
|
const t = await getToken('teacher');
|
|
teacherToken = t.token;
|
|
});
|
|
|
|
// ── 1. 401 without token ──────────────────────────────────────────────────
|
|
it('returns 401 without auth token', async () => {
|
|
const res = await inject('GET', '/api/admin/overview', null, null);
|
|
assert.equal(res.status, 401, `expected 401, got ${res.status}`);
|
|
});
|
|
|
|
// ── 2. 403 for non-admin ──────────────────────────────────────────────────
|
|
it('returns 403 for teacher (non-admin)', async () => {
|
|
const res = await inject('GET', '/api/admin/overview', null, teacherToken);
|
|
assert.equal(res.status, 403, `expected 403, got ${res.status}`);
|
|
});
|
|
|
|
// ── 3. Returns expected shape ─────────────────────────────────────────────
|
|
it('returns correct shape with all required fields', async () => {
|
|
const res = await inject('GET', '/api/admin/overview', null, adminToken);
|
|
assert.equal(res.status, 200, `expected 200, got ${res.status}: ${JSON.stringify(res.body)}`);
|
|
|
|
const body = res.body;
|
|
// Numeric fields
|
|
const numericFields = [
|
|
'newUsers24h', 'newSessions24h', 'activeUsers24h',
|
|
'classesTotal', 'abandonedSessions24h',
|
|
];
|
|
for (const field of numericFields) {
|
|
assert.ok(field in body, `missing field: ${field}`);
|
|
assert.equal(typeof body[field], 'number', `${field} should be a number, got ${typeof body[field]}`);
|
|
}
|
|
|
|
// Array fields
|
|
const arrayFields = [
|
|
'stuckSessions', 'bannedThisWeek', 'topSessions24h',
|
|
'worstSessions24h', 'sessionsBySubject24h',
|
|
];
|
|
for (const field of arrayFields) {
|
|
assert.ok(field in body, `missing array field: ${field}`);
|
|
assert.ok(Array.isArray(body[field]), `${field} should be an array`);
|
|
}
|
|
|
|
// Sparks object
|
|
assert.ok('sparks' in body, 'missing sparks field');
|
|
assert.ok(Array.isArray(body.sparks.users), 'sparks.users should be an array');
|
|
assert.ok(Array.isArray(body.sparks.sessions), 'sparks.sessions should be an array');
|
|
assert.ok(Array.isArray(body.sparks.active), 'sparks.active should be an array');
|
|
|
|
// Inventory object
|
|
assert.ok('inventory' in body, 'missing inventory field');
|
|
assert.ok(typeof body.inventory === 'object', 'inventory should be an object');
|
|
});
|
|
|
|
// ── 4. Empty DB (no sessions) returns zeros, not crashes ─────────────────
|
|
it('does not crash on empty DB — returns zero counts', async () => {
|
|
const res = await inject('GET', '/api/admin/overview', null, adminToken);
|
|
assert.equal(res.status, 200, `expected 200, got ${res.status}`);
|
|
// counts should be >= 0 (not negative, not NaN, not null)
|
|
assert.ok(res.body.newSessions24h >= 0, 'newSessions24h should be >= 0');
|
|
assert.ok(res.body.abandonedSessions24h >= 0, 'abandonedSessions24h should be >= 0');
|
|
});
|
|
});
|