mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 04:55:12 +02:00
Preserve contributor history and current-main behavior while resolving the exact reviewed follow-up.
Source-PR: https://github.com/affaan-m/ECC/pull/3244
Source-Parent: 5ffe239ee5
Review-Manifest-SHA256: c18c5d1d4b29c0b78fc1638be8f5d72dc6141235c6ec26cb0dd20c91911cdbfe
253 lines
11 KiB
JavaScript
253 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { openBrowser, openerCommandFor } = require('../../scripts/lib/platform-launch');
|
|
|
|
// These values are private fixtures; no test reads or changes process.env.
|
|
const privateEnvironment = Object.freeze({
|
|
Path: '/synthetic/bin', ECC_BROWSER_URL: 'old-upper',
|
|
ecc_browser_url: 'old-lower', EcC_BrOwSeR_Url: 'old-mixed', FIXTURE_VALUE: 'keep',
|
|
});
|
|
const expectedWindowsScript = `$ErrorActionPreference = 'Stop'
|
|
try {
|
|
$value = [System.Environment]::GetEnvironmentVariable('ECC_BROWSER_URL', 'Process')
|
|
[System.Environment]::SetEnvironmentVariable('ECC_BROWSER_URL', $null, 'Process')
|
|
$uri = $null
|
|
if (-not [System.Uri]::TryCreate($value, [System.UriKind]::Absolute, [ref]$uri) -or @('http', 'https') -notcontains $uri.Scheme -or $uri.UserInfo) { exit 1 }
|
|
$info = New-Object System.Diagnostics.ProcessStartInfo
|
|
$info.FileName = $value
|
|
$info.UseShellExecute = $true
|
|
[void][System.Diagnostics.Process]::Start($info)
|
|
} catch { exit 1 }
|
|
`;
|
|
|
|
function fakeChild() {
|
|
return { on() {}, unref() {} };
|
|
}
|
|
|
|
function captureLaunch(url, platform) {
|
|
const calls = [];
|
|
const result = openBrowser(url, platform, (command, args, options) => {
|
|
calls.push({ command, args, options });
|
|
return fakeChild();
|
|
}, privateEnvironment);
|
|
return { result, calls };
|
|
}
|
|
|
|
test('openerCommandFor: darwin returns open', () => {
|
|
assert.deepEqual(openerCommandFor('darwin', 'http://x'), ['open', ['http://x']]);
|
|
});
|
|
|
|
test('openerCommandFor: win32 returns a static encoded PowerShell command', () => {
|
|
const [command, args] = openerCommandFor('win32', 'http://x');
|
|
assert.equal(command, 'powershell.exe');
|
|
assert.deepEqual(args.slice(0, 4), ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand']);
|
|
assert.equal(args.length, 5);
|
|
assert.equal(Buffer.from(args[4], 'base64').toString('utf16le'), expectedWindowsScript);
|
|
});
|
|
|
|
test('openerCommandFor: linux returns xdg-open', () => {
|
|
assert.deepEqual(openerCommandFor('linux', 'http://x'), ['xdg-open', ['http://x']]);
|
|
});
|
|
|
|
test('openerCommandFor: unknown falls through to xdg-open', () => {
|
|
assert.deepEqual(openerCommandFor('freebsd', 'http://x'), ['xdg-open', ['http://x']]);
|
|
});
|
|
|
|
test('openBrowser: invalid url returns invalid-url without spawning', () => {
|
|
let calls = 0;
|
|
const launch = () => { calls += 1; };
|
|
assert.deepEqual(openBrowser('', 'linux', launch), { opened: false, reason: 'invalid-url' });
|
|
assert.deepEqual(openBrowser(null, 'linux', launch), { opened: false, reason: 'invalid-url' });
|
|
assert.equal(calls, 0);
|
|
});
|
|
|
|
test('openBrowser: reports synchronous launcher failures', () => {
|
|
const withCode = () => { throw Object.assign(new Error('missing launcher'), { code: 'ENOENT' }); };
|
|
const withoutCode = () => { throw new Error('launcher failed'); };
|
|
assert.deepEqual(openBrowser('http://localhost:0', 'linux', withCode), {
|
|
opened: false, reason: 'spawn-threw:ENOENT',
|
|
});
|
|
assert.deepEqual(openBrowser('http://localhost:0', 'linux', withoutCode), {
|
|
opened: false, reason: 'spawn-threw:unknown',
|
|
});
|
|
});
|
|
|
|
test('openBrowser: installs an error listener and detaches the launcher', () => {
|
|
const handlers = new Map();
|
|
let unrefCalls = 0;
|
|
let launchCalls = 0;
|
|
const child = {
|
|
on(event, listener) {
|
|
handlers.set(event, listener);
|
|
},
|
|
unref() {
|
|
assert.equal(typeof handlers.get('error'), 'function', 'listen before detaching');
|
|
unrefCalls += 1;
|
|
},
|
|
};
|
|
|
|
const result = openBrowser('http://localhost:0', 'linux', (command, args, options) => {
|
|
launchCalls += 1;
|
|
assert.equal(command, 'xdg-open');
|
|
assert.deepEqual(args, ['http://localhost:0/']);
|
|
assert.deepEqual(options, { detached: true, stdio: 'ignore', shell: false });
|
|
return child;
|
|
});
|
|
|
|
assert.deepEqual(result, { opened: true, reason: 'spawned' });
|
|
assert.equal(launchCalls, 1);
|
|
assert.equal(unrefCalls, 1);
|
|
assert.equal(typeof handlers.get('error'), 'function');
|
|
assert.doesNotThrow(() => handlers.get('error')({ code: 'ENOENT' }));
|
|
});
|
|
|
|
test('openBrowser: a detach failure does not escape after the listener is installed', () => {
|
|
let listener;
|
|
const result = openBrowser('http://localhost:0', 'linux', () => ({
|
|
on(event, callback) {
|
|
assert.equal(event, 'error');
|
|
listener = callback;
|
|
},
|
|
unref() {
|
|
assert.equal(typeof listener, 'function');
|
|
throw new Error('cannot detach');
|
|
},
|
|
}));
|
|
assert.deepEqual(result, { opened: true, reason: 'spawned' });
|
|
assert.doesNotThrow(() => listener({ code: 'EACCES' }));
|
|
});
|
|
|
|
|
|
for (const [name, value] of [
|
|
['empty', ''], ['null', null], ['undefined', undefined], ['number', 42],
|
|
['boolean', true], ['array', ['https://example.test']],
|
|
['boxed string', Object('https://example.test')],
|
|
['object', { toString() { throw new Error('must not coerce'); } }],
|
|
['leading whitespace', ' https://example.test'],
|
|
['trailing whitespace', 'https://example.test '],
|
|
['tab', 'https://example.test/\tdata'],
|
|
['newline', 'https://example.test/\ndata'],
|
|
['carriage return', 'https://example.test/\rdata'],
|
|
['NUL', 'https://example.test/\u0000data'],
|
|
['DEL', 'https://example.test/\u007fdata'],
|
|
['backslash', 'https://example.test/\\data'],
|
|
['relative', '/example'], ['protocol relative', '//example.test'],
|
|
['javascript', 'javascript:alert(1)'], ['data', 'data:text/plain,hello'],
|
|
['file', 'file:///tmp/example'], ['custom protocol', 'app://example'],
|
|
['missing host', 'https://'], ['missing double slash', 'https:example.test'],
|
|
['credentials', 'https://user:password@example.test'],
|
|
['username', 'https://user@example.test'], ['password only', 'https://:password@example.test'],
|
|
['invalid port', 'https://example.test:65536/'],
|
|
['invalid IPv6', 'http://::1:3000/'], ['invalid host', 'https://exa mple.test/'],
|
|
]) {
|
|
test(`openBrowser: rejects ${name} before every platform dispatch`, () => {
|
|
for (const platform of ['win32', 'darwin', 'linux']) {
|
|
const { result, calls } = captureLaunch(value, platform);
|
|
assert.deepEqual(result, { opened: false, reason: 'invalid-url' });
|
|
assert.equal(calls.length, 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
test('openBrowser: rejects every raw ASCII control byte without coercion', () => {
|
|
for (let code = 0; code < 32; code += 1) {
|
|
const { result, calls } = captureLaunch(`https://example.test/a${String.fromCharCode(code)}b`, 'win32');
|
|
assert.deepEqual(result, { opened: false, reason: 'invalid-url' });
|
|
assert.equal(calls.length, 0);
|
|
}
|
|
});
|
|
|
|
for (const [input, normalized] of [
|
|
['http://localhost:0', 'http://localhost:0/'],
|
|
['HTTP://127.0.0.1:3000/test?q=1#frag', 'http://127.0.0.1:3000/test?q=1#frag'],
|
|
['https://[::1]:443', 'https://[::1]/'],
|
|
['HTTPS://EXAMPLE.TEST:443', 'https://example.test/'],
|
|
['https://example.test/a%20b?q=a&x=b#fragment', 'https://example.test/a%20b?q=a&x=b#fragment'],
|
|
['https://b\u00fccher.example/caf\u00e9', 'https://xn--bcher-kva.example/caf%C3%A9'],
|
|
]) {
|
|
test(`openBrowser: normalizes an accepted HTTP/S URL on every platform (${input})`, () => {
|
|
for (const platform of ['darwin', 'linux', 'freebsd', 'win32']) {
|
|
const { result, calls } = captureLaunch(input, platform);
|
|
assert.deepEqual(result, { opened: true, reason: 'spawned' });
|
|
assert.equal(calls.length, 1);
|
|
const { command, args, options } = calls[0];
|
|
assert.equal(options.shell, false);
|
|
assert.equal(options.detached, true);
|
|
assert.equal(options.stdio, 'ignore');
|
|
assert.notEqual(options.windowsVerbatimArguments, true);
|
|
if (platform === 'win32') {
|
|
assert.equal(command, 'powershell.exe');
|
|
assert.equal(options.env.ECC_BROWSER_URL, normalized);
|
|
assert.equal(options.windowsHide, true);
|
|
} else {
|
|
assert.equal(command, platform === 'darwin' ? 'open' : 'xdg-open');
|
|
assert.deepEqual(args, [normalized]);
|
|
assert.equal(Object.hasOwn(options, 'env'), false);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
test('openBrowser: different untrusted URL data leaves Windows code and argv identical', () => {
|
|
const inputs = [
|
|
'https://example.test/?left=1&right=2',
|
|
`https://example.test/?q="';$(Get-Process);&next=%COMSPEC%&name=caf\u00e9`,
|
|
'https://example.test/?q=%22%26%0d%0a&next=https%3A%2F%2Fexample.test',
|
|
];
|
|
let firstArgs;
|
|
for (const input of inputs) {
|
|
const { result, calls } = captureLaunch(input, 'win32');
|
|
assert.deepEqual(result, { opened: true, reason: 'spawned' });
|
|
assert.equal(calls.length, 1);
|
|
const { command, args, options } = calls[0];
|
|
assert.equal(command, 'powershell.exe');
|
|
assert.deepEqual(args.slice(0, 4), ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand']);
|
|
assert.equal(args.length, 5);
|
|
assert.equal(Buffer.from(args[4], 'base64').toString('utf16le'), expectedWindowsScript);
|
|
assert.ok(!args.some(arg => arg.includes(input)));
|
|
if (firstArgs) assert.deepEqual(args, firstArgs);
|
|
else firstArgs = args;
|
|
assert.equal(options.env.ECC_BROWSER_URL, new URL(input).href);
|
|
assert.equal(options.shell, false);
|
|
}
|
|
assert.doesNotMatch(expectedWindowsScript, /Invoke-Expression|Start-Process|\.Arguments|cmd|ExecutionPolicy/);
|
|
assert.ok(expectedWindowsScript.indexOf('::SetEnvironmentVariable') < expectedWindowsScript.indexOf('::Start($info)'));
|
|
});
|
|
|
|
test('openBrowser: Windows child environment removes case-insensitive aliases without mutation', () => {
|
|
const before = { ...privateEnvironment };
|
|
const { calls } = captureLaunch('https://example.test/', 'win32');
|
|
assert.equal(calls.length, 1);
|
|
const childEnvironment = calls[0].options.env;
|
|
assert.notEqual(childEnvironment, privateEnvironment);
|
|
assert.deepEqual(Object.keys(childEnvironment).filter(key => key.toLowerCase() === 'ecc_browser_url'), ['ECC_BROWSER_URL']);
|
|
assert.equal(childEnvironment.ECC_BROWSER_URL, 'https://example.test/');
|
|
assert.equal(childEnvironment.Path, before.Path);
|
|
assert.equal(childEnvironment.FIXTURE_VALUE, before.FIXTURE_VALUE);
|
|
assert.deepEqual(privateEnvironment, before);
|
|
});
|
|
|
|
test('openBrowser: default platform still uses the injected launcher and private environment', () => {
|
|
const { result, calls } = captureLaunch('https://example.test', undefined);
|
|
assert.deepEqual(result, { opened: true, reason: 'spawned' });
|
|
assert.equal(calls.length, 1);
|
|
assert.deepEqual([calls[0].command, calls[0].args], openerCommandFor(process.platform, 'https://example.test/'));
|
|
});
|
|
|
|
for (const [error, reason] of [
|
|
[{ code: 'EACCES' }, 'child-error:EACCES'],
|
|
[new Error('no code'), 'child-error:spawn-error'],
|
|
]) {
|
|
test(`openBrowser: captures an already-signalled child failure (${reason})`, () => {
|
|
let unrefCalls = 0;
|
|
const result = openBrowser('https://example.test/', 'win32', () => ({
|
|
on(event, listener) { assert.equal(event, 'error'); listener(error); },
|
|
unref() { unrefCalls += 1; },
|
|
}), privateEnvironment);
|
|
assert.deepEqual(result, { opened: false, reason });
|
|
assert.equal(unrefCalls, 1);
|
|
});
|
|
}
|