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>
This commit is contained in:
197
internal/db/db_test.go
Normal file
197
internal/db/db_test.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func openTestDB(t *testing.T) *DB {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "test.db")
|
||||
d, err := Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = d.Close() })
|
||||
return d
|
||||
}
|
||||
|
||||
func TestOpenAndMigrate(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
|
||||
if err := d.Migrate(); err != nil {
|
||||
t.Fatalf("Migrate: %v", err)
|
||||
}
|
||||
|
||||
// Verify schema version.
|
||||
ver, err := d.SchemaVersion()
|
||||
if err != nil {
|
||||
t.Fatalf("SchemaVersion: %v", err)
|
||||
}
|
||||
if ver != 2 {
|
||||
t.Fatalf("schema version: got %d, want 2", ver)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateIdempotent(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
|
||||
if err := d.Migrate(); err != nil {
|
||||
t.Fatalf("Migrate (first): %v", err)
|
||||
}
|
||||
if err := d.Migrate(); err != nil {
|
||||
t.Fatalf("Migrate (second): %v", err)
|
||||
}
|
||||
|
||||
ver, err := d.SchemaVersion()
|
||||
if err != nil {
|
||||
t.Fatalf("SchemaVersion: %v", err)
|
||||
}
|
||||
if ver != 2 {
|
||||
t.Fatalf("schema version: got %d, want 2", ver)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTablesExist(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
if err := d.Migrate(); err != nil {
|
||||
t.Fatalf("Migrate: %v", err)
|
||||
}
|
||||
|
||||
tables := []string{
|
||||
"schema_migrations",
|
||||
"repositories",
|
||||
"manifests",
|
||||
"tags",
|
||||
"blobs",
|
||||
"manifest_blobs",
|
||||
"uploads",
|
||||
"policy_rules",
|
||||
"audit_log",
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
var name string
|
||||
err := d.QueryRow(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table,
|
||||
).Scan(&name)
|
||||
if err != nil {
|
||||
t.Fatalf("table %q not found: %v", table, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestForeignKeyEnforcement(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
if err := d.Migrate(); err != nil {
|
||||
t.Fatalf("Migrate: %v", err)
|
||||
}
|
||||
|
||||
// Inserting a manifest with a nonexistent repository_id should fail.
|
||||
_, err := d.Exec(`INSERT INTO manifests (repository_id, digest, media_type, content, size)
|
||||
VALUES (9999, 'sha256:abc', 'application/json', '{}', 2)`)
|
||||
if err == nil {
|
||||
t.Fatal("expected foreign key violation, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagCascadeOnManifestDelete(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
if err := d.Migrate(); err != nil {
|
||||
t.Fatalf("Migrate: %v", err)
|
||||
}
|
||||
|
||||
// Create a repository, manifest, and tag.
|
||||
_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('testrepo')`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert repo: %v", err)
|
||||
}
|
||||
|
||||
_, err = d.Exec(`INSERT INTO manifests (repository_id, digest, media_type, content, size)
|
||||
VALUES (1, 'sha256:abc123', 'application/vnd.oci.image.manifest.v1+json', '{}', 2)`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert manifest: %v", err)
|
||||
}
|
||||
|
||||
_, err = d.Exec(`INSERT INTO tags (repository_id, name, manifest_id) VALUES (1, 'latest', 1)`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert tag: %v", err)
|
||||
}
|
||||
|
||||
// Delete the manifest — tag should cascade.
|
||||
_, err = d.Exec(`DELETE FROM manifests WHERE id = 1`)
|
||||
if err != nil {
|
||||
t.Fatalf("delete manifest: %v", err)
|
||||
}
|
||||
|
||||
var count int
|
||||
if err := d.QueryRow(`SELECT COUNT(*) FROM tags`).Scan(&count); err != nil {
|
||||
t.Fatalf("count tags: %v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatalf("tags after manifest delete: got %d, want 0", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestBlobsCascadeOnManifestDelete(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
if err := d.Migrate(); err != nil {
|
||||
t.Fatalf("Migrate: %v", err)
|
||||
}
|
||||
|
||||
_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('testrepo')`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert repo: %v", err)
|
||||
}
|
||||
|
||||
_, err = d.Exec(`INSERT INTO manifests (repository_id, digest, media_type, content, size)
|
||||
VALUES (1, 'sha256:abc123', 'application/vnd.oci.image.manifest.v1+json', '{}', 2)`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert manifest: %v", err)
|
||||
}
|
||||
|
||||
_, err = d.Exec(`INSERT INTO blobs (digest, size) VALUES ('sha256:layer1', 1024)`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert blob: %v", err)
|
||||
}
|
||||
|
||||
_, err = d.Exec(`INSERT INTO manifest_blobs (manifest_id, blob_id) VALUES (1, 1)`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert manifest_blobs: %v", err)
|
||||
}
|
||||
|
||||
// Delete manifest — manifest_blobs should cascade, blob should remain.
|
||||
_, err = d.Exec(`DELETE FROM manifests WHERE id = 1`)
|
||||
if err != nil {
|
||||
t.Fatalf("delete manifest: %v", err)
|
||||
}
|
||||
|
||||
var mbCount int
|
||||
if err := d.QueryRow(`SELECT COUNT(*) FROM manifest_blobs`).Scan(&mbCount); err != nil {
|
||||
t.Fatalf("count manifest_blobs: %v", err)
|
||||
}
|
||||
if mbCount != 0 {
|
||||
t.Fatalf("manifest_blobs after delete: got %d, want 0", mbCount)
|
||||
}
|
||||
|
||||
// Blob row should still exist (GC handles file cleanup).
|
||||
var blobCount int
|
||||
if err := d.QueryRow(`SELECT COUNT(*) FROM blobs`).Scan(&blobCount); err != nil {
|
||||
t.Fatalf("count blobs: %v", err)
|
||||
}
|
||||
if blobCount != 1 {
|
||||
t.Fatalf("blobs after manifest delete: got %d, want 1", blobCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWALMode(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
|
||||
var mode string
|
||||
if err := d.QueryRow(`PRAGMA journal_mode`).Scan(&mode); err != nil {
|
||||
t.Fatalf("PRAGMA journal_mode: %v", err)
|
||||
}
|
||||
if mode != "wal" {
|
||||
t.Fatalf("journal_mode: got %q, want %q", mode, "wal")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user