From e762bc2930092089ab6e848ab469651ccba0b40b Mon Sep 17 00:00:00 2001 From: Gree44 Date: Sat, 25 Jul 2026 16:24:32 +0200 Subject: [PATCH 01/22] test(integration): update auth-admin-bypasses-setup-gate for admin route split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Abgedeckte Funktion: - App.tsx Admin-Short-Circuit + neue admin-Route-Struktur mit ProtectedRoute Getesteter Nutzerfluss: - Admin ohne OpenStack-Projekt besucht /dashboard → Dashboard rendert - Admin besucht /admin/projects → AdminProjectOverview rendert (ProtectedRoute requireAdmin lässt durch) Getestete Fachlogik: - Non-Lecturer-Branch überspringt listOpenstackProjects() (0 Calls) - needsSetup bleibt false → kein /setup-Redirect Geprüfte Fehlerfälle: - (n/a) Geprüfte Edge Cases: - Auch bei explizit leerer Projektliste kein Setup-Zwang Betroffene Komponenten: - src/App.tsx, src/components/ProtectedRoute.tsx, src/pages/AdminProjectOverview.tsx Warum dieser Test wichtig ist: - Nach dem Admin-Rework (Split von /admin in /admin/projects|templates|lecturers) sichert der Test, dass Admins weiterhin ohne Projekt in den Adminbereich kommen. Aktualisierung: alte /admin-Route + AdminMonitoring-Heading existieren nicht mehr; Test zielt jetzt auf /admin/projects + Projektübersicht. --- .../auth/auth-admin-bypasses-setup-gate.cy.ts | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/cypress/e2e/auth/auth-admin-bypasses-setup-gate.cy.ts b/cypress/e2e/auth/auth-admin-bypasses-setup-gate.cy.ts index 889515c..66bdd01 100644 --- a/cypress/e2e/auth/auth-admin-bypasses-setup-gate.cy.ts +++ b/cypress/e2e/auth/auth-admin-bypasses-setup-gate.cy.ts @@ -26,7 +26,13 @@ // 1. /dashboard renders normally for the admin (URL + Dashboard heading). // 2. listOpenstackProjects() is NEVER called for the admin — proof that // the non-lecturer branch of the effect actually ran. -// 3. /admin is reachable and AdminMonitoring renders. +// 3. An admin route is reachable and its page renders. +// +// NOTE (staging update): the old single /admin route was split into +// /admin/projects, /admin/templates and /admin/lecturers (each wrapped in +// ). Plain /admin no longer resolves to a page, +// so we now assert against /admin/projects (AdminProjectOverview, the admin +// landing page,

Projektübersicht

). describe("Auth · admin bypasses the OpenStack setup gate", () => { beforeEach(() => { @@ -42,7 +48,7 @@ describe("Auth · admin bypasses the OpenStack setup gate", () => { }).as("getProjectsEmpty"); }); - it("renders /dashboard without calling listOpenstackProjects and lets /admin load", () => { + it("renders /dashboard without calling listOpenstackProjects and lets an admin route load", () => { cy.loginAs("admin", "/dashboard"); // Final URL must be /dashboard — no redirect to /setup. The admin is past @@ -65,18 +71,19 @@ describe("Auth · admin bypasses the OpenStack setup gate", () => { // length assertion would flip from 0 to 1. cy.get("@getProjectsEmpty.all").should("have.length", 0); - // Now verify /admin is reachable. We re-issue cy.loginAs so the Keycloak - // stub is re-installed via onBeforeLoad before the bundle re-evaluates on - // the fresh page load — a bare cy.visit would let the real keycloak-js - // module run and stall on its network init. - cy.loginAs("admin", "/admin"); - cy.url().should("include", "/admin"); - cy.location("pathname").should("eq", "/admin"); + // Now verify an admin route is reachable. We re-issue cy.loginAs so the + // Keycloak stub is re-installed via onBeforeLoad before the bundle + // re-evaluates on the fresh page load — a bare cy.visit would let the real + // keycloak-js module run and stall on its network init. + cy.loginAs("admin", "/admin/projects"); + cy.url().should("include", "/admin/projects"); + cy.location("pathname").should("eq", "/admin/projects"); - // AdminMonitoring.tsx renders

Administration

at the top of the - // page (line 395) — unconditional, present before any fetched data - // resolves. Good stable anchor. - cy.contains("h1", "Administration").should("be.visible"); + // AdminProjectOverview.tsx renders

Projektübersicht

at the top of + // the page — unconditional, present before any fetched data resolves. Good + // stable anchor. (Also proves ProtectedRoute requireAdmin lets the admin + // through rather than bouncing to /dashboard.) + cy.contains("h1", "Projektübersicht").should("be.visible"); // Still no project fetch — admin remains short-circuited across both // route visits. From a3d1705f47abcc27b6c0389559181e9880dcd58e Mon Sep 17 00:00:00 2001 From: Gree44 Date: Sat, 25 Jul 2026 16:25:36 +0200 Subject: [PATCH 02/22] test(integration): update admin-approve-template-version for admin route split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Abgedeckte Funktion: - AdminTemplateApprovals Approval-Queue (aus AdminMonitoring herausgelöst) Getesteter Nutzerfluss: - Admin → /admin/templates → Queue mit zwei pending Versions → Schnell genehmigen → Row verschwindet Getestete Fachlogik: - getTemplateVersionsQueue() lädt Queue - approveTemplateVersion() POSTet /template-versions//approve - Row-Removal via lokalem State (kein Refetch) Geprüfte Fehlerfälle: - (n/a — happy path) Geprüfte Edge Cases: - Korrekte Row wird approved, andere bleibt sichtbar Betroffene Komponenten: - src/pages/AdminTemplateApprovals.tsx, src/api/github.ts Warum dieser Test wichtig ist: - Approval ist der einzige Weg, public Templates in den AppStore-Katalog zu bringen. Aktualisierung: Route /admin → /admin/templates nach dem Admin-Rework; Selektoren/Endpunkte/Fixture unverändert gültig. --- .../admin-approve-template-version.cy.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/cypress/e2e/admin/admin-approve-template-version.cy.ts b/cypress/e2e/admin/admin-approve-template-version.cy.ts index 0c9c57d..b41b5e2 100644 --- a/cypress/e2e/admin/admin-approve-template-version.cy.ts +++ b/cypress/e2e/admin/admin-approve-template-version.cy.ts @@ -2,9 +2,9 @@ // admin-approve-template-version // ────────────────────────────── -// P0 success-path test: an admin on /admin sees the Template-Freigaben card -// populated with pending public template-versions (loaded via GET -// /api/v1/template-versions/queue). Clicking "Schnell genehmigen" on a row +// P0 success-path test: an admin on /admin/templates sees the Template- +// Freigaben card populated with pending public template-versions (loaded via +// GET /api/v1/template-versions/queue). Clicking "Schnell genehmigen" on a row // must fire POST /api/v1/template-versions//approve and the row must // disappear from the queue UI on success. // @@ -15,11 +15,13 @@ // versions pile up indefinitely and the AppStore stops growing — both real // user-visible regressions. // -// AdminMonitoring.tsx renders one card per pending TemplateVersionQueueItem -// (template name + version pill + "Schnell genehmigen" / "Details prüfen" -// buttons). handleApprove() calls approveTemplateVersion() from -// src/api/github.ts and then removes the version from local state. The -// per-row removal is the user-visible signal that the action succeeded. +// AdminTemplateApprovals.tsx (route /admin/templates, split out of the old +// AdminMonitoring.tsx during the admin rework) renders one card per pending +// TemplateVersionQueueItem (template name + version pill + "Schnell +// genehmigen" / "Ablehnen" buttons). handleApprove() calls +// approveTemplateVersion() from src/api/github.ts and then removes the +// version from local state. The per-row removal is the user-visible signal +// that the action succeeded. // // Fixture // queue-pending.json mirrors the QueueResponse shape from @@ -53,7 +55,7 @@ describe("Admin · approves a pending template-version from the queue", () => { }); it("clicks 'Schnell genehmigen' on the first pending version and the row disappears", () => { - cy.loginAs("admin", "/admin"); + cy.loginAs("admin", "/admin/templates"); // The queue load is the load-bearing fetch for the Template-Freigaben // card. Waiting on it proves the GET fired AND that the response landed @@ -77,7 +79,7 @@ describe("Admin · approves a pending template-version from the queue", () => { }).as("getQueueEmpty"); // Click the "Schnell genehmigen" button scoped to the WordPress row. - // The button text is exact (AdminMonitoring.tsx line ~900). We scope to + // The button text is exact (AdminTemplateApprovals.tsx:350). We scope to // the row by locating the template-name

and walking up to the // surrounding Card (`data-slot="card"`) — both pending rows render the // same button text, so unscoped cy.contains would race between them. From a14815f7d79a687317e03ec087e8d3f2e95519d6 Mon Sep 17 00:00:00 2001 From: Gree44 Date: Sat, 25 Jul 2026 16:32:28 +0200 Subject: [PATCH 03/22] test(integration): fix detail-delete-confirmation-flow for owner-only gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Abgedeckte Funktion: - DeploymentDetails Delete-Flow unter neuem Owner-Only-Gate (fix/126) Getesteter Nutzerfluss: - Owner-Lecturer öffnet /deployment/ → Löschen → Bestätigen → DELETE → /dashboard Getestete Fachlogik: - canManageDeployment = deployment.ownerId === currentUser.userId - Delete-Button ist nur für den Owner (oder Admin) aktiv Geprüfte Fehlerfälle: - (n/a — happy path) Geprüfte Edge Cases: - owner_id der Fixture == sub der lecturer-Fixture (user-lec-1) aktiviert die Aktion Betroffene Komponenten: - src/pages/DeploymentDetails.tsx, src/pages/DeploymentDetailsPage.tsx, cypress/fixtures/deployments/detail-running.json Warum dieser Test wichtig ist: - Nach Einführung des Owner-Gates würde der Delete-Button ohne passendes owner_id in der Fixture deaktiviert bleiben; der Test würde falsch-negativ fehlschlagen. Fixture spiegelt jetzt einen Owner-Zugriff wider. Hinweis: detail-running.json wird von mehreren Detail-Specs geteilt; owner_id aktiviert nur owner-gebundene Aktionen und lässt Rendering-Assertions unberührt (mit dashboard-row-click, detail-renders-status-and-phases gegengeprüft). --- cypress/fixtures/deployments/detail-running.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/fixtures/deployments/detail-running.json b/cypress/fixtures/deployments/detail-running.json index 23e044e..2dc5033 100644 --- a/cypress/fixtures/deployments/detail-running.json +++ b/cypress/fixtures/deployments/detail-running.json @@ -3,6 +3,7 @@ "message": "ok", "data": { "id": "dep-alpha-0001", + "owner_id": "user-lec-1", "name": "test-deploy-alpha", "template_version_id": "tv-alpha-1", "course_id": "course-alpha", From 265a90df5532274074b39ccb336c94090c10ff7f Mon Sep 17 00:00:00 2001 From: Gree44 Date: Sat, 25 Jul 2026 16:33:59 +0200 Subject: [PATCH 04/22] test(integration): reactivate auth-non-admin-cannot-access-admin-route (gate fixed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Abgedeckte Funktion: - ProtectedRoute requireAdmin-Guard auf admin-Routen Getesteter Nutzerfluss: - Lecturer ruft /admin/templates direkt auf → Redirect zu /dashboard, kein Admin-Content Getestete Fachlogik: - ProtectedRoute: requireAdmin && !isAdmin → Navigate /dashboard replace - AdminTemplateApprovals mountet nicht → loadQueue()-Effect feuert nicht - Template-Freigaben-Heading nicht im DOM Geprüfte Fehlerfälle: - (n/a — permission gate) Geprüfte Edge Cases: - Defense-in-Depth: echter Redirect statt CSS-Hide; queue-Fetch = 0 Calls Betroffene Komponenten: - src/components/ProtectedRoute.tsx, src/App.tsx, src/pages/AdminTemplateApprovals.tsx Warum dieser Test wichtig ist: - Der zuvor dokumentierte Privilege-Escalation-Befund (SECURITY-FINDINGS.md #1) ist durch den Admin-Rework behoben. Der zuvor geskippte Test ist jetzt aktiv und sichert den Guard gegen Regression ab. SECURITY-FINDINGS.md #1 auf RESOLVED aktualisiert. --- cypress/SECURITY-FINDINGS.md | 12 +- ...-non-admin-cannot-access-admin-route.cy.ts | 126 +++++++----------- 2 files changed, 56 insertions(+), 82 deletions(-) diff --git a/cypress/SECURITY-FINDINGS.md b/cypress/SECURITY-FINDINGS.md index ed06387..76a4703 100644 --- a/cypress/SECURITY-FINDINGS.md +++ b/cypress/SECURITY-FINDINGS.md @@ -8,13 +8,17 @@ **Severity**: High (privilege escalation, defence-in-depth) **Discovered while writing**: `cypress/e2e/auth/auth-non-admin-cannot-access-admin-route.cy.ts` -**State**: Test is `describe.skip(...)` until the fix lands. +**State**: ✅ **RESOLVED on `staging`** (admin-area rework). The test is now un-skipped and active — it passes against the fixed code and guards against regression. -### Finding +### Resolution -`AdminMonitoring.tsx` has no role gate. `App.tsx` line 159 wires `` at `/admin` for any authenticated user. The Sidebar nav link IS gated (`Sidebar.tsx` line 121 checks `isAdmin`), but that is only a UI affordance, not a security boundary. +The admin-area rework replaced the single ungated `/admin` route with three routes — `/admin/projects`, `/admin/templates`, `/admin/lecturers` — each wrapped in `` (`src/components/ProtectedRoute.tsx`). For a non-admin, `requireAdmin && !isAdmin` returns ``, so `AdminTemplateApprovals` / `AdminProjectOverview` never mount and their data effects never run. `AdminMonitoring.tsx` (the old ungated page) is now dead code. The regression test asserts: lecturer → `/admin/templates` redirects to `/dashboard`, the "Template-Freigaben" H1 is absent, and the approval-queue fetch never fires. -A user authenticated with `roles=["lecturer"]` (no admin role) who navigates directly to `/admin` sees: +### Original finding (for the record) + +`AdminMonitoring.tsx` had no role gate. `App.tsx` wired `` at `/admin` for any authenticated user. The Sidebar nav link IS gated (`Sidebar.tsx` checks `isAdmin`), but that is only a UI affordance, not a security boundary. + +A user authenticated with `roles=["lecturer"]` (no admin role) who navigated directly to `/admin` saw: - the full `Administration` heading and tabbed UI; - `getAllDeployments(null)` is fired — returns the **global** deployment list (every lecturer's work), not just their own; diff --git a/cypress/e2e/auth/auth-non-admin-cannot-access-admin-route.cy.ts b/cypress/e2e/auth/auth-non-admin-cannot-access-admin-route.cy.ts index 5ee89b5..d393e7e 100644 --- a/cypress/e2e/auth/auth-non-admin-cannot-access-admin-route.cy.ts +++ b/cypress/e2e/auth/auth-non-admin-cannot-access-admin-route.cy.ts @@ -3,99 +3,69 @@ // auth-non-admin-cannot-access-admin-route // ──────────────────────────────────────── // P0 permission test: a lecturer (roles=["lecturer"], NOT "admin") navigating -// directly to /admin must NOT see the AdminMonitoring page contents. The -// "Administration" heading, the deployments/approval-queue UI, and any admin- -// only data-fetches must all be absent for the non-admin user. +// directly to an admin route (/admin/templates) must be redirected away and +// must NOT see any admin page contents or trigger admin-only data fetches. // -// ⚠️ CURRENTLY SKIPPED — see cypress/SECURITY-FINDINGS.md +// History +// When this test was first written the app had NO route-level role gate: +// AdminMonitoring was wired at /admin for any authenticated user, so a +// lecturer who knew the URL got the full admin UI. That was recorded as a +// privilege-escalation finding in cypress/SECURITY-FINDINGS.md and the test +// was left as describe.skip. // -// Discovered while authoring this test: AdminMonitoring.tsx has no role -// gate, and App.tsx exposes /admin to any authenticated user. A lecturer -// who knows the URL gets the full admin UI (global deployments, template -// approval queue, approve/reject buttons). The Sidebar gate is the only -// defense and it's UI-only, not a security boundary. +// The staging rework FIXED it: admin pages now live at /admin/projects, +// /admin/templates and /admin/lecturers, each wrapped in +// (src/components/ProtectedRoute.tsx). For a +// non-admin, requireAdmin && !isAdmin → . +// The component never mounts, so its data effects never run. This test is +// now un-skipped and pins that guard. // -// The test below is the regression test you want once the gate is added -// to AdminMonitoring (a one-liner using useCurrentUser().isAdmin). It is -// left in the repo as `describe.skip` so: -// 1. The expected behaviour is documented in executable form. -// 2. A future contributor can drop the .skip the moment the prod fix -// lands and the assertions will pin the gate in place. +// We pin three invariants for a lecturer hitting /admin/templates: +// 1. The browser is redirected to /dashboard (ProtectedRoute guard fired). +// 2. The AdminTemplateApprovals "Template-Freigaben" H1 is NOT in the DOM. +// 3. The admin-only approval-queue fetch (GET /template-versions/queue) +// NEVER fires — proof the page short-circuited before its effects ran. // -// See cypress/SECURITY-FINDINGS.md for the full write-up. -// -// Why this test exists -// App.tsx (line 159) wires at /admin for ANY authenticated -// user — there is no route-level role check. The Sidebar.tsx admin link IS -// gated (only rendered when realm_access.roles contains "admin"), but a -// lecturer who knows the URL can still hit /admin directly. -// -// The defense-in-depth invariant is therefore that AdminMonitoring itself -// must short-circuit for non-admins (via useCurrentUser().isAdmin) BEFORE it -// mounts the data effects that load the template-versions approval queue, -// the global deployments list, courses, keycloak groups, and flavors. -// -// We pin three invariants: -// 1. The admin "Administration" H1 is NOT in the DOM for a lecturer. -// 2. The Sidebar admin nav link () is NOT rendered for a -// lecturer — the Sidebar's own gate stays honest. -// 3. The admin-only data fetch for the template-versions approval queue -// (`getTemplateVersions` alias) NEVER fires — proof that the page -// short-circuited before its useEffects ran. -// -// If this test fails on assertion #1 or #3, AdminMonitoring is missing its -// role gate and lecturers can read (and act on) every deployment and -// template-approval in the system. That is a privilege-escalation finding -// that must NOT be silently weakened by relaxing the assertion. +// If any of these regress, the requireAdmin guard has been removed or +// weakened and lecturers regain access to the admin surface — a +// privilege-escalation regression. -describe.skip("Auth · non-admin lecturer cannot access /admin (pending gate fix)", () => { +describe("Auth · non-admin lecturer cannot access an admin route", () => { beforeEach(() => { // Defaults satisfy App.tsx's bootstrap: lecturer has a project (single.json), - // so `needsSetup === false` and the router actually mounts /admin instead - // of redirecting to /setup. Every other alias (getTemplateVersions, - // getDeployments, getCourses, …) gets its standard fixture so any rogue - // fetch is captured for the length assertions below. + // so needsSetup === false and the router evaluates the real route tree + // (including the ProtectedRoute guards) instead of the /setup gate. cy.mockApi(); + + // Dedicated alias for the admin-only approval-queue endpoint. If the guard + // regresses and AdminTemplateApprovals mounts, its loadQueue() effect would + // hit this — and the length-0 assertion below would flip. + cy.intercept("GET", /\/api\/v1\/template-versions\/queue(\?.*)?$/, { + fixture: "template-versions/queue-pending.json", + }).as("getQueuePending"); }); - it("does not render AdminMonitoring or its sidebar link for a lecturer", () => { - cy.loginAs("lecturer", "/admin"); + it("redirects a lecturer off /admin/templates to /dashboard without mounting the admin page", () => { + cy.loginAs("lecturer", "/admin/templates"); - // Wait for the bootstrap project-check to resolve. App.tsx fires - // listOpenstackProjects() for lecturers; once it returns single.json the - // needsSetup gate evaluates to false and the /admin route can mount (or - // — once the role gate is in place — short-circuit). Waiting here means - // any subsequent length-0 assertion is meaningful: the page has had its - // chance to mount and decide. + // Bootstrap project-check resolves first (lecturer path fetches projects). cy.wait("@getProjects"); - // URL is still /admin — App.tsx does not redirect lecturers off the - // route (the gate, when added, should be component-internal: render - // nothing / a denied state instead of navigating away). - cy.location("pathname").should("eq", "/admin"); + // Invariant #1 — ProtectedRoute requireAdmin bounces the lecturer to + // /dashboard. Cypress retries until the redirect settles, so no fixed wait. + cy.location("pathname").should("eq", "/dashboard"); - // Invariant #1 — the AdminMonitoring "Administration" H1 (line 395 of - // AdminMonitoring.tsx) MUST NOT exist in the DOM. If a future gate - // renders a "Zugriff verweigert" message in its place, that's fine — - // this assertion only cares that the admin H1 itself is gone. Using - // .should("not.exist") rather than .should("not.be.visible") because - // the gate is expected to return null/empty for non-admins, not hide - // the element with CSS. - cy.contains(/^Administration$/).should("not.exist"); + // The dashboard actually rendered (we landed somewhere real, not a blank). + cy.contains("h1", "Dashboard").should("be.visible"); - // Invariant #2 — the Sidebar admin link is only rendered when - // `realm_access.roles` includes "admin" (Sidebar.tsx line 121). For a - // lecturer the link must not be in the DOM at all. - // This is a stable selector — the icon-only link has no text but its - // href is fixed. - cy.get('a[href="/admin"]').should("not.exist"); + // Invariant #2 — the AdminTemplateApprovals "Template-Freigaben" H1 must + // never be in the DOM for a lecturer. .should("not.exist") because the + // guard redirects rather than CSS-hiding. + cy.contains(/^Template-Freigaben$/).should("not.exist"); - // Invariant #3 — load-bearing: the admin-only template-versions - // approval-queue fetch MUST NOT fire. AdminMonitoring's loadQueue() - // runs in a useEffect at mount; if the component short-circuits - // before mounting (because of the role gate), the effect never runs - // and the intercept is never matched. This is what proves the gate - // is real rather than a CSS hide. - cy.get("@getTemplateVersions.all").should("have.length", 0); + // Invariant #3 — load-bearing: the admin-only approval-queue fetch MUST + // NOT fire. If the guard let the component mount, loadQueue() would hit + // @getQueuePending. Zero calls proves the redirect happened before mount. + cy.get("@getQueuePending.all").should("have.length", 0); }); }); From 9f681f573e1ca49f40353b024b768caa9379a466 Mon Sep 17 00:00:00 2001 From: Gree44 Date: Sat, 25 Jul 2026 16:37:05 +0200 Subject: [PATCH 05/22] docs(cypress): add delta test matrix for staging update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Erfasst die Reparaturen bestehender Tests nach dem großen staging-Update und die geplanten neuen Feature-Tests (Student-Self-Service, Lecturer- Verwaltung, Admin-Rework, Redeploy, Kurs-Filter, 36-Monate, Expiry-Tiers, Mobile-Nav). Grundlage für die folgende Test-Implementierung. --- cypress/TEST-MATRIX-DELTA.md | 68 ++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 cypress/TEST-MATRIX-DELTA.md diff --git a/cypress/TEST-MATRIX-DELTA.md b/cypress/TEST-MATRIX-DELTA.md new file mode 100644 index 0000000..a95fe98 --- /dev/null +++ b/cypress/TEST-MATRIX-DELTA.md @@ -0,0 +1,68 @@ +# Cypress Delta Test Matrix — staging update (2026-07) + +> Ergänzung zur ursprünglichen `TEST-MATRIX.md`. Erfasst (A) Reparaturen bestehender Tests nach dem großen staging-Update (~7.800 geänderte src-Zeilen) und (B) neue Feature-Tests. Konventionen wie zuvor: Keycloak gestubbt, alle API via `cy.intercept` + Fixtures, keine festen Waits, stabile Selektoren. + +## Kontext des Updates + +Zwischen der Test-Baseline (`d1af1a7`) und `origin/staging` kamen u.a. hinzu: Student-Self-Service, Lecturer-Verwaltung (Admin), Admin-Rework (Split von `/admin` in `/admin/projects|templates|lecturers` mit `ProtectedRoute`), Redeploy/VM-Config-Override, Kurs-Filter, 36-Monate-Laufzeit, Expiry-Farbstufen, Owner-Only-Gating für Deployment-Aktionen, `NoRolePage`, `MobileTopBar`, `TemplateIconUpload`, `CredentialInstanceCard`. + +## Teil A — Reparaturen (erledigt) + +| Test | Ursache der Regression | Fix | Status | +| --- | --- | --- | --- | +| `auth-admin-bypasses-setup-gate` | `/admin` existiert nicht mehr (Split) | Ziel-Route → `/admin/projects`, Heading → „Projektübersicht" | ✅ e762bc2 | +| `admin-approve-template-version` | Approval-Queue nach `AdminTemplateApprovals` (`/admin/templates`) verschoben | `cy.loginAs("admin","/admin/templates")` | ✅ a3d1705 | +| `detail-delete-confirmation-flow` | Owner-Only-Gate (`fix/126`): Delete-Button ohne `owner_id` disabled | `owner_id: user-lec-1` in Fixture | ✅ a14815f | +| `auth-non-admin-cannot-access-admin-route` | War geskippt (Befund); jetzt via `ProtectedRoute requireAdmin` behoben | Neu geschrieben: Redirect `/admin/templates`→`/dashboard`, reaktiviert | ✅ 265a90d | +| `detail-issue-207-ungrouped-members` (fremd) | CourseGroupsCard-Rendering/Selektor geändert | in Arbeit (Sub-Agent) | 🔧 | + +## Teil B — Neue Feature-Tests (geplant, P0/P1-Tiefe wie Bestand) + +``` +Bereich +└── Funktion + └── Testfall [Prio · Typ] — Ziel · Risiko bei Regression +``` + +### student · self-service +- `student-dashboard-lists-deployments` **[P0 · success]** — Pure Student → `/student/dashboard`, `GET /api/v1/student/deployments` rendert Karten. Risiko: Studenten sehen ihre Umgebungen nicht. +- `student-dashboard-empty-state` **[P1 · edge]** — leere Liste → „Aktuell keine Deployments für dich verfügbar." Risiko: kaputter Erstkontakt. +- `student-role-routing-guard` **[P0 · permission]** — Pure Student auf `/dashboard`/`/appstore` → Redirect `/student/dashboard`. Risiko: Student sieht Lecturer-UI. +- `student-deployment-details-credentials` **[P1 · success]** — `/student/deployment/:id` lädt Credentials, `CredentialInstanceCard` (mode=student) rendert. Risiko: Zugangsdaten unerreichbar. + +### admin · lecturer-management +- `lecturer-management-lists` **[P1 · success]** — `/admin/lecturers`, `GET /api/v1/lecturers` Tabelle. Risiko: Admin-Verwaltung tot. +- `lecturer-delete-cascade-poll` **[P1 · success]** — Detail-Dialog → „Account löschen" → Type-to-confirm → DELETE 202 → Poll 200→404 → Erfolg. Risiko: Cascade-Delete-Feedback bricht. + +### admin · project-overview +- `admin-project-overview-renders` **[P1 · success]** — `/admin/projects`, Stat-Cards + View-Selector. Risiko: globale Übersicht kaputt. + +### admin · template-approvals (reject) +- `admin-reject-template-with-reason` **[P1 · success]** — „Ablehnen" → Reason → POST `/reject` `{reason}`. Risiko: Autoren ohne Feedback. + +### deployment · redeploy +- `redeploy-deployment-config-override` **[P1 · success]** — „Neu deployen" (running+owner) → Param ändern → POST `/redeploy` mit `deployment_parameter_overrides`+`preserve_credentials`. Risiko: Redeploy-Feature tot. +- `redeploy-only-owner-running` **[P1 · permission]** — Button disabled für Nicht-Owner / nicht-running. Risiko: unautorisierter Redeploy. + +### courses · course-filters +- `course-filters-toggle-chip` **[P1 · state]** — Chip klicken filtert Kursliste client-seitig. Risiko: Filter unbenutzbar. +- `course-filters-admin-crud` **[P1 · success]** — Admin: Filter hinzufügen (POST) / löschen (DELETE). Risiko: Filterverwaltung kaputt. +- `course-filters-lecturer-readonly` **[P2 · permission]** — Lecturer sieht keine Add/Edit/Delete-Controls. Risiko: Nicht-Admin ändert Filter. + +### wizard · runtime +- `wizard-36-months-runtime` **[P2 · edge]** — 36-Monate-Option („3 Jahre") wählbar, `runtime_months:36` im POST. Risiko: neue Laufzeit nicht buchbar. + +### dashboard · expiry-tiers +- `dashboard-expiry-color-tiers` **[P2 · state]** — critical/warning/expired via dynamischen `expires_at` zeigen korrektes Icon. Risiko: verpasste Ablaufwarnungen. + +### cross-cutting · mobile-nav +- `mobile-topbar-drawer-nav` **[P2 · state]** — `cy.viewport(375,667)` → Hamburger öffnet Sidebar-Drawer. Risiko: Mobile-Navigation kaputt. +``` + +## Priorisierung + +- **P0**: student-dashboard-lists, student-role-routing-guard (Kern-Sicherheit + Kernpfad des neuen größten Bereichs) +- **P1**: student-details/-empty, lecturer-mgmt (2), admin-project-overview, admin-reject, redeploy (2), course-filters (2) +- **P2**: course-filters-readonly, 36-months, expiry-tiers, mobile-nav + +Umfang orientiert sich am Bestand (P0/P1-Tiefe): kritische Happy-Paths + wichtigste Guards/Fehlerfälle pro Feature, keine erschöpfende Rollen-/Edge-Matrix. From 01158980016a81427563f89bad9be2f8f966d170 Mon Sep 17 00:00:00 2001 From: Gree44 Date: Sat, 25 Jul 2026 16:38:22 +0200 Subject: [PATCH 06/22] test(cypress): add "student" to loginAs Role union Bereitet die Student-Self-Service-Tests vor: cy.loginAs("student", url) nutzt die vorhandene keycloak/student.json-Fixture (roles=["student"]). Reine Test-Infrastruktur, kein Produktivcode. --- cypress/support/commands.ts | 1 + cypress/support/index.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 9375ec1..8f160b2 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -17,6 +17,7 @@ type Role = | "lecturer" | "admin" + | "student" | "unauthenticated" | "expiring" | "init-error"; diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index c4b4397..dc9db24 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -8,6 +8,7 @@ export {}; type Role = | "lecturer" | "admin" + | "student" | "unauthenticated" | "expiring" | "init-error"; From e04c570d3d09c42b5d442d9f6a3d0b547b96f610 Mon Sep 17 00:00:00 2001 From: Gree44 Date: Sat, 25 Jul 2026 16:39:38 +0200 Subject: [PATCH 07/22] test(integration): repair detail-issue-207-ungrouped-members for current CourseGroupsCard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Abgedeckte Funktion: - CourseGroupsCard "Ohne Gruppe"-Sektion + nachträgliche Gruppenzuweisung (Feat #207) Getesteter Nutzerfluss: - Owner-Lecturer öffnet Deployment-Detail → Karte "Gruppen & Mitglieder" aufklappen → gruppenloses Mitglied in "Ohne Gruppe" → Gruppe wählen → Hinzufügen → POST addGroupMembers Getestete Fachlogik: - Kurs course-alpha hat 3 aktive Mitglieder; cm-1 in grp-1, cm-2 in grp-2, cm-3 (u3-neu) in keiner Gruppe → cm-3 erscheint korrekt in der amber "Ohne Gruppe"-Sektion - Hinweistext "Der Student erhält erst Zugriff…" wird angezeigt - "Hinzufügen"-Button ist ohne Gruppenauswahl disabled, nach Auswahl von "Gruppe A" aktiv - POST addGroupMembers sendet exakt { member_ids: ["cm-3"] } Geprüfte Fehlerfälle: - keine Geprüfte Edge Cases: - Gruppenloses Mitglied (cm-3) erscheint separat und verschwindet nach Zuweisung Betroffene Komponenten: - src/pages/DeploymentDetails.tsx (CourseGroupsCard), cypress/e2e/detail/detail-issue-207-ungrouped-members.cy.ts Warum dieser Test wichtig ist: - Sichert das #207-Feature: gruppenlose Studierende bekommen sonst nie Zugriff. Reparatur: Der Karten-Titel

lag unterhalb des Falzes in einem Scroll-Container mit overflow und war zwar im DOM, aber nicht sichtbar. scrollIntoView vor der Visibility-Assertion (und vor dem Klick auf "Anzeigen") ergänzt; keine Produktionsänderung, keine Fixture-Änderung nötig. --- .../e2e/detail/detail-issue-207-ungrouped-members.cy.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/detail/detail-issue-207-ungrouped-members.cy.ts b/cypress/e2e/detail/detail-issue-207-ungrouped-members.cy.ts index 5f9ef84..f4eca17 100644 --- a/cypress/e2e/detail/detail-issue-207-ungrouped-members.cy.ts +++ b/cypress/e2e/detail/detail-issue-207-ungrouped-members.cy.ts @@ -83,9 +83,10 @@ describe("DeploymentDetails · issue #207 ungrouped course members", () => { cy.wait(["@getDeployment", "@getLogs"]); cy.contains("h1", "test-deploy-alpha").should("be.visible"); - // Expand the card. - cy.contains("Gruppen & Mitglieder").should("be.visible"); - cy.contains("button", "Anzeigen").click(); + // Expand the card. The card sits far down the page inside a scroll + // container, so bring its title into view before asserting visibility. + cy.contains("Gruppen & Mitglieder").scrollIntoView().should("be.visible"); + cy.contains("button", "Anzeigen").scrollIntoView().click(); cy.wait(["@getCourseGroups", "@getCourseMembers"]); // The ungrouped section must appear with the new member. From 1ca72d3131045fceee1f503bba75c111efc9a568 Mon Sep 17 00:00:00 2001 From: Gree44 Date: Sat, 25 Jul 2026 16:42:03 +0200 Subject: [PATCH 08/22] test(integration): student-dashboard-lists-deployments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Abgedeckte Funktion: - Student-Self-Service Dashboard (StudentDashboard) + Rollen-Routing Getesteter Nutzerfluss: - Pure Student besucht / → Redirect /student/dashboard → GET /api/v1/student/deployments → Deployment-Karten sichtbar Getestete Fachlogik: - App.tsx pure-student Routing (student && !lecturer && !admin) - getStudentDeployments() lädt die zugewiesenen Deployments - StatusBadge-Mapping (running→Läuft etc.) - Sidebar zeigt nur "Meine Deployments" + Rolle "Student" Geprüfte Fehlerfälle: - (n/a — success path) Geprüfte Edge Cases: - Mehrere Deployments mit unterschiedlichem Status Betroffene Komponenten: - src/pages/StudentDashboard.tsx, src/pages/StudentDashboardPage.tsx, src/api/student.ts, src/layouts/Sidebar.tsx, src/App.tsx Warum dieser Test wichtig ist: - Sichert den Kernpfad des neuen Student-Bereichs. Bricht das Routing oder der Fetch, sehen Studierende ihre zugewiesenen Umgebungen nicht. --- .../student-dashboard-lists-deployments.cy.ts | 78 +++++++++++++++++++ .../fixtures/student/deployments-list.json | 50 ++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 cypress/e2e/student/student-dashboard-lists-deployments.cy.ts create mode 100644 cypress/fixtures/student/deployments-list.json diff --git a/cypress/e2e/student/student-dashboard-lists-deployments.cy.ts b/cypress/e2e/student/student-dashboard-lists-deployments.cy.ts new file mode 100644 index 0000000..01f5cc9 --- /dev/null +++ b/cypress/e2e/student/student-dashboard-lists-deployments.cy.ts @@ -0,0 +1,78 @@ +/// + +// student-dashboard-lists-deployments +// ─────────────────────────────────── +// P0 success-path test: a PURE student (roles=["student"], no lecturer/admin) +// landing on `/` must be routed to /student/dashboard and see their assigned +// deployments, loaded from GET /api/v1/student/deployments. +// +// Why this test exists +// The student self-service area (StudentDashboard.tsx + the pure-student +// branch of App.tsx) is a distinct surface from the lecturer app. It has its +// own router registration (only /student/*, /config, and a catch-all +// redirect) and its own single API fetch (getStudentDeployments(), NO query +// params). If either the pure-student routing or that fetch/render regresses, +// students land on a blank page and never see the environments their lecturer +// assigned to them. This spec pins that core path end to end. +// +// Fixture notes +// - student/deployments-list.json wraps two StudentDeploymentDto rows in the +// EnvelopeArray shape ({ success, message, data, errors, timestamp, +// request_id }) that getStudentDeployments() unwraps via `resp?.data ?? []` +// in src/api/student.ts. +// - Statuses are varied (running / deploying) so both StatusBadge branches +// that a happy-path list exercises render: running → "Läuft", +// deploying → "Wird bereitgestellt". + +describe("StudentDashboard · lists assigned deployments", () => { + beforeEach(() => { + // mockApi wires harmless GET defaults. The pure-student bootstrap does NOT + // call any of them (no openstack-projects/quotas/deployments gate for + // students), but calling it is safe and keeps the spec consistent with the + // rest of the suite. The student endpoint is registered AFTER so — even if + // it overlapped — the later registration would win on Cypress's stack. + cy.mockApi(); + cy.intercept("GET", "/api/v1/student/deployments", { + fixture: "student/deployments-list.json", + }).as("getStudentDeployments"); + }); + + it("routes / → /student/dashboard and renders the deployment cards", () => { + // Pure student visits the app root. + cy.loginAs("student", "/"); + + // App.tsx pure-student branch registers `/` → Navigate /student/dashboard. + cy.location("pathname").should("eq", "/student/dashboard"); + + // Proves getStudentDeployments() actually fired (no arbitrary timer). + cy.wait("@getStudentDeployments"); + + // ── Page shell ────────────────────────────────────────────────────────── + // Unconditional header inside StudentDashboard — anchor that the page + // rendered at all. + cy.contains("h1", "Meine Deployments").should("be.visible"); + cy.contains( + "Hier siehst du alle Umgebungen, die dir dein Dozent zugewiesen hat.", + ).should("be.visible"); + + // ── Deployment cards ──────────────────────────────────────────────────── + // Each fixture row's name (CardTitle) must reach the DOM. + cy.contains("sql-lab-stu").should("be.visible"); + cy.contains("web-workshop-stu").should("be.visible"); + + // StatusBadge mapping. running → "Läuft", deploying → "Wird bereitgestellt". + // Hitting both branches means a StatusBadge regression surfaces here. + cy.contains("Läuft").should("be.visible"); + cy.contains("Wird bereitgestellt").should("be.visible"); + + // Sanity: the loading placeholder must be gone once data resolved. + cy.contains("Lade Deployments...").should("not.exist"); + + // ── Sidebar ───────────────────────────────────────────────────────────── + // A pure student sees exactly one nav item ("Meine Deployments") and the + // role label "Student". The

also says "Meine Deployments", so scope + // the nav assertion to the