From a4ba108cef5ee641da9f149b2c5891e67c2a7e05 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 14 Oct 2026 20:03:15 +0000 Subject: [PATCH] VAL-05: login-test-migrierte-zugangsdaten Co-Authored-By: Claude Sonnet 5 --- docs/VAL-05-PRUEFPROTOKOLL.md | 86 ++++++++++++++ internal/migrationuser/val05_login_test.go | 129 +++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 docs/VAL-05-PRUEFPROTOKOLL.md create mode 100644 internal/migrationuser/val05_login_test.go diff --git a/docs/VAL-05-PRUEFPROTOKOLL.md b/docs/VAL-05-PRUEFPROTOKOLL.md new file mode 100644 index 00000000..50cc9229 --- /dev/null +++ b/docs/VAL-05-PRUEFPROTOKOLL.md @@ -0,0 +1,86 @@ +# VAL-05 — Prüfprotokoll: Login-Test mit migrierten Zugangsdaten + +Datum: 2026-10-14 +Testhost: root@192.168.1.204 (Wegwerf-Postgres-Cluster, Port 5433, `TEST_ADMIN_DSN` aus `/etc/nexarch-dev/core-dev.env`, kein Bezug zu `dms_dev`/`archivdms`) +Arbeitsverzeichnis: frischer Klon `git@…/nexarch.git` nach `/root/val05-work-*` auf Host 204 — nicht `/root/nexarch-code`, nicht der lokale Checkout. + +## Auftrag und Testart + +VAL-05 ist kein neuer Migrationsschritt, sondern ein QA-Verifikationswerkzeug: ein automatisierter, echt ausgeführter Login-Test gegen den realen Core-Login-Endpunkt (`internal/auth.LoginService.Login`, IAM-01/IAM-04/IAM-05) mit Zugangsdaten, wie sie COR-02 (`internal/migrationuser.Migrator.MigrateApproved`) aus einer freigegebenen `user_migration_map` in eine Tenant-Datenbank übernimmt. Weder der Login-Endpunkt noch der eigentliche Passwort-Vergleich (`auth.VerifyPassword`, bcrypt) werden gemockt. + +## Synthetische Testdaten — Substitutionsentscheidung + +Keine echten archivmail-/archivdms-Zugangsdaten verwendet. Je Altsystem (`archivmail`, `archivdms`) 5 synthetische Testkonten mit: +- fiktiver E-Mail-Adresse unter `@example.invalid`, +- selbst erzeugtem Klartext-Testpasswort, +- bcrypt-Hash daraus per `bcrypt.GenerateFromPassword` (`bcrypt.MinCost`, nur für Testlaufzeit relevant) — genau das Verfahren, das laut Auftrag in beiden Altsystemen bereits verwendet wird (kein Rehash in COR-02). + +Die Testkonten werden als `user_migration_map`-Einträge eingefügt (`archivmail_user_id` bzw. `archivdms_user_id` gesetzt, je nach simuliertem Herkunftssystem), genehmigt (`approved_at` gesetzt), und dann durch den echten COR-02-Migrator in eine frische, physisch eigene Tenant-Testdatenbank übernommen — exakt der Pfad, den ein realer Cutover durchläuft, nur mit erfundenen Quelldaten statt eines Exports aus einem Altsystem. + +Keine Zugangsdaten im Code: `TEST_ADMIN_DSN` ausschließlich aus der Umgebung (`/etc/nexarch-dev/core-dev.env`), nie im Klartext ausgegeben; Testpasswörter nur innerhalb des Testprozesses verwendet, nicht in Tool-Ausgaben protokolliert. + +## Ausgangslage geprüft + +- COR-02 (`internal/migrationuser`) ist fertig: `Migrator.MigrateApproved` übernimmt freigegebene `user_migration_map`-Einträge 1:1 (inkl. bcrypt-Hash) in die Ziel-Tenant-DB, idempotent. +- Core-Login-Endpunkt (IAM-01, `internal/auth/login.go`): `LoginService.Login(ctx, email, password)` lädt den Benutzer tenant-scoped über `user.TenantUserStore.GetByEmailForAuth`, prüft mit `auth.VerifyPassword` (timing-safer bcrypt-Vergleich) und stellt bei Erfolg ein Token über `TokenIssuer` aus. Bestehendes Muster für genau diesen End-to-End-Weg bereits vorhanden: `internal/e2e/tenant_onboarding_test.go` (QA-02) sowie `internal/migrationuser/migrate_test.go` (COR-02-Integrationstests, gleiche Helfer `setupRegistrySchema`/`createSynthTenantDB`/`newUUIDv4` wiederverwendet). + +## Umsetzung + +- Neue Testdatei `internal/migrationuser/val05_login_test.go`, `TestVAL05_LoginMitMigriertenZugangsdaten`, zwei Subtests `archivmail` und `archivdms` (jeweils eigenes Registry-Schema über `testdb.Pool`, damit `MapStore.Approved` im zweiten Subtest nicht die bereits migrierten und aufgeräumten Einträge des ersten sieht). +- Je Subtest: 5 synthetische Map-Einträge anlegen und freigeben → `Migrator.MigrateApproved` ausführen → Tenant-DSN über `tenant.Registry.GetByID` auflösen → `auth.NewLoginService` mit echtem `TenantUserStore` auf der migrierten Tenant-DB instanziieren → für jedes der 5 Konten `Login(ctx, email, klartextpasswort)` aufrufen, Token verifizieren, `claims.UserID` gegen die migrierte `nexarch_user_id` prüfen. +- Zusätzlich je Subtest ein Login-Versuch mit bewusst falschem Testpasswort auf dem ersten Konto. +- Keine Produktionscode-Änderung nötig — reines Testskript, nutzt ausschließlich bestehende, bereits geprüfte Pakete (`internal/migrationuser`, `internal/auth`, `internal/user`, `internal/tenant`, `internal/testdb`). + +## Akzeptanzkriterien + +| # | Kriterium | Ergebnis | +|---|---|---| +| 1 | Für jeden Tenant ist mindestens ein erfolgreicher Login-Test mit migriertem Konto dokumentiert | Erfüllt — je Altsystem (archivmail, archivdms) 5 erfolgreiche Logins mit migrierten Konten, siehe Testlauf unten | +| 2 | Fehlgeschlagene Logins sind vor Freischaltung des Tenants geklärt | Erfüllt für die Testinstanz — keine unerklärten Fehlschläge; der einzige erwartete Fehlschlag (bewusst falsches Testpasswort) wird korrekt abgewiesen und ist dokumentiert, kein echter Tenant wird durch dieses Werkzeug freigeschaltet | + +## Prüfungen vor der Abnahme + +1. **Login-Test für 5 Testkonten je Altsystem erfolgreich.** + Ergebnis: erreicht — `TestVAL05_LoginMitMigriertenZugangsdaten/archivmail` und `/archivdms`, je 5/5 migrierte Konten erfolgreich angemeldet, Token verifiziert, `claims.UserID` korrekt. + +2. **Bewusst falsches Testpasswort wird korrekt abgewiesen.** + Ergebnis: erreicht — Login mit `bewusst-falsches-testpasswort-` liefert in beiden Subtests einen Fehler (`login mit falschem passwort hätte scheitern müssen` nicht ausgelöst). + +### Realer Testlauf + +``` +cd /root/val05-work-1792007933 +set -a && source /etc/nexarch-dev/core-dev.env && set +a +go test ./internal/migrationuser/... -run TestVAL05 -v + +=== RUN TestVAL05_LoginMitMigriertenZugangsdaten +=== RUN TestVAL05_LoginMitMigriertenZugangsdaten/archivmail +=== RUN TestVAL05_LoginMitMigriertenZugangsdaten/archivdms +--- PASS: TestVAL05_LoginMitMigriertenZugangsdaten (1.98s) + --- PASS: TestVAL05_LoginMitMigriertenZugangsdaten/archivmail (1.19s) + --- PASS: TestVAL05_LoginMitMigriertenZugangsdaten/archivdms (0.79s) +PASS +ok gitea.perlbach24.de/scripte/nexarch/internal/migrationuser 1.991s +``` + +Zusätzlich (Regression/Build): +``` +go build ./... -> keine Fehler +go vet ./... -> keine Befunde +go test ./internal/migrationuser/... -v -> alle Tests PASS + (TestMigrateApproved, TestMigrateApproved_UnknownTenantFails, + TestMigrateApproved_LDAPFields, TestVAL05_LoginMitMigriertenZugangsdaten) + -> keine Regression in bestehenden COR-02-Tests +``` + +## Entscheidungen (dokumentiert, keine Rückfrage nötig) + +- Test liegt im Paket `internal/migrationuser` (nicht `internal/e2e`), weil er direkt die paketinternen Testhelfer (`setupRegistrySchema`, `createSynthTenantDB`, `newUUIDv4`) aus `migrate_test.go` wiederverwendet statt sie zu duplizieren oder zu exportieren. +- Je Subtest ein eigenes Registry-Schema (`testdb.Pool`) statt eines gemeinsamen: `MapStore.Approved()` liest ohne Tenant-Filter alle freigegebenen Zeilen — bei geteiltem Schema hätte der zweite Subtest versucht, die bereits migrierte und wieder gelöschte Tenant-DB des ersten Subtests erneut zu verbinden. +- Unterscheidung archivmail/archivdms ausschließlich über `archivmail_user_id`/`archivdms_user_id` im Map-Eintrag (wie im realen Schema vorgesehen), nicht über einen eigenen Tenant pro Altsystem in der Produktionslogik — das bildet exakt ab, wie COR-02 Herkunft bereits unterscheidet. +- Kein Login über HTTP-Handler (`internal/auth.Handler.Login`), sondern direkt über `auth.LoginService.Login`: das ist derselbe Code, den der HTTP-Handler aufruft (siehe `internal/auth/handler.go`), und vermeidet zusätzlichen Netzwerk-/Serveraufbau für ein reines QA-Verifikationswerkzeug. +- Dieses Werkzeug ersetzt nicht die schriftliche Tenant-Freigabe (Produkt-DNA: „Kein Tenant wird freigeschaltet, dessen Verifikation nicht schriftlich vorliegt“) — es liefert den technischen Nachweis, der dieser schriftlichen Freigabe je Tenant zugrunde liegt. + +## Offene Punkte (außerhalb VAL-05-Scope) + +- Für den echten Cutover ist dieser Test mit den tatsächlichen Produktions-Tenant-Datenbanken (nach echtem COR-02-Lauf) auszuführen, nicht nur mit synthetischen Fixtures — dafür reicht `go test ./internal/migrationuser/... -run TestMigrateApproved` als Vorlage nicht, da echte Zugangsdaten laut DSGVO-Vorgabe nicht in automatisierten Tests/Prompts verwendet werden dürfen. Die manuelle, dokumentierte Stichprobe je echtem Tenant bleibt ein separater, von Menschen durchzuführender Schritt vor der schriftlichen Freigabe. diff --git a/internal/migrationuser/val05_login_test.go b/internal/migrationuser/val05_login_test.go new file mode 100644 index 00000000..0abf7872 --- /dev/null +++ b/internal/migrationuser/val05_login_test.go @@ -0,0 +1,129 @@ +// VAL-05: Login-Test mit migrierten Zugangsdaten. Treibt COR-02 +// (Migrator.MigrateApproved) UND den echten Core-Login-Endpunkt +// (auth.LoginService.Login, IAM-01) gegen eine echte Postgres-Instanz, je +// Altsystem (archivmail, archivdms) mit 5 synthetischen Testkonten +// (fiktive E-Mails, selbst gehashte Test-Passwoerter, keine echten +// Kunden-/Zugangsdaten). Ohne TEST_ADMIN_DSN uebersprungen. +package migrationuser + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/jackc/pgx/v5/pgxpool" + "golang.org/x/crypto/bcrypt" + + "gitea.perlbach24.de/scripte/nexarch/internal/auth" + "gitea.perlbach24.de/scripte/nexarch/internal/tenant" + "gitea.perlbach24.de/scripte/nexarch/internal/testdb" + "gitea.perlbach24.de/scripte/nexarch/internal/user" +) + +type val05TestAccount struct { + id string + email string + password string +} + +func TestVAL05_LoginMitMigriertenZugangsdaten(t *testing.T) { + adminDSN := os.Getenv("TEST_ADMIN_DSN") + if adminDSN == "" { + t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen") + } + ctx := context.Background() + + for _, altsystem := range []string{"archivmail", "archivdms"} { + altsystem := altsystem + t.Run(altsystem, func(t *testing.T) { + // Eigenes Registry-Schema je Subtest (nicht geteilt): sonst + // sieht MigrateApproved im zweiten Subtest auch die bereits + // migrierten und wieder aufgeraeumten Eintraege des ersten. + registryPool := testdb.Pool(t, adminDSN) + setupRegistrySchema(t, ctx, registryPool) + registry := tenant.NewRegistry(registryPool) + suffix := testdb.Suffix() + + tenantID := newUUIDv4() + createSynthTenantDB(t, ctx, adminDSN, registryPool, tenantID) + + accounts := make([]val05TestAccount, 0, 5) + for i := 1; i <= 5; i++ { + pw := fmt.Sprintf("val05-%s-pw-%d-%s", altsystem, i, suffix) + hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.MinCost) + if err != nil { + t.Fatalf("test-hash erzeugen: %v", err) + } + acc := val05TestAccount{ + id: newUUIDv4(), + email: fmt.Sprintf("val05.%s.user.%d.%s@example.invalid", altsystem, i, suffix), + password: pw, + } + accounts = append(accounts, acc) + + var archivmailID, archivdmsID any + switch altsystem { + case "archivmail": + archivmailID = int64(9000 + i) + case "archivdms": + archivdmsID = int64(9000 + i) + } + if _, err := registryPool.Exec(ctx, ` + INSERT INTO user_migration_map (archivmail_user_id, archivdms_user_id, nexarch_tenant_id, nexarch_user_id, email, name, password_hash, approved_by, approved_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, 'qa-tester', now()) + `, archivmailID, archivdmsID, tenantID, acc.id, acc.email, fmt.Sprintf("VAL-05 Testkonto %s %d", altsystem, i), string(hash)); err != nil { + t.Fatalf("map-eintrag anlegen: %v", err) + } + } + + mapStore := NewMapStore(registryPool) + migrator := NewMigrator(mapStore, registry) + defer migrator.Close() + + res, err := migrator.MigrateApproved(ctx) + if err != nil { + t.Fatalf("MigrateApproved: %v", err) + } + if len(res.Migrated) == 0 { + t.Fatal("COR-02-Migration hat keine Benutzer uebernommen") + } + + tn, err := registry.GetByID(ctx, tenantID) + if err != nil { + t.Fatalf("tenant laden: %v", err) + } + tenantPool, err := pgxpool.New(ctx, tn.DBDSN) + if err != nil { + t.Fatalf("tenant-db verbinden: %v", err) + } + defer tenantPool.Close() + userStore := user.NewTenantUserStore(tenantPool) + + issuer := auth.NewTokenIssuer("val05-test-signing-secret-" + suffix) + login := auth.NewLoginService(userStore, issuer, tn.Slug) + + for _, acc := range accounts { + token, err := login.Login(ctx, acc.email, acc.password) + if err != nil { + t.Fatalf("login fuer migriertes konto %q (%s) fehlgeschlagen: %v", acc.email, altsystem, err) + } + if token == "" { + t.Fatalf("login fuer %q lieferte leeres token", acc.email) + } + claims, err := issuer.Verify(token) + if err != nil { + t.Fatalf("token fuer %q nicht verifizierbar: %v", acc.email, err) + } + if claims.UserID != acc.id { + t.Fatalf("token user id = %q, erwartet %q", claims.UserID, acc.id) + } + } + + firstAcc := accounts[0] + if _, err := login.Login(ctx, firstAcc.email, "bewusst-falsches-testpasswort-"+suffix); err == nil { + t.Fatalf("login mit falschem passwort fuer %q haette scheitern muessen", firstAcc.email) + } + }) + } +} -- 2.47.3