Files
golink/golink_test.go
T
Will NorrisandWill Norris f00de63b45 parse link short name just from request path
Previously, we were parsing from r.RequestURI, which includes both the
path and query string.  This causes problem for requests like go/who?q
which try to lookup a link named "who?q" rather than "who" (see #77).

For now, this just ignores the request query string. Eventually we
should probably retain the query string, but this begins by parsing out
the short name properly.

Updates #77

Signed-off-by: Will Norris <will@tailscale.com>
2023-05-16 10:17:35 -07:00

444 lines
11 KiB
Go

// Copyright 2022 Tailscale Inc & Contributors
// SPDX-License-Identifier: BSD-3-Clause
package golink
import (
"errors"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"golang.org/x/net/xsrftoken"
)
func init() {
// tests always need golink to be run in dev mode
*dev = ":8080"
}
func TestServeGo(t *testing.T) {
var err error
db, err = NewSQLiteDB(":memory:")
if err != nil {
t.Fatal(err)
}
db.Save(&Link{Short: "who", Long: "http://who/"})
db.Save(&Link{Short: "me", Long: "/who/{{.User}}"})
db.Save(&Link{Short: "invalid-var", Long: "/who/{{.Invalid}}"})
tests := []struct {
name string
link string
currentUser func(*http.Request) (string, error)
wantStatus int
wantLink string
}{
{
name: "simple link",
link: "/who",
wantStatus: http.StatusFound,
wantLink: "http://who/",
},
{
name: "simple link, anonymous request",
link: "/who",
currentUser: func(*http.Request) (string, error) { return "", nil },
wantStatus: http.StatusFound,
wantLink: "http://who/",
},
{
name: "simple link with path",
link: "/who/p",
wantStatus: http.StatusFound,
wantLink: "http://who/p",
},
{
name: "simple link with query",
link: "/who?q",
wantStatus: http.StatusFound,
wantLink: "http://who/", // TODO: eventually http://who/?q
},
{
name: "simple link with path and query",
link: "/who/p?q",
wantStatus: http.StatusFound,
wantLink: "http://who/p", // TODO: eventually http://who/p?q
},
{
name: "user link",
link: "/me",
wantStatus: http.StatusFound,
wantLink: "/who/foo@example.com",
},
{
name: "unknown link",
link: "/does-not-exist",
wantStatus: http.StatusNotFound,
},
{
name: "unknown variable",
link: "/invalid-var",
wantStatus: http.StatusInternalServerError,
},
{
name: "user link, anonymous request",
link: "/me",
currentUser: func(*http.Request) (string, error) { return "", nil },
wantStatus: http.StatusUnauthorized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.currentUser != nil {
oldCurrentUser := currentUser
currentUser = tt.currentUser
t.Cleanup(func() {
currentUser = oldCurrentUser
})
}
r := httptest.NewRequest("GET", tt.link, nil)
w := httptest.NewRecorder()
serveGo(w, r)
if w.Code != tt.wantStatus {
t.Errorf("serveGo(%q) = %d; want %d", tt.link, w.Code, tt.wantStatus)
}
if gotLink := w.Header().Get("Location"); gotLink != tt.wantLink {
t.Errorf("serveGo(%q) = %q; want %q", tt.link, gotLink, tt.wantLink)
}
})
}
}
func TestServeSave(t *testing.T) {
var err error
db, err = NewSQLiteDB(":memory:")
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
short string
long string
allowUnknownUsers bool
currentUser func(*http.Request) (string, error)
wantStatus int
}{
{
name: "missing short",
short: "",
long: "http://who/",
wantStatus: http.StatusBadRequest,
},
{
name: "missing long",
short: "",
long: "http://who/",
wantStatus: http.StatusBadRequest,
},
{
name: "save simple link",
short: "who",
long: "http://who/",
wantStatus: http.StatusOK,
},
{
name: "disallow editing another's link",
short: "who",
long: "http://who/",
currentUser: func(*http.Request) (string, error) { return "bar@example.com", nil },
wantStatus: http.StatusForbidden,
},
{
name: "disallow unknown users",
short: "who2",
long: "http://who/",
currentUser: func(*http.Request) (string, error) { return "", errors.New("") },
wantStatus: http.StatusInternalServerError,
},
{
name: "allow unknown users",
short: "who2",
long: "http://who/",
allowUnknownUsers: true,
currentUser: func(*http.Request) (string, error) { return "", nil },
wantStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.currentUser != nil {
oldCurrentUser := currentUser
currentUser = tt.currentUser
t.Cleanup(func() {
currentUser = oldCurrentUser
})
}
oldAllowUnknownUsers := *allowUnknownUsers
*allowUnknownUsers = tt.allowUnknownUsers
t.Cleanup(func() { *allowUnknownUsers = oldAllowUnknownUsers })
r := httptest.NewRequest("POST", "/", strings.NewReader(url.Values{
"short": {tt.short},
"long": {tt.long},
}.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
serveSave(w, r)
if w.Code != tt.wantStatus {
t.Errorf("serveSave(%q, %q) = %d; want %d", tt.short, tt.long, w.Code, tt.wantStatus)
}
})
}
}
func TestServeDelete(t *testing.T) {
var err error
db, err = NewSQLiteDB(":memory:")
if err != nil {
t.Fatal(err)
}
db.Save(&Link{Short: "a", Owner: "a@example.com"})
db.Save(&Link{Short: "foo", Owner: "foo@example.com"})
xsrf := func(short string) string {
return xsrftoken.Generate(xsrfKey, "foo@example.com", short)
}
tests := []struct {
name string
short string
xsrf string
currentUser func(*http.Request) (string, error)
wantStatus int
}{
{
name: "missing short",
short: "",
wantStatus: http.StatusBadRequest,
},
{
name: "non-existant link",
short: "does-not-exist",
wantStatus: http.StatusNotFound,
},
{
name: "unowned link",
short: "a",
wantStatus: http.StatusForbidden,
},
{
name: "invalid xsrf",
short: "foo",
xsrf: xsrf("invalid"),
wantStatus: http.StatusBadRequest,
},
{
name: "valid xsrf",
short: "foo",
xsrf: xsrf("foo"),
wantStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.currentUser != nil {
oldCurrentUser := currentUser
currentUser = tt.currentUser
t.Cleanup(func() {
currentUser = oldCurrentUser
})
}
r := httptest.NewRequest("POST", "/.delete/"+tt.short, strings.NewReader(url.Values{
"xsrf": {tt.xsrf},
}.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
serveDelete(w, r)
if w.Code != tt.wantStatus {
t.Errorf("serveDelete(%q) = %d; want %d", tt.short, w.Code, tt.wantStatus)
}
})
}
}
func TestExpandLink(t *testing.T) {
tests := []struct {
name string // test name
long string // long URL for golink
now time.Time // current time
user string // current user resolving link
remainder string // remainder of URL path after golink name
wantErr bool // whether we expect an error
want string // expected redirect URL
}{
{
name: "dont-mangle-escapes",
long: "http://host.com/foo%2f/bar",
want: "http://host.com/foo%2f/bar",
},
{
name: "dont-mangle-escapes-and-remainder",
long: "http://host.com/foo%2f/bar",
remainder: "extra",
want: "http://host.com/foo%2f/bar/extra",
},
{
name: "remainder-insert-slash",
long: "http://host.com/foo",
remainder: "extra",
want: "http://host.com/foo/extra",
},
{
name: "remainder-long-as-trailing-slash",
long: "http://host.com/foo/",
remainder: "extra",
want: "http://host.com/foo/extra",
},
{
name: "var-expansions-time",
long: `https://roamresearch.com/#/app/ts-corp/page/{{.Now.Format "01-02-2006"}}`,
want: "https://roamresearch.com/#/app/ts-corp/page/06-02-2022",
now: time.Date(2022, 06, 02, 1, 2, 3, 4, time.UTC),
},
{
name: "var-expansions-user",
long: `http://host.com/{{.User}}`,
user: "foo@example.com",
want: "http://host.com/foo@example.com",
},
{
name: "var-expansions-no-user",
long: `http://host.com/{{.User}}`,
wantErr: true,
},
{
name: "unknown-field",
long: `http://host.com/{{.Foo}}`,
wantErr: true,
},
{
name: "template-no-path",
long: "https://calendar.google.com/{{with .Path}}calendar/embed?mode=week&src={{.}}@tailscale.com{{end}}",
want: "https://calendar.google.com/",
},
{
name: "template-with-path",
long: "https://calendar.google.com/{{with .Path}}calendar/embed?mode=week&src={{.}}@tailscale.com{{end}}",
remainder: "amelie",
want: "https://calendar.google.com/calendar/embed?mode=week&src=amelie@tailscale.com",
},
{
name: "template-with-pathescape-func",
long: "http://host.com/{{PathEscape .Path}}",
remainder: "a/b",
want: "http://host.com/a%2Fb",
},
{
name: "template-with-queryescape-func",
long: "http://host.com/{{QueryEscape .Path}}",
remainder: "a+b",
want: "http://host.com/a%2Bb",
},
{
name: "template-with-trimsuffix-func",
long: `http://host.com/{{TrimSuffix .Path "/"}}`,
remainder: "a/",
want: "http://host.com/a",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := expandLink(tt.long, expandEnv{Now: tt.now, Path: tt.remainder, user: tt.user})
if (err != nil) != tt.wantErr {
t.Fatalf("expandLink(%q) returned error %v; want %v", tt.long, err, tt.wantErr)
}
if got != tt.want {
t.Errorf("expandLink(%q) = %q; want %q", tt.long, got, tt.want)
}
})
}
}
func TestResolveLink(t *testing.T) {
var err error
db, err = NewSQLiteDB(":memory:")
if err != nil {
t.Fatal(err)
}
db.Save(&Link{Short: "meet", Long: "https://meet.google.com/lookup/"})
db.Save(&Link{Short: "cs", Long: "http://codesearch/{{with .Path}}search?q={{.}}{{end}}"})
db.Save(&Link{Short: "m", Long: "http://go/meet"})
db.Save(&Link{Short: "chat", Long: "/meet"})
tests := []struct {
link string
want string
}{
{
link: "meet",
want: "https://meet.google.com/lookup/",
},
{
link: "meet/foo",
want: "https://meet.google.com/lookup/foo",
},
{
link: "go/meet/foo",
want: "https://meet.google.com/lookup/foo",
},
{
link: "http://go/meet/foo",
want: "https://meet.google.com/lookup/foo",
},
{
// if absolute URL provided, host doesn't actually matter
link: "http://mygo/meet/foo",
want: "https://meet.google.com/lookup/foo",
},
{
link: "cs",
want: "http://codesearch/",
},
{
link: "cs/term",
want: "http://codesearch/search?q=term",
},
{
// aliased go links with hostname
link: "m/foo",
want: "https://meet.google.com/lookup/foo",
},
{
// aliased go links without hostname
link: "chat/foo",
want: "https://meet.google.com/lookup/foo",
},
}
for _, tt := range tests {
name := "golink " + tt.link
t.Run(name, func(t *testing.T) {
got, err := resolveLink(tt.link)
if err != nil {
t.Error(err)
}
if got != tt.want {
t.Errorf("ResolveLink(%q) = %q; want %q", tt.link, got, tt.want)
}
})
}
}