UNPKG

actionhero

Version:

The reusable, scalable, and quick node.js API server for stateless and stateful applications

350 lines (308 loc) 11.6 kB
// Note: These tests will only run on *nix operating systems // You can use SKIP_CLI_TEST_SETUP=true to skip the setup portion of these tests if you are testing this file repeatedly import * as fs from "fs"; import * as path from "path"; import { spawn } from "child_process"; import axios, { AxiosError } from "axios"; import { isRunning } from "../../src/modules/utils/isRunning"; import { sleep } from "../../src/modules/utils/sleep"; const testDir = path.join(process.cwd(), "tmp", "actionheroTestProject"); const binary = "./node_modules/.bin/actionhero"; console.log(`testDir: ${testDir}`); const port = 18080 + parseInt(process.env.JEST_WORKER_ID || "0"); const host = "localhost"; let pid: number; let AHPath: string; class ErrorWithStd extends Error { stderr: string; stdout: string; pid: number; exitCode: number; } const doCommand = async ( command: string, useCwd = true, extraEnv = {}, ): Promise<{ stderr: string; stdout: string; pid: number; exitCode: number; }> => { return new Promise((resolve, reject) => { const parts = command.split(" "); const bin = parts.shift() as string; const args = parts; let stdout = ""; let stderr = ""; // we don't want the CLI commands to source typescript files // when running jest, it will reset NODE_ENV=test // but sometimes we do /shrug/ const env = Object.assign({ ...process.env }, extraEnv); delete env.NODE_ENV; delete env.TS_JEST; const cmd = spawn(bin, args, { cwd: useCwd ? testDir : __dirname, env: env, }); cmd.stdout.on("data", (data) => { stdout += data.toString(); }); cmd.stderr.on("data", (data) => { stderr += data.toString(); }); pid = cmd.pid ?? -1; cmd.on("close", (exitCode) => { if (stderr.length > 0 || exitCode !== 0) { const error = new ErrorWithStd(stderr); error.stderr = stderr; error.stdout = stdout; error.pid = pid; error.exitCode = exitCode ?? -1; return reject(error); } return resolve({ stderr, stdout, pid, exitCode }); }); }); }; describe("Core: CLI", () => { if (process.platform === "win32") { console.log("*** CANNOT RUN CLI TESTS ON WINDOWS. Sorry. ***"); } else { beforeAll(async () => { if (process.env.SKIP_CLI_TEST_SETUP === "true") { return; } const sourcePackage = path.normalize( path.join(__dirname, "/../../templates/package.json.template"), ); AHPath = path.normalize(path.join(__dirname, "/../..")); await doCommand(`rm -rf ${testDir}`, false); await doCommand(`mkdir -p ${testDir}`, false); await doCommand(`cp ${sourcePackage} ${testDir}/package.json`); const data = fs.readFileSync(testDir + "/package.json").toString(); const result = data.replace(/%%versionNumber%%/g, `file:${AHPath}`); fs.writeFileSync(`${testDir}/package.json`, result); }); test("should have made the test dir", () => { expect(fs.existsSync(testDir)).toEqual(true); expect(fs.existsSync(testDir + "/package.json")).toEqual(true); }); test("can call npm install in the new project", async () => { try { await doCommand("npm install --ignore-scripts"); } catch (error) { // we might get warnings about package.json locks, etc. we want to ignore them if (error.toString().indexOf("npm") < 0) { throw error; } expect(error.exitCode).toEqual(0); } }, 120000); test("can generate a new project", async () => { const { stdout } = await doCommand(`${binary} generate`); expect(stdout).toMatch("❤️ the Actionhero Team"); [ "tsconfig.json", "src/server.ts", "src/actions", "src/tasks", "src/initializers", "src/servers", "src/bin", "src/actions/swagger.ts", "src/actions/status.ts", "src/config", "src/config/api.ts", "src/config/errors.ts", "src/config/plugins.ts", "src/config/logger.ts", "src/config/redis.ts", "src/config/routes.ts", "src/config/tasks.ts", "src/config/web.ts", "src/config/websocket.ts", "pids", "log", "public", "public/index.html", "public/chat.html", "public/swagger.html", "public/css/cosmo.css", "public/javascript", "public/logo/actionhero.png", "__tests__", "__tests__/actions/status.ts", ".gitignore", ].forEach((f) => { expect(fs.existsSync(testDir + "/" + f)).toEqual(true); }); }, 20000); test("the project can be compiled", async () => { const { stdout } = await doCommand(`npm run build`); expect(stdout).toMatch("tsc"); }, 20000); test("can call the help command", async () => { const { stdout } = await doCommand(`${binary} help`); expect(stdout).toMatch(/generate-action/); expect(stdout).toMatch(/Usage: actionhero \[options\] \[command\]/); expect(stdout).toMatch(/generate-server/); expect(stdout).toMatch(/generate-server \[options\]/); }, 20000); test("can call the version command (after generate)", async () => { const { stdout } = await doCommand(`${binary} --version`); expect(stdout).toContain("0.1.0"); // this project's version }, 20000); test("will show a warning with bogus input", async () => { try { await doCommand(`${binary} not-a-thing`); throw new Error("should not get here"); } catch (error) { expect(error).toBeTruthy(); expect(error.exitCode).toEqual(1); expect(error.stderr).toMatch(/unknown command 'not-a-thing'/); } }, 20000); describe("generating files", () => { afterAll(() => { const files = [ `${testDir}/src/actions/myAction.ts`, `${testDir}/__tests__/actions/myAction.ts`, `${testDir}/src/tasks/myTask.ts`, `${testDir}/__tests__/tasks/myTask.ts`, `${testDir}/src/bin/myCommand.ts`, `${testDir}/src/servers/myServer.ts`, `${testDir}/src/initializers/myInitializer.ts`, ]; files.forEach((f) => { if (fs.existsSync(f)) { fs.unlinkSync(f); } }); }); test("can generate an action", async () => { await doCommand( `${binary} generate-action --name=myAction --description=my_description`, ); const actionData = String( fs.readFileSync(`${testDir}/src/actions/myAction.ts`), ); expect(actionData).toMatch(/export class MyAction extends Action/); expect(actionData).toMatch(/this.name = "myAction"/); const testData = String( fs.readFileSync(`${testDir}/__tests__/actions/myAction.ts`), ); expect(testData).toMatch('describe("Action: myAction"'); }, 20000); test("can generate a task", async () => { await doCommand( `${binary} generate-task --name=myTask --description=my_description --queue=my_queue --frequency=12345`, ); const taskData = String( fs.readFileSync(`${testDir}/src/tasks/myTask.ts`), ); expect(taskData).toMatch(/export class MyTask extends Task/); expect(taskData).toMatch(/this.name = "myTask"/); expect(taskData).toMatch(/this.queue = "my_queue"/); expect(taskData).toMatch(/this.frequency = 12345/); const testData = String( fs.readFileSync(`${testDir}/__tests__/tasks/myTask.ts`), ); expect(testData).toMatch('describe("Task: myTask"'); }, 20000); test("can generate a CLI command", async () => { await doCommand( `${binary} generate-cli --name=myCommand --description=my_description --example=my_example`, ); const data = String(fs.readFileSync(`${testDir}/src/bin/myCommand.ts`)); expect(data).toMatch(/this.name = "myCommand"/); expect(data).toMatch(/this.example = "my_example"/); }, 20000); test("can generate a server", async () => { await doCommand(`${binary} generate-server --name=myServer`); const data = String( fs.readFileSync(`${testDir}/src/servers/myServer.ts`), ); expect(data).toMatch(/this.type = "myServer"/); expect(data).toMatch(/canChat: false/); expect(data).toMatch(/logConnections: true/); expect(data).toMatch(/logExits: true/); expect(data).toMatch(/sendWelcomeMessage: false/); }, 20000); test("can generate an initializer", async () => { await doCommand( `${binary} generate-initializer --name=myInitializer --stopPriority=123`, ); const data = String( fs.readFileSync(`${testDir}/src/initializers/myInitializer.ts`), ); expect(data).toMatch(/this.loadPriority = 1000/); expect(data).toMatch(/this.startPriority = 1000/); expect(data).toMatch(/this.stopPriority = 123/); expect(data).toMatch(/async initialize\(\) {/); expect(data).toMatch(/async start\(\) {/); expect(data).toMatch(/async stop\(\) {/); }, 20000); }); test("can call npm test in the new project and not fail", async () => { // since prettier no longer works with node < 10, we need to skip this test const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)![1]); if (nodeVersion < 10) { console.log( `skipping 'npm test' because this node version ${nodeVersion} < 10.0.0`, ); return; } // jest writes to stderr for some reason, so we need to test for the exit code here try { await doCommand("npm test", true, { NODE_ENV: "test" }); } catch (error) { if (error.exitCode !== 0) { throw error; } } }, 120000); describe("can run the server", () => { let serverPid: number; beforeAll(async function () { doCommand(`node dist/server.js`, true, { PORT: port }); await sleep(5000); serverPid = pid; }, 20000); afterAll(async () => { if (isRunning(serverPid)) { await doCommand(`kill ${serverPid}`); } }); test("can boot the server", async () => { const response = await axios(`http://${host}:${port}/api/status`); expect(response.data.serverInformation.serverName).toEqual( "my_actionhero_project", ); }); test("can handle signals to reboot", async () => { await doCommand(`kill -s USR2 ${serverPid}`); await sleep(3000); const response = await axios(`http://${host}:${port}/api/status`); expect(response.data.serverInformation.serverName).toEqual( "my_actionhero_project", ); }, 5000); test("can handle signals to stop", async () => { await doCommand(`kill ${serverPid}`); await sleep(1000); try { await axios.get(`http://${host}:${port}/api/status`); throw new Error("should not get here"); } catch (error) { if (error instanceof AxiosError) { expect(error.toString()).toMatch( /ECONNREFUSED|ECONNRESET|RequestError|AggregateError/, ); } else throw error; } }); // test('will shutdown after the alloted time') }); } });