'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'); }); });