Files
patrickandClaude Sonnet 5.5 0c6a943440
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 1m2s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 5s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 5s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 22s
CI / Web (dms-browser) (push) Failing after 1m22s
CI / Web (pdf-viewer) (push) Failing after 53s
CI / Web (shl) (push) Failing after 11s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 1m6s
CI / Core-Modul (Build/Vet/Test) (pull_request) Failing after 1m0s
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 1m22s
CI / Web (pdf-viewer) (pull_request) Failing after 57s
CI / Web (shl) (pull_request) Failing after 12s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Successful in 10m56s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Failing after 3m16s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Failing after 1m46s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 11m24s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Successful in 11m44s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 2s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Failing after 3s
DB-Integrationstests / db-tests (pull_request) Canceled after 0s
dms: testdb aus common statt interner Kopie
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 23:37:00 +01:00

238 lines
8.7 KiB
Go

package migrationfolder
import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
)
// memorySource ist eine In-Memory-Testquelle: simuliert das SQL-Export von
// archivdms' akten-Tabelle rein SYNTHETISCH — fake Hierarchie, keine
// echten archivdms-Daten (Produktarchitektur-Vorgabe, DSGVO).
type memorySource struct {
folders []SourceFolder
documentFolders []SourceDocumentFolder
}
func (s *memorySource) Folders(ctx context.Context) ([]SourceFolder, error) {
return s.folders, nil
}
func (s *memorySource) DocumentFolders(ctx context.Context) ([]SourceDocumentFolder, error) {
return s.documentFolders, nil
}
type staticUserResolver struct{ userID string }
func (r staticUserResolver) ResolveUserID(ctx context.Context, sourceUserID string) (string, error) {
return r.userID, nil
}
func strPtr(s string) *string { return &s }
func newSyntheticSource() *memorySource {
return &memorySource{
folders: []SourceFolder{
{SourceFolderID: "akte-mandant-mustermann", SourceParentFolderID: nil, Name: "Mandant Mustermann", SourceUserID: "archivdms-user-1"},
{SourceFolderID: "akte-vorgang-kauf", SourceParentFolderID: strPtr("akte-mandant-mustermann"), Name: "Vorgang Hauskauf", SourceUserID: "archivdms-user-1"},
{SourceFolderID: "akte-vorgang-kauf-unterlagen", SourceParentFolderID: strPtr("akte-vorgang-kauf"), Name: "Unterlagen", SourceUserID: "archivdms-user-1"},
{SourceFolderID: "akte-mandant-schmidt", SourceParentFolderID: nil, Name: "Mandant Schmidt", SourceUserID: "archivdms-user-1"},
},
documentFolders: []SourceDocumentFolder{
{SourceDocumentID: "archivdms-doc-1", SourceFolderID: "akte-vorgang-kauf-unterlagen"},
{SourceDocumentID: "archivdms-doc-2", SourceFolderID: "akte-mandant-schmidt"},
},
}
}
func setupMigratorTest(t *testing.T) (*Migrator, *pgxpool.Pool, string) {
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)
t.Cleanup(func() { pool.Close() })
if _, err := pool.Exec(ctx, `
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS folders (
id UUID PRIMARY KEY, parent_folder_id UUID REFERENCES folders(id) ON DELETE CASCADE,
name TEXT NOT NULL, path TEXT NOT NULL, created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_folders_path ON folders (path text_pattern_ops);
CREATE TABLE IF NOT EXISTS documents (
id UUID PRIMARY KEY, folder_id UUID REFERENCES folders(id) ON DELETE SET NULL, title TEXT NOT NULL,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
`); err != nil {
t.Fatalf("schema: %v", err)
}
t.Cleanup(func() {
ctx := context.Background()
_, _ = pool.Exec(ctx, `TRUNCATE documents, folders, users CASCADE`)
})
var userID string
if err := pool.QueryRow(ctx, `
INSERT INTO users (email, name) VALUES ($1, 'Migrations-Testbenutzer') RETURNING id
`, fmt.Sprintf("dok02-test-%d@example.test", time.Now().UnixNano())).Scan(&userID); err != nil {
t.Fatalf("testbenutzer anlegen: %v", err)
}
migrator := NewMigrator(pool, newSyntheticSource(), staticUserResolver{userID: userID}, "acme-test")
for _, docID := range []string{"archivdms-doc-1", "archivdms-doc-2"} {
targetID := deriveUUID("document", "acme-test", docID)
if _, err := pool.Exec(ctx, `
INSERT INTO documents (id, title, created_by) VALUES ($1, $2, $3)
`, targetID, "Testdokument "+docID, userID); err != nil {
t.Fatalf("testdokument %q anlegen: %v", docID, err)
}
}
return migrator, pool, userID
}
// Akzeptanzkriterium 1: jede Akte/jeder Ordner ist nach der Migration mit
// derselben Hierarchie wie im Altsystem vorhanden.
func TestMigrateAll_AkzeptanzkriteriumHierarchieErhalten(t *testing.T) {
migrator, pool, _ := setupMigratorTest(t)
ctx := context.Background()
res, err := migrator.MigrateAll(ctx)
if err != nil {
t.Fatalf("MigrateAll: %v", err)
}
if res.FoldersMigrated != 4 {
t.Fatalf("erwartet 4 migrierte akten, habe %+v", res)
}
var folderCount int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM folders`).Scan(&folderCount); err != nil {
t.Fatalf("akten zaehlen: %v", err)
}
if folderCount != 4 {
t.Fatalf("erwartet 4 akten in der db, habe %d", folderCount)
}
rootID := deriveUUID("folder", "acme-test", "akte-mandant-mustermann")
childID := deriveUUID("folder", "acme-test", "akte-vorgang-kauf")
grandchildID := deriveUUID("folder", "acme-test", "akte-vorgang-kauf-unterlagen")
var parentFolderID *string
var path string
if err := pool.QueryRow(ctx, `SELECT parent_folder_id, path FROM folders WHERE id = $1`, childID).Scan(&parentFolderID, &path); err != nil {
t.Fatalf("kind-akte lesen: %v", err)
}
if parentFolderID == nil || *parentFolderID != rootID {
t.Fatalf("erwartet parent_folder_id %q, habe %v", rootID, parentFolderID)
}
if !strings.Contains(path, "/"+rootID+"/") || !strings.HasSuffix(path, "/"+childID+"/") {
t.Fatalf("pfad der kind-akte spiegelt hierarchie nicht wider: %q", path)
}
var grandchildPath string
if err := pool.QueryRow(ctx, `SELECT path FROM folders WHERE id = $1`, grandchildID).Scan(&grandchildPath); err != nil {
t.Fatalf("enkel-akte lesen: %v", err)
}
expectedPrefix := "/" + rootID + "/" + childID + "/"
if !strings.HasPrefix(grandchildPath, expectedPrefix) {
t.Fatalf("erwartet pfad-praefix %q, habe %q", expectedPrefix, grandchildPath)
}
}
// Akzeptanzkriterium 2: jedes migrierte Dokument ist der korrekten Akte
// zugeordnet.
func TestMigrateAll_AkzeptanzkriteriumDokumenteKorrektVerknuepft(t *testing.T) {
migrator, pool, _ := setupMigratorTest(t)
ctx := context.Background()
res, err := migrator.MigrateAll(ctx)
if err != nil {
t.Fatalf("MigrateAll: %v", err)
}
if res.DocumentsLinked != 2 {
t.Fatalf("erwartet 2 verknuepfte dokumente, habe %+v", res)
}
doc1ID := deriveUUID("document", "acme-test", "archivdms-doc-1")
expectedFolder1 := deriveUUID("folder", "acme-test", "akte-vorgang-kauf-unterlagen")
var folderID1 string
if err := pool.QueryRow(ctx, `SELECT folder_id FROM documents WHERE id = $1`, doc1ID).Scan(&folderID1); err != nil {
t.Fatalf("dokument 1 lesen: %v", err)
}
if folderID1 != expectedFolder1 {
t.Fatalf("dokument 1 ist der falschen akte zugeordnet: erwartet %q, habe %q", expectedFolder1, folderID1)
}
doc2ID := deriveUUID("document", "acme-test", "archivdms-doc-2")
expectedFolder2 := deriveUUID("folder", "acme-test", "akte-mandant-schmidt")
var folderID2 string
if err := pool.QueryRow(ctx, `SELECT folder_id FROM documents WHERE id = $1`, doc2ID).Scan(&folderID2); err != nil {
t.Fatalf("dokument 2 lesen: %v", err)
}
if folderID2 != expectedFolder2 {
t.Fatalf("dokument 2 ist der falschen akte zugeordnet: erwartet %q, habe %q", expectedFolder2, folderID2)
}
}
// Wiederholbarkeit: ein erneuter Lauf gegen dieselbe Quelle erzeugt keine
// Duplikate (gleiches Prinzip wie DOK-01/migrationdocument).
func TestMigrateAll_Wiederholbar(t *testing.T) {
migrator, pool, _ := setupMigratorTest(t)
ctx := context.Background()
if _, err := migrator.MigrateAll(ctx); err != nil {
t.Fatalf("erster lauf: %v", err)
}
res2, err := migrator.MigrateAll(ctx)
if err != nil {
t.Fatalf("zweiter lauf: %v", err)
}
if res2.FoldersMigrated != 0 || res2.FoldersSkipped != 4 {
t.Fatalf("zweiter lauf haette keine neuen akten anlegen duerfen, habe %+v", res2)
}
var folderCount int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM folders`).Scan(&folderCount); err != nil {
t.Fatalf("akten zaehlen: %v", err)
}
if folderCount != 4 {
t.Fatalf("erneuter lauf hat duplikate erzeugt: %d akten", folderCount)
}
}
// Pruefung: zyklische Elternreferenz in der Quelle wird erkannt statt eine
// Endlosrekursion auszuloesen.
func TestMigrateAll_ZyklusWirdErkannt(t *testing.T) {
migrator, _, _ := setupMigratorTest(t)
src := migrator.source.(*memorySource)
src.folders = []SourceFolder{
{SourceFolderID: "a", SourceParentFolderID: strPtr("b"), Name: "A", SourceUserID: "archivdms-user-1"},
{SourceFolderID: "b", SourceParentFolderID: strPtr("a"), Name: "B", SourceUserID: "archivdms-user-1"},
}
src.documentFolders = nil
ctx := context.Background()
_, err := migrator.MigrateAll(ctx)
if err == nil || !strings.Contains(err.Error(), "zyklus") {
t.Fatalf("erwartet zyklus-fehler, habe %v", err)
}
}