diff options
Diffstat (limited to 'src/util')
| -rw-r--r-- | src/util/config.test.ts | 472 | ||||
| -rw-r--r-- | src/util/config.ts | 122 | ||||
| -rw-r--r-- | src/util/logger.test.ts | 6 | ||||
| -rw-r--r-- | src/util/logger.ts | 9 |
4 files changed, 8 insertions, 601 deletions
diff --git a/src/util/config.test.ts b/src/util/config.test.ts deleted file mode 100644 index 4343392..0000000 --- a/src/util/config.test.ts +++ /dev/null @@ -1,472 +0,0 @@ -const exit = jest - .spyOn(process, "exit") - .mockImplementation(() => undefined as never); - -import { Writable } from "stream"; -import winston from "winston"; - -let output = ""; - -jest.mock("./logger", () => { - const stream = new Writable(); - stream._write = (chunk, _encoding, next) => { - output = output += chunk.toString(); - next(); - }; - const streamTransport = new winston.transports.Stream({ stream }); - - return { - __esModule: true, - default: () => ({ - format: winston.format.combine( - winston.format.splat(), - winston.format.simple() - ), - transports: [streamTransport], - }), - }; -}); - -import { - configOrError, - persistTemplate, - handleStartup, - handleShutdown, - initializeToken, -} from "./config"; -import { - Logger, - MongoClient, - MongoError, - ReadConcern, - ReadPreference, - WriteConcern, -} from "mongodb"; -import { Server } from "http"; -import path from "path"; -import fs from "fs"; -import * as templates from "../templates"; -import { EnvConfig } from "../metadata"; - -const CommonMocks = { - connect: jest.fn(), - isConnected: jest.fn(), - logout: jest.fn(), - watch: jest.fn(), - startSession: jest.fn(), - withSession: jest.fn(), - addListener: jest.fn(), - on: jest.fn(), - once: jest.fn(), - prependListener: jest.fn(), - prependOnceListener: jest.fn(), - removeAllListeners: jest.fn(), - removeListener: jest.fn(), - off: jest.fn(), - setMaxListeners: jest.fn(), - getMaxListeners: jest.fn(), - listeners: jest.fn(), - listenerCount: jest.fn(), - rawListeners: jest.fn(), - emit: jest.fn(), - eventNames: jest.fn(), -}; - -const MongoMock = (p: Promise<void>): jest.Mock<MongoClient, void[]> => - jest.fn<MongoClient, void[]>(() => ({ - ...CommonMocks, - close: jest.fn(() => p), - readPreference: ReadPreference.nearest, - bsonOptions: {}, - logger: new Logger("a"), - getLogger: jest.fn(), - options: { - hosts: [], - readPreference: ReadPreference.nearest, - readConcern: new ReadConcern("local"), - loadBalanced: true, - serverApi: { version: "1" }, - compressors: [], - writeConcern: new WriteConcern(), - dbName: "", - metadata: { - driver: { name: "", version: "" }, - os: { type: "", name: "linux", architecture: "", version: "" }, - platform: "linx", - }, - tls: true, - toURI: jest.fn(), - autoEncryption: {}, - connectTimeoutMS: 0, - directConnection: true, - driverInfo: {}, - forceServerObjectId: true, - minHeartbeatFrequencyMS: 0, - heartbeatFrequencyMS: 0, - keepAlive: false, - keepAliveInitialDelay: 0, - localThresholdMS: 0, - logger: new Logger("a"), - maxIdleTimeMS: 0, - maxPoolSize: 0, - minPoolSize: 0, - monitorCommands: true, - noDelay: true, - pkFactory: { createPk: jest.fn() }, - promiseLibrary: {}, - raw: true, - replicaSet: "", - retryReads: true, - retryWrites: true, - serverSelectionTimeoutMS: 0, - socketTimeoutMS: 0, - tlsAllowInvalidCertificates: true, - tlsAllowInvalidHostnames: true, - tlsInsecure: false, - waitQueueTimeoutMS: 0, - zlibCompressionLevel: 0, - srvMaxHosts: 1, - srvServiceName: "", - }, - serverApi: { version: "1" }, - autoEncrypter: undefined, - readConcern: new ReadConcern("local"), - writeConcern: new WriteConcern(), - db: jest.fn(), - })); -const ServerMock = (mockErr: Error | undefined): jest.Mock<Server, void[]> => - jest.fn<Server, void[]>(() => ({ - ...CommonMocks, - connections: 0, - setTimeout: jest.fn(), - timeout: 0, - headersTimeout: 0, - keepAliveTimeout: 0, - close: function (c: (err?: Error | undefined) => void): Server { - c(mockErr); - return this; - }, - maxHeadersCount: 0, - maxConnections: 0, - maxRequestsPerSocket: 0, - listen: jest.fn(), - listening: true, - address: jest.fn(), - getConnections: jest.fn(), - ref: jest.fn(), - requestTimeout: 3600, - unref: jest.fn(), - })); - -describe("initializeToken", () => { - it("Should generate a UUID", () => { - // Arrange - output = ""; - - // Act - const result = initializeToken(); - - // Assert - expect(result).toMatch(/([a-f0-9]{8}(-[a-f0-9]{4}){4}[a-f0-9]{8})/); - expect(output).toContain(result); - }); -}); - -describe("configOrError", () => { - beforeEach(() => { - exit.mockClear(); - }); - - it("should exit when a env var does not exist", () => { - // Arrange - - // Act - let result; - try { - result = configOrError("APPLESAUCE"); - } catch (err) { - // - } - - // Assert - expect(result).toBeUndefined(); - expect(exit).toHaveBeenCalledWith(1); - }); - - it("should return the expected env var", () => { - // Arrange - process.env.CHRYSANTHEMUM = "hello"; - - // Act - const result = configOrError("CHRYSANTHEMUM"); - - // Assert - expect(result).toEqual(process.env.CHRYSANTHEMUM); - expect(exit).toHaveBeenCalledTimes(0); - }); -}); - -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(); - }); - - const config = { - hostDir: "/apple", - publicDir: "/public", - } as EnvConfig; - const confStartup = (): Promise<MongoClient> => - handleStartup("", config, "localhost"); - - 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 confStartup(); - - 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 superClient = {} as MongoClient; - 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 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 confStartup(); - - expect(fsAccess).toHaveBeenCalledTimes(1); - expect(exit).toHaveBeenCalledWith(1); - expect(pathAbsolute).not.toHaveBeenCalled(); - expect(mongoClient).not.toHaveBeenCalled(); - expect(processTemplate).not.toHaveBeenCalled(); - expect(result).toBeUndefined(); - processTemplate.mockRestore(); - mongoClient.mockRestore(); - 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)) - ); - const processTemplate = jest - .spyOn(templates, "default") - .mockImplementation( - (template: templates.Template) => new Promise((res) => res(template)) - ); - - const result = await confStartup(); - - expect(fsAccess).toHaveBeenCalledTimes(1); - expect(pathAbsolute).toHaveBeenCalledTimes(1); - expect(exit).toHaveBeenCalledWith(1); - expect(mongoClient).not.toHaveBeenCalled(); - expect(processTemplate).not.toHaveBeenCalled(); - expect(result).toBeUndefined(); - processTemplate.mockRestore(); - 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)) - ); - - const result = await confStartup(); - - expect(fsAccess).toHaveBeenCalledTimes(1); - expect(pathAbsolute).toHaveBeenCalledTimes(1); - expect(mongoClient).toHaveBeenCalledTimes(1); - expect(exit).toHaveBeenCalledWith(1); - expect(processTemplate).not.toHaveBeenCalled(); - expect(result).toBeUndefined(); - processTemplate.mockRestore(); - mongoClient.mockRestore(); - pathAbsolute.mockRestore(); - pathJoin.mockRestore(); - fsAccess.mockRestore(); - }); -}); - -describe("handleShutdown", () => { - beforeEach(() => { - exit.mockClear(); - // we don't use the MongoMock or ServerMock to directly test, so no mockClear needed - }); - - it("should exit gracefully without error", async () => { - // Arrange - const mongo = MongoMock(new Promise((r) => r()))(); - const server = ServerMock(undefined)(); - - // Act - try { - await handleShutdown(mongo, server)("SIGINT"); - } catch (err) { - // - } - - // Assert - expect(exit).toHaveBeenCalledWith(0); - }); - - it("should exit with error with Mongo error", async () => { - // Arrange - const mongo = MongoMock(new Promise((_, r) => r()))(); - const server = ServerMock(undefined)(); - - // Act - try { - await handleShutdown(mongo, server)("SIGINT"); - } catch (err) { - // - } - - // Assert - expect(exit).toHaveBeenCalledWith(1); - }); - - it("should exit with error with Server error", async () => { - // Arrange - const mongo = MongoMock(new Promise((r) => r()))(); - const server = ServerMock(Error("oh noooo"))(); - - // Act - try { - await handleShutdown(mongo, server)("SIGINT"); - } catch (err) { - // - } - - // Assert - expect(exit).toHaveBeenCalledWith(1); - }); -}); diff --git a/src/util/config.ts b/src/util/config.ts deleted file mode 100644 index c8639fb..0000000 --- a/src/util/config.ts +++ /dev/null @@ -1,122 +0,0 @@ -import winston from "winston"; -import { MongoClient, MongoError } from "mongodb"; -import { Server } from "http"; -import path from "path"; -import fs from "fs"; -import { v4 as uuid } from "uuid"; - -import loggerConfig from "./logger"; -import processTemplate, { Template } from "../templates"; -import { EnvConfig } from "../metadata"; - -const logger = winston.createLogger(loggerConfig("ROOT")); - -export const initializeToken = (): string => { - //TODO check for token in hostDir/persist created token in hostDir so it's not regenerated on startup - const newToken = uuid(); - - logger.warn( - "TOKEN variable not provided, using this value instead: %s", - newToken - ); - logger.warn( - "Use this provided token to push your coverage reports to the server." - ); - - return newToken; -}; - -export const configOrError = (varName: string): string => { - const value = process.env[varName]; - if (value !== undefined) { - return value; - } else { - logger.error("%s must be defined", varName); - process.exit(1); - } -}; - -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.warn( - "Could not generate '%s' from template file, but file already exists: %s", - input.outputFile, - err1 - ); - } -}; - -export const handleStartup = async ( - mongoUri: string, - config: EnvConfig, - targetUrl: string -): Promise<MongoClient> => { - try { - const { hostDir, publicDir } = config; - await fs.promises.access(hostDir, fs.constants.R_OK | fs.constants.W_OK); - if (!path.isAbsolute(hostDir)) { - await Promise.reject("hostDir must be an absolute path"); - } - - const mongo = await MongoClient.connect(mongoUri).catch((err: MongoError) => - Promise.reject(err.message ?? "Unable to connect to database") - ); - - await persistTemplate({ - inputFile: path.join(publicDir, "templates", "sh.tmpl"), - outputFile: path.join(hostDir, "sh"), - context: { TARGET_URL: targetUrl }, - } as Template); - await persistTemplate({ - inputFile: path.join(publicDir, "templates", "index.html.tmpl"), - outputFile: path.join(hostDir, "index.html"), - context: { - TARGET_URL: targetUrl, - CURL_HTTPS: targetUrl.includes("https") - ? "--proto '=https' --tlsv1.2 " - : "", - }, - } as Template); - - return mongo; - } catch (err) { - logger.error("Error occurred during startup: %s", err); - process.exit(1); - } -}; - -export const handleShutdown = - (mongo: MongoClient, server: Server) => - (signal: NodeJS.Signals): Promise<void> => { - logger.warn("%s signal received. Closing shop.", signal); - - return mongo - .close() - .then(() => { - logger.info("MongoDB client connection closed."); - return new Promise((res, rej) => - server.close((err) => { - logger.info("Express down."); - (err ? rej : res)(err); - }) - ); - }) - .then(() => process.exit(0)) - .catch(() => process.exit(1)); - }; diff --git a/src/util/logger.test.ts b/src/util/logger.test.ts index a673bad..a7e81c1 100644 --- a/src/util/logger.test.ts +++ b/src/util/logger.test.ts @@ -12,7 +12,7 @@ describe("Logger configurer", () => { }; // Act - const result = configureLogger(clazz); + const result = configureLogger(clazz, adapter.level); const actual = result.format.transform(Object.assign({}, adapter)); // Assert @@ -34,7 +34,7 @@ describe("Logger configurer", () => { }; // Act - const result = configureLogger(label); + const result = configureLogger(label, adapter.level); const actual = result.format.transform(Object.assign({}, adapter)); // Assert @@ -51,7 +51,7 @@ describe("Logger configurer", () => { const label = "aaa"; // Act - const result = configureLogger(label); + const result = configureLogger(label, "info"); // Assert expect(result.transports).toBeInstanceOf(Array); diff --git a/src/util/logger.ts b/src/util/logger.ts index d779718..9b0957d 100644 --- a/src/util/logger.ts +++ b/src/util/logger.ts @@ -4,8 +4,7 @@ import * as Transport from "winston-transport"; const { combine, splat, timestamp, label, colorize, printf } = winston.format; const { Console } = winston.transports; -const LOG_LEVEL = process.env.LOG_LEVEL ?? "info"; - +// Standard console message formatting const consoleFormat = combine( colorize(), printf(({ level, message, label, timestamp }) => { @@ -16,9 +15,9 @@ const consoleFormat = combine( /** * Provides standard logging format and output for the server. */ -export default ( +const loggerConfig = ( clazz: string, - level: string = LOG_LEVEL + level: string ): { format: Format; transports: Transport[]; @@ -26,3 +25,5 @@ export default ( format: combine(splat(), timestamp(), label({ label: clazz })), transports: [new Console({ level: level, format: consoleFormat })], }); + +export default loggerConfig; |
