internal/vuln: refactor vulndb client to prepare for v1 client

This change refactors the internal/vuln package to prepare for
the Client struct to support both the legacy
vulndb client and the new v1 client (introduced in a later CL).

The Client struct now contains only an embedded client interface which
is implemented by the legacyClient struct. The client interface is
essentially the same as the x/vuln Client interface, except that the
method GetByModule is replaced by ByPackage, which filters vulns
based on module, package and version.

The legacy client is refactored to implement ByPackage. In pkgsite,
ByPackage is used only by VulnsForPackage, whose behavior and interface
are not modified by this change. The rest of the legacy client is
essentially a pass-through to the x/vuln Client.

Once we have confidence that the v1 client works, the dependency on
the x/vuln Client will be removed completely.

Change-Id: Ia4f0d587ac4cfa28980f45ddbe8d609b5989666a
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/474695
Reviewed-by: Julie Qiu <julieqiu@google.com>
Reviewed-by: Tatiana Bradley <tatianabradley@google.com>
Run-TryBot: Tatiana Bradley <tatianabradley@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
diff --git a/internal/vuln/client.go b/internal/vuln/client.go
index 9a6393b..02cccb1 100644
--- a/internal/vuln/client.go
+++ b/internal/vuln/client.go
@@ -13,37 +13,64 @@
 
 // Client reads Go vulnerability databases.
 type Client struct {
-	c vulnc.Client
+	legacy *legacyClient
 }
 
 // NewClient returns a client that can read from the vulnerability
-// database in src (a URL or local directory).
+// database in src (a URL representing either a http or file source).
 func NewClient(src string) (*Client, error) {
-	c, err := vulnc.NewClient([]string{src}, vulnc.Options{
+	legacyCli, err := vulnc.NewClient([]string{src}, vulnc.Options{
 		HTTPCache: newCache(),
 	})
 	if err != nil {
 		return nil, err
 	}
-	return &Client{c: c}, nil
+
+	return &Client{legacy: &legacyClient{legacyCli}}, nil
 }
 
-// ByModule returns the OSV entries that affect the given module path.
-func (c *Client) ByModule(ctx context.Context, modulePath string) ([]*osv.Entry, error) {
-	return c.c.GetByModule(ctx, modulePath)
+type PackageRequest struct {
+	// Module is the module path to filter on.
+	// ByPackage will only return entries that affect this module.
+	// This must be set (if empty, ByPackage will always return nil).
+	Module string
+	// The package path to filter on.
+	// ByPackage will only return entries that affect this package.
+	// If empty, ByPackage will not filter based on the package.
+	Package string
+	// The version to filter on.
+	// ByPackage will only return entries affected at this module
+	// version.
+	// If empty, ByPackage will not filter based on version.
+	Version string
 }
 
-// ByID returns the OSV entry with the given ID.
+func (c *Client) ByPackage(ctx context.Context, req *PackageRequest) (_ []*osv.Entry, err error) {
+	return c.cli(ctx).ByPackage(ctx, req)
+}
+
 func (c *Client) ByID(ctx context.Context, id string) (*osv.Entry, error) {
-	return c.c.GetByID(ctx, id)
+	return c.cli(ctx).ByID(ctx, id)
 }
 
-// ByAlias returns the OSV entries that have the given alias.
 func (c *Client) ByAlias(ctx context.Context, alias string) ([]*osv.Entry, error) {
-	return c.c.GetByAlias(ctx, alias)
+	return c.cli(ctx).ByAlias(ctx, alias)
 }
 
-// IDs returns the IDs of all the entries in the database.
 func (c *Client) IDs(ctx context.Context) ([]string, error) {
-	return c.c.ListIDs(ctx)
+	return c.cli(ctx).IDs(ctx)
+}
+
+func (c *Client) cli(ctx context.Context) client {
+	return c.legacy
+}
+
+// client is an interface used temporarily to allow us to support
+// both the legacy and v1 databases. It will be removed once we have
+// confidence that the v1 client is working.
+type client interface {
+	ByPackage(ctx context.Context, req *PackageRequest) (_ []*osv.Entry, err error)
+	ByID(ctx context.Context, id string) (*osv.Entry, error)
+	ByAlias(ctx context.Context, alias string) ([]*osv.Entry, error)
+	IDs(ctx context.Context) ([]string, error)
 }
diff --git a/internal/vuln/client_legacy.go b/internal/vuln/client_legacy.go
new file mode 100644
index 0000000..98009e9
--- /dev/null
+++ b/internal/vuln/client_legacy.go
@@ -0,0 +1,76 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vuln
+
+import (
+	"context"
+
+	vulnc "golang.org/x/vuln/client"
+	"golang.org/x/vuln/osv"
+)
+
+type legacyClient struct {
+	vulnc.Client
+}
+
+func (c *legacyClient) ByPackage(ctx context.Context, req *PackageRequest) (_ []*osv.Entry, err error) {
+	// Get all the vulns for this module.
+	moduleEntries, err := c.GetByModule(ctx, req.Module)
+	if err != nil {
+		return nil, err
+	}
+
+	// Filter out entries that do not apply to this package/version.
+	var packageEntries []*osv.Entry
+	for _, e := range moduleEntries {
+		if isAffected(e, req) {
+			packageEntries = append(packageEntries, e)
+		}
+	}
+
+	return packageEntries, nil
+}
+
+// ByID returns the OSV entry with the given ID.
+func (c *legacyClient) ByID(ctx context.Context, id string) (*osv.Entry, error) {
+	return c.GetByID(ctx, id)
+}
+
+// ByAlias returns the OSV entries that have the given alias.
+func (c *legacyClient) ByAlias(ctx context.Context, alias string) ([]*osv.Entry, error) {
+	return c.GetByAlias(ctx, alias)
+}
+
+// IDs returns the IDs of all the entries in the database.
+func (c *legacyClient) IDs(ctx context.Context) ([]string, error) {
+	return c.ListIDs(ctx)
+}
+
+func isAffected(e *osv.Entry, req *PackageRequest) bool {
+	for _, a := range e.Affected {
+		// a.Package.Name is Go "module" name. Go package path is a.EcosystemSpecific.Imports.Path.
+		if a.Package.Name != req.Module || !a.Ranges.AffectsSemver(req.Version) {
+			continue
+		}
+		if packageMatches := func() bool {
+			if req.Package == "" {
+				return true //  match module only
+			}
+			if len(a.EcosystemSpecific.Imports) == 0 {
+				return true // no package info available, so match on module
+			}
+			for _, p := range a.EcosystemSpecific.Imports {
+				if req.Package == p.Path {
+					return true // package matches
+				}
+			}
+			return false
+		}(); !packageMatches {
+			continue
+		}
+		return true
+	}
+	return false
+}
diff --git a/internal/vuln/client_test.go b/internal/vuln/client_test.go
new file mode 100644
index 0000000..a9de101
--- /dev/null
+++ b/internal/vuln/client_test.go
@@ -0,0 +1,327 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vuln
+
+import (
+	"context"
+	"reflect"
+	"strings"
+	"testing"
+	"time"
+
+	"golang.org/x/vuln/osv"
+)
+
+var (
+	jan1999  = time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC)
+	jan2000  = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
+	jan2002  = time.Date(2002, 1, 1, 0, 0, 0, 0, time.UTC)
+	jan2003  = time.Date(2003, 1, 1, 0, 0, 0, 0, time.UTC)
+	testOSV1 = osv.Entry{
+		ID:        "GO-1999-0001",
+		Published: jan1999,
+		Modified:  jan2000,
+		Aliases:   []string{"CVE-1999-1111"},
+		Details:   "Some details",
+		Affected: []osv.Affected{
+			{
+				Package: osv.Package{
+					Name:      "stdlib",
+					Ecosystem: "Go",
+				},
+				Ranges: osv.Affects{
+					osv.AffectsRange{
+						Type: "SEMVER",
+						Events: []osv.RangeEvent{
+							{Introduced: "0"}, {Fixed: "1.1.0"},
+							{Introduced: "1.2.0"},
+							{Fixed: "1.2.2"},
+						}}},
+				DatabaseSpecific: osv.DatabaseSpecific{
+					URL: "https://pkg.go.dev/vuln/GO-1999-0001"},
+				EcosystemSpecific: osv.EcosystemSpecific{
+					Imports: []osv.EcosystemSpecificImport{{Path: "package", Symbols: []string{"Symbol"}}}}},
+		},
+		References: []osv.Reference{
+			{Type: "FIX", URL: "https://example.com/cl/123"},
+		}}
+	testOSV2 = osv.Entry{
+		ID:        "GO-2000-0002",
+		Published: jan2000,
+		Modified:  jan2002,
+		Aliases:   []string{"CVE-1999-2222"},
+		Details:   "Some details",
+		Affected: []osv.Affected{
+			{
+				Package: osv.Package{
+					Name:      "example.com/module",
+					Ecosystem: "Go",
+				},
+				Ranges: osv.Affects{
+					osv.AffectsRange{
+						Type: "SEMVER", Events: []osv.RangeEvent{{Introduced: "0"},
+							{Fixed: "1.2.0"},
+						}}},
+				DatabaseSpecific: osv.DatabaseSpecific{URL: "https://pkg.go.dev/vuln/GO-2000-0002"}, EcosystemSpecific: osv.EcosystemSpecific{
+					Imports: []osv.EcosystemSpecificImport{{Path: "example.com/module/package",
+						Symbols: []string{"Symbol"},
+					}}}}},
+		References: []osv.Reference{
+			{Type: "FIX", URL: "https://example.com/cl/543"},
+		}}
+	testOSV3 = osv.Entry{
+		ID:        "GO-2000-0003",
+		Published: jan2000,
+		Modified:  jan2003,
+		Aliases:   []string{"CVE-1999-3333", "GHSA-xxxx-yyyy-zzzz"},
+		Details:   "Some details",
+		Affected: []osv.Affected{
+			{
+				Package: osv.Package{
+					Name:      "example.com/module",
+					Ecosystem: "Go",
+				},
+				Ranges: osv.Affects{
+					osv.AffectsRange{
+						Type: "SEMVER",
+						Events: []osv.RangeEvent{
+							{Introduced: "0"}, {Fixed: "1.1.0"},
+						}}},
+				DatabaseSpecific: osv.DatabaseSpecific{
+					URL: "https://pkg.go.dev/vuln/GO-2000-0003",
+				},
+				EcosystemSpecific: osv.EcosystemSpecific{Imports: []osv.EcosystemSpecificImport{
+					{
+						Path:    "example.com/module/package",
+						Symbols: []string{"Symbol"},
+					},
+					{
+						Path: "example.com/module/package2",
+					},
+				}}}},
+		References: []osv.Reference{
+			{Type: "FIX", URL: "https://example.com/cl/000"},
+		}}
+)
+
+func TestByPackage(t *testing.T) {
+	runClientTest(t, func(t *testing.T, c client) {
+		tests := []struct {
+			name string
+			req  *PackageRequest
+			want []*osv.Entry
+		}{
+			{
+				name: "match on package",
+				req: &PackageRequest{
+					Module:  "example.com/module",
+					Package: "example.com/module/package2",
+				},
+				want: []*osv.Entry{&testOSV3},
+			},
+			{
+				// package affects OSV2 and OSV3, but version
+				// only applies to OSV2
+				name: "match on package version",
+				req: &PackageRequest{
+					Module:  "example.com/module",
+					Package: "example.com/module/package",
+					Version: "1.1.0",
+				},
+				want: []*osv.Entry{&testOSV2},
+			},
+			{
+				// when the package is not specified, only the
+				// module is used.
+				name: "match on module",
+				req: &PackageRequest{
+					Module:  "example.com/module",
+					Package: "",
+					Version: "1.0.0",
+				},
+				want: []*osv.Entry{&testOSV2, &testOSV3},
+			},
+			{
+				name: "stdlib",
+				req: &PackageRequest{
+					Module:  "stdlib",
+					Package: "package",
+					Version: "1.0.0",
+				},
+				want: []*osv.Entry{&testOSV1},
+			},
+			{
+				// when no version is specified, all entries for the module
+				// should show up
+				name: "no version",
+				req: &PackageRequest{
+					Module: "stdlib",
+				},
+				want: []*osv.Entry{&testOSV1},
+			},
+			{
+				name: "unaffected version",
+				req: &PackageRequest{
+					Module:  "stdlib",
+					Version: "3.0.0",
+				},
+				want: nil,
+			},
+			{
+				name: "v prefix ok - in range",
+				req: &PackageRequest{
+					Module:  "stdlib",
+					Version: "v1.0.0",
+				},
+				want: []*osv.Entry{&testOSV1},
+			},
+			{
+				name: "v prefix ok - out of range",
+				req: &PackageRequest{
+					Module:  "stdlib",
+					Version: "v3.0.0",
+				},
+				want: nil,
+			},
+			{
+				name: "go prefix ok - in range",
+				req: &PackageRequest{
+					Module:  "stdlib",
+					Version: "go1.0.0",
+				},
+				want: []*osv.Entry{&testOSV1},
+			},
+			{
+				name: "go prefix ok - out of range",
+				req: &PackageRequest{
+					Module:  "stdlib",
+					Version: "go3.0.0",
+				},
+				want: nil,
+			},
+		}
+
+		for _, test := range tests {
+			t.Run(test.name, func(t *testing.T) {
+				ctx := context.Background()
+				got, err := c.ByPackage(ctx, test.req)
+				if err != nil {
+					t.Fatal(err)
+				}
+				if !reflect.DeepEqual(got, test.want) {
+					t.Errorf("ByPackage(%s) = %s, want %s", test.req, ids(got), ids(test.want))
+				}
+			})
+		}
+	})
+}
+
+func ids(entries []*osv.Entry) string {
+	var ids []string
+	for _, entry := range entries {
+		ids = append(ids, entry.ID)
+	}
+	return "[" + strings.Join(ids, ",") + "]"
+}
+
+func TestByAlias(t *testing.T) {
+	runClientTest(t, func(t *testing.T, c client) {
+		tests := []struct {
+			name  string
+			alias string
+			want  []*osv.Entry
+		}{
+			{
+				name:  "CVE",
+				alias: "CVE-1999-1111",
+				want:  []*osv.Entry{&testOSV1},
+			},
+			{
+				name:  "GHSA",
+				alias: "GHSA-xxxx-yyyy-zzzz",
+				want:  []*osv.Entry{&testOSV3},
+			},
+			{
+				name:  "Not found",
+				alias: "CVE-0000-0000",
+				want:  nil,
+			},
+		}
+
+		for _, test := range tests {
+			t.Run(test.name, func(t *testing.T) {
+				ctx := context.Background()
+				got, err := c.ByAlias(ctx, test.alias)
+				if err != nil {
+					t.Fatal(err)
+				}
+				if !reflect.DeepEqual(got, test.want) {
+					t.Errorf("ByAlias(%s) = %v, want %v", test.alias, got, test.want)
+				}
+			})
+		}
+	})
+}
+
+func TestByID(t *testing.T) {
+	runClientTest(t, func(t *testing.T, c client) {
+		tests := []struct {
+			id   string
+			want *osv.Entry
+		}{
+			{
+				id:   testOSV1.ID,
+				want: &testOSV1,
+			},
+			{
+				id:   testOSV2.ID,
+				want: &testOSV2,
+			},
+			{
+				id:   "invalid",
+				want: nil,
+			},
+		}
+
+		for _, test := range tests {
+			t.Run(test.id, func(t *testing.T) {
+				ctx := context.Background()
+				got, err := c.ByID(ctx, test.id)
+				if err != nil {
+					t.Fatal(err)
+				}
+				if !reflect.DeepEqual(got, test.want) {
+					t.Errorf("ByID(%s) = %v, want %v", test.id, got, test.want)
+				}
+			})
+		}
+	})
+}
+
+func TestIDs(t *testing.T) {
+	runClientTest(t, func(t *testing.T, c client) {
+		ctx := context.Background()
+
+		got, err := c.IDs(ctx)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		want := []string{testOSV1.ID, testOSV2.ID, testOSV3.ID}
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("IDs = %v, want %v", got, want)
+		}
+	})
+}
+
+// Run the test legacy client.
+// TODO(tatianabradley): Run test for v1 client once implemented.
+func runClientTest(t *testing.T, test func(*testing.T, client)) {
+	legacy := newTestLegacyClient([]*osv.Entry{&testOSV1, &testOSV2, &testOSV3})
+
+	t.Run("legacy", func(t *testing.T) {
+		test(t, legacy)
+	})
+}
diff --git a/internal/vuln/test_client.go b/internal/vuln/test_client.go
index 5915da4..12d88e0 100644
--- a/internal/vuln/test_client.go
+++ b/internal/vuln/test_client.go
@@ -13,7 +13,11 @@
 
 // NewTestClient creates an in-memory client for use in tests.
 func NewTestClient(entries []*osv.Entry) *Client {
-	c := &vulndbTestClient{
+	return &Client{legacy: newTestLegacyClient(entries)}
+}
+
+func newTestLegacyClient(entries []*osv.Entry) *legacyClient {
+	c := &testVulnClient{
 		entries:          entries,
 		aliasToIDs:       map[string][]string{},
 		modulesToEntries: map[string][]*osv.Entry{},
@@ -26,21 +30,22 @@
 			c.modulesToEntries[affected.Package.Name] = append(c.modulesToEntries[affected.Package.Name], e)
 		}
 	}
-	return &Client{c: c}
+	return &legacyClient{c}
 }
 
-type vulndbTestClient struct {
+// Implements x/vuln.Client.
+type testVulnClient struct {
 	vulnc.Client
 	entries          []*osv.Entry
 	aliasToIDs       map[string][]string
 	modulesToEntries map[string][]*osv.Entry
 }
 
-func (c *vulndbTestClient) GetByModule(_ context.Context, module string) ([]*osv.Entry, error) {
+func (c *testVulnClient) GetByModule(_ context.Context, module string) ([]*osv.Entry, error) {
 	return c.modulesToEntries[module], nil
 }
 
-func (c *vulndbTestClient) GetByID(_ context.Context, id string) (*osv.Entry, error) {
+func (c *testVulnClient) GetByID(_ context.Context, id string) (*osv.Entry, error) {
 	for _, e := range c.entries {
 		if e.ID == id {
 			return e, nil
@@ -49,7 +54,7 @@
 	return nil, nil
 }
 
-func (c *vulndbTestClient) ListIDs(context.Context) ([]string, error) {
+func (c *testVulnClient) ListIDs(context.Context) ([]string, error) {
 	var ids []string
 	for _, e := range c.entries {
 		ids = append(ids, e.ID)
@@ -57,7 +62,7 @@
 	return ids, nil
 }
 
-func (c *vulndbTestClient) GetByAlias(ctx context.Context, alias string) ([]*osv.Entry, error) {
+func (c *testVulnClient) GetByAlias(ctx context.Context, alias string) ([]*osv.Entry, error) {
 	ids := c.aliasToIDs[alias]
 	if len(ids) == 0 {
 		return nil, nil
diff --git a/internal/vuln/vulns.go b/internal/vuln/vulns.go
index 8d32c43..e2992ed 100644
--- a/internal/vuln/vulns.go
+++ b/internal/vuln/vulns.go
@@ -11,7 +11,6 @@
 	"go/token"
 	"strings"
 
-	"golang.org/x/mod/semver"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/stdlib"
 	"golang.org/x/pkgsite/internal/version"
@@ -64,20 +63,30 @@
 	} else if modulePath == stdlib.ModulePath {
 		modulePath = vulnStdlibModulePath
 	}
-	// Get all the vulns for this module.
-	entries, err := vc.ByModule(ctx, modulePath)
+
+	// Get all the vulns for this package/version.
+	entries, err := vc.ByPackage(ctx, &PackageRequest{Module: modulePath, Package: packagePath, Version: vers})
 	if err != nil {
 		return nil, err
 	}
-	// Each entry describes a single vuln. Select the ones that apply to this
-	// package at this version.
-	var vulns []Vuln
-	for _, e := range entries {
-		if vuln, ok := entryVuln(e, modulePath, packagePath, vers); ok {
-			vulns = append(vulns, vuln)
+
+	return toVulns(entries), nil
+}
+
+func toVulns(entries []*osv.Entry) []Vuln {
+	if len(entries) == 0 {
+		return nil
+	}
+
+	vulns := make([]Vuln, len(entries))
+	for i, e := range entries {
+		vulns[i] = Vuln{
+			ID:      e.ID,
+			Details: e.Details,
 		}
 	}
-	return vulns, nil
+
+	return vulns
 }
 
 // AffectedPackage holds information about a package affected by a certain vulnerability.
@@ -112,48 +121,6 @@
 	return affected
 }
 
-func entryVuln(e *osv.Entry, modulePath, packagePath, ver string) (Vuln, bool) {
-	for _, a := range e.Affected {
-		// a.Package.Name is Go "module" name. Go package path is a.EcosystemSpecific.Imports.Path.
-		if a.Package.Name != modulePath || !a.Ranges.AffectsSemver(ver) {
-			continue
-		}
-		if packageMatches := func() bool {
-			if packagePath == "" {
-				return true //  match module only
-			}
-			if len(a.EcosystemSpecific.Imports) == 0 {
-				return true // no package info available, so match on module
-			}
-			for _, p := range a.EcosystemSpecific.Imports {
-				if packagePath == p.Path {
-					return true // package matches
-				}
-			}
-			return false
-		}(); !packageMatches {
-			continue
-		}
-		// Choose the latest fixed version, if any.
-		var fixed string
-		for _, r := range a.Ranges {
-			if r.Type == osv.TypeGit {
-				continue
-			}
-			for _, re := range r.Events {
-				if re.Fixed != "" && (fixed == "" || semver.Compare(re.Fixed, fixed) > 0) {
-					fixed = re.Fixed
-				}
-			}
-		}
-		return Vuln{
-			ID:      e.ID,
-			Details: e.Details,
-		}, true
-	}
-	return Vuln{}, false
-}
-
 // A pair is like an osv.Range, but each pair is a self-contained 2-tuple
 // (introduced version, fixed version).
 type pair struct {
diff --git a/internal/vuln/vulns_test.go b/internal/vuln/vulns_test.go
index d3ce181..10e79c5 100644
--- a/internal/vuln/vulns_test.go
+++ b/internal/vuln/vulns_test.go
@@ -27,6 +27,8 @@
 			EcosystemSpecific: osv.EcosystemSpecific{
 				Imports: []osv.EcosystemSpecificImport{{
 					Path: "bad.com",
+				}, {
+					Path: "bad.com/bad",
 				}},
 			},
 		}, {
@@ -43,9 +45,25 @@
 			},
 		}},
 	}
-	stdlib := osv.Entry{
+	e2 := osv.Entry{
 		ID: "GO-2",
 		Affected: []osv.Affected{{
+			Package: osv.Package{Name: "bad.com"},
+			Ranges: []osv.AffectsRange{{
+				Type:   osv.TypeSemver,
+				Events: []osv.RangeEvent{{Introduced: "0"}, {Fixed: "1.2.0"}},
+			}},
+			EcosystemSpecific: osv.EcosystemSpecific{
+				Imports: []osv.EcosystemSpecificImport{{
+					Path: "bad.com/pkg",
+				},
+				},
+			},
+		}},
+	}
+	stdlib := osv.Entry{
+		ID: "GO-3",
+		Affected: []osv.Affected{{
 			Package: osv.Package{Name: "stdlib"},
 			Ranges: []osv.AffectsRange{{
 				Type:   osv.TypeSemver,
@@ -59,7 +77,7 @@
 		}},
 	}
 
-	vc := NewTestClient([]*osv.Entry{&e, &stdlib})
+	vc := NewTestClient([]*osv.Entry{&e, &e2, &stdlib})
 
 	testCases := []struct {
 		mod, pkg, version string
@@ -73,6 +91,9 @@
 			"bad.com", "bad.com", "v1.0.0", []Vuln{{ID: "GO-1"}},
 		},
 		{
+			"bad.com", "bad.com/bad", "v1.0.0", []Vuln{{ID: "GO-1"}},
+		},
+		{
 			"bad.com", "bad.com/ok", "v1.0.0", nil, // bad.com/ok isn't affected.
 		},
 		{
@@ -86,7 +107,7 @@
 			"good.com", "", "v1.0.0", nil,
 		},
 		{
-			"bad.com", "", "v1.0.0", []Vuln{{ID: "GO-1"}},
+			"bad.com", "", "v1.0.0", []Vuln{{ID: "GO-1"}, {ID: "GO-2"}},
 		},
 		{
 			"bad.com", "", "v1.3.0", nil,
@@ -96,7 +117,7 @@
 		},
 		// Vulns for stdlib
 		{
-			"std", "net/http", "go1.19.3", []Vuln{{ID: "GO-2"}},
+			"std", "net/http", "go1.19.3", []Vuln{{ID: "GO-3"}},
 		},
 		{
 			"std", "net/http", "v0.0.0-20230104211531-bae7d772e800", nil,
@@ -108,7 +129,7 @@
 	for _, tc := range testCases {
 		got := VulnsForPackage(ctx, tc.mod, tc.version, tc.pkg, vc)
 		if diff := cmp.Diff(tc.want, got); diff != "" {
-			t.Errorf("VulnsForPackage(%q, %q, %q) = %+v, mismatch (-want, +got):\n%s", tc.mod, tc.version, tc.pkg, tc.want, diff)
+			t.Errorf("VulnsForPackage(mod=%q, v=%q, pkg=%q) = %+v, want %+v, diff (-want, +got):\n%s", tc.mod, tc.version, tc.pkg, got, tc.want, diff)
 		}
 	}
 }