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>
115 lines
5.2 KiB
JavaScript
115 lines
5.2 KiB
JavaScript
'use strict';
|
|
/**
|
|
* Integration tests: DELETE /api/admin/sessions/:id
|
|
* Covers: session removal + cascade, audit entry, 404 for missing, 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('Admin Session Delete', () => {
|
|
let adminToken, studentUser, subjectId, sessionId;
|
|
|
|
before(async () => {
|
|
const a = await getToken('admin');
|
|
adminToken = a.token;
|
|
|
|
const s = await getToken('student');
|
|
studentUser = s;
|
|
|
|
// Ensure subject exists
|
|
let subj = db.prepare("SELECT id FROM subjects WHERE slug = 'admin-sess-test'").get();
|
|
if (!subj) {
|
|
db.prepare("INSERT INTO subjects (slug, name, icon) VALUES ('admin-sess-test', 'Admin Sess Test', 'test')").run();
|
|
subj = db.prepare("SELECT id FROM subjects WHERE slug = 'admin-sess-test'").get();
|
|
}
|
|
subjectId = subj.id;
|
|
|
|
// Create a question with options
|
|
const qId = db.prepare(
|
|
'INSERT INTO questions (subject_id, text, difficulty) VALUES (?, ?, 1)'
|
|
).run(subjectId, 'Admin session delete test question').lastInsertRowid;
|
|
db.prepare('INSERT INTO options (question_id, text, is_correct) VALUES (?, ?, 0)').run(qId, 'Wrong');
|
|
db.prepare('INSERT INTO options (question_id, text, is_correct) VALUES (?, ?, 1)').run(qId, 'Correct');
|
|
|
|
// Start a session via API
|
|
const sessRes = await inject('POST', '/api/sessions', {
|
|
subject_slug: 'admin-sess-test', count: 1, mode: 'exam',
|
|
}, studentUser.token);
|
|
assert.equal(sessRes.status, 201, `session create failed: ${JSON.stringify(sessRes.body)}`);
|
|
sessionId = sessRes.body.session_id;
|
|
|
|
// Answer to create user_answers row
|
|
const q = sessRes.body.questions[0];
|
|
await inject('POST', `/api/sessions/${sessionId}/answer`, {
|
|
question_id: q.id, option_id: q.options[0].id,
|
|
}, studentUser.token);
|
|
});
|
|
|
|
// ── 1. Non-admin gets 403 ────────────────────────────────────────────────
|
|
it('student gets 403 when trying to delete a session', async () => {
|
|
const res = await inject('DELETE', `/api/admin/sessions/${sessionId}`, null, studentUser.token);
|
|
assert.equal(res.status, 403, `expected 403, got ${res.status}`);
|
|
});
|
|
|
|
// ── 2. 404 for non-existent session ─────────────────────────────────────
|
|
it('returns 404 for non-existent session id', async () => {
|
|
const res = await inject('DELETE', '/api/admin/sessions/999999', null, adminToken);
|
|
assert.equal(res.status, 404, `expected 404, got ${res.status}: ${JSON.stringify(res.body)}`);
|
|
});
|
|
|
|
// ── 3. Session delete cascades user_answers and session_questions ─────────
|
|
it('deletes session and cascades related rows', async () => {
|
|
// Verify rows exist before delete
|
|
const answersBefore = db.prepare('SELECT COUNT(*) AS n FROM user_answers WHERE session_id = ?')
|
|
.get(sessionId).n;
|
|
const sqBefore = db.prepare('SELECT COUNT(*) AS n FROM session_questions WHERE session_id = ?')
|
|
.get(sessionId).n;
|
|
|
|
// The session must still exist
|
|
const sessBefore = db.prepare('SELECT id FROM test_sessions WHERE id = ?').get(sessionId);
|
|
assert.ok(sessBefore, 'session should exist before delete');
|
|
|
|
const res = await inject('DELETE', `/api/admin/sessions/${sessionId}`, null, adminToken);
|
|
assert.equal(res.status, 200, `expected 200, got ${res.status}: ${JSON.stringify(res.body)}`);
|
|
assert.equal(res.body.ok, true);
|
|
|
|
// Session gone
|
|
const sessAfter = db.prepare('SELECT id FROM test_sessions WHERE id = ?').get(sessionId);
|
|
assert.equal(sessAfter, undefined, 'session should be deleted');
|
|
|
|
// user_answers gone
|
|
const answersAfter = db.prepare('SELECT COUNT(*) AS n FROM user_answers WHERE session_id = ?')
|
|
.get(sessionId).n;
|
|
assert.equal(answersAfter, 0, 'user_answers should be cleared after session delete');
|
|
|
|
// session_questions gone
|
|
const sqAfter = db.prepare('SELECT COUNT(*) AS n FROM session_questions WHERE session_id = ?')
|
|
.get(sessionId).n;
|
|
assert.equal(sqAfter, 0, 'session_questions should be cleared after session delete');
|
|
});
|
|
|
|
// ── 4. Audit entry created on delete ─────────────────────────────────────
|
|
it('creates audit log entry on session delete', async () => {
|
|
// Create and immediately delete another session
|
|
const sessRes = await inject('POST', '/api/sessions', {
|
|
subject_slug: 'admin-sess-test', count: 1, mode: 'practice',
|
|
}, studentUser.token);
|
|
assert.equal(sessRes.status, 201);
|
|
const newSessId = sessRes.body.session_id;
|
|
|
|
const countBefore = db.prepare(
|
|
"SELECT COUNT(*) AS n FROM admin_audit_log WHERE action = 'session.delete'"
|
|
).get().n;
|
|
|
|
await inject('DELETE', `/api/admin/sessions/${newSessId}`, null, adminToken);
|
|
|
|
const countAfter = db.prepare(
|
|
"SELECT COUNT(*) AS n FROM admin_audit_log WHERE action = 'session.delete'"
|
|
).get().n;
|
|
assert.ok(countAfter > countBefore, 'Expected session.delete audit entry to be created');
|
|
});
|
|
});
|