Skip to content

Commit

Permalink
Mock npm view in tests that spawn ncu in a child process.
Browse files Browse the repository at this point in the history
  • Loading branch information
raineorshine committed Feb 23, 2023
1 parent 97fa810 commit 6d01108
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 52 deletions.
33 changes: 33 additions & 0 deletions src/package-managers/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { print } from '../lib/logging'
import * as versionUtil from '../lib/version-util'
import { GetVersion } from '../types/GetVersion'
import { Index } from '../types/IndexType'
import { MockedVersions } from '../types/MockedVersions'
import { NpmConfig } from '../types/NpmConfig'
import { NpmOptions } from '../types/NpmOptions'
import { Options } from '../types/Options'
Expand Down Expand Up @@ -270,6 +271,32 @@ export async function packageAuthorChanged(
return false
}

/** Creates a function with the same signature as viewMany that always returns the given versions. */
export const mockViewMany =
(mockReturnedVersions: MockedVersions) =>
(name: string, fields: string[], currentVersion: Version, options: Options): Promise<Packument> => {
const version =
typeof mockReturnedVersions === 'function'
? mockReturnedVersions(options)?.[name]
: typeof mockReturnedVersions === 'string'
? mockReturnedVersions
: mockReturnedVersions[name]

const packument = {
name,
engines: { node: '' },
time: { [version || '']: new Date().toISOString() },
version: version || '',
// versions are not needed in nested packument
versions: [],
}

return Promise.resolve({
...packument,
versions: [packument],
})
}

/**
* Returns an object of specified values retrieved by npm view.
*
Expand All @@ -286,6 +313,12 @@ export async function viewMany(
retried = 0,
npmConfigLocal?: NpmConfig,
): Promise<Packument> {
// See: /test/helpers/stubNpmView
if (process.env.STUB_NPM_VIEW) {
const mockReturnedVersions = JSON.parse(process.env.STUB_NPM_VIEW)
return mockViewMany(mockReturnedVersions)(packageName, fields, currentVersion, options)
}

if (currentVersion && (!semver.validRange(currentVersion) || versionUtil.isWildCard(currentVersion))) {
return Promise.resolve({} as Packument)
}
Expand Down
5 changes: 5 additions & 0 deletions src/types/MockedVersions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Index } from './IndexType'
import { Options } from './Options'
import { Version } from './Version'

export type MockedVersions = Version | Index<Version> | ((options: Options) => Index<Version> | null)
82 changes: 62 additions & 20 deletions test/bin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import spawn from 'spawn-please'
import stubNpmView from './helpers/stubNpmView'

chai.should()
chai.use(chaiAsPromised)
Expand All @@ -15,55 +16,79 @@ process.env.NCU_TESTS = 'true'
const bin = path.join(__dirname, '../build/src/bin/cli.js')

describe('bin', async function () {
it('runs from the command line', async () => {
await spawn('node', [bin], '{}')
it('fetch latest version from registry (not stubbed)', async () => {
const output = await spawn(
'node',
[bin, '--jsonUpgraded', '--stdin'],
'{ "dependencies": { "ncu-test-v2": "1.0.0" } }',
)
const pkgData = JSON.parse(output)
pkgData.should.have.property('ncu-test-v2')
})

it('output only upgraded with --jsonUpgraded', async () => {
const output = await spawn('node', [bin, '--jsonUpgraded', '--stdin'], '{ "dependencies": { "express": "1" } }')
const pkgData = JSON.parse(output) as Record<string, unknown>
pkgData.should.have.property('express')
const stub = stubNpmView('99.9.9', { spawn: true })
const output = await spawn(
'node',
[bin, '--jsonUpgraded', '--stdin'],
'{ "dependencies": { "ncu-test-v2": "1.0.0" } }',
)
const pkgData = JSON.parse(output)
pkgData.should.have.property('ncu-test-v2')
stub.restore()
})

it('--loglevel verbose', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const output = await spawn('node', [bin, '--loglevel', 'verbose'], '{ "dependencies": { "ncu-test-v2": "1.0.0" } }')
output.should.containIgnoreCase('Initializing')
output.should.containIgnoreCase('Running in local mode')
output.should.containIgnoreCase('Finding package file data')
stub.restore()
})

it('--verbose', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const output = await spawn('node', [bin, '--verbose'], '{ "dependencies": { "ncu-test-v2": "1.0.0" } }')
output.should.containIgnoreCase('Initializing')
output.should.containIgnoreCase('Running in local mode')
output.should.containIgnoreCase('Finding package file data')
stub.restore()
})

it('accept stdin', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const output = await spawn('node', [bin, '--stdin'], '{ "dependencies": { "express": "1" } }')
output.trim().should.startWith('express')
stub.restore()
})

it('reject out-of-date stdin with errorLevel 2', async () => {
return spawn(
const stub = stubNpmView('99.9.9', { spawn: true })
await spawn(
'node',
[bin, '--stdin', '--errorLevel', '2'],
'{ "dependencies": { "express": "1" } }',
).should.eventually.be.rejectedWith('Dependencies not up-to-date')
stub.restore()
})

it('fall back to package.json search when receiving empty content on stdin', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const stdout = await spawn('node', [bin, '--stdin'])
stdout
.toString()
.trim()
.should.match(/^Checking .+package.json/)
stub.restore()
})

it('use package.json in cwd by default', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const output = await spawn('node', [bin, '--jsonUpgraded'], { cwd: path.join(__dirname, 'test-data/ncu') })
const pkgData = JSON.parse(output)
pkgData.should.have.property('express')
stub.restore()
})

it('throw error if there is no package', async () => {
Expand All @@ -82,6 +107,7 @@ describe('bin', async function () {
})

it('read --packageFile', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-'))
const pkgFile = path.join(tempDir, 'package.json')
await fs.writeFile(pkgFile, '{ "dependencies": { "express": "1" } }', 'utf-8')
Expand All @@ -91,10 +117,12 @@ describe('bin', async function () {
pkgData.should.have.property('express')
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
stub.restore()
}
})

it('write to --packageFile', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-'))
const pkgFile = path.join(tempDir, 'package.json')
await fs.writeFile(pkgFile, '{ "dependencies": { "express": "1" } }', 'utf-8')
Expand All @@ -106,10 +134,12 @@ describe('bin', async function () {
upgradedPkg.dependencies.express.should.not.equal('1')
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
stub.restore()
}
})

it('write to --packageFile if errorLevel=2 and upgrades', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-'))
const pkgFile = path.join(tempDir, 'package.json')
await fs.writeFile(pkgFile, '{ "dependencies": { "express": "1" } }', 'utf-8')
Expand All @@ -124,10 +154,12 @@ describe('bin', async function () {
upgradedPkg.dependencies.express.should.not.equal('1')
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
stub.restore()
}
})

it('write to --packageFile with jsonUpgraded flag', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-'))
const pkgFile = path.join(tempDir, 'package.json')
await fs.writeFile(pkgFile, '{ "dependencies": { "express": "1" } }', 'utf-8')
Expand All @@ -139,10 +171,12 @@ describe('bin', async function () {
ugradedPkg.dependencies.express.should.not.equal('1')
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
stub.restore()
}
})

it('ignore stdin if --packageFile is specified', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-check-updates-'))
const pkgFile = path.join(tempDir, 'package.json')
await fs.writeFile(pkgFile, '{ "dependencies": { "express": "1" } }', 'utf-8')
Expand All @@ -154,15 +188,19 @@ describe('bin', async function () {
upgradedPkg.dependencies.express.should.not.equal('1')
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
stub.restore()
}
})

it('suppress stdout when --silent is provided', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const output = await spawn('node', [bin, '--silent'], '{ "dependencies": { "express": "1" } }')
output.trim().should.equal('')
stub.restore()
})

it('quote arguments with spaces in upgrade hint', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const pkgData = {
dependencies: {
'ncu-test-v2': '^1.0.0',
Expand All @@ -177,7 +215,22 @@ describe('bin', async function () {
output.should.include('"ncu-test-v2 ncu-test-tag"')
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
stub.restore()
}
})

it('ignore file: and link: protocols', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
const { default: stripAnsi } = await import('strip-ansi')
const dependencies = {
editor: 'file:../editor',
event: 'link:../link',
workspace: 'workspace:../workspace',
}
const output = await spawn('node', [bin, '--stdin'], JSON.stringify({ dependencies }))

stripAnsi(output)!.should.not.include('No package versions were returned.')
stub.restore()
})
})

Expand All @@ -195,13 +248,15 @@ describe('embedded versions', () => {
})

it('strip prefix from npm alias in "to" output', async () => {
const stub = stubNpmView('99.9.9', { spawn: true })
// use dynamic import for ESM module
const { default: stripAnsi } = await import('strip-ansi')
const dependencies = {
request: 'npm:ncu-test-v2@1.0.0',
}
const output = await spawn('node', [bin, '--stdin'], JSON.stringify({ dependencies }))
stripAnsi(output).trim().should.equal('request npm:ncu-test-v2@1.0.0 → 2.0.0')
stripAnsi(output).trim().should.equal('request npm:ncu-test-v2@1.0.0 → 99.9.9')
stub.restore()
})
})

Expand Down Expand Up @@ -252,17 +307,4 @@ describe('option-specific help', () => {
const output = await spawn('node', [bin, '--help', '--help'])
output.trim().should.not.include('Usage')
})

it('ignore file: and link: protocols', async () => {
const { default: stripAnsi } = await import('strip-ansi')
const dependencies = {
editor: 'file:../editor',
event: 'link:../link',
}
const output = await spawn('node', [bin, '--stdin'], JSON.stringify({ dependencies }))

stripAnsi(output)!.should.not.include(
'No package versions were returned. This is likely a problem with your installed npm',
)
})
})
53 changes: 21 additions & 32 deletions test/helpers/stubNpmView.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,26 @@
import sinon from 'sinon'
import * as npmPackageManager from '../../src/package-managers/npm'
import { Index } from '../../src/types/IndexType'
import { Options } from '../../src/types/Options'
import { Version } from '../../src/types/Version'
import { MockedVersions } from '../../src/types/MockedVersions'

type MockedVersions = Index<Version>
type MockedVersionsMatcher = (options: Options) => Index<Version> | null

/** Stubs the npmView function from package-managers/npm. Only works when importing ncu directly in tests, not when the binary is spawned. Returns the stub object. Call stub.restore() after assertions to restore the original function. */
const stubNpmView = (mockReturnedVersions: Version | MockedVersions | MockedVersionsMatcher) =>
sinon
.stub(npmPackageManager, 'viewManyMemoized')
.callsFake((name: string, fields: string[], currentVersion: Version, options: Options) => {
const version =
typeof mockReturnedVersions === 'function'
? mockReturnedVersions(options)?.[name]
: typeof mockReturnedVersions === 'string'
? mockReturnedVersions
: mockReturnedVersions[name]

const packument = {
name,
engines: { node: '' },
time: { [version || '']: new Date().toISOString() },
version: version || '',
// versions are not needed in nested packument
versions: [],
}

return Promise.resolve({
...packument,
versions: [packument],
})
})
/** Stubs the npmView function from package-managers/npm. Returns the stub object. Call stub.restore() after assertions to restore the original function. Set spawn:true to stub ncu spawned as a child process. */
const stubNpmView = (mockReturnedVersions: MockedVersions, { spawn }: { spawn?: boolean } = {}) => {
// stub child process
// the only way to stub functionality in spawned child processes is to pass data through process.env and stub internally
if (spawn) {
process.env.STUB_NPM_VIEW = JSON.stringify(mockReturnedVersions)
return {
restore: () => {
// eslint-disable-next-line fp/no-delete
delete process.env.STUB_NPM_VIEW
},
}
}
// stub module
else {
return sinon
.stub(npmPackageManager, 'viewManyMemoized')
.callsFake(npmPackageManager.mockViewMany(mockReturnedVersions))
}
}

export default stubNpmView

0 comments on commit 6d01108

Please sign in to comment.