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:
@@ -1,38 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
// Phase 2B classroom workflow — the actual acceptance test from
|
||||
// docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md: instructor creates a cohort, adds three students,
|
||||
// assigns curriculum, prepares Labs for the whole cohort, two students complete different
|
||||
// scenarios, instructor resets one student's Lab, instructor destroys all cohort Labs after
|
||||
// class — progress survives every step.
|
||||
// Phase 2B classroom workflow — the acceptance test from
|
||||
// docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md, updated for Phase 3A's per-instructor accounts:
|
||||
// instructor creates a cohort, adds three students, assigns curriculum, prepares Labs for the
|
||||
// whole cohort, two students complete different scenarios, instructor resets one student's
|
||||
// Lab, instructor destroys all cohort Labs after class — progress survives every step.
|
||||
//
|
||||
// Runs against a live platform/docker-compose.dev.yml stack. Slow (three real k3s boots,
|
||||
// concurrently). No framework, matching project convention.
|
||||
//
|
||||
// Usage: cd platform && docker compose -f docker-compose.dev.yml up -d
|
||||
// ADMIN_PASSWORD=<value from platform/.env> node test/e2e-phase2b.js
|
||||
// (cd api && PLATFORM_DB_URL=... node bootstrap-owner.js OWNER_EMAIL "Owner" OWNER_PASSWORD)
|
||||
// OWNER_EMAIL=... OWNER_PASSWORD=... node test/e2e-phase2b.js
|
||||
|
||||
const assert = require('assert');
|
||||
const { makeSession } = require('./session-helper');
|
||||
|
||||
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
|
||||
if (!ADMIN_PASSWORD) { console.error('Set ADMIN_PASSWORD (see platform/.env)'); process.exit(1); }
|
||||
const COHORT_ID = `e2e-cohort-${Date.now()}`;
|
||||
|
||||
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 };
|
||||
},
|
||||
};
|
||||
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);
|
||||
}
|
||||
const COHORT_ID = `e2e-cohort-${Date.now()}`;
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
async function test(name, fn) {
|
||||
@@ -41,15 +31,15 @@ async function test(name, fn) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Phase 2B classroom workflow acceptance test — slow, real k3s boots\n');
|
||||
console.log('Phase 2B classroom workflow acceptance test (Phase 3A auth) — slow, real k3s boots\n');
|
||||
|
||||
const admin = makeSession();
|
||||
const other = makeSession();
|
||||
const admin = makeSession(GW);
|
||||
const other = makeSession(GW);
|
||||
|
||||
await test('admin auth: wrong password rejected, correct password issues a session', async () => {
|
||||
const bad = await admin.request('POST', '/api/admin/login', { password: 'definitely-wrong' });
|
||||
await test('instructor auth: wrong password rejected, correct password issues a session', async () => {
|
||||
const bad = await admin.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: 'definitely-wrong' });
|
||||
assert.equal(bad.status, 401);
|
||||
const good = await admin.request('POST', '/api/admin/login', { password: ADMIN_PASSWORD });
|
||||
const good = await admin.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD });
|
||||
assert.equal(good.status, 200);
|
||||
});
|
||||
|
||||
@@ -89,8 +79,8 @@ async function main() {
|
||||
});
|
||||
|
||||
const [aliceId, bobId] = students;
|
||||
const alice = makeSession();
|
||||
const bob = makeSession();
|
||||
const alice = makeSession(GW);
|
||||
const bob = makeSession(GW);
|
||||
|
||||
await test('students: Alice and Bob log in and launch their ALREADY-PREPARED labs instantly', async () => {
|
||||
await alice.request('POST', '/api/login', { student_id: aliceId });
|
||||
@@ -140,9 +130,6 @@ async function main() {
|
||||
});
|
||||
|
||||
await test('regression: Phase 1 boundary still holds — no Lab container publishes a host port', async () => {
|
||||
// Sanity check via the admin environment list — this doesn't re-run the Provisioner's own
|
||||
// iptables tests (see platform/provisioner/test/run.js for those), just confirms nothing
|
||||
// in the Phase 2B layer introduced a published port.
|
||||
const { body: envs } = await admin.request('GET', `/api/admin/students/${aliceId}/environment`);
|
||||
assert.ok(envs.every((e) => !e.internal_port || e.internal_port === 80));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user