d360c9a5ba
- 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>
180 lines
4.9 KiB
Go
180 lines
4.9 KiB
Go
package audit_test
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/archivmail/internal/audit"
|
|
)
|
|
|
|
func newTestAudit(t *testing.T) *audit.Logger {
|
|
t.Helper()
|
|
dsn := os.Getenv("TEST_DATABASE_URL")
|
|
if dsn == "" {
|
|
t.Skip("TEST_DATABASE_URL not set — skipping (needs PostgreSQL)")
|
|
}
|
|
schema := "autest_" + strings.ToLower(strings.ReplaceAll(t.Name(), "/", "_"))
|
|
// truncate schema name to 63 chars (PostgreSQL limit)
|
|
if len(schema) > 63 {
|
|
schema = schema[:63]
|
|
}
|
|
|
|
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)
|
|
|
|
sep := "?"
|
|
if strings.Contains(dsn, "?") {
|
|
sep = "&"
|
|
}
|
|
schemaDSN := dsn + sep + "search_path=" + schema
|
|
|
|
logger := slog.New(slog.NewTextHandler(os.Discard, nil))
|
|
l, err := audit.New(schemaDSN, t.TempDir(), logger)
|
|
if err != nil {
|
|
t.Fatalf("audit.New: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
l.Close()
|
|
conn2, _ := pgx.Connect(context.Background(), dsn)
|
|
if conn2 != nil {
|
|
conn2.Exec(context.Background(), "DROP SCHEMA "+schema+" CASCADE")
|
|
conn2.Close(context.Background())
|
|
}
|
|
})
|
|
return l
|
|
}
|
|
|
|
func TestLogAndQuery(t *testing.T) {
|
|
l := newTestAudit(t)
|
|
|
|
l.Log(audit.Entry{
|
|
EventType: audit.EventLogin,
|
|
Username: "alice",
|
|
IPAddress: "192.168.1.1",
|
|
Success: true,
|
|
})
|
|
l.Log(audit.Entry{
|
|
EventType: audit.EventSearch,
|
|
Username: "alice",
|
|
IPAddress: "192.168.1.1",
|
|
Query: "invoice",
|
|
Success: true,
|
|
})
|
|
l.Log(audit.Entry{
|
|
EventType: audit.EventLogin,
|
|
Username: "bob",
|
|
IPAddress: "10.0.0.1",
|
|
Success: false,
|
|
Detail: "wrong password",
|
|
})
|
|
|
|
all, total, err := l.Query(audit.QueryFilter{PageSize: 50})
|
|
if err != nil {
|
|
t.Fatalf("Query all: %v", err)
|
|
}
|
|
if total != 3 {
|
|
t.Errorf("expected 3 entries, got %d", total)
|
|
}
|
|
_ = all
|
|
}
|
|
|
|
func TestQueryByUsername(t *testing.T) {
|
|
l := newTestAudit(t)
|
|
|
|
l.Log(audit.Entry{EventType: audit.EventLogin, Username: "alice", Success: true})
|
|
l.Log(audit.Entry{EventType: audit.EventSearch, Username: "alice", Success: true})
|
|
l.Log(audit.Entry{EventType: audit.EventLogin, Username: "bob", Success: true})
|
|
|
|
entries, total, _ := l.Query(audit.QueryFilter{Username: "alice", PageSize: 50})
|
|
if total != 2 {
|
|
t.Errorf("alice: expected 2 entries, got %d", total)
|
|
}
|
|
for _, e := range entries {
|
|
if e.Username != "alice" {
|
|
t.Errorf("got entry for user %q in alice filter", e.Username)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQueryByEventType(t *testing.T) {
|
|
l := newTestAudit(t)
|
|
|
|
l.Log(audit.Entry{EventType: audit.EventLogin, Username: "alice", Success: true})
|
|
l.Log(audit.Entry{EventType: audit.EventSearch, Username: "alice", Success: true})
|
|
l.Log(audit.Entry{EventType: audit.EventMailView, Username: "alice", MailID: "abc123", Success: true})
|
|
|
|
_, total, _ := l.Query(audit.QueryFilter{EventType: audit.EventSearch, PageSize: 50})
|
|
if total != 1 {
|
|
t.Errorf("search event filter: expected 1, got %d", total)
|
|
}
|
|
}
|
|
|
|
func TestQueryByMailID(t *testing.T) {
|
|
l := newTestAudit(t)
|
|
|
|
l.Log(audit.Entry{EventType: audit.EventMailView, Username: "alice", MailID: "mail-001", Success: true})
|
|
l.Log(audit.Entry{EventType: audit.EventMailView, Username: "bob", MailID: "mail-001", Success: true})
|
|
l.Log(audit.Entry{EventType: audit.EventMailView, Username: "alice", MailID: "mail-002", Success: true})
|
|
|
|
_, total, _ := l.Query(audit.QueryFilter{MailID: "mail-001", PageSize: 50})
|
|
if total != 2 {
|
|
t.Errorf("mailID filter: expected 2, got %d", total)
|
|
}
|
|
}
|
|
|
|
func TestQueryDateRange(t *testing.T) {
|
|
l := newTestAudit(t)
|
|
|
|
l.Log(audit.Entry{EventType: audit.EventLogin, Username: "alice", Success: true})
|
|
l.Log(audit.Entry{EventType: audit.EventLogin, Username: "bob", Success: true})
|
|
|
|
// Query with future date range — should return 0
|
|
future := time.Now().Add(24 * time.Hour)
|
|
futureEnd := time.Now().Add(48 * time.Hour)
|
|
_, total, _ := l.Query(audit.QueryFilter{From: &future, To: &futureEnd, PageSize: 50})
|
|
if total != 0 {
|
|
t.Errorf("future date range should return 0, got %d", total)
|
|
}
|
|
|
|
// Query with past-to-now range — should return all
|
|
past := time.Now().Add(-1 * time.Minute)
|
|
now := time.Now().Add(1 * time.Minute)
|
|
_, total, _ = l.Query(audit.QueryFilter{From: &past, To: &now, PageSize: 50})
|
|
if total != 2 {
|
|
t.Errorf("current date range should return 2, got %d", total)
|
|
}
|
|
}
|
|
|
|
func TestQueryPagination(t *testing.T) {
|
|
l := newTestAudit(t)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
l.Log(audit.Entry{EventType: audit.EventSearch, Username: "alice", Success: true})
|
|
}
|
|
|
|
page0, total, _ := l.Query(audit.QueryFilter{PageSize: 4, Page: 0})
|
|
_, _, _ = l.Query(audit.QueryFilter{PageSize: 4, Page: 1})
|
|
page2, _, _ := l.Query(audit.QueryFilter{PageSize: 4, Page: 2})
|
|
|
|
if total != 10 {
|
|
t.Errorf("total = %d, want 10", total)
|
|
}
|
|
if len(page0) != 4 {
|
|
t.Errorf("page 0 len = %d, want 4", len(page0))
|
|
}
|
|
if len(page2) != 2 {
|
|
t.Errorf("page 2 len = %d, want 2", len(page2))
|
|
}
|
|
}
|