actionhero
Version:
The reusable, scalable, and quick node.js API server for stateless and stateful applications
350 lines (308 loc) • 11.6 kB
text/typescript
// 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')
});
}
});