forked from mirrors/pkg-proxy
GitHub zipball URLs produce filenames without .zip extension, breaking browse source. Minified version expansion shares nested map references, causing dist URL corruption when versions inherit unchanged dist fields.
445 lines
12 KiB
Go
445 lines
12 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/proxy/internal/cooldown"
|
|
)
|
|
|
|
func TestComposerRewriteMetadata(t *testing.T) {
|
|
h := &ComposerHandler{
|
|
proxy: testProxy(),
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
input := `{
|
|
"packages": {
|
|
"symfony/console": [
|
|
{
|
|
"version": "6.0.0",
|
|
"dist": {
|
|
"url": "https://repo.packagist.org/files/symfony/console/6.0.0/abc123.zip",
|
|
"type": "zip"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
t.Fatalf("failed to parse output: %v", err)
|
|
}
|
|
|
|
packages := result["packages"].(map[string]any)
|
|
versions := packages["symfony/console"].([]any)
|
|
v := versions[0].(map[string]any)
|
|
dist := v["dist"].(map[string]any)
|
|
|
|
expected := "http://localhost:8080/composer/files/symfony/console/6.0.0/abc123.zip"
|
|
if dist["url"] != expected {
|
|
t.Errorf("dist url = %q, want %q", dist["url"], expected)
|
|
}
|
|
}
|
|
|
|
func TestComposerRewriteMetadataExpandsMinified(t *testing.T) {
|
|
h := &ComposerHandler{
|
|
proxy: testProxy(),
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
// Minified format: first version has all fields, subsequent versions
|
|
// only include fields that changed. The proxy must expand this so every
|
|
// version has all fields (including "name").
|
|
input := `{
|
|
"minified": "composer/2.0",
|
|
"packages": {
|
|
"symfony/console": [
|
|
{
|
|
"name": "symfony/console",
|
|
"description": "Symfony Console Component",
|
|
"version": "6.0.0",
|
|
"dist": {
|
|
"url": "https://repo.packagist.org/files/symfony/console/6.0.0/abc123.zip",
|
|
"type": "zip"
|
|
}
|
|
},
|
|
{
|
|
"version": "5.4.0",
|
|
"dist": {
|
|
"url": "https://repo.packagist.org/files/symfony/console/5.4.0/def456.zip",
|
|
"type": "zip"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
t.Fatalf("failed to parse output: %v", err)
|
|
}
|
|
|
|
// The minified key should be removed from output
|
|
if _, ok := result["minified"]; ok {
|
|
t.Error("expected minified key to be removed from output")
|
|
}
|
|
|
|
packages := result["packages"].(map[string]any)
|
|
versions := packages["symfony/console"].([]any)
|
|
|
|
// Second version should have inherited the "name" and "description" fields
|
|
v1 := versions[1].(map[string]any)
|
|
if v1["name"] != "symfony/console" {
|
|
t.Errorf("second version name = %v, want %q", v1["name"], "symfony/console")
|
|
}
|
|
if v1["description"] != "Symfony Console Component" {
|
|
t.Errorf("second version description = %v, want %q", v1["description"], "Symfony Console Component")
|
|
}
|
|
}
|
|
|
|
func TestComposerRewriteMetadataMinifiedDevReset(t *testing.T) {
|
|
h := &ComposerHandler{
|
|
proxy: testProxy(),
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
// The ~dev sentinel resets the inheritance chain for dev versions.
|
|
input := `{
|
|
"minified": "composer/2.0",
|
|
"packages": {
|
|
"symfony/console": [
|
|
{
|
|
"name": "symfony/console",
|
|
"description": "Symfony Console Component",
|
|
"license": ["MIT"],
|
|
"version": "6.0.0",
|
|
"dist": {
|
|
"url": "https://repo.packagist.org/files/symfony/console/6.0.0/abc123.zip",
|
|
"type": "zip"
|
|
}
|
|
},
|
|
"~dev",
|
|
{
|
|
"name": "symfony/console",
|
|
"version": "dev-main",
|
|
"dist": {
|
|
"url": "https://repo.packagist.org/files/symfony/console/dev-main/xyz789.zip",
|
|
"type": "zip"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
t.Fatalf("failed to parse output: %v", err)
|
|
}
|
|
|
|
packages := result["packages"].(map[string]any)
|
|
versions := packages["symfony/console"].([]any)
|
|
|
|
if len(versions) != 2 {
|
|
t.Fatalf("expected 2 versions, got %d", len(versions))
|
|
}
|
|
|
|
// Dev version should NOT have inherited "license" or "description"
|
|
// from the tagged version (the ~dev sentinel resets inheritance).
|
|
devVersion := versions[1].(map[string]any)
|
|
if devVersion["version"] != "dev-main" {
|
|
t.Errorf("dev version = %v, want %q", devVersion["version"], "dev-main")
|
|
}
|
|
if _, ok := devVersion["license"]; ok {
|
|
t.Error("dev version should not have inherited license field after ~dev reset")
|
|
}
|
|
if _, ok := devVersion["description"]; ok {
|
|
t.Error("dev version should not have inherited description field after ~dev reset")
|
|
}
|
|
}
|
|
|
|
func TestComposerRewriteMetadataCooldownPreservesNames(t *testing.T) {
|
|
now := time.Now()
|
|
old := now.Add(-10 * 24 * time.Hour).Format(time.RFC3339)
|
|
veryOld := now.Add(-20 * 24 * time.Hour).Format(time.RFC3339)
|
|
recent := now.Add(-1 * time.Hour).Format(time.RFC3339)
|
|
|
|
proxy := &Proxy{Logger: slog.Default()}
|
|
proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
|
|
h := &ComposerHandler{
|
|
proxy: proxy,
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
// Minified format where "name" only appears in first version.
|
|
// When cooldown filters the first version, remaining versions must
|
|
// still have the "name" field after expansion.
|
|
input := `{
|
|
"minified": "composer/2.0",
|
|
"packages": {
|
|
"symfony/console": [
|
|
{
|
|
"name": "symfony/console",
|
|
"description": "Symfony Console Component",
|
|
"version": "7.0.0",
|
|
"time": "` + recent + `",
|
|
"dist": {"url": "https://repo.packagist.org/7.0.0.zip", "type": "zip"}
|
|
},
|
|
{
|
|
"version": "6.0.0",
|
|
"time": "` + old + `",
|
|
"dist": {"url": "https://repo.packagist.org/6.0.0.zip", "type": "zip"}
|
|
},
|
|
{
|
|
"version": "5.0.0",
|
|
"time": "` + veryOld + `",
|
|
"dist": {"url": "https://repo.packagist.org/5.0.0.zip", "type": "zip"}
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
t.Fatalf("failed to parse output: %v", err)
|
|
}
|
|
|
|
packages := result["packages"].(map[string]any)
|
|
versions := packages["symfony/console"].([]any)
|
|
|
|
// v7.0.0 should be filtered by cooldown, leaving v6.0.0 and v5.0.0
|
|
if len(versions) != 2 {
|
|
t.Fatalf("expected 2 versions after cooldown, got %d", len(versions))
|
|
}
|
|
|
|
// Both remaining versions must have the "name" field
|
|
for _, v := range versions {
|
|
vmap := v.(map[string]any)
|
|
if vmap["name"] != "symfony/console" {
|
|
t.Errorf("version %v missing name field, got %v", vmap["version"], vmap["name"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestComposerRewriteDistURLGitHubZipball(t *testing.T) {
|
|
// GitHub zipball URLs end with a bare commit hash, no file extension.
|
|
// The proxy must produce a filename with .zip extension so that the
|
|
// archives library can detect the format when browsing source.
|
|
h := &ComposerHandler{
|
|
proxy: testProxy(),
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
vmap := map[string]any{
|
|
"version": "v7.4.8",
|
|
"dist": map[string]any{
|
|
"url": "https://api.github.com/repos/symfony/asset/zipball/d2e2f014ccd6ec9fae8dbe6336a4164346a2a856",
|
|
"type": "zip",
|
|
"shasum": "",
|
|
"reference": "d2e2f014ccd6ec9fae8dbe6336a4164346a2a856",
|
|
},
|
|
}
|
|
|
|
h.rewriteDistURL(vmap, "symfony/asset", "v7.4.8")
|
|
|
|
dist := vmap["dist"].(map[string]any)
|
|
url := dist["url"].(string)
|
|
|
|
// The rewritten URL's filename must have a .zip extension
|
|
if !strings.HasSuffix(url, ".zip") {
|
|
t.Errorf("rewritten dist URL filename has no .zip extension: %s", url)
|
|
}
|
|
}
|
|
|
|
func TestComposerRewriteMetadataGitHubZipballFilenames(t *testing.T) {
|
|
// End-to-end: metadata with GitHub zipball URLs should produce
|
|
// download URLs that end in .zip so browse source can open them.
|
|
h := &ComposerHandler{
|
|
proxy: testProxy(),
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
input := `{
|
|
"packages": {
|
|
"symfony/config": [
|
|
{
|
|
"version": "v7.4.8",
|
|
"dist": {
|
|
"url": "https://api.github.com/repos/symfony/config/zipball/c7369cc1da250fcbfe0c5a9d109e419661549c39",
|
|
"type": "zip",
|
|
"reference": "c7369cc1da250fcbfe0c5a9d109e419661549c39"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
t.Fatalf("failed to parse output: %v", err)
|
|
}
|
|
|
|
packages := result["packages"].(map[string]any)
|
|
versions := packages["symfony/config"].([]any)
|
|
v := versions[0].(map[string]any)
|
|
dist := v["dist"].(map[string]any)
|
|
url := dist["url"].(string)
|
|
|
|
if !strings.HasSuffix(url, ".zip") {
|
|
t.Errorf("rewritten URL should end in .zip, got %s", url)
|
|
}
|
|
}
|
|
|
|
func TestComposerExpandMinifiedSharedDistReferences(t *testing.T) {
|
|
// When a minified version inherits the dist field from a previous version
|
|
// (i.e. it doesn't include its own dist), expanding + rewriting must not
|
|
// corrupt the dist URLs via shared map references.
|
|
h := &ComposerHandler{
|
|
proxy: testProxy(),
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
// In this minified payload, v5.3.0 does NOT include a dist field,
|
|
// so it inherits v5.4.0's dist. After expansion and URL rewriting,
|
|
// each version must have its own correct dist URL.
|
|
input := `{
|
|
"minified": "composer/2.0",
|
|
"packages": {
|
|
"vendor/pkg": [
|
|
{
|
|
"name": "vendor/pkg",
|
|
"version": "5.4.0",
|
|
"dist": {
|
|
"url": "https://api.github.com/repos/vendor/pkg/zipball/aaa111",
|
|
"type": "zip",
|
|
"reference": "aaa111"
|
|
}
|
|
},
|
|
{
|
|
"version": "5.3.0"
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
t.Fatalf("failed to parse output: %v", err)
|
|
}
|
|
|
|
packages := result["packages"].(map[string]any)
|
|
versions := packages["vendor/pkg"].([]any)
|
|
if len(versions) != 2 {
|
|
t.Fatalf("expected 2 versions, got %d", len(versions))
|
|
}
|
|
|
|
v1 := versions[0].(map[string]any)
|
|
v2 := versions[1].(map[string]any)
|
|
|
|
dist1 := v1["dist"].(map[string]any)
|
|
dist2 := v2["dist"].(map[string]any)
|
|
|
|
url1 := dist1["url"].(string)
|
|
url2 := dist2["url"].(string)
|
|
|
|
// Each version must have its own URL with its own version in the path
|
|
if !strings.Contains(url1, "/5.4.0/") {
|
|
t.Errorf("v5.4.0 dist URL should contain /5.4.0/, got %s", url1)
|
|
}
|
|
if !strings.Contains(url2, "/5.3.0/") {
|
|
t.Errorf("v5.3.0 dist URL should contain /5.3.0/, got %s", url2)
|
|
}
|
|
|
|
// The two URLs must be different
|
|
if url1 == url2 {
|
|
t.Errorf("both versions have the same dist URL (shared reference bug): %s", url1)
|
|
}
|
|
}
|
|
|
|
func TestComposerRewriteMetadataCooldown(t *testing.T) {
|
|
now := time.Now()
|
|
old := now.Add(-10 * 24 * time.Hour).Format(time.RFC3339)
|
|
recent := now.Add(-1 * time.Hour).Format(time.RFC3339)
|
|
|
|
proxy := &Proxy{Logger: slog.Default()}
|
|
proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
|
|
h := &ComposerHandler{
|
|
proxy: proxy,
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
input := `{
|
|
"packages": {
|
|
"symfony/console": [
|
|
{
|
|
"version": "5.0.0",
|
|
"time": "` + old + `",
|
|
"dist": {"url": "https://repo.packagist.org/5.0.0.zip", "type": "zip"}
|
|
},
|
|
{
|
|
"version": "6.0.0",
|
|
"time": "` + recent + `",
|
|
"dist": {"url": "https://repo.packagist.org/6.0.0.zip", "type": "zip"}
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal(output, &result); err != nil {
|
|
t.Fatalf("failed to parse output: %v", err)
|
|
}
|
|
|
|
packages := result["packages"].(map[string]any)
|
|
versions := packages["symfony/console"].([]any)
|
|
|
|
if len(versions) != 1 {
|
|
t.Fatalf("expected 1 version after cooldown, got %d", len(versions))
|
|
}
|
|
|
|
v := versions[0].(map[string]any)
|
|
if v["version"] != "5.0.0" {
|
|
t.Errorf("expected version 5.0.0, got %v", v["version"])
|
|
}
|
|
}
|