diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 4e336ae5daf21..afa21a3899956 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -690,6 +690,7 @@ graph LR; npmcli-mock-globals-->npmcli-eslint-config["@npmcli/eslint-config"]; npmcli-mock-globals-->npmcli-template-oss["@npmcli/template-oss"]; npmcli-mock-globals-->tap; + npmcli-mock-registry-->json-stringify-safe; npmcli-mock-registry-->nock; npmcli-mock-registry-->npm-package-arg; npmcli-mock-registry-->npmcli-arborist["@npmcli/arborist"]; diff --git a/mock-registry/lib/index.js b/mock-registry/lib/index.js index 924af05d5b6c4..91f1de5b52e0d 100644 --- a/mock-registry/lib/index.js +++ b/mock-registry/lib/index.js @@ -2,6 +2,7 @@ const pacote = require('pacote') const Arborist = require('@npmcli/arborist') const npa = require('npm-package-arg') const Nock = require('nock') +const stringify = require('json-stringify-safe') class MockRegistry { #tap @@ -39,7 +40,7 @@ class MockRegistry { // mocked with a 404, 500, etc. // XXX: this is opt-in currently because it breaks some existing CLI // tests. We should work towards making this the default for all tests. - t.fail(`Unmatched request: ${JSON.stringify(req, null, 2)}`) + t.fail(`Unmatched request: ${stringify(req, null, 2)}`) } } @@ -337,9 +338,9 @@ class MockRegistry { } nock = nock.reply(200, manifest) if (tarballs) { - for (const version in tarballs) { + for (const [version, tarball] of Object.entries(tarballs)) { const m = manifest.versions[version] - nock = await this.tarball({ manifest: m, tarball: tarballs[version] }) + nock = await this.tarball({ manifest: m, tarball }) } } this.nock = nock diff --git a/mock-registry/package.json b/mock-registry/package.json index c85877288d3ef..6ab00a246705a 100644 --- a/mock-registry/package.json +++ b/mock-registry/package.json @@ -47,6 +47,7 @@ "@npmcli/arborist": "^6.1.1", "@npmcli/eslint-config": "^4.0.1", "@npmcli/template-oss": "4.14.1", + "json-stringify-safe": "^5.0.1", "nock": "^13.3.0", "npm-package-arg": "^10.1.0", "pacote": "^15.0.8", diff --git a/package-lock.json b/package-lock.json index d2e83b0b7da09..83076f01a52b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -222,6 +222,7 @@ "@npmcli/arborist": "^6.1.1", "@npmcli/eslint-config": "^4.0.1", "@npmcli/template-oss": "4.14.1", + "json-stringify-safe": "^5.0.1", "nock": "^13.3.0", "npm-package-arg": "^10.1.0", "pacote": "^15.0.8", diff --git a/smoke-tests/test/fixtures/setup.js b/smoke-tests/test/fixtures/setup.js index c17e9fbacee2c..fc54667eb7d77 100644 --- a/smoke-tests/test/fixtures/setup.js +++ b/smoke-tests/test/fixtures/setup.js @@ -9,7 +9,7 @@ const httpProxy = require('http-proxy') const { SMOKE_PUBLISH_NPM, SMOKE_PUBLISH_TARBALL, CI, PATH, Path, TAP_CHILD_ID = '0' } = process.env const PROXY_PORT = 12345 + (+TAP_CHILD_ID) -const HTTP_PROXY = `http://localhost:${PROXY_PORT}` +const HTTP_PROXY = `http://localhost:${PROXY_PORT}/` const NODE_PATH = process.execPath const CLI_ROOT = resolve(process.cwd(), '..') @@ -64,12 +64,13 @@ const getCleanPaths = async () => { }) } -const createRegistry = async (t, { debug } = {}) => { +const createRegistry = async (t, { debug, ...opts } = {}) => { const registry = new MockRegistry({ tap: t, registry: 'http://smoke-test-registry.club/', debug, strict: true, + ...opts, }) const proxy = httpProxy.createProxyServer({}) @@ -81,7 +82,7 @@ const createRegistry = async (t, { debug } = {}) => { return registry } -module.exports = async (t, { testdir = {}, debug } = {}) => { +module.exports = async (t, { testdir = {}, debug, registry: _registry = {} } = {}) => { const debugLog = debug || CI ? (...a) => console.error(...a) : () => {} const cleanPaths = await getCleanPaths() @@ -105,7 +106,7 @@ module.exports = async (t, { testdir = {}, debug } = {}) => { globalNodeModules: join(root, 'global', GLOBAL_NODE_MODULES), } - const registry = await createRegistry(t, { debug }) + const registry = await createRegistry(t, { ..._registry, debug }) // update notifier should never be written t.afterEach((t) => { @@ -168,7 +169,13 @@ module.exports = async (t, { testdir = {}, debug } = {}) => { return stdout } - const baseNpm = async ({ cwd, cmd, argv = [], proxy = true, ...opts } = {}, ...args) => { + const baseNpm = async (baseOpts, ...args) => { + const hasMoreOpts = args[args.length - 1] && typeof args[args.length - 1] === 'object' + const { cwd, cmd, argv = [], proxy = true, ...opts } = { + ...baseOpts, + ...hasMoreOpts ? args.pop() : {}, + } + const isGlobal = args.some(a => ['-g', '--global', '--global=true'].includes(a)) const defaultFlags = [ @@ -218,6 +225,10 @@ module.exports = async (t, { testdir = {}, debug } = {}) => { argv: [NPM_PATH], }, ...args) + const npmLocalError = async () => { + throw new Error('npmLocal cannot be called during smoke-publish') + } + // helpers for reading/writing files and their source const readFile = async (f) => { const file = await fs.readFile(join(paths.project, f), 'utf-8') @@ -226,15 +237,22 @@ module.exports = async (t, { testdir = {}, debug } = {}) => { return { npmPath, - npmLocal: SMOKE_PUBLISH_NPM ? async () => { - throw new Error('npmLocal cannot be called during smoke-publish') - } : npmLocal, + npmLocal: SMOKE_PUBLISH_NPM ? npmLocalError : npmLocal, npm: SMOKE_PUBLISH_NPM ? npmPath : npm, spawn: baseSpawn, readFile, getPath, paths, registry, + npmLocalTarball: async () => { + if (SMOKE_PUBLISH_TARBALL) { + return SMOKE_PUBLISH_TARBALL + } + if (SMOKE_PUBLISH_NPM) { + return await npmLocalError() + } + return await npmLocal('pack', `--pack-destination=${root}`).then(r => join(root, r)) + }, } } @@ -244,3 +262,4 @@ module.exports.CLI_ROOT = CLI_ROOT module.exports.WINDOWS = WINDOWS module.exports.SMOKE_PUBLISH = !!SMOKE_PUBLISH_NPM module.exports.SMOKE_PUBLISH_TARBALL = SMOKE_PUBLISH_TARBALL +module.exports.HTTP_PROXY = HTTP_PROXY diff --git a/smoke-tests/test/npm-replace-global.js b/smoke-tests/test/npm-replace-global.js index ee1ac5d89a778..71074097db90a 100644 --- a/smoke-tests/test/npm-replace-global.js +++ b/smoke-tests/test/npm-replace-global.js @@ -10,14 +10,38 @@ const which = async (cmd, opts) => { return path ? join(dirname(path), basename(path, extname(path))) : null } -t.test('npm replace global', async t => { +const setupNpmGlobal = async (t, opts) => { + const mock = await setup(t, opts) + + return { + ...mock, + getPaths: async () => { + const binContents = await fs.readdir(mock.paths.globalBin).then(results => results + .filter(p => p !== '.npmrc' && p !== 'node_modules') + .map(p => basename(p, extname(p))) + .reduce((set, p) => set.add(p), new Set())) + + return { + npmRoot: await mock.npmPath('help').then(setup.getNpmRoot), + pathNpm: await which('npm', { path: mock.getPath(), nothrow: true }), + globalNpm: await which('npm', { nothrow: true }), + pathNpx: await which('npx', { path: mock.getPath(), nothrow: true }), + globalNpx: await which('npx', { nothrow: true }), + binContents: [...binContents], + nodeModulesContents: await fs.readdir(join(mock.paths.globalNodeModules, 'npm')), + } + }, + } +} + +t.test('pack and replace global self', async t => { const { npm, - npmLocal, + npmLocalTarball, npmPath, - getPath, - paths: { root, globalBin, globalNodeModules }, - } = await setup(t, { + getPaths, + paths: { globalBin, globalNodeModules }, + } = await setupNpmGlobal(t, { testdir: { project: { 'package.json': { name: 'npm', version: '999.999.999' }, @@ -25,25 +49,7 @@ t.test('npm replace global', async t => { }, }) - const getPaths = async () => { - const binContents = await fs.readdir(globalBin).then(results => results - .filter(p => p !== '.npmrc' && p !== 'node_modules') - .map(p => basename(p, extname(p))) - .reduce((set, p) => set.add(p), new Set())) - - return { - npmRoot: await npmPath('help').then(setup.getNpmRoot), - pathNpm: await which('npm', { path: getPath(), nothrow: true }), - globalNpm: await which('npm', { nothrow: true }), - pathNpx: await which('npx', { path: getPath(), nothrow: true }), - globalNpx: await which('npx', { nothrow: true }), - binContents: [...binContents], - nodeModulesContents: await fs.readdir(join(globalNodeModules, 'npm')), - } - } - - const tarball = setup.SMOKE_PUBLISH_TARBALL ?? - await npmLocal('pack', `--pack-destination=${root}`).then(r => join(root, r)) + const tarball = await npmLocalTarball() await npm('install', tarball, '--global') @@ -80,3 +86,45 @@ t.test('npm replace global', async t => { t.strictSame(postPaths.binContents, [], 'bin is empty') t.strictSame(postPaths.nodeModulesContents, ['package.json'], 'contents is only package.json') }) + +t.test('publish and replace global self', async t => { + const { + npm, + registry, + npmLocal, + npmLocalTarball, + getPaths, + } = await setupNpmGlobal(t, { + testdir: { + home: { + '.npmrc': `${setup.HTTP_PROXY.slice(5)}:_authToken = test-token`, + }, + }, + }) + + const tarball = await npmLocalTarball() + + let publishedPackument = null + const pkg = require('../../package.json') + const { name, version } = pkg + + registry.nock.put('/npm', body => { + if (body._id === 'npm' && body.versions[version]) { + publishedPackument = body.versions[version] + return true + } + return false + }).reply(201, {}) + await npmLocal('publish', { proxy: true }) + + await registry.package({ + manifest: registry.manifest({ name, packuments: [publishedPackument] }), + tarballs: { [version]: tarball }, + times: 2, + }) + + await npm('install', 'npm', '--global', '--prefer-online') + + // this throws right now + await getPaths() +})