mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 23:59:16 -05:00
Every npm and Composer download ran GetLocalArtifact, a full artifact row lookup, even with hosted packages disabled. localVersions already returns early in that case, so metadata does not list pushes, and /generic/hosted/ is only served while hosting is enabled. GetLocalArtifact now returns nil without a lookup when hosting is off. Downloads then take the normal proxy path. An archive pushed earlier is still answered from storage by that path and is never replaced by the upstream copy; only cooldown now applies to it, as metadata no longer lists it either. The hosted docs say so.
369 lines
15 KiB
Go
369 lines
15 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/cooldown"
|
|
"github.com/git-pkgs/registries/fetch"
|
|
)
|
|
|
|
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 {
|
|
proxy *Proxy
|
|
handler http.Handler
|
|
upstreamRequests *int
|
|
fetcher *mockFetcher
|
|
}
|
|
|
|
// 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()
|
|
upstreamRequests := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
upstreamRequests++
|
|
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)
|
|
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
proxy.HostedEnabled = true
|
|
proxy.HTTPClient = srv.Client()
|
|
return npmHostedFixture{proxy: proxy, handler: NewNPMHandler(proxy, "http://proxy.local", srv.URL).Routes(),
|
|
upstreamRequests: &upstreamRequests, fetcher: fetcher}
|
|
}
|
|
|
|
func (f npmHostedFixture) push(t *testing.T, name, version, manifest string) HostedArtifact {
|
|
t.Helper()
|
|
target, err := HostedTargetFor("npm", name, version, "")
|
|
if err != nil {
|
|
t.Fatalf("HostedTargetFor: %v", err)
|
|
}
|
|
pushed, err := f.proxy.PushLocalArtifact(context.Background(), "npm", target, bytes.NewReader(npmTarball(manifest)), "", "")
|
|
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")
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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"])
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|