Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-10 08:09:17 -05:00
pkg-proxy/internal/handler/composer_rewrite_test.go
2026-10-19 08:35:46 +01:00

282 lines
10 KiB
Go

package handler
import (
"encoding/json"
"log/slog"
"maps"
"strings"
"testing"
"time"
"github.com/git-pkgs/cooldown"
)
// composerVersions decodes rewritten metadata and returns packageName's
// versions as a Composer client sees them: minified lists are expanded the
// way Composer's MetadataMinifier::expand does it.
func composerVersions(t *testing.T, output []byte, packageName string) []map[string]any {
t.Helper()
var doc struct {
Minified string `json:"minified"`
Packages map[string]any `json:"packages"`
}
if err := json.Unmarshal(output, &doc); err != nil {
t.Fatalf("output is not JSON: %v\n%s", err, output)
}
var versions []map[string]any
var expanded map[string]any
entries, _ := doc.Packages[packageName].([]any)
for _, entry := range entries {
if doc.Minified == "composer/2.0" && entry == "~dev" {
expanded = nil
continue
}
fields, ok := entry.(map[string]any)
if !ok {
t.Fatalf("version entry is not an object: %v", entry)
}
if doc.Minified != "composer/2.0" || expanded == nil {
expanded = fields
versions = append(versions, fields)
continue
}
expanded = maps.Clone(expanded)
for k, v := range fields {
if v == composerUnset {
delete(expanded, k)
continue
}
expanded[k] = v
}
versions = append(versions, expanded)
}
return versions
}
func versionNames(versions []map[string]any) []string {
names := make([]string, len(versions))
for i, v := range versions {
names[i], _ = v["version"].(string)
}
return names
}
func distURL(t *testing.T, version map[string]any) string {
t.Helper()
dist, ok := version["dist"].(map[string]any)
if !ok {
t.Fatalf("version %v has no dist object", version["version"])
}
url, _ := dist["url"].(string)
return url
}
func cooldownComposerHandler() *ComposerHandler {
proxy := &Proxy{Logger: slog.Default()}
proxy.Cooldown = &cooldown.Config{Default: "3d"}
return &ComposerHandler{proxy: proxy, proxyURL: "http://proxy"}
}
// TestComposerRewriteMetadataKeepsUpstreamBytes checks that only dist URLs
// change: numbers encoding/json would round through float64, characters it
// would HTML-escape and the key order all reach the client as upstream sent
// them.
func TestComposerRewriteMetadataKeepsUpstreamBytes(t *testing.T) {
h := &ComposerHandler{proxy: testProxy(), proxyURL: "http://proxy"}
input := `{"packages":{"v/p":[{"version":"1.0.0","extra":{"big":12345678901234567890,"html":"<b>&</b>"},` +
`"dist":{"type":"zip","url":"https://example.com/abc.zip","reference":"abc"}}]},"security-advisories":[]}`
out, _, err := h.rewriteMetadataKeeping([]byte(input))
if err != nil {
t.Fatal(err)
}
want := strings.Replace(input, "https://example.com/abc.zip", "http://proxy/composer/files/v/p/1.0.0/abc.zip", 1)
if string(out) != want {
t.Errorf("rewrite =\n%s\nwant\n%s", out, want)
}
}
// TestComposerRewriteMetadataStaysMinified checks that minified metadata is
// rewritten without expanding it: each entry still carries only what changed
// from the one before, plus its own dist now that dist URLs name the version.
func TestComposerRewriteMetadataStaysMinified(t *testing.T) {
h := &ComposerHandler{proxy: testProxy(), proxyURL: "http://proxy"}
input := `{"minified":"composer/2.0","packages":{"v/p":[` +
`{"name":"v/p","version":"2.0.0","require":{"php": ">=8.1"},"dist":{"url":"https://example.com/a.zip","type":"zip"}},` +
`{"version":"1.1.0","dist":{"url":"https://example.com/b.zip","type":"zip"}},` +
`{"version":"1.0.0","require":"__unset"}` +
`]}}`
out, _, err := h.rewriteMetadataKeeping([]byte(input))
if err != nil {
t.Fatal(err)
}
want := `{"minified":"composer/2.0","packages":{"v/p":[` +
`{"name":"v/p","version":"2.0.0","require":{"php": ">=8.1"},"dist":{"url":"http://proxy/composer/files/v/p/2.0.0/a.zip","type":"zip"}},` +
`{"version":"1.1.0","dist":{"url":"http://proxy/composer/files/v/p/1.1.0/b.zip","type":"zip"}},` +
`{"version":"1.0.0","dist":{"url":"http://proxy/composer/files/v/p/1.0.0/b.zip","type":"zip"},"require":"__unset"}` +
`]}}`
if string(out) != want {
t.Errorf("rewrite =\n%s\nwant\n%s", out, want)
}
}
// TestComposerRewriteMetadataCooldownCarriesChanges checks that the changes a
// filtered entry made to the inherited fields still reach the next entry
// that is kept.
func TestComposerRewriteMetadataCooldownCarriesChanges(t *testing.T) {
old := time.Now().Add(-10 * 24 * time.Hour).Format(time.RFC3339)
recent := time.Now().Add(-1 * time.Hour).Format(time.RFC3339)
input := `{"minified":"composer/2.0","packages":{"v/p":[
{"name":"v/p","description":"Seven","version":"7.0.0","time":"` + recent + `",
"require-dev":{"phpunit/phpunit":"^11"},"dist":{"url":"https://example.com/7.zip","type":"zip"}},
{"version":"6.4.0","time":"` + old + `","dist":{"url":"https://example.com/640.zip","type":"zip"}},
{"description":"Backport","version":"5.4.9","time":"` + recent + `","require-dev":"__unset",
"dist":{"url":"https://example.com/549.zip","type":"zip"}},
{"version":"5.4.8","time":"` + old + `"}
]}}`
out, _, err := cooldownComposerHandler().rewriteMetadataKeeping([]byte(input))
if err != nil {
t.Fatal(err)
}
versions := composerVersions(t, out, "v/p")
if got := versionNames(versions); strings.Join(got, ",") != "6.4.0,5.4.8" {
t.Fatalf("versions = %v, want [6.4.0 5.4.8]", got)
}
v640, v548 := versions[0], versions[1]
if v640["name"] != "v/p" || v640["description"] != "Seven" || v640["require-dev"] == nil {
t.Errorf("6.4.0 lost fields inherited from the filtered 7.0.0: %v", v640)
}
if v548["description"] != "Backport" {
t.Errorf("5.4.8 description = %v, want the one set by the filtered 5.4.9", v548["description"])
}
if _, ok := v548["require-dev"]; ok {
t.Errorf("5.4.8 require-dev = %v, want it unset by the filtered 5.4.9", v548["require-dev"])
}
if got := distURL(t, v548); got != "http://proxy/composer/files/v/p/5.4.8/549.zip" {
t.Errorf("5.4.8 dist url = %q", got)
}
}
// TestComposerRewriteMetadataCooldownAfterDevReset checks that a "~dev"
// reset survives when the first dev entry after it is filtered.
func TestComposerRewriteMetadataCooldownAfterDevReset(t *testing.T) {
old := time.Now().Add(-10 * 24 * time.Hour).Format(time.RFC3339)
recent := time.Now().Add(-1 * time.Hour).Format(time.RFC3339)
input := `{"minified":"composer/2.0","packages":{"v/p":[
{"name":"v/p","license":["MIT"],"version":"1.0.0","time":"` + old + `",
"dist":{"url":"https://example.com/1.zip","type":"zip"}},
"~dev",
{"name":"v/p","description":"Dev","version":"dev-main","time":"` + recent + `",
"dist":{"url":"https://example.com/main.zip","type":"zip"}},
{"version":"1.x-dev","time":"` + old + `"}
]}}`
out, _, err := cooldownComposerHandler().rewriteMetadataKeeping([]byte(input))
if err != nil {
t.Fatal(err)
}
versions := composerVersions(t, out, "v/p")
if got := versionNames(versions); strings.Join(got, ",") != "1.0.0,1.x-dev" {
t.Fatalf("versions = %v, want [1.0.0 1.x-dev]", got)
}
dev := versions[1]
if _, ok := dev["license"]; ok {
t.Error("1.x-dev inherited license across the ~dev reset")
}
if dev["name"] != "v/p" || dev["description"] != "Dev" {
t.Errorf("1.x-dev lost fields inherited from the filtered dev-main: %v", dev)
}
if got := distURL(t, dev); got != "http://proxy/composer/files/v/p/1.x-dev/main.zip" {
t.Errorf("1.x-dev dist url = %q", got)
}
}
func TestComposerRewriteMetadataInvalidJSON(t *testing.T) {
h := &ComposerHandler{proxy: testProxy(), proxyURL: "http://proxy"}
if _, _, err := h.rewriteMetadataKeeping([]byte(`{"packages":{"v/p":[{"version":"1.0.0"}]}`)); err == nil {
t.Error("expected an error for truncated JSON")
}
}
func TestComposerRewriteMetadataWithoutPackages(t *testing.T) {
h := &ComposerHandler{proxy: testProxy(), proxyURL: "http://proxy"}
for _, input := range []string{`{"other":1}`, `{"packages":[]}`} {
out, _, err := h.rewriteMetadataKeeping([]byte(input))
if err != nil {
t.Fatalf("%s: %v", input, err)
}
if string(out) != input {
t.Errorf("rewrite of %s = %s, want it unchanged", input, out)
}
}
}
func TestComposerDistURL(t *testing.T) {
minified := `{"minified":"composer/2.0","packages":{"v/p":[
{"name":"v/p","version":"3.0.0","dist":{"url":"https://example.com/3.zip","type":"zip"}},
{"version":"2.0.0"},
{"version":"1.0.0","dist":"__unset"},
"~dev",
{"version":"dev-main","dist":{"url":"https://example.com/main.zip","type":"zip"}}
]}}`
expanded := `{"packages":{"v/p":[
{"version":"2.0.0","dist":{"url":"https://example.com/2.zip","type":"zip"}},
{"version":"1.0.0"}
]}}`
tests := []struct {
name, body, version, want string
}{
{"own dist", minified, "3.0.0", "https://example.com/3.zip"},
{"inherited dist", minified, "2.0.0", "https://example.com/3.zip"},
{"unset dist", minified, "1.0.0", ""},
{"after dev reset", minified, "dev-main", "https://example.com/main.zip"},
{"missing version", minified, "9.9.9", ""},
{"expanded", expanded, "2.0.0", "https://example.com/2.zip"},
{"expanded inherits nothing", expanded, "1.0.0", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := composerDistURL([]byte(tt.body), "v/p", tt.version)
if err != nil {
t.Fatal(err)
}
if got.url != tt.want {
t.Errorf("composerDistURL(%s) = %q, want %q", tt.version, got.url, tt.want)
}
})
}
}
func TestComposerDistURLMalformed(t *testing.T) {
if _, err := composerDistURL([]byte(`{"packages":{"v/p":[{"version":`), "v/p", "1.0.0"); err == nil {
t.Error("expected an error for truncated JSON")
}
}
// TestComposerRewriteMetadataRewritesEveryPackage checks that each package
// list in the document is rewritten under its own name, as before.
func TestComposerRewriteMetadataRewritesEveryPackage(t *testing.T) {
h := &ComposerHandler{proxy: testProxy(), proxyURL: "http://proxy"}
input := `{"packages":{
"v/a":[{"version":"1.0.0","dist":{"url":"https://example.com/a.zip","type":"zip"}}],
"v/b":[{"version":"2.0.0","dist":{"url":"https://example.com/b.zip","type":"zip"}}],
"v/c":"not a list"
}}`
out, _, err := h.rewriteMetadataKeeping([]byte(input))
if err != nil {
t.Fatal(err)
}
if got := distURL(t, composerVersions(t, out, "v/a")[0]); got != "http://proxy/composer/files/v/a/1.0.0/a.zip" {
t.Errorf("v/a dist url = %q", got)
}
if got := distURL(t, composerVersions(t, out, "v/b")[0]); got != "http://proxy/composer/files/v/b/2.0.0/b.zip" {
t.Errorf("v/b dist url = %q", got)
}
if !strings.Contains(string(out), `"v/c":"not a list"`) {
t.Errorf("non-list package value was not copied unchanged:\n%s", out)
}
}