Skip to content

Commit a4c00dc

Browse files
committed
goproxytest: handle /@latest endpoint
The Go module proxy protocol defines a $base/$module/@latest endpoint that returns the .info JSON for the latest version of a module. The handler only matched paths containing /@v/, so /@latest got a 404. Add a check for the /@latest suffix before the /@v/ routing. When matched, find the highest release version, falling back to pre-release versions if no release exists, and serve its .info file. The version selection is in a standalone latestVersion function that takes an iter.Seq[string], following the same algorithm as cue-lang/cue's LatestVersion in internal/mod/modload/query.go. The test is a direct HTTP unit test rather than a testscript like testdata/list.txt because the go command does not expose /@latest responses to the user; it uses the endpoint internally as a fallback. There is no go subcommand whose output we could assert on in a script to verify that /@latest is served correctly.
1 parent 19311e8 commit a4c00dc

5 files changed

Lines changed: 153 additions & 0 deletions

File tree

goproxytest/proxy.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"encoding/json"
2929
"fmt"
3030
"io/fs"
31+
"iter"
3132
"log"
3233
"net"
3334
"net/http"
@@ -148,6 +149,44 @@ func (srv *Server) handler(w http.ResponseWriter, r *http.Request) {
148149
return
149150
}
150151
path := strings.TrimPrefix(r.URL.Path, "/mod/")
152+
if enc, ok := strings.CutSuffix(path, "/@latest"); ok {
153+
modPath, err := module.UnescapePath(enc)
154+
if err != nil {
155+
srv.logf("go proxy_test: %v\n", err)
156+
http.NotFound(w, r)
157+
return
158+
}
159+
best := latestVersion(func(yield func(string) bool) {
160+
for _, m := range srv.modList {
161+
if m.Path != modPath {
162+
continue
163+
}
164+
if err := module.Check(m.Path, m.Version); err != nil {
165+
continue
166+
}
167+
if !yield(m.Version) {
168+
return
169+
}
170+
}
171+
})
172+
if best == "" {
173+
http.NotFound(w, r)
174+
return
175+
}
176+
a := srv.readArchive(modPath, best)
177+
if a == nil {
178+
http.NotFound(w, r)
179+
return
180+
}
181+
for _, f := range a.Files {
182+
if f.Name == ".info" {
183+
w.Write(f.Data)
184+
return
185+
}
186+
}
187+
http.NotFound(w, r)
188+
return
189+
}
151190
i := strings.Index(path, "/@v/")
152191
if i < 0 {
153192
http.NotFound(w, r)
@@ -347,3 +386,22 @@ func (srv *Server) readArchive(path, vers string) *txtar.Archive {
347386
}).(*txtar.Archive)
348387
return a
349388
}
389+
390+
// latestVersion returns the latest version from the given sequence,
391+
// preferring release versions over pre-release versions.
392+
// It returns the empty string if the sequence is empty.
393+
func latestVersion(versions iter.Seq[string]) string {
394+
var bestRelease, bestAny string
395+
for v := range versions {
396+
if semver.Prerelease(v) == "" && (bestRelease == "" || semver.Compare(v, bestRelease) > 0) {
397+
bestRelease = v
398+
}
399+
if bestAny == "" || semver.Compare(v, bestAny) > 0 {
400+
bestAny = v
401+
}
402+
}
403+
if bestRelease != "" {
404+
return bestRelease
405+
}
406+
return bestAny
407+
}

goproxytest/proxy_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
package goproxytest_test
66

77
import (
8+
"encoding/json"
9+
"io"
10+
"net/http"
811
"path/filepath"
912
"testing"
1013

@@ -77,3 +80,60 @@ func TestSetup(t *testing.T) {
7780
testscript.Run(t, p)
7881
})
7982
}
83+
84+
func TestLatest(t *testing.T) {
85+
srv := goproxytest.NewTestServer(t, filepath.Join("testdata", "mod"), "")
86+
87+
tests := []struct {
88+
name string
89+
module string
90+
want string
91+
}{
92+
{
93+
name: "release preferred over prerelease",
94+
module: "fruit.com",
95+
want: "v1.1.0",
96+
},
97+
{
98+
name: "prerelease only",
99+
module: "prerelease.example",
100+
want: "v0.2.0-rc.1",
101+
},
102+
{
103+
name: "unknown module",
104+
module: "noexist.example",
105+
want: "",
106+
},
107+
}
108+
for _, test := range tests {
109+
t.Run(test.name, func(t *testing.T) {
110+
resp, err := http.Get(srv.URL + "/" + test.module + "/@latest")
111+
if err != nil {
112+
t.Fatal(err)
113+
}
114+
defer resp.Body.Close()
115+
if test.want == "" {
116+
if resp.StatusCode != 404 {
117+
t.Fatalf("got status %d, want 404", resp.StatusCode)
118+
}
119+
return
120+
}
121+
if resp.StatusCode != 200 {
122+
t.Fatalf("got status %d, want 200", resp.StatusCode)
123+
}
124+
body, err := io.ReadAll(resp.Body)
125+
if err != nil {
126+
t.Fatal(err)
127+
}
128+
var info struct {
129+
Version string
130+
}
131+
if err := json.Unmarshal(body, &info); err != nil {
132+
t.Fatalf("invalid JSON response: %v", err)
133+
}
134+
if info.Version != test.want {
135+
t.Fatalf("got version %q, want %q", info.Version, test.want)
136+
}
137+
})
138+
}
139+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- .mod --
2+
module fruit.com
3+
4+
-- .info --
5+
{"Version":"v1.2.0-beta.1","Time":"2018-10-22T18:45:39Z"}
6+
7+
-- go.mod --
8+
module fruit.com
9+
10+
-- fruit/fruit.go --
11+
package main
12+
13+
const Name = "Grape"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- .mod --
2+
module prerelease.example
3+
4+
-- .info --
5+
{"Version":"v0.1.0-alpha.1","Time":"2018-10-22T18:45:39Z"}
6+
7+
-- go.mod --
8+
module prerelease.example
9+
10+
-- x.go --
11+
package x
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- .mod --
2+
module prerelease.example
3+
4+
-- .info --
5+
{"Version":"v0.2.0-rc.1","Time":"2018-10-22T18:45:39Z"}
6+
7+
-- go.mod --
8+
module prerelease.example
9+
10+
-- x.go --
11+
package x

0 commit comments

Comments
 (0)