mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-08-23 12:24:57 -04:00
Cooldown filtering only ran when rewriting metadata, so a version could be missing from the npm packument and the PyPI simple index while its tarball stayed reachable. Lockfiles record artifact URLs verbatim, so npm ci and pinned pip requirements reach handleDownload without ever requesting metadata. The shared artifact path has no publish time to check against, since updateCacheDB upserts versions without PublishedAt and the column is only set by enrichment. Each handler now resolves the publish time from metadata it already fetches and returns 404 while a version is inside the window. Versions with no usable publish time are still served, as they are when filtering metadata.
534 lines
14 KiB
Go
534 lines
14 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"
|
|
)
|
|
|
|
const testVersion100 = "1.0.0"
|
|
|
|
func testProxy() *Proxy {
|
|
return &Proxy{
|
|
Logger: slog.Default(),
|
|
HTTPClient: http.DefaultClient,
|
|
}
|
|
}
|
|
|
|
func TestNPMExtractVersionFromFilename(t *testing.T) {
|
|
h := &NPMHandler{}
|
|
|
|
tests := []struct {
|
|
packageName string
|
|
filename string
|
|
want string
|
|
}{
|
|
{"lodash", "lodash-4.17.21.tgz", "4.17.21"},
|
|
{"@babel/core", "core-7.23.0.tgz", "7.23.0"},
|
|
{"@types/node", "node-20.10.0.tgz", "20.10.0"},
|
|
{"express", "express-4.18.2.tgz", "4.18.2"},
|
|
{"lodash", "lodash.tgz", ""}, // no version
|
|
{"lodash", "lodash-4.17.21.zip", ""}, // wrong extension
|
|
{"lodash", "other-4.17.21.tgz", ""}, // wrong package name
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := h.extractVersionFromFilename(tt.packageName, tt.filename)
|
|
if got != tt.want {
|
|
t.Errorf("extractVersionFromFilename(%q, %q) = %q, want %q",
|
|
tt.packageName, tt.filename, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNPMHandlerUsesConfiguredUpstream(t *testing.T) {
|
|
t.Run("metadata", 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 npm-token" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"versions":{}}`)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.HTTPClient = upstream.Client()
|
|
proxy.AuthForURL = func(string) (string, string) {
|
|
return "Authorization", "Bearer npm-token"
|
|
}
|
|
h := NewNPMHandler(proxy, "http://proxy.test", upstream.URL+"/root/")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/testpkg", 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 != "/root/testpkg" {
|
|
t.Errorf("upstream path = %q, want %q", requestPath, "/root/testpkg")
|
|
}
|
|
if authHeader != "Bearer npm-token" {
|
|
t.Errorf("Authorization = %q, want %q", authHeader, "Bearer npm-token")
|
|
}
|
|
})
|
|
|
|
t.Run("download", func(t *testing.T) {
|
|
proxy, _, _, artifactFetcher := setupTestProxy(t)
|
|
artifactFetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("package")),
|
|
ContentType: "application/gzip",
|
|
}
|
|
h := NewNPMHandler(proxy, "http://proxy.test", "https://npm.example.test/root/")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/testpkg/-/testpkg-1.0.0.tgz", 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://npm.example.test/root/testpkg/-/testpkg-1.0.0.tgz"
|
|
if artifactFetcher.fetchedURL != want {
|
|
t.Errorf("fetched URL = %q, want %q", artifactFetcher.fetchedURL, want)
|
|
}
|
|
})
|
|
|
|
t.Run("scoped download", func(t *testing.T) {
|
|
proxy, _, _, artifactFetcher := setupTestProxy(t)
|
|
artifactFetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("package")),
|
|
ContentType: "application/gzip",
|
|
}
|
|
h := NewNPMHandler(proxy, "http://proxy.test", "https://npm.example.test/root/")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/@scope/name/-/name-1.0.0.tgz", 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://npm.example.test/root/@scope/name/-/name-1.0.0.tgz"
|
|
if artifactFetcher.fetchedURL != want {
|
|
t.Errorf("fetched URL = %q, want %q", artifactFetcher.fetchedURL, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNPMRewriteMetadata(t *testing.T) {
|
|
h := &NPMHandler{
|
|
proxy: testProxy(),
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
input := `{
|
|
"name": "lodash",
|
|
"versions": {
|
|
"4.17.21": {
|
|
"name": "lodash",
|
|
"version": "4.17.21",
|
|
"dist": {
|
|
"tarball": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
|
"shasum": "abc123"
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata("lodash", []byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
t.Fatalf("failed to parse output: %v", err)
|
|
}
|
|
|
|
versions := result["versions"].(map[string]any)
|
|
v := versions["4.17.21"].(map[string]any)
|
|
dist := v["dist"].(map[string]any)
|
|
tarball := dist["tarball"].(string)
|
|
|
|
expected := "http://localhost:8080/npm/lodash/-/lodash-4.17.21.tgz"
|
|
if tarball != expected {
|
|
t.Errorf("tarball = %q, want %q", tarball, expected)
|
|
}
|
|
}
|
|
|
|
func TestNPMRewriteMetadataScopedPackage(t *testing.T) {
|
|
h := &NPMHandler{
|
|
proxy: testProxy(),
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
input := `{
|
|
"name": "@babel/core",
|
|
"versions": {
|
|
"7.23.0": {
|
|
"name": "@babel/core",
|
|
"version": "7.23.0",
|
|
"dist": {
|
|
"tarball": "https://registry.npmjs.org/@babel/core/-/core-7.23.0.tgz"
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata("@babel/core", []byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
t.Fatalf("failed to parse output: %v", err)
|
|
}
|
|
|
|
versions := result["versions"].(map[string]any)
|
|
v := versions["7.23.0"].(map[string]any)
|
|
dist := v["dist"].(map[string]any)
|
|
tarball := dist["tarball"].(string)
|
|
|
|
expected := "http://localhost:8080/npm/@babel%2Fcore/-/core-7.23.0.tgz"
|
|
if tarball != expected {
|
|
t.Errorf("tarball = %q, want %q", tarball, expected)
|
|
}
|
|
}
|
|
|
|
func TestNPMHandlerMetadataProxy(t *testing.T) {
|
|
// Create a mock upstream server
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/testpkg" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"name": "testpkg",
|
|
"versions": {
|
|
"1.0.0": {
|
|
"name": "testpkg",
|
|
"version": "1.0.0",
|
|
"dist": {
|
|
"tarball": "https://registry.npmjs.org/testpkg/-/testpkg-1.0.0.tgz"
|
|
}
|
|
}
|
|
}
|
|
}`))
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &NPMHandler{
|
|
proxy: testProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
// Test metadata request
|
|
req := httptest.NewRequest(http.MethodGet, "/testpkg", nil)
|
|
req.SetPathValue("name", "testpkg")
|
|
|
|
w := httptest.NewRecorder()
|
|
h.handlePackageMetadata(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
// Check that tarball URL was rewritten
|
|
versions := result["versions"].(map[string]any)
|
|
v := versions[testVersion100].(map[string]any)
|
|
dist := v["dist"].(map[string]any)
|
|
tarball := dist["tarball"].(string)
|
|
|
|
if tarball != "http://proxy.local/npm/testpkg/-/testpkg-1.0.0.tgz" {
|
|
t.Errorf("tarball URL not rewritten correctly: %s", tarball)
|
|
}
|
|
}
|
|
|
|
func TestNPMRewriteMetadataCooldown(t *testing.T) {
|
|
now := time.Now()
|
|
old := now.Add(-10 * 24 * time.Hour).Format(time.RFC3339)
|
|
recent := now.Add(-1 * time.Hour).Format(time.RFC3339)
|
|
|
|
proxy := testProxy()
|
|
proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
|
|
h := &NPMHandler{
|
|
proxy: proxy,
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
input := `{
|
|
"name": "testpkg",
|
|
"dist-tags": {"latest": "2.0.0"},
|
|
"time": {
|
|
"1.0.0": "` + old + `",
|
|
"2.0.0": "` + recent + `"
|
|
},
|
|
"versions": {
|
|
"1.0.0": {
|
|
"name": "testpkg",
|
|
"version": "1.0.0",
|
|
"dist": {
|
|
"tarball": "https://registry.npmjs.org/testpkg/-/testpkg-1.0.0.tgz"
|
|
}
|
|
},
|
|
"2.0.0": {
|
|
"name": "testpkg",
|
|
"version": "2.0.0",
|
|
"dist": {
|
|
"tarball": "https://registry.npmjs.org/testpkg/-/testpkg-2.0.0.tgz"
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata("testpkg", []byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
t.Fatalf("failed to parse output: %v", err)
|
|
}
|
|
|
|
versions := result["versions"].(map[string]any)
|
|
|
|
// Old version should remain
|
|
if _, ok := versions[testVersion100]; !ok {
|
|
t.Error("version 1.0.0 should not be filtered")
|
|
}
|
|
|
|
// Recent version should be filtered
|
|
if _, ok := versions["2.0.0"]; ok {
|
|
t.Error("version 2.0.0 should be filtered by cooldown")
|
|
}
|
|
|
|
// dist-tags.latest should be updated to 1.0.0
|
|
distTags := result["dist-tags"].(map[string]any)
|
|
if distTags["latest"] != testVersion100 {
|
|
t.Errorf("dist-tags.latest = %q, want %q", distTags["latest"], testVersion100)
|
|
}
|
|
}
|
|
|
|
func TestNPMRewriteMetadataCooldownExemptPackage(t *testing.T) {
|
|
now := time.Now()
|
|
recent := now.Add(-1 * time.Hour).Format(time.RFC3339)
|
|
|
|
proxy := testProxy()
|
|
proxy.Cooldown = &cooldown.Config{
|
|
Default: "3d",
|
|
Packages: map[string]string{"pkg:npm/testpkg": "0"},
|
|
}
|
|
|
|
h := &NPMHandler{
|
|
proxy: proxy,
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
input := `{
|
|
"name": "testpkg",
|
|
"time": {"1.0.0": "` + recent + `"},
|
|
"versions": {
|
|
"1.0.0": {
|
|
"name": "testpkg",
|
|
"version": "1.0.0",
|
|
"dist": {"tarball": "https://registry.npmjs.org/testpkg/-/testpkg-1.0.0.tgz"}
|
|
}
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata("testpkg", []byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
t.Fatalf("failed to parse output: %v", err)
|
|
}
|
|
|
|
versions := result["versions"].(map[string]any)
|
|
if _, ok := versions[testVersion100]; !ok {
|
|
t.Error("exempt package version should not be filtered")
|
|
}
|
|
}
|
|
|
|
func TestNPMHandlerUsesAbbreviatedMetadata(t *testing.T) {
|
|
var gotAccept string
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotAccept = r.Header.Get("Accept")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"name": "testpkg",
|
|
"versions": {
|
|
"1.0.0": {
|
|
"name": "testpkg",
|
|
"version": "1.0.0",
|
|
"dist": {
|
|
"tarball": "https://registry.npmjs.org/testpkg/-/testpkg-1.0.0.tgz"
|
|
}
|
|
}
|
|
}
|
|
}`))
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
t.Run("no cooldown uses combined accept header", func(t *testing.T) {
|
|
h := &NPMHandler{
|
|
proxy: testProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/testpkg", nil)
|
|
w := httptest.NewRecorder()
|
|
h.handlePackageMetadata(w, req)
|
|
|
|
if gotAccept != npmAcceptDefault {
|
|
t.Errorf("Accept = %q, want %q", gotAccept, npmAcceptDefault)
|
|
}
|
|
})
|
|
|
|
t.Run("cooldown enabled uses full metadata only", func(t *testing.T) {
|
|
proxy := testProxy()
|
|
proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
|
|
h := &NPMHandler{
|
|
proxy: proxy,
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/testpkg", nil)
|
|
w := httptest.NewRecorder()
|
|
h.handlePackageMetadata(w, req)
|
|
|
|
if gotAccept != contentTypeJSON {
|
|
t.Errorf("Accept = %q, want %q (cooldown requires full metadata)", gotAccept, contentTypeJSON)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNPMHandlerMetadataNotFound(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &NPMHandler{
|
|
proxy: testProxy(),
|
|
upstreamURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil)
|
|
req.SetPathValue("name", "nonexistent")
|
|
|
|
w := httptest.NewRecorder()
|
|
h.handlePackageMetadata(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func TestNPMDownloadCooldown(t *testing.T) {
|
|
now := time.Now()
|
|
packument := `{
|
|
"name": "leftpad",
|
|
"dist-tags": {"latest": "2.0.0"},
|
|
"time": {
|
|
"1.0.0": "` + now.Add(-30*24*time.Hour).Format(time.RFC3339) + `",
|
|
"2.0.0": "` + now.Add(-1*time.Hour).Format(time.RFC3339) + `"
|
|
},
|
|
"versions": {"1.0.0": {}, "2.0.0": {}}
|
|
}`
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", contentTypeJSON)
|
|
_, _ = io.WriteString(w, packument)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
tests := []struct {
|
|
name string
|
|
version string
|
|
wantStatus int
|
|
}{
|
|
{"published before the window serves the tarball", testVersion100, http.StatusOK},
|
|
{"published inside the window is withheld", "2.0.0", http.StatusNotFound},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
proxy.HTTPClient = upstream.Client()
|
|
proxy.Cooldown = &cooldown.Config{Default: "7d"}
|
|
fetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("tarball data")),
|
|
ContentType: "application/octet-stream",
|
|
}
|
|
|
|
h := NewNPMHandler(proxy, "http://proxy.test", upstream.URL)
|
|
srv := httptest.NewServer(h.Routes())
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Get(srv.URL + "/leftpad/-/leftpad-" + tt.version + ".tgz")
|
|
if err != nil {
|
|
t.Fatalf("request failed: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != tt.wantStatus {
|
|
t.Errorf("status = %d, want %d", resp.StatusCode, tt.wantStatus)
|
|
}
|
|
if tt.wantStatus == http.StatusNotFound && fetcher.fetchCalled {
|
|
t.Error("fetched a version that is still inside the cooldown window")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNPMDownloadCooldownDisabled(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
t.Error("metadata must not be fetched when cooldown is disabled")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
proxy.HTTPClient = upstream.Client()
|
|
fetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("tarball data")),
|
|
ContentType: "application/octet-stream",
|
|
}
|
|
|
|
h := NewNPMHandler(proxy, "http://proxy.test", upstream.URL)
|
|
|
|
if h.versionInCooldown(httptest.NewRequest(http.MethodGet, "/", nil), "leftpad", testVersion100) {
|
|
t.Error("versionInCooldown = true, want false when cooldown is not configured")
|
|
}
|
|
}
|