mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-06-02 08:38:17 -04:00
- Bump github.com/git-pkgs/registries to v0.6.0: the fetcher now honours HTTP_PROXY, gates dialled IPs against the safehttp block list, and Version.Integrity is populated for pub, julia and nuget - Replace internal/cooldown with github.com/git-pkgs/cooldown v0.1.1 (identical surface, lifted from this repo) - Update docs/architecture.md to point at the external package
445 lines
12 KiB
Go
445 lines
12 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/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"])
|
|
}
|
|
}
|