Merge PR #290: QA-07: Pruefgate Workflow & Benachrichtigung
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
This commit is contained in:
commit
49fbc27632
3 files changed
+425
No files matched your search
@@ -0,0 +1,235 @@
|
||||
# QA-07 — Prüfgate Workflow & Benachrichtigung
|
||||
|
||||
Datum: 2026-10-15
|
||||
Board: DMS-Kanban, Ticket QA-07
|
||||
Art: QA-Prüfgate (Prozesstests, Zeitraffer) — kein neues Feature.
|
||||
|
||||
## Geprüfter Code (main, zum Zeitpunkt der Prüfung)
|
||||
|
||||
- Workflow-Modul (`workflow/internal/...`):
|
||||
- `approval` — APR-01 Freigabe-Postfach/Genehmigungsansicht (Backend)
|
||||
- `delegation` — APR-04 Vertretungsmodell
|
||||
- `escalation` — TSK-03 Erinnerungen & Eskalationsregeln
|
||||
- `escalationnotify` — Anbindung TSK-03 an Core-Dispatcher (TSK-05)
|
||||
- `routingengine` / `routingrule` — RTE-02 Routing-Engine-Ausführung
|
||||
- `dualcheck` — Vier-Augen-Prinzip (Zweitbestätigung)
|
||||
- Core-Modul (`internal/...`):
|
||||
- `notify` — CFG-02 zentraler Dispatcher (`notification_jobs`)
|
||||
- `notifyprefs` — CFG-04 Benachrichtigungs-Einstellungen, `EnqueueIfAllowed`
|
||||
- `notifyapi` — CFG-05 HTTP-Wrapper für Fremdmodule (Workflow ruft darüber auf)
|
||||
|
||||
Alle genannten Pakete waren bei Prüfungsbeginn bereits vollständig implementiert
|
||||
und mit eigenen Tests abgedeckt (u.a. QA-01-Testmuster: `TEST_TENANT_DSN`,
|
||||
`TEST_ADMIN_DSN`, `testdb.Pool`, echte Migrationen, Skip ohne DSN). Diese
|
||||
Prüfung baut darauf auf und ergänzt zwei gezielte Lücken.
|
||||
|
||||
## Arbeitsumgebung
|
||||
|
||||
- Host 204, Wegwerf-Checkout `/root/qa-07-work-1792096871` (main, frisch geklont
|
||||
von `https://gitea.perlbach24.de/scripte/nexarch.git`).
|
||||
- Postgres-Testdatenbanken über `/etc/nexarch-dev/tenant-dev.env`
|
||||
(`TEST_TENANT_DSN`) und `/etc/nexarch-dev/core-dev.env` (`TEST_ADMIN_DSN`) —
|
||||
echte Postgres-Instanzen, kein Mocking der Datenbank.
|
||||
- Go 1.26.5.
|
||||
|
||||
## Prozesspfade — Nachweis (Akzeptanzkriterium 1)
|
||||
|
||||
| Pfad | Nachweis (Testfunktion, Paket) | Ergebnis |
|
||||
|---|---|---|
|
||||
| Ablehnung (Decide → rejected) | `TestDecide_ReasonIsRecorded`, `approval` | PASS |
|
||||
| Vertretung (Vertretung übernimmt Aufgabe) | `TestCreate_WithActiveDelegation_AssignsToSubstitute`, `approval` | PASS |
|
||||
| Vertretung endet automatisch | `TestActiveSubstituteFor_EndsAutomatically`, `delegation` | PASS |
|
||||
| Missbrauch: Selbst-Vertretung | `TestCreate_SelfSubstitutionAbuseIsRejectedAtRuleCreation`, `approval` | PASS |
|
||||
| Eskalation bei Fristablauf | `TestProcessDue_EscalatesPastDueTask`, `escalation` | PASS |
|
||||
| Erinnerung vor Fristablauf (Zeitraffer) | `TestProcessDue_ReminderFiresOnlyAtConfiguredOffset`, `escalation` | PASS |
|
||||
| Erledigte Aufgabe eskaliert nicht | `TestProcessDue_CompletedTaskNeverEscalates`, `escalation` | PASS |
|
||||
| Eskalation löst genau eine Benachrichtigung aus | `TestRunAt_DispatchesEscalationWithCorrectEventType`, `escalationnotify` | PASS |
|
||||
|
||||
Alle vier im Ticket genannten Pfade (Ablehnung, Vertretung, Eskalation,
|
||||
Fristablauf) sind damit mindestens einmal real gegen Postgres durchlaufen.
|
||||
AC1 erfüllt.
|
||||
|
||||
## Prüfung 1 — Zeitraffer-Test über simulierte Fristen
|
||||
|
||||
Ziel: Fristlogik ohne echtes Warten prüfbar, über austauschbare Zeitquelle.
|
||||
|
||||
Befund: `escalation.Store.ProcessDue(ctx, now time.Time)` nimmt den
|
||||
Referenzzeitpunkt bereits als Parameter entgegen (keine `time.Now()`-Aufrufe
|
||||
im Kernpfad) — austauschbare Zeitquelle ist strukturell bereits vorhanden.
|
||||
`escalationnotify.RunAt(..., now time.Time)` reicht denselben Zeitpunkt
|
||||
durch.
|
||||
|
||||
Zielwert: Erinnerung bei konfiguriertem Vorlauf löst aus, Eskalation bei
|
||||
Fälligkeit noch nicht — und umgekehrt, rein über simulierte `now`-Werte,
|
||||
ohne reale Wartezeit.
|
||||
|
||||
Test: `TestProcessDue_ReminderFiresOnlyAtConfiguredOffset` (`escalation`) —
|
||||
Fälligkeit real +10 Tage, simuliertes `now` = +8 Tage (Erinnerung bei
|
||||
Vorlauf 3 Tage fällig, Eskalation noch nicht).
|
||||
|
||||
Ergebnis: PASS — Erinnerung ausgelöst, Eskalation korrekt noch nicht
|
||||
ausgelöst.
|
||||
|
||||
```
|
||||
=== RUN TestProcessDue_ReminderFiresOnlyAtConfiguredOffset
|
||||
--- PASS: TestProcessDue_ReminderFiresOnlyAtConfiguredOffset
|
||||
```
|
||||
|
||||
Zusätzlich: `TestProcessDue_Idempotent` belegt, dass ein wiederholter
|
||||
Joblauf mit demselben `now` keine zweite Eskalation erzeugt (Zeitraffer +
|
||||
Idempotenz kombiniert).
|
||||
|
||||
## Prüfung 2 — Parallelentscheidungen erzeugen keinen Fehlzustand
|
||||
|
||||
Ziel: Zwei Vertreter entscheiden gleichzeitig über dieselbe Freigabe — kein
|
||||
inkonsistenter Endzustand, Race-Test gegen echte Postgres-Testdatenbank.
|
||||
|
||||
Bereits vorhanden: `TestDecide_ConcurrentDecisionsProduceNoInconsistentState`
|
||||
(`approval`) — zwei parallele `Decide`-Aufrufe desselben Zuständigen
|
||||
(`approved` vs. `rejected`), über `statemachine.Engine` serialisiert
|
||||
(`ErrConcurrentTransition` → `ErrAlreadyDecided`).
|
||||
|
||||
**Ergänzt (QA-07):** `TestDecide_ConcurrentSubstitutesOnDelegatedTask`
|
||||
(`workflow/internal/approval/approval_qa07_test.go`) — realisiert das
|
||||
Ticket-Szenario direkt mit Vertretung (APR-04): Aufgabe wird per
|
||||
Delegationsregel an `bob` umgeleitet, danach entscheiden drei Akteure
|
||||
parallel über dieselbe Aufgabe: `bob` zweimal (zwei Geräte/Tabs derselben
|
||||
Vertretung) und `mallory` (nicht eingetragene zweite Person) einmal.
|
||||
|
||||
Befund/Designgrenze: Das System kennt je Aufgabe genau einen Zuständigen
|
||||
(`assignee`); zwei *gleichzeitig gültige* Vertretungen für dieselbe Aufgabe
|
||||
sind durch das Datenmodell ausgeschlossen (`ActiveSubstituteFor` liefert
|
||||
deterministisch genau eine Vertretung, siehe
|
||||
`TestActiveSubstituteFor_OverlappingRulesResolveToMostRecentlyCreated`).
|
||||
Das real relevante Risiko ist daher die gleichzeitige Mehrfachentscheidung
|
||||
durch denselben (einzigen) Zuständigen bzw. ein unbefugter Parallelversuch
|
||||
— genau das deckt der ergänzte Test ab.
|
||||
|
||||
Zielwert: genau 1 Erfolg, genau 1 `ErrAlreadyDecided` (zweiter Versuch von
|
||||
`bob`), genau 1 `ErrForbidden` (`mallory`), Endstatus konsistent
|
||||
(`approved` oder `rejected`, nie beides/keins), `DecidedBy == "bob"`.
|
||||
|
||||
Ergebnis: PASS, real gegen Postgres (`TEST_TENANT_DSN`), 3 nebenläufige
|
||||
Goroutinen:
|
||||
|
||||
```
|
||||
=== RUN TestDecide_ConcurrentSubstitutesOnDelegatedTask
|
||||
--- PASS: TestDecide_ConcurrentSubstitutesOnDelegatedTask (0.36s)
|
||||
```
|
||||
|
||||
Kein Fund (keine Lücke): Die Engine serialisiert korrekt über
|
||||
`ErrConcurrentTransition`, die Vertretungszuordnung bleibt eindeutig.
|
||||
|
||||
## Prüfung 3 — Zustellprotokoll gegen erwartete Ereignisse abgeglichen
|
||||
|
||||
Ziel: jedes erwartete Ereignis erscheint genau einmal im Zustellprotokoll
|
||||
(`notification_jobs`, Core CFG-02), kein Duplikat, keine Lücke.
|
||||
|
||||
Bereits vorhanden (Mock-Dispatcher-Ebene):
|
||||
`TestRunAt_RepeatedRunDoesNotDoubleDispatch` (`escalationnotify`) — belegt,
|
||||
dass ein wiederholter Joblauf keinen zweiten Dispatcher-Aufruf erzeugt
|
||||
(gegen `httptest`-Fake, nicht gegen das echte Zustellprotokoll).
|
||||
|
||||
**Ergänzt (QA-07):**
|
||||
`TestE2E_DeliveryLogMatchesExpectedWorkflowEvents`
|
||||
(`internal/notifyapi/qa07_e2e_test.go`) — echter End-zu-Ende-Pfad: echter
|
||||
`notify.Dispatcher` + echter `notifyprefs.Store` + echter
|
||||
`notifyapi`-HTTP-Server (derselbe Pfad, den Workflow über
|
||||
`notifyclient`/CFG-05 nutzt), gegen echte Postgres-Testdatenbank
|
||||
(`TEST_ADMIN_DSN`). Simuliert 5 Ereignisse aus den vier Prozesspfaden
|
||||
(Ablehnung, Vertretung, Eskalation, Fristablauf-Erinnerung) parallel, davon
|
||||
eines mit deaktivierter Präferenz (CFG-04, `EnqueueIfAllowed`). Liest danach
|
||||
`notification_jobs` direkt aus der DB und gleicht jeden erwarteten Empfänger
|
||||
gegen genau 1 Protokolleintrag ab; der deaktivierte Kanal darf 0 Einträge
|
||||
erzeugen.
|
||||
|
||||
Zielwert: 4 aktivierte Ereignisse → je genau 1 Zeile in `notification_jobs`;
|
||||
1 deaktiviertes Ereignis → 0 Zeilen.
|
||||
|
||||
Ergebnis: PASS.
|
||||
|
||||
```
|
||||
=== RUN TestE2E_DeliveryLogMatchesExpectedWorkflowEvents
|
||||
--- PASS: TestE2E_DeliveryLogMatchesExpectedWorkflowEvents (0.19s)
|
||||
```
|
||||
|
||||
Kein Fund (keine Lücke): Dispatcher und Präferenzfilter erzeugen weder
|
||||
Duplikate noch Verluste; der Service-Token-geschützte HTTP-Pfad (CFG-05)
|
||||
verhält sich identisch zum direkten `EnqueueIfAllowed`-Aufruf.
|
||||
|
||||
## Testläufe (vollständig)
|
||||
|
||||
```
|
||||
$ cd workflow && go test ./...
|
||||
ok .../workflow-app
|
||||
ok .../internal/approval 9.732s
|
||||
ok .../internal/approvalapi 4.354s
|
||||
ok .../internal/backchannel 8.814s
|
||||
ok .../internal/backchannelapi 0.721s
|
||||
ok .../internal/calendar 3.386s
|
||||
ok .../internal/caseobj 6.576s
|
||||
ok .../internal/caseoverview 5.064s
|
||||
ok .../internal/caseoverviewapi 2.496s
|
||||
ok .../internal/delegation (cached)
|
||||
ok .../internal/dualcheck (cached)
|
||||
ok .../internal/escalation (cached)
|
||||
ok .../internal/escalationnotify (cached)
|
||||
ok .../internal/eventtrigger 4.141s
|
||||
ok .../internal/migrationworkflow 0.697s
|
||||
ok .../internal/moduleadapter 7.076s
|
||||
ok .../internal/notifyclient 0.008s
|
||||
ok .../internal/processdef 2.756s
|
||||
ok .../internal/reminder 3.533s
|
||||
ok .../internal/routingengine (cached)
|
||||
ok .../internal/routingrule 2.225s
|
||||
ok .../internal/routingruleapi 4.951s
|
||||
ok .../internal/routingsim 2.554s
|
||||
ok .../internal/routingsuggestclient 0.006s
|
||||
ok .../internal/statemachine 31.843s
|
||||
ok .../internal/stepretry 1.831s
|
||||
ok .../internal/task 2.217s
|
||||
|
||||
$ go test ./internal/notify/... ./internal/notifyprefs/... ./internal/notifyapi/... -v
|
||||
--- PASS: TestDispatcher_EnqueueAndProcess
|
||||
--- PASS: TestProcessDue_RetriesThenGivesUpAfterMaxAttempts
|
||||
--- PASS: TestQueue_SurvivesRestartWithoutMessageLoss
|
||||
--- PASS: TestProcessDue_ConcurrentDispatchBothDelivered
|
||||
--- PASS: TestHandlerSetAndListMine
|
||||
--- PASS: TestHandlerListMine_NeverShowsOtherUsersPreferences
|
||||
--- PASS: TestHandlerListTenantOverview
|
||||
--- PASS: TestIsEnabled_DefaultsToTrueWithoutPreference
|
||||
--- PASS: TestSetAndIsEnabled
|
||||
--- PASS: TestSet_DoesNotAffectOtherUser
|
||||
--- PASS: TestEnqueueIfAllowed_SkipsWhenDisabled
|
||||
--- PASS: TestEnqueueIfAllowed_EnqueuesWhenEnabled
|
||||
--- PASS: TestListForTenant_ShowsAllUsers
|
||||
--- PASS: TestEnqueueHandler_MissingTokenReturns401
|
||||
--- PASS: TestEnqueueHandler_MatchesDirectEnqueueIfAllowed
|
||||
--- PASS: TestEnqueueHandler_RealForeignModuleClient
|
||||
--- PASS: TestE2E_DeliveryLogMatchesExpectedWorkflowEvents
|
||||
```
|
||||
|
||||
`go build ./...` und `go vet ./...` beider Module fehlerfrei.
|
||||
|
||||
`go test -race` konnte in dieser nicht-interaktiven SSH-Sitzung nicht
|
||||
ausgeführt werden (`gcc`/cgo nicht im PATH des nicht-interaktiven Shells
|
||||
verfügbar, laut Arbeitsumgebungsvorgabe keine Toolchain-Installationen auf
|
||||
Host 204). Die Nebenläufigkeitsprüfungen (Prüfung 2, Prüfung 3) laufen
|
||||
stattdessen mit echten nebenläufigen Goroutinen gegen die echte
|
||||
Postgres-Testdatenbank — der eigentlich geforderte Nachweis (DB-Race, nicht
|
||||
Go-Memory-Race) ist damit erbracht.
|
||||
|
||||
## Ergebnis
|
||||
|
||||
Alle drei geforderten Prüfungen bestanden. Alle vier Prozesspfade
|
||||
durchlaufen. Keine Produktionslücke gefunden, die eine Korrektur
|
||||
erforderte — Dispatcher, Präferenzfilter, Eskalations-Engine und
|
||||
Zustand-Serialisierung verhalten sich bereits korrekt. Ergänzt wurden zwei
|
||||
gezielte End-zu-Ende-/Race-Tests, die die bestehende Unit-Testdecke um die
|
||||
im Ticket explizit geforderten Szenarien (Vertretungsrace, echtes
|
||||
Zustellprotokoll statt Mock) erweitern:
|
||||
|
||||
- `workflow/internal/approval/approval_qa07_test.go`
|
||||
- `internal/notifyapi/qa07_e2e_test.go`
|
||||
|
||||
Akzeptanzkriterien 1–3 sind damit nachgewiesen erfüllt.
|
||||
@@ -0,0 +1,95 @@
|
||||
package notifyapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// QA-07 Pruefung 3: Zustellprotokoll (notification_jobs, Core CFG-02) wird
|
||||
// gegen die aus den simulierten Workflow-Prozesspfaden (Ablehnung, Eskalation,
|
||||
// Fristablauf) erwarteten Ereignisse abgeglichen -- jedes erwartete Ereignis
|
||||
// erscheint genau einmal, kein Duplikat, keine Luecke. Treibt denselben
|
||||
// HTTP-Pfad an, den workflow/internal/escalationnotify.DispatchDue und
|
||||
// Aehnliches live gegen Core CFG-05 nutzen.
|
||||
func TestE2E_DeliveryLogMatchesExpectedWorkflowEvents(t *testing.T) {
|
||||
prefs, dispatcher, pool := setupTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
Mount(mux, prefs, dispatcher, testToken)
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
tenant := "acme"
|
||||
if err := prefs.Set(ctx, tenant, "cfg05_qa07_muted", "workflow.task.escalated", "email", false); err != nil {
|
||||
t.Fatalf("praeferenz deaktivieren: %v", err)
|
||||
}
|
||||
|
||||
type expectedEvent struct {
|
||||
userID string
|
||||
eventType string
|
||||
recipient string
|
||||
}
|
||||
events := []expectedEvent{
|
||||
{userID: "cfg05_qa07_alice", eventType: "workflow.approval.rejected", recipient: "cfg05_qa07_alice@acme.example"},
|
||||
{userID: "cfg05_qa07_bob", eventType: "workflow.approval.delegated", recipient: "cfg05_qa07_bob@acme.example"},
|
||||
{userID: "cfg05_qa07_carol", eventType: "workflow.task.escalated", recipient: "cfg05_qa07_carol@acme.example"},
|
||||
{userID: "cfg05_qa07_dave", eventType: "workflow.task.reminder_due", recipient: "cfg05_qa07_dave@acme.example"},
|
||||
{userID: "cfg05_qa07_muted", eventType: "workflow.task.escalated", recipient: "cfg05_qa07_muted@acme.example"},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, ev := range events {
|
||||
wg.Add(1)
|
||||
go func(ev expectedEvent) {
|
||||
defer wg.Done()
|
||||
resp := post(t, server, testToken, enqueueRequest{
|
||||
TenantSlug: tenant, UserID: ev.userID, EventType: ev.eventType,
|
||||
Channel: "email", Recipient: ev.recipient,
|
||||
Payload: map[string]any{"task_id": "qa07-" + ev.userID},
|
||||
})
|
||||
_ = resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("enqueue fuer %s/%s: status = %d", ev.userID, ev.eventType, resp.StatusCode)
|
||||
}
|
||||
}(ev)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
rows, err := pool.Query(ctx, `
|
||||
SELECT recipient, payload->>'task_id'
|
||||
FROM notification_jobs
|
||||
WHERE recipient LIKE 'cfg05\_qa07\_%' ESCAPE '\'
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("zustellprotokoll lesen: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
seen := map[string]int{}
|
||||
for rows.Next() {
|
||||
var recipient, taskID string
|
||||
if err := rows.Scan(&recipient, &taskID); err != nil {
|
||||
t.Fatalf("zeile lesen: %v", err)
|
||||
}
|
||||
seen[recipient]++
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
t.Fatalf("iterieren: %v", err)
|
||||
}
|
||||
|
||||
for _, ev := range events {
|
||||
if ev.userID == "cfg05_qa07_muted" {
|
||||
continue
|
||||
}
|
||||
if seen[ev.recipient] != 1 {
|
||||
t.Fatalf("empfaenger %s: %d eintraege im zustellprotokoll, want genau 1 (kein duplikat, keine luecke)", ev.recipient, seen[ev.recipient])
|
||||
}
|
||||
}
|
||||
if n := seen["cfg05_qa07_muted@acme.example"]; n != 0 {
|
||||
t.Fatalf("deaktivierter kanal hat trotzdem %d eintraege im zustellprotokoll erzeugt, want 0", n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/delegation"
|
||||
)
|
||||
|
||||
// QA-07 Pruefung 2: Parallelentscheidung zweier Vertreter ueber dieselbe
|
||||
// Freigabe erzeugt keinen Fehlzustand. Die Aufgabe wird per Vertretung
|
||||
// (APR-04) an bob umgeleitet; bob entscheidet zweimal parallel (z.B. zwei
|
||||
// Tabs/Geraete derselben Vertretung), waehrend ein zweiter, nicht
|
||||
// eingetragener Vertreter (mallory) parallel ebenfalls zu entscheiden
|
||||
// versucht. Erwartung: genau eine Entscheidung gewinnt, alle anderen
|
||||
// bekommen einen definierten Fehler, der Endzustand ist konsistent.
|
||||
func TestDecide_ConcurrentSubstitutesOnDelegatedTask(t *testing.T) {
|
||||
svc := newTestService(t)
|
||||
delegationStore := delegation.NewStore(svc.pool)
|
||||
svc.WithDelegation(delegationStore)
|
||||
ctx := context.Background()
|
||||
now := time.Now().UTC()
|
||||
|
||||
if _, err := delegationStore.CreateRule(ctx, "alice", "bob", now.Add(-time.Hour), now.Add(time.Hour)); err != nil {
|
||||
t.Fatalf("createrule: %v", err)
|
||||
}
|
||||
|
||||
task, err := svc.Create(ctx, "alice", "dms:document:qa07", "system")
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
if task.Assignee != "bob" {
|
||||
t.Fatalf("erwartet zuweisung an bob, erhalten %q", task.Assignee)
|
||||
}
|
||||
|
||||
type attempt struct {
|
||||
actor string
|
||||
decision Decision
|
||||
}
|
||||
attempts := []attempt{
|
||||
{actor: "bob", decision: Approved},
|
||||
{actor: "bob", decision: Rejected},
|
||||
{actor: "mallory", decision: Approved},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make(chan error, len(attempts))
|
||||
for _, a := range attempts {
|
||||
wg.Add(1)
|
||||
go func(a attempt) {
|
||||
defer wg.Done()
|
||||
_, err := svc.Decide(ctx, task.ID, a.actor, a.decision, "parallele vertretung")
|
||||
results <- err
|
||||
}(a)
|
||||
}
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
var successes, alreadyDecided, forbidden int
|
||||
for err := range results {
|
||||
switch {
|
||||
case err == nil:
|
||||
successes++
|
||||
case errors.Is(err, ErrAlreadyDecided):
|
||||
alreadyDecided++
|
||||
case errors.Is(err, ErrForbidden):
|
||||
forbidden++
|
||||
default:
|
||||
t.Fatalf("unerwarteter fehler: %v", err)
|
||||
}
|
||||
}
|
||||
if successes != 1 {
|
||||
t.Fatalf("successes=%d, want genau 1", successes)
|
||||
}
|
||||
if alreadyDecided != 1 {
|
||||
t.Fatalf("alreadyDecided=%d, want genau 1 (bobs zweiter versuch)", alreadyDecided)
|
||||
}
|
||||
if forbidden != 1 {
|
||||
t.Fatalf("forbidden=%d, want genau 1 (mallory ist nicht die eingetragene vertretung)", forbidden)
|
||||
}
|
||||
|
||||
got, err := svc.Get(ctx, task.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if got.Status != "approved" && got.Status != "rejected" {
|
||||
t.Fatalf("status = %q, want approved oder rejected, kein inkonsistenter zustand", got.Status)
|
||||
}
|
||||
if got.DecidedBy != "bob" {
|
||||
t.Fatalf("decidedBy = %q, want bob (einzig zustaendige vertretung)", got.DecidedBy)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user