Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/handler/cargo_test.go
simon chrzanowski aa0b543ae8
Stop forwarding custom auth headers across redirects
Handlers set the configured upstream auth header on the request itself
before handing it to the client. When upstream redirected to another host,
Go's client copied that header to the next hop: it strips only
Authorization and Cookie, so a "header" type credential such as
X-JFrog-Art-Api reached the redirect target. The auth transport already
applies credentials per hop, scoped to each hop's URL.

- Remove Proxy.applyUpstreamAuth and Proxy.AuthForURL; auth comes only
  from the transport
- Add a server test that redirects a header-authenticated upstream to
  another host and asserts the header stays with the upstream
- Wire handler tests' auth through the transport, as the server does
2026-10-14 08:38:51 +02:00

275 lines
7.1 KiB
Go

package handler
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/git-pkgs/cooldown"
"github.com/git-pkgs/registries/fetch"
)
func cargoTestProxy() *Proxy {
return &Proxy{
Logger: slog.Default(),
HTTPClient: http.DefaultClient,
}
}
func TestCargoBuildIndexPath(t *testing.T) {
h := &CargoHandler{}
tests := []struct {
name string
want string
}{
{"a", "1/a"},
{"ab", "2/ab"},
{"abc", "3/a/abc"},
{"abcd", "ab/cd/abcd"},
{"serde", "se/rd/serde"},
{"tokio", "to/ki/tokio"},
{"A", "1/a"}, // lowercase
{"SERDE", "se/rd/serde"}, // lowercase
{"rand_core", "ra/nd/rand_core"},
}
for _, tt := range tests {
got := h.buildIndexPath(tt.name)
if got != tt.want {
t.Errorf("buildIndexPath(%q) = %q, want %q", tt.name, got, tt.want)
}
}
}
func TestCargoConfigEndpoint(t *testing.T) {
h := &CargoHandler{
proxyURL: "http://localhost:8080",
}
req := httptest.NewRequest(http.MethodGet, "/config.json", nil)
w := httptest.NewRecorder()
h.handleConfig(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
var config CargoConfig
if err := json.Unmarshal(w.Body.Bytes(), &config); err != nil {
t.Fatalf("failed to parse config: %v", err)
}
expectedDL := "http://localhost:8080/cargo/crates/{crate}/{version}/download"
if config.DL != expectedDL {
t.Errorf("DL = %q, want %q", config.DL, expectedDL)
}
}
func TestCargoHandlerUsesConfiguredUpstreams(t *testing.T) {
t.Run("index", func(t *testing.T) {
var requestPath, authHeader string
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestPath = r.URL.Path
authHeader = r.Header.Get("Authorization")
if authHeader != "Bearer cargo-token" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "text/plain")
_, _ = io.WriteString(w, `{"name":"serde","vers":"1.0.0"}`)
}))
defer upstream.Close()
proxy, _, _, _ := setupTestProxy(t)
proxy.HTTPClient = upstream.Client()
authorizeUpstream(proxy, "Bearer cargo-token")
h := NewCargoHandler(
proxy,
"http://proxy.test",
upstream.URL+"/index/",
"https://crates.example.test/files/",
)
req := httptest.NewRequest(http.MethodGet, "/se/rd/serde", nil)
w := httptest.NewRecorder()
h.Routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
}
if requestPath != "/index/se/rd/serde" {
t.Errorf("upstream path = %q, want %q", requestPath, "/index/se/rd/serde")
}
if authHeader != "Bearer cargo-token" {
t.Errorf("Authorization = %q, want %q", authHeader, "Bearer cargo-token")
}
})
t.Run("download", func(t *testing.T) {
proxy, _, _, artifactFetcher := setupTestProxy(t)
artifactFetcher.artifact = &fetch.Artifact{
Body: io.NopCloser(strings.NewReader("crate")),
ContentType: "application/gzip",
}
h := NewCargoHandler(
proxy,
"http://proxy.test",
"https://index.example.test/root/",
"https://crates.example.test/files/",
)
req := httptest.NewRequest(http.MethodGet, "/crates/serde/1.0.0/download", nil)
w := httptest.NewRecorder()
h.Routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
}
want := "https://crates.example.test/files/serde/serde-1.0.0.crate"
if artifactFetcher.fetchedURL != want {
t.Errorf("fetched URL = %q, want %q", artifactFetcher.fetchedURL, want)
}
})
}
func TestCargoIndexProxy(t *testing.T) {
// Create a mock upstream index server
indexContent := `{"name":"serde","vers":"1.0.0","deps":[],"cksum":"abc123"}
{"name":"serde","vers":"1.0.1","deps":[],"cksum":"def456"}`
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/se/rd/serde" {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte(indexContent))
}))
defer upstream.Close()
h := &CargoHandler{
proxy: cargoTestProxy(),
indexURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/se/rd/serde", nil)
req.SetPathValue("a", "se")
req.SetPathValue("b", "rd")
req.SetPathValue("name", "serde")
w := httptest.NewRecorder()
h.handleIndex(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
if w.Body.String() != indexContent {
t.Errorf("body = %q, want %q", w.Body.String(), indexContent)
}
}
func TestCargoIndexNotFound(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer upstream.Close()
h := &CargoHandler{
proxy: cargoTestProxy(),
indexURL: upstream.URL,
proxyURL: "http://proxy.local",
}
req := httptest.NewRequest(http.MethodGet, "/no/ne/nonexistent", nil)
req.SetPathValue("a", "no")
req.SetPathValue("b", "ne")
req.SetPathValue("name", "nonexistent")
w := httptest.NewRecorder()
h.handleIndex(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
}
}
func TestCargoRoutes(t *testing.T) {
h := &CargoHandler{
proxy: cargoTestProxy(),
proxyURL: "http://proxy.local",
}
routes := h.Routes()
// Test that config.json route exists
req := httptest.NewRequest(http.MethodGet, "/config.json", nil)
w := httptest.NewRecorder()
routes.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("config.json status = %d, want %d", w.Code, http.StatusOK)
}
}
type filterTestCase struct {
line string
expected bool
}
func TestCargoCooldown(t *testing.T) {
now := time.Now()
createCase := func(name string, version string, age time.Duration, expected bool) filterTestCase {
return filterTestCase{line: `{"name":"` + name + `","vers":"` + version + `","cksum":"abcd","features":{},"yanked":false,"pubtime":"` + now.Add(-1*age).Format(time.RFC3339) + `"}`, expected: expected}
}
testCases := []filterTestCase{
// one week ago
createCase("serde", "1.0.0", 168*time.Hour, true),
// one hour ago
createCase("serde", "1.0.1", 1*time.Hour, false),
// two hours ago with custom filter (1h)
createCase("tokio", "1.0.0", 2*time.Hour, true),
// one hour ago with custom filter (1h)
createCase("tokio", "1.0.0", 1*time.Minute, false),
}
var testInput strings.Builder
var expectedOutput strings.Builder
for _, testCase := range testCases {
testInput.WriteString(testCase.line + "\n")
if testCase.expected {
expectedOutput.WriteString(testCase.line + "\n")
}
}
proxy := testProxy()
proxy.Cooldown = &cooldown.Config{
Default: "3d",
Packages: map[string]string{"pkg:cargo/tokio": "1h"},
}
h := &CargoHandler{
proxy: proxy,
proxyURL: "http://localhost:8080",
}
recorder := httptest.NewRecorder()
h.applyCooldownFiltering(recorder, []byte(testInput.String()))
output := recorder.Body.String()
if output != expectedOutput.String() {
t.Errorf("output = %q, want %q", output, expectedOutput.String())
}
}