test(phase3a): add identity and RBAC end-to-end coverage

New e2e-phase3a.js (21 cases): no account enumeration, CSRF positive/negative, login rate
limiting, RBAC across all three roles, cohort-scoped instructor access (visibility + route
access), last-owner protection, live role/status re-evaluation (no separate revoke needed),
password change (old password rejected after, other sessions revoked), audit log content
(including a no-secrets-in-metadata assertion), student session RBAC/logout, and session
persistence across a REAL docker compose restart api mid-test.

New session-helper.js — a shared cookie-jar test helper (Node's fetch doesn't persist
cookies across calls) that also auto-attaches X-CSRF-Token from whichever rn_csrf_*
cookie it's holding, replacing the single-cookie-string trackers e2e-phase2a.js and
e2e-phase2b.js each had their own copy of.

e2e-phase2a.js and e2e-phase2b.js updated for the new auth model: admin login is now a real
bootstrapped OWNER_EMAIL/OWNER_PASSWORD account instead of the old shared ADMIN_PASSWORD.
All cases still passing: 10/10 and 12/12 respectively.
This commit is contained in:
2026-08-18 13:36:45 -05:00
parent 1ca5dd3995
commit face0ae132
4 changed files with 315 additions and 66 deletions
+20 -31
View File
@@ -1,38 +1,26 @@
#!/usr/bin/env node
// Phase 2A student-flow regression, updated for Phase 2B's session-cookie architecture.
// Originally used X-Student-Id headers and a ?sid= query param — both mechanisms are gone as
// of Phase 2B (docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md §3); this version exercises the exact
// same flow through the real rn_session cookie instead, since that's how it actually works now.
// Phase 2A student-flow regression, updated for Phase 3A's persistent session + per-instructor
// auth model. Originally used X-Student-Id headers and a ?sid= query param — both mechanisms
// are long gone; this version exercises the same flow through the real rn_session cookie, with
// the admin roster-add step now going through a real instructor account instead of a shared
// ADMIN_PASSWORD.
//
// Runs against a live platform/docker-compose.dev.yml stack. Slow (real k3s boots). No
// framework, matching project convention.
//
// Usage: cd platform && docker compose -f docker-compose.dev.yml up -d && node test/e2e-phase2a.js
// Usage: cd platform && docker compose -f docker-compose.dev.yml up -d
// (cd api && PLATFORM_DB_URL=... node bootstrap-owner.js OWNER_EMAIL "Owner" OWNER_PASSWORD)
// OWNER_EMAIL=... OWNER_PASSWORD=... node test/e2e-phase2a.js
const assert = require('assert');
const { makeSession } = require('./session-helper');
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
const COHORT = `e2e-test-${Date.now()}`;
// Minimal manual cookie jar — Node's fetch doesn't persist cookies across calls the way a
// browser does, so each "session" here explicitly carries its own Set-Cookie forward.
function makeSession() {
let cookie = '';
return {
async request(method, path, body) {
const headers = { 'Content-Type': 'application/json' };
if (cookie) headers.Cookie = cookie;
const res = await fetch(`${GW}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
const setCookie = res.headers.get('set-cookie');
if (setCookie) cookie = setCookie.split(';')[0];
const text = await res.text();
let json; try { json = JSON.parse(text); } catch { json = text; }
return { status: res.status, body: json };
},
rawFetch(path) {
return fetch(`${GW}${path}`, { headers: cookie ? { Cookie: cookie } : {} });
},
};
const OWNER_EMAIL = process.env.OWNER_EMAIL;
const OWNER_PASSWORD = process.env.OWNER_PASSWORD;
if (!OWNER_EMAIL || !OWNER_PASSWORD) {
console.error('Set OWNER_EMAIL and OWNER_PASSWORD (bootstrap one first with platform/api/bootstrap-owner.js)');
process.exit(1);
}
let passed = 0, failed = 0;
@@ -42,10 +30,11 @@ async function test(name, fn) {
}
async function main() {
console.log('Phase 2A student-flow regression (Phase 2B session cookies) — slow, real k3s boots\n');
console.log('Phase 2A student-flow regression (Phase 3A sessions/auth) — slow, real k3s boots\n');
const admin = makeSession();
await admin.request('POST', '/api/admin/login', { password: process.env.ADMIN_PASSWORD });
const admin = makeSession(GW);
const { status: loginStatus } = await admin.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD });
assert.equal(loginStatus, 200, 'owner login must succeed — did you bootstrap one?');
let alice, bob;
await test('roster: add two students via admin', async () => {
@@ -55,8 +44,8 @@ async function main() {
assert.notEqual(alice, bob);
});
const aliceSession = makeSession();
const bobSession = makeSession();
const aliceSession = makeSession(GW);
const bobSession = makeSession(GW);
await test('login: student session cookie is issued and required for curriculum', async () => {
const { status } = await aliceSession.request('POST', '/api/login', { student_id: alice });