Files
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

165 lines
5.6 KiB
Go

package goproxy
import (
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-billy/v5/util"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/pktline"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/protocol/packp"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/server"
"github.com/go-git/go-git/v5/storage/memory"
"tiny-proxy/internal/config"
)
// newGitServer builds a real repository from the fake repo's history (one
// commit per fakeCommit, on main) and serves it over smart HTTP at
// /<name>.git, using go-git's own upload-pack server. Tags become
// annotated tags, so peeling is exercised. It returns the server and the
// real commit hashes, in fakeRepo order.
func newGitServer(t *testing.T, name string, fr *fakeRepo) (*httptest.Server, []string) {
t.Helper()
st := memory.NewStorage()
fs := memfs.New()
r, err := git.InitWithOptions(st, fs, git.InitOptions{DefaultBranch: plumbing.NewBranchReferenceName("main")})
if err != nil {
t.Fatal(err)
}
wt, err := r.Worktree()
if err != nil {
t.Fatal(err)
}
byFake := map[string]plumbing.Hash{}
var hashes []string
for _, c := range fr.commits {
for _, p := range sortedKeys(c.files) {
if err := util.WriteFile(fs, p, []byte(c.files[p]), 0o644); err != nil {
t.Fatal(err)
}
if _, err := wt.Add(p); err != nil {
t.Fatal(err)
}
}
sig := &object.Signature{Name: "t", Email: "t@test", When: c.time}
h, err := wt.Commit("commit", &git.CommitOptions{Author: sig, Committer: sig})
if err != nil {
t.Fatal(err)
}
byFake[c.hash] = h
hashes = append(hashes, h.String())
}
for name, fake := range fr.tags {
sig := &object.Signature{Name: "t", Email: "t@test", When: t1}
if _, err := r.CreateTag(name, byFake[fake], &git.CreateTagOptions{Tagger: sig, Message: name}); err != nil {
t.Fatal(err)
}
}
ep, err := transport.NewEndpoint("file:///" + name)
if err != nil {
t.Fatal(err)
}
srv := server.NewServer(server.MapLoader{ep.String(): st})
mux := http.NewServeMux()
mux.HandleFunc("GET /"+name+".git/info/refs", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("service") != transport.UploadPackServiceName {
http.Error(w, "dumb http not supported", http.StatusForbidden)
return
}
sess, err := srv.NewUploadPackSession(ep, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ar, err := sess.AdvertisedReferencesContext(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ar.Prefix = [][]byte{[]byte("# service=" + transport.UploadPackServiceName), pktline.Flush}
w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
ar.Encode(w)
})
mux.HandleFunc("POST /"+name+".git/git-upload-pack", func(w http.ResponseWriter, r *http.Request) {
req := packp.NewUploadPackRequest()
if err := req.Decode(r.Body); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sess, err := srv.NewUploadPackSession(ep, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, err := sess.UploadPack(r.Context(), req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/x-git-upload-pack-result")
resp.Encode(w)
})
ts := httptest.NewServer(mux)
t.Cleanup(ts.Close)
return ts, hashes
}
func TestGitSource(t *testing.T) {
ts, hashes := newGitServer(t, "lib", newLibRepo())
h := newTestHandler(t, goRepo(
config.UpstreamConf{Kind: config.GoKindGit, URL: ts.URL, Modules: []string{"go.test/*"}, Repo: "{1}.git"},
))
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.mod", "module example.com/lib\n")
// The branch head is untagged: a pseudo-version built from the real
// hash, which then resolves for .mod and .zip.
pseudo := "v0.0.0-20250104030405-" + hashes[2][:12]
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/"+pseudo+".zip")
if w.Code != http.StatusOK {
t.Fatalf("pseudo zip = %d %s", w.Code, w.Body)
}
p := "go.test/lib@" + pseudo + "/"
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)
}
wantBody(t, h, base+"/lib/v2/@v/v2.0.0.mod", "module example.com/lib/v2\n")
wantBody(t, h, base+"/lib/sub/@v/list", "v0.1.0\n")
wantStatus(t, h, base+"/missing/@v/list", http.StatusNotFound)
}
func TestGitSourceAbbreviatedHash(t *testing.T) {
ts, hashes := newGitServer(t, "lib", newLibRepo())
src, err := newGitSource(http.DefaultClient, ts.URL, "lib.git", "", "")
if err != nil {
t.Fatal(err)
}
ctx := t.Context()
// A ref tip resolves by any prefix; an older commit once it was seen.
c, err := src.Commit(ctx, hashes[2][:8])
if err != nil || c.Hash != hashes[2] {
t.Fatalf("tip by prefix = %+v, %v", c, err)
}
if _, err := src.Commit(ctx, strings.Repeat("0", 12)); err == nil {
t.Error("unknown abbreviated hash resolved")
}
if c, err := src.Commit(ctx, "v1.0.0"); err != nil || c.Hash != hashes[0] {
t.Errorf("annotated tag = %+v, %v (want peeled %s)", c, err, hashes[0])
}
}