From 913fbc5fe5b1d349f972377ad592e9ea1360a912 Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:35:22 +0100 Subject: [PATCH 1/2] test: add interactive CLI tests --- .gitignore | 3 +- cli/npm-shrinkwrap.json | 205 +++++++++++++++++++++++-- cli/package.json | 6 +- cli/test/iapp.test.ts | 326 ++++++++++++++++++++++++++++++++++++++++ cli/test/test-utils.ts | 114 ++++++++++++++ cli/test/tsconfig.json | 8 + cli/tsconfig.base.json | 15 ++ cli/tsconfig.json | 14 +- 8 files changed, 666 insertions(+), 25 deletions(-) create mode 100644 cli/test/iapp.test.ts create mode 100644 cli/test/test-utils.ts create mode 100644 cli/test/tsconfig.json create mode 100644 cli/tsconfig.base.json diff --git a/.gitignore b/.gitignore index 75d279a9..8f5b09db 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ api/.env api/sig .tags -cli/dist \ No newline at end of file +cli/dist +cli/test/out \ No newline at end of file diff --git a/cli/npm-shrinkwrap.json b/cli/npm-shrinkwrap.json index 3331e109..c1a67e8b 100644 --- a/cli/npm-shrinkwrap.json +++ b/cli/npm-shrinkwrap.json @@ -38,9 +38,11 @@ "@types/debug": "^4.1.12", "@types/dockerode": "^3.3.37", "@types/figlet": "^1.7.0", + "@types/node": "^25.0.3", "@types/prompts": "^2.4.9", "@types/ws": "^8.18.1", "@types/yargs": "^17.0.33", + "cli-testing-library": "^3.0.1", "eslint": "^9.24.0", "eslint-plugin-unicorn": "^58.0.0", "globals": "^16.0.0", @@ -80,6 +82,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@balena/dockerignore": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", @@ -1249,11 +1261,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~7.16.0" } }, "node_modules/@types/normalize-package-data": { @@ -2122,6 +2135,75 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-testing-library": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cli-testing-library/-/cli-testing-library-3.0.1.tgz", + "integrity": "sha512-fkQ8D2hQS53RP3s0yuCMHmTfPUMEqtVtJG0rs13MNE2khnkSaY8MsNxN7rSJZAzOOVsSg2I2F2XjEITJwf5dFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "picocolors": "^1.1.1", + "redent": "^4.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "strip-final-newline": "^4.0.0", + "tree-kill": "^1.2.2" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/crutchcorn" + }, + "peerDependencies": { + "@jest/expect": "^29.0.0", + "@jest/globals": "^29.0.0", + "vitest": "^3.0.0" + }, + "peerDependenciesMeta": { + "@jest/expect": { + "optional": true + }, + "@jest/globals": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/cli-testing-library/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-testing-library/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -2722,6 +2804,21 @@ "node": ">=14.0.0" } }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, "node_modules/ethers/node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", @@ -2973,9 +3070,10 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -4648,6 +4746,23 @@ "node": ">= 6" } }, + "node_modules/redent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -4896,6 +5011,52 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -5050,6 +5211,19 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", @@ -5150,6 +5324,16 @@ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", "license": "MIT" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", @@ -5275,9 +5459,10 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.1.0", diff --git a/cli/package.json b/cli/package.json index 20689279..62daa800 100644 --- a/cli/package.json +++ b/cli/package.json @@ -13,7 +13,9 @@ "build:watch": "npm run build -- --watch", "check-format": "prettier --check .", "format": "prettier --write .", - "lint": "eslint src" + "lint": "eslint src", + "test:pre": "echo 'testing' && which iapp || (echo 'iapp CLI is not installed' && exit 1)", + "test": "npm run test:pre && node --test test/**/*.test.ts" }, "author": "iExec", "license": "Apache-2.0", @@ -60,9 +62,11 @@ "@types/debug": "^4.1.12", "@types/dockerode": "^3.3.37", "@types/figlet": "^1.7.0", + "@types/node": "^25.0.3", "@types/prompts": "^2.4.9", "@types/ws": "^8.18.1", "@types/yargs": "^17.0.33", + "cli-testing-library": "^3.0.1", "eslint": "^9.24.0", "eslint-plugin-unicorn": "^58.0.0", "globals": "^16.0.0", diff --git a/cli/test/iapp.test.ts b/cli/test/iapp.test.ts new file mode 100644 index 00000000..0d2e50bb --- /dev/null +++ b/cli/test/iapp.test.ts @@ -0,0 +1,326 @@ +import { test, beforeEach, after, afterEach, describe } from 'node:test'; +import { join } from 'node:path'; +import assert from 'node:assert/strict'; +import { render, cleanup } from 'cli-testing-library'; +import { + IAPP_COMMAND, + initIappProject, + checkDockerImageContent, + createTestDir, + removeTestDir, + retry, +} from './test-utils.ts'; + +// Final cleanup code after all tests +after(async () => { + await cleanup(); +}); + +/** + * test directory + */ +let testDir: string; + +beforeEach(async (t) => { + // create a unique test directory for each test + testDir = await createTestDir(t); +}); + +afterEach(async () => { + // remove test directory after each test + // comment the line below to keep the test directories for debugging + await removeTestDir(testDir); +}); + +test('iapp help command works', async () => { + const { findByText, debug, clear } = await render(IAPP_COMMAND, ['help'], { + cwd: testDir, + }); + await findByText('iapp [args]'); + // debug(); + clear(); +}); + +test('iapp -v command works', async () => { + const { findByText, debug, clear } = await render(IAPP_COMMAND, ['-v'], { + cwd: testDir, + }); + await findByText('1.3.3'); + // debug(); + clear(); +}); + +test('iapp init command works', async () => { + const { findByText, clear, debug, userEvent } = await render( + IAPP_COMMAND, + ['init'], + { + cwd: testDir, + } + ); + await findByText("What's your project name?"); + // debug(); + clear(); + userEvent.keyboard('[Enter]'); + await findByText('Which language do you want to use?'); + // debug(); + clear(); + userEvent.keyboard('[Enter]'); + await findByText('What kind of project do you want to init?'); + // debug(); + clear(); + userEvent.keyboard('[Enter]'); + await findByText('Steps to Get Started:'); + // debug(); + clear(); +}); + +describe('JavaScript iApp', () => { + describe('Hello World', () => { + describe('iapp test', () => { + const projectName = 'test-iapp'; + + // Initialize a test iApp project before each test + beforeEach(async () => { + await initIappProject({ + testDir, + projectName, + template: 'JavaScript', + projectType: 'Hello World', + }); + }); + + test('iapp test command works', async () => { + const { findByText, debug, clear, userEvent, getStdallStr } = + await render(IAPP_COMMAND, ['test'], { + cwd: join(testDir, projectName), + }); + // wait for docker build and test run + await retry(() => findByText('Would you like to see the app logs?'), { + retries: 8, + delay: 3000, + }); + // extract docker image id from stdout + const std = getStdallStr(); + const dockerImageIdMatch = std.match( + /App docker image built \(sha256:[a-f0-9]{64}\)/ + ); + assert.ok(dockerImageIdMatch, 'Docker image ID not found in output'); + const dockerImageId = dockerImageIdMatch![0].split('(')[1].slice(0, -1); + + // debug(); + clear(); + userEvent.keyboard('n'); + await findByText('Would you like to see the result?'); + // debug(); + clear(); + userEvent.keyboard('n'); + await findByText('When ready run iapp deploy'); + // debug(); + clear(); + + // check built docker image content + await checkDockerImageContent({ + dockerImageId, + expectedFiles: [ + 'Dockerfile', + 'README.md', + 'node_modules', + 'package-lock.json', + 'package.json', + 'src', + ], + }); + }); + }); + }); + + describe('Advanced', () => { + describe('iapp test', () => { + const projectName = 'test-iapp'; + + // Initialize a test iApp project before each test + beforeEach(async () => { + await initIappProject({ + testDir, + projectName, + template: 'JavaScript', + projectType: 'Advanced', + }); + }); + + test('iapp test command works', async () => { + const { findByText, debug, clear, userEvent, getStdallStr } = + await render(IAPP_COMMAND, ['test'], { + cwd: join(testDir, projectName), + }); + await findByText('Do you want to attach an app secret to your iApp?'); + userEvent.keyboard('y'); + // debug() + clear(); + await findByText('What is the app secret?'); + userEvent.keyboard('mySuperSecretAppSecret[Enter]'); + // debug() + clear(); + await findByText('Do you want to save this app secret to your config?'); + userEvent.keyboard('y'); + // debug() + clear(); + // wait for docker build and test run + await retry(() => findByText('Would you like to see the app logs?'), { + retries: 8, + delay: 3000, + }); + // extract docker image id from stdout + const std = getStdallStr(); + const dockerImageIdMatch = std.match( + /App docker image built \(sha256:[a-f0-9]{64}\)/ + ); + assert.ok(dockerImageIdMatch, 'Docker image ID not found in output'); + const dockerImageId = dockerImageIdMatch![0].split('(')[1].slice(0, -1); + + // debug(); + clear(); + userEvent.keyboard('n'); + await findByText('Would you like to see the result?'); + // debug(); + clear(); + userEvent.keyboard('n'); + await findByText('When ready run iapp deploy'); + // debug(); + clear(); + + // check built docker image content + await checkDockerImageContent({ + dockerImageId, + expectedFiles: [ + 'Dockerfile', + 'README.md', + 'node_modules', + 'package-lock.json', + 'package.json', + 'src', + ], + }); + }); + }); + }); +}); + +describe('Python iApp', () => { + describe('Hello World', () => { + describe('iapp test', () => { + const projectName = 'test-iapp'; + + // Initialize a test iApp project before each test + beforeEach(async () => { + await initIappProject({ + testDir, + projectName, + template: 'Python3.13', + projectType: 'Hello World', + }); + }); + + test('iapp test command works', async () => { + const { findByText, debug, clear, userEvent, getStdallStr } = + await render(IAPP_COMMAND, ['test'], { + cwd: join(testDir, projectName), + }); + // wait for docker build and test run + await retry(() => findByText('Would you like to see the app logs?'), { + retries: 8, + delay: 3000, + }); + // extract docker image id from stdout + const std = getStdallStr(); + const dockerImageIdMatch = std.match( + /App docker image built \(sha256:[a-f0-9]{64}\)/ + ); + assert.ok(dockerImageIdMatch, 'Docker image ID not found in output'); + const dockerImageId = dockerImageIdMatch![0].split('(')[1].slice(0, -1); + + // debug(); + clear(); + userEvent.keyboard('n'); + await findByText('Would you like to see the result?'); + // debug(); + clear(); + userEvent.keyboard('n'); + await findByText('When ready run iapp deploy'); + // debug(); + clear(); + + // check built docker image content + await checkDockerImageContent({ + dockerImageId, + expectedFiles: ['Dockerfile', 'README.md', 'requirements.txt', 'src'], + }); + }); + }); + }); + + describe('Advanced', () => { + describe('iapp test', () => { + const projectName = 'test-iapp'; + + // Initialize a test iApp project before each test + beforeEach(async () => { + await initIappProject({ + testDir, + projectName, + template: 'Python3.13', + projectType: 'Advanced', + }); + }); + + test('iapp test command works', async () => { + const { findByText, debug, clear, userEvent, getStdallStr } = + await render(IAPP_COMMAND, ['test'], { + cwd: join(testDir, projectName), + }); + await findByText('Do you want to attach an app secret to your iApp?'); + userEvent.keyboard('y'); + // debug() + clear(); + await findByText('What is the app secret?'); + userEvent.keyboard('mySuperSecretAppSecret[Enter]'); + // debug() + clear(); + await findByText('Do you want to save this app secret to your config?'); + userEvent.keyboard('y'); + // debug() + clear(); + // wait for docker build and test run + await retry(() => findByText('Would you like to see the app logs?'), { + retries: 8, + delay: 3000, + }); + // extract docker image id from stdout + const std = getStdallStr(); + const dockerImageIdMatch = std.match( + /App docker image built \(sha256:[a-f0-9]{64}\)/ + ); + assert.ok(dockerImageIdMatch, 'Docker image ID not found in output'); + const dockerImageId = dockerImageIdMatch![0].split('(')[1].slice(0, -1); + + // debug(); + clear(); + userEvent.keyboard('n'); + await findByText('Would you like to see the result?'); + // debug(); + clear(); + userEvent.keyboard('n'); + await findByText('When ready run iapp deploy'); + // debug(); + clear(); + + // check built docker image content + await checkDockerImageContent({ + dockerImageId, + expectedFiles: ['Dockerfile', 'README.md', 'requirements.txt', 'src'], + }); + }); + }); + }); +}); diff --git a/cli/test/test-utils.ts b/cli/test/test-utils.ts new file mode 100644 index 00000000..27bd41bc --- /dev/null +++ b/cli/test/test-utils.ts @@ -0,0 +1,114 @@ +import { render } from 'cli-testing-library'; +import assert from 'node:assert'; +import { mkdir, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import type { SuiteContext, TestContext } from 'node:test'; + +export const IAPP_COMMAND = 'iapp'; + +export const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +const date = Date.now(); +let testIndex = 0; + +export const createTestDir = async (t: TestContext | SuiteContext) => { + // create a unique test directory for each test + const testDir = join( + process.cwd(), + 'test', + 'out', + `${date} ${testIndex++} ${t.name}` + ); + await mkdir(testDir, { recursive: true }); + return testDir; +}; + +export const removeTestDir = async (testDir: string) => { + await rm(testDir, { recursive: true, force: true }); +}; + +export const retry = ( + fn: () => Promise, + { retries = 3, delay = 1000 }: { retries?: number; delay?: number } = {} +): Promise => { + return fn().catch((error) => { + if (retries <= 0) { + throw error; + } + return sleep(delay).then(() => retry(fn, { retries: retries - 1, delay })); + }); +}; + +export const initIappProject = async ({ + testDir, + projectName, + template = 'JavaScript', + projectType = 'Hello World', +}: { + testDir: string; + projectName: string; + template: 'JavaScript' | 'Python3.13'; + projectType: 'Hello World' | 'Advanced'; +}) => { + const { findByText, userEvent } = await render(IAPP_COMMAND, ['init'], { + cwd: testDir, + }); + await findByText("What's your project name?"); + userEvent.keyboard(`${projectName}[Enter]`); + await findByText('Which language do you want to use?'); + if (template === 'JavaScript') { + userEvent.keyboard('[Enter]'); + } else if (template === 'Python3.13') { + userEvent.keyboard('[ArrowDown]'); + userEvent.keyboard('[Enter]'); + } + await findByText('What kind of project do you want to init?'); + if (projectType === 'Hello World') { + userEvent.keyboard('[Enter]'); + } else if (projectType === 'Advanced') { + userEvent.keyboard('[ArrowDown]'); + userEvent.keyboard('[Enter]'); + await findByText('Would you like to use args inside your iApp?'); + userEvent.keyboard('y'); + await findByText('Would you like to use input files inside your iApp?'); + userEvent.keyboard('y'); + await findByText( + 'Would you like to use requester secrets inside your iApp?' + ); + userEvent.keyboard('y'); + await findByText('Would you like to use protected data inside your iApp?'); + userEvent.keyboard('[ArrowDown]'); + // userEvent.keyboard('[ArrowDown]'); // to select bulk + userEvent.keyboard('[Enter]'); + await findByText('Would you like to use an app secret inside your iApp?'); + userEvent.keyboard('y'); + } + await findByText('Steps to Get Started:'); +}; + +export const checkDockerImageContent = async ({ + dockerImageId, + expectedFiles, +}: { + dockerImageId: string; + expectedFiles: string[]; +}) => { + const { findByText, getStdallStr } = await render('docker', [ + 'run', + '--rm', + '--entrypoint=ls', + dockerImageId, + ]); + await findByText(expectedFiles[expectedFiles.length - 1]); // wait for latest expected file + const output = getStdallStr(); + const expectedOutput = expectedFiles.join('\n'); + assert( + output === expectedOutput, + `Docker image content does not match expected files + - Expected: +${expectedOutput} + - Got: +${output}` + ); +}; diff --git a/cli/test/tsconfig.json b/cli/test/tsconfig.json new file mode 100644 index 00000000..9098a84a --- /dev/null +++ b/cli/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["**/*"], + "compilerOptions": { + "allowImportingTsExtensions": true, + "noEmit": true + } +} diff --git a/cli/tsconfig.base.json b/cli/tsconfig.base.json new file mode 100644 index 00000000..31ffe8ec --- /dev/null +++ b/cli/tsconfig.base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "node16", + "target": "ES2022", + "declaration": true, + "sourceMap": true, + "outDir": "dist", + "esModuleInterop": true, + "moduleResolution": "nodenext", + "skipLibCheck": true, + "allowJs": false, + "strict": true, + "noImplicitAny": true + } +} diff --git a/cli/tsconfig.json b/cli/tsconfig.json index 1217f0ae..b8fb364b 100644 --- a/cli/tsconfig.json +++ b/cli/tsconfig.json @@ -1,17 +1,5 @@ { - "compilerOptions": { - "module": "node16", - "target": "ES2022", - "declaration": true, - "sourceMap": true, - "outDir": "dist", - "esModuleInterop": true, - "moduleResolution": "nodenext", - "skipLibCheck": true, - "allowJs": false, - "strict": true, - "noImplicitAny": true - }, + "extends": "./tsconfig.base.json", "include": ["src/**/*"], "exclude": ["node_modules", "templates"] } From 628132a31fc3e28dbfac3dc01d3cd4a85113d081 Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:35:44 +0100 Subject: [PATCH 2/2] ci: run CLI tests in CI --- .github/workflows/cli-pr-checks.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cli-pr-checks.yml b/.github/workflows/cli-pr-checks.yml index 24ba260d..f63de681 100644 --- a/.github/workflows/cli-pr-checks.yml +++ b/.github/workflows/cli-pr-checks.yml @@ -1,6 +1,4 @@ name: CLI PR checks -description: check the CLI PR - on: pull_request: paths: ['cli/**'] @@ -10,7 +8,7 @@ concurrency: cancel-in-progress: true jobs: - check-code: + test: runs-on: ubuntu-latest steps: @@ -19,7 +17,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'npm' cache-dependency-path: 'cli/npm-shrinkwrap.json' @@ -39,12 +37,17 @@ jobs: working-directory: cli run: npm run lint - - name: Test no crash + - name: Install iapp cli working-directory: cli run: | npm i -g . iapp -h + - name: Test + working-directory: cli + run: | + npm run test + npm-dry-run: uses: ./.github/workflows/reusable-cli-npm.yml with: