Files
mcr/internal/db/audit_test.go
Kyle Isom fde66be9c1 Phase 1: config loading, database migrations, audit log
- internal/config: TOML config with env overrides (MCR_ prefix),
  required field validation, same-filesystem check, defaults
- internal/db: SQLite via modernc.org/sqlite, WAL mode, 2 migrations
  (core registry tables + policy/audit), foreign key cascades
- internal/db: audit log write/list with filtering and pagination
- deploy/examples/mcr.toml: annotated example configuration
- .golangci.yaml: disable fieldalignment (readability over micro-opt)
- checkpoint skill copied from mcias

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:14:19 -07:00

175 lines
4.5 KiB
Go

package db
import (
"testing"
)
func migratedTestDB(t *testing.T) *DB {
t.Helper()
d := openTestDB(t)
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate: %v", err)
}
return d
}
func TestWriteAndListAuditEvents(t *testing.T) {
d := migratedTestDB(t)
err := d.WriteAuditEvent("login_ok", "user-uuid-1", "", "", "10.0.0.1", nil)
if err != nil {
t.Fatalf("WriteAuditEvent: %v", err)
}
err = d.WriteAuditEvent("manifest_pushed", "user-uuid-1", "myapp", "sha256:abc", "10.0.0.1",
map[string]string{"tag": "latest"})
if err != nil {
t.Fatalf("WriteAuditEvent: %v", err)
}
events, err := d.ListAuditEvents(AuditFilter{})
if err != nil {
t.Fatalf("ListAuditEvents: %v", err)
}
if len(events) != 2 {
t.Fatalf("event count: got %d, want 2", len(events))
}
// Most recent first.
if events[0].EventType != "manifest_pushed" {
t.Fatalf("first event type: got %q, want %q", events[0].EventType, "manifest_pushed")
}
if events[0].Repository != "myapp" {
t.Fatalf("repository: got %q, want %q", events[0].Repository, "myapp")
}
if events[0].Digest != "sha256:abc" {
t.Fatalf("digest: got %q, want %q", events[0].Digest, "sha256:abc")
}
if events[0].Details["tag"] != "latest" {
t.Fatalf("details.tag: got %q, want %q", events[0].Details["tag"], "latest")
}
}
func TestListAuditEventsFilterByType(t *testing.T) {
d := migratedTestDB(t)
_ = d.WriteAuditEvent("login_ok", "u1", "", "", "", nil)
_ = d.WriteAuditEvent("manifest_pushed", "u1", "repo", "", "", nil)
_ = d.WriteAuditEvent("login_ok", "u2", "", "", "", nil)
events, err := d.ListAuditEvents(AuditFilter{EventType: "login_ok"})
if err != nil {
t.Fatalf("ListAuditEvents: %v", err)
}
if len(events) != 2 {
t.Fatalf("event count: got %d, want 2", len(events))
}
for _, e := range events {
if e.EventType != "login_ok" {
t.Fatalf("unexpected event type: %q", e.EventType)
}
}
}
func TestListAuditEventsFilterByActor(t *testing.T) {
d := migratedTestDB(t)
_ = d.WriteAuditEvent("login_ok", "actor-a", "", "", "", nil)
_ = d.WriteAuditEvent("login_ok", "actor-b", "", "", "", nil)
events, err := d.ListAuditEvents(AuditFilter{ActorID: "actor-a"})
if err != nil {
t.Fatalf("ListAuditEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("event count: got %d, want 1", len(events))
}
if events[0].ActorID != "actor-a" {
t.Fatalf("actor_id: got %q, want %q", events[0].ActorID, "actor-a")
}
}
func TestListAuditEventsFilterByRepository(t *testing.T) {
d := migratedTestDB(t)
_ = d.WriteAuditEvent("manifest_pushed", "u1", "repo-a", "", "", nil)
_ = d.WriteAuditEvent("manifest_pushed", "u1", "repo-b", "", "", nil)
events, err := d.ListAuditEvents(AuditFilter{Repository: "repo-a"})
if err != nil {
t.Fatalf("ListAuditEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("event count: got %d, want 1", len(events))
}
}
func TestListAuditEventsPagination(t *testing.T) {
d := migratedTestDB(t)
for i := range 5 {
_ = d.WriteAuditEvent("login_ok", "u1", "", "", "", map[string]string{"i": string(rune('0' + i))})
}
// First page.
page1, err := d.ListAuditEvents(AuditFilter{Limit: 2, Offset: 0})
if err != nil {
t.Fatalf("ListAuditEvents page 1: %v", err)
}
if len(page1) != 2 {
t.Fatalf("page 1 count: got %d, want 2", len(page1))
}
// Second page.
page2, err := d.ListAuditEvents(AuditFilter{Limit: 2, Offset: 2})
if err != nil {
t.Fatalf("ListAuditEvents page 2: %v", err)
}
if len(page2) != 2 {
t.Fatalf("page 2 count: got %d, want 2", len(page2))
}
// Pages should not overlap.
if page1[0].ID == page2[0].ID {
t.Fatal("page 1 and page 2 overlap")
}
// Third page (partial).
page3, err := d.ListAuditEvents(AuditFilter{Limit: 2, Offset: 4})
if err != nil {
t.Fatalf("ListAuditEvents page 3: %v", err)
}
if len(page3) != 1 {
t.Fatalf("page 3 count: got %d, want 1", len(page3))
}
}
func TestListAuditEventsNullFields(t *testing.T) {
d := migratedTestDB(t)
// Write event with all optional fields empty.
err := d.WriteAuditEvent("gc_started", "", "", "", "", nil)
if err != nil {
t.Fatalf("WriteAuditEvent: %v", err)
}
events, err := d.ListAuditEvents(AuditFilter{})
if err != nil {
t.Fatalf("ListAuditEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("event count: got %d, want 1", len(events))
}
e := events[0]
if e.ActorID != "" {
t.Fatalf("actor_id: got %q, want empty", e.ActorID)
}
if e.Repository != "" {
t.Fatalf("repository: got %q, want empty", e.Repository)
}
if e.Details != nil {
t.Fatalf("details: got %v, want nil", e.Details)
}
}