From b452d584f99fe0f568c633506d919ea97eabb606 Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Thu, 28 Mar 2024 16:47:49 -0700 Subject: [PATCH] chore(endomoat): CLI tests --- packages/endomoat/test/cli.spec.js | 194 ++++++++++++++++++ packages/endomoat/test/fixture/basic/app.js | 3 + .../fixture/basic/lavamoat/node/policy.json | 1 + .../endomoat/test/fixture/basic/package.json | 8 + packages/endomoat/test/fixture/no-deps/.npmrc | 1 - .../endomoat/test/snapshots/cli.spec.js.md | 59 ++++++ .../endomoat/test/snapshots/cli.spec.js.snap | Bin 0 -> 564 bytes 7 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 packages/endomoat/test/cli.spec.js create mode 100644 packages/endomoat/test/fixture/basic/app.js create mode 100644 packages/endomoat/test/fixture/basic/lavamoat/node/policy.json create mode 100644 packages/endomoat/test/fixture/basic/package.json delete mode 100644 packages/endomoat/test/fixture/no-deps/.npmrc create mode 100644 packages/endomoat/test/snapshots/cli.spec.js.md create mode 100644 packages/endomoat/test/snapshots/cli.spec.js.snap diff --git a/packages/endomoat/test/cli.spec.js b/packages/endomoat/test/cli.spec.js new file mode 100644 index 0000000000..3e6b783336 --- /dev/null +++ b/packages/endomoat/test/cli.spec.js @@ -0,0 +1,194 @@ +import test from 'ava' +import { execFile } from 'node:child_process' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' +import { readJsonFile } from '../src/util.js' + +const execFileAsync = promisify(execFile) + +/** + * RegExp to match ANSI escape codes + * + * @see {@link https://github.com/chalk/ansi-regex} + */ +const ANSI_REGEX = new RegExp( + // eslint-disable-next-line no-control-regex + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', + 'g' +) + +/** + * Path to the CLI entry point + */ +const CLI_PATH = fileURLToPath(new URL('../src/cli.js', import.meta.url)) + +const BASIC_ENTRYPOINT = fileURLToPath( + new URL('./fixture/basic/app.js', import.meta.url) +) +const BASIC_ENTRYPOINT_CWD = path.dirname(BASIC_ENTRYPOINT) + +/** + * @typedef {import('node:child_process').ExecFileException['code']} ExitCode + */ + +/** + * Possible types for the `expected` argument of the {@link testCLI} macro. + * + * Execution output will be automatically trimmed and stripped of ANSI escape + * codes (colors). + * + * If a `string`, it is compared to `stdout`. + * + * @template [Ctx=unknown] Default is `unknown` + * @typedef {string + * | { code: ExitCode } + * | { + * stdout: string + * code?: ExitCode + * } + * | { + * stderr: string + * code?: ExitCode + * } + * | { + * stdout: string + * stderr: string + * code?: ExitCode + * } + * | (( + * t: import('ava').ExecutionContext, + * result: { + * stdout: string + * stderr: string + * code?: ExitCode + * } + * ) => void | Promise)} ExecEndomoatExpectation + */ + +/** + * Run the `endomoat` CLI with the provided arguments + * + * @param {string[]} args CLI arguments + * @returns {Promise<{ stdout: string; stderr: string; code: ExitCode }>} + */ +async function runCli(args) { + await Promise.resolve() + + /** @type {string} */ + let stdout + /** @type {string} */ + let stderr + /** @type {import('node:child_process').ExecFileException['code']} */ + let code + + try { + ;({ stdout, stderr } = await execFileAsync( + process.execPath, + [CLI_PATH, ...args], + { encoding: 'utf8' } + )) + } catch (err) { + ;({ stdout, stderr, code } = /** + * @type {import('node:child_process').ExecFileException & { + * stdout: string + * stderr: string + * }} + */ (err)) + } + return { stdout, stderr, code } +} + +/** + * Macro to run the `endomoat` CLI with the provided arguments, and optionally + * perform assertions on the output. + * + * @todo Could create a factory function so it could be generic for any CLI + */ +const testCLI = test.macro( + /** + * @template [Ctx=unknown] Default is `unknown` + * @param {import('ava').ExecutionContext} t Exec context + * @param {string[]} args CLI arguments + * @param {ExecEndomoatExpectation} [expected] Expected output or + * callback + * @returns {Promise} + */ + async (t, args, expected) => { + await Promise.resolve() + + t.log(`executing: ${process.execPath} ${CLI_PATH} ${args.join(' ')}`) + + const { stdout, stderr, code } = await runCli(args) + + const trimmedStdout = stdout.trim().replace(ANSI_REGEX, '') + const trimmedStderr = stderr.trim().replace(ANSI_REGEX, '') + + switch (typeof expected) { + case 'string': + t.is(trimmedStdout, expected, 'stdout does not match expected value') + break + case 'function': + await expected(t, { + stdout: trimmedStdout, + stderr: trimmedStderr, + code, + }) + break + case 'object': + if ('stdout' in expected) { + t.is( + trimmedStdout, + expected.stdout, + 'stdout does not match expected value' + ) + } + if ('stderr' in expected) { + t.is( + trimmedStderr, + expected.stderr, + 'stderr does not match expected value' + ) + } + if ('code' in expected) { + t.is(code, expected.code, 'exit code does not match expected value') + } + break + default: + t.snapshot( + { trimmedStderr, trimmedStdout, code }, + 'execution output does not match expected snapshot' + ) + } + } +) + +test('--help prints help', testCLI, ['--help']) + +test('--version', testCLI, ['--version'], async (t, { stdout }) => { + const { version } = /** @type {import('type-fest').PackageJson} */ ( + await readJsonFile(new URL('../package.json', import.meta.url)) + ) + t.is(stdout, `${version}`) +}) + +test('run - --help prints help', testCLI, ['run', '--help']) + +test( + 'run - missing entrypoint', + testCLI, + ['run'], + async (t, { code, stderr, stdout }) => { + t.plan(3) + t.is(code, 1) + t.is(stdout, '') + t.regex(stderr, /Not enough non-option arguments: got 0, need at least 1/) + } +) + +test( + 'run - basic execution', + testCLI, + ['run', BASIC_ENTRYPOINT, '--cwd', BASIC_ENTRYPOINT_CWD], + 'hello world' +) diff --git a/packages/endomoat/test/fixture/basic/app.js b/packages/endomoat/test/fixture/basic/app.js new file mode 100644 index 0000000000..bc7bccb1b7 --- /dev/null +++ b/packages/endomoat/test/fixture/basic/app.js @@ -0,0 +1,3 @@ +export const hello = 'world' + +console.log(`hello ${hello}`) diff --git a/packages/endomoat/test/fixture/basic/lavamoat/node/policy.json b/packages/endomoat/test/fixture/basic/lavamoat/node/policy.json new file mode 100644 index 0000000000..b0369a8fbe --- /dev/null +++ b/packages/endomoat/test/fixture/basic/lavamoat/node/policy.json @@ -0,0 +1 @@ +{"resources": {}} \ No newline at end of file diff --git a/packages/endomoat/test/fixture/basic/package.json b/packages/endomoat/test/fixture/basic/package.json new file mode 100644 index 0000000000..dcbec9cd58 --- /dev/null +++ b/packages/endomoat/test/fixture/basic/package.json @@ -0,0 +1,8 @@ +{ + "name": "basic", + "version": "1.0.0", + "type": "module", + "license": "ISC", + "private": true, + "main": "app.js" +} diff --git a/packages/endomoat/test/fixture/no-deps/.npmrc b/packages/endomoat/test/fixture/no-deps/.npmrc deleted file mode 100644 index e60ae71529..0000000000 --- a/packages/endomoat/test/fixture/no-deps/.npmrc +++ /dev/null @@ -1 +0,0 @@ -ignore-scripts=false diff --git a/packages/endomoat/test/snapshots/cli.spec.js.md b/packages/endomoat/test/snapshots/cli.spec.js.md new file mode 100644 index 0000000000..09a36fdf24 --- /dev/null +++ b/packages/endomoat/test/snapshots/cli.spec.js.md @@ -0,0 +1,59 @@ +# Snapshot report for `test/cli.spec.js` + +The actual snapshot is saved in `cli.spec.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## --help prints help + +> execution output does not match expected snapshot + + { + code: undefined, + trimmedStderr: '', + trimmedStdout: `endomoat ␊ + ␊ + Run an application␊ + ␊ + Positionals:␊ + entrypoint Path to the application entry point [string] [required]␊ + ␊ + Options:␊ + --help Show help [boolean]␊ + --version Show version number [boolean]␊ + -p, --policy Filepath to a policy file␊ + [string] [default: "lavamoat/node/policy.json"]␊ + -o, --policy-override, --override Filepath to a policy override file␊ + [string] [default: "lavamoat/node/policy-override.json"]␊ + --policy-debug, --pd Filepath to a policy debug file␊ + [string] [default: "lavamoat/node/policy-debug.json"]␊ + --cwd Path to application root directory␊ + [string] [default: (current directory)]`, + } + +## run - --help prints help + +> execution output does not match expected snapshot + + { + code: undefined, + trimmedStderr: '', + trimmedStdout: `endomoat run ␊ + ␊ + Run an application␊ + ␊ + Positionals:␊ + entrypoint Path to the application entry point [string] [required]␊ + ␊ + Options:␊ + --help Show help [boolean]␊ + --version Show version number [boolean]␊ + -p, --policy Filepath to a policy file␊ + [string] [default: "lavamoat/node/policy.json"]␊ + -o, --policy-override, --override Filepath to a policy override file␊ + [string] [default: "lavamoat/node/policy-override.json"]␊ + --policy-debug, --pd Filepath to a policy debug file␊ + [string] [default: "lavamoat/node/policy-debug.json"]␊ + --cwd Path to application root directory␊ + [string] [default: (current directory)]`, + } diff --git a/packages/endomoat/test/snapshots/cli.spec.js.snap b/packages/endomoat/test/snapshots/cli.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..b13dfbb1470bb52f201f565631c558e8fa6ea46c GIT binary patch literal 564 zcmV-40?YkDRzVL94**J$i=n-+|1iI?`D_xdz@NQ-|}CN$c8P3B+1Zv z@WD9Fq0CM0q|YAf{dvLP48SCpGFlQut#0akc`E6Eu!0cW^=W`Thkw2EQQ z!MK|d32~Vn zlU{$$frvqyfQl*1x8LIinXgq50=hN2HzTw%MLTe(CA&NBb0LHz+<|E C@(&9D literal 0 HcmV?d00001