diff --git a/cypress/REPORT-STAGING-UPDATE.md b/cypress/REPORT-STAGING-UPDATE.md new file mode 100644 index 0000000..5c7c62d --- /dev/null +++ b/cypress/REPORT-STAGING-UPDATE.md @@ -0,0 +1,101 @@ +# Abschlussbericht — Cypress-Suite-Update auf staging-Stand (2026-07) + +> Aktualisierung der bestehenden Cypress-Integrationstest-Suite an den neuen `origin/staging`-Stand (~7.800 geänderte `src/`-Zeilen seit der ursprünglichen Test-Baseline `d1af1a7`). Reparatur veralteter Tests + neue Feature-Abdeckung. Arbeitsstand: Worktree-Branch `integrationstests-staging-update`. + +## Ergebnis + +| Check | Ergebnis | +| --- | --- | +| **Cypress-Suite** | **46/46 Tests grün** über **30 Specs**, 0 failing (~61s) | +| **TypeScript** (`tsc --noEmit -p cypress/tsconfig.json`) | 0 Fehler | +| **ESLint** | 0 Fehler, 40 Warnings (alle pre-existing) | +| **Build** (`vite build`) | success | + +## Zahlen + +| Metrik | Wert | +| --- | --- | +| Commits in dieser Session | **21** | +| Reparierte bestehende Tests | **5** | +| Reaktivierte Tests (vorher `skip`) | **1** (Admin-Guard) | +| Neue Feature-Tests | **11** (2× P0, 7× P1, 2× P2) | +| Neue Fixtures | ~12 (Student, Lecturer, Admin-Deployments, Redeploy-Params, Course-Filter, …) | +| Produktivcode-Änderungen | **0** | +| Neue `data-testid` | **0** (alle Tests über role/text/id/aria-Selektoren) | + +## Kontext des Updates + +`origin/staging` hatte sich seit der ersten Suite massiv weiterentwickelt. Der aktuelle Checkout (`feature/192`) und die alte `integrationstests`-Arbeit waren bereits nach `staging` gemerged. Neu hinzugekommen: Student-Self-Service, Lecturer-Verwaltung (Admin), Admin-Rework (Split von `/admin`), Redeploy/VM-Config-Override, Kurs-Filter, 36-Monate-Laufzeit, Expiry-Farbstufen, Owner-Only-Gating, `ProtectedRoute`, `MobileTopBar`, `CredentialInstanceCard`. + +## Teil A — Reparaturen (6) + +| Test | Ursache | Fix | Commit | +| --- | --- | --- | --- | +| `auth-admin-bypasses-setup-gate` | `/admin` gesplittet | Ziel → `/admin/projects`, Heading „Projektübersicht" | e762bc2 | +| `admin-approve-template-version` | Queue nach `/admin/templates` verschoben | Route angepasst | a3d1705 | +| `detail-delete-confirmation-flow` | Owner-Only-Gate (fix/126) | `owner_id` in Fixture | a14815f | +| `auth-non-admin-cannot-access-admin-route` | war geskippt; Befund via `ProtectedRoute` behoben | neu geschrieben (Redirect-Assertion) + reaktiviert | 265a90d | +| `detail-issue-207-ungrouped-members` (fremd) | Card-Titel off-screen | `scrollIntoView()` | e04c570 | +| `visual-capture` (fremd) | Mobile: `cy.contains` traf versteckten Sidebar-Link; falscher Reference-Pfad | `cy.contains("h1", …)` + Pfad-Fix | 6d58f9f, 20477c3 | + +## Teil B — Neue Feature-Tests (11) + +### Student-Self-Service +- `student-dashboard-lists-deployments` **[P0]** (1ca72d3) — Routing `/` → `/student/dashboard` + Deployment-Karten +- `student-role-routing-guard` **[P0]** (134f4e1) — Student auf `/dashboard`/`/appstore` → Redirect; keine Lecturer-Calls +- `student-deployment-details-credentials` **[P1]** (60fec05) — VMs + Credentials (CredentialInstanceCard mode=student) + +### Admin-Bereich +- `lecturer-management-lists` **[P1]** (f35a9ed) — Lecturer-Tabelle + Detail-Dialog +- `lecturer-delete-cascade-poll` **[P1]** (13fb357) — Type-to-confirm + DELETE 202 + Poll 200→404 +- `admin-project-overview-renders` **[P1]** (b231c48) — Stat-Cards + View-Selector (Dozent/Kurs/Datum) +- `admin-reject-template-with-reason` **[P1]** (93bc5cb) — Reject-Flow mit Begründung im Payload + +### Deployment / Courses +- `redeploy-deployment-config-override` **[P1]** (ce5e739) — RedeployDialog, nur geänderte Params im Override +- `course-filters-toggle-chip` **[P1]** (317482c) — client-seitige Chip-Filterung +- `course-filters-admin-crud` **[P1]** (aac5a7d) — Admin: Filter anlegen (POST) + löschen (DELETE) + +### Wizard / Dashboard +- `wizard-36-months-runtime` **[P2]** (26fc782) — „3 Jahre"-Laufzeit → `runtime_months: 36` +- `dashboard-expiry-color-tiers` **[P2]** (fde35a6) — warning/critical/expired-Indikatoren (relative Daten) + +## Infrastruktur-Anpassungen + +- **`Role`-Union** um `"student"` erweitert (commands.ts + index.d.ts, Commit 0115898) — nutzt bestehende `keycloak/student.json`. +- **Delta-Matrix** `cypress/TEST-MATRIX-DELTA.md` (9f681f5) dokumentiert Reparaturen + neue Tests. +- **`cy.mockApi`** deckte bereits `template-versions/queue` + `openstack/flavors` ab (aus der ersten Session). + +## Bestätigte Sicherheits-Verbesserung + +Der ursprünglich in `SECURITY-FINDINGS.md` dokumentierte Privilege-Escalation-Befund (`/admin` ohne Frontend-Rollen-Gate) ist auf `staging` **behoben**: `ProtectedRoute requireAdmin` leitet Nicht-Admins auf `/dashboard` um. Der zuvor geskippte Test ist reaktiviert und sichert den Guard; das Finding wurde auf **RESOLVED** aktualisiert. + +## Umgebungs-Hinweis (nicht committed) + +Auf diesem Rechner (Apple M2, aber **x64-node unter Rosetta**) kollidiert Cypress' gebündeltes arm64-esbuild mit dem x64-node beim Kompilieren der TS-Config. Lokaler Workaround: `ESBUILD_BINARY_PATH` auf ein passendes x64-esbuild-0.28.0-Binary setzen — **nur für Cypress**, nicht für Vite (das braucht 0.25.12). Ein lokales Runner-Skript `/tmp/local-cypress-run.sh` kapselt das. **Nichts davon wurde committed** — auf einer arch-konsistenten Maschine / in CI ist der Workaround unnötig und würde dort sogar stören. `node_modules` im Worktree ist ein Symlink auf den Haupt-Checkout. + +## Verbliebene Risiken / Nicht abgedeckt + +- **`TemplateIconUpload`** (Multipart-Icon-Upload im AppStore-Owner-Dialog) — nicht getestet (Datei-Upload + Multipart-Intercept aufwändig). +- **SSH-Key-Download** im Student-Detail (Blob + Content-Disposition) — nur Anzeige getestet, nicht der Download. +- **`NoRolePage`** — laut Analyse ist `/no-role` nicht als Route registriert (nur ProtectedRoute-Redirect-Ziel), landet über Catch-all bei `/dashboard`. Potenzielle Lücke, nicht durch Test fixiert. +- **Rename-Flow** der Course-Filter (PATCH) — nur Create/Delete getestet. +- **Fehlerpfade** vieler neuer Features (403/404/409) — überwiegend Happy-Path abgedeckt, analog zur bestehenden P0/P1-Tiefe. +- **Redeploy pro VM** (Instance-Redeploy) — nur der Deployment-weite Redeploy getestet. + +## Mögliche zukünftige Testfälle + +1. `student-credentials-error-403/404` — Fehlerpfade der Credentials +2. `redeploy-instance-per-vm` — VM-einzelner Redeploy +3. `redeploy-only-owner-or-running` — Permission/Status-Gate des Redeploy-Buttons +4. `course-filters-rename` — PATCH-Flow +5. `course-filters-lecturer-readonly` — Lecturer sieht keine CRUD-Controls +6. `template-icon-upload` — Multipart-Upload + Preview +7. `lecturer-delete-timeout` — Poll läuft 30s ohne 404 → Timeout-State +8. `admin-project-overview-drilldown` — Klick in Dozent/Kurs-Gruppe → Detailtabelle +9. `mobile-topbar-drawer-nav` — Hamburger-Drawer-Navigation (Viewport-Test) +10. `wizard-extend-runtime` — Laufzeit-Verlängerung (PATCH /extend) auf der Detailseite + +## Nächste Schritte + +Branch `integrationstests-staging-update` ist **21 Commits vor `origin/staging`**. Nach Freigabe: `git push` + PR nach `staging`. Für CI müsste `npm run test:e2e` (via `start-server-and-test`) noch als Pipeline-Schritt ergänzt werden, damit die Suite auf jedem PR läuft — auf arch-konsistenten Runnern ohne den esbuild-Workaround. 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/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. 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. diff --git a/cypress/e2e/admin/admin-project-overview-renders.cy.ts b/cypress/e2e/admin/admin-project-overview-renders.cy.ts new file mode 100644 index 0000000..16d0067 --- /dev/null +++ b/cypress/e2e/admin/admin-project-overview-renders.cy.ts @@ -0,0 +1,110 @@ +/// + +// admin-project-overview-renders +// ────────────────────────────── +// P1 success-path test: an admin on /admin/projects sees the global +// project/deployment overview — three stat cards plus the grouping view +// selector — populated from getAllDeployments(null). +// +// Why this test exists +// AdminProjectOverview.tsx (route /admin/projects, gated by +// ) is the central admin monitoring surface +// after the admin rework. On mount it calls getAllDeployments(null) which +// hits GET /api/v1/deployments WITHOUT an openstack_project_id query — the +// admin bypasses the per-project filter and sees every deployment across +// all lecturers. The page derives three stat cards (Gesamt / Aktiv=running / +// Inaktiv=non-running) and a "Ansicht wählen" selector that regroups the +// list by lecturer / course / date. If the global fetch or the grouping +// breaks, the admin loses the fleet-wide overview. +// +// deployment_parameters.teacher shape +// extractTeacher() JSON.parses deployment.deployment_parameters (a STRING) +// and reads parsed.teacher.{id,first_name,last_name,email}; the rendered +// name is `${first_name} ${last_name}`, falling back to "Unbekannt" when the +// field is missing/unparsable. The fixture therefore stores +// deployment_parameters as a JSON string embedding a teacher object — this +// matches the task hint. +// +// Fixture (deployments/admin-list.json) +// Envelope { success, data:[...] } with 3 deployments: 2 running + 1 failed +// → Gesamt 3, Aktiv 2, Inaktiv 1. Two distinct teachers (Petra Professorin +// ×2 incl. the failed one, Dieter Dozent ×1) and three course.name values so +// both the lecturer and course groupings have something to render. The +// courses list is mockApi's default (empty), so extractCourse falls back to +// deployment.course.name. + +describe("Admin · project overview renders the global deployment fleet", () => { + beforeEach(() => { + // Default GETs (projects/quotas/deployments/courses/keycloak-groups/...) + // so the admin shell bootstraps. We override the deployments list below. + cy.mockApi(); + + // getAllDeployments(null) → GET /api/v1/deployments with NO + // openstack_project_id. The regex matches the bare path or a query string + // but not deeper /deployments/ paths. Registered AFTER mockApi so it + // wins on Cypress' reverse-registration stack. + cy.intercept("GET", /\/api\/v1\/deployments(\?[^/]*)?$/, { + fixture: "deployments/admin-list.json", + }).as("getAdminDeployments"); + + // getKeycloakGroups() hits the Keycloak admin REST API directly + // (GET /admin/realms/{realm}/groups), not the backend proxy — intercept it + // so the mount effect resolves instead of erroring. + cy.intercept("GET", "**/admin/realms/*/groups*", { + fixture: "keycloak/groups-direct.json", + }).as("getKcAdminGroups"); + }); + + it("shows stat cards and switches the grouping view, all from getAllDeployments(null)", () => { + cy.loginAs("admin", "/admin/projects"); + + // The fleet-wide fetch is the load-bearing request for this page. + // Waiting on it proves the GET fired AND asserts the admin variant sends + // no openstack_project_id filter. + cy.wait("@getAdminDeployments") + .its("request.url") + .should("not.include", "openstack_project_id"); + + // Header. + cy.contains("h1", "Projektübersicht").should("be.visible"); + + // Three stat cards. Gesamt reflects the 3 fixture rows, Aktiv the 2 + // running, Inaktiv the 1 failed. Scope the count assertion to each card + // so we prove the label ↔ number pairing rather than "some 3 on screen". + cy.contains("p", "Gesamte Deployments") + .parent() + .should("contain.text", "3"); + cy.contains("p", "Aktive Deployments") + .parent() + .should("contain.text", "2"); + cy.contains("p", "Inaktive Deployments") + .parent() + .should("contain.text", "1"); + + // Default "Nach Dozent" view: teacher names extracted from + // deployment_parameters.teacher are the grouping rows. Deployment NAMES + // are NOT shown in this aggregate view — that is the tell that lets us + // prove the date view later actually changes the grouping. + cy.get('select[aria-label="Ansicht wählen"]').should("have.value", "lecturer"); + cy.contains("Petra Professorin").should("be.visible"); + cy.contains("Dieter Dozent").should("be.visible"); + cy.contains("admin-deploy-alpha").should("not.exist"); + + // Switch to "Nach Kurs": rows are now course names (from course.name, + // since the courses list is empty). The "Kurs" column header appears and + // the individual course names render. + cy.get('select[aria-label="Ansicht wählen"]').select("course"); + cy.contains("th", "Kurs").should("be.visible"); + cy.contains("Kurs Alpha").should("be.visible"); + cy.contains("Kurs Beta").should("be.visible"); + + // Switch to "Nach Datum": the list flattens to individual deployments + // sorted by date, so the per-deployment NAME now appears — grouping- + // specific proof distinct from the lecturer/course aggregates. + cy.get('select[aria-label="Ansicht wählen"]').select("date"); + cy.contains("admin-deploy-alpha").should("be.visible"); + cy.contains("admin-deploy-gamma").should("be.visible"); + // The failed row's status is surfaced only in the date view's Status cell. + cy.contains("failed").should("be.visible"); + }); +}); diff --git a/cypress/e2e/admin/admin-reject-template-with-reason.cy.ts b/cypress/e2e/admin/admin-reject-template-with-reason.cy.ts new file mode 100644 index 0000000..5230e06 --- /dev/null +++ b/cypress/e2e/admin/admin-reject-template-with-reason.cy.ts @@ -0,0 +1,111 @@ +/// + +// admin-reject-template-with-reason +// ───────────────────────────────── +// P1 success-path test: an admin on /admin/templates rejects a pending public +// template-version WITH a typed reason. The rejection must fire +// POST /api/v1/template-versions//reject with { reason } in the body, and +// the rejected row must disappear from the queue UI while the other pending +// row stays visible. +// +// Why this test exists +// Without a working reject-with-reason flow, template authors (Dozenten) get +// no feedback about WHY their version was rejected — the rejection_reason is +// what the version overview shows them so they can submit a correction. +// Breaking this silently leaves authors stuck. +// +// ACTUAL reject interaction (verified against AdminTemplateApprovals.tsx) +// The reject flow is a TWO-STEP inline interaction, NOT an immediate POST and +// NOT a modal dialog: +// 1. Each pending card starts with an "Ablehnen" button (+ "Schnell +// genehmigen"). Clicking "Ablehnen" only sets selectedVersionId — it +// reveals an inline