aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin J Hoerr <kjhoerr@protonmail.com>2020-01-11 21:41:41 -0500
committerKevin J Hoerr <kjhoerr@protonmail.com>2020-01-11 21:41:41 -0500
commitc7ebf8009e27256db7eb36fa259c250bd80dbf09 (patch)
tree93e111f5e4c1084de9f9105a7b5a06b5dc2464eb
parentedfdc5cfcfa9b7df9f5c7b5ff53f432b0579b433 (diff)
downloadao-coverage-c7ebf8009e27256db7eb36fa259c250bd80dbf09.tar.gz
ao-coverage-c7ebf8009e27256db7eb36fa259c250bd80dbf09.tar.bz2
ao-coverage-c7ebf8009e27256db7eb36fa259c250bd80dbf09.zip
#8 Add router unit tests using supertest
Also moved the template processing from index to router.
-rw-r--r--CHANGELOG.md5
-rw-r--r--package-lock.json106
-rw-r--r--package.json3
-rw-r--r--src/index.ts35
-rw-r--r--src/routes.test.ts239
-rw-r--r--src/routes.ts34
6 files changed, 387 insertions, 35 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff2655a..818c383 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- landing page provided at / based on template
+
### Changed
- More descriptive output from bash template, with links to the files
+- Moved template processing to router, so unit tests can be run without build
## [0.3.3]
diff --git a/package-lock.json b/package-lock.json
index 2416ed2..8c5a11c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -487,6 +487,12 @@
"@types/node": "*"
}
},
+ "@types/cookiejar": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz",
+ "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==",
+ "dev": true
+ },
"@types/eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
@@ -608,6 +614,25 @@
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==",
"dev": true
},
+ "@types/superagent": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.4.tgz",
+ "integrity": "sha512-SRH2q6/5/nhOkAuLXm3azRGjBYpoKCZWh138Rt1AxSIyE6/1b9uClIH2V+JfyDtjIvgr5yQqYgNUmdpbneJoZQ==",
+ "dev": true,
+ "requires": {
+ "@types/cookiejar": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/supertest": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.8.tgz",
+ "integrity": "sha512-wcax7/ip4XSSJRLbNzEIUVy2xjcBIZZAuSd2vtltQfRK7kxhx5WMHbLHkYdxN3wuQCrwpYrg86/9byDjPXoGMA==",
+ "dev": true,
+ "requires": {
+ "@types/superagent": "*"
+ }
+ },
"@types/tough-cookie": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz",
@@ -1410,6 +1435,12 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
+ "cookiejar": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
+ "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==",
+ "dev": true
+ },
"copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
@@ -2328,6 +2359,12 @@
"mime-types": "^2.1.12"
}
},
+ "formidable": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz",
+ "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==",
+ "dev": true
+ },
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@@ -5908,6 +5945,75 @@
"integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
"dev": true
},
+ "superagent": {
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
+ "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==",
+ "dev": true,
+ "requires": {
+ "component-emitter": "^1.2.0",
+ "cookiejar": "^2.1.0",
+ "debug": "^3.1.0",
+ "extend": "^3.0.0",
+ "form-data": "^2.3.1",
+ "formidable": "^1.2.0",
+ "methods": "^1.1.1",
+ "mime": "^1.4.1",
+ "qs": "^6.5.1",
+ "readable-stream": "^2.3.5"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "supertest": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz",
+ "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==",
+ "dev": true,
+ "requires": {
+ "methods": "^1.1.2",
+ "superagent": "^3.8.3"
+ }
+ },
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
diff --git a/package.json b/package.json
index 7a974f3..87068f8 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
},
"devDependencies": {
"@types/jest": "^24.0.24",
+ "@types/supertest": "^2.0.8",
"@types/triple-beam": "^1.3.0",
"@typescript-eslint/eslint-plugin": "^2.12.0",
"@typescript-eslint/parser": "^2.12.0",
@@ -44,6 +45,7 @@
"eslint-plugin-prettier": "^3.1.2",
"jest": "^24.9.0",
"prettier": "^1.19.1",
+ "supertest": "^4.0.2",
"triple-beam": "^1.3.0",
"ts-jest": "^24.2.0",
"tsc-watch": "^4.0.0"
@@ -54,6 +56,7 @@
"collectCoverageFrom": [
"src/**/*.ts",
"!src/**/*.test.ts",
+ "!src/**/__tests__/**/*.ts",
"!src/index.ts"
],
"roots": [
diff --git a/src/index.ts b/src/index.ts
index 71da73c..76620e8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -9,7 +9,6 @@ import expressWinston from "express-winston";
dotenv.config();
-import processTemplate, { Template } from "./templates";
import routes from "./routes";
import Metadata from "./metadata";
import loggerConfig from "./util/logger";
@@ -18,7 +17,6 @@ import { configOrError, handleShutdown } from "./util/config";
// Start-up configuration
const BIND_ADDRESS = process.env.BIND_ADDRESS ?? "localhost";
const PORT = Number(process.env.PORT ?? 3000);
-const TARGET_URL = process.env.TARGET_URL ?? "http://localhost:3000";
const logger = winston.createLogger(loggerConfig("ROOT"));
@@ -32,39 +30,6 @@ if (!path.isAbsolute(HOST_DIR)) {
process.exit(1);
}
-// prepare template files
-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);
- });
-
new MongoClient(MONGO_URI, { useUnifiedTopology: true }).connect(
(err, mongo) => {
if (err !== null) {
diff --git a/src/routes.test.ts b/src/routes.test.ts
new file mode 100644
index 0000000..b4979d6
--- /dev/null
+++ b/src/routes.test.ts
@@ -0,0 +1,239 @@
+import _request from "supertest";
+import express from "express";
+import dotenv from "dotenv";
+import fs from "fs";
+import path from "path";
+
+dotenv.config();
+
+process.env.UPLOAD_LIMIT = "40000";
+
+import { configOrError } from "./util/config";
+import routes from "./routes";
+import Metadata from "./metadata";
+import { Db } from "mongodb";
+import { badgen } from "badgen";
+import { BranchNotFoundError } from "./errors";
+
+type MetadataMockType = {
+ database: Db;
+ getHeadCommit: jest.Mock;
+ updateBranch: jest.Mock;
+ 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 => ({
+ database: {} as Db,
+ getHeadCommit: headCommit,
+ updateBranch: updateBranch,
+ createRepository: jest.fn()
+});
+
+const request = (mockMeta: MetadataMockType = mock()) => {
+ 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 () => {
+ const res = await request()
+ .get("/bash")
+ .expect(200);
+ expect(res.text).toMatch("curl -X POST");
+ expect(res.text).toMatch(`url="${TARGET_URL}"`);
+ });
+ });
+
+ describe("GET /", () => {
+ it("should return the index HTML file containing the bash command", async () => {
+ 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", () => {
+
+ 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({
+ label: "coverage",
+ status: "120%",
+ color: "#E1C"
+ });
+
+ 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);
+ });
+
+ describe("GET /v1/:org/:repo/:branch/:commit.html", () => {
+ it("should retrieve the stored report file", async () => {
+ const res = await request()
+ .get("/v1/testorg/testrepo/testbranch/testcommit.html")
+ .expect("Content-Type", /html/)
+ .expect(200);
+ const buffer = await fs.promises.readFile(actual_report);
+
+ 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);
+ });
+ });
+
+ describe("GET /v1/:org/:repo/:branch.html", () => {
+ it("should retrieve the stored report file with the associated head commit", async () => {
+ const mockMeta = mock();
+ const res = await request(mockMeta)
+ .get("/v1/testorg/testrepo/testbranch.html")
+ .expect("Content-Type", /html/)
+ .expect(200);
+ const buffer = await fs.promises.readFile(actual_report);
+
+ 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);
+ });
+
+ 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);
+ });
+
+ 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);
+ });
+ });
+
+ describe("GET /v1/:org/:repo/:branch/:commit.svg", () => {
+ it("should retrieve the stored report badge", async () => {
+ const res = await request()
+ .get("/v1/testorg/testrepo/testbranch/testcommit.svg")
+ .expect("Content-Type", /svg/)
+ .expect(200);
+
+ expect(res.body.toString("utf-8")).toEqual(fake_badge);
+ });
+
+ it("should return 404 if file does not exist", async () => {
+ await request().get("/v1/neorg/nerepo/nebranch/necommit.svg").expect(404);
+ });
+ });
+
+ describe("GET /v1/:org/:repo/:branch.svg", () => {
+ it("should retrieve the stored report badge with the associated head commit", async () => {
+ const mockMeta = mock();
+ const res = await request(mockMeta)
+ .get("/v1/testorg/testrepo/testbranch.svg")
+ .expect("Content-Type", /svg/)
+ .expect(200);
+
+ expect(mockMeta.getHeadCommit).toHaveBeenCalledTimes(1);
+ expect(res.body.toString("utf-8")).toEqual(fake_badge);
+ });
+
+ it("should return 404 if file does not exist", async () => {
+ 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);
+ });
+
+ 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);
+ });
+ });
+});
+
+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"));
+
+ beforeEach(async () => {
+ await fs.promises.rmdir(report_path).catch(() => { });
+ });
+
+ 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`)
+ .send(await data)
+ .expect(200);
+
+ 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);
+ });
+
+ it("should return 401 when token is not correct", async () => {
+ await request()
+ .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`)
+ .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`)
+ .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]);
+ await request()
+ .post(`/v1/testorg/testrepo/newthis/newthat.html?token=${TOKEN}&format=tarpaulin`)
+ .send(big_data)
+ .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`)
+ .send(await data)
+ .expect(500);
+ });
+
+ 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`)
+ .send(await data)
+ .expect(500);
+ });
+ });
+}); \ No newline at end of file
diff --git a/src/routes.ts b/src/routes.ts
index 9ce3bbc..92d3542 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -5,6 +5,7 @@ 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";
@@ -14,12 +15,45 @@ 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"))