blob: 285005decb12e60a6c1620fbd4d7067961485361 [file]
// Copyright 2026 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 api
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/osv"
"golang.org/x/pkgsite/internal/testing/fakedatasource"
"golang.org/x/pkgsite/internal/testing/sample"
"golang.org/x/pkgsite/internal/vuln"
)
func TestServePackage(t *testing.T) {
ctx := context.Background()
ds := fakedatasource.New()
const (
pkgPath = "example.com/a/b"
modulePath1 = "example.com/a"
modulePath2 = "example.com/a/b"
version = "v1.2.3"
)
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{
ModulePath: "example.com",
Version: version,
LatestVersion: "v1.2.4",
},
Licenses: sample.Licenses(),
Units: []*internal.Unit{{
UnitMeta: internal.UnitMeta{
Path: "example.com/pkg",
ModuleInfo: internal.ModuleInfo{
ModulePath: "example.com",
Version: version,
LatestVersion: "v1.2.4",
},
Name: "pkg",
},
Documentation: []*internal.Documentation{sample.Documentation("linux", "amd64", sample.DocContents)},
Licenses: sample.LicenseMetadata(),
Imports: []string{pkgPath},
}},
})
for _, mp := range []string{modulePath1, modulePath2} {
u := &internal.Unit{
UnitMeta: internal.UnitMeta{
Path: pkgPath,
ModuleInfo: internal.ModuleInfo{
ModulePath: mp,
Version: version,
LatestVersion: version,
},
Name: "b",
},
Documentation: []*internal.Documentation{
{
GOOS: "linux",
GOARCH: "amd64",
Synopsis: "Synopsis for " + mp,
},
},
}
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{
ModulePath: mp,
Version: version,
LatestVersion: version,
},
Units: []*internal.Unit{u},
})
}
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{
ModulePath: "example.com",
Version: "v1.2.4",
LatestVersion: "v1.2.4",
},
Units: []*internal.Unit{{
UnitMeta: internal.UnitMeta{
Path: "example.com/pkg",
ModuleInfo: internal.ModuleInfo{
ModulePath: "example.com",
Version: "v1.2.4",
LatestVersion: "v1.2.4",
},
Name: "pkg",
},
Documentation: []*internal.Documentation{{
GOOS: "linux",
GOARCH: "amd64",
Synopsis: "Basic synopsis",
}},
}},
})
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{
ModulePath: "example.com/ex",
Version: "v1.0.0",
LatestVersion: "v1.0.0",
},
Units: []*internal.Unit{{
UnitMeta: internal.UnitMeta{
Path: "example.com/ex/pkg",
ModuleInfo: internal.ModuleInfo{
ModulePath: "example.com/ex",
Version: "v1.0.0",
LatestVersion: "v1.0.0",
},
Name: "pkg",
},
Documentation: []*internal.Documentation{sample.DocumentationWithExamples("linux", "amd64", "", `
import "fmt"
func Example() {
fmt.Println("hello")
// Output: hello
}
`)},
}},
})
for _, test := range []struct {
name string
url string
wantStatus int
want any // Can be *Package or *Error
}{
{
name: "basic metadata",
url: "/v1/package/example.com/pkg?version=v1.2.3",
wantStatus: http.StatusOK,
want: &Package{
Path: "example.com/pkg",
ModulePath: "example.com",
ModuleVersion: version,
Synopsis: "This is a package synopsis for GOOS=linux, GOARCH=amd64",
IsLatest: false,
GOOS: "linux",
GOARCH: "amd64",
},
},
{
name: "ambiguous path",
url: "/v1/package/example.com/a/b?version=v1.2.3",
wantStatus: http.StatusBadRequest,
want: &Error{
Code: http.StatusBadRequest,
Message: "ambiguous package path",
Candidates: []Candidate{
{ModulePath: "example.com/a/b", PackagePath: "example.com/a/b"},
{ModulePath: "example.com/a", PackagePath: "example.com/a/b"},
},
},
},
{
name: "disambiguated path",
url: "/v1/package/example.com/a/b?version=v1.2.3&module=example.com/a",
wantStatus: http.StatusOK,
want: &Package{
Path: pkgPath,
ModulePath: modulePath1,
ModuleVersion: version,
Synopsis: "Synopsis for " + modulePath1,
IsLatest: true,
GOOS: "linux",
GOARCH: "amd64",
},
},
{
name: "default build context",
url: "/v1/package/example.com/pkg?version=v1.2.3",
wantStatus: http.StatusOK,
want: &Package{
Path: "example.com/pkg",
ModulePath: "example.com",
ModuleVersion: version,
Synopsis: "This is a package synopsis for GOOS=linux, GOARCH=amd64",
IsLatest: false,
GOOS: "linux",
GOARCH: "amd64",
},
},
{
name: "latest version",
url: "/v1/package/example.com/pkg?version=v1.2.4",
wantStatus: http.StatusOK,
want: &Package{
Path: "example.com/pkg",
ModulePath: "example.com",
ModuleVersion: "v1.2.4",
Synopsis: "Basic synopsis",
IsLatest: true,
GOOS: "linux",
GOARCH: "amd64",
},
},
{
name: "doc",
url: "/v1/package/example.com/pkg?version=v1.2.3&doc=text",
wantStatus: http.StatusOK,
want: &Package{
Path: "example.com/pkg",
ModulePath: "example.com",
ModuleVersion: version,
Synopsis: "This is a package synopsis for GOOS=linux, GOARCH=amd64",
GOOS: "linux",
GOARCH: "amd64",
Docs: "package p\n\nPackage p is a package.\n\n# Links\n\n- pkg.go.dev, https://pkg.go.dev\n\nVARIABLES\n\nvar V int\n\n",
},
},
{
name: "doc with examples",
url: "/v1/package/example.com/ex/pkg?version=v1.0.0&doc=text&examples=true",
wantStatus: http.StatusOK,
want: &Package{
Path: "example.com/ex/pkg",
ModulePath: "example.com/ex",
ModuleVersion: "v1.0.0",
Synopsis: "This is a package synopsis for GOOS=linux, GOARCH=amd64",
IsLatest: true,
GOOS: "linux",
GOARCH: "amd64",
Docs: "package pkg\n\nPackage pkg is a package.\n\nExample:\n\t{\n\t\tfmt.Println(\"hello\")\n\t}\n\n\tOutput:\n\thello\n\n",
},
},
{
name: "examples without doc (returns 400)",
url: "/v1/package/example.com/ex/pkg?version=v1.0.0&examples=true",
wantStatus: http.StatusBadRequest,
want: &Error{
Code: http.StatusBadRequest,
Message: "examples require doc format to be specified",
},
},
{
name: "package not found",
url: "/v1/package/nonexistent.com/pkg",
wantStatus: http.StatusNotFound,
want: &Error{Code: 404, Message: "not found"},
},
{
name: "doc without examples",
url: "/v1/package/example.com/ex/pkg?version=v1.0.0&doc=text&examples=false",
wantStatus: http.StatusOK,
want: &Package{
Path: "example.com/ex/pkg",
ModulePath: "example.com/ex",
ModuleVersion: "v1.0.0",
Synopsis: "This is a package synopsis for GOOS=linux, GOARCH=amd64",
IsLatest: true,
GOOS: "linux",
GOARCH: "amd64",
Docs: "package pkg\n\nPackage pkg is a package.\n\n",
},
},
{
name: "invalid doc format",
url: "/v1/package/example.com/pkg?version=v1.2.3&doc=invalid",
wantStatus: http.StatusBadRequest,
want: &Error{
Code: http.StatusBadRequest,
Message: "bad doc format: need one of 'text', 'md', 'markdown' or 'html'",
},
},
{
name: "empty doc format",
url: "/v1/package/example.com/pkg?version=v1.2.3&doc=",
wantStatus: http.StatusOK,
want: &Package{
Path: "example.com/pkg",
ModulePath: "example.com",
ModuleVersion: version,
Synopsis: "This is a package synopsis for GOOS=linux, GOARCH=amd64",
GOOS: "linux",
GOARCH: "amd64",
Docs: "",
},
},
{
name: "licenses",
url: "/v1/package/example.com/pkg?version=v1.2.3&licenses=true",
wantStatus: http.StatusOK,
want: &Package{
Path: "example.com/pkg",
ModulePath: "example.com",
ModuleVersion: version,
Synopsis: "This is a package synopsis for GOOS=linux, GOARCH=amd64",
IsLatest: false,
GOOS: "linux",
GOARCH: "amd64",
Licenses: []License{
{
Types: []string{sample.LicenseType},
FilePath: sample.LicenseFilePath,
Contents: "Lorem Ipsum",
},
},
},
},
{
name: "imports",
url: "/v1/package/example.com/pkg?version=v1.2.3&imports=true",
wantStatus: http.StatusOK,
want: &Package{
Path: "example.com/pkg",
ModulePath: "example.com",
ModuleVersion: version,
Synopsis: "This is a package synopsis for GOOS=linux, GOARCH=amd64",
IsLatest: false,
GOOS: "linux",
GOARCH: "amd64",
Imports: []string{pkgPath},
},
},
} {
t.Run(test.name, func(t *testing.T) {
r := httptest.NewRequest("GET", test.url, nil)
w := httptest.NewRecorder()
if err := ServePackage(w, r, ds); err != nil {
ServeError(w, r, err)
}
if w.Code != test.wantStatus {
t.Errorf("status = %d, want %d. Body: %s", w.Code, test.wantStatus, w.Body.String())
}
if test.want != nil {
got, err := unmarshalResponse[Package](w.Body.Bytes())
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(test.want, got, cmpopts.IgnoreUnexported(Error{})); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
})
}
}
func TestServeModule(t *testing.T) {
ctx := context.Background()
ds := fakedatasource.New()
const (
modulePath = "example.com"
version = "v1.2.3"
)
mi1 := sample.ModuleInfo(modulePath, version)
mi1.LatestVersion = "v1.2.4"
mi1.HasGoMod = true
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: *mi1,
Licenses: sample.Licenses(),
Units: []*internal.Unit{{
UnitMeta: internal.UnitMeta{
Path: modulePath,
ModuleInfo: *mi1,
},
Readme: &internal.Readme{Filepath: "README.md", Contents: "Hello world"},
Licenses: sample.LicenseMetadata(),
}},
})
mi2 := sample.ModuleInfo(modulePath, "v1.2.4")
mi2.LatestVersion = "v1.2.4"
mi2.HasGoMod = true
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: *mi2,
Units: []*internal.Unit{{
UnitMeta: internal.UnitMeta{
Path: modulePath,
ModuleInfo: *mi2,
},
}},
})
for _, test := range []struct {
name string
url string
wantStatus int
want any
}{
{
name: "basic module metadata",
url: "/v1/module/example.com?version=v1.2.3",
wantStatus: http.StatusOK,
want: &Module{
Path: modulePath,
Version: version,
IsRedistributable: true,
HasGoMod: true,
RepoURL: "https://example.com",
},
},
{
name: "latest module metadata",
url: "/v1/module/example.com?version=v1.2.4",
wantStatus: http.StatusOK,
want: &Module{
Path: modulePath,
Version: "v1.2.4",
IsLatest: true,
IsRedistributable: true,
HasGoMod: true,
RepoURL: "https://example.com",
},
},
{
name: "bad version",
url: "/v1/module/example.com?version=nope",
wantStatus: http.StatusNotFound,
want: &Error{Code: 404, Message: "not found"},
},
{
name: "module not found",
url: "/v1/module/nonexistent.com",
wantStatus: http.StatusNotFound,
want: &Error{Code: 404, Message: "not found"},
},
{
name: "missing module path",
url: "/v1/module/",
wantStatus: http.StatusBadRequest,
want: &Error{Code: 400, Message: "missing module path"},
},
{
name: "module with readme",
url: "/v1/module/example.com?version=v1.2.3&readme=true",
wantStatus: http.StatusOK,
want: &Module{
Path: modulePath,
Version: version,
IsRedistributable: true,
HasGoMod: true,
RepoURL: "https://example.com",
Readme: &Readme{
Filepath: "README.md",
Contents: "Hello world",
},
},
},
{
name: "module with licenses",
url: "/v1/module/example.com?version=v1.2.3&licenses=true",
wantStatus: http.StatusOK,
want: &Module{
Path: modulePath,
Version: version,
IsRedistributable: true,
HasGoMod: true,
RepoURL: "https://example.com",
Licenses: []License{
{
Types: []string{"MIT"},
FilePath: "LICENSE",
Contents: "Lorem Ipsum",
},
},
},
},
} {
t.Run(test.name, func(t *testing.T) {
r := httptest.NewRequest("GET", test.url, nil)
w := httptest.NewRecorder()
if err := ServeModule(w, r, ds); err != nil {
ServeError(w, r, err)
}
if w.Code != test.wantStatus {
t.Errorf("status = %d, want %d", w.Code, test.wantStatus)
}
if test.want != nil {
got, err := unmarshalResponse[Module](w.Body.Bytes())
if err != nil {
t.Fatalf("unmarshaling: %v", err)
}
if diff := cmp.Diff(test.want, got, cmpopts.IgnoreUnexported(Error{})); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
})
}
}
func TestServeModuleVersions(t *testing.T) {
ctx := context.Background()
ds := fakedatasource.New()
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{ModulePath: "example.com", Version: "v1.0.0"},
Units: []*internal.Unit{{UnitMeta: internal.UnitMeta{Path: "example.com"}}},
})
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{ModulePath: "example.com", Version: "v1.1.0"},
Units: []*internal.Unit{{UnitMeta: internal.UnitMeta{Path: "example.com"}}},
})
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{ModulePath: "example.com/v2", Version: "v2.0.0"},
Units: []*internal.Unit{{UnitMeta: internal.UnitMeta{Path: "example.com/v2"}}},
})
for _, test := range []struct {
name string
url string
wantStatus int
wantCount int
want any
}{
{
name: "all versions (cross-major)",
url: "/v1/versions/example.com",
wantStatus: http.StatusOK,
wantCount: 3,
},
{
name: "module not found",
url: "/v1/versions/nonexistent.com",
wantStatus: http.StatusNotFound,
want: &Error{Code: 404, Message: "not found"},
},
{
name: "missing module path",
url: "/v1/versions/",
wantStatus: http.StatusBadRequest,
want: &Error{Code: 400, Message: "missing module path"},
},
{
name: "with limit",
url: "/v1/versions/example.com?limit=1",
wantStatus: http.StatusOK,
wantCount: 1,
},
{
name: "pagination",
url: "/v1/versions/example.com?limit=1&token=1",
wantStatus: http.StatusOK,
wantCount: 1,
},
} {
t.Run(test.name, func(t *testing.T) {
r := httptest.NewRequest("GET", test.url, nil)
w := httptest.NewRecorder()
if err := ServeModuleVersions(w, r, ds); err != nil {
ServeError(w, r, err)
}
if w.Code != test.wantStatus {
t.Errorf("status = %d, want %d", w.Code, test.wantStatus)
}
if test.wantStatus == http.StatusOK {
var got PaginatedResponse[internal.ModuleInfo]
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if len(got.Items) != test.wantCount {
t.Errorf("count = %d, want %d", len(got.Items), test.wantCount)
}
}
})
}
}
func TestServeModulePackages(t *testing.T) {
ctx := context.Background()
ds := fakedatasource.New()
const (
modulePath = "example.com"
version = "v1.0.0"
)
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{ModulePath: modulePath, Version: version},
Units: []*internal.Unit{
{
UnitMeta: internal.UnitMeta{Path: modulePath, Name: "pkg1"},
Documentation: []*internal.Documentation{
{Synopsis: "synopsis for pkg1"},
},
},
{
UnitMeta: internal.UnitMeta{Path: modulePath + "/sub", Name: "pkg2"},
Documentation: []*internal.Documentation{
{Synopsis: "synopsis for pkg2"},
},
},
},
})
for _, test := range []struct {
name string
url string
wantStatus int
wantCount int
wantTotal int
wantToken string
want any
}{
{
name: "all packages",
url: "/v1/packages/example.com?version=v1.0.0",
wantStatus: http.StatusOK,
wantCount: 2,
wantTotal: 2,
},
{
name: "module not found",
url: "/v1/packages/nonexistent.com?version=v1.0.0",
wantStatus: http.StatusNotFound,
want: &Error{Code: 404, Message: "not found"},
},
{
name: "missing module path",
url: "/v1/packages/",
wantStatus: http.StatusBadRequest,
want: &Error{Code: 400, Message: "missing module path"},
},
{
name: "filtering",
url: "/v1/packages/example.com?version=v1.0.0&filter=sub",
wantStatus: http.StatusOK,
wantCount: 1,
wantTotal: 1,
},
{
name: "filtering synopsis",
url: "/v1/packages/example.com?version=v1.0.0&filter=pkg2",
wantStatus: http.StatusOK,
wantCount: 1,
wantTotal: 1,
},
{
name: "limit and token",
url: "/v1/packages/example.com?version=v1.0.0&limit=1",
wantStatus: http.StatusOK,
wantCount: 1,
wantTotal: 2,
wantToken: "1",
},
{
name: "next page",
url: "/v1/packages/example.com?version=v1.0.0&limit=1&token=1",
wantStatus: http.StatusOK,
wantCount: 1,
wantTotal: 2,
},
} {
t.Run(test.name, func(t *testing.T) {
r := httptest.NewRequest("GET", test.url, nil)
w := httptest.NewRecorder()
if err := ServeModulePackages(w, r, ds); err != nil {
ServeError(w, r, err)
}
if w.Code != test.wantStatus {
t.Errorf("status = %d, want %d", w.Code, test.wantStatus)
}
if test.wantStatus == http.StatusOK {
var got PaginatedResponse[Package]
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if len(got.Items) != test.wantCount {
t.Errorf("count = %d, want %d", len(got.Items), test.wantCount)
}
if got.Total != test.wantTotal {
t.Errorf("total = %d, want %d", got.Total, test.wantTotal)
}
if got.NextPageToken != test.wantToken {
t.Errorf("token = %q, want %q", got.NextPageToken, test.wantToken)
}
}
})
}
}
func TestServeSearch(t *testing.T) {
ctx := context.Background()
ds := fakedatasource.New()
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{ModulePath: "example.com", Version: "v1.0.0"},
Units: []*internal.Unit{{
UnitMeta: internal.UnitMeta{
Path: "example.com/pkg",
ModuleInfo: internal.ModuleInfo{ModulePath: "example.com", Version: "v1.0.0"},
Name: "pkg",
},
Documentation: []*internal.Documentation{{Synopsis: "A great package."}},
}},
})
for _, test := range []struct {
name string
url string
wantStatus int
wantCount int
}{
{
name: "basic search",
url: "/v1/search?q=great",
wantStatus: http.StatusOK,
wantCount: 1,
},
{
name: "no results",
url: "/v1/search?q=nonexistent",
wantStatus: http.StatusOK,
wantCount: 0,
},
{
name: "missing query",
url: "/v1/search",
wantStatus: http.StatusBadRequest,
},
{
name: "search with filter",
url: "/v1/search?q=great&filter=example.com",
wantStatus: http.StatusOK,
wantCount: 1,
},
{
name: "search with non-matching filter",
url: "/v1/search?q=great&filter=nomatch",
wantStatus: http.StatusOK,
wantCount: 0,
},
} {
t.Run(test.name, func(t *testing.T) {
r := httptest.NewRequest("GET", test.url, nil)
w := httptest.NewRecorder()
err := ServeSearch(w, r, ds)
if err != nil {
ServeError(w, r, err)
}
if w.Code != test.wantStatus {
t.Errorf("%s: status = %d, want %d", test.name, w.Code, test.wantStatus)
}
if test.wantStatus == http.StatusOK {
var got PaginatedResponse[SearchResult]
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("%s: json.Unmarshal: %v", test.name, err)
}
if len(got.Items) != test.wantCount {
t.Errorf("%s: count = %d, want %d", test.name, len(got.Items), test.wantCount)
}
}
})
}
}
func TestServeSearchPagination(t *testing.T) {
ctx := context.Background()
ds := fakedatasource.New()
for i := 0; i < 10; i++ {
pkgPath := "example.com/pkg" + strconv.Itoa(i)
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{ModulePath: pkgPath, Version: "v1.0.0"},
Units: []*internal.Unit{{
UnitMeta: internal.UnitMeta{
Path: pkgPath,
ModuleInfo: internal.ModuleInfo{ModulePath: pkgPath, Version: "v1.0.0"},
Name: "pkg",
},
Documentation: []*internal.Documentation{{Synopsis: "Synopsis" + strconv.Itoa(i)}},
}},
})
}
for _, test := range []struct {
name string
url string
wantCount int
wantTotal int
wantNextToken string
}{
{
name: "first page",
url: "/v1/search?q=Synopsis&limit=3",
wantCount: 3,
wantTotal: 10,
wantNextToken: "3",
},
{
name: "second page",
url: "/v1/search?q=Synopsis&limit=3&token=3",
wantCount: 3,
wantTotal: 10,
wantNextToken: "6",
},
{
name: "last page",
url: "/v1/search?q=Synopsis&limit=3&token=9",
wantCount: 1,
wantTotal: 10,
wantNextToken: "",
},
} {
t.Run(test.name, func(t *testing.T) {
r := httptest.NewRequest("GET", test.url, nil)
w := httptest.NewRecorder()
if err := ServeSearch(w, r, ds); err != nil {
ServeError(w, r, err)
}
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", w.Code)
}
var got PaginatedResponse[SearchResult]
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if len(got.Items) != test.wantCount {
t.Errorf("count = %d, want %d", len(got.Items), test.wantCount)
}
if got.Total != test.wantTotal {
t.Errorf("total = %d, want %d", got.Total, test.wantTotal)
}
if got.NextPageToken != test.wantNextToken {
t.Errorf("nextToken = %q, want %q", got.NextPageToken, test.wantNextToken)
}
})
}
}
func TestServePackageSymbols(t *testing.T) {
ctx := context.Background()
ds := fakedatasource.New()
const (
pkgPath = "example.com/pkg"
modulePath = "example.com"
version = "v1.0.0"
)
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{ModulePath: modulePath, Version: version},
Units: []*internal.Unit{{
UnitMeta: internal.UnitMeta{
Path: pkgPath,
ModuleInfo: internal.ModuleInfo{ModulePath: modulePath, Version: version},
Name: "pkg",
},
Symbols: map[internal.BuildContext][]*internal.Symbol{
{GOOS: "linux", GOARCH: "amd64"}: {
{
SymbolMeta: internal.SymbolMeta{Name: "LinuxSym", Kind: internal.SymbolKindFunction},
GOOS: "linux",
GOARCH: "amd64",
},
{
SymbolMeta: internal.SymbolMeta{Name: "T", Kind: internal.SymbolKindType},
GOOS: "linux",
GOARCH: "amd64",
Children: []*internal.SymbolMeta{
{Name: "T.M", Kind: internal.SymbolKindMethod, ParentName: "T"},
},
},
},
{GOOS: "windows", GOARCH: "amd64"}: {
{SymbolMeta: internal.SymbolMeta{Name: "WindowsSym", Kind: internal.SymbolKindFunction}, GOOS: "windows", GOARCH: "amd64"},
},
{GOOS: "js", GOARCH: "wasm"}: {
{SymbolMeta: internal.SymbolMeta{Name: "WasmSym", Kind: internal.SymbolKindFunction}, GOOS: "js", GOARCH: "wasm"},
},
},
}},
})
for _, test := range []struct {
name string
url string
wantStatus int
wantCount int
wantName string // Check name of the first symbol to verify build context
want any
}{
{
name: "default best match (linux)",
url: "/v1/symbols/example.com/pkg?version=v1.0.0",
wantStatus: http.StatusOK,
wantCount: 2,
wantName: "LinuxSym",
},
{
name: "package not found",
url: "/v1/symbols/nonexistent.com/pkg?version=v1.0.0",
wantStatus: http.StatusNotFound,
want: &Error{Code: 404, Message: "not found"},
},
{
name: "missing package path",
url: "/v1/symbols/",
wantStatus: http.StatusBadRequest,
want: &Error{Code: 400, Message: "missing package path"},
},
{
name: "explicit linux",
url: "/v1/symbols/example.com/pkg?version=v1.0.0&goos=linux&goarch=amd64",
wantStatus: http.StatusOK,
wantCount: 2,
wantName: "LinuxSym",
},
{
name: "version latest",
url: "/v1/symbols/example.com/pkg?version=latest",
wantStatus: http.StatusOK,
wantCount: 2,
wantName: "LinuxSym",
},
{
name: "explicit windows",
url: "/v1/symbols/example.com/pkg?version=v1.0.0&goos=windows&goarch=amd64",
wantStatus: http.StatusOK,
wantCount: 1,
wantName: "WindowsSym",
},
{
name: "explicit wasm",
url: "/v1/symbols/example.com/pkg?version=v1.0.0&goos=js&goarch=wasm",
wantStatus: http.StatusOK,
wantCount: 1,
wantName: "WasmSym",
},
{
name: "not found build context",
url: "/v1/symbols/example.com/pkg?version=v1.0.0&goos=darwin&goarch=amd64",
wantStatus: http.StatusNotFound,
want: &Error{
Code: http.StatusNotFound,
Message: "not found",
},
},
} {
t.Run(test.name, func(t *testing.T) {
r := httptest.NewRequest("GET", test.url, nil)
w := httptest.NewRecorder()
if err := ServePackageSymbols(w, r, ds); err != nil {
ServeError(w, r, err)
}
if w.Code != test.wantStatus {
t.Errorf("status = %d, want %d. Body: %s", w.Code, test.wantStatus, w.Body.String())
}
if test.want != nil {
if want, ok := test.want.(*Error); ok {
var got Error
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if diff := cmp.Diff(want, &got, cmpopts.IgnoreUnexported(Error{})); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
}
}
if test.wantStatus == http.StatusOK {
var got PaginatedResponse[Symbol]
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if len(got.Items) != test.wantCount {
t.Errorf("count = %d, want %d", len(got.Items), test.wantCount)
}
if test.wantName != "" && got.Items[0].Name != test.wantName {
t.Errorf("first symbol = %q, want %q", got.Items[0].Name, test.wantName)
}
}
})
}
}
func TestServePackageImportedBy(t *testing.T) {
ctx := context.Background()
ds := fakedatasource.New()
const (
pkgPath = "example.com/pkg"
modulePath = "example.com"
version = "v1.0.0"
)
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{ModulePath: modulePath, Version: version},
Units: []*internal.Unit{
{UnitMeta: internal.UnitMeta{Path: pkgPath, ModuleInfo: internal.ModuleInfo{ModulePath: modulePath, Version: version}}},
{
UnitMeta: internal.UnitMeta{Path: "example.com/other", ModuleInfo: internal.ModuleInfo{ModulePath: modulePath, Version: version}},
Imports: []string{pkgPath},
},
},
})
for _, test := range []struct {
name string
url string
wantStatus int
wantCount int
want any
}{
{
name: "all imported by",
url: "/v1/imported-by/example.com/pkg?version=v1.0.0",
wantStatus: http.StatusOK,
wantCount: 1,
},
} {
t.Run(test.name, func(t *testing.T) {
r := httptest.NewRequest("GET", test.url, nil)
w := httptest.NewRecorder()
if err := ServePackageImportedBy(w, r, ds); err != nil {
ServeError(w, r, err)
}
if w.Code != test.wantStatus {
t.Errorf("status = %d, want %d", w.Code, test.wantStatus)
}
if test.wantStatus == http.StatusOK {
var got PackageImportedBy
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if len(got.ImportedBy.Items) != test.wantCount {
t.Errorf("count = %d, want %d", len(got.ImportedBy.Items), test.wantCount)
}
}
})
}
}
func TestServeVulnerabilities(t *testing.T) {
ds := fakedatasource.New()
vc, err := vuln.NewInMemoryClient([]*osv.Entry{
{
ID: "VULN-1",
Summary: "Vulnerability 1",
Affected: []osv.Affected{
{
Module: osv.Module{Path: "example.com"},
Ranges: []osv.Range{{Type: osv.RangeTypeSemver, Events: []osv.RangeEvent{{Introduced: "0"}, {Fixed: "1.1.0"}}}},
},
},
},
})
if err != nil {
t.Fatal(err)
}
for _, test := range []struct {
name string
url string
wantStatus int
wantCount int
want any
}{
{
name: "all vulns",
url: "/v1/vulns/example.com?version=v1.0.0",
wantStatus: http.StatusOK,
wantCount: 1,
},
{
name: "no vulns",
url: "/v1/vulns/example.com?version=v1.2.0",
wantStatus: http.StatusOK,
wantCount: 0,
},
} {
t.Run(test.name, func(t *testing.T) {
r := httptest.NewRequest("GET", test.url, nil)
w := httptest.NewRecorder()
if err := ServeVulnerabilities(vc)(w, r, ds); err != nil {
ServeError(w, r, err)
}
if w.Code != test.wantStatus {
t.Errorf("status = %d, want %d", w.Code, test.wantStatus)
}
if test.wantStatus == http.StatusOK {
var got PaginatedResponse[Vulnerability]
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
if len(got.Items) != test.wantCount {
t.Errorf("count = %d, want %d", len(got.Items), test.wantCount)
}
}
})
}
}
// unmarshalResponse unmarshals an API response into either
// a *T or an *Error.
func unmarshalResponse[T any](data []byte) (any, error) {
d := json.NewDecoder(bytes.NewReader(data))
d.DisallowUnknownFields()
var t T
err1 := d.Decode(&t)
if err1 == nil {
return &t, nil
}
d = json.NewDecoder(bytes.NewReader(data))
d.DisallowUnknownFields()
var e Error
err2 := d.Decode(&e)
if err2 == nil {
return &e, nil
}
return nil, errors.Join(err1, err2)
}
func TestCacheControl(t *testing.T) {
ctx := context.Background()
ds := fakedatasource.New()
const modulePath = "example.com"
for _, v := range []string{"v1.0.0", "master"} {
ds.MustInsertModule(ctx, &internal.Module{
ModuleInfo: internal.ModuleInfo{
ModulePath: modulePath,
Version: v,
},
Units: []*internal.Unit{{
UnitMeta: internal.UnitMeta{
Path: modulePath,
ModuleInfo: internal.ModuleInfo{
ModulePath: modulePath,
Version: v,
},
},
}},
})
}
for _, test := range []struct {
version string
want string
}{
{"v1.0.0", "public, max-age=10800"},
{"latest", "public, max-age=3600"},
{"master", "public, max-age=3600"},
{"", "public, max-age=3600"},
} {
t.Run(test.version, func(t *testing.T) {
url := "/v1/module/" + modulePath
if test.version != "" {
url += "?version=" + test.version
}
r := httptest.NewRequest("GET", url, nil)
w := httptest.NewRecorder()
if err := ServeModule(w, r, ds); err != nil {
t.Fatal(err)
}
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", w.Code, http.StatusOK)
}
got := w.Header().Get("Cache-Control")
if got != test.want {
t.Errorf("Cache-Control = %q, want %q", got, test.want)
}
})
}
}