|
| 1 | +import fs from 'fs'; |
| 2 | +import path from 'path'; |
| 3 | +import { afterEach, describe, expect, it } from '@jest/globals'; |
| 4 | +import { checkNpmAuthEnvPassthrough, filterPathForNpm } from '../../packageManager/npmAuthEnvPassthrough'; |
| 5 | +import { BeachballError } from '../../types/BeachballError'; |
| 6 | +import { initMockLogs } from '../../__fixtures__/mockLogs'; |
| 7 | +import { tmpdir, removeTempDir } from '../../__fixtures__/tmpdir'; |
| 8 | + |
| 9 | +describe('filterPathForNpm', () => { |
| 10 | + const makePathEnv = (...parts: string[]) => parts.join(path.delimiter); |
| 11 | + |
| 12 | + it('keeps a plain path unchanged', () => { |
| 13 | + const p = makePathEnv('/usr/local/bin', '/usr/bin', '/bin'); |
| 14 | + expect(filterPathForNpm(p)).toEqual(p); |
| 15 | + }); |
| 16 | + |
| 17 | + it('removes entries whose basename starts with yarn--', () => { |
| 18 | + const p = makePathEnv('/usr/local/bin', '/tmp/yarn--1234567890', '/usr/bin'); |
| 19 | + expect(filterPathForNpm(p)).toEqual(makePathEnv('/usr/local/bin', '/usr/bin')); |
| 20 | + }); |
| 21 | + |
| 22 | + it('removes entries whose basename starts with xfs-', () => { |
| 23 | + const p = makePathEnv('/usr/local/bin', '/tmp/xfs-abc123', '/usr/bin'); |
| 24 | + expect(filterPathForNpm(p)).toEqual(makePathEnv('/usr/local/bin', '/usr/bin')); |
| 25 | + }); |
| 26 | + |
| 27 | + it('removes multiple filtered entries', () => { |
| 28 | + const p = makePathEnv('/usr/local/bin', '/tmp/yarn--abc', '/tmp/xfs-xyz', '/usr/bin'); |
| 29 | + expect(filterPathForNpm(p)).toEqual(makePathEnv('/usr/local/bin', '/usr/bin')); |
| 30 | + }); |
| 31 | + |
| 32 | + it('keeps entries where yarn-- or xfs- appears in a parent segment but not the basename', () => { |
| 33 | + const p = makePathEnv('/home/user/yarn--stuff/tools', '/home/user/xfs-stuff/tools'); |
| 34 | + expect(filterPathForNpm(p)).toEqual(p); |
| 35 | + }); |
| 36 | + |
| 37 | + it('handles a single entry path', () => { |
| 38 | + expect(filterPathForNpm('/usr/local/bin')).toEqual('/usr/local/bin'); |
| 39 | + }); |
| 40 | + |
| 41 | + it('returns empty string for empty input', () => { |
| 42 | + expect(filterPathForNpm('')).toEqual(''); |
| 43 | + }); |
| 44 | +}); |
| 45 | + |
| 46 | +describe('checkNpmAuthEnvPassthrough', () => { |
| 47 | + const logs = initMockLogs(); |
| 48 | + const commonOptions = { path: process.cwd(), registry: 'https://registry.npmjs.org/' }; |
| 49 | + |
| 50 | + let wrapperDir: string | undefined; |
| 51 | + |
| 52 | + afterEach(() => { |
| 53 | + wrapperDir && removeTempDir(wrapperDir); |
| 54 | + wrapperDir = undefined; |
| 55 | + }); |
| 56 | + |
| 57 | + it('passes when the real node binary is on PATH', async () => { |
| 58 | + // process.execPath's directory is on PATH in the test environment |
| 59 | + await checkNpmAuthEnvPassthrough({ ...commonOptions }); |
| 60 | + expect(logs.getMockLines('error')).toEqual(''); |
| 61 | + }); |
| 62 | + |
| 63 | + it('passes when PATH is given explicitly with node on it', async () => { |
| 64 | + const nodeBinDir = path.dirname(process.execPath); |
| 65 | + await checkNpmAuthEnvPassthrough({ ...commonOptions, pathEnv: nodeBinDir }); |
| 66 | + expect(logs.getMockLines('error')).toEqual(''); |
| 67 | + }); |
| 68 | + |
| 69 | + // A shebang-based wrapper only works on POSIX |
| 70 | + // eslint-disable-next-line no-restricted-properties |
| 71 | + const itPosixLike = path.delimiter === ':' ? it : it.skip; |
| 72 | + itPosixLike('throws when node is wrapped by a script that drops special env vars', async () => { |
| 73 | + wrapperDir = tmpdir({ prefix: 'beachball-test-wrapper-' }); |
| 74 | + const wrapperScript = path.join(wrapperDir, 'node'); |
| 75 | + |
| 76 | + // Create a `node` wrapper script (using the real node binary in the shebang to avoid PATH lookup) |
| 77 | + // that mimics POSIX sh behavior of dropping invalid env var names. |
| 78 | + // We can't directly use `/bin/sh` because on some systems such as macOS, `sh` is actually `bash`. |
| 79 | + fs.writeFileSync( |
| 80 | + wrapperScript, |
| 81 | + [ |
| 82 | + `#!${process.execPath}`, |
| 83 | + `const { spawnSync } = require('child_process');`, |
| 84 | + `const filteredEnv = Object.fromEntries(`, |
| 85 | + ` Object.entries(process.env).filter(([k]) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(k))`, |
| 86 | + `);`, |
| 87 | + `const r = spawnSync(process.execPath, process.argv.slice(2), { env: filteredEnv, stdio: 'inherit' });`, |
| 88 | + `process.exit(r.status ?? 1);`, |
| 89 | + ].join('\n'), |
| 90 | + { mode: 0o755 } |
| 91 | + ); |
| 92 | + |
| 93 | + const pathWithWrapper = wrapperDir + path.delimiter + process.env.PATH; |
| 94 | + |
| 95 | + await expect(checkNpmAuthEnvPassthrough({ ...commonOptions, pathEnv: pathWithWrapper })).rejects.toThrow( |
| 96 | + BeachballError |
| 97 | + ); |
| 98 | + const errorLogs = logs.getMockLines('error'); |
| 99 | + expect(errorLogs).toContain('The environment variable used to pass the npm auth token'); |
| 100 | + expect(errorLogs).toContain(`Your PATH:\n${pathWithWrapper}`); |
| 101 | + expect(errorLogs).toContain('node bin/beachball.js <args>'); |
| 102 | + }); |
| 103 | +}); |
0 commit comments