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") } }