/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ import { execAsync } from "./exec_async.js"; import { spawn } from "child_process"; import getPort from "get-port"; import waitPort from "wait-port"; import { promises as fs } from "fs"; import path from "node:path"; import chalk from "chalk"; import _locreq from "locreq"; import { module_dirname } from "../utils/module-dirname.js"; import EventEmitter from "events"; const locreq = _locreq(module_dirname(import.meta.url)); export const callSealgenBin = () => `node ${locreq.resolve("lib/cli.js")}`; const sealious_minimal_url = "https://hub.sealcode.org/source/sealious-minimal.git"; const sealious_minimal_commit = "77e3794"; export class RealAppTest { public node_pid: number; public app_port: number; public mongo_port: number; public app_path: string; public container_name: string; public emitter = new EventEmitter(); static async init(): Promise { const test_app = new RealAppTest(); test_app.node_pid = 0; const timestamp = Date.now(); test_app.app_path = `/tmp/sealious-minimal-${timestamp}`; test_app.container_name = `sealgen-test-mongo-${timestamp}`; console.info("\tcloning sealious-minimal..."); await execAsync( `git clone ${sealious_minimal_url} ${test_app.app_path} && cd ${test_app.app_path} && git checkout ${sealious_minimal_commit}` ); console.info("\tinstalling dependencies and sealgen..."); await execAsync( `cd ${ test_app.app_path } && npm ci rm -rf node_modules/@sealcode/sealgen && ln -s $OLDPWD node_modules/@sealcode/sealgen && ${callSealgenBin()} make-env` ); console.info( "\tstarting the database...(if timeouts here, run 'docker pull mongo:4.4-bionic' to solve)" ); await test_app.runSealgenCommand("build"); test_app.app_port = await getPort(); return test_app; } async start() { this.mongo_port = await getPort(); await execAsync( `docker run -p ${this.mongo_port}:27017 --name ${this.container_name} -d mongo:4.4-bionic` ); const env = { ...process.env, SEALIOUS_MONGO_PORT: this.mongo_port.toString(), SEALIOUS_PORT: this.app_port.toString(), }; const node_args = [`${this.app_path}/dist/back/index.js`]; console.info( "EXECUTING APP", `${Object.entries(env) .map(([key, value]) => `${key}=${value}`) .join(" ")} node ${node_args.join(" ")}` ); const app_start = spawn(`node`, node_args, { env, }); app_start.stdout.on("data", (data) => console.info("APP OUTPUT:", data.toString()) ); app_start.stderr.on("data", (data) => console.info(data.toString())); app_start.on("exit", (code) => { this.emitter.emit("exit", code); this.emitter.emit("process_error"); }); if (app_start.pid == undefined) throw new Error("node failed to start"); this.node_pid = app_start.pid; await waitPort({ host: "localhost", port: this.app_port, output: "silent", timeout: 6000, }); console.info( chalk.yellow( `App is listening on http://localhost:${this.app_port}` ) ); } async close() { try { if (this.node_pid != 0) await execAsync(`kill -9 ${this.node_pid}`); } catch (_error) { throw new Error( "node process exited unexpectedly, please inspect the output" ); } await Promise.all([ fs.rm(this.app_path, { recursive: true, force: true }), execAsync(`docker rm -f ${this.container_name}`), ]); } async runSealgenCommand(command: string) { await execAsync( `cd ${ this.app_path } && ${callSealgenBin()} ${command} && ${callSealgenBin()} build` ); await this.build(); } async build() { await execAsync(`cd ${this.app_path} && ${callSealgenBin()} build`); } async throwIfItKillsTheApp(cb: () => Promise): Promise { let finished = false; // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor return new Promise(async (resolve, reject) => { const error_handler = () => { if (!finished) { reject(); } finished = true; }; this.emitter.addListener("process_error", error_handler); try { const result = await cb(); if (!finished) { resolve(result); finished = true; } } catch (e) { if (!finished) { reject(e); finished = true; } } this.emitter.removeListener("process_error", error_handler); }); } async httpGET(path: string): Promise<{ status: number; response: string }> { return this.throwIfItKillsTheApp(async () => { const ret = await fetch(`http://localhost:${this.app_port}${path}`); const status = ret.status; const response = await ret.text(); return { status, response }; }); } async httpPOST( path: string, body: Record ): Promise<{ status: number; response: string }> { return this.throwIfItKillsTheApp(async () => { const ret = await fetch( `http://localhost:${this.app_port}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), } ); const status = ret.status; const response = await ret.text(); return { status, response }; }); } async addFile(target_path: string, content: string) { console.info( chalk.yellow("Inserting file at path"), chalk.green(target_path), "with content\n", content, "\n\n\n\n=====\n" ); await fs.writeFile(path.resolve(this.app_path, target_path), content); await this.build(); } fullURL(path: string): string { return `http://localhost:${this.app_port}${ path.startsWith("/") ? "" : "/" }${path}`; } }