// TestAppDispatcher builds an application with a test dispatcher and ensures // that requests are properly dispatched and the handlers are constructed. // This only tests the dispatch mechanism. The underlying dispatchers must be // tested individually. func TestAppDispatcher(t *testing.T) { driver := inmemory.New() ctx := context.Background() app := &App{ Config: configuration.Configuration{}, Context: ctx, router: v2.Router(), driver: driver, registry: storage.NewRegistryWithDriver(ctx, driver, memorycache.NewInMemoryBlobDescriptorCacheProvider(), true, true), } server := httptest.NewServer(app) router := v2.Router() serverURL, err := url.Parse(server.URL) if err != nil { t.Fatalf("error parsing server url: %v", err) } varCheckingDispatcher := func(expectedVars map[string]string) dispatchFunc { return func(ctx *Context, r *http.Request) http.Handler { // Always checks the same name context if ctx.Repository.Name() != getName(ctx) { t.Fatalf("unexpected name: %q != %q", ctx.Repository.Name(), "foo/bar") } // Check that we have all that is expected for expectedK, expectedV := range expectedVars { if ctx.Value(expectedK) != expectedV { t.Fatalf("unexpected %s in context vars: %q != %q", expectedK, ctx.Value(expectedK), expectedV) } } // Check that we only have variables that are expected for k, v := range ctx.Value("vars").(map[string]string) { _, ok := expectedVars[k] if !ok { // name is checked on context // We have an unexpected key, fail t.Fatalf("unexpected key %q in vars with value %q", k, v) } } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) } } // unflatten a list of variables, suitable for gorilla/mux, to a map[string]string unflatten := func(vars []string) map[string]string { m := make(map[string]string) for i := 0; i < len(vars)-1; i = i + 2 { m[vars[i]] = vars[i+1] } return m } for _, testcase := range []struct { endpoint string vars []string }{ { endpoint: v2.RouteNameManifest, vars: []string{ "name", "foo/bar", "reference", "sometag", }, }, { endpoint: v2.RouteNameTags, vars: []string{ "name", "foo/bar", }, }, { endpoint: v2.RouteNameBlob, vars: []string{ "name", "foo/bar", "digest", "tarsum.v1+bogus:abcdef0123456789", }, }, { endpoint: v2.RouteNameBlobUpload, vars: []string{ "name", "foo/bar", }, }, { endpoint: v2.RouteNameBlobUploadChunk, vars: []string{ "name", "foo/bar", "uuid", "theuuid", }, }, } { app.register(testcase.endpoint, varCheckingDispatcher(unflatten(testcase.vars))) route := router.GetRoute(testcase.endpoint).Host(serverURL.Host) u, err := route.URL(testcase.vars...) if err != nil { t.Fatal(err) } resp, err := http.Get(u.String()) if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusOK { t.Fatalf("unexpected status code: %v != %v", resp.StatusCode, http.StatusOK) } } }
// TestBlobDescriptorServiceIsApplied ensures that blobDescriptorService middleware gets applied. // It relies on the fact that blobDescriptorService requires higher levels to set repository object on given // context. If the object isn't given, its method will err out. func TestBlobDescriptorServiceIsApplied(t *testing.T) { ctx := context.Background() // don't do any authorization check installFakeAccessController(t) m := fakeBlobDescriptorService(t) // to make other unit tests working defer m.changeUnsetRepository(false) testImage, err := registrytest.NewImageForManifest("user/app", registrytest.SampleImageManifestSchema1, true) if err != nil { t.Fatal(err) } testImageStream := registrytest.TestNewImageStreamObject("user", "app", "latest", testImage.Name, "") client := &testclient.Fake{} client.AddReactor("get", "imagestreams", imagetest.GetFakeImageStreamGetHandler(t, *testImageStream)) client.AddReactor("get", "images", registrytest.GetFakeImageGetHandler(t, *testImage)) // TODO: get rid of those nasty global vars backupRegistryClient := DefaultRegistryClient DefaultRegistryClient = makeFakeRegistryClient(client, fake.NewSimpleClientset()) defer func() { // set it back once this test finishes to make other unit tests working DefaultRegistryClient = backupRegistryClient }() app := handlers.NewApp(ctx, &configuration.Configuration{ Loglevel: "debug", Auth: map[string]configuration.Parameters{ fakeAuthorizerName: {"realm": fakeAuthorizerName}, }, Storage: configuration.Storage{ "inmemory": configuration.Parameters{}, "cache": configuration.Parameters{ "blobdescriptor": "inmemory", }, "delete": configuration.Parameters{ "enabled": true, }, }, Middleware: map[string][]configuration.Middleware{ "registry": {{Name: "openshift"}}, "repository": {{Name: "openshift"}}, "storage": {{Name: "openshift"}}, }, }) server := httptest.NewServer(app) router := v2.Router() serverURL, err := url.Parse(server.URL) if err != nil { t.Fatalf("error parsing server url: %v", err) } os.Setenv("DOCKER_REGISTRY_URL", serverURL.Host) desc, _, err := registrytest.UploadTestBlob(serverURL, nil, "user/app") if err != nil { t.Fatal(err) } for _, tc := range []struct { name string method string endpoint string vars []string unsetRepository bool expectedStatus int expectedMethodInvocations map[string]int }{ { name: "get blob with repository unset", method: http.MethodGet, endpoint: v2.RouteNameBlob, vars: []string{ "name", "user/app", "digest", desc.Digest.String(), }, unsetRepository: true, expectedStatus: http.StatusInternalServerError, expectedMethodInvocations: map[string]int{"Stat": 1}, }, { name: "get blob", method: http.MethodGet, endpoint: v2.RouteNameBlob, vars: []string{ "name", "user/app", "digest", desc.Digest.String(), }, expectedStatus: http.StatusOK, // 1st stat is invoked in (*distribution/registry/handlers.blobHandler).GetBlob() as a // check of blob existence // 2nd stat happens in (*errorBlobStore).ServeBlob() invoked by the same GetBlob handler // 3rd stat is done by (*blobServiceListener).ServeBlob once the blob serving is finished; // it may happen with a slight delay after the blob was served expectedMethodInvocations: map[string]int{"Stat": 3}, }, { name: "stat blob with repository unset", method: http.MethodHead, endpoint: v2.RouteNameBlob, vars: []string{ "name", "user/app", "digest", desc.Digest.String(), }, unsetRepository: true, expectedStatus: http.StatusInternalServerError, expectedMethodInvocations: map[string]int{"Stat": 1}, }, { name: "stat blob", method: http.MethodHead, endpoint: v2.RouteNameBlob, vars: []string{ "name", "user/app", "digest", desc.Digest.String(), }, expectedStatus: http.StatusOK, // 1st stat is invoked in (*distribution/registry/handlers.blobHandler).GetBlob() as a // check of blob existence // 2nd stat happens in (*errorBlobStore).ServeBlob() invoked by the same GetBlob handler // 3rd stat is done by (*blobServiceListener).ServeBlob once the blob serving is finished; // it may happen with a slight delay after the blob was served expectedMethodInvocations: map[string]int{"Stat": 3}, }, { name: "delete blob with repository unset", method: http.MethodDelete, endpoint: v2.RouteNameBlob, vars: []string{ "name", "user/app", "digest", desc.Digest.String(), }, unsetRepository: true, expectedStatus: http.StatusInternalServerError, expectedMethodInvocations: map[string]int{"Stat": 1}, }, { name: "delete blob", method: http.MethodDelete, endpoint: v2.RouteNameBlob, vars: []string{ "name", "user/app", "digest", desc.Digest.String(), }, expectedStatus: http.StatusAccepted, expectedMethodInvocations: map[string]int{"Stat": 1, "Clear": 1}, }, { name: "get manifest with repository unset", method: http.MethodGet, endpoint: v2.RouteNameManifest, vars: []string{ "name", "user/app", "reference", "latest", }, unsetRepository: true, // failed because we trying to get manifest from storage driver first. expectedStatus: http.StatusNotFound, // manifest can't be retrieved from etcd expectedMethodInvocations: map[string]int{"Stat": 1}, }, { name: "get manifest", method: http.MethodGet, endpoint: v2.RouteNameManifest, vars: []string{ "name", "user/app", "reference", "latest", }, expectedStatus: http.StatusOK, // manifest is retrieved from etcd expectedMethodInvocations: map[string]int{"Stat": 3}, }, { name: "delete manifest with repository unset", method: http.MethodDelete, endpoint: v2.RouteNameManifest, vars: []string{ "name", "user/app", "reference", testImage.Name, }, unsetRepository: true, expectedStatus: http.StatusInternalServerError, // we don't allow to delete manifests from etcd; in this case, we attempt to delete layer link expectedMethodInvocations: map[string]int{"Stat": 1}, }, { name: "delete manifest", method: http.MethodDelete, endpoint: v2.RouteNameManifest, vars: []string{ "name", "user/app", "reference", testImage.Name, }, expectedStatus: http.StatusNotFound, // we don't allow to delete manifests from etcd; in this case, we attempt to delete layer link expectedMethodInvocations: map[string]int{"Stat": 1}, }, } { m.clearStats() m.changeUnsetRepository(tc.unsetRepository) route := router.GetRoute(tc.endpoint).Host(serverURL.Host) u, err := route.URL(tc.vars...) if err != nil { t.Errorf("[%s] failed to build route: %v", tc.name, err) continue } req, err := http.NewRequest(tc.method, u.String(), nil) if err != nil { t.Errorf("[%s] failed to make request: %v", tc.name, err) } client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Errorf("[%s] failed to do the request: %v", tc.name, err) continue } defer resp.Body.Close() if resp.StatusCode != tc.expectedStatus { t.Errorf("[%s] unexpected status code: %v != %v", tc.name, resp.StatusCode, tc.expectedStatus) } if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { content, err := ioutil.ReadAll(resp.Body) if err != nil { t.Errorf("[%s] failed to read body: %v", tc.name, err) } else if len(content) > 0 { errs := errcode.Errors{} err := errs.UnmarshalJSON(content) if err != nil { t.Logf("[%s] failed to parse body as error: %v", tc.name, err) t.Logf("[%s] received body: %v", tc.name, string(content)) } else { t.Logf("[%s] received errors: %#+v", tc.name, errs) } } } stats, err := m.getStats(tc.expectedMethodInvocations, time.Second*5) if err != nil { t.Errorf("[%s] failed to get stats: %v", tc.name, err) } for method, exp := range tc.expectedMethodInvocations { invoked := stats[method] if invoked != exp { t.Errorf("[%s] unexpected number of invocations of method %q: %v != %v", tc.name, method, invoked, exp) } } for method, invoked := range stats { if _, ok := tc.expectedMethodInvocations[method]; !ok { t.Errorf("[%s] unexpected method %q invoked %d times", tc.name, method, invoked) } } } }