|
| 1 | +import assert from 'node:assert/strict' |
| 2 | +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' |
| 3 | +import { tmpdir } from 'node:os' |
| 4 | +import path from 'node:path' |
| 5 | +import { beforeEach, test } from 'node:test' |
| 6 | +import { fileURLToPath } from 'node:url' |
| 7 | +import { build } from 'esbuild' |
| 8 | + |
| 9 | +const mocks = { |
| 10 | + '@actions/core': ` |
| 11 | + import { mock } from 'node:test' |
| 12 | + export const info = mock.fn() |
| 13 | + export const setFailed = mock.fn() |
| 14 | + export const setSecret = mock.fn() |
| 15 | + export const startGroup = mock.fn() |
| 16 | + export const endGroup = mock.fn() |
| 17 | + `, |
| 18 | + child_process: ` |
| 19 | + import { mock } from 'node:test' |
| 20 | + export const spawnSync = mock.fn() |
| 21 | + `, |
| 22 | +} |
| 23 | +const { outputFiles } = await build({ |
| 24 | + stdin: { |
| 25 | + contents: `export { runPnpmInstall } from './index.ts'; export * as core from '@actions/core'; export { spawnSync } from 'child_process'`, |
| 26 | + resolveDir: fileURLToPath(new URL('.', import.meta.url)), |
| 27 | + }, |
| 28 | + bundle: true, platform: 'node', format: 'esm', write: false, |
| 29 | + plugins: [{ |
| 30 | + name: 'mock-process-boundary', |
| 31 | + setup(builder) { |
| 32 | + builder.onResolve({ filter: /.*/ }, args => { |
| 33 | + if (Object.hasOwn(mocks, args.path)) return { path: args.path, namespace: 'mock' } |
| 34 | + }) |
| 35 | + builder.onLoad({ filter: /.*/, namespace: 'mock' }, args => ({ contents: mocks[args.path] })) |
| 36 | + }, |
| 37 | + }], |
| 38 | +}) |
| 39 | +const { runPnpmInstall, core, spawnSync } = await import( |
| 40 | + `data:text/javascript;base64,${Buffer.from(outputFiles[0].text).toString('base64')}` |
| 41 | +) |
| 42 | +const token = 'test-token-$(must-not-run)' |
| 43 | +const inputs = { |
| 44 | + workingDirectory: '.', packageJsonFile: 'package.json', |
| 45 | + registry: { registryUrl: 'https://registry.example/npm/', registryToken: token }, |
| 46 | +} |
| 47 | + |
| 48 | +beforeEach(t => { |
| 49 | + const workspace = mkdtempSync(path.join(tmpdir(), 'setup-registry-install-')) |
| 50 | + writeFileSync(path.join(workspace, 'package.json'), '{}') |
| 51 | + const previous = [process.env.GITHUB_WORKSPACE, process.env['INPUT_REGISTRY-TOKEN']] |
| 52 | + process.env.GITHUB_WORKSPACE = workspace |
| 53 | + process.env['INPUT_REGISTRY-TOKEN'] = token |
| 54 | + t.after(() => { |
| 55 | + for (const [i, key] of ['GITHUB_WORKSPACE', 'INPUT_REGISTRY-TOKEN'].entries()) { |
| 56 | + if (previous[i] === undefined) delete process.env[key] |
| 57 | + else process.env[key] = previous[i] |
| 58 | + } |
| 59 | + rmSync(workspace, { recursive: true, force: true }) |
| 60 | + }) |
| 61 | + for (const fn of Object.values(core)) fn.mock.resetCalls() |
| 62 | + spawnSync.mock.resetCalls() |
| 63 | + spawnSync.mock.mockImplementation((_bin, args) => args[0] === '--version' |
| 64 | + ? { status: 0, stdout: '12.5.1\n' } |
| 65 | + : { status: 0 }) |
| 66 | +}) |
| 67 | + |
| 68 | +for (const [name, result, failed] of [ |
| 69 | + ['success', { status: 0 }, false], |
| 70 | + ['failure', { status: 1 }, true], |
| 71 | + ['signal', { status: null, signal: 'SIGTERM' }, true], |
| 72 | + ['spawn error', { error: new Error('Unable to spawn pnpm') }, true], |
| 73 | +]) { |
| 74 | + test(`install ${name} keeps credentials in the install child only`, () => { |
| 75 | + spawnSync.mock.mockImplementation((_bin, args) => args[0] === '--version' |
| 76 | + ? { status: 0, stdout: '11.6.0' } : result) |
| 77 | + runPnpmInstall(inputs) |
| 78 | + assert.equal(spawnSync.mock.callCount(), 2) |
| 79 | + const versionEnv = spawnSync.mock.calls[0].arguments[2].env |
| 80 | + const [bin, args, options] = spawnSync.mock.calls[1].arguments |
| 81 | + assert.equal(bin, 'pnpm') |
| 82 | + assert.deepEqual(args, ['install']) |
| 83 | + assert.equal(options.shell, undefined) |
| 84 | + assert.equal(options.env['pnpm_config_//registry.example/npm/:_authToken'], token) |
| 85 | + assert.equal(options.env['INPUT_REGISTRY-TOKEN'], undefined) |
| 86 | + assert.ok(!Object.values(versionEnv).includes(token)) |
| 87 | + assert.equal(process.env['pnpm_config_//registry.example/npm/:_authToken'], undefined) |
| 88 | + assert.equal(core.setFailed.mock.callCount(), failed ? 1 : 0) |
| 89 | + assert.deepEqual(core.setSecret.mock.calls[0].arguments, [token]) |
| 90 | + for (const fn of [core.info, core.startGroup, core.setFailed]) { |
| 91 | + assert.ok(!JSON.stringify(fn.mock.calls).includes(token)) |
| 92 | + } |
| 93 | + }) |
| 94 | +} |
| 95 | + |
| 96 | +test('older pnpm cannot silently attempt an unauthenticated install', () => { |
| 97 | + spawnSync.mock.mockImplementation(() => ({ status: 0, stdout: '11.5.0' })) |
| 98 | + runPnpmInstall(inputs) |
| 99 | + assert.equal(spawnSync.mock.callCount(), 1) |
| 100 | + assert.match(core.setFailed.mock.calls[0].arguments[0], /11\.6\.0/) |
| 101 | +}) |
| 102 | + |
| 103 | +test('an install without registry credentials retains existing flags and environment', () => { |
| 104 | + runPnpmInstall({ ...inputs, registry: undefined, requireLockfile: false }, true) |
| 105 | + assert.equal(spawnSync.mock.callCount(), 1) |
| 106 | + assert.deepEqual(spawnSync.mock.calls[0].arguments[1], ['install', '--no-runtime']) |
| 107 | + assert.equal(spawnSync.mock.calls[0].arguments[2].env.PATH, process.env.PATH) |
| 108 | +}) |
| 109 | + |
| 110 | +test('a missing required lockfile never starts an authenticated process', () => { |
| 111 | + runPnpmInstall({ ...inputs, requireLockfile: true }) |
| 112 | + assert.equal(spawnSync.mock.callCount(), 0) |
| 113 | + assert.equal(core.setFailed.mock.callCount(), 1) |
| 114 | +}) |
0 commit comments