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