VAL-05: login-test-migrierte-zugangsdaten
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) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) 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
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Sonnet 5 committed 2026-10-14 20:03:15 +00:00
1 parent 1bd7e2a953
commit a4ba108cef
2 files changed
+215

No files matched your search

+86
View File
@@ -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-<suffix>` 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.
+129
View File
@@ -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)
}
})
}
}