From 314311c61b8f341715c168199d52976ee3237077 Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 21 Sep 2022 13:07:30 -0700 Subject: feat: separate login/adduser, remove auth types (#5550) The difference between `adduser` and `login` depends on the `auth-type`. - `web`: the POST to `/-/v1/login` contains a `{ create: true }` value in its payload for `adduser` - `legacy` the `PUT` request to `/-/user/org.couchdb.user:${username}` contains an `email` value in its payload for `adduser`. BREAKING CHANGE: `login`, `adduser`, and `auth-type` changes - This removes all `auth-type` configs except `web` and `legacy`. - `login` and `adduser` are now separate commands that send different data to the registry. - `auth-type` config values `web` and `legacy` only try their respective methods, npm no longer tries them all and waits to see which one doesn't fail. --- test/fixtures/mock-registry.js | 57 +++++- test/lib/auth/legacy.js | 429 ----------------------------------------- test/lib/auth/oauth.js | 28 --- test/lib/auth/saml.js | 28 --- test/lib/auth/sso.js | 236 ----------------------- test/lib/commands/adduser.js | 315 +++++++++++++++--------------- test/lib/commands/login.js | 151 +++++++++++++++ 7 files changed, 357 insertions(+), 887 deletions(-) delete mode 100644 test/lib/auth/legacy.js delete mode 100644 test/lib/auth/oauth.js delete mode 100644 test/lib/auth/saml.js delete mode 100644 test/lib/auth/sso.js create mode 100644 test/lib/commands/login.js (limited to 'test') diff --git a/test/fixtures/mock-registry.js b/test/fixtures/mock-registry.js index 65d475962..d978929b6 100644 --- a/test/fixtures/mock-registry.js +++ b/test/fixtures/mock-registry.js @@ -111,17 +111,12 @@ class MockRegistry { } } - couchlogin ({ username, password, email, otp, token = 'npm_default-test-token' }) { - this.nock = this.nock - .post('/-/v1/login').reply(401, { error: 'You must be logged in to publish packages.' }) - if (otp) { - // TODO otp failure results in a 401 with - // {"ok":false,"error":"failed to authenticate: Could not authenticate ${username}: bad otp"} - } + couchadduser ({ username, email, password, token = 'npm_default-test-token' }) { this.nock = this.nock.put(`/-/user/org.couchdb.user:${username}`, body => { this.#tap.match(body, { _id: `org.couchdb.user:${username}`, name: username, + email, // Sole difference from couchlogin password, type: 'user', roles: [], @@ -131,13 +126,59 @@ class MockRegistry { } return true }).reply(201, { - ok: true, id: 'org.couchdb.user:undefined', rev: '_we_dont_use_revs_any_more', token, }) } + couchlogin ({ username, password, token = 'npm_default-test-token' }) { + this.nock = this.nock.put(`/-/user/org.couchdb.user:${username}`, body => { + this.#tap.match(body, { + _id: `org.couchdb.user:${username}`, + name: username, + password, + type: 'user', + roles: [], + }) + if (!body.date) { + return false + } + return true + }).reply(201, { + id: 'org.couchdb.user:undefined', + rev: '_we_dont_use_revs_any_more', + token, + }) + } + + webadduser ({ username, password, token = 'npm_default-test-token' }) { + const doneUrl = new URL('/npm-cli-test/done', this.#registry).href + const loginUrl = new URL('/npm-cli-test/login', this.#registry).href + this.nock = this.nock + .post('/-/v1/login', body => { + this.#tap.ok(body.create) // Sole difference from weblogin + this.#tap.ok(body.hostname) + return true + }) + .reply(200, { doneUrl, loginUrl }) + .get('/npm-cli-test/done') + .reply(200, { token }) + } + + weblogin ({ token = 'npm_default-test-token' }) { + const doneUrl = new URL('/npm-cli-test/done', this.#registry).href + const loginUrl = new URL('/npm-cli-test/login', this.#registry).href + this.nock = this.nock + .post('/-/v1/login', body => { + this.#tap.ok(body.hostname) + return true + }) + .reply(200, { doneUrl, loginUrl }) + .get('/npm-cli-test/done') + .reply(200, { token }) + } + // team can be a team or a username getPackages ({ team, packages = {}, times = 1 }) { if (team.startsWith('@')) { diff --git a/test/lib/auth/legacy.js b/test/lib/auth/legacy.js deleted file mode 100644 index 39d977d43..000000000 --- a/test/lib/auth/legacy.js +++ /dev/null @@ -1,429 +0,0 @@ -const t = require('tap') - -let log = '' - -const token = '24528a24f240' -const profile = {} -const read = {} -const legacy = t.mock('../../../lib/auth/legacy.js', { - 'proc-log': { - info: (...msgs) => { - log += msgs.join(' ') - }, - }, - 'npm-profile': profile, - '../../../lib/utils/open-url-prompt.js': (_npm, url) => { - if (!url) { - throw Object.assign(new Error('failed open url'), { code: 'ERROR' }) - } - }, - '../../../lib/utils/read-user-info.js': read, -}) - -const npm = { - config: { - get: () => null, - }, -} - -t.test('login using username/password with token result', async (t) => { - profile.login = () => { - return { token } - } - - const { - message, - newCreds, - } = await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as u on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'login Authorized user u', - 'should have correct message result' - ) - - t.same( - newCreds, - { token }, - 'should return expected obj from profile.login' - ) - - log = '' - delete profile.login -}) - -t.test('login using username/password with user info result', async (t) => { - profile.login = () => { - return null - } - - const { - message, - newCreds, - } = await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as u on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.same( - newCreds, - { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - 'should return used credentials' - ) - - log = '' - delete profile.login -}) - -t.test('login otp requested', async (t) => { - t.plan(5) - - profile.login = () => Promise.reject(Object.assign( - new Error('needs otp'), - { code: 'EOTP' } - )) - profile.loginCouch = (username, password, { otp }) => { - t.equal(username, 'u', 'should use provided username to loginCouch') - t.equal(password, 'p', 'should use provided password to loginCouch') - t.equal(otp, '1234', 'should use provided otp code to loginCouch') - - return { token } - } - read.otp = () => Promise.resolve('1234') - - const { - message, - newCreds, - } = await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as u on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.same( - newCreds, - { token }, - 'should return token from loginCouch result' - ) - - log = '' - delete profile.login - delete profile.loginCouch - delete read.otp -}) - -t.test('login missing basic credential info', async (t) => { - profile.login = () => Promise.reject(Object.assign( - new Error('missing info'), - { code: 'ERROR' } - )) - - await t.rejects( - legacy(npm, { - creds: { - username: 'u', - password: 'p', - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { code: 'ERROR' }, - 'should throw server response error' - ) - - log = '' - delete profile.login -}) - -t.test('create new user when user not found', async (t) => { - t.plan(6) - - profile.login = () => Promise.reject(Object.assign( - new Error('User does not exist'), - { code: 'ERROR' } - )) - profile.adduserCouch = (username, email, password) => { - t.equal(username, 'u', 'should use provided username to adduserCouch') - t.equal(email, 'u@npmjs.org', 'should use provided email to adduserCouch') - t.equal(password, 'p', 'should use provided password to adduserCouch') - - return { token } - } - - const { - message, - newCreds, - } = await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as u on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'login Authorized user u', - 'should have correct message result' - ) - - t.same( - newCreds, - { token }, - 'should return expected obj from profile.login' - ) - - log = '' - delete profile.adduserCouch - delete profile.login -}) - -t.test('prompts for user info if required', async (t) => { - t.plan(4) - - profile.login = async (opener, prompt, opts) => { - t.equal(opts.creds.alwaysAuth, true, 'should have refs to creds if any') - await opener('https://registry.npmjs.org/-/v1/login') - const creds = await prompt(opts.creds) - return creds - } - read.username = () => Promise.resolve('foo') - read.password = () => Promise.resolve('pass') - read.email = () => Promise.resolve('foo@npmjs.org') - - const { - message, - newCreds, - } = await legacy(npm, { - creds: { - alwaysAuth: true, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as foo on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'login Authorized user foo', - 'should have correct message result' - ) - - t.same( - newCreds, - { - username: 'foo', - password: 'pass', - email: 'foo@npmjs.org', - alwaysAuth: true, - }, - 'should return result from profile.login containing prompt info' - ) - - log = '' - delete profile.login - delete read.username - delete read.password - delete read.email -}) - -t.test('request otp when creating new user', async (t) => { - t.plan(3) - - profile.login = () => Promise.reject(Object.assign( - new Error('User does not exist'), - { code: 'ERROR' } - )) - profile.adduserCouch = () => Promise.reject(Object.assign( - new Error('needs otp'), - { code: 'EOTP' } - )) - profile.loginCouch = (username, password, { otp }) => { - t.equal(username, 'u', 'should use provided username to loginCouch') - t.equal(password, 'p', 'should use provided password to loginCouch') - t.equal(otp, '1234', 'should now use provided otp code to loginCouch') - - return { token } - } - read.otp = () => Promise.resolve('1234') - - await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - log = '' - delete profile.adduserCouch - delete profile.login - delete profile.loginCouch - delete read.otp -}) - -t.test('unknown error during user creation', async (t) => { - profile.login = () => Promise.reject(Object.assign( - new Error('missing info'), - { code: 'ERROR' } - )) - profile.adduserCouch = () => Promise.reject(Object.assign( - new Error('unkown error'), - { code: 'ERROR' } - )) - - await t.rejects( - legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { code: 'ERROR' }, - 'should throw unknown error' - ) - - log = '' - delete profile.adduserCouch - delete profile.login -}) - -t.test('open url error', async (t) => { - profile.login = async (opener, prompt, opts) => { - await opener() - } - - await t.rejects( - legacy(npm, { - creds: { - username: 'u', - password: 'p', - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { message: 'failed open url', code: 'ERROR' }, - 'should throw unknown error' - ) - - log = '' - delete profile.login -}) - -t.test('login no credentials provided', async (t) => { - profile.login = () => ({ token }) - - await legacy(npm, { - creds: { - username: undefined, - password: undefined, - email: undefined, - alwaysAuth: undefined, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - log, - 'login Authorized', - 'should have correct message result' - ) - - log = '' - delete profile.login -}) - -t.test('scoped login', async (t) => { - profile.login = () => ({ token }) - - const { message } = await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://diff-registry.npmjs.org/', - scope: 'myscope', - }) - - t.equal( - message, - 'Logged in as u to scope myscope on https://diff-registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'login Authorized user u', - 'should have correct message result' - ) - - log = '' - delete profile.login -}) diff --git a/test/lib/auth/oauth.js b/test/lib/auth/oauth.js deleted file mode 100644 index 0c317fb9a..000000000 --- a/test/lib/auth/oauth.js +++ /dev/null @@ -1,28 +0,0 @@ -const t = require('tap') - -t.test('oauth login', (t) => { - t.plan(3) - const oauthOpts = { - creds: {}, - registry: 'https://diff-registry.npmjs.org/', - scope: 'myscope', - } - - const npm = { - config: { - set: (key, value) => { - t.equal(key, 'sso-type', 'should define sso-type') - t.equal(value, 'oauth', 'should set sso-type to oauth') - }, - }, - } - const oauth = t.mock('../../../lib/auth/oauth.js', { - '../../../lib/auth/sso.js': (npm, opts) => { - t.equal(opts, oauthOpts, 'should forward opts') - }, - }) - - oauth(npm, oauthOpts) - - t.end() -}) diff --git a/test/lib/auth/saml.js b/test/lib/auth/saml.js deleted file mode 100644 index 1558e0db8..000000000 --- a/test/lib/auth/saml.js +++ /dev/null @@ -1,28 +0,0 @@ -const t = require('tap') - -t.test('saml login', (t) => { - t.plan(3) - const samlOpts = { - creds: {}, - registry: 'https://diff-registry.npmjs.org/', - scope: 'myscope', - } - - const npm = { - config: { - set: (key, value) => { - t.equal(key, 'sso-type', 'should define sso-type') - t.equal(value, 'saml', 'should set sso-type to saml') - }, - }, - } - const saml = t.mock('../../../lib/auth/saml.js', { - '../../../lib/auth/sso.js': (npm, opts) => { - t.equal(opts, samlOpts, 'should forward opts') - }, - }) - - saml(npm, samlOpts) - - t.end() -}) diff --git a/test/lib/auth/sso.js b/test/lib/auth/sso.js deleted file mode 100644 index 8d70077ad..000000000 --- a/test/lib/auth/sso.js +++ /dev/null @@ -1,236 +0,0 @@ -const t = require('tap') - -let log = '' - -const _flatOptions = { - ssoType: 'oauth', -} -const token = '24528a24f240' -const SSO_URL = 'https://registry.npmjs.org/{SSO_URL}' -const profile = {} -const npmFetch = {} -const sso = t.mock('../../../lib/auth/sso.js', { - 'proc-log': { - info: (...msgs) => { - log += msgs.join(' ') + '\n' - }, - }, - 'npm-profile': profile, - 'npm-registry-fetch': npmFetch, - '../../../lib/utils/open-url.js': async (npm, url, msg) => { - if (!url) { - throw Object.assign( - new Error('failed open url'), - { code: 'ERROR' } - ) - } - }, -}) - -const npm = { - flatOptions: _flatOptions, -} - -t.test('empty login', async (t) => { - _flatOptions.ssoType = false - - await t.rejects( - sso(npm, {}), - { message: 'Missing option: sso-type' }, - 'should throw if no sso-type defined in flatOptions' - ) - - _flatOptions.ssoType = 'oauth' - log = '' -}) - -t.test('simple login', async (t) => { - t.plan(6) - - profile.loginCouch = (username, password, opts) => { - t.equal(username, 'npm_oauth_auth_dummy_user', 'should use dummy user') - t.equal(password, 'placeholder', 'should use dummy password') - t.same( - opts, - { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - ssoType: 'oauth', - }, - 'should use dummy password' - ) - - return { token, sso: SSO_URL } - } - npmFetch.json = () => Promise.resolve({ username: 'foo' }) - - const { - message, - newCreds, - } = await sso(npm, { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as foo on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'adduser Polling for validated SSO session\nadduser Authorized user foo\n', - 'should have correct logged info msg' - ) - - t.same( - newCreds, - { token }, - 'should return expected resulting credentials' - ) - - log = '' - delete profile.loginCouch - delete npmFetch.json -}) - -t.test('polling retry', async (t) => { - t.plan(3) - - profile.loginCouch = () => ({ token, sso: SSO_URL }) - npmFetch.json = () => { - // assert expected values during retry - npmFetch.json = (url, { registry, forceAuth: { token: expected } }) => { - t.equal( - url, - '/-/whoami', - 'should reach for expected endpoint' - ) - - t.equal( - registry, - 'https://registry.npmjs.org/', - 'should use expected registry value' - ) - - t.equal( - expected, - token, - 'should use expected token retrieved from initial loginCouch' - ) - - return Promise.resolve({ username: 'foo' }) - } - - // initial fetch returns retry code - return Promise.reject(Object.assign( - new Error('nothing yet'), - { code: 'E401' } - )) - } - - await sso(npm, { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - log = '' - delete profile.loginCouch - delete npmFetch.json -}) - -t.test('polling error', async (t) => { - profile.loginCouch = () => ({ token, sso: SSO_URL }) - npmFetch.json = () => Promise.reject(Object.assign( - new Error('unknown error'), - { code: 'ERROR' } - )) - - await t.rejects( - sso(npm, { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { message: 'unknown error', code: 'ERROR' }, - 'should throw unknown error' - ) - - log = '' - delete profile.loginCouch - delete npmFetch.json -}) - -t.test('no token retrieved from loginCouch', async (t) => { - profile.loginCouch = () => ({}) - - await t.rejects( - sso(npm, { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { message: 'no SSO token returned' }, - 'should throw no SSO token returned error' - ) - - log = '' - delete profile.loginCouch -}) - -t.test('no sso url retrieved from loginCouch', async (t) => { - profile.loginCouch = () => Promise.resolve({ token }) - - await t.rejects( - sso(npm, { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { message: 'no SSO URL returned by services' }, - 'should throw no SSO url returned error' - ) - - log = '' - delete profile.loginCouch -}) - -t.test('scoped login', async (t) => { - profile.loginCouch = () => ({ token, sso: SSO_URL }) - npmFetch.json = () => Promise.resolve({ username: 'foo' }) - - const { - message, - newCreds, - } = await sso(npm, { - creds: {}, - registry: 'https://diff-registry.npmjs.org/', - scope: 'myscope', - }) - - t.equal( - message, - 'Logged in as foo to scope myscope on https://diff-registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'adduser Polling for validated SSO session\nadduser Authorized user foo\n', - 'should have correct logged info msg' - ) - - t.same( - newCreds, - { token }, - 'should return expected resulting credentials' - ) - - log = '' - delete profile.loginCouch - delete npmFetch.json -}) diff --git a/test/lib/commands/adduser.js b/test/lib/commands/adduser.js index 94e58a6d3..4ff65e57f 100644 --- a/test/lib/commands/adduser.js +++ b/test/lib/commands/adduser.js @@ -1,7 +1,7 @@ const t = require('tap') -const path = require('path') const fs = require('fs') -const os = require('os') +const path = require('path') +const ini = require('ini') const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') const mockGlobals = require('../../fixtures/mock-globals.js') @@ -14,170 +14,169 @@ t.test('usage', async t => { t.match(adduser.usage, 'adduser', 'usage has command name in it') }) -t.test('simple login', async t => { - const stdin = new stream.PassThrough() - stdin.write('test-user\n') - stdin.write('test-password\n') - stdin.write('test-email@npmjs.org\n') - mockGlobals(t, { - 'process.stdin': stdin, - 'process.stdout': new stream.PassThrough(), // to quiet readline - }, { replace: true }) - const { npm, home } = await loadMockNpm(t, { - homeDir: { - // These all get cleaned up by config.setCredentialsByURI - '.npmrc': [ - '_token=user', - '_password=user', - 'username=user', - '_auth=user', - '_authtoken=user', - '-authtoken=user', - '_authToken=user', - '//registry.npmjs.org/:_authToken=user', - '//registry.npmjs.org/:always-auth=user', - '//registry.npmjs.org/:email=test-email-old@npmjs.org', - ].join('\n'), - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), +t.test('legacy', async t => { + t.test('simple adduser', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + stdin.write('test-email@npmjs.org\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + homeDir: { + // These all get cleaned up by config.setCredentialsByURI + '.npmrc': [ + '_token=user', + '_password=user', + 'username=user', + '_auth=user', + '_authtoken=user', + '-authtoken=user', + '_authToken=user', + '//registry.npmjs.org/:_authToken=user', + '//registry.npmjs.org/:always-auth=user', + '//registry.npmjs.org/:email=test-email-old@npmjs.org', + ].join('\n'), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.couchadduser({ + username: 'test-user', + password: 'test-password', + email: 'test-email@npmjs.org', + token: 'npm_test-token', + }) + await npm.exec('adduser', []) + t.same(npm.config.get('email'), 'test-email-old@npmjs.org') + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + email: 'test-email-old@npmjs.org', + }, 'should only have token and un-nerfed old email') }) - registry.couchlogin({ - username: 'test-user', - password: 'test-password', - email: 'test-email@npmjs.org', - token: 'npm_test-token', - }) - await npm.exec('adduser', []) - t.same(npm.config.get('email'), 'test-email-old@npmjs.org') - t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') - const rc = fs.readFileSync(path.join(home, '.npmrc'), 'utf8') - t.same( - rc.trim().split(os.EOL), [ - '//registry.npmjs.org/:_authToken=npm_test-token', - 'email=test-email-old@npmjs.org', - ], 'should only have token and un-nerfed old email' - ) -}) -t.test('bad auth type', async t => { - const { npm } = await loadMockNpm(t, { - config: { - 'auth-type': 'foo', - }, + t.test('scoped adduser', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + stdin.write('test-email@npmjs.org\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + config: { + scope: '@myscope', + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.couchadduser({ + username: 'test-user', + password: 'test-password', + email: 'test-email@npmjs.org', + token: 'npm_test-token', + }) + await npm.exec('adduser', []) + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + t.same(npm.config.get('@myscope:registry'), 'https://registry.npmjs.org/') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + '@myscope:registry': 'https://registry.npmjs.org/', + }, 'should only have token and scope:registry') }) - await t.rejects(npm.exec('adduser', []), { - message: 'no such auth module', - }) -}) -t.test('auth-type sso warning', async t => { - const { logs } = await loadMockNpm(t, { - config: { - 'auth-type': 'sso', - }, + t.test('scoped adduser with valid scoped registry config', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + stdin.write('test-email@npmjs.org\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + homeDir: { + '.npmrc': '@myscope:registry=https://diff-registry.npmjs.org', + }, + config: { + scope: '@myscope', + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: 'https://diff-registry.npmjs.org', + }) + registry.couchadduser({ + username: 'test-user', + password: 'test-password', + email: 'test-email@npmjs.org', + token: 'npm_test-token', + }) + await npm.exec('adduser', []) + t.same(npm.config.get('//diff-registry.npmjs.org/:_authToken'), 'npm_test-token') + t.same(npm.config.get('@myscope:registry'), 'https://diff-registry.npmjs.org') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '@myscope:registry': 'https://diff-registry.npmjs.org', + '//diff-registry.npmjs.org/:_authToken': 'npm_test-token', + }, 'should only have token and scope:registry') }) - t.matchSnapshot({ warn: logs.warn }, 'warning') -}) -t.test('scoped login', async t => { - const stdin = new stream.PassThrough() - stdin.write('test-user\n') - stdin.write('test-password\n') - stdin.write('test-email@npmjs.org\n') - mockGlobals(t, { - 'process.stdin': stdin, - 'process.stdout': new stream.PassThrough(), // to quiet readline - }, { replace: true }) - const { npm, home } = await loadMockNpm(t, { - config: { - scope: '@myscope', - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), + t.test('save config failure', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + stdin.write('test-email@npmjs.org\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm } = await loadMockNpm(t, { + homeDir: { + '.npmrc': {}, + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.couchadduser({ + username: 'test-user', + password: 'test-password', + email: 'test-email@npmjs.org', + token: 'npm_test-token', + }) + await t.rejects(npm.exec('adduser', [])) }) - registry.couchlogin({ - username: 'test-user', - password: 'test-password', - email: 'test-email@npmjs.org', - token: 'npm_test-token', - }) - await npm.exec('adduser', []) - t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') - t.same(npm.config.get('@myscope:registry'), 'https://registry.npmjs.org/') - const rc = fs.readFileSync(path.join(home, '.npmrc'), 'utf8') - t.same( - rc.trim().split(os.EOL), [ - '//registry.npmjs.org/:_authToken=npm_test-token', - '@myscope:registry=https://registry.npmjs.org/', - ], 'should only have token and scope:registry') -}) - -t.test('scoped login with valid scoped registry config', async t => { - const stdin = new stream.PassThrough() - stdin.write('test-user\n') - stdin.write('test-password\n') - stdin.write('test-email@npmjs.org\n') - mockGlobals(t, { - 'process.stdin': stdin, - 'process.stdout': new stream.PassThrough(), // to quiet readline - }, { replace: true }) - const { npm, home } = await loadMockNpm(t, { - homeDir: { - '.npmrc': '@myscope:registry=https://diff-registry.npmjs.org', - }, - config: { - scope: '@myscope', - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: 'https://diff-registry.npmjs.org', - }) - registry.couchlogin({ - username: 'test-user', - password: 'test-password', - email: 'test-email@npmjs.org', - token: 'npm_test-token', - }) - await npm.exec('adduser', []) - t.same(npm.config.get('//diff-registry.npmjs.org/:_authToken'), 'npm_test-token') - t.same(npm.config.get('@myscope:registry'), 'https://diff-registry.npmjs.org') - const rc = fs.readFileSync(path.join(home, '.npmrc'), 'utf8') - t.same(rc.trim().split(os.EOL), - [ - '@myscope:registry=https://diff-registry.npmjs.org', - '//diff-registry.npmjs.org/:_authToken=npm_test-token', - ], 'should only have token and scope:registry') + t.end() }) -t.test('save config failure', async t => { - const stdin = new stream.PassThrough() - stdin.write('test-user\n') - stdin.write('test-password\n') - stdin.write('test-email@npmjs.org\n') - mockGlobals(t, { - 'process.stdin': stdin, - 'process.stdout': new stream.PassThrough(), // to quiet readline - }, { replace: true }) - const { npm } = await loadMockNpm(t, { - homeDir: { - '.npmrc': {}, - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - }) - registry.couchlogin({ - username: 'test-user', - password: 'test-password', - email: 'test-email@npmjs.org', - token: 'npm_test-token', +t.test('web', t => { + t.test('basic adduser', async t => { + const { npm, home } = await loadMockNpm(t, { + config: { 'auth-type': 'web' }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.webadduser({ token: 'npm_test-token' }) + await npm.exec('adduser', []) + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + }) }) - await t.rejects(npm.exec('adduser', [])) + t.end() }) diff --git a/test/lib/commands/login.js b/test/lib/commands/login.js new file mode 100644 index 000000000..8d2742131 --- /dev/null +++ b/test/lib/commands/login.js @@ -0,0 +1,151 @@ +const t = require('tap') +const fs = require('fs') +const path = require('path') +const ini = require('ini') + +const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') +const mockGlobals = require('../../fixtures/mock-globals.js') +const MockRegistry = require('../../fixtures/mock-registry.js') +const stream = require('stream') + +t.test('usage', async t => { + const { npm } = await loadMockNpm(t) + const login = await npm.cmd('login') + t.match(login.usage, 'login', 'usage has command name in it') +}) + +t.test('legacy', t => { + t.test('basic login', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + config: { 'auth-type': 'legacy' }, + homeDir: { + // These all get cleaned up by config.setCredentialsByURI + '.npmrc': [ + '_token=user', + '_password=user', + 'username=user', + '_auth=user', + '_authtoken=user', + '-authtoken=user', + '_authToken=user', + '//registry.npmjs.org/:_authToken=user', + '//registry.npmjs.org/:always-auth=user', + '//registry.npmjs.org/:email=test-email-old@npmjs.org', + ].join('\n'), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.couchlogin({ + username: 'test-user', + password: 'test-password', + token: 'npm_test-token', + }) + await npm.exec('login', []) + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + email: 'test-email-old@npmjs.org', + }, 'should only have token and un-nerfed old email') + }) + + t.test('scoped login default registry', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + config: { + 'auth-type': 'legacy', + scope: '@npmcli', + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.couchlogin({ + username: 'test-user', + password: 'test-password', + token: 'npm_test-token', + }) + await npm.exec('login', []) + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + t.same(npm.config.get('@npmcli:registry'), 'https://registry.npmjs.org/') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + '@npmcli:registry': 'https://registry.npmjs.org/', + }, 'should only have token and scope:registry') + }) + + t.test('scoped login scoped registry', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + config: { + 'auth-type': 'legacy', + scope: '@npmcli', + }, + homeDir: { + '.npmrc': '@npmcli:registry=https://diff-registry.npmjs.org', + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: 'https://diff-registry.npmjs.org', + }) + registry.couchlogin({ + username: 'test-user', + password: 'test-password', + token: 'npm_test-token', + }) + await npm.exec('login', []) + t.same(npm.config.get('//diff-registry.npmjs.org/:_authToken'), 'npm_test-token') + t.same(npm.config.get('@npmcli:registry'), 'https://diff-registry.npmjs.org') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '@npmcli:registry': 'https://diff-registry.npmjs.org', + '//diff-registry.npmjs.org/:_authToken': 'npm_test-token', + }, 'should only have token and scope:registry') + }) + t.end() +}) + +t.test('web', t => { + t.test('basic login', async t => { + const { npm, home } = await loadMockNpm(t, { + config: { 'auth-type': 'web' }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.weblogin({ token: 'npm_test-token' }) + await npm.exec('login', []) + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + }) + }) + t.end() +}) -- cgit v1.2.3