Ebene 2 ("Antrag stellen") als einseitiges Formular statt mehrseitigem
Assistenten — jede zusätzliche Seite kostet Zeit, und laut Spezifikation
wird ein Antrag umgangen, wenn er länger als fünf Minuten dauert.
- GET /antraege/neu: Fragebogen-Formular (Abschnitte A-D exakt nach
Spezifikation). Adaptive Folgefragen (C2: welche Art von Entscheidung,
C3: welche Art von Erkennung) werden rein per CSS :has() ein-/
ausgeblendet, kein JavaScript nötig — visuell mit Chromium-Screenshots
verifiziert (unchecked vs. checked).
- POST /antraege: legt an und reicht direkt ein (entwurf->eingereicht in
einem Schritt, kein Zwischenspeichern als Entwurf für v1). Die
antworten-JSON nutzt exakt dieselben Fakten-Schlüssel wie
rules/*.yaml (b1-b7, c1-c5, c2_folge, c3_art) — Schritt 3 kann sie
direkt auswerten, ohne Felder umzubenennen. "Unsicher" wird bewusst
NICHT zu "ja" normalisiert (das ist eine Auswertungsregel für Schritt
3, keine Speicherregel) — der Antrag hält fest, was der Mitarbeiter
tatsächlich geantwortet hat.
- GET /antraege: eigene Anträge (Ebene 2 sieht nur eigene, nicht die
des ganzen Mandanten — dafür neue Store-Methode
ListAntraegeForUser, getrennt von ListAntraegeForAccount für den
späteren Ebene-3-Posteingang).
- GET /antraege/{id}: Detail, fremder Antrag liefert 404 (nicht 403,
gleiches Muster wie überall sonst im Projekt).
- betreiber-Rolle wird von allen Antrags-Routen weggeleitet (Ebene 5
ist technisch getrennt vom Mandantenbereich).
rules/OPEN.md Punkt 3 (fehlende Fragebogen-Unterscheidung für die
"verboten"-Varianten) damit geklärt: c3_art existiert jetzt mit den
bereits in rules/kivo_einstufung.yaml erwarteten Werten.
Volle Testsuite inkl. echter Postgres-Tests grün; End-to-End gegen
einen laufenden Server verifiziert (Antrag anlegen, antworten-JSON in
der DB korrekt, Detail-/Listenansicht, Mandantentrennung).
615 lines
19 KiB
Go
615 lines
19 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
|
|
abteilungen map[string][]store.Abteilung // accountID -> Abteilungen
|
|
antraege map[string]store.Antrag
|
|
}
|
|
|
|
func newFakeStore() *fakeStore {
|
|
return &fakeStore{
|
|
accounts: map[string]store.Account{},
|
|
users: map[string]store.User{},
|
|
usersByEmail: map[string]string{},
|
|
sessions: map[string]store.Session{},
|
|
abteilungen: map[string][]store.Abteilung{},
|
|
antraege: map[string]store.Antrag{},
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (f *fakeStore) ListAbteilungenForAccount(ctx context.Context, accountID string) ([]store.Abteilung, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.abteilungen[accountID], nil
|
|
}
|
|
|
|
func (f *fakeStore) CreateAntrag(ctx context.Context, accountID, erstellerUserID string, abteilungID *string, titel string) (store.Antrag, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
a := store.Antrag{
|
|
ID: f.newID(), AccountID: accountID, ErstellerUserID: erstellerUserID, AbteilungID: abteilungID,
|
|
Titel: titel, Status: "entwurf", Antworten: []byte(`{}`), CreatedAt: time.Now(), UpdatedAt: time.Now(),
|
|
}
|
|
f.antraege[a.ID] = a
|
|
return a, nil
|
|
}
|
|
|
|
func (f *fakeStore) GetAntrag(ctx context.Context, id string) (store.Antrag, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
a, ok := f.antraege[id]
|
|
if !ok {
|
|
return store.Antrag{}, store.ErrNotFound
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
func (f *fakeStore) UpdateAntragFelder(ctx context.Context, id, titel, beschreibung, ergebnis, haeufigkeit string, antworten []byte) (store.Antrag, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
a, ok := f.antraege[id]
|
|
if !ok {
|
|
return store.Antrag{}, store.ErrNotFound
|
|
}
|
|
a.Titel, a.Beschreibung, a.Ergebnis, a.Haeufigkeit, a.Antworten = titel, beschreibung, ergebnis, haeufigkeit, antworten
|
|
a.UpdatedAt = time.Now()
|
|
f.antraege[id] = a
|
|
return a, nil
|
|
}
|
|
|
|
func (f *fakeStore) SetAntragStatus(ctx context.Context, id, status string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
a, ok := f.antraege[id]
|
|
if !ok {
|
|
return store.ErrNotFound
|
|
}
|
|
a.Status = status
|
|
f.antraege[id] = a
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeStore) ListAntraegeForUser(ctx context.Context, erstellerUserID string) ([]store.Antrag, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
var out []store.Antrag
|
|
for _, a := range f.antraege {
|
|
if a.ErstellerUserID == erstellerUserID {
|
|
out = append(out, a)
|
|
}
|
|
}
|
|
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())
|
|
}
|
|
}
|