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:
@@ -0,0 +1,53 @@
|
||||
// Shared cookie-jar test helper for the e2e suites (Phase 2A/2B/3A). Node's fetch doesn't
|
||||
// persist cookies across calls the way a browser does, and Phase 3A's CSRF double-submit
|
||||
// pattern means every state-changing request needs an X-CSRF-Token header echoing whichever
|
||||
// rn_csrf_* cookie the jar is currently holding — this centralizes both instead of repeating
|
||||
// slightly-different versions in every test file.
|
||||
|
||||
function makeSession(baseUrl) {
|
||||
const jar = new Map(); // cookie name -> value
|
||||
|
||||
function cookieHeader() {
|
||||
return [...jar].map(([k, v]) => `${k}=${v}`).join('; ');
|
||||
}
|
||||
function csrfToken() {
|
||||
return jar.get('rn_csrf_s') || jar.get('rn_csrf_i') || '';
|
||||
}
|
||||
function absorbSetCookie(res) {
|
||||
const cookies = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : [];
|
||||
for (const c of cookies) {
|
||||
const [pair] = c.split(';');
|
||||
const eq = pair.indexOf('=');
|
||||
if (eq === -1) continue;
|
||||
const name = pair.slice(0, eq).trim();
|
||||
const value = pair.slice(eq + 1).trim();
|
||||
if (value === '') jar.delete(name); else jar.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
async request(method, path, body) {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
const cookies = cookieHeader();
|
||||
if (cookies) headers.Cookie = cookies;
|
||||
if (!['GET', 'HEAD'].includes(method)) headers['X-CSRF-Token'] = csrfToken();
|
||||
const res = await fetch(`${baseUrl}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
|
||||
absorbSetCookie(res);
|
||||
const text = await res.text();
|
||||
let json; try { json = JSON.parse(text); } catch { json = text; }
|
||||
return { status: res.status, body: json };
|
||||
},
|
||||
rawFetch(path) {
|
||||
const cookies = cookieHeader();
|
||||
return fetch(`${baseUrl}${path}`, { headers: cookies ? { Cookie: cookies } : {} });
|
||||
},
|
||||
hasCookie(name) { return jar.has(name); },
|
||||
getCookie(name) { return jar.get(name); },
|
||||
// Lets a test simulate "this browser was never issued a CSRF cookie" or "attach the wrong
|
||||
// one" for a negative CSRF test, without hand-rolling a second fetch call.
|
||||
setCookie(name, value) { jar.set(name, value); },
|
||||
clearCookie(name) { jar.delete(name); },
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { makeSession };
|
||||
Reference in New Issue
Block a user