Skip to content

Commit bcbb883

Browse files
committed
Add failing tests for composer dist URL and shared reference bugs
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.
1 parent 33d99e3 commit bcbb883

File tree

1 file changed

+146
-0
lines changed

1 file changed

+146
-0
lines changed

internal/handler/composer_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package handler
33
import (
44
"encoding/json"
55
"log/slog"
6+
"strings"
67
"testing"
78
"time"
89

@@ -245,6 +246,151 @@ func TestComposerRewriteMetadataCooldownPreservesNames(t *testing.T) {
245246
}
246247
}
247248

249+
func TestComposerRewriteDistURLGitHubZipball(t *testing.T) {
250+
// GitHub zipball URLs end with a bare commit hash, no file extension.
251+
// The proxy must produce a filename with .zip extension so that the
252+
// archives library can detect the format when browsing source.
253+
h := &ComposerHandler{
254+
proxy: testProxy(),
255+
proxyURL: "http://localhost:8080",
256+
}
257+
258+
vmap := map[string]any{
259+
"version": "v7.4.8",
260+
"dist": map[string]any{
261+
"url": "https://api.github.com/repos/symfony/asset/zipball/d2e2f014ccd6ec9fae8dbe6336a4164346a2a856",
262+
"type": "zip",
263+
"shasum": "",
264+
"reference": "d2e2f014ccd6ec9fae8dbe6336a4164346a2a856",
265+
},
266+
}
267+
268+
h.rewriteDistURL(vmap, "symfony/asset", "v7.4.8")
269+
270+
dist := vmap["dist"].(map[string]any)
271+
url := dist["url"].(string)
272+
273+
// The rewritten URL's filename must have a .zip extension
274+
if !strings.HasSuffix(url, ".zip") {
275+
t.Errorf("rewritten dist URL filename has no .zip extension: %s", url)
276+
}
277+
}
278+
279+
func TestComposerRewriteMetadataGitHubZipballFilenames(t *testing.T) {
280+
// End-to-end: metadata with GitHub zipball URLs should produce
281+
// download URLs that end in .zip so browse source can open them.
282+
h := &ComposerHandler{
283+
proxy: testProxy(),
284+
proxyURL: "http://localhost:8080",
285+
}
286+
287+
input := `{
288+
"packages": {
289+
"symfony/config": [
290+
{
291+
"version": "v7.4.8",
292+
"dist": {
293+
"url": "https://api.github.com/repos/symfony/config/zipball/c7369cc1da250fcbfe0c5a9d109e419661549c39",
294+
"type": "zip",
295+
"reference": "c7369cc1da250fcbfe0c5a9d109e419661549c39"
296+
}
297+
}
298+
]
299+
}
300+
}`
301+
302+
output, err := h.rewriteMetadata([]byte(input))
303+
if err != nil {
304+
t.Fatalf("rewriteMetadata failed: %v", err)
305+
}
306+
307+
var result map[string]any
308+
if err := json.Unmarshal(output, &result); err != nil {
309+
t.Fatalf("failed to parse output: %v", err)
310+
}
311+
312+
packages := result["packages"].(map[string]any)
313+
versions := packages["symfony/config"].([]any)
314+
v := versions[0].(map[string]any)
315+
dist := v["dist"].(map[string]any)
316+
url := dist["url"].(string)
317+
318+
if !strings.HasSuffix(url, ".zip") {
319+
t.Errorf("rewritten URL should end in .zip, got %s", url)
320+
}
321+
}
322+
323+
func TestComposerExpandMinifiedSharedDistReferences(t *testing.T) {
324+
// When a minified version inherits the dist field from a previous version
325+
// (i.e. it doesn't include its own dist), expanding + rewriting must not
326+
// corrupt the dist URLs via shared map references.
327+
h := &ComposerHandler{
328+
proxy: testProxy(),
329+
proxyURL: "http://localhost:8080",
330+
}
331+
332+
// In this minified payload, v5.3.0 does NOT include a dist field,
333+
// so it inherits v5.4.0's dist. After expansion and URL rewriting,
334+
// each version must have its own correct dist URL.
335+
input := `{
336+
"minified": "composer/2.0",
337+
"packages": {
338+
"vendor/pkg": [
339+
{
340+
"name": "vendor/pkg",
341+
"version": "5.4.0",
342+
"dist": {
343+
"url": "https://api.github.com/repos/vendor/pkg/zipball/aaa111",
344+
"type": "zip",
345+
"reference": "aaa111"
346+
}
347+
},
348+
{
349+
"version": "5.3.0"
350+
}
351+
]
352+
}
353+
}`
354+
355+
output, err := h.rewriteMetadata([]byte(input))
356+
if err != nil {
357+
t.Fatalf("rewriteMetadata failed: %v", err)
358+
}
359+
360+
var result map[string]any
361+
if err := json.Unmarshal(output, &result); err != nil {
362+
t.Fatalf("failed to parse output: %v", err)
363+
}
364+
365+
packages := result["packages"].(map[string]any)
366+
versions := packages["vendor/pkg"].([]any)
367+
if len(versions) != 2 {
368+
t.Fatalf("expected 2 versions, got %d", len(versions))
369+
}
370+
371+
v1 := versions[0].(map[string]any)
372+
v2 := versions[1].(map[string]any)
373+
374+
dist1 := v1["dist"].(map[string]any)
375+
dist2 := v2["dist"].(map[string]any)
376+
377+
url1 := dist1["url"].(string)
378+
url2 := dist2["url"].(string)
379+
380+
// Each version must have its own URL with its own version in the path
381+
if !strings.Contains(url1, "/5.4.0/") {
382+
t.Errorf("v5.4.0 dist URL should contain /5.4.0/, got %s", url1)
383+
}
384+
if !strings.Contains(url2, "/5.3.0/") {
385+
t.Errorf("v5.3.0 dist URL should contain /5.3.0/, got %s", url2)
386+
}
387+
388+
// The two URLs must be different
389+
if url1 == url2 {
390+
t.Errorf("both versions have the same dist URL (shared reference bug): %s", url1)
391+
}
392+
}
393+
248394
func TestComposerRewriteMetadataCooldown(t *testing.T) {
249395
now := time.Now()
250396
old := now.Add(-10 * 24 * time.Hour).Format(time.RFC3339)

0 commit comments

Comments
 (0)