Deklarix war eine Pre-Publish-Kennzeichnungsprüfung für Werbe-Content
(UWG/MStV). Dieser Scope wird komplett verworfen und durch eine
KI-Antragsprüfung ersetzt: Mitarbeitende beschreiben ein KI-Vorhaben,
das System leitet Datenklasse und KI-VO-Einstufung ab, gleicht sie
gegen einen Werkzeugkatalog ab und erzeugt einen Entscheidungsvorschlag
mit Herleitung — ein Mensch entscheidet, das System bereitet nur vor.
BREAKING CHANGE: Migration 0008 droppt alle werberechtsspezifischen
Tabellen (submission, finding, extraction, evidence_package,
participant, platform_connection, asset). account/app_user/session/
audit_log bleiben (Mandantentrennung, Login, Protokollierung sind
produktunabhängig) — app_user.role wechselt von
creator/agentur/marke/kanzlei/admin zu den fünf neuen Rollen
mitarbeiter/verantwortlicher/pruefer/admin/betreiber (vier
Mandanten-Rollen + eine plattformweite, siehe CLAUDE.md).
Entfernt: internal/extract, internal/dossier, internal/evidence,
internal/socialconnect, alte rules/*.yaml (UWG-Regeln), testdata/golden
— alles ausschließlich für das alte Produkt.
Neu, Phase 1 der Baureihenfolge ("Datenmodell, Regelwerk als YAML,
Katalogstruktur"):
- Store: abteilung (Stammdaten), werkzeug + werkzeug_sperre (der
eigentliche Wert des Produkts — zentral gepflegter Katalog mit
mandantenspezifischen Ergänzungen/Sperrungen, Pflichtfelder
letzte_pruefung/quelle für jede Zusicherung), antrag (Fragebogen-
Grundgerüst, Antworten als JSONB für den adaptiven Fragebogen aus
Phase 2).
- internal/rules komplett neu: lädt und validiert drei YAML-
Regelwerke (Datenklasse-Ableitung, KI-VO-Einstufung, Anforderungs-
profil) aus rules/*.yaml — noch ohne Auswertungslogik gegen echte
Fragebogen-Antworten (das ist Phase 3, bewusst erst nach dem
Fragebogen aus Phase 2, der die exakten Fakten-Feldnamen festlegt).
Offene fachliche Annahmen (Rangfolge der Datenklassen, Fragebogen-
Lücke für die "verboten"-Varianten) explizit in rules/OPEN.md
dokumentiert statt geraten.
- Web-Layer auf Minimalgerüst reduziert, das kompiliert und die neue
Rollenwelt trägt: Firma-Registrierung (Ebene 1, erster Nutzer wird
admin), Login/Logout, Plattform-Bereich (Ebene 5, nur betreiber:
Dashboard, Accounts-Übersicht, Audit-Log) — Fragebogen (Ebene 2) und
Fachebene (Ebene 3) folgen in den nächsten Phasen.
- CLAUDE.md komplett neu geschrieben: Produktbeschreibung, Fünf-Ebenen-
Rollenmodell, Fragebogen-Spezifikation, Ableitungstabellen,
Werkzeugkatalog, Bewertungslogik (geplant), Onboarding, offene
Punkte (u. a. Postgres-RLS-Frage aus der Frontend-Spezifikation
noch nicht entschieden, "Admin und Verantwortlicher gleichzeitig"
beim Onboarding noch nicht datenmodelliert).
Volle Testsuite inkl. echter Postgres-Tests grün. End-to-End gegen
einen laufenden Server verifiziert: Firma-Registrierung legt Account +
admin-Nutzer an, Betreiber-Login leitet zu /betreiber, mandanten-
übergreifende Accounts-Liste sichtbar für betreiber, 404 für
mitarbeiter auf /betreiber, 303 zu /login ohne Sitzung.
547 lines
17 KiB
Go
547 lines
17 KiB
Go
package web_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/netcell-it/deklarix/internal/auth"
|
|
"github.com/netcell-it/deklarix/internal/store"
|
|
"github.com/netcell-it/deklarix/internal/web"
|
|
)
|
|
|
|
const testSessionCookie = "deklarix_session"
|
|
|
|
// fakeStore ist eine In-Memory-Implementierung von web.Store, damit die
|
|
// Handler-Tests keine echte Postgres-Instanz brauchen.
|
|
type fakeStore struct {
|
|
mu sync.Mutex
|
|
nextID int
|
|
accounts map[string]store.Account
|
|
users map[string]store.User
|
|
usersByEmail map[string]string // email -> user id
|
|
sessions map[string]store.Session
|
|
auditLog []store.AuditEntry
|
|
}
|
|
|
|
func newFakeStore() *fakeStore {
|
|
return &fakeStore{
|
|
accounts: map[string]store.Account{},
|
|
users: map[string]store.User{},
|
|
usersByEmail: map[string]string{},
|
|
sessions: map[string]store.Session{},
|
|
}
|
|
}
|
|
|
|
func (f *fakeStore) newID() string {
|
|
f.nextID++
|
|
return fmt.Sprintf("id-%d", f.nextID)
|
|
}
|
|
|
|
func (f *fakeStore) CreateAccount(ctx context.Context, name string) (store.Account, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
acc := store.Account{ID: f.newID(), Name: name, CreatedAt: time.Now()}
|
|
f.accounts[acc.ID] = acc
|
|
return acc, nil
|
|
}
|
|
|
|
func (f *fakeStore) GetAccount(ctx context.Context, id string) (store.Account, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
acc, ok := f.accounts[id]
|
|
if !ok {
|
|
return store.Account{}, store.ErrNotFound
|
|
}
|
|
return acc, nil
|
|
}
|
|
|
|
func (f *fakeStore) ListAccounts(ctx context.Context) ([]store.Account, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
var out []store.Account
|
|
for _, acc := range f.accounts {
|
|
out = append(out, acc)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (f *fakeStore) CreateUser(ctx context.Context, accountID, email, passwordHash, role string) (store.User, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if _, exists := f.usersByEmail[email]; exists {
|
|
return store.User{}, fmt.Errorf("fakeStore: email %s bereits vergeben", email)
|
|
}
|
|
u := store.User{
|
|
ID: f.newID(), AccountID: accountID, Email: email, PasswordHash: passwordHash,
|
|
Role: role, CreatedAt: time.Now(),
|
|
}
|
|
f.users[u.ID] = u
|
|
f.usersByEmail[email] = u.ID
|
|
return u, nil
|
|
}
|
|
|
|
func (f *fakeStore) GetUserByEmail(ctx context.Context, email string) (store.User, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
id, ok := f.usersByEmail[email]
|
|
if !ok {
|
|
return store.User{}, store.ErrNotFound
|
|
}
|
|
return f.users[id], nil
|
|
}
|
|
|
|
func (f *fakeStore) GetUser(ctx context.Context, id string) (store.User, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
u, ok := f.users[id]
|
|
if !ok {
|
|
return store.User{}, store.ErrNotFound
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
func (f *fakeStore) ListUsersForAccount(ctx context.Context, accountID string) ([]store.User, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
var out []store.User
|
|
for _, u := range f.users {
|
|
if u.AccountID == accountID {
|
|
out = append(out, u)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (f *fakeStore) CreateSession(ctx context.Context, token, userID string, expiresAt time.Time) (store.Session, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
sess := store.Session{Token: token, UserID: userID, ExpiresAt: expiresAt, CreatedAt: time.Now()}
|
|
f.sessions[token] = sess
|
|
return sess, nil
|
|
}
|
|
|
|
func (f *fakeStore) GetSession(ctx context.Context, token string) (store.Session, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
sess, ok := f.sessions[token]
|
|
if !ok {
|
|
return store.Session{}, store.ErrNotFound
|
|
}
|
|
return sess, nil
|
|
}
|
|
|
|
func (f *fakeStore) DeleteSession(ctx context.Context, token string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
delete(f.sessions, token)
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeStore) CreateAuditEntry(ctx context.Context, actorUserID, action, targetType, targetID, details string) (store.AuditEntry, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
e := store.AuditEntry{
|
|
ID: f.newID(), ActorUserID: actorUserID, Action: action, TargetType: targetType,
|
|
TargetID: targetID, Details: details, CreatedAt: time.Now(),
|
|
}
|
|
f.auditLog = append(f.auditLog, e)
|
|
return e, nil
|
|
}
|
|
|
|
func (f *fakeStore) ListAuditLog(ctx context.Context, limit int) ([]store.AuditEntry, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
out := make([]store.AuditEntry, len(f.auditLog))
|
|
for i, e := range f.auditLog {
|
|
out[len(f.auditLog)-1-i] = e
|
|
}
|
|
if len(out) > limit {
|
|
out = out[:limit]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ─── Test-Setup ───────────────────────────────────────────────────────
|
|
|
|
func newServer(t *testing.T, fs *fakeStore) *web.Server {
|
|
t.Helper()
|
|
s, err := web.NewServer(fs)
|
|
if err != nil {
|
|
t.Fatalf("NewServer: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// seedAccountWithRole legt direkt im fakeStore (ohne HTTP) einen
|
|
// Account, einen Nutzer und eine gültige Sitzung an und liefert das
|
|
// Session-Cookie.
|
|
func seedAccountWithRole(t *testing.T, fs *fakeStore, accountName, email, role string) *http.Cookie {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
acc, err := fs.CreateAccount(ctx, accountName)
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
hash, err := auth.HashPassword("test-passwort-123")
|
|
if err != nil {
|
|
t.Fatalf("HashPassword: %v", err)
|
|
}
|
|
user, err := fs.CreateUser(ctx, acc.ID, email, hash, role)
|
|
if err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
token, err := auth.NewSessionToken()
|
|
if err != nil {
|
|
t.Fatalf("NewSessionToken: %v", err)
|
|
}
|
|
if _, err := fs.CreateSession(ctx, token, user.ID, time.Now().Add(time.Hour)); err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
return &http.Cookie{Name: testSessionCookie, Value: token}
|
|
}
|
|
|
|
func seedAccount(t *testing.T, fs *fakeStore, accountName, email string) *http.Cookie {
|
|
t.Helper()
|
|
return seedAccountWithRole(t, fs, accountName, email, "mitarbeiter")
|
|
}
|
|
|
|
func newAuthedTestServer(t *testing.T) (*web.Server, *fakeStore, *http.Cookie) {
|
|
t.Helper()
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
cookie := seedAccount(t, fs, "Test-Mandant", "test@example.com")
|
|
return s, fs, cookie
|
|
}
|
|
|
|
func postForm(t *testing.T, s *web.Server, cookie *http.Cookie, path string, form url.Values) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
if cookie != nil {
|
|
req.AddCookie(cookie)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
s.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
func getWithCookie(t *testing.T, s *web.Server, cookie *http.Cookie, path string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
if cookie != nil {
|
|
req.AddCookie(cookie)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
s.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
// ─── Tests: Health, statische Assets (kein Auth nötig) ────────────────
|
|
|
|
func TestHandleHealth(t *testing.T) {
|
|
s, _, _ := newAuthedTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
|
w := httptest.NewRecorder()
|
|
s.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
if w.Body.String() != `{"ok":true}` {
|
|
t.Fatalf("body = %q, want {\"ok\":true}", w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleStaticServesHTMX(t *testing.T) {
|
|
s, _, _ := newAuthedTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/static/htmx.min.js", nil)
|
|
w := httptest.NewRecorder()
|
|
s.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
if w.Body.Len() < 1000 {
|
|
t.Fatalf("htmx.min.js suspiciously small: %d bytes", w.Body.Len())
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Auth (Registrierung, Login, Logout, Zugriffsschutz) ───────
|
|
|
|
func TestRegisterThenLoginThenAccessProtectedPage(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
|
|
regResp := postForm(t, s, nil, "/register", url.Values{
|
|
"account_name": {"Meine Firma"}, "email": {"neu@example.com"},
|
|
"password": {"ein-sicheres-passwort"},
|
|
})
|
|
if regResp.Code != http.StatusSeeOther {
|
|
t.Fatalf("register status = %d, want 303, body: %s", regResp.Code, regResp.Body.String())
|
|
}
|
|
cookies := regResp.Result().Cookies()
|
|
if len(cookies) == 0 {
|
|
t.Fatal("expected a session cookie to be set after registration")
|
|
}
|
|
|
|
indexResp := getWithCookie(t, s, cookies[0], "/")
|
|
if indexResp.Code != http.StatusOK {
|
|
t.Fatalf("index status with fresh session = %d, want 200", indexResp.Code)
|
|
}
|
|
|
|
var created store.User
|
|
for _, u := range fs.users {
|
|
created = u
|
|
}
|
|
if created.Role != "admin" {
|
|
t.Fatalf("expected the first user of a new company to have role admin, got %q", created.Role)
|
|
}
|
|
}
|
|
|
|
func TestRegisterRejectsDuplicateEmail(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
form := url.Values{
|
|
"account_name": {"A"}, "email": {"doppelt@example.com"},
|
|
"password": {"ein-sicheres-passwort"},
|
|
}
|
|
if resp := postForm(t, s, nil, "/register", form); resp.Code != http.StatusSeeOther {
|
|
t.Fatalf("first register status = %d, want 303", resp.Code)
|
|
}
|
|
resp := postForm(t, s, nil, "/register", form)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("second register status = %d, want 200 (re-rendered form with error)", resp.Code)
|
|
}
|
|
if !strings.Contains(resp.Body.String(), "schon vergeben") {
|
|
t.Errorf("expected a duplicate-email error, got: %s", resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestLoginWithCorrectPassword(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
hash, err := auth.HashPassword("richtiges-passwort")
|
|
if err != nil {
|
|
t.Fatalf("HashPassword: %v", err)
|
|
}
|
|
acc, err := fs.CreateAccount(context.Background(), "Bestehender Mandant")
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
if _, err := fs.CreateUser(context.Background(), acc.ID, "bestehend@example.com", hash, "mitarbeiter"); err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
|
|
resp := postForm(t, s, nil, "/login", url.Values{"email": {"bestehend@example.com"}, "password": {"richtiges-passwort"}})
|
|
if resp.Code != http.StatusSeeOther {
|
|
t.Fatalf("login status = %d, want 303, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
if len(resp.Result().Cookies()) == 0 {
|
|
t.Fatal("expected a session cookie after successful login")
|
|
}
|
|
}
|
|
|
|
func TestLoginRedirectsBetreiberToPlatformDashboard(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
hash, err := auth.HashPassword("admin-passwort")
|
|
if err != nil {
|
|
t.Fatalf("HashPassword: %v", err)
|
|
}
|
|
acc, err := fs.CreateAccount(context.Background(), "Deklarix Betreiber")
|
|
if err != nil {
|
|
t.Fatalf("CreateAccount: %v", err)
|
|
}
|
|
if _, err := fs.CreateUser(context.Background(), acc.ID, "betreiber@example.com", hash, "betreiber"); err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
|
|
resp := postForm(t, s, nil, "/login", url.Values{"email": {"betreiber@example.com"}, "password": {"admin-passwort"}})
|
|
if resp.Code != http.StatusSeeOther {
|
|
t.Fatalf("status = %d, want 303, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
if loc := resp.Header().Get("Location"); loc != "/betreiber" {
|
|
t.Fatalf("Location = %q, want /betreiber", loc)
|
|
}
|
|
}
|
|
|
|
func TestLoginRejectsWrongPassword(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
hash, err := auth.HashPassword("richtiges-passwort")
|
|
if err != nil {
|
|
t.Fatalf("HashPassword: %v", err)
|
|
}
|
|
acc, _ := fs.CreateAccount(context.Background(), "X")
|
|
if _, err := fs.CreateUser(context.Background(), acc.ID, "x@example.com", hash, "mitarbeiter"); err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
|
|
resp := postForm(t, s, nil, "/login", url.Values{"email": {"x@example.com"}, "password": {"falsch"}})
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200 (re-rendered login form)", resp.Code)
|
|
}
|
|
if len(resp.Result().Cookies()) != 0 {
|
|
t.Fatal("expected no session cookie for a failed login")
|
|
}
|
|
}
|
|
|
|
func TestLoginRejectsUnknownEmailWithSameMessageAsWrongPassword(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
|
|
resp := postForm(t, s, nil, "/login", url.Values{"email": {"gibtsnicht@example.com"}, "password": {"irgendwas"}})
|
|
if !strings.Contains(resp.Body.String(), "E-Mail oder Passwort falsch") {
|
|
t.Errorf("expected the generic invalid-credentials message, got: %s", resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestIndexRedirectsToLoginWithoutSession(t *testing.T) {
|
|
s, _, _ := newAuthedTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
s.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusSeeOther {
|
|
t.Fatalf("status = %d, want 303 redirect to /login", w.Code)
|
|
}
|
|
if loc := w.Header().Get("Location"); loc != "/login" {
|
|
t.Fatalf("Location = %q, want /login", loc)
|
|
}
|
|
}
|
|
|
|
func TestLogoutClearsSession(t *testing.T) {
|
|
s, fs, cookie := newAuthedTestServer(t)
|
|
|
|
if resp := getWithCookie(t, s, cookie, "/"); resp.Code != http.StatusOK {
|
|
t.Fatalf("index before logout = %d, want 200", resp.Code)
|
|
}
|
|
|
|
logoutResp := postForm(t, s, cookie, "/logout", url.Values{})
|
|
if logoutResp.Code != http.StatusSeeOther {
|
|
t.Fatalf("logout status = %d, want 303", logoutResp.Code)
|
|
}
|
|
if _, err := fs.GetSession(context.Background(), cookie.Value); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatal("expected the session to be deleted from the store after logout")
|
|
}
|
|
|
|
if resp := getWithCookie(t, s, cookie, "/"); resp.Code != http.StatusSeeOther {
|
|
t.Fatalf("index after logout = %d, want 303 redirect (session no longer valid)", resp.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleIndexRendersForMitarbeiter(t *testing.T) {
|
|
s, _, cookie := newAuthedTestServer(t)
|
|
|
|
resp := getWithCookie(t, s, cookie, "/")
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", resp.Code)
|
|
}
|
|
}
|
|
|
|
// ─── Tests: Plattform-Bereich (nur Betreiber) ─────────────────────────
|
|
|
|
func TestBetreiberRoutesRejectNonBetreiberWith404(t *testing.T) {
|
|
s, _, cookie := newAuthedTestServer(t)
|
|
|
|
for _, path := range []string{"/betreiber", "/betreiber/accounts", "/betreiber/audit-log"} {
|
|
resp := getWithCookie(t, s, cookie, path)
|
|
if resp.Code != http.StatusNotFound {
|
|
t.Errorf("GET %s status = %d, want 404 for a non-betreiber user", path, resp.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBetreiberRoutesRedirectToLoginWithoutSession(t *testing.T) {
|
|
s, _, _ := newAuthedTestServer(t)
|
|
|
|
resp := getWithCookie(t, s, nil, "/betreiber")
|
|
if resp.Code != http.StatusSeeOther {
|
|
t.Fatalf("status = %d, want 303 redirect to /login", resp.Code)
|
|
}
|
|
}
|
|
|
|
func TestBetreiberDashboardAccessibleForBetreiber(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
|
|
|
|
resp := getWithCookie(t, s, betreiberCookie, "/betreiber")
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestBetreiberAccountListShowsAllAccountsAcrossTenants(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
|
|
seedAccount(t, fs, "Mandant A", "a@example.com")
|
|
seedAccount(t, fs, "Mandant B", "b@example.com")
|
|
|
|
resp := getWithCookie(t, s, betreiberCookie, "/betreiber/accounts")
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
body := resp.Body.String()
|
|
for _, want := range []string{"Mandant A", "Mandant B", "Deklarix Betreiber"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("expected %q in the betreiber account list, got: %s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBetreiberAccountDetailShowsUsers(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
|
|
seedAccount(t, fs, "Mandant A", "mitarbeiter-a@example.com")
|
|
|
|
var accID string
|
|
for id, acc := range fs.accounts {
|
|
if acc.Name == "Mandant A" {
|
|
accID = id
|
|
}
|
|
}
|
|
|
|
resp := getWithCookie(t, s, betreiberCookie, "/betreiber/accounts/"+accID)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
if !strings.Contains(resp.Body.String(), "mitarbeiter-a@example.com") {
|
|
t.Errorf("expected the user's email on the account detail page, got: %s", resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestBetreiberAuditLogShowsEntries(t *testing.T) {
|
|
fs := newFakeStore()
|
|
s := newServer(t, fs)
|
|
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
|
|
betreiber, err := fs.GetUserByEmail(context.Background(), "betreiber@example.com")
|
|
if err != nil {
|
|
t.Fatalf("GetUserByEmail: %v", err)
|
|
}
|
|
if _, err := fs.CreateAuditEntry(context.Background(), betreiber.ID, "werkzeug.aktualisiert", "werkzeug", "w1", "manuell geprüft"); err != nil {
|
|
t.Fatalf("CreateAuditEntry: %v", err)
|
|
}
|
|
|
|
resp := getWithCookie(t, s, betreiberCookie, "/betreiber/audit-log")
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
|
}
|
|
if !strings.Contains(resp.Body.String(), "werkzeug.aktualisiert") {
|
|
t.Errorf("expected the audit entry on the audit log page, got: %s", resp.Body.String())
|
|
}
|
|
}
|