CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (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
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
203 lines
6.8 KiB
Go
203 lines
6.8 KiB
Go
package processdef
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/migrate"
|
|
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/testdb"
|
|
)
|
|
|
|
func moduleRoot(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 newRegistry(t *testing.T) *Registry {
|
|
t.Helper()
|
|
dsn := os.Getenv("TEST_TENANT_DSN")
|
|
if dsn == "" {
|
|
t.Skip("TEST_TENANT_DSN nicht gesetzt")
|
|
}
|
|
pool := testdb.Pool(t, dsn)
|
|
migrations, err := migrate.Load(filepath.Join(moduleRoot(t), "migrations", "tenant"))
|
|
if err != nil {
|
|
t.Fatalf("migrationen laden: %v", err)
|
|
}
|
|
if _, err := migrate.Up(context.Background(), pool, migrations); err != nil {
|
|
t.Fatalf("migrationen anwenden: %v", err)
|
|
}
|
|
return NewRegistry(pool)
|
|
}
|
|
|
|
const validDef = `{
|
|
"name": "freigabe",
|
|
"states": ["entwurf", "geprueft", "freigegeben"],
|
|
"initial": "entwurf",
|
|
"transitions": [
|
|
{"from": "entwurf", "to": "geprueft"},
|
|
{"from": "geprueft", "to": "freigegeben"}
|
|
]
|
|
}`
|
|
|
|
// Akzeptanzkriterium 1 / Pruefung 1: gueltige Definition wird geladen, eine
|
|
// strukturell fehlerhafte Definition wird beim Laden abgewiesen statt
|
|
// unvalidiert uebernommen zu werden.
|
|
func TestLoad_ValidatesSchema(t *testing.T) {
|
|
r := newRegistry(t)
|
|
ctx := context.Background()
|
|
|
|
if _, _, err := r.Load(ctx, []byte(validDef)); err != nil {
|
|
t.Fatalf("gueltige definition abgewiesen: %v", err)
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
json string
|
|
}{
|
|
{"kein name", `{"states":["a"],"initial":"a"}`},
|
|
{"startzustand unbekannt", `{"name":"x","states":["a","b"],"initial":"c"}`},
|
|
{"uebergang referenziert unbekannten zustand", `{"name":"x","states":["a","b"],"initial":"a","transitions":[{"from":"a","to":"c"}]}`},
|
|
{"kaputtes json", `{nicht valide`},
|
|
{"keine zustaende", `{"name":"x","states":[],"initial":""}`},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
if _, _, err := r.Load(ctx, []byte(c.json)); !errors.Is(err, ErrInvalidDefinition) {
|
|
t.Fatalf("erwartet ErrInvalidDefinition, bekam %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 3 / Pruefung 2: ein Versionswechsel waehrend eine
|
|
// Instanz laeuft, veraendert deren Verhalten nicht rueckwirkend — die
|
|
// Instanz bleibt an ihre Startversion gebunden, inklusive eines dort
|
|
// erlaubten Uebergangs, der in der neuen Version nicht mehr existiert.
|
|
func TestVersionSwitch_DoesNotAffectRunningInstance(t *testing.T) {
|
|
r := newRegistry(t)
|
|
ctx := context.Background()
|
|
|
|
v1 := `{"name":"vertrag","states":["entwurf","aktiv"],"initial":"entwurf","transitions":[{"from":"entwurf","to":"aktiv"}]}`
|
|
if _, version, err := r.Load(ctx, []byte(v1)); err != nil || version != 1 {
|
|
t.Fatalf("v1 laden: version=%d err=%v", version, err)
|
|
}
|
|
engine, err := r.BuildEngine(ctx)
|
|
if err != nil {
|
|
t.Fatalf("engine bauen: %v", err)
|
|
}
|
|
inst, err := r.Start(ctx, engine, "vertrag", "tester")
|
|
if err != nil {
|
|
t.Fatalf("instanz starten: %v", err)
|
|
}
|
|
|
|
// Version 2 entfernt den Uebergang entwurf->aktiv wieder (nur ein
|
|
// gesonderter Endzustand), ohne Codeaenderung, rein per neuer
|
|
// Definition.
|
|
v2 := `{"name":"vertrag","states":["entwurf","abgelehnt"],"initial":"entwurf","transitions":[{"from":"entwurf","to":"abgelehnt"}]}`
|
|
if _, version, err := r.Load(ctx, []byte(v2)); err != nil || version != 2 {
|
|
t.Fatalf("v2 laden: version=%d err=%v", version, err)
|
|
}
|
|
engine2, err := r.BuildEngine(ctx)
|
|
if err != nil {
|
|
t.Fatalf("engine nach v2 neu bauen: %v", err)
|
|
}
|
|
|
|
// Die bereits laufende v1-Instanz darf weiterhin ihren v1-Uebergang
|
|
// ausfuehren, obwohl v2 jetzt aktiv ist.
|
|
if err := engine2.Transition(ctx, inst.ID, "aktiv", "tester", "abschluss"); err != nil {
|
|
t.Fatalf("v1-instanz sollte weiterhin nach v1 uebergehen koennen: %v", err)
|
|
}
|
|
|
|
// Eine NEUE Instanz bekommt dagegen v2 und kennt den v1-Uebergang
|
|
// "aktiv" gar nicht mehr.
|
|
inst2, err := r.Start(ctx, engine2, "vertrag", "tester")
|
|
if err != nil {
|
|
t.Fatalf("neue instanz starten: %v", err)
|
|
}
|
|
if err := engine2.Transition(ctx, inst2.ID, "aktiv", "tester", "sollte fehlschlagen"); err == nil {
|
|
t.Fatal("neue v2-instanz sollte den v1-only-uebergang nicht kennen")
|
|
}
|
|
if err := engine2.Transition(ctx, inst2.ID, "abgelehnt", "tester", "v2-uebergang"); err != nil {
|
|
t.Fatalf("v2-instanz sollte ihren eigenen uebergang ausfuehren koennen: %v", err)
|
|
}
|
|
}
|
|
|
|
// Pruefung 3: Rollback auf die Vorgaengerversion funktioniert, neue
|
|
// Instanzen bekommen danach wieder die aeltere Version.
|
|
func TestRollbackTo_RestoresPreviousVersionForNewInstances(t *testing.T) {
|
|
r := newRegistry(t)
|
|
ctx := context.Background()
|
|
|
|
v1 := `{"name":"urlaub","states":["beantragt","genehmigt"],"initial":"beantragt","transitions":[{"from":"beantragt","to":"genehmigt"}]}`
|
|
if _, _, err := r.Load(ctx, []byte(v1)); err != nil {
|
|
t.Fatalf("v1 laden: %v", err)
|
|
}
|
|
v2 := `{"name":"urlaub","states":["beantragt","abgelehnt"],"initial":"beantragt","transitions":[{"from":"beantragt","to":"abgelehnt"}]}`
|
|
if _, _, err := r.Load(ctx, []byte(v2)); err != nil {
|
|
t.Fatalf("v2 laden: %v", err)
|
|
}
|
|
active, err := r.ActiveVersion(ctx, "urlaub")
|
|
if err != nil || active != 2 {
|
|
t.Fatalf("aktive version vor rollback: %d %v", active, err)
|
|
}
|
|
|
|
if err := r.RollbackTo(ctx, "urlaub", 1); err != nil {
|
|
t.Fatalf("rollback: %v", err)
|
|
}
|
|
active, err = r.ActiveVersion(ctx, "urlaub")
|
|
if err != nil || active != 1 {
|
|
t.Fatalf("aktive version nach rollback: %d %v", active, err)
|
|
}
|
|
|
|
engine, err := r.BuildEngine(ctx)
|
|
if err != nil {
|
|
t.Fatalf("engine bauen: %v", err)
|
|
}
|
|
inst, err := r.Start(ctx, engine, "urlaub", "tester")
|
|
if err != nil {
|
|
t.Fatalf("instanz nach rollback starten: %v", err)
|
|
}
|
|
if err := engine.Transition(ctx, inst.ID, "genehmigt", "tester", "v1-uebergang nach rollback"); err != nil {
|
|
t.Fatalf("nach rollback sollte v1-uebergang wieder verfuegbar sein: %v", err)
|
|
}
|
|
|
|
if err := r.RollbackTo(ctx, "urlaub", 99); !errors.Is(err, ErrUnknownVersion) {
|
|
t.Fatalf("rollback auf unbekannte version: erwartet ErrUnknownVersion, bekam %v", err)
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 1: mehrere Prozesse unabhaengig ladbar, Version zaehlt
|
|
// je Prozessname separat hoch.
|
|
func TestLoad_VersionsPerProcessNameIndependent(t *testing.T) {
|
|
r := newRegistry(t)
|
|
ctx := context.Background()
|
|
|
|
if _, v, err := r.Load(ctx, []byte(validDef)); err != nil || v != 1 {
|
|
t.Fatalf("erste version von freigabe: %d %v", v, err)
|
|
}
|
|
other := `{"name":"andererprozess","states":["a"],"initial":"a"}`
|
|
if _, v, err := r.Load(ctx, []byte(other)); err != nil || v != 1 {
|
|
t.Fatalf("erste version von andererprozess: %d %v", v, err)
|
|
}
|
|
if _, v, err := r.Load(ctx, []byte(validDef)); err != nil || v != 2 {
|
|
t.Fatalf("zweite version von freigabe: %d %v", v, err)
|
|
}
|
|
}
|