diff --git a/.gitea/workflows/workflow-pflichttest-gate.yml b/.gitea/workflows/workflow-pflichttest-gate.yml new file mode 100644 index 00000000..d99c4de0 --- /dev/null +++ b/.gitea/workflows/workflow-pflichttest-gate.yml @@ -0,0 +1,23 @@ +name: Workflow-Pflichttest-Gate + +on: + pull_request: + paths: + - "workflow/**" + +jobs: + pflichttest-gate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + - name: Gate bauen + working-directory: workflow + run: go build -o /tmp/pflichttestgate ./cmd/pflichttestgate + - name: Geänderte Dateien gegen Pflichttest-Regel prüfen + run: | + git diff --name-only "origin/${{ github.base_ref }}...HEAD" | /tmp/pflichttestgate diff --git a/workflow/cmd/pflichttestgate/main.go b/workflow/cmd/pflichttestgate/main.go new file mode 100644 index 00000000..90ec56be --- /dev/null +++ b/workflow/cmd/pflichttestgate/main.go @@ -0,0 +1,39 @@ +// Command pflichttestgate ist das CI-Gate aus workflow/docs/TESTSTRATEGIE.md +// Abschnitt 4. Aufruf: pflichttestgate < geaenderte-dateien.txt +package main + +import ( + "bufio" + "fmt" + "os" + + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/pflichttestgate" +) + +func main() { + var changedFiles []string + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + line := scanner.Text() + if line != "" { + changedFiles = append(changedFiles, line) + } + } + if err := scanner.Err(); err != nil { + fmt.Fprintf(os.Stderr, "pflichttestgate: eingabe konnte nicht gelesen werden: %v\n", err) + os.Exit(2) + } + + violations := pflichttestgate.CheckDiff(changedFiles) + if len(violations) == 0 { + fmt.Println("pflichttestgate: bestanden — alle sicherheitskritischen Aenderungen haben begleitende Tests.") + return + } + + fmt.Fprintln(os.Stderr, "pflichttestgate: FEHLGESCHLAGEN — Pflichttest fehlt fuer:") + for _, v := range violations { + fmt.Fprintf(os.Stderr, " - Package %q (Datei %q hat keine begleitende _test.go-Aenderung)\n", v.Package, v.ChangedFile) + } + fmt.Fprintln(os.Stderr, "\nSiehe workflow/docs/TESTSTRATEGIE.md Abschnitt 4.") + os.Exit(1) +} diff --git a/workflow/docs/QA-01-PRUEFPROTOKOLL.md b/workflow/docs/QA-01-PRUEFPROTOKOLL.md new file mode 100644 index 00000000..5fd76473 --- /dev/null +++ b/workflow/docs/QA-01-PRUEFPROTOKOLL.md @@ -0,0 +1,99 @@ +# QA-01 – Pruefprotokoll (Teststrategie Workflow) + +Stand: 2026-10-16. Umgebung: frischer Klon auf Host 204 +(`/root/wfqa-01-work-1792178879/code`), Branch +`feature/qa-01-teststrategie-workflow`, Testdatenbank ueber +`TEST_TENANT_DSN`/`/etc/nexarch-dev/tenant-dev.env` (Wegwerf-DB `dms_dev`- +Infrastruktur, keine echten Mandantendaten). + +## Akzeptanzkriterien + +1. **Teststrategie deckt Engine, Genehmigungen, Aufgaben und Routing ab** — + **erfuellt.** `workflow/docs/TESTSTRATEGIE.md` Abschnitt 2/3 ordnet allen vier + Bereichen konkrete, real existierende Beispieltests je Testebene zu + (Unit/Integration/Vertrag/E2E). +2. **Testdatenbestand ist realistisch und anonymisiert** — **erfuellt.** + `workflow/testdata/fixtures/{engine,approvals,tasks,routing}.json`, synthetisches + Kanzlei-Szenario (Aktenzeichen, Posteingaenge, Fristsachen), Personen/Gruppen als + Slugs statt Klarnamen/E-Mail. Automatisiert geprueft durch + `TestFixtures_ContainNoEmailLikePII` (kein `@`-Zeichen in den Fixture-Dateien). +3. **Verantwortlichkeit je Testebene ist dokumentiert** — **erfuellt.** + `workflow/docs/TESTSTRATEGIE.md` Abschnitt 6 (Tabelle: Testebene → + verantwortlich → wann). +4. **Pflichttests fuer sicherheitsrelevanten Code als verbindliches Merge-Gate, + analog Core QA-01** — **erfuellt.** `workflow/internal/pflichttestgate` + + `workflow/cmd/pflichttestgate` (Code-Kopie des Core-/Mail-Musters) decken sieben + Pakete ab: `svcauth`, `moduleadapter`, `tenantregistryclient`, `backchannel`, + `backchannelapi`, `dualcheck`, `delegation`. `.gitea/workflows/workflow- + pflichttest-gate.yml` fuehrt das Gate bei jedem PR auf `workflow/**` aus. + +## Prüfungen vor der Abnahme + +### Prüfung 1: Teststrategie-Dokument gegen alle vier Bereiche geprüft + +**Durchgefuehrt, bestanden.** Jede Zeile der Tabelle in Abschnitt 3 wurde gegen den +tatsaechlichen Code verifiziert (Dateien existieren, Tests sind gruen — siehe +Testlauf unten). Keine Luecke in der Abdeckung der vier Bereiche gefunden; eine +echte Luecke ausserhalb der vier Bereiche (fehlende Tests fuer `svcauth` und +`tenantregistryclient` selbst) wurde gefunden, dokumentiert und mit dem Merge-Gate +abgesichert statt stillschweigend uebergangen (Teststrategie Abschnitt 4/7). + +### Prüfung 2: Testdatenbestand enthält keine echten Personendaten + +**Durchgefuehrt, bestanden.** +- Automatisiert: `go test ./internal/fixtureset/... -run TestFixtures_ContainNoEmailLikePII -v` → PASS. +- Manuell: alle vier JSON-Dateien durchgesehen. Ausschliesslich Slugs + (`anwalt.weber`, `anwalt.santos`, `ra-fachangestellte.kowalski`, + `ra-fachangestellte.yilmaz`, `team-scanstelle`), frei erfundene Aktenzeichen + (`AZ-2026-0142`, `AZ-2026-0198`) und Posteingangs-IDs. Keine realen Namen, + keine E-Mail-Adressen, kein Bezug zu einem echten Alt-/Kundensystem. + +### Prüfung 3: Zweite Person kann Teststrategie ohne Rückfragen anwenden + +**Nicht durchgefuehrt — offener Punkt.** Erfordert eine echte zweite Person; in +dieser Session nicht verfuegbar (dieselbe ehrliche Einschraenkung wie bereits bei +Mail-QA-01 fuer die entsprechende Bug-Tracking-Pruefung dokumentiert). Siehe +Teststrategie Abschnitt 7/8. + +## Negativtest des Gates selbst (Pflicht aus Akzeptanzkriterium 4) + +Realer Lauf auf Host 204, kompiliertes Binary `workflow/cmd/pflichttestgate`: + +``` +--- FAIL case (missing test) --- +$ printf 'workflow/internal/dualcheck/dualcheck.go\n' | pflichttestgate +pflichttestgate: FEHLGESCHLAGEN — Pflichttest fehlt fuer: + - Package "workflow/internal/dualcheck" (Datei "workflow/internal/dualcheck/dualcheck.go" hat keine begleitende _test.go-Aenderung) +exit=1 + +--- PASS case (with test) --- +$ printf 'workflow/internal/dualcheck/dualcheck.go\nworkflow/internal/dualcheck/dualcheck_test.go\n' | pflichttestgate +pflichttestgate: bestanden — alle sicherheitskritischen Aenderungen haben begleitende Tests. +exit=0 +``` + +Zusaetzlich `go test ./internal/pflichttestgate/... -v`: alle vier Faelle +(fehlender Test je der sieben sensiblen Pakete einzeln, alle sieben gleichzeitig, +begleiteter Test, nicht-sensibler Pfad) PASS. + +## Realer Testlauf des gesamten Workflow-Moduls + +`go build ./...`, `go vet ./...` und +`go test ./... -p 1 -count=1 -timeout 300s` auf Host 204 gegen +`TEST_TENANT_DSN` (dms_dev-Infrastruktur): **alle Pakete gruen**, inklusive der neu +hinzugekommenen `internal/fixtureset` (saet alle vier Fixtures real in eine frische +Testdatenbank, prueft zusaetzlich, dass die Vier-Augen-Regel eine +Selbstbestaetigung ablehnt und die Vertretungsregel korrekt auf die hinterlegte +Person aufloest) und `internal/pflichttestgate`. + +`svcauth` und `tenantregistryclient` zeigen weiterhin `[no test files]` — das ist +die dokumentierte, bewusst stehengelassene Luecke (Teststrategie Abschnitt 7/8): +sie ist jetzt durch das Merge-Gate abgesichert (keine kuenftige Aenderung an diesen +Dateien ohne begleitenden Test mehr moeglich), aber nicht rueckwirkend durch dieses +Ticket behoben — das waere eine Erweiterung ueber den Ticketumfang hinaus gewesen. + +## Gesamtergebnis + +Alle vier Akzeptanzkriterien erfuellt. Von den drei Pruefungen vor der Abnahme sind +zwei durchgefuehrt und bestanden; Pruefung 3 (zweite Person) ist offen und +dokumentiert, nicht verschwiegen. diff --git a/workflow/docs/TESTSTRATEGIE.md b/workflow/docs/TESTSTRATEGIE.md new file mode 100644 index 00000000..a4f2f2b3 --- /dev/null +++ b/workflow/docs/TESTSTRATEGIE.md @@ -0,0 +1,166 @@ +# NEXARCH Workflow – Teststrategie + +Stand: 2026-10-16. Ticket: QA-01. Vorbild: Core `QA-01` (`docs/TESTSTRATEGIE-CORE.md`, +Fertig), dieselbe Struktur wie Mail (`mail/docs/TESTSTRATEGIE-MAIL.md`) und AI +(`ai/docs/TESTSTRATEGIE-AI.md`) — fuer Workflow uebernommen und um die vier +Teilbereiche ergaenzt, die dieses Modul tatsaechlich hat: Engine, Genehmigungen, +Aufgaben, Routing. + +## 1. Warum dieses Dokument existiert + +archivdms (Vorgaengerprojekt) hatte 0 Tests fuer `auth`/`storage`/`permissions.go` +bei insgesamt nur 2 `_test.go`-Dateien in ganz `internal/`; archivmail testete 2 von +18 Modulen. NEXARCH Core legte deshalb in QA-01 fest: **Testpflicht fuer Auth, +Tenant-Scoping und Zugriffspruefung ist ein Merge-Gate, keine Nachruestung.** +Workflow uebernimmt denselben Grundsatz — hier zusaetzlich konkret gemacht fuer die +sicherheitsrelevanten Stellen, die Workflow tatsaechlich hat (Abschnitt 4), statt +den Grundsatz nur zu wiederholen. + +Workflow ist zum Zeitpunkt dieses Tickets kein Neubau mehr wie Mail bei dessen +QA-01: Engine (ENG-01/ENG-03/ENG-04), Genehmigungen (APR-01 bis APR-04), Aufgaben +(TSK-01/TSK-02) und Routing (RTE-01 bis RTE-05) existieren bereits mit eigenen +`_test.go`-Dateien je Ticket. Dieses Dokument macht explizit, was bisher implizit je +Ticket entschieden wurde, und schliesst die eine echte Luecke, die eine Durchsicht +ergeben hat (Abschnitt 4, 7). + +## 2. Testpyramide + +| Ebene | Werkzeug | Umfang | +|---|---|---| +| Unit | `go test` (Standardbibliothek) | Einzelne Funktionen/Typen ohne externe Abhaengigkeit, z. B. `processdef.Parse`-Validierung, `routingrule.Rule.Matches`, `delegation`-Zeitraumlogik | +| Integration | `go test` gegen echte PostgreSQL-Instanz (`testdb.Pool`, eigenes Schema je Testlauf) | Repository-/Service-Schicht: `statemachine.Engine`, `approval.Service`, `dualcheck.Service`, `task.Store`, `routingrule.Store` — alle bestehenden `*_test.go` in `internal/*` folgen bereits diesem Muster | +| Vertragstest (Modulgrenze) | `go test` gegen `moduleadapter`-HTTP-Schnittstelle (ENG-02), echter `httptest.Server`-Roundtrip | DMS/Mail rufen Workflow ausschliesslich ueber `internal/moduleadapter` auf; `moduleadapter_test.go`/`handler_test.go` sind die bestehende Grundlage. Ein modulübergreifender Vertragstest mit echten DMS-/Mail-Konsumenten (analog `archive-contract-tests.yml`/`contract-tests.yml`) ist noch nicht eingerichtet, siehe Abschnitt 7 | +| E2E | Echter HTTP-Roundtrip ueber `cmd/workflow-app` (`main_test.go` startet den echten Mux) bis zum ersten Workflow-Frontend-Ticket, danach Playwright/Jest gegen die echten UIs (`web/task-board`, `web/workflow-approvals`, `web/routing-rules`, `web/workflow-cases`) | Vollstaendiger Request-Response-Zyklus, kein reiner Funktionsaufruf | + +**E2E-Zwischenloesung begruendet:** Dieselbe Begruendung wie bei Mail — ein echter +HTTP-Roundtrip ueber den tatsaechlich gestarteten Dienst (`cmd/workflow-app`, kein +reiner In-Process-Aufruf) ist die ehrliche Untergrenze, bis die UI-Tickets fertig +sind und Playwright/Jest eine echte Oberflaeche zum Testen haben. + +## 3. Beispieltests je Testart und Bereich (Akzeptanzkriterium 1) + +Kein Wegwerf-Demo — Verweis auf real existierende, bereits gruene Tests je Bereich: + +| Bereich | Unit | Integration | Vertrag/E2E | +|---|---|---|---| +| Engine | `internal/processdef/processdef_test.go` (Schema-Validierung) | `internal/statemachine/statemachine_test.go`, `history_test.go` (Zustandsautomat, Historie, optimistische Sperre) | `cmd/workflow-app/main_test.go` | +| Genehmigungen | `internal/dualcheck/dualcheck_test.go` (Regel-Logik ohne DB-Seiteneffekt, soweit isolierbar) | `internal/approval/approval_test.go`, `approval_delegation_test.go`, `internal/delegation/delegation_test.go` | `internal/approvalapi/handler_test.go` | +| Aufgaben | — (Validierung in `task.go` wird integriert mitgetestet) | `internal/task/task_test.go` | `internal/caseoverviewapi/caseoverviewapi_test.go` (Aufgaben-Bezug in der Fallübersicht) | +| Routing | `internal/routingrule/routingrule_test.go` (`Matches`-Bedingungsauswertung) | `internal/routingengine/routingengine_test.go`, `internal/routingsim/routingsim_test.go` | `internal/routingruleapi/handler_test.go` | + +Zusaetzlich fuer QA-01 real durchgefuehrt: `internal/fixtureset/fixtureset_test.go` +saet den unter Abschnitt 5 beschriebenen Testdatenbestand ueber die echten +Domaenen-Services in eine frische Testdatenbank und prueft das Ergebnis (siehe +Abschnitt 6, Pruefung 2) — ein weiteres Integrationstest-Beispiel, das alle vier +Bereiche in einem Lauf abdeckt. + +## 4. Pflichttests als Merge-Gate fuer sicherheitsrelevanten Code (Akzeptanzkriterium 4) + +Verbindlich fuer jeden Pull Request, der Dateien in einem der folgenden Pakete +aendert — **jede geaenderte `.go`-Datei muss von einer geaenderten oder neuen +`_test.go`-Datei im selben Package begleitet sein**, identisch zur Regel aus Core +`QA-01`: + +- **Auth-nahe Logik:** `workflow/internal/svcauth/` (Service-Token-Vergleich in + konstanter Zeit), `workflow/internal/moduleadapter/` (prueft `X-Service-Token` + gegen `svcauth.Equal` — der tatsaechliche Durchsetzungspunkt) +- **Tenant-Scoping:** `workflow/internal/tenantregistryclient/` (fragt Core TEN-01, + muss bei Fehler fail-closed bleiben), `workflow/internal/backchannel/` und + `workflow/internal/backchannelapi/` (extern erreichbarer Eingang, der den + Tenant-Slug vor jeder Aktion gegen die Registry verifiziert, VOR-06) +- **Zugriffspruefung (Vier-Augen/Vertretung):** `workflow/internal/dualcheck/` + (APR-02, `ErrSameAsCreator` verhindert Selbstbestaetigung), + `workflow/internal/delegation/` (APR-04, `ErrSelfSubstitution` verhindert + Selbst-Vertretung als Umgehung des Vier-Augen-Prinzips) + +Diese sieben Pakete wurden bewusst ausgewaehlt, nicht ganz Engine/Genehmigungen/ +Aufgaben/Routing: Business-Logik wie `task.go` oder `routingengine.go` ist bereits +heute durchgaengig getestet (Abschnitt 3), aber kein Tenant-/Auth-Gate im engeren +Sinn. Diese sieben Stellen sind es, weil sie entweder eine Identitaet pruefen +(svcauth, moduleadapter, tenantregistryclient) oder eine Umgehung eines +Zugriffskontrollmechanismus verhindern (dualcheck, delegation, backchannel/-api). + +`internal/pflichttestgate` implementiert das Gate (Code-Kopie des Musters aus Core +`internal/pflichttestgate` bzw. Mail `mail/internal/pflichttestgate` — Workflow ist +ein eigenstaendiges Go-Modul und kann die anderen nicht importieren). +`.gitea/workflows/workflow-pflichttest-gate.yml` fuehrt es gegen jeden PR-Diff aus, +der Dateien unter `workflow/**` aendert. + +Negativtest des Gates selbst (Pruefung 1 dieses Tickets): +`internal/pflichttestgate/gate_test.go` simuliert Diffs mit geaenderten Dateien in +allen sieben Paketen ohne begleitende Testdatei und erwartet je einen Verstoss; +ein zusaetzlicher Fall prueft, dass eine begleitende `_test.go`-Datei das Gate +bestehen laesst und ein nicht-sensibler Pfad ignoriert wird. + +## 5. Testdatenbestand (Akzeptanzkriterium 2) + +`testdata/fixtures/*.json` haelt einen realistischen, vollstaendig synthetischen +Datenbestand fuer alle vier Bereiche, angelehnt an das tatsaechliche Szenario, fuer +das Workflow gebaut wird (Kanzlei-Akte/Posteingang ueber DMS/Mail): + +- `engine.json` — zwei Prozessdefinitionen (`dokumentfreigabe`, `fristsache`) im + ENG-03-Format plus `seed_instances`, die je Definition eine echte Instanz starten + und mindestens einen Uebergang ausfuehren (reale `workflow_instances`-/ + `workflow_instance_history`-Zeilen, keine reinen Definitionsstubs) +- `approvals.json` — drei Genehmigungsaufgaben, zwei aktivierte Vier-Augen- + Prozesstypen (`dualcheck_process_types`) und eine Vertretungsregel + (`delegation_rules`) +- `tasks.json` — drei Aufgaben (Person- und Gruppenzuweisung, alle drei + Prioritaetsstufen, mit Modul-Referenzen auf DMS/Mail-Objekte) +- `routing.json` — drei Routing-Regeln mit unterschiedlicher Prioritaet und allen + drei Bedingungsoperatoren (`equals`, `in`) sowie beiden Aktionstypen + +**Anonymisierung:** Personen/Gruppen sind durchgaengig Slugs +(`anwalt.weber`, `ra-fachangestellte.kowalski`, `team-scanstelle`) statt +Klarnamen oder E-Mail-Adressen; Aktenzeichen (`AZ-2026-0142`) und +Posteingangs-IDs sind frei erfunden und folgen keinem echten Nummernkreis. +`internal/fixtureset/fixtureset_test.go` (`TestFixtures_ContainNoEmailLikePII`) +prueft automatisiert, dass keine der Fixture-Dateien ein `@`-Zeichen enthaelt — +eine harte, maschinell pruefbare Untergrenze gegen versehentlich eingestreute +E-Mail-Adressen oder aehnliche Kontaktdaten. + +`internal/fixtureset` laedt die JSON-Dateien und saet sie ueber die echten +Domaenen-Services (`processdef.Registry`, `approval.Service`, `dualcheck.Rules`, +`delegation.Store`, `task.Store`, `routingrule.Store`) statt per Roh-SQL — die +Fixtures durchlaufen damit dieselben Validierungen wie Produktionsdaten +(z. B. `ErrInvalidAssigneeType`, Schema-Pruefung von `processdef.Parse`). + +## 6. Verantwortlichkeit je Testebene (Akzeptanzkriterium 3) + +| Testebene | Verantwortlich | Wann | +|---|---|---| +| Unit | Entwickler:in des Tickets | Lokal vor jedem Commit | +| Integration (`testdb`/`TEST_TENANT_DSN`) | Entwickler:in des Tickets | Lokal vor jedem Commit; in CI bei jedem Push/PR (`ci.yml`-Aequivalent fuer Workflow, sobald eingerichtet — siehe Abschnitt 7) | +| Pflichttest-Gate (Abschnitt 4) | CI (`workflow-pflichttest-gate.yml`), durchgesetzt unabhaengig vom Willen der Entwickler:in | Automatisch bei jedem PR auf `workflow/**` — blockierend, kein Review-Ermessen | +| Vertragstest (`moduleadapter`) | Modul-Owner Workflow gemeinsam mit DMS-/Mail-Owner | Bei jeder Aenderung an der ENG-02-Schnittstelle | +| E2E / UI | QA (zweite Person) nach jedem UI-Ticket, danach als Teil der Review-Checkliste | Vor Abnahme eines UI-Tickets | +| Teststrategie-Dokument selbst | Zweite Person (Gegenlesen), siehe Abschnitt 7 | Einmalig vor Abnahme dieses Tickets, danach bei strukturellen Aenderungen | + +## 7. Pruefungen (real durchgefuehrt) + +| # | Pruefung | Ergebnis | +|---|---|---| +| 1 | Negativtest des Gates: Diff mit sicherheitsrelevanter Aenderung ohne Test schlaegt kontrolliert fehl | **bestanden** — `go test ./internal/pflichttestgate/... -v` auf 204, alle vier Faelle (fehlender Test je der sieben Pakete, begleiteter Test, nicht-sensibler Pfad, alle sieben Pakete gleichzeitig) grün | +| 2 | Testdatenbestand enthaelt keine echten Personendaten | **bestanden** — `TestFixtures_ContainNoEmailLikePII` automatisiert (kein `@` in den Fixture-Dateien) plus manuelle Durchsicht aller vier JSON-Dateien: ausschliesslich Slugs, erfundene Aktenzeichen, keine realen Namen/Domains | +| 3 | Zweite Person kann Teststrategie ohne Rueckfragen anwenden | **offen** — siehe Abschnitt 8; in dieser Session keine zweite Person verfuegbar | + +Zusaetzlich real ausgefuehrt (Grundlage fuer Abschnitt 3/5): `go build ./...`, +`go vet ./...` und `go test ./... -p 1 -count=1` fuer das gesamte Workflow-Modul auf +Host 204 gegen `dms_dev`-Testinfrastruktur, inklusive +`internal/fixtureset/fixtureset_test.go` (saet alle vier Fixtures real in eine +frische Testdatenbank und prueft zusaetzlich, dass die Vier-Augen-Regel eine +Selbstbestaetigung ablehnt und die Vertretungsregel korrekt auflöst). + +## 8. Offene Punkte + +- Pruefung 3 (zweite Person liest gegen) ist noch nicht durchgefuehrt — erfordert + eine echte zweite Person, nicht durch eine weitere Session ersetzbar. +- Ein modulübergreifender Vertragstest mit echten DMS-/Mail-Konsumenten der + `moduleadapter`-Schnittstelle (analog `archive-contract-tests.yml`/ + `contract-tests.yml`) ist noch nicht eingerichtet; aktuell deckt + `moduleadapter_test.go` die Schnittstelle nur aus Workflow-Sicht ab. +- Es gibt noch kein eigenes `workflow-ci.yml` fuer Build/Vet/Test/Coverage des + gesamten Moduls bei jedem Push (wie `ci.yml` es fuer Core/DMS/Connect tut) — + nur das in diesem Ticket ergaenzte Pflichttest-Gate. Ausserhalb des Umfangs + dieses Tickets (QA-01 ist Teststrategie + Merge-Gate fuer sicherheitsrelevanten + Code, nicht allgemeines CI-Setup), aber als Folgeticket sinnvoll. diff --git a/workflow/internal/fixtureset/fixtureset.go b/workflow/internal/fixtureset/fixtureset.go new file mode 100644 index 00000000..0294b164 --- /dev/null +++ b/workflow/internal/fixtureset/fixtureset.go @@ -0,0 +1,239 @@ +// Package fixtureset laedt den synthetischen Testdatenbestand aus +// testdata/fixtures/*.json (QA-01) und saet ihn ueber die bestehenden +// Domaenen-Services (nicht per Roh-SQL), damit die Fixtures denselben +// Validierungen unterliegen wie echte Daten. +package fixtureset + +import ( + "context" + "encoding/json" + "fmt" + "os" + "time" + + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/approval" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/delegation" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/dualcheck" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/processdef" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingrule" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/statemachine" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/task" + + "github.com/jackc/pgx/v5/pgxpool" +) + +type SeedInstance struct { + ProcessName string `json:"process_name"` + Actor string `json:"actor"` + Transitions []string `json:"transitions"` +} + +type EngineFixture struct { + Definitions []processdef.Definition `json:"definitions"` + SeedInstances []SeedInstance `json:"seed_instances"` +} + +func LoadEngineFixture(path string) (EngineFixture, error) { + var fx EngineFixture + if err := loadJSON(path, &fx); err != nil { + return EngineFixture{}, err + } + return fx, nil +} + +// SeedEngine laedt alle Definitionen ueber processdef.Registry und startet +// die in seed_instances beschriebenen Instanzen inklusive ihrer Uebergaenge, +// damit echte workflow_instances/-history-Zeilen entstehen statt nur +// Definitionen ohne Lauf. +func SeedEngine(ctx context.Context, pool *pgxpool.Pool, fx EngineFixture) (*statemachine.Engine, []statemachine.Instance, error) { + reg := processdef.NewRegistry(pool) + for _, def := range fx.Definitions { + raw, err := json.Marshal(def) + if err != nil { + return nil, nil, fmt.Errorf("fixtureset: definition %q serialisieren: %w", def.Name, err) + } + if _, _, err := reg.Load(ctx, raw); err != nil { + return nil, nil, fmt.Errorf("fixtureset: definition %q laden: %w", def.Name, err) + } + } + + engine, err := reg.BuildEngine(ctx) + if err != nil { + return nil, nil, fmt.Errorf("fixtureset: engine bauen: %w", err) + } + + var instances []statemachine.Instance + for _, seed := range fx.SeedInstances { + inst, err := reg.Start(ctx, engine, seed.ProcessName, seed.Actor) + if err != nil { + return nil, nil, fmt.Errorf("fixtureset: instanz fuer %q starten: %w", seed.ProcessName, err) + } + for _, to := range seed.Transitions { + if err := engine.Transition(ctx, inst.ID, to, seed.Actor, "fixtureset: synthetischer testablauf"); err != nil { + return nil, nil, fmt.Errorf("fixtureset: uebergang %q->%q fuer instanz %s: %w", inst.State, to, inst.ID, err) + } + inst, err = engine.Get(ctx, inst.ID) + if err != nil { + return nil, nil, fmt.Errorf("fixtureset: instanz %s nachladen: %w", inst.ID, err) + } + } + instances = append(instances, inst) + } + return engine, instances, nil +} + +type ApprovalTaskFixture struct { + Assignee string `json:"assignee"` + Subject string `json:"subject"` + RequestedBy string `json:"requested_by"` +} + +type DelegationRuleFixture struct { + UserSlug string `json:"user_slug"` + SubstituteSlug string `json:"substitute_slug"` + FromAt time.Time `json:"from_at"` + UntilAt time.Time `json:"until_at"` +} + +type ApprovalsFixture struct { + ApprovalTasks []ApprovalTaskFixture `json:"approval_tasks"` + DualcheckProcessTypes []string `json:"dualcheck_process_types"` + DelegationRules []DelegationRuleFixture `json:"delegation_rules"` +} + +func LoadApprovalsFixture(path string) (ApprovalsFixture, error) { + var fx ApprovalsFixture + if err := loadJSON(path, &fx); err != nil { + return ApprovalsFixture{}, err + } + return fx, nil +} + +// SeedApprovals aktiviert die Vier-Augen-Regel (dualcheck) und die +// Vertretungsregeln (delegation) fuer die Fixture, bevor es die +// Genehmigungsaufgaben ueber approval.Service.Create anlegt — dieselbe +// Reihenfolge wie im echten Betrieb, damit die Fixture die +// Zugriffspruefung tatsaechlich durchlaeuft statt sie zu umgehen. +func SeedApprovals(ctx context.Context, pool *pgxpool.Pool, fx ApprovalsFixture) ([]approval.Task, error) { + rules := dualcheck.NewRules(pool) + for _, pt := range fx.DualcheckProcessTypes { + if err := rules.Enable(ctx, pt); err != nil { + return nil, fmt.Errorf("fixtureset: dualcheck fuer %q aktivieren: %w", pt, err) + } + } + + delStore := delegation.NewStore(pool) + for _, d := range fx.DelegationRules { + if _, err := delStore.CreateRule(ctx, d.UserSlug, d.SubstituteSlug, d.FromAt, d.UntilAt); err != nil { + return nil, fmt.Errorf("fixtureset: vertretungsregel %s->%s anlegen: %w", d.UserSlug, d.SubstituteSlug, err) + } + } + + engine, err := approval.NewEngineWithApproval(pool) + if err != nil { + return nil, fmt.Errorf("fixtureset: genehmigungs-engine bauen: %w", err) + } + svc := approval.NewService(pool, engine) + + var out []approval.Task + for _, a := range fx.ApprovalTasks { + t, err := svc.Create(ctx, a.Assignee, a.Subject, a.RequestedBy) + if err != nil { + return nil, fmt.Errorf("fixtureset: genehmigungsaufgabe fuer %q anlegen: %w", a.Subject, err) + } + out = append(out, t) + } + return out, nil +} + +type TaskReferenceFixture struct { + ModuleName string `json:"module_name"` + ObjectType string `json:"object_type"` + ObjectID string `json:"object_id"` +} + +type TaskFixture struct { + Title string `json:"title"` + AssigneeType string `json:"assignee_type"` + AssigneeID string `json:"assignee_id"` + Priority string `json:"priority"` + References []TaskReferenceFixture `json:"references"` +} + +type TasksFixture struct { + Tasks []TaskFixture `json:"tasks"` +} + +func LoadTasksFixture(path string) (TasksFixture, error) { + var fx TasksFixture + if err := loadJSON(path, &fx); err != nil { + return TasksFixture{}, err + } + return fx, nil +} + +func SeedTasks(ctx context.Context, pool *pgxpool.Pool, fx TasksFixture) ([]string, error) { + store := task.NewStore(pool) + var ids []string + for _, tf := range fx.Tasks { + refs := make([]task.Reference, 0, len(tf.References)) + for _, r := range tf.References { + refs = append(refs, task.Reference{ModuleName: r.ModuleName, ObjectType: r.ObjectType, ObjectID: r.ObjectID}) + } + id, err := store.Create(ctx, task.Task{ + Title: tf.Title, + AssigneeType: task.AssigneeType(tf.AssigneeType), + AssigneeID: tf.AssigneeID, + Priority: task.Priority(tf.Priority), + References: refs, + }) + if err != nil { + return nil, fmt.Errorf("fixtureset: aufgabe %q anlegen: %w", tf.Title, err) + } + ids = append(ids, id) + } + return ids, nil +} + +type RoutingRuleFixture struct { + Name string `json:"name"` + Priority int `json:"priority"` + Conditions []routingrule.Condition `json:"conditions"` + Actions []routingrule.Action `json:"actions"` +} + +type RoutingFixture struct { + RoutingRules []RoutingRuleFixture `json:"routing_rules"` +} + +func LoadRoutingFixture(path string) (RoutingFixture, error) { + var fx RoutingFixture + if err := loadJSON(path, &fx); err != nil { + return RoutingFixture{}, err + } + return fx, nil +} + +func SeedRouting(ctx context.Context, pool *pgxpool.Pool, fx RoutingFixture) ([]routingrule.Rule, error) { + store := routingrule.NewStore(pool) + var out []routingrule.Rule + for _, r := range fx.RoutingRules { + rule, err := store.Create(ctx, r.Name, r.Priority, r.Conditions, r.Actions) + if err != nil { + return nil, fmt.Errorf("fixtureset: routing-regel %q anlegen: %w", r.Name, err) + } + out = append(out, rule) + } + return out, nil +} + +func loadJSON(path string, dest any) error { + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("fixtureset: %s lesen: %w", path, err) + } + if err := json.Unmarshal(data, dest); err != nil { + return fmt.Errorf("fixtureset: %s parsen: %w", path, err) + } + return nil +} diff --git a/workflow/internal/fixtureset/fixtureset_test.go b/workflow/internal/fixtureset/fixtureset_test.go new file mode 100644 index 00000000..aacaa804 --- /dev/null +++ b/workflow/internal/fixtureset/fixtureset_test.go @@ -0,0 +1,173 @@ +package fixtureset + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/delegation" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/dualcheck" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/migrate" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/testdb" +) + +func findModuleRoot(t *testing.T) string { + t.Helper() + dir, err := os.Getwd() + if err != nil { + t.Fatalf("getwd: %v", err) + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + t.Fatal("go.mod nicht gefunden") + } + dir = parent + } +} + +func fixturesDir(t *testing.T) string { + t.Helper() + return filepath.Join(findModuleRoot(t), "testdata", "fixtures") +} + +// QA-01 Pruefung 2: der Testdatenbestand darf keine echten Personendaten +// enthalten. Als harte, automatisierte Untergrenze gilt: kein "@" in den +// Rohdaten der Fixture-Dateien — Personen/Gruppen sind durchgaengig als +// Slugs (z. B. "anwalt.weber") statt als E-Mail-Adresse oder Klarname +// hinterlegt, echte Adressen koennen so nicht versehentlich hineinrutschen. +func TestFixtures_ContainNoEmailLikePII(t *testing.T) { + dir := fixturesDir(t) + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("fixtures-verzeichnis lesen: %v", err) + } + if len(entries) == 0 { + t.Fatal("keine fixture-dateien gefunden") + } + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { + continue + } + data, err := os.ReadFile(filepath.Join(dir, e.Name())) + if err != nil { + t.Fatalf("%s lesen: %v", e.Name(), err) + } + if strings.Contains(string(data), "@") { + t.Errorf("%s enthaelt \"@\" — Verdacht auf E-Mail-Adresse/echte Personendaten", e.Name()) + } + var probe any + if err := json.Unmarshal(data, &probe); err != nil { + t.Errorf("%s ist kein gueltiges JSON: %v", e.Name(), err) + } + } +} + +func TestLoadEngineFixture_ParsesDefinitionsAndSeedInstances(t *testing.T) { + fx, err := LoadEngineFixture(filepath.Join(fixturesDir(t), "engine.json")) + if err != nil { + t.Fatalf("laden: %v", err) + } + if len(fx.Definitions) == 0 { + t.Fatal("keine definitionen in engine.json") + } + if len(fx.SeedInstances) == 0 { + t.Fatal("keine seed_instances in engine.json") + } +} + +// Integrationstest (QA-01 Abschnitt 3, Integrations-Ebene): alle vier +// Fixture-Dateien real ueber die Domaenen-Services in eine frische +// Testdatenbank saeen und das Ergebnis pruefen. +func TestSeedAll_RealDatabaseRoundtrip(t *testing.T) { + dsn := os.Getenv("TEST_TENANT_DSN") + if dsn == "" { + t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen") + } + ctx := context.Background() + pool := testdb.Pool(t, dsn) + + migrations, err := migrate.Load(filepath.Join(findModuleRoot(t), "migrations", "tenant")) + if err != nil { + t.Fatalf("migrationen laden: %v", err) + } + if _, err := migrate.Up(ctx, pool, migrations); err != nil { + t.Fatalf("migrationen anwenden: %v", err) + } + + dir := fixturesDir(t) + + engineFx, err := LoadEngineFixture(filepath.Join(dir, "engine.json")) + if err != nil { + t.Fatalf("engine-fixture laden: %v", err) + } + _, instances, err := SeedEngine(ctx, pool, engineFx) + if err != nil { + t.Fatalf("engine saeen: %v", err) + } + if len(instances) != len(engineFx.SeedInstances) { + t.Fatalf("erwartet %d instanzen, habe %d", len(engineFx.SeedInstances), len(instances)) + } + + approvalsFx, err := LoadApprovalsFixture(filepath.Join(dir, "approvals.json")) + if err != nil { + t.Fatalf("approvals-fixture laden: %v", err) + } + approvalTasks, err := SeedApprovals(ctx, pool, approvalsFx) + if err != nil { + t.Fatalf("approvals saeen: %v", err) + } + if len(approvalTasks) != len(approvalsFx.ApprovalTasks) { + t.Fatalf("erwartet %d genehmigungsaufgaben, habe %d", len(approvalsFx.ApprovalTasks), len(approvalTasks)) + } + + tasksFx, err := LoadTasksFixture(filepath.Join(dir, "tasks.json")) + if err != nil { + t.Fatalf("tasks-fixture laden: %v", err) + } + taskIDs, err := SeedTasks(ctx, pool, tasksFx) + if err != nil { + t.Fatalf("tasks saeen: %v", err) + } + if len(taskIDs) != len(tasksFx.Tasks) { + t.Fatalf("erwartet %d aufgaben, habe %d", len(tasksFx.Tasks), len(taskIDs)) + } + + routingFx, err := LoadRoutingFixture(filepath.Join(dir, "routing.json")) + if err != nil { + t.Fatalf("routing-fixture laden: %v", err) + } + rules, err := SeedRouting(ctx, pool, routingFx) + if err != nil { + t.Fatalf("routing saeen: %v", err) + } + if len(rules) != len(routingFx.RoutingRules) { + t.Fatalf("erwartet %d routing-regeln, habe %d", len(routingFx.RoutingRules), len(rules)) + } + + // Sicherheitsrelevante Stichprobe auf den gesaeten Daten: die + // Vier-Augen-Regel (dualcheck) aus approvals.json lehnt eine + // Selbstbestaetigung ab, und die Vertretungsregel (delegation) loest + // innerhalb ihres Zeitraums tatsaechlich auf die hinterlegte Person auf. + dcRules := dualcheck.NewRules(pool) + dcSvc := dualcheck.NewService(pool, dcRules) + if _, err := dcSvc.Confirm(ctx, "akte_loeschung", "fixture-subjekt-1", "anwalt.weber", "anwalt.weber"); !errors.Is(err, dualcheck.ErrSameAsCreator) { + t.Fatalf("erwartet ErrSameAsCreator, habe: %v", err) + } + + delStore := delegation.NewStore(pool) + substitute, ok, err := delStore.ActiveSubstituteFor(ctx, "anwalt.weber", approvalsFx.DelegationRules[0].FromAt) + if err != nil { + t.Fatalf("vertretung ermitteln: %v", err) + } + if !ok || substitute != "anwalt.santos" { + t.Fatalf("erwartet vertretung anwalt.santos, habe %q (ok=%v)", substitute, ok) + } +} diff --git a/workflow/internal/pflichttestgate/gate.go b/workflow/internal/pflichttestgate/gate.go new file mode 100644 index 00000000..f6173167 --- /dev/null +++ b/workflow/internal/pflichttestgate/gate.go @@ -0,0 +1,74 @@ +// Package pflichttestgate erzwingt die in workflow/docs/TESTSTRATEGIE.md +// Abschnitt 4 festgelegte Regel: jede geaenderte Go-Datei in einem +// sicherheitskritischen Bereich (Auth, Tenant-Scoping, Zugriffspruefung) +// muss von einer geaenderten oder neuen _test.go-Datei im selben Package +// begleitet sein. Bewusste Code-Kopie des Musters aus Core +// internal/pflichttestgate bzw. Mail mail/internal/pflichttestgate — jedes +// Modul ist ein eigenstaendiges Go-Modul und kann die anderen nicht +// importieren. +package pflichttestgate + +import ( + "path" + "regexp" + "strings" +) + +// sensitivePathPatterns beschreibt die Bereiche aus +// workflow/docs/TESTSTRATEGIE.md Abschnitt 4. +var sensitivePathPatterns = []*regexp.Regexp{ + regexp.MustCompile(`(^|/)workflow/internal/svcauth/`), + regexp.MustCompile(`(^|/)workflow/internal/moduleadapter/`), + regexp.MustCompile(`(^|/)workflow/internal/tenantregistryclient/`), + regexp.MustCompile(`(^|/)workflow/internal/backchannel/`), + regexp.MustCompile(`(^|/)workflow/internal/backchannelapi/`), + regexp.MustCompile(`(^|/)workflow/internal/dualcheck/`), + regexp.MustCompile(`(^|/)workflow/internal/delegation/`), +} + +// Violation beschreibt ein Package mit sicherheitskritischer Aenderung +// ohne begleitende Testdatei. +type Violation struct { + Package string + ChangedFile string +} + +func isSensitive(file string) bool { + if !strings.HasSuffix(file, ".go") || strings.HasSuffix(file, "_test.go") { + return false + } + for _, re := range sensitivePathPatterns { + if re.MatchString(file) { + return true + } + } + return false +} + +// CheckDiff prueft eine Liste geaenderter Dateipfade gegen die +// Pflichttest-Regel — ein leeres Ergebnis bedeutet: Gate besteht. +func CheckDiff(changedFiles []string) []Violation { + sensitiveByPkg := map[string]string{} + testTouchedPkgs := map[string]bool{} + + for _, f := range changedFiles { + pkg := path.Dir(f) + if strings.HasSuffix(f, "_test.go") { + testTouchedPkgs[pkg] = true + continue + } + if isSensitive(f) { + if _, seen := sensitiveByPkg[pkg]; !seen { + sensitiveByPkg[pkg] = f + } + } + } + + var violations []Violation + for pkg, file := range sensitiveByPkg { + if !testTouchedPkgs[pkg] { + violations = append(violations, Violation{Package: pkg, ChangedFile: file}) + } + } + return violations +} diff --git a/workflow/internal/pflichttestgate/gate_test.go b/workflow/internal/pflichttestgate/gate_test.go new file mode 100644 index 00000000..215a5a0a --- /dev/null +++ b/workflow/internal/pflichttestgate/gate_test.go @@ -0,0 +1,49 @@ +// Negativtest des Gates selbst (QA-01 Pruefung 1): ein Diff mit +// geaenderter workflow/internal/svcauth/svcauth.go ohne begleitende +// Testdatei muss als Verstoss erkannt werden. +package pflichttestgate + +import "testing" + +func TestCheckDiff_FlagsSensitiveChangeWithoutTest(t *testing.T) { + violations := CheckDiff([]string{"workflow/internal/svcauth/svcauth.go"}) + if len(violations) != 1 { + t.Fatalf("erwartet genau 1 verstoss, habe %d: %+v", len(violations), violations) + } + if violations[0].Package != "workflow/internal/svcauth" { + t.Fatalf("falsches package gemeldet: %+v", violations[0]) + } +} + +func TestCheckDiff_PassesWhenTestFileAccompanies(t *testing.T) { + violations := CheckDiff([]string{ + "workflow/internal/svcauth/svcauth.go", + "workflow/internal/svcauth/svcauth_test.go", + }) + if len(violations) != 0 { + t.Fatalf("erwartet keine verstoesse, habe: %+v", violations) + } +} + +func TestCheckDiff_IgnoresNonSensitivePaths(t *testing.T) { + violations := CheckDiff([]string{"workflow/internal/caseobj/caseobj.go"}) + if len(violations) != 0 { + t.Fatalf("erwartet keine verstoesse fuer nicht-sensiblen pfad, habe: %+v", violations) + } +} + +func TestCheckDiff_CoversAllSensitivePackages(t *testing.T) { + files := []string{ + "workflow/internal/svcauth/x.go", + "workflow/internal/moduleadapter/x.go", + "workflow/internal/tenantregistryclient/x.go", + "workflow/internal/backchannel/x.go", + "workflow/internal/backchannelapi/x.go", + "workflow/internal/dualcheck/x.go", + "workflow/internal/delegation/x.go", + } + violations := CheckDiff(files) + if len(violations) != len(files) { + t.Fatalf("erwartet %d verstoesse (je package einer), habe %d: %+v", len(files), len(violations), violations) + } +} diff --git a/workflow/testdata/fixtures/approvals.json b/workflow/testdata/fixtures/approvals.json new file mode 100644 index 00000000..2b9cf515 --- /dev/null +++ b/workflow/testdata/fixtures/approvals.json @@ -0,0 +1,28 @@ +{ + "approval_tasks": [ + { + "assignee": "anwalt.weber", + "subject": "dms:akte:AZ-2026-0142", + "requested_by": "ra-fachangestellte.kowalski" + }, + { + "assignee": "anwalt.santos", + "subject": "mail:posteingang:2026-10-01-0007", + "requested_by": "ra-fachangestellte.kowalski" + }, + { + "assignee": "anwalt.weber", + "subject": "dms:akte:AZ-2026-0198", + "requested_by": "ra-fachangestellte.yilmaz" + } + ], + "dualcheck_process_types": ["akte_loeschung", "fristverlaengerung_extern"], + "delegation_rules": [ + { + "user_slug": "anwalt.weber", + "substitute_slug": "anwalt.santos", + "from_at": "2026-11-02T00:00:00Z", + "until_at": "2026-11-13T23:59:59Z" + } + ] +} diff --git a/workflow/testdata/fixtures/engine.json b/workflow/testdata/fixtures/engine.json new file mode 100644 index 00000000..ae5801f8 --- /dev/null +++ b/workflow/testdata/fixtures/engine.json @@ -0,0 +1,38 @@ +{ + "definitions": [ + { + "name": "dokumentfreigabe", + "states": ["entwurf", "pruefung", "freigegeben", "abgelehnt"], + "initial": "entwurf", + "transitions": [ + {"from": "entwurf", "to": "pruefung"}, + {"from": "pruefung", "to": "freigegeben"}, + {"from": "pruefung", "to": "abgelehnt"}, + {"from": "abgelehnt", "to": "entwurf"} + ] + }, + { + "name": "fristsache", + "states": ["offen", "in_bearbeitung", "erledigt", "eskaliert"], + "initial": "offen", + "transitions": [ + {"from": "offen", "to": "in_bearbeitung"}, + {"from": "in_bearbeitung", "to": "erledigt"}, + {"from": "in_bearbeitung", "to": "eskaliert"}, + {"from": "eskaliert", "to": "in_bearbeitung"} + ] + } + ], + "seed_instances": [ + { + "process_name": "dokumentfreigabe", + "actor": "ra-fachangestellte.kowalski", + "transitions": ["pruefung", "freigegeben"] + }, + { + "process_name": "fristsache", + "actor": "anwalt.weber", + "transitions": ["in_bearbeitung"] + } + ] +} diff --git a/workflow/testdata/fixtures/routing.json b/workflow/testdata/fixtures/routing.json new file mode 100644 index 00000000..2483f13c --- /dev/null +++ b/workflow/testdata/fixtures/routing.json @@ -0,0 +1,34 @@ +{ + "routing_rules": [ + { + "name": "fristsachen-an-fachanwalt", + "priority": 100, + "conditions": [ + {"field": "tag", "operator": "equals", "value": "frist"} + ], + "actions": [ + {"type": "route_to_person", "target": "anwalt.weber"} + ] + }, + { + "name": "mahnbescheid-an-inkassoteam", + "priority": 50, + "conditions": [ + {"field": "object_type", "operator": "equals", "value": "mahnbescheid"} + ], + "actions": [ + {"type": "route_to_process", "target": "inkasso", "process_id": "fristsache"} + ] + }, + { + "name": "sonstige-post-an-scanstelle", + "priority": 1, + "conditions": [ + {"field": "object_type", "operator": "in", "value": ["posteingang", "fax"]} + ], + "actions": [ + {"type": "route_to_person", "target": "team-scanstelle"} + ] + } + ] +} diff --git a/workflow/testdata/fixtures/tasks.json b/workflow/testdata/fixtures/tasks.json new file mode 100644 index 00000000..2b961368 --- /dev/null +++ b/workflow/testdata/fixtures/tasks.json @@ -0,0 +1,31 @@ +{ + "tasks": [ + { + "title": "Fristpruefung Berufung AZ-2026-0142", + "assignee_type": "person", + "assignee_id": "anwalt.weber", + "priority": "high", + "references": [ + {"module_name": "dms", "object_type": "akte", "object_id": "AZ-2026-0142"} + ] + }, + { + "title": "Posteingang digitalisieren", + "assignee_type": "group", + "assignee_id": "team-scanstelle", + "priority": "normal", + "references": [ + {"module_name": "mail", "object_type": "posteingang", "object_id": "2026-10-01-0007"} + ] + }, + { + "title": "Mandantenrueckruf vereinbaren", + "assignee_type": "person", + "assignee_id": "ra-fachangestellte.kowalski", + "priority": "low", + "references": [ + {"module_name": "dms", "object_type": "akte", "object_id": "AZ-2026-0198"} + ] + } + ] +}