diff options
| author | Kevin J Hoerr <kjhoerr@protonmail.com> | 2022-04-07 06:13:09 +0000 |
|---|---|---|
| committer | Kevin J Hoerr <kjhoerr@protonmail.com> | 2022-04-07 06:13:09 +0000 |
| commit | 4af6310a9c42fbc81ab82d6253becf1f3fdeebac (patch) | |
| tree | 26cb1d976912a32879121746500bb9a69417a885 | |
| parent | 4ba501e2caea4d6dc483ae7f8779031810700228 (diff) | |
| download | ao-coverage-4af6310a9c42fbc81ab82d6253becf1f3fdeebac.tar.gz ao-coverage-4af6310a9c42fbc81ab82d6253becf1f3fdeebac.tar.bz2 ao-coverage-4af6310a9c42fbc81ab82d6253becf1f3fdeebac.zip | |
#17 Move TOKEN from EnvConfig to database
26 files changed, 440 insertions, 317 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json index 1ab3567..957ff80 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,6 +15,21 @@ "outFiles": [ "${workspaceFolder}/**/*.js" ] + }, + { + "type": "node", + "name": "vscode-jest-tests", + "request": "launch", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "cwd": "/workspace/src", + "runtimeExecutable": "yarn", + "args": [ + "jest", + "--runInBand", + "--watchAll=false" + ] } ] }
\ No newline at end of file diff --git a/.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.9-7ef8e73557-1b9c4c87a6.zip b/.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.9-7ef8e73557-1b9c4c87a6.zip Binary files differnew file mode 100644 index 0000000..0af20fc --- /dev/null +++ b/.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.9-7ef8e73557-1b9c4c87a6.zip diff --git a/.yarn/cache/@types-bcrypt-npm-5.0.0-c074c165c2-063c32c7a5.zip b/.yarn/cache/@types-bcrypt-npm-5.0.0-c074c165c2-063c32c7a5.zip Binary files differnew file mode 100644 index 0000000..24c07c9 --- /dev/null +++ b/.yarn/cache/@types-bcrypt-npm-5.0.0-c074c165c2-063c32c7a5.zip diff --git a/.yarn/cache/aproba-npm-2.0.0-8716bcfde6-5615cadcfb.zip b/.yarn/cache/aproba-npm-2.0.0-8716bcfde6-5615cadcfb.zip Binary files differnew file mode 100644 index 0000000..6b14888 --- /dev/null +++ b/.yarn/cache/aproba-npm-2.0.0-8716bcfde6-5615cadcfb.zip diff --git a/.yarn/cache/are-we-there-yet-npm-2.0.0-7d2f5201ce-6c80b4fd04.zip b/.yarn/cache/are-we-there-yet-npm-2.0.0-7d2f5201ce-6c80b4fd04.zip Binary files differnew file mode 100644 index 0000000..41d8c66 --- /dev/null +++ b/.yarn/cache/are-we-there-yet-npm-2.0.0-7d2f5201ce-6c80b4fd04.zip diff --git a/.yarn/cache/bcrypt-npm-5.0.1-6815be1cfe-b59625519f.zip b/.yarn/cache/bcrypt-npm-5.0.1-6815be1cfe-b59625519f.zip Binary files differnew file mode 100644 index 0000000..7f16cd9 --- /dev/null +++ b/.yarn/cache/bcrypt-npm-5.0.1-6815be1cfe-b59625519f.zip diff --git a/.yarn/cache/color-support-npm-1.1.3-3be5c53455-9b73568176.zip b/.yarn/cache/color-support-npm-1.1.3-3be5c53455-9b73568176.zip Binary files differnew file mode 100644 index 0000000..625a79f --- /dev/null +++ b/.yarn/cache/color-support-npm-1.1.3-3be5c53455-9b73568176.zip diff --git a/.yarn/cache/detect-libc-npm-2.0.1-2699cb2ac4-ccb05fcabb.zip b/.yarn/cache/detect-libc-npm-2.0.1-2699cb2ac4-ccb05fcabb.zip Binary files differnew file mode 100644 index 0000000..f4bfff0 --- /dev/null +++ b/.yarn/cache/detect-libc-npm-2.0.1-2699cb2ac4-ccb05fcabb.zip diff --git a/.yarn/cache/gauge-npm-3.0.2-9e22f7af9e-81296c00c7.zip b/.yarn/cache/gauge-npm-3.0.2-9e22f7af9e-81296c00c7.zip Binary files differnew file mode 100644 index 0000000..92db251 --- /dev/null +++ b/.yarn/cache/gauge-npm-3.0.2-9e22f7af9e-81296c00c7.zip diff --git a/.yarn/cache/node-addon-api-npm-3.2.1-a29528f81d-2369986bb0.zip b/.yarn/cache/node-addon-api-npm-3.2.1-a29528f81d-2369986bb0.zip Binary files differnew file mode 100644 index 0000000..038beb4 --- /dev/null +++ b/.yarn/cache/node-addon-api-npm-3.2.1-a29528f81d-2369986bb0.zip diff --git a/.yarn/cache/node-fetch-npm-2.6.7-777aa2a6df-8d816ffd1e.zip b/.yarn/cache/node-fetch-npm-2.6.7-777aa2a6df-8d816ffd1e.zip Binary files differnew file mode 100644 index 0000000..db222e2 --- /dev/null +++ b/.yarn/cache/node-fetch-npm-2.6.7-777aa2a6df-8d816ffd1e.zip diff --git a/.yarn/cache/npmlog-npm-5.0.1-366cab64a2-516b266302.zip b/.yarn/cache/npmlog-npm-5.0.1-366cab64a2-516b266302.zip Binary files differnew file mode 100644 index 0000000..d2eec07 --- /dev/null +++ b/.yarn/cache/npmlog-npm-5.0.1-366cab64a2-516b266302.zip diff --git a/.yarn/cache/string-width-npm-4.2.3-2c27177bae-e52c10dc3f.zip b/.yarn/cache/string-width-npm-4.2.3-2c27177bae-e52c10dc3f.zip Binary files differnew file mode 100644 index 0000000..9b4c088 --- /dev/null +++ b/.yarn/cache/string-width-npm-4.2.3-2c27177bae-e52c10dc3f.zip diff --git a/.yarn/cache/tr46-npm-0.0.3-de53018915-726321c5ea.zip b/.yarn/cache/tr46-npm-0.0.3-de53018915-726321c5ea.zip Binary files differnew file mode 100644 index 0000000..2e6949b --- /dev/null +++ b/.yarn/cache/tr46-npm-0.0.3-de53018915-726321c5ea.zip diff --git a/.yarn/cache/webidl-conversions-npm-3.0.1-60310f6a2b-c92a0a6ab9.zip b/.yarn/cache/webidl-conversions-npm-3.0.1-60310f6a2b-c92a0a6ab9.zip Binary files differnew file mode 100644 index 0000000..96867a6 --- /dev/null +++ b/.yarn/cache/webidl-conversions-npm-3.0.1-60310f6a2b-c92a0a6ab9.zip diff --git a/.yarn/cache/whatwg-url-npm-5.0.0-374fb45e60-b8daed4ad3.zip b/.yarn/cache/whatwg-url-npm-5.0.0-374fb45e60-b8daed4ad3.zip Binary files differnew file mode 100644 index 0000000..5deef33 --- /dev/null +++ b/.yarn/cache/whatwg-url-npm-5.0.0-374fb45e60-b8daed4ad3.zip diff --git a/.yarn/cache/wide-align-npm-1.1.5-889d77e592-d5fc37cd56.zip b/.yarn/cache/wide-align-npm-1.1.5-889d77e592-d5fc37cd56.zip Binary files differnew file mode 100644 index 0000000..4dc7fcc --- /dev/null +++ b/.yarn/cache/wide-align-npm-1.1.5-889d77e592-d5fc37cd56.zip diff --git a/package.json b/package.json index 2c1d74e..01d60c5 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "license": "Parity-7.0.0", "dependencies": { "badgen": "^3.2.2", + "bcrypt": "^5.0.1", "bson": "^4.6.2", "dotenv": "^16.0.0", "express": "^4.17.3", @@ -39,6 +40,7 @@ }, "devDependencies": { "@microsoft/tsdoc": "^0.13.2", + "@types/bcrypt": "^5.0.0", "@types/express": "^4.17.13", "@types/jest": "^27.4.1", "@types/jsdom": "^16.2.14", diff --git a/src/config.test.ts b/src/config.test.ts index b7c869f..456a6e3 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -2,177 +2,34 @@ const exit = jest .spyOn(process, "exit") .mockImplementation(() => undefined as never); -import { Writable } from "stream"; import winston from "winston"; -import loggerConfig from "./util/logger"; - -let output = ""; -const logger = winston.createLogger(loggerConfig("TEST", "debug")); - -jest.mock("./util/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 { MongoClient, MongoError } from "mongodb"; import { Server } from "http"; import path from "path"; import fs from "fs"; + +import loggerConfig from "./util/logger"; 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 = ""; +import Metadata, { EnvConfig } from "./metadata"; - // Act - const result = initializeToken(logger); +jest.mock("./util/logger", () => ({ + __esModule: true, + default: () => ({ + format: winston.format.combine( + winston.format.splat(), + winston.format.simple() + ), + transports: [new winston.transports.Console({ silent: true })], + }), +})); - // Assert - expect(result).toMatch(/([a-f0-9]{8}(-[a-f0-9]{4}){4}[a-f0-9]{8})/); - expect(output).toContain(result); - }); -}); +const LOGGER = winston.createLogger(loggerConfig("TEST", "debug")); describe("configOrError", () => { beforeEach(() => { @@ -185,7 +42,7 @@ describe("configOrError", () => { // Act let result; try { - result = configOrError("APPLESAUCE", logger); + result = configOrError("APPLESAUCE", LOGGER); } catch (err) { // } @@ -200,7 +57,7 @@ describe("configOrError", () => { process.env.CHRYSANTHEMUM = "hello"; // Act - const result = configOrError("CHRYSANTHEMUM", logger); + const result = configOrError("CHRYSANTHEMUM", LOGGER); // Assert expect(result).toEqual(process.env.CHRYSANTHEMUM); @@ -226,7 +83,7 @@ describe("persistTemplate", () => { ); const fsAccess = jest.spyOn(fs.promises, "access").mockResolvedValue(); - await persistTemplate(template, logger); + await persistTemplate(template, LOGGER); expect(processTemplate).toHaveBeenCalledWith(template); expect(fsAccess).not.toHaveBeenCalled(); @@ -246,7 +103,7 @@ describe("persistTemplate", () => { .mockRejectedValue("baa"); const fsAccess = jest.spyOn(fs.promises, "access").mockResolvedValue(); - await persistTemplate(template, logger); + await persistTemplate(template, LOGGER); expect(processTemplate).toHaveBeenCalledWith(template); expect(fsAccess).toHaveBeenCalledWith( @@ -269,7 +126,7 @@ describe("persistTemplate", () => { .mockRejectedValue("baz"); const fsAccess = jest.spyOn(fs.promises, "access").mockRejectedValue("bar"); - await persistTemplate(template, logger); + await persistTemplate(template, LOGGER); expect(processTemplate).toHaveBeenCalledWith(template); expect(fsAccess).toHaveBeenCalledWith( @@ -290,40 +147,13 @@ describe("handleStartup", () => { const config = { hostDir: "/apple", publicDir: "/public", + dbUri: "localhost:27017", + dbName: "bongo", targetUrl: "localhost", + logLevel: "trace", } as EnvConfig; - const confStartup = (): Promise<MongoClient> => handleStartup(config, logger); - - 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(); - }); + const confStartup = (): Promise<Metadata> => + handleStartup(config, undefined, LOGGER); it("should exit if HOST_DIR is not read/write accessible", async () => { const superClient = {} as MongoClient; @@ -426,12 +256,18 @@ describe("handleShutdown", () => { it("should exit gracefully without error", async () => { // Arrange - const mongo = MongoMock(new Promise((r) => r()))(); - const server = ServerMock(undefined)(); + const metadata = { + close: () => Promise.resolve(), + } as Metadata; + const server = { + close: (callback?: ((err?: Error | undefined) => void) | undefined) => { + callback !== undefined && callback(); + }, + } as Server; // Act try { - await handleShutdown(mongo, server, logger)("SIGINT"); + await handleShutdown(metadata, server, LOGGER)("SIGINT"); } catch (err) { // } @@ -440,14 +276,20 @@ describe("handleShutdown", () => { expect(exit).toHaveBeenCalledWith(0); }); - it("should exit with error with Mongo error", async () => { + it("should exit with error with Metadata error", async () => { // Arrange - const mongo = MongoMock(new Promise((_, r) => r()))(); - const server = ServerMock(undefined)(); + const metadata = { + close: () => Promise.reject(), + } as Metadata; + const server = { + close: (callback?: ((err?: Error | undefined) => void) | undefined) => { + callback !== undefined && callback(); + }, + } as Server; // Act try { - await handleShutdown(mongo, server, logger)("SIGINT"); + await handleShutdown(metadata, server, LOGGER)("SIGINT"); } catch (err) { // } @@ -458,12 +300,18 @@ describe("handleShutdown", () => { it("should exit with error with Server error", async () => { // Arrange - const mongo = MongoMock(new Promise((r) => r()))(); - const server = ServerMock(Error("oh noooo"))(); + const metadata = { + close: () => Promise.resolve(), + } as Metadata; + const server = { + close: (callback?: ((err?: Error | undefined) => void) | undefined) => { + callback !== undefined && callback(Error("NOOO")); + }, + } as Server; // Act try { - await handleShutdown(mongo, server, logger)("SIGINT"); + await handleShutdown(metadata, server, LOGGER)("SIGINT"); } catch (err) { // } diff --git a/src/config.ts b/src/config.ts index 1908c10..4f4a970 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,28 +3,9 @@ import { MongoClient, MongoError } from "mongodb"; import { Server } from "http"; import path from "path"; import fs from "fs"; -import { v4 as uuid } from "uuid"; import processTemplate, { Template } from "./templates"; -import { EnvConfig } from "./metadata"; - -/** - * 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(); - - 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; -}; +import Metadata, { EnvConfig } from "./metadata"; /** * Get environment variable or exit application if it doesn't exist @@ -84,8 +65,9 @@ export const persistTemplate = async ( */ export const handleStartup = async ( config: EnvConfig, + token: string | undefined, logger: winston.Logger -): Promise<MongoClient> => { +): Promise<Metadata> => { try { const { hostDir, publicDir, dbUri, targetUrl } = config; await fs.promises.access(hostDir, fs.constants.R_OK | fs.constants.W_OK); @@ -119,7 +101,11 @@ export const handleStartup = async ( logger ); - return mongo; + const metadata = new Metadata(mongo, config); + + await metadata.initializeToken(token); + + return metadata; } catch (err) { logger.error("Error occurred during startup: %s", err); process.exit(1); @@ -131,13 +117,12 @@ export const handleStartup = async ( * and close open connections. */ export const handleShutdown = - (mongo: MongoClient, server: Server, logger: winston.Logger) => + (metadata: Metadata, server: Server, logger: winston.Logger) => async (signal: NodeJS.Signals): Promise<void> => { logger.warn("%s signal received. Closing shop.", signal); try { - await mongo.close(); - logger.info("MongoDB client connection closed."); + await metadata.close(); // must await for callback - wrapped in Promise await new Promise((res, rej) => diff --git a/src/index.ts b/src/index.ts index ae15509..dcfc011 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,14 +5,9 @@ import expressWinston from "express-winston"; import path from "path"; import routes from "./routes"; -import Metadata, { EnvConfig } from "./metadata"; +import { EnvConfig } from "./metadata"; import loggerConfig from "./util/logger"; -import { - configOrError, - handleStartup, - handleShutdown, - initializeToken, -} from "./config"; +import { configOrError, handleStartup, handleShutdown } from "./config"; dotenv.config(); @@ -38,15 +33,13 @@ const ENV_CONFIG: EnvConfig = { 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, }; -handleStartup(ENV_CONFIG, logger).then((mongo) => { +handleStartup(ENV_CONFIG, process.env.TOKEN, logger).then((metadata) => { const app: express.Application = express(); - const metadata = new Metadata(mongo.db(ENV_CONFIG.dbName), ENV_CONFIG); app.use( expressWinston.logger({ @@ -75,6 +68,6 @@ handleStartup(ENV_CONFIG, logger).then((mongo) => { // application exit handling const signalCodes: NodeJS.Signals[] = ["SIGTERM", "SIGHUP", "SIGINT"]; signalCodes.map((code: NodeJS.Signals) => { - process.on(code, handleShutdown(mongo, server, logger)); + process.on(code, handleShutdown(metadata, server, logger)); }); }); diff --git a/src/metadata.test.ts b/src/metadata.test.ts index 439bf7e..62a235c 100644 --- a/src/metadata.test.ts +++ b/src/metadata.test.ts @@ -1,18 +1,44 @@ -import Metadata, { isError, EnvConfig, HeadIdentity } from "./metadata"; import { Db, MongoClient, Collection } from "mongodb"; +import { Writable } from "stream"; +import winston from "winston"; import { BranchNotFoundError } from "./errors"; +import Metadata, { isError, EnvConfig, HeadIdentity } from "./metadata"; jest.mock("mongodb"); +let output = ""; +jest.mock("./util/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], + }), + }; +}); + const defaultEnvConfig = { - token: "llama", uploadLimit: 12, hostDir: "pineapple", publicDir: ".dir", stage1: 132, stage2: 1.0, }; -const defaultMetadata = () => - new Metadata(new Db({} as MongoClient, ""), defaultEnvConfig as EnvConfig); +const defaultMetadata = () => { + jest + .spyOn(MongoClient.prototype, "db") + .mockImplementation(() => new Db({} as MongoClient, "")); + return new Metadata(new MongoClient(""), defaultEnvConfig as EnvConfig); +}; describe("isError", () => { it("should return false when object is a HeadContext", () => { @@ -372,16 +398,34 @@ describe("createRepository", () => { }); }); -describe("getToken", () => { - it("should return the token from EnvConfig", () => { +describe("initializeToken", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("Should generate a UUID", async () => { // Arrange const metadata = defaultMetadata(); + const collectionMethod = jest + .spyOn(Db.prototype, "collection") + .mockImplementation(() => new Collection()); + const countMethod = jest + .spyOn(Collection.prototype, "countDocuments") + .mockImplementation(() => Promise.resolve(0)); + const replaceMethod = jest + .spyOn(Collection.prototype, "findOneAndReplace") + .mockImplementation(() => Promise.resolve({ ok: 1 })); + output = ""; // Act - const result = metadata.getToken(); + const result = await metadata.initializeToken(); // Assert - expect(result).toEqual(defaultEnvConfig.token); + expect(result).toEqual(true); + expect(output).toMatch(/([a-f0-9]{8}(-[a-f0-9]{4}){4}[a-f0-9]{8})/); + expect(collectionMethod).toHaveBeenCalledTimes(2); + expect(countMethod).toHaveBeenCalledTimes(1); + expect(replaceMethod).toHaveBeenCalledTimes(1); }); }); diff --git a/src/metadata.ts b/src/metadata.ts index 184580f..8899e37 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -1,5 +1,7 @@ -import { Db } from "mongodb"; +import { Db, MongoClient } from "mongodb"; import winston from "winston"; +import bcrypt from "bcrypt"; +import { v4 as uuid } from "uuid"; import loggerConfig from "./util/logger"; import { BranchNotFoundError } from "./errors"; @@ -18,6 +20,10 @@ interface BranchList { [branch: string]: Branch; } +interface SystemConfig { + tokenHashed: string; +} + export interface HeadIdentity { organization: string; repository: string; @@ -51,8 +57,6 @@ export interface EnvConfig { 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 */ @@ -74,14 +78,21 @@ export const isError = ( * Handles data routing for application */ class Metadata { - database: Db; + private dbClient: MongoClient; + private database: Db; config: EnvConfig; logger: winston.Logger; - constructor(client: Db, data: EnvConfig) { - this.logger = winston.createLogger(loggerConfig("META", data.logLevel)); - this.database = client; + constructor(client: MongoClient, data: EnvConfig) { + this.dbClient = client; + this.database = client.db(data.dbName); this.config = data; + this.logger = winston.createLogger(loggerConfig("META", data.logLevel)); + } + + async close(): Promise<void> { + await this.dbClient.close(); + this.logger.info("Database client connection closed."); } /** @@ -157,10 +168,60 @@ class Metadata { } /** - * Retrieve the application token from configuration + * Check whether the provided token matches the hashed token */ - getToken(): string { - return this.config.token; + async checkToken(token: string): Promise<boolean> { + const result = await this.database + .collection<SystemConfig>("sysconfig") + .findOne({}); + + if (result !== null) { + return bcrypt.compare(token, result.tokenHashed); + } else { + return Promise.reject(Error("No system configuration in place")); + } + } + + /** + * Generate a token for use as the user self-identifier. + * + * If the token is passed after it already exists, it will be overwritten. + */ + async initializeToken(token?: string | undefined): Promise<boolean> { + const config = await this.database + .collection<SystemConfig>("sysconfig") + .countDocuments(); + + if (config > 0 && token === undefined) { + return true; + } + + const useToken = + token === undefined + ? (() => { + const newToken = uuid(); + + this.logger.warn( + "TOKEN variable not provided, using this value instead: %s", + newToken + ); + this.logger.warn( + "Use this provided token to push your coverage reports to the server." + ); + + return newToken; + })() + : token; + + const sysconfig = { + tokenHashed: await bcrypt.hash(useToken, 10), + }; + + const result = await this.database + .collection<SystemConfig>("sysconfig") + .findOneAndReplace({}, sysconfig, { upsert: true }); + + return result.ok === 1; } /** diff --git a/src/routes.test.ts b/src/routes.test.ts index f151d79..188cd13 100644 --- a/src/routes.test.ts +++ b/src/routes.test.ts @@ -24,7 +24,6 @@ jest.mock("./util/logger", () => ({ import loggerConfig from "./util/logger"; import dotenv from "dotenv"; -import { Db } from "mongodb"; dotenv.config(); const LOGGER = winston.createLogger(loggerConfig("TEST", "debug")); @@ -33,7 +32,7 @@ const HOST_DIR = (() => { const dir = path.join(__dirname, "..", "dist"); console.warn( - `WARNING: HOST_DIR is not set - this is used to query files in src/routes.test.ts. Using '${dir}' as default HOST_DIR.` + `HOST_DIR is not set - this is used to query files in src/routes.test.ts. Using '${dir}' as default HOST_DIR.` ); return dir; })(); @@ -41,7 +40,6 @@ const TARGET_URL = "https://localhost:3000"; const TOKEN = "THISISCORRECT"; const config: EnvConfig = { - token: TOKEN, // should be just larger than the example report used uploadLimit: Number(40000), hostDir: HOST_DIR, @@ -58,9 +56,7 @@ const config: EnvConfig = { const defaultMetadata = { logger: LOGGER, - database: {} as Db, config: config, - getToken: () => config.token, getUploadLimit: () => config.uploadLimit, getHostDir: () => config.hostDir, getPublicDir: () => config.publicDir, @@ -79,11 +75,14 @@ const defaultMetadata = { ), updateBranch: jest.fn(() => Promise.resolve(true)), createRepository: jest.fn(() => Promise.resolve(true)), + checkToken: (token: string) => Promise.resolve(token === TOKEN), + close: jest.fn(), }; const request = async (): Promise<SuperTest<Test>> => { const app = express(); - app.use(routes(defaultMetadata as Metadata)); + // The unknown is dbClient, since it's a private member. It's not used anyways for routes + app.use(routes(defaultMetadata as unknown as Metadata)); return _request(app); }; diff --git a/src/routes.ts b |
