blob: 2a048ad143581e9fc8fd9b9dc8be2c1b3600254b [file] [log] [blame]
// Copyright 2020 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 or at
// https://developers.google.com/open-source/licenses/bsd.
package main
import (
"bufio"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"github.com/golang/gddo/gddo-server/dynconfig"
"github.com/google/go-cmp/cmp"
)
func TestHandlePkgGoDevRedirect(t *testing.T) {
for _, test := range []struct {
name, url, wantLocationHeader, wantSetCookieHeader string
wantStatusCode int
cookie *http.Cookie
}{
{
name: "test pkggodev-redirect param is on",
url: "http://godoc.org/net/http?redirect=on",
wantLocationHeader: "https://pkg.go.dev/net/http?utm_source=godoc",
wantSetCookieHeader: "pkggodev-redirect=on; Path=/",
wantStatusCode: http.StatusFound,
},
{
name: "test pkggodev-redirect param is off",
url: "http://godoc.org/net/http?redirect=off",
wantLocationHeader: "",
wantSetCookieHeader: "pkggodev-redirect=; Path=/; Max-Age=0",
wantStatusCode: http.StatusOK,
},
{
name: "test pkggodev-redirect param is unset",
url: "http://godoc.org/net/http",
wantLocationHeader: "",
wantSetCookieHeader: "",
wantStatusCode: http.StatusOK,
},
{
name: "toggle enabled pkggodev-redirect cookie",
url: "http://godoc.org/net/http?redirect=off",
cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "true"},
wantLocationHeader: "",
wantSetCookieHeader: "pkggodev-redirect=; Path=/; Max-Age=0",
wantStatusCode: http.StatusOK,
},
{
name: "pkggodev-redirect enabled cookie should redirect",
url: "http://godoc.org/net/http",
cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
wantLocationHeader: "https://pkg.go.dev/net/http?utm_source=godoc",
wantSetCookieHeader: "",
wantStatusCode: http.StatusFound,
},
{
name: "do not redirect if user is returning from pkg.go.dev",
url: "http://godoc.org/net/http?utm_source=backtogodoc",
cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
wantStatusCode: http.StatusOK,
},
} {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest("GET", test.url, nil)
if test.cookie != nil {
req.AddCookie(test.cookie)
}
handler := pkgGoDevRedirectHandler(nil, func(w http.ResponseWriter, r *http.Request) error {
return nil
})
w := httptest.NewRecorder()
err := handler(w, req)
if err != nil {
t.Fatal(err)
}
resp := w.Result()
if got, want := resp.Header.Get("Location"), test.wantLocationHeader; got != want {
t.Errorf("Location header mismatch: got %q; want %q", got, want)
}
if got, want := resp.Header.Get("Set-Cookie"), test.wantSetCookieHeader; got != want {
t.Errorf("Set-Cookie header mismatch: got %q; want %q", got, want)
}
if got, want := resp.StatusCode, test.wantStatusCode; got != want {
t.Errorf("Status code mismatch: got %d; want %d", got, want)
}
})
}
}
func TestPkgGoDevURL(t *testing.T) {
testCases := []struct {
from, to string
}{
{
from: "https://godoc.org",
to: "https://pkg.go.dev?utm_source=godoc",
},
{
from: "https://godoc.org/-/about",
to: "https://pkg.go.dev/about?utm_source=godoc",
},
{
from: "https://godoc.org/-/go",
to: "https://pkg.go.dev/std?utm_source=godoc",
},
{
from: "https://godoc.org/-/subrepo",
to: "https://pkg.go.dev/search?q=golang.org%2Fx&utm_source=godoc",
},
{
from: "https://godoc.org/C",
to: "https://pkg.go.dev/C?utm_source=godoc",
},
{
from: "https://godoc.org/?q=foo",
to: "https://pkg.go.dev/search?q=foo&utm_source=godoc",
},
{
from: "https://godoc.org/cloud.google.com/go/storage",
to: "https://pkg.go.dev/cloud.google.com/go/storage?utm_source=godoc",
},
{
from: "https://godoc.org/cloud.google.com/go/storage?imports",
to: "https://pkg.go.dev/cloud.google.com/go/storage?tab=imports&utm_source=godoc",
},
{
from: "https://godoc.org/cloud.google.com/go/storage?importers",
to: "https://pkg.go.dev/cloud.google.com/go/storage?tab=importedby&utm_source=godoc",
},
{
from: "https://godoc.org/cloud.google.com/go/storage?status.svg",
to: "https://pkg.go.dev/badge/cloud.google.com/go/storage?utm_source=godoc",
},
{
from: "https://godoc.org/cloud.google.com/go/storage?status.png",
to: "https://pkg.go.dev/badge/cloud.google.com/go/storage?utm_source=godoc",
},
{
from: "https://godoc.org/github.com/golang/go",
to: "https://pkg.go.dev/std?utm_source=godoc",
},
{
from: "https://godoc.org/github.com/golang/go/src",
to: "https://pkg.go.dev/std?utm_source=godoc",
},
{
from: "https://godoc.org/github.com/golang/go/src/cmd/vet",
to: "https://pkg.go.dev/cmd/vet?utm_source=godoc",
},
{
from: "https://godoc.org/golang.org/x/vgo/vendor/cmd/go/internal/modfile",
to: "https://pkg.go.dev/?utm_source=godoc",
},
{
from: "https://godoc.org/golang.org/x/vgo/vendor",
to: "https://pkg.go.dev/?utm_source=godoc",
},
{
from: "https://godoc.org/cryptoscope.co/go/specialκ",
to: "https://golang.org/issue/43036",
},
{
from: "https://godoc.org/github.com/badimportpath//doubleslash",
to: "https://pkg.go.dev/github.com/badimportpath//doubleslash?utm_source=godoc",
},
{
from: "https://godoc.org/github.com/google/go-containerregistry/",
to: "https://pkg.go.dev/github.com/google/go-containerregistry?utm_source=godoc",
},
}
for _, tc := range testCases {
t.Run(strings.ReplaceAll(tc.from, "/", " "), func(t *testing.T) {
u, err := url.Parse(tc.from)
if err != nil {
t.Fatalf("url.Parse(%q): %v", tc.from, err)
}
to := pkgGoDevURL(u)
if got, want := to.String(), tc.to; got != want {
t.Errorf("pkgGoDevURL(%q) = %q; want %q", u, got, want)
}
})
}
}
func TestNewGDDOEvent(t *testing.T) {
for _, test := range []struct {
name string
url string
cookie *http.Cookie
want *gddoEvent
}{
{
name: "home page request",
url: "https://godoc.org",
want: &gddoEvent{
Host: "godoc.org",
Path: "",
UsePkgGoDev: false,
},
},
{
name: "home page request with cookie on should redirect",
url: "https://godoc.org",
cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
want: &gddoEvent{
Host: "godoc.org",
Path: "",
UsePkgGoDev: true,
},
},
{
name: "about page request",
url: "https://godoc.org/-/about",
want: &gddoEvent{
Host: "godoc.org",
Path: "/-/about",
UsePkgGoDev: false,
},
},
{
name: "request with search query parameter",
url: "https://godoc.org/?q=test",
want: &gddoEvent{
Host: "godoc.org",
Path: "/",
UsePkgGoDev: false,
},
},
{
name: "package page request",
url: "https://godoc.org/net/http",
want: &gddoEvent{
Host: "godoc.org",
Path: "/net/http",
UsePkgGoDev: false,
},
},
{
name: "package page request with wrong cookie on should not redirect",
url: "https://godoc.org/net/http",
cookie: &http.Cookie{Name: "bogus-cookie", Value: "on"},
want: &gddoEvent{
Host: "godoc.org",
Path: "/net/http",
UsePkgGoDev: false,
},
},
{
name: "package page request with query parameter off should not redirect",
url: "https://godoc.org/net/http?redirect=off",
cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
want: &gddoEvent{
Host: "godoc.org",
Path: "/net/http",
UsePkgGoDev: false,
},
},
{
name: "package page request with cookie on should redirect",
url: "https://godoc.org/net/http",
cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
want: &gddoEvent{
Host: "godoc.org",
Path: "/net/http",
UsePkgGoDev: true,
},
},
{
name: "package page request with query parameter on should redirect",
url: "https://godoc.org/net/http?redirect=on",
cookie: &http.Cookie{Name: "pkggodev-redirect", Value: ""},
want: &gddoEvent{
Host: "godoc.org",
Path: "/net/http",
UsePkgGoDev: true,
},
},
{
name: "api request",
url: "https://api.godoc.org/imports/net/http",
want: &gddoEvent{
Host: "api.godoc.org",
Path: "/imports/net/http",
UsePkgGoDev: false,
},
},
{
name: "api requests should never redirect, even with cookie on",
url: "https://api.godoc.org/imports/net/http",
cookie: &http.Cookie{Name: "pkggodev-redirect", Value: "on"},
want: &gddoEvent{
Host: "api.godoc.org",
Path: "/imports/net/http",
UsePkgGoDev: false,
},
},
{
name: "api requests should never redirect, even with query parameter on",
url: "https://api.godoc.org/imports/net/http?redirect=on",
cookie: &http.Cookie{Name: "pkggodev-redirect", Value: ""},
want: &gddoEvent{
Host: "api.godoc.org",
Path: "/imports/net/http",
UsePkgGoDev: false,
},
},
} {
t.Run(test.name, func(t *testing.T) {
want := test.want
want.Latency = 100
want.URL = test.url
want.Header = http.Header{}
want.IsRobot = true
r := httptest.NewRequest("GET", test.url, nil)
if test.cookie != nil {
r.AddCookie(test.cookie)
want.Header.Add("Cookie", test.cookie.String())
}
got := newGDDOEvent(r, want.Latency, want.IsRobot, http.StatusOK)
want.Status = http.StatusOK
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestNewGDDOEventFromRequests(t *testing.T) {
for _, test := range []struct {
name string
requestURI string
host string
want *gddoEvent
}{
{
name: "absolute index path",
requestURI: "https://godoc.org",
host: "godoc.org",
want: &gddoEvent{
Host: "godoc.org",
Path: "",
URL: "https://godoc.org",
},
},
{
name: "absolute index path with trailing slash",
requestURI: "https://godoc.org/",
host: "godoc.org",
want: &gddoEvent{
Host: "godoc.org",
Path: "/",
URL: "https://godoc.org/",
},
},
{
name: "relative index path",
requestURI: "/",
host: "godoc.org",
want: &gddoEvent{
Host: "godoc.org",
Path: "/",
URL: "https://godoc.org/",
},
},
{
name: "absolute about path",
requestURI: "https://godoc.org/-/about",
host: "godoc.org",
want: &gddoEvent{
Host: "godoc.org",
Path: "/-/about",
URL: "https://godoc.org/-/about",
},
},
{
name: "relative about path",
requestURI: "/-/about",
host: "godoc.org",
want: &gddoEvent{
Host: "godoc.org",
Path: "/-/about",
URL: "https://godoc.org/-/about",
},
},
{
name: "absolute package path",
requestURI: "https://godoc.org/net/http",
host: "godoc.org",
want: &gddoEvent{
Host: "godoc.org",
Path: "/net/http",
URL: "https://godoc.org/net/http",
},
},
{
name: "relative package path",
requestURI: "/net/http",
host: "godoc.org",
want: &gddoEvent{
Host: "godoc.org",
Path: "/net/http",
URL: "https://godoc.org/net/http",
},
},
{
name: "absolute path with query parameters",
requestURI: "https://godoc.org/net/http?q=test",
host: "godoc.org",
want: &gddoEvent{
Host: "godoc.org",
Path: "/net/http",
URL: "https://godoc.org/net/http?q=test",
},
},
{
name: "relative path with query parameters",
requestURI: "/net/http?q=test",
host: "godoc.org",
want: &gddoEvent{
Host: "godoc.org",
Path: "/net/http",
URL: "https://godoc.org/net/http?q=test",
},
},
{
name: "absolute api path",
requestURI: "https://api.godoc.org/imports/net/http",
host: "api.godoc.org",
want: &gddoEvent{
Host: "api.godoc.org",
Path: "/imports/net/http",
URL: "https://api.godoc.org/imports/net/http",
},
},
{
name: "relative api path",
requestURI: "/imports/net/http",
host: "api.godoc.org",
want: &gddoEvent{
Host: "api.godoc.org",
Path: "/imports/net/http",
URL: "https://api.godoc.org/imports/net/http",
},
},
} {
t.Run(test.name, func(t *testing.T) {
want := test.want
want.Latency = 100
want.Header = http.Header{}
want.IsRobot = false
requestLine := "GET " + test.requestURI + " HTTP/1.1\r\nHost: " + test.host + "\r\n\r\n"
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(requestLine)))
if err != nil {
t.Fatal("invalid NewRequest arguments; " + err.Error())
}
got := newGDDOEvent(req, want.Latency, want.IsRobot, http.StatusOK)
want.Status = http.StatusOK
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestShouldTeeRequest(t *testing.T) {
for _, test := range []struct {
urlPath string
want bool
}{
{"/", true},
{"/-/about", true},
{"/net/http", true},
{"/_ah/ready", false},
{"/_ah/warmup", false},
{"/-/bootstrap.min.css", false},
{"/-/bootstrap.min.js", false},
{"/-/bot", false},
{"/-/jquery-2.0.3.min.js", false},
{"/-/refresh", false},
{"/-/sidebar.css", false},
{"/-/site.css", false},
{"/-/site.js", false},
{"/BingSiteAuth.xml", false},
{"/google3d2f3cd4cc2bb44b.html", false},
{"/humans.txt", false},
{"/robots.txt", false},
{"/third_party/jquery.timeago.js", false},
} {
if got := shouldTeeRequest(test.urlPath); got != test.want {
t.Errorf("shouldTeeRequest(%q): %t; want %t", test.urlPath, got, test.want)
}
}
}
func TestShouldRedirectURLForSnapshot(t *testing.T) {
var testRequests []*http.Request
for _, tu := range []string{
"https://api.godoc.org/-/about",
"https://api.godoc.org/?q=http",
"https://api.godoc.org/-/net/http",
"https://godoc.org",
"https://godoc.org/-/about",
"https://godoc.org/-/aboutfoo",
"https://godoc.org/-/about/foobar",
"https://godoc.org/-/bot",
"https://godoc.org/-/go",
"https://godoc.org/-/subrepo",
"https://godoc.org/?q=http",
"https://godoc.org/cmd",
"https://godoc.org/net/http",
"https://godoc.org/cloud.google.com/go",
"https://godoc.org/cloud.google.com/go/pubsub",
"https://godoc.org/cloud.google.com/go/storage",
"https://godoc.org/cloud.google.com/go/storage/internal",
"https://godoc.org/github.com/my/module",
"https://godoc.org/github.com/my/module/package",
} {
u, err := url.Parse(tu)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest("GET", u.String(), nil)
req.URL = u
req.Form = u.Query()
testRequests = append(testRequests, req)
}
for _, test := range []struct {
name string
snapshot *dynconfig.DynamicConfig
redirect map[string]bool
}{
{
name: "redirect homepage",
snapshot: &dynconfig.DynamicConfig{
RedirectHomepage: true,
},
redirect: map[string]bool{"https://godoc.org": true},
},
{
name: "redirect search page",
snapshot: &dynconfig.DynamicConfig{
RedirectSearch: true,
},
redirect: map[string]bool{"https://godoc.org/?q=http": true},
},
{
name: "redirect static page",
snapshot: &dynconfig.DynamicConfig{
RedirectPaths: []string{"/-/about", "/-/subrepo", "/-/go"},
},
redirect: map[string]bool{
"https://godoc.org/-/about": true,
"https://godoc.org/-/go": true,
"https://godoc.org/-/subrepo": true,
},
},
{
name: "redirect stdlib packages",
snapshot: &dynconfig.DynamicConfig{
RedirectStdlib: true,
},
redirect: map[string]bool{
"https://godoc.org/net/http": true,
"https://godoc.org/cmd": true,
},
},
{
name: "redirect cloud.google.com/go",
snapshot: &dynconfig.DynamicConfig{
RedirectPaths: []string{"/cloud.google.com/go"},
},
redirect: map[string]bool{
"https://godoc.org/cloud.google.com/go": true,
"https://godoc.org/cloud.google.com/go/pubsub": true,
"https://godoc.org/cloud.google.com/go/storage": true,
"https://godoc.org/cloud.google.com/go/storage/internal": true,
},
},
} {
t.Run(test.name, func(t *testing.T) {
for _, req := range testRequests {
u := req.URL.String()
got := shouldRedirectURLForSnapshot(req, test.snapshot)
if test.redirect[u] && !got {
t.Errorf("%q should redirect but didn't", u)
}
if !test.redirect[u] && got {
t.Errorf("%q should not redirect and did", u)
}
}
})
}
}
func TestShouldRedirectURLForSnapshot_RolloutPercentage(t *testing.T) {
checkRollout := func(t *testing.T, paths []string, rollout uint, want uint) {
t.Helper()
var inExperiment int
for _, p := range paths {
req, err := http.NewRequest("GET", "http://godoc.org/"+p, nil)
if err != nil {
t.Fatal(err)
}
snapshot := &dynconfig.DynamicConfig{RedirectRollout: rollout}
if shouldRedirectURLForSnapshot(req, snapshot) {
inExperiment++
}
}
if rollout == 0 {
if inExperiment != 0 {
t.Fatalf("rollout is 0 and inExperiment = %d; want = 0", inExperiment)
}
return
}
got := uint(100 * inExperiment / len(paths))
if got != want {
t.Errorf("rollout = %d; want = %d", got, want)
}
}
var paths []string
for host := range vcsHostsWithThreeElementRepoName {
for i := 0; i < 1000; i++ {
p := host + "/" + strconv.Itoa(i) + "/foo"
paths = append(paths, p)
}
}
pathsWithCustomHost := paths
for i := 0; i < 1000; i++ {
pathsWithCustomHost = append(pathsWithCustomHost, "mymodule.com/"+strconv.Itoa(i))
}
for _, rollout := range []uint{0, 33, 47, 50, 53, 75, 100} {
t.Run(fmt.Sprintf("%d", rollout), func(t *testing.T) {
checkRollout(t, paths, rollout, rollout)
})
t.Run(fmt.Sprintf("customhost %d", rollout), func(t *testing.T) {
// Map of rollout set to expected rollout percentage. Numbers are
// skewed because all my.module.com/<i> paths are added, and they
// will not be opted in.
want := map[uint]uint{0: 0, 33: 28, 47: 40, 50: 42, 53: 45, 75: 64, 100: 100}[rollout]
checkRollout(t, pathsWithCustomHost, rollout, want)
})
}
}