Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 23:59:16 -05:00
pkg-proxy/internal/handler/npm_hosted_test.go

369 lines
15 KiB
Go
Raw Permalink Normal View History

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