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

534 lines
14 KiB
Go
Raw Permalink Normal View History

2026-01-20 21:52:44 +00:00
package handler
import (
"encoding/json"
"io"
2026-01-20 21:52:44 +00:00
"log/slog"
"net/http"
"net/http/httptest"
"strings"
2026-01-20 21:52:44 +00:00
"testing"
"time"
"github.com/git-pkgs/cooldown"
"github.com/git-pkgs/registries/fetch"
2026-01-20 21:52:44 +00:00
)
const testVersion100 = "1.0.0"
2026-01-20 21:52:44 +00:00
func testProxy() *Proxy {
return &Proxy{
Logger: slog.Default(),
HTTPClient: http.DefaultClient,
2026-01-20 21:52:44 +00:00
}
}
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
2026-01-20 21:52:44 +00:00
}
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)
}
})
}
2026-01-20 21:52:44 +00:00
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)
2026-01-20 21:52:44 +00:00
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)
}
})
}
2026-01-20 21:52:44 +00:00
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")
}
}