diff --git a/backend/src/test/kotlin/com/taskowolf/workflows/WorkflowTransitionGuardTest.kt b/backend/src/test/kotlin/com/taskowolf/workflows/WorkflowTransitionGuardTest.kt index 6b726ee..63604d4 100644 --- a/backend/src/test/kotlin/com/taskowolf/workflows/WorkflowTransitionGuardTest.kt +++ b/backend/src/test/kotlin/com/taskowolf/workflows/WorkflowTransitionGuardTest.kt @@ -16,6 +16,8 @@ import com.taskowolf.workflows.infrastructure.WorkflowStatusRepository import com.taskowolf.workflows.infrastructure.WorkflowTransitionRepository import io.mockk.every import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows @@ -165,4 +167,51 @@ class WorkflowTransitionGuardTest { service.validateTransition(issue, toStatusId, actor) } } + + // ---- Write path (B4): updateGuards must persist the polymorphic "type" discriminator ---- + + private fun storedGuardsAfterUpdate(guards: List): String { + val transitionId = UUID.randomUUID() + val transition = WorkflowTransition(workflow = mockk(), toStatus = mockk()) + every { transitionRepository.findById(transitionId) } returns java.util.Optional.of(transition) + every { transitionRepository.save(any()) } answers { firstArg() } + + service.updateGuards(transitionId, guards) + + verify { transitionRepository.save(transition) } + return transition.guards!! + } + + @Test + fun `updateGuards writes the type discriminator for every guard`() { + val stored = storedGuardsAfterUpdate( + listOf(RequiredFieldGuard("storyPoints"), RoleRestrictionGuard(listOf("ADMIN", "MEMBER"))) + ) + + val nodes = mapper.readTree(stored) + assertEquals("REQUIRED_FIELD", nodes[0].get("type")?.asString()) + assertEquals("storyPoints", nodes[0].get("field")?.asString()) + assertEquals("ROLE_RESTRICTION", nodes[1].get("type")?.asString()) + val roles = nodes[1].get("roles") + assertEquals(listOf("ADMIN", "MEMBER"), (0 until roles.size()).map { roles[it].asString() }) + } + + @Test + fun `guards written by updateGuards are read back by validateTransition`() { + val stored = storedGuardsAfterUpdate(listOf(RequiredFieldGuard("storyPoints"))) + + val workflowId = UUID.randomUUID() + val fromStatusId = UUID.randomUUID() + val toStatusId = UUID.randomUUID() + val issue = issueWithWorkflow(workflowId, fromStatusId, storyPoints = null) + val transition = mockk() + every { transition.guards } returns stored + every { + transitionRepository.findByWorkflowIdAndFromStatusIdAndToStatusId(workflowId, fromStatusId, toStatusId) + } returns transition + + // Before the fix the untyped JSON made readValue throw (→ 500); now the guard itself rejects the move. + val ex = assertThrows { service.validateTransition(issue, toStatusId, actor) } + assertEquals("workflow.transitionFieldRequired", ex.messageKey) + } } diff --git a/docs/superpowers/backlog-overview.md b/docs/superpowers/backlog-overview.md index ab1dcf3..f3824fe 100644 --- a/docs/superpowers/backlog-overview.md +++ b/docs/superpowers/backlog-overview.md @@ -23,20 +23,20 @@ | 16 | Backend-Text-Lokalisierung (Spring `MessageSource`) | Full-Stack | ✅ **AUSGELIEFERT** (Phasen 1–3 alle auf `main`: P1 Fundament+Pilot PR #83, P2 voller API-Fehler-Sweep PRs #84/#85/#86/#87/#90, P3 E-Mails+In-App-Notifications in Empfänger-Sprache PR #92 squash `00665f5`; en/de mit EN-Fallback, 3 CI-Gates grün [MessagesParity/KeyedReferenceIntegrity/NoUnkeyedUserFacingThrow]; **Release v1.0.15**. Offen: Wolfgangs manueller DE/EN-Smoke [post-release]) | | M1 | Dependency-/Base-Image-Wartung (Dependabot-Bündel) | Ops/Maintenance | ✅ **AUSGELIEFERT** (PRs #81/#62/#60/#59/#82, Release v1.0.14) | | M2 | jackson-databind @JsonView-Bypass-CVEs + Dependabot-Bündel | Ops/Security | ✅ **AUSGELIEFERT** (jackson-bom 2.21.4→2.21.5 PR #93 [GHSA-mhm7-754m-9p8w + GHSA-5gvw-p9qm-jgwh/CVE-2026-59889]; backend-deps #89 [postgresql 42.7.12], Actions #88; Release v1.0.15) | -| M3 | Spring Boot 4 Migration (Spring Framework 7) | Ops/Security | ✅ **AUSGELIEFERT** (Phase 1 PR #156, Phase 2 PR #164, IMAP-Fix PR #166; Release v1.0.21). Jackson 2 bleibt nur für springdoc/swagger-core. Folgepunkte: B4–B7, H4 | +| M3 | Spring Boot 4 Migration (Spring Framework 7) | Ops/Security | ✅ **AUSGELIEFERT** (Phase 1 PR #156, Phase 2 PR #164, IMAP-Fix PR #166; Release v1.0.21). Jackson 2 bleibt nur für springdoc/swagger-core. Folgepunkte B4–B7, H4 in v1.0.22 ausgeliefert | | H1 | nginx `index.html` no-cache Härtung | Ops/Hardening | ✅ **AUSGELIEFERT** (PR #51, Release v1.0.10) | | H2 | Notification-Prefs PUT: unbekannter Typ → 400 leakt Enum-Namen | Hardening | ✅ **AUSGELIEFERT** (PR #50, Release v1.0.10) | | H3 | `changePassword`: `newPassword` erlaubt reine Leerzeichen | Hardening | ✅ **AUSGELIEFERT** (PR #50, Release v1.0.10) | | B1 | User können ihre eigene Rolle ändern | Bug | ✅ **AUSGELIEFERT** (PR #53, Release v1.0.11) | -| B2 | Read-only greift nicht: User können Tickets trotz Read-only ändern | Bug | ✅ **GEMERGT** (PR #173, 2026-10-24, noch nicht released): Regressionstest VIEWER PATCH/DELETE → 403 + Admin-only-Konfigseiten clientseitig auf `isAdmin` gegated | +| B2 | Read-only greift nicht: User können Tickets trotz Read-only ändern | Bug | ✅ **AUSGELIEFERT** (PR #173, Release v1.0.22): Regressionstest VIEWER PATCH/DELETE → 403 + Admin-only-Konfigseiten clientseitig auf `isAdmin` gegated | | B3 | Logout-Button verschwindet, wenn das Menü länger als der Bildschirm ist | Bug/UI | ✅ **AUSGELIEFERT** (PR #54, Release v1.0.11) | -| B4 | Transition-Guards: Issue über Transition mit Guards verschieben → 500 | Bug/Backend | ✅ **GEMERGT** (PR #171, 2026-10-24, noch nicht released): `writerFor(TypeReference)` + V31-Repair gespeicherter Guards | -| B5 | SSO über die Oberfläche nicht nutzbar (Link, nginx-Proxy, Callback-Route) | Bug/Full-Stack | ✅ **GEMERGT** (PR #174, 2026-10-24, noch nicht released): Link, Proxy, Callback-Route + Refresh-Token, Fragment statt Query, `TW_BASE_URL`; E2E-Smoke grün | -| B6 | Unbekannte Pfade liefern 500 statt 404 | Bug/Backend | ✅ **GEMERGT** (PR #170, 2026-10-24, noch nicht released): Spring-`ErrorResponse`-Status respektiert (404/405/400) | -| B7 | Dev-Profil mit H2 startet nicht (`gen_random_uuid()` ab V11) | Bug/Dev-Setup | ✅ **GEMERGT** (PR #172, 2026-10-24, noch nicht released): Dev-Profil auf lokales Postgres, H2 nur noch Test-Scope | -| H4 | Jackson-3-Feinschliff: `asString()` im Webhook-Eingang, Mapper-Empfehlung im ai-guide | Hardening/Docs | ✅ **GEMERGT** (PR #178, 2026-10-24, noch nicht released): Webhook-`asString("")` (500 belegt), `builderWithJackson2Defaults`, ai-guide, Guard-Asserts | -| H5 | Auth-Rate-Limit vertraut client-gesetztem `X-Forwarded-For` (umgehbar) bzw. teilt sich hinter nginx einen Bucket | Security/Hardening | ✅ **GEMERGT** (PR #177, 2026-10-24, noch nicht released): XFF ignoriert, `X-Real-IP` nur von privatem Peer, 429 keyed | -| H6 | Prod-Compose veröffentlicht Postgres auf `5432:5432` (alle Interfaces) | Security/Ops | ✅ **UMGESETZT** (2026-10-24): prod + dev binden Postgres nur noch an `127.0.0.1` | +| B4 | Transition-Guards: Issue über Transition mit Guards verschieben → 500 | Bug/Backend | ✅ **AUSGELIEFERT** (PR #171, Release v1.0.22): `writerFor(TypeReference)` + V31-Repair gespeicherter Guards | +| B5 | SSO über die Oberfläche nicht nutzbar (Link, nginx-Proxy, Callback-Route) | Bug/Full-Stack | ✅ **AUSGELIEFERT** (PR #174, Release v1.0.22): Link, Proxy, Callback-Route + Refresh-Token, Fragment statt Query, `TW_BASE_URL`; E2E-Smoke grün | +| B6 | Unbekannte Pfade liefern 500 statt 404 | Bug/Backend | ✅ **AUSGELIEFERT** (PR #170, Release v1.0.22): Spring-`ErrorResponse`-Status respektiert (404/405/400) | +| B7 | Dev-Profil mit H2 startet nicht (`gen_random_uuid()` ab V11) | Bug/Dev-Setup | ✅ **AUSGELIEFERT** (PR #172, Release v1.0.22): Dev-Profil auf lokales Postgres, H2 nur noch Test-Scope | +| H4 | Jackson-3-Feinschliff: `asString()` im Webhook-Eingang, Mapper-Empfehlung im ai-guide | Hardening/Docs | ✅ **AUSGELIEFERT** (PR #178, Release v1.0.22): Webhook-`asString("")` (500 belegt), `builderWithJackson2Defaults`, ai-guide, Guard-Asserts | +| H5 | Auth-Rate-Limit vertraut client-gesetztem `X-Forwarded-For` (umgehbar) bzw. teilt sich hinter nginx einen Bucket | Security/Hardening | ✅ **AUSGELIEFERT** (PR #177, Release v1.0.22): XFF ignoriert, `X-Real-IP` nur von privatem Peer, 429 keyed | +| H6 | Prod-Compose veröffentlicht Postgres auf `5432:5432` (alle Interfaces) | Security/Ops | ✅ **AUSGELIEFERT** (PR #179, Release v1.0.22): prod + dev binden Postgres nur noch an `127.0.0.1` | ## M1 — Dependency-/Base-Image-Wartung (Dependabot-Bündel) > ✅ **AUSGELIEFERT** (2026-07-18, Release **v1.0.14**). Reines Wartungsrelease, @@ -67,7 +67,7 @@ > Plan Phase 2: `docs/superpowers/plans/2026-10-19-m3-phase2-jackson3.md` > > **Folgepunkte (zurückgestellt, eigene Einträge):** B4 Transition-Guards 500 · B5 SSO-UI-Verdrahtung · -> B6 unbekannte Pfade 500 · B7 Dev-Profil H2 · H4 Jackson-3-Feinschliff. +> B6 unbekannte Pfade 500 · B7 Dev-Profil H2 · H4 Jackson-3-Feinschliff. Alle in **v1.0.22** ausgeliefert. **Warum jetzt:** Spring Boot 3.5.16 (Framework 6.2.19) ist das letzte OSS-Release der 3.5-Linie auf Maven Central; neue spring-webmvc-CVEs werden nur noch in Framework 7.0.x @@ -346,7 +346,7 @@ möglich sein. Berührungspunkt: Rollen-/Rechte-Verwaltung aus #9 serverseitig unterbinden) **und** UI (eigene Rolle nicht editierbar anzeigen). ## B2 — Read-only greift nicht: User können Tickets trotz Read-only ändern 🐞 -> Gemeldet 2026-07-11. Status: **✅ GEMERGT (PR #173, 2026-10-24, noch nicht released)** — (a) Regressionstest + (b) Admin-Gating umgesetzt, s. u. +> Gemeldet 2026-07-11. Status: **✅ AUSGELIEFERT (PR #173, Release v1.0.22)** — (a) Regressionstest + (b) Admin-Gating umgesetzt, s. u. > Zwei Fehldiagnosen unterwegs korrigiert (beide durch fehlerhafte Greps): (1) das > Backend blockt Read-only bereits — Reproduktion VIEWER `PATCH /issues/{id}` → > **403**, Titel unverändert (jeder Ticket-Write via `@PreAuthorize canWrite` bzw. @@ -383,7 +383,7 @@ unten gepinnt / `