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
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:
1 parent
1bd7e2a953
commit
a4ba108cef
2 files changed
+215
No files matched your search
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user