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>
This commit is contained in:
141
internal/oci/routes_test.go
Normal file
141
internal/oci/routes_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package oci
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseOCIPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want ociPathInfo
|
||||
wantOK bool
|
||||
}{
|
||||
{
|
||||
name: "simple repo manifest by tag",
|
||||
path: "myrepo/manifests/latest",
|
||||
want: ociPathInfo{name: "myrepo", kind: "manifests", reference: "latest"},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "multi-segment repo manifest by tag",
|
||||
path: "org/team/app/manifests/v1.0",
|
||||
want: ociPathInfo{name: "org/team/app", kind: "manifests", reference: "v1.0"},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "manifest by digest",
|
||||
path: "myrepo/manifests/sha256:abc123def456",
|
||||
want: ociPathInfo{name: "myrepo", kind: "manifests", reference: "sha256:abc123def456"},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "simple repo blob",
|
||||
path: "myrepo/blobs/sha256:abc123",
|
||||
want: ociPathInfo{name: "myrepo", kind: "blobs", reference: "sha256:abc123"},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "multi-segment repo blob",
|
||||
path: "org/team/app/blobs/sha256:abc123",
|
||||
want: ociPathInfo{name: "org/team/app", kind: "blobs", reference: "sha256:abc123"},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "simple repo tags list",
|
||||
path: "myrepo/tags/list",
|
||||
want: ociPathInfo{name: "myrepo", kind: "tags", reference: "list"},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "multi-segment repo tags list",
|
||||
path: "org/app/tags/list",
|
||||
want: ociPathInfo{name: "org/app", kind: "tags", reference: "list"},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "empty path",
|
||||
path: "",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "just repo name",
|
||||
path: "myrepo",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "unknown operation",
|
||||
path: "myrepo/unknown/ref",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "manifests with no ref",
|
||||
path: "myrepo/manifests/",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "blobs with no digest",
|
||||
path: "myrepo/blobs/",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "tags without list suffix",
|
||||
path: "myrepo/tags/something",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "no repo name before manifests",
|
||||
path: "/manifests/latest",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "upload initiate (trailing slash)",
|
||||
path: "myrepo/blobs/uploads/",
|
||||
want: ociPathInfo{name: "myrepo", kind: "uploads", reference: ""},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "upload initiate (no trailing slash)",
|
||||
path: "myrepo/blobs/uploads",
|
||||
want: ociPathInfo{name: "myrepo", kind: "uploads", reference: ""},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "upload with uuid",
|
||||
path: "myrepo/blobs/uploads/abc-123-def",
|
||||
want: ociPathInfo{name: "myrepo", kind: "uploads", reference: "abc-123-def"},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "multi-segment repo upload",
|
||||
path: "org/team/app/blobs/uploads/uuid-456",
|
||||
want: ociPathInfo{name: "org/team/app", kind: "uploads", reference: "uuid-456"},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "multi-segment repo upload initiate",
|
||||
path: "org/team/app/blobs/uploads/",
|
||||
want: ociPathInfo{name: "org/team/app", kind: "uploads", reference: ""},
|
||||
wantOK: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, ok := parseOCIPath(tt.path)
|
||||
if ok != tt.wantOK {
|
||||
t.Fatalf("parseOCIPath(%q) ok = %v, want %v", tt.path, ok, tt.wantOK)
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if got.name != tt.want.name {
|
||||
t.Errorf("name: got %q, want %q", got.name, tt.want.name)
|
||||
}
|
||||
if got.kind != tt.want.kind {
|
||||
t.Errorf("kind: got %q, want %q", got.kind, tt.want.kind)
|
||||
}
|
||||
if got.reference != tt.want.reference {
|
||||
t.Errorf("reference: got %q, want %q", got.reference, tt.want.reference)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user