Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-08-23 04:14:57 -04:00
pkg-proxy/internal/handler/npm_test.go
oscar-broman 4fa903e01e
Enforce cooldown on artifact downloads (#240)
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.
2026-08-10 09:27:03 +01:00

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