@@ -3,6 +3,7 @@ package handler
33import (
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+
248394func TestComposerRewriteMetadataCooldown (t * testing.T ) {
249395 now := time .Now ()
250396 old := now .Add (- 10 * 24 * time .Hour ).Format (time .RFC3339 )
0 commit comments