CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (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 / Web (dms-browser) (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
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) 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 (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
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
Tests gegen synthetische Fixtures je Akzeptanzkriterium sowie Pruefprotokoll mit real ausgefuehrten Ergebnissen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
457 lines
17 KiB
Go
457 lines
17 KiB
Go
package migrationuser
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/tenant"
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/testdb"
|
|
)
|
|
|
|
// Integrationstest gegen eine echte Postgres-Instanz mit synthetischen
|
|
// Benutzer-Map-Eintraegen (fiktive E-Mails/Namen, selbst gehashte
|
|
// Test-Passwoerter — keine echten archivmail-/archivdms-Kundendaten,
|
|
// keine echten Passwort-Hashes). Siehe Pruefprotokoll COR-02 fuer die
|
|
// dokumentierte Begruendung. Ohne TEST_ADMIN_DSN uebersprungen.
|
|
func setupRegistrySchema(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
|
|
t.Helper()
|
|
if _, err := pool.Exec(ctx, `
|
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
CREATE TABLE IF NOT EXISTS tenants (
|
|
id UUID PRIMARY KEY,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
db_name TEXT NOT NULL UNIQUE,
|
|
db_dsn TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
previous_status TEXT,
|
|
deletion_scheduled_at TIMESTAMPTZ,
|
|
retention_block_reason TEXT,
|
|
retention_checked_at TIMESTAMPTZ
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tenant_migration_map (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
archivmail_tenant_id BIGINT,
|
|
archivdms_tenant_id BIGINT,
|
|
nexarch_tenant_id UUID NOT NULL UNIQUE,
|
|
slug TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
approved_by TEXT,
|
|
approved_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT tenant_migration_map_source_present
|
|
CHECK (archivmail_tenant_id IS NOT NULL OR archivdms_tenant_id IS NOT NULL)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS user_migration_map (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
archivmail_user_id BIGINT,
|
|
archivdms_user_id BIGINT,
|
|
nexarch_tenant_id UUID NOT NULL REFERENCES tenant_migration_map(nexarch_tenant_id),
|
|
nexarch_user_id UUID NOT NULL UNIQUE,
|
|
email TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
approved_by TEXT,
|
|
approved_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
ldap_uid TEXT,
|
|
ldap_synced_at TIMESTAMPTZ,
|
|
ldap_source TEXT,
|
|
CONSTRAINT user_migration_map_source_present
|
|
CHECK (archivmail_user_id IS NOT NULL OR archivdms_user_id IS NOT NULL)
|
|
);
|
|
`); err != nil {
|
|
t.Fatalf("registry-schema: %v", err)
|
|
}
|
|
}
|
|
|
|
const tenantUsersSchema = `
|
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
password_hash TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
ldap_uid TEXT,
|
|
ldap_synced_at TIMESTAMPTZ,
|
|
ldap_source TEXT
|
|
);`
|
|
|
|
// createSynthTenantDB legt eine eigene, echte Tenant-Datenbank (users-Tabelle)
|
|
// an und registriert sie in der Registry — Stellvertreter fuer das Ergebnis
|
|
// von COR-01 (ProvisionWithID), ohne dessen gesamten Provisioner hier
|
|
// erneut aufzurufen.
|
|
func createSynthTenantDB(t *testing.T, ctx context.Context, adminDSN string, registryPool *pgxpool.Pool, tenantID string) string {
|
|
t.Helper()
|
|
adminPool, err := pgxpool.New(ctx, adminDSN)
|
|
if err != nil {
|
|
t.Fatalf("admin pool: %v", err)
|
|
}
|
|
defer adminPool.Close()
|
|
|
|
dbName := "cor02_" + testdb.Suffix()
|
|
if _, err := adminPool.Exec(ctx, fmt.Sprintf(`CREATE DATABASE %q`, dbName)); err != nil {
|
|
t.Fatalf("tenant-db anlegen: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
cleanupPool, err := pgxpool.New(context.Background(), adminDSN)
|
|
if err == nil {
|
|
_, _ = cleanupPool.Exec(context.Background(), fmt.Sprintf(`DROP DATABASE IF EXISTS %q`, dbName))
|
|
cleanupPool.Close()
|
|
}
|
|
})
|
|
|
|
dsn := strings.Replace(adminDSN, "/postgres?", "/"+dbName+"?", 1)
|
|
tenantPool, err := pgxpool.New(ctx, dsn)
|
|
if err != nil {
|
|
t.Fatalf("tenant-db verbinden: %v", err)
|
|
}
|
|
if _, err := tenantPool.Exec(ctx, tenantUsersSchema); err != nil {
|
|
t.Fatalf("tenant-schema anwenden: %v", err)
|
|
}
|
|
tenantPool.Close()
|
|
|
|
slug := "synth_" + testdb.Suffix()
|
|
if _, err := registryPool.Exec(ctx, `
|
|
INSERT INTO tenants (id, slug, name, db_name, db_dsn, status)
|
|
VALUES ($1, $2, $3, $4, $5, 'active')
|
|
`, tenantID, slug, "Synthetische Test GmbH", dbName, dsn); err != nil {
|
|
t.Fatalf("tenant registrieren: %v", err)
|
|
}
|
|
|
|
// user_migration_map referenziert tenant_migration_map(nexarch_tenant_id)
|
|
// (COR-02 setzt auf COR-01 auf) — fuer den Test genuegt der freigegebene
|
|
// Map-Eintrag selbst, ohne den vollen COR-01-Provisioner erneut laufen zu
|
|
// lassen.
|
|
if _, err := registryPool.Exec(ctx, `
|
|
INSERT INTO tenant_migration_map (archivmail_tenant_id, archivdms_tenant_id, nexarch_tenant_id, slug, name, approved_by, approved_at)
|
|
VALUES (1, NULL, $1, $2, $3, 'qa-tester', now())
|
|
`, tenantID, slug, "Synthetische Test GmbH"); err != nil {
|
|
t.Fatalf("tenant-map-eintrag anlegen: %v", err)
|
|
}
|
|
|
|
return dsn
|
|
}
|
|
|
|
func TestMigrateApproved(t *testing.T) {
|
|
adminDSN := os.Getenv("TEST_ADMIN_DSN")
|
|
if adminDSN == "" {
|
|
t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen")
|
|
}
|
|
ctx := context.Background()
|
|
|
|
registryPool := testdb.Pool(t, adminDSN)
|
|
setupRegistrySchema(t, ctx, registryPool)
|
|
registry := tenant.NewRegistry(registryPool)
|
|
|
|
tenantID := newUUIDv4()
|
|
createSynthTenantDB(t, ctx, adminDSN, registryPool, tenantID)
|
|
|
|
type synthEntry struct {
|
|
id string
|
|
email string
|
|
name string
|
|
passwordHash string
|
|
approved bool
|
|
}
|
|
|
|
suffix := testdb.Suffix()
|
|
entries := make([]synthEntry, 0, 6)
|
|
for i := 1; i <= 5; i++ {
|
|
// Fiktives Test-Passwort, selbst gehasht — kein echter archivmail-/
|
|
// archivdms-Hash.
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(fmt.Sprintf("synth-test-pw-%d-%s", i, suffix)), bcrypt.MinCost)
|
|
if err != nil {
|
|
t.Fatalf("test-hash erzeugen: %v", err)
|
|
}
|
|
entries = append(entries, synthEntry{
|
|
id: newUUIDv4(),
|
|
email: fmt.Sprintf("synth.user.%d.%s@example.invalid", i, suffix),
|
|
name: fmt.Sprintf("Synthetischer Nutzer %d %s", i, suffix),
|
|
passwordHash: string(hash),
|
|
approved: true,
|
|
})
|
|
}
|
|
unapprovedHash, err := bcrypt.GenerateFromPassword([]byte("synth-test-pw-unapproved-"+suffix), bcrypt.MinCost)
|
|
if err != nil {
|
|
t.Fatalf("test-hash erzeugen: %v", err)
|
|
}
|
|
unapproved := synthEntry{
|
|
id: newUUIDv4(),
|
|
email: "synth.unapproved." + suffix + "@example.invalid",
|
|
name: "Unfreigegeben " + suffix,
|
|
passwordHash: string(unapprovedHash),
|
|
approved: false,
|
|
}
|
|
entries = append(entries, unapproved)
|
|
|
|
for i, e := range entries {
|
|
approvedAt := "NULL"
|
|
approvedBy := "NULL"
|
|
if e.approved {
|
|
approvedAt = "now()"
|
|
approvedBy = "'qa-tester'"
|
|
}
|
|
_, 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, NULL, $2, $3, $4, $5, $6, `+approvedBy+`, `+approvedAt+`)
|
|
`, int64(2000+i), tenantID, e.id, e.email, e.name, e.passwordHash)
|
|
if err != nil {
|
|
t.Fatalf("map-eintrag %d anlegen: %v", i, err)
|
|
}
|
|
}
|
|
|
|
mapStore := NewMapStore(registryPool)
|
|
migrator := NewMigrator(mapStore, registry)
|
|
defer migrator.Close()
|
|
|
|
// AC 1: genau ein Lauf uebernimmt jeden freigegebenen Benutzer, Hash
|
|
// byte-identisch (kein Rehash).
|
|
res, err := migrator.MigrateApproved(ctx)
|
|
if err != nil {
|
|
t.Fatalf("MigrateApproved: %v", err)
|
|
}
|
|
if len(res.Migrated) != 5 {
|
|
t.Fatalf("erwarte 5 migrierte Benutzer, habe %d", len(res.Migrated))
|
|
}
|
|
|
|
tenantPool := migrator.tenantPools[tenantID]
|
|
for _, e := range entries[:5] {
|
|
var gotEmail, gotHash string
|
|
if err := tenantPool.QueryRow(ctx, `SELECT email, password_hash FROM users WHERE id = $1`, e.id).Scan(&gotEmail, &gotHash); err != nil {
|
|
t.Fatalf("benutzer %q nach migration nicht gefunden: %v", e.id, err)
|
|
}
|
|
if gotEmail != e.email {
|
|
t.Fatalf("email nicht korrekt uebernommen: erwartet %s, habe %s", e.email, gotEmail)
|
|
}
|
|
if gotHash != e.passwordHash {
|
|
t.Fatalf("passwort-hash wurde veraendert (rehash?): erwartet %s, habe %s", e.passwordHash, gotHash)
|
|
}
|
|
}
|
|
|
|
// AC 2: der unfreigegebene Eintrag wurde nicht uebernommen.
|
|
var count int
|
|
if err := tenantPool.QueryRow(ctx, `SELECT count(*) FROM users WHERE id = $1`, unapproved.id).Scan(&count); err != nil {
|
|
t.Fatalf("duplikat-pruefung unfreigegeben: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("unfreigegebener benutzer %q haette nicht uebernommen werden duerfen", unapproved.id)
|
|
}
|
|
|
|
// AC 3: erneuter Lauf gegen dieselbe (bereits migrierte) Quelle erzeugt
|
|
// keine Duplikate.
|
|
res2, err := migrator.MigrateApproved(ctx)
|
|
if err != nil {
|
|
t.Fatalf("zweiter MigrateApproved-Lauf: %v", err)
|
|
}
|
|
if len(res2.Migrated) != 5 {
|
|
t.Fatalf("erwarte weiterhin 5 Benutzer nach erneutem Lauf, habe %d", len(res2.Migrated))
|
|
}
|
|
for _, e := range entries[:5] {
|
|
var count int
|
|
if err := tenantPool.QueryRow(ctx, `SELECT count(*) FROM users WHERE id = $1`, e.id).Scan(&count); err != nil {
|
|
t.Fatalf("duplikat-pruefung: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("benutzer %q ist nach erneutem Lauf %d mal vorhanden, erwartet 1", e.id, count)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestMigrateApproved_SkipsUnknownTenant stellt sicher, dass ein Map-Eintrag
|
|
// mit nicht existierendem Tenant einen klaren Fehler liefert statt still
|
|
// Daten zu verlieren oder in die falsche Datenbank zu schreiben.
|
|
func TestMigrateApproved_UnknownTenantFails(t *testing.T) {
|
|
adminDSN := os.Getenv("TEST_ADMIN_DSN")
|
|
if adminDSN == "" {
|
|
t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen")
|
|
}
|
|
ctx := context.Background()
|
|
|
|
registryPool := testdb.Pool(t, adminDSN)
|
|
setupRegistrySchema(t, ctx, registryPool)
|
|
registry := tenant.NewRegistry(registryPool)
|
|
|
|
unknownTenantID := newUUIDv4()
|
|
userID := newUUIDv4()
|
|
suffix := testdb.Suffix()
|
|
hash, err := bcrypt.GenerateFromPassword([]byte("synth-test-pw-"+suffix), bcrypt.MinCost)
|
|
if err != nil {
|
|
t.Fatalf("test-hash erzeugen: %v", err)
|
|
}
|
|
|
|
// nexarch_tenant_id muss laut Schema in tenant_migration_map vorkommen,
|
|
// aber nicht zwingend bereits in tenants registriert sein (COR-01 kann
|
|
// zeitlich nach dieser Map-Zeile laufen) — genau dieser Fall wird hier
|
|
// geprueft.
|
|
if _, err := registryPool.Exec(ctx, `
|
|
INSERT INTO tenant_migration_map (archivmail_tenant_id, archivdms_tenant_id, nexarch_tenant_id, slug, name, approved_by, approved_at)
|
|
VALUES (9999, NULL, $1, $2, $3, 'qa-tester', now())
|
|
`, unknownTenantID, "synth_"+suffix, "Synthetische Test GmbH "+suffix); err != nil {
|
|
t.Fatalf("tenant-map-eintrag anlegen: %v", err)
|
|
}
|
|
|
|
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 (3000, NULL, $1, $2, $3, $4, $5, 'qa-tester', now())
|
|
`, unknownTenantID, userID, "synth.orphan."+suffix+"@example.invalid", "Synthetischer Nutzer "+suffix, string(hash)); err != nil {
|
|
t.Fatalf("user-map-eintrag anlegen: %v", err)
|
|
}
|
|
|
|
mapStore := NewMapStore(registryPool)
|
|
migrator := NewMigrator(mapStore, registry)
|
|
defer migrator.Close()
|
|
|
|
if _, err := migrator.MigrateApproved(ctx); err == nil {
|
|
t.Fatal("erwarte fehler bei unbekanntem tenant, habe nil")
|
|
}
|
|
}
|
|
|
|
// TestMigrateApproved_LDAPFields deckt COR-04 ab: archivdms-Konten mit
|
|
// voller LDAP-Tiefe (ldap_uid + ldap_synced_at) werden 1:1 uebernommen
|
|
// (Akzeptanzkriterium 1); ein archivmail-Konto mit nur dem einfacheren
|
|
// ldap_source-Feld ist trotz fehlender ldap_uid/ldap_synced_at als
|
|
// LDAP-gebunden erkennbar (Akzeptanzkriterium 2); ein Konto ganz ohne
|
|
// LDAP-Angaben wird trotzdem migriert (Akzeptanzkriterium 3).
|
|
func TestMigrateApproved_LDAPFields(t *testing.T) {
|
|
adminDSN := os.Getenv("TEST_ADMIN_DSN")
|
|
if adminDSN == "" {
|
|
t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen")
|
|
}
|
|
ctx := context.Background()
|
|
|
|
registryPool := testdb.Pool(t, adminDSN)
|
|
setupRegistrySchema(t, ctx, registryPool)
|
|
registry := tenant.NewRegistry(registryPool)
|
|
|
|
tenantID := newUUIDv4()
|
|
createSynthTenantDB(t, ctx, adminDSN, registryPool, tenantID)
|
|
|
|
suffix := testdb.Suffix()
|
|
|
|
archivdmsUserID := newUUIDv4()
|
|
archivdmsHash, err := bcrypt.GenerateFromPassword([]byte("synth-test-pw-dms-"+suffix), bcrypt.MinCost)
|
|
if err != nil {
|
|
t.Fatalf("test-hash erzeugen: %v", err)
|
|
}
|
|
archivdmsEmail := "synth.dms." + suffix + "@example.invalid"
|
|
archivdmsLDAPUID := "dms-ldap-uid-" + suffix
|
|
|
|
archivmailUserID := newUUIDv4()
|
|
archivmailHash, err := bcrypt.GenerateFromPassword([]byte("synth-test-pw-mail-"+suffix), bcrypt.MinCost)
|
|
if err != nil {
|
|
t.Fatalf("test-hash erzeugen: %v", err)
|
|
}
|
|
archivmailEmail := "synth.mail." + suffix + "@example.invalid"
|
|
archivmailLDAPSource := "archivmail-ldap"
|
|
|
|
plainUserID := newUUIDv4()
|
|
plainHash, err := bcrypt.GenerateFromPassword([]byte("synth-test-pw-plain-"+suffix), bcrypt.MinCost)
|
|
if err != nil {
|
|
t.Fatalf("test-hash erzeugen: %v", err)
|
|
}
|
|
plainEmail := "synth.plain." + suffix + "@example.invalid"
|
|
|
|
// archivdms: volle LDAP-Tiefe (ldap_uid + ldap_synced_at).
|
|
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, ldap_uid, ldap_synced_at)
|
|
VALUES (NULL, $1, $2, $3, $4, $5, $6, 'qa-tester', now(), $7, now())
|
|
`, int64(4001), tenantID, archivdmsUserID, archivdmsEmail, "Synthetischer DMS-Nutzer "+suffix, string(archivdmsHash), archivdmsLDAPUID); err != nil {
|
|
t.Fatalf("archivdms-map-eintrag anlegen: %v", err)
|
|
}
|
|
|
|
// archivmail: nur das einfachere ldap_source-Feld, ldap_uid/ldap_synced_at
|
|
// fehlen noch.
|
|
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, ldap_source)
|
|
VALUES ($1, NULL, $2, $3, $4, $5, $6, 'qa-tester', now(), $7)
|
|
`, int64(4002), tenantID, archivmailUserID, archivmailEmail, "Synthetischer Mail-Nutzer "+suffix, string(archivmailHash), archivmailLDAPSource); err != nil {
|
|
t.Fatalf("archivmail-map-eintrag anlegen: %v", err)
|
|
}
|
|
|
|
// Konto ganz ohne LDAP-Bezug.
|
|
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, NULL, $2, $3, $4, $5, $6, 'qa-tester', now())
|
|
`, int64(4003), tenantID, plainUserID, plainEmail, "Synthetischer Nutzer ohne LDAP "+suffix, string(plainHash)); err != nil {
|
|
t.Fatalf("map-eintrag ohne ldap 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) != 3 {
|
|
t.Fatalf("erwarte 3 migrierte Benutzer, habe %d", len(res.Migrated))
|
|
}
|
|
|
|
tenantPool := migrator.tenantPools[tenantID]
|
|
|
|
// AC 1: archivdms-Feldtiefe 1:1 uebernommen.
|
|
var dmsUID, dmsSource *string
|
|
var dmsSyncedAt *time.Time
|
|
if err := tenantPool.QueryRow(ctx, `SELECT ldap_uid, ldap_synced_at, ldap_source FROM users WHERE id = $1`, archivdmsUserID).
|
|
Scan(&dmsUID, &dmsSyncedAt, &dmsSource); err != nil {
|
|
t.Fatalf("archivdms-konto nach migration lesen: %v", err)
|
|
}
|
|
if dmsUID == nil || *dmsUID != archivdmsLDAPUID {
|
|
t.Fatalf("erwarte ldap_uid %q, habe %v", archivdmsLDAPUID, dmsUID)
|
|
}
|
|
if dmsSyncedAt == nil {
|
|
t.Fatal("erwarte gesetztes ldap_synced_at fuer archivdms-konto")
|
|
}
|
|
|
|
// AC 2: archivmail-Konto trotz fehlender ldap_uid/ldap_synced_at als
|
|
// LDAP-gebunden erkennbar (ueber ldap_source).
|
|
var mailUID, mailSource *string
|
|
var mailSyncedAt *time.Time
|
|
if err := tenantPool.QueryRow(ctx, `SELECT ldap_uid, ldap_synced_at, ldap_source FROM users WHERE id = $1`, archivmailUserID).
|
|
Scan(&mailUID, &mailSyncedAt, &mailSource); err != nil {
|
|
t.Fatalf("archivmail-konto nach migration lesen: %v", err)
|
|
}
|
|
if mailSource == nil || *mailSource != archivmailLDAPSource {
|
|
t.Fatalf("erwarte ldap_source %q, habe %v", archivmailLDAPSource, mailSource)
|
|
}
|
|
if mailUID != nil {
|
|
t.Fatalf("erwarte kein ldap_uid fuer archivmail-konto, habe %v", *mailUID)
|
|
}
|
|
|
|
// AC 3: fehlende LDAP-Felder blockieren die Migration des Kontos nicht.
|
|
var plainCount int
|
|
if err := tenantPool.QueryRow(ctx, `SELECT count(*) FROM users WHERE id = $1`, plainUserID).Scan(&plainCount); err != nil {
|
|
t.Fatalf("konto ohne ldap pruefen: %v", err)
|
|
}
|
|
if plainCount != 1 {
|
|
t.Fatalf("erwarte konto ohne ldap-bezug trotzdem migriert, habe count %d", plainCount)
|
|
}
|
|
}
|
|
|
|
func newUUIDv4() string {
|
|
b := make([]byte, 16)
|
|
if _, err := rand.Read(b); err != nil {
|
|
panic(err)
|
|
}
|
|
b[6] = (b[6] & 0x0f) | 0x40
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
|
}
|