Skip to content

Commit f682445

Browse files
nlfisaacs
authored andcommitted
refactor deprecate command and add tests
PR-URL: #2302 Credit: @nlf Close: #2302 Reviewed-by: @ruyadorno
1 parent 2848f59 commit f682445

2 files changed

Lines changed: 192 additions & 54 deletions

File tree

lib/deprecate.js

Lines changed: 58 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,68 +5,72 @@ const fetch = require('npm-registry-fetch')
55
const otplease = require('./utils/otplease.js')
66
const npa = require('npm-package-arg')
77
const semver = require('semver')
8-
const getItentity = require('./utils/get-identity')
8+
const getIdentity = require('./utils/get-identity.js')
9+
const libaccess = require('libnpmaccess')
10+
const usageUtil = require('./utils/usage.js')
911

10-
module.exports = deprecate
12+
const UsageError = () =>
13+
Object.assign(new Error(`\nUsage: ${usage}`), {
14+
code: 'EUSAGE',
15+
})
1116

12-
deprecate.usage = 'npm deprecate <pkg>[@<version>] <message>'
17+
const usage = usageUtil(
18+
'deprecate',
19+
'npm deprecate <pkg>[@<version>] <message>'
20+
)
1321

14-
deprecate.completion = function (opts, cb) {
15-
return Promise.resolve().then(() => {
16-
if (opts.conf.argv.remain.length > 2)
17-
return
18-
return getItentity(npm.flatOptions).then(username => {
19-
if (username) {
20-
// first, get a list of remote packages this user owns.
21-
// once we have a user account, then don't complete anything.
22-
// get the list of packages by user
23-
return fetch(
24-
`/-/by-user/${encodeURIComponent(username)}`,
25-
npm.flatOptions
26-
).then(list => list[username])
27-
}
22+
const completion = (opts, cb) => {
23+
if (opts.conf.argv.remain.length > 1)
24+
return cb(null, [])
25+
26+
return getIdentity(npm.flatOptions).then((username) => {
27+
return libaccess.lsPackages(username, npm.flatOptions).then((packages) => {
28+
return Object.keys(packages)
29+
.filter((name) => packages[name] === 'write' &&
30+
(opts.conf.argv.remain.length === 0 || name.startsWith(opts.conf.argv.remain[0]))
31+
)
2832
})
29-
}).then(() => cb(), er => cb(er))
33+
}).then((list) => cb(null, list), (err) => cb(err))
3034
}
3135

32-
function deprecate ([pkg, msg], opts, cb) {
33-
if (typeof cb !== 'function') {
34-
cb = opts
35-
opts = null
36-
}
37-
opts = opts || npm.flatOptions
38-
return Promise.resolve().then(() => {
39-
if (msg == null)
40-
throw new Error(`Usage: ${deprecate.usage}`)
41-
// fetch the data and make sure it exists.
42-
const p = npa(pkg)
36+
const cmd = (args, cb) =>
37+
deprecate(args)
38+
.then(() => cb())
39+
.catch(err => cb(err.code === 'EUSAGE' ? err.message : err))
40+
41+
const deprecate = async ([pkg, msg]) => {
42+
if (!pkg || !msg)
43+
throw UsageError()
44+
45+
// fetch the data and make sure it exists.
46+
const p = npa(pkg)
47+
// npa makes the default spec "latest", but for deprecation
48+
// "*" is the appropriate default.
49+
const spec = p.rawSpec === '' ? '*' : p.fetchSpec
4350

44-
// npa makes the default spec "latest", but for deprecation
45-
// "*" is the appropriate default.
46-
const spec = p.rawSpec === '' ? '*' : p.fetchSpec
51+
if (semver.validRange(spec, true) === null)
52+
throw new Error(`invalid version range: ${spec}`)
4753

48-
if (semver.validRange(spec, true) === null)
49-
throw new Error('invalid version range: ' + spec)
54+
const uri = '/' + p.escapedName
55+
const packument = await fetch.json(uri, {
56+
...npm.flatOptions,
57+
spec: p,
58+
query: { write: true },
59+
})
5060

51-
const uri = '/' + p.escapedName
52-
return fetch.json(uri, {
53-
...opts,
54-
spec: p,
55-
query: { write: true },
56-
}).then(packument => {
57-
// filter all the versions that match
58-
Object.keys(packument.versions)
59-
.filter(v => semver.satisfies(v, spec))
60-
.forEach(v => {
61-
packument.versions[v].deprecated = msg
62-
})
63-
return otplease(opts, opts => fetch(uri, {
64-
...opts,
65-
spec: p,
66-
method: 'PUT',
67-
body: packument,
68-
ignoreBody: true,
69-
}))
61+
Object.keys(packument.versions)
62+
.filter(v => semver.satisfies(v, spec))
63+
.forEach(v => {
64+
packument.versions[v].deprecated = msg
7065
})
71-
}).then(() => cb(), cb)
66+
67+
return otplease(npm.flatOptions, opts => fetch(uri, {
68+
...opts,
69+
spec: p,
70+
method: 'PUT',
71+
body: packument,
72+
ignoreBody: true,
73+
}))
7274
}
75+
76+
module.exports = Object.assign(cmd, { completion, usage })

test/lib/deprecate.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
const { test } = require('tap')
2+
const requireInject = require('require-inject')
3+
4+
let getIdentityImpl = () => 'someperson'
5+
let npmFetchBody = null
6+
7+
const npmFetch = async (uri, opts) => {
8+
npmFetchBody = opts.body
9+
}
10+
11+
npmFetch.json = async (uri, opts) => {
12+
return {
13+
versions: {
14+
'1.0.0': {},
15+
'1.0.1': {},
16+
},
17+
}
18+
}
19+
20+
const deprecate = requireInject('../../lib/deprecate.js', {
21+
'../../lib/npm.js': {
22+
flatOptions: { registry: 'https://registry.npmjs.org' },
23+
},
24+
'../../lib/utils/get-identity.js': async () => getIdentityImpl(),
25+
'../../lib/utils/otplease.js': async (opts, fn) => fn(opts),
26+
libnpmaccess: {
27+
lsPackages: async () => ({ foo: 'write', bar: 'write', baz: 'write', buzz: 'read' }),
28+
},
29+
'npm-registry-fetch': npmFetch,
30+
})
31+
32+
test('completion', async t => {
33+
const defaultIdentityImpl = getIdentityImpl
34+
t.teardown(() => {
35+
getIdentityImpl = defaultIdentityImpl
36+
})
37+
38+
const { completion } = deprecate
39+
40+
const testComp = (argv, expect) => {
41+
return new Promise((resolve, reject) => {
42+
completion({ conf: { argv: { remain: argv } } }, (err, res) => {
43+
if (err)
44+
return reject(err)
45+
46+
t.strictSame(res, expect, `completion: ${argv}`)
47+
resolve()
48+
})
49+
})
50+
}
51+
52+
await testComp([], ['foo', 'bar', 'baz'])
53+
await testComp(['b'], ['bar', 'baz'])
54+
await testComp(['fo'], ['foo'])
55+
await testComp(['g'], [])
56+
await testComp(['foo', 'something'], [])
57+
58+
getIdentityImpl = () => {
59+
throw new Error('unknown failure')
60+
}
61+
62+
t.rejects(testComp([], []), /unknown failure/)
63+
})
64+
65+
test('no args', t => {
66+
deprecate([], (err) => {
67+
t.match(err, /Usage: npm deprecate/, 'logs usage')
68+
t.end()
69+
})
70+
})
71+
72+
test('only one arg', t => {
73+
deprecate(['foo'], (err) => {
74+
t.match(err, /Usage: npm deprecate/, 'logs usage')
75+
t.end()
76+
})
77+
})
78+
79+
test('invalid semver range', t => {
80+
deprecate(['foo@notaversion', 'this will fail'], (err) => {
81+
t.match(err, /invalid version range/, 'logs semver error')
82+
t.end()
83+
})
84+
})
85+
86+
test('deprecates given range', t => {
87+
t.teardown(() => {
88+
npmFetchBody = null
89+
})
90+
91+
deprecate(['foo@1.0.0', 'this version is deprecated'], (err) => {
92+
if (err)
93+
throw err
94+
95+
t.match(npmFetchBody, {
96+
versions: {
97+
'1.0.0': {
98+
deprecated: 'this version is deprecated',
99+
},
100+
'1.0.1': {
101+
// the undefined here is necessary to ensure that we absolutely
102+
// did not assign this property
103+
deprecated: undefined,
104+
},
105+
},
106+
})
107+
108+
t.end()
109+
})
110+
})
111+
112+
test('deprecates all versions when no range is specified', t => {
113+
t.teardown(() => {
114+
npmFetchBody = null
115+
})
116+
117+
deprecate(['foo', 'this version is deprecated'], (err) => {
118+
if (err)
119+
throw err
120+
121+
t.match(npmFetchBody, {
122+
versions: {
123+
'1.0.0': {
124+
deprecated: 'this version is deprecated',
125+
},
126+
'1.0.1': {
127+
deprecated: 'this version is deprecated',
128+
},
129+
},
130+
})
131+
132+
t.end()
133+
})
134+
})

0 commit comments

Comments
 (0)