Skip to content

Commit 0cb7a21

Browse files
zpavlinovicFiloSottile
authored andcommitted
client: adds unit tests and addresses minor issues.
Change-Id: I9151991794618c11cca9dffb3b79ebbb42989d16 Reviewed-on: https://team-review.git.corp.google.com/c/golang/vulndb/+/1036403 Reviewed-by: Roland Shoemaker <[email protected]>
1 parent 3455efa commit 0cb7a21

File tree

3 files changed

+153
-11
lines changed

3 files changed

+153
-11
lines changed

client/cache.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
// it does not implement file locking. When ported to the stdlib it
1717
// should use cmd/go/internal/lockedfile.
1818

19-
// The cahce uses a single JSON index file for each vulnerability database
19+
// The cache uses a single JSON index file for each vulnerability database
2020
// which contains the map from packages to the time the last
2121
// vulnerability for that package was added/modified and the time that
2222
// the index was retrieved from the vulnerability database. The JSON

client/client.go

+10-10
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"net/http"
88
"net/url"
99
"os"
10-
"path"
1110
"path/filepath"
1211
"strings"
1312
"time"
@@ -57,7 +56,7 @@ func (ls *localSource) Index() (osv.DBIndex, error) {
5756
}
5857

5958
type httpSource struct {
60-
url string
59+
url string // the base URI of the source (without trailing "/"). e.g. https://vuln.golang.org
6160
c *http.Client
6261
cache Cache
6362
dbName string
@@ -82,7 +81,7 @@ func (hs *httpSource) Index() (osv.DBIndex, error) {
8281
}
8382
}
8483

85-
req, err := http.NewRequest("GET", path.Join(hs.url, "index.json"), nil)
84+
req, err := http.NewRequest("GET", fmt.Sprintf("%s/index.json", hs.url), nil)
8685
if err != nil {
8786
return nil, err
8887
}
@@ -135,10 +134,10 @@ func (hs *httpSource) Get(packages []string) ([]*osv.Entry, error) {
135134
}
136135
if cached, err := hs.cache.ReadEntries(hs.dbName, p); err != nil {
137136
return nil, err
138-
} else if cached != nil {
137+
} else if len(cached) != 0 {
139138
var stale bool
140-
for _, e := range entries {
141-
if e.LastModified.Before(lastModified) {
139+
for _, c := range cached {
140+
if c.LastModified.Before(lastModified) {
142141
stale = true
143142
break
144143
}
@@ -155,7 +154,7 @@ func (hs *httpSource) Get(packages []string) ([]*osv.Entry, error) {
155154
}
156155

157156
for _, p := range stillNeed {
158-
resp, err := hs.c.Get(path.Join(hs.url, p+".json"))
157+
resp, err := hs.c.Get(fmt.Sprintf("%s/%s.json", hs.url, p))
159158
if err != nil {
160159
return nil, err
161160
}
@@ -182,7 +181,7 @@ func (hs *httpSource) Get(packages []string) ([]*osv.Entry, error) {
182181
}
183182
}
184183
}
185-
return nil, nil
184+
return entries, nil
186185
}
187186

188187
type Client struct {
@@ -197,9 +196,10 @@ type Options struct {
197196
func NewClient(sources []string, opts Options) (*Client, error) {
198197
c := &Client{}
199198
for _, uri := range sources {
199+
uri = strings.TrimRight(uri, "/")
200200
// should parse the URI out here instead of in there
201201
switch {
202-
case strings.HasPrefix("http://", uri) || strings.HasPrefix("https://", uri):
202+
case strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://"):
203203
hs := &httpSource{url: uri}
204204
url, err := url.Parse(uri)
205205
if err != nil {
@@ -215,7 +215,7 @@ func NewClient(sources []string, opts Options) (*Client, error) {
215215
hs.c = new(http.Client)
216216
}
217217
c.sources = append(c.sources, hs)
218-
case strings.HasPrefix("file://", uri):
218+
case strings.HasPrefix(uri, "file://"):
219219
url, err := url.Parse(uri)
220220
if err != nil {
221221
return nil, err

client/client_test.go

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package client
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"net/http"
7+
"os"
8+
"path"
9+
"testing"
10+
"time"
11+
12+
"golang.org/x/vulndb/osv"
13+
)
14+
15+
var testVuln1 string = `[
16+
{"ID":"ID1","Package":{"Name":"golang.org/example/one","Ecosystem":"go"}, "Summary":"",
17+
"Severity":2,"Affects":{"Ranges":[{"Type":2,"Introduced":"","Fixed":"v2.2.0"}]},
18+
"ecosystem_specific":{"Symbols":["some_symbol_1"]
19+
}}]`
20+
21+
var testVuln2 string = `[
22+
{"ID":"ID2","Package":{"Name":"golang.org/example/two","Ecosystem":"go"}, "Summary":"",
23+
"Severity":2,"Affects":{"Ranges":[{"Type":2,"Introduced":"","Fixed":"v2.1.0"}]},
24+
"ecosystem_specific":{"Symbols":["some_symbol_2"]
25+
}}]`
26+
27+
// index containing timestamps for packages in testVuln1 and testVuln2.
28+
var index string = `{
29+
"golang.org/example/one": "2020-03-09T10:00:00.81362141-07:00",
30+
"golang.org/example/two": "2019-02-05T09:00:00.31561157-07:00"
31+
}`
32+
33+
func serveTestVuln1(w http.ResponseWriter, req *http.Request) {
34+
fmt.Fprintf(w, testVuln1)
35+
}
36+
37+
func serveTestVuln2(w http.ResponseWriter, req *http.Request) {
38+
fmt.Fprintf(w, testVuln2)
39+
}
40+
41+
func serveIndex(w http.ResponseWriter, req *http.Request) {
42+
fmt.Fprintf(w, index)
43+
}
44+
45+
// cachedTestVuln2 returns a function creating a local cache
46+
// for db with `dbName` with a version of testVuln2 where
47+
// Summary="cached" and LastModified happened after entry
48+
// in the `index` for the same pkg.
49+
func cachedTestVuln2(dbName string) func() Cache {
50+
return func() Cache {
51+
c := &fsCache{}
52+
e := &osv.Entry{
53+
ID: "ID2",
54+
Summary: "cached",
55+
LastModified: time.Now(),
56+
}
57+
c.WriteEntries(dbName, "golang.org/example/two", []*osv.Entry{e})
58+
return c
59+
}
60+
}
61+
62+
// createDirAndFile creates a directory `dir` if such directory does
63+
// not exist and creates a `file` with `content` in the directory.
64+
func createDirAndFile(dir, file, content string) error {
65+
if err := os.MkdirAll(dir, 0755); err != nil {
66+
return err
67+
}
68+
return ioutil.WriteFile(path.Join(dir, file), []byte(content), 0644)
69+
}
70+
71+
// localDB creates a local db with testVuln1, testVuln2, and index as contents.
72+
func localDB(t *testing.T) (string, error) {
73+
dbName := t.TempDir()
74+
75+
if err := createDirAndFile(path.Join(dbName, "/golang.org/example/"), "one.json", testVuln1); err != nil {
76+
return "", err
77+
}
78+
if err := createDirAndFile(path.Join(dbName, "/golang.org/example/"), "two.json", testVuln2); err != nil {
79+
return "", err
80+
}
81+
if err := createDirAndFile(path.Join(dbName, ""), "index.json", index); err != nil {
82+
return "", err
83+
}
84+
return dbName, nil
85+
}
86+
87+
func TestClient(t *testing.T) {
88+
// Create a local http database.
89+
http.HandleFunc("/golang.org/example/one.json", serveTestVuln1)
90+
http.HandleFunc("/golang.org/example/two.json", serveTestVuln2)
91+
http.HandleFunc("/index.json", serveIndex)
92+
go func() { http.ListenAndServe(":8080", nil) }()
93+
94+
// Create a local file database.
95+
localDBName, err := localDB(t)
96+
if err != nil {
97+
t.Fatal(err)
98+
}
99+
defer os.RemoveAll(localDBName)
100+
101+
for _, test := range []struct {
102+
name string
103+
source string
104+
createCache func() Cache
105+
noVulns int
106+
summaries map[string]string
107+
}{
108+
// Test the http client without any cache.
109+
{name: "http-no-cache", source: "http://localhost:8080", createCache: func() Cache { return nil }, noVulns: 2, summaries: map[string]string{"ID1": "", "ID2": ""}},
110+
// Test the http client with empty cache.
111+
{name: "http-empty-cache", source: "http://localhost:8080", createCache: func() Cache { return &fsCache{} }, noVulns: 2, summaries: map[string]string{"ID1": "", "ID2": ""}},
112+
// Test the client with non-stale cache containing a version of testVuln2 where Summary="cached".
113+
{name: "http-cache", source: "http://localhost:8080", createCache: cachedTestVuln2("localhost"), noVulns: 2, summaries: map[string]string{"ID1": "", "ID2": "cached"}},
114+
// Repeat the same for local file client.
115+
{name: "file-no-cache", source: "file://" + localDBName, createCache: func() Cache { return nil }, noVulns: 2, summaries: map[string]string{"ID1": "", "ID2": ""}},
116+
{name: "file-empty-cache", source: "file://" + localDBName, createCache: func() Cache { return &fsCache{} }, noVulns: 2, summaries: map[string]string{"ID1": "", "ID2": ""}},
117+
// Cache does not play a role in local file databases.
118+
{name: "file-cache", source: "file://" + localDBName, createCache: cachedTestVuln2(localDBName), noVulns: 2, summaries: map[string]string{"ID1": "", "ID2": ""}},
119+
} {
120+
// Create fresh cache location each time.
121+
cacheRoot = t.TempDir()
122+
123+
client, err := NewClient([]string{test.source}, Options{HTTPCache: test.createCache()})
124+
if err != nil {
125+
t.Fatal(err)
126+
}
127+
128+
vulns, err := client.Get([]string{"golang.org/example/one", "golang.org/example/two"})
129+
if err != nil {
130+
t.Fatal(err)
131+
}
132+
if len(vulns) != test.noVulns {
133+
t.Errorf("want %v vulns for %s; got %v", test.noVulns, test.name, len(vulns))
134+
}
135+
136+
for _, v := range vulns {
137+
if s, ok := test.summaries[v.ID]; !ok || v.Summary != s {
138+
t.Errorf("want '%s' summary for vuln with id %v in %s; got '%s'", s, v.ID, test.name, v.Summary)
139+
}
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)