blob: 33499261137dc9f8ddb700e1e3acdea5f7152ddd [file] [log] [blame]
// Copyright 2014 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 gosrc
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"path"
"regexp"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
var testWeb = map[string]string{
// Package at root of a GitHub repo.
"https://alice.org/pkg": `<head> <meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg"></head>`,
// Package in sub-directory.
"https://alice.org/pkg/sub": `<head> <meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg"><body>`,
// Fallback to http.
"http://alice.org/pkg/http": `<head> <meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">`,
// Meta tag in sub-directory does not match meta tag at root.
"https://alice.org/pkg/mismatch": `<head> <meta name="go-import" content="alice.org/pkg hg https://github.com/alice/pkg">`,
// More than one matching meta tag.
"http://alice.org/pkg/multiple": `<head> ` +
`<meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">` +
`<meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">`,
// Package with go-source meta tag.
"https://alice.org/pkg/source": `<head>` +
`<meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">` +
`<meta name="go-source" content="alice.org/pkg http://alice.org/pkg http://alice.org/pkg{/dir} http://alice.org/pkg{/dir}?f={file}#Line{line}">`,
"https://alice.org/pkg/ignore": `<head>` +
`<title>Hello</title>` +
// Unknown meta name
`<meta name="go-junk" content="alice.org/pkg http://alice.org/pkg http://alice.org/pkg{/dir} http://alice.org/pkg{/dir}?f={file}#Line{line}">` +
// go-source before go-meta
`<meta name="go-source" content="alice.org/pkg http://alice.org/pkg http://alice.org/pkg{/dir} http://alice.org/pkg{/dir}?f={file}#Line{line}">` +
// go-import tag for the package
`<meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">` +
// go-import with wrong number of fields
`<meta name="go-import" content="alice.org/pkg https://github.com/alice/pkg">` +
// go-import with no fields
`<meta name="go-import" content="">` +
// go-source with wrong number of fields
`<meta name="go-source" content="alice.org/pkg blah">` +
// meta tag for a different package
`<meta name="go-import" content="alice.org/other git https://github.com/alice/other">` +
// meta tag for a different package
`<meta name="go-import" content="alice.org/other git https://github.com/alice/other">` +
`</head>` +
// go-import outside of head
`<meta name="go-import" content="alice.org/pkg git https://github.com/alice/pkg">`,
// Package at root of a Git repo.
"https://bob.com/pkg": `<head> <meta name="go-import" content="bob.com/pkg git https://vcs.net/bob/pkg.git">`,
// Package at in sub-directory of a Git repo.
"https://bob.com/pkg/sub": `<head> <meta name="go-import" content="bob.com/pkg git https://vcs.net/bob/pkg.git">`,
// Package with go-source meta tag.
"https://bob.com/pkg/source": `<head>` +
`<meta name="go-import" content="bob.com/pkg git https://vcs.net/bob/pkg.git">` +
`<meta name="go-source" content="bob.com/pkg http://bob.com/pkg http://bob.com/pkg{/dir}/ http://bob.com/pkg{/dir}/?f={file}#Line{line}">`,
// Meta refresh to godoc.org
"http://rsc.io/benchstat": `<!DOCTYPE html><html><head>` +
`<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>` +
`<meta name="go-import" content="rsc.io/benchstat git https://github.com/rsc/benchstat">` +
`<meta http-equiv="refresh" content="0; url=https://godoc.org/rsc.io/benchstat">` +
`</head>`,
// Package with go-source meta tag, where {file} appears on the right of '#' in the file field URL template.
"https://azul3d.org/examples": `<!DOCTYPE html><html><head>` +
`<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>` +
`<meta name="go-import" content="azul3d.org/examples git https://github.com/azul3d/examples">` +
`<meta name="go-source" content="azul3d.org/examples https://github.com/azul3d/examples https://gotools.org/azul3d.org/examples{/dir} https://gotools.org/azul3d.org/examples{/dir}#{file}-L{line}">` +
`<meta http-equiv="refresh" content="0; url=https://godoc.org/azul3d.org/examples">` +
`</head>`,
"https://azul3d.org/examples/abs": `<!DOCTYPE html><html><head>` +
`<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>` +
`<meta name="go-import" content="azul3d.org/examples git https://github.com/azul3d/examples">` +
`<meta name="go-source" content="azul3d.org/examples https://github.com/azul3d/examples https://gotools.org/azul3d.org/examples{/dir} https://gotools.org/azul3d.org/examples{/dir}#{file}-L{line}">` +
`<meta http-equiv="refresh" content="0; url=https://godoc.org/azul3d.org/examples/abs">` +
`</head>`,
// Multiple go-import meta tags; one of which is a vgo-special mod vcs type
"http://myitcv.io/blah2": `<!DOCTYPE html><html><head>` +
`<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>` +
`<meta name="go-import" content="myitcv.io/blah2 git https://github.com/myitcv/x">` +
`<meta name="go-import" content="myitcv.io/blah2 mod https://raw.githubusercontent.com/myitcv/pubx/master">` +
`<meta name="go-source" content="myitcv.io https://github.com/myitcv/x/wiki https://github.com/myitcv/x/tree/master{/dir} https://github.com/myitcv/x/blob/master{/dir}/{file}#L{line}">` +
`</head>`,
// The repo element of go-import includes "../"
"http://my.host/pkg": `<head> <meta name="go-import" content="my.host/pkg git http://vcs.net/myhost/../../tmp/pkg.git"></head>`,
}
var getDynamicTests = []struct {
importPath string
dir *Directory
}{
{"alice.org/pkg", &Directory{
BrowseURL: "https://github.com/alice/pkg",
ImportPath: "alice.org/pkg",
LineFmt: "%s#L%d",
ProjectName: "pkg",
ProjectRoot: "alice.org/pkg",
ProjectURL: "https://alice.org/pkg",
ResolvedPath: "github.com/alice/pkg",
VCS: "git",
Files: []*File{{Name: "main.go", BrowseURL: "https://github.com/alice/pkg/blob/master/main.go"}},
}},
{"alice.org/pkg/sub", &Directory{
BrowseURL: "https://github.com/alice/pkg/tree/master/sub",
ImportPath: "alice.org/pkg/sub",
LineFmt: "%s#L%d",
ProjectName: "pkg",
ProjectRoot: "alice.org/pkg",
ProjectURL: "https://alice.org/pkg",
ResolvedPath: "github.com/alice/pkg/sub",
VCS: "git",
Files: []*File{{Name: "main.go", BrowseURL: "https://github.com/alice/pkg/blob/master/sub/main.go"}},
}},
{"alice.org/pkg/http", &Directory{
BrowseURL: "https://github.com/alice/pkg/tree/master/http",
ImportPath: "alice.org/pkg/http",
LineFmt: "%s#L%d",
ProjectName: "pkg",
ProjectRoot: "alice.org/pkg",
ProjectURL: "https://alice.org/pkg",
ResolvedPath: "github.com/alice/pkg/http",
VCS: "git",
Files: []*File{{Name: "main.go", BrowseURL: "https://github.com/alice/pkg/blob/master/http/main.go"}},
}},
{"alice.org/pkg/source", &Directory{
BrowseURL: "http://alice.org/pkg/source",
ImportPath: "alice.org/pkg/source",
LineFmt: "%s#Line%d",
ProjectName: "pkg",
ProjectRoot: "alice.org/pkg",
ProjectURL: "http://alice.org/pkg",
ResolvedPath: "github.com/alice/pkg/source",
VCS: "git",
Files: []*File{{Name: "main.go", BrowseURL: "http://alice.org/pkg/source?f=main.go"}},
}},
{"alice.org/pkg/ignore", &Directory{
BrowseURL: "http://alice.org/pkg/ignore",
ImportPath: "alice.org/pkg/ignore",
LineFmt: "%s#Line%d",
ProjectName: "pkg",
ProjectRoot: "alice.org/pkg",
ProjectURL: "http://alice.org/pkg",
ResolvedPath: "github.com/alice/pkg/ignore",
VCS: "git",
Files: []*File{{Name: "main.go", BrowseURL: "http://alice.org/pkg/ignore?f=main.go"}},
}},
{"alice.org/pkg/mismatch", nil},
{"alice.org/pkg/multiple", nil},
{"alice.org/pkg/notfound", nil},
{"bob.com/pkg", &Directory{
ImportPath: "bob.com/pkg",
ProjectName: "pkg",
ProjectRoot: "bob.com/pkg",
ProjectURL: "https://bob.com/pkg",
ResolvedPath: "vcs.net/bob/pkg.git",
VCS: "git",
Files: []*File{{Name: "main.go"}},
}},
{"bob.com/pkg/sub", &Directory{
ImportPath: "bob.com/pkg/sub",
ProjectName: "pkg",
ProjectRoot: "bob.com/pkg",
ProjectURL: "https://bob.com/pkg",
ResolvedPath: "vcs.net/bob/pkg.git/sub",
VCS: "git",
Files: []*File{{Name: "main.go"}},
}},
{"bob.com/pkg/source", &Directory{
BrowseURL: "http://bob.com/pkg/source/",
ImportPath: "bob.com/pkg/source",
LineFmt: "%s#Line%d",
ProjectName: "pkg",
ProjectRoot: "bob.com/pkg",
ProjectURL: "http://bob.com/pkg",
ResolvedPath: "vcs.net/bob/pkg.git/source",
VCS: "git",
Files: []*File{{Name: "main.go", BrowseURL: "http://bob.com/pkg/source/?f=main.go"}},
}},
{"rsc.io/benchstat", &Directory{
BrowseURL: "https://github.com/rsc/benchstat",
ImportPath: "rsc.io/benchstat",
LineFmt: "%s#L%d",
ProjectName: "benchstat",
ProjectRoot: "rsc.io/benchstat",
ProjectURL: "https://github.com/rsc/benchstat",
ResolvedPath: "github.com/rsc/benchstat",
VCS: "git",
Files: []*File{{Name: "main.go", BrowseURL: "https://github.com/rsc/benchstat/blob/master/main.go"}},
}},
{"azul3d.org/examples/abs", &Directory{
BrowseURL: "https://gotools.org/azul3d.org/examples/abs",
ImportPath: "azul3d.org/examples/abs",
LineFmt: "%s-L%d",
ProjectName: "examples",
ProjectRoot: "azul3d.org/examples",
ProjectURL: "https://github.com/azul3d/examples",
ResolvedPath: "github.com/azul3d/examples/abs",
VCS: "git",
Files: []*File{{Name: "main.go", BrowseURL: "https://gotools.org/azul3d.org/examples/abs#main.go"}},
}},
{"myitcv.io/blah2", &Directory{
BrowseURL: "https://github.com/myitcv/x",
ImportPath: "myitcv.io/blah2",
LineFmt: "%s#L%d",
ProjectName: "blah2",
ProjectRoot: "myitcv.io/blah2",
ProjectURL: "http://myitcv.io/blah2",
ResolvedPath: "github.com/myitcv/x",
VCS: "git",
Files: []*File{{Name: "main.go", BrowseURL: "https://github.com/myitcv/x/blob/master/main.go"}},
}},
{"my.host/pkg", nil},
}
type testTransport map[string]string
func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
statusCode := http.StatusOK
req.URL.RawQuery = ""
body, ok := t[req.URL.String()]
if !ok {
statusCode = http.StatusNotFound
}
resp := &http.Response{
StatusCode: statusCode,
Body: ioutil.NopCloser(strings.NewReader(body)),
}
return resp, nil
}
var githubPattern = regexp.MustCompile(`^github\.com/(?P<owner>[a-z0-9A-Z_.\-]+)/(?P<repo>[a-z0-9A-Z_.\-]+)(?P<dir>/[a-z0-9A-Z_.\-/]*)?$`)
func testGet(ctx context.Context, client *http.Client, match map[string]string, etag string) (*Directory, error) {
importPath := match["importPath"]
if m := githubPattern.FindStringSubmatch(importPath); m != nil {
browseURL := fmt.Sprintf("https://github.com/%s/%s", m[1], m[2])
if m[3] != "" {
browseURL = fmt.Sprintf("%s/tree/master%s", browseURL, m[3])
}
return &Directory{
BrowseURL: browseURL,
ImportPath: importPath,
LineFmt: "%s#L%d",
ProjectName: m[2],
ProjectRoot: fmt.Sprintf("github.com/%s/%s", m[1], m[2]),
ProjectURL: fmt.Sprintf("https://github.com/%s/%s", m[1], m[2]),
VCS: "git",
Files: []*File{{
Name: "main.go",
BrowseURL: fmt.Sprintf("https://github.com/%s/%s/blob/master%s/main.go", m[1], m[2], m[3]),
}},
}, nil
}
if strings.HasPrefix(match["repo"], "vcs.net") {
return &Directory{
ImportPath: importPath,
ProjectName: path.Base(match["repo"]),
ProjectRoot: fmt.Sprintf("%s.%s", match["repo"], match["vcs"]),
VCS: match["vcs"],
Files: []*File{{Name: "main.go"}},
}, nil
}
return nil, errNoMatch
}
func TestGetDynamic(t *testing.T) {
savedServices := services
savedGetVCSDirFn := getVCSDirFn
defer func() {
services = savedServices
getVCSDirFn = savedGetVCSDirFn
}()
services = []*service{{pattern: regexp.MustCompile(".*"), get: testGet}}
getVCSDirFn = testGet
client := &http.Client{Transport: testTransport(testWeb)}
for _, tt := range getDynamicTests {
dir, err := getDynamic(context.Background(), client, tt.importPath, "")
if tt.dir == nil {
if err == nil {
t.Errorf("getDynamic(ctx, client, %q, etag) did not return expected error", tt.importPath)
}
continue
}
if err != nil {
t.Errorf("getDynamic(ctx, client, %q, etag) return unexpected error: %v", tt.importPath, err)
continue
}
if !cmp.Equal(dir, tt.dir) {
t.Errorf("getDynamic(client, %q, etag) =\n %+v,\nwant %+v", tt.importPath, dir, tt.dir)
for i, f := range dir.Files {
var want *File
if i < len(tt.dir.Files) {
want = tt.dir.Files[i]
}
t.Errorf("file %d = %+v, want %+v", i, f, want)
}
}
}
}
// TestMaybeRedirect tests that MaybeRedirect redirects
// and does not redirect as expected, in various situations.
// See https://github.com/golang/gddo/issues/507
// and https://github.com/golang/gddo/issues/579.
func TestMaybeRedirect(t *testing.T) {
type repo struct {
ImportComment string
ResolvedGitHubPath string
}
// robpike.io/ivy package.
// Vanity import path, hosted on GitHub, with import comment.
ivy := repo{
ImportComment: "robpike.io/ivy",
ResolvedGitHubPath: "github.com/robpike/ivy",
}
// go4.org/sort package.
// Vanity import path, hosted on GitHub, without import comment.
go4sort := repo{
ResolvedGitHubPath: "github.com/go4org/go4/sort",
}
// github.com/teamwork/validate package.
// Hosted on GitHub, with import comment that doesn't match canonical GitHub case.
// See issue https://github.com/golang/gddo/issues/507.
gtv := repo{
ImportComment: "github.com/teamwork/validate",
ResolvedGitHubPath: "github.com/Teamwork/validate", // Note that this differs from import comment.
}
tests := []struct {
name string
repo repo
requestPath string
wantRedirect string // Empty string means no redirect.
}{
// ivy.
{
repo: ivy, name: "ivy repo: access canonical path -> no redirect",
requestPath: "robpike.io/ivy",
},
{
repo: ivy, name: "ivy repo: access GitHub path -> redirect to import comment",
requestPath: "github.com/robpike/ivy",
wantRedirect: "robpike.io/ivy",
},
{
repo: ivy, name: "ivy repo: access GitHub path with weird casing -> redirect to import comment",
requestPath: "github.com/RoBpIkE/iVy",
wantRedirect: "robpike.io/ivy",
},
// go4sort.
{
repo: go4sort, name: "go4sort repo: access canonical path -> no redirect",
requestPath: "go4.org/sort",
},
{
repo: go4sort, name: "go4sort repo: access GitHub path -> no redirect",
requestPath: "github.com/go4org/go4/sort",
},
{
repo: go4sort, name: "go4sort repo: access GitHub path with weird casing -> redirect to resolved GitHub case",
requestPath: "github.com/gO4oRg/Go4/sort",
wantRedirect: "github.com/go4org/go4/sort",
},
// gtv.
{
repo: gtv, name: "gtv repo: access canonical path -> no redirect",
requestPath: "github.com/teamwork/validate",
},
{
repo: gtv, name: "gtv repo: access canonical GitHub path -> redirect to import comment",
requestPath: "github.com/Teamwork/validate",
wantRedirect: "github.com/teamwork/validate",
},
{
repo: gtv, name: "gtv repo: access GitHub path with weird casing -> redirect to import comment",
requestPath: "github.com/tEaMwOrK/VaLiDaTe",
wantRedirect: "github.com/teamwork/validate",
},
}
for _, tt := range tests {
var want error
if tt.wantRedirect != "" {
want = NotFoundError{
Message: "not at canonical import path",
Redirect: tt.wantRedirect,
}
}
got := MaybeRedirect(tt.requestPath, tt.repo.ImportComment, tt.repo.ResolvedGitHubPath)
if got != want {
t.Errorf("%s: got error %v, want %v", tt.name, got, want)
}
}
}