Files
pulse/pkg/audit/sqlite_logger_test.go
T
rcourtman c3fcddf94c sqlite: bound every store's page cache to SQLite's default
Five stores set private page caches of 16 to 64 MB per connection.
The modernc driver allocates that cache with anonymous mmap outside
the Go heap, so GC and GOMEMLIMIT never see it, and each page costs
about twice its size. Measured: reading a 107 MB database through a
64 MB cache_size added 117 MB of resident memory, against 6 MB at
SQLite's 2 MB default. On a long-running dogfood install about 165 MB
of 360 MB resident sat outside the Go heap, with a 126 MB audit
database behind a 64 MB cache (#1803).

Audit, RBAC, notification queue, unified resources and the alert
event log (writer and both readers) now take pdb.PageCachePragma.
Pages that miss it come from the kernel file cache, which the kernel
can reclaim. The audit suite, including its large legacy migration,
takes the same time as before (309 s on both trees).

pkg/db tests reject any other cache_size in production code and bound
the resident growth of a 30 MB scan. Each store's test pins its
effective value. Negative controls: restoring the audit cache fails
the source scan, and a 64 MB reader cache grows a scan by 55 MB
against a 16 MB budget. modernc sorts _pragma values before applying
them, so a second cache_size in one DSN cannot reliably override.

Related to #1803
2026-10-21 14:16:08 +01:00

837 lines
21 KiB
Go

package audit
import (
"database/sql"
"errors"
"fmt"
"testing"
"time"
"github.com/google/uuid"
pdb "github.com/rcourtman/pulse-go-rewrite/pkg/db"
)
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 TestNewSQLiteLoggerExplicitForeverRetention(t *testing.T) {
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
DataDir: t.TempDir(),
CryptoMgr: newMockCryptoManager(),
RetentionDays: 0,
RetentionConfigured: true,
})
if err != nil {
t.Fatalf("NewSQLiteLogger failed: %v", err)
}
defer logger.Close()
if logger.GetRetentionDays() != 0 {
t.Fatalf("retention days = %d, want forever (0)", logger.GetRetentionDays())
}
}
func TestNewSQLiteLoggerPreservesConfiguredCleanupInterval(t *testing.T) {
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
DataDir: t.TempDir(),
RetentionDays: 30,
RetentionConfigured: true,
CleanupInterval: 6 * time.Hour,
})
if err != nil {
t.Fatalf("NewSQLiteLogger failed: %v", err)
}
defer logger.Close()
if logger.cleanupInterval != 6*time.Hour {
t.Fatalf("cleanup interval = %s, want 6h", logger.cleanupInterval)
}
}
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.Record(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.Record(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.Record(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.Record(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.Record(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.Record(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 TestSQLiteLoggerLoadsPersistedRetentionDays(t *testing.T) {
dataDir := t.TempDir()
first, err := NewSQLiteLogger(SQLiteLoggerConfig{
DataDir: dataDir,
CryptoMgr: newMockCryptoManager(),
})
if err != nil {
t.Fatalf("NewSQLiteLogger failed: %v", err)
}
first.SetRetentionDays(45)
if err := first.Close(); err != nil {
t.Fatalf("close first logger: %v", err)
}
restarted, err := NewSQLiteLogger(SQLiteLoggerConfig{
DataDir: dataDir,
CryptoMgr: newMockCryptoManager(),
})
if err != nil {
t.Fatalf("restart logger: %v", err)
}
defer restarted.Close()
if restarted.GetRetentionDays() != 45 {
t.Fatalf("persisted retention days = %d, want 45", restarted.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.Record(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.Record(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)
}
}
// The audit database grows large on long-running installs, so it must take the
// shared page cache bound. Its former private 64 MB cache held about 120 MB of
// resident memory outside the Go heap (#1803).
func TestSQLiteLoggerUsesSharedPageCacheBound(t *testing.T) {
logger, err := NewSQLiteLogger(SQLiteLoggerConfig{
DataDir: t.TempDir(),
CryptoMgr: newMockCryptoManager(),
RetentionDays: 30,
})
if err != nil {
t.Fatalf("NewSQLiteLogger failed: %v", err)
}
defer logger.Close()
assertSharedPageCacheBound(t, "audit", logger.db)
}
func assertSharedPageCacheBound(t *testing.T, name string, conn interface {
QueryRow(query string, args ...any) *sql.Row
}) {
t.Helper()
var size int
if err := conn.QueryRow(`PRAGMA cache_size`).Scan(&size); err != nil {
t.Fatalf("%s: read cache_size: %v", name, err)
}
if size != -pdb.MaxPageCacheKiB {
t.Fatalf("%s: cache_size = %d, want %d from pdb.PageCachePragma", name, size, -pdb.MaxPageCacheKiB)
}
}