blob: 1bb7e25a46684cca812e94c5f7db8bd8444fb259 [file] [log] [blame]
// 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 frontend
import (
"context"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/google/safehtml/template"
"github.com/jba/templatecheck"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/frontend/page"
"golang.org/x/pkgsite/internal/frontend/versions"
"golang.org/x/pkgsite/internal/postgres"
"golang.org/x/pkgsite/internal/proxy/proxytest"
"golang.org/x/pkgsite/static"
thirdparty "golang.org/x/pkgsite/third_party"
)
const testTimeout = 5 * time.Second
var testDB *postgres.DB
func TestMain(m *testing.M) {
postgres.RunDBTests("discovery_frontend_test", m, &testDB)
}
type testModule struct {
path string
redistributable bool
versions []string
packages []testPackage
}
type testPackage struct {
name string
suffix string
readmeContents string
readmeFilePath string
docs []*internal.Documentation
}
func newTestServer(t *testing.T, proxyModules []*proxytest.Module, cacher Cacher) (*Server, http.Handler, func()) {
t.Helper()
s, err := NewServer(ServerConfig{
DataSourceGetter: func(context.Context) internal.DataSource { return testDB },
TemplateFS: template.TrustedFSFromEmbed(static.FS),
// Use the embedded FSs here to make sure they're tested.
// Integration tests will use the actual directories.
StaticFS: static.FS,
ThirdPartyFS: thirdparty.FS,
StaticPath: "../../static",
})
if err != nil {
t.Fatal(err)
}
mux := http.NewServeMux()
s.Install(mux.Handle, cacher, nil)
return s, mux, func() {
postgres.ResetTestDB(testDB, t)
}
}
func TestHTMLInjection(t *testing.T) {
_, handler, _ := newTestServer(t, nil, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, httptest.NewRequest("GET", "/<em>UHOH</em>", nil))
if strings.Contains(w.Body.String(), "<em>") {
t.Error("User input was rendered unescaped.")
}
}
func mustRequest(urlPath string, t *testing.T) *http.Request {
t.Helper()
r, err := http.NewRequest(http.MethodGet, "http://localhost"+urlPath, nil)
if err != nil {
t.Fatal(err)
}
return r
}
func TestDetailsTTL(t *testing.T) {
tests := []struct {
r *http.Request
want time.Duration
}{
{mustRequest("/host.com/module@v1.2.3/suffix", t), longTTL},
{mustRequest("/host.com/module/suffix", t), shortTTL},
{mustRequest("/host.com/module@v1.2.3/suffix?tab=overview", t), longTTL},
{mustRequest("/host.com/module@v1.2.3/suffix?tab=versions", t), defaultTTL},
{mustRequest("/host.com/module@v1.2.3/suffix?tab=importedby", t), defaultTTL},
{
func() *http.Request {
r := mustRequest("/host.com/module@v1.2.3/suffix?tab=overview", t)
r.Header.Set("user-agent",
"Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)")
return r
}(),
tinyTTL,
},
}
for _, test := range tests {
if got := detailsTTL(test.r); got != test.want {
t.Errorf("detailsTTL(%v) = %v, want %v", test.r, got, test.want)
}
}
}
func TestTagRoute(t *testing.T) {
mustRequest := func(url string) *http.Request {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Fatal(err)
}
return req
}
tests := []struct {
route string
req *http.Request
want string
}{
{"/pkg", mustRequest("http://localhost/pkg/foo?tab=versions"), "pkg-versions"},
{"/", mustRequest("http://localhost/foo?tab=imports"), "imports"},
{"/search", mustRequest("http://localhost/search?q=net&m=vuln"), "search-vuln"},
{"/search", mustRequest("http://localhost/search?q=net&m=package"), "search-package"},
{"/search", mustRequest("http://localhost/search?q=net&m=symbol"), "search-symbol"},
{"/search", mustRequest("http://localhost/search?q=net"), "search-package"},
}
for _, test := range tests {
t.Run(test.want, func(t *testing.T) {
if got := TagRoute(test.route, test.req); got != test.want {
t.Errorf("TagRoute(%q, %v) = %q, want %q", test.route, test.req, got, test.want)
}
})
}
}
func TestCheckTemplates(t *testing.T) {
// Perform additional checks on parsed templates.
staticFS := template.TrustedFSFromEmbed(static.FS)
templates, err := parsePageTemplates(staticFS)
if err != nil {
t.Fatal(err)
}
for _, c := range []struct {
name string
subs []string
typeval any
}{
{"badge", nil, badgePage{}},
// error.tmpl omitted because relies on an associated "message" template
// that's parsed on demand; see renderErrorPage above.
{"fetch", nil, page.ErrorPage{}},
{"homepage", nil, homepage{}},
{"license-policy", nil, licensePolicyPage{}},
{"search", nil, SearchPage{}},
{"search-help", nil, page.BasePage{}},
{"unit/main", nil, UnitPage{}},
{
"unit/main",
[]string{"unit-outline", "unit-readme", "unit-doc", "unit-files", "unit-directories"},
MainDetails{},
},
{"unit/importedby", nil, UnitPage{}},
{"unit/importedby", []string{"importedby"}, ImportedByDetails{}},
{"unit/imports", nil, UnitPage{}},
{"unit/imports", []string{"imports"}, ImportsDetails{}},
{"unit/licenses", nil, UnitPage{}},
{"unit/licenses", []string{"licenses"}, LicensesDetails{}},
{"unit/versions", nil, UnitPage{}},
{"unit/versions", []string{"versions"}, versions.VersionsDetails{}},
{"vuln", nil, page.BasePage{}},
{"vuln/list", nil, VulnListPage{}},
{"vuln/entry", nil, VulnEntryPage{}},
} {
t.Run(c.name, func(t *testing.T) {
tm := templates[c.name]
if tm == nil {
t.Fatalf("no template %q", c.name)
}
if c.subs == nil {
if err := templatecheck.CheckSafe(tm, c.typeval); err != nil {
t.Fatal(err)
}
} else {
for _, n := range c.subs {
s := tm.Lookup(n)
if s == nil {
t.Fatalf("no sub-template %q of %q", n, c.name)
}
if err := templatecheck.CheckSafe(s, c.typeval); err != nil {
t.Fatalf("%s: %v", n, err)
}
}
}
})
}
}
func TestStripScheme(t *testing.T) {
for _, test := range []struct {
url, want string
}{
{"http://github.com", "github.com"},
{"https://github.com/path/to/something", "github.com/path/to/something"},
{"example.com", "example.com"},
{"chrome-extension://abcd", "abcd"},
{"nonwellformed.com/path?://query=1", "query=1"},
} {
if got := stripScheme(test.url); got != test.want {
t.Errorf("%q: got %q, want %q", test.url, got, test.want)
}
}
}
func TestInstallFS(t *testing.T) {
s, handler, teardown := newTestServer(t, nil, nil)
defer teardown()
s.InstallFS("/dir", os.DirFS("."))
// Request this file.
w := httptest.NewRecorder()
handler.ServeHTTP(w, httptest.NewRequest("GET", "/files/dir/frontend_test.go", nil))
if w.Code != http.StatusOK {
t.Errorf("got status code = %d, want %d", w.Code, http.StatusOK)
}
if want := "TestInstallFS"; !strings.Contains(w.Body.String(), want) {
t.Errorf("body does not contain %q", want)
}
}