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.
54 lines
2.2 KiB
JavaScript
54 lines
2.2 KiB
JavaScript
// 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 };
|