2026-01-20 21:52:44 +00:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-06 12:55:15 +01:00
|
|
|
"bytes"
|
|
|
|
|
"compress/gzip"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"io"
|
2026-01-20 21:52:44 +00:00
|
|
|
"log/slog"
|
2026-04-06 12:55:15 +01:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
2026-01-20 21:52:44 +00:00
|
|
|
"testing"
|
2026-04-06 12:55:15 +01:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/cooldown"
|
|
|
|
|
"google.golang.org/protobuf/encoding/protowire"
|
2026-01-20 21:52:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestHexParseTarballFilename(t *testing.T) {
|
|
|
|
|
h := &HexHandler{proxy: &Proxy{Logger: slog.Default()}}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
filename string
|
|
|
|
|
wantName string
|
|
|
|
|
wantVersion string
|
|
|
|
|
}{
|
|
|
|
|
{"phoenix-1.7.10.tar", "phoenix", "1.7.10"},
|
|
|
|
|
{"ecto-3.11.0.tar", "ecto", "3.11.0"},
|
|
|
|
|
{"phoenix_live_view-0.20.1.tar", "phoenix_live_view", "0.20.1"},
|
|
|
|
|
{"invalid", "", ""},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
name, version := h.parseTarballFilename(tt.filename)
|
|
|
|
|
if name != tt.wantName || version != tt.wantVersion {
|
|
|
|
|
t.Errorf("parseTarballFilename(%q) = (%q, %q), want (%q, %q)",
|
|
|
|
|
tt.filename, name, version, tt.wantName, tt.wantVersion)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-06 12:55:15 +01:00
|
|
|
|
|
|
|
|
// buildHexRelease encodes a Release protobuf message.
|
|
|
|
|
func buildHexRelease(version string) []byte {
|
|
|
|
|
var release []byte
|
|
|
|
|
// field 1 = version (string)
|
|
|
|
|
release = protowire.AppendTag(release, 1, protowire.BytesType)
|
|
|
|
|
release = protowire.AppendString(release, version)
|
|
|
|
|
// field 2 = inner_checksum (bytes) - required
|
|
|
|
|
release = protowire.AppendTag(release, 2, protowire.BytesType)
|
|
|
|
|
release = protowire.AppendBytes(release, []byte("fakechecksum1234567890123456789012"))
|
|
|
|
|
// field 5 = outer_checksum (bytes)
|
|
|
|
|
release = protowire.AppendTag(release, 5, protowire.BytesType)
|
|
|
|
|
release = protowire.AppendBytes(release, []byte("outerchecksum123456789012345678901"))
|
|
|
|
|
return release
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buildHexPackage encodes a Package protobuf message.
|
|
|
|
|
func buildHexPackage(name string, versions []string) []byte {
|
|
|
|
|
var pkg []byte
|
|
|
|
|
for _, v := range versions {
|
|
|
|
|
release := buildHexRelease(v)
|
|
|
|
|
pkg = protowire.AppendTag(pkg, 1, protowire.BytesType)
|
|
|
|
|
pkg = protowire.AppendBytes(pkg, release)
|
|
|
|
|
}
|
|
|
|
|
// field 2 = name
|
|
|
|
|
pkg = protowire.AppendTag(pkg, 2, protowire.BytesType)
|
|
|
|
|
pkg = protowire.AppendString(pkg, name)
|
|
|
|
|
// field 3 = repository
|
|
|
|
|
pkg = protowire.AppendTag(pkg, 3, protowire.BytesType)
|
|
|
|
|
pkg = protowire.AppendString(pkg, "hexpm")
|
|
|
|
|
return pkg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buildHexSigned wraps a payload in a Signed protobuf message and gzips it.
|
|
|
|
|
func buildHexSigned(payload []byte) []byte {
|
|
|
|
|
var signed []byte
|
|
|
|
|
signed = protowire.AppendTag(signed, 1, protowire.BytesType)
|
|
|
|
|
signed = protowire.AppendBytes(signed, payload)
|
|
|
|
|
// field 2 = signature (optional, add a fake one)
|
|
|
|
|
signed = protowire.AppendTag(signed, 2, protowire.BytesType)
|
|
|
|
|
signed = protowire.AppendBytes(signed, []byte("fakesignature"))
|
|
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
gw := gzip.NewWriter(&buf)
|
|
|
|
|
_, _ = gw.Write(signed)
|
|
|
|
|
_ = gw.Close()
|
|
|
|
|
return buf.Bytes()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHexFilterPackageReleases(t *testing.T) {
|
|
|
|
|
pkg := buildHexPackage("phoenix", []string{testVersion100, "2.0.0", "3.0.0"})
|
|
|
|
|
|
|
|
|
|
filtered, err := filterPackageReleases(pkg, map[string]bool{"2.0.0": true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract remaining versions
|
|
|
|
|
var versions []string
|
|
|
|
|
data := filtered
|
|
|
|
|
for len(data) > 0 {
|
|
|
|
|
num, wtype, n := protowire.ConsumeTag(data)
|
|
|
|
|
if n < 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
data = data[n:]
|
|
|
|
|
switch wtype {
|
|
|
|
|
case protowire.BytesType:
|
|
|
|
|
v, vn := protowire.ConsumeBytes(data)
|
|
|
|
|
if vn < 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if num == 1 { // release field
|
|
|
|
|
version := extractReleaseVersion(v)
|
|
|
|
|
if version != "" {
|
|
|
|
|
versions = append(versions, version)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
data = data[vn:]
|
|
|
|
|
case protowire.VarintType:
|
|
|
|
|
_, vn := protowire.ConsumeVarint(data)
|
|
|
|
|
if vn < 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
data = data[vn:]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(versions) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 versions, got %d: %v", len(versions), versions)
|
|
|
|
|
}
|
|
|
|
|
if versions[0] != testVersion100 || versions[1] != "3.0.0" {
|
|
|
|
|
t.Errorf("expected [1.0.0, 3.0.0], got %v", versions)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHexFilterSignedPackage(t *testing.T) {
|
|
|
|
|
pkg := buildHexPackage("phoenix", []string{testVersion100, "2.0.0"})
|
|
|
|
|
gzipped := buildHexSigned(pkg)
|
|
|
|
|
|
|
|
|
|
h := &HexHandler{
|
|
|
|
|
proxy: testProxy(),
|
|
|
|
|
proxyURL: "http://proxy.local",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filtered, err := h.filterSignedPackage(gzipped, map[string]bool{"2.0.0": true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decompress and check
|
|
|
|
|
gr, err := gzip.NewReader(bytes.NewReader(filtered))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
signed, err := io.ReadAll(gr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
payload, err := extractProtobufBytes(signed, 1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check that only version 1.0.0 remains
|
|
|
|
|
version := extractReleaseVersion(mustExtractFirstRelease(t, payload))
|
|
|
|
|
if version != testVersion100 {
|
|
|
|
|
t.Errorf("expected version 1.0.0, got %s", version)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify no signature in the output
|
|
|
|
|
_, err = extractProtobufBytes(signed, 2)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Error("expected no signature in filtered output")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func mustExtractFirstRelease(t *testing.T, payload []byte) []byte {
|
|
|
|
|
t.Helper()
|
|
|
|
|
data := payload
|
|
|
|
|
for len(data) > 0 {
|
|
|
|
|
num, wtype, n := protowire.ConsumeTag(data)
|
|
|
|
|
if n < 0 {
|
|
|
|
|
t.Fatal("invalid protobuf")
|
|
|
|
|
}
|
|
|
|
|
data = data[n:]
|
|
|
|
|
if wtype == protowire.BytesType {
|
|
|
|
|
v, vn := protowire.ConsumeBytes(data)
|
|
|
|
|
if vn < 0 {
|
|
|
|
|
t.Fatal("invalid bytes")
|
|
|
|
|
}
|
|
|
|
|
if num == 1 {
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
data = data[vn:]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
t.Fatal("no release found")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHexExtractReleaseVersion(t *testing.T) {
|
|
|
|
|
release := buildHexRelease("1.2.3")
|
|
|
|
|
version := extractReleaseVersion(release)
|
|
|
|
|
if version != "1.2.3" {
|
|
|
|
|
t.Errorf("expected 1.2.3, got %s", version)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHexHandlePackagesWithCooldown(t *testing.T) {
|
|
|
|
|
now := time.Now()
|
|
|
|
|
oldTime := now.Add(-7 * 24 * time.Hour).Format(time.RFC3339Nano)
|
|
|
|
|
recentTime := now.Add(-1 * time.Hour).Format(time.RFC3339Nano)
|
|
|
|
|
|
|
|
|
|
pkg := buildHexPackage("testpkg", []string{testVersion100, "2.0.0"})
|
|
|
|
|
gzippedProto := buildHexSigned(pkg)
|
|
|
|
|
|
|
|
|
|
apiJSON, _ := json.Marshal(hexPackageAPI{
|
|
|
|
|
Releases: []hexRelease{
|
|
|
|
|
{Version: testVersion100, InsertedAt: oldTime},
|
|
|
|
|
{Version: "2.0.0", InsertedAt: recentTime},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Serve both the protobuf repo and the JSON API from the same test server
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
switch r.URL.Path {
|
|
|
|
|
case "/packages/testpkg":
|
|
|
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
|
|
|
_, _ = w.Write(gzippedProto)
|
|
|
|
|
case "/api/packages/testpkg":
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = w.Write(apiJSON)
|
|
|
|
|
default:
|
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
proxy := testProxy()
|
|
|
|
|
proxy.Cooldown = &cooldown.Config{
|
|
|
|
|
Default: "3d",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Override hexAPIURL for testing by using the upstream URL
|
|
|
|
|
h := &HexHandler{
|
|
|
|
|
proxy: proxy,
|
|
|
|
|
upstreamURL: upstream.URL,
|
|
|
|
|
proxyURL: "http://proxy.local",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We need to override the API URL - but it's a const. Let's test via the lower-level methods instead.
|
|
|
|
|
// Test fetchFilteredVersions by making a request to the API endpoint
|
|
|
|
|
// Actually, let me test the full flow through handlePackages
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/packages/testpkg", nil)
|
|
|
|
|
req.SetPathValue("name", "testpkg")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
// Since hexAPIURL is a const pointing to hex.pm, we can't easily override it in tests.
|
|
|
|
|
// Instead test the protobuf filtering directly which is the core logic.
|
|
|
|
|
filtered, err := h.filterSignedPackage(gzippedProto, map[string]bool{"2.0.0": true})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify only version 1.0.0 survives
|
|
|
|
|
gr, _ := gzip.NewReader(bytes.NewReader(filtered))
|
|
|
|
|
signed, _ := io.ReadAll(gr)
|
|
|
|
|
payload, _ := extractProtobufBytes(signed, 1)
|
|
|
|
|
|
|
|
|
|
var versions []string
|
|
|
|
|
data := payload
|
|
|
|
|
for len(data) > 0 {
|
|
|
|
|
num, wtype, n := protowire.ConsumeTag(data)
|
|
|
|
|
if n < 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
data = data[n:]
|
|
|
|
|
if wtype == protowire.BytesType {
|
|
|
|
|
v, vn := protowire.ConsumeBytes(data)
|
|
|
|
|
if vn < 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if num == 1 {
|
|
|
|
|
if ver := extractReleaseVersion(v); ver != "" {
|
|
|
|
|
versions = append(versions, ver)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
data = data[vn:]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(versions) != 1 || versions[0] != testVersion100 {
|
|
|
|
|
t.Errorf("expected [1.0.0], got %v", versions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ = w
|
|
|
|
|
_ = req
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHexHandlePackagesWithoutCooldown(t *testing.T) {
|
|
|
|
|
pkg := buildHexPackage("testpkg", []string{testVersion100})
|
|
|
|
|
gzipped := buildHexSigned(pkg)
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
|
|
|
_, _ = w.Write(gzipped)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
h := &HexHandler{
|
|
|
|
|
proxy: testProxy(), // no cooldown
|
|
|
|
|
upstreamURL: upstream.URL,
|
|
|
|
|
proxyURL: "http://proxy.local",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/packages/testpkg", nil)
|
|
|
|
|
req.SetPathValue("name", "testpkg")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
h.handlePackages(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want %d", w.Code, http.StatusOK)
|
|
|
|
|
}
|
|
|
|
|
}
|