package registry import ( "bytes" "context" "encoding/json" "io" "net/http" "net/http/httptest" "os" "testing" "github.com/bnema/zerowrap" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks" "github.com/bnema/gordon/internal/domain" ) func testLogger() zerowrap.Logger { return zerowrap.Default() } func TestHandler_Base(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) req := httptest.NewRequest("GET", "/v2/", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "registry/2.0", rec.Header().Get("Docker-Distribution-API-Version")) } func TestHandler_NotFound(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) req := httptest.NewRequest("GET", "/v2/unknown/path", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusNotFound, rec.Code) } func TestHandler_GetManifest_Success(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) manifestData := []byte(`{"schemaVersion": 2}`) registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{ Name: "myapp", Reference: "latest", ContentType: "application/vnd.docker.distribution.manifest.v2+json", Data: manifestData, }, nil) req := httptest.NewRequest("GET", "/v2/myapp/manifests/latest", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", rec.Header().Get("Content-Type")) assert.Equal(t, manifestData, rec.Body.Bytes()) } func TestHandler_GetManifest_HEAD(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) manifestData := []byte(`{"schemaVersion": 2}`) registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{ Name: "myapp", Reference: "latest", ContentType: "application/vnd.docker.distribution.manifest.v2+json", Data: manifestData, }, nil) req := httptest.NewRequest("HEAD", "/v2/myapp/manifests/latest", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", rec.Header().Get("Content-Type")) // HEAD should not return body assert.Empty(t, rec.Body.Bytes()) } func TestHandler_GetManifest_NotFound(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "notexists").Return(nil, assert.AnError) req := httptest.NewRequest("GET", "/v2/myapp/manifests/notexists", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusNotFound, rec.Code) assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) } func TestHandler_GetManifest_NestedName(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) manifestData := []byte(`{"schemaVersion": 2}`) registrySvc.EXPECT().GetManifest(mock.Anything, "org/project/app", "v1.0").Return(&domain.Manifest{ Name: "org/project/app", Reference: "v1.0", ContentType: "application/vnd.docker.distribution.manifest.v2+json", Data: manifestData, }, nil) req := httptest.NewRequest("GET", "/v2/org/project/app/manifests/v1.0", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) } // TestHandler_ReservedLookingRepositoryIsServedExactly proves the handler // dispatches on the same repository the parser resolves from the last // route marker, so authorization cannot name a different repository than // the one accessed. func TestHandler_ReservedLookingRepositoryIsServedExactly(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) registrySvc.EXPECT().GetManifest(mock.Anything, "manifests/victim", "latest").Return(&domain.Manifest{ Name: "manifests/victim", Reference: "latest", ContentType: "application/vnd.docker.distribution.manifest.v2+json", Data: []byte(`{"schemaVersion": 2}`), }, nil) req := httptest.NewRequest("GET", "/v2/manifests/victim/manifests/latest", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) } func TestHandler_PutManifest_Success(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) manifestData := []byte(`{"schemaVersion": 2}`) registrySvc.EXPECT().PutManifest(mock.Anything, mock.MatchedBy(func(m *domain.Manifest) bool { return m.Name == "myapp" && m.Reference == "latest" && m.ContentType == "application/vnd.docker.distribution.manifest.v2+json" })).Return("sha256:abc123", nil) req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData)) req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusCreated, rec.Code) assert.Equal(t, "sha256:abc123", rec.Header().Get("Docker-Content-Digest")) assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/manifests/latest") } func TestHandler_PutManifest_MissingContentType(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) manifestData := []byte(`{"schemaVersion": 2}`) req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData)) // No Content-Type header rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestHandler_PutManifest_StorageError(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) manifestData := []byte(`{"schemaVersion": 2}`) registrySvc.EXPECT().PutManifest(mock.Anything, mock.Anything).Return("", assert.AnError) req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData)) req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusInternalServerError, rec.Code) } func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) req := httptest.NewRequest("DELETE", "/v2/myapp/manifests/latest", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusMethodNotAllowed, rec.Code) } func TestHandler_GetBlob_Success(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) server := httptest.NewServer(handler) defer server.Close() // Create a temp file for http.ServeFile tmpFile, err := os.CreateTemp("", "blob-*") assert.NoError(t, err) defer tmpFile.Close() blobContent := []byte("blob content here") _, err = tmpFile.Write(blobContent) assert.NoError(t, err) registrySvc.EXPECT().GetBlobPath(mock.Anything, "myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(tmpFile.Name(), nil) resp, err := server.Client().Get(server.URL + "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4") require.NoError(t, err) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, blobContent, body) } func TestHandler_GetBlob_NotFound(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) server := httptest.NewServer(handler) defer server.Close() registrySvc.EXPECT().GetBlobPath(mock.Anything, "myapp", "sha256:0000000000000000000000000000000000000000000000000000000000000000").Return("", assert.AnError) resp, err := server.Client().Get(server.URL + "/v2/myapp/blobs/sha256:0000000000000000000000000000000000000000000000000000000000000000") require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusNotFound, resp.StatusCode) } func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) req := httptest.NewRequest("POST", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusMethodNotAllowed, rec.Code) assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) } func TestHandler_StartBlobUpload_Success(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil) req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusAccepted, rec.Code) assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000") assert.Equal(t, "0-0", rec.Header().Get("Range")) } func TestHandler_StartBlobUpload_Error(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("", assert.AnError) req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusInternalServerError, rec.Code) assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) } func TestHandler_BlobUpload_PATCH(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) chunkData := []byte("chunk content") registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusAccepted, rec.Code) assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000") } func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) chunkData := []byte("final chunk") registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) registrySvc.EXPECT().FinishUpload(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil) req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData)) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusCreated, rec.Code) assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4") assert.Equal(t, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", rec.Header().Get("Docker-Content-Digest")) } func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) chunkData := []byte("final chunk") // Note: Invalid digest format is rejected by validation before reaching storage layer // This test verifies that invalid digests are properly rejected req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:wrong", bytes.NewReader(chunkData)) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) } func TestHandler_ListTags_Success(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil) req := httptest.NewRequest("GET", "/v2/myapp/tags/list", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) var response struct { Name string `json:"name"` Tags []string `json:"tags"` } assert.NoError(t, json.NewDecoder(rec.Body).Decode(&response)) assert.Equal(t, "myapp", response.Name) assert.Contains(t, response.Tags, "latest") } func TestHandler_ListTags_NotFound(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) registrySvc.EXPECT().ListTags(mock.Anything, "notexists").Return(nil, assert.AnError) req := httptest.NewRequest("GET", "/v2/notexists/tags/list", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusNotFound, rec.Code) assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) } func TestHandler_ListTags_MethodNotAllowed(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) req := httptest.NewRequest("POST", "/v2/myapp/tags/list", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusMethodNotAllowed, rec.Code) assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) } func TestHandler_RegisterRoutes(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) mux := http.NewServeMux() handler.RegisterRoutes(mux) // Verify routes are registered by making a request through the mux req := httptest.NewRequest("GET", "/v2/", nil) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) } func TestHandler_StartBlobUpload_ReturnsDockerUploadUUID(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil) req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusAccepted, rec.Code) assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID")) } func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) chunkData := []byte("chunk content") registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusAccepted, rec.Code) assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID")) } func TestDefaultMaxBlobChunkSize_Is95MB(t *testing.T) { assert.Equal(t, int64(95*1024*1024), int64(DefaultMaxBlobChunkSize)) } func TestHandler_BlobUpload_PATCH_RespectsConfiguredMaxBlobChunkSize(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), 5) // The handler wraps r.Body in MaxBytesReader and passes it to AppendBlobChunk. // The mock must read the body to trigger MaxBytesError, which the handler detects. registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything). Run(func(_ context.Context, _ string, _ string, data io.Reader, _ int64, _ int64) { // Drain the reader to trigger MaxBytesError after the storage layer may // have written the limit-sized prefix. _, _ = io.ReadAll(data) }). Return(int64(0), &http.MaxBytesError{Limit: 5}) registrySvc.EXPECT().CancelUpload(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000").Return(nil) req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("chunk content"))) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code) } func TestHandler_BlobUpload_PATCH_ReturnsRangeHeader(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) chunkData := []byte("chunk content") registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusAccepted, rec.Code) // OCI spec: Range header should reflect total received bytes assert.Equal(t, "0-12", rec.Header().Get("Range")) } func TestDefaultMaxBlobSize_Is1GB(t *testing.T) { assert.Equal(t, int64(1024*1024*1024), int64(DefaultMaxBlobSize)) } func TestHandler_BlobUpload_PATCH_MaxBlobSizeExceededReturnsSizeInvalidAndCancels(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10) registrySvc.EXPECT().AppendBlobChunk( mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, int64(6), int64(10), ).Return(int64(0), domain.ErrBlobSizeExceeded) registrySvc.EXPECT().CancelUpload(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000").Return(nil) req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("123456"))) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code) assert.Contains(t, rec.Body.String(), "SIZE_INVALID") } func TestHandler_BlobUpload_PATCH_ContentLengthTooLargeReturnsSizeInvalidAndCancels(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10) registrySvc.EXPECT().AppendBlobChunk( mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, int64(11), int64(10), ).Return(int64(0), domain.ErrBlobSizeExceeded) registrySvc.EXPECT().CancelUpload(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000").Return(nil) req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("12345678901"))) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code) assert.Contains(t, rec.Body.String(), "SIZE_INVALID") } func TestHandler_BlobUpload_PUT_FinalizeUnderMaxBlobSize(t *testing.T) { registrySvc := inmocks.NewMockRegistryService(t) handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10) chunkData := []byte("1234567890") registrySvc.EXPECT().AppendBlobChunk( mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, int64(len(chunkData)), int64(10), ).Return(int64(len(chunkData)), nil) registrySvc.EXPECT().FinishUpload(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil) req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData)) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusCreated, rec.Code) assert.Equal(t, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", rec.Header().Get("Docker-Content-Digest")) }