github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/lockfile/npm/packagelockjson_test.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package npm_test 16 17 import ( 18 "os" 19 "path/filepath" 20 "testing" 21 22 "deps.dev/util/resolve" 23 "deps.dev/util/resolve/schema" 24 "github.com/google/go-cmp/cmp" 25 "github.com/google/osv-scalibr/clients/clienttest" 26 scalibrfs "github.com/google/osv-scalibr/fs" 27 "github.com/google/osv-scalibr/guidedremediation/internal/lockfile/npm" 28 "github.com/google/osv-scalibr/guidedremediation/result" 29 ) 30 31 func TestReadV1(t *testing.T) { 32 // This lockfile was generated using a private registry with https://verdaccio.org/ 33 // Mock packages were published to it and installed with npm. 34 rw, err := npm.GetReadWriter() 35 if err != nil { 36 t.Fatalf("error creating ReadWriter: %v", err) 37 } 38 fsys := scalibrfs.DirFS("./testdata/v1") 39 got, err := rw.Read("package-lock.json", fsys) 40 if err != nil { 41 t.Fatalf("error reading lockfile: %v", err) 42 } 43 44 if err := got.Canon(); err != nil { 45 t.Fatalf("failed canonicalizing got graph: %v", err) 46 } 47 48 want, err := schema.ParseResolve(` 49 r 1.0.0 50 @fake-registry/a@^1.2.3 1.2.3 51 $b@^1.0.0 52 b: @fake-registry/b@^1.0.1 1.0.1 53 Dev KnownAs a-dev|@fake-registry/a@^2.3.4 2.3.4 54 # all indirect dependencies become regular because it's impossible to tell type in v1 55 @fake-registry/b@^2.0.0 2.0.0 56 @fake-registry/c@^1.0.0 1.1.1 57 # peerDependencies are not supported in v1 58 @fake-registry/d@^2.0.0 2.2.2 59 # v1 does not support workspaces 60 `, resolve.NPM) 61 if err != nil { 62 t.Fatalf("error parsing want graph: %v", err) 63 } 64 65 if err := want.Canon(); err != nil { 66 t.Fatalf("failed canonicalizing want graph: %v", err) 67 } 68 69 if diff := cmp.Diff(want, got); diff != "" { 70 t.Errorf("npm lockfile mismatch (-want +got):\n%s", diff) 71 } 72 } 73 74 func TestReadV2(t *testing.T) { 75 // This lockfile was generated using a private registry with https://verdaccio.org/ 76 // Mock packages were published to it and installed with npm. 77 rw, err := npm.GetReadWriter() 78 if err != nil { 79 t.Fatalf("error creating ReadWriter: %v", err) 80 } 81 fsys := scalibrfs.DirFS("./testdata/v2") 82 got, err := rw.Read("package-lock.json", fsys) 83 if err != nil { 84 t.Fatalf("error reading lockfile: %v", err) 85 } 86 87 if err := got.Canon(); err != nil { 88 t.Fatalf("failed canonicalizing got graph: %v", err) 89 } 90 91 want, err := schema.ParseResolve(` 92 r 1.0.0 93 @fake-registry/a@^1.2.3 1.2.3 94 Opt|$b@^1.0.0 95 b: @fake-registry/b@^1.0.1 1.0.1 96 Dev KnownAs a-dev|@fake-registry/a@^2.3.4 2.3.4 97 @fake-registry/b@^2.0.0 2.0.0 98 c: @fake-registry/c@^1.0.0 1.1.1 99 Scope peer|$d@^2.0.0 100 d: @fake-registry/d@^2.0.0 2.2.2 101 # workspace 102 w@* 1.0.0 103 Dev|@fake-registry/a@^2.3.4 2.3.4 104 @fake-registry/b@^2.0.0 2.0.0 105 $c@^1.0.0 106 $d@^2.0.0 107 `, resolve.NPM) 108 if err != nil { 109 t.Fatalf("error parsing want graph: %v", err) 110 } 111 112 if err := want.Canon(); err != nil { 113 t.Fatalf("failed canonicalizing want graph: %v", err) 114 } 115 116 if diff := cmp.Diff(want, got); diff != "" { 117 t.Errorf("npm lockfile mismatch (-want +got):\n%s", diff) 118 } 119 } 120 121 func TestTypeOrdering(t *testing.T) { 122 // Testing the behavior when a package is included in multiple dependency type fields. 123 // Empirically, devDependencies > optionalDependencies > dependencies > peerDependencies 124 125 // This lockfile was manually constructed. 126 rw, err := npm.GetReadWriter() 127 if err != nil { 128 t.Fatalf("error creating ReadWriter: %v", err) 129 } 130 fsys := scalibrfs.DirFS("./testdata/type_order") 131 got, err := rw.Read("package-lock.json", fsys) 132 if err != nil { 133 t.Fatalf("error reading lockfile: %v", err) 134 } 135 136 if err := got.Canon(); err != nil { 137 t.Fatalf("failed canonicalizing got graph: %v", err) 138 } 139 140 want, err := schema.ParseResolve(` 141 root 1.0.0 142 Dev|a@4.0.0 4.0.0 143 Opt|b@3.0.0 3.0.0 144 c@2.0.0 2.0.0 145 Scope peer|d@1.0.0 1.0.0 146 `, resolve.NPM) 147 if err != nil { 148 t.Fatalf("error parsing want graph: %v", err) 149 } 150 151 if err := want.Canon(); err != nil { 152 t.Fatalf("failed canonicalizing want graph: %v", err) 153 } 154 155 if diff := cmp.Diff(want, got); diff != "" { 156 t.Errorf("npm lockfile mismatch (-want +got):\n%s", diff) 157 } 158 } 159 160 func TestPeerMeta(t *testing.T) { 161 // Testing the behavior with peerDependencies and peerDependenciesMeta. 162 163 // This lockfile was manually constructed. 164 rw, err := npm.GetReadWriter() 165 if err != nil { 166 t.Fatalf("error creating ReadWriter: %v", err) 167 } 168 fsys := scalibrfs.DirFS("./testdata/peer_meta") 169 got, err := rw.Read("package-lock.json", fsys) 170 if err != nil { 171 t.Fatalf("error reading lockfile: %v", err) 172 } 173 174 if err := got.Canon(); err != nil { 175 t.Fatalf("failed canonicalizing got graph: %v", err) 176 } 177 178 want, err := schema.ParseResolve(` 179 root 1.0.0 180 dep@^1.0.0 1.0.0 181 p2: Opt Scope peer|peer2@^2.0.0 2.0.0 182 Scope peer KnownAs peer3|peer2@^3.0.0 3.0.0 183 $p2@^2.0.0 184 `, resolve.NPM) 185 if err != nil { 186 t.Fatalf("error parsing want graph: %v", err) 187 } 188 189 if err := want.Canon(); err != nil { 190 t.Fatalf("failed canonicalizing want graph: %v", err) 191 } 192 193 if diff := cmp.Diff(want, got); diff != "" { 194 t.Errorf("npm lockfile mismatch (-want +got):\n%s", diff) 195 } 196 } 197 198 func TestWrite(t *testing.T) { 199 // Set up mock npm registry 200 srv := clienttest.NewMockHTTPServer(t) 201 srv.SetResponseFromFile(t, "/@fake-registry%2fa/1.2.4", "testdata/fake_registry/a-1.2.4.json") 202 srv.SetResponseFromFile(t, "/@fake-registry%2fa/2.3.5", "testdata/fake_registry/a-2.3.5.json") 203 204 // Create output directory with npmrc pointing to the registry 205 outDir := t.TempDir() 206 if err := os.WriteFile(filepath.Join(outDir, ".npmrc"), []byte("registry="+srv.URL+"\n"), 0644); err != nil { 207 t.Fatalf("error writing npmrc: %v", err) 208 } 209 210 // Create patches to write 211 patches := []result.Patch{ 212 { 213 PackageUpdates: []result.PackageUpdate{ 214 { 215 Name: "@fake-registry/a", 216 VersionFrom: "1.2.3", 217 VersionTo: "1.2.4", 218 }, 219 { 220 Name: "@fake-registry/a", 221 VersionFrom: "2.3.4", 222 VersionTo: "2.3.5", 223 }, 224 }, 225 }, 226 } 227 228 want, err := os.ReadFile("testdata/write/want.package-lock.json") 229 if err != nil { 230 t.Fatalf("error reading want lockfile: %v", err) 231 } 232 233 // Write the patched lockfile 234 rw, err := npm.GetReadWriter() 235 if err != nil { 236 t.Fatalf("error creating ReadWriter: %v", err) 237 } 238 gotPath := filepath.Join(outDir, "package-lock.json") 239 if err := rw.Write("write/package-lock.json", scalibrfs.DirFS("testdata"), patches, gotPath); err != nil { 240 t.Fatalf("error writing lockfile: %v", err) 241 } 242 got, err := os.ReadFile(gotPath) 243 if err != nil { 244 t.Fatalf("error reading got lockfile: %v", err) 245 } 246 247 if diff := cmp.Diff(want, got); diff != "" { 248 t.Errorf("npm lockfile mismatch (-want +got):\n%s", diff) 249 } 250 }