Files
patrickandClaude Sonnet 5 8b87e97183 MAP-03: user-export-email-abgleich-tenant
Export der users-Tabellen aus archivmail/archivdms (internal/usermatch)
und E-Mail-Abgleich ausschliesslich innerhalb bereits per MAP-02
freigegebener Tenants (tenantapproval.Store.Approved()). Kandidatenliste
als CSV, kein automatischer Merge -- Freigabe bleibt MAP-04.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-14 20:03:57 +00:00

87 lines
2.7 KiB
Go

package usermatch
import (
"context"
"os"
"testing"
"gitea.perlbach24.de/scripte/nexarch/internal/testdb"
)
// Integrationstest gegen eine echte Postgres-Instanz mit frei erfundenen
// Benutzer-Zeilen (E-Mails wie "synth-user-a@..."), niemals echte
// archivmail-/archivdms-Kundendaten. Siehe docs/MAP-03-PRUEFPROTOKOLL.md.
// Ohne TEST_ADMIN_DSN uebersprungen.
func TestExportUsers_Vollstaendigkeit(t *testing.T) {
adminDSN := os.Getenv("TEST_ADMIN_DSN")
if adminDSN == "" {
t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen")
}
ctx := context.Background()
pool := testdb.Pool(t, adminDSN)
defer pool.Close()
if _, err := pool.Exec(ctx, `
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
email TEXT NOT NULL,
name TEXT NOT NULL,
password_hash TEXT NOT NULL DEFAULT ''
);
`); err != nil {
t.Fatalf("schema: %v", err)
}
suffix := testdb.Suffix()
rows := []ExportedUser{
{SourceID: 1, TenantSourceID: 10, Email: "synth-user-a-" + suffix + "@synth.example", Name: "Synthetische Nutzerin A", PasswordHash: "hash-a"},
{SourceID: 2, TenantSourceID: 10, Email: "synth-user-b-" + suffix + "@synth.example", Name: "Synthetischer Nutzer B", PasswordHash: "hash-b"},
{SourceID: 3, TenantSourceID: 11, Email: "synth-user-c-" + suffix + "@synth.example", Name: "Synthetischer Nutzer C", PasswordHash: "hash-c"},
}
for _, r := range rows {
if _, err := pool.Exec(ctx, `INSERT INTO users (id, tenant_id, email, name, password_hash) VALUES ($1,$2,$3,$4,$5)`,
r.SourceID, r.TenantSourceID, r.Email, r.Name, r.PasswordHash); err != nil {
t.Fatalf("insert: %v", err)
}
}
t.Cleanup(func() {
for _, r := range rows {
_, _ = pool.Exec(ctx, `DELETE FROM users WHERE email = $1`, r.Email)
}
})
wantCount, err := CountUsers(ctx, pool)
if err != nil {
t.Fatalf("CountUsers: %v", err)
}
got, err := ExportUsers(ctx, pool, "archivmail")
if err != nil {
t.Fatalf("ExportUsers: %v", err)
}
// Pruefung 1: Export-Vollstaendigkeit gegen Quelltabellen-Zeilenzahl.
if len(got) != wantCount {
t.Fatalf("export unvollstaendig: habe %d zeilen, quelltabelle hat %d", len(got), wantCount)
}
byEmail := make(map[string]ExportedUser, len(got))
for _, g := range got {
if g.Source != "archivmail" {
t.Fatalf("source nicht gesetzt: %+v", g)
}
byEmail[g.Email] = g
}
for _, r := range rows {
g, ok := byEmail[r.Email]
if !ok {
t.Fatalf("zeile %q fehlt im export", r.Email)
}
if g.Name != r.Name || g.TenantSourceID != r.TenantSourceID || g.SourceID != r.SourceID || g.PasswordHash != r.PasswordHash {
t.Fatalf("zeile %q nicht korrekt exportiert: habe %+v, erwarte %+v", r.Email, g, r)
}
}
}