Files
volumen/internal/audit/audit_test.go
T
petrbalvin f8ed33df83
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Initial commit
Assisted-by: GLM 5.3
2026-09-29 10:03:32 +02:00

150 lines
4.5 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package audit
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestDisabledLogWritesNothing(t *testing.T) {
l := New("")
if l.Enabled() {
t.Fatal("Enabled = true for empty path")
}
l.Record(Entry{User: "admin", Action: "login"})
}
func TestRecordAppendsJSONLines(t *testing.T) {
path := filepath.Join(t.TempDir(), "sub", "audit.log")
l := New(path)
if !l.Enabled() {
t.Fatal("Enabled = false for a real path")
}
l.Record(Entry{User: "admin", Action: "post.created", Resource: "hello", IP: "127.0.0.1"})
l.Record(Entry{User: "admin", Action: "post.deleted", Detail: map[string]any{"slug": "hello"}})
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
lines := strings.Split(strings.TrimSuffix(string(data), "\n"), "\n")
if len(lines) != 2 {
t.Fatalf("got %d lines, want 2: %q", len(lines), data)
}
var first map[string]any
if err := json.Unmarshal([]byte(lines[0]), &first); err != nil {
t.Fatalf("line is not JSON: %v", err)
}
if first["user"] != "admin" || first["action"] != "post.created" ||
first["resource"] != "hello" || first["ip"] != "127.0.0.1" {
t.Fatalf("first entry = %v", first)
}
if _, ok := first["detail"]; ok {
t.Fatalf("unexpected detail in first entry: %v", first)
}
var second map[string]any
if err := json.Unmarshal([]byte(lines[1]), &second); err != nil {
t.Fatalf("line is not JSON: %v", err)
}
detail, ok := second["detail"].(map[string]any)
if !ok || detail["slug"] != "hello" {
t.Fatalf("second entry detail = %v", second)
}
ts, _ := first["ts"].(string)
if len(ts) < 19 || !strings.Contains(ts, "T") {
t.Fatalf("ts = %q, want ISO timestamp", ts)
}
}
func TestRecordSurvivesUnwritablePath(t *testing.T) {
dir := t.TempDir()
l := New(filepath.Join(dir, "file-as-dir", "x", "audit.log"))
if err := os.WriteFile(filepath.Join(dir, "file-as-dir"), []byte("x"), 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
l.Record(Entry{User: "admin", Action: "login"}) // must not panic
}
// The handler opens the file once and appends to it, and Close releases
// the handle.
func TestFileIsOpenedOnceAndClosed(t *testing.T) {
path := filepath.Join(t.TempDir(), "audit.log")
l := New(path)
for range 50 {
l.Record(Entry{User: "admin", Action: "post.updated", Resource: "hello"})
}
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if got := strings.Count(string(data), "\n"); got != 50 {
t.Fatalf("lines = %d, want 50", got)
}
// A write after Close reopens it rather than losing the line.
l.Record(Entry{User: "admin", Action: "post.deleted"})
data, err = os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if got := strings.Count(string(data), "\n"); got != 51 {
t.Fatalf("lines = %d, want 51", got)
}
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
}
// An unwritable destination is reported once and never fails the caller.
func TestUnwritableDestinationIsNotFatal(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.log")
if err := os.MkdirAll(path, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
l := New(path)
l.Record(Entry{User: "admin", Action: "post.created"})
l.Record(Entry{User: "admin", Action: "post.created"})
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
}
// A transient open failure must not silence the log for the life of the
// process: once the obstruction is gone, the next record writes.
func TestRecordRecoversAfterThePathBecomesWritable(t *testing.T) {
dir := t.TempDir()
// A regular file where the log's parent directory should be makes
// MkdirAll fail.
blocker := filepath.Join(dir, "blocked")
if err := os.WriteFile(blocker, []byte("x"), 0o644); err != nil {
t.Fatalf("write blocker: %v", err)
}
log := New(filepath.Join(blocker, "sub", "audit.log"))
log.Record(Entry{User: "u", Action: "first"})
if err := log.Close(); err != nil {
t.Fatalf("close: %v", err)
}
if err := os.Remove(blocker); err != nil {
t.Fatalf("remove blocker: %v", err)
}
log.Record(Entry{User: "u", Action: "second"})
if err := log.Close(); err != nil {
t.Fatalf("close: %v", err)
}
raw, err := os.ReadFile(filepath.Join(dir, "blocked", "sub", "audit.log"))
if err != nil {
t.Fatalf("the audit log never recovered: %v", err)
}
if !strings.Contains(string(raw), `"action":"second"`) {
t.Fatalf("recovered log = %s", raw)
}
}