- 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>
175 lines
4.5 KiB
Go
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)
|
|
}
|
|
}
|