2026-10-13 13:23:29 +02:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
2026-10-14 13:21:57 +02:00
|
|
|
"io"
|
2026-10-13 13:23:29 +02:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
2026-10-13 13:34:52 +02:00
|
|
|
|
|
|
|
|
"github.com/git-pkgs/cooldown"
|
2026-10-14 13:21:57 +02:00
|
|
|
"github.com/git-pkgs/registries/fetch"
|
2026-10-13 13:23:29 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const requestPackument = `{
|
|
|
|
|
"name": "request",
|
|
|
|
|
"dist-tags": {"latest": "2.88.2"},
|
|
|
|
|
"versions": {
|
|
|
|
|
"2.88.2": {"name": "request", "version": "2.88.2", "dependencies": {"qs": "~6.5.1"},
|
|
|
|
|
"dist": {"tarball": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-upstream2"}},
|
|
|
|
|
"2.88.3": {"name": "request", "version": "2.88.3", "dependencies": {"old": "1.0.0"},
|
|
|
|
|
"dist": {"tarball": "https://registry.npmjs.org/request/-/request-2.88.3.tgz", "integrity": "sha512-upstream3"}}
|
|
|
|
|
},
|
|
|
|
|
"time": {"modified": "2020-02-11T00:00:00Z", "2.88.2": "2020-02-11T00:00:00Z", "2.88.3": "2020-02-12T00:00:00Z"}
|
|
|
|
|
}`
|
|
|
|
|
|
|
|
|
|
type npmHostedFixture struct {
|
2026-10-13 13:42:34 +02:00
|
|
|
proxy *Proxy
|
|
|
|
|
handler http.Handler
|
|
|
|
|
upstreamRequests *int
|
2026-10-14 13:21:57 +02:00
|
|
|
fetcher *mockFetcher
|
2026-10-13 13:23:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newNPMHostedFixture serves packuments from upstream, keyed by request path,
|
|
|
|
|
// and answers 404 for anything else.
|
|
|
|
|
func newNPMHostedFixture(t *testing.T, upstream map[string]string) npmHostedFixture {
|
|
|
|
|
t.Helper()
|
2026-10-13 13:42:34 +02:00
|
|
|
upstreamRequests := 0
|
2026-10-13 13:23:29 +02:00
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2026-10-13 13:42:34 +02:00
|
|
|
upstreamRequests++
|
2026-10-13 13:23:29 +02:00
|
|
|
body, ok := upstream[r.URL.EscapedPath()]
|
|
|
|
|
if !ok {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = w.Write([]byte(body))
|
|
|
|
|
}))
|
|
|
|
|
t.Cleanup(srv.Close)
|
|
|
|
|
|
2026-10-14 13:21:57 +02:00
|
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
2026-10-13 13:23:29 +02:00
|
|
|
proxy.HostedEnabled = true
|
|
|
|
|
proxy.HTTPClient = srv.Client()
|
2026-10-14 13:21:57 +02:00
|
|
|
return npmHostedFixture{proxy: proxy, handler: NewNPMHandler(proxy, "http://proxy.local", srv.URL).Routes(),
|
|
|
|
|
upstreamRequests: &upstreamRequests, fetcher: fetcher}
|
2026-10-13 13:23:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f npmHostedFixture) push(t *testing.T, name, version, manifest string) HostedArtifact {
|
|
|
|
|
t.Helper()
|
Host generic files and share one push path across hosted ecosystems
Hosted ecosystems are now entries in one table: each defines its package
names, how the stored filename is chosen, how the manifest is read and which
namespaces are served from pushes alone. npm and Composer move there
unchanged, and generic is added as a third entry.
Generic files are pushed to /api/hosted/generic/{name}/{version}/{file}
and served without a token from /generic/hosted/{name}/{version}/{file},
with the sha256 as ETag. They have no manifest, a version can hold several
files, and they never come from an upstream. "hosted" is reserved as an
upstream.generic name. DELETE with a file removes only that file.
For all hosted ecosystems:
- A second push of the same file is refused with 409. Clients pin pushed
files by version and often by hash, so a published file is no longer
silently replaced. Delete it first to push it again.
- PUT takes an optional ?sha256= and refuses bytes that don't match.
proxy hosted push always sends it.
Versions of generic files carry no version-level integrity, since it could
only describe one of their files; each file is checked against its own
sha256.
Refs #133, #134, #535
2026-10-20 19:39:14 +02:00
|
|
|
target, err := HostedTargetFor("npm", name, version, "")
|
2026-10-13 13:23:29 +02:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("HostedTargetFor: %v", err)
|
|
|
|
|
}
|
Host generic files and share one push path across hosted ecosystems
Hosted ecosystems are now entries in one table: each defines its package
names, how the stored filename is chosen, how the manifest is read and which
namespaces are served from pushes alone. npm and Composer move there
unchanged, and generic is added as a third entry.
Generic files are pushed to /api/hosted/generic/{name}/{version}/{file}
and served without a token from /generic/hosted/{name}/{version}/{file},
with the sha256 as ETag. They have no manifest, a version can hold several
files, and they never come from an upstream. "hosted" is reserved as an
upstream.generic name. DELETE with a file removes only that file.
For all hosted ecosystems:
- A second push of the same file is refused with 409. Clients pin pushed
files by version and often by hash, so a published file is no longer
silently replaced. Delete it first to push it again.
- PUT takes an optional ?sha256= and refuses bytes that don't match.
proxy hosted push always sends it.
Versions of generic files carry no version-level integrity, since it could
only describe one of their files; each file is checked against its own
sha256.
Refs #133, #134, #535
2026-10-20 19:39:14 +02:00
|
|
|
pushed, err := f.proxy.PushLocalArtifact(context.Background(), "npm", target, bytes.NewReader(npmTarball(manifest)), "", "")
|
2026-10-13 13:23:29 +02:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("PushLocalArtifact: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return pushed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (f npmHostedFixture) get(t *testing.T, path string) *httptest.ResponseRecorder {
|
|
|
|
|
t.Helper()
|
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
|
f.handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
|
|
|
|
return rec
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type npmTestPackument struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
DistTags map[string]string `json:"dist-tags"`
|
|
|
|
|
Versions map[string]npmTestVersion `json:"versions"`
|
|
|
|
|
Time map[string]string `json:"time"`
|
|
|
|
|
Extra map[string]json.RawMessage `json:"-"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type npmTestVersion struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Version string `json:"version"`
|
|
|
|
|
ID string `json:"_id"`
|
|
|
|
|
Dependencies map[string]string `json:"dependencies"`
|
|
|
|
|
Dist struct {
|
|
|
|
|
Tarball string `json:"tarball"`
|
|
|
|
|
Integrity string `json:"integrity"`
|
|
|
|
|
} `json:"dist"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodePackument(t *testing.T, rec *httptest.ResponseRecorder) npmTestPackument {
|
|
|
|
|
t.Helper()
|
|
|
|
|
if rec.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, body %s", rec.Code, rec.Body)
|
|
|
|
|
}
|
|
|
|
|
var doc npmTestPackument
|
|
|
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &doc); err != nil {
|
|
|
|
|
t.Fatalf("decoding packument: %v\n%s", err, rec.Body)
|
|
|
|
|
}
|
|
|
|
|
return doc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNPMMetadataListsPushedVersion(t *testing.T) {
|
|
|
|
|
f := newNPMHostedFixture(t, map[string]string{"/request": requestPackument})
|
|
|
|
|
pushed := f.push(t, "request", "2.88.4", `{"name":"request","version":"2.88.4","dependencies":{"qs":"~6.5.2"}}`)
|
|
|
|
|
|
|
|
|
|
doc := decodePackument(t, f.get(t, "/request"))
|
|
|
|
|
|
|
|
|
|
local, ok := doc.Versions["2.88.4"]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("pushed version missing from versions: %v", doc.Versions)
|
|
|
|
|
}
|
|
|
|
|
if local.Name != "request" || local.Version != "2.88.4" || local.ID != "request@2.88.4" || local.Dependencies["qs"] != "~6.5.2" {
|
|
|
|
|
t.Errorf("pushed version entry = %+v", local)
|
|
|
|
|
}
|
|
|
|
|
if local.Dist.Tarball != "http://proxy.local/npm/request/-/request-2.88.4.tgz" {
|
|
|
|
|
t.Errorf("tarball = %q", local.Dist.Tarball)
|
|
|
|
|
}
|
|
|
|
|
if !strings.HasPrefix(local.Dist.Integrity, "sha512-") {
|
|
|
|
|
t.Errorf("integrity = %q, want the pushed archive's sha512", local.Dist.Integrity)
|
|
|
|
|
}
|
|
|
|
|
if doc.Time["2.88.4"] != pushed.PushedAt.Format(time.RFC3339) {
|
|
|
|
|
t.Errorf("time[2.88.4] = %q, want %q", doc.Time["2.88.4"], pushed.PushedAt.Format(time.RFC3339))
|
|
|
|
|
}
|
|
|
|
|
if doc.Versions["2.88.2"].Dist.Tarball != "http://proxy.local/npm/request/-/request-2.88.2.tgz" {
|
|
|
|
|
t.Errorf("upstream version not rewritten: %+v", doc.Versions["2.88.2"])
|
|
|
|
|
}
|
|
|
|
|
if doc.DistTags["latest"] != "2.88.2" {
|
|
|
|
|
t.Errorf("dist-tags.latest = %q, want upstream's 2.88.2", doc.DistTags["latest"])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNPMMetadataPushedVersionReplacesUpstream(t *testing.T) {
|
|
|
|
|
f := newNPMHostedFixture(t, map[string]string{"/request": requestPackument})
|
|
|
|
|
f.push(t, "request", "2.88.3", `{"name":"request","version":"2.88.3","dependencies":{"qs":"~6.5.2"}}`)
|
|
|
|
|
|
|
|
|
|
rec := f.get(t, "/request")
|
|
|
|
|
if body := rec.Body.String(); strings.Contains(body, "sha512-upstream3") || strings.Count(body, `"2.88.3":{`) != 1 {
|
|
|
|
|
t.Fatalf("upstream entry for the pushed version still listed:\n%s", body)
|
|
|
|
|
}
|
|
|
|
|
local := decodePackument(t, rec).Versions["2.88.3"]
|
|
|
|
|
if local.Dependencies["qs"] != "~6.5.2" || local.Dependencies["old"] != "" {
|
|
|
|
|
t.Errorf("dependencies = %v, want the pushed package.json's", local.Dependencies)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNPMMetadataForPackageOnlyPushed(t *testing.T) {
|
|
|
|
|
f := newNPMHostedFixture(t, nil)
|
|
|
|
|
f.push(t, "@acme/private", "1.10.0", `{"name":"@acme/private","version":"1.10.0"}`)
|
|
|
|
|
f.push(t, "@acme/private", "1.9.0", `{"name":"@acme/private","version":"1.9.0"}`)
|
|
|
|
|
|
|
|
|
|
doc := decodePackument(t, f.get(t, "/@acme/private"))
|
|
|
|
|
if doc.Name != "@acme/private" || len(doc.Versions) != 2 {
|
|
|
|
|
t.Errorf("packument = %+v", doc)
|
|
|
|
|
}
|
|
|
|
|
if doc.DistTags["latest"] != "1.10.0" {
|
|
|
|
|
t.Errorf("dist-tags.latest = %q, want the highest pushed version 1.10.0", doc.DistTags["latest"])
|
|
|
|
|
}
|
|
|
|
|
if doc.Versions["1.9.0"].Dist.Tarball != "http://proxy.local/npm/@acme%2Fprivate/-/private-1.9.0.tgz" {
|
|
|
|
|
t.Errorf("tarball = %q", doc.Versions["1.9.0"].Dist.Tarball)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if rec := f.get(t, "/@acme/other"); rec.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("unknown package status = %d, want 404", rec.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNPMVersionMetadataForPushedVersion(t *testing.T) {
|
|
|
|
|
f := newNPMHostedFixture(t, map[string]string{"/request": requestPackument})
|
|
|
|
|
f.push(t, "request", "2.88.4", `{"name":"request","version":"2.88.4","dependencies":{"qs":"~6.5.2"}}`)
|
|
|
|
|
|
|
|
|
|
rec := f.get(t, "/request/2.88.4")
|
|
|
|
|
if rec.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, body %s", rec.Code, rec.Body)
|
|
|
|
|
}
|
|
|
|
|
var version npmTestVersion
|
|
|
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &version); err != nil {
|
|
|
|
|
t.Fatalf("decoding version: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if version.Version != "2.88.4" || version.Dist.Tarball != "http://proxy.local/npm/request/-/request-2.88.4.tgz" {
|
|
|
|
|
t.Errorf("version document = %+v", version)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNPMMetadataFollowsPushAndDelete(t *testing.T) {
|
|
|
|
|
f := newNPMHostedFixture(t, map[string]string{"/request": requestPackument})
|
|
|
|
|
f.proxy.CacheMetadata = true
|
|
|
|
|
f.proxy.MetadataTTL = time.Hour
|
|
|
|
|
f.proxy.SetMetadataRewriteCacheSize(1 << 20)
|
|
|
|
|
|
|
|
|
|
before := f.get(t, "/request")
|
|
|
|
|
if before.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d", before.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f.push(t, "request", "2.88.4", `{"name":"request","version":"2.88.4"}`)
|
|
|
|
|
pushed := f.get(t, "/request")
|
|
|
|
|
if _, ok := decodePackument(t, pushed).Versions["2.88.4"]; !ok {
|
|
|
|
|
t.Fatal("pushed version not listed while the upstream document is cached")
|
|
|
|
|
}
|
|
|
|
|
if pushed.Header().Get("ETag") == before.Header().Get("ETag") {
|
|
|
|
|
t.Error("ETag unchanged after a push")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := f.proxy.DeleteLocalVersion("npm", "request", "2.88.4"); err != nil {
|
|
|
|
|
t.Fatalf("DeleteLocalVersion: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, ok := decodePackument(t, f.get(t, "/request")).Versions["2.88.4"]; ok {
|
|
|
|
|
t.Error("deleted version still listed")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNPMMetadataIgnoresPushesWhenHostedDisabled(t *testing.T) {
|
|
|
|
|
f := newNPMHostedFixture(t, map[string]string{"/request": requestPackument})
|
|
|
|
|
f.push(t, "request", "2.88.4", `{"name":"request","version":"2.88.4"}`)
|
|
|
|
|
f.proxy.HostedEnabled = false
|
|
|
|
|
|
|
|
|
|
if _, ok := decodePackument(t, f.get(t, "/request")).Versions["2.88.4"]; ok {
|
|
|
|
|
t.Error("pushed version listed with hosted packages disabled")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-13 13:34:52 +02:00
|
|
|
|
2026-10-25 17:32:22 +01:00
|
|
|
// With hosted packages disabled, a pushed archive is not served ahead of the
|
|
|
|
|
// upstream, but it is still never replaced by the upstream copy: the
|
|
|
|
|
// download is answered from its stored record like any cached archive.
|
|
|
|
|
func TestNPMDownloadWithHostedDisabledKeepsPush(t *testing.T) {
|
|
|
|
|
f := newNPMHostedFixture(t, map[string]string{"/request": requestPackument})
|
|
|
|
|
manifest := `{"name":"request","version":"2.88.3"}`
|
|
|
|
|
f.push(t, "request", "2.88.3", manifest)
|
|
|
|
|
f.proxy.HostedEnabled = false
|
|
|
|
|
|
|
|
|
|
rec := f.get(t, "/request/-/request-2.88.3.tgz")
|
|
|
|
|
if rec.Code != http.StatusOK || rec.Body.String() != string(npmTarball(manifest)) {
|
|
|
|
|
t.Errorf("download status = %d, want 200 and the pushed tarball", rec.Code)
|
|
|
|
|
}
|
|
|
|
|
if f.fetcher.fetchCalled {
|
|
|
|
|
t.Error("the upstream tarball was fetched for a pushed version")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 13:34:52 +02:00
|
|
|
func TestNPMDownloadOfPushedPackageWithCooldown(t *testing.T) {
|
|
|
|
|
f := newNPMHostedFixture(t, nil)
|
|
|
|
|
f.proxy.Cooldown = &cooldown.Config{Default: "7d"}
|
|
|
|
|
f.push(t, "@acme/private", "1.0.0", `{"name":"@acme/private","version":"1.0.0"}`)
|
|
|
|
|
|
|
|
|
|
rec := f.get(t, "/@acme/private/-/private-1.0.0.tgz")
|
|
|
|
|
if rec.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("download status = %d, body %s, want the pushed tarball", rec.Code, rec.Body)
|
|
|
|
|
}
|
|
|
|
|
if _, ok := decodePackument(t, f.get(t, "/@acme/private")).Versions["1.0.0"]; !ok {
|
|
|
|
|
t.Error("pushed version missing from the packument with cooldown enabled")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNPMDownloadOfPushedOverrideOfVersionInCooldown(t *testing.T) {
|
|
|
|
|
recent := time.Now().Add(-time.Hour).Format(time.RFC3339)
|
|
|
|
|
packument := `{"name":"request","dist-tags":{"latest":"2.88.2"},
|
|
|
|
|
"versions":{
|
|
|
|
|
"2.88.2":{"name":"request","version":"2.88.2","dist":{"tarball":"https://registry.npmjs.org/request/-/request-2.88.2.tgz"}},
|
|
|
|
|
"2.88.3":{"name":"request","version":"2.88.3","dist":{"tarball":"https://registry.npmjs.org/request/-/request-2.88.3.tgz"}}},
|
|
|
|
|
"time":{"2.88.2":"2020-02-11T00:00:00Z","2.88.3":"` + recent + `"}}`
|
|
|
|
|
f := newNPMHostedFixture(t, map[string]string{"/request": packument})
|
|
|
|
|
f.proxy.Cooldown = &cooldown.Config{Default: "7d"}
|
|
|
|
|
f.push(t, "request", "2.88.3", `{"name":"request","version":"2.88.3"}`)
|
|
|
|
|
|
|
|
|
|
if _, ok := decodePackument(t, f.get(t, "/request")).Versions["2.88.3"]; !ok {
|
|
|
|
|
t.Fatal("pushed version missing from the packument")
|
|
|
|
|
}
|
|
|
|
|
rec := f.get(t, "/request/-/request-2.88.3.tgz")
|
|
|
|
|
if rec.Code != http.StatusOK || rec.Body.String() != string(npmTarball(`{"name":"request","version":"2.88.3"}`)) {
|
|
|
|
|
t.Errorf("download status = %d, want 200 and the pushed tarball: the packument lists it, so the download must not be withheld", rec.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-13 13:42:34 +02:00
|
|
|
|
|
|
|
|
func TestNPMHostedOnlyScopeNeverAsksUpstream(t *testing.T) {
|
|
|
|
|
squatter := `{"name":"@acme/private","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.1":{"name":"@acme/private","version":"1.0.1",
|
|
|
|
|
"dist":{"tarball":"https://registry.npmjs.org/@acme/private/-/private-1.0.1.tgz"}}}}`
|
|
|
|
|
f := newNPMHostedFixture(t, map[string]string{"/@acme%2fprivate": squatter, "/@acme%2Fprivate": squatter, "/request": requestPackument})
|
|
|
|
|
f.proxy.HostedNPMScopes = []string{"@acme"}
|
|
|
|
|
f.push(t, "@acme/private", "1.0.0", `{"name":"@acme/private","version":"1.0.0"}`)
|
|
|
|
|
|
|
|
|
|
doc := decodePackument(t, f.get(t, "/@acme/private"))
|
|
|
|
|
if len(doc.Versions) != 1 || doc.DistTags["latest"] != "1.0.0" {
|
|
|
|
|
t.Errorf("packument = %+v, want only the pushed 1.0.0", doc)
|
|
|
|
|
}
|
|
|
|
|
if rec := f.get(t, "/@acme/private/latest"); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"version":"1.0.0"`) {
|
|
|
|
|
t.Errorf("latest status = %d, body %s", rec.Code, rec.Body)
|
|
|
|
|
}
|
|
|
|
|
if rec := f.get(t, "/@acme/private/1.0.1"); rec.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("unpushed version status = %d, want 404", rec.Code)
|
|
|
|
|
}
|
|
|
|
|
if rec := f.get(t, "/@acme/private/-/private-1.0.1.tgz"); rec.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("unpushed tarball status = %d, want 404", rec.Code)
|
|
|
|
|
}
|
|
|
|
|
if rec := f.get(t, "/@acme/other"); rec.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("unpushed package status = %d, want 404", rec.Code)
|
|
|
|
|
}
|
|
|
|
|
if rec := f.get(t, "/@acme/private/-/private-1.0.0.tgz"); rec.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("pushed tarball status = %d, want 200", rec.Code)
|
|
|
|
|
}
|
|
|
|
|
if *f.upstreamRequests != 0 {
|
|
|
|
|
t.Errorf("hosted-only scope sent %d requests upstream", *f.upstreamRequests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if rec := f.get(t, "/request"); rec.Code != http.StatusOK || *f.upstreamRequests == 0 {
|
|
|
|
|
t.Errorf("package outside the scope status = %d after %d upstream requests, want it fetched upstream", rec.Code, *f.upstreamRequests)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-13 15:16:55 +02:00
|
|
|
|
|
|
|
|
func TestNPMDownloadOfUnreadablePushedTarball(t *testing.T) {
|
|
|
|
|
f := newNPMHostedFixture(t, map[string]string{"/request": requestPackument})
|
|
|
|
|
f.push(t, "request", "2.88.3", `{"name":"request","version":"2.88.3"}`)
|
|
|
|
|
removeStored(t, f.proxy, "pkg:npm/request@2.88.3", "request-2.88.3.tgz")
|
|
|
|
|
|
|
|
|
|
if rec := f.get(t, "/request/-/request-2.88.3.tgz"); rec.Code != http.StatusServiceUnavailable {
|
|
|
|
|
t.Errorf("download status = %d, body %s, want 503 rather than the upstream tarball", rec.Code, rec.Body)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-13 15:18:49 +02:00
|
|
|
|
|
|
|
|
func TestNPMMetadataLeavesOutDeniedPushedVersions(t *testing.T) {
|
|
|
|
|
f := newNPMHostedFixture(t, nil)
|
|
|
|
|
f.push(t, "@acme/private", "1.0.0", `{"name":"@acme/private","version":"1.0.0"}`)
|
|
|
|
|
f.push(t, "@acme/private", "1.1.0", `{"name":"@acme/private","version":"1.1.0"}`)
|
|
|
|
|
setTestDenylist(t, f.proxy, "pkg:npm/@acme/private@1.1.0")
|
|
|
|
|
|
|
|
|
|
doc := decodePackument(t, f.get(t, "/@acme/private"))
|
|
|
|
|
if _, ok := doc.Versions["1.1.0"]; ok || len(doc.Versions) != 1 {
|
|
|
|
|
t.Errorf("versions = %v, want the denied 1.1.0 left out", doc.Versions)
|
|
|
|
|
}
|
|
|
|
|
if doc.DistTags["latest"] != "1.0.0" {
|
|
|
|
|
t.Errorf("dist-tags.latest = %q, want 1.0.0", doc.DistTags["latest"])
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-14 13:21:57 +02:00
|
|
|
|
|
|
|
|
func TestNPMDownloadOfPushedTarballWhileStreamingArtifacts(t *testing.T) {
|
|
|
|
|
const manifest = `{"name":"request","version":"2.88.3"}`
|
|
|
|
|
f := newNPMHostedFixture(t, map[string]string{"/request": requestPackument})
|
|
|
|
|
f.fetcher.artifact = &fetch.Artifact{Body: io.NopCloser(strings.NewReader("upstream tarball")), ContentType: "application/gzip"}
|
|
|
|
|
f.push(t, "request", "2.88.3", manifest)
|
|
|
|
|
f.proxy.StreamArtifacts = true
|
|
|
|
|
|
|
|
|
|
rec := f.get(t, "/request/-/request-2.88.3.tgz")
|
|
|
|
|
if rec.Code != http.StatusOK || rec.Body.String() != string(npmTarball(manifest)) {
|
|
|
|
|
t.Errorf("download status = %d, body %q, want 200 and the pushed tarball", rec.Code, rec.Body)
|
|
|
|
|
}
|
|
|
|
|
if f.fetcher.fetchCalled {
|
|
|
|
|
t.Error("a pushed tarball was fetched upstream while streaming artifacts")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNPMDownloadOfUnreadablePushedTarballWhileStreamingArtifacts(t *testing.T) {
|
|
|
|
|
f := newNPMHostedFixture(t, map[string]string{"/request": requestPackument})
|
|
|
|
|
f.push(t, "request", "2.88.3", `{"name":"request","version":"2.88.3"}`)
|
|
|
|
|
removeStored(t, f.proxy, "pkg:npm/request@2.88.3", "request-2.88.3.tgz")
|
|
|
|
|
f.proxy.StreamArtifacts = true
|
|
|
|
|
|
|
|
|
|
if rec := f.get(t, "/request/-/request-2.88.3.tgz"); rec.Code != http.StatusServiceUnavailable {
|
|
|
|
|
t.Errorf("download status = %d, body %s, want 503 rather than the upstream tarball", rec.Code, rec.Body)
|
|
|
|
|
}
|
|
|
|
|
}
|