Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-08-23 12:24:57 -04:00
pkg-proxy/internal/handler/homebrew_test.go
2026-08-17 09:40:49 +01:00

307 lines
11 KiB
Go

package handler
import (
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"github.com/git-pkgs/registries/fetch"
)
func TestHomebrewHandler_PreservesSignedResponseAndClientValidators(t *testing.T) {
body := " {\n \"payload\": \"signed bytes\",\n \"signatures\": []\n}\n"
etag := `"homebrew-api-etag"`
lastModified := time.Date(2026, time.August, 14, 9, 30, 0, 0, time.UTC)
requests := 0
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.Method != http.MethodGet {
t.Errorf("upstream method = %s, want GET", r.Method)
}
if r.URL.Path != "/api/internal/packages.arm64_tahoe.jws.json" {
t.Errorf("upstream path = %q", r.URL.Path)
}
if got := r.Header.Get("Authorization"); got != "" {
t.Errorf("upstream Authorization = %q, want empty", got)
}
if got := r.Header.Get("Cookie"); got != "" {
t.Errorf("upstream Cookie = %q, want empty", got)
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("ETag", etag)
w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat))
_, _ = io.WriteString(w, body)
}))
defer upstream.Close()
proxy, _, _, _ := setupTestProxy(t)
proxy.CacheMetadata = true
proxy.MetadataTTL = time.Hour
proxy.HTTPClient = upstream.Client()
h := NewHomebrewHandler(proxy, upstream.URL+"/api").Routes()
req := httptest.NewRequest(http.MethodGet, "/internal/packages.arm64_tahoe.jws.json", nil)
req.Header.Set("Authorization", "Bearer client-secret")
req.Header.Set("Cookie", "session=client-secret")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
}
if got := w.Body.String(); got != body {
t.Errorf("body = %q, want byte-for-byte %q", got, body)
}
if got := w.Header().Get("Content-Type"); got != "application/json" {
t.Errorf("Content-Type = %q, want application/json", got)
}
wantContentLength := strconv.Itoa(len(body))
if got := w.Header().Get("Content-Length"); got != wantContentLength {
t.Errorf("Content-Length = %q, want %q", got, wantContentLength)
}
if got := w.Header().Get("ETag"); got != etag {
t.Errorf("ETag = %q, want %q", got, etag)
}
if got := w.Header().Get("Last-Modified"); got != lastModified.Format(http.TimeFormat) {
t.Errorf("Last-Modified = %q, want %q", got, lastModified.Format(http.TimeFormat))
}
conditionalRequest := httptest.NewRequest(http.MethodGet, "/internal/packages.arm64_tahoe.jws.json", nil)
conditionalRequest.Header.Set("If-None-Match", etag)
conditional := httptest.NewRecorder()
h.ServeHTTP(conditional, conditionalRequest)
if conditional.Code != http.StatusNotModified {
t.Fatalf("conditional status = %d, want %d", conditional.Code, http.StatusNotModified)
}
if got := conditional.Header().Get("ETag"); got != etag {
t.Errorf("conditional ETag = %q, want %q", got, etag)
}
if conditional.Body.Len() != 0 {
t.Errorf("conditional body length = %d, want 0", conditional.Body.Len())
}
modifiedSinceRequest := httptest.NewRequest(http.MethodGet, "/internal/packages.arm64_tahoe.jws.json", nil)
modifiedSinceRequest.Header.Set("If-Modified-Since", lastModified.Format(http.TimeFormat))
modifiedSince := httptest.NewRecorder()
h.ServeHTTP(modifiedSince, modifiedSinceRequest)
if modifiedSince.Code != http.StatusNotModified {
t.Fatalf("If-Modified-Since status = %d, want %d", modifiedSince.Code, http.StatusNotModified)
}
if got := modifiedSince.Header().Get("Last-Modified"); got != lastModified.Format(http.TimeFormat) {
t.Errorf("conditional Last-Modified = %q, want %q", got, lastModified.Format(http.TimeFormat))
}
if requests != 1 {
t.Errorf("upstream requests = %d, want 1", requests)
}
}
func TestHomebrewHandler_HeadPreservesUpstreamMethodWithMetadataCacheEnabled(t *testing.T) {
body := `{"payload":"signed bytes","signatures":[]}`
upstreamMethod := ""
upstreamAuthorization := ""
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upstreamMethod = r.Method
upstreamAuthorization = r.Header.Get("Authorization")
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
w.Header().Set("ETag", `"head-etag"`)
if r.Method != http.MethodHead {
_, _ = io.WriteString(w, body)
}
}))
defer upstream.Close()
proxy, _, _, _ := setupTestProxy(t)
proxy.CacheMetadata = true
proxy.MetadataTTL = time.Hour
proxy.HTTPClient = upstream.Client()
h := NewHomebrewHandler(proxy, upstream.URL+"/api").Routes()
headRequest := httptest.NewRequest(http.MethodHead, "/formula.jws.json", nil)
headRequest.Header.Set("Authorization", "Bearer client-secret")
head := httptest.NewRecorder()
h.ServeHTTP(head, headRequest)
if head.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", head.Code, http.StatusOK)
}
if upstreamMethod != http.MethodHead {
t.Errorf("upstream method = %q, want HEAD", upstreamMethod)
}
if upstreamAuthorization != "" {
t.Errorf("upstream Authorization = %q, want empty", upstreamAuthorization)
}
if head.Body.Len() != 0 {
t.Errorf("body length = %d, want 0", head.Body.Len())
}
if got := head.Header().Get("Content-Length"); got != strconv.Itoa(len(body)) {
t.Errorf("Content-Length = %q, want %d", got, len(body))
}
if got := head.Header().Get("ETag"); got != `"head-etag"` {
t.Errorf("ETag = %q, want %q", got, `"head-etag"`)
}
}
func TestHomebrewHandler_ServesStaleCachedResponseWhenUpstreamFails(t *testing.T) {
body := `{"payload":"signed bytes","signatures":[]}`
available := true
requests := 0
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
requests++
if !available {
http.Error(w, "unavailable", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("ETag", `"stale-etag"`)
_, _ = io.WriteString(w, body)
}))
defer upstream.Close()
proxy, _, _, _ := setupTestProxy(t)
proxy.CacheMetadata = true
proxy.MetadataTTL = 5 * time.Millisecond
proxy.HTTPClient = upstream.Client()
h := NewHomebrewHandler(proxy, upstream.URL+"/api").Routes()
first := httptest.NewRecorder()
h.ServeHTTP(first, httptest.NewRequest(http.MethodGet, "/formula.jws.json", nil))
if first.Code != http.StatusOK {
t.Fatalf("warm status = %d, want %d", first.Code, http.StatusOK)
}
time.Sleep(10 * time.Millisecond)
available = false
stale := httptest.NewRecorder()
h.ServeHTTP(stale, httptest.NewRequest(http.MethodGet, "/formula.jws.json", nil))
if stale.Code != http.StatusOK {
t.Fatalf("stale status = %d, want %d; body: %s", stale.Code, http.StatusOK, stale.Body.String())
}
if got := stale.Body.String(); got != body {
t.Errorf("stale body = %q, want %q", got, body)
}
if got := stale.Header().Get("Warning"); got != containerStaleWarning {
t.Errorf("Warning = %q, want %q", got, containerStaleWarning)
}
if requests != 2 {
t.Errorf("upstream requests = %d, want 2", requests)
}
}
func TestHomebrewHandler_ProxiesSupportedAPIPaths(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, r.URL.RequestURI())
}))
defer upstream.Close()
proxy, _, _, _ := setupTestProxy(t)
proxy.HTTPClient = upstream.Client()
h := NewHomebrewHandler(proxy, upstream.URL+"/api").Routes()
paths := []string{
"/formula.jws.json",
"/cask.jws.json",
"/formula/jq.json",
"/cask/firefox.json",
"/internal/packages.arm64_tahoe.jws.json?download=1",
}
for _, requestPath := range paths {
t.Run(requestPath, func(t *testing.T) {
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, requestPath, nil))
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", w.Code, http.StatusOK)
}
if got, want := w.Body.String(), "/api"+requestPath; got != want {
t.Errorf("upstream request = %q, want %q", got, want)
}
})
}
}
func TestHomebrewHandler_RejectsUnsupportedRequests(t *testing.T) {
proxy, _, _, _ := setupTestProxy(t)
h := NewHomebrewHandler(proxy, "https://example.test/api").Routes()
method := httptest.NewRecorder()
h.ServeHTTP(method, httptest.NewRequest(http.MethodPost, "/formula.jws.json", nil))
if method.Code != http.StatusMethodNotAllowed {
t.Errorf("POST status = %d, want %d", method.Code, http.StatusMethodNotAllowed)
}
if got := method.Header().Get("Allow"); got != "GET, HEAD" {
t.Errorf("Allow = %q, want GET, HEAD", got)
}
root := httptest.NewRecorder()
h.ServeHTTP(root, httptest.NewRequest(http.MethodGet, "/", nil))
if root.Code != http.StatusNotFound {
t.Errorf("root status = %d, want %d", root.Code, http.StatusNotFound)
}
traversal := httptest.NewRecorder()
h.ServeHTTP(traversal, httptest.NewRequest(http.MethodGet, "/%2e%2e/secret", nil))
if traversal.Code != http.StatusNotFound {
t.Errorf("traversal status = %d, want %d", traversal.Code, http.StatusNotFound)
}
}
func TestRegisterHomebrewArtifacts(t *testing.T) {
h := &ContainerHandler{registryURL: dockerHubRegistry}
artifactUpstream := "https://homebrew-proxy.example.com"
RegisterHomebrewArtifacts(h, artifactUpstream+"/")
if got := h.registryURLFor("homebrew/core/jq"); got != artifactUpstream {
t.Errorf("homebrew/core registry = %q, want %q", got, artifactUpstream)
}
if got := h.registryURLFor("homebrew/cask/firefox"); got != "" {
t.Errorf("other Homebrew registry = %q, want blocked", got)
}
if got := h.registryURLFor("library/nginx"); got != dockerHubRegistry {
t.Errorf("unrelated registry = %q, want %q", got, dockerHubRegistry)
}
}
func TestRegisterHomebrewArtifactsRejectsOtherHomebrewRoutes(t *testing.T) {
upstreamRequests := 0
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
upstreamRequests++
_, _ = io.WriteString(w, "unexpected upstream response")
}))
defer upstream.Close()
proxy, _, _, fetcher := setupTestProxy(t)
proxy.HTTPClient = upstream.Client()
fetcher.artifact = &fetch.Artifact{
Body: io.NopCloser(strings.NewReader("unexpected upstream blob")),
ContentType: "application/octet-stream",
}
h := &ContainerHandler{proxy: proxy, registryURL: upstream.URL}
RegisterHomebrewArtifacts(h, "https://ghcr.io")
const digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
paths := []string{
"/homebrew/cask/firefox/blobs/" + digest,
"/homebrew/cask/firefox/manifests/latest",
"/homebrew/cask/firefox/tags/list",
}
for _, path := range paths {
t.Run(path, func(t *testing.T) {
w := httptest.NewRecorder()
h.Routes().ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
if w.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d; body: %s", w.Code, http.StatusNotFound, w.Body.String())
}
})
}
if fetcher.fetchCalled {
t.Error("blocked Homebrew blob reached the artifact fetcher")
}
if upstreamRequests != 0 {
t.Errorf("blocked Homebrew routes made %d upstream requests, want 0", upstreamRequests)
}
}