From 7538bbf0668ad04a0574adf1fe52e212e71af3ed Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Tue, 16 Aug 2016 13:45:59 +0800 Subject: [PATCH] UT for package utils --- Deploy/coverage4gotest.sh | 2 +- utils/log/level.go | 3 +- utils/log/level_test.go | 61 +++ utils/log/log_test.go | 9 - utils/log/logger_test.go | 156 +++++++ utils/registry/auth/authorizer_test.go | 28 +- utils/registry/auth/tokenauthorizer_test.go | 52 +-- utils/registry/manifest_test.go | 56 +++ utils/registry/registry.go | 14 +- utils/registry/registry_test.go | 150 ++++++ utils/registry/repository.go | 29 +- utils/registry/repository_test.go | 494 ++++++++++++++------ utils/registry/transport_test.go | 60 +++ utils/test/test.go | 88 ++++ 14 files changed, 977 insertions(+), 225 deletions(-) create mode 100644 utils/log/level_test.go delete mode 100644 utils/log/log_test.go create mode 100644 utils/log/logger_test.go create mode 100644 utils/registry/manifest_test.go create mode 100644 utils/registry/registry_test.go create mode 100644 utils/registry/transport_test.go create mode 100644 utils/test/test.go diff --git a/Deploy/coverage4gotest.sh b/Deploy/coverage4gotest.sh index dadbf256f..8fb49b38f 100755 --- a/Deploy/coverage4gotest.sh +++ b/Deploy/coverage4gotest.sh @@ -1,5 +1,5 @@ #!/bin/bash - +set -e echo "mode: set" >>profile.cov for dir in $(go list ./... | grep -v -E 'vendor|tests') do diff --git a/utils/log/level.go b/utils/log/level.go index 84bfc394f..fa39ea075 100644 --- a/utils/log/level.go +++ b/utils/log/level.go @@ -17,6 +17,7 @@ package log import ( "fmt" + "strings" ) // Level ... @@ -56,7 +57,7 @@ func (l Level) string() (lvl string) { func parseLevel(lvl string) (level Level, err error) { - switch lvl { + switch strings.ToLower(lvl) { case "debug": level = DebugLevel case "info": diff --git a/utils/log/level_test.go b/utils/log/level_test.go new file mode 100644 index 000000000..e80adff67 --- /dev/null +++ b/utils/log/level_test.go @@ -0,0 +1,61 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package log + +import ( + "testing" +) + +func TestString(t *testing.T) { + m := map[Level]string{ + DebugLevel: "DEBUG", + InfoLevel: "INFO", + WarningLevel: "WARNING", + ErrorLevel: "ERROR", + FatalLevel: "FATAL", + -1: "UNKNOWN", + } + + for level, str := range m { + if level.string() != str { + t.Errorf("unexpected string: %s != %s", level.string(), str) + } + } +} + +func TestParseLevel(t *testing.T) { + m := map[string]Level{ + "DEBUG": DebugLevel, + "INFO": InfoLevel, + "WARNING": WarningLevel, + "ERROR": ErrorLevel, + "FATAL": FatalLevel, + } + + for str, level := range m { + l, err := parseLevel(str) + if err != nil { + t.Errorf("failed to parse level: %v", err) + } + if l != level { + t.Errorf("unexpected level: %d != %d", l, level) + } + } + + if _, err := parseLevel("UNKNOWN"); err == nil { + t.Errorf("unexpected behaviour: should be error here") + } +} diff --git a/utils/log/log_test.go b/utils/log/log_test.go deleted file mode 100644 index 9f93ee776..000000000 --- a/utils/log/log_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package log - -import ( - "testing" -) - -func TestMain(t *testing.T) { -} - diff --git a/utils/log/logger_test.go b/utils/log/logger_test.go new file mode 100644 index 000000000..7b2e6de18 --- /dev/null +++ b/utils/log/logger_test.go @@ -0,0 +1,156 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package log + +import ( + "bytes" + "os" + "strings" + "testing" +) + +var ( + message = "message" +) + +func TestSetx(t *testing.T) { + logger := New(nil, nil, WarningLevel) + logger.SetOutput(os.Stdout) + fmt := NewTextFormatter() + logger.SetFormatter(fmt) + logger.SetLevel(DebugLevel) + + if logger.out != os.Stdout { + t.Errorf("unexpected outer: %v != %v", logger.out, os.Stdout) + } + + if logger.fmtter != fmt { + t.Errorf("unexpected formatter: %v != %v", logger.fmtter, fmt) + } + + if logger.lvl != DebugLevel { + t.Errorf("unexpected log level: %v != %v", logger.lvl, DebugLevel) + } +} + +func TestDebug(t *testing.T) { + buf := enter() + defer exit() + + Debug(message) + + str := buf.String() + if len(str) != 0 { + t.Errorf("unexpected message: %s != %s", str, "") + } +} + +func TestDebugf(t *testing.T) { + buf := enter() + defer exit() + + Debugf("%s", message) + + str := buf.String() + if len(str) != 0 { + t.Errorf("unexpected message: %s != %s", str, "") + } +} + +func TestInfo(t *testing.T) { + buf := enter() + defer exit() + + Info(message) + + str := buf.String() + if strings.HasSuffix(str, "[INFO] message") { + t.Errorf("unexpected message: %s != %s", str, "") + } +} + +func TestInfof(t *testing.T) { + buf := enter() + defer exit() + + Infof("%s", message) + + str := buf.String() + if strings.HasSuffix(str, "[INFO] message") { + t.Errorf("unexpected message: %s != %s", str, "") + } +} + +func TestWarning(t *testing.T) { + buf := enter() + defer exit() + + Warning(message) + + str := buf.String() + if strings.HasSuffix(str, "[WARNING] message") { + t.Errorf("unexpected message: %s != %s", str, "") + } +} + +func TestWarningf(t *testing.T) { + buf := enter() + defer exit() + + Warningf("%s", message) + + str := buf.String() + if strings.HasSuffix(str, "[WARNING] message") { + t.Errorf("unexpected message: %s != %s", str, "") + } +} + +func TestError(t *testing.T) { + buf := enter() + defer exit() + + Error(message) + + str := buf.String() + if strings.HasSuffix(str, "[ERROR] message") { + t.Errorf("unexpected message: %s != %s", str, "") + } +} + +func TestErrorf(t *testing.T) { + buf := enter() + defer exit() + + Errorf("%s", message) + + str := buf.String() + if strings.HasSuffix(str, "[ERROR] message") { + t.Errorf("unexpected message: %s != %s", str, "") + } +} + +func enter() *bytes.Buffer { + b := make([]byte, 0, 32) + buf := bytes.NewBuffer(b) + + logger.SetOutput(buf) + + return buf +} + +func exit() { + logger.SetOutput(os.Stdout) +} diff --git a/utils/registry/auth/authorizer_test.go b/utils/registry/auth/authorizer_test.go index cfebac3ea..ffc2944e1 100644 --- a/utils/registry/auth/authorizer_test.go +++ b/utils/registry/auth/authorizer_test.go @@ -17,15 +17,26 @@ package auth import ( "net/http" - "net/http/httptest" "strings" "testing" "github.com/docker/distribution/registry/client/auth" + "github.com/vmware/harbor/utils/test" ) func TestNewAuthorizerStore(t *testing.T) { - server := newRegistryServer() + handler := test.Handler(&test.Response{ + StatusCode: http.StatusUnauthorized, + Headers: map[string]string{ + "Www-Authenticate": "Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\"", + }, + }) + + server := test.NewServer(&test.RequestHandlerMapping{ + Method: "GET", + Pattern: "/v2/", + Handler: handler, + }) defer server.Close() _, err := NewAuthorizerStore(server.URL, false, nil) @@ -76,16 +87,3 @@ func TestModify(t *testing.T) { t.Fatal("\"Authorization\" header does not start with \"Bearer\"") } } - -func newRegistryServer() *httptest.Server { - mux := http.NewServeMux() - mux.HandleFunc("/v2/", handlePing) - - return httptest.NewServer(mux) -} - -func handlePing(w http.ResponseWriter, r *http.Request) { - challenge := "Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\"" - w.Header().Set("Www-Authenticate", challenge) - w.WriteHeader(http.StatusUnauthorized) -} diff --git a/utils/registry/auth/tokenauthorizer_test.go b/utils/registry/auth/tokenauthorizer_test.go index 6bf8cf34f..da601ba48 100644 --- a/utils/registry/auth/tokenauthorizer_test.go +++ b/utils/registry/auth/tokenauthorizer_test.go @@ -16,20 +16,29 @@ package auth import ( - "encoding/json" "net/http" - "net/http/httptest" "testing" - "time" -) -var ( - token = "token" + "github.com/vmware/harbor/utils/test" ) func TestAuthorizeOfStandardTokenAuthorizer(t *testing.T) { - tokenServer := newTokenServer() - defer tokenServer.Close() + handler := test.Handler(&test.Response{ + Body: []byte(` + { + "token":"token", + "expires_in":300, + "issued_at":"2016-08-17T23:17:58+08:00" + } + `), + }) + + server := test.NewServer(&test.RequestHandlerMapping{ + Method: "GET", + Pattern: "/token", + Handler: handler, + }) + defer server.Close() authorizer := NewStandardTokenAuthorizer(nil, false, "repository", "library/ubuntu", "pull") req, err := http.NewRequest("GET", "http://registry", nil) @@ -38,7 +47,7 @@ func TestAuthorizeOfStandardTokenAuthorizer(t *testing.T) { } params := map[string]string{ - "realm": tokenServer.URL + "/token", + "realm": server.URL + "/token", } if err := authorizer.Authorize(req, params); err != nil { @@ -46,8 +55,8 @@ func TestAuthorizeOfStandardTokenAuthorizer(t *testing.T) { } tk := req.Header.Get("Authorization") - if tk != "Bearer "+token { - t.Errorf("unexpected token: %s != %s", tk, "Bearer "+token) + if tk != "Bearer token" { + t.Errorf("unexpected token: %s != %s", tk, "Bearer token") } } @@ -58,24 +67,3 @@ func TestSchemeOfStandardTokenAuthorizer(t *testing.T) { } } - -func newTokenServer() *httptest.Server { - mux := http.NewServeMux() - mux.HandleFunc("/token", handleToken) - - return httptest.NewServer(mux) -} - -func handleToken(w http.ResponseWriter, r *http.Request) { - result := map[string]interface{}{} - result["token"] = token - result["expires_in"] = 300 - result["issued_at"] = time.Now().Format(time.RFC3339) - - encoder := json.NewEncoder(w) - if err := encoder.Encode(result); err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } -} diff --git a/utils/registry/manifest_test.go b/utils/registry/manifest_test.go new file mode 100644 index 000000000..66c7a6abc --- /dev/null +++ b/utils/registry/manifest_test.go @@ -0,0 +1,56 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package registry + +import ( + "testing" + + "github.com/docker/distribution/manifest/schema2" +) + +func TestUnMarshal(t *testing.T) { + b := []byte(`{ + "schemaVersion":2, + "mediaType":"application/vnd.docker.distribution.manifest.v2+json", + "config":{ + "mediaType":"application/vnd.docker.container.image.v1+json", + "size":1473, + "digest":"sha256:c54a2cc56cbb2f04003c1cd4507e118af7c0d340fe7e2720f70976c4b75237dc" + }, + "layers":[ + { + "mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip", + "size":974, + "digest":"sha256:c04b14da8d1441880ed3fe6106fb2cc6fa1c9661846ac0266b8a5ec8edf37b7c" + } + ] +}`) + + manifest, _, err := UnMarshal(schema2.MediaTypeManifest, b) + if err != nil { + t.Fatalf("failed to parse manifest: %v", err) + } + + refs := manifest.References() + if len(refs) != 1 { + t.Fatalf("unexpected length of reference: %d != %d", len(refs), 1) + } + + digest := "sha256:c04b14da8d1441880ed3fe6106fb2cc6fa1c9661846ac0266b8a5ec8edf37b7c" + if refs[0].Digest.String() != digest { + t.Errorf("unexpected digest: %s != %s", refs[0].Digest.String(), digest) + } +} diff --git a/utils/registry/registry.go b/utils/registry/registry.go index 0ef6e9dc9..4934c8e8d 100644 --- a/utils/registry/registry.go +++ b/utils/registry/registry.go @@ -48,11 +48,6 @@ func NewRegistry(endpoint string, client *http.Client) (*Registry, error) { // NewRegistryWithModifiers returns an instance of Registry according to the modifiers func NewRegistryWithModifiers(endpoint string, insecure bool, modifiers ...Modifier) (*Registry, error) { - u, err := utils.ParseEndpoint(endpoint) - if err != nil { - return nil, err - } - t := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: insecure, @@ -61,12 +56,9 @@ func NewRegistryWithModifiers(endpoint string, insecure bool, modifiers ...Modif transport := NewTransport(t, modifiers...) - return &Registry{ - Endpoint: u, - client: &http.Client{ - Transport: transport, - }, - }, nil + return NewRegistry(endpoint, &http.Client{ + Transport: transport, + }) } // Catalog ... diff --git a/utils/registry/registry_test.go b/utils/registry/registry_test.go new file mode 100644 index 000000000..ce9853db2 --- /dev/null +++ b/utils/registry/registry_test.go @@ -0,0 +1,150 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package registry + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "testing" + + "github.com/vmware/harbor/utils/test" +) + +func TestNewRegistryWithModifiers(t *testing.T) { + _, err := NewRegistryWithModifiers("http://registry.org", false, nil) + if err != nil { + t.Errorf("fail to crearte client of registry: %v", err) + } +} + +func TestPing(t *testing.T) { + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "GET", + Pattern: "/v2/", + Handler: test.Handler(nil), + }) + defer server.Close() + + client, err := newRegistryClient(server.URL) + if err != nil { + t.Fatalf("failed to create client for registry: %v", err) + } + + if err = client.Ping(); err != nil { + t.Errorf("failed to ping registry: %v", err) + } +} + +func TestCatalog(t *testing.T) { + repositories := make([]string, 0, 1001) + for i := 0; i < 1001; i++ { + repositories = append(repositories, strconv.Itoa(i)) + } + + handler := func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + last := q.Get("last") + n, err := strconv.Atoi(q.Get("n")) + if err != nil || n <= 0 { + n = 1000 + } + + length := len(repositories) + + begin := length + if len(last) == 0 { + begin = 0 + } else { + for i, repository := range repositories { + if repository == last { + begin = i + 1 + break + } + } + } + + end := begin + n + if end > length { + end = length + } + + w.Header().Set(http.CanonicalHeaderKey("Content-Type"), "application/json") + if end < length { + u, err := url.Parse("/v2/_catalog") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + values := u.Query() + values.Add("last", repositories[end-1]) + values.Add("n", strconv.Itoa(n)) + + u.RawQuery = values.Encode() + + link := fmt.Sprintf("<%s>; rel=\"next\"", u.String()) + w.Header().Set(http.CanonicalHeaderKey("link"), link) + } + + repos := struct { + Repositories []string `json:"repositories"` + }{ + Repositories: []string{}, + } + + if begin < length { + repos.Repositories = repositories[begin:end] + } + + b, err := json.Marshal(repos) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(b) + + } + + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "GET", + Pattern: "/v2/_catalog", + Handler: handler, + }) + defer server.Close() + + client, err := newRegistryClient(server.URL) + if err != nil { + t.Fatalf("failed to create client for registry: %v", err) + } + + repos, err := client.Catalog() + if err != nil { + t.Fatalf("failed to catalog repositories: %v", err) + } + + if len(repos) != len(repositories) { + t.Errorf("unexpected length of repositories: %d != %d", len(repos), len(repositories)) + } +} + +func newRegistryClient(url string) (*Registry, error) { + return NewRegistry(url, &http.Client{}) +} diff --git a/utils/registry/repository.go b/utils/registry/repository.go index 5ae2eb9d7..88d0182b3 100644 --- a/utils/registry/repository.go +++ b/utils/registry/repository.go @@ -61,13 +61,6 @@ func NewRepository(name, endpoint string, client *http.Client) (*Repository, err // NewRepositoryWithModifiers returns an instance of Repository according to the modifiers func NewRepositoryWithModifiers(name, endpoint string, insecure bool, modifiers ...Modifier) (*Repository, error) { - name = strings.TrimSpace(name) - - u, err := utils.ParseEndpoint(endpoint) - if err != nil { - return nil, err - } - t := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: insecure, @@ -76,13 +69,9 @@ func NewRepositoryWithModifiers(name, endpoint string, insecure bool, modifiers transport := NewTransport(t, modifiers...) - return &Repository{ - Name: name, - Endpoint: u, - client: &http.Client{ - Transport: transport, - }, - }, nil + return NewRepository(name, endpoint, &http.Client{ + Transport: transport, + }) } func parseError(err error) error { @@ -347,7 +336,7 @@ func (r *Repository) PullBlob(digest string) (size int64, data io.ReadCloser, er data = resp.Body return } - + // can not close the connect if the status code is 200 defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) @@ -428,7 +417,6 @@ func (r *Repository) PushBlob(digest string, size int64, data io.Reader) error { if err != nil { return err } - return r.monolithicBlobUpload(location, digest, size, data) } @@ -482,5 +470,12 @@ func buildInitiateBlobUploadURL(endpoint, repoName string) string { } func buildMonolithicBlobUploadURL(location, digest string) string { - return fmt.Sprintf("%s&digest=%s", location, digest) + query := "" + if strings.ContainsRune(location, '?') { + query = "&" + } else { + query = "?" + } + query += fmt.Sprintf("digest=%s", digest) + return fmt.Sprintf("%s%s", location, query) } diff --git a/utils/registry/repository_test.go b/utils/registry/repository_test.go index f1c4c3c2d..07c837b53 100644 --- a/utils/registry/repository_test.go +++ b/utils/registry/repository_test.go @@ -16,179 +16,395 @@ package registry import ( - "encoding/json" + "bytes" "fmt" + "io/ioutil" "net/http" - "net/http/httptest" - "os" + "net/url" + "strconv" "strings" "testing" - "time" - "github.com/vmware/harbor/utils/registry/auth" + "github.com/docker/distribution/manifest/schema2" registry_error "github.com/vmware/harbor/utils/registry/error" + "github.com/vmware/harbor/utils/test" ) var ( - username = "user" - password = "P@ssw0rd" - repo = "samalba/my-app" - tags = tagResp{Tags: []string{"1.0", "2.0", "3.0"}} - validToken = "valid_token" - invalidToken = "invalid_token" - credential auth.Credential - registryServer *httptest.Server - tokenServer *httptest.Server - repositoryClient *Repository + repository = "library/hello-world" + tag = "latest" + + mediaType = schema2.MediaTypeManifest + manifest = []byte("manifest") + + blob = []byte("blob") + + uuid = "0663ff44-63bb-11e6-8b77-86f30ca893d3" + + digest = "sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b" ) -type tagResp struct { - Tags []string `json:"tags"` -} - -func TestMain(m *testing.M) { - //log.SetLevel(log.DebugLevel) - credential = auth.NewBasicAuthCredential(username, password) - - tokenServer = initTokenServer() - defer tokenServer.Close() - - registryServer = initRegistryServer() - defer registryServer.Close() - - os.Exit(m.Run()) -} - -func initRegistryServer() *httptest.Server { - mux := http.NewServeMux() - mux.HandleFunc("/v2/", servePing) - mux.HandleFunc(fmt.Sprintf("/v2/%s/tags/list", repo), serveTaglisting) - - return httptest.NewServer(mux) -} - -//response ping request: http://registry/v2 -func servePing(w http.ResponseWriter, r *http.Request) { - if !isTokenValid(r) { - challenge(w) - return +func TestNewRepositoryWithModifiers(t *testing.T) { + _, err := NewRepositoryWithModifiers("library/ubuntu", + "http://registry.org", true, nil) + if err != nil { + t.Fatalf("failed to create client for repository: %v", err) } } -func serveTaglisting(w http.ResponseWriter, r *http.Request) { - if !isTokenValid(r) { - challenge(w) - return - } - - if err := json.NewEncoder(w).Encode(tags); err != nil { - w.Write([]byte(err.Error())) - w.WriteHeader(http.StatusInternalServerError) - return - } - -} - -func isTokenValid(r *http.Request) bool { - valid := false - auth := r.Header.Get(http.CanonicalHeaderKey("Authorization")) - if len(auth) != 0 { - auth = strings.TrimSpace(auth) - index := strings.Index(auth, "Bearer") - token := auth[index+6:] - token = strings.TrimSpace(token) - if token == validToken { - valid = true +func TestBlobExist(t *testing.T) { + handler := func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + dgt := path[strings.LastIndex(path, "/")+1 : len(path)] + if dgt == digest { + w.Header().Add(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(blob))) + w.Header().Add(http.CanonicalHeaderKey("Docker-Content-Digest"), digest) + w.Header().Add(http.CanonicalHeaderKey("Content-Type"), "application/octet-stream") + return } - } - return valid -} -func challenge(w http.ResponseWriter) { - challenge := "Bearer realm=\"" + tokenServer.URL + "/service/token\",service=\"token-service\"" - w.Header().Set("Www-Authenticate", challenge) - w.WriteHeader(http.StatusUnauthorized) - return -} - -func initTokenServer() *httptest.Server { - mux := http.NewServeMux() - mux.HandleFunc("/service/token", serveToken) - - return httptest.NewServer(mux) -} - -func serveToken(w http.ResponseWriter, r *http.Request) { - u, p, ok := r.BasicAuth() - if !ok || u != username || p != password { - w.WriteHeader(http.StatusUnauthorized) - return + w.WriteHeader(http.StatusNotFound) } - result := make(map[string]interface{}) - result["token"] = validToken - result["expires_in"] = 300 - result["issued_at"] = time.Now().Format(time.RFC3339) + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "HEAD", + Pattern: fmt.Sprintf("/v2/%s/blobs/", repository), + Handler: handler, + }) + defer server.Close() - encoder := json.NewEncoder(w) - if err := encoder.Encode(result); err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return + client, err := newRepository(server.URL) + if err != nil { + err = parseError(err) + t.Fatalf("failed to create client for repository: %v", err) + } + + exist, err := client.BlobExist(digest) + if err != nil { + t.Fatalf("failed to check the existence of blob: %v", err) + } + + if !exist { + t.Errorf("blob should exist on registry, but it does not exist") + } + + exist, err = client.BlobExist("invalid_digest") + if err != nil { + t.Fatalf("failed to check the existence of blob: %v", err) + } + + if exist { + t.Errorf("blob should not exist on registry, but it exists") + } +} + +func TestPullBlob(t *testing.T) { + handler := test.Handler(&test.Response{ + Headers: map[string]string{ + "Content-Length": strconv.Itoa(len(blob)), + "Docker-Content-Digest": digest, + "Content-Type": "application/octet-stream", + }, + Body: blob, + }) + + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "GET", + Pattern: fmt.Sprintf("/v2/%s/blobs/%s", repository, digest), + Handler: handler, + }) + defer server.Close() + + client, err := newRepository(server.URL) + if err != nil { + t.Fatalf("failed to create client for repository: %v", err) + } + + size, reader, err := client.PullBlob(digest) + if err != nil { + t.Fatalf("failed to pull blob: %v", err) + } + + if size != int64(len(blob)) { + t.Errorf("unexpected size of blob: %d != %d", size, len(blob)) + } + + b, err := ioutil.ReadAll(reader) + if err != nil { + t.Fatalf("failed to read from reader: %v", err) + } + + if bytes.Compare(b, blob) != 0 { + t.Errorf("unexpected blob: %s != %s", string(b), string(blob)) + } +} + +func TestPushBlob(t *testing.T) { + location := "" + initUploadHandler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Add(http.CanonicalHeaderKey("Content-Length"), "0") + w.Header().Add(http.CanonicalHeaderKey("Location"), location) + w.Header().Add(http.CanonicalHeaderKey("Range"), "0-0") + w.Header().Add(http.CanonicalHeaderKey("Docker-Upload-UUID"), uuid) + w.WriteHeader(http.StatusAccepted) + } + + monolithicUploadHandler := test.Handler(&test.Response{ + StatusCode: http.StatusCreated, + Headers: map[string]string{ + "Content-Length": "0", + "Location": fmt.Sprintf("/v2/%s/blobs/%s", repository, digest), + "Docker-Content-Digest": digest, + }, + }) + + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "POST", + Pattern: fmt.Sprintf("/v2/%s/blobs/uploads/", repository), + Handler: initUploadHandler, + }, + &test.RequestHandlerMapping{ + Method: "PUT", + Pattern: fmt.Sprintf("/v2/%s/blobs/uploads/%s", repository, uuid), + Handler: monolithicUploadHandler, + }) + defer server.Close() + location = fmt.Sprintf("%s/v2/%s/blobs/uploads/%s", server.URL, repository, uuid) + + client, err := newRepository(server.URL) + if err != nil { + t.Fatalf("failed to create client for repository: %v", err) + } + + if err = client.PushBlob(digest, int64(len(blob)), bytes.NewReader(blob)); err != nil { + t.Fatalf("failed to push blob: %v", err) + } +} + +func TestDeleteBlob(t *testing.T) { + handler := test.Handler(&test.Response{ + StatusCode: http.StatusAccepted, + }) + + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "DELETE", + Pattern: fmt.Sprintf("/v2/%s/blobs/%s", repository, digest), + Handler: handler, + }) + defer server.Close() + + client, err := newRepository(server.URL) + if err != nil { + t.Fatalf("failed to create client for repository: %v", err) + } + + if err = client.DeleteBlob(digest); err != nil { + t.Fatalf("failed to delete blob: %v", err) + } +} + +func TestManifestExist(t *testing.T) { + handler := func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + tg := path[strings.LastIndex(path, "/")+1 : len(path)] + if tg == tag { + w.Header().Add(http.CanonicalHeaderKey("Docker-Content-Digest"), digest) + w.Header().Add(http.CanonicalHeaderKey("Content-Type"), mediaType) + return + } + + w.WriteHeader(http.StatusNotFound) + } + + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "HEAD", + Pattern: fmt.Sprintf("/v2/%s/manifests/%s", repository, tag), + Handler: handler, + }) + defer server.Close() + + client, err := newRepository(server.URL) + if err != nil { + t.Fatalf("failed to create client for repository: %v", err) + } + + d, exist, err := client.ManifestExist(tag) + if err != nil { + t.Fatalf("failed to check the existence of manifest: %v", err) + } + + if !exist || d != digest { + t.Errorf("manifest should exist on registry, but it does not exist") + } + + _, exist, err = client.ManifestExist("invalid_tag") + if err != nil { + t.Fatalf("failed to check the existence of manifest: %v", err) + } + + if exist { + t.Errorf("manifest should not exist on registry, but it exists") + } +} + +func TestPullManifest(t *testing.T) { + handler := test.Handler(&test.Response{ + Headers: map[string]string{ + "Docker-Content-Digest": digest, + "Content-Type": mediaType, + }, + Body: manifest, + }) + + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "GET", + Pattern: fmt.Sprintf("/v2/%s/manifests/%s", repository, tag), + Handler: handler, + }) + defer server.Close() + + client, err := newRepository(server.URL) + if err != nil { + t.Fatalf("failed to create client for repository: %v", err) + } + + d, md, payload, err := client.PullManifest(tag, []string{mediaType}) + if err != nil { + t.Fatalf("failed to pull manifest: %v", err) + } + + if d != digest { + t.Errorf("unexpected digest of manifest: %s != %s", d, digest) + } + + if md != mediaType { + t.Errorf("unexpected media type of manifest: %s != %s", md, mediaType) + } + + if bytes.Compare(payload, manifest) != 0 { + t.Errorf("unexpected manifest: %s != %s", string(payload), string(manifest)) + } +} + +func TestPushManifest(t *testing.T) { + handler := test.Handler(&test.Response{ + StatusCode: http.StatusCreated, + Headers: map[string]string{ + "Content-Length": "0", + "Docker-Content-Digest": digest, + "Location": "", + }, + }) + + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "PUT", + Pattern: fmt.Sprintf("/v2/%s/manifests/%s", repository, tag), + Handler: handler, + }) + defer server.Close() + + client, err := newRepository(server.URL) + if err != nil { + t.Fatalf("failed to create client for repository: %v", err) + } + + d, err := client.PushManifest(tag, mediaType, manifest) + if err != nil { + t.Fatalf("failed to pull manifest: %v", err) + } + + if d != digest { + t.Errorf("unexpected digest of manifest: %s != %s", d, digest) + } +} + +func TestDeleteTag(t *testing.T) { + manifestExistHandler := test.Handler(&test.Response{ + Headers: map[string]string{ + "Docker-Content-Digest": digest, + "Content-Type": mediaType, + }, + }) + + deleteManifestandler := test.Handler(&test.Response{ + StatusCode: http.StatusAccepted, + }) + + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "HEAD", + Pattern: fmt.Sprintf("/v2/%s/manifests/", repository), + Handler: manifestExistHandler, + }, + &test.RequestHandlerMapping{ + Method: "DELETE", + Pattern: fmt.Sprintf("/v2/%s/manifests/%s", repository, digest), + Handler: deleteManifestandler, + }) + defer server.Close() + + client, err := newRepository(server.URL) + if err != nil { + t.Fatalf("failed to create client for repository: %v", err) + } + + if err = client.DeleteTag(tag); err != nil { + t.Fatalf("failed to delete tag: %v", err) } } func TestListTag(t *testing.T) { - client, err := newRepositoryClient(registryServer.URL, true, credential, - repo, "repository", repo, "pull", "push", "*") + handler := test.Handler(&test.Response{ + Headers: map[string]string{ + "Content-Type": "application/json", + }, + Body: []byte(fmt.Sprintf("{\"name\": \"%s\",\"tags\": [\"%s\"]}", repository, tag)), + }) + + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "GET", + Pattern: fmt.Sprintf("/v2/%s/tags/list", repository), + Handler: handler, + }) + defer server.Close() + + client, err := newRepository(server.URL) if err != nil { - t.Error(err) + t.Fatalf("failed to create client for repository: %v", err) } - list, err := client.ListTag() + tags, err := client.ListTag() if err != nil { - t.Error(err) - return - } - if len(list) != len(tags.Tags) { - t.Errorf("expected length: %d, actual length: %d", len(tags.Tags), len(list)) - return + t.Fatalf("failed to list tags: %v", err) } -} - -func TestListTagWithInvalidCredential(t *testing.T) { - credential := auth.NewBasicAuthCredential(username, "wrong_password") - client, err := newRepositoryClient(registryServer.URL, true, credential, - repo, "repository", repo, "pull", "push", "*") - if err != nil { - t.Error(err) + if len(tags) != 1 { + t.Fatalf("unexpected length of tags: %d != %d", len(tags), 1) } - if _, err = client.ListTag(); err != nil { - e, ok := err.(*registry_error.Error) - if ok && e.StatusCode == http.StatusUnauthorized { - return - } - - t.Error(err) - return + if tags[0] != tag { + t.Errorf("unexpected tag: %s != %s", tags[0], tag) } } -func newRepositoryClient(endpoint string, insecure bool, credential auth.Credential, repository, scopeType, scopeName string, - scopeActions ...string) (*Repository, error) { - - authorizer := auth.NewStandardTokenAuthorizer(credential, insecure, scopeType, scopeName, scopeActions...) - - store, err := auth.NewAuthorizerStore(endpoint, true, authorizer) - if err != nil { - return nil, err +func TestParseError(t *testing.T) { + err := &url.Error{ + Err: ®istry_error.Error{}, } - - client, err := NewRepositoryWithModifiers(repository, endpoint, insecure, store) - if err != nil { - return nil, err + e := parseError(err) + if _, ok := e.(*registry_error.Error); !ok { + t.Errorf("error type does not match registry error") } - return client, nil +} + +func newRepository(endpoint string) (*Repository, error) { + return NewRepository(repository, endpoint, &http.Client{}) } diff --git a/utils/registry/transport_test.go b/utils/registry/transport_test.go new file mode 100644 index 000000000..997ca3c84 --- /dev/null +++ b/utils/registry/transport_test.go @@ -0,0 +1,60 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package registry + +import ( + "fmt" + "net/http" + "testing" + + "github.com/vmware/harbor/utils/test" +) + +type simpleModifier struct { +} + +func (s *simpleModifier) Modify(req *http.Request) error { + req.Header.Set("Authorization", "token") + return nil +} + +func TestRoundTrip(t *testing.T) { + server := test.NewServer( + &test.RequestHandlerMapping{ + Method: "GET", + Pattern: "/", + Handler: test.Handler(nil), + }) + transport := NewTransport(&http.Transport{}, &simpleModifier{}) + client := &http.Client{ + Transport: transport, + } + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/", server.URL), nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + + if _, err := client.Do(req); err != nil { + t.Fatalf("failed to send request: %s", err) + } + + header := req.Header.Get("Authorization") + if header != "token" { + t.Errorf("unexpected header: %s != %s", header, "token") + } + +} diff --git a/utils/test/test.go b/utils/test/test.go new file mode 100644 index 000000000..37a435c24 --- /dev/null +++ b/utils/test/test.go @@ -0,0 +1,88 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package test + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "strings" +) + +// RequestHandlerMapping is a mapping between request and its handler +type RequestHandlerMapping struct { + // Method is the method the request used + Method string + // Pattern is the pattern the request must match + Pattern string + // Handler is the handler which handles the request + Handler func(http.ResponseWriter, *http.Request) +} + +// ServeHTTP ... +func (rhm *RequestHandlerMapping) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if len(rhm.Method) != 0 && r.Method != strings.ToUpper(rhm.Method) { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + rhm.Handler(w, r) +} + +// Response is a response used for unit test +type Response struct { + // StatusCode is the status code of the response + StatusCode int + // Headers are the headers of the response + Headers map[string]string + // Boby is the body of the response + Body []byte +} + +// Handler returns a handler function which handle requst according to +// the response provided +func Handler(resp *Response) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if resp == nil { + return + } + + for k, v := range resp.Headers { + w.Header().Add(http.CanonicalHeaderKey(k), v) + } + + if resp.StatusCode == 0 { + resp.StatusCode = http.StatusOK + } + w.WriteHeader(resp.StatusCode) + + if len(resp.Body) != 0 { + io.Copy(w, bytes.NewReader(resp.Body)) + } + } +} + +// NewServer creates a HTTP server for unit test +func NewServer(mappings ...*RequestHandlerMapping) *httptest.Server { + mux := http.NewServeMux() + + for _, mapping := range mappings { + mux.Handle(mapping.Pattern, mapping) + } + + return httptest.NewServer(mux) +}