Files
mcr/internal/oci/upload_test.go
Kyle Isom d5580f01f2 Migrate module path from kyle/ to mc/ org
All import paths updated to git.wntrmute.dev/mc/. Bumps mcdsl to v1.2.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:05:59 -07:00

292 lines
8.0 KiB
Go

package oci
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-chi/chi/v5"
"git.wntrmute.dev/mc/mcr/internal/auth"
"git.wntrmute.dev/mc/mcr/internal/storage"
)
// testHandlerWithStorage creates a handler with real storage in t.TempDir().
func testHandlerWithStorage(t *testing.T, fdb *fakeDB) (*Handler, *chi.Mux) {
t.Helper()
dir := t.TempDir()
store := storage.New(dir+"/layers", dir+"/uploads")
h := NewHandler(fdb, store, allowAll(), nil, nil)
router := chi.NewRouter()
router.Mount("/v2", h.Router())
return h, router
}
func authedPushRequest(method, path string, body []byte) *http.Request {
var reader *bytes.Reader
if body != nil {
reader = bytes.NewReader(body)
} else {
reader = bytes.NewReader(nil)
}
req := httptest.NewRequest(method, path, reader)
claims := &auth.Claims{
Subject: "pusher",
AccountType: "human",
Roles: []string{"user"},
}
return req.WithContext(auth.ContextWithClaims(req.Context(), claims))
}
func TestUploadInitiate(t *testing.T) {
fdb := newFakeDB()
_, router := testHandlerWithStorage(t, fdb)
req := authedPushRequest("POST", "/v2/myrepo/blobs/uploads/", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusAccepted {
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusAccepted)
}
loc := rr.Header().Get("Location")
if !strings.HasPrefix(loc, "/v2/myrepo/blobs/uploads/") {
t.Fatalf("Location: got %q", loc)
}
uuid := rr.Header().Get("Docker-Upload-UUID")
if uuid == "" {
t.Fatal("Docker-Upload-UUID header missing")
}
rng := rr.Header().Get("Range")
if rng != "0-0" {
t.Fatalf("Range: got %q, want %q", rng, "0-0")
}
// Verify repo was implicitly created.
if _, ok := fdb.repos["myrepo"]; !ok {
t.Fatal("repository should have been implicitly created")
}
}
func TestUploadInitiateUniqueUUIDs(t *testing.T) {
fdb := newFakeDB()
_, router := testHandlerWithStorage(t, fdb)
uuids := make(map[string]bool)
for range 5 {
req := authedPushRequest("POST", "/v2/myrepo/blobs/uploads/", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusAccepted {
t.Fatalf("status: got %d", rr.Code)
}
uuid := rr.Header().Get("Docker-Upload-UUID")
if uuids[uuid] {
t.Fatalf("duplicate UUID: %s", uuid)
}
uuids[uuid] = true
}
}
func TestMonolithicUpload(t *testing.T) {
fdb := newFakeDB()
_, router := testHandlerWithStorage(t, fdb)
// Step 1: Initiate upload.
req := authedPushRequest("POST", "/v2/myrepo/blobs/uploads/", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusAccepted {
t.Fatalf("initiate status: got %d", rr.Code)
}
uuid := rr.Header().Get("Docker-Upload-UUID")
// Step 2: Complete upload with body and digest in a single PUT.
blobData := []byte("hello world blob data")
sum := sha256.Sum256(blobData)
digest := "sha256:" + hex.EncodeToString(sum[:])
putURL := "/v2/myrepo/blobs/uploads/" + uuid + "?digest=" + digest
req = authedPushRequest("PUT", putURL, blobData)
rr = httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusCreated {
t.Fatalf("complete status: got %d, body: %s", rr.Code, rr.Body.String())
}
loc := rr.Header().Get("Location")
if !strings.Contains(loc, digest) {
t.Fatalf("Location should contain digest: got %q", loc)
}
dcd := rr.Header().Get("Docker-Content-Digest")
if dcd != digest {
t.Fatalf("Docker-Content-Digest: got %q, want %q", dcd, digest)
}
// Verify blob was inserted in fake DB.
if !fdb.allBlobs[digest] {
t.Fatal("blob should exist in DB after upload")
}
}
func TestChunkedUpload(t *testing.T) {
fdb := newFakeDB()
_, router := testHandlerWithStorage(t, fdb)
// Step 1: Initiate.
req := authedPushRequest("POST", "/v2/myrepo/blobs/uploads/", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
uuid := rr.Header().Get("Docker-Upload-UUID")
// Step 2: PATCH chunk 1.
chunk1 := []byte("chunk-one-data-")
req = authedPushRequest("PATCH", "/v2/myrepo/blobs/uploads/"+uuid, chunk1)
rr = httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusAccepted {
t.Fatalf("patch 1 status: got %d", rr.Code)
}
// Step 3: PATCH chunk 2.
chunk2 := []byte("chunk-two-data")
req = authedPushRequest("PATCH", "/v2/myrepo/blobs/uploads/"+uuid, chunk2)
rr = httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusAccepted {
t.Fatalf("patch 2 status: got %d", rr.Code)
}
// Step 4: Complete with PUT.
allData := append(chunk1, chunk2...)
sum := sha256.Sum256(allData)
digest := "sha256:" + hex.EncodeToString(sum[:])
req = authedPushRequest("PUT", "/v2/myrepo/blobs/uploads/"+uuid+"?digest="+digest, nil)
rr = httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusCreated {
t.Fatalf("complete status: got %d, body: %s", rr.Code, rr.Body.String())
}
if rr.Header().Get("Docker-Content-Digest") != digest {
t.Fatalf("Docker-Content-Digest: got %q, want %q", rr.Header().Get("Docker-Content-Digest"), digest)
}
}
func TestUploadDigestMismatch(t *testing.T) {
fdb := newFakeDB()
_, router := testHandlerWithStorage(t, fdb)
// Initiate.
req := authedPushRequest("POST", "/v2/myrepo/blobs/uploads/", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
uuid := rr.Header().Get("Docker-Upload-UUID")
// Complete with wrong digest.
blobData := []byte("some data")
wrongDigest := "sha256:0000000000000000000000000000000000000000000000000000000000000000"
req = authedPushRequest("PUT", "/v2/myrepo/blobs/uploads/"+uuid+"?digest="+wrongDigest, blobData)
rr = httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusBadRequest)
}
var body ociErrorResponse
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
t.Fatalf("decode error: %v", err)
}
if len(body.Errors) != 1 || body.Errors[0].Code != "DIGEST_INVALID" {
t.Fatalf("error code: got %+v, want DIGEST_INVALID", body.Errors)
}
}
func TestUploadStatus(t *testing.T) {
fdb := newFakeDB()
_, router := testHandlerWithStorage(t, fdb)
// Initiate.
req := authedPushRequest("POST", "/v2/myrepo/blobs/uploads/", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
uuid := rr.Header().Get("Docker-Upload-UUID")
// Check status.
req = authedPushRequest("GET", "/v2/myrepo/blobs/uploads/"+uuid, nil)
rr = httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusNoContent {
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusNoContent)
}
if rr.Header().Get("Docker-Upload-UUID") != uuid {
t.Fatalf("Docker-Upload-UUID: got %q", rr.Header().Get("Docker-Upload-UUID"))
}
}
func TestUploadCancel(t *testing.T) {
fdb := newFakeDB()
_, router := testHandlerWithStorage(t, fdb)
// Initiate.
req := authedPushRequest("POST", "/v2/myrepo/blobs/uploads/", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
uuid := rr.Header().Get("Docker-Upload-UUID")
// Cancel.
req = authedPushRequest("DELETE", "/v2/myrepo/blobs/uploads/"+uuid, nil)
rr = httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusNoContent {
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusNoContent)
}
// Verify upload was removed from DB.
if _, ok := fdb.uploads[uuid]; ok {
t.Fatal("upload should have been deleted from DB")
}
}
func TestUploadNonexistentUUID(t *testing.T) {
fdb := newFakeDB()
_, router := testHandlerWithStorage(t, fdb)
req := authedPushRequest("PATCH", "/v2/myrepo/blobs/uploads/nonexistent-uuid", []byte("data"))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusNotFound)
}
var body ociErrorResponse
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
t.Fatalf("decode error: %v", err)
}
if len(body.Errors) != 1 || body.Errors[0].Code != "BLOB_UPLOAD_UNKNOWN" {
t.Fatalf("error code: got %+v, want BLOB_UPLOAD_UNKNOWN", body.Errors)
}
}