%PDF- <> %âãÏÓ endobj 2 0 obj <> endobj 3 0 obj <>/ExtGState<>/ProcSet[/PDF/Text/ImageB/ImageC/ImageI] >>/Annots[ 28 0 R 29 0 R] /MediaBox[ 0 0 595.5 842.25] /Contents 4 0 R/Group<>/Tabs/S>> endobj ºaâÚÎΞ-ÌE1ÍØÄ÷{òò2ÿ ÛÖ^ÔÀá TÎ{¦?§®¥kuµùÕ5sLOšuY>endobj 2 0 obj<>endobj 2 0 obj<>endobj 2 0 obj<>endobj 2 0 obj<> endobj 2 0 obj<>endobj 2 0 obj<>es 3 0 R>> endobj 2 0 obj<> ox[ 0.000000 0.000000 609.600000 935.600000]/Fi endobj 3 0 obj<> endobj 7 1 obj<>/ProcSet[/PDF/Text/ImageB/ImageC/ImageI]>>/Subtype/Form>> stream
const t = require('tap') const { fake: mockNpm } = require('../../fixtures/mock-npm') let result = '' const config = { otp: '', json: false, parseable: false, registry: 'https://registry.npmjs.org/', } const flatOptions = { registry: 'https://registry.npmjs.org/', } const npm = mockNpm({ config, flatOptions, output: (...msg) => { result = result ? `${result}\n${msg.join('\n')}` : msg.join('\n') }, }) const mocks = { npmlog: { gauge: { show () {} }, }, 'proc-log': { 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/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(() => { result = '' flatOptions.otp = '' config.json = false config.parseable = false config.registry = 'https://registry.npmjs.org/' }) const Profile = t.mock('../../../lib/commands/profile.js', mocks) const profile = new Profile(npm) t.test('no args', async t => { await t.rejects(profile.exec([]), profile.usage) }) t.test('profile get no args', t => { const npmProfile = { async get () { return userProfile }, } const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, }) const profile = new Profile(npm) t.test('default output', async t => { await profile.exec(['get']) t.matchSnapshot(result, 'should output table with contents') }) t.test('--json', async t => { config.json = true await profile.exec(['get']) t.same(JSON.parse(result), userProfile, 'should output json profile result') }) t.test('--parseable', async t => { config.parseable = true await profile.exec(['get']) t.matchSnapshot(result, 'should output all profile info as parseable result') }) t.test('no tfa enabled', async t => { const npmProfile = { async get () { return { ...userProfile, tfa: null, } }, } const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, }) const profile = new Profile(npm) await profile.exec(['get']) t.matchSnapshot(result, 'should output expected profile values') }) t.test('unverified email', async t => { const npmProfile = { async get () { return { ...userProfile, email_verified: false, } }, } const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, }) const profile = new Profile(npm) await profile.exec(['get']) t.matchSnapshot(result, 'should output table with contents') }) t.test('profile has cidr_whitelist item', async t => { const npmProfile = { async get () { return { ...userProfile, cidr_whitelist: ['192.168.1.1'], } }, } const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, }) const profile = new Profile(npm) await profile.exec(['get']) t.matchSnapshot(result, 'should output table with contents') }) t.end() }) t.test('profile get <key>', t => { const npmProfile = { async get () { return userProfile }, } const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, }) const profile = new Profile(npm) t.test('default output', async t => { await profile.exec(['get', 'name']) t.equal(result, 'foo', 'should output value result') }) t.test('--json', async t => { config.json = true await profile.exec(['get', 'name']) t.same( JSON.parse(result), userProfile, 'should output json profile result ignoring args filter' ) }) t.test('--parseable', async t => { config.parseable = true await profile.exec(['get', 'name']) t.matchSnapshot(result, 'should output parseable result value') }) t.end() }) t.test('profile get multiple args', t => { const npmProfile = { async get () { return userProfile }, } const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, }) const profile = new Profile(npm) t.test('default output', async t => { await profile.exec(['get', 'name', 'email', 'github']) t.matchSnapshot(result, 'should output all keys') }) t.test('--json', async t => { config.json = true await profile.exec(['get', 'name', 'email', 'github']) t.same(JSON.parse(result), userProfile, 'should output json profile result and ignore args') }) t.test('--parseable', async t => { config.parseable = true await profile.exec(['get', 'name', 'email', 'github']) t.matchSnapshot(result, 'should output parseable profile value results') }) t.test('comma separated', async t => { await profile.exec(['get', 'name,email,github']) t.matchSnapshot(result, 'should output all keys') }) 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', async t => { await t.rejects( profile.exec(['set']), /npm profile set <prop> <value>/, 'should throw proper usage message' ) }) t.test('no value', async t => { await t.rejects( profile.exec(['set', 'email']), /npm profile set <prop> <value>/, 'should throw proper usage message' ) }) t.test('set password', async t => { await t.rejects( profile.exec(['set', 'password', '1234']), /Do not include your current or new passwords on the command line./, 'should throw an error refusing to set password from args' ) }) t.test('unwritable key', async t => { await await t.rejects( profile.exec(['set', 'name', 'foo']), /"name" is not a property we can set./, 'should throw the unwritable key error' ) }) t.test('writable key', t => { t.test('default output', async t => { t.plan(2) const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile(t), }) const profile = new Profile(npm) await profile.exec(['set', 'fullname', 'Lorem Ipsum']) t.equal(result, 'Set\nfullname\nto\nLorem Ipsum', 'should output set key success msg') }) t.test('--json', async t => { t.plan(2) config.json = true const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile(t), }) const profile = new Profile(npm) await profile.exec(['set', 'fullname', 'Lorem Ipsum']) t.same( JSON.parse(result), { fullname: 'Lorem Ipsum', }, 'should output json set key success msg' ) }) t.test('--parseable', async t => { t.plan(2) config.parseable = true const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile(t), }) const profile = new Profile(npm) await profile.exec(['set', 'fullname', 'Lorem Ipsum']) t.matchSnapshot(result, 'should output parseable set key success msg') }) t.end() }) t.test('write new email', async 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 = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, }) const profile = new Profile(npm) await profile.exec(['set', 'email', 'foo@npmjs.com']) t.equal(result, 'Set\nemail\nto\nfoo@npmjs.com', 'should output set key success msg') }) t.test('change password', async 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 = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../../lib/utils/read-user-info.js': readUserInfo, }) const profile = new Profile(npm) await profile.exec(['set', 'password']) t.equal(result, 'Set\npassword', 'should output set password success msg') }) t.test('password confirmation mismatch', async 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 Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, npmlog: { gauge: { show () {}, }, }, 'proc-log': { 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' ) }, }, 'npm-profile': npmProfile, '../../../lib/utils/read-user-info.js': readUserInfo, }) const profile = new Profile(npm) await profile.exec(['set', 'password']) t.equal(result, 'Set\npassword', 'should output set password success msg') }) t.end() }) t.test('enable-2fa', t => { t.test('invalid args', async t => { await t.rejects( profile.exec(['enable-2fa', 'foo', 'bar']), /npm profile enable-2fa \[auth-and-writes|auth-only\]/, 'should throw usage error' ) }) t.test('invalid two factor auth mode', async t => { await t.rejects( profile.exec(['enable-2fa', 'foo']), /Invalid two-factor authentication mode "foo"/, 'should throw invalid auth mode error' ) }) t.test('no support for --json output', async t => { config.json = true await t.rejects( profile.exec(['enable-2fa', 'auth-only']), 'Enabling two-factor authentication is an interactive ' + 'operation and JSON output mode is not available', 'should throw no support msg' ) }) t.test('no support for --parseable output', async t => { config.parseable = true await t.rejects( profile.exec(['enable-2fa', 'auth-only']), 'Enabling two-factor authentication is an interactive ' + 'operation and parseable output mode is not available', 'should throw no support msg' ) }) t.test('no bearer tokens returned by registry', async 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 = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, }) const profile = new Profile(npm) await t.rejects( profile.exec(['enable-2fa', 'auth-only']), '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', async 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 = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, }) const profile = new Profile(npm) await t.rejects( profile.exec(['enable-2fa', 'auth-only']), '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('no auth found', async t => { npm.config.getCredentialsByURI = () => ({}) const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, }) const profile = new Profile(npm) await t.rejects( profile.exec(['enable-2fa', 'auth-only']), 'You need to be logged in to registry ' + 'https://registry.npmjs.org/ in order to enable 2fa' ) }) t.test('from basic auth, asks for otp', async 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') } } npm.config.setCredentialsByURI = (registry, { token }) => { t.equal(registry, flatOptions.registry, 'should set expected registry') t.equal(token, 'token', 'should set expected token') } npm.config.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: ', 'should ask for otp confirmation' ) return '123456' }, } const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../../lib/utils/read-user-info.js': readUserInfo, }) const profile = new Profile(npm) await profile.exec(['enable-2fa', 'auth-only']) 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', async t => { t.plan(4) 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 node/no-callback-literal */ generate: (url, cb) => cb('qrcode'), } const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, 'qrcode-terminal': qrcode, '../../../lib/utils/read-user-info.js': readUserInfo, }) const profile = new Profile(npm) await profile.exec(['enable-2fa', 'auth-only']) t.matchSnapshot(result, 'should output 2fa enablement success msgs') }) t.test('from token and set otp, retrieves invalid otp', async t => { 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 = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../../lib/utils/read-user-info.js': readUserInfo, }) const profile = new Profile(npm) await t.rejects( profile.exec(['enable-2fa', 'auth-only']), /Unknown error enabling two-factor authentication./, 'should throw invalid 2fa auth url error' ) }) t.test('from token auth provides --otp config arg', async t => { flatOptions.otp = '123456' 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 = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../../lib/utils/read-user-info.js': readUserInfo, }) const profile = new Profile(npm) await profile.exec(['enable-2fa', 'auth-and-writes']) t.equal( result, 'Two factor authentication mode changed to: auth-and-writes', 'should output success msg' ) }) t.test('missing tfa from user profile', async 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 = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../../lib/utils/read-user-info.js': readUserInfo, }) const profile = new Profile(npm) await profile.exec(['enable-2fa', 'auth-only']) t.equal( result, 'Two factor authentication mode changed to: auth-only', 'should output success msg' ) }) t.test('defaults to auth-and-writes permission if no mode specified', async 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 = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../../lib/utils/read-user-info.js': readUserInfo, }) const profile = new Profile(npm) await profile.exec(['enable-2fa']) t.equal( result, 'Two factor authentication mode changed to: auth-and-writes', 'should enable 2fa with auth-and-writes permission' ) }) t.end() }) t.test('disable-2fa', t => { t.test('no tfa enabled', async t => { const npmProfile = { async get () { return { ...userProfile, tfa: null, } }, } const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, }) const profile = new Profile(npm) await profile.exec(['disable-2fa']) t.equal(result, 'Two factor authentication not enabled.', 'should output already disalbed msg') }) t.test('requests otp', t => { const npmProfile = t => ({ async get () { return userProfile }, async set (newProfile, conf) { t.same( 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: ', 'should ask for otp confirmation' ) return '1234' }, }) t.test('default output', async t => { const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile(t), '../../../lib/utils/read-user-info.js': readUserInfo(t), }) const profile = new Profile(npm) await profile.exec(['disable-2fa']) t.equal(result, 'Two factor authentication disabled.', 'should output already disabled msg') }) t.test('--json', async t => { config.json = true const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile(t), '../../../lib/utils/read-user-info.js': readUserInfo(t), }) const profile = new Profile(npm) await profile.exec(['disable-2fa']) t.same(JSON.parse(result), { tfa: false }, 'should output json already disabled msg') }) t.test('--parseable', async t => { config.parseable = true const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile(t), '../../../lib/utils/read-user-info.js': readUserInfo(t), }) const profile = new Profile(npm) await profile.exec(['disable-2fa']) t.equal(result, 'tfa\tfalse', 'should output parseable already disabled msg') }) t.end() }) t.test('--otp config already set', async t => { t.plan(3) flatOptions.otp = '123456' const npmProfile = { async get () { return userProfile }, async set (newProfile, conf) { t.same( 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 = t.mock('../../../lib/commands/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../../lib/utils/read-user-info.js': readUserInfo, }) const profile = new Profile(npm) await profile.exec(['disable-2fa']) t.equal(result, 'Two factor authentication disabled.', 'should output already disalbed msg') }) t.end() }) t.test('unknown subcommand', async t => { await t.rejects( profile.exec(['asfd']), /Unknown profile command: asfd/, 'should throw unknown cmd error' ) }) t.test('completion', t => { const testComp = async ({ t, argv, expect, title }) => { t.resolveMatch(profile.completion({ conf: { argv: { remain: argv } } }), expect, title) } t.test('npm profile autocomplete', async t => { await 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', async t => { await 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', async t => { const noAutocompleteCmds = ['disable-2fa', 'disable-tfa', 'get', 'set'] for (const subcmd of noAutocompleteCmds) { await testComp({ t, argv: ['npm', 'profile', subcmd], expect: [], title: `${subcmd} should have no autocomplete`, }) } t.end() }) t.test('npm profile unknown subcommand autocomplete', async t => { t.rejects( profile.completion({ conf: { argv: { remain: ['npm', 'profile', 'asdf'] } } }), { message: 'asdf not recognized' }, 'should throw unknown cmd error' ) t.end() }) t.end() })