blob: 18c594356eeb589c8c085a21fd562a0b9f6d5876 [file] [log] [blame]
// Copyright 2019 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 frontend
import (
"context"
"fmt"
"net/http/httptest"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/experiment"
"golang.org/x/pkgsite/internal/licenses"
"golang.org/x/pkgsite/internal/postgres"
"golang.org/x/pkgsite/internal/testing/sample"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/vulndb/osv"
)
func TestSearchQueryAndMode(t *testing.T) {
for _, test := range []struct {
name, m, q, wantSearchMode string
}{
{
name: "symbol: prefix in symbol mode",
m: searchModeSymbol,
q: "#foo",
wantSearchMode: searchModeSymbol,
},
{
name: "symbol: prefix in package mode",
m: searchModeSymbol,
q: "#foo",
wantSearchMode: searchModeSymbol,
},
{
name: "search in package mode",
m: searchModePackage,
q: "foo",
wantSearchMode: searchModePackage,
},
{
name: "search in symbol mode",
m: searchModeSymbol,
q: "foo",
wantSearchMode: searchModeSymbol,
},
} {
t.Run(test.name, func(t *testing.T) {
u := fmt.Sprintf("/search?q=%s&m=%s", test.q, test.m)
r := httptest.NewRequest("GET", u, nil)
r = r.WithContext(experiment.NewContext(r.Context(), internal.ExperimentSymbolSearch))
gotSearchMode := searchMode(r)
if gotSearchMode != test.wantSearchMode {
t.Errorf("searchQueryAndMode(%q) = %q; want = %q",
u, gotSearchMode, test.wantSearchMode)
}
})
}
}
func TestFetchSearchPage(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
defer postgres.ResetTestDB(testDB, t)
ctx = experiment.NewContext(ctx, internal.ExperimentVulns)
var (
now = sample.NowTruncated()
moduleFoo = &internal.Module{
ModuleInfo: internal.ModuleInfo{
ModulePath: "github.com/mod/foo",
Version: "v1.0.0",
CommitTime: now,
IsRedistributable: true,
},
Units: []*internal.Unit{
{
UnitMeta: internal.UnitMeta{
ModuleInfo: internal.ModuleInfo{
ModulePath: "github.com/mod/foo",
Version: "v1.0.0",
CommitTime: now,
IsRedistributable: true,
},
Name: "foo",
Path: "github.com/mod/foo",
Licenses: sample.LicenseMetadata(),
IsRedistributable: true,
},
Documentation: []*internal.Documentation{{
GOOS: sample.GOOS,
GOARCH: sample.GOARCH,
Synopsis: "foo is a package.",
Source: []byte{},
}},
Readme: &internal.Readme{
Filepath: "readme",
Contents: "readme",
},
},
},
}
moduleBar = &internal.Module{
ModuleInfo: internal.ModuleInfo{
ModulePath: "github.com/mod/bar",
Version: "v1.0.0",
CommitTime: now,
IsRedistributable: true,
},
Units: []*internal.Unit{
{
UnitMeta: internal.UnitMeta{
ModuleInfo: internal.ModuleInfo{
CommitTime: now,
ModulePath: "github.com/mod/bar",
Version: "v1.0.0",
IsRedistributable: true,
},
Name: "bar",
Path: "github.com/mod/bar",
Licenses: sample.LicenseMetadata(),
IsRedistributable: true,
},
Documentation: []*internal.Documentation{{
GOOS: sample.GOOS,
GOARCH: sample.GOARCH,
Synopsis: "bar is used by foo.",
Source: []byte{},
}},
Readme: &internal.Readme{
Filepath: "readme",
Contents: "readme",
},
},
},
}
vulnEntries = []*osv.Entry{{
ID: "test",
Details: "vuln",
Affected: []osv.Affected{{
Package: osv.Package{Name: "github.com/mod/foo"},
Ranges: []osv.AffectsRange{{
Type: osv.TypeSemver,
Events: []osv.RangeEvent{{Introduced: "1.0.0"}, {Fixed: "1.9.0"}},
}},
}},
}}
getVulnEntries = func(modulePath string) ([]*osv.Entry, error) {
if modulePath == moduleFoo.ModulePath {
return vulnEntries, nil
}
return nil, nil
}
)
for _, m := range []*internal.Module{moduleFoo, moduleBar} {
postgres.MustInsertModule(ctx, t, testDB, m)
}
for _, test := range []struct {
name, query string
modules []*internal.Module
wantSearchPage *SearchPage
}{
{
name: "want expected search page",
query: "foo bar",
wantSearchPage: &SearchPage{
PackageTabQuery: "foo bar",
Pagination: pagination{
TotalCount: 1,
ResultCount: 1,
PrevPage: 0,
NextPage: 0,
Limit: 20,
DefaultLimit: 25,
MaxLimit: 100,
Page: 1,
Pages: []int{1},
},
Results: []*SearchResult{
{
Name: moduleBar.Packages()[0].Name,
PackagePath: moduleBar.Packages()[0].Path,
ModulePath: moduleBar.ModulePath,
Version: "v1.0.0",
Synopsis: moduleBar.Packages()[0].Documentation[0].Synopsis,
DisplayVersion: moduleBar.Version,
Licenses: []string{"MIT"},
CommitTime: elapsedTime(moduleBar.CommitTime),
},
},
},
},
{
name: "want only foo search page",
query: "package",
wantSearchPage: &SearchPage{
PackageTabQuery: "package",
Pagination: pagination{
TotalCount: 1,
ResultCount: 1,
PrevPage: 0,
NextPage: 0,
Limit: 20,
DefaultLimit: 25,
MaxLimit: 100,
Page: 1,
Pages: []int{1},
},
Results: []*SearchResult{
{
Name: moduleFoo.Packages()[0].Name,
PackagePath: moduleFoo.Packages()[0].Path,
ModulePath: moduleFoo.ModulePath,
Version: "v1.0.0",
Synopsis: moduleFoo.Packages()[0].Documentation[0].Synopsis,
DisplayVersion: moduleFoo.Version,
Licenses: []string{"MIT"},
CommitTime: elapsedTime(moduleFoo.CommitTime),
Vulns: []Vuln{{ID: "test", Details: "vuln", FixedVersion: "v1.9.0"}},
},
},
},
},
} {
t.Run(test.name, func(t *testing.T) {
got, err := fetchSearchPage(ctx, testDB, test.query, "", paginationParams{limit: 20, page: 1}, false, getVulnEntries)
if err != nil {
t.Fatalf("fetchSearchPage(db, %q): %v", test.query, err)
}
opts := cmp.Options{
cmp.AllowUnexported(SearchPage{}, pagination{}),
cmpopts.IgnoreFields(SearchResult{}, "NumImportedBy"),
cmpopts.IgnoreFields(licenses.Metadata{}, "FilePath"),
cmpopts.IgnoreFields(basePage{}, "MetaDescription"),
}
if diff := cmp.Diff(test.wantSearchPage, got, opts...); diff != "" {
t.Errorf("fetchSearchPage(db, %q) mismatch (-want +got):\n%s", test.query, diff)
}
})
}
}
func TestNewSearchResult(t *testing.T) {
for _, test := range []struct {
name string
tag language.Tag
in postgres.SearchResult
want SearchResult
}{
{
name: "basic",
tag: language.English,
in: postgres.SearchResult{
Name: "pkg",
PackagePath: "m.com/pkg",
ModulePath: "m.com",
Version: "v1.0.0",
NumImportedBy: 3,
},
want: SearchResult{
Name: "pkg",
PackagePath: "m.com/pkg",
ModulePath: "m.com",
Version: "v1.0.0",
DisplayVersion: "v1.0.0",
NumImportedBy: "3",
},
},
{
name: "command",
tag: language.English,
in: postgres.SearchResult{
Name: "main",
PackagePath: "m.com/cmd",
ModulePath: "m.com",
Version: "v1.0.0",
NumImportedBy: 1234,
},
want: SearchResult{
Name: "cmd",
PackagePath: "m.com/cmd",
ModulePath: "m.com",
Version: "v1.0.0",
DisplayVersion: "v1.0.0",
ChipText: "command",
NumImportedBy: "1,234",
},
},
{
name: "stdlib",
tag: language.English,
in: postgres.SearchResult{
Name: "math",
PackagePath: "math",
ModulePath: "std",
Version: "v1.14.0",
},
want: SearchResult{
Name: "math",
PackagePath: "math",
ModulePath: "std",
Version: "v1.14.0",
DisplayVersion: "go1.14",
ChipText: "standard library",
NumImportedBy: "0",
},
},
{
name: "German",
tag: language.German,
in: postgres.SearchResult{
Name: "pkg",
PackagePath: "m.com/pkg",
ModulePath: "m.com",
Version: "v1.0.0",
NumImportedBy: 3456,
},
want: SearchResult{
Name: "pkg",
PackagePath: "m.com/pkg",
ModulePath: "m.com",
Version: "v1.0.0",
DisplayVersion: "v1.0.0",
NumImportedBy: "3.456",
},
},
} {
t.Run(test.name, func(t *testing.T) {
pr := message.NewPrinter(test.tag)
got := newSearchResult(&test.in, false, pr)
test.want.CommitTime = "unknown"
if diff := cmp.Diff(&test.want, got); diff != "" {
t.Errorf("mimatch (-want, +got):\n%s", diff)
}
})
}
}
func TestSearchRequestRedirectPath(t *testing.T) {
// Experiments need to be set in the context, for DB work, and as
// a middleware, for request handling.
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
defer postgres.ResetTestDB(testDB, t)
golangTools := sample.Module("golang.org/x/tools", sample.VersionString, "internal/lsp")
std := sample.Module("std", sample.VersionString,
"cmd/go", "cmd/go/internal/auth", "fmt")
modules := []*internal.Module{golangTools, std}
for _, v := range modules {
postgres.MustInsertModule(ctx, t, testDB, v)
}
for _, test := range []struct {
name string
query string
want string
}{
{"module", "golang.org/x/tools", "/golang.org/x/tools"},
{"directory", "golang.org/x/tools/internal", "/golang.org/x/tools/internal"},
{"package", "golang.org/x/tools/internal/lsp", "/golang.org/x/tools/internal/lsp"},
{"stdlib package does not redirect", "errors", ""},
{"stdlib package does redirect", "cmd/go", "/cmd/go"},
{"stdlib directory does redirect", "cmd/go/internal", "/cmd/go/internal"},
{"std does not redirect", "std", ""},
{"non-existent path does not redirect", "github.com/non-existent", ""},
{"trim URL scheme from query", "https://golang.org/x/tools", "/golang.org/x/tools"},
} {
t.Run(test.name, func(t *testing.T) {
if got := searchRequestRedirectPath(ctx, testDB, test.query); got != test.want {
t.Errorf("searchRequestRedirectPath(ctx, %q) = %q; want = %q", test.query, got, test.want)
}
})
}
}
func TestElapsedTime(t *testing.T) {
now := sample.NowTruncated()
testCases := []struct {
name string
date time.Time
elapsedTime string
}{
{
name: "one_hour_ago",
date: now.Add(time.Hour * -1),
elapsedTime: "1 hour ago",
},
{
name: "hours_ago",
date: now.Add(time.Hour * -2),
elapsedTime: "2 hours ago",
},
{
name: "today",
date: now.Add(time.Hour * -8),
elapsedTime: "today",
},
{
name: "one_day_ago",
date: now.Add(time.Hour * 24 * -1),
elapsedTime: "1 day ago",
},
{
name: "days_ago",
date: now.Add(time.Hour * 24 * -5),
elapsedTime: "5 days ago",
},
{
name: "more_than_6_days_ago",
date: now.Add(time.Hour * 24 * -14),
elapsedTime: now.Add(time.Hour * 24 * -14).Format("Jan _2, 2006"),
},
{
name: "zero",
date: time.Time{},
elapsedTime: "unknown",
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
elapsedTime := elapsedTime(test.date)
if elapsedTime != test.elapsedTime {
t.Errorf("elapsedTime(%q) = %s, want %s", test.date, elapsedTime, test.elapsedTime)
}
})
}
}
func TestSymbolSynopsis(t *testing.T) {
for _, test := range []struct {
name string
r *postgres.SearchResult
want string
}{
{
"struct field",
&postgres.SearchResult{
SymbolName: "Foo.Bar",
SymbolSynopsis: "Bar string",
SymbolKind: internal.SymbolKindField,
},
`
type Foo struct {
Bar string
}
`,
},
{
"interface method",
&postgres.SearchResult{
SymbolName: "Foo.Bar",
SymbolSynopsis: "Bar func() string",
SymbolKind: internal.SymbolKindMethod,
},
`
type Foo interface {
Bar func() string
}
`,
},
} {
t.Run(test.name, func(t *testing.T) {
got := symbolSynopsis(test.r)
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch(-want, +got): %s", diff)
}
})
}
}
func TestShouldDefaultToSymbolSearch(t *testing.T) {
for _, test := range []struct {
q string
want bool
}{
{"barista.run", false},
{"github.com", false},
{"julie.io", false},
{"my.name", false},
{"sql", false},
{"sql.DB", true},
{"sql.DB.Begin", true},
{"yaml.v2", false},
{"gopkg.in", false},
{"Unmarshal", true},
} {
t.Run(test.q, func(t *testing.T) {
got := shouldDefaultToSymbolSearch(test.q)
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
}