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) } }