Files
mcr/internal/db/admin_test.go
Kyle Isom dddc66f31b Phases 5, 6, 8: OCI pull/push paths and admin REST API
Phase 5 (OCI pull): internal/oci/ package with manifest GET/HEAD by
tag/digest, blob GET/HEAD with repo membership check, tag listing with
OCI pagination, catalog listing. Multi-segment repo names via
parseOCIPath() right-split routing. DB query layer in
internal/db/repository.go.

Phase 6 (OCI push): blob uploads (monolithic and chunked) with
uploadManager tracking in-progress BlobWriters, manifest push
implementing full ARCHITECTURE.md §5 flow in a single SQLite
transaction (create repo, upsert manifest, populate manifest_blobs,
atomic tag move). Digest verification on both blob commit and manifest
push-by-digest.

Phase 8 (admin REST): /v1 endpoints for auth (login/logout/health),
repository management (list/detail/delete), policy CRUD with engine
reload, audit log listing with filters, GC trigger/status stubs.
RequireAdmin middleware, platform-standard error format.

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

594 lines
15 KiB
Go

package db
import (
"errors"
"testing"
)
// seedAdminRepo inserts a repository with manifests, tags, and blobs for admin tests.
func seedAdminRepo(t *testing.T, d *DB, name string, tagNames []string) int64 {
t.Helper()
_, err := d.Exec(`INSERT INTO repositories (name) VALUES (?)`, name)
if err != nil {
t.Fatalf("insert repo %q: %v", name, err)
}
var repoID int64
if err := d.QueryRow(`SELECT id FROM repositories WHERE name = ?`, name).Scan(&repoID); err != nil {
t.Fatalf("select repo id: %v", err)
}
_, err = d.Exec(
`INSERT INTO manifests (repository_id, digest, media_type, content, size)
VALUES (?, ?, 'application/vnd.oci.image.manifest.v1+json', '{}', 1024)`,
repoID, "sha256:aaa-"+name,
)
if err != nil {
t.Fatalf("insert manifest for %q: %v", name, err)
}
var manifestID int64
if err := d.QueryRow(`SELECT id FROM manifests WHERE repository_id = ?`, repoID).Scan(&manifestID); err != nil {
t.Fatalf("select manifest id: %v", err)
}
for _, tag := range tagNames {
_, err := d.Exec(`INSERT INTO tags (repository_id, name, manifest_id) VALUES (?, ?, ?)`,
repoID, tag, manifestID)
if err != nil {
t.Fatalf("insert tag %q: %v", tag, err)
}
}
return repoID
}
func TestListRepositoriesWithMetadata(t *testing.T) {
d := migratedTestDB(t)
seedAdminRepo(t, d, "alpha/app", []string{"latest", "v1.0"})
seedAdminRepo(t, d, "bravo/lib", []string{"latest"})
repos, err := d.ListRepositoriesWithMetadata(50, 0)
if err != nil {
t.Fatalf("ListRepositoriesWithMetadata: %v", err)
}
if len(repos) != 2 {
t.Fatalf("repo count: got %d, want 2", len(repos))
}
// Ordered by name ASC.
if repos[0].Name != "alpha/app" {
t.Fatalf("first repo name: got %q, want %q", repos[0].Name, "alpha/app")
}
if repos[0].TagCount != 2 {
t.Fatalf("alpha/app tag count: got %d, want 2", repos[0].TagCount)
}
if repos[0].ManifestCount != 1 {
t.Fatalf("alpha/app manifest count: got %d, want 1", repos[0].ManifestCount)
}
if repos[0].TotalSize != 1024 {
t.Fatalf("alpha/app total size: got %d, want 1024", repos[0].TotalSize)
}
if repos[0].CreatedAt == "" {
t.Fatal("alpha/app created_at: expected non-empty")
}
if repos[1].Name != "bravo/lib" {
t.Fatalf("second repo name: got %q, want %q", repos[1].Name, "bravo/lib")
}
if repos[1].TagCount != 1 {
t.Fatalf("bravo/lib tag count: got %d, want 1", repos[1].TagCount)
}
}
func TestListRepositoriesWithMetadataEmpty(t *testing.T) {
d := migratedTestDB(t)
repos, err := d.ListRepositoriesWithMetadata(50, 0)
if err != nil {
t.Fatalf("ListRepositoriesWithMetadata: %v", err)
}
if repos != nil {
t.Fatalf("expected nil repos, got %v", repos)
}
}
func TestListRepositoriesWithMetadataPagination(t *testing.T) {
d := migratedTestDB(t)
seedAdminRepo(t, d, "alpha/app", []string{"latest"})
seedAdminRepo(t, d, "bravo/lib", []string{"latest"})
seedAdminRepo(t, d, "charlie/svc", []string{"latest"})
repos, err := d.ListRepositoriesWithMetadata(2, 0)
if err != nil {
t.Fatalf("ListRepositoriesWithMetadata page 1: %v", err)
}
if len(repos) != 2 {
t.Fatalf("page 1 count: got %d, want 2", len(repos))
}
if repos[0].Name != "alpha/app" {
t.Fatalf("page 1 first: got %q, want %q", repos[0].Name, "alpha/app")
}
repos, err = d.ListRepositoriesWithMetadata(2, 2)
if err != nil {
t.Fatalf("ListRepositoriesWithMetadata page 2: %v", err)
}
if len(repos) != 1 {
t.Fatalf("page 2 count: got %d, want 1", len(repos))
}
if repos[0].Name != "charlie/svc" {
t.Fatalf("page 2 first: got %q, want %q", repos[0].Name, "charlie/svc")
}
}
func TestGetRepositoryDetail(t *testing.T) {
d := migratedTestDB(t)
seedAdminRepo(t, d, "myorg/myapp", []string{"latest", "v1.0"})
detail, err := d.GetRepositoryDetail("myorg/myapp")
if err != nil {
t.Fatalf("GetRepositoryDetail: %v", err)
}
if detail.Name != "myorg/myapp" {
t.Fatalf("name: got %q, want %q", detail.Name, "myorg/myapp")
}
if detail.CreatedAt == "" {
t.Fatal("created_at: expected non-empty")
}
if len(detail.Tags) != 2 {
t.Fatalf("tag count: got %d, want 2", len(detail.Tags))
}
// Tags ordered by name ASC.
if detail.Tags[0].Name != "latest" {
t.Fatalf("first tag: got %q, want %q", detail.Tags[0].Name, "latest")
}
if detail.Tags[0].Digest == "" {
t.Fatal("first tag digest: expected non-empty")
}
if detail.Tags[1].Name != "v1.0" {
t.Fatalf("second tag: got %q, want %q", detail.Tags[1].Name, "v1.0")
}
if len(detail.Manifests) != 1 {
t.Fatalf("manifest count: got %d, want 1", len(detail.Manifests))
}
if detail.Manifests[0].Size != 1024 {
t.Fatalf("manifest size: got %d, want 1024", detail.Manifests[0].Size)
}
if detail.TotalSize != 1024 {
t.Fatalf("total size: got %d, want 1024", detail.TotalSize)
}
}
func TestGetRepositoryDetailNotFound(t *testing.T) {
d := migratedTestDB(t)
_, err := d.GetRepositoryDetail("nonexistent/repo")
if !errors.Is(err, ErrRepoNotFound) {
t.Fatalf("expected ErrRepoNotFound, got %v", err)
}
}
func TestGetRepositoryDetailEmptyRepo(t *testing.T) {
d := migratedTestDB(t)
_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('empty/repo')`)
if err != nil {
t.Fatalf("insert repo: %v", err)
}
detail, err := d.GetRepositoryDetail("empty/repo")
if err != nil {
t.Fatalf("GetRepositoryDetail: %v", err)
}
if len(detail.Tags) != 0 {
t.Fatalf("tags: got %d, want 0", len(detail.Tags))
}
if len(detail.Manifests) != 0 {
t.Fatalf("manifests: got %d, want 0", len(detail.Manifests))
}
if detail.TotalSize != 0 {
t.Fatalf("total size: got %d, want 0", detail.TotalSize)
}
}
func TestDeleteRepository(t *testing.T) {
d := migratedTestDB(t)
seedAdminRepo(t, d, "myorg/myapp", []string{"latest"})
if err := d.DeleteRepository("myorg/myapp"); err != nil {
t.Fatalf("DeleteRepository: %v", err)
}
// Verify it's gone.
_, err := d.GetRepositoryDetail("myorg/myapp")
if !errors.Is(err, ErrRepoNotFound) {
t.Fatalf("expected ErrRepoNotFound after delete, got %v", err)
}
// Verify cascade: manifests and tags should be gone.
var manifestCount int
if err := d.QueryRow(`SELECT COUNT(*) FROM manifests`).Scan(&manifestCount); err != nil {
t.Fatalf("count manifests: %v", err)
}
if manifestCount != 0 {
t.Fatalf("manifests after delete: got %d, want 0", manifestCount)
}
var tagCount int
if err := d.QueryRow(`SELECT COUNT(*) FROM tags`).Scan(&tagCount); err != nil {
t.Fatalf("count tags: %v", err)
}
if tagCount != 0 {
t.Fatalf("tags after delete: got %d, want 0", tagCount)
}
}
func TestDeleteRepositoryNotFound(t *testing.T) {
d := migratedTestDB(t)
err := d.DeleteRepository("nonexistent/repo")
if !errors.Is(err, ErrRepoNotFound) {
t.Fatalf("expected ErrRepoNotFound, got %v", err)
}
}
func TestCreatePolicyRule(t *testing.T) {
d := migratedTestDB(t)
rule := PolicyRuleRow{
Priority: 50,
Description: "allow CI push",
Effect: "allow",
Roles: []string{"ci"},
Actions: []string{"registry:push", "registry:pull"},
Repositories: []string{"production/*"},
Enabled: true,
CreatedBy: "admin-uuid",
}
id, err := d.CreatePolicyRule(rule)
if err != nil {
t.Fatalf("CreatePolicyRule: %v", err)
}
if id == 0 {
t.Fatal("expected non-zero ID")
}
got, err := d.GetPolicyRule(id)
if err != nil {
t.Fatalf("GetPolicyRule: %v", err)
}
if got.Priority != 50 {
t.Fatalf("priority: got %d, want 50", got.Priority)
}
if got.Description != "allow CI push" {
t.Fatalf("description: got %q, want %q", got.Description, "allow CI push")
}
if got.Effect != "allow" {
t.Fatalf("effect: got %q, want %q", got.Effect, "allow")
}
if len(got.Roles) != 1 || got.Roles[0] != "ci" {
t.Fatalf("roles: got %v, want [ci]", got.Roles)
}
if len(got.Actions) != 2 {
t.Fatalf("actions: got %d, want 2", len(got.Actions))
}
if len(got.Repositories) != 1 || got.Repositories[0] != "production/*" {
t.Fatalf("repositories: got %v, want [production/*]", got.Repositories)
}
if !got.Enabled {
t.Fatal("enabled: got false, want true")
}
if got.CreatedBy != "admin-uuid" {
t.Fatalf("created_by: got %q, want %q", got.CreatedBy, "admin-uuid")
}
if got.CreatedAt == "" {
t.Fatal("created_at: expected non-empty")
}
if got.UpdatedAt == "" {
t.Fatal("updated_at: expected non-empty")
}
}
func TestCreatePolicyRuleDisabled(t *testing.T) {
d := migratedTestDB(t)
rule := PolicyRuleRow{
Priority: 10,
Description: "disabled rule",
Effect: "deny",
Actions: []string{"registry:delete"},
Enabled: false,
}
id, err := d.CreatePolicyRule(rule)
if err != nil {
t.Fatalf("CreatePolicyRule: %v", err)
}
got, err := d.GetPolicyRule(id)
if err != nil {
t.Fatalf("GetPolicyRule: %v", err)
}
if got.Enabled {
t.Fatal("enabled: got true, want false")
}
}
func TestGetPolicyRuleNotFound(t *testing.T) {
d := migratedTestDB(t)
_, err := d.GetPolicyRule(9999)
if !errors.Is(err, ErrPolicyRuleNotFound) {
t.Fatalf("expected ErrPolicyRuleNotFound, got %v", err)
}
}
func TestListPolicyRules(t *testing.T) {
d := migratedTestDB(t)
// Insert rules with different priorities (out of order).
rule1 := PolicyRuleRow{
Priority: 50,
Description: "rule A",
Effect: "allow",
Actions: []string{"registry:pull"},
Enabled: true,
}
rule2 := PolicyRuleRow{
Priority: 10,
Description: "rule B",
Effect: "deny",
Actions: []string{"registry:delete"},
Enabled: true,
}
rule3 := PolicyRuleRow{
Priority: 30,
Description: "rule C",
Effect: "allow",
Actions: []string{"registry:push"},
Enabled: false,
}
if _, err := d.CreatePolicyRule(rule1); err != nil {
t.Fatalf("CreatePolicyRule 1: %v", err)
}
if _, err := d.CreatePolicyRule(rule2); err != nil {
t.Fatalf("CreatePolicyRule 2: %v", err)
}
if _, err := d.CreatePolicyRule(rule3); err != nil {
t.Fatalf("CreatePolicyRule 3: %v", err)
}
rules, err := d.ListPolicyRules(50, 0)
if err != nil {
t.Fatalf("ListPolicyRules: %v", err)
}
if len(rules) != 3 {
t.Fatalf("rule count: got %d, want 3", len(rules))
}
// Should be ordered by priority ASC: 10, 30, 50.
if rules[0].Priority != 10 {
t.Fatalf("first rule priority: got %d, want 10", rules[0].Priority)
}
if rules[0].Description != "rule B" {
t.Fatalf("first rule description: got %q, want %q", rules[0].Description, "rule B")
}
if rules[1].Priority != 30 {
t.Fatalf("second rule priority: got %d, want 30", rules[1].Priority)
}
if rules[2].Priority != 50 {
t.Fatalf("third rule priority: got %d, want 50", rules[2].Priority)
}
// Verify enabled flags.
if !rules[0].Enabled {
t.Fatal("rule B enabled: got false, want true")
}
if rules[1].Enabled {
t.Fatal("rule C enabled: got true, want false")
}
}
func TestListPolicyRulesEmpty(t *testing.T) {
d := migratedTestDB(t)
rules, err := d.ListPolicyRules(50, 0)
if err != nil {
t.Fatalf("ListPolicyRules: %v", err)
}
if rules != nil {
t.Fatalf("expected nil rules, got %v", rules)
}
}
func TestUpdatePolicyRule(t *testing.T) {
d := migratedTestDB(t)
rule := PolicyRuleRow{
Priority: 50,
Description: "original",
Effect: "allow",
Actions: []string{"registry:pull"},
Enabled: true,
}
id, err := d.CreatePolicyRule(rule)
if err != nil {
t.Fatalf("CreatePolicyRule: %v", err)
}
// Update priority and description.
updates := PolicyRuleRow{
Priority: 25,
Description: "updated",
}
if err := d.UpdatePolicyRule(id, updates); err != nil {
t.Fatalf("UpdatePolicyRule: %v", err)
}
got, err := d.GetPolicyRule(id)
if err != nil {
t.Fatalf("GetPolicyRule: %v", err)
}
if got.Priority != 25 {
t.Fatalf("priority: got %d, want 25", got.Priority)
}
if got.Description != "updated" {
t.Fatalf("description: got %q, want %q", got.Description, "updated")
}
// Effect should be unchanged.
if got.Effect != "allow" {
t.Fatalf("effect: got %q, want %q (unchanged)", got.Effect, "allow")
}
// Actions should be unchanged.
if len(got.Actions) != 1 || got.Actions[0] != "registry:pull" {
t.Fatalf("actions: got %v, want [registry:pull] (unchanged)", got.Actions)
}
}
func TestUpdatePolicyRuleBody(t *testing.T) {
d := migratedTestDB(t)
rule := PolicyRuleRow{
Priority: 50,
Description: "test",
Effect: "allow",
Actions: []string{"registry:pull"},
Enabled: true,
}
id, err := d.CreatePolicyRule(rule)
if err != nil {
t.Fatalf("CreatePolicyRule: %v", err)
}
// Update rule body fields.
updates := PolicyRuleRow{
Effect: "deny",
Actions: []string{"registry:push", "registry:delete"},
Roles: []string{"ci"},
}
if err := d.UpdatePolicyRule(id, updates); err != nil {
t.Fatalf("UpdatePolicyRule: %v", err)
}
got, err := d.GetPolicyRule(id)
if err != nil {
t.Fatalf("GetPolicyRule: %v", err)
}
if got.Effect != "deny" {
t.Fatalf("effect: got %q, want %q", got.Effect, "deny")
}
if len(got.Actions) != 2 {
t.Fatalf("actions: got %d, want 2", len(got.Actions))
}
if len(got.Roles) != 1 || got.Roles[0] != "ci" {
t.Fatalf("roles: got %v, want [ci]", got.Roles)
}
}
func TestUpdatePolicyRuleNotFound(t *testing.T) {
d := migratedTestDB(t)
err := d.UpdatePolicyRule(9999, PolicyRuleRow{Description: "nope"})
if !errors.Is(err, ErrPolicyRuleNotFound) {
t.Fatalf("expected ErrPolicyRuleNotFound, got %v", err)
}
}
func TestSetPolicyRuleEnabled(t *testing.T) {
d := migratedTestDB(t)
rule := PolicyRuleRow{
Priority: 50,
Description: "test",
Effect: "allow",
Actions: []string{"registry:pull"},
Enabled: true,
}
id, err := d.CreatePolicyRule(rule)
if err != nil {
t.Fatalf("CreatePolicyRule: %v", err)
}
// Disable the rule.
if err := d.SetPolicyRuleEnabled(id, false); err != nil {
t.Fatalf("SetPolicyRuleEnabled(false): %v", err)
}
got, err := d.GetPolicyRule(id)
if err != nil {
t.Fatalf("GetPolicyRule: %v", err)
}
if got.Enabled {
t.Fatal("enabled: got true, want false")
}
// Re-enable.
if err := d.SetPolicyRuleEnabled(id, true); err != nil {
t.Fatalf("SetPolicyRuleEnabled(true): %v", err)
}
got, err = d.GetPolicyRule(id)
if err != nil {
t.Fatalf("GetPolicyRule: %v", err)
}
if !got.Enabled {
t.Fatal("enabled: got false, want true")
}
}
func TestSetPolicyRuleEnabledNotFound(t *testing.T) {
d := migratedTestDB(t)
err := d.SetPolicyRuleEnabled(9999, true)
if !errors.Is(err, ErrPolicyRuleNotFound) {
t.Fatalf("expected ErrPolicyRuleNotFound, got %v", err)
}
}
func TestDeletePolicyRule(t *testing.T) {
d := migratedTestDB(t)
rule := PolicyRuleRow{
Priority: 50,
Description: "to delete",
Effect: "allow",
Actions: []string{"registry:pull"},
Enabled: true,
}
id, err := d.CreatePolicyRule(rule)
if err != nil {
t.Fatalf("CreatePolicyRule: %v", err)
}
if err := d.DeletePolicyRule(id); err != nil {
t.Fatalf("DeletePolicyRule: %v", err)
}
// Verify it's gone.
_, err = d.GetPolicyRule(id)
if !errors.Is(err, ErrPolicyRuleNotFound) {
t.Fatalf("expected ErrPolicyRuleNotFound after delete, got %v", err)
}
}
func TestDeletePolicyRuleNotFound(t *testing.T) {
d := migratedTestDB(t)
err := d.DeletePolicyRule(9999)
if !errors.Is(err, ErrPolicyRuleNotFound) {
t.Fatalf("expected ErrPolicyRuleNotFound, got %v", err)
}
}