diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 382b7f71..69a5afbd 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -955,14 +955,6 @@ func (rh *RouteHandler) PatchBlobUpload(response http.ResponseWriter, request *h return } - contentRange := request.Header.Get("Content-Range") - if contentRange == "" { - rh.c.Log.Warn().Str("actual", request.Header.Get("Content-Range")).Msg("invalid content range") - response.WriteHeader(http.StatusRequestedRangeNotSatisfiable) - - return - } - var from, to int64 if from, to, err = getContentRange(request); err != nil || (to-from)+1 != contentLength { response.WriteHeader(http.StatusRequestedRangeNotSatisfiable) diff --git a/pkg/api/routes_test.go b/pkg/api/routes_test.go new file mode 100644 index 00000000..f4ddb3a9 --- /dev/null +++ b/pkg/api/routes_test.go @@ -0,0 +1,1642 @@ +//go:build extended +// +build extended + +package api_test + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gorilla/mux" + "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" + . "github.com/smartystreets/goconvey/convey" + zerr "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/api" + "zotregistry.io/zot/pkg/api/config" + "zotregistry.io/zot/pkg/api/constants" + "zotregistry.io/zot/pkg/storage" + "zotregistry.io/zot/pkg/test" +) + +var ErrUnexpectedError = errors.New("error: unexpected error") + +type MockedImageStore struct { + dirExistsFn func(d string) bool + rootDirFn func() string + initRepoFn func(name string) error + validateRepoFn func(name string) (bool, error) + getRepositoriesFn func() ([]string, error) + getImageTagsFn func(repo string) ([]string, error) + getImageManifestFn func(repo string, reference string) ([]byte, string, string, error) + putImageManifestFn func(repo string, reference string, mediaType string, body []byte) (string, error) + deleteImageManifestFn func(repo string, reference string) error + blobUploadPathFn func(repo string, uuid string) string + newBlobUploadFn func(repo string) (string, error) + getBlobUploadFn func(repo string, uuid string) (int64, error) + blobUploadInfoFn func(repo string, uuid string) (int64, error) + putBlobChunkStreamedFn func(repo string, uuid string, body io.Reader) (int64, error) + putBlobChunkFn func(repo string, uuid string, from int64, to int64, body io.Reader) (int64, error) + finishBlobUploadFn func(repo string, uuid string, body io.Reader, digest string) error + fullBlobUploadFn func(repo string, body io.Reader, digest string) (string, int64, error) + dedupeBlobFn func(src string, dstDigest digest.Digest, dst string) error + deleteBlobUploadFn func(repo string, uuid string) error + blobPathFn func(repo string, digest digest.Digest) string + checkBlobFn func(repo string, digest string) (bool, int64, error) + getBlobFn func(repo string, digest string, mediaType string) (io.Reader, int64, error) + deleteBlobFn func(repo string, digest string) error + getIndexContentFn func(repo string) ([]byte, error) + getBlobContentFn func(repo, digest string) ([]byte, error) + getReferrersFn func(repo, digest string, mediaType string) ([]artifactspec.Descriptor, error) + urlForPathFn func(path string) (string, error) + runGCPeriodicallyFn func(gcInterval time.Duration) +} + +func (is *MockedImageStore) Lock(t *time.Time) { +} + +func (is *MockedImageStore) Unlock(t *time.Time) { +} + +func (is *MockedImageStore) RUnlock(t *time.Time) { +} + +func (is *MockedImageStore) RLock(t *time.Time) { +} + +func (is *MockedImageStore) DirExists(d string) bool { + if is != nil && is.dirExistsFn != nil { + return is.dirExistsFn(d) + } + + return true +} + +func (is *MockedImageStore) RootDir() string { + if is != nil && is.rootDirFn != nil { + return is.rootDirFn() + } + + return "" +} + +func (is *MockedImageStore) InitRepo(name string) error { + if is != nil && is.initRepoFn != nil { + return is.initRepoFn(name) + } + + return nil +} + +func (is *MockedImageStore) ValidateRepo(name string) (bool, error) { + if is != nil && is.validateRepoFn != nil { + return is.validateRepoFn(name) + } + + return true, nil +} + +func (is *MockedImageStore) GetRepositories() ([]string, error) { + if is != nil && is.getRepositoriesFn != nil { + return is.getRepositoriesFn() + } + + return []string{}, nil +} + +func (is *MockedImageStore) GetImageManifest(repo string, reference string) ([]byte, string, string, error) { + if is != nil && is.getImageManifestFn != nil { + return is.getImageManifestFn(repo, reference) + } + + return []byte{}, "", "", nil +} + +func (is *MockedImageStore) PutImageManifest( + repo string, + reference string, + mediaType string, + body []byte, +) (string, error) { + if is != nil && is.putImageManifestFn != nil { + return is.putImageManifestFn(repo, reference, mediaType, body) + } + + return "", nil +} + +func (is *MockedImageStore) GetImageTags(name string) ([]string, error) { + if is != nil && is.getImageTagsFn != nil { + return is.getImageTagsFn(name) + } + + return []string{}, nil +} + +func (is *MockedImageStore) DeleteImageManifest(name string, reference string) error { + if is != nil && is.deleteImageManifestFn != nil { + return is.deleteImageManifestFn(name, reference) + } + + return nil +} + +func (is *MockedImageStore) NewBlobUpload(repo string) (string, error) { + if is != nil && is.newBlobUploadFn != nil { + return is.newBlobUploadFn(repo) + } + + return "", nil +} + +func (is *MockedImageStore) GetBlobUpload(repo string, uuid string) (int64, error) { + if is != nil && is.getBlobUploadFn != nil { + return is.getBlobUploadFn(repo, uuid) + } + + return 0, nil +} + +func (is *MockedImageStore) BlobUploadInfo(repo string, uuid string) (int64, error) { + if is != nil && is.blobUploadInfoFn != nil { + return is.blobUploadInfoFn(repo, uuid) + } + + return 0, nil +} + +func (is *MockedImageStore) BlobUploadPath(repo string, uuid string) string { + if is != nil && is.blobUploadPathFn != nil { + return is.blobUploadPathFn(repo, uuid) + } + + return "" +} + +func (is *MockedImageStore) PutBlobChunkStreamed(repo string, uuid string, body io.Reader) (int64, error) { + if is != nil && is.putBlobChunkStreamedFn != nil { + return is.putBlobChunkStreamedFn(repo, uuid, body) + } + + return 0, nil +} + +func (is *MockedImageStore) PutBlobChunk( + repo string, + uuid string, + from int64, + to int64, + body io.Reader, +) (int64, error) { + if is != nil && is.putBlobChunkFn != nil { + return is.putBlobChunkFn(repo, uuid, from, to, body) + } + + return 0, nil +} + +func (is *MockedImageStore) FinishBlobUpload(repo string, uuid string, body io.Reader, digest string) error { + if is != nil && is.finishBlobUploadFn != nil { + return is.finishBlobUploadFn(repo, uuid, body, digest) + } + + return nil +} + +func (is *MockedImageStore) FullBlobUpload(repo string, body io.Reader, digest string) (string, int64, error) { + if is != nil && is.fullBlobUploadFn != nil { + return is.fullBlobUploadFn(repo, body, digest) + } + + return "", 0, nil +} + +func (is *MockedImageStore) DedupeBlob(src string, dstDigest digest.Digest, dst string) error { + if is != nil && is.dedupeBlobFn != nil { + return is.dedupeBlobFn(src, dstDigest, dst) + } + + return nil +} + +func (is *MockedImageStore) DeleteBlob(repo string, digest string) error { + if is != nil && is.deleteBlobFn != nil { + return is.deleteBlobFn(repo, digest) + } + + return nil +} + +func (is *MockedImageStore) BlobPath(repo string, digest digest.Digest) string { + if is != nil && is.blobPathFn != nil { + return is.blobPathFn(repo, digest) + } + + return "" +} + +func (is *MockedImageStore) CheckBlob(repo string, digest string) (bool, int64, error) { + if is != nil && is.checkBlobFn != nil { + return is.checkBlobFn(repo, digest) + } + + return true, 0, nil +} + +func (is *MockedImageStore) GetBlob(repo string, digest string, mediaType string) (io.Reader, int64, error) { + if is != nil && is.getBlobFn != nil { + return is.getBlobFn(repo, digest, mediaType) + } + + return &io.LimitedReader{}, 0, nil +} + +func (is *MockedImageStore) DeleteBlobUpload(repo string, digest string) error { + if is != nil && is.deleteBlobUploadFn != nil { + return is.deleteBlobUploadFn(repo, digest) + } + + return nil +} + +func (is *MockedImageStore) GetIndexContent(repo string) ([]byte, error) { + if is != nil && is.getIndexContentFn != nil { + return is.getIndexContentFn(repo) + } + + return []byte{}, nil +} + +func (is *MockedImageStore) GetBlobContent(repo string, digest string) ([]byte, error) { + if is != nil && is.getBlobContentFn != nil { + return is.getBlobContentFn(repo, digest) + } + + return []byte{}, nil +} + +func (is *MockedImageStore) GetReferrers( + repo string, + digest string, + mediaType string, +) ([]artifactspec.Descriptor, error) { + if is != nil && is.getReferrersFn != nil { + return is.getReferrersFn(repo, digest, mediaType) + } + + return []artifactspec.Descriptor{}, nil +} + +func (is *MockedImageStore) URLForPath(path string) (string, error) { + if is != nil && is.urlForPathFn != nil { + return is.urlForPathFn(path) + } + + return "", nil +} + +func (is *MockedImageStore) RunGCPeriodically(gcInterval time.Duration) { + if is != nil && is.runGCPeriodicallyFn != nil { + is.runGCPeriodicallyFn(gcInterval) + } +} + +func TestRoutes(t *testing.T) { + Convey("Make a new controller", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + ctlr := api.NewController(conf) + + ctlr.Config.Storage.RootDirectory = t.TempDir() + ctlr.Config.Storage.Commit = true + + err := test.CopyFiles("../../test/data", ctlr.Config.Storage.RootDirectory) + if err != nil { + panic(err) + } + + go startServer(ctlr) + defer stopServer(ctlr) + test.WaitTillServerReady(baseURL) + + rthdlr := api.NewRouteHandler(ctlr) + + // NOTE: the url or method itself doesn't matter below since we are calling the handlers directly, + // so path routing is bypassed + + Convey("Get manifest", func() { + // overwrite controller storage + ctlr.StoreController.DefaultStore = &MockedImageStore{ + getImageManifestFn: func(repo string, reference string) ([]byte, string, string, error) { + return []byte{}, "", "", zerr.ErrRepoBadVersion + }, + } + + request, _ := http.NewRequestWithContext(context.TODO(), "GET", baseURL, nil) + request = mux.SetURLVars(request, map[string]string{ + "name": "test", + "reference": "b8b1231908844a55c251211c7a67ae3c809fb86a081a8eeb4a715e6d7d65625c", + }) + response := httptest.NewRecorder() + + rthdlr.GetManifest(response, request) + + resp := response.Result() + + defer resp.Body.Close() + So(resp, ShouldNotBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusNotFound) + }) + + Convey("UpdateManifest ", func() { + testUpdateManifest := func(urlVars map[string]string, ism *MockedImageStore) int { + ctlr.StoreController.DefaultStore = ism + str := []byte("test") + request, _ := http.NewRequestWithContext(context.TODO(), "PUT", baseURL, bytes.NewBuffer(str)) + request = mux.SetURLVars(request, urlVars) + request.Header.Add("Content-Type", ispec.MediaTypeImageManifest) + response := httptest.NewRecorder() + + rthdlr.UpdateManifest(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + // repo not found + statusCode := testUpdateManifest( + map[string]string{ + "name": "test", + "reference": "reference", + }, + &MockedImageStore{ + putImageManifestFn: func(repo, reference, mediaType string, body []byte) (string, error) { + return "", zerr.ErrRepoNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + // ErrManifestNotFound + statusCode = testUpdateManifest( + map[string]string{ + "name": "test", + "reference": "reference", + }, + + &MockedImageStore{ + putImageManifestFn: func(repo, reference, mediaType string, body []byte) (string, error) { + return "", zerr.ErrManifestNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + // ErrBadManifest + statusCode = testUpdateManifest( + map[string]string{ + "name": "test", + "reference": "reference", + }, + &MockedImageStore{ + putImageManifestFn: func(repo, reference, mediaType string, body []byte) (string, error) { + return "", zerr.ErrBadManifest + }, + }) + So(statusCode, ShouldEqual, http.StatusBadRequest) + // ErrBlobNotFound + statusCode = testUpdateManifest( + map[string]string{ + "name": "test", + "reference": "reference", + }, + &MockedImageStore{ + putImageManifestFn: func(repo, reference, mediaType string, body []byte) (string, error) { + return "", zerr.ErrBlobNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusBadRequest) + + // ErrRepoBadVersion + statusCode = testUpdateManifest( + map[string]string{ + "name": "test", + "reference": "reference", + }, + &MockedImageStore{ + putImageManifestFn: func(repo, reference, mediaType string, body []byte) (string, error) { + return "", zerr.ErrRepoBadVersion + }, + }) + So(statusCode, ShouldEqual, http.StatusInternalServerError) + }) + + Convey("DeleteManifest", func() { + testDeleteManifest := func(headers map[string]string, urlVars map[string]string, ism *MockedImageStore) int { + ctlr.StoreController.DefaultStore = ism + request, _ := http.NewRequestWithContext(context.Background(), "DELETE", baseURL, nil) + request = mux.SetURLVars(request, urlVars) + for k, v := range headers { + request.Header.Add(k, v) + } + response := httptest.NewRecorder() + + rthdlr.DeleteManifest(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + + // ErrRepoNotFound + statusCode := testDeleteManifest( + map[string]string{}, + map[string]string{ + "name": "ErrManifestNotFound", + "reference": "reference", + }, + &MockedImageStore{ + deleteImageManifestFn: func(repo, reference string) error { + return zerr.ErrRepoNotFound + }, + }, + ) + So(statusCode, ShouldEqual, http.StatusBadRequest) + + // ErrManifestNotFound + statusCode = testDeleteManifest( + map[string]string{}, + map[string]string{ + "name": "ErrManifestNotFound", + "reference": "reference", + }, + &MockedImageStore{ + deleteImageManifestFn: func(repo, reference string) error { + return zerr.ErrManifestNotFound + }, + }, + ) + So(statusCode, ShouldEqual, http.StatusNotFound) + + // ErrUnexpectedError + statusCode = testDeleteManifest( + map[string]string{}, + map[string]string{ + "name": "ErrUnexpectedError", + "reference": "reference", + }, + &MockedImageStore{ + deleteImageManifestFn: func(repo, reference string) error { + return ErrUnexpectedError + }, + }, + ) + So(statusCode, ShouldEqual, http.StatusInternalServerError) + + // ErrBadManifest + statusCode = testDeleteManifest( + map[string]string{}, + map[string]string{ + "name": "ErrBadManifest", + "reference": "reference", + }, + &MockedImageStore{ + deleteImageManifestFn: func(repo, reference string) error { + return zerr.ErrBadManifest + }, + }, + ) + So(statusCode, ShouldEqual, http.StatusBadRequest) + }) + + Convey("DeleteBlob", func() { + testDeleteBlob := func(urlVars map[string]string, ism *MockedImageStore) int { + ctlr.StoreController.DefaultStore = ism + request, _ := http.NewRequestWithContext(context.TODO(), "DELETE", baseURL, nil) + request = mux.SetURLVars(request, urlVars) + response := httptest.NewRecorder() + + rthdlr.DeleteBlob(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + + // ErrUnexpectedError + statusCode := testDeleteBlob( + map[string]string{ + "name": "ErrUnexpectedError", + "digest": "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621", + }, + &MockedImageStore{ + deleteBlobFn: func(repo, digest string) error { + return ErrUnexpectedError + }, + }) + So(statusCode, ShouldEqual, http.StatusInternalServerError) + + statusCode = testDeleteBlob( + map[string]string{ + "name": "ErrBadBlobDigest", + "digest": "sha256:7b8437f04f83f084b7ed68ad8c4a4947e12fc4e1b006b38129bac89114ec3621", + }, + &MockedImageStore{ + deleteBlobFn: func(repo, digest string) error { + return zerr.ErrBadBlobDigest + }, + }) + So(statusCode, ShouldEqual, http.StatusBadRequest) + + // ErrBlobNotFound + statusCode = testDeleteBlob( + map[string]string{ + "name": "ErrBlobNotFound", + "digest": "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621", + }, + &MockedImageStore{ + deleteBlobFn: func(repo, digest string) error { + return zerr.ErrBlobNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + + // ErrRepoNotFound + statusCode = testDeleteBlob( + map[string]string{ + "name": "ErrRepoNotFound", + "digest": "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621", + }, + &MockedImageStore{ + deleteBlobFn: func(repo, digest string) error { + return zerr.ErrRepoNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + }) + + // Check Blob + Convey("CheckBlob", func() { + testCheckBlob := func(urlVars map[string]string, ism *MockedImageStore) int { + ctlr.StoreController.DefaultStore = ism + request, _ := http.NewRequestWithContext(context.TODO(), "HEAD", baseURL, nil) + request = mux.SetURLVars(request, urlVars) + response := httptest.NewRecorder() + + rthdlr.CheckBlob(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + + // ErrBadBlobDigest + statusCode := testCheckBlob( + map[string]string{ + "name": "ErrBadBlobDigest", + "digest": "1234", + }, + &MockedImageStore{ + checkBlobFn: func(repo, digest string) (bool, int64, error) { + return true, 0, zerr.ErrBadBlobDigest + }, + }) + So(statusCode, ShouldEqual, http.StatusBadRequest) + + // ErrRepoNotFound + statusCode = testCheckBlob( + map[string]string{ + "name": "ErrRepoNotFound", + "digest": "1234", + }, + &MockedImageStore{ + checkBlobFn: func(repo, digest string) (bool, int64, error) { + return true, 0, zerr.ErrRepoNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + + // ErrBlobNotFound + statusCode = testCheckBlob( + map[string]string{ + "name": "ErrBlobNotFound", + "digest": "1234", + }, + &MockedImageStore{ + checkBlobFn: func(repo, digest string) (bool, int64, error) { + return true, 0, zerr.ErrBlobNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + + // ErrUnexpectedError + statusCode = testCheckBlob( + map[string]string{ + "name": "ErrUnexpectedError", + "digest": "1234", + }, + &MockedImageStore{ + checkBlobFn: func(repo, digest string) (bool, int64, error) { + return true, 0, ErrUnexpectedError + }, + }) + So(statusCode, ShouldEqual, http.StatusInternalServerError) + + // Error Check Blob is not ok + statusCode = testCheckBlob( + map[string]string{ + "name": "Check Blob Not Ok", + "digest": "1234", + }, + &MockedImageStore{ + checkBlobFn: func(repo, digest string) (bool, int64, error) { + return false, 0, nil + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + }) + + Convey("GetBlob", func() { + testGetBlob := func(urlVars map[string]string, ism *MockedImageStore) int { + ctlr.StoreController.DefaultStore = ism + request, _ := http.NewRequestWithContext(context.TODO(), "GET", baseURL, nil) + request = mux.SetURLVars(request, urlVars) + response := httptest.NewRecorder() + + rthdlr.GetBlob(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + // ErrRepoNotFound + statusCode := testGetBlob( + map[string]string{ + "name": "ErrRepoNotFound", + "digest": "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621", + }, + &MockedImageStore{ + getBlobFn: func(repo, digest, mediaType string) (io.Reader, int64, error) { + return bytes.NewBuffer([]byte("")), 0, zerr.ErrRepoNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + + // ErrRepoNotFound + statusCode = testGetBlob( + map[string]string{ + "name": "ErrRepoNotFound", + "digest": "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621", + }, + &MockedImageStore{ + getBlobFn: func(repo, digest, mediaType string) (io.Reader, int64, error) { + return bytes.NewBuffer([]byte("")), 0, zerr.ErrBadBlobDigest + }, + }) + So(statusCode, ShouldEqual, http.StatusBadRequest) + }) + + Convey("CreateBlobUpload", func() { + testCreateBlobUpload := func( + query []struct{ k, v string }, + headers map[string]string, + ism *MockedImageStore, + ) int { + ctlr.StoreController.DefaultStore = ism + request, _ := http.NewRequestWithContext(context.TODO(), "POST", baseURL, nil) + request = mux.SetURLVars(request, + map[string]string{ + "name": "test", + "mount": "1234", + }) + + q := request.URL.Query() + for _, qe := range query { + q.Add(qe.k, qe.v) + } + request.URL.RawQuery = q.Encode() + + for k, v := range headers { + request.Header.Add(k, v) + } + + response := httptest.NewRecorder() + + rthdlr.CreateBlobUpload(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + + // ErrRepoNotFound + statusCode := testCreateBlobUpload( + []struct{ k, v string }{ + {"mount", "1234"}, + }, + map[string]string{}, + &MockedImageStore{ + newBlobUploadFn: func(repo string) (string, error) { + return "", zerr.ErrRepoNotFound + }, + checkBlobFn: func(repo, digest string) (bool, int64, error) { + return true, 0, zerr.ErrRepoNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + + // a full blob upload if multiple digests are present + statusCode = testCreateBlobUpload( + []struct{ k, v string }{ + {"digest", "1234"}, + {"digest", "5234"}, + }, + map[string]string{}, + &MockedImageStore{ + newBlobUploadFn: func(repo string) (string, error) { + return "", zerr.ErrRepoNotFound + }, + checkBlobFn: func(repo, digest string) (bool, int64, error) { + return true, 0, zerr.ErrRepoNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusBadRequest) + + // a full blob upload if content type is wrong + statusCode = testCreateBlobUpload( + []struct{ k, v string }{ + {"digest", "1234"}, + }, + map[string]string{ + "Content-Type": "badContentType", + }, + &MockedImageStore{ + newBlobUploadFn: func(repo string) (string, error) { + return "", zerr.ErrRepoNotFound + }, + checkBlobFn: func(repo, digest string) (bool, int64, error) { + return true, 0, zerr.ErrRepoNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusUnsupportedMediaType) + + // digest prezent imgStore err + statusCode = testCreateBlobUpload( + []struct{ k, v string }{ + {"digest", "1234"}, + }, + map[string]string{ + "Content-Type": constants.BinaryMediaType, + "Content-Length": "100", + }, + &MockedImageStore{ + fullBlobUploadFn: func(repo string, body io.Reader, digest string) (string, int64, error) { + return "session", 0, zerr.ErrBadBlobDigest + }, + }) + So(statusCode, ShouldEqual, http.StatusInternalServerError) + + // digest prezent bad length + statusCode = testCreateBlobUpload( + []struct{ k, v string }{ + {"digest", "1234"}, + }, + map[string]string{ + "Content-Type": constants.BinaryMediaType, + "Content-Length": "100", + }, + &MockedImageStore{ + fullBlobUploadFn: func(repo string, body io.Reader, digest string) (string, int64, error) { + return "session", 20, nil + }, + }) + So(statusCode, ShouldEqual, http.StatusInternalServerError) + + // newBlobUpload not found + statusCode = testCreateBlobUpload( + []struct{ k, v string }{}, + map[string]string{ + "Content-Type": constants.BinaryMediaType, + "Content-Length": "100", + }, + &MockedImageStore{ + newBlobUploadFn: func(repo string) (string, error) { + return "", zerr.ErrRepoNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + + // newBlobUpload unexpected error + statusCode = testCreateBlobUpload( + []struct{ k, v string }{}, + map[string]string{ + "Content-Type": constants.BinaryMediaType, + "Content-Length": "100", + }, + &MockedImageStore{ + newBlobUploadFn: func(repo string) (string, error) { + return "", ErrUnexpectedError + }, + }) + So(statusCode, ShouldEqual, http.StatusInternalServerError) + }) + + Convey("GetBlobUpload", func() { + testGetBlobUpload := func( + query []struct{ k, v string }, + headers map[string]string, + vars map[string]string, + ism *MockedImageStore, + ) int { + ctlr.StoreController.DefaultStore = ism + request, _ := http.NewRequestWithContext(context.TODO(), "GET", baseURL, nil) + request = mux.SetURLVars(request, vars) + + q := request.URL.Query() + for _, qe := range query { + q.Add(qe.k, qe.v) + } + request.URL.RawQuery = q.Encode() + + for k, v := range headers { + request.Header.Add(k, v) + } + + response := httptest.NewRecorder() + + rthdlr.GetBlobUpload(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + + // ErrBadUploadRange + statusCode := testGetBlobUpload( + []struct{ k, v string }{}, + map[string]string{}, + map[string]string{ + "name": "test", + "session_id": "1234", + }, + &MockedImageStore{ + getBlobUploadFn: func(repo, uuid string) (int64, error) { + return 0, zerr.ErrBadUploadRange + }, + }) + So(statusCode, ShouldEqual, http.StatusBadRequest) + + // ErrBadBlobDigest + statusCode = testGetBlobUpload( + []struct{ k, v string }{ + {"mount", "1234"}, + }, + map[string]string{}, + map[string]string{ + "name": "test", + "session_id": "1234", + }, + &MockedImageStore{ + getBlobUploadFn: func(repo, uuid string) (int64, error) { + return 0, zerr.ErrBadBlobDigest + }, + }) + So(statusCode, ShouldEqual, http.StatusBadRequest) + + // ErrRepoNotFound + statusCode = testGetBlobUpload( + []struct{ k, v string }{ + {"mount", "1234"}, + }, + map[string]string{}, + map[string]string{ + "name": "test", + "session_id": "1234", + }, + &MockedImageStore{ + getBlobUploadFn: func(repo, uuid string) (int64, error) { + return 0, zerr.ErrRepoNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + + // ErrUploadNotFound + statusCode = testGetBlobUpload( + []struct{ k, v string }{ + {"mount", "1234"}, + }, + map[string]string{}, + map[string]string{ + "name": "test", + "session_id": "1234", + }, + &MockedImageStore{ + getBlobUploadFn: func(repo, uuid string) (int64, error) { + return 0, zerr.ErrUploadNotFound + }, + }) + So(statusCode, ShouldEqual, http.StatusNotFound) + + // ErrUploadNotFound + statusCode = testGetBlobUpload( + []struct{ k, v string }{ + {"mount", "1234"}, + }, + map[string]string{}, + map[string]string{ + "name": "test", + "session_id": "1234", + }, + &MockedImageStore{ + getBlobUploadFn: func(repo, uuid string) (int64, error) { + return 0, ErrUnexpectedError + }, + }) + So(statusCode, ShouldEqual, http.StatusInternalServerError) + }) + + Convey("PatchBlobUpload", func() { + testPatchBlobUpload := func( + query []struct{ k, v string }, + headers map[string]string, + vars map[string]string, + ism *MockedImageStore, + ) int { + ctlr.StoreController.DefaultStore = ism + request, _ := http.NewRequestWithContext(context.TODO(), "PATCH", baseURL, nil) + + request = mux.SetURLVars(request, vars) + + q := request.URL.Query() + for _, qe := range query { + q.Add(qe.k, qe.v) + } + request.URL.RawQuery = q.Encode() + + for k, v := range headers { + request.Header.Add(k, v) + } + + response := httptest.NewRecorder() + + rthdlr.PatchBlobUpload(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + + status := testPatchBlobUpload( + []struct{ k, v string }{}, + map[string]string{ + "Content-Length": "abc", + "Content-Range": "abc", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{}, + ) + So(status, ShouldEqual, http.StatusBadRequest) + + status = testPatchBlobUpload( + []struct{ k, v string }{}, + map[string]string{ + "Content-Length": "100", + "Content-Range": "1-50", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{}, + ) + So(status, ShouldEqual, http.StatusRequestedRangeNotSatisfiable) + + status = testPatchBlobUpload( + []struct{ k, v string }{}, + map[string]string{ + "Content-Length": "100", + "Content-Range": "1-100", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + putBlobChunkFn: func(repo, uuid string, from, to int64, body io.Reader) (int64, error) { + return 100, zerr.ErrRepoNotFound + }, + }, + ) + So(status, ShouldEqual, http.StatusNotFound) + + status = testPatchBlobUpload( + []struct{ k, v string }{}, + map[string]string{ + "Content-Length": "100", + "Content-Range": "1-100", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + putBlobChunkFn: func(repo, uuid string, from, to int64, body io.Reader) (int64, error) { + return 100, zerr.ErrUploadNotFound + }, + }, + ) + So(status, ShouldEqual, http.StatusNotFound) + + status = testPatchBlobUpload( + []struct{ k, v string }{}, + map[string]string{ + "Content-Length": "100", + "Content-Range": "1-100", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + putBlobChunkFn: func(repo, uuid string, from, to int64, body io.Reader) (int64, error) { + return 100, ErrUnexpectedError + }, + }, + ) + So(status, ShouldEqual, http.StatusInternalServerError) + }) + + Convey("UpdateBlobUpload", func() { + testUpdateBlobUpload := func( + query []struct{ k, v string }, + headers map[string]string, + vars map[string]string, + ism *MockedImageStore, + ) int { + ctlr.StoreController.DefaultStore = ism + request, _ := http.NewRequestWithContext(context.TODO(), "PATCH", baseURL, nil) + + request = mux.SetURLVars(request, vars) + + q := request.URL.Query() + for _, qe := range query { + q.Add(qe.k, qe.v) + } + request.URL.RawQuery = q.Encode() + + for k, v := range headers { + request.Header.Add(k, v) + } + + response := httptest.NewRecorder() + + rthdlr.UpdateBlobUpload(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + + status := testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "", + "Content-Range": "", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{}, + ) + So(status, ShouldEqual, http.StatusBadRequest) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "100", + "Content-Range": "badRange", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{}, + ) + So(status, ShouldEqual, http.StatusRequestedRangeNotSatisfiable) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "100", + "Content-Range": "1-100", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + putBlobChunkFn: func(repo, uuid string, from, to int64, body io.Reader) (int64, error) { + return 0, zerr.ErrBadUploadRange + }, + }, + ) + So(status, ShouldEqual, http.StatusBadRequest) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "100", + "Content-Range": "1-100", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + putBlobChunkFn: func(repo, uuid string, from, to int64, body io.Reader) (int64, error) { + return 0, zerr.ErrRepoNotFound + }, + }, + ) + So(status, ShouldEqual, http.StatusNotFound) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "100", + "Content-Range": "1-100", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + putBlobChunkFn: func(repo, uuid string, from, to int64, body io.Reader) (int64, error) { + return 0, zerr.ErrUploadNotFound + }, + }, + ) + So(status, ShouldEqual, http.StatusNotFound) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "100", + "Content-Range": "1-100", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + putBlobChunkFn: func(repo, uuid string, from, to int64, body io.Reader) (int64, error) { + return 0, ErrUnexpectedError + }, + }, + ) + So(status, ShouldEqual, http.StatusInternalServerError) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "0", + "Content-Range": "", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + finishBlobUploadFn: func(repo, uuid string, body io.Reader, digest string) error { + return zerr.ErrBadBlobDigest + }, + }, + ) + So(status, ShouldEqual, http.StatusBadRequest) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "0", + "Content-Range": "", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + finishBlobUploadFn: func(repo, uuid string, body io.Reader, digest string) error { + return zerr.ErrBadUploadRange + }, + }, + ) + So(status, ShouldEqual, http.StatusBadRequest) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "0", + "Content-Range": "", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + finishBlobUploadFn: func(repo, uuid string, body io.Reader, digest string) error { + return zerr.ErrRepoNotFound + }, + }, + ) + So(status, ShouldEqual, http.StatusNotFound) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "0", + "Content-Range": "", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + finishBlobUploadFn: func(repo, uuid string, body io.Reader, digest string) error { + return zerr.ErrUploadNotFound + }, + }, + ) + So(status, ShouldEqual, http.StatusNotFound) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "0", + "Content-Range": "", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + finishBlobUploadFn: func(repo, uuid string, body io.Reader, digest string) error { + return ErrUnexpectedError + }, + }, + ) + So(status, ShouldEqual, http.StatusInternalServerError) + }) + + Convey("DeleteBlobUpload", func() { + testDeleteBlobUpload := func( + query []struct{ k, v string }, + headers map[string]string, + vars map[string]string, + ism *MockedImageStore, + ) int { + ctlr.StoreController.DefaultStore = ism + request, _ := http.NewRequestWithContext(context.TODO(), "PATCH", baseURL, nil) + + request = mux.SetURLVars(request, vars) + + q := request.URL.Query() + for _, qe := range query { + q.Add(qe.k, qe.v) + } + request.URL.RawQuery = q.Encode() + + for k, v := range headers { + request.Header.Add(k, v) + } + + response := httptest.NewRecorder() + + rthdlr.DeleteBlobUpload(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + + status := testDeleteBlobUpload( + []struct{ k, v string }{}, + map[string]string{}, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + deleteBlobUploadFn: func(repo, uuid string) error { + return zerr.ErrRepoNotFound + }, + }, + ) + So(status, ShouldEqual, http.StatusNotFound) + + status = testDeleteBlobUpload( + []struct{ k, v string }{}, + map[string]string{}, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + deleteBlobUploadFn: func(repo, uuid string) error { + return zerr.ErrUploadNotFound + }, + }, + ) + So(status, ShouldEqual, http.StatusNotFound) + + status = testDeleteBlobUpload( + []struct{ k, v string }{}, + map[string]string{}, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + deleteBlobUploadFn: func(repo, uuid string) error { + return ErrUnexpectedError + }, + }, + ) + So(status, ShouldEqual, http.StatusInternalServerError) + }) + + Convey("ListRepositories", func() { + testListRepositoriesWithSubstores := func( + query []struct{ k, v string }, + headers map[string]string, + vars map[string]string, + ism *MockedImageStore, + ) int { + ctlr.StoreController.DefaultStore = ism + ctlr.StoreController.SubStore = map[string]storage.ImageStore{ + "test": &MockedImageStore{ + getRepositoriesFn: func() ([]string, error) { + return []string{}, ErrUnexpectedError + }, + }, + } + request, _ := http.NewRequestWithContext(context.TODO(), "GET", baseURL, nil) + + request = mux.SetURLVars(request, vars) + + q := request.URL.Query() + for _, qe := range query { + q.Add(qe.k, qe.v) + } + request.URL.RawQuery = q.Encode() + + for k, v := range headers { + request.Header.Add(k, v) + } + + response := httptest.NewRecorder() + + rthdlr.ListRepositories(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + + testListRepositories := func( + query []struct{ k, v string }, + headers map[string]string, + vars map[string]string, + ism *MockedImageStore, + ) int { + ctlr.StoreController.DefaultStore = ism + ctlr.StoreController.SubStore = map[string]storage.ImageStore{} + request, _ := http.NewRequestWithContext(context.TODO(), "PATCH", baseURL, nil) + + request = mux.SetURLVars(request, vars) + + q := request.URL.Query() + for _, qe := range query { + q.Add(qe.k, qe.v) + } + request.URL.RawQuery = q.Encode() + + for k, v := range headers { + request.Header.Add(k, v) + } + + response := httptest.NewRecorder() + + rthdlr.ListRepositories(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + // with substores + status := testListRepositoriesWithSubstores( + []struct{ k, v string }{}, + map[string]string{}, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + getRepositoriesFn: func() ([]string, error) { + return []string{}, ErrUnexpectedError + }, + }, + ) + So(status, ShouldEqual, http.StatusInternalServerError) + + status = testListRepositories( + []struct{ k, v string }{}, + map[string]string{}, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + getRepositoriesFn: func() ([]string, error) { + return []string{}, ErrUnexpectedError + }, + }, + ) + So(status, ShouldEqual, http.StatusInternalServerError) + }) + + Convey("ListRepositories with Authz", func() { + ctlr.StoreController.DefaultStore = &MockedImageStore{ + getRepositoriesFn: func() ([]string, error) { + return []string{"repo"}, nil + }, + } + ctlr.StoreController.SubStore = map[string]storage.ImageStore{ + "test1": &MockedImageStore{ + getRepositoriesFn: func() ([]string, error) { + return []string{"repo1"}, nil + }, + }, + "test2": &MockedImageStore{ + getRepositoriesFn: func() ([]string, error) { + return []string{"repo2"}, nil + }, + }, + } + + // make the user an admin + // acCtx := api.NewAccessControlContext(map[string]bool{}, true) + // ctx := context.WithValue(context.Background(), "ctx", acCtx) + ctx := context.Background() + request, _ := http.NewRequestWithContext(ctx, "GET", baseURL, nil) + request = mux.SetURLVars(request, map[string]string{ + "name": "repo", + "session_id": "test", + }) + response := httptest.NewRecorder() + + rthdlr.ListRepositories(response, request) + + resp := response.Result() + defer resp.Body.Close() + + So(resp.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("Helper functions", func() { + testUpdateBlobUpload := func( + query []struct{ k, v string }, + headers map[string]string, + vars map[string]string, + ism *MockedImageStore, + ) int { + ctlr.StoreController.DefaultStore = ism + request, _ := http.NewRequestWithContext(context.TODO(), "PATCH", baseURL, nil) + + request = mux.SetURLVars(request, vars) + + q := request.URL.Query() + for _, qe := range query { + q.Add(qe.k, qe.v) + } + request.URL.RawQuery = q.Encode() + + for k, v := range headers { + request.Header.Add(k, v) + } + + response := httptest.NewRecorder() + + rthdlr.UpdateBlobUpload(response, request) + + resp := response.Result() + defer resp.Body.Close() + + return resp.StatusCode + } + + status := testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "0", + "Content-Range": "a-100", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + finishBlobUploadFn: func(repo, uuid string, body io.Reader, digest string) error { + return zerr.ErrUploadNotFound + }, + }, + ) + So(status, ShouldEqual, http.StatusRequestedRangeNotSatisfiable) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "0", + "Content-Range": "20-a", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + finishBlobUploadFn: func(repo, uuid string, body io.Reader, digest string) error { + return zerr.ErrUploadNotFound + }, + }, + ) + So(status, ShouldEqual, http.StatusRequestedRangeNotSatisfiable) + + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "0", + "Content-Range": "20-1", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &MockedImageStore{ + finishBlobUploadFn: func(repo, uuid string, body io.Reader, digest string) error { + return zerr.ErrUploadNotFound + }, + }, + ) + So(status, ShouldEqual, http.StatusRequestedRangeNotSatisfiable) + }) + }) +}