CVE-2021-43616: Insufficient Verification of Data Authenticity (CWE-345)
value: The npm ci command in npm 7.x and 8.x through 8.1.3 proceeds with an installation even if dependency information in package-lock.json differs from package.json. This behavior is inconsistent with the documentation, and makes it easier for attackers to install malware that was supposed to have been blocked by an exact version match requirement in package-lock.json. NOTE: The npm team believes this is not a vulnerability. It would require someone to socially engineer package.json which has different dependencies than package-lock.json. That user would have to have file system or write access to change dependencies. The npm team states preventing malicious actors from socially engineering or gaining file system access is outside the scope of the npm CLI.
CWE description: The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data.
CVSS v3 base score: 9.8
Published: 2021-11-13T18:15Z
Repository: https://github.com/npm/cli
Language: JavaScript
Files changed: lib/commands/ci.js, lib/utils/validate-lockfile.js, smoke-tests/index.js, tap-snapshots/smoke-tests/index.js.test.cjs, tap-snapshots/test/lib/commands/ci.js.test.cjs, tap-snapshots/test/lib/utils/validate-lockfile.js.test.cjs, test/lib/commands/ci.js, test/lib/utils/validate-lockfile.js
Commit date: 2022-02-03 14:23:46 -0500
Commit message: fix(ci): lock file validation

Make sure to validate any lock file (either package-lock.json or
npm-shrinkwrap.json) against the current install. This will properly
throw an error in case any of the dependencies being installed don't
match the dependencies that are currently listed in the lock file.

Fixes: https://github.com/npm/cli/issues/2701
Fixes: https://github.com/npm/cli/issues/3947

Diff: diff --git a/lib/commands/ci.js b/lib/commands/ci.js
index 2c2f8da86..376a85d60 100644
--- a/lib/commands/ci.js
+++ b/lib/commands/ci.js
@@ -4,10 +4,11 @@ const rimraf = util.promisify(require('rimraf'))
 const reifyFinish = require('../utils/reify-finish.js')
 const runScript = require('@npmcli/run-script')
 const fs = require('fs')
 const readdir = util.promisify(fs.readdir)
 const log = require('../utils/log-shim.js')
+const validateLockfile = require('../utils/validate-lockfile.js')
 
 const removeNodeModules = async where => {
   const rimrafOpts = { glob: false }
   process.emit('time', 'npm-ci:rm')
   const path = `${where}/node_modules`
@@ -53,10 +54,32 @@ class CI extends ArboristWorkspaceCmd {
           'later to generate a package-lock.json file, then try again.'
         throw new Error(msg)
       }),
       removeNodeModules(where),
     ])
+
+    // retrieves inventory of packages from loaded virtual tree (lock file)
+    const virtualInventory = new Map(arb.virtualTree.inventory)
+
+    // build ideal tree step needs to come right after retrieving the virtual
+    // inventory since it's going to erase the previous ref to virtualTree
+    await arb.buildIdealTree()
+
+    // verifies that the packages from the ideal tree will match
+    // the same versions that are present in the virtual tree (lock file)
+    // throws a validation error in case of mismatches
+    const errors = validateLockfile(virtualInventory, arb.idealTree.inventory)
+    if (errors.length) {
+      throw new Error(
+        '`npm ci` can only install packages when your package.json and ' +
+        'package-lock.json or npm-shrinkwrap.json are in sync. Please ' +
+        'update your lock file with `npm install` ' +
+        'before continuing.\n\n' +
+        errors.join('\n') + '\n'
+      )
+    }
+
     await arb.reify(opts)
 
     const ignoreScripts = this.npm.config.get('ignore-scripts')
     // run the same set of scripts that `npm install` runs.
     if (!ignoreScripts) {
diff --git a/lib/utils/validate-lockfile.js b/lib/utils/validate-lockfile.js
new file mode 100644
index 000000000..29161ec55
--- /dev/null
+++ b/lib/utils/validate-lockfile.js
@@ -0,0 +1,29 @@
+// compares the inventory of package items in the tree
+// that is about to be installed (idealTree) with the inventory
+// of items stored in the package-lock file (virtualTree)
+//
+// Returns empty array if no errors found or an array populated
+// with an entry for each validation error found.
+function validateLockfile (virtualTree, idealTree) {
+  const errors = []
+
+  // loops through the inventory of packages resulted by ideal tree,
+  // for each package compares the versions with the version stored in the
+  // package-lock and adds an error to the list in case of mismatches
+  for (const [key, entry] of idealTree.entries()) {
+    const lock = virtualTree.get(key)
+
+    if (!lock) {
+      errors.push(`Missing: ${entry.name}@${entry.version} from lock file`)
+      continue
+    }
+
+    if (entry.version !== lock.version) {
+      errors.push(`Invalid: lock file's ${lock.name}@${lock.version} does ` +
+      `not satisfy ${entry.name}@${entry.version}`)
+    }
+  }
+  return errors
+}
+
+module.exports = validateLockfile
diff --git a/smoke-tests/index.js b/smoke-tests/index.js
index 83e54ae9f..464187da0 100644
--- a/smoke-tests/index.js
+++ b/smoke-tests/index.js
@@ -19,10 +19,11 @@ t.cleanSnapshot = s =>
     .join('{CWD}')
     .replace(/\\+/g, '/')
     .replace(/\r\n/g, '\n')
     .replace(/ \(in a browser\)/g, '')
     .replace(/^npm@.* /gm, 'npm ')
+    .replace(/^.*debug-[0-9]+.log$/gm, '')
 
 // setup server
 const { start, stop, registry } = require('./server.js')
 t.before(start)
 t.teardown(stop)
@@ -318,5 +319,33 @@ t.test('npm update --save', async t => {
     JSON.parse(readFile('package-lock.json')).packages['node_modules/abbrev'].version,
     '1.1.1',
     'should have expected update --save lockfile result'
   )
 })
+
+t.test('npm ci', async t => {
+  await exec(`${npmBin} uninstall abbrev`)
+  await exec(`${npmBin} install abbrev@1.0.4 --save-exact`)
+
+  t.equal(
+    JSON.parse(readFile('package-lock.json')).packages['node_modules/abbrev'].version,
+    '1.0.4',
+    'should have stored exact installed version'
+  )
+
+  await exec(`${npmBin} pkg set "dependencies.abbrev=^1.1.1"`)
+
+  try {
+    const npmOpts = [
+      `--registry=${registry}`,
+      `--cache="${cacheLocation}"`,
+      `--userconfig="${userconfigLocation}"`,
+      '--no-audit',
+      '--no-update-notifier',
+      '--loglevel=error',
+    ].join(' ')
+    const npmBin = `"${process.execPath}" "${npmLocation}" ${npmOpts}`
+    await exec(`${npmBin} ci`)
+  } catch (err) {
+    t.matchSnapshot(err.stderr, 'should throw mismatch deps in lock file error')
+  }
+})
diff --git a/tap-snapshots/smoke-tests/index.js.test.cjs b/tap-snapshots/smoke-tests/index.js.test.cjs
index c1316e04d..5fa3977a8 100644
--- a/tap-snapshots/smoke-tests/index.js.test.cjs
+++ b/tap-snapshots/smoke-tests/index.js.test.cjs
@@ -38,10 +38,21 @@ or on the command line via: npm <command> --key=value
 More configuration info: npm help config
 Configuration fields: npm help 7 config
 
 npm {CWD}
 
+`
+
+exports[`smoke-tests/index.js TAP npm ci > should throw mismatch deps in lock file error 1`] = `
+npm ERR! \`npm ci\` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with \`npm install\` before continuing.
+npm ERR! 
+npm ERR! Invalid: lock file's abbrev@1.0.4 does not satisfy abbrev@1.1.1
+npm ERR! 
+
+npm ERR! A complete log of this run can be found in:
+
+
 `
 
 exports[`smoke-tests/index.js TAP npm diff > should have expected diff output 1`] = `
 diff --git a/package.json b/package.json
 index v1.0.4..v1.1.1 100644
diff --git a/tap-snapshots/test/lib/commands/ci.js.test.cjs b/tap-snapshots/test/lib/commands/ci.js.test.cjs
new file mode 100644
index 000000000..d6a747177
--- /dev/null
+++ b/tap-snapshots/test/lib/commands/ci.js.test.cjs
@@ -0,0 +1,13 @@
+/* IMPORTANT
+ * This snapshot file is auto-generated, but designed for humans.
+ * It should be checked into source control and tracked carefully.
+ * Re-generate by setting TAP_SNAPSHOT=1 and running tests.
+ * Make sure to inspect the output below.  Do not ignore changes!
+ */
+'use strict'
+exports[`test/lib/commands/ci.js TAP should throw error when ideal inventory mismatches virtual > must match snapshot 1`] = `
+\`npm ci\` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with \`npm install\` before continuing.
+
+Invalid: lock file's foo@1.0.0 does not satisfy foo@2.0.0
+
+`
diff --git a/tap-snapshots/test/lib/utils/validate-lockfile.js.test.cjs b/tap-snapshots/test/lib/utils/validate-lockfile.js.test.cjs
new file mode 100644
index 000000000..98a51267b
--- /dev/null
+++ b/tap-snapshots/test/lib/utils/validate-lockfile.js.test.cjs
@@ -0,0 +1,35 @@
+/* IMPORTANT
+ * This snapshot file is auto-generated, but designed for humans.
+ * It should be checked into source control and tracked carefully.
+ * Re-generate by setting TAP_SNAPSHOT=1 and running tests.
+ * Make sure to inspect the output below.  Do not ignore changes!
+ */
+'use strict'
+exports[`test/lib/utils/validate-lockfile.js TAP extra inventory items on idealTree > should have missing entries error 1`] = `
+Array [
+  "Missing: baz@3.0.0 from lock file",
+]
+`
+
+exports[`test/lib/utils/validate-lockfile.js TAP extra inventory items on virtualTree > should have no errors if finding virtualTree extra items 1`] = `
+Array []
+`
+
+exports[`test/lib/utils/validate-lockfile.js TAP identical inventory for both idealTree and virtualTree > should have no errors on identical inventories 1`] = `
+Array []
+`
+
+exports[`test/lib/utils/validate-lockfile.js TAP mismatching versions on inventory > should have errors for each mismatching version 1`] = `
+Array [
+  "Invalid: lock file's foo@1.0.0 does not satisfy foo@2.0.0",
+  "Invalid: lock file's bar@2.0.0 does not satisfy bar@3.0.0",
+]
+`
+
+exports[`test/lib/utils/validate-lockfile.js TAP missing virtualTree inventory > should have errors for each mismatching version 1`] = `
+Array [
+  "Missing: foo@1.0.0 from lock file",
+  "Missing: bar@2.0.0 from lock file",
+  "Missing: baz@3.0.0 from lock file",
+]
+`
diff --git a/test/lib/commands/ci.js b/test/lib/commands/ci.js
index 537d0784f..e077c99ee 100644
--- a/test/lib/commands/ci.js
+++ b/test/lib/commands/ci.js
@@ -17,10 +17,21 @@ t.test('should ignore scripts with --ignore-scripts', async t => {
     '@npmcli/arborist': function () {
       this.loadVirtual = async () => {}
       this.reify = () => {
         REIFY_CALLED = true
       }
+      this.buildIdealTree = () => {}
+      this.virtualTree = {
+        inventory: new Map([
+          ['foo', { name: 'foo', version: '1.0.0' }],
+        ]),
+      }
+      this.idealTree = {
+        inventory: new Map([
+          ['foo', { name: 'foo', version: '1.0.0' }],
+        ]),
+      }
     },
   })
 
   const npm = mockNpm({
     globalDir: 'path/to/node_modules/',
@@ -97,10 +108,21 @@ t.test('should use Arborist and run-script', async t => {
         return Promise.resolve(true)
       }
       this.reify = () => {
         t.ok(true, 'reify is called')
       }
+      this.buildIdealTree = () => {}
+      this.virtualTree = {
+        inventory: new Map([
+          ['foo', { name: 'foo', version: '1.0.0' }],
+        ]),
+      }
+      this.idealTree = {
+        inventory: new Map([
+          ['foo', { name: 'foo', version: '1.0.0' }],
+        ]),
+      }
     },
     rimraf: (path, ...args) => {
       actualRimrafs++
       t.ok(path, 'rimraf called with path')
       // callback is always last arg
@@ -136,10 +158,21 @@ t.test('should pass flatOptions to Arborist.reify', async t => {
     '@npmcli/arborist': function () {
       this.loadVirtual = () => Promise.resolve(true)
       this.reify = async (options) => {
         t.equal(options.production, true, 'should pass flatOptions to Arborist.reify')
       }
+      this.buildIdealTree = () => {}
+      this.virtualTree = {
+        inventory: new Map([
+          ['foo', { name: 'foo', version: '1.0.0' }],
+        ]),
+      }
+      this.idealTree = {
+        inventory: new Map([
+          ['foo', { name: 'foo', version: '1.0.0' }],
+        ]),
+      }
     },
   })
   const npm = mockNpm({
     prefix: 'foo',
     flatOptions: {
@@ -216,10 +249,21 @@ t.test('should remove existing node_modules before installing', async t => {
         // check if node_modules was removed before reifying
         const contents = await readdir(testDir)
         const nodeModules = contents.filter((path) => path.startsWith('node_modules'))
         t.same(nodeModules, ['node_modules'], 'should only have the node_modules directory')
       }
+      this.buildIdealTree = () => {}
+      this.virtualTree = {
+        inventory: new Map([
+          ['foo', { name: 'foo', version: '1.0.0' }],
+        ]),
+      }
+      this.idealTree = {
+        inventory: new Map([
+          ['foo', { name: 'foo', version: '1.0.0' }],
+        ]),
+      }
     },
   })
 
   const npm = mockNpm({
     prefix: testDir,
@@ -229,5 +273,43 @@ t.test('should remove existing node_modules before installing', async t => {
   })
   const ci = new CI(npm)
 
   await ci.exec(null)
 })
+
+t.test('should throw error when ideal inventory mismatches virtual', async t => {
+  const CI = t.mock('../../../lib/commands/ci.js', {
+    '../../../lib/utils/reify-finish.js': async () => {},
+    '@npmcli/run-script': ({ event }) => {},
+    '@npmcli/arborist': function () {
+      this.loadVirtual = async () => {}
+      this.reify = () => {}
+      this.buildIdealTree = () => {}
+      this.virtualTree = {
+        inventory: new Map([
+          ['foo', { name: 'foo', version: '1.0.0' }],
+        ]),
+      }
+      this.idealTree = {
+        inventory: new Map([
+          ['foo', { name: 'foo', version: '2.0.0' }],
+        ]),
+      }
+    },
+  })
+
+  const npm = mockNpm({
+    globalDir: 'path/to/node_modules/',
+    prefix: 'foo',
+    config: {
+      global: false,
+      'ignore-scripts': true,
+    },
+  })
+  const ci = new CI(npm)
+
+  try {
+    await ci.exec([])
+  } catch (err) {
+    t.matchSnapshot(err.message)
+  }
+})
diff --git a/test/lib/utils/validate-lockfile.js b/test/lib/utils/validate-lockfile.js
new file mode 100644
index 000000000..25939c5f8
--- /dev/null
+++ b/test/lib/utils/validate-lockfile.js
@@ -0,0 +1,82 @@
+const t = require('tap')
+const validateLockfile = require('../../../lib/utils/validate-lockfile.js')
+
+t.test('identical inventory for both idealTree and virtualTree', async t => {
+  t.matchSnapshot(
+    validateLockfile(
+      new Map([
+        ['foo', { name: 'foo', version: '1.0.0' }],
+        ['bar', { name: 'bar', version: '2.0.0' }],
+      ]),
+      new Map([
+        ['foo', { name: 'foo', version: '1.0.0' }],
+        ['bar', { name: 'bar', version: '2.0.0' }],
+      ])
+    ),
+    'should have no errors on identical inventories'
+  )
+})
+
+t.test('extra inventory items on idealTree', async t => {
+  t.matchSnapshot(
+    validateLockfile(
+      new Map([
+        ['foo', { name: 'foo', version: '1.0.0' }],
+        ['bar', { name: 'bar', version: '2.0.0' }],
+      ]),
+      new Map([
+        ['foo', { name: 'foo', version: '1.0.0' }],
+        ['bar', { name: 'bar', version: '2.0.0' }],
+        ['baz', { name: 'baz', version: '3.0.0' }],
+      ])
+    ),
+    'should have missing entries error'
+  )
+})
+
+t.test('extra inventory items on virtualTree', async t => {
+  t.matchSnapshot(
+    validateLockfile(
+      new Map([
+        ['foo', { name: 'foo', version: '1.0.0' }],
+        ['bar', { name: 'bar', version: '2.0.0' }],
+        ['baz', { name: 'baz', version: '3.0.0' }],
+      ]),
+      new Map([
+        ['foo', { name: 'foo', version: '1.0.0' }],
+        ['bar', { name: 'bar', version: '2.0.0' }],
+      ])
+    ),
+    'should have no errors if finding virtualTree extra items'
+  )
+})
+
+t.test('mismatching versions on inventory', async t => {
+  t.matchSnapshot(
+    validateLockfile(
+      new Map([
+        ['foo', { name: 'foo', version: '1.0.0' }],
+        ['bar', { name: 'bar', version: '2.0.0' }],
+      ]),
+      new Map([
+        ['foo', { name: 'foo', version: '2.0.0' }],
+        ['bar', { name: 'bar', version: '3.0.0' }],
+      ])
+    ),
+    'should have errors for each mismatching version'
+  )
+})
+
+t.test('missing virtualTree inventory', async t => {
+  t.matchSnapshot(
+    validateLockfile(
+      new Map([]),
+      new Map([
+        ['foo', { name: 'foo', version: '1.0.0' }],
+        ['bar', { name: 'bar', version: '2.0.0' }],
+        ['baz', { name: 'baz', version: '3.0.0' }],
+      ])
+    ),
+    'should have errors for each mismatching version'
+  )
+})

Fixed code: const validateLockfile = require('../utils/validate-lockfile.js')

    // retrieves inventory of packages from loaded virtual tree (lock file)
    const virtualInventory = new Map(arb.virtualTree.inventory)

    // build ideal tree step needs to come right after retrieving the virtual
    // inventory since it's going to erase the previous ref to virtualTree
    await arb.buildIdealTree()

    // verifies that the packages from the ideal tree will match
    // the same versions that are present in the virtual tree (lock file)
    // throws a validation error in case of mismatches
    const errors = validateLockfile(virtualInventory, arb.idealTree.inventory)
    if (errors.length) {
      throw new Error(
        '`npm ci` can only install packages when your package.json and ' +
        'package-lock.json or npm-shrinkwrap.json are in sync. Please ' +
        'update your lock file with `npm install` ' +
        'before continuing.\n\n' +
        errors.join('\n') + '\n'
      )
    }

// compares the inventory of package items in the tree
// that is about to be installed (idealTree) with the inventory
// of items stored in the package-lock file (virtualTree)
//
// Returns empty array if no errors found or an array populated
// with an entry for each validation error found.
function validateLockfile (virtualTree, idealTree) {
  const errors = []

  // loops through the inventory of packages resulted by ideal tree,
  // for each package compares the versions with the version stored in the
  // package-lock and adds an error to the list in case of mismatches
  for (const [key, entry] of idealTree.entries()) {
    const lock = virtualTree.get(key)

    if (!lock) {
      errors.push(`Missing: ${entry.name}@${entry.version} from lock file`)
      continue
    }

    if (entry.version !== lock.version) {
      errors.push(`Invalid: lock file's ${lock.name}@${lock.version} does ` +
      `not satisfy ${entry.name}@${entry.version}`)
    }
  }
  return errors
}

module.exports = validateLockfile
    .replace(/^.*debug-[0-9]+.log$/gm, '')

t.test('npm ci', async t => {
  await exec(`${npmBin} uninstall abbrev`)
  await exec(`${npmBin} install abbrev@1.0.4 --save-exact`)

  t.equal(
    JSON.parse(readFile('package-lock.json')).packages['node_modules/abbrev'].version,
    '1.0.4',
    'should have stored exact installed version'
  )

  await exec(`${npmBin} pkg set "dependencies.abbrev=^1.1.1"`)

  try {
    const npmOpts = [
      `--registry=${registry}`,
      `--cache="${cacheLocation}"`,
      `--userconfig="${userconfigLocation}"`,
      '--no-audit',
      '--no-update-notifier',
      '--loglevel=error',
    ].join(' ')
    const npmBin = `"${process.execPath}" "${npmLocation}" ${npmOpts}`
    await exec(`${npmBin} ci`)
  } catch (err) {
    t.matchSnapshot(err.stderr, 'should throw mismatch deps in lock file error')
  }
})
`

exports[`smoke-tests/index.js TAP npm ci > should throw mismatch deps in lock file error 1`] = `
npm ERR! \`npm ci\` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with \`npm install\` before continuing.
npm ERR! 
npm ERR! Invalid: lock file's abbrev@1.0.4 does not satisfy abbrev@1.1.1
npm ERR! 

npm ERR! A complete log of this run can be found in:


/* IMPORTANT
 * This snapshot file is auto-generated, but designed for humans.
 * It should be checked into source control and tracked carefully.
 * Re-generate by setting TAP_SNAPSHOT=1 and running tests.
 * Make sure to inspect the output below.  Do not ignore changes!
 */
'use strict'
exports[`test/lib/commands/ci.js TAP should throw error when ideal inventory mismatches virtual > must match snapshot 1`] = `
\`npm ci\` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with \`npm install\` before continuing.

Invalid: lock file's foo@1.0.0 does not satisfy foo@2.0.0

`
/* IMPORTANT
 * This snapshot file is auto-generated, but designed for humans.
 * It should be checked into source control and tracked carefully.
 * Re-generate by setting TAP_SNAPSHOT=1 and running tests.
 * Make sure to inspect the output below.  Do not ignore changes!
 */
'use strict'
exports[`test/lib/utils/validate-lockfile.js TAP extra inventory items on idealTree > should have missing entries error 1`] = `
Array [
  "Missing: baz@3.0.0 from lock file",
]
`

exports[`test/lib/utils/validate-lockfile.js TAP extra inventory items on virtualTree > should have no errors if finding virtualTree extra items 1`] = `
Array []
`

exports[`test/lib/utils/validate-lockfile.js TAP identical inventory for both idealTree and virtualTree > should have no errors on identical inventories 1`] = `
Array []
`

exports[`test/lib/utils/validate-lockfile.js TAP mismatching versions on inventory > should have errors for each mismatching version 1`] = `
Array [
  "Invalid: lock file's foo@1.0.0 does not satisfy foo@2.0.0",
  "Invalid: lock file's bar@2.0.0 does not satisfy bar@3.0.0",
]
`

exports[`test/lib/utils/validate-lockfile.js TAP missing virtualTree inventory > should have errors for each mismatching version 1`] = `
Array [
  "Missing: foo@1.0.0 from lock file",
  "Missing: bar@2.0.0 from lock file",
  "Missing: baz@3.0.0 from lock file",
]
`
      this.buildIdealTree = () => {}
      this.virtualTree = {
        inventory: new Map([
          ['foo', { name: 'foo', version: '1.0.0' }],
        ]),
      }
      this.idealTree = {
        inventory: new Map([
          ['foo', { name: 'foo', version: '1.0.0' }],
        ]),
      }
      this.buildIdealTree = () => {}
      this.virtualTree = {
        inventory: new Map([
          ['foo', { name: 'foo', version: '1.0.0' }],
        ]),
      }
      this.idealTree = {
        inventory: new Map([
          ['foo', { name: 'foo', version: '1.0.0' }],
        ]),
      }
      this.buildIdealTree = () => {}
      this.virtualTree = {
        inventory: new Map([
          ['foo', { name: 'foo', version: '1.0.0' }],
        ]),
      }
      this.idealTree = {
        inventory: new Map([
          ['foo', { name: 'foo', version: '1.0.0' }],
        ]),
      }
      this.buildIdealTree = () => {}
      this.virtualTree = {
        inventory: new Map([
          ['foo', { name: 'foo', version: '1.0.0' }],
        ]),
      }
      this.idealTree = {
        inventory: new Map([
          ['foo', { name: 'foo', version: '1.0.0' }],
        ]),
      }

t.test('should throw error when ideal inventory mismatches virtual', async t => {
  const CI = t.mock('../../../lib/commands/ci.js', {
    '../../../lib/utils/reify-finish.js': async () => {},
    '@npmcli/run-script': ({ event }) => {},
    '@npmcli/arborist': function () {
      this.loadVirtual = async () => {}
      this.reify = () => {}
      this.buildIdealTree = () => {}
      this.virtualTree = {
        inventory: new Map([
          ['foo', { name: 'foo', version: '1.0.0' }],
        ]),
      }
      this.idealTree = {
        inventory: new Map([
          ['foo', { name: 'foo', version: '2.0.0' }],
        ]),
      }
    },
  })

  const npm = mockNpm({
    globalDir: 'path/to/node_modules/',
    prefix: 'foo',
    config: {
      global: false,
      'ignore-scripts': true,
    },
  })
  const ci = new CI(npm)

  try {
    await ci.exec([])
  } catch (err) {
    t.matchSnapshot(err.message)
  }
})
const t = require('tap')
const validateLockfile = require('../../../lib/utils/validate-lockfile.js')

t.test('identical inventory for both idealTree and virtualTree', async t => {
  t.matchSnapshot(
    validateLockfile(
      new Map([
        ['foo', { name: 'foo', version: '1.0.0' }],
        ['bar', { name: 'bar', version: '2.0.0' }],
      ]),
      new Map([
        ['foo', { name: 'foo', version: '1.0.0' }],
        ['bar', { name: 'bar', version: '2.0.0' }],
      ])
    ),
    'should have no errors on identical inventories'
  )
})

t.test('extra inventory items on idealTree', async t => {
  t.matchSnapshot(
    validateLockfile(
      new Map([
        ['foo', { name: 'foo', version: '1.0.0' }],
        ['bar', { name: 'bar', version: '2.0.0' }],
      ]),
      new Map([
        ['foo', { name: 'foo', version: '1.0.0' }],
        ['bar', { name: 'bar', version: '2.0.0' }],
        ['baz', { name: 'baz', version: '3.0.0' }],
      ])
    ),
    'should have missing entries error'
  )
})

t.test('extra inventory items on virtualTree', async t => {
  t.matchSnapshot(
    validateLockfile(
      new Map([
        ['foo', { name: 'foo', version: '1.0.0' }],
        ['bar', { name: 'bar', version: '2.0.0' }],
        ['baz', { name: 'baz', version: '3.0.0' }],
      ]),
      new Map([
        ['foo', { name: 'foo', version: '1.0.0' }],
        ['bar', { name: 'bar', version: '2.0.0' }],
      ])
    ),
    'should have no errors if finding virtualTree extra items'
  )
})

t.test('mismatching versions on inventory', async t => {
  t.matchSnapshot(
    validateLockfile(
      new Map([
        ['foo', { name: 'foo', version: '1.0.0' }],
        ['bar', { name: 'bar', version: '2.0.0' }],
      ]),
      new Map([
        ['foo', { name: 'foo', version: '2.0.0' }],
        ['bar', { name: 'bar', version: '3.0.0' }],
      ])
    ),
    'should have errors for each mismatching version'
  )
})

t.test('missing virtualTree inventory', async t => {
  t.matchSnapshot(
    validateLockfile(
      new Map([]),
      new Map([
        ['foo', { name: 'foo', version: '1.0.0' }],
        ['bar', { name: 'bar', version: '2.0.0' }],
        ['baz', { name: 'baz', version: '3.0.0' }],
      ])
    ),
    'should have errors for each mismatching version'
  )
})