diff options
author | Ruy Adorno <ruyadorno@hotmail.com> | 2020-12-11 22:17:16 +0300 |
---|---|---|
committer | isaacs <i@izs.me> | 2020-12-18 23:02:27 +0300 |
commit | cba3341dae4c92541049dc976e82e2ba19566e95 (patch) | |
tree | 58cd8ea6579879b285d92a7b5a211e3f7a9a799b /test | |
parent | 44d4331058c53909ada62470b23b2185102b2128 (diff) |
fix: npm profile refactor
- Fixes using `--otp` config option in `npm profile enable-2fa`
- Prevents trying to enable 2fa if no user is logged in
- Setting email should not require password
- Add `test/lib/profile.js` tests
- Async/await `lib/profile.js` refactor and more
- Fixes: https://github.com/npm/statusboard/issues/164
PR-URL: https://github.com/npm/cli/pull/2373
Credit: @ruyadorno
Close: #2373
Reviewed-by: @isaacs
Diffstat (limited to 'test')
-rw-r--r-- | test/lib/profile.js | 1465 |
1 files changed, 1465 insertions, 0 deletions
diff --git a/test/lib/profile.js b/test/lib/profile.js new file mode 100644 index 000000000..48a558cac --- /dev/null +++ b/test/lib/profile.js @@ -0,0 +1,1465 @@ +const t = require('tap') +const requireInject = require('require-inject') + +let result = '' +const flatOptions = { + otp: '', + json: false, + parseable: false, + registry: 'https://registry.npmjs.org/', +} +const npm = { config: {}, flatOptions: { ...flatOptions }} +const mocks = { + ansistyles: { bright: a => a }, + npmlog: { + gauge: { show () {} }, + info () {}, + notice () {}, + warn () {}, + }, + 'npm-profile': { + async get () {}, + async set () {}, + async createToken () {}, + }, + 'qrcode-terminal': { generate: (url, cb) => cb() }, + 'cli-table3': class extends Array { + toString () { + return this + .filter(Boolean) + .map(i => [...Object.entries(i)] + .map(i => i.join(': '))) + .join('\n') + } + }, + '../../lib/npm.js': npm, + '../../lib/utils/output.js': (...msg) => { + result += msg.join('\n') + }, + '../../lib/utils/pulse-till-done.js': { + withPromise: async a => a, + }, + '../../lib/utils/otplease.js': async (opts, fn) => fn(opts), + '../../lib/utils/usage.js': () => 'usage instructions', + '../../lib/utils/read-user-info.js': { + async password () {}, + async otp () {}, + }, +} +const userProfile = { + tfa: { pending: false, mode: 'auth-and-writes' }, + name: 'foo', + email: 'foo@github.com', + email_verified: true, + created: '2015-02-26T01:26:37.384Z', + updated: '2020-08-12T16:19:35.326Z', + cidr_whitelist: null, + fullname: 'Foo Bar', + homepage: 'https://github.com', + freenode: 'foobar', + twitter: 'https://twitter.com/npmjs', + github: 'https://github.com/npm', +} + +t.afterEach(cb => { + result = '' + npm.config = {} + npm.flatOptions = { ...flatOptions } + cb() +}) + +const profile = requireInject('../../lib/profile.js', mocks) + +t.test('no args', t => { + profile([], err => { + t.match( + err, + /usage instructions/, + 'should throw usage instructions' + ) + t.end() + }) +}) + +t.test('profile get no args', t => { + const npmProfile = { + async get () { + return userProfile + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + }) + + t.test('default output', t => { + profile(['get'], err => { + if (err) + throw err + + t.matchSnapshot( + result, + 'should output table with contents' + ) + t.end() + }) + }) + + t.test('--json', t => { + npm.flatOptions.json = true + + profile(['get'], err => { + if (err) + throw err + + t.deepEqual( + JSON.parse(result), + userProfile, + 'should output json profile result' + ) + t.end() + }) + }) + + t.test('--parseable', t => { + npm.flatOptions.parseable = true + + profile(['get'], err => { + if (err) + throw err + + t.matchSnapshot( + result, + 'should output all profile info as parseable result' + ) + t.end() + }) + }) + + t.test('no tfa enabled', t => { + const npmProfile = { + async get () { + return { + ...userProfile, + tfa: null, + } + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + }) + + profile(['get'], err => { + if (err) + throw err + + t.matchSnapshot( + result, + 'should output expected profile values' + ) + t.end() + }) + }) + + t.test('unverified email', t => { + const npmProfile = { + async get () { + return { + ...userProfile, + email_verified: false, + } + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + }) + + profile(['get'], err => { + if (err) + throw err + + t.matchSnapshot( + result, + 'should output table with contents' + ) + t.end() + }) + }) + + t.test('profile has cidr_whitelist item', t => { + const npmProfile = { + async get () { + return { + ...userProfile, + cidr_whitelist: ['192.168.1.1'], + } + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + }) + + profile(['get'], err => { + if (err) + throw err + + t.matchSnapshot( + result, + 'should output table with contents' + ) + t.end() + }) + }) + + t.end() +}) + +t.test('profile get <key>', t => { + const npmProfile = { + async get () { + return userProfile + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + }) + + t.test('default output', t => { + profile(['get', 'name'], err => { + if (err) + throw err + + t.equal( + result, + 'foo', + 'should output value result' + ) + t.end() + }) + }) + + t.test('--json', t => { + npm.flatOptions.json = true + + profile(['get', 'name'], err => { + if (err) + throw err + + t.deepEqual( + JSON.parse(result), + userProfile, + 'should output json profile result ignoring args filter' + ) + t.end() + }) + }) + + t.test('--parseable', t => { + npm.flatOptions.parseable = true + + profile(['get', 'name'], err => { + if (err) + throw err + + t.matchSnapshot( + result, + 'should output parseable result value' + ) + t.end() + }) + }) + + t.end() +}) + +t.test('profile get multiple args', t => { + const npmProfile = { + async get () { + return userProfile + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + }) + + t.test('default output', t => { + profile(['get', 'name', 'email', 'github'], err => { + if (err) + throw err + + t.matchSnapshot( + result, + 'should output all keys' + ) + t.end() + }) + }) + + t.test('--json', t => { + npm.flatOptions.json = true + + profile(['get', 'name', 'email', 'github'], err => { + if (err) + throw err + + t.deepEqual( + JSON.parse(result), + userProfile, + 'should output json profile result and ignore args' + ) + t.end() + }) + }) + + t.test('--parseable', t => { + npm.flatOptions.parseable = true + + profile(['get', 'name', 'email', 'github'], err => { + if (err) + throw err + + t.matchSnapshot( + result, + 'should output parseable profile value results' + ) + t.end() + }) + }) + + t.test('comma separated', t => { + profile(['get', 'name,email,github'], err => { + if (err) + throw err + + t.matchSnapshot( + result, + 'should output all keys' + ) + t.end() + }) + }) + + t.end() +}) + +t.test('profile set <key> <value>', t => { + const npmProfile = t => ({ + async get () { + return userProfile + }, + async set (newUser, conf) { + t.match( + newUser, + { + fullname: 'Lorem Ipsum', + }, + 'should set new value to key' + ) + return { + ...userProfile, + ...newUser, + } + }, + }) + + t.test('no key', t => { + profile(['set'], err => { + t.match( + err, + /npm profile set <prop> <value>/, + 'should throw proper usage message' + ) + t.end() + }) + }) + + t.test('no value', t => { + profile(['set', 'email'], err => { + t.match( + err, + /npm profile set <prop> <value>/, + 'should throw proper usage message' + ) + t.end() + }) + }) + + t.test('set password', t => { + profile(['set', 'password', '1234'], err => { + t.match( + err, + /Do not include your current or new passwords on the command line./, + 'should throw an error refusing to set password from args' + ) + t.end() + }) + }) + + t.test('unwritable key', t => { + profile(['set', 'name', 'foo'], err => { + t.match( + err, + /"name" is not a property we can set./, + 'should throw the unwritable key error' + ) + t.end() + }) + }) + + t.test('writable key', t => { + t.test('default output', t => { + t.plan(2) + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile(t), + }) + + profile(['set', 'fullname', 'Lorem Ipsum'], err => { + if (err) + throw err + + t.equal( + result, + 'Set\nfullname\nto\nLorem Ipsum', + 'should output set key success msg' + ) + }) + }) + + t.test('--json', t => { + t.plan(2) + + npm.flatOptions.json = true + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile(t), + }) + + profile(['set', 'fullname', 'Lorem Ipsum'], err => { + if (err) + throw err + + t.deepEqual( + JSON.parse(result), + { + fullname: 'Lorem Ipsum', + }, + 'should output json set key success msg' + ) + }) + }) + + t.test('--parseable', t => { + t.plan(2) + + npm.flatOptions.parseable = true + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile(t), + }) + + profile(['set', 'fullname', 'Lorem Ipsum'], err => { + if (err) + throw err + + t.matchSnapshot( + result, + 'should output parseable set key success msg' + ) + }) + }) + + t.end() + }) + + t.test('write new email', t => { + t.plan(3) + + const npmProfile = { + async get () { + return userProfile + }, + async set (newUser, conf) { + t.match( + newUser, + { + email: 'foo@npmjs.com', + }, + 'should set new value to email' + ) + t.match( + conf, + npm.flatOptions, + 'should forward flatOptions config' + ) + return { + ...userProfile, + ...newUser, + } + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + }) + + profile(['set', 'email', 'foo@npmjs.com'], err => { + if (err) + throw err + + t.equal( + result, + 'Set\nemail\nto\nfoo@npmjs.com', + 'should output set key success msg' + ) + }) + }) + + t.test('change password', t => { + t.plan(6) + + const npmProfile = { + async get () { + return userProfile + }, + async set (newUser, conf) { + t.match( + newUser, + { + password: { + old: 'currentpassword1234', + new: 'newpassword1234', + }, + }, + 'should set new password' + ) + t.match( + conf, + npm.flatOptions, + 'should forward flatOptions config' + ) + return { + ...userProfile, + } + }, + } + + const readUserInfo = { + async password (label) { + if (label === 'Current password: ') + t.ok('should interactively ask for password confirmation') + else if (label === 'New password: ') + t.ok('should interactively ask for new password') + else if (label === ' Again: ') + t.ok('should interactively ask for new password confirmation') + else + throw new Error('Unexpected label: ' + label) + + return label === 'Current password: ' + ? 'currentpassword1234' + : 'newpassword1234' + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + '../../lib/utils/read-user-info.js': readUserInfo, + }) + + profile(['set', 'password'], err => { + if (err) + throw err + + t.equal( + result, + 'Set\npassword', + 'should output set password success msg' + ) + t.end() + }) + }) + + t.test('password confirmation mismatch', t => { + t.plan(3) + let passwordPromptCount = 0 + + const npmProfile = { + async get () { + return userProfile + }, + async set (newUser, conf) { + return { + ...userProfile, + } + }, + } + + const readUserInfo = { + async password (label) { + passwordPromptCount++ + + switch (label) { + case 'Current password: ': + return 'currentpassword1234' + case 'New password: ': + return passwordPromptCount < 3 + ? 'password-that-will-not-be-confirmed' + : 'newpassword' + case ' Again: ': + return 'newpassword' + default: + return 'password1234' + } + }, + } + + const npmlog = { + gauge: { + show () {}, + }, + warn (title, msg) { + t.equal(title, 'profile', 'should use expected profile') + t.equal( + msg, + 'Passwords do not match, please try again.', + 'should log password mismatch message' + ) + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + npmlog, + 'npm-profile': npmProfile, + '../../lib/utils/read-user-info.js': readUserInfo, + }) + + profile(['set', 'password'], err => { + if (err) + throw err + + t.equal( + result, + 'Set\npassword', + 'should output set password success msg' + ) + t.end() + }) + }) + + t.end() +}) + +t.test('enable-2fa', t => { + t.test('invalid args', t => { + profile(['enable-2fa', 'foo', 'bar'], err => { + t.match( + err, + /npm profile enable-2fa \[auth-and-writes|auth-only\]/, + 'should throw usage error' + ) + t.end() + }) + }) + + t.test('invalid two factor auth mode', t => { + profile(['enable-2fa', 'foo'], err => { + t.match( + err, + /Invalid two-factor authentication mode "foo"/, + 'should throw invalid auth mode error' + ) + t.end() + }) + }) + + t.test('no support for --json output', t => { + npm.flatOptions.json = true + + profile(['enable-2fa', 'auth-only'], err => { + t.match( + err.message, + 'Enabling two-factor authentication is an interactive ' + + 'operation and JSON output mode is not available', + 'should throw no support msg' + ) + t.end() + }) + }) + + t.test('no support for --parseable output', t => { + npm.flatOptions.parseable = true + + profile(['enable-2fa', 'auth-only'], err => { + t.match( + err.message, + 'Enabling two-factor authentication is an interactive ' + + 'operation and parseable output mode is not available', + 'should throw no support msg' + ) + t.end() + }) + }) + + t.test('no bearer tokens returned by registry', t => { + t.plan(3) + + // mock legacy basic auth style + npm.config.getCredentialsByURI = reg => { + t.equal(reg, flatOptions.registry, 'should use expected registry') + return { auth: Buffer.from('foo:bar').toString('base64') } + } + + const npmProfile = { + async createToken (pass) { + t.match(pass, 'bar', 'should use password for basic auth') + return {} + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + }) + + profile(['enable-2fa', 'auth-only'], err => { + t.match( + err.message, + 'Your registry https://registry.npmjs.org/ does ' + + 'not seem to support bearer tokens. Bearer tokens ' + + 'are required for two-factor authentication', + 'should throw no support msg' + ) + }) + }) + + t.test('from basic username/password auth', t => { + // mock legacy basic auth style with user/pass + npm.config.getCredentialsByURI = () => { + return { username: 'foo', password: 'bar' } + } + + const npmProfile = { + async createToken (pass) { + return {} + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + }) + + profile(['enable-2fa', 'auth-only'], err => { + t.match( + err.message, + 'Your registry https://registry.npmjs.org/ does ' + + 'not seem to support bearer tokens. Bearer tokens ' + + 'are required for two-factor authentication', + 'should throw no support msg' + ) + t.end() + }) + }) + + t.test('no auth found', t => { + npm.config.getCredentialsByURI = () => ({}) + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + }) + + profile(['enable-2fa', 'auth-only'], err => { + t.match( + err.message, + 'You need to be logged in to registry ' + + 'https://registry.npmjs.org/ in order to enable 2fa' + ) + t.end() + }) + }) + + t.test('from basic auth, asks for otp', t => { + t.plan(10) + + // mock legacy basic auth style + npm.config = { + getCredentialsByURI (reg) { + t.equal(reg, flatOptions.registry, 'should use expected registry') + return { auth: Buffer.from('foo:bar').toString('base64') } + }, + setCredentialsByURI (registry, { token }) { + t.equal(registry, flatOptions.registry, 'should set expected registry') + t.equal(token, 'token', 'should set expected token') + }, + save (type) { + t.equal(type, 'user', 'should save to user config') + }, + } + + const npmProfile = { + async createToken (pass) { + t.match(pass, 'bar', 'should use password for basic auth') + return { token: 'token' } + }, + async get () { + return userProfile + }, + async set (newProfile, conf) { + t.match( + newProfile, + { + tfa: { + mode: 'auth-only', + }, + }, + 'should set tfa mode' + ) + t.match( + conf, + { + ...npm.flatOptions, + otp: '123456', + }, + 'should forward flatOptions config' + ) + return { + ...userProfile, + tfa: null, + } + }, + } + + const readUserInfo = { + async password () { + t.ok('should interactively ask for password confirmation') + return 'password1234' + }, + async otp (label) { + t.equal( + label, + 'Enter one-time password from your authenticator app: ', + 'should ask for otp confirmation' + ) + return '123456' + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + '../../lib/utils/read-user-info.js': readUserInfo, + }) + + profile(['enable-2fa', 'auth-only'], err => { + if (err) + throw err + + t.equal( + result, + 'Two factor authentication mode changed to: auth-only', + 'should output success msg' + ) + }) + }) + + t.test('from token and set otp, retries on pending and verifies with qrcode', t => { + t.plan(4) + + npm.flatOptions.otp = '1234' + + npm.config = { + getCredentialsByURI () { + return { token: 'token' } + }, + } + + let setCount = 0 + const npmProfile = { + async get () { + return { + ...userProfile, + tfa: { + pending: true, + }, + } + }, + async set (newProfile, conf) { + setCount++ + + // when profile response shows that 2fa is pending the + // first time calling npm-profile.set should reset 2fa + if (setCount === 1) { + t.match( + newProfile, + { + tfa: { + password: 'password1234', + mode: 'disable', + }, + }, + 'should reset 2fa' + ) + } else if (setCount === 2) { + t.match( + newProfile, + { + tfa: { + mode: 'auth-only', + }, + }, + 'should set tfa mode approprietly in follow-up call' + ) + } else if (setCount === 3) { + t.match( + newProfile, + { + tfa: ['123456'], + }, + 'should set tfa as otp code?' + ) + return { + ...userProfile, + tfa: [ + '123456', + '789101', + ], + } + } + + return { + ...userProfile, + tfa: 'otpauth://foo?secret=1234', + } + }, + } + + const readUserInfo = { + async password () { + return 'password1234' + }, + async otp (label) { + return '123456' + }, + } + + const qrcode = { + // eslint-disable-next-line standard/no-callback-literal + generate: (url, cb) => cb('qrcode'), + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + 'qrcode-terminal': qrcode, + '../../lib/utils/read-user-info.js': readUserInfo, + }) + + profile(['enable-2fa', 'auth-only'], err => { + if (err) + throw err + + t.matchSnapshot( + result, + 'should output 2fa enablement success msgs' + ) + }) + }) + + t.test('from token and set otp, retrieves invalid otp', t => { + npm.flatOptions.otp = '1234' + + npm.config = { + getCredentialsByURI () { + return { token: 'token' } + }, + } + + const npmProfile = { + async get () { + return { + ...userProfile, + tfa: { + pending: true, + }, + } + }, + async set (newProfile, conf) { + return { + ...userProfile, + tfa: 'http://foo?secret=1234', + } + }, + } + + const readUserInfo = { + async password () { + return 'password1234' + }, + async otp (label) { + return '123456' + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + '../../lib/utils/read-user-info.js': readUserInfo, + }) + + profile(['enable-2fa', 'auth-only'], err => { + t.match( + err, + /Unknown error enabling two-factor authentication./, + 'should throw invalid 2fa auth url error' + ) + t.end() + }) + }) + + t.test('from token auth provides --otp config arg', t => { + npm.flatOptions.otp = '123456' + + npm.config = { + getCredentialsByURI (reg) { + return { token: 'token' } + }, + } + + const npmProfile = { + async get () { + return userProfile + }, + async set (newProfile, conf) { + return { + ...userProfile, + tfa: null, + } + }, + } + + const readUserInfo = { + async password () { + return 'password1234' + }, + async otp () { + throw new Error('should not ask for otp') + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + '../../lib/utils/read-user-info.js': readUserInfo, + }) + + profile(['enable-2fa', 'auth-and-writes'], err => { + if (err) + throw err + + t.equal( + result, + 'Two factor authentication mode changed to: auth-and-writes', + 'should output success msg' + ) + t.end() + }) + }) + + t.test('missing tfa from user profile', t => { + npm.config = { + getCredentialsByURI (reg) { + return { token: 'token' } + }, + } + + const npmProfile = { + async get () { + return { + ...userProfile, + tfa: undefined, + } + }, + async set (newProfile, conf) { + return { + ...userProfile, + tfa: null, + } + }, + } + + const readUserInfo = { + async password () { + return 'password1234' + }, + async otp () { + return '123456' + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + '../../lib/utils/read-user-info.js': readUserInfo, + }) + + profile(['enable-2fa', 'auth-only'], err => { + if (err) + throw err + + t.equal( + result, + 'Two factor authentication mode changed to: auth-only', + 'should output success msg' + ) + t.end() + }) + }) + + t.test('defaults to auth-and-writes permission if no mode specified', t => { + npm.config = { + getCredentialsByURI (reg) { + return { token: 'token' } + }, + } + + const npmProfile = { + async get () { + return { + ...userProfile, + tfa: undefined, + } + }, + async set (newProfile, conf) { + return { + ...userProfile, + tfa: null, + } + }, + } + + const readUserInfo = { + async password () { + return 'password1234' + }, + async otp () { + return '123456' + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + '../../lib/utils/read-user-info.js': readUserInfo, + }) + + profile(['enable-2fa'], err => { + if (err) + throw err + + t.equal( + result, + 'Two factor authentication mode changed to: auth-and-writes', + 'should enable 2fa with auth-and-writes permission' + ) + t.end() + }) + }) + + t.end() +}) + +t.test('disable-2fa', t => { + t.test('no tfa enabled', t => { + const npmProfile = { + async get () { + return { + ...userProfile, + tfa: null, + } + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + }) + + profile(['disable-2fa'], err => { + if (err) + throw err + + t.equal( + result, + 'Two factor authentication not enabled.', + 'should output already disalbed msg' + ) + t.end() + }) + }) + + t.test('requests otp', t => { + const npmProfile = t => ({ + async get () { + return userProfile + }, + async set (newProfile, conf) { + t.deepEqual( + newProfile, + { + tfa: { + password: 'password1234', + mode: 'disable', + }, + }, + 'should send the new info for setting in profile' + ) + t.match( + conf, + { + ...npm.flatOptions, + otp: '1234', + }, + 'should forward flatOptions config' + ) + }, + }) + + const readUserInfo = t => ({ + async password () { + t.ok('should interactively ask for password confirmation') + return 'password1234' + }, + async otp (label) { + t.equal( + label, + 'Enter one-time password from your authenticator app: ', + 'should ask for otp confirmation' + ) + return '1234' + }, + }) + + t.test('default output', t => { + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile(t), + '../../lib/utils/read-user-info.js': readUserInfo(t), + }) + + profile(['disable-2fa'], err => { + if (err) + throw err + + t.equal( + result, + 'Two factor authentication disabled.', + 'should output already disabled msg' + ) + t.end() + }) + }) + + t.test('--json', t => { + npm.flatOptions.json = true + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile(t), + '../../lib/utils/read-user-info.js': readUserInfo(t), + }) + + profile(['disable-2fa'], err => { + if (err) + throw err + + t.deepEqual( + JSON.parse(result), + { tfa: false }, + 'should output json already disabled msg' + ) + t.end() + }) + }) + + t.test('--parseable', t => { + npm.flatOptions.parseable = true + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile(t), + '../../lib/utils/read-user-info.js': readUserInfo(t), + }) + + profile(['disable-2fa'], err => { + if (err) + throw err + + t.equal( + result, + 'tfa\tfalse', + 'should output parseable already disabled msg' + ) + t.end() + }) + }) + + t.end() + }) + + t.test('--otp config already set', t => { + t.plan(3) + + npm.flatOptions.otp = '123456' + + const npmProfile = { + async get () { + return userProfile + }, + async set (newProfile, conf) { + t.deepEqual( + newProfile, + { + tfa: { + password: 'password1234', + mode: 'disable', + }, + }, + 'should send the new info for setting in profile' + ) + t.match( + conf, + { + ...npm.flatOptions, + otp: '123456', + }, + 'should forward flatOptions config' + ) + }, + } + + const readUserInfo = { + async password () { + return 'password1234' + }, + async otp (label) { + throw new Error('should not ask for otp') + }, + } + + const profile = requireInject('../../lib/profile.js', { + ...mocks, + 'npm-profile': npmProfile, + '../../lib/utils/read-user-info.js': readUserInfo, + }) + + profile(['disable-2fa'], err => { + if (err) + throw err + + t.equal( + result, + 'Two factor authentication disabled.', + 'should output already disalbed msg' + ) + }) + }) + + t.end() +}) + +t.test('unknown subcommand', t => { + profile(['asfd'], err => { + t.match( + err, + /Unknown profile command: asfd/, + 'should throw unknown cmd error' + ) + t.end() + }) +}) + +t.test('completion', t => { + const { completion } = profile + + const testComp = ({ t, argv, expect, title }) => { + completion({ conf: { argv: { remain: argv } } }, (err, res) => { + if (err) + throw err + + t.strictSame(res, expect, title) + }) + } + + t.test('npm profile autocomplete', t => { + testComp({ + t, + argv: ['npm', 'profile'], + expect: ['enable-2fa', 'disable-2fa', 'get', 'set'], + title: 'should auto complete with subcommands', + }) + + t.end() + }) + + t.test('npm profile enable autocomplete', t => { + testComp({ + t, + argv: ['npm', 'profile', 'enable-2fa'], + expect: ['auth-and-writes', 'auth-only'], + title: 'should auto complete with auth types', + }) + + t.end() + }) + + t.test('npm profile <subcmd> no autocomplete', t => { + const noAutocompleteCmds = ['disable-2fa', 'disable-tfa', 'get', 'set'] + for (const subcmd of noAutocompleteCmds) { + testComp({ + t, + argv: ['npm', 'profile', subcmd], + expect: [], + title: `${subcmd} should have no autocomplete`, + }) + } + + t.end() + }) + + t.test('npm profile unknown subcommand autocomplete', t => { + completion({ + conf: { + argv: { + remain: ['npm', 'profile', 'asdf'], + }, + }, + }, (err, res) => { + t.match( + err, + /asdf not recognized/, + 'should throw unknown cmd error' + ) + + t.end() + }) + }) + + t.end() +}) |