package caseobj import ( "context" "errors" "os" "path/filepath" "testing" "gitea.perlbach24.de/scripte/nexarch/common/testdb" "gitea.perlbach24.de/scripte/nexarch/workflow/internal/migrate" ) 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 newTestService(t *testing.T) *Service { t.Helper() 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) } eng, err := NewEngineWithCase(pool) if err != nil { t.Fatalf("engine anlegen: %v", err) } return NewService(pool, eng) } // AK1 / Pruefung 1: Akte referenziert Objekte aus mehreren Modulen // gleichzeitig, ohne deren Rohdaten zu duplizieren -- nur Modul, Typ // und ID werden gespeichert. func TestCreate_ReferencesMultipleModulesSimultaneously(t *testing.T) { svc := newTestService(t) ctx := context.Background() refs := []Reference{ {ModuleName: "dms", ObjectType: "document", ObjectID: "doc-123"}, {ModuleName: "mail", ObjectType: "message", ObjectID: "msg-456"}, } c, err := svc.Create(ctx, "", "verantwortliche.person", "tester", refs) if err != nil { t.Fatalf("create: %v", err) } if len(c.References) != 2 { t.Fatalf("erwarte 2 referenzen, habe %d", len(c.References)) } got, err := svc.Get(ctx, c.ID) if err != nil { t.Fatalf("get: %v", err) } if len(got.References) != 2 { t.Fatalf("nach get: erwarte 2 referenzen, habe %d", len(got.References)) } if got.Status != "open" { t.Errorf("erwarte status open, habe %q", got.Status) } dmsIDs, err := svc.ByReference(ctx, "dms", "document", "doc-123") if err != nil { t.Fatalf("byreference: %v", err) } if len(dmsIDs) != 1 || dmsIDs[0] != c.ID { t.Fatalf("erwarte genau die angelegte akte ueber die referenz, habe %v", dmsIDs) } } // AK2 / Pruefung 2: Statuswechsel im Test erzeugt Protokolleintrag mit // Person und Zeitpunkt (die Historie kommt von der Engine, ENG-04). func TestTransition_LogsHistoryEntryWithActorAndTimestamp(t *testing.T) { svc := newTestService(t) ctx := context.Background() c, err := svc.Create(ctx, "", "verantwortliche.person", "anlegerin", nil) if err != nil { t.Fatalf("create: %v", err) } if err := svc.Transition(ctx, c.ID, "in_progress", "bearbeiterin", "uebernahme"); err != nil { t.Fatalf("transition: %v", err) } hist, err := svc.History(ctx, c.ID) if err != nil { t.Fatalf("history: %v", err) } found := false for _, e := range hist { if e.ToState == "in_progress" && e.Actor == "bearbeiterin" && !e.OccurredAt.IsZero() { found = true } } if !found { t.Fatalf("erwarte protokolleintrag fuer den uebergang mit akteur und zeitpunkt, habe %+v", hist) } got, err := svc.Get(ctx, c.ID) if err != nil { t.Fatalf("get: %v", err) } if got.Status != "in_progress" { t.Errorf("erwarte status in_progress, habe %q", got.Status) } // Unzulaessiger Uebergang (geschlossen->offen ist nicht deklariert) // wird von der Engine abgewiesen, nicht stillschweigend akzeptiert // (bereits durch ENG-01 abgesichert, hier nur der Vertrag ueber // caseobj bestaetigt). if err := svc.Transition(ctx, c.ID, "closed", "bearbeiterin", ""); err != nil { t.Fatalf("in_progress->closed sollte erlaubt sein: %v", err) } if err := svc.Transition(ctx, c.ID, "open", "bearbeiterin", ""); err == nil { t.Fatal("closed->open ist nicht deklariert und sollte abgewiesen werden") } } // AK3 / Pruefung 3: Zwei Akten desselben Tenants (dieselbe DB) mit // identischem Aktenzeichen werden abgewiesen. func TestCreate_DuplicateCaseNumberIsRejected(t *testing.T) { svc := newTestService(t) ctx := context.Background() if _, err := svc.Create(ctx, "AK-DUP-1", "person.eins", "tester", nil); err != nil { t.Fatalf("erste akte anlegen: %v", err) } _, err := svc.Create(ctx, "AK-DUP-1", "person.zwei", "tester", nil) if !errors.Is(err, ErrDuplicateCaseNumber) { t.Fatalf("erwarte ErrDuplicateCaseNumber, habe %v", err) } } // Zusatz: automatisch erzeugte Aktenzeichen folgen dem konfigurierten // Format und sind untereinander eindeutig. func TestCreate_AutoGeneratedCaseNumberIsUniqueAndFollowsFormat(t *testing.T) { svc := newTestService(t) ctx := context.Background() c1, err := svc.Create(ctx, "", "person.eins", "tester", nil) if err != nil { t.Fatalf("erste akte: %v", err) } c2, err := svc.Create(ctx, "", "person.zwei", "tester", nil) if err != nil { t.Fatalf("zweite akte: %v", err) } if c1.CaseNumber == "" || c2.CaseNumber == "" { t.Fatal("erwarte nicht-leere generierte aktenzeichen") } if c1.CaseNumber == c2.CaseNumber { t.Fatalf("erwarte unterschiedliche generierte aktenzeichen, beide %q", c1.CaseNumber) } } // Zusatz (AK3, konfigurierbares Format): ein geaendertes Format wirkt // auf nachfolgend erzeugte Aktenzeichen. func TestSetCaseNumberFormat_AppliesToFutureCaseNumbers(t *testing.T) { svc := newTestService(t) ctx := context.Background() if err := SetCaseNumberFormat(ctx, svc.pool, "SONDER-{SEQ}"); err != nil { t.Fatalf("format setzen: %v", err) } c, err := svc.Create(ctx, "", "person", "tester", nil) if err != nil { t.Fatalf("akte anlegen: %v", err) } if len(c.CaseNumber) < 8 || c.CaseNumber[:7] != "SONDER-" { t.Fatalf("erwarte aktenzeichen mit prefix SONDER-, habe %q", c.CaseNumber) } }