fetch_ml/tests/unit/audit/alert_test.go
Jeremie Fraeys a981e89005
feat(security): add audit subsystem and tenant isolation
Implement comprehensive audit and security infrastructure:
- Immutable audit logs with platform-specific backends (Linux/Other)
- Sealed log entries with tamper-evident checksums
- Audit alert system for real-time security notifications
- Log rotation with retention policies
- Checkpoint-based audit verification

Add multi-tenant security features:
- Tenant manager with quota enforcement
- Middleware for tenant authentication/authorization
- Per-tenant cryptographic key isolation
- Supply chain security for container verification
- Cross-platform secure file utilities (Unix/Windows)

Add test coverage:
- Unit tests for audit alerts and sealed logs
- Platform-specific audit backend tests
2026-02-26 12:03:45 -05:00

171 lines
4 KiB
Go

package audit_test
import (
"context"
"strings"
"testing"
"time"
"github.com/jfraeys/fetch_ml/internal/audit"
)
// mockLogger implements the logger interface for testing
type mockLogger struct {
errors []string
warns []string
}
func (m *mockLogger) Error(msg string, keysAndValues ...any) {
m.errors = append(m.errors, msg)
}
func (m *mockLogger) Warn(msg string, keysAndValues ...any) {
m.warns = append(m.warns, msg)
}
func TestLoggingAlerter_CriticalAlert(t *testing.T) {
mock := &mockLogger{}
alerter := audit.NewLoggingAlerter(mock)
alert := audit.TamperAlert{
DetectedAt: time.Now(),
Severity: "critical",
Description: "TAMPERING DETECTED in audit log",
FilePath: "/var/log/audit/audit.log",
ExpectedHash: "abc123",
ActualHash: "def456",
}
err := alerter.Alert(context.Background(), alert)
if err != nil {
t.Fatalf("Alert failed: %v", err)
}
if len(mock.errors) != 1 {
t.Errorf("Expected 1 error log, got %d", len(mock.errors))
}
if !strings.Contains(mock.errors[0], "TAMPERING DETECTED") {
t.Errorf("Expected error to contain 'TAMPERING DETECTED', got %s", mock.errors[0])
}
if len(mock.warns) != 0 {
t.Errorf("Expected 0 warn logs, got %d", len(mock.warns))
}
}
func TestLoggingAlerter_WarningAlert(t *testing.T) {
mock := &mockLogger{}
alerter := audit.NewLoggingAlerter(mock)
alert := audit.TamperAlert{
DetectedAt: time.Now(),
Severity: "warning",
Description: "Potential tampering detected",
FilePath: "/var/log/audit/audit.log",
}
err := alerter.Alert(context.Background(), alert)
if err != nil {
t.Fatalf("Alert failed: %v", err)
}
if len(mock.warns) != 1 {
t.Errorf("Expected 1 warn log, got %d", len(mock.warns))
}
if !strings.Contains(mock.warns[0], "Potential tampering") {
t.Errorf("Expected warn to contain 'Potential tampering', got %s", mock.warns[0])
}
if len(mock.errors) != 0 {
t.Errorf("Expected 0 error logs, got %d", len(mock.errors))
}
}
func TestLoggingAlerter_NilLogger(t *testing.T) {
alerter := audit.NewLoggingAlerter(nil)
alert := audit.TamperAlert{
DetectedAt: time.Now(),
Severity: "critical",
Description: "Test",
}
// Should not panic or error
err := alerter.Alert(context.Background(), alert)
if err != nil {
t.Fatalf("Alert with nil logger failed: %v", err)
}
}
func TestMultiAlerter(t *testing.T) {
mock1 := &mockLogger{}
mock2 := &mockLogger{}
alerter1 := audit.NewLoggingAlerter(mock1)
alerter2 := audit.NewLoggingAlerter(mock2)
multi := audit.NewMultiAlerter(alerter1, alerter2)
alert := audit.TamperAlert{
DetectedAt: time.Now(),
Severity: "critical",
Description: "Test multi",
}
err := multi.Alert(context.Background(), alert)
if err != nil {
t.Fatalf("Multi alert failed: %v", err)
}
// Both alerters should have logged
if len(mock1.errors) != 1 {
t.Errorf("Expected alerter1 to have 1 error, got %d", len(mock1.errors))
}
if len(mock2.errors) != 1 {
t.Errorf("Expected alerter2 to have 1 error, got %d", len(mock2.errors))
}
}
func TestMultiAlerter_Empty(t *testing.T) {
multi := audit.NewMultiAlerter()
alert := audit.TamperAlert{
DetectedAt: time.Now(),
Severity: "critical",
Description: "Test empty",
}
// Should not error with no alerters
err := multi.Alert(context.Background(), alert)
if err != nil {
t.Fatalf("Multi alert with no alerters failed: %v", err)
}
}
func TestTamperAlert_Struct(t *testing.T) {
now := time.Now()
alert := audit.TamperAlert{
DetectedAt: now,
Severity: "critical",
Description: "Test description",
ExpectedHash: "expected",
ActualHash: "actual",
FilePath: "/path/to/file",
}
if alert.DetectedAt != now {
t.Error("DetectedAt mismatch")
}
if alert.Severity != "critical" {
t.Error("Severity mismatch")
}
if alert.Description != "Test description" {
t.Error("Description mismatch")
}
if alert.ExpectedHash != "expected" {
t.Error("ExpectedHash mismatch")
}
if alert.ActualHash != "actual" {
t.Error("ActualHash mismatch")
}
if alert.FilePath != "/path/to/file" {
t.Error("FilePath mismatch")
}
}