diff --git a/src/common/utils/clair/client_test.go b/src/common/utils/clair/client_test.go new file mode 100644 index 000000000..4f0317cce --- /dev/null +++ b/src/common/utils/clair/client_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2017 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 clair + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils/clair/test" +) + +var ( + notificationID = "ec45ec87-bfc8-4129-a1c3-d2b82622175a" + layerName = "03adedf41d4e0ea1b2458546a5b4717bf5f24b23489b25589e20c692aaf84d19" + client *Client +) + +func TestMain(m *testing.M) { + mockClairServer := test.NewMockServer() + defer mockClairServer.Close() + client = NewClient(mockClairServer.URL, nil) + rc := m.Run() + if rc != 0 { + os.Exit(rc) + } +} + +func TestListNamespaces(t *testing.T) { + assert := assert.New(t) + ns, err := client.ListNamespaces() + assert.Nil(err) + assert.Equal(25, len(ns)) +} + +func TestNotifications(t *testing.T) { + assert := assert.New(t) + n, err := client.GetNotification(notificationID) + assert.Nil(err) + assert.Equal(notificationID, n.Name) + _, err = client.GetNotification("noexist") + assert.NotNil(err) + err = client.DeleteNotification(notificationID) + assert.Nil(err) +} + +func TestLaysers(t *testing.T) { + assert := assert.New(t) + layer := models.ClairLayer{ + Name: "fakelayer", + ParentName: "parent", + Path: "http://registry:5000/layers/xxx", + } + err := client.ScanLayer(layer) + assert.Nil(err) + data, err := client.GetResult(layerName) + assert.Nil(err) + assert.Equal(layerName, data.Layer.Name) + _, err = client.GetResult("notexist") + assert.NotNil(err) +} diff --git a/src/common/utils/clair/test/notification.json b/src/common/utils/clair/test/notification.json new file mode 100644 index 000000000..34c2ae17a --- /dev/null +++ b/src/common/utils/clair/test/notification.json @@ -0,0 +1,62 @@ +{ + "Notification": { + "Name": "ec45ec87-bfc8-4129-a1c3-d2b82622175a", + "Created": "1456247389", + "Notified": "1456246708", + "Limit": 2, + "Page": "gAAAAABWzJaC2JCH6Apr_R1f2EkjGdibnrKOobTcYXBWl6t0Cw6Q04ENGIymB6XlZ3Zi0bYt2c-2cXe43fvsJ7ECZhZz4P8C8F9efr_SR0HPiejzQTuG0qAzeO8klogFfFjSz2peBvgP", + "NextPage": "gAAAAABWzJaCTyr6QXP2aYsCwEZfWIkU2GkNplSMlTOhLJfiR3LorBv8QYgEIgyOvZRmHQEzJKvkI6TP2PkRczBkcD17GE89btaaKMqEX14yHDgyfQvdasW1tj3-5bBRt0esKi9ym5En", + "New": { + "Vulnerability": { + "Name": "CVE-TEST", + "NamespaceName": "debian:8", + "Description": "New CVE", + "Severity": "Low", + "FixedIn": [ + { + "Name": "grep", + "NamespaceName": "debian:8", + "Version": "2.25" + } + ] + }, + "OrderedLayersIntroducingVulnerability": [ + { + "Index": 1, + "LayerName": "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6" + }, + { + "Index": 2, + "LayerName": "3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d3f4be1916b12d" + } + ], + "LayersIntroducingVulnerability": [ + "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6", + "3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d182371916b12d" + ] + }, + "Old": { + "Vulnerability": { + "Name": "CVE-TEST", + "NamespaceName": "debian:8", + "Description": "New CVE", + "Severity": "Low", + "FixedIn": [] + }, + "OrderedLayersIntroducingVulnerability": [ + { + "Index": 1, + "LayerName": "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6" + }, + { + "Index": 2, + "LayerName": "3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d3f4be1916b12d" + } + ], + "LayersIntroducingVulnerability": [ + "3b59c795b34670618fbcace4dac7a27c5ecec156812c9e2c90d3f4be1916b12d", + "523ef1d23f222195488575f52a39c729c76a8c5630c9a194139cb246fb212da6" + ] + } + } +} \ No newline at end of file diff --git a/src/common/utils/clair/test/ns.json b/src/common/utils/clair/test/ns.json new file mode 100644 index 000000000..2faa49eb1 --- /dev/null +++ b/src/common/utils/clair/test/ns.json @@ -0,0 +1 @@ +{"Namespaces":[{"Name":"debian:7","VersionFormat":"dpkg"},{"Name":"debian:unstable","VersionFormat":"dpkg"},{"Name":"debian:9","VersionFormat":"dpkg"},{"Name":"debian:10","VersionFormat":"dpkg"},{"Name":"debian:8","VersionFormat":"dpkg"},{"Name":"alpine:v3.6","VersionFormat":"dpkg"},{"Name":"alpine:v3.5","VersionFormat":"dpkg"},{"Name":"alpine:v3.4","VersionFormat":"dpkg"},{"Name":"alpine:v3.3","VersionFormat":"dpkg"},{"Name":"oracle:6","VersionFormat":"rpm"},{"Name":"oracle:7","VersionFormat":"rpm"},{"Name":"oracle:5","VersionFormat":"rpm"},{"Name":"ubuntu:14.04","VersionFormat":"dpkg"},{"Name":"ubuntu:15.10","VersionFormat":"dpkg"},{"Name":"ubuntu:17.04","VersionFormat":"dpkg"},{"Name":"ubuntu:16.04","VersionFormat":"dpkg"},{"Name":"ubuntu:12.04","VersionFormat":"dpkg"},{"Name":"ubuntu:13.04","VersionFormat":"dpkg"},{"Name":"ubuntu:14.10","VersionFormat":"dpkg"},{"Name":"ubuntu:12.10","VersionFormat":"dpkg"},{"Name":"ubuntu:16.10","VersionFormat":"dpkg"},{"Name":"ubuntu:15.04","VersionFormat":"dpkg"},{"Name":"centos:6","VersionFormat":"rpm"},{"Name":"centos:7","VersionFormat":"rpm"},{"Name":"centos:5","VersionFormat":"rpm"}]} diff --git a/src/common/utils/clair/test/server.go b/src/common/utils/clair/test/server.go new file mode 100644 index 000000000..ebdaf8ac5 --- /dev/null +++ b/src/common/utils/clair/test/server.go @@ -0,0 +1,117 @@ +// Copyright (c) 2017 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 ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "path" + "runtime" + "strings" + + "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils/log" +) + +func currPath() string { + _, f, _, ok := runtime.Caller(0) + if !ok { + panic("Failed to get current directory") + } + return path.Dir(f) +} + +func serveFile(rw http.ResponseWriter, p string) { + data, err := ioutil.ReadFile(p) + if err != nil { + http.Error(rw, err.Error(), 500) + } + + _, err2 := rw.Write(data) + if err2 != nil { + http.Error(rw, err2.Error(), 500) + } +} + +type notificationHandler struct { + id string +} + +func (n *notificationHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + suffix := strings.TrimPrefix(req.URL.Path, "/v1/notifications/") + if req.Method == http.MethodDelete { + rw.WriteHeader(200) + } else if req.Method == http.MethodGet { + if strings.HasPrefix(suffix, n.id) { + serveFile(rw, path.Join(currPath(), "notification.json")) + } else { + rw.WriteHeader(404) + } + } else { + rw.WriteHeader(http.StatusMethodNotAllowed) + } +} + +type layerHandler struct { + name string +} + +func (l *layerHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodPost { + data, err := ioutil.ReadAll(req.Body) + defer req.Body.Close() + if err != nil { + http.Error(rw, err.Error(), 500) + } + layer := &models.ClairLayerEnvelope{} + if err := json.Unmarshal(data, layer); err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + } + rw.WriteHeader(http.StatusCreated) + } else if req.Method == http.MethodGet { + name := strings.TrimPrefix(req.URL.Path, "/v1/layers/") + if name == l.name { + serveFile(rw, path.Join(currPath(), "total-12.json")) + } else { + http.Error(rw, fmt.Sprintf("Invalid layer name: %s", name), http.StatusNotFound) + } + } else { + http.Error(rw, "", http.StatusMethodNotAllowed) + } +} + +// NewMockServer ... +func NewMockServer() *httptest.Server { + mux := http.NewServeMux() + mux.HandleFunc("/v1/namespaces", func(rw http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodGet { + serveFile(rw, path.Join(currPath(), "ns.json")) + } else { + rw.WriteHeader(http.StatusMethodNotAllowed) + } + }) + mux.Handle("/v1/notifications/", ¬ificationHandler{id: "ec45ec87-bfc8-4129-a1c3-d2b82622175a"}) + mux.Handle("/v1/layers", &layerHandler{name: "03adedf41d4e0ea1b2458546a5b4717bf5f24b23489b25589e20c692aaf84d19"}) + mux.Handle("/v1/layers/", &layerHandler{name: "03adedf41d4e0ea1b2458546a5b4717bf5f24b23489b25589e20c692aaf84d19"}) + mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + log.Infof("method: %s, path: %s", req.Method, req.URL.Path) + rw.WriteHeader(http.StatusNotFound) + }, + ) + return httptest.NewServer(mux) +}