Dateien nach "/" hochladen
This commit is contained in:
+146
@@ -0,0 +1,146 @@
|
|||||||
|
package audit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/mailarchive/internal/audit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestAudit(t *testing.T) *audit.Logger {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Discard, nil))
|
||||||
|
l, err := audit.New(filepath.Join(dir, "audit.db"), dir, logger)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("audit.New: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { l.Close() })
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mailarchive/internal/auth"
|
||||||
|
"github.com/mailarchive/internal/userstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestAuth(t *testing.T) (*auth.Manager, *userstore.Store) {
|
||||||
|
t.Helper()
|
||||||
|
store, err := userstore.New(filepath.Join(t.TempDir(), "users.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("userstore.New: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { store.Close() })
|
||||||
|
|
||||||
|
// Seed a test user
|
||||||
|
store.Create(userstore.CreateUserRequest{
|
||||||
|
Username: "testadmin",
|
||||||
|
Email: "admin@example.com",
|
||||||
|
Password: "adminpass",
|
||||||
|
Role: userstore.RoleAdmin,
|
||||||
|
})
|
||||||
|
store.Create(userstore.CreateUserRequest{
|
||||||
|
Username: "regularuser",
|
||||||
|
Email: "user@example.com",
|
||||||
|
Password: "userpass",
|
||||||
|
Role: userstore.RoleUser,
|
||||||
|
})
|
||||||
|
|
||||||
|
mgr := auth.New(store, nil, "test-jwt-secret-32chars-long-enough")
|
||||||
|
return mgr, store
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginSuccess(t *testing.T) {
|
||||||
|
mgr, _ := newTestAuth(t)
|
||||||
|
|
||||||
|
token, user, err := mgr.Login("testadmin", "adminpass")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Login: %v", err)
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
t.Error("expected non-empty token")
|
||||||
|
}
|
||||||
|
if user.Username != "testadmin" {
|
||||||
|
t.Errorf("Username = %q", user.Username)
|
||||||
|
}
|
||||||
|
if user.Role != userstore.RoleAdmin {
|
||||||
|
t.Errorf("Role = %q", user.Role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginWrongPassword(t *testing.T) {
|
||||||
|
mgr, _ := newTestAuth(t)
|
||||||
|
|
||||||
|
if _, _, err := mgr.Login("testadmin", "wrongpass"); err == nil {
|
||||||
|
t.Error("expected error for wrong password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginUnknownUser(t *testing.T) {
|
||||||
|
mgr, _ := newTestAuth(t)
|
||||||
|
|
||||||
|
if _, _, err := mgr.Login("nobody", "pw"); err == nil {
|
||||||
|
t.Error("expected error for unknown user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenValidation(t *testing.T) {
|
||||||
|
mgr, _ := newTestAuth(t)
|
||||||
|
|
||||||
|
token, _, _ := mgr.Login("testadmin", "adminpass")
|
||||||
|
sess, err := mgr.ValidateToken(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ValidateToken: %v", err)
|
||||||
|
}
|
||||||
|
if sess.Username != "testadmin" {
|
||||||
|
t.Errorf("Session Username = %q", sess.Username)
|
||||||
|
}
|
||||||
|
if sess.Role != userstore.RoleAdmin {
|
||||||
|
t.Errorf("Session Role = %q", sess.Role)
|
||||||
|
}
|
||||||
|
if sess.JTI == "" {
|
||||||
|
t.Error("Session JTI should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenTampering(t *testing.T) {
|
||||||
|
mgr, _ := newTestAuth(t)
|
||||||
|
|
||||||
|
token, _, _ := mgr.Login("testadmin", "adminpass")
|
||||||
|
tampered := token + "x"
|
||||||
|
|
||||||
|
if _, err := mgr.ValidateToken(tampered); err == nil {
|
||||||
|
t.Error("tampered token should fail validation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogout(t *testing.T) {
|
||||||
|
mgr, _ := newTestAuth(t)
|
||||||
|
|
||||||
|
token, _, _ := mgr.Login("testadmin", "adminpass")
|
||||||
|
|
||||||
|
// Token valid before logout
|
||||||
|
if _, err := mgr.ValidateToken(token); err != nil {
|
||||||
|
t.Fatalf("token should be valid before logout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.Logout(token); err != nil {
|
||||||
|
t.Fatalf("Logout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token invalid after logout
|
||||||
|
if _, err := mgr.ValidateToken(token); err == nil {
|
||||||
|
t.Error("token should be invalid after logout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasRole(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
userRole string
|
||||||
|
required string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{userstore.RoleAdmin, userstore.RoleAdmin, true},
|
||||||
|
{userstore.RoleAdmin, userstore.RoleAuditor, true},
|
||||||
|
{userstore.RoleAdmin, userstore.RoleUser, true},
|
||||||
|
{userstore.RoleAuditor, userstore.RoleAdmin, false},
|
||||||
|
{userstore.RoleAuditor, userstore.RoleAuditor, true},
|
||||||
|
{userstore.RoleAuditor, userstore.RoleUser, true},
|
||||||
|
{userstore.RoleUser, userstore.RoleAdmin, false},
|
||||||
|
{userstore.RoleUser, userstore.RoleAuditor, false},
|
||||||
|
{userstore.RoleUser, userstore.RoleUser, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := auth.HasRole(tt.userRole, tt.required)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("HasRole(%q, %q) = %v, want %v", tt.userRole, tt.required, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultipleSessionsIndependent(t *testing.T) {
|
||||||
|
mgr, _ := newTestAuth(t)
|
||||||
|
|
||||||
|
token1, _, _ := mgr.Login("testadmin", "adminpass")
|
||||||
|
token2, _, _ := mgr.Login("testadmin", "adminpass")
|
||||||
|
|
||||||
|
if token1 == token2 {
|
||||||
|
t.Error("two logins should produce different tokens (different JTIs)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout session 1, session 2 should still work
|
||||||
|
mgr.Logout(token1)
|
||||||
|
if _, err := mgr.ValidateToken(token2); err != nil {
|
||||||
|
t.Errorf("session 2 should still be valid after session 1 logout: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user