test(phase8): add enrollment operations coverage and Winter Break registration rehearsal

- e2e-phase8.js (16/16): offering CRUD/transitions/readiness, registration open/closed
  enforcement, capacity + concurrent final-seat race through the real registration
  endpoints, the public serializer's safety, website integration idempotency (sequential +
  concurrent), adult/minor paths, required-agreement conversion blocking with version
  snapshotting, offering->cohort membership, guardian recipient resolution, RBAC, and
  confirmation ROOT_ V1/self-paced/classroom flows are unaffected.
- test-phase8-winter-camp-registration.js (12/12): the full commercial-path story with no
  payment code — website submits a real minor registration (real guardian created/matched),
  guardian agreement acceptance, a simulated admin-side payment confirmation, conversion to
  real enrollment/cohort, real account activation, an independent adult path, two students
  genuinely competing for the final seat, and a REAL Lab task completed by the minor
  student with the instructor then seeing the real progress.
- e2e-phase7.js: fixed two now-stale test assumptions the new server-authoritative
  registration-window enforcement broke (an offering left in draft that Phase 7's own test
  never transitioned before registering against it) and widened a file-allowlist check for
  the new, legitimate public-offerings-routes.js.

Regression: e2e-phase6f.js 17/17, e2e-phase5b.js 32/32, e2e-phase5c.js 29/29, all fresh-stack.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-26 23:59:32 -05:00
co-authored by Claude Sonnet 5
parent 40362ec0c9
commit aa63d09fb8
4 changed files with 502 additions and 3 deletions
+12 -3
View File
@@ -45,10 +45,12 @@ async function main() {
const row = psql(`SELECT family FROM programs WHERE program_id = 'p7-bootcamp-check-${RUN_ID}'`);
assert.equal(row, 'bootcamp');
const src = execSync('grep -rln "recommended_min_age\\|recommended_max_age" src/*.js', { cwd: `${__dirname}/../api`, stdio: 'pipe' }).toString();
// Only offerings.js/admin-offerings-routes.js should ever mention these fields, and neither
// is an authorization/RBAC file — confirmed by name, not content-sniffing.
// Only offerings.js/admin-offerings-routes.js (Phase 7) and public-offerings-routes.js
// (Phase 8's public read-only serializer — displays the fields, never authorizes on them)
// should ever mention these fields, and none of the three is an authorization/RBAC file —
// confirmed by name, not content-sniffing.
for (const file of src.trim().split('\n').filter(Boolean)) {
assert.ok(/offerings\.js|admin-offerings-routes\.js/.test(file), `unexpected file referencing age-guidance fields: ${file}`);
assert.ok(/offerings\.js|admin-offerings-routes\.js|public-offerings-routes\.js/.test(file), `unexpected file referencing age-guidance fields: ${file}`);
}
});
@@ -95,6 +97,11 @@ async function main() {
const b = await owner.request('POST', '/api/admin/offerings', { offeringId: offeringB, slug: `p7-camp-b-${RUN_ID}`, name: 'Camp B (self-paced)', programId: PROGRAM, deliveryMode: 'self_paced' });
assert.equal(b.status, 201);
assert.equal(b.body.capacity, null, 'a self-paced offering should be unlimited by default');
// Phase 8 made registration-window enforcement real (server-side, not just client hiding a
// button) — a still-draft offering now correctly rejects registration attempts. offeringB
// needs to be open before anything in this file registers against it.
await owner.request('POST', `/api/admin/offerings/${offeringB}/transition`, { status: 'published' });
await owner.request('POST', `/api/admin/offerings/${offeringB}/transition`, { status: 'enrollment_open' });
});
// ── 5: capacity ───────────────────────────────────────────────────────────────────────────
@@ -208,6 +215,8 @@ async function main() {
const agedOffering = `off-aged-${RUN_ID}`;
const create = await owner.request('POST', '/api/admin/offerings', { offeringId: agedOffering, slug: `p7-aged-${RUN_ID}`, name: 'Aged Offering', programId: PROGRAM, deliveryMode: 'workshop', recommendedMinAge: 15, recommendedMaxAge: 18 });
assert.equal(create.status, 201);
await owner.request('POST', `/api/admin/offerings/${agedOffering}/transition`, { status: 'published' });
await owner.request('POST', `/api/admin/offerings/${agedOffering}/transition`, { status: 'enrollment_open' });
const reg = await owner.request('POST', `/api/admin/offerings/${agedOffering}/registrations`, { registrantDisplayName: 'Any Age Registrant', registrantEmail: `anyage-${RUN_ID}@example.com` });
assert.equal(reg.status, 201, 'no age field on the registration itself, and none is checked');
});