Files
MyGoNavi/internal/sqlaudit/store_test.go
Syngnat 7c8e8d8dd3 feat(sql-audit): 新增 SQL 审计中心并完善事务追踪
- 新增脱敏审计存储、筛选、保留策略、完整性校验与 JSON/CSV 导出
- 覆盖查询编辑器、事务、导入同步、对象操作、AI/MCP 与 Web 运行时入口
- 完善事务完整语句日志、Windows 快捷键映射及 SQL 分析布局
- 补充多语言、健康状态、数据目录迁移和回归测试
2026-07-12 15:24:27 +08:00

653 lines
24 KiB
Go

package sqlaudit
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
)
func openTestStore(t *testing.T) *Store {
t.Helper()
store, err := Open(filepath.Join(t.TempDir(), "audit", "sql_audit.db"))
if err != nil {
t.Fatalf("Open returned error: %v", err)
}
t.Cleanup(func() {
if err := store.Close(); err != nil && !errors.Is(err, ErrClosed) {
t.Errorf("Close returned error: %v", err)
}
})
return store
}
func sampleEvent(id string, timestamp int64) Event {
return Event{
ID: id,
Timestamp: timestamp,
EventType: "query",
Status: "success",
ConnectionID: "conn-main",
ConnectionFingerprint: "postgres://admin:raw-secret@db.example/app",
DBType: "mysql",
Database: "analytics",
QueryID: "query-1",
Source: "query_editor",
BoundaryMode: BoundaryModeDriverAPI,
SQLText: "SELECT * FROM users WHERE id = 42 AND token = 'raw-token'",
DurationMs: 25,
RowsReturned: 1,
}
}
func TestOpenConfiguresSQLiteAndDefaultSettings(t *testing.T) {
store := openTestStore(t)
settings, err := store.GetSettings()
if err != nil {
t.Fatalf("GetSettings returned error: %v", err)
}
if settings != DefaultSettings() {
t.Fatalf("unexpected defaults: %#v", settings)
}
var journalMode string
if err := store.db.QueryRow(`PRAGMA journal_mode`).Scan(&journalMode); err != nil {
t.Fatalf("read journal mode: %v", err)
}
if strings.ToLower(journalMode) != "wal" {
t.Fatalf("journal mode must be WAL, got %q", journalMode)
}
var synchronous int
if err := store.db.QueryRow(`PRAGMA synchronous`).Scan(&synchronous); err != nil {
t.Fatalf("read synchronous mode: %v", err)
}
if synchronous != 2 {
t.Fatalf("synchronous must be FULL (2), got %d", synchronous)
}
if got := store.db.Stats().MaxOpenConnections; got != 4 {
t.Fatalf("MaxOpenConnections=%d, want 4", got)
}
connections := make([]interface{ Close() error }, 0, 4)
for index := 0; index < 4; index++ {
conn, err := store.db.Conn(context.Background())
if err != nil {
t.Fatalf("acquire pooled connection %d: %v", index+1, err)
}
connections = append(connections, conn)
var busyTimeout, foreignKeys, connectionSynchronous int
var connectionJournal string
if err := conn.QueryRowContext(context.Background(), `PRAGMA busy_timeout`).Scan(&busyTimeout); err != nil {
t.Fatalf("read connection %d busy_timeout: %v", index+1, err)
}
if err := conn.QueryRowContext(context.Background(), `PRAGMA foreign_keys`).Scan(&foreignKeys); err != nil {
t.Fatalf("read connection %d foreign_keys: %v", index+1, err)
}
if err := conn.QueryRowContext(context.Background(), `PRAGMA synchronous`).Scan(&connectionSynchronous); err != nil {
t.Fatalf("read connection %d synchronous: %v", index+1, err)
}
if err := conn.QueryRowContext(context.Background(), `PRAGMA journal_mode`).Scan(&connectionJournal); err != nil {
t.Fatalf("read connection %d journal_mode: %v", index+1, err)
}
if busyTimeout != 5000 || foreignKeys != 1 || connectionSynchronous != 2 || strings.ToLower(connectionJournal) != "wal" {
t.Fatalf("connection %d PRAGMAs unexpected: busy=%d foreign=%d sync=%d journal=%q",
index+1, busyTimeout, foreignKeys, connectionSynchronous, connectionJournal)
}
}
for _, conn := range connections {
if err := conn.Close(); err != nil {
t.Fatalf("close pooled connection: %v", err)
}
}
if runtime.GOOS != "windows" {
dirInfo, err := os.Stat(filepath.Dir(store.Path()))
if err != nil {
t.Fatalf("stat audit directory: %v", err)
}
if got := dirInfo.Mode().Perm(); got != 0o700 {
t.Fatalf("audit directory mode = %#o, want 0700", got)
}
fileInfo, err := os.Stat(store.Path())
if err != nil {
t.Fatalf("stat audit database: %v", err)
}
if got := fileInfo.Mode().Perm(); got != 0o600 {
t.Fatalf("audit database mode = %#o, want 0600", got)
}
}
}
func TestOpenSupportsURIReservedCharactersInPath(t *testing.T) {
path := filepath.Join(t.TempDir(), "audit space # percent% amp&", "sql audit.db")
store, err := Open(path)
if err != nil {
t.Fatalf("Open reserved-character path returned error: %v", err)
}
if err := store.Append(sampleEvent("reserved-path", time.Now().UnixMilli())); err != nil {
_ = store.Close()
t.Fatalf("Append reserved-character path returned error: %v", err)
}
if err := store.Close(); err != nil {
t.Fatalf("Close reserved-character path returned error: %v", err)
}
if _, err := os.Stat(path); err != nil {
t.Fatalf("SQLite database was not created at literal path %q: %v", path, err)
}
if info, err := os.Stat(path + "-wal"); err == nil && info.Size() > 0 {
t.Fatalf("Close should checkpoint/truncate WAL before migration, size=%d", info.Size())
} else if err != nil && !os.IsNotExist(err) {
t.Fatalf("stat WAL after Close: %v", err)
}
}
func TestAppendSanitizesAndQueryFiltersWithSummary(t *testing.T) {
store := openTestStore(t)
now := time.Now().UnixMilli()
first := sampleEvent("event-1", now-100)
first.TransactionID = "tx-1"
if err := store.Append(first); err != nil {
t.Fatalf("Append first returned error: %v", err)
}
second := sampleEvent("event-2", now)
second.Status = "error"
second.TransactionID = "tx-2"
second.Error = "password=driver-secret failed near 'raw-token'"
if err := store.Append(second); err != nil {
t.Fatalf("Append second returned error: %v", err)
}
page, err := store.Query(Filter{Database: "analytics", Page: 1, PageSize: 1})
if err != nil {
t.Fatalf("Query returned error: %v", err)
}
if page.Total != 2 || len(page.Items) != 1 || page.Items[0].ID != "event-2" {
t.Fatalf("unexpected page: %#v", page)
}
if page.Summary.TotalEvents != 2 || page.Summary.SuccessCount != 1 ||
page.Summary.ErrorCount != 1 || page.Summary.TransactionCount != 2 {
t.Fatalf("unexpected summary: %#v", page.Summary)
}
event := page.Items[0]
for _, secret := range []string{"42", "raw-token", "driver-secret", "admin", "raw-secret", "db.example"} {
if strings.Contains(event.SQLText+event.Error+event.ConnectionFingerprint, secret) {
t.Fatalf("stored audit event leaked %q: %#v", secret, event)
}
}
if !event.SQLRedacted || event.SQLFingerprint == "" || len(event.ConnectionFingerprint) != 64 {
t.Fatalf("event was not safely normalized: %#v", event)
}
errorPage, err := store.Query(Filter{Status: "error", Search: "password", PageSize: 10})
if err != nil || errorPage.Total != 1 || len(errorPage.Items) != 1 {
t.Fatalf("filtered query failed: page=%#v err=%v", errorPage, err)
}
assertAuditStorageFilesDoNotContain(t, store.Path(), []string{"raw-token", "driver-secret", "raw-secret"})
}
func TestQuerySupportsContractFiltersAndEscapesSearchWildcards(t *testing.T) {
store := openTestStore(t)
now := time.Now().UnixMilli()
target := sampleEvent("filter-target", now)
target.EventType = "TRANSACTION_COMMIT"
target.Status = "SUCCESS"
target.TransactionID = "tx-filter"
target.Source = "SYSTEM"
target.DBType = "MYSQL"
if err := store.Append(target); err != nil {
t.Fatalf("Append target returned error: %v", err)
}
other := sampleEvent("filter-other", now+100)
other.ConnectionID = "conn-other"
other.Database = "reporting"
other.TransactionID = "tx-other"
if err := store.Append(other); err != nil {
t.Fatalf("Append other returned error: %v", err)
}
page, err := store.Query(Filter{
ConnectionID: "conn-main",
Database: "analytics",
DBType: "MYSQL",
EventType: "TRANSACTION_COMMIT",
Status: "SUCCESS",
TransactionID: "tx-filter",
Source: "SYSTEM",
FromTimestamp: now - 1,
ToTimestamp: now + 1,
PageSize: 10,
})
if err != nil || page.Total != 1 || len(page.Items) != 1 || page.Items[0].ID != "filter-target" {
t.Fatalf("contract filters did not isolate target: page=%#v err=%v", page, err)
}
page, err = store.Query(Filter{Search: `%_' OR 1=1 --`, PageSize: 10})
if err != nil || page.Total != 0 {
t.Fatalf("literal wildcard/injection-like search should not broaden results: page=%#v err=%v", page, err)
}
all, err := store.Query(Filter{PageSize: 10})
if err != nil || len(all.Items) == 0 {
t.Fatalf("load events for fingerprint search: page=%#v err=%v", all, err)
}
fingerprintPrefix := all.Items[0].SQLFingerprint[:12]
page, err = store.Query(Filter{Search: fingerprintPrefix, PageSize: 10})
if err != nil || page.Total == 0 {
t.Fatalf("fingerprint search should find matching events: page=%#v err=%v", page, err)
}
}
func TestMetadataAndDisabledCaptureModes(t *testing.T) {
store := openTestStore(t)
settings := DefaultSettings()
settings.CaptureMode = CaptureModeMetadata
if err := store.UpdateSettings(settings); err != nil {
t.Fatalf("UpdateSettings(metadata) returned error: %v", err)
}
if err := store.Append(sampleEvent("metadata-event", time.Now().UnixMilli())); err != nil {
t.Fatalf("Append metadata event returned error: %v", err)
}
page, err := store.Query(Filter{PageSize: 10})
if err != nil || len(page.Items) != 1 {
t.Fatalf("query metadata event: page=%#v err=%v", page, err)
}
if page.Items[0].SQLText != "" || page.Items[0].SQLFingerprint == "" || !page.Items[0].SQLRedacted {
t.Fatalf("metadata capture persisted SQL text: %#v", page.Items[0])
}
settings.Enabled = false
if err := store.UpdateSettings(settings); err != nil {
t.Fatalf("UpdateSettings(disabled) returned error: %v", err)
}
if err := store.Append(sampleEvent("disabled-event", time.Now().UnixMilli())); err != nil {
t.Fatalf("disabled Append should be a no-op: %v", err)
}
page, err = store.Query(Filter{PageSize: 10})
if err != nil || page.Total != 1 {
t.Fatalf("disabled capture should not append: page=%#v err=%v", page, err)
}
}
func TestControlBoundariesAreAtomicAndBypassDisabledCapture(t *testing.T) {
store := openTestStore(t)
settings := DefaultSettings()
settings.Enabled = false
settings.CaptureMode = CaptureModeMetadata
control := Event{ID: "settings-control", EventType: "audit_settings_change", Status: "success", Source: "audit_control"}
if err := store.UpdateSettingsWithControl(settings, control); err != nil {
t.Fatalf("UpdateSettingsWithControl returned error: %v", err)
}
if err := store.Append(sampleEvent("disabled-ordinary", time.Now().UnixMilli())); err != nil {
t.Fatalf("disabled ordinary append returned error: %v", err)
}
page, err := store.Query(Filter{PageSize: 10})
if err != nil || page.Total != 1 || page.Items[0].ID != "settings-control" {
t.Fatalf("disabled control boundary was not retained: page=%#v err=%v", page, err)
}
if !strings.Contains(page.Items[0].SQLText, "FROM_ENABLED_ON TO_ENABLED_OFF") {
t.Fatalf("settings control boundary lacks safe old/new metadata: %q", page.Items[0].SQLText)
}
deleted, err := store.ClearWithControl(0, Event{ID: "clear-control", EventType: "audit_clear", Status: "success", Source: "audit_control"})
if err != nil || deleted != 1 {
t.Fatalf("ClearWithControl deleted=%d err=%v, want 1", deleted, err)
}
page, err = store.Query(Filter{PageSize: 10})
if err != nil || page.Total != 1 || page.Items[0].ID != "clear-control" || page.Items[0].RowsAffected != 1 {
t.Fatalf("clear control boundary was not atomically retained: page=%#v err=%v", page, err)
}
if page.Items[0].SQLText != "AUDIT CLEAR ALL" {
t.Fatalf("metadata capture erased the clear control descriptor: %q", page.Items[0].SQLText)
}
}
func TestAppendNeverPersistsRedisOrMessagePayloadSecrets(t *testing.T) {
store := openTestStore(t)
now := time.Now().UnixMilli()
redisEvent := sampleEvent("redis-secret", now)
redisEvent.DBType = "redis"
redisEvent.SQLText = `HSET account:42 email private@example.test token redis-token-secret`
redisEvent.Error = `AUTH failed password=redis-error-secret`
if err := store.Append(redisEvent); err != nil {
t.Fatalf("Append Redis event returned error: %v", err)
}
mqEvent := sampleEvent("mq-secret", now+1)
mqEvent.DBType = "rocketmq"
mqEvent.SQLText = `PUBLISH orders {"token":"mq-payload-secret","card":"4111111111111111"}`
mqEvent.Error = `publish rejected token=mq-error-secret`
if err := store.Append(mqEvent); err != nil {
t.Fatalf("Append MQ event returned error: %v", err)
}
mqEventSecond := sampleEvent("mq-secret-second", now+2)
mqEventSecond.DBType = "rocketmq"
mqEventSecond.SQLText = `PUBLISH orders {"token":"different-low-entropy-secret"}`
if err := store.Append(mqEventSecond); err != nil {
t.Fatalf("Append second MQ event returned error: %v", err)
}
page, err := store.Query(Filter{PageSize: 10})
if err != nil || len(page.Items) != 3 {
t.Fatalf("query non-SQL audit events: page=%#v err=%v", page, err)
}
byID := map[string]Event{}
for _, event := range page.Items {
byID[event.ID] = event
}
if got := byID["redis-secret"].SQLText; got != "HSET account:42 email ? token ?" {
t.Fatalf("unexpected Redis audit text: %q", got)
}
if got := byID["mq-secret"].SQLText; got != "" {
t.Fatalf("message payload must be metadata-only, got %q", got)
}
if byID["mq-secret"].SQLFingerprint == "" {
t.Fatal("metadata-only message event still needs a safe fingerprint")
}
if byID["mq-secret"].SQLFingerprint != byID["mq-secret-second"].SQLFingerprint {
t.Fatalf("metadata-only fingerprint must not depend on message payload: first=%s second=%s",
byID["mq-secret"].SQLFingerprint, byID["mq-secret-second"].SQLFingerprint)
}
assertAuditStorageFilesDoNotContain(t, store.Path(), []string{
"private@example.test", "redis-token-secret", "redis-error-secret",
"mq-payload-secret", "4111111111111111", "mq-error-secret", "different-low-entropy-secret",
})
}
func TestIntegrityDetectsTampering(t *testing.T) {
store := openTestStore(t)
now := time.Now().UnixMilli()
for index := 0; index < 2; index++ {
if err := store.Append(sampleEvent("integrity-"+string(rune('a'+index)), now+int64(index))); err != nil {
t.Fatalf("Append returned error: %v", err)
}
}
report, err := store.VerifyIntegrity()
if err != nil || !report.Valid || !report.WeakValidation || report.CheckedRecords != 2 {
t.Fatalf("unexpected valid integrity report: %#v err=%v", report, err)
}
if _, err := store.db.Exec(`UPDATE sql_audit_events SET sql_text='tampered' WHERE sequence=1`); err != nil {
t.Fatalf("tamper test database: %v", err)
}
report, err = store.VerifyIntegrity()
if err != nil || report.Valid || report.InvalidSequence != 1 {
t.Fatalf("tampering was not detected: %#v err=%v", report, err)
}
}
func TestClearAndRecordLimitPreserveRemainingHashes(t *testing.T) {
store := openTestStore(t)
now := time.Now().UnixMilli()
for index := 0; index < 3; index++ {
if err := store.Append(sampleEvent("clear-"+string(rune('a'+index)), now-3000+int64(index*1000))); err != nil {
t.Fatalf("Append returned error: %v", err)
}
}
beforeClear, err := store.Query(Filter{PageSize: 10})
if err != nil || len(beforeClear.Items) != 3 {
t.Fatalf("query before clear: page=%#v err=%v", beforeClear, err)
}
retainedHash := beforeClear.Items[0].Hash
deleted, err := store.Clear(now - 1500)
if err != nil || deleted != 2 {
t.Fatalf("Clear deleted=%d err=%v, want 2", deleted, err)
}
page, err := store.Query(Filter{PageSize: 10})
if err != nil || len(page.Items) != 1 || page.Items[0].PrevHash == "" {
t.Fatalf("clear rewrote or lost the retained chain anchor: page=%#v err=%v", page, err)
}
if page.Items[0].Hash != retainedHash {
t.Fatalf("clear must not rewrite retained event hash: before=%s after=%s", retainedHash, page.Items[0].Hash)
}
if report, err := store.VerifyIntegrity(); err != nil || !report.Valid || !report.PartialChain || !report.TruncatedPrefix {
t.Fatalf("chain invalid after clear: report=%#v err=%v", report, err)
}
settings := DefaultSettings()
settings.MaxRecords = 2
if err := store.UpdateSettings(settings); err != nil {
t.Fatalf("UpdateSettings returned error: %v", err)
}
for index := 0; index < 3; index++ {
if err := store.Append(sampleEvent("limited-"+string(rune('a'+index)), now+int64(index))); err != nil {
t.Fatalf("Append limited event returned error: %v", err)
}
}
page, err = store.Query(Filter{PageSize: 10})
if err != nil || page.Total != 2 {
t.Fatalf("record limit not enforced: page=%#v err=%v", page, err)
}
if report, err := store.VerifyIntegrity(); err != nil || !report.Valid || !report.PartialChain {
t.Fatalf("chain invalid after record-limit pruning: report=%#v err=%v", report, err)
}
}
func TestClearDeletesOnlyContiguousPrefixForOutOfOrderTimestamps(t *testing.T) {
store := openTestStore(t)
now := time.Now().UnixMilli()
for _, event := range []Event{
sampleEvent("prefix-old", now-10_000),
sampleEvent("prefix-new", now),
sampleEvent("later-sequence-old-time", now-20_000),
} {
if err := store.Append(event); err != nil {
t.Fatalf("Append returned error: %v", err)
}
}
before, err := store.Query(Filter{PageSize: 10})
if err != nil || len(before.Items) != 3 {
t.Fatalf("query before clear: page=%#v err=%v", before, err)
}
hashes := map[string]string{}
for _, event := range before.Items {
hashes[event.ID] = event.Hash
}
deleted, err := store.Clear(now - 5_000)
if err != nil || deleted != 1 {
t.Fatalf("Clear should delete only one contiguous prefix event: deleted=%d err=%v", deleted, err)
}
after, err := store.Query(Filter{PageSize: 10})
if err != nil || len(after.Items) != 2 {
t.Fatalf("query after clear: page=%#v err=%v", after, err)
}
for _, event := range after.Items {
if event.Hash != hashes[event.ID] {
t.Fatalf("retained event %s hash was rewritten", event.ID)
}
}
if report, err := store.VerifyIntegrity(); err != nil || !report.Valid || !report.PartialChain {
t.Fatalf("partial chain should remain verifiable: report=%#v err=%v", report, err)
}
}
func TestConcurrentAppendKeepsUniqueOrderedChain(t *testing.T) {
store := openTestStore(t)
const count = 32
now := time.Now().UnixMilli()
var wait sync.WaitGroup
errorsCh := make(chan error, count)
for index := 0; index < count; index++ {
wait.Add(1)
go func(index int) {
defer wait.Done()
event := sampleEvent("concurrent-"+time.UnixMilli(int64(index)).Format("150405.000000000"), now+int64(index))
errorsCh <- store.Append(event)
}(index)
}
wait.Wait()
close(errorsCh)
for err := range errorsCh {
if err != nil {
t.Fatalf("concurrent Append returned error: %v", err)
}
}
page, err := store.Query(Filter{PageSize: count})
if err != nil || page.Total != count {
t.Fatalf("concurrent events missing: total=%d err=%v", page.Total, err)
}
seen := make(map[int64]struct{}, count)
for _, event := range page.Items {
if _, duplicate := seen[event.Sequence]; duplicate {
t.Fatalf("duplicate sequence %d", event.Sequence)
}
seen[event.Sequence] = struct{}{}
}
if report, err := store.VerifyIntegrity(); err != nil || !report.Valid || report.CheckedRecords != count {
t.Fatalf("concurrent chain invalid: report=%#v err=%v", report, err)
}
}
func TestWALReaderDoesNotBlockSynchronousAppend(t *testing.T) {
store := openTestStore(t)
now := time.Now().UnixMilli()
if err := store.Append(sampleEvent("wal-seed", now)); err != nil {
t.Fatalf("Append seed returned error: %v", err)
}
reader, err := store.db.Conn(context.Background())
if err != nil {
t.Fatalf("acquire reader connection: %v", err)
}
defer reader.Close()
if _, err := reader.ExecContext(context.Background(), `BEGIN`); err != nil {
t.Fatalf("begin reader transaction: %v", err)
}
rows, err := reader.QueryContext(context.Background(), `SELECT id FROM sql_audit_events ORDER BY sequence`)
if err != nil {
_, _ = reader.ExecContext(context.Background(), `ROLLBACK`)
t.Fatalf("open reader cursor: %v", err)
}
defer rows.Close()
if !rows.Next() {
t.Fatal("reader cursor should expose seed event")
}
var seedID string
if err := rows.Scan(&seedID); err != nil || seedID != "wal-seed" {
t.Fatalf("scan reader cursor id=%q err=%v", seedID, err)
}
done := make(chan error, 1)
go func() {
done <- store.Append(sampleEvent("wal-writer", now+1))
}()
select {
case err := <-done:
if err != nil {
t.Fatalf("Append while WAL reader active returned error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("active WAL reader blocked synchronous audit append")
}
if err := rows.Close(); err != nil {
t.Fatalf("close reader rows: %v", err)
}
if _, err := reader.ExecContext(context.Background(), `ROLLBACK`); err != nil {
t.Fatalf("rollback reader transaction: %v", err)
}
}
func TestAppendBatchIsAtomicAndKeepsOneContinuousChain(t *testing.T) {
store := openTestStore(t)
now := time.Now().UnixMilli()
if err := store.Append(sampleEvent("before-batch", now)); err != nil {
t.Fatalf("Append before batch returned error: %v", err)
}
batch := []Event{
sampleEvent("batch-1", now+1),
sampleEvent("batch-2", now+2),
sampleEvent("batch-3", now+3),
}
if err := store.AppendBatch(batch); err != nil {
t.Fatalf("AppendBatch returned error: %v", err)
}
page, err := store.Query(Filter{PageSize: 10})
if err != nil || page.Total != 4 {
t.Fatalf("query after batch: page=%#v err=%v", page, err)
}
report, err := store.VerifyIntegrity()
if err != nil || !report.Valid || report.CheckedRecords != 4 {
t.Fatalf("integrity after batch: report=%#v err=%v", report, err)
}
duplicateBatch := []Event{
sampleEvent("atomic-new", now+4),
sampleEvent("batch-2", now+5),
}
if err := store.AppendBatch(duplicateBatch); err == nil {
t.Fatal("AppendBatch with duplicate ID should fail")
}
page, err = store.Query(Filter{Search: "atomic-new", PageSize: 10})
if err != nil || page.Total != 0 {
t.Fatalf("failed batch should roll back every event: page=%#v err=%v", page, err)
}
}
func TestAppendBatchLargerThanMaxRecordsFailsAtomically(t *testing.T) {
store := openTestStore(t)
settings := DefaultSettings()
settings.MaxRecords = 3
if err := store.UpdateSettings(settings); err != nil {
t.Fatalf("UpdateSettings returned error: %v", err)
}
now := time.Now().UnixMilli()
if err := store.Append(sampleEvent("existing-before-large-batch", now)); err != nil {
t.Fatalf("Append existing event returned error: %v", err)
}
batch := make([]Event, 0, 6)
for index := 0; index < 6; index++ {
batch = append(batch, sampleEvent(fmt.Sprintf("large-batch-%d", index), now+int64(index+1)))
}
if err := store.AppendBatch(batch); !errors.Is(err, ErrBatchExceedsMaxRecords) {
t.Fatalf("AppendBatch error = %v, want ErrBatchExceedsMaxRecords", err)
}
page, err := store.Query(Filter{PageSize: 10})
if err != nil {
t.Fatalf("Query returned error: %v", err)
}
if page.Total != 1 || len(page.Items) != 1 || page.Items[0].ID != "existing-before-large-batch" {
t.Fatalf("oversized batch must not modify existing audit records: %#v", page)
}
if report, err := store.VerifyIntegrity(); err != nil || !report.Valid || report.CheckedRecords != 1 {
t.Fatalf("large-batch chain invalid: report=%#v err=%v", report, err)
}
}
func TestSettingsValidationAndClosedStore(t *testing.T) {
store := openTestStore(t)
invalid := DefaultSettings()
invalid.CaptureMode = "full"
if err := store.UpdateSettings(invalid); err == nil {
t.Fatal("full/raw SQL capture mode must be rejected")
}
if err := store.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
if err := store.Append(sampleEvent("closed", time.Now().UnixMilli())); !errors.Is(err, ErrClosed) {
t.Fatalf("Append after Close error=%v, want ErrClosed", err)
}
}
func assertAuditStorageFilesDoNotContain(t *testing.T, databasePath string, secrets []string) {
t.Helper()
for _, path := range []string{databasePath, databasePath + "-wal", databasePath + "-shm"} {
payload, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
continue
}
t.Fatalf("read SQLite audit storage file %q: %v", path, err)
}
for _, secret := range secrets {
if strings.Contains(string(payload), secret) {
t.Fatalf("SQLite audit storage file %q contains raw secret %q", path, secret)
}
}
}
}