diff options
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | src/routes.test.ts | 11 | ||||
| -rw-r--r-- | src/util/config.test.ts | 182 | ||||
| -rw-r--r-- | src/util/config.ts | 12 |
4 files changed, 201 insertions, 8 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 397e0af..48b0715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- 404 page to handle unknown paths + ### Changed - Moved startup processes to async handleStartup function diff --git a/src/routes.test.ts b/src/routes.test.ts index 3579011..90084f6 100644 --- a/src/routes.test.ts +++ b/src/routes.test.ts @@ -117,6 +117,17 @@ describe("Static files", () => { .expect(200); expect(res.text).toEqual(buffer.toString("utf-8")); }); + + it("should return 404.html at unhandled endpoints", async () => { + const buffer = await fs.promises.readFile( + path.join(staticRoot, "404.html") + ); + + const res = await request() + .get("/thisisnotanendpoint") + .expect(404); + expect(res.text).toEqual(buffer.toString("utf-8")); + }); }); describe("Badges and reports", () => { diff --git a/src/util/config.test.ts b/src/util/config.test.ts index 418edb7..b1e7df3 100644 --- a/src/util/config.test.ts +++ b/src/util/config.test.ts @@ -2,9 +2,17 @@ const exit = jest .spyOn(process, "exit") .mockImplementation(() => undefined as never); -import { configOrError, handleShutdown } from "./config"; -import { MongoClient } from "mongodb"; +import { + configOrError, + persistTemplate, + handleStartup, + handleShutdown +} from "./config"; +import { MongoClient, MongoError } from "mongodb"; import { Server } from "http"; +import path from "path"; +import fs from "fs"; +import * as templates from "../templates"; const CommonMocks = { connect: jest.fn(), @@ -92,6 +100,176 @@ describe("configOrError", () => { }); }); +describe("persistTemplate", () => { + beforeEach(() => { + exit.mockClear(); + }); + + it("should generate a template without error", async () => { + const template = { + inputFile: "/mnt/c/Windows/System32", + outputFile: "./helloworld.txt", + context: { EXAMPLE: "this" } + } as templates.Template; + const processTemplate = jest + .spyOn(templates, "default") + .mockImplementation( + (template: templates.Template) => new Promise(res => res(template)) + ); + const fsAccess = jest.spyOn(fs.promises, "access").mockResolvedValue(); + + await persistTemplate(template); + + expect(processTemplate).toHaveBeenCalledWith(template); + expect(fsAccess).not.toHaveBeenCalled(); + expect(exit).not.toHaveBeenCalled(); + processTemplate.mockRestore(); + fsAccess.mockRestore(); + }); + + it("should exit without error if template does not generate but file already exists", async () => { + const template = { + inputFile: "/mnt/c/Windows/System32", + outputFile: "./helloworld.txt", + context: { EXAMPLE: "this" } + } as templates.Template; + const processTemplate = jest + .spyOn(templates, "default") + .mockRejectedValue("baa"); + const fsAccess = jest.spyOn(fs.promises, "access").mockResolvedValue(); + + await persistTemplate(template); + + expect(processTemplate).toHaveBeenCalledWith(template); + expect(fsAccess).toHaveBeenCalledWith( + template.outputFile, + fs.constants.R_OK + ); + expect(exit).not.toHaveBeenCalled(); + processTemplate.mockRestore(); + fsAccess.mockRestore(); + }); + + it("should exit with error if template does not generate and file does not exist", async () => { + const template = { + inputFile: "/mnt/c/Windows/System32", + outputFile: "./helloworld.txt", + context: { EXAMPLE: "this" } + } as templates.Template; + const processTemplate = jest + .spyOn(templates, "default") + .mockRejectedValue("baz"); + const fsAccess = jest.spyOn(fs.promises, "access").mockRejectedValue("bar"); + + await persistTemplate(template); + + expect(processTemplate).toHaveBeenCalledWith(template); + expect(fsAccess).toHaveBeenCalledWith( + template.outputFile, + fs.constants.R_OK + ); + expect(exit).toHaveBeenCalledWith(1); + processTemplate.mockRestore(); + fsAccess.mockRestore(); + }); +}); + +describe("handleStartup", () => { + beforeEach(() => { + exit.mockClear(); + }); + + it("should pass back MongoClient", async () => { + const superClient = {} as MongoClient; + const fsAccess = jest.spyOn(fs.promises, "access").mockResolvedValue(); + const pathAbsolute = jest.spyOn(path, "isAbsolute").mockReturnValue(true); + const pathJoin = jest.spyOn(path, "join").mockReturnValue("path"); + const mongoClient = jest.spyOn(MongoClient, "connect").mockImplementation( + () => new Promise<MongoClient>(res => res(superClient)) + ); + const processTemplate = jest + .spyOn(templates, "default") + .mockImplementation( + (template: templates.Template) => new Promise(res => res(template)) + ); + + const result = await handleStartup(); + + expect(fsAccess).toHaveBeenCalledTimes(1); + expect(pathAbsolute).toHaveBeenCalledTimes(1); + expect(mongoClient).toHaveBeenCalledTimes(1); + expect(processTemplate).toHaveBeenCalledTimes(2); + expect(exit).not.toHaveBeenCalled(); + expect(result).toEqual(superClient); + processTemplate.mockRestore(); + mongoClient.mockRestore(); + pathAbsolute.mockRestore(); + pathJoin.mockRestore(); + fsAccess.mockRestore(); + }); + + it("should exit if HOST_DIR is not read/write accessible", async () => { + const fsAccess = jest.spyOn(fs.promises, "access").mockRejectedValue("boo"); + const pathAbsolute = jest.spyOn(path, "isAbsolute").mockReturnValue(true); + const pathJoin = jest.spyOn(path, "join").mockReturnValue("path"); + + const result = await handleStartup(); + + expect(fsAccess).toHaveBeenCalledTimes(1); + expect(exit).toHaveBeenCalledWith(1); + expect(pathAbsolute).not.toHaveBeenCalled(); + expect(result).toBeUndefined(); + pathAbsolute.mockRestore(); + pathJoin.mockRestore(); + fsAccess.mockRestore(); + }); + + it("should exit if HOST_DIR is not absolute path", async () => { + const superClient = {} as MongoClient; + const fsAccess = jest.spyOn(fs.promises, "access").mockResolvedValue(); + const pathAbsolute = jest.spyOn(path, "isAbsolute").mockReturnValue(false); + const pathJoin = jest.spyOn(path, "join").mockReturnValue("path"); + const mongoClient = jest.spyOn(MongoClient, "connect").mockImplementation( + () => new Promise<MongoClient>(res => res(superClient)) + ); + + await handleStartup(); + + expect(pathAbsolute).toHaveBeenCalledTimes(1); + expect(exit).toHaveBeenCalledWith(1); + mongoClient.mockRestore(); + pathAbsolute.mockRestore(); + pathJoin.mockRestore(); + fsAccess.mockRestore(); + }); + + it("should exit if MongoClient has error", async () => { + const fsAccess = jest.spyOn(fs.promises, "access").mockResolvedValue(); + const pathAbsolute = jest.spyOn(path, "isAbsolute").mockReturnValue(true); + const pathJoin = jest.spyOn(path, "join").mockReturnValue("path"); + const mongoClient = jest + .spyOn(MongoClient, "connect") + .mockImplementation( + () => new Promise((_, rej) => rej({ message: "aaahhh" } as MongoError)) + ); + const processTemplate = jest + .spyOn(templates, "default") + .mockImplementation( + (template: templates.Template) => new Promise(res => res(template)) + ); + + await handleStartup(); + + expect(mongoClient).toHaveBeenCalledTimes(1); + expect(exit).toHaveBeenCalledWith(1); + processTemplate.mockRestore(); + mongoClient.mockRestore(); + pathAbsolute.mockRestore(); + pathJoin.mockRestore(); + fsAccess.mockRestore(); + }); +}); + describe("handleShutdown", () => { beforeEach(() => { exit.mockClear(); diff --git a/src/util/config.ts b/src/util/config.ts index d2ad130..195d85b 100644 --- a/src/util/config.ts +++ b/src/util/config.ts @@ -57,12 +57,12 @@ export const handleStartup = async (): Promise<MongoClient> => { 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); - }); + const mongo = await MongoClient.connect(MONGO_URI, { + useUnifiedTopology: true + }).catch((err: MongoError) => { + logger.error(err.message ?? "Unable to connect to database"); + process.exit(1); + }); await persistTemplate({ inputFile: path.join( |
