aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKevin J Hoerr <kjhoerr@protonmail.com>2020-04-26 21:53:26 -0400
committerKevin J Hoerr <kjhoerr@protonmail.com>2020-04-26 21:53:26 -0400
commitddecabba54eb24ab4ac07a67621f805d3ad9e2ce (patch)
tree7422ecca20f3a66ce1e6bc2839db454eacc1fbd2 /src
parentee068fcbf0f409ac91d6559438e84fb89e654a15 (diff)
downloadao-coverage-ddecabba54eb24ab4ac07a67621f805d3ad9e2ce.tar.gz
ao-coverage-ddecabba54eb24ab4ac07a67621f805d3ad9e2ce.tar.bz2
ao-coverage-ddecabba54eb24ab4ac07a67621f805d3ad9e2ce.zip
These changes moved a lot of the startup async to run in a streamlined
async init function. This brings more logic "to light", so it should probably have unit tests added to check the edge cases. As a bonus, no async runs as a result of route initialization. Speaking of routes, it might be nice to trim down the route calls themselves with async functions, if possible. The upload routes in particular use a lot of async. Just a note for the future.
Diffstat (limited to 'src')
-rw-r--r--src/formats.ts10
-rw-r--r--src/index.ts82
-rw-r--r--src/routes.test.ts217
-rw-r--r--src/routes.ts58
-rw-r--r--src/util/config.test.ts10
-rw-r--r--src/util/config.ts75
6 files changed, 302 insertions, 150 deletions
diff --git a/src/formats.ts b/src/formats.ts
index d22ea3e..7638399 100644
--- a/src/formats.ts
+++ b/src/formats.ts
@@ -32,12 +32,12 @@ export const defaultColorMatches = (
coverage >= style.stage1
? 76
: coverage >= style.stage2
- ? Math.floor(
+ ? Math.floor(
((style.stage1 - coverage) / (style.stage1 - style.stage2)) * 10
) *
- 16 +
+ 16 +
76
- : 225 + Math.floor(coverage / (style.stage2 / 11));
+ : 225 + Math.floor(coverage / (style.stage2 / 11));
const result = gradient.toString(16);
return (result.length === 1 ? "0" : "") + result + "1";
};
@@ -75,11 +75,11 @@ const FormatsObj: FormatObj = {
}
},
- listFormats: function () {
+ listFormats: function() {
return Object.keys(this.formats);
},
- getFormat: function (format: string) {
+ getFormat: function(format: string) {
return this.formats[format];
}
};
diff --git a/src/index.ts b/src/index.ts
index 76620e8..e4f8db1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,9 +1,5 @@
import dotenv from "dotenv";
import express from "express";
-import { MongoClient } from "mongodb";
-import path from "path";
-import fs from "fs";
-
import winston from "winston";
import expressWinston from "express-winston";
@@ -12,57 +8,41 @@ dotenv.config();
import routes from "./routes";
import Metadata from "./metadata";
import loggerConfig from "./util/logger";
-import { configOrError, handleShutdown } from "./util/config";
+import { handleStartup, handleShutdown } from "./util/config";
// Start-up configuration
const BIND_ADDRESS = process.env.BIND_ADDRESS ?? "localhost";
+const MONGO_DB = process.env.MONGO_DB ?? "ao-coverage";
const PORT = Number(process.env.PORT ?? 3000);
const logger = winston.createLogger(loggerConfig("ROOT"));
-const MONGO_URI = configOrError("MONGO_URI");
-const MONGO_DB = process.env.MONGO_DB ?? "ao-coverage";
-const HOST_DIR = configOrError("HOST_DIR");
-
-fs.accessSync(HOST_DIR, fs.constants.R_OK | fs.constants.W_OK);
-if (!path.isAbsolute(HOST_DIR)) {
- logger.error("HOST_DIR must be an absolute path");
- process.exit(1);
-}
-
-new MongoClient(MONGO_URI, { useUnifiedTopology: true }).connect(
- (err, mongo) => {
- if (err !== null) {
- logger.error(err ?? "Unable to connect to database");
- process.exit(1);
- }
-
- const app: express.Application = express();
- const metadata = new Metadata(mongo.db(MONGO_DB));
-
- app.use(
- expressWinston.logger({
- ...loggerConfig("HTTP"),
- colorize: true,
- // filter out token query param from URL
- msg:
- '{{req.method}} {{req.url.replace(/token=[-\\w.~]*(&*)/, "token=$1")}} - {{res.statusCode}} {{res.responseTime}}ms'
- })
- );
-
- // actual app routes
- app.use(routes(metadata));
-
- app.use(expressWinston.errorLogger(loggerConfig("_ERR")));
-
- const server = app.listen(PORT, BIND_ADDRESS, () => {
- logger.info("Express has started: http://%s:%d/", BIND_ADDRESS, PORT);
- });
-
- // application exit handling
- const signalCodes: NodeJS.Signals[] = ["SIGTERM", "SIGHUP", "SIGINT"];
- signalCodes.map((code: NodeJS.Signals) => {
- process.on(code, handleShutdown(mongo, server));
- });
- }
-);
+handleStartup().then(mongo => {
+ const app: express.Application = express();
+ const metadata = new Metadata(mongo.db(MONGO_DB));
+
+ app.use(
+ expressWinston.logger({
+ ...loggerConfig("HTTP"),
+ colorize: true,
+ // filter out token query param from URL
+ msg:
+ '{{req.method}} {{req.url.replace(/token=[-\\w.~]*(&*)/, "token=$1")}} - {{res.statusCode}} {{res.responseTime}}ms'
+ })
+ );
+
+ // actual app routes
+ app.use(routes(metadata));
+
+ app.use(expressWinston.errorLogger(loggerConfig("_ERR")));
+
+ const server = app.listen(PORT, BIND_ADDRESS, () => {
+ logger.info("Express has started: http://%s:%d/", BIND_ADDRESS, PORT);
+ });
+
+ // application exit handling
+ const signalCodes: NodeJS.Signals[] = ["SIGTERM", "SIGHUP", "SIGINT"];
+ signalCodes.map((code: NodeJS.Signals) => {
+ process.on(code, handleShutdown(mongo, server));
+ });
+});
diff --git a/src/routes.test.ts b/src/routes.test.ts
index b4979d6..3579011 100644
--- a/src/routes.test.ts
+++ b/src/routes.test.ts
@@ -1,4 +1,4 @@
-import _request from "supertest";
+import _request, { SuperTest, Test } from "supertest";
import express from "express";
import dotenv from "dotenv";
import fs from "fs";
@@ -8,9 +8,10 @@ dotenv.config();
process.env.UPLOAD_LIMIT = "40000";
-import { configOrError } from "./util/config";
+import { configOrError, persistTemplate } from "./util/config";
import routes from "./routes";
import Metadata from "./metadata";
+import { Template } from "./templates";
import { Db } from "mongodb";
import { badgen } from "badgen";
import { BranchNotFoundError } from "./errors";
@@ -22,28 +23,44 @@ type MetadataMockType = {
createRepository: jest.Mock;
};
-const mock = (headCommit: jest.Mock = jest.fn(() => new Promise(solv => solv("testcommit"))), updateBranch: jest.Mock = jest.fn(() => new Promise(solv => solv(true)))): MetadataMockType => ({
+const mock = (
+ headCommit: jest.Mock = jest.fn(
+ () => new Promise(solv => solv("testcommit"))
+ ),
+ updateBranch: jest.Mock = jest.fn(() => new Promise(solv => solv(true)))
+): MetadataMockType => ({
database: {} as Db,
getHeadCommit: headCommit,
updateBranch: updateBranch,
createRepository: jest.fn()
});
-const request = (mockMeta: MetadataMockType = mock()) => {
+const request = (mockMeta: MetadataMockType = mock()): SuperTest<Test> => {
const app = express();
app.use(routes(mockMeta as Metadata));
return _request(app);
-}
+};
const HOST_DIR = configOrError("HOST_DIR");
const TARGET_URL = process.env.TARGET_URL ?? "http://localhost:3000";
const TOKEN = process.env.TOKEN ?? "";
describe("templates", () => {
-
describe("GET /bash", () => {
- it("should return the bash file containing tbe curl command", async () => {
+ it("should return the bash file containing the curl command", async () => {
+ await persistTemplate({
+ inputFile: path.join(
+ __dirname,
+ "..",
+ "public",
+ "templates",
+ "bash.template"
+ ),
+ outputFile: path.join(HOST_DIR, "bash"),
+ context: { TARGET_URL }
+ } as Template);
+
const res = await request()
.get("/bash")
.expect(200);
@@ -54,20 +71,69 @@ describe("templates", () => {
describe("GET /", () => {
it("should return the index HTML file containing the bash command", async () => {
+ await persistTemplate({
+ inputFile: path.join(
+ __dirname,
+ "..",
+ "public",
+ "templates",
+ "index.html.template"
+ ),
+ outputFile: path.join(HOST_DIR, "index.html"),
+ context: { TARGET_URL }
+ } as Template);
+
const res = await request()
.get("/")
.expect("Content-Type", /html/)
.expect(200);
expect(res.text).toMatch(`bash &lt;(curl -s ${TARGET_URL}/bash)`);
- })
+ });
});
});
-describe("Badges and reports", () => {
+describe("Static files", () => {
+ const staticRoot = path.join(__dirname, "..", "public", "static");
+
+ it("should return favicon.ico at GET /favicon.ico", async () => {
+ const buffer = await fs.promises.readFile(
+ path.join(staticRoot, "favicon.ico")
+ );
+
+ await request()
+ .get("/favicon.ico")
+ .expect("Content-Type", /icon/)
+ .expect(buffer)
+ .expect(200);
+ });
+
+ it("should return index.css at GET /static/index.css", async () => {
+ const buffer = await fs.promises.readFile(
+ path.join(staticRoot, "index.css")
+ );
+
+ const res = await request()
+ .get("/static/index.css")
+ .expect(200);
+ expect(res.text).toEqual(buffer.toString("utf-8"));
+ });
+});
- const report_path = path.join(HOST_DIR, "testorg", "testrepo", "testbranch", "testcommit");
- const actual_report = path.join(__dirname, "..", "example_reports", "tarpaulin-report.html");
- const fake_badge = badgen({
+describe("Badges and reports", () => {
+ const reportPath = path.join(
+ HOST_DIR,
+ "testorg",
+ "testrepo",
+ "testbranch",
+ "testcommit"
+ );
+ const actualReport = path.join(
+ __dirname,
+ "..",
+ "example_reports",
+ "tarpaulin-report.html"
+ );
+ const fakeBadge = badgen({
label: "coverage",
status: "120%",
color: "#E1C"
@@ -75,9 +141,12 @@ describe("Badges and reports", () => {
beforeAll(async () => {
// place test files on HOST_DIR
- await fs.promises.mkdir(report_path, { recursive: true });
- await fs.promises.copyFile(actual_report, path.join(report_path, "index.html"));
- await fs.promises.writeFile(path.join(report_path, "badge.svg"), fake_badge);
+ await fs.promises.mkdir(reportPath, { recursive: true });
+ await fs.promises.copyFile(
+ actualReport,
+ path.join(reportPath, "index.html")
+ );
+ await fs.promises.writeFile(path.join(reportPath, "badge.svg"), fakeBadge);
});
describe("GET /v1/:org/:repo/:branch/:commit.html", () => {
@@ -86,13 +155,15 @@ describe("Badges and reports", () => {
.get("/v1/testorg/testrepo/testbranch/testcommit.html")
.expect("Content-Type", /html/)
.expect(200);
- const buffer = await fs.promises.readFile(actual_report);
+ const buffer = await fs.promises.readFile(actualReport);
expect(res.text).toEqual(buffer.toString("utf-8"));
});
it("should return 404 if file does not exist", async () => {
- await request().get("/v1/neorg/nerepo/nebranch/necommit.html").expect(404);
+ await request()
+ .get("/v1/neorg/nerepo/nebranch/necommit.html")
+ .expect(404);
});
});
@@ -103,24 +174,32 @@ describe("Badges and reports", () => {
.get("/v1/testorg/testrepo/testbranch.html")
.expect("Content-Type", /html/)
.expect(200);
- const buffer = await fs.promises.readFile(actual_report);
+ const buffer = await fs.promises.readFile(actualReport);
expect(mockMeta.getHeadCommit).toHaveBeenCalledTimes(1);
expect(res.text).toEqual(buffer.toString("utf-8"));
});
it("should return 404 if file does not exist", async () => {
- await request().get("/v1/neorg/nerepo/nebranch.html").expect(404);
+ await request()
+ .get("/v1/neorg/nerepo/nebranch.html")
+ .expect(404);
});
it("should return 404 if head commit not found", async () => {
- const head = jest.fn(() => new Promise(solv => solv(new BranchNotFoundError())));
- await request(mock(head)).get("/v1/testorg/testrepo/testbranch.html").expect(404);
+ const head = jest.fn(
+ () => new Promise(solv => solv(new BranchNotFoundError()))
+ );
+ await request(mock(head))
+ .get("/v1/testorg/testrepo/testbranch.html")
+ .expect(404);
});
it("should return 500 if promise is rejected", async () => {
const head = jest.fn(() => new Promise((_, rej) => rej("fooey")));
- await request(mock(head)).get("/v1/testorg/testrepo/testbranch.html").expect(500);
+ await request(mock(head))
+ .get("/v1/testorg/testrepo/testbranch.html")
+ .expect(500);
});
});
@@ -131,11 +210,13 @@ describe("Badges and reports", () => {
.expect("Content-Type", /svg/)
.expect(200);
- expect(res.body.toString("utf-8")).toEqual(fake_badge);
+ expect(res.body.toString("utf-8")).toEqual(fakeBadge);
});
it("should return 404 if file does not exist", async () => {
- await request().get("/v1/neorg/nerepo/nebranch/necommit.svg").expect(404);
+ await request()
+ .get("/v1/neorg/nerepo/nebranch/necommit.svg")
+ .expect(404);
});
});
@@ -148,82 +229,124 @@ describe("Badges and reports", () => {
.expect(200);
expect(mockMeta.getHeadCommit).toHaveBeenCalledTimes(1);
- expect(res.body.toString("utf-8")).toEqual(fake_badge);
+ expect(res.body.toString("utf-8")).toEqual(fakeBadge);
});
it("should return 404 if file does not exist", async () => {
- await request().get("/v1/neorg/nerepo/nebranch.svg").expect(404);
+ await request()
+ .get("/v1/neorg/nerepo/nebranch.svg")
+ .expect(404);
});
it("should return 404 if head commit not found", async () => {
- const head = jest.fn(() => new Promise(solv => solv(new BranchNotFoundError())));
- await request(mock(head)).get("/v1/testorg/testrepo/testbranch.svg").expect(404);
+ const head = jest.fn(
+ () => new Promise(solv => solv(new BranchNotFoundError()))
+ );
+ await request(mock(head))
+ .get("/v1/testorg/testrepo/testbranch.svg")
+ .expect(404);
});
it("should return 500 if promise is rejected", async () => {
const head = jest.fn(() => new Promise((_, rej) => rej("fooey")));
- await request(mock(head)).get("/v1/testorg/testrepo/testbranch.svg").expect(500);
+ await request(mock(head))
+ .get("/v1/testorg/testrepo/testbranch.svg")
+ .expect(500);
});
});
});
describe("Uploads", () => {
-
- const report_path = path.join(HOST_DIR, "testorg", "testrepo", "newthis", "newthat");
- const data = fs.promises.readFile(path.join(__dirname, "..", "example_reports", "tarpaulin-report.html"));
+ const reportPath = path.join(
+ HOST_DIR,
+ "testorg",
+ "testrepo",
+ "newthis",
+ "newthat"
+ );
+ const data = fs.promises.readFile(
+ path.join(__dirname, "..", "example_reports", "tarpaulin-report.html")
+ );
beforeEach(async () => {
- await fs.promises.rmdir(report_path).catch(() => { });
+ try {
+ await fs.promises.rmdir(reportPath);
+ } catch (err) {
+ // ignore failures for rmdir
+ }
});
describe("POST /v1/:org/:repo/:branch/:commit.html", () => {
it("should upload the report and generate a badge", async () => {
const mockMeta = mock();
await request(mockMeta)
- .post(`/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=tarpaulin`)
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=tarpaulin`
+ )
.send(await data)
.expect(200);
- expect(mockMeta.updateBranch).toBeCalledWith({ organization: "testorg", repository: "testrepo", branch: "newthis", head: "newthat" });
+ expect(mockMeta.updateBranch).toBeCalledWith({
+ organization: "testorg",
+ repository: "testrepo",
+ branch: "newthis",
+ head: "newthat"
+ });
expect(mockMeta.updateBranch).toHaveBeenCalledTimes(1);
- await fs.promises.access(path.join(report_path, "index.html"), fs.constants.R_OK);
- await fs.promises.access(path.join(report_path, "badge.svg"), fs.constants.R_OK);
+ await fs.promises.access(
+ path.join(reportPath, "index.html"),
+ fs.constants.R_OK
+ );
+ await fs.promises.access(
+ path.join(reportPath, "badge.svg"),
+ fs.constants.R_OK
+ );
});
it("should return 401 when token is not correct", async () => {
await request()
- .post(`/v1/testorg/testrepo/newthis/newthat.html?token=wrong&format=tarpaulin`)
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.html?token=wrong&format=tarpaulin`
+ )
.send(await data)
.expect(401);
});
it("should return 406 with an invalid format", async () => {
await request()
- .post(`/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=pepperoni`)
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=pepperoni`
+ )
.send(await data)
.expect(406);
});
it("should return 400 when request body is not the appropriate format", async () => {
await request()
- .post(`/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=tarpaulin`)
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=tarpaulin`
+ )
.send("This is not a file")
.expect(400);
});
it("should return 413 when request body is not the appropriate format", async () => {
const file = await data;
- const big_data = Buffer.concat([file, file]);
+ const bigData = Buffer.concat([file, file]);
await request()
- .post(`/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=tarpaulin`)
- .send(big_data)
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=tarpaulin`
+ )
+ .send(bigData)
.expect(413);
});
it("should return 500 when Metadata does not create branch", async () => {
const update = jest.fn(() => new Promise(solv => solv(false)));
await request(mock(jest.fn(), update))
- .post(`/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=tarpaulin`)
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=tarpaulin`
+ )
.send(await data)
.expect(500);
});
@@ -231,9 +354,11 @@ describe("Uploads", () => {
it("should return 500 when promise chain is rejected", async () => {
const update = jest.fn(() => new Promise((_, rej) => rej("fooey 2")));
await request(mock(jest.fn(), update))
- .post(`/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=tarpaulin`)
+ .post(
+ `/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=tarpaulin`
+ )
.send(await data)
.expect(500);
});
});
-}); \ No newline at end of file
+});
diff --git a/src/routes.ts b/src/routes.ts
index 8cb4555..ff3220c 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -5,7 +5,6 @@ import winston from "winston";
import path from "path";
import fs from "fs";
-import processTemplate, { Template } from "./templates";
import formats, { GradientStyle } from "./formats";
import Metadata, { HeadIdentity } from "./metadata";
import { configOrError } from "./util/config";
@@ -15,48 +14,15 @@ import { Messages } from "./errors";
const TOKEN = process.env.TOKEN ?? "";
const UPLOAD_LIMIT = Number(process.env.UPLOAD_LIMIT ?? 4194304);
const HOST_DIR = configOrError("HOST_DIR");
-const TARGET_URL = process.env.TARGET_URL ?? "http://localhost:3000";
const logger = winston.createLogger(loggerConfig("HTTP"));
export default (metadata: Metadata): Router => {
const router = Router();
- const bashTemplate = {
- inputFile: path.join(__dirname, "..", "public", "templates", "bash.template"),
- outputFile: path.join(HOST_DIR, "bash"),
- context: { TARGET_URL }
- } as Template;
- const indexTemplate = {
- inputFile: path.join(__dirname, "..", "public", "templates", "index.html.template"),
- outputFile: path.join(HOST_DIR, "index.html"),
- context: { TARGET_URL }
- } as Template;
-
- processTemplate(bashTemplate)
- .then(template => {
- logger.debug("Generated '%s' from template file", template.outputFile);
- })
- .then(() => processTemplate(indexTemplate))
- .then(template => {
- logger.debug("Generated '%s' from template file", template.outputFile);
- })
- .catch(err => {
- logger.error("Unable to process template file: %s", err);
-
- // if the output file exists, then we are fine with continuing without
- return fs.promises.access(bashTemplate.outputFile, fs.constants.R_OK);
- })
- .then(() => fs.promises.access(indexTemplate.outputFile, fs.constants.R_OK))
- .catch(err => {
- logger.error("Cannot proceed: %s", err);
-
- process.exit(1);
- });
-
// serve landing page
router.get("/", (_, res) => {
- res.sendFile(path.join(HOST_DIR, "index.html"))
+ res.sendFile(path.join(HOST_DIR, "index.html"));
});
// serve script for posting coverage report
@@ -73,7 +39,8 @@ export default (metadata: Metadata): Router => {
res.sendFile(path.join(__dirname, "..", "public", "static", "favicon.ico"));
});
router.use(
- "/static", express.static(path.join(__dirname, "..", "public", "static"))
+ "/static",
+ express.static(path.join(__dirname, "..", "public", "static"))
);
// Upload HTML file
@@ -86,7 +53,7 @@ export default (metadata: Metadata): Router => {
return res.status(401).send(Messages.InvalidToken);
}
- if (typeof format !== 'string' || !formats.listFormats().includes(format)) {
+ if (typeof format !== "string" || !formats.listFormats().includes(format)) {
return res.status(406).send(Messages.InvalidFormat);
}
@@ -149,9 +116,12 @@ export default (metadata: Metadata): Router => {
result
? res.status(200).send()
: res.status(500).send(Messages.UnknownError)
- ).catch(err => {
- logger.error(err ?? "Unknown error occurred while processing POST request");
- return res.status(500).send(Messages.UnknownError)
+ )
+ .catch(err => {
+ logger.error(
+ err ?? "Unknown error occurred while processing POST request"
+ );
+ return res.status(500).send(Messages.UnknownError);
});
});
});
@@ -189,7 +159,9 @@ export default (metadata: Metadata): Router => {
}
},
err => {
- logger.error(err ?? "Error occurred while fetching commit for GET request");
+ logger.error(
+ err ?? "Error occurred while fetching commit for GET request"
+ );
res.status(500).send(Messages.UnknownError);
}
);
@@ -213,7 +185,9 @@ export default (metadata: Metadata): Router => {
}
},
err => {
- logger.error(err ?? "Error occurred while fetching commit for GET request");
+ logger.error(
+ err ?? "Error occurred while fetching commit for GET request"
+ );
res.status(500).send(Messages.UnknownError);
}
);
diff --git a/src/util/config.test.ts b/src/util/config.test.ts
index 19d7564..418edb7 100644
--- a/src/util/config.test.ts
+++ b/src/util/config.test.ts
@@ -1,11 +1,11 @@
+const exit = jest
+ .spyOn(process, "exit")
+ .mockImplementation(() => undefined as never);
+
import { configOrError, handleShutdown } from "./config";
import { MongoClient } from "mongodb";
import { Server } from "http";
-const exit = jest.spyOn(process, "exit").mockImplementation(() => {
- throw Error("");
-});
-
const CommonMocks = {
connect: jest.fn(),
isConnected: jest.fn(),
@@ -111,7 +111,7 @@ describe("handleShutdown", () => {
}
// Assert
- expect(exit).toHaveBeenCalledWith(1);
+ expect(exit).toHaveBeenCalledWith(0);
});
it("should exit with error with Mongo error", async () => {
diff --git a/src/util/config.ts b/src/util/config.ts
index 1eddda3..b3a1ead 100644
--- a/src/util/config.ts
+++ b/src/util/config.ts
@@ -1,8 +1,11 @@
import winston from "winston";
-import { MongoClient } from "mongodb";
+import { MongoClient, MongoError } from "mongodb";
import { Server } from "http";
+import path from "path";
+import fs from "fs";
import loggerConfig from "./logger";
+import processTemplate, { Template } from "../templates";
const logger = winston.createLogger(loggerConfig("ROOT"));
@@ -16,6 +19,76 @@ export const configOrError = (varName: string): string => {
}
};
+export const persistTemplate = async (input: Template): Promise<void> => {
+ try {
+ const template = await processTemplate(input);
+ logger.debug("Generated '%s' from template file", template.outputFile);
+ } catch (err1) {
+ try {
+ await fs.promises.access(input.outputFile, fs.constants.R_OK);
+ } catch (err2) {
+ logger.error(
+ "Error while generating '%s' from template file: %s",
+ input.outputFile,
+ err1
+ );
+ logger.error("Cannot proceed due to error: %s", err2);
+
+ process.exit(1);
+ }
+ // if the output file exists, then we are fine with continuing without
+ logger.warning(
+ "Could not generate '%s' from template file, but file already exists: %s",
+ input.outputFile,
+ err1
+ );
+ }
+};
+
+const MONGO_URI = configOrError("MONGO_URI");
+const TARGET_URL = process.env.TARGET_URL ?? "http://localhost:3000";
+const HOST_DIR = configOrError("HOST_DIR");
+
+export const handleStartup = async (): Promise<MongoClient> => {
+ await fs.promises.access(HOST_DIR, fs.constants.R_OK | fs.constants.W_OK);
+ if (!path.isAbsolute(HOST_DIR)) {
+ logger.error("HOST_DIR must be an absolute path");
+ process.exit(1);
+ }
+
+ const mongo = await new MongoClient(MONGO_URI, { useUnifiedTopology: true })
+ .connect()
+ .catch((err: MongoError) => {
+ logger.error(err.message ?? "Unable to connect to database");
+ process.exit(1);
+ });
+
+ await persistTemplate({
+ inputFile: path.join(
+ __dirname,
+ "..",
+ "public",
+ "templates",
+ "bash.template"
+ ),
+ outputFile: path.join(HOST_DIR, "bash"),
+ context: { TARGET_URL }
+ } as Template);
+ await persistTemplate({
+ inputFile: path.join(
+ __dirname,
+ "..",
+ "public",
+ "templates",
+ "index.html.template"
+ ),
+ outputFile: path.join(HOST_DIR, "index.html"),
+ context: { TARGET_URL }
+ } as Template);
+
+ return mongo;
+};
+
export const handleShutdown = (mongo: MongoClient, server: Server) => (
signal: NodeJS.Signals
): Promise<void> => {