feat(PROJ-17): Admin Dashboard Systemauslastung immer anzeigen

- Systemauslastungs-Sektion wird immer gerendert (nicht nur bei Erfolg)
- Fehlermeldung wenn /api/admin/system/stats nicht erreichbar ist
- Feature-Status auf In Review gesetzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-14 11:43:19 +01:00
parent a893084a88
commit d360c9a5ba
68 changed files with 11938 additions and 435 deletions
+279
View File
@@ -0,0 +1,279 @@
package userstore_test
import (
"context"
"os"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5"
"github.com/archivmail/internal/userstore"
)
func newTestStore(t *testing.T) *userstore.Store {
t.Helper()
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
t.Skip("TEST_DATABASE_URL not set — skipping (needs PostgreSQL)")
}
// Use a unique schema per test to isolate
schema := "test_" + strings.ReplaceAll(t.Name(), "/", "_")
schema = strings.ToLower(schema)
// Append schema to DSN
sep := "?"
if strings.Contains(dsn, "?") {
sep = "&"
}
schemaDSN := dsn + sep + "search_path=" + schema
// Create schema
ctx := context.Background()
conn, err := pgx.Connect(ctx, dsn)
if err != nil {
t.Fatalf("connect: %v", err)
}
conn.Exec(ctx, "CREATE SCHEMA IF NOT EXISTS "+schema)
conn.Close(ctx)
s, err := userstore.New(schemaDSN)
if err != nil {
t.Fatalf("userstore.New: %v", err)
}
t.Cleanup(func() {
s.Close()
conn2, _ := pgx.Connect(context.Background(), dsn)
if conn2 != nil {
conn2.Exec(context.Background(), "DROP SCHEMA "+schema+" CASCADE")
conn2.Close(context.Background())
}
})
return s
}
func TestCreateAndGetUser(t *testing.T) {
s := newTestStore(t)
u, err := s.Create(userstore.CreateUserRequest{
Username: "alice",
Email: "alice@example.com",
Password: "secret123",
Role: userstore.RoleAdmin,
})
if err != nil {
t.Fatalf("Create: %v", err)
}
if u.ID == 0 {
t.Error("expected non-zero ID")
}
if u.Username != "alice" {
t.Errorf("Username = %q", u.Username)
}
if u.Role != userstore.RoleAdmin {
t.Errorf("Role = %q", u.Role)
}
if u.Source != "local" {
t.Errorf("Source = %q, want local", u.Source)
}
got, err := s.GetByID(u.ID)
if err != nil {
t.Fatalf("GetByID: %v", err)
}
if got.Email != "alice@example.com" {
t.Errorf("Email = %q", got.Email)
}
}
func TestVerifyPassword(t *testing.T) {
s := newTestStore(t)
_, err := s.Create(userstore.CreateUserRequest{
Username: "bob", Email: "bob@example.com",
Password: "correcthorse", Role: userstore.RoleUser,
})
if err != nil {
t.Fatal(err)
}
// Correct password
u, err := s.VerifyPassword("bob", "correcthorse")
if err != nil {
t.Errorf("VerifyPassword correct: %v", err)
}
if u.Username != "bob" {
t.Errorf("Username = %q", u.Username)
}
// Wrong password
if _, err := s.VerifyPassword("bob", "wrongpassword"); err == nil {
t.Error("expected error for wrong password")
}
// Non-existent user
if _, err := s.VerifyPassword("nobody", "x"); err == nil {
t.Error("expected error for unknown user")
}
}
func TestUpdateUser(t *testing.T) {
s := newTestStore(t)
u, _ := s.Create(userstore.CreateUserRequest{
Username: "carol", Email: "carol@old.com",
Password: "pw", Role: userstore.RoleUser,
})
newEmail := "carol@new.com"
newRole := userstore.RoleAuditor
updated, err := s.Update(u.ID, userstore.UpdateUserRequest{
Email: &newEmail,
Role: &newRole,
})
if err != nil {
t.Fatalf("Update: %v", err)
}
if updated.Email != "carol@new.com" {
t.Errorf("Email after update = %q", updated.Email)
}
if updated.Role != userstore.RoleAuditor {
t.Errorf("Role after update = %q", updated.Role)
}
}
func TestDisableUser(t *testing.T) {
s := newTestStore(t)
u, _ := s.Create(userstore.CreateUserRequest{
Username: "dave", Email: "dave@x.com",
Password: "pw", Role: userstore.RoleUser,
})
active := false
s.Update(u.ID, userstore.UpdateUserRequest{Active: &active})
if _, err := s.VerifyPassword("dave", "pw"); err == nil {
t.Error("disabled user should not be able to login")
}
}
func TestDeleteUser(t *testing.T) {
s := newTestStore(t)
u, _ := s.Create(userstore.CreateUserRequest{
Username: "eve", Email: "eve@x.com",
Password: "pw", Role: userstore.RoleUser,
})
if err := s.Delete(u.ID); err != nil {
t.Fatalf("Delete: %v", err)
}
if _, err := s.GetByID(u.ID); err == nil {
t.Error("GetByID should error after delete")
}
// Delete non-existent should error
if err := s.Delete(u.ID); err == nil {
t.Error("second delete should return error")
}
}
func TestListUsers(t *testing.T) {
s := newTestStore(t)
users := []userstore.CreateUserRequest{
{Username: "u1", Email: "u1@x.com", Password: "pw", Role: userstore.RoleUser},
{Username: "u2", Email: "u2@x.com", Password: "pw", Role: userstore.RoleAdmin},
{Username: "u3", Email: "u3@x.com", Password: "pw", Role: userstore.RoleAuditor},
{Username: "u4", Email: "u4@x.com", Password: "pw", Role: userstore.RoleUser},
}
for _, req := range users {
s.Create(req)
}
all, err := s.List("")
if err != nil {
t.Fatalf("List all: %v", err)
}
if len(all) != 4 {
t.Errorf("List all: got %d, want 4", len(all))
}
admins, _ := s.List(userstore.RoleAdmin)
if len(admins) != 1 {
t.Errorf("List admin: got %d, want 1", len(admins))
}
regular, _ := s.List(userstore.RoleUser)
if len(regular) != 2 {
t.Errorf("List user: got %d, want 2", len(regular))
}
}
func TestTokenBlacklist(t *testing.T) {
s := newTestStore(t)
jti := "test-jti-12345"
expires := time.Now().Add(1 * time.Hour)
if err := s.BlacklistToken(jti, expires); err != nil {
t.Fatalf("BlacklistToken: %v", err)
}
blacklisted, err := s.IsBlacklisted(jti)
if err != nil {
t.Fatalf("IsBlacklisted: %v", err)
}
if !blacklisted {
t.Error("token should be blacklisted")
}
// Non-blacklisted token
bl2, _ := s.IsBlacklisted("other-jti")
if bl2 {
t.Error("unknown token should not be blacklisted")
}
}
func TestCleanExpiredTokens(t *testing.T) {
s := newTestStore(t)
// Add an already-expired token
s.BlacklistToken("expired-jti", time.Now().Add(-1*time.Hour))
// Add a valid token
s.BlacklistToken("valid-jti", time.Now().Add(1*time.Hour))
if err := s.CleanExpiredTokens(); err != nil {
t.Fatalf("CleanExpiredTokens: %v", err)
}
bl, _ := s.IsBlacklisted("expired-jti")
if bl {
t.Error("expired token should be cleaned up")
}
bl2, _ := s.IsBlacklisted("valid-jti")
if !bl2 {
t.Error("valid token should still be blacklisted")
}
}
func TestUpsertLDAPUser(t *testing.T) {
s := newTestStore(t)
u, err := s.UpsertLDAPUser("ldapuser", "ldap@corp.com", userstore.RoleAuditor)
if err != nil {
t.Fatalf("UpsertLDAPUser: %v", err)
}
if u.Source != "ldap" {
t.Errorf("Source = %q, want ldap", u.Source)
}
// Second upsert should update, not duplicate
u2, err := s.UpsertLDAPUser("ldapuser", "ldap@corp.com", userstore.RoleAuditor)
if err != nil {
t.Fatalf("UpsertLDAPUser second: %v", err)
}
if u2.ID != u.ID {
t.Error("second upsert should not create a new record")
}
all, _ := s.List("")
if len(all) != 1 {
t.Errorf("expected 1 user after double upsert, got %d", len(all))
}
}