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
diff options
context:
space:
mode:
-rw-r--r--lib/profile.js516
-rw-r--r--tap-snapshots/test-lib-profile.js-TAP.test.js90
-rw-r--r--test/lib/profile.js1465
3 files changed, 1836 insertions, 235 deletions
diff --git a/lib/profile.js b/lib/profile.js
index a29837c75..24f026ce8 100644
--- a/lib/profile.js
+++ b/lib/profile.js
@@ -1,35 +1,36 @@
-const ansistyles = require('ansistyles')
const inspect = require('util').inspect
+const { URL } = require('url')
+const ansistyles = require('ansistyles')
const log = require('npmlog')
+const npmProfile = require('npm-profile')
+const qrcodeTerminal = require('qrcode-terminal')
+const Table = require('cli-table3')
+
const npm = require('./npm.js')
const otplease = require('./utils/otplease.js')
const output = require('./utils/output.js')
-const profile = require('npm-profile')
const pulseTillDone = require('./utils/pulse-till-done.js')
-const qrcodeTerminal = require('qrcode-terminal')
const readUserInfo = require('./utils/read-user-info.js')
-const Table = require('cli-table3')
-const { URL } = require('url')
+const usageUtil = require('./utils/usage.js')
-module.exports = profileCmd
-
-profileCmd.usage =
- 'npm profile enable-2fa [auth-only|auth-and-writes]\n' +
- 'npm profile disable-2fa\n' +
- 'npm profile get [<key>]\n' +
+const usage = usageUtil(
+ 'npm profile enable-2fa [auth-only|auth-and-writes]\n',
+ 'npm profile disable-2fa\n',
+ 'npm profile get [<key>]\n',
'npm profile set <key> <value>'
+)
-profileCmd.subcommands = ['enable-2fa', 'disable-2fa', 'get', 'set']
-
-profileCmd.completion = function (opts, cb) {
+const completion = (opts, cb) => {
var argv = opts.conf.argv.remain
+ const subcommands = ['enable-2fa', 'disable-2fa', 'get', 'set']
+
+ if (!argv[2])
+ return cb(null, subcommands)
+
switch (argv[2]) {
case 'enable-2fa':
case 'enable-tfa':
- if (argv.length === 3)
- return cb(null, ['auth-and-writes', 'auth-only'])
- else
- return cb(null, [])
+ return cb(null, ['auth-and-writes', 'auth-only'])
case 'disable-2fa':
case 'disable-tfa':
@@ -41,35 +42,33 @@ profileCmd.completion = function (opts, cb) {
}
}
-function withCb (prom, cb) {
- prom.then((value) => cb(null, value), cb)
-}
+const cmd = (args, cb) => profile(args).then(() => cb()).catch(cb)
-function profileCmd (args, cb) {
+const profile = async (args) => {
if (args.length === 0)
- return cb(new Error(profileCmd.usage))
+ throw new Error(usage)
+
log.gauge.show('profile')
- switch (args[0]) {
+
+ const [subcmd, ...opts] = args
+
+ switch (subcmd) {
case 'enable-2fa':
case 'enable-tfa':
case 'enable2fa':
case 'enabletfa':
- withCb(enable2fa(args.slice(1)), cb)
- break
+ return enable2fa(opts)
case 'disable-2fa':
case 'disable-tfa':
case 'disable2fa':
case 'disabletfa':
- withCb(disable2fa(), cb)
- break
+ return disable2fa()
case 'get':
- withCb(get(args.slice(1)), cb)
- break
+ return get(opts)
case 'set':
- withCb(set(args.slice(1)), cb)
- break
+ return set(opts)
default:
- cb(new Error('Unknown profile command: ' + args[0]))
+ throw new Error('Unknown profile command: ' + subcmd)
}
}
@@ -86,53 +85,62 @@ const knownProfileKeys = [
'updated',
]
-function get (args) {
+const get = async args => {
const tfa = 'two-factor auth'
- const conf = npm.flatOptions
- return pulseTillDone.withPromise(profile.get(conf)).then((info) => {
- if (!info.cidr_whitelist)
- delete info.cidr_whitelist
- if (conf.json) {
- output(JSON.stringify(info, null, 2))
- return
- }
- const cleaned = {}
- knownProfileKeys.forEach((k) => {
- cleaned[k] = info[k] || ''
- })
- Object.keys(info).filter((k) => !(k in cleaned)).forEach((k) => {
- cleaned[k] = info[k] || ''
- })
- delete cleaned.tfa
- delete cleaned.email_verified
- cleaned.email += info.email_verified ? ' (verified)' : '(unverified)'
- if (info.tfa && !info.tfa.pending)
- cleaned[tfa] = info.tfa.mode
- else
- cleaned[tfa] = 'disabled'
-
- if (args.length) {
- const values = args // comma or space separated ↓
- .join(',').split(/,/).map((arg) => arg.trim()).filter((arg) => arg !== '')
- .map((arg) => cleaned[arg])
- .join('\t')
- output(values)
- } else {
- if (conf.parseable) {
- Object.keys(info).forEach((key) => {
- if (key === 'tfa')
- output(`${key}\t${cleaned[tfa]}`)
- else
- output(`${key}\t${info[key]}`)
- })
- } else {
- const table = new Table()
- for (const k of Object.keys(cleaned))
- table.push({ [ansistyles.bright(k)]: cleaned[k] })
- output(table.toString())
+ const conf = { ...npm.flatOptions }
+
+ const info = await pulseTillDone.withPromise(npmProfile.get(conf))
+
+ if (!info.cidr_whitelist)
+ delete info.cidr_whitelist
+
+ if (conf.json) {
+ output(JSON.stringify(info, null, 2))
+ return
+ }
+
+ // clean up and format key/values for output
+ const cleaned = {}
+ for (const key of knownProfileKeys)
+ cleaned[key] = info[key] || ''
+
+ const unknownProfileKeys = Object.keys(info).filter((k) => !(k in cleaned))
+ for (const key of unknownProfileKeys)
+ cleaned[key] = info[key] || ''
+
+ delete cleaned.tfa
+ delete cleaned.email_verified
+ cleaned.email += info.email_verified ? ' (verified)' : '(unverified)'
+
+ if (info.tfa && !info.tfa.pending)
+ cleaned[tfa] = info.tfa.mode
+ else
+ cleaned[tfa] = 'disabled'
+
+ if (args.length) {
+ const values = args // comma or space separated
+ .join(',')
+ .split(/,/)
+ .filter((arg) => arg.trim() !== '')
+ .map((arg) => cleaned[arg])
+ .join('\t')
+ output(values)
+ } else {
+ if (conf.parseable) {
+ for (const key of Object.keys(info)) {
+ if (key === 'tfa')
+ output(`${key}\t${cleaned[tfa]}`)
+ else
+ output(`${key}\t${info[key]}`)
}
+ } else {
+ const table = new Table()
+ for (const key of Object.keys(cleaned))
+ table.push({ [ansistyles.bright(key)]: cleaned[key] })
+
+ output(table.toString())
}
- })
+ }
}
const writableProfileKeys = [
@@ -145,83 +153,87 @@ const writableProfileKeys = [
'github',
]
-function set (args) {
- const conf = npm.flatOptions
+const set = async (args) => {
+ const conf = { ...npm.flatOptions }
const prop = (args[0] || '').toLowerCase().trim()
+
let value = args.length > 1 ? args.slice(1).join(' ') : null
+
+ const readPasswords = async () => {
+ const newpassword = await readUserInfo.password('New password: ')
+ const confirmedpassword = await readUserInfo.password(' Again: ')
+
+ if (newpassword !== confirmedpassword) {
+ log.warn('profile', 'Passwords do not match, please try again.')
+ return readPasswords()
+ }
+
+ return newpassword
+ }
+
if (prop !== 'password' && value === null)
- return Promise.reject(Error('npm profile set <prop> <value>'))
+ throw new Error('npm profile set <prop> <value>')
if (prop === 'password' && value !== null) {
- return Promise.reject(Error(
+ throw new Error(
'npm profile set password\n' +
- 'Do not include your current or new passwords on the command line.'))
+ 'Do not include your current or new passwords on the command line.')
}
- if (writableProfileKeys.indexOf(prop) === -1)
- return Promise.reject(Error(`"${prop}" is not a property we can set. Valid properties are: ` + writableProfileKeys.join(', ')))
-
- return Promise.resolve().then(() => {
- if (prop === 'password') {
- return readUserInfo.password('Current password: ').then((current) => {
- return readPasswords().then((newpassword) => {
- value = { old: current, new: newpassword }
- })
- })
- } else if (prop === 'email') {
- return readUserInfo.password('Password: ').then((current) => {
- return { password: current, email: value }
- })
- }
- function readPasswords () {
- return readUserInfo.password('New password: ').then((password1) => {
- return readUserInfo.password(' Again: ').then((password2) => {
- if (password1 !== password2) {
- log.warn('profile', 'Passwords do not match, please try again.')
- return readPasswords()
- }
- return password1
- })
- })
- }
- }).then(() => {
- // FIXME: Work around to not clear everything other than what we're setting
- return pulseTillDone.withPromise(profile.get(conf).then((user) => {
- const newUser = {}
- writableProfileKeys.forEach((k) => {
- newUser[k] = user[k]
- })
- newUser[prop] = value
- return otplease(conf, conf => profile.set(newUser, conf))
- .then((result) => {
- if (conf.json)
- output(JSON.stringify({ [prop]: result[prop] }, null, 2))
- else if (conf.parseable)
- output(prop + '\t' + result[prop])
- else if (result[prop] != null)
- output('Set', prop, 'to', result[prop])
- else
- output('Set', prop)
- })
- }))
- })
+
+ if (writableProfileKeys.indexOf(prop) === -1) {
+ throw new Error(`"${prop}" is not a property we can set. ` +
+ `Valid properties are: ` + writableProfileKeys.join(', '))
+ }
+
+ if (prop === 'password') {
+ const current = await readUserInfo.password('Current password: ')
+ const newpassword = await readPasswords()
+
+ value = { old: current, new: newpassword }
+ }
+
+ // FIXME: Work around to not clear everything other than what we're setting
+ const user = await pulseTillDone.withPromise(npmProfile.get(conf))
+ const newUser = {}
+
+ for (const key of writableProfileKeys)
+ newUser[key] = user[key]
+
+ newUser[prop] = value
+
+ const result = await otplease(conf, conf => npmProfile.set(newUser, conf))
+
+ if (conf.json)
+ output(JSON.stringify({ [prop]: result[prop] }, null, 2))
+ else if (conf.parseable)
+ output(prop + '\t' + result[prop])
+ else if (result[prop] != null)
+ output('Set', prop, 'to', result[prop])
+ else
+ output('Set', prop)
}
-function enable2fa (args) {
+const enable2fa = async (args) => {
if (args.length > 1)
- return Promise.reject(new Error('npm profile enable-2fa [auth-and-writes|auth-only]'))
+ throw new Error('npm profile enable-2fa [auth-and-writes|auth-only]')
const mode = args[0] || 'auth-and-writes'
if (mode !== 'auth-only' && mode !== 'auth-and-writes') {
- return Promise.reject(new Error(`Invalid two-factor authentication mode "${mode}".\n` +
+ throw new Error(
+ `Invalid two-factor authentication mode "${mode}".\n` +
'Valid modes are:\n' +
' auth-only - Require two-factor authentication only when logging in\n' +
- ' auth-and-writes - Require two-factor authentication when logging in AND when publishing'))
+ ' auth-and-writes - Require two-factor authentication when logging in ' +
+ 'AND when publishing'
+ )
}
- const conf = npm.flatOptions
+
+ const conf = { ...npm.flatOptions }
if (conf.json || conf.parseable) {
- return Promise.reject(new Error(
+ throw new Error(
'Enabling two-factor authentication is an interactive operation and ' +
- (conf.json ? 'JSON' : 'parseable') + ' output mode is not available'))
+ (conf.json ? 'JSON' : 'parseable') + ' output mode is not available'
+ )
}
const info = {
@@ -230,119 +242,153 @@ function enable2fa (args) {
},
}
- return Promise.resolve().then(() => {
- // if they're using legacy auth currently then we have to update them to a
- // bearer token before continuing.
- const auth = getAuth(conf)
- if (auth.basic) {
- log.info('profile', 'Updating authentication to bearer token')
- return profile.createToken(
- auth.basic.password, false, [], conf
- ).then((result) => {
- if (!result.token) {
- throw new Error(`Your registry ${conf.registry} does not seem to ` +
- 'support bearer tokens. Bearer tokens are required for ' +
- 'two-factor authentication')
- }
- npm.config.setCredentialsByURI(conf.registry, { token: result.token })
- return npm.config.save('user')
- })
+ // if they're using legacy auth currently then we have to
+ // update them to a bearer token before continuing.
+ const auth = getAuth(conf)
+
+ if (!auth.basic && !auth.token) {
+ throw new Error(
+ 'You need to be logged in to registry ' +
+ `${conf.registry} in order to enable 2fa`
+ )
+ }
+
+ if (auth.basic) {
+ log.info('profile', 'Updating authentication to bearer token')
+ const result = await npmProfile.createToken(
+ auth.basic.password, false, [], conf
+ )
+
+ if (!result.token) {
+ throw new Error(
+ `Your registry ${conf.registry} does not seem to ` +
+ 'support bearer tokens. Bearer tokens are required for ' +
+ 'two-factor authentication'
+ )
}
- }).then(() => {
- log.notice('profile', 'Enabling two factor authentication for ' + mode)
- return readUserInfo.password()
- }).then((password) => {
- info.tfa.password = password
- log.info('profile', 'Determine if tfa is pending')
- return pulseTillDone.withPromise(profile.get(conf)).then((info) => {
- if (!info.tfa)
- return
- if (info.tfa.pending) {
- log.info('profile', 'Resetting two-factor authentication')
- return pulseTillDone.withPromise(profile.set({ tfa: { password, mode: 'disable' } }, conf))
- } else {
- if (conf.auth.otp)
- return
- return readUserInfo.otp('Enter one-time password from your authenticator app: ').then((otp) => {
- conf.auth.otp = otp
- })
- }
- })
- }).then(() => {
- log.info('profile', 'Setting two-factor authentication to ' + mode)
- return pulseTillDone.withPromise(profile.set(info, conf))
- }).then((challenge) => {
- if (challenge.tfa === null) {
- output('Two factor authentication mode changed to: ' + mode)
- return
+
+ npm.config.setCredentialsByURI(conf.registry, { token: result.token })
+ await npm.config.save('user')
+ }
+
+ log.notice('profile', 'Enabling two factor authentication for ' + mode)
+ const password = await readUserInfo.password()
+ info.tfa.password = password
+
+ log.info('profile', 'Determine if tfa is pending')
+ const userInfo = await pulseTillDone.withPromise(npmProfile.get(conf))
+
+ if (userInfo && userInfo.tfa && userInfo.tfa.pending) {
+ log.info('profile', 'Resetting two-factor authentication')
+ await pulseTillDone.withPromise(
+ npmProfile.set({ tfa: { password, mode: 'disable' } }, conf)
+ )
+ } else if (userInfo && userInfo.tfa) {
+ if (conf.otp)
+ conf.otp = conf.otp
+ else {
+ const otp = await readUserInfo.otp(
+ 'Enter one-time password from your authenticator app: '
+ )
+ conf.otp = otp
}
- if (typeof challenge.tfa !== 'string' || !/^otpauth:[/][/]/.test(challenge.tfa))
- throw new Error('Unknown error enabling two-factor authentication. Expected otpauth URL, got: ' + inspect(challenge.tfa))
-
- const otpauth = new URL(challenge.tfa)
- const secret = otpauth.searchParams.get('secret')
- return qrcode(challenge.tfa).then((code) => {
- output('Scan into your authenticator app:\n' + code + '\n Or enter code:', secret)
- }).then((code) => {
- return readUserInfo.otp('And an OTP code from your authenticator: ')
- }).then((otp1) => {
- log.info('profile', 'Finalizing two-factor authentication')
- return profile.set({ tfa: [otp1] }, conf)
- }).then((result) => {
- output('2FA successfully enabled. Below are your recovery codes, please print these out.')
- output('You will need these to recover access to your account if you lose your authentication device.')
- result.tfa.forEach((c) => output('\t' + c))
- })
- })
+ }
+
+ log.info('profile', 'Setting two-factor authentication to ' + mode)
+ const challenge = await pulseTillDone.withPromise(npmProfile.set(info, conf))
+
+ if (challenge.tfa === null) {
+ output('Two factor authentication mode changed to: ' + mode)
+ return
+ }
+
+ const badResponse = typeof challenge.tfa !== 'string'
+ || !/^otpauth:[/][/]/.test(challenge.tfa)
+ if (badResponse) {
+ throw new Error(
+ 'Unknown error enabling two-factor authentication. Expected otpauth URL' +
+ ', got: ' + inspect(challenge.tfa)
+ )
+ }
+
+ const otpauth = new URL(challenge.tfa)
+ const secret = otpauth.searchParams.get('secret')
+ const code = await qrcode(challenge.tfa)
+
+ output(
+ 'Scan into your authenticator app:\n' + code + '\n Or enter code:', secret
+ )
+
+ const interactiveOTP =
+ await readUserInfo.otp('And an OTP code from your authenticator: ')
+
+ log.info('profile', 'Finalizing two-factor authentication')
+
+ const result = await npmProfile.set({ tfa: [interactiveOTP] }, conf)
+
+ output(
+ '2FA successfully enabled. Below are your recovery codes, ' +
+ 'please print these out.'
+ )
+ output(
+ 'You will need these to recover access to your account ' +
+ 'if you lose your authentication device.'
+ )
+
+ for (const tfaCode of result.tfa)
+ output('\t' + tfaCode)
}
-function getAuth (conf) {
+const getAuth = conf => {
const creds = npm.config.getCredentialsByURI(conf.registry)
- let auth
+ const auth = {}
+
if (creds.token)
- auth = { token: creds.token }
+ auth.token = creds.token
else if (creds.username)
- auth = { basic: { username: creds.username, password: creds.password } }
+ auth.basic = { username: creds.username, password: creds.password }
else if (creds.auth) {
const basic = Buffer.from(creds.auth, 'base64').toString().split(':', 2)
- auth = { basic: { username: basic[0], password: basic[1] } }
- } else
- auth = {}
+ auth.basic = { username: basic[0], password: basic[1] }
+ }
if (conf.otp)
auth.otp = conf.otp
+
return auth
}
-function disable2fa (args) {
- let conf = npm.flatOptions
- return pulseTillDone.withPromise(profile.get(conf)).then((info) => {
- if (!info.tfa || info.tfa.pending) {
- output('Two factor authentication not enabled.')
- return
- }
- return readUserInfo.password().then((password) => {
- return Promise.resolve().then(() => {
- if (conf.otp)
- return
- return readUserInfo.otp('Enter one-time password from your authenticator: ').then((otp) => {
- conf = { ...conf, otp }
- })
- }).then(() => {
- log.info('profile', 'disabling tfa')
- return pulseTillDone.withPromise(profile.set({ tfa: { password: password, mode: 'disable' } }, conf)).then(() => {
- if (conf.json)
- output(JSON.stringify({ tfa: false }, null, 2))
- else if (conf.parseable)
- output('tfa\tfalse')
- else
- output('Two factor authentication disabled.')
- })
- })
- })
- })
-}
+const disable2fa = async args => {
+ const conf = { ...npm.flatOptions }
+ const info = await pulseTillDone.withPromise(npmProfile.get(conf))
+
+ if (!info.tfa || info.tfa.pending) {
+ output('Two factor authentication not enabled.')
+ return
+ }
+
+ const password = await readUserInfo.password()
-function qrcode (url) {
- return new Promise((resolve) => qrcodeTerminal.generate(url, resolve))
+ if (!conf.otp) {
+ const msg = 'Enter one-time password from your authenticator app: '
+ conf.otp = await readUserInfo.otp(msg)
+ }
+
+ log.info('profile', 'disabling tfa')
+
+ await pulseTillDone.withPromise(npmProfile.set({
+ tfa: { password: password, mode: 'disable' },
+ }, conf))
+
+ if (conf.json)
+ output(JSON.stringify({ tfa: false }, null, 2))
+ else if (conf.parseable)
+ output('tfa\tfalse')
+ else
+ output('Two factor authentication disabled.')
}
+
+const qrcode = url =>
+ new Promise((resolve) => qrcodeTerminal.generate(url, resolve))
+
+module.exports = Object.assign(cmd, { usage, completion })
diff --git a/tap-snapshots/test-lib-profile.js-TAP.test.js b/tap-snapshots/test-lib-profile.js-TAP.test.js
new file mode 100644
index 000000000..bb838ad92
--- /dev/null
+++ b/tap-snapshots/test-lib-profile.js-TAP.test.js
@@ -0,0 +1,90 @@
+/* IMPORTANT
+ * This snapshot file is auto-generated, but designed for humans.
+ * It should be checked into source control and tracked carefully.
+ * Re-generate by setting TAP_SNAPSHOT=1 and running tests.
+ * Make sure to inspect the output below. Do not ignore changes!
+ */
+'use strict'
+exports[`test/lib/profile.js TAP enable-2fa from token and set otp, retries on pending and verifies with qrcode > should output 2fa enablement success msgs 1`] = `
+Scan into your authenticator app:
+qrcode
+ Or enter code:
+12342FA successfully enabled. Below are your recovery codes, please print these out.You will need these to recover access to your account if you lose your authentication device. 123456 789101
+`
+
+exports[`test/lib/profile.js TAP profile get <key> --parseable > should output parseable result value 1`] = `
+foo
+`
+
+exports[`test/lib/profile.js TAP profile get multiple args --parseable > should output parseable profile value results 1`] = `
+foo foo@github.com (verified) https://github.com/npm
+`
+
+exports[`test/lib/profile.js TAP profile get multiple args comma separated > should output all keys 1`] = `
+foo foo@github.com (verified) https://github.com/npm
+`
+
+exports[`test/lib/profile.js TAP profile get multiple args default output > should output all keys 1`] = `
+foo foo@github.com (verified) https://github.com/npm
+`
+
+exports[`test/lib/profile.js TAP profile get no args --parseable > should output all profile info as parseable result 1`] = `
+tfa auth-and-writesname fooemail foo@github.comemail_verified truecreated 2015-02-26T01:26:37.384Zupdated 2020-08-12T16:19:35.326Zfullname Foo Barhomepage https://github.comfreenode foobartwitter https://twitter.com/npmjsgithub https://github.com/npm
+`
+
+exports[`test/lib/profile.js TAP profile get no args default output > should output table with contents 1`] = `
+name: foo
+email: foo@github.com (verified)
+two-factor auth: auth-and-writes
+fullname: Foo Bar
+homepage: https://github.com
+freenode: foobar
+twitter: https://twitter.com/npmjs
+github: https://github.com/npm
+created: 2015-02-26T01:26:37.384Z
+updated: 2020-08-12T16:19:35.326Z
+`
+
+exports[`test/lib/profile.js TAP profile get no args no tfa enabled > should output expected profile values 1`] = `
+name: foo
+email: foo@github.com (verified)
+two-factor auth: disabled
+fullname: Foo Bar
+homepage: https://github.com
+freenode: foobar
+twitter: https://twitter.com/npmjs
+github: https://github.com/npm
+created: 2015-02-26T01:26:37.384Z
+updated: 2020-08-12T16:19:35.326Z
+`
+
+exports[`test/lib/profile.js TAP profile get no args profile has cidr_whitelist item > should output table with contents 1`] = `
+name: foo
+email: foo@github.com (verified)
+two-factor auth: auth-and-writes
+fullname: Foo Bar
+homepage: https://github.com
+freenode: foobar
+twitter: https://twitter.com/npmjs
+github: https://github.com/npm
+created: 2015-02-26T01:26:37.384Z
+updated: 2020-08-12T16:19:35.326Z
+cidr_whitelist: 192.168.1.1
+`
+
+exports[`test/lib/profile.js TAP profile get no args unverified email > should output table with contents 1`] = `
+name: foo
+email: foo@github.com(unverified)
+two-factor auth: auth-and-writes
+fullname: Foo Bar
+homepage: https://github.com
+freenode: foobar
+twitter: https://twitter.com/npmjs
+github: https://github.com/npm
+created: 2015-02-26T01:26:37.384Z
+updated: 2020-08-12T16:19:35.326Z
+`
+
+exports[`test/lib/profile.js TAP profile set <key> <value> writable key --parseable > should output parseable set key success msg 1`] = `
+fullname Lorem Ipsum
+`
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()
+})