mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
746 lines
18 KiB
Go
746 lines
18 KiB
Go
package audit
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func TestNewSQLiteLogger(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
if logger.GetRetentionDays() != 30 {
|
|
t.Errorf("Expected retention days 30, got %d", logger.GetRetentionDays())
|
|
}
|
|
}
|
|
|
|
func TestNewSQLiteLoggerDefaultRetention(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
// RetentionDays not set
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
if logger.GetRetentionDays() != 90 {
|
|
t.Errorf("Expected default retention days 90, got %d", logger.GetRetentionDays())
|
|
}
|
|
}
|
|
|
|
func TestIsPersistentLoggerUnwrapsAsyncConsoleBackend(t *testing.T) {
|
|
console := NewConsoleLogger()
|
|
if IsPersistentLogger(console) {
|
|
t.Fatal("console logger must not be persistent")
|
|
}
|
|
|
|
asyncConsole := NewAsyncLogger(console, AsyncLoggerConfig{BufferSize: 1})
|
|
defer asyncConsole.Close()
|
|
if IsPersistentLogger(asyncConsole) {
|
|
t.Fatal("async console logger must not be persistent")
|
|
}
|
|
}
|
|
|
|
func TestAuditSQLiteStoreErrorClassification(t *testing.T) {
|
|
if !IsStoreBusyError(fmt.Errorf("query failed: %w", errors.New("database is locked (5) (SQLITE_BUSY)"))) {
|
|
t.Fatal("expected SQLITE_BUSY error to be classified as store busy")
|
|
}
|
|
if IsStoreBusyError(errors.New("no such table: audit_events")) {
|
|
t.Fatal("missing audit table must not be classified as transient busy")
|
|
}
|
|
if !IsStoreUnavailableError(errors.New("no such table: audit_events")) {
|
|
t.Fatal("missing audit table must be classified as store unavailable")
|
|
}
|
|
if !IsStoreUnavailableError(errors.New("database disk image is malformed")) {
|
|
t.Fatal("corrupt audit database must be classified as store unavailable")
|
|
}
|
|
}
|
|
|
|
func TestWithSQLiteRetryRetriesTransientBusyErrors(t *testing.T) {
|
|
previousSleep := auditSQLiteRetrySleep
|
|
auditSQLiteRetrySleep = func(time.Duration) {}
|
|
t.Cleanup(func() {
|
|
auditSQLiteRetrySleep = previousSleep
|
|
})
|
|
|
|
attempts := 0
|
|
err := withSQLiteRetry("test_retry", func() error {
|
|
attempts++
|
|
if attempts < 3 {
|
|
return errors.New("database is locked (5) (SQLITE_BUSY)")
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected retry to recover, got %v", err)
|
|
}
|
|
if attempts != 3 {
|
|
t.Fatalf("attempts = %d, want 3", attempts)
|
|
}
|
|
}
|
|
|
|
func TestWithSQLiteRetryDoesNotRetryPermanentErrors(t *testing.T) {
|
|
attempts := 0
|
|
err := withSQLiteRetry("test_no_retry", func() error {
|
|
attempts++
|
|
return errors.New("no such table: audit_events")
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected permanent error")
|
|
}
|
|
if attempts != 1 {
|
|
t.Fatalf("attempts = %d, want 1", attempts)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerLog(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
event := Event{
|
|
ID: uuid.NewString(),
|
|
Timestamp: time.Now(),
|
|
EventType: "test_event",
|
|
User: "testuser",
|
|
IP: "192.168.1.1",
|
|
Path: "/api/test",
|
|
Success: true,
|
|
Details: "test details",
|
|
}
|
|
|
|
err = logger.Log(event)
|
|
if err != nil {
|
|
t.Fatalf("Log failed: %v", err)
|
|
}
|
|
|
|
// Query the event back
|
|
events, err := logger.Query(QueryFilter{ID: event.ID})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
|
|
if len(events) != 1 {
|
|
t.Fatalf("Expected 1 event, got %d", len(events))
|
|
}
|
|
|
|
retrieved := events[0]
|
|
if retrieved.ID != event.ID {
|
|
t.Errorf("ID mismatch: expected %s, got %s", event.ID, retrieved.ID)
|
|
}
|
|
if retrieved.EventType != event.EventType {
|
|
t.Errorf("EventType mismatch: expected %s, got %s", event.EventType, retrieved.EventType)
|
|
}
|
|
if retrieved.User != event.User {
|
|
t.Errorf("User mismatch: expected %s, got %s", event.User, retrieved.User)
|
|
}
|
|
if retrieved.Success != event.Success {
|
|
t.Errorf("Success mismatch: expected %v, got %v", event.Success, retrieved.Success)
|
|
}
|
|
|
|
// Event should have a signature
|
|
if retrieved.Signature == "" {
|
|
t.Error("Expected event to have a signature")
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerQuery(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
// Log several events
|
|
now := time.Now()
|
|
events := []Event{
|
|
{ID: "e1", Timestamp: now.Add(-2 * time.Hour), EventType: "login", User: "alice", Success: true},
|
|
{ID: "e2", Timestamp: now.Add(-1 * time.Hour), EventType: "login", User: "bob", Success: true},
|
|
{ID: "e3", Timestamp: now, EventType: "logout", User: "alice", Success: true},
|
|
{ID: "e4", Timestamp: now, EventType: "login", User: "charlie", Success: false},
|
|
}
|
|
|
|
for _, e := range events {
|
|
if err := logger.Log(e); err != nil {
|
|
t.Fatalf("Log failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Test event type filter
|
|
t.Run("FilterByEventType", func(t *testing.T) {
|
|
results, err := logger.Query(QueryFilter{EventType: "login"})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
if len(results) != 3 {
|
|
t.Errorf("Expected 3 login events, got %d", len(results))
|
|
}
|
|
})
|
|
|
|
// Test user filter
|
|
t.Run("FilterByUser", func(t *testing.T) {
|
|
results, err := logger.Query(QueryFilter{User: "alice"})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
if len(results) != 2 {
|
|
t.Errorf("Expected 2 events for alice, got %d", len(results))
|
|
}
|
|
})
|
|
|
|
// Test success filter
|
|
t.Run("FilterBySuccess", func(t *testing.T) {
|
|
success := false
|
|
results, err := logger.Query(QueryFilter{Success: &success})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
if len(results) != 1 {
|
|
t.Errorf("Expected 1 failed event, got %d", len(results))
|
|
}
|
|
})
|
|
|
|
// Test time range filter
|
|
t.Run("FilterByTimeRange", func(t *testing.T) {
|
|
start := now.Add(-90 * time.Minute)
|
|
end := now.Add(-30 * time.Minute)
|
|
results, err := logger.Query(QueryFilter{StartTime: &start, EndTime: &end})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
if len(results) != 1 {
|
|
t.Errorf("Expected 1 event in time range, got %d", len(results))
|
|
}
|
|
})
|
|
|
|
// Test limit and offset
|
|
t.Run("LimitAndOffset", func(t *testing.T) {
|
|
results, err := logger.Query(QueryFilter{Limit: 2, Offset: 1})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
if len(results) != 2 {
|
|
t.Errorf("Expected 2 events with limit, got %d", len(results))
|
|
}
|
|
})
|
|
|
|
// SQLite requires LIMIT when using OFFSET; OFFSET-only should still work.
|
|
t.Run("OffsetWithoutLimit", func(t *testing.T) {
|
|
results, err := logger.Query(QueryFilter{Offset: 1})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
if len(results) != 3 {
|
|
t.Errorf("Expected 3 events with offset-only query, got %d", len(results))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSQLiteLoggerQueryHandlesLegacyDatetimeTimestampRows(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
legacyTime := time.Date(2026, 7, 5, 16, 4, 13, 0, time.UTC)
|
|
if _, err := logger.db.Exec(`
|
|
INSERT INTO audit_events (id, timestamp, event_type, user, ip, path, success, details, signature)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
"legacy-datetime",
|
|
legacyTime,
|
|
"startup",
|
|
sql.NullString{},
|
|
sql.NullString{},
|
|
"/api/audit",
|
|
1,
|
|
"legacy Pro runtime timestamp",
|
|
"legacy-signature",
|
|
); err != nil {
|
|
t.Fatalf("insert legacy datetime row: %v", err)
|
|
}
|
|
|
|
events, err := logger.Query(QueryFilter{ID: "legacy-datetime", Limit: 1})
|
|
if err != nil {
|
|
t.Fatalf("Query failed for legacy datetime timestamp row: %v", err)
|
|
}
|
|
if len(events) != 1 {
|
|
t.Fatalf("Expected 1 event, got %d", len(events))
|
|
}
|
|
if !events[0].Timestamp.Equal(legacyTime) {
|
|
t.Fatalf("timestamp = %s, want %s", events[0].Timestamp, legacyTime)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerQueryHandlesLegacyGoMonotonicTimestampRows(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
const legacyTimestamp = "2026-07-05 08:51:23.65653076 +0000 UTC m=+0.009025344"
|
|
want := time.Date(2026, 7, 5, 8, 51, 23, 656530760, time.UTC)
|
|
if _, err := logger.db.Exec(`
|
|
INSERT INTO audit_events (id, timestamp, event_type, user, ip, path, success, details, signature)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
"legacy-go-monotonic",
|
|
legacyTimestamp,
|
|
"startup",
|
|
sql.NullString{},
|
|
sql.NullString{},
|
|
"/api/audit",
|
|
1,
|
|
"legacy Go monotonic timestamp",
|
|
"legacy-signature",
|
|
); err != nil {
|
|
t.Fatalf("insert legacy Go monotonic row: %v", err)
|
|
}
|
|
|
|
events, err := logger.Query(QueryFilter{ID: "legacy-go-monotonic", Limit: 1})
|
|
if err != nil {
|
|
t.Fatalf("Query failed for legacy Go monotonic timestamp row: %v", err)
|
|
}
|
|
if len(events) != 1 {
|
|
t.Fatalf("Expected 1 event, got %d", len(events))
|
|
}
|
|
if !events[0].Timestamp.Equal(want) {
|
|
t.Fatalf("timestamp = %s, want %s", events[0].Timestamp, want)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerCount(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
// Log several events
|
|
for i := 0; i < 5; i++ {
|
|
event := Event{
|
|
ID: uuid.NewString(),
|
|
Timestamp: time.Now(),
|
|
EventType: "test",
|
|
Success: i%2 == 0,
|
|
}
|
|
if err := logger.Log(event); err != nil {
|
|
t.Fatalf("Log failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Count all
|
|
count, err := logger.Count(QueryFilter{})
|
|
if err != nil {
|
|
t.Fatalf("Count failed: %v", err)
|
|
}
|
|
if count != 5 {
|
|
t.Errorf("Expected count 5, got %d", count)
|
|
}
|
|
|
|
// Count successful
|
|
success := true
|
|
count, err = logger.Count(QueryFilter{Success: &success})
|
|
if err != nil {
|
|
t.Fatalf("Count failed: %v", err)
|
|
}
|
|
if count != 3 {
|
|
t.Errorf("Expected 3 successful events, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerVerifySignature(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
event := Event{
|
|
ID: uuid.NewString(),
|
|
Timestamp: time.Now(),
|
|
EventType: "verify_test",
|
|
User: "testuser",
|
|
Success: true,
|
|
}
|
|
|
|
if err := logger.Log(event); err != nil {
|
|
t.Fatalf("Log failed: %v", err)
|
|
}
|
|
|
|
// Query the event back
|
|
events, err := logger.Query(QueryFilter{ID: event.ID})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
|
|
if len(events) != 1 {
|
|
t.Fatalf("Expected 1 event, got %d", len(events))
|
|
}
|
|
|
|
// Verify should succeed
|
|
if !logger.VerifySignature(events[0]) {
|
|
t.Error("VerifySignature should return true for valid event")
|
|
}
|
|
|
|
// Tamper with event and verify should fail
|
|
tamperedEvent := events[0]
|
|
tamperedEvent.Details = "tampered"
|
|
if logger.VerifySignature(tamperedEvent) {
|
|
t.Error("VerifySignature should return false for tampered event")
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerWebhooks(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
// Initially no webhooks
|
|
urls := logger.GetWebhookURLs()
|
|
if len(urls) != 0 {
|
|
t.Errorf("Expected no webhooks initially, got %d", len(urls))
|
|
}
|
|
|
|
// Add webhooks
|
|
testURLs := []string{"https://example.com/webhook1", "https://example.com/webhook2"}
|
|
err = logger.UpdateWebhookURLs(testURLs)
|
|
if err != nil {
|
|
t.Fatalf("UpdateWebhookURLs failed: %v", err)
|
|
}
|
|
|
|
urls = logger.GetWebhookURLs()
|
|
if len(urls) != 2 {
|
|
t.Errorf("Expected 2 webhooks, got %d", len(urls))
|
|
}
|
|
|
|
// Clear webhooks
|
|
err = logger.UpdateWebhookURLs([]string{})
|
|
if err != nil {
|
|
t.Fatalf("UpdateWebhookURLs (clear) failed: %v", err)
|
|
}
|
|
|
|
urls = logger.GetWebhookURLs()
|
|
if len(urls) != 0 {
|
|
t.Errorf("Expected 0 webhooks after clear, got %d", len(urls))
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerRetention(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 1, // 1 day retention for testing
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
// Log an old event (2 days ago)
|
|
oldEvent := Event{
|
|
ID: "old-event",
|
|
Timestamp: time.Now().Add(-48 * time.Hour),
|
|
EventType: "old",
|
|
Success: true,
|
|
}
|
|
if err := logger.Log(oldEvent); err != nil {
|
|
t.Fatalf("Log old event failed: %v", err)
|
|
}
|
|
|
|
// Log a recent event
|
|
newEvent := Event{
|
|
ID: "new-event",
|
|
Timestamp: time.Now(),
|
|
EventType: "new",
|
|
Success: true,
|
|
}
|
|
if err := logger.Log(newEvent); err != nil {
|
|
t.Fatalf("Log new event failed: %v", err)
|
|
}
|
|
|
|
// Run cleanup
|
|
logger.cleanupOldEvents()
|
|
|
|
// Old event should be deleted
|
|
events, err := logger.Query(QueryFilter{ID: "old-event"})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
if len(events) != 0 {
|
|
t.Error("Old event should have been deleted")
|
|
}
|
|
|
|
// New event should still exist
|
|
events, err = logger.Query(QueryFilter{ID: "new-event"})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
if len(events) != 1 {
|
|
t.Error("New event should still exist")
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerSetRetentionDays(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
logger.SetRetentionDays(60)
|
|
if logger.GetRetentionDays() != 60 {
|
|
t.Errorf("Expected retention days 60, got %d", logger.GetRetentionDays())
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerCloseIsIdempotent(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
|
|
if err := logger.Close(); err != nil {
|
|
t.Fatalf("first Close failed: %v", err)
|
|
}
|
|
if err := logger.Close(); err != nil {
|
|
t.Fatalf("second Close failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerCloseCancelsStartupRetentionCleanup(t *testing.T) {
|
|
origDelay := retentionStartupCleanupDelay
|
|
origStartupCleanup := retentionStartupCleanup
|
|
retentionStartupCleanupDelay = 25 * time.Millisecond
|
|
startupCleanupCalled := make(chan struct{}, 1)
|
|
retentionStartupCleanup = func(l *SQLiteLogger) {
|
|
select {
|
|
case startupCleanupCalled <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
t.Cleanup(func() {
|
|
retentionStartupCleanupDelay = origDelay
|
|
retentionStartupCleanup = origStartupCleanup
|
|
})
|
|
|
|
tempDir := t.TempDir()
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
|
|
if err := logger.Close(); err != nil {
|
|
t.Fatalf("Close failed: %v", err)
|
|
}
|
|
|
|
time.Sleep(75 * time.Millisecond)
|
|
select {
|
|
case <-startupCleanupCalled:
|
|
t.Fatal("startup retention cleanup should not run after logger shutdown")
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerPersistence(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
// Create logger and log an event
|
|
logger1, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
|
|
event := Event{
|
|
ID: "persist-test",
|
|
Timestamp: time.Now(),
|
|
EventType: "persistence_test",
|
|
User: "testuser",
|
|
Success: true,
|
|
}
|
|
if err := logger1.Log(event); err != nil {
|
|
t.Fatalf("Log failed: %v", err)
|
|
}
|
|
|
|
// Close the logger
|
|
logger1.Close()
|
|
|
|
// Create a new logger with same data dir
|
|
logger2, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger (reload) failed: %v", err)
|
|
}
|
|
defer logger2.Close()
|
|
|
|
// Query the event - should still exist
|
|
events, err := logger2.Query(QueryFilter{ID: "persist-test"})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
if len(events) != 1 {
|
|
t.Error("Event should persist across logger restarts")
|
|
}
|
|
|
|
// Signature should still verify
|
|
if len(events) > 0 && !logger2.VerifySignature(events[0]) {
|
|
t.Error("Signature should still verify after restart")
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerConcurrentAccess(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
// Concurrent writes
|
|
done := make(chan bool)
|
|
for i := 0; i < 10; i++ {
|
|
go func(n int) {
|
|
for j := 0; j < 10; j++ {
|
|
event := Event{
|
|
ID: uuid.NewString(),
|
|
Timestamp: time.Now(),
|
|
EventType: "concurrent_test",
|
|
User: "user",
|
|
Success: true,
|
|
}
|
|
if err := logger.Log(event); err != nil {
|
|
t.Errorf("Concurrent log failed: %v", err)
|
|
}
|
|
}
|
|
done <- true
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
for i := 0; i < 10; i++ {
|
|
<-done
|
|
}
|
|
|
|
// Verify count
|
|
count, err := logger.Count(QueryFilter{EventType: "concurrent_test"})
|
|
if err != nil {
|
|
t.Fatalf("Count failed: %v", err)
|
|
}
|
|
if count != 100 {
|
|
t.Errorf("Expected 100 events, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteLoggerCloseIdempotent(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
|
|
DataDir: tempDir,
|
|
CryptoMgr: newMockCryptoManager(),
|
|
RetentionDays: 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteLogger failed: %v", err)
|
|
}
|
|
|
|
if err := logger.Close(); err != nil {
|
|
t.Fatalf("first Close failed: %v", err)
|
|
}
|
|
if err := logger.Close(); err != nil {
|
|
t.Fatalf("second Close failed: %v", err)
|
|
}
|
|
}
|