mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
Cooldown only filtered the metadata. A composer.lock names the proxy's /files URL directly, so `composer install` fetched versions still inside the cooldown window without ever seeing the filtered metadata. npm already checks the publish time on tarball downloads. - Return the version's publish time with its dist URL, following the minified format's inheritance - Answer 404 for a version in cooldown before fetching its archive - Read the download lookup's metadata through ReadMetadata, so it has the same size limit as other metadata reads
1077 lines
36 KiB
Go
1077 lines
36 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/cooldown"
|
|
"github.com/git-pkgs/purl"
|
|
"github.com/git-pkgs/registries/fetch"
|
|
)
|
|
|
|
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 TestComposerRewriteMetadataMinifiedInheritance(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 keeps the format, so once
|
|
// Composer expands it every version still 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)
|
|
}
|
|
|
|
if result["minified"] != "composer/2.0" {
|
|
t.Errorf("minified = %v, want the format kept", result["minified"])
|
|
}
|
|
|
|
versions := composerVersions(t, output, "symfony/console")
|
|
|
|
// Second version should have inherited the "name" and "description" fields
|
|
v1 := versions[1]
|
|
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)
|
|
}
|
|
|
|
versions := composerVersions(t, output, "symfony/console")
|
|
|
|
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]
|
|
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 TestComposerRewriteMetadataUnset(t *testing.T) {
|
|
h := &ComposerHandler{
|
|
proxy: &Proxy{Logger: slog.Default()},
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
// In the minified format, "__unset" removes a field from the inherited
|
|
// state. v1.29.0 has require-dev, v1.28.0 unsets it, v1.27.0 inherits the
|
|
// unset state. Composer rejects metadata where require-dev (or any link
|
|
// field) is the literal string "__unset" rather than an object.
|
|
input := `{
|
|
"minified": "composer/2.0",
|
|
"packages": {
|
|
"venturecraft/revisionable": [
|
|
{
|
|
"name": "venturecraft/revisionable",
|
|
"version": "1.29.0",
|
|
"require": {"php": ">=5.4"},
|
|
"require-dev": {"orchestra/testbench": "~3.0"},
|
|
"dist": {"url": "https://example.com/a.zip", "type": "zip"}
|
|
},
|
|
{
|
|
"version": "1.28.0",
|
|
"require-dev": "__unset"
|
|
},
|
|
{
|
|
"version": "1.27.0"
|
|
},
|
|
{
|
|
"version": "1.26.0",
|
|
"require-dev": {"foo/bar": "1.0"}
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
output, err := h.rewriteMetadata([]byte(input))
|
|
if err != nil {
|
|
t.Fatalf("rewriteMetadata failed: %v", err)
|
|
}
|
|
|
|
versions := composerVersions(t, output, "venturecraft/revisionable")
|
|
if len(versions) != 4 {
|
|
t.Fatalf("expected 4 versions, got %d", len(versions))
|
|
}
|
|
|
|
byVersion := map[string]map[string]any{}
|
|
for _, vmap := range versions {
|
|
byVersion[vmap["version"].(string)] = vmap
|
|
}
|
|
|
|
if _, ok := byVersion["1.29.0"]["require-dev"].(map[string]any); !ok {
|
|
t.Errorf("1.29.0 require-dev should be an object, got %T", byVersion["1.29.0"]["require-dev"])
|
|
}
|
|
if rd, ok := byVersion["1.28.0"]["require-dev"]; ok {
|
|
t.Errorf("1.28.0 require-dev should be absent, got %v", rd)
|
|
}
|
|
if rd, ok := byVersion["1.27.0"]["require-dev"]; ok {
|
|
t.Errorf("1.27.0 require-dev should be absent (inherited unset), got %v", rd)
|
|
}
|
|
if _, ok := byVersion["1.26.0"]["require-dev"].(map[string]any); !ok {
|
|
t.Errorf("1.26.0 require-dev should be an object, got %T", byVersion["1.26.0"]["require-dev"])
|
|
}
|
|
if _, ok := byVersion["1.27.0"]["require"].(map[string]any); !ok {
|
|
t.Error("1.27.0 should still inherit require from 1.29.0")
|
|
}
|
|
}
|
|
|
|
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 once Composer expands them.
|
|
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)
|
|
}
|
|
|
|
versions := composerVersions(t, output, "symfony/console")
|
|
|
|
// 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 _, vmap := range versions {
|
|
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",
|
|
}
|
|
|
|
url, ok := h.proxyDistURL("symfony/asset", "v7.4.8",
|
|
"https://api.github.com/repos/symfony/asset/zipball/d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", "zip")
|
|
if !ok {
|
|
t.Fatal("dist URL was not rewritten")
|
|
}
|
|
|
|
// 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), rewriting must still give each
|
|
// version a dist URL naming its own version.
|
|
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 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)
|
|
}
|
|
|
|
versions := composerVersions(t, output, "vendor/pkg")
|
|
if len(versions) != 2 {
|
|
t.Fatalf("expected 2 versions, got %d", len(versions))
|
|
}
|
|
|
|
url1 := distURL(t, versions[0])
|
|
url2 := distURL(t, versions[1])
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// TestComposerDownloadDevVersionUsesDevMetadata is a regression test for the
|
|
// bug that made it impossible to install a *-dev dependency from dist.
|
|
//
|
|
// Packagist serves development versions (e.g. "3.x-dev", "dev-master") from a
|
|
// separate "{package}~dev.json" metadata file; the regular "{package}.json"
|
|
// file contains only tagged releases. The download handler used to fetch only
|
|
// the regular file, so it could never find the dist URL for a dev version and
|
|
// returned 404 — causing Composer to silently fall back to a git clone.
|
|
//
|
|
// This test serves both files from a mock upstream and asserts that:
|
|
// - the OLD behavior (regular file only) cannot resolve the dev version, and
|
|
// - the FIXED behavior (consulting the ~dev file) does.
|
|
func TestComposerDownloadDevVersionUsesDevMetadata(t *testing.T) {
|
|
const (
|
|
pkg = "phpmd/phpmd"
|
|
vendor = "phpmd"
|
|
name = "phpmd"
|
|
version = "3.x-dev"
|
|
distURL = "https://api.github.com/repos/phpmd/phpmd/zipball/2a9217f60aaf27bf6ddad9188f254d020ab70745"
|
|
)
|
|
|
|
// Regular metadata: tagged releases only — no dev versions.
|
|
stableBody := `{
|
|
"packages": {
|
|
"phpmd/phpmd": [
|
|
{"version": "2.15.0", "dist": {"url": "https://example.com/2.15.0.zip", "type": "zip"}}
|
|
]
|
|
}
|
|
}`
|
|
|
|
// ~dev metadata: where the 3.x-dev version actually lives.
|
|
devBody := `{
|
|
"packages": {
|
|
"phpmd/phpmd": [
|
|
{"version": "3.x-dev", "dist": {"url": "` + distURL + `", "type": "zip"}}
|
|
]
|
|
}
|
|
}`
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/p2/phpmd/phpmd.json":
|
|
_, _ = w.Write([]byte(stableBody))
|
|
case "/p2/phpmd/phpmd~dev.json":
|
|
_, _ = w.Write([]byte(devBody))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
h := &ComposerHandler{
|
|
proxy: testProxy(),
|
|
repoURL: srv.URL,
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// OLD behavior: fetching only the regular file fails to resolve the dev
|
|
// version, which is what produced the 404 before the fix.
|
|
stableURL := srv.URL + "/p2/phpmd/phpmd.json"
|
|
found, err := h.findDownloadURLFromMetadata(ctx, stableURL, pkg, version)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error fetching regular metadata: %v", err)
|
|
}
|
|
if found.url != "" {
|
|
t.Fatalf("regular metadata unexpectedly contained dev version %q (got %q); "+
|
|
"the test no longer reproduces the original bug", version, found.url)
|
|
}
|
|
|
|
// FIXED behavior: the handler consults the ~dev file (it is first in the
|
|
// candidate list for dev versions) and resolves the dist URL.
|
|
urls := h.metadataURLsForVersion(vendor, name, version)
|
|
if len(urls) == 0 || !strings.HasSuffix(urls[0], "/p2/phpmd/phpmd~dev.json") {
|
|
t.Fatalf("dev version should consult the ~dev metadata file first, got %v", urls)
|
|
}
|
|
|
|
var resolved string
|
|
for _, u := range urls {
|
|
found, err := h.findDownloadURLFromMetadata(ctx, u, pkg, version)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error fetching metadata %q: %v", u, err)
|
|
}
|
|
resolved = found.url
|
|
if resolved != "" {
|
|
break
|
|
}
|
|
}
|
|
|
|
if resolved != distURL {
|
|
t.Errorf("dev version dist URL = %q, want %q", resolved, distURL)
|
|
}
|
|
}
|
|
|
|
const (
|
|
composerCommitA = "1111111111111111111111111111111111111111"
|
|
composerCommitB = "2222222222222222222222222222222222222222"
|
|
)
|
|
|
|
// setupComposerBranch returns a caching Composer handler whose upstream lists
|
|
// distPath as the dist of v/p's dev-master and 1.0.0 and serves archives by
|
|
// request URI.
|
|
func setupComposerBranch(t *testing.T, distPath string, archives map[string]string) (http.Handler, *Proxy) {
|
|
t.Helper()
|
|
var srv *httptest.Server
|
|
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/p2/v/p.json" || r.URL.Path == "/p2/v/p~dev.json" {
|
|
dist := fmt.Sprintf(`"dist":{"url":%q,"type":"zip"}`, srv.URL+distPath)
|
|
_, _ = fmt.Fprintf(w, `{"packages":{"v/p":[{"version":"dev-master",%s},{"version":"1.0.0",%s}]}}`, dist, dist)
|
|
return
|
|
}
|
|
body, ok := archives[r.URL.RequestURI()]
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_, _ = io.WriteString(w, body)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(srv.Client()), fetch.WithMaxRetries(0))
|
|
proxy.Fetcher = fetcher
|
|
t.Cleanup(func() { _ = fetcher.Close() })
|
|
return NewComposerHandlerWithUpstreams(proxy, "http://proxy", srv.URL, srv.URL).Routes(), proxy
|
|
}
|
|
|
|
// cachedComposerFile returns the cached bytes of v/p dev-master's filename,
|
|
// or "" when nothing is cached under it.
|
|
func cachedComposerFile(t *testing.T, proxy *Proxy, filename string) string {
|
|
t.Helper()
|
|
cached, err := proxy.GetCachedArtifact(context.Background(), "composer", "v/p", "dev-master", filename)
|
|
if err != nil {
|
|
t.Fatalf("reading cache for %s: %v", filename, err)
|
|
}
|
|
if cached == nil {
|
|
return ""
|
|
}
|
|
defer func() { _ = cached.Reader.Close() }()
|
|
data, err := io.ReadAll(cached.Reader)
|
|
if err != nil {
|
|
t.Fatalf("reading cached %s: %v", filename, err)
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
// TestComposerDownloadPinnedBranchCommit checks that a lock file pinning an
|
|
// older commit of a branch gets that commit rather than the head the branch's
|
|
// metadata lists now, and that the head is not cached under the older name.
|
|
// The second round is served from the cache.
|
|
func TestComposerDownloadPinnedBranchCommit(t *testing.T) {
|
|
tests := []struct {
|
|
name, distPath, filename, cached string
|
|
}{
|
|
{"github zipball", "/repos/v/p/zipball/%s", "%s.zip", "%s.zip"},
|
|
{"github zipball without .zip", "/repos/v/p/zipball/%s", "%s", "%s"},
|
|
{"bitbucket archive", "/v/p/get/%s.zip", "%s.zip", "%s.zip"},
|
|
{"gitlab archive", "/api/v4/projects/v%%2Fp/repository/archive.zip?sha=%s", "archive.zip?sha=%s", "archive-%s.zip"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
h, proxy := setupComposerBranch(t, fmt.Sprintf(tt.distPath, composerCommitB), map[string]string{
|
|
fmt.Sprintf(tt.distPath, composerCommitA): "commit A",
|
|
fmt.Sprintf(tt.distPath, composerCommitB): "commit B",
|
|
})
|
|
for range 2 {
|
|
for _, c := range []struct{ commit, want string }{
|
|
{composerCommitA, "commit A"},
|
|
{composerCommitB, "commit B"},
|
|
} {
|
|
filename, want := fmt.Sprintf(tt.filename, c.commit), c.want
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/files/v/p/dev-master/"+filename, nil))
|
|
if w.Code != http.StatusOK || w.Body.String() != want {
|
|
t.Errorf("%s: status %d, body %q, want 200 %q", filename, w.Code, w.Body.String(), want)
|
|
}
|
|
cached := fmt.Sprintf(tt.cached, c.commit)
|
|
if got := cachedComposerFile(t, proxy, cached); got != want {
|
|
t.Errorf("%s cached as %q, want %q", cached, got, want)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// composerMovingBranch is an upstream whose dev-master head can move. It
|
|
// serves the archives of commits A and B under distPath and counts how often
|
|
// they are fetched.
|
|
type composerMovingBranch struct {
|
|
h http.Handler
|
|
proxy *Proxy
|
|
url string
|
|
head atomic.Value
|
|
fetches atomic.Int32
|
|
}
|
|
|
|
func setupComposerMovingBranch(t *testing.T, distPath string) *composerMovingBranch {
|
|
t.Helper()
|
|
b := &composerMovingBranch{}
|
|
b.head.Store(composerCommitA)
|
|
archives := map[string]string{
|
|
fmt.Sprintf(distPath, composerCommitA): "commit A",
|
|
fmt.Sprintf(distPath, composerCommitB): "commit B",
|
|
}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/p2/v/p.json" || r.URL.Path == "/p2/v/p~dev.json" {
|
|
dist := b.url + fmt.Sprintf(distPath, b.head.Load())
|
|
_, _ = fmt.Fprintf(w, `{"packages":{"v/p":[{"version":"dev-master","dist":{"url":%q,"type":"zip"}}]}}`, dist)
|
|
return
|
|
}
|
|
body, ok := archives[r.URL.RequestURI()]
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
b.fetches.Add(1)
|
|
_, _ = io.WriteString(w, body)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
b.url = srv.URL
|
|
|
|
b.proxy, _, _, _ = setupTestProxy(t)
|
|
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(srv.Client()), fetch.WithMaxRetries(0))
|
|
b.proxy.Fetcher = fetcher
|
|
t.Cleanup(func() { _ = fetcher.Close() })
|
|
b.h = NewComposerHandlerWithUpstreams(b.proxy, "http://proxy", srv.URL, srv.URL).Routes()
|
|
return b
|
|
}
|
|
|
|
// TestComposerDownloadBranchMovesOn checks a lock file pinning A while the
|
|
// branch moves from A to B: A is cached while it is the head and still served
|
|
// from the cache afterwards, and B, the head a new lock file pins, comes from
|
|
// upstream.
|
|
func TestComposerDownloadBranchMovesOn(t *testing.T) {
|
|
tests := []struct {
|
|
name, distPath, filename, cached string
|
|
}{
|
|
{"github zipball", "/repos/v/p/zipball/%s", "%s.zip", "%s.zip"},
|
|
{"gitlab archive", "/api/v4/projects/v%%2Fp/repository/archive.zip?sha=%s", "archive.zip?sha=%s", "archive-%s.zip"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
b := setupComposerMovingBranch(t, tt.distPath)
|
|
get := func(commit, want string, wantFetches int32) {
|
|
t.Helper()
|
|
filename := fmt.Sprintf(tt.filename, commit)
|
|
w := httptest.NewRecorder()
|
|
b.h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/files/v/p/dev-master/"+filename, nil))
|
|
if w.Code != http.StatusOK || w.Body.String() != want {
|
|
t.Errorf("%s: status %d, body %q, want 200 %q", filename, w.Code, w.Body.String(), want)
|
|
}
|
|
if got := cachedComposerFile(t, b.proxy, fmt.Sprintf(tt.cached, commit)); got != want {
|
|
t.Errorf("%s cached as %q, want %q", filename, got, want)
|
|
}
|
|
if n := b.fetches.Load(); n != wantFetches {
|
|
t.Errorf("after %s: %d upstream fetches, want %d", filename, n, wantFetches)
|
|
}
|
|
}
|
|
get(composerCommitA, "commit A", 1)
|
|
b.head.Store(composerCommitB)
|
|
get(composerCommitA, "commit A", 1)
|
|
get(composerCommitB, "commit B", 2)
|
|
get(composerCommitB, "commit B", 2)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestComposerDownloadGitLabSharedName checks that an archive an older proxy
|
|
// cached under GitLab's shared archive.zip name is dropped once a commit is
|
|
// fetched under its own name.
|
|
func TestComposerDownloadGitLabSharedName(t *testing.T) {
|
|
distPath := "/api/v4/projects/v%%2Fp/repository/archive.zip?sha=%s"
|
|
b := setupComposerMovingBranch(t, distPath)
|
|
shared, err := b.proxy.GetOrFetchArtifactFromURL(context.Background(), "composer", "v/p", "dev-master",
|
|
"archive.zip", b.url+fmt.Sprintf(distPath, composerCommitB))
|
|
if err != nil {
|
|
t.Fatalf("seeding shared entry: %v", err)
|
|
}
|
|
_ = shared.Reader.Close()
|
|
|
|
w := httptest.NewRecorder()
|
|
b.h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/files/v/p/dev-master/archive.zip?sha="+composerCommitA, nil))
|
|
if w.Code != http.StatusOK || w.Body.String() != "commit A" {
|
|
t.Errorf("status %d, body %q, want 200 %q", w.Code, w.Body.String(), "commit A")
|
|
}
|
|
if got := cachedComposerFile(t, b.proxy, "archive.zip"); got != "" {
|
|
t.Errorf("archive.zip still cached as %q", got)
|
|
}
|
|
}
|
|
|
|
// TestComposerDownloadCachedFromOtherCommit checks that an archive an older
|
|
// proxy cached under a commit's name but fetched from another commit's URL is
|
|
// fetched again, while a correct entry is served from the cache.
|
|
func TestComposerDownloadCachedFromOtherCommit(t *testing.T) {
|
|
zipball := "/repos/v/p/zipball/"
|
|
var fetches atomic.Int32
|
|
var srv *httptest.Server
|
|
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/p2/v/p.json", "/p2/v/p~dev.json":
|
|
_, _ = fmt.Fprintf(w, `{"packages":{"v/p":[{"version":"dev-master","dist":{"url":%q,"type":"zip"}}]}}`, srv.URL+zipball+composerCommitB)
|
|
case zipball + composerCommitA:
|
|
fetches.Add(1)
|
|
_, _ = io.WriteString(w, "commit A")
|
|
case zipball + composerCommitB:
|
|
fetches.Add(1)
|
|
_, _ = io.WriteString(w, "commit B")
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(srv.Client()), fetch.WithMaxRetries(0))
|
|
proxy.Fetcher = fetcher
|
|
t.Cleanup(func() { _ = fetcher.Close() })
|
|
h := NewComposerHandlerWithUpstreams(proxy, "http://proxy", srv.URL, srv.URL).Routes()
|
|
|
|
// What proxy versions before the pinned-commit fix stored: the head
|
|
// under the name of the commit the lock file pins.
|
|
stale, err := proxy.GetOrFetchArtifactFromURL(context.Background(), "composer", "v/p", "dev-master",
|
|
composerCommitA+".zip", srv.URL+zipball+composerCommitB)
|
|
if err != nil {
|
|
t.Fatalf("seeding stale entry: %v", err)
|
|
}
|
|
_ = stale.Reader.Close()
|
|
fetches.Store(0)
|
|
|
|
for range 2 {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/files/v/p/dev-master/"+composerCommitA+".zip", nil))
|
|
if w.Code != http.StatusOK || w.Body.String() != "commit A" {
|
|
t.Errorf("status %d, body %q, want 200 %q", w.Code, w.Body.String(), "commit A")
|
|
}
|
|
}
|
|
if got := cachedComposerFile(t, proxy, composerCommitA+".zip"); got != "commit A" {
|
|
t.Errorf("cached as %q, want %q", got, "commit A")
|
|
}
|
|
if n := fetches.Load(); n != 1 {
|
|
t.Errorf("upstream archive fetched %d times, want 1", n)
|
|
}
|
|
}
|
|
|
|
// TestComposerDownloadGitLabTaggedRelease checks that a tagged release on
|
|
// GitLab is only served as the commit its metadata lists.
|
|
func TestComposerDownloadGitLabTaggedRelease(t *testing.T) {
|
|
archive := "/api/v4/projects/v%2Fp/repository/archive.zip?sha="
|
|
h, _ := setupComposerBranch(t, archive+composerCommitB, map[string]string{
|
|
archive + composerCommitA: "commit A",
|
|
archive + composerCommitB: "commit B",
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/files/v/p/1.0.0/archive.zip?sha="+composerCommitA, nil))
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("other commit: status %d, body %q, want 404", w.Code, w.Body.String())
|
|
}
|
|
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/files/v/p/1.0.0/archive.zip?sha="+composerCommitB, nil))
|
|
if w.Code != http.StatusOK || w.Body.String() != "commit B" {
|
|
t.Errorf("listed commit: status %d, body %q, want 200 %q", w.Code, w.Body.String(), "commit B")
|
|
}
|
|
}
|
|
|
|
// TestComposerDownloadBranchWithSlash checks that a branch whose name holds a
|
|
// slash gets a dist URL the download route matches.
|
|
func TestComposerDownloadBranchWithSlash(t *testing.T) {
|
|
const version = "dev-feature/x"
|
|
var srv *httptest.Server
|
|
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/zip/x.zip" {
|
|
_, _ = io.WriteString(w, "feature")
|
|
return
|
|
}
|
|
_, _ = fmt.Fprintf(w, `{"packages":{"v/p":[{"version":%q,"dist":{"url":%q,"type":"zip"}}]}}`, version, srv.URL+"/zip/x.zip")
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(srv.Client()), fetch.WithMaxRetries(0))
|
|
proxy.Fetcher = fetcher
|
|
t.Cleanup(func() { _ = fetcher.Close() })
|
|
h := NewComposerHandlerWithUpstreams(proxy, "http://proxy", srv.URL, srv.URL)
|
|
|
|
distURL, ok := h.proxyDistURL("v/p", version, srv.URL+"/zip/x.zip", "zip")
|
|
if want := "http://proxy/composer/files/v/p/dev-feature%2Fx/x.zip"; !ok || distURL != want {
|
|
t.Fatalf("dist URL = %q, want %q", distURL, want)
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
h.Routes().ServeHTTP(w, httptest.NewRequest(http.MethodGet, strings.TrimPrefix(distURL, "http://proxy/composer"), nil))
|
|
if w.Code != http.StatusOK || w.Body.String() != "feature" {
|
|
t.Errorf("status %d, body %q, want 200 %q", w.Code, w.Body.String(), "feature")
|
|
}
|
|
}
|
|
|
|
// TestComposerDownloadUnlistedFile checks that a file the metadata no longer
|
|
// lists, and that names no commit to fetch instead, is a 404 rather than the
|
|
// listed archive cached under the requested name.
|
|
func TestComposerDownloadUnlistedFile(t *testing.T) {
|
|
tests := []struct {
|
|
name, distPath, listed, stale string
|
|
}{
|
|
{"plain name", "/dist/main.zip", "main.zip", "old.zip"},
|
|
{"escaped name", "/dist/p%2Bdev-main.zip", "p%2Bdev-main.zip", "old.zip"},
|
|
{"gitlab archive with other query", "/api/v4/projects/v%2Fp/repository/archive.zip?sha=" + composerCommitB,
|
|
"archive.zip?sha=" + composerCommitB, "archive.zip?sha=" + composerCommitA + "&path=src"},
|
|
{"branch name for a commit", "/repos/v/p/zipball/" + composerCommitB, composerCommitB + ".zip", "master.zip"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
h, proxy := setupComposerBranch(t, tt.distPath, map[string]string{tt.distPath: "head"})
|
|
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/files/v/p/dev-master/"+tt.stale, nil))
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("%s: status %d, body %q, want 404", tt.stale, w.Code, w.Body.String())
|
|
}
|
|
staleName, _, _ := strings.Cut(tt.stale, "?")
|
|
if got := cachedComposerFile(t, proxy, staleName); got != "" {
|
|
t.Errorf("%s cached as %q, want nothing", staleName, got)
|
|
}
|
|
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/files/v/p/dev-master/"+tt.listed, nil))
|
|
if w.Code != http.StatusOK || w.Body.String() != "head" {
|
|
t.Errorf("%s: status %d, body %q, want 200 %q", tt.listed, w.Code, w.Body.String(), "head")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestComposerDownloadTaggedReleaseCommit checks that a tagged release is only
|
|
// served as the commit its metadata lists, so a client cannot have another
|
|
// commit stored as that release.
|
|
func TestComposerDownloadTaggedReleaseCommit(t *testing.T) {
|
|
zipball := "/repos/v/p/zipball/"
|
|
h, proxy := setupComposerBranch(t, zipball+composerCommitB, map[string]string{
|
|
zipball + composerCommitA: "commit A",
|
|
zipball + composerCommitB: "commit B",
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/files/v/p/1.0.0/"+composerCommitA+".zip", nil))
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("other commit: status %d, body %q, want 404", w.Code, w.Body.String())
|
|
}
|
|
cached, err := proxy.GetCachedArtifact(context.Background(), "composer", "v/p", "1.0.0", composerCommitA+".zip")
|
|
if err != nil || cached != nil {
|
|
t.Errorf("other commit cached as 1.0.0: %v, %v", cached, err)
|
|
}
|
|
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/files/v/p/1.0.0/"+composerCommitB+".zip", nil))
|
|
if w.Code != http.StatusOK || w.Body.String() != "commit B" {
|
|
t.Errorf("listed commit: status %d, body %q, want 200 %q", w.Code, w.Body.String(), "commit B")
|
|
}
|
|
}
|
|
|
|
func TestComposerUpstreamDistURL(t *testing.T) {
|
|
const zipball = "https://api.github.com/repos/v/p/zipball/"
|
|
const gitlab = "https://gitlab.com/api/v4/projects/v%2Fp/repository/archive.zip?sha="
|
|
sha256A := strings.Repeat("a", 64)
|
|
tests := []struct {
|
|
name, listed, distType, filename, want string
|
|
}{
|
|
{"listed file", zipball + composerCommitB, "zip", composerCommitB + ".zip", zipball + composerCommitB},
|
|
{"older commit", zipball + composerCommitB, "zip", composerCommitA + ".zip", zipball + composerCommitA},
|
|
{"sha256 commit", zipball + composerCommitB, "zip", sha256A + ".zip", zipball + sha256A},
|
|
{"bare listed commit", zipball + composerCommitB, "zip", composerCommitB, zipball + composerCommitB},
|
|
{"bare older commit", zipball + composerCommitB, "zip", composerCommitA, zipball + composerCommitA},
|
|
{"tar dist keeps bare name", zipball + composerCommitB, "tar", composerCommitA, zipball + composerCommitA},
|
|
{"short hash", zipball + composerCommitB, "zip", "1111111.zip", ""},
|
|
{"upper-case hash", zipball + composerCommitB, "zip", strings.ToUpper(sha256A) + ".zip", ""},
|
|
{"other name", "https://example.com/dist/main.zip", "zip", "old.zip", ""},
|
|
{"gitlab older commit", gitlab + composerCommitB, "zip", "archive.zip?sha=" + composerCommitA, gitlab + composerCommitA},
|
|
{"gitlab short hash", gitlab + composerCommitB, "zip", "archive.zip?sha=1111111", ""},
|
|
{"gitlab other archive", gitlab + composerCommitB, "zip", "other.zip?sha=" + composerCommitA, ""},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, ok := upstreamDistURL(tt.listed, tt.distType, tt.filename, true)
|
|
if got != tt.want || ok != (tt.want != "") {
|
|
t.Errorf("upstreamDistURL(%q, %q) = %q, %v; want %q", tt.listed, tt.filename, got, ok, tt.want)
|
|
}
|
|
})
|
|
}
|
|
if got, ok := upstreamDistURL(zipball+composerCommitB, "zip", composerCommitA+".zip", false); ok {
|
|
t.Errorf("tagged release rebuilt to %q, want only the listed archive", got)
|
|
}
|
|
if got, ok := upstreamDistURL(gitlab+composerCommitB, "zip", "archive.zip?sha="+composerCommitA, false); ok {
|
|
t.Errorf("tagged GitLab release rebuilt to %q, want only the listed archive", got)
|
|
}
|
|
}
|
|
|
|
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"])
|
|
}
|
|
}
|
|
|
|
func TestComposerRetentionKey(t *testing.T) {
|
|
tests := []struct {
|
|
key string
|
|
want string
|
|
ok bool
|
|
}{
|
|
{"pkg:composer/symfony/console", "pkg:composer/symfony/console", true},
|
|
{"pkg:composer/Symfony/Console", "pkg:composer/symfony/console", true},
|
|
{"pkg:composer/console", "", false},
|
|
{"pkg:composer/symfony/console/extra", "", false},
|
|
{"pkg:npm/symfony/console", "", false},
|
|
}
|
|
for _, tt := range tests {
|
|
p, err := purl.Parse(tt.key)
|
|
if err != nil {
|
|
t.Fatalf("Parse(%s): %v", tt.key, err)
|
|
}
|
|
got, ok := composerRetentionKey(p)
|
|
if ok != tt.ok || (ok && got != tt.want) {
|
|
t.Errorf("composerRetentionKey(%s) = %q, %v, want %q, %v", tt.key, got, ok, tt.want, tt.ok)
|
|
}
|
|
}
|
|
if _, ok := composerRetentionKey(nil); ok {
|
|
t.Error("composerRetentionKey(nil) accepted")
|
|
}
|
|
}
|
|
|
|
// TestComposerDownloadCooldown checks that an archive still in cooldown is
|
|
// withheld when a lock file requests it directly, without the metadata that
|
|
// would have filtered it out. In minified metadata a version inherits the
|
|
// time of the entry before it.
|
|
func TestComposerDownloadCooldown(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)
|
|
|
|
var archiveRequests atomic.Int32
|
|
var srv *httptest.Server
|
|
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
dist := func(version string) string {
|
|
return fmt.Sprintf(`"dist":{"url":%q,"type":"zip"}`, srv.URL+"/archives/"+version+".zip")
|
|
}
|
|
switch r.URL.Path {
|
|
case "/p2/v/p.json":
|
|
_, _ = fmt.Fprintf(w, `{"minified":"composer/2.0","packages":{"v/p":[`+
|
|
`{"version":"2.0.0","time":%q,%s},`+
|
|
`{"version":"1.9.0",%s},`+
|
|
`{"version":"1.0.0","time":%q,%s}]}}`,
|
|
recent, dist("2.0.0"), dist("1.9.0"), old, dist("1.0.0"))
|
|
case "/p2/v/p~dev.json":
|
|
http.NotFound(w, r)
|
|
default:
|
|
archiveRequests.Add(1)
|
|
_, _ = io.WriteString(w, "archive "+r.URL.Path)
|
|
}
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(srv.Client()), fetch.WithMaxRetries(0))
|
|
proxy.Fetcher = fetcher
|
|
t.Cleanup(func() { _ = fetcher.Close() })
|
|
h := NewComposerHandlerWithUpstreams(proxy, "http://proxy", srv.URL, srv.URL).Routes()
|
|
|
|
tests := []struct {
|
|
version string
|
|
wantStatus int
|
|
}{
|
|
{"2.0.0", http.StatusNotFound},
|
|
{"1.9.0", http.StatusNotFound},
|
|
{"1.0.0", http.StatusOK},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.version, func(t *testing.T) {
|
|
archiveRequests.Store(0)
|
|
req := httptest.NewRequest(http.MethodGet, "/files/v/p/"+tt.version+"/"+tt.version+".zip", nil)
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
|
|
if w.Code != tt.wantStatus {
|
|
t.Fatalf("status = %d, want %d (body %q)", w.Code, tt.wantStatus, w.Body.String())
|
|
}
|
|
wantRequests := int32(0)
|
|
if tt.wantStatus == http.StatusOK {
|
|
wantRequests = 1
|
|
}
|
|
if got := archiveRequests.Load(); got != wantRequests {
|
|
t.Errorf("upstream archive requests = %d, want %d", got, wantRequests)
|
|
}
|
|
})
|
|
}
|
|
}
|