2021-04-15 21:53:45 -04:00
|
|
|
const t = require('tap')
|
2023-01-16 22:38:23 -05:00
|
|
|
const tmock = require('../../fixtures/tmock')
|
2023-06-08 05:24:49 -07:00
|
|
|
const mockNpm = require('../../fixtures/mock-npm')
|
2024-05-30 04:21:05 -07:00
|
|
|
const EventEmitter = require('node:events')
|
2021-02-08 16:16:45 -05:00
|
|
|
|
2023-06-08 05:24:49 -07:00
|
|
|
const mockOpenUrl = async (t, args, { openerResult, ...config } = {}) => {
|
|
|
|
let openerUrl = null
|
|
|
|
let openerOpts = null
|
|
|
|
|
|
|
|
const open = async (url, options) => {
|
|
|
|
openerUrl = url
|
|
|
|
openerOpts = options
|
|
|
|
if (openerResult) {
|
|
|
|
throw openerResult
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const mock = await mockNpm(t, { config })
|
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
const { openUrl } = tmock(t, '{LIB}/utils/open-url.js', {
|
2023-06-08 05:24:49 -07:00
|
|
|
'@npmcli/promise-spawn': { open },
|
|
|
|
})
|
2021-02-08 16:16:45 -05:00
|
|
|
|
2023-06-08 05:24:49 -07:00
|
|
|
const openWithNpm = (...a) => openUrl(mock.npm, ...a)
|
2022-12-06 22:18:33 -05:00
|
|
|
|
2023-06-08 05:24:49 -07:00
|
|
|
if (args) {
|
|
|
|
await openWithNpm(...args)
|
2022-12-06 22:18:33 -05:00
|
|
|
}
|
2021-02-08 16:16:45 -05:00
|
|
|
|
2024-05-30 04:21:05 -07:00
|
|
|
mock.npm.finish()
|
|
|
|
|
2023-06-08 05:24:49 -07:00
|
|
|
return {
|
|
|
|
...mock,
|
|
|
|
openUrl: openWithNpm,
|
|
|
|
openerUrl: () => openerUrl,
|
|
|
|
openerOpts: () => openerOpts,
|
|
|
|
}
|
|
|
|
}
|
2021-02-08 16:16:45 -05:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
const mockOpenUrlPrompt = async (t, {
|
|
|
|
questionShouldResolve = true,
|
|
|
|
openUrlPromptInterrupted = false,
|
|
|
|
openerResult = null,
|
|
|
|
isTTY = true,
|
|
|
|
abort = false,
|
|
|
|
url: openUrl = 'https://www.npmjs.com',
|
|
|
|
...config
|
|
|
|
}) => {
|
|
|
|
const mock = await mockNpm(t, {
|
|
|
|
globals: {
|
|
|
|
'process.stdin.isTTY': isTTY,
|
|
|
|
'process.stdout.isTTY': isTTY,
|
|
|
|
},
|
|
|
|
config,
|
|
|
|
})
|
2021-02-08 16:16:45 -05:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
let openerUrl = null
|
|
|
|
let openerOpts = null
|
2021-02-08 16:16:45 -05:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
const { openUrlPrompt } = tmock(t, '{LIB}/utils/open-url.js', {
|
|
|
|
'@npmcli/promise-spawn': {
|
|
|
|
open: async (url, options) => {
|
|
|
|
openerUrl = url
|
|
|
|
openerOpts = options
|
|
|
|
if (openerResult) {
|
|
|
|
throw openerResult
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'node:readline/promises': {
|
|
|
|
createInterface: () => {
|
|
|
|
return Object.assign(new EventEmitter(), {
|
|
|
|
question: async (p, { signal } = {}) => {
|
|
|
|
if (questionShouldResolve !== true) {
|
|
|
|
await new Promise((res, rej) => {
|
|
|
|
if (signal) {
|
|
|
|
signal.addEventListener('abort', () => {
|
|
|
|
const err = new Error('abort')
|
|
|
|
err.name = 'AbortError'
|
|
|
|
rej(err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
close: () => {},
|
|
|
|
once: function (event, cb) {
|
|
|
|
if (openUrlPromptInterrupted && event === 'SIGINT') {
|
|
|
|
cb()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
})
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
2021-10-07 20:21:11 +00:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
let error
|
|
|
|
const abortController = new AbortController()
|
|
|
|
const args = [mock.npm, openUrl, 'npm home', 'prompt', { signal: abortController.signal }]
|
|
|
|
if (abort) {
|
|
|
|
mock.open = openUrlPrompt(...args)
|
|
|
|
} else {
|
|
|
|
await openUrlPrompt(...args).catch((er) => error = er)
|
|
|
|
}
|
2021-11-18 20:58:02 +00:00
|
|
|
|
2024-05-30 04:21:05 -07:00
|
|
|
mock.npm.finish()
|
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
return {
|
|
|
|
...mock,
|
|
|
|
openerUrl,
|
|
|
|
openerOpts,
|
|
|
|
OUTPUT: mock.joinedOutput(),
|
|
|
|
error,
|
|
|
|
abortController,
|
|
|
|
}
|
|
|
|
}
|
2021-11-18 20:58:02 +00:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
t.test('open url prompt', async t => {
|
|
|
|
t.test('does not open a url in non-interactive environments', async t => {
|
|
|
|
const { openerUrl, openerOpts } = await mockOpenUrlPrompt(t, { isTTY: false })
|
2021-02-08 16:16:45 -05:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
t.equal(openerUrl, null, 'did not open')
|
|
|
|
t.same(openerOpts, null, 'did not open')
|
|
|
|
})
|
2021-02-08 16:16:45 -05:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
t.test('opens a url', async t => {
|
|
|
|
const { OUTPUT, openerUrl, openerOpts } = await mockOpenUrlPrompt(t, { browser: true })
|
2021-02-08 16:16:45 -05:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
t.equal(openerUrl, 'https://www.npmjs.com', 'opened the given url')
|
|
|
|
t.same(openerOpts, { command: null }, 'passed command as null (the default)')
|
|
|
|
t.matchSnapshot(OUTPUT)
|
|
|
|
})
|
2021-02-08 16:16:45 -05:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
t.test('opens a url with browser string', async t => {
|
|
|
|
const { openerUrl, openerOpts } = await mockOpenUrlPrompt(t, { browser: 'firefox' })
|
|
|
|
|
|
|
|
t.equal(openerUrl, 'https://www.npmjs.com', 'opened the given url')
|
|
|
|
// FIXME: browser string is parsed as a boolean in config layer
|
|
|
|
// this is a bug that should be fixed or the config should not allow it
|
|
|
|
t.same(openerOpts, { command: null }, 'passed command as null (the default)')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('prints json output', async t => {
|
|
|
|
const { OUTPUT } = await mockOpenUrlPrompt(t, { json: true })
|
|
|
|
|
|
|
|
t.matchSnapshot(OUTPUT)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('returns error for non-https url', async t => {
|
|
|
|
const { error, OUTPUT, openerUrl, openerOpts } = await mockOpenUrlPrompt(t, {
|
|
|
|
url: 'ftp://www.npmjs.com',
|
|
|
|
})
|
|
|
|
|
|
|
|
t.match(error, /Invalid URL/, 'got the correct error')
|
|
|
|
t.equal(openerUrl, null, 'did not open')
|
|
|
|
t.same(openerOpts, null, 'did not open')
|
|
|
|
t.same(OUTPUT, '', 'printed no output')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('does not open url if canceled', async t => {
|
|
|
|
const { openerUrl, openerOpts, open, abortController } = await mockOpenUrlPrompt(t, {
|
|
|
|
questionShouldResolve: false,
|
|
|
|
abort: true,
|
|
|
|
})
|
|
|
|
|
|
|
|
abortController.abort()
|
|
|
|
|
|
|
|
await open
|
2023-06-08 05:24:49 -07:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
t.equal(openerUrl, null, 'did not open')
|
|
|
|
t.same(openerOpts, null, 'did not open')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('returns error when opener errors', async t => {
|
|
|
|
const { error, openerUrl } = await mockOpenUrlPrompt(t, {
|
|
|
|
openerResult: Object.assign(new Error('Opener failed'), { code: 1 }),
|
|
|
|
})
|
|
|
|
|
|
|
|
t.match(error, /Opener failed/, 'got the correct error')
|
|
|
|
t.equal(openerUrl, 'https://www.npmjs.com', 'did not open')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('does not error when opener can not find command', async t => {
|
|
|
|
const { OUTPUT, error, openerUrl } = await mockOpenUrlPrompt(t, {
|
|
|
|
openerResult: Object.assign(new Error('Opener failed'), { code: 127 }),
|
|
|
|
})
|
|
|
|
|
|
|
|
t.notOk(error, 'Did not error')
|
|
|
|
t.equal(openerUrl, 'https://www.npmjs.com', 'did not open')
|
|
|
|
t.matchSnapshot(OUTPUT, 'Outputs extra Browser unavailable message and url')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('throws "canceled" error on SIGINT', async t => {
|
|
|
|
const { open } = await mockOpenUrlPrompt(t, {
|
|
|
|
questionShouldResolve: false,
|
|
|
|
openUrlPromptInterrupted: true,
|
|
|
|
abort: true,
|
|
|
|
})
|
|
|
|
|
|
|
|
await t.rejects(open, /canceled/, 'message is canceled')
|
|
|
|
})
|
2021-03-04 17:40:28 -05:00
|
|
|
})
|
2021-02-08 16:16:45 -05:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
t.test('open url', async t => {
|
|
|
|
t.test('opens a url', async t => {
|
|
|
|
const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t,
|
|
|
|
['https://www.npmjs.com', 'npm home'])
|
|
|
|
t.equal(openerUrl(), 'https://www.npmjs.com', 'opened the given url')
|
|
|
|
t.same(openerOpts(), { command: null }, 'passed command as null (the default)')
|
|
|
|
t.same(joinedOutput(), '', 'printed no output')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('returns error for non-https url', async t => {
|
|
|
|
const { openUrl, openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t)
|
|
|
|
await t.rejects(
|
|
|
|
openUrl('ftp://www.npmjs.com', 'npm home'),
|
|
|
|
/Invalid URL/,
|
|
|
|
'got the correct error'
|
|
|
|
)
|
|
|
|
t.equal(openerUrl(), null, 'did not open')
|
|
|
|
t.same(openerOpts(), null, 'did not open')
|
|
|
|
t.same(joinedOutput(), '', 'printed no output')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('returns error for file url', async t => {
|
|
|
|
const { openUrl, openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t)
|
|
|
|
await t.rejects(
|
|
|
|
openUrl('file:///usr/local/bin/ls', 'npm home'),
|
|
|
|
/Invalid URL/,
|
|
|
|
'got the correct error'
|
|
|
|
)
|
|
|
|
t.equal(openerUrl(), null, 'did not open')
|
|
|
|
t.same(openerOpts(), null, 'did not open')
|
|
|
|
t.same(joinedOutput(), '', 'printed no output')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('file url allowed if explicitly asked for', async t => {
|
|
|
|
const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t,
|
|
|
|
['file:///man/page/npm-install', 'npm home', true])
|
|
|
|
t.equal(openerUrl(), 'file:///man/page/npm-install', 'opened the given url')
|
|
|
|
t.same(openerOpts(), { command: null }, 'passed command as null (the default)')
|
|
|
|
t.same(joinedOutput(), '', 'printed no output')
|
2021-02-08 16:16:45 -05:00
|
|
|
})
|
2023-06-08 05:24:49 -07:00
|
|
|
|
2024-05-16 05:38:49 -07:00
|
|
|
t.test('returns error for non-parseable url', async t => {
|
|
|
|
const { openUrl, openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t)
|
|
|
|
await t.rejects(
|
|
|
|
openUrl('git+ssh://user@host:repo.git', 'npm home'),
|
|
|
|
/Invalid URL/,
|
|
|
|
'got the correct error'
|
|
|
|
)
|
|
|
|
t.equal(openerUrl(), null, 'did not open')
|
|
|
|
t.same(openerOpts(), null, 'did not open')
|
|
|
|
t.same(joinedOutput(), '', 'printed no output')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('encodes non-URL-safe characters in url provided', async t => {
|
|
|
|
const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t,
|
|
|
|
['https://www.npmjs.com/|cat', 'npm home'])
|
|
|
|
t.equal(openerUrl(), 'https://www.npmjs.com/%7Ccat', 'opened the encoded url')
|
|
|
|
t.same(openerOpts(), { command: null }, 'passed command as null (the default)')
|
|
|
|
t.same(joinedOutput(), '', 'printed no output')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('opens a url with the given browser', async t => {
|
|
|
|
const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t,
|
|
|
|
['https://www.npmjs.com', 'npm home'], { browser: 'chrome' })
|
|
|
|
t.equal(openerUrl(), 'https://www.npmjs.com', 'opened the given url')
|
|
|
|
// FIXME: browser string is parsed as a boolean in config layer
|
|
|
|
// this is a bug that should be fixed or the config should not allow it
|
|
|
|
t.same(openerOpts(), { command: null }, 'passed the given browser as command')
|
|
|
|
t.same(joinedOutput(), '', 'printed no output')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('prints where to go when browser is disabled', async t => {
|
|
|
|
const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t,
|
|
|
|
['https://www.npmjs.com', 'npm home'], { browser: false })
|
|
|
|
t.equal(openerUrl(), null, 'did not open')
|
|
|
|
t.same(openerOpts(), null, 'did not open')
|
|
|
|
t.matchSnapshot(joinedOutput(), 'printed expected message')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('prints where to go when browser is disabled and json is enabled', async t => {
|
|
|
|
const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t,
|
|
|
|
['https://www.npmjs.com', 'npm home'], { browser: false, json: true })
|
|
|
|
t.equal(openerUrl(), null, 'did not open')
|
|
|
|
t.same(openerOpts(), null, 'did not open')
|
|
|
|
t.matchSnapshot(joinedOutput(), 'printed expected message')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('prints where to go when given browser does not exist', async t => {
|
|
|
|
const { openerUrl, openerOpts, joinedOutput } = await mockOpenUrl(t,
|
|
|
|
['https://www.npmjs.com', 'npm home'],
|
|
|
|
{
|
|
|
|
openerResult: Object.assign(new Error('failed'), { code: 127 }),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
t.equal(openerUrl(), 'https://www.npmjs.com', 'tried to open the correct url')
|
|
|
|
t.same(openerOpts(), { command: null }, 'tried to use the correct browser')
|
|
|
|
t.matchSnapshot(joinedOutput(), 'printed expected message')
|
|
|
|
})
|
|
|
|
|
|
|
|
t.test('handles unknown opener error', async t => {
|
|
|
|
const { openUrl } = await mockOpenUrl(t, null, {
|
|
|
|
browser: 'firefox',
|
|
|
|
openerResult: Object.assign(new Error('failed'), { code: 'ENOBRIAN' }),
|
|
|
|
})
|
|
|
|
|
|
|
|
await t.rejects(openUrl('https://www.npmjs.com', 'npm home'), 'failed', 'got the correct error')
|
|
|
|
})
|
2021-02-08 16:16:45 -05:00
|
|
|
})
|