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>
430 lines
11 KiB
Go
430 lines
11 KiB
Go
package db
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
// seedTestRepo inserts a repository, manifest, tag, blob, and manifest_blob
|
|
// link for use in repository query tests. It returns the repository ID.
|
|
func seedTestRepo(t *testing.T, d *DB) int64 {
|
|
t.Helper()
|
|
|
|
_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('myorg/myapp')`)
|
|
if err != nil {
|
|
t.Fatalf("insert repo: %v", err)
|
|
}
|
|
|
|
var repoID int64
|
|
if err := d.QueryRow(`SELECT id FROM repositories WHERE name = 'myorg/myapp'`).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 (?, 'sha256:aaaa', 'application/vnd.oci.image.manifest.v1+json', '{"layers":[]}', 15)`,
|
|
repoID,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("insert manifest: %v", err)
|
|
}
|
|
|
|
var manifestID int64
|
|
if err := d.QueryRow(`SELECT id FROM manifests WHERE digest = 'sha256:aaaa'`).Scan(&manifestID); err != nil {
|
|
t.Fatalf("select manifest id: %v", err)
|
|
}
|
|
|
|
_, err = d.Exec(
|
|
`INSERT INTO tags (repository_id, name, manifest_id) VALUES (?, 'latest', ?)`,
|
|
repoID, manifestID,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("insert tag: %v", err)
|
|
}
|
|
|
|
_, err = d.Exec(`INSERT INTO blobs (digest, size) VALUES ('sha256:bbbb', 2048)`)
|
|
if err != nil {
|
|
t.Fatalf("insert blob: %v", err)
|
|
}
|
|
|
|
var blobID int64
|
|
if err := d.QueryRow(`SELECT id FROM blobs WHERE digest = 'sha256:bbbb'`).Scan(&blobID); err != nil {
|
|
t.Fatalf("select blob id: %v", err)
|
|
}
|
|
|
|
_, err = d.Exec(
|
|
`INSERT INTO manifest_blobs (manifest_id, blob_id) VALUES (?, ?)`,
|
|
manifestID, blobID,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("insert manifest_blob: %v", err)
|
|
}
|
|
|
|
return repoID
|
|
}
|
|
|
|
func TestGetRepositoryByName_Found(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
seedTestRepo(t, d)
|
|
|
|
id, err := d.GetRepositoryByName("myorg/myapp")
|
|
if err != nil {
|
|
t.Fatalf("GetRepositoryByName: %v", err)
|
|
}
|
|
if id == 0 {
|
|
t.Fatal("expected non-zero repository ID")
|
|
}
|
|
}
|
|
|
|
func TestGetRepositoryByName_NotFound(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
_, err := d.GetRepositoryByName("nonexistent")
|
|
if !errors.Is(err, ErrRepoNotFound) {
|
|
t.Fatalf("expected ErrRepoNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetManifestByTag_Found(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
repoID := seedTestRepo(t, d)
|
|
|
|
m, err := d.GetManifestByTag(repoID, "latest")
|
|
if err != nil {
|
|
t.Fatalf("GetManifestByTag: %v", err)
|
|
}
|
|
if m.Digest != "sha256:aaaa" {
|
|
t.Fatalf("digest: got %q, want %q", m.Digest, "sha256:aaaa")
|
|
}
|
|
if m.MediaType != "application/vnd.oci.image.manifest.v1+json" {
|
|
t.Fatalf("media type: got %q, want OCI manifest", m.MediaType)
|
|
}
|
|
if m.Size != 15 {
|
|
t.Fatalf("size: got %d, want 15", m.Size)
|
|
}
|
|
if string(m.Content) != `{"layers":[]}` {
|
|
t.Fatalf("content: got %q, want {\"layers\":[]}", string(m.Content))
|
|
}
|
|
}
|
|
|
|
func TestGetManifestByTag_NotFound(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
repoID := seedTestRepo(t, d)
|
|
|
|
_, err := d.GetManifestByTag(repoID, "v0.0.0-nonexistent")
|
|
if !errors.Is(err, ErrManifestNotFound) {
|
|
t.Fatalf("expected ErrManifestNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetManifestByDigest_Found(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
repoID := seedTestRepo(t, d)
|
|
|
|
m, err := d.GetManifestByDigest(repoID, "sha256:aaaa")
|
|
if err != nil {
|
|
t.Fatalf("GetManifestByDigest: %v", err)
|
|
}
|
|
if m.Digest != "sha256:aaaa" {
|
|
t.Fatalf("digest: got %q, want %q", m.Digest, "sha256:aaaa")
|
|
}
|
|
if m.RepositoryID != repoID {
|
|
t.Fatalf("repository_id: got %d, want %d", m.RepositoryID, repoID)
|
|
}
|
|
}
|
|
|
|
func TestGetManifestByDigest_NotFound(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
repoID := seedTestRepo(t, d)
|
|
|
|
_, err := d.GetManifestByDigest(repoID, "sha256:nonexistent")
|
|
if !errors.Is(err, ErrManifestNotFound) {
|
|
t.Fatalf("expected ErrManifestNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBlobExistsInRepo_Exists(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
repoID := seedTestRepo(t, d)
|
|
|
|
exists, err := d.BlobExistsInRepo(repoID, "sha256:bbbb")
|
|
if err != nil {
|
|
t.Fatalf("BlobExistsInRepo: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatal("expected blob to exist in repo")
|
|
}
|
|
}
|
|
|
|
func TestBlobExistsInRepo_NotInThisRepo(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
seedTestRepo(t, d) // creates blob sha256:bbbb in myorg/myapp
|
|
|
|
// Create a second repo with no manifests linking the blob.
|
|
_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('other/repo')`)
|
|
if err != nil {
|
|
t.Fatalf("insert other repo: %v", err)
|
|
}
|
|
var otherRepoID int64
|
|
if err := d.QueryRow(`SELECT id FROM repositories WHERE name = 'other/repo'`).Scan(&otherRepoID); err != nil {
|
|
t.Fatalf("select other repo id: %v", err)
|
|
}
|
|
|
|
exists, err := d.BlobExistsInRepo(otherRepoID, "sha256:bbbb")
|
|
if err != nil {
|
|
t.Fatalf("BlobExistsInRepo: %v", err)
|
|
}
|
|
if exists {
|
|
t.Fatal("expected blob to NOT exist in other repo")
|
|
}
|
|
}
|
|
|
|
func TestBlobExistsInRepo_BlobDoesNotExist(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
repoID := seedTestRepo(t, d)
|
|
|
|
exists, err := d.BlobExistsInRepo(repoID, "sha256:nonexistent")
|
|
if err != nil {
|
|
t.Fatalf("BlobExistsInRepo: %v", err)
|
|
}
|
|
if exists {
|
|
t.Fatal("expected blob to not exist")
|
|
}
|
|
}
|
|
|
|
func TestListTags_WithTags(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
repoID := seedTestRepo(t, d)
|
|
|
|
// Add more tags pointing to the same manifest.
|
|
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 []string{"v1.0", "v2.0", "beta"} {
|
|
_, 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)
|
|
}
|
|
}
|
|
|
|
tags, err := d.ListTags(repoID, "", 100)
|
|
if err != nil {
|
|
t.Fatalf("ListTags: %v", err)
|
|
}
|
|
|
|
// Expect alphabetical: beta, latest, v1.0, v2.0
|
|
want := []string{"beta", "latest", "v1.0", "v2.0"}
|
|
if len(tags) != len(want) {
|
|
t.Fatalf("tags count: got %d, want %d", len(tags), len(want))
|
|
}
|
|
for i, tag := range tags {
|
|
if tag != want[i] {
|
|
t.Fatalf("tags[%d]: got %q, want %q", i, tag, want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListTags_Pagination(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
repoID := seedTestRepo(t, d)
|
|
|
|
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 []string{"v1.0", "v2.0", "beta"} {
|
|
_, 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)
|
|
}
|
|
}
|
|
|
|
// First page: 2 tags starting from beginning.
|
|
tags, err := d.ListTags(repoID, "", 2)
|
|
if err != nil {
|
|
t.Fatalf("ListTags page 1: %v", err)
|
|
}
|
|
if len(tags) != 2 {
|
|
t.Fatalf("page 1 count: got %d, want 2", len(tags))
|
|
}
|
|
if tags[0] != "beta" || tags[1] != "latest" {
|
|
t.Fatalf("page 1: got %v, want [beta, latest]", tags)
|
|
}
|
|
|
|
// Second page: after "latest".
|
|
tags, err = d.ListTags(repoID, "latest", 2)
|
|
if err != nil {
|
|
t.Fatalf("ListTags page 2: %v", err)
|
|
}
|
|
if len(tags) != 2 {
|
|
t.Fatalf("page 2 count: got %d, want 2", len(tags))
|
|
}
|
|
if tags[0] != "v1.0" || tags[1] != "v2.0" {
|
|
t.Fatalf("page 2: got %v, want [v1.0, v2.0]", tags)
|
|
}
|
|
|
|
// Third page: after "v2.0" — no more tags.
|
|
tags, err = d.ListTags(repoID, "v2.0", 2)
|
|
if err != nil {
|
|
t.Fatalf("ListTags page 3: %v", err)
|
|
}
|
|
if len(tags) != 0 {
|
|
t.Fatalf("page 3 count: got %d, want 0", len(tags))
|
|
}
|
|
}
|
|
|
|
func TestListTags_Empty(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
// Create a repo with no tags.
|
|
_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('empty/repo')`)
|
|
if err != nil {
|
|
t.Fatalf("insert repo: %v", err)
|
|
}
|
|
var repoID int64
|
|
if err := d.QueryRow(`SELECT id FROM repositories WHERE name = 'empty/repo'`).Scan(&repoID); err != nil {
|
|
t.Fatalf("select repo id: %v", err)
|
|
}
|
|
|
|
tags, err := d.ListTags(repoID, "", 100)
|
|
if err != nil {
|
|
t.Fatalf("ListTags: %v", err)
|
|
}
|
|
if tags != nil {
|
|
t.Fatalf("expected nil tags, got %v", tags)
|
|
}
|
|
}
|
|
|
|
func TestListRepositoryNames_WithRepos(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
for _, name := range []string{"charlie/app", "alpha/lib", "bravo/svc"} {
|
|
_, err := d.Exec(`INSERT INTO repositories (name) VALUES (?)`, name)
|
|
if err != nil {
|
|
t.Fatalf("insert repo %q: %v", name, err)
|
|
}
|
|
}
|
|
|
|
names, err := d.ListRepositoryNames("", 100)
|
|
if err != nil {
|
|
t.Fatalf("ListRepositoryNames: %v", err)
|
|
}
|
|
|
|
want := []string{"alpha/lib", "bravo/svc", "charlie/app"}
|
|
if len(names) != len(want) {
|
|
t.Fatalf("names count: got %d, want %d", len(names), len(want))
|
|
}
|
|
for i, n := range names {
|
|
if n != want[i] {
|
|
t.Fatalf("names[%d]: got %q, want %q", i, n, want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListRepositoryNames_Pagination(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
for _, name := range []string{"charlie/app", "alpha/lib", "bravo/svc"} {
|
|
_, err := d.Exec(`INSERT INTO repositories (name) VALUES (?)`, name)
|
|
if err != nil {
|
|
t.Fatalf("insert repo %q: %v", name, err)
|
|
}
|
|
}
|
|
|
|
// First page: 2.
|
|
names, err := d.ListRepositoryNames("", 2)
|
|
if err != nil {
|
|
t.Fatalf("ListRepositoryNames page 1: %v", err)
|
|
}
|
|
if len(names) != 2 {
|
|
t.Fatalf("page 1 count: got %d, want 2", len(names))
|
|
}
|
|
if names[0] != "alpha/lib" || names[1] != "bravo/svc" {
|
|
t.Fatalf("page 1: got %v", names)
|
|
}
|
|
|
|
// Second page: after "bravo/svc".
|
|
names, err = d.ListRepositoryNames("bravo/svc", 2)
|
|
if err != nil {
|
|
t.Fatalf("ListRepositoryNames page 2: %v", err)
|
|
}
|
|
if len(names) != 1 {
|
|
t.Fatalf("page 2 count: got %d, want 1", len(names))
|
|
}
|
|
if names[0] != "charlie/app" {
|
|
t.Fatalf("page 2: got %v", names)
|
|
}
|
|
}
|
|
|
|
func TestListRepositoryNames_Empty(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
names, err := d.ListRepositoryNames("", 100)
|
|
if err != nil {
|
|
t.Fatalf("ListRepositoryNames: %v", err)
|
|
}
|
|
if names != nil {
|
|
t.Fatalf("expected nil names, got %v", names)
|
|
}
|
|
}
|