Files
tiny-proxy/internal/goproxy/handler_test.go
MRLokopandClaude Opus 5.5 42abe48da1 Add a Go module proxy with per-module routing rules
A go vrepo serves the GOPROXY protocol at /go/<vrepo>/. Its upstreams
are ordered rules matched by module path (GOPRIVATE-style patterns);
each one relays to another module proxy, or builds .info/.mod/.zip,
@v/list and @latest itself from a git repository: through the Gitea
or GitLab REST API, or over git smart-HTTP (go-git) for any other
host, without a git binary. Module layout follows the go command:
<dir>/vX.Y.Z tags, /vN major subdirectories, +incompatible, nested
modules and vendor/ left out of zips. go.mod is served as committed,
so forks keeping their upstream module path work through replace.

Repo templates map captured pattern elements to repository paths
({1}, {2}...). vanity_hosts answers ?go-get=1 with a go-import meta
tag pointing at this proxy or at the repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:53:25 +03:00

661 lines
22 KiB
Go

package goproxy
import (
"archive/zip"
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"slices"
"sort"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"tiny-proxy/internal/config"
"tiny-proxy/internal/proxycache"
"tiny-proxy/internal/storage"
)
// fakeCommit is one commit of a fake repository: its full file set.
type fakeCommit struct {
hash string
time time.Time
files map[string]string
}
// fakeRepo is a repository a fake forge serves.
type fakeRepo struct {
commits []*fakeCommit // oldest first; the default branch is the last
tags map[string]string
branches map[string]string
}
func (r *fakeRepo) rev(rev string) *fakeCommit {
if h, ok := r.branches[rev]; ok {
rev = h
} else if h, ok := r.tags[rev]; ok {
rev = h
}
if len(rev) < 7 {
return nil
}
for _, c := range r.commits {
if strings.HasPrefix(c.hash, rev) {
return c
}
}
return nil
}
var (
t1 = time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)
t2 = t1.Add(24 * time.Hour)
t3 = t2.Add(24 * time.Hour)
)
// libFiles is a repo holding a root module, a nested module in sub/ and a
// /v2 module in the v2/ major subdirectory.
func libFiles(lib string) map[string]string {
return map[string]string{
"go.mod": "module example.com/lib\n",
"lib.go": lib,
"LICENSE": "MIT\n",
"sub/go.mod": "module example.com/lib/sub\n",
"sub/sub.go": "package sub\n",
"vendor/x/x.go": "package x\n",
"v2/go.mod": "module example.com/lib/v2\n",
"v2/lib.go": "package lib\n",
"docs/README.md": "docs\n",
}
}
func newLibRepo() *fakeRepo {
a := &fakeCommit{hash: strings.Repeat("a", 40), time: t1, files: libFiles("package lib // A\n")}
b := &fakeCommit{hash: strings.Repeat("b", 40), time: t2, files: libFiles("package lib // B\n")}
d := &fakeCommit{hash: "d" + strings.Repeat("0123456789", 3) + "abcdefabc", time: t3, files: libFiles("package lib // D\n")}
return &fakeRepo{
commits: []*fakeCommit{a, b, d},
tags: map[string]string{"v1.0.0": a.hash, "sub/v0.1.0": a.hash, "v2.0.0": a.hash, "v1.1.0-rc.1": b.hash, "not-semver": a.hash},
branches: map[string]string{"main": d.hash},
}
}
// newLegacyRepo has no go.mod: its v3 tag is a +incompatible version.
func newLegacyRepo() *fakeRepo {
c := &fakeCommit{hash: strings.Repeat("c", 40), time: t1, files: map[string]string{"legacy.go": "package legacy\n"}}
return &fakeRepo{
commits: []*fakeCommit{c},
tags: map[string]string{"v1.0.0": c.hash, "v3.0.0": c.hash},
branches: map[string]string{"main": c.hash},
}
}
// forge is a fake Gitea or GitLab server over a set of repos keyed by
// "owner/name".
type forge struct {
srv *httptest.Server
repos map[string]*fakeRepo
hits atomic.Int32
mu sync.Mutex
auth []string // Authorization (or PRIVATE-TOKEN) headers seen
}
func newForge(t *testing.T, kind config.GoKind, repos map[string]*fakeRepo) *forge {
f := &forge{repos: repos}
var h http.HandlerFunc
if kind == config.GoKindGitea {
h = f.gitea
} else {
h = f.gitlab
}
f.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
f.hits.Add(1)
f.mu.Lock()
f.auth = append(f.auth, r.Header.Get("Authorization"))
f.mu.Unlock()
h(w, r)
}))
t.Cleanup(f.srv.Close)
return f
}
func (f *forge) sawAuth(want string) bool {
f.mu.Lock()
defer f.mu.Unlock()
return slices.Contains(f.auth, want)
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}
func archive(w http.ResponseWriter, top string, c *fakeCommit) {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
zw.Create(top + "/")
for _, name := range sortedKeys(c.files) {
fw, _ := zw.Create(top + "/" + name)
io.WriteString(fw, c.files[name])
}
zw.Close()
w.Header().Set("Content-Type", "application/zip")
w.Write(buf.Bytes())
}
func sortedKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func (f *forge) gitea(w http.ResponseWriter, r *http.Request) {
rest, ok := strings.CutPrefix(r.URL.Path, "/api/v1/repos/")
if !ok {
http.NotFound(w, r)
return
}
parts := strings.SplitN(rest, "/", 3)
if len(parts) < 2 {
http.NotFound(w, r)
return
}
name := parts[0] + "/" + parts[1]
repo, ok := f.repos[name]
if !ok {
http.NotFound(w, r)
return
}
sub := ""
if len(parts) == 3 {
sub = parts[2]
}
q := r.URL.Query()
switch {
case sub == "":
writeJSON(w, map[string]string{"default_branch": "main"})
case sub == "tags":
page := q.Get("page")
type tagJSON struct {
Name string `json:"name"`
Commit map[string]string `json:"commit"`
}
var out []tagJSON
if page == "1" {
for _, n := range sortedKeys(repo.tags) {
out = append(out, tagJSON{Name: n, Commit: map[string]string{"sha": repo.tags[n]}})
}
}
writeJSON(w, out)
case sub == "commits":
c := repo.rev(q.Get("sha"))
if c == nil {
http.NotFound(w, r)
return
}
writeJSON(w, []any{map[string]any{"sha": c.hash, "commit": map[string]any{"committer": map[string]any{"date": c.time}}}})
case strings.HasPrefix(sub, "raw/"):
c := repo.rev(q.Get("ref"))
data, ok := "", false
if c != nil {
data, ok = c.files[strings.TrimPrefix(sub, "raw/")]
}
if !ok {
http.NotFound(w, r)
return
}
io.WriteString(w, data)
case strings.HasPrefix(sub, "archive/") && strings.HasSuffix(sub, ".zip"):
c := repo.rev(strings.TrimSuffix(strings.TrimPrefix(sub, "archive/"), ".zip"))
if c == nil {
http.NotFound(w, r)
return
}
archive(w, parts[1], c)
default:
http.NotFound(w, r)
}
}
func (f *forge) gitlab(w http.ResponseWriter, r *http.Request) {
// The project id is the URL-encoded full path: work on the raw path.
rest, ok := strings.CutPrefix(r.URL.EscapedPath(), "/api/v4/projects/")
if !ok {
http.NotFound(w, r)
return
}
id, sub, _ := strings.Cut(rest, "/")
name, _ := url.PathUnescape(id)
repo, ok := f.repos[name]
if !ok {
http.NotFound(w, r)
return
}
q := r.URL.Query()
switch {
case sub == "":
writeJSON(w, map[string]string{"default_branch": "main"})
case sub == "repository/tags":
type tagJSON struct {
Name string `json:"name"`
Commit map[string]string `json:"commit"`
}
var out []tagJSON
if q.Get("page") == "1" {
for _, n := range sortedKeys(repo.tags) {
out = append(out, tagJSON{Name: n, Commit: map[string]string{"id": repo.tags[n]}})
}
}
writeJSON(w, out)
case strings.HasPrefix(sub, "repository/commits/"):
ref, _ := url.PathUnescape(strings.TrimPrefix(sub, "repository/commits/"))
c := repo.rev(ref)
if c == nil {
http.NotFound(w, r)
return
}
writeJSON(w, map[string]any{"id": c.hash, "committed_date": c.time})
case strings.HasPrefix(sub, "repository/files/") && strings.HasSuffix(sub, "/raw"):
file, _ := url.PathUnescape(strings.TrimSuffix(strings.TrimPrefix(sub, "repository/files/"), "/raw"))
c := repo.rev(q.Get("ref"))
data, ok := "", false
if c != nil {
data, ok = c.files[file]
}
if !ok {
http.NotFound(w, r)
return
}
io.WriteString(w, data)
case sub == "repository/archive.zip":
c := repo.rev(q.Get("sha"))
if c == nil {
http.NotFound(w, r)
return
}
archive(w, name[strings.LastIndex(name, "/")+1:]+"-"+c.hash, c)
default:
http.NotFound(w, r)
}
}
func newTestHandler(t *testing.T, vrepo config.VRepo) *Handler {
t.Helper()
ups := vrepo.AllUpstreams()
clients := map[string][]*http.Client{}
for range ups {
clients[vrepo.Name] = append(clients[vrepo.Name], http.DefaultClient)
}
store, err := storage.NewLocalFS(t.TempDir())
if err != nil {
t.Fatal(err)
}
return NewHandler([]config.VRepo{vrepo}, clients, proxycache.New(store))
}
func goRepo(ups ...config.UpstreamConf) config.VRepo {
for i := range ups {
if ups[i].Kind == "" {
ups[i].Kind = config.GoKindGoproxy
}
if ups[i].GoGet == "" {
ups[i].GoGet = config.GoGetMod
}
}
return config.VRepo{
Name: "go", Type: config.VRepoGo, Upstreams: ups,
Auth: config.AuthConfig{Mode: config.AuthPublic},
Cache: config.CacheConfig{MetadataTTL: time.Minute, NotFoundTTL: time.Minute},
}
}
func serve(t *testing.T, h http.Handler, path string) *httptest.ResponseRecorder {
t.Helper()
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
return w
}
func wantBody(t *testing.T, h http.Handler, path string, want string) {
t.Helper()
w := serve(t, h, path)
if w.Code != http.StatusOK {
t.Fatalf("GET %s = %d %s", path, w.Code, w.Body)
}
if got := w.Body.String(); got != want {
t.Errorf("GET %s =\n%q\nwant\n%q", path, got, want)
}
}
func wantStatus(t *testing.T, h http.Handler, path string, want int) {
t.Helper()
if w := serve(t, h, path); w.Code != want {
t.Errorf("GET %s = %d %s, want %d", path, w.Code, w.Body, want)
}
}
func zipNames(t *testing.T, body []byte) []string {
t.Helper()
zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
t.Fatal(err)
}
var names []string
for _, f := range zr.File {
names = append(names, f.Name)
}
sort.Strings(names)
return names
}
func infoJSON(v string, t time.Time) string {
data, _ := json.Marshal(info{Version: v, Time: t})
return string(data) + "\n"
}
// TestForges runs the same GOPROXY scenarios against the Gitea and GitLab
// sources.
func TestForges(t *testing.T) {
for _, kind := range []config.GoKind{config.GoKindGitea, config.GoKindGitlab} {
t.Run(string(kind), func(t *testing.T) {
f := newForge(t, kind, map[string]*fakeRepo{"Team/lib": newLibRepo(), "Team/legacy": newLegacyRepo()})
h := newTestHandler(t, goRepo(config.UpstreamConf{
Kind: kind, URL: f.srv.URL, Modules: []string{"go.test/*"}, Repo: "Team/{1}",
}))
const base = "/go/go/go.test"
wantBody(t, h, base+"/lib/@v/list", "v1.0.0\nv1.1.0-rc.1\n")
wantBody(t, h, base+"/lib/@latest", infoJSON("v1.0.0", t1))
wantBody(t, h, base+"/lib/@v/v1.0.0.info", infoJSON("v1.0.0", t1))
wantBody(t, h, base+"/lib/@v/v1.0.0.mod", "module example.com/lib\n")
// A tagged branch head is its release; an untagged one a
// pseudo-version, which then resolves by its short hash.
wantBody(t, h, base+"/lib/@v/bbbbbbbbbbbb.info", infoJSON("v1.1.0-rc.1", t2))
pseudo := "v0.0.0-20250104030405-d01234567890"
wantBody(t, h, base+"/lib/@v/main.info", infoJSON(pseudo, t3))
wantBody(t, h, base+"/lib/@v/"+pseudo+".mod", "module example.com/lib\n")
w := serve(t, h, base+"/lib/@v/v1.0.0.zip")
if w.Code != http.StatusOK {
t.Fatalf("zip = %d %s", w.Code, w.Body)
}
// Nested modules (sub/, v2/) and vendor/ are left out.
p := "go.test/lib@v1.0.0/"
if got, want := zipNames(t, w.Body.Bytes()), []string{p + "LICENSE", p + "docs/README.md", p + "go.mod", p + "lib.go"}; !slices.Equal(got, want) {
t.Errorf("zip files = %v, want %v", got, want)
}
// sub/ is its own module with sub/-prefixed tags, and gets the
// repo's LICENSE.
wantBody(t, h, base+"/lib/sub/@v/list", "v0.1.0\n")
w = serve(t, h, base+"/lib/sub/@v/v0.1.0.zip")
p = "go.test/lib/sub@v0.1.0/"
if got, want := zipNames(t, w.Body.Bytes()), []string{p + "LICENSE", p + "go.mod", p + "sub.go"}; !slices.Equal(got, want) {
t.Errorf("sub zip files = %v, want %v", got, want)
}
// /v2 lives in the v2/ major subdirectory.
wantBody(t, h, base+"/lib/v2/@v/list", "v2.0.0\n")
wantBody(t, h, base+"/lib/v2/@v/v2.0.0.mod", "module example.com/lib/v2\n")
w = serve(t, h, base+"/lib/v2/@v/v2.0.0.zip")
p = "go.test/lib/v2@v2.0.0/"
if got, want := zipNames(t, w.Body.Bytes()), []string{p + "LICENSE", p + "go.mod", p + "lib.go"}; !slices.Equal(got, want) {
t.Errorf("v2 zip files = %v, want %v", got, want)
}
// v2.0.0 of the root module has a go.mod: not +incompatible.
wantStatus(t, h, base+"/lib/@v/v2.0.0+incompatible.info", http.StatusNotFound)
wantStatus(t, h, base+"/lib/@v/v2.0.0.info", http.StatusNotFound)
// Without go.mod, v3 is +incompatible and gets a synthesized one.
wantBody(t, h, base+"/legacy/@v/list", "v1.0.0\nv3.0.0+incompatible\n")
wantBody(t, h, base+"/legacy/@v/v3.0.0+incompatible.mod", "module go.test/legacy\n")
wantStatus(t, h, base+"/missing/@v/list", http.StatusNotFound)
wantStatus(t, h, base+"/lib/@v/v9.9.9.info", http.StatusNotFound)
})
}
}
func TestVersionedFilesCachedForever(t *testing.T) {
f := newForge(t, config.GoKindGitea, map[string]*fakeRepo{"o/lib": newLibRepo()})
h := newTestHandler(t, goRepo(config.UpstreamConf{Kind: config.GoKindGitea, URL: f.srv.URL, Modules: []string{"go.test/*"}, Repo: "o/{1}"}))
wantStatus(t, h, "/go/go/go.test/lib/@v/v1.0.0.zip", http.StatusOK)
hits := f.hits.Load()
wantStatus(t, h, "/go/go/go.test/lib/@v/v1.0.0.zip", http.StatusOK)
if f.hits.Load() != hits {
t.Errorf("second zip request reached the forge")
}
}
func TestForgeAuth(t *testing.T) {
repos := map[string]*fakeRepo{"o/lib": newLibRepo()}
gitea := newForge(t, config.GoKindGitea, repos)
gitlab := newForge(t, config.GoKindGitlab, repos)
h := newTestHandler(t, goRepo(
config.UpstreamConf{Kind: config.GoKindGitea, URL: gitea.srv.URL, Modules: []string{"gitea.test/*"}, Repo: "o/{1}", Password: "gt-token"},
config.UpstreamConf{Kind: config.GoKindGitlab, URL: gitlab.srv.URL, Modules: []string{"gitlab.test/*"}, Repo: "o/{1}", Password: "gl-token"},
))
wantStatus(t, h, "/go/go/gitea.test/lib/@v/list", http.StatusOK)
wantStatus(t, h, "/go/go/gitlab.test/lib/@v/list", http.StatusOK)
if !gitea.sawAuth("token gt-token") {
t.Errorf("gitea saw %v, want a token header", gitea.auth)
}
if !gitlab.sawAuth("Bearer gl-token") {
t.Errorf("gitlab saw %v, want a Bearer header", gitlab.auth)
}
}
// stubProxy is a GOPROXY upstream answering from a path → body map.
func stubProxy(t *testing.T, missStatus int, files map[string]string) (*httptest.Server, *atomic.Int32) {
var hits atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits.Add(1)
if body, ok := files[r.URL.Path]; ok {
io.WriteString(w, body)
return
}
http.Error(w, "not found", missStatus)
}))
t.Cleanup(srv.Close)
return srv, &hits
}
func TestRuleOrderAndFallthrough(t *testing.T) {
f := newForge(t, config.GoKindGitea, map[string]*fakeRepo{"o/lib": newLibRepo()})
// The first proxy has nothing (410, as proxy.golang.org answers), the
// second serves the public module.
empty, emptyHits := stubProxy(t, http.StatusGone, nil)
public, _ := stubProxy(t, http.StatusNotFound, map[string]string{
"/rsc.io/quote/@v/list": "v1.5.2\n",
"/go.test/other/@v/list": "v0.1.0\n",
"/github.com/!burnt!sushi/toml/@v/list": "v1.0.0\n",
})
h := newTestHandler(t, goRepo(
config.UpstreamConf{Kind: config.GoKindGitea, URL: f.srv.URL, Modules: []string{"go.test/*"}, Repo: "o/{1}"},
config.UpstreamConf{URL: empty.URL},
config.UpstreamConf{URL: public.URL},
))
wantBody(t, h, "/go/go/rsc.io/quote/@v/list", "v1.5.2\n")
if emptyHits.Load() != 1 {
t.Errorf("empty proxy hits = %d, want 1", emptyHits.Load())
}
// Not in the gitea repo set: falls through to the proxies.
wantBody(t, h, "/go/go/go.test/other/@v/list", "v0.1.0\n")
// Served from git, never asked of the proxies.
before := emptyHits.Load()
wantBody(t, h, "/go/go/go.test/lib/@v/list", "v1.0.0\nv1.1.0-rc.1\n")
if emptyHits.Load() != before {
t.Error("git-served module reached the proxies")
}
// Case-encoded paths are relayed as sent.
wantBody(t, h, "/go/go/github.com/!burnt!sushi/toml/@v/list", "v1.0.0\n")
}
func TestUpstreamOutageIsNotNotFound(t *testing.T) {
down := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "boom", http.StatusInternalServerError)
}))
t.Cleanup(down.Close)
h := newTestHandler(t, goRepo(config.UpstreamConf{Kind: config.GoKindGitea, URL: down.URL, Modules: []string{"go.test/*"}, Repo: "o/{1}"}))
wantStatus(t, h, "/go/go/go.test/lib/@v/list", http.StatusBadGateway)
}
func TestNoRule(t *testing.T) {
f := newForge(t, config.GoKindGitea, map[string]*fakeRepo{"o/lib": newLibRepo()})
h := newTestHandler(t, goRepo(config.UpstreamConf{Kind: config.GoKindGitea, URL: f.srv.URL, Modules: []string{"go.test/*"}, Repo: "o/{1}"}))
wantStatus(t, h, "/go/go/rsc.io/quote/@v/list", http.StatusNotFound)
if f.hits.Load() != 0 {
t.Error("unmatched module reached the forge")
}
}
func TestBadRequests(t *testing.T) {
h := newTestHandler(t, goRepo(config.UpstreamConf{URL: "http://127.0.0.1:1"}))
for _, p := range []string{
"/go/go/rsc.io/quote/@v/main.zip", // .zip needs a version
"/go/go/rsc.io/quote/@v/main.mod", // .mod too
"/go/go/rsc.io/quote/@v/v1.0.0.tar.gz", // unknown file
"/go/go/rsc.io/quote", // no @v
"/go/go/Rsc.io/quote/@v/list", // not case-encoded
"/go/nope/rsc.io/quote/@v/list", // unknown vrepo
} {
wantStatus(t, h, p, http.StatusNotFound)
}
wantStatus(t, h, "/go/go/../etc/@v/list", http.StatusBadRequest)
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodPut, "/go/go/rsc.io/quote/@v/list", nil))
if w.Code != http.StatusMethodNotAllowed {
t.Errorf("PUT = %d", w.Code)
}
}
func TestAuthEnforced(t *testing.T) {
public, _ := stubProxy(t, http.StatusNotFound, map[string]string{"/rsc.io/quote/@v/list": "v1.5.2\n"})
vr := goRepo(config.UpstreamConf{URL: public.URL})
vr.Auth = config.AuthConfig{Mode: config.AuthPassword, Password: "pw"}
h := newTestHandler(t, vr)
wantStatus(t, h, "/go/go/rsc.io/quote/@v/list", http.StatusUnauthorized)
r := httptest.NewRequest(http.MethodGet, "/go/go/rsc.io/quote/@v/list", nil)
r.SetBasicAuth("any", "pw")
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("with password = %d", w.Code)
}
}
func TestVanity(t *testing.T) {
vr := goRepo(
config.UpstreamConf{Kind: config.GoKindGitea, URL: "https://gitea.test", Modules: []string{"go.test/team/*"}, Repo: "Team/{1}"},
config.UpstreamConf{Kind: config.GoKindGit, URL: "https://git.test/", Modules: []string{"go.test/raw/*"}, Repo: "{1}", GoGet: config.GoGetGit},
config.UpstreamConf{URL: "https://proxy.golang.org"},
)
vr.VanityHosts = []string{"go.test"}
h := newTestHandler(t, vr)
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "next") })
v := h.Vanity(next)
lookup := func(path string, hdr map[string]string) *httptest.ResponseRecorder {
r := httptest.NewRequest(http.MethodGet, "http://go.test"+path, nil)
for k, val := range hdr {
r.Header.Set(k, val)
}
w := httptest.NewRecorder()
v.ServeHTTP(w, r)
return w
}
w := lookup("/team/lib/v2/pkg?go-get=1", map[string]string{"X-Forwarded-Proto": "https"})
if want := `content="go.test/team/lib mod https://go.test/go/go"`; !strings.Contains(w.Body.String(), want) {
t.Errorf("mod vanity = %s, want %s", w.Body, want)
}
w = lookup("/raw/tool?go-get=1", nil)
if want := `content="go.test/raw/tool git https://git.test/tool.git"`; !strings.Contains(w.Body.String(), want) {
t.Errorf("git vanity = %s, want %s", w.Body, want)
}
if w := lookup("/elsewhere/x?go-get=1", nil); w.Code != http.StatusNotFound {
t.Errorf("unmatched vanity = %d", w.Code)
}
if w := lookup("/team/lib", nil); w.Body.String() != "next" {
t.Errorf("non go-get request = %q, want passed through", w.Body)
}
r := httptest.NewRequest(http.MethodGet, "http://other.test/team/lib?go-get=1", nil)
w = httptest.NewRecorder()
v.ServeHTTP(w, r)
if w.Body.String() != "next" {
t.Errorf("other host = %q, want passed through", w.Body)
}
}
func TestMatch(t *testing.T) {
cases := []struct {
pattern, mod string
caps []string
root, rest string
ok bool
}{
{"go.test/*", "go.test/lib", []string{"lib"}, "go.test/lib", "", true},
{"go.test/*", "go.test/lib/sub/v2", []string{"lib"}, "go.test/lib", "sub/v2", true},
{"go.test/*/*", "go.test/a/b/c", []string{"a", "b"}, "go.test/a/b", "c", true},
{"go.test/lib", "go.test/lib/x", nil, "go.test/lib", "x", true},
{"*.test/x-*", "go.test/x-y", []string{"go.test", "x-y"}, "go.test/x-y", "", true},
{"go.test/*", "go.test", nil, "", "", false},
{"go.test/*", "other.test/lib", nil, "", "", false},
}
for _, c := range cases {
caps, root, rest, ok := match(c.pattern, c.mod)
if ok != c.ok || root != c.root || rest != c.rest || !slices.Equal(caps, c.caps) {
t.Errorf("match(%q, %q) = %v %q %q %v", c.pattern, c.mod, caps, root, rest, ok)
}
}
if got := expand("Team/{1}-{2}.git", []string{"a", "b"}); got != "Team/a-b.git" {
t.Errorf("expand = %q", got)
}
}
func TestLayout(t *testing.T) {
cases := []struct{ mod, root, rest, codeDir, pathMajor string }{
{"go.test/lib", "go.test/lib", "", "", ""},
{"go.test/lib/v3", "go.test/lib", "v3", "", "/v3"},
{"go.test/lib/sub", "go.test/lib", "sub", "sub", ""},
{"go.test/lib/sub/v2", "go.test/lib", "sub/v2", "sub", "/v2"},
// The suffix is part of the repo path itself: no major layout.
{"go.test/v2", "go.test/v2", "", "", ""},
}
for _, c := range cases {
l := layoutFor(c.mod, c.root, c.rest)
if l.codeDir != c.codeDir || l.pathMajor != c.pathMajor {
t.Errorf("layoutFor(%q) = %+v", c.mod, l)
}
}
}
func TestParseRequest(t *testing.T) {
rq, ok := parseRequest("github.com/!burnt!sushi/toml/@v/v1.2.3-!r!c1.info")
if !ok || rq.mod != "github.com/BurntSushi/toml" || rq.version != "v1.2.3-RC1" || rq.ext != ".info" {
t.Errorf("parse = %+v %v", rq, ok)
}
if !rq.immutable() {
t.Error("v1.2.3-RC1.info not immutable")
}
rq, ok = parseRequest("go.test/lib/@latest")
if !ok || rq.file != "@latest" || rq.immutable() {
t.Errorf("latest = %+v %v", rq, ok)
}
rq, _ = parseRequest("go.test/lib/@v/v1.0.0.zip")
if !rq.immutable() {
t.Error("v1.0.0.zip not immutable")
}
rq, _ = parseRequest("go.test/lib/@v/main.info")
if rq.immutable() {
t.Error("main.info immutable")
}
}