package npm import ( "encoding/json" "fmt" "github.com/git-pkgs/cooldown" "strings" "testing" "time" ) // TestNPMRewriteMetadataKeepsUpstreamBytes checks that only tarball URLs // change: everything else, including numbers encoding/json would round // through float64 and characters it would HTML-escape, is copied verbatim. func TestNPMRewriteMetadataKeepsUpstreamBytes(t *testing.T) { options := Options{ProxyURL: "http://proxy"} input := `{"name":"demo","_big":12345678901234567890,"readme":"&",` + `"versions":{"1.0.0":{"version":"1.0.0","dist":{"shasum":"s","tarball":"https://registry.npmjs.org/demo/-/demo-1.0.0.tgz","integrity":"i"}}}}` out, _, err := Rewrite("demo", []byte(input), options) if err != nil { t.Fatal(err) } want := strings.Replace(input, "https://registry.npmjs.org/demo/-/demo-1.0.0.tgz", "http://proxy/npm/demo/-/demo-1.0.0.tgz", 1) if string(out) != want { t.Errorf("rewrite =\n%s\nwant\n%s", out, want) } } func TestNPMRewriteMetadataUnusualEntries(t *testing.T) { options := Options{ProxyURL: "http://proxy"} input := `{ "versions": { "1.0.0": "not an object", "2.0.0": {"dist": "no object either"}, "3.0.0": {"dist": {"tarball": 7}}, "4.0.0": {"dist": {"tarball": "https://registry.npmjs.org/demo/-/renamed.tgz"}}, "5.0.0": {"dist": {}} } }` out, _, err := Rewrite("demo", []byte(input), options) if err != nil { t.Fatal(err) } var doc struct { Versions map[string]json.RawMessage `json:"versions"` } if err := json.Unmarshal(out, &doc); err != nil { t.Fatalf("output is not JSON: %v\n%s", err, out) } for v, want := range map[string]string{ "1.0.0": `"not an object"`, "2.0.0": `{"dist": "no object either"}`, "3.0.0": `{"dist": {"tarball": 7}}`, "4.0.0": `{"dist": {"tarball": "http://proxy/npm/demo/-/demo-4.0.0.tgz"}}`, "5.0.0": `{"dist": {}}`, } { if got := string(doc.Versions[v]); got != want { t.Errorf("%s = %s, want %s", v, got, want) } } } func TestNPMRewriteMetadataInvalidJSON(t *testing.T) { options := Options{ProxyURL: "http://proxy"} for _, input := range []string{`{"versions": {}`, `[1]`, `{"versions": {}} trailing`} { if _, _, err := Rewrite("demo", []byte(input), options); err == nil { t.Errorf("%s: expected an error", input) } } } func TestNPMRewriteMetadataNoVersions(t *testing.T) { options := Options{ProxyURL: "http://proxy"} input := `{"name":"demo","versions":[]}` out, _, err := Rewrite("demo", []byte(input), options) if err != nil || string(out) != input { t.Errorf("rewrite = %s, %v; want input unchanged", out, err) } options.DeniedVersions = map[string]bool{"1.0.0": true} if _, _, err := Rewrite("demo", []byte(input), options); err == nil { t.Error("expected an error when a denylisted package has no versions object") } } // TestNPMRewriteMetadataCooldownAndDenylist runs both filters together and // checks that versions, their time entries and the tags pointing at them are // all removed, with latest moved to the newest version left. func TestNPMRewriteMetadataCooldownAndDenylist(t *testing.T) { now := time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC) ts := func(age time.Duration) string { return now.Add(-age).UTC().Format(time.RFC3339) } options := Options{ProxyURL: "http://proxy", EvaluatedAt: time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC)} options.Cooldown = &cooldown.Config{Default: "3d"} options.DeniedVersions = map[string]bool{"2.0.0": true} version := func(v string) string { return fmt.Sprintf(`%q:{"version":%q,"dist":{"tarball":"https://registry.npmjs.org/demo/-/demo-%s.tgz"}}`, v, v, v) } input := `{"name":"demo",` + `"dist-tags":{"latest":"3.0.0","beta":"2.0.0","next":"1.0.0"},` + `"versions":{` + version("1.0.0") + `,` + version("2.0.0") + `,` + version("3.0.0") + `},` + `"time":{"created":"` + ts(1000*time.Hour) + `","1.0.0":"` + ts(900*time.Hour) + `","2.0.0":"` + ts(800*time.Hour) + `","3.0.0":"` + ts(time.Hour) + `"}}` out, _, err := Rewrite("demo", []byte(input), options) if err != nil { t.Fatal(err) } var doc struct { DistTags map[string]string `json:"dist-tags"` Versions map[string]json.RawMessage `json:"versions"` Time map[string]string `json:"time"` } if err := json.Unmarshal(out, &doc); err != nil { t.Fatalf("output is not JSON: %v\n%s", err, out) } if len(doc.Versions) != 1 || doc.Versions["1.0.0"] == nil { t.Errorf("versions = %v, want only 1.0.0", doc.Versions) } if _, ok := doc.Time["2.0.0"]; ok { t.Error("time still lists denylisted 2.0.0") } if _, ok := doc.Time["3.0.0"]; ok { t.Error("time still lists 3.0.0, which is inside cooldown") } if _, ok := doc.Time["created"]; !ok { t.Error("time lost its created entry") } want := map[string]string{"latest": "1.0.0", "next": "1.0.0"} if fmt.Sprint(doc.DistTags) != fmt.Sprint(want) { t.Errorf("dist-tags = %v, want %v", doc.DistTags, want) } if !strings.Contains(string(doc.Versions["1.0.0"]), "http://proxy/npm/demo/-/demo-1.0.0.tgz") { t.Errorf("1.0.0 tarball not rewritten: %s", doc.Versions["1.0.0"]) } } // TestNPMRewriteMetadataDuplicateKeys checks that a repeated top-level key // cannot smuggle an unfiltered copy of the versions past the filters. func TestNPMRewriteMetadataDuplicateKeys(t *testing.T) { options := Options{ProxyURL: "http://proxy", EvaluatedAt: time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC)} options.DeniedVersions = map[string]bool{"2.0.0": true} input := `{"versions":{"2.0.0":{"dist":{"tarball":"https://registry.npmjs.org/demo/-/demo-2.0.0.tgz"}}},` + `"versions":{"1.0.0":{"dist":{"tarball":"https://registry.npmjs.org/demo/-/demo-1.0.0.tgz"}},"2.0.0":{"dist":{"tarball":"https://registry.npmjs.org/demo/-/demo-2.0.0.tgz"}}}}` out, _, err := Rewrite("demo", []byte(input), options) if err != nil { t.Fatal(err) } if strings.Contains(string(out), "2.0.0") || strings.Count(string(out), `"versions"`) != 1 { t.Errorf("rewrite = %s", out) } } func TestAbbreviated(t *testing.T) { for body, want := range map[string]bool{ `{"name":"demo","modified":"2024-01-01T00:00:00Z","versions":{}}`: true, `{"name":"demo","modified":"2024-01-01T00:00:00Z","time":{},"versions":{}}`: false, `{"name":"demo","versions":{}}`: false, `["modified"]`: false, `{"modified":`: false, } { if got := Abbreviated([]byte(body)); got != want { t.Errorf("Abbreviated(%s) = %v, want %v", body, got, want) } } } // TestNPMRewriteAttestationsURL checks that dist.attestations.url points at // the proxy whichever order dist lists its members in, and that nothing else // in dist changes. func TestNPMRewriteAttestationsURL(t *testing.T) { options := Options{ProxyURL: "http://proxy"} tests := []struct{ name, version, dist, want string }{ {"demo", "1.0.0", `{"tarball":"https://registry.npmjs.org/demo/-/demo-1.0.0.tgz","attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/demo@1.0.0","provenance":{"predicateType":"p"}}}`, `{"tarball":"http://proxy/npm/demo/-/demo-1.0.0.tgz","attestations":{"url":"http://proxy/npm/-/npm/v1/attestations/demo@1.0.0","provenance":{"predicateType":"p"}}}`}, {"@scope/demo", "2.0.0-rc.1", `{"attestations":{"provenance":{},"url":"https://registry.npmjs.org/-/npm/v1/attestations/@scope%2fdemo@2.0.0-rc.1"},"tarball":"https://registry.npmjs.org/@scope/demo/-/demo-2.0.0-rc.1.tgz"}`, `{"attestations":{"provenance":{},"url":"http://proxy/npm/-/npm/v1/attestations/@scope%2Fdemo@2.0.0-rc.1"},"tarball":"http://proxy/npm/@scope%2Fdemo/-/demo-2.0.0-rc.1.tgz"}`}, {"demo", "3.0.0", `{"attestations":{"url":7}}`, `{"attestations":{"url":7}}`}, } for _, tt := range tests { input := `{"versions":{"` + tt.version + `":{"dist":` + tt.dist + `}}}` out, _, err := Rewrite(tt.name, []byte(input), options) if err != nil { t.Fatal(err) } want := `{"versions":{"` + tt.version + `":{"dist":` + tt.want + `}}}` if string(out) != want { t.Errorf("%s@%s: rewrite =\n%s\nwant\n%s", tt.name, tt.version, out, want) } } } // TestNPMRewriteCooldownDropsTags checks that cooldown, like the denylist, // drops a tag other than latest that names a version it withholds, in both // the in-memory and the streamed rewrite. func TestNPMRewriteCooldownDropsTags(t *testing.T) { now := time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC) ts := func(age time.Duration) string { return now.Add(-age).UTC().Format(time.RFC3339) } options := Options{ProxyURL: "http://proxy", EvaluatedAt: now, Cooldown: &cooldown.Config{Default: "3d"}} input := `{"name":"demo",` + `"dist-tags":{"latest":"3.0.0","next":"3.0.0","legacy":"1.0.0"},` + `"versions":{"1.0.0":{},"2.0.0":{},"3.0.0":{}},` + `"time":{"1.0.0":"` + ts(900*time.Hour) + `","2.0.0":"` + ts(800*time.Hour) + `","3.0.0":"` + ts(time.Hour) + `"}}` rewrite := map[string]func() ([]byte, []string, error){ "memory": func() ([]byte, []string, error) { return Rewrite("demo", []byte(input), options) }, "stream": func() ([]byte, []string, error) { return rewriteStream("demo", []byte(input), options) }, } for name, run := range rewrite { t.Run(name, func(t *testing.T) { out, _, err := run() if err != nil { t.Fatal(err) } var doc struct { DistTags map[string]string `json:"dist-tags"` } if err := json.Unmarshal(out, &doc); err != nil { t.Fatalf("output is not JSON: %v\n%s", err, out) } want := map[string]string{"latest": "2.0.0", "legacy": "1.0.0"} if fmt.Sprint(doc.DistTags) != fmt.Sprint(want) { t.Errorf("dist-tags = %v, want %v", doc.DistTags, want) } }) } }