go-crypt/trust/audit_test.go
Snider ee58e790a0
Some checks failed
Security Scan / security (push) Failing after 11s
Test / test (push) Failing after 29s
feat: modernise to Go 1.26 iterators and stdlib helpers
Add iter.Seq iterators for trust registry (ListSeq), audit log
(EventsSeq, QuerySeq), and approval store (PendingSeq). Use
slices.DeleteFunc in session store, slices.SortFunc in testcmd,
range-over-int in benchmarks.

Co-Authored-By: Gemini <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-23 05:47:13 +00:00

301 lines
6.9 KiB
Go

package trust
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- AuditLog basic ---
func TestAuditRecord_Good(t *testing.T) {
log := NewAuditLog(nil)
result := EvalResult{
Decision: Allow,
Agent: "Athena",
Cap: CapPushRepo,
Reason: "capability repo.push allowed for tier full",
}
err := log.Record(result, "host-uk/core")
require.NoError(t, err)
assert.Equal(t, 1, log.Len())
}
func TestAuditRecord_Good_EntryFields(t *testing.T) {
log := NewAuditLog(nil)
result := EvalResult{
Decision: Deny,
Agent: "BugSETI-001",
Cap: CapPushRepo,
Reason: "denied",
}
err := log.Record(result, "host-uk/core")
require.NoError(t, err)
entries := log.Entries()
require.Len(t, entries, 1)
e := entries[0]
assert.Equal(t, "BugSETI-001", e.Agent)
assert.Equal(t, CapPushRepo, e.Cap)
assert.Equal(t, "host-uk/core", e.Repo)
assert.Equal(t, Deny, e.Decision)
assert.Equal(t, "denied", e.Reason)
assert.False(t, e.Timestamp.IsZero())
}
func TestAuditRecord_Good_NoRepo(t *testing.T) {
log := NewAuditLog(nil)
result := EvalResult{
Decision: Allow,
Agent: "Athena",
Cap: CapCommentIssue,
Reason: "ok",
}
err := log.Record(result, "")
require.NoError(t, err)
entries := log.Entries()
require.Len(t, entries, 1)
assert.Empty(t, entries[0].Repo)
}
func TestAuditEntries_Good_Snapshot(t *testing.T) {
log := NewAuditLog(nil)
log.Record(EvalResult{Agent: "A", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "")
entries := log.Entries()
require.Len(t, entries, 1)
// Mutating the snapshot should not affect the log.
entries[0].Agent = "MUTATED"
assert.Equal(t, "A", log.Entries()[0].Agent)
}
func TestAuditEntries_Good_Empty(t *testing.T) {
log := NewAuditLog(nil)
assert.Empty(t, log.Entries())
}
func TestAuditEntries_Good_AppendOnly(t *testing.T) {
log := NewAuditLog(nil)
for i := range 5 {
log.Record(EvalResult{
Agent: fmt.Sprintf("agent-%d", i),
Cap: CapPushRepo,
Decision: Allow,
Reason: "ok",
}, "")
}
assert.Equal(t, 5, log.Len())
}
// --- EntriesFor ---
func TestAuditEntriesFor_Good(t *testing.T) {
log := NewAuditLog(nil)
log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "")
log.Record(EvalResult{Agent: "Clotho", Cap: CapCreatePR, Decision: Allow, Reason: "ok"}, "")
log.Record(EvalResult{Agent: "Athena", Cap: CapMergePR, Decision: Allow, Reason: "ok"}, "")
athenaEntries := log.EntriesFor("Athena")
assert.Len(t, athenaEntries, 2)
for _, e := range athenaEntries {
assert.Equal(t, "Athena", e.Agent)
}
// Test iterator version
count := 0
for e := range log.EntriesForSeq("Athena") {
assert.Equal(t, "Athena", e.Agent)
count++
}
assert.Equal(t, 2, count)
}
func TestAuditEntriesSeq_Good(t *testing.T) {
log := NewAuditLog(nil)
log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "")
log.Record(EvalResult{Agent: "Clotho", Cap: CapCreatePR, Decision: Allow, Reason: "ok"}, "")
count := 0
for range log.EntriesSeq() {
count++
}
assert.Equal(t, 2, count)
}
func TestAuditEntriesFor_Bad_NotFound(t *testing.T) {
log := NewAuditLog(nil)
log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "")
assert.Empty(t, log.EntriesFor("NonExistent"))
}
// --- Writer output ---
func TestAuditRecord_Good_WritesToWriter(t *testing.T) {
var buf bytes.Buffer
log := NewAuditLog(&buf)
result := EvalResult{
Decision: Allow,
Agent: "Athena",
Cap: CapPushRepo,
Reason: "allowed",
}
err := log.Record(result, "host-uk/core")
require.NoError(t, err)
// Should have written a JSON line.
output := buf.String()
assert.True(t, strings.HasSuffix(output, "\n"))
var entry AuditEntry
err = json.Unmarshal([]byte(output), &entry)
require.NoError(t, err)
assert.Equal(t, "Athena", entry.Agent)
assert.Equal(t, CapPushRepo, entry.Cap)
assert.Equal(t, Allow, entry.Decision)
assert.Equal(t, "host-uk/core", entry.Repo)
}
func TestAuditRecord_Good_MultipleLines(t *testing.T) {
var buf bytes.Buffer
log := NewAuditLog(&buf)
for i := range 3 {
log.Record(EvalResult{
Agent: fmt.Sprintf("agent-%d", i),
Cap: CapPushRepo,
Decision: Allow,
Reason: "ok",
}, "")
}
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
assert.Len(t, lines, 3)
// Each line should be valid JSON.
for _, line := range lines {
var entry AuditEntry
err := json.Unmarshal([]byte(line), &entry)
assert.NoError(t, err)
}
}
func TestAuditRecord_Bad_WriterError(t *testing.T) {
log := NewAuditLog(&failWriter{})
result := EvalResult{
Decision: Allow,
Agent: "Athena",
Cap: CapPushRepo,
Reason: "ok",
}
err := log.Record(result, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "write failed")
// Entry should still be recorded in memory.
assert.Equal(t, 1, log.Len())
}
// failWriter always returns an error.
type failWriter struct{}
func (f *failWriter) Write(_ []byte) (int, error) {
return 0, io.ErrClosedPipe
}
// --- Decision JSON marshalling ---
func TestDecisionJSON_Good_RoundTrip(t *testing.T) {
decisions := []Decision{Deny, Allow, NeedsApproval}
expected := []string{`"deny"`, `"allow"`, `"needs_approval"`}
for i, d := range decisions {
data, err := json.Marshal(d)
require.NoError(t, err)
assert.Equal(t, expected[i], string(data))
var decoded Decision
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
assert.Equal(t, d, decoded)
}
}
func TestDecisionJSON_Bad_UnknownString(t *testing.T) {
var d Decision
err := json.Unmarshal([]byte(`"invalid"`), &d)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unknown decision")
}
func TestDecisionJSON_Bad_NonString(t *testing.T) {
var d Decision
err := json.Unmarshal([]byte(`42`), &d)
assert.Error(t, err)
}
// --- Concurrent audit logging ---
func TestAuditConcurrent_Good(t *testing.T) {
var buf bytes.Buffer
log := NewAuditLog(&buf)
const n = 10
var wg sync.WaitGroup
wg.Add(n)
for i := range n {
go func(idx int) {
defer wg.Done()
log.Record(EvalResult{
Agent: fmt.Sprintf("agent-%d", idx),
Cap: CapPushRepo,
Decision: Allow,
Reason: "ok",
}, "")
}(i)
}
wg.Wait()
assert.Equal(t, n, log.Len())
}
// --- Integration: PolicyEngine + AuditLog ---
func TestAuditPolicyIntegration_Good(t *testing.T) {
var buf bytes.Buffer
log := NewAuditLog(&buf)
pe := newTestEngine(t)
// Evaluate and record
result := pe.Evaluate("Athena", CapPushRepo, "host-uk/core")
err := log.Record(result, "host-uk/core")
require.NoError(t, err)
result = pe.Evaluate("BugSETI-001", CapPushRepo, "host-uk/core")
err = log.Record(result, "host-uk/core")
require.NoError(t, err)
assert.Equal(t, 2, log.Len())
// Verify entries match evaluation results.
entries := log.Entries()
assert.Equal(t, Allow, entries[0].Decision)
assert.Equal(t, Deny, entries[1].Decision)
}