Skip to content

Commit 6d01108

Browse files
committed
Mock npm view in tests that spawn ncu in a child process.
1 parent 97fa810 commit 6d01108

File tree

4 files changed

+121
-52
lines changed

4 files changed

+121
-52
lines changed

src/package-managers/npm.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { print } from '../lib/logging'
2020
import * as versionUtil from '../lib/version-util'
2121
import { GetVersion } from '../types/GetVersion'
2222
import { Index } from '../types/IndexType'
23+
import { MockedVersions } from '../types/MockedVersions'
2324
import { NpmConfig } from '../types/NpmConfig'
2425
import { NpmOptions } from '../types/NpmOptions'
2526
import { Options } from '../types/Options'
@@ -270,6 +271,32 @@ export async function packageAuthorChanged(
270271
return false
271272
}
272273

274+
/** Creates a function with the same signature as viewMany that always returns the given versions. */
275+
export const mockViewMany =
276+
(mockReturnedVersions: MockedVersions) =>
277+
(name: string, fields: string[], currentVersion: Version, options: Options): Promise<Packument> => {
278+
const version =
279+
typeof mockReturnedVersions === 'function'
280+
? mockReturnedVersions(options)?.[name]
281+
: typeof mockReturnedVersions === 'string'
282+
? mockReturnedVersions
283+
: mockReturnedVersions[name]
284+
285+
const packument = {
286+
name,
287+
engines: { node: '' },
288+
time: { [version || '']: new Date().toISOString() },
289+
version: version || '',
290+
// versions are not needed in nested packument
291+
versions: [],
292+
}
293+
294+
return Promise.resolve({
295+
...packument,
296+
versions: [packument],
297+
})
298+
}
299+
273300
/**
274301
* Returns an object of specified values retrieved by npm view.
275302
*
@@ -286,6 +313,12 @@ export async function viewMany(
286313
retried = 0,
287314
npmConfigLocal?: NpmConfig,
288315
): Promise<Packument> {
316+
// See: /test/helpers/stubNpmView
317+
if (process.env.STUB_NPM_VIEW) {
318+
const mockReturnedVersions = JSON.parse(process.env.STUB_NPM_VIEW)
319+
return mockViewMany(mockReturnedVersions)(packageName, fields, currentVersion, options)
320+
}
321+
289322
if (currentVersion && (!semver.validRange(currentVersion) || versionUtil.isWildCard(currentVersion))) {
290323
return Promise.resolve({} as Packument)
291324
}

src/types/MockedVersions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Index } from './IndexType'
2+
import { Options } from './Options'
3+
import { Version } from './Version'
4+
5+
export type MockedVersions = Version | Index<Version> | ((options: Options) => Index<Version> | null)

test/bin.test.ts

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import fs from 'fs/promises'
55
import os from 'os'
66
import path from 'path'
77
import spawn from 'spawn-please'
8+
import stubNpmView from './helpers/stubNpmView'
89

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

1718
describe('bin', async function () {
18-
it('runs from the command line', async () => {
19-
await spawn('node', [bin], '{}')
19+
it('fetch latest version from registry (not stubbed)', async () => {
20+
const output = await spawn(
21+
'node',
22+
[bin, '--jsonUpgraded', '--stdin'],
23+
'{ "dependencies": { "ncu-test-v2": "1.0.0" } }',
24+
)
25+
const pkgData = JSON.parse(output)
26+
pkgData.should.have.property('ncu-test-v2')
2027
})
2128

2229
it('output only upgraded with --jsonUpgraded', async () => {
23-
const output = await spawn('node', [bin, '--jsonUpgraded', '--stdin'], '{ "dependencies": { "express": "1" } }')
24-
const pkgData = JSON.parse(output) as Record<string, unknown>
25-
pkgData.should.have.property('express')
30+
const stub = stubNpmView('99.9.9', { spawn: true })
31+
const output = await spawn(
32+
'node',
33+
[bin, '--jsonUpgraded', '--stdin'],
34+
'{ "dependencies": { "ncu-test-v2": "1.0.0" } }',
35+
)
36+
const pkgData = JSON.parse(output)
37+
pkgData.should.have.property('ncu-test-v2')
38+
stub.restore()
2639
})
2740

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

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

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

4766
it('reject out-of-date stdin with errorLevel 2', async () => {
48-
return spawn(
67+
const stub = stubNpmView('99.9.9', { spawn: true })
68+
await spawn(
4969
'node',
5070
[bin, '--stdin', '--errorLevel', '2'],
5171
'{ "dependencies": { "express": "1" } }',
5272
).should.eventually.be.rejectedWith('Dependencies not up-to-date')
73+
stub.restore()
5374
})
5475

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

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

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

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

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

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

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

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

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

165202
it('quote arguments with spaces in upgrade hint', async () => {
203+
const stub = stubNpmView('99.9.9', { spawn: true })
166204
const pkgData = {
167205
dependencies: {
168206
'ncu-test-v2': '^1.0.0',
@@ -177,7 +215,22 @@ describe('bin', async function () {
177215
output.should.include('"ncu-test-v2 ncu-test-tag"')
178216
} finally {
179217
await fs.rm(tempDir, { recursive: true, force: true })
218+
stub.restore()
219+
}
220+
})
221+
222+
it('ignore file: and link: protocols', async () => {
223+
const stub = stubNpmView('99.9.9', { spawn: true })
224+
const { default: stripAnsi } = await import('strip-ansi')
225+
const dependencies = {
226+
editor: 'file:../editor',
227+
event: 'link:../link',
228+
workspace: 'workspace:../workspace',
180229
}
230+
const output = await spawn('node', [bin, '--stdin'], JSON.stringify({ dependencies }))
231+
232+
stripAnsi(output)!.should.not.include('No package versions were returned.')
233+
stub.restore()
181234
})
182235
})
183236

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

197250
it('strip prefix from npm alias in "to" output', async () => {
251+
const stub = stubNpmView('99.9.9', { spawn: true })
198252
// use dynamic import for ESM module
199253
const { default: stripAnsi } = await import('strip-ansi')
200254
const dependencies = {
201255
request: 'npm:ncu-test-v2@1.0.0',
202256
}
203257
const output = await spawn('node', [bin, '--stdin'], JSON.stringify({ dependencies }))
204-
stripAnsi(output).trim().should.equal('request npm:ncu-test-v2@1.0.0 → 2.0.0')
258+
stripAnsi(output).trim().should.equal('request npm:ncu-test-v2@1.0.0 → 99.9.9')
259+
stub.restore()
205260
})
206261
})
207262

@@ -252,17 +307,4 @@ describe('option-specific help', () => {
252307
const output = await spawn('node', [bin, '--help', '--help'])
253308
output.trim().should.not.include('Usage')
254309
})
255-
256-
it('ignore file: and link: protocols', async () => {
257-
const { default: stripAnsi } = await import('strip-ansi')
258-
const dependencies = {
259-
editor: 'file:../editor',
260-
event: 'link:../link',
261-
}
262-
const output = await spawn('node', [bin, '--stdin'], JSON.stringify({ dependencies }))
263-
264-
stripAnsi(output)!.should.not.include(
265-
'No package versions were returned. This is likely a problem with your installed npm',
266-
)
267-
})
268310
})

test/helpers/stubNpmView.ts

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,26 @@
11
import sinon from 'sinon'
22
import * as npmPackageManager from '../../src/package-managers/npm'
3-
import { Index } from '../../src/types/IndexType'
4-
import { Options } from '../../src/types/Options'
5-
import { Version } from '../../src/types/Version'
3+
import { MockedVersions } from '../../src/types/MockedVersions'
64

7-
type MockedVersions = Index<Version>
8-
type MockedVersionsMatcher = (options: Options) => Index<Version> | null
9-
10-
/** 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. */
11-
const stubNpmView = (mockReturnedVersions: Version | MockedVersions | MockedVersionsMatcher) =>
12-
sinon
13-
.stub(npmPackageManager, 'viewManyMemoized')
14-
.callsFake((name: string, fields: string[], currentVersion: Version, options: Options) => {
15-
const version =
16-
typeof mockReturnedVersions === 'function'
17-
? mockReturnedVersions(options)?.[name]
18-
: typeof mockReturnedVersions === 'string'
19-
? mockReturnedVersions
20-
: mockReturnedVersions[name]
21-
22-
const packument = {
23-
name,
24-
engines: { node: '' },
25-
time: { [version || '']: new Date().toISOString() },
26-
version: version || '',
27-
// versions are not needed in nested packument
28-
versions: [],
29-
}
30-
31-
return Promise.resolve({
32-
...packument,
33-
versions: [packument],
34-
})
35-
})
5+
/** 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. */
6+
const stubNpmView = (mockReturnedVersions: MockedVersions, { spawn }: { spawn?: boolean } = {}) => {
7+
// stub child process
8+
// the only way to stub functionality in spawned child processes is to pass data through process.env and stub internally
9+
if (spawn) {
10+
process.env.STUB_NPM_VIEW = JSON.stringify(mockReturnedVersions)
11+
return {
12+
restore: () => {
13+
// eslint-disable-next-line fp/no-delete
14+
delete process.env.STUB_NPM_VIEW
15+
},
16+
}
17+
}
18+
// stub module
19+
else {
20+
return sinon
21+
.stub(npmPackageManager, 'viewManyMemoized')
22+
.callsFake(npmPackageManager.mockViewMany(mockReturnedVersions))
23+
}
24+
}
3625

3726
export default stubNpmView

0 commit comments

Comments
 (0)