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>
279 lines
6.9 KiB
Go
279 lines
6.9 KiB
Go
package db
|
|
|
|
import "testing"
|
|
|
|
func TestGetOrCreateRepositoryNew(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
id, err := d.GetOrCreateRepository("newrepo")
|
|
if err != nil {
|
|
t.Fatalf("GetOrCreateRepository: %v", err)
|
|
}
|
|
if id <= 0 {
|
|
t.Fatalf("id: got %d, want > 0", id)
|
|
}
|
|
|
|
// Second call should return the same ID.
|
|
id2, err := d.GetOrCreateRepository("newrepo")
|
|
if err != nil {
|
|
t.Fatalf("GetOrCreateRepository (second): %v", err)
|
|
}
|
|
if id2 != id {
|
|
t.Fatalf("id mismatch: got %d, want %d", id2, id)
|
|
}
|
|
}
|
|
|
|
func TestGetOrCreateRepositoryExisting(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 ('existing')`)
|
|
if err != nil {
|
|
t.Fatalf("insert repo: %v", err)
|
|
}
|
|
|
|
id, err := d.GetOrCreateRepository("existing")
|
|
if err != nil {
|
|
t.Fatalf("GetOrCreateRepository: %v", err)
|
|
}
|
|
if id <= 0 {
|
|
t.Fatalf("id: got %d, want > 0", id)
|
|
}
|
|
}
|
|
|
|
func TestBlobExists(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
_, err := d.Exec(`INSERT INTO blobs (digest, size) VALUES ('sha256:aaa', 100)`)
|
|
if err != nil {
|
|
t.Fatalf("insert blob: %v", err)
|
|
}
|
|
|
|
exists, err := d.BlobExists("sha256:aaa")
|
|
if err != nil {
|
|
t.Fatalf("BlobExists: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatal("expected blob to exist")
|
|
}
|
|
|
|
exists, err = d.BlobExists("sha256:nonexistent")
|
|
if err != nil {
|
|
t.Fatalf("BlobExists (nonexistent): %v", err)
|
|
}
|
|
if exists {
|
|
t.Fatal("expected blob to not exist")
|
|
}
|
|
}
|
|
|
|
func TestInsertBlob(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
if err := d.InsertBlob("sha256:bbb", 200); err != nil {
|
|
t.Fatalf("InsertBlob: %v", err)
|
|
}
|
|
|
|
exists, err := d.BlobExists("sha256:bbb")
|
|
if err != nil {
|
|
t.Fatalf("BlobExists: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatal("expected blob to exist after insert")
|
|
}
|
|
|
|
// Insert again — should be a no-op (INSERT OR IGNORE).
|
|
if err := d.InsertBlob("sha256:bbb", 200); err != nil {
|
|
t.Fatalf("InsertBlob (dup): %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPushManifestByTag(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
// Insert blobs first.
|
|
if err := d.InsertBlob("sha256:config111", 50); err != nil {
|
|
t.Fatalf("insert config blob: %v", err)
|
|
}
|
|
if err := d.InsertBlob("sha256:layer111", 1000); err != nil {
|
|
t.Fatalf("insert layer blob: %v", err)
|
|
}
|
|
|
|
content := []byte(`{"schemaVersion":2}`)
|
|
params := PushManifestParams{
|
|
RepoName: "myrepo",
|
|
Digest: "sha256:manifest111",
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Content: content,
|
|
Size: int64(len(content)),
|
|
Tag: "latest",
|
|
BlobDigests: []string{"sha256:config111", "sha256:layer111"},
|
|
}
|
|
|
|
if err := d.PushManifest(params); err != nil {
|
|
t.Fatalf("PushManifest: %v", err)
|
|
}
|
|
|
|
// Verify repository was created.
|
|
repoID, err := d.GetRepositoryByName("myrepo")
|
|
if err != nil {
|
|
t.Fatalf("GetRepositoryByName: %v", err)
|
|
}
|
|
if repoID <= 0 {
|
|
t.Fatalf("repo id: got %d, want > 0", repoID)
|
|
}
|
|
|
|
// Verify manifest exists.
|
|
m, err := d.GetManifestByDigest(repoID, "sha256:manifest111")
|
|
if err != nil {
|
|
t.Fatalf("GetManifestByDigest: %v", err)
|
|
}
|
|
if m.MediaType != "application/vnd.oci.image.manifest.v1+json" {
|
|
t.Fatalf("media type: got %q", m.MediaType)
|
|
}
|
|
if m.Size != int64(len(content)) {
|
|
t.Fatalf("size: got %d, want %d", m.Size, len(content))
|
|
}
|
|
|
|
// Verify tag points to manifest.
|
|
m2, err := d.GetManifestByTag(repoID, "latest")
|
|
if err != nil {
|
|
t.Fatalf("GetManifestByTag: %v", err)
|
|
}
|
|
if m2.Digest != "sha256:manifest111" {
|
|
t.Fatalf("tag digest: got %q", m2.Digest)
|
|
}
|
|
|
|
// Verify manifest_blobs join table.
|
|
var mbCount int
|
|
if err := d.QueryRow(`SELECT COUNT(*) FROM manifest_blobs WHERE manifest_id = ?`, m.ID).Scan(&mbCount); err != nil {
|
|
t.Fatalf("count manifest_blobs: %v", err)
|
|
}
|
|
if mbCount != 2 {
|
|
t.Fatalf("manifest_blobs count: got %d, want 2", mbCount)
|
|
}
|
|
}
|
|
|
|
func TestPushManifestByDigest(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
content := []byte(`{"schemaVersion":2}`)
|
|
params := PushManifestParams{
|
|
RepoName: "myrepo",
|
|
Digest: "sha256:manifest222",
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Content: content,
|
|
Size: int64(len(content)),
|
|
Tag: "", // push by digest — no tag
|
|
}
|
|
|
|
if err := d.PushManifest(params); err != nil {
|
|
t.Fatalf("PushManifest: %v", err)
|
|
}
|
|
|
|
// Verify no tag was created.
|
|
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("tag count: got %d, want 0", tagCount)
|
|
}
|
|
}
|
|
|
|
func TestPushManifestTagMove(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
// Push first manifest with tag "latest".
|
|
content1 := []byte(`{"schemaVersion":2,"v":"1"}`)
|
|
if err := d.PushManifest(PushManifestParams{
|
|
RepoName: "myrepo",
|
|
Digest: "sha256:first",
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Content: content1,
|
|
Size: int64(len(content1)),
|
|
Tag: "latest",
|
|
}); err != nil {
|
|
t.Fatalf("PushManifest (first): %v", err)
|
|
}
|
|
|
|
// Push second manifest with same tag "latest" — should atomically move tag.
|
|
content2 := []byte(`{"schemaVersion":2,"v":"2"}`)
|
|
if err := d.PushManifest(PushManifestParams{
|
|
RepoName: "myrepo",
|
|
Digest: "sha256:second",
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Content: content2,
|
|
Size: int64(len(content2)),
|
|
Tag: "latest",
|
|
}); err != nil {
|
|
t.Fatalf("PushManifest (second): %v", err)
|
|
}
|
|
|
|
repoID, err := d.GetRepositoryByName("myrepo")
|
|
if err != nil {
|
|
t.Fatalf("GetRepositoryByName: %v", err)
|
|
}
|
|
|
|
m, err := d.GetManifestByTag(repoID, "latest")
|
|
if err != nil {
|
|
t.Fatalf("GetManifestByTag: %v", err)
|
|
}
|
|
if m.Digest != "sha256:second" {
|
|
t.Fatalf("tag should point to second manifest, got %q", m.Digest)
|
|
}
|
|
}
|
|
|
|
func TestPushManifestIdempotent(t *testing.T) {
|
|
d := openTestDB(t)
|
|
if err := d.Migrate(); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
content := []byte(`{"schemaVersion":2}`)
|
|
params := PushManifestParams{
|
|
RepoName: "myrepo",
|
|
Digest: "sha256:manifest333",
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Content: content,
|
|
Size: int64(len(content)),
|
|
Tag: "latest",
|
|
}
|
|
|
|
// Push twice — should not fail.
|
|
if err := d.PushManifest(params); err != nil {
|
|
t.Fatalf("PushManifest (first): %v", err)
|
|
}
|
|
if err := d.PushManifest(params); err != nil {
|
|
t.Fatalf("PushManifest (second): %v", err)
|
|
}
|
|
|
|
// Verify only one manifest exists.
|
|
var mCount int
|
|
if err := d.QueryRow(`SELECT COUNT(*) FROM manifests`).Scan(&mCount); err != nil {
|
|
t.Fatalf("count manifests: %v", err)
|
|
}
|
|
if mCount != 1 {
|
|
t.Fatalf("manifest count: got %d, want 1", mCount)
|
|
}
|
|
}
|