CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Failing after 1m0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 4s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 4s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 24s
CI / Web (dms-browser) (push) Failing after 1m23s
CI / Web (pdf-viewer) (push) Failing after 59s
CI / Web (shl) (push) Failing after 13s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 56s
Workflow-Schnittstellen-Vertragstests / contract-tests (push) Failing after 59s
CI / Core-Modul (Build/Vet/Test) (pull_request) Failing after 1m1s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Successful in 4s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Successful in 3s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Failing after 21s
CI / Web (dms-browser) (pull_request) Failing after 1m17s
CI / Web (pdf-viewer) (pull_request) Failing after 59s
CI / Web (shl) (pull_request) Failing after 13s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Successful in 3m51s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Failing after 56s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Failing after 2m6s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 11m50s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Successful in 11m41s
Workflow-Schnittstellen-Vertragstests / contract-tests (pull_request) Failing after 41s
Workflow-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 11m18s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 3s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Failing after 3s
DB-Integrationstests / db-tests (pull_request) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
235 lines
8.7 KiB
Go
235 lines
8.7 KiB
Go
package migrationworkflow
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
|
)
|
|
|
|
// memorySource ist eine In-Memory-Testquelle: simuliert den SQL-Export von
|
|
// archivdms rein synthetisch (DSGVO: keine echten Altsystem-Daten).
|
|
type memorySource struct {
|
|
cases []SourceCase
|
|
history map[string][]SourceHistoryEntry
|
|
}
|
|
|
|
func (s *memorySource) Cases(ctx context.Context) ([]SourceCase, error) {
|
|
return s.cases, nil
|
|
}
|
|
|
|
func (s *memorySource) History(ctx context.Context, sourceCaseID string) ([]SourceHistoryEntry, error) {
|
|
return s.history[sourceCaseID], nil
|
|
}
|
|
|
|
// staticUserResolver bildet archivdms-Akteurs-IDs 1:1 auf eine feste
|
|
// Ziel-Benutzer-ID ab (Benutzermigration ist COR-02, nicht Teil von WFL-01).
|
|
type staticUserResolver struct{ known map[string]string }
|
|
|
|
func (r staticUserResolver) ResolveUserID(ctx context.Context, sourceActorID string) (string, error) {
|
|
id, ok := r.known[sourceActorID]
|
|
if !ok {
|
|
return "", fmt.Errorf("unbekannter testakteur %q", sourceActorID)
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
func newSyntheticSource() *memorySource {
|
|
return &memorySource{
|
|
cases: []SourceCase{
|
|
{SourceCaseID: "archivdms-case-1", ProcessName: "case", FinalState: "closed", Status: StatusCompleted},
|
|
{SourceCaseID: "archivdms-case-2", ProcessName: "case", FinalState: "closed", Status: StatusAborted},
|
|
{SourceCaseID: "archivdms-case-3", ProcessName: "case", FinalState: "in_progress", Status: StatusRunning},
|
|
},
|
|
history: map[string][]SourceHistoryEntry{
|
|
"archivdms-case-1": {
|
|
{FromState: "", ToState: "open", SourceActor: "archivdms-user-1", Reason: "gestartet", OccurredAtRFC3339: "2025-01-10T09:00:00Z"},
|
|
{FromState: "open", ToState: "in_progress", SourceActor: "archivdms-user-1", Reason: "bearbeitung begonnen", OccurredAtRFC3339: "2025-01-11T09:00:00Z"},
|
|
{FromState: "in_progress", ToState: "closed", SourceActor: "archivdms-user-2", Reason: "abgeschlossen", OccurredAtRFC3339: "2025-01-12T09:00:00Z"},
|
|
},
|
|
"archivdms-case-2": {
|
|
{FromState: "", ToState: "open", SourceActor: "archivdms-user-1", Reason: "gestartet", OccurredAtRFC3339: "2025-02-01T09:00:00Z"},
|
|
{FromState: "open", ToState: "closed", SourceActor: "archivdms-user-1", Reason: "abgebrochen", OccurredAtRFC3339: "2025-02-02T09:00:00Z"},
|
|
},
|
|
"archivdms-case-3": {
|
|
{FromState: "", ToState: "open", SourceActor: "archivdms-user-1", Reason: "gestartet", OccurredAtRFC3339: "2025-03-01T09:00:00Z"},
|
|
{FromState: "open", ToState: "in_progress", SourceActor: "archivdms-user-1", Reason: "bearbeitung begonnen", OccurredAtRFC3339: "2025-03-02T09:00:00Z"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func setupMigratorTest(t *testing.T) (*Migrator, *pgxpool.Pool) {
|
|
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)
|
|
|
|
if _, err := pool.Exec(ctx, `
|
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
CREATE TABLE IF NOT EXISTS workflow_instances (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
process_name TEXT NOT NULL,
|
|
state TEXT NOT NULL,
|
|
version INT NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE TABLE IF NOT EXISTS workflow_instance_history (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
instance_id UUID NOT NULL REFERENCES workflow_instances(id) ON DELETE CASCADE,
|
|
from_state TEXT NOT NULL,
|
|
to_state TEXT NOT NULL,
|
|
actor TEXT NOT NULL,
|
|
reason TEXT NOT NULL DEFAULT '',
|
|
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
`); err != nil {
|
|
t.Fatalf("schema: %v", err)
|
|
}
|
|
|
|
resolver := staticUserResolver{known: map[string]string{
|
|
"archivdms-user-1": "11111111-1111-1111-1111-111111111111",
|
|
"archivdms-user-2": "22222222-2222-2222-2222-222222222222",
|
|
}}
|
|
migrator := NewMigrator(pool, newSyntheticSource(), resolver)
|
|
return migrator, pool
|
|
}
|
|
|
|
// TestMigrateCompleted_AkzeptanzkriteriumAuffindbar prueft Akzeptanzkriterium
|
|
// 1: jeder abgeschlossene Vorgang ist nach der Migration als Historieneintrag
|
|
// auffindbar, inklusive vollstaendiger Uebergangshistorie.
|
|
func TestMigrateCompleted_AkzeptanzkriteriumAuffindbar(t *testing.T) {
|
|
migrator, pool := setupMigratorTest(t)
|
|
ctx := context.Background()
|
|
|
|
res, err := migrator.MigrateCompleted(ctx)
|
|
if err != nil {
|
|
t.Fatalf("MigrateCompleted: %v", err)
|
|
}
|
|
if res.InstancesMigrated != 2 || res.HistoryEntriesMigrated != 5 {
|
|
t.Fatalf("erwartet 2 instanzen/5 historieneintraege, habe %+v", res)
|
|
}
|
|
|
|
id1 := instanceID("archivdms-case-1")
|
|
var state string
|
|
var version int
|
|
if err := pool.QueryRow(ctx, `SELECT state, version FROM workflow_instances WHERE id = $1`, id1).Scan(&state, &version); err != nil {
|
|
t.Fatalf("instanz 1 lesen: %v", err)
|
|
}
|
|
if state != "closed" || version != 4 {
|
|
t.Fatalf("erwartet state=closed version=4, habe state=%q version=%d", state, version)
|
|
}
|
|
|
|
var historyCount int
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM workflow_instance_history WHERE instance_id = $1`, id1).Scan(&historyCount); err != nil {
|
|
t.Fatalf("historie zaehlen: %v", err)
|
|
}
|
|
if historyCount != 3 {
|
|
t.Fatalf("erwartet 3 historieneintraege fuer instanz 1, habe %d", historyCount)
|
|
}
|
|
}
|
|
|
|
// TestMigrateCompleted_AkzeptanzkriteriumKeineLaufendeInstanz prueft den
|
|
// sicherheitskritischen Teil der Kachel (Akzeptanzkriterium 2): eine NICHT
|
|
// abgeschlossene/laufende Workflow-Instanz in der Quelle wird NICHT
|
|
// migriert, auch nicht halb (keine Zeile in workflow_instances).
|
|
func TestMigrateCompleted_AkzeptanzkriteriumKeineLaufendeInstanz(t *testing.T) {
|
|
migrator, pool := setupMigratorTest(t)
|
|
ctx := context.Background()
|
|
|
|
res, err := migrator.MigrateCompleted(ctx)
|
|
if err != nil {
|
|
t.Fatalf("MigrateCompleted: %v", err)
|
|
}
|
|
if res.InstancesSkippedOpen != 1 {
|
|
t.Fatalf("erwartet 1 uebersprungene laufende instanz, habe %+v", res)
|
|
}
|
|
|
|
id3 := instanceID("archivdms-case-3")
|
|
var exists bool
|
|
err = pool.QueryRow(ctx, `SELECT true FROM workflow_instances WHERE id = $1`, id3).Scan(&exists)
|
|
if err == nil {
|
|
t.Fatalf("laufende instanz archivdms-case-3 wurde migriert, darf nicht sein")
|
|
}
|
|
|
|
var historyCount int
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM workflow_instance_history WHERE instance_id = $1`, id3).Scan(&historyCount); err != nil {
|
|
t.Fatalf("historie zaehlen: %v", err)
|
|
}
|
|
if historyCount != 0 {
|
|
t.Fatalf("laufende instanz hat historieneintraege, darf nicht sein: %d", historyCount)
|
|
}
|
|
}
|
|
|
|
// TestOpenCases_AkzeptanzkriteriumListeOffenerInstanzen prueft
|
|
// Akzeptanzkriterium 3/Pruefung 2: die Liste offener Instanzen vor dem
|
|
// Cutover ist vollstaendig und enthaelt ausschliesslich nicht abgeschlossene
|
|
// Vorgaenge.
|
|
func TestOpenCases_AkzeptanzkriteriumListeOffenerInstanzen(t *testing.T) {
|
|
migrator, _ := setupMigratorTest(t)
|
|
ctx := context.Background()
|
|
|
|
open, err := migrator.OpenCases(ctx)
|
|
if err != nil {
|
|
t.Fatalf("OpenCases: %v", err)
|
|
}
|
|
if len(open) != 1 || open[0].SourceCaseID != "archivdms-case-3" {
|
|
t.Fatalf("erwartet genau archivdms-case-3 als offene instanz, habe %+v", open)
|
|
}
|
|
}
|
|
|
|
// TestMigrateCompleted_AkzeptanzkriteriumWiederholbar prueft, dass ein
|
|
// erneuter Lauf gegen dieselbe Quelle keine Duplikate erzeugt.
|
|
func TestMigrateCompleted_AkzeptanzkriteriumWiederholbar(t *testing.T) {
|
|
migrator, pool := setupMigratorTest(t)
|
|
ctx := context.Background()
|
|
|
|
if _, err := migrator.MigrateCompleted(ctx); err != nil {
|
|
t.Fatalf("erster lauf: %v", err)
|
|
}
|
|
res2, err := migrator.MigrateCompleted(ctx)
|
|
if err != nil {
|
|
t.Fatalf("zweiter lauf: %v", err)
|
|
}
|
|
if res2.InstancesMigrated != 0 || res2.InstancesSkippedExisting != 2 {
|
|
t.Fatalf("zweiter lauf haette nichts neu migrieren duerfen, habe %+v", res2)
|
|
}
|
|
|
|
var instanceCount, historyCount int
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM workflow_instances`).Scan(&instanceCount); err != nil {
|
|
t.Fatalf("instanzen zaehlen: %v", err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT count(*) FROM workflow_instance_history`).Scan(&historyCount); err != nil {
|
|
t.Fatalf("historie zaehlen: %v", err)
|
|
}
|
|
if instanceCount != 2 || historyCount != 5 {
|
|
t.Fatalf("erneuter lauf hat duplikate erzeugt: %d instanzen/%d historieneintraege", instanceCount, historyCount)
|
|
}
|
|
}
|
|
|
|
// TestMigrateCompleted_FailClosedBeiUnbekanntemAkteur prueft, dass ein
|
|
// nicht aufloesbarer Akteur die Migration dieses Vorgangs mit einem klaren
|
|
// Fehler abbricht statt ihn mit einem Platzhalter zu uebernehmen.
|
|
func TestMigrateCompleted_FailClosedBeiUnbekanntemAkteur(t *testing.T) {
|
|
migrator, _ := setupMigratorTest(t)
|
|
ctx := context.Background()
|
|
|
|
src := migrator.source.(*memorySource)
|
|
src.history["archivdms-case-1"][0].SourceActor = "archivdms-user-unbekannt"
|
|
|
|
_, err := migrator.MigrateCompleted(ctx)
|
|
if !errors.Is(err, ErrUnresolvedActor) {
|
|
t.Fatalf("erwartet ErrUnresolvedActor, habe %v", err)
|
|
}
|
|
}
|