Either was read as whole and could leave a pinned version unnamed (review of #128).
216 lines
8.9 KiB
Go
216 lines
8.9 KiB
Go
package main
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
var knownHere = map[string]bool{"@novox/mesh-sdk": true, "@novox/ui": true, "sdk": true}
|
|
|
|
func pins(ps []Pinned) string {
|
|
var out []string
|
|
for _, p := range ps {
|
|
out = append(out, p.Package+"@"+p.Version)
|
|
}
|
|
sort.Strings(out)
|
|
return strings.Join(out, " ")
|
|
}
|
|
|
|
func TestAManifestNamesItsRegistryRangesAndNothingElse(t *testing.T) {
|
|
got, err := ReadManifest([]byte(`{
|
|
"dependencies": {"@novox/mesh-sdk": "^0.1.0", "left-pad": "^1.0.0", "@novox/ui": "file:../ui"},
|
|
"devDependencies": {"sdk": "npm:@novox/ui@~2.0.0"},
|
|
"peerDependencies": {"@novox/ui": "workspace:*"},
|
|
"optionalDependencies": {"@novox/ui": "1.x"}
|
|
}`), "r:package.json", knownHere)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var said []string
|
|
for _, w := range got {
|
|
said = append(said, w.Package+" "+w.Range)
|
|
}
|
|
sort.Strings(said)
|
|
if strings.Join(said, ",") != "@novox/mesh-sdk ^0.1.0,@novox/ui 1.x,@novox/ui ~2.0.0" {
|
|
t.Fatalf("read %v", said)
|
|
}
|
|
}
|
|
|
|
func TestAnNpmLockfileOfEveryVersionNamesWhatItPins(t *testing.T) {
|
|
v3 := `{"lockfileVersion":3,"packages":{"":{"name":"x"},
|
|
"node_modules/@novox/mesh-sdk":{"version":"0.1.3"},
|
|
"node_modules/a/node_modules/@novox/mesh-sdk":{"version":"0.1.1"},
|
|
"node_modules/sdk":{"name":"@novox/ui","version":"2.0.4"},
|
|
"node_modules/left-pad":{"version":"1.3.0"}}}`
|
|
got, err := ReadLock("package-lock.json", []byte(v3), "r:package-lock.json", knownHere)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pins(got) != "@novox/mesh-sdk@0.1.1 @novox/mesh-sdk@0.1.3 @novox/ui@2.0.4" {
|
|
t.Fatalf("v3: %s", pins(got))
|
|
}
|
|
v1 := `{"lockfileVersion":1,"dependencies":{"@novox/mesh-sdk":{"version":"0.1.2","dependencies":{"@novox/ui":{"version":"1.0.0"}}},
|
|
"sdk":{"version":"npm:@novox/ui@2.0.1"}}}`
|
|
got, err = ReadLock("npm-shrinkwrap.json", []byte(v1), "r", knownHere)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pins(got) != "@novox/mesh-sdk@0.1.2 @novox/ui@1.0.0 @novox/ui@2.0.1" {
|
|
t.Fatalf("v1: %s", pins(got))
|
|
}
|
|
if _, err := ReadLock("package-lock.json", []byte("{not json"), "r", knownHere); err == nil {
|
|
t.Fatal("a broken lockfile read as empty")
|
|
}
|
|
}
|
|
|
|
func TestAYarnLockOfEitherFormatNamesWhatItPins(t *testing.T) {
|
|
classic := "# yarn lockfile v1\n\n\"@novox/mesh-sdk@^0.1.0\", \"@novox/mesh-sdk@^0.1.2\":\n version \"0.1.4\"\n resolved \"x\"\n dependencies:\n left-pad \"^1\"\n\nleft-pad@^1:\n version \"1.3.0\"\n"
|
|
if got := pins(mustYarn(t, classic)); got != "@novox/mesh-sdk@0.1.4" {
|
|
t.Fatalf("classic: %s", got)
|
|
}
|
|
berry := "__metadata:\n version: 6\n\n\"@novox/ui@npm:^2.0.0\":\n version: 2.0.7\n resolution: \"@novox/ui@npm:2.0.7\"\n"
|
|
if got := pins(mustYarn(t, berry)); got != "@novox/ui@2.0.7" {
|
|
t.Fatalf("berry: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestAPnpmLockNamesWhatItPinsInEachFormat(t *testing.T) {
|
|
lock := "lockfileVersion: '9.0'\npackages:\n '@novox/mesh-sdk@0.1.5':\n resolution: {}\n /@novox/ui/2.0.2:\n resolution: {}\n /@other/novox/ui@9.9.9:\n /x-sdk@1.0.0:\n"
|
|
if got := pins(readTextLock([]byte(lock), "r", knownHere)); got != "@novox/mesh-sdk@0.1.5 @novox/ui@2.0.2" {
|
|
t.Fatalf("pnpm: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestOnlyTheRepositorysOwnManifestsAreRead(t *testing.T) {
|
|
for p, want := range map[string]bool{"package.json": true, "modules/x/package-lock.json": true, "yarn.lock": true,
|
|
"pnpm-lock.yaml": true, "node_modules/a/package.json": false, "README.md": false, "a/package.json.bak": false} {
|
|
if interesting(p) != want {
|
|
t.Errorf("%s: %v", p, !want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lockfiles as npm writes them: a `packages` entry's `dependencies` is a map of name to range, which a
|
|
// reader expecting nested entries cannot read (the shape every lockfile on the forge had).
|
|
|
|
const lockV1 = `{
|
|
"name": "app", "version": "1.0.0", "lockfileVersion": 1, "requires": true,
|
|
"dependencies": {
|
|
"@novox/mesh-sdk": {
|
|
"version": "0.1.2",
|
|
"resolved": "https://forge.example/api/packages/novox/npm/@novox/mesh-sdk/-/0.1.2/mesh-sdk-0.1.2.tgz",
|
|
"integrity": "sha512-x",
|
|
"requires": {"@novox/ui": "^1.0.0", "left-pad": "^1.3.0"},
|
|
"dependencies": {
|
|
"@novox/ui": {"version": "1.0.0", "resolved": "https://forge.example/x.tgz", "integrity": "sha512-y",
|
|
"requires": {"left-pad": "^1.0.0"},
|
|
"dependencies": {"@novox/mesh-sdk": {"version": "0.1.0", "requires": {}}}}
|
|
}
|
|
},
|
|
"left-pad": {"version": "1.3.0", "dev": true},
|
|
"sdk": {"version": "npm:@novox/ui@2.0.1"},
|
|
"local": {"version": "file:../local", "requires": {"@novox/ui": "*"}}
|
|
}
|
|
}`
|
|
|
|
const lockV2 = `{
|
|
"name": "app", "version": "1.0.0", "lockfileVersion": 2, "requires": true,
|
|
"packages": {
|
|
"": {"name": "app", "version": "1.0.0", "dependencies": {"@novox/mesh-sdk": "^0.1.0"},
|
|
"devDependencies": {"@novox/ui": "~1.1.0"}, "engines": {"node": ">=20"}},
|
|
"node_modules/@novox/mesh-sdk": {"version": "0.1.3", "resolved": "https://forge.example/a.tgz",
|
|
"integrity": "sha512-a", "dependencies": {"left-pad": "^1.3.0"}, "peerDependencies": {"@novox/ui": "*"},
|
|
"optionalDependencies": {"fsevents": "^2"}, "bin": {"sdk": "bin/sdk.js"}, "engines": {"node": ">=18"}},
|
|
"node_modules/@novox/ui": {"version": "1.1.4", "dev": true, "funding": [{"type": "x", "url": "y"}]},
|
|
"node_modules/left-pad": {"version": "1.3.0"}
|
|
},
|
|
"dependencies": {
|
|
"@novox/mesh-sdk": {"version": "0.1.3", "requires": {"left-pad": "^1.3.0"}},
|
|
"@novox/ui": {"version": "1.1.4", "dev": true},
|
|
"left-pad": {"version": "1.3.0"}
|
|
}
|
|
}`
|
|
|
|
const lockV3 = `{
|
|
"name": "app", "version": "1.0.0", "lockfileVersion": 3, "requires": true,
|
|
"packages": {
|
|
"": {"name": "app", "workspaces": ["packages/*"], "dependencies": {"@novox/mesh-sdk": "^0.1.0", "sdk": "npm:@novox/ui@^2.0.0"}},
|
|
"node_modules/@novox/mesh-sdk": {"version": "0.1.5", "license": "MIT", "dependencies": {"@novox/ui": "^1.0.0"}},
|
|
"node_modules/@novox/ui": {"version": "1.0.9", "devDependencies": {"typescript": "^5"}},
|
|
"node_modules/a/node_modules/@novox/mesh-sdk": {"version": "0.1.1", "dependencies": {"left-pad": "1.x"}},
|
|
"node_modules/a/node_modules/b/node_modules/@novox/ui": {"version": "1.0.2"},
|
|
"node_modules/sdk": {"name": "@novox/ui", "version": "2.0.4", "resolved": "https://forge.example/u.tgz"},
|
|
"node_modules/@novox/workspace-thing": {"resolved": "packages/thing", "link": true},
|
|
"node_modules/@novox/linked": {"resolved": "packages/linked", "link": true, "version": "9.9.9"},
|
|
"packages/thing": {"name": "@novox/workspace-thing", "version": "0.0.1", "dependencies": {"@novox/ui": "^1.0.0"}},
|
|
"node_modules/left-pad": {"version": "1.3.0", "peerDependencies": {"x": "*"}, "optionalDependencies": {"y": "1"}}
|
|
}
|
|
}`
|
|
|
|
var knownLocks = map[string]bool{"@novox/mesh-sdk": true, "@novox/ui": true, "@novox/workspace-thing": true, "@novox/linked": true}
|
|
|
|
func TestALockfileVersion1NamesEveryNestedVersion(t *testing.T) {
|
|
got, err := ReadLock("package-lock.json", []byte(lockV1), "r", knownLocks)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pins(got) != "@novox/mesh-sdk@0.1.0 @novox/mesh-sdk@0.1.2 @novox/ui@1.0.0 @novox/ui@2.0.1" {
|
|
t.Fatalf("v1: %s", pins(got))
|
|
}
|
|
}
|
|
|
|
func TestALockfileVersion2ReadsBothHalves(t *testing.T) {
|
|
got, err := ReadLock("package-lock.json", []byte(lockV2), "r", knownLocks)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Each half names both; a version is kept once it is named, so naming it twice is harmless.
|
|
if pins(got) != "@novox/mesh-sdk@0.1.3 @novox/mesh-sdk@0.1.3 @novox/ui@1.1.4 @novox/ui@1.1.4" {
|
|
t.Fatalf("v2: %s", pins(got))
|
|
}
|
|
}
|
|
|
|
func TestALockfileVersion3ReadsPathsAliasesAndSkipsLinks(t *testing.T) {
|
|
got, err := ReadLock("package-lock.json", []byte(lockV3), "r", knownLocks)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "@novox/mesh-sdk@0.1.1 @novox/mesh-sdk@0.1.5 @novox/ui@1.0.2 @novox/ui@1.0.9 @novox/ui@2.0.4"
|
|
if pins(got) != want {
|
|
t.Fatalf("v3: %s\nwant %s", pins(got), want)
|
|
}
|
|
}
|
|
|
|
func TestAnEntryOfAnUnknownShapeIsSaidAndTheRestIsRead(t *testing.T) {
|
|
lock := `{"lockfileVersion":3,"packages":{
|
|
"node_modules/@novox/ui":{"version":"1.0.0"},
|
|
"node_modules/@novox/mesh-sdk":{"version":{"odd":true}}},
|
|
"dependencies":{"@novox/ui":"not an object"}}`
|
|
got, err := ReadLock("package-lock.json", []byte(lock), "r", knownLocks)
|
|
if pins(got) != "@novox/ui@1.0.0" {
|
|
t.Fatalf("read %s", pins(got))
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "2 entries not read") ||
|
|
!strings.Contains(err.Error(), `packages["node_modules/@novox/mesh-sdk"]`) {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|
|
|
|
func mustYarn(t *testing.T, content string) []Pinned {
|
|
t.Helper()
|
|
out, err := readYarnLock([]byte(content), "r", knownHere)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// A yarn lockfile with a line longer than the scanner holds is an error, not a file read to its end.
|
|
func TestAYarnLockfileCutShortIsAnError(t *testing.T) {
|
|
long := "\"@novox/mesh-sdk@^0.1.0\":\n version \"0.1.4\"\n# " + strings.Repeat("x", 2<<20) + "\n\"@novox/ui@^2\":\n version \"2.0.7\"\n"
|
|
if _, err := readYarnLock([]byte(long), "r", knownHere); err == nil {
|
|
t.Fatal("a lockfile the scanner stopped in was read as whole")
|
|
}
|
|
}
|