Bislang gab es keine vom Nutzer-Rollenmodell (creator/agentur/marke/
kanzlei) getrennte Betreiber-Rolle — jede Verwaltungsaufgabe (welche
Kanzlei darf im öffentlichen Verzeichnis stehen, wer sind unsere
Accounts) wäre nur per Hand in der Datenbank möglich gewesen. Admin
ist von Anfang an als fünfte app_user-Rolle im Datenmodell verankert,
nicht nachträglich aufgesetzt.
Migration 0004:
- app_user.role erlaubt zusätzlich 'admin' (kein Self-Service-Weg
dorthin — /register bietet die Rolle nicht an, erster Admin wird
einmalig per SQL angelegt, siehe CLAUDE.md).
- account.verified: Freigabe fürs kostenlose Kanzlei-Verzeichnis
(§ 49b Abs. 3 BRAO: reine Auflistung, kein Routing/keine Vermittlung).
- audit_log: append-only-Protokoll jeder Admin-Aktion (gleicher Trigger
wie finding/extraction/evidence_package).
Neue Routen:
- GET /admin, /admin/accounts, /admin/accounts/{id}: Accounts-Übersicht
und -Detail (Logins je Account), requireAdmin (404 statt 403 für
angemeldete Nicht-Admins, wie beim bestehenden Mandanten-404-Muster).
- POST /admin/accounts/{id}/verifizieren: Kanzlei-Freigabe umschalten,
schreibt einen Audit-Log-Eintrag.
- GET /admin/audit-log: Protokoll ansehen.
- GET /kanzleien: öffentliches Verzeichnis (kein Login), zeigt nur
Accounts, die sowohl verified sind als auch einen Nutzer der Rolle
"kanzlei" haben.
Volle Testsuite inkl. echter Postgres-Tests grün; End-to-End manuell
gegen einen laufenden Server verifiziert (Admin-Login, Verify-Toggle,
Erscheinen im öffentlichen Verzeichnis, Audit-Log-Eintrag, 404 für
Nicht-Admin-Zugriff).
222 lines
6.2 KiB
Go
222 lines
6.2 KiB
Go
package store_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
func TestAppUserRoleAllowsAdmin(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
|
|
u, err := s.CreateUser(ctx, accID, "admin@example.com", "hash", "admin")
|
|
if err != nil {
|
|
t.Fatalf("CreateUser mit role=admin: %v", err)
|
|
}
|
|
if u.Role != "admin" {
|
|
t.Fatalf("Role = %q, want admin", u.Role)
|
|
}
|
|
}
|
|
|
|
func TestAccountVerifiedDefaultsFalseAndCanBeSet(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
acc, err := s.CreateAccount(ctx, "Kanzlei Musterfrau")
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
if acc.Verified {
|
|
t.Fatal("expected a new account to be unverified by default")
|
|
}
|
|
|
|
updated, err := s.SetAccountVerified(ctx, acc.ID, true)
|
|
if err != nil {
|
|
t.Fatalf("SetAccountVerified: %v", err)
|
|
}
|
|
if !updated.Verified {
|
|
t.Fatal("expected the account to be verified after SetAccountVerified(true)")
|
|
}
|
|
|
|
got, err := s.GetAccount(ctx, acc.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetAccount: %v", err)
|
|
}
|
|
if !got.Verified {
|
|
t.Fatal("expected verified=true to persist")
|
|
}
|
|
}
|
|
|
|
func TestSetAccountVerifiedNotFound(t *testing.T) {
|
|
s := openTestStore(t)
|
|
_, err := s.SetAccountVerified(context.Background(), "00000000-0000-0000-0000-000000000000", true)
|
|
if err == nil {
|
|
t.Fatal("expected an error for an unknown account")
|
|
}
|
|
}
|
|
|
|
func TestListAccounts(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
before, err := s.ListAccounts(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListAccounts: %v", err)
|
|
}
|
|
acc, err := s.CreateAccount(ctx, "Neuer Mandant fuer ListAccounts")
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
|
|
after, err := s.ListAccounts(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListAccounts: %v", err)
|
|
}
|
|
if len(after) != len(before)+1 {
|
|
t.Fatalf("expected exactly one more account, got %d -> %d", len(before), len(after))
|
|
}
|
|
found := false
|
|
for _, a := range after {
|
|
if a.ID == acc.ID {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("expected the newly created account in ListAccounts")
|
|
}
|
|
}
|
|
|
|
func TestListVerifiedKanzleienRequiresBothVerifiedAndKanzleiRole(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
// Verifiziert, aber kein Kanzlei-Nutzer -> darf nicht auftauchen.
|
|
verifiedNonKanzlei, err := s.CreateAccount(ctx, "Verifizierte Marke")
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
if _, err := s.CreateUser(ctx, verifiedNonKanzlei.ID, "marke@example.com", "hash", "marke"); err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
if _, err := s.SetAccountVerified(ctx, verifiedNonKanzlei.ID, true); err != nil {
|
|
t.Fatalf("SetAccountVerified: %v", err)
|
|
}
|
|
|
|
// Kanzlei-Nutzer, aber nicht verifiziert -> darf nicht auftauchen.
|
|
unverifiedKanzlei, err := s.CreateAccount(ctx, "Unverifizierte Kanzlei")
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
if _, err := s.CreateUser(ctx, unverifiedKanzlei.ID, "unverifiziert@example.com", "hash", "kanzlei"); err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
|
|
// Beides erfuellt -> muss auftauchen.
|
|
verifiedKanzlei, err := s.CreateAccount(ctx, "Verifizierte Kanzlei")
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
if _, err := s.CreateUser(ctx, verifiedKanzlei.ID, "verifiziert@example.com", "hash", "kanzlei"); err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
if _, err := s.SetAccountVerified(ctx, verifiedKanzlei.ID, true); err != nil {
|
|
t.Fatalf("SetAccountVerified: %v", err)
|
|
}
|
|
|
|
list, err := s.ListVerifiedKanzleien(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListVerifiedKanzleien: %v", err)
|
|
}
|
|
byID := map[string]bool{}
|
|
for _, a := range list {
|
|
byID[a.ID] = true
|
|
}
|
|
if byID[verifiedNonKanzlei.ID] {
|
|
t.Error("verified non-kanzlei account should not appear in the directory")
|
|
}
|
|
if byID[unverifiedKanzlei.ID] {
|
|
t.Error("unverified kanzlei account should not appear in the directory")
|
|
}
|
|
if !byID[verifiedKanzlei.ID] {
|
|
t.Error("expected the verified kanzlei account in the directory")
|
|
}
|
|
}
|
|
|
|
func TestListUsersForAccount(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
otherAccID := testAccountID(t, s)
|
|
|
|
if _, err := s.CreateUser(ctx, accID, "eins@example.com", "hash", "creator"); err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
if _, err := s.CreateUser(ctx, accID, "zwei@example.com", "hash", "agentur"); err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
if _, err := s.CreateUser(ctx, otherAccID, "fremd@example.com", "hash", "marke"); err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
|
|
list, err := s.ListUsersForAccount(ctx, accID)
|
|
if err != nil {
|
|
t.Fatalf("ListUsersForAccount: %v", err)
|
|
}
|
|
if len(list) != 2 {
|
|
t.Fatalf("expected exactly 2 users for this account, got %d: %+v", len(list), list)
|
|
}
|
|
}
|
|
|
|
func TestAuditLogCreateAndList(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
admin, err := s.CreateUser(ctx, accID, "admin-audit@example.com", "hash", "admin")
|
|
if err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
|
|
entry, err := s.CreateAuditEntry(ctx, admin.ID, "account.verified", "account", accID, "manuell freigegeben")
|
|
if err != nil {
|
|
t.Fatalf("CreateAuditEntry: %v", err)
|
|
}
|
|
if entry.ActorUserID != admin.ID {
|
|
t.Fatalf("ActorUserID = %q, want %q", entry.ActorUserID, admin.ID)
|
|
}
|
|
|
|
list, err := s.ListAuditLog(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("ListAuditLog: %v", err)
|
|
}
|
|
if len(list) == 0 {
|
|
t.Fatal("expected at least one audit entry")
|
|
}
|
|
if list[0].ID != entry.ID {
|
|
t.Fatalf("expected the newest entry first, got %+v", list[0])
|
|
}
|
|
}
|
|
|
|
func TestAuditLogIsAppendOnly(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
admin, err := s.CreateUser(ctx, accID, "admin-appendonly@example.com", "hash", "admin")
|
|
if err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
entry, err := s.CreateAuditEntry(ctx, admin.ID, "account.verified", "account", accID, "")
|
|
if err != nil {
|
|
t.Fatalf("CreateAuditEntry: %v", err)
|
|
}
|
|
|
|
_, err = s.Pool.Exec(ctx, `UPDATE audit_log SET action = 'geaendert' WHERE id = $1`, entry.ID)
|
|
if err == nil {
|
|
t.Fatal("expected UPDATE on audit_log to be rejected by the append-only trigger")
|
|
}
|
|
_, err = s.Pool.Exec(ctx, `DELETE FROM audit_log WHERE id = $1`, entry.ID)
|
|
if err == nil {
|
|
t.Fatal("expected DELETE on audit_log to be rejected by the append-only trigger")
|
|
}
|
|
}
|