Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorRuy Adorno <ruyadorno@hotmail.com>2020-12-11 22:17:16 +0300
committerisaacs <i@izs.me>2020-12-18 23:02:27 +0300
commitcba3341dae4c92541049dc976e82e2ba19566e95 (patch)
tree58cd8ea6579879b285d92a7b5a211e3f7a9a799b /test
parent44d4331058c53909ada62470b23b2185102b2128 (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.js1465
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()
+})