Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 07:39:19 -05:00
pkg-proxy/internal/handler/composer_hosted_test.go
Simon Chrzanowski 5c798069a1 Treat a null notification-url as absent for routed packages
Composer falls back to the default repository's notify-batch when a
version's notification-url is null, which would report installs of a
routed private package to Packagist. Replace null with an empty string
as is already done for pushed versions.

Also drop the pushed-version check's local url variable, which shadowed
net/url, and test that a pushed manifest's own URL is kept.
2026-11-08 10:06:20 +01:00

270 lines
11 KiB
Go

package handler
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/git-pkgs/registries/fetch"
)
const monologMetadata = `{"minified":"composer/2.0","packages":{"monolog/monolog":[
{"name":"monolog/monolog","version":"3.5.0","version_normalized":"3.5.0.0",
"require":{"php":">=8.1","psr/log":"^2.0"},"support":{"issues":"https://github.com/Seldaek/monolog/issues"},
"dist":{"type":"zip","url":"https://api.github.com/repos/Seldaek/monolog/zipball/aaa","reference":"aaa","shasum":""},
"time":"2023-10-27T15:32:31+00:00"},
{"version":"3.4.0","version_normalized":"3.4.0.0",
"dist":{"type":"zip","url":"https://api.github.com/repos/Seldaek/monolog/zipball/bbb","reference":"bbb","shasum":""}}
]}}`
const monologDevMetadata = `{"minified":"composer/2.0","packages":{"monolog/monolog":[
{"name":"monolog/monolog","version":"dev-main","version_normalized":"dev-main",
"dist":{"type":"zip","url":"https://api.github.com/repos/Seldaek/monolog/zipball/ccc","reference":"ccc","shasum":""}}
]}}`
type composerHostedFixture struct {
proxy *Proxy
handler http.Handler
upstreamRequests *int
fetcher *mockFetcher
}
func newComposerHostedFixture(t *testing.T, upstream map[string]string) composerHostedFixture {
t.Helper()
upstreamRequests := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upstreamRequests++
body, ok := upstream[r.URL.Path]
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()
h := NewComposerHandlerWithUpstreams(proxy, "http://proxy.local", srv.URL, srv.URL)
return composerHostedFixture{proxy: proxy, handler: h.Routes(), upstreamRequests: &upstreamRequests, fetcher: fetcher}
}
func (f composerHostedFixture) push(t *testing.T, name, version, manifest string) HostedArtifact {
t.Helper()
target, err := HostedTargetFor("composer", name, version, "")
if err != nil {
t.Fatalf("HostedTargetFor: %v", err)
}
pushed, err := f.proxy.PushLocalArtifact(context.Background(), "composer", target, bytes.NewReader(composerZip("", manifest)), "", "")
if err != nil {
t.Fatalf("PushLocalArtifact: %v", err)
}
return pushed
}
func (f composerHostedFixture) versions(t *testing.T, path, packageName string) map[string]map[string]any {
t.Helper()
rec := httptest.NewRecorder()
f.handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
if rec.Code != http.StatusOK {
t.Fatalf("GET %s status = %d, body %s", path, rec.Code, rec.Body)
}
byVersion := map[string]map[string]any{}
for _, v := range composerVersions(t, rec.Body.Bytes(), packageName) {
version, _ := v["version"].(string)
if _, dup := byVersion[version]; dup {
t.Fatalf("version %s listed twice:\n%s", version, rec.Body)
}
byVersion[version] = v
}
return byVersion
}
func TestComposerMetadataListsPushedVersion(t *testing.T) {
f := newComposerHostedFixture(t, map[string]string{"/p2/monolog/monolog.json": monologMetadata})
pushed := f.push(t, "monolog/monolog", "3.6.0", `{"name":"monolog/monolog","require":{"php":">=8.1","psr/log":"^3.0"},"repositories":[{"type":"vcs","url":"x"}]}`)
versions := f.versions(t, "/p2/monolog/monolog.json", "monolog/monolog")
local, ok := versions["3.6.0"]
if !ok {
t.Fatalf("pushed version missing: %v", versions)
}
if require, _ := local["require"].(map[string]any); require["psr/log"] != "^3.0" {
t.Errorf("require = %v, want the pushed composer.json's", local["require"])
}
for _, inherited := range []string{"version_normalized", "support", "repositories"} {
if _, ok := local[inherited]; ok {
t.Errorf("pushed version carries %s = %v", inherited, local[inherited])
}
}
dist, _ := local["dist"].(map[string]any)
if dist["url"] != "http://proxy.local/composer/files/monolog/monolog/3.6.0/monolog-3.6.0.zip" ||
dist["type"] != "zip" || dist["reference"] != pushed.SHA256 || dist["shasum"] != "" {
t.Errorf("dist = %v", dist)
}
if local["time"] != pushed.PushedAt.Format(time.RFC3339) {
t.Errorf("time = %v, want %s", local["time"], pushed.PushedAt.Format(time.RFC3339))
}
if _, ok := versions["3.4.0"]; !ok {
t.Error("upstream version 3.4.0 missing")
}
}
func TestComposerMetadataPushedVersionReplacesUpstream(t *testing.T) {
f := newComposerHostedFixture(t, map[string]string{"/p2/monolog/monolog.json": monologMetadata})
f.push(t, "monolog/monolog", "3.5.0", `{"name":"monolog/monolog","require":{"php":">=8.1","psr/log":"^3.0"}}`)
versions := f.versions(t, "/p2/monolog/monolog.json", "monolog/monolog")
local := versions["3.5.0"]
if require, _ := local["require"].(map[string]any); require["psr/log"] != "^3.0" {
t.Errorf("require = %v, want the pushed composer.json's", local["require"])
}
if dist, _ := local["dist"].(map[string]any); dist["url"] != "http://proxy.local/composer/files/monolog/monolog/3.5.0/monolog-3.5.0.zip" {
t.Errorf("dist = %v", dist)
}
if v34 := versions["3.4.0"]; v34["version_normalized"] != "3.4.0.0" {
t.Errorf("upstream 3.4.0 expanded to %v", v34)
}
}
func TestComposerMetadataSplitsPushedDevVersions(t *testing.T) {
f := newComposerHostedFixture(t, map[string]string{
"/p2/monolog/monolog.json": monologMetadata,
"/p2/monolog/monolog~dev.json": monologDevMetadata,
})
f.push(t, "monolog/monolog", "dev-patched", `{"name":"monolog/monolog"}`)
f.push(t, "monolog/monolog", "3.6.0", `{"name":"monolog/monolog"}`)
tagged := f.versions(t, "/p2/monolog/monolog.json", "monolog/monolog")
dev := f.versions(t, "/p2/monolog/monolog~dev.json", "monolog/monolog")
if _, ok := tagged["dev-patched"]; ok {
t.Error("pushed dev version listed with the tagged versions")
}
if _, ok := dev["dev-patched"]; !ok {
t.Errorf("pushed dev version missing from the dev file: %v", dev)
}
if _, ok := dev["3.6.0"]; ok {
t.Error("pushed tagged version listed with the dev versions")
}
if _, ok := dev["dev-main"]; !ok {
t.Error("upstream dev-main missing")
}
}
func TestComposerMetadataForPackageOnlyPushed(t *testing.T) {
f := newComposerHostedFixture(t, nil)
f.push(t, "acme/private", "1.0.0", `{"name":"acme/private","require":{"php":">=8.1"}}`)
versions := f.versions(t, "/p2/acme/private.json", "acme/private")
if _, ok := versions["1.0.0"]; !ok || len(versions) != 1 {
t.Errorf("versions = %v, want only 1.0.0", versions)
}
rec := httptest.NewRecorder()
f.handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/p2/acme/private~dev.json", nil))
if rec.Code != http.StatusNotFound {
t.Errorf("dev file of a package with no pushed dev versions status = %d, want 404", rec.Code)
}
}
func TestComposerHostedOnlyVendorNeverAsksUpstream(t *testing.T) {
squatter := `{"packages":{"acme/private":[{"name":"acme/private","version":"1.0.1",
"dist":{"type":"zip","url":"https://api.github.com/repos/evil/private/zipball/ddd","reference":"ddd"}}]}}`
f := newComposerHostedFixture(t, map[string]string{
"/p2/acme/private.json": squatter,
"/p2/monolog/monolog.json": monologMetadata,
})
f.proxy.HostedComposerVendors = []string{"acme"}
f.push(t, "acme/private", "1.0.0", `{"name":"acme/private"}`)
versions := f.versions(t, "/p2/acme/private.json", "acme/private")
if _, ok := versions["1.0.0"]; !ok || len(versions) != 1 {
t.Errorf("versions = %v, want only the pushed 1.0.0", versions)
}
for _, path := range []string{"/p2/acme/private~dev.json", "/p2/acme/other.json", "/files/acme/private/1.0.1/ddd.zip"} {
rec := httptest.NewRecorder()
f.handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
if rec.Code != http.StatusNotFound {
t.Errorf("GET %s status = %d, want 404", path, rec.Code)
}
}
if *f.upstreamRequests != 0 {
t.Errorf("hosted-only vendor sent %d requests upstream", *f.upstreamRequests)
}
f.versions(t, "/p2/monolog/monolog.json", "monolog/monolog")
if *f.upstreamRequests == 0 {
t.Error("package outside the vendor was not fetched upstream")
}
}
func TestComposerDownloadOfUnreadablePushedArchive(t *testing.T) {
f := newComposerHostedFixture(t, map[string]string{"/p2/monolog/monolog.json": monologMetadata})
f.push(t, "monolog/monolog", "3.5.0", `{"name":"monolog/monolog"}`)
removeStored(t, f.proxy, "pkg:composer/monolog/monolog@3.5.0", "monolog-3.5.0.zip")
rec := httptest.NewRecorder()
f.handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/files/monolog/monolog/3.5.0/monolog-3.5.0.zip", nil))
if rec.Code != http.StatusServiceUnavailable {
t.Errorf("download status = %d, body %s, want 503 rather than the upstream archive", rec.Code, rec.Body)
}
}
func TestComposerMetadataLeavesOutDeniedPushedVersions(t *testing.T) {
f := newComposerHostedFixture(t, map[string]string{"/p2/monolog/monolog.json": monologMetadata})
f.push(t, "monolog/monolog", "3.6.0", `{"name":"monolog/monolog"}`)
setTestDenylist(t, f.proxy, "pkg:composer/monolog/monolog@3.6.0")
if _, ok := f.versions(t, "/p2/monolog/monolog.json", "monolog/monolog")["3.6.0"]; ok {
t.Error("denied pushed version listed")
}
}
func TestComposerDownloadOfPushedArchiveWhileStreamingArtifacts(t *testing.T) {
const manifest = `{"name":"monolog/monolog"}`
f := newComposerHostedFixture(t, map[string]string{"/p2/monolog/monolog.json": monologMetadata})
f.fetcher.artifact = &fetch.Artifact{Body: io.NopCloser(strings.NewReader("upstream archive")), ContentType: "application/zip"}
f.push(t, "monolog/monolog", "3.5.0", manifest)
f.proxy.StreamArtifacts = true
rec := httptest.NewRecorder()
f.handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/files/monolog/monolog/3.5.0/monolog-3.5.0.zip", nil))
if rec.Code != http.StatusOK || rec.Body.String() != string(composerZip("", manifest)) {
t.Errorf("download status = %d, want 200 and the pushed archive", rec.Code)
}
if f.fetcher.fetchCalled {
t.Error("a pushed archive was fetched upstream while streaming artifacts")
}
}
func TestComposerPushedVersionsDisableDefaultNotification(t *testing.T) {
f := newComposerHostedFixture(t, map[string]string{"/p2/monolog/monolog.json": monologMetadata})
f.proxy.HostedComposerVendors = []string{"acme"}
f.push(t, "monolog/monolog", "3.6.0", `{"name":"monolog/monolog"}`)
f.push(t, "acme/private", "1.0.0", `{"name":"acme/private"}`)
f.push(t, "other/private", "1.0.0", `{"name":"other/private"}`)
// Composer reads a null notification-url as absent.
f.push(t, "acme/nulled", "1.0.0", `{"name":"acme/nulled","notification-url":null}`)
f.push(t, "acme/own", "1.0.0", `{"name":"acme/own","notification-url":"https://acme.example.com/notify"}`)
for _, tc := range []struct{ path, name, version, want string }{
{"/p2/monolog/monolog.json", "monolog/monolog", "3.6.0", ""},
{"/p2/acme/private.json", "acme/private", "1.0.0", ""},
{"/p2/other/private.json", "other/private", "1.0.0", ""},
{"/p2/acme/nulled.json", "acme/nulled", "1.0.0", ""},
// A manifest's own URL is kept.
{"/p2/acme/own.json", "acme/own", "1.0.0", "https://acme.example.com/notify"},
} {
v := f.versions(t, tc.path, tc.name)[tc.version]
if got, ok := v["notification-url"]; !ok || got != tc.want {
t.Errorf("%s %s notification-url = %v (set %v), want %q", tc.name, tc.version, got, ok, tc.want)
}
}
}