diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.test.ts (renamed from src/util/config.test.ts) | 29 | ||||
| -rw-r--r-- | src/config.ts (renamed from src/util/config.ts) | 79 | ||||
| -rw-r--r-- | src/index.ts | 53 | ||||
| -rw-r--r-- | src/metadata.ts | 118 | ||||
| -rw-r--r-- | src/routes.test.ts | 25 | ||||
| -rw-r--r-- | src/routes.ts | 18 | ||||
| -rw-r--r-- | src/templates.ts | 6 | ||||
| -rw-r--r-- | src/util/logger.test.ts | 6 | ||||
| -rw-r--r-- | src/util/logger.ts | 9 |
9 files changed, 226 insertions, 117 deletions
diff --git a/src/util/config.test.ts b/src/config.test.ts index 4343392..c7a8be4 100644 --- a/src/util/config.test.ts +++ b/src/config.test.ts @@ -4,10 +4,12 @@ const exit = jest import { Writable } from "stream"; import winston from "winston"; +import loggerConfig from "./util/logger"; let output = ""; +const logger = winston.createLogger(loggerConfig("TEST", "debug")); -jest.mock("./logger", () => { +jest.mock("./util/logger", () => { const stream = new Writable(); stream._write = (chunk, _encoding, next) => { output = output += chunk.toString(); @@ -45,8 +47,8 @@ import { import { Server } from "http"; import path from "path"; import fs from "fs"; -import * as templates from "../templates"; -import { EnvConfig } from "../metadata"; +import * as templates from "./templates"; +import { EnvConfig } from "./metadata"; const CommonMocks = { connect: jest.fn(), @@ -164,7 +166,7 @@ describe("initializeToken", () => { output = ""; // Act - const result = initializeToken(); + const result = initializeToken(logger); // Assert expect(result).toMatch(/([a-f0-9]{8}(-[a-f0-9]{4}){4}[a-f0-9]{8})/); @@ -183,7 +185,7 @@ describe("configOrError", () => { // Act let result; try { - result = configOrError("APPLESAUCE"); + result = configOrError("APPLESAUCE", logger); } catch (err) { // } @@ -198,7 +200,7 @@ describe("configOrError", () => { process.env.CHRYSANTHEMUM = "hello"; // Act - const result = configOrError("CHRYSANTHEMUM"); + const result = configOrError("CHRYSANTHEMUM", logger); // Assert expect(result).toEqual(process.env.CHRYSANTHEMUM); @@ -224,7 +226,7 @@ describe("persistTemplate", () => { ); const fsAccess = jest.spyOn(fs.promises, "access").mockResolvedValue(); - await persistTemplate(template); + await persistTemplate(template, logger); expect(processTemplate).toHaveBeenCalledWith(template); expect(fsAccess).not.toHaveBeenCalled(); @@ -244,7 +246,7 @@ describe("persistTemplate", () => { .mockRejectedValue("baa"); const fsAccess = jest.spyOn(fs.promises, "access").mockResolvedValue(); - await persistTemplate(template); + await persistTemplate(template, logger); expect(processTemplate).toHaveBeenCalledWith(template); expect(fsAccess).toHaveBeenCalledWith( @@ -267,7 +269,7 @@ describe("persistTemplate", () => { .mockRejectedValue("baz"); const fsAccess = jest.spyOn(fs.promises, "access").mockRejectedValue("bar"); - await persistTemplate(template); + await persistTemplate(template, logger); expect(processTemplate).toHaveBeenCalledWith(template); expect(fsAccess).toHaveBeenCalledWith( @@ -288,9 +290,10 @@ describe("handleStartup", () => { const config = { hostDir: "/apple", publicDir: "/public", + targetUrl: "localhost" } as EnvConfig; const confStartup = (): Promise<MongoClient> => - handleStartup("", config, "localhost"); + handleStartup(config, logger); it("should pass back MongoClient", async () => { const superClient = {} as MongoClient; @@ -429,7 +432,7 @@ describe("handleShutdown", () => { // Act try { - await handleShutdown(mongo, server)("SIGINT"); + await handleShutdown(mongo, server, logger)("SIGINT"); } catch (err) { // } @@ -445,7 +448,7 @@ describe("handleShutdown", () => { // Act try { - await handleShutdown(mongo, server)("SIGINT"); + await handleShutdown(mongo, server, logger)("SIGINT"); } catch (err) { // } @@ -461,7 +464,7 @@ describe("handleShutdown", () => { // Act try { - await handleShutdown(mongo, server)("SIGINT"); + await handleShutdown(mongo, server, logger)("SIGINT"); } catch (err) { // } diff --git a/src/util/config.ts b/src/config.ts index c8639fb..e836552 100644 --- a/src/util/config.ts +++ b/src/config.ts @@ -5,13 +5,13 @@ 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"; +import processTemplate, { Template } from "./templates"; +import { EnvConfig } from "./metadata"; -const logger = winston.createLogger(loggerConfig("ROOT")); - -export const initializeToken = (): string => { +/** + * Generate a token for use as the user self-identifier + */ +export const initializeToken = (logger: winston.Logger): string => { //TODO check for token in hostDir/persist created token in hostDir so it's not regenerated on startup const newToken = uuid(); @@ -26,7 +26,10 @@ export const initializeToken = (): string => { return newToken; }; -export const configOrError = (varName: string): string => { +/** + * Get environment variable or exit application if it doesn't exist + */ +export const configOrError = (varName: string, logger: winston.Logger): string => { const value = process.env[varName]; if (value !== undefined) { return value; @@ -36,7 +39,10 @@ export const configOrError = (varName: string): string => { } }; -export const persistTemplate = async (input: Template): Promise<void> => { +/** + * Process a template and persist based on template. + */ +export const persistTemplate = async (input: Template, logger: winston.Logger): Promise<void> => { try { const template = await processTemplate(input); logger.debug("Generated '%s' from template file", template.outputFile); @@ -62,19 +68,23 @@ export const persistTemplate = async (input: Template): Promise<void> => { } }; -export const handleStartup = async ( - mongoUri: string, - config: EnvConfig, - targetUrl: string -): Promise<MongoClient> => { +/** + * Handle server start-up functions: + * + * - Open database connection + * - Generate documents from templates based on configuration + * + * If one of these actions cannot be performed, it will call `process.exit(1)`. + */ +export const handleStartup = async (config: EnvConfig, logger: winston.Logger): Promise<MongoClient> => { try { - const { hostDir, publicDir } = config; + const { hostDir, publicDir, dbUri, targetUrl } = 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) => + const mongo = await MongoClient.connect(dbUri).catch((err: MongoError) => Promise.reject(err.message ?? "Unable to connect to database") ); @@ -82,7 +92,7 @@ export const handleStartup = async ( inputFile: path.join(publicDir, "templates", "sh.tmpl"), outputFile: path.join(hostDir, "sh"), context: { TARGET_URL: targetUrl }, - } as Template); + } as Template, logger); await persistTemplate({ inputFile: path.join(publicDir, "templates", "index.html.tmpl"), outputFile: path.join(hostDir, "index.html"), @@ -92,7 +102,7 @@ export const handleStartup = async ( ? "--proto '=https' --tlsv1.2 " : "", }, - } as Template); + } as Template, logger); return mongo; } catch (err) { @@ -101,22 +111,27 @@ export const handleStartup = async ( } }; +/** + * Callback for NodeJS `process.on()` to handle shutdown signals + * and close open connections. + */ export const handleShutdown = - (mongo: MongoClient, server: Server) => - (signal: NodeJS.Signals): Promise<void> => { + (mongo: MongoClient, server: Server, logger: winston.Logger) => + async (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)); + try { + await mongo.close(); + logger.info("MongoDB client connection closed."); + + // must await for callback - wrapped in Promise + await new Promise((res, rej) => server.close((err) => { + logger.info("Express down."); + (err ? rej : res)(err); + })); + + process.exit(0); + } catch (e) { + process.exit(1); + } }; diff --git a/src/index.ts b/src/index.ts index 1de4b10..d101ba3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,6 @@ import winston from "winston"; import expressWinston from "express-winston"; import path from "path"; -dotenv.config(); - import routes from "./routes"; import Metadata, { EnvConfig } from "./metadata"; import loggerConfig from "./util/logger"; @@ -14,32 +12,45 @@ import { handleStartup, handleShutdown, initializeToken, -} 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 MONGO_URI = configOrError("MONGO_URI"); -const TARGET_URL = process.env.TARGET_URL ?? "http://localhost:3000"; +} from "./config"; + +dotenv.config(); + +const LOG_LEVEL = process.env.LOG_LEVEL ?? "info"; +const logger = winston.createLogger(loggerConfig("ROOT", LOG_LEVEL)); + +// Application-wide configuration settings const ENV_CONFIG: EnvConfig = { - token: process.env.TOKEN ?? initializeToken(), + // Express configuration + bindAddress: process.env.BIND_ADDRESS ?? "localhost", + port: Number(process.env.PORT ?? 3000), uploadLimit: Number(process.env.UPLOAD_LIMIT ?? 4194304), + + // Database connection information + dbName: process.env.MONGO_DB ?? "ao-coverage", + dbUri: configOrError("MONGO_URI", logger), + + // Where the server should say it's located + targetUrl: process.env.TARGET_URL ?? "http://localhost:3000", + + // Directories used for serving static or uploaded files publicDir: path.join(__dirname, "..", "public"), - hostDir: configOrError("HOST_DIR"), + hostDir: configOrError("HOST_DIR", logger), + + // Application configuration + token: process.env.TOKEN ?? initializeToken(logger), stage1: Number(process.env.STAGE_1 ?? 95), stage2: Number(process.env.STAGE_2 ?? 80), + logLevel: LOG_LEVEL, }; -const logger = winston.createLogger(loggerConfig("ROOT")); - -handleStartup(MONGO_URI, ENV_CONFIG, TARGET_URL).then((mongo) => { +handleStartup(ENV_CONFIG, logger).then((mongo) => { const app: express.Application = express(); - const metadata = new Metadata(mongo.db(MONGO_DB), ENV_CONFIG); + const metadata = new Metadata(mongo.db(ENV_CONFIG.dbName), ENV_CONFIG); app.use( expressWinston.logger({ - ...loggerConfig("HTTP"), + ...loggerConfig("HTTP", ENV_CONFIG.logLevel), colorize: true, // filter out token query param from URL msg: '{{req.method}} {{req.url.replace(/token=[-\\w.~]*(&*)/, "token=$1")}} - {{res.statusCode}} {{res.responseTime}}ms', @@ -49,15 +60,15 @@ handleStartup(MONGO_URI, ENV_CONFIG, TARGET_URL).then((mongo) => { // actual app routes app.use(routes(metadata)); - app.use(expressWinston.errorLogger(loggerConfig("_ERR"))); + app.use(expressWinston.errorLogger(loggerConfig("_ERR", ENV_CONFIG.logLevel))); - const server = app.listen(PORT, BIND_ADDRESS, () => { - logger.info("Express has started: http://%s:%d/", BIND_ADDRESS, PORT); + const server = app.listen(ENV_CONFIG.port, ENV_CONFIG.bindAddress, () => { + logger.info("Express has started: http://%s:%d/", ENV_CONFIG.bindAddress, ENV_CONFIG.port); }); // application exit handling const signalCodes: NodeJS.Signals[] = ["SIGTERM", "SIGHUP", "SIGINT"]; signalCodes.map((code: NodeJS.Signals) => { - process.on(code, handleShutdown(mongo, server)); + process.on(code, handleShutdown(mongo, server, logger)); }); }); diff --git a/src/metadata.ts b/src/metadata.ts index 3cba605..6f6d401 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -10,12 +10,6 @@ interface HeadContext { format: string; } -export const isError = ( - obj: HeadContext | BranchNotFoundError -): obj is BranchNotFoundError => { - return Object.keys(obj).includes("name"); -}; - interface Branch { head: HeadContext | string; } @@ -37,49 +31,62 @@ export interface Repository { branches: BranchList; } -const logger = winston.createLogger(loggerConfig("META")); - +/** + * Holds environment configuration items for the application + */ export interface EnvConfig { - token: string; + /** Express value to bind to a given address */ + bindAddress: string; + /** Express value for port */ + port: number; + /** Express value to limit uploads to server */ uploadLimit: number; + /** Configuration for the database name */ + dbName: string; + /** Configuration for the database URI */ + dbUri: string; + /** The address given for communicating back to the server */ + targetUrl: string; + /** The host directory for uploaded files */ hostDir: string; + /** The public directory for static files */ publicDir: string; + /** The application server token to serve as user self-identifier */ + token: string; + /** Gradient setting 1 */ stage1: number; + /** Gradient setting 2 */ stage2: number; + /** Log level across application */ + logLevel: string; } +/** + * Check if provided response is a known application error + */ +export const isError = ( + obj: HeadContext | BranchNotFoundError +): obj is BranchNotFoundError => { + return Object.keys(obj).includes("name"); +}; + +/** + * Handles data routing for application + */ class Metadata { database: Db; config: EnvConfig; + logger: winston.Logger; constructor(client: Db, data: EnvConfig) { + this.logger = winston.createLogger(loggerConfig("META", data.logLevel)); this.database = client; this.config = data; } - getToken(): string { - return this.config.token; - } - - getUploadLimit(): number { - return this.config.uploadLimit; - } - - getHostDir(): string { - return this.config.hostDir; - } - - getPublicDir(): string { - return this.config.publicDir; - } - - getGradientStyle(): GradientStyle { - return { - stage1: this.config.stage1, - stage2: this.config.stage2, - }; - } - + /** + * Retrieve the latest commit to the given branch + */ async getHeadCommit( organization: string, repository: string, @@ -98,7 +105,7 @@ class Metadata { const head = typeof limb.head === "string" ? limb.head : limb.head.commit; const format = typeof limb.head === "string" ? "tarpaulin" : limb.head.format; - logger.debug( + this.logger.debug( "Found commit %s for ORB %s/%s/%s (format %s)", head, organization, @@ -112,6 +119,9 @@ class Metadata { } } + /** + * Update the database with the latest commit to a branch + */ async updateBranch(identity: HeadIdentity): Promise<boolean> { const { organization, repository: name, branch, head } = identity; const result = await this.database @@ -128,6 +138,9 @@ class Metadata { return result.ok === 1; } + /** + * Add a repository metadata document to the database + */ async createRepository(identity: HeadIdentity): Promise<boolean> { const { organization, repository: name, branch, head } = identity; const repo: Repository = { @@ -142,6 +155,45 @@ class Metadata { return result.acknowledged; } + + /** + * Retrieve the application token from configuration + */ + getToken(): string { + return this.config.token; + } + + /** + * Retrieve the upload limit for files from configuration + */ + getUploadLimit(): number { + return this.config.uploadLimit; + } + + /** + * Retrieve the host for uploaded documents directory from configuration + */ + getHostDir(): string { + return this.config.hostDir; + } + + /** + * Retrieve the public static file directory from configuration + */ + getPublicDir(): string { + return this.config.publicDir; + } + + /** + * Retrieve the gradient style from configuration + */ + getGradientStyle(): GradientStyle { + return { + stage1: this.config.stage1, + stage2: this.config.stage2, + }; + } + } export default Metadata; diff --git a/src/routes.test.ts b/src/routes.test.ts index 12210bf..5d7839c 100644 --- a/src/routes.test.ts +++ b/src/routes.test.ts @@ -8,6 +8,8 @@ import express from "express"; import dotenv from "dotenv"; import fs from "fs"; import path from "path"; +import { Writable } from "stream"; +import winston from "winston"; dotenv.config(); @@ -23,9 +25,6 @@ test("HOST_DIR must be readable and writable", () => { ).not.toThrowError(); }); -import { Writable } from "stream"; -import winston from "winston"; - let output = ""; jest.mock("./util/logger", () => { @@ -48,7 +47,8 @@ jest.mock("./util/logger", () => { }; }); -import { configOrError, persistTemplate } from "./util/config"; +import { configOrError, persistTemplate } from "./config"; +import loggerConfig from "./util/logger"; import routes from "./routes"; import Metadata, { EnvConfig } from "./metadata"; import { Template } from "./templates"; @@ -57,6 +57,7 @@ import { badgen } from "badgen"; import { BranchNotFoundError } from "./errors"; type MetadataMockType = { + logger: winston.Logger; database: Db; config: EnvConfig; getHeadCommit: jest.Mock; @@ -68,15 +69,22 @@ type MetadataMockType = { updateBranch: jest.Mock; createRepository: jest.Mock; }; +const logger = winston.createLogger(loggerConfig("TEST", "debug")); const config = { token: "THISISCORRECT", // should be just larger than the example report used uploadLimit: Number(40000), - hostDir: configOrError("HOST_DIR"), + hostDir: configOrError("HOST_DIR", logger), publicDir: path.join(__dirname, "..", "public"), stage1: 95, stage2: 80, + bindAddress: "localhost", + targetUrl: "http://localhost:3000/", + port: 3000, + dbName: "ao-coverage", + dbUri: "localhost", + logLevel: "debug", }; const mock = ( headCommit: jest.Mock = jest.fn( @@ -85,6 +93,7 @@ const mock = ( ), updateBranch: jest.Mock = jest.fn(() => new Promise((solv) => solv(true))) ): MetadataMockType => ({ + logger, database: {} as Db, config: config, getToken: jest.fn(() => config.token), @@ -109,7 +118,7 @@ const request = async ( return _request(app); }; -const HOST_DIR = configOrError("HOST_DIR"); +const HOST_DIR = configOrError("HOST_DIR", logger); const TARGET_URL = "https://localhost:3000"; describe("templates", () => { @@ -119,7 +128,7 @@ describe("templates", () => { inputFile: path.join(__dirname, "..", "public", "templates", "sh.tmpl"), outputFile: path.join(HOST_DIR, "sh"), context: { TARGET_URL }, - } as Template); + } as Template, logger); const res = await (await request()).get("/sh").expect(200); expect(exit).not.toHaveBeenCalled(); @@ -140,7 +149,7 @@ describe("templates", () => { ), outputFile: path.join(HOST_DIR, "index.html"), context: { TARGET_URL, CURL_HTTPS: "--https " }, - } as Template); + } as Template, logger); const res = await (await request()) .get("/") diff --git a/src/routes.ts b/src/routes.ts index a8360ca..0998345 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -9,11 +9,16 @@ import Metadata, { HeadIdentity, isError } from "./metadata"; import loggerConfig from "./util/logger"; import { InvalidReportDocumentError, Messages } from "./errors"; -const logger = winston.createLogger(loggerConfig("HTTP")); - -export default (metadata: Metadata): Router => { +/** + * Provide routes from application state + */ +const routes = (metadata: Metadata): Router => { const router = Router(); + const logger = winston.createLogger(loggerConfig("HTTP", metadata.logger.level)); + /** + * Persist uploaded coverage report and creates coverage badge + */ const commitFormatDocs = async ( contents: string, identity: HeadIdentity, @@ -138,6 +143,9 @@ export default (metadata: Metadata): Router => { }); }); + /** + * Read a file from the host directory. + */ const retrieveFile = ( res: express.Response, identity: HeadIdentity, @@ -238,6 +246,7 @@ export default (metadata: Metadata): Router => { retrieveFile(res, identity, format.fileName); }); + // provide hard link for commit router.get("/v1/:org/:repo/:branch/:commit.xml", (req, res) => { const { org, repo, branch, commit } = req.params; const format = formats.formats.cobertura; @@ -250,6 +259,7 @@ export default (metadata: Metadata): Router => { retrieveFile(res, identity, format.fileName); }); + // return 404 for all other routes router.use((_, res) => { res.status(404); res.sendFile(path.join(metadata.getPublicDir(), "static", "404.html")); @@ -257,3 +267,5 @@ export default (metadata: Metadata): Router => { return router; }; + +export default routes; diff --git a/src/templates.ts b/src/templates.ts index 648bb56..26451cf 100644 --- a/src/templates.ts +++ b/src/templates.ts @@ -1,6 +1,9 @@ import handlebars from "handlebars"; import fs from "fs"; +/** + * Information for processing a template file into the output file + */ export interface Template { inputFile: string; outputFile: string; @@ -8,6 +11,9 @@ export interface Template { data?: string; } +/** + * Process input file and produce file at given output location + */ export default async (_template: Template): Promise<Template> => { const buffer = await fs.promises.readFile(_template.inputFile, "utf-8"); 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; |
