From ab3219d22a8f804d961c17d970527b52328e24a7 Mon Sep 17 00:00:00 2001 From: DevSide Date: Sun, 20 Oct 2024 14:43:41 +0200 Subject: [PATCH] refactor: convert to typescript, move logic inside service --- .github/workflows/ci.yml | 1 + .gitignore | 1 + .npmignore | 6 + package.json | 22 +- .../{integrations.js => integrations.ts} | 16 +- bin/dynamock.js => src/bin/dynamock.ts | 6 +- src/{configuration.js => configuration.ts} | 25 +- src/createServer.js | 185 --------- src/createServer.ts | 140 +++++++ src/fixtures.js | 242 ------------ src/fixtures.ts | 362 ++++++++++++++++++ src/properties.js | 54 --- src/properties.ts | 70 ++++ src/service.ts | 130 +++++++ src/{utils.js => utils.ts} | 25 +- tsconfig.json | 22 ++ yarn.lock | 298 +++++++++++++- 17 files changed, 1095 insertions(+), 510 deletions(-) rename src/__tests__/{integrations.js => integrations.ts} (98%) rename bin/dynamock.js => src/bin/dynamock.ts (78%) rename src/{configuration.js => configuration.ts} (51%) delete mode 100644 src/createServer.js create mode 100644 src/createServer.ts delete mode 100644 src/fixtures.js create mode 100644 src/fixtures.ts delete mode 100644 src/properties.js create mode 100644 src/properties.ts create mode 100644 src/service.ts rename src/{utils.js => utils.ts} (65%) create mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 429725e..ef77f4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: - run: yarn install - run: yarn format - run: yarn lint + - run: yarn build - run: yarn test:bin - run: yarn test --coverage - run: yarn check-git diff --git a/.gitignore b/.gitignore index 4f7b496..ad980b1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ **/node_modules **/*.log coverage +dist # Other **/.idea diff --git a/.npmignore b/.npmignore index 97e3fc5..886b3f5 100644 --- a/.npmignore +++ b/.npmignore @@ -7,3 +7,9 @@ coverage *.iml **/__tests__ **/__mocks__ +.github +.yarn +.yarnrc.yml +src +test +.tsconfig \ No newline at end of file diff --git a/package.json b/package.json index 78f1fbf..a13409e 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,15 @@ "author": "DevSide", "license": "MIT", "type": "module", - "bin": "bin/dynamock.js", - "main": "src/createServer.js", + "bin": "./dist/bin/dynamock.js", + "main": "dist/createServer.js", "scripts": { + "build": "rm -rf dist/ && tsc && chmod +x ./dist/bin/dynamock.js && yarn link", "check-git": "git status -uno --porcelain && [ -z \"$(git status -uno --porcelain)\" ] || (echo 'Git working directory not clean'; false)", "format": "npx @biomejs/biome format --write src", "lint": "npx @biomejs/biome lint --write src", "release": "semantic-release", + "ts": "tsc --noEmit", "test": "NODE_OPTIONS='--enable-source-maps --experimental-vm-modules' jest src --coverage", "test:bin": "node test/bin.js" }, @@ -28,7 +30,13 @@ "node": ">=20" }, "jest": { - "transform": {} + "preset": "ts-jest", + "transform": { + "^.+\\.ts$": "ts-jest" + }, + "moduleNameMapper": { + "^(\\.\\.?\\/.+)\\.js$": "$1" + } }, "dependencies": { "@hapi/joi": "17.1.1", @@ -43,9 +51,15 @@ "@semantic-release/github": "^11.0.0", "@semantic-release/npm": "^12.0.1", "@semantic-release/release-notes-generator": "^14.0.1", + "@types/cookie-parser": "^1.4.7", + "@types/express": "^5.0.0", + "@types/hapi__joi": "^17.1.14", + "@types/supertest": "^6.0.2", "jest": "^29.7.0", "semantic-release": "^24.1.2", - "supertest": "^7.0.0" + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "typescript": "^5.6.3" }, "packageManager": "yarn@4.5.0" } diff --git a/src/__tests__/integrations.js b/src/__tests__/integrations.ts similarity index 98% rename from src/__tests__/integrations.js rename to src/__tests__/integrations.ts index 12fc50d..24538a6 100644 --- a/src/__tests__/integrations.js +++ b/src/__tests__/integrations.ts @@ -2,11 +2,13 @@ import { beforeAll, afterEach, afterAll, describe, test } from '@jest/globals' import { dirname } from 'node:path' import { mkdirSync, writeFileSync } from 'node:fs' import supertest from 'supertest' -import { createServer } from '../createServer' +import { createServer } from '../createServer.js' + +type Method = 'options' | 'put' | 'get' | 'post' | 'head' | 'delete' | 'patch' describe('integrations.js', () => { - let server - let request + let server = createServer() + let request = supertest.agent(server) beforeAll((done) => { server = createServer() @@ -18,7 +20,6 @@ describe('integrations.js', () => { afterAll((done) => { server.close(done) - server = null }) describe('manipulate configuration', () => { @@ -444,7 +445,7 @@ describe('integrations.js', () => { }) .expect(201) - await request[method](path) + await request[method as Method](path) .set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', @@ -539,7 +540,7 @@ describe('integrations.js', () => { }) .expect(201) - await request[method](path).expect(shouldMatch ? 200 : 404) + await request[method as Method](path).expect(shouldMatch ? 200 : 404) }, ) }) @@ -680,6 +681,7 @@ describe('integrations.js', () => { const cookies = Object.entries(values) .reduce((acc, [key, value]) => { + // @ts-ignore acc.push(`${key}=${value}`) return acc }, []) @@ -1006,12 +1008,14 @@ describe('integrations.js', () => { if (property === 'headers') { for (const key in expectedPropertyValue) { + // @ts-ignore r.expect(key, expectedPropertyValue[key]) } } else { const cookieValue = [] for (const key in expectedPropertyValue) { + // @ts-ignore cookieValue.push(`${key}=${expectedPropertyValue[key]}; Path=/`) } diff --git a/bin/dynamock.js b/src/bin/dynamock.ts similarity index 78% rename from bin/dynamock.js rename to src/bin/dynamock.ts index e5611a2..92e7b1c 100755 --- a/bin/dynamock.js +++ b/src/bin/dynamock.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { createServer } from "../src/createServer.js" +import { createServer } from '../createServer.js' const [, , port, host = '127.0.0.1'] = process.argv @@ -10,11 +10,11 @@ if (!port) { const server = createServer() -server.listen(port, host, () => { +server.listen(Number(port), host, () => { console.log(`dynamock is running on port ${port}...`) }) -function shutDown () { +function shutDown() { console.log('dynamock is shutting down gracefully') server.close(() => { process.exit(0) diff --git a/src/configuration.js b/src/configuration.ts similarity index 51% rename from src/configuration.js rename to src/configuration.ts index ceeb3cd..fd48dd4 100644 --- a/src/configuration.js +++ b/src/configuration.ts @@ -1,5 +1,18 @@ import Joi from '@hapi/joi' +export type ConfigurationObjectType = { + [key: string]: { + [key: string]: string + } +} + +export type ConfigurationType = { + cors: null | '*' + headers: ConfigurationObjectType + query: ConfigurationObjectType + cookies: ConfigurationObjectType +} + const schema = Joi.object({ cors: Joi.alternatives([Joi.string().valid('*'), Joi.object().valid(null)]), headers: Joi.object(), @@ -7,11 +20,11 @@ const schema = Joi.object({ cookies: Joi.object(), }).required() -export function validateConfiguration(unsafeConfiguration) { +export function validateConfiguration(unsafeConfiguration: unknown) { return schema.validate(unsafeConfiguration).error } -export function createConfiguration() { +export function createConfiguration(): ConfigurationType { return { cors: null, headers: {}, @@ -20,7 +33,13 @@ export function createConfiguration() { } } -export function updateConfiguration(configuration, cors, headers, query, cookies) { +export function updateConfiguration( + configuration: ConfigurationType, + cors: undefined | null | '*', + headers: undefined | ConfigurationObjectType, + query: undefined | ConfigurationObjectType, + cookies: undefined | ConfigurationObjectType, +) { if (cors !== undefined) { configuration.cors = cors === '*' ? '*' : null } diff --git a/src/createServer.js b/src/createServer.js deleted file mode 100644 index 6aa4f0d..0000000 --- a/src/createServer.js +++ /dev/null @@ -1,185 +0,0 @@ -import express from 'express' -import cookieParser from 'cookie-parser' -import { REQUEST_PROPERTIES, requestPropertyMatch, RESPONSE_PROPERTIES, useResponseProperties } from './properties.js' -import { getFixtureIterator, registerFixture, removeFixture, removeFixtures, validateFixture } from './fixtures.js' -import { createConfiguration, updateConfiguration, validateConfiguration } from './configuration.js' -import { createServer as createHTTPServer } from 'node:http' - -function resError(res, status, message) { - return res.status(status).send({ message: `[FIXTURE SERVER ERROR ${status}]: ${message}` }) -} - -function badRequest(res, message) { - return resError(res, 400, message) -} - -function conflict(res, message) { - return resError(res, 409, message) -} - -export function createServer() { - const app = express() - const server = createHTTPServer(app) - const corsAllowAllHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': '*', - 'Access-Control-Allow-Headers': '*', - } - - app.use(express.json({ limit: '10mb' })) - app.use(cookieParser()) - app.disable('x-powered-by') - app.disable('etag') - - let configuration = createConfiguration() - - app.post('/___fixtures', (req, res) => { - const unsafeFixture = req.body - - const validationError = validateFixture(unsafeFixture, configuration) - - if (validationError) { - return badRequest(res, validationError) - } - - const { conflictError, fixtureId } = registerFixture(unsafeFixture, configuration) - - if (conflictError) { - return conflict(res, conflictError) - } - - res.status(201).send({ id: fixtureId }) - }) - - app.post('/___fixtures/bulk', (req, res) => { - const fixtures = req.body - const fixtureIds = [] - - const cleanUpOnError = () => { - for (const { id } of fixtureIds) { - removeFixture(id) - } - } - - for (const unsafeFixture of fixtures) { - const validationError = validateFixture(unsafeFixture, configuration) - - if (validationError) { - cleanUpOnError() - - return badRequest(res, validationError) - } - - const { conflictError, fixtureId } = registerFixture(unsafeFixture, configuration) - - if (conflictError) { - cleanUpOnError() - - return conflict(res, conflictError) - } - - fixtureIds.push({ id: fixtureId }) - } - - res.status(201).send(fixtureIds) - }) - - app.delete('/___fixtures', (req, res) => { - removeFixtures() - res.status(204).send({}) - }) - - app.delete('/___fixtures/:id', (req, res) => { - removeFixture(req.params.id) - res.status(204).send() - }) - - app.get('/___config', (req, res) => { - res.status(200).send(configuration) - }) - - app.put('/___config', (req, res) => { - const error = validateConfiguration(req.body) - - if (error) { - return badRequest(res, error.message) - } - - const { cors, headers, query, cookies } = req.body - updateConfiguration(configuration, cors, headers, query, cookies) - - res.status(200).send(configuration) - }) - - app.delete('/___config', (req, res) => { - configuration = createConfiguration() - res.status(204).send() - }) - - app.use(function fixtureHandler(req, res, next) { - if (req.method === 'OPTIONS' && configuration.cors === '*') { - return res.set(corsAllowAllHeaders).status(200).send() - } - - fixtureLoop: for (const [fixtureId, fixture] of getFixtureIterator()) { - const { request, responses } = fixture - - if (!requestPropertyMatch(req, request, 'path') || !requestPropertyMatch(req, request, 'method')) { - continue - } - - for (const property of REQUEST_PROPERTIES) { - if (!requestPropertyMatch(req, request, property)) { - continue fixtureLoop - } - } - - const response = responses[0] - const options = response.options || {} - - const send = () => { - res.status(response.status || 200) - - // Loop over RESPONSE_PROPERTIES which has the right order - // avoiding "Can't set headers after they are sent" - for (const property of RESPONSE_PROPERTIES) { - if (response[property] !== undefined) { - useResponseProperties[property](req, res, response[property]) - } - } - - if (configuration.cors === '*') { - res.set(corsAllowAllHeaders) - } - } - - if (response.options?.delay) { - setTimeout(send, response.options.delay) - } else { - send() - } - - // The fixture has been or will be consumed - // When the response is delayed, we need to remove it before it returns - if (options.lifetime === undefined || options.lifetime === 1) { - if (responses.length > 1) { - responses.shift() - } else { - removeFixture(fixtureId) - } - } else if (options.lifetime > 0) { - options.lifetime-- - } - - return - } - - next() - }) - - server.on('close', () => { - removeFixtures() - }) - - return server -} diff --git a/src/createServer.ts b/src/createServer.ts new file mode 100644 index 0000000..ea9716b --- /dev/null +++ b/src/createServer.ts @@ -0,0 +1,140 @@ +import express, { type NextFunction, type Request, type Response } from 'express' +import cookieParser from 'cookie-parser' +import { REQUEST_PROPERTIES, requestPropertyMatch, RESPONSE_PROPERTIES, useResponseProperties } from './properties.js' +import { getFixtureIterator, removeFixture } from './fixtures.js' +import { createServer as createHTTPServer } from 'node:http' +import { + createService, + createServiceFixture, + createServiceFixtures, + deleteConfiguration, + deleteServiceFixture, + deleteServiceFixtures, + getServiceConfiguration, + hasServiceCors, + resetService, + updateServiceConfiguration, +} from './service.js' + +export function createServer() { + const app = express() + const server = createHTTPServer(app) + const corsAllowAllHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': '*', + 'Access-Control-Allow-Headers': '*', + } + + app.use(express.json({ limit: '10mb' })) + app.use(cookieParser()) + app.disable('x-powered-by') + app.disable('etag') + + const service = createService() + + app.post('/___fixtures', (req, res) => { + const [status, data] = createServiceFixture(service, req.body) + res.status(status).send(data) + }) + + app.post('/___fixtures/bulk', (req, res) => { + const [status, data] = createServiceFixtures(service, req.body) + res.status(status).send(data) + }) + + app.delete('/___fixtures', (req, res) => { + const [status, data] = deleteServiceFixtures(service) + res.status(status).send(data) + }) + + app.delete('/___fixtures/:id', (req, res) => { + const [status] = deleteServiceFixture(service, req.params.id) + res.status(status).send() + }) + + app.get('/___config', (req, res) => { + const [status, data] = getServiceConfiguration(service) + res.status(status).send(data) + }) + + app.put('/___config', (req, res) => { + const [status, data] = updateServiceConfiguration(service, req.body) + res.status(status).send(data) + }) + + app.delete('/___config', (req, res) => { + const [status] = deleteConfiguration(service) + res.status(status).send() + }) + + app.use(function fixtureHandler(req: Request, res: Response, next: NextFunction) { + if (req.method === 'OPTIONS' && hasServiceCors(service)) { + res.set(corsAllowAllHeaders).status(200).send() + return + } + + fixtureLoop: for (const [fixtureId, fixture] of getFixtureIterator(service.fixtureStorage)) { + const { request, responses } = fixture + // console.log("fixtureHandler", request) + + if (!requestPropertyMatch(req, request, 'path') || !requestPropertyMatch(req, request, 'method')) { + continue + } + + for (const property of REQUEST_PROPERTIES) { + if (!requestPropertyMatch(req, request, property)) { + // console.log("fixtureHandler no match " + property) + continue fixtureLoop + } + } + + const response = responses[0] + const options = response.options || {} + + const send = () => { + res.status(response.status || 200) + + // Loop over RESPONSE_PROPERTIES which has the right order + // avoiding "Can't set headers after they are sent" + for (const property of RESPONSE_PROPERTIES) { + if (response[property] !== undefined && property in useResponseProperties) { + // @ts-ignore + useResponseProperties[property](req, res, response[property]) + } + } + + if (service.configuration.cors === '*') { + res.set(corsAllowAllHeaders) + } + } + + if (response.options?.delay) { + setTimeout(send, response.options.delay) + } else { + send() + } + + // The fixture has been or will be consumed + // When the response is delayed, we need to remove it before it returns + if (options.lifetime === undefined || options.lifetime === 1) { + if (responses.length > 1) { + responses.shift() + } else { + removeFixture(service.fixtureStorage, fixtureId) + } + } else if (options.lifetime > 0) { + options.lifetime-- + } + + return + } + + next() + }) + + server.on('close', () => { + resetService(service) + }) + + return server +} diff --git a/src/fixtures.js b/src/fixtures.js deleted file mode 100644 index c344307..0000000 --- a/src/fixtures.js +++ /dev/null @@ -1,242 +0,0 @@ -import { hash, isObjectEmpty, sortObjectKeysRecurs } from './utils.js' -import querystring from 'node:querystring' -// TODO: remove Joi with a lightweight composable validation library -import Joi from '@hapi/joi' - -const fixtureStorage = new Map() - -export function validateFixture(unsafeFixture, configuration) { - const schemaProperty = Joi.alternatives([ - Joi.array().items( - Joi.custom((value, helpers) => { - const path = helpers.state.path - const property = path[path.length - 2] - - if (!configuration[property][value]) { - throw new Error(`${value} not found in configuration`) - } - - return value - }), - Joi.object(), - ), - Joi.object(), - ]) - - const optionsStrictOrAllowRegex = Joi.object({ - strict: Joi.bool(), - allowRegex: Joi.bool(), - }).invalid({ strict: true, allowRegex: true }) - - const requestSchema = Joi.object({ - body: Joi.any(), - path: Joi.string().required(), - method: Joi.alternatives([ - Joi.string().regex(/^(head|delete|put|post|get|options|patch|\*)$/i), - Joi.custom((value, helpers) => { - const options = helpers.state.ancestors[0].options || {} - const allowMethodRegex = options.method?.allowRegex - - if (!allowMethodRegex) { - throw new Error(`Method ${value} is not a valid method`) - } - - if (typeof value !== 'string') { - return helpers.error('string.invalid') - } - - return value - }), - ]).required(), - headers: schemaProperty, - cookies: schemaProperty, - query: schemaProperty, - options: Joi.object({ - path: Joi.object({ - allowRegex: Joi.bool(), - disableEncodeURI: Joi.bool(), - }).invalid({ allowRegex: true, disableEncodeURI: true }), - method: Joi.object({ - allowRegex: Joi.bool(), - }), - headers: optionsStrictOrAllowRegex, - cookies: optionsStrictOrAllowRegex, - query: optionsStrictOrAllowRegex, - body: optionsStrictOrAllowRegex, - }), - }) - - const responseSchema = Joi.object({ - status: Joi.number().integer().min(200).max(600), - body: Joi.any(), - filepath: Joi.string(), - headers: schemaProperty, - cookies: schemaProperty, - options: Joi.object({ - delay: Joi.number().integer().min(0), - lifetime: Joi.number().integer().min(0), - }), - }).or('body', 'filepath') - - const schema = Joi.object({ - request: requestSchema.required(), - response: responseSchema, - responses: Joi.array().items(responseSchema), - }) - .or('response', 'responses') - .required() - - const error = schema.validate(unsafeFixture).error - - if (error) { - return error.message - } - - return '' -} - -function normalizeArrayMatcher(property, propertyValue, configuration) { - const result = {} - - // Merge with configuration - for (const propertyItem of propertyValue) { - if (typeof propertyItem === 'string') { - Object.assign(result, configuration[property][propertyItem]) - } else { - Object.assign(result, propertyItem) - } - } - - return result -} - -function normalizePath(request) { - // extract query from path is needed and move it in query property - const indexQueryString = request.path.indexOf('?') - - if (indexQueryString >= 0) { - const path = request.path.substring(0, indexQueryString) - const query = querystring.parse(request.path.substring(indexQueryString + 1)) - - request.path = path - - if (request.query) { - Object.assign(request.query, query) - } else { - request.query = query - } - } - - const pathOptions = request.options?.path - - if (!pathOptions || (!pathOptions.allowRegex && !pathOptions.disableEncodeURI)) { - request.path = encodeURI(request.path) - } -} - -function normalizeFixture(fixture, configuration) { - const request = sortObjectKeysRecurs(fixture.request) - const responses = sortObjectKeysRecurs(fixture.responses || [fixture.response]) - - for (const property in request) { - if (property === 'body') { - continue - } - - if (property === 'method') { - request.method = request.method.toUpperCase() - continue - } - - if (property === 'path') { - normalizePath(request) - continue - } - - const propertyValue = request[property] - - if (Array.isArray(propertyValue)) { - request[property] = normalizeArrayMatcher(property, propertyValue, configuration) - } - - if (typeof propertyValue === 'object' && isObjectEmpty(propertyValue)) { - delete request[property] - } - - // RFC 2616 - if (property === 'headers') { - const headers = request[property] - - for (const key in headers) { - const lowerCaseKey = key.toLowerCase() - - if (key !== lowerCaseKey) { - headers[lowerCaseKey] = headers[key] - delete headers[key] - } - } - } - - // Some properties only manipulates string values - if (property === 'headers' || property === 'cookies' || property === 'query') { - const requestProperty = request[property] - - for (const key in requestProperty) { - if (typeof requestProperty[key] !== 'string') { - requestProperty[key] = JSON.stringify(requestProperty[key]) - } - } - } - } - - for (const response of responses) { - for (const property in response) { - if (property === 'body' || property === 'filepath') { - continue - } - - const propertyValue = response[property] - - if (Array.isArray(propertyValue)) { - response[property] = normalizeArrayMatcher(property, propertyValue, configuration) - } - } - } - - return { request, responses } -} - -function createFixtureId(fixture) { - return hash(JSON.stringify(fixture.request)) -} - -export function registerFixture(newFixture, configuration) { - const fixture = normalizeFixture(newFixture, configuration) - const fixtureId = createFixtureId(fixture) - - if (fixtureStorage.has(fixtureId)) { - return { - conflictError: `Route ${fixture.request.method} ${fixture.request.path} is already registered`, - fixtureId, - } - } - - fixtureStorage.set(fixtureId, fixture) - - return { - conflictError: '', - fixtureId, - } -} - -export function getFixtureIterator() { - return fixtureStorage -} - -export function removeFixture(fixtureId) { - return fixtureStorage.delete(fixtureId) -} - -export function removeFixtures() { - fixtureStorage.clear() -} diff --git a/src/fixtures.ts b/src/fixtures.ts new file mode 100644 index 0000000..72cf596 --- /dev/null +++ b/src/fixtures.ts @@ -0,0 +1,362 @@ +import { URLSearchParams } from 'node:url' +import { hash, isObjectEmpty, type NonEmptyArray, sortObjectKeysRecurs } from './utils.js' +// TODO: remove Joi with a lightweight composable validation library +import Joi from '@hapi/joi' +import type { ConfigurationType } from './configuration.js' + +export type FixtureStorageType = { + storage: Map +} + +export type FixtureRequestOptionsType = null | { + path?: { + allowRegex?: boolean + disableEncodeURI?: boolean + } + method?: { + allowRegex?: boolean + } + headers?: { + strict?: boolean + allowRegex?: boolean + } + cookies?: { + strict?: boolean + allowRegex?: boolean + } + query?: { + strict?: boolean + allowRegex?: boolean + } + body?: { + strict?: boolean + allowRegex?: boolean + } +} + +export type FixtureRequestType = { + path: string + method: string + headers?: null | { [key: string]: string } | ({ [key: string]: string } | string)[] + cookies?: null | { [key: string]: string } | ({ [key: string]: string } | string)[] + query?: null | { [key: string]: string } | ({ [key: string]: string } | string)[] + body?: null | { [key: string]: string } + options?: FixtureRequestOptionsType +} + +type FixtureResponseType = { + status?: number + headers?: null | { [key: string]: string } | ({ [key: string]: string } | string)[] + cookies?: null | { [key: string]: string } | ({ [key: string]: string } | string)[] + query?: null | { [key: string]: string } | ({ [key: string]: string } | string)[] + body?: null | { [key: string]: string } + filepath?: string + options?: null | { + delay?: number + lifetime?: number + } +} + +type FixtureType = + | { + request: FixtureRequestType + response: FixtureResponseType + } + | { + request: FixtureRequestType + responses: NonEmptyArray + } + +export type NormalizedFixtureRequestType = { + path: string + method: string + headers?: null | { [key: string]: string } + cookies?: null | { [key: string]: string } + query?: null | { [key: string]: string } + body?: null | { [key: string]: string } + options?: FixtureRequestOptionsType +} + +export type NormalizedFixtureResponseType = { + status?: number + headers?: null | { [key: string]: string } + cookies?: null | { [key: string]: string } + query?: null | { [key: string]: string } + body?: null | { [key: string]: string } + filepath?: string + options?: null | { + delay?: number + lifetime?: number + } +} + +export type NormalizedFixtureType = { + request: NormalizedFixtureRequestType + responses: NonEmptyArray +} + +export function createFixtureStorage() { + return { + storage: new Map(), + } +} + +export function validateFixture( + unsafeFixture: unknown, + configuration: ConfigurationType, +): [null, string] | [FixtureType, string] { + const schemaProperty = Joi.alternatives([ + Joi.array().items( + Joi.custom((value, helpers) => { + const path = helpers.state.path + const property = path?.[path.length - 2] + + if (property === 'headers' || property === 'query' || property === 'cookies') { + if (!configuration[property]?.[value]) { + throw new Error(`${value} not found in configuration`) + } + } + + return value + }), + Joi.object(), + ), + Joi.object(), + ]) + + const optionsStrictOrAllowRegex = Joi.object({ + strict: Joi.bool(), + allowRegex: Joi.bool(), + }).invalid({ strict: true, allowRegex: true }) + + const requestSchema = Joi.object({ + body: Joi.any(), + path: Joi.string().required(), + method: Joi.alternatives([ + Joi.string().regex(/^(head|delete|put|post|get|options|patch|\*)$/i), + Joi.custom((value, helpers) => { + const options = helpers.state.ancestors[0].options || {} + const allowMethodRegex = options.method?.allowRegex + + if (!allowMethodRegex) { + throw new Error(`Method ${value} is not a valid method`) + } + + if (typeof value !== 'string') { + return helpers.error('string.invalid') + } + + return value + }), + ]).required(), + headers: schemaProperty, + cookies: schemaProperty, + query: schemaProperty, + options: Joi.object({ + path: Joi.object({ + allowRegex: Joi.bool(), + disableEncodeURI: Joi.bool(), + }).invalid({ allowRegex: true, disableEncodeURI: true }), + method: Joi.object({ + allowRegex: Joi.bool(), + }), + headers: optionsStrictOrAllowRegex, + cookies: optionsStrictOrAllowRegex, + query: optionsStrictOrAllowRegex, + body: optionsStrictOrAllowRegex, + }), + }) + + const responseSchema = Joi.object({ + status: Joi.number().integer().min(200).max(600), + body: Joi.any(), + filepath: Joi.string(), + headers: schemaProperty, + cookies: schemaProperty, + options: Joi.object({ + delay: Joi.number().integer().min(0), + lifetime: Joi.number().integer().min(0), + }), + }).or('body', 'filepath') + + const schema = Joi.object({ + request: requestSchema.required(), + response: responseSchema, + responses: Joi.array().items(responseSchema), + }) + .or('response', 'responses') + .required() + + const error = schema.validate(unsafeFixture).error + + if (error) { + return [null, error.message] + } + + // Use "as" temporarily until new validation lib like Zod + return [unsafeFixture as FixtureType, ''] +} + +function normalizeArrayMatcher( + property: 'headers' | 'cookies' | 'query', + propertyValue: ({ [key: string]: string } | string)[], + configuration: ConfigurationType, +) { + const result = {} + + // Merge with configuration + for (const propertyItem of propertyValue) { + if (typeof propertyItem === 'string') { + Object.assign(result, configuration[property][propertyItem]) + } else { + Object.assign(result, propertyItem) + } + } + + return result +} + +function normalizePath(request: FixtureRequestType) { + // extract query from path is needed and move it in query property + const indexQueryString = request.path.indexOf('?') + + if (indexQueryString >= 0) { + const path = request.path.substring(0, indexQueryString) + const searchParams = new URLSearchParams(request.path.substring(indexQueryString + 1)) + const query = Object.fromEntries(searchParams.entries()) + + request.path = path + + if (request.query) { + Object.assign(request.query, query) + } else { + request.query = query + } + } + + const pathOptions = request.options?.path + + if (!pathOptions || (!pathOptions.allowRegex && !pathOptions.disableEncodeURI)) { + request.path = encodeURI(request.path) + } +} + +function normalizeFixture(fixture: FixtureType, configuration: ConfigurationType): NormalizedFixtureType { + const request = sortObjectKeysRecurs(fixture.request) as FixtureRequestType + const responses = sortObjectKeysRecurs( + 'response' in fixture ? [fixture.response] : fixture.responses, + ) as NonEmptyArray + + for (const property in request) { + if (property === 'body') { + continue + } + + if (property === 'method') { + request.method = request.method.toUpperCase() + continue + } + + if (property === 'path') { + normalizePath(request) + continue + } + + if (property === 'headers' || property === 'cookies' || property === 'query') { + const propertyValue = request[property] + + if (Array.isArray(propertyValue)) { + request[property] = normalizeArrayMatcher(property, propertyValue, configuration) + } + + if (typeof propertyValue === 'object' && isObjectEmpty(propertyValue)) { + delete request[property] + } + } + + // RFC 2616 + if (property === 'headers') { + const headers = request[property] as { [key: string]: string } + + for (const key in headers) { + const lowerCaseKey = key.toLowerCase() + + if (key !== lowerCaseKey) { + headers[lowerCaseKey] = headers[key] + delete headers[key] + } + } + } + + // Some properties only manipulates string values + if (property === 'headers' || property === 'cookies' || property === 'query') { + const requestProperty = request[property] as { [key: string]: string } + + for (const key in requestProperty) { + if (typeof requestProperty[key] !== 'string') { + requestProperty[key] = JSON.stringify(requestProperty[key]) + } + } + } + } + + for (const response of responses) { + for (const property in response) { + if (property === 'body' || property === 'filepath') { + continue + } + + if (property === 'headers' || property === 'cookies' || property === 'query') { + const propertyValue = response[property] + + if (Array.isArray(propertyValue)) { + response[property] = normalizeArrayMatcher(property, propertyValue, configuration) + } + } + } + } + + return { + request: request as NormalizedFixtureRequestType, + responses: responses as NonEmptyArray, + } +} + +function createFixtureId(fixture: FixtureType) { + return hash(JSON.stringify(fixture.request)) +} + +export function registerFixture( + { storage }: FixtureStorageType, + newFixture: FixtureType, + configuration: ConfigurationType, +) { + const fixture = normalizeFixture(newFixture, configuration) + const fixtureId = createFixtureId(fixture) + + if (storage.has(fixtureId)) { + return { + conflictError: `Route ${fixture.request.method} ${fixture.request.path} is already registered`, + fixtureId, + } + } + + storage.set(fixtureId, fixture) + + return { + conflictError: '', + fixtureId, + } +} + +export function getFixtureIterator({ storage }: FixtureStorageType) { + return storage +} + +export function removeFixture({ storage }: FixtureStorageType, fixtureId: string) { + return storage.delete(fixtureId) +} + +export function removeFixtures({ storage }: FixtureStorageType) { + storage.clear() +} diff --git a/src/properties.js b/src/properties.js deleted file mode 100644 index 64a42f4..0000000 --- a/src/properties.js +++ /dev/null @@ -1,54 +0,0 @@ -import { deepStrictEqual } from 'node:assert' -import { isIncluded, matchRegex } from './utils.js' - -export const REQUEST_PROPERTIES = ['headers', 'body', 'query', 'cookies'] -export const RESPONSE_PROPERTIES = ['headers', 'cookies', 'filepath', 'body'] - -export function requestPropertyMatch(request, match, property) { - let requestProperty = request[property] - let matchProperty = match[property] - const optionsProperty = match.options?.[property] || {} - - if (property === 'path' || property === 'method') { - if (matchProperty === '*') { - return true - } - - if (optionsProperty.allowRegex) { - return matchRegex(matchProperty, requestProperty) - } - - return matchProperty === requestProperty - } - - if (optionsProperty.strict) { - if (property !== 'body') { - matchProperty = matchProperty || {} - requestProperty = requestProperty || {} - } - - try { - deepStrictEqual(matchProperty, requestProperty) - return true - } catch (_) { - return false - } - } - - if (!matchProperty && property !== 'body') { - return true - } - - return isIncluded(matchProperty, requestProperty, !!optionsProperty.allowRegex) -} - -export const useResponseProperties = { - filepath: (req, res, value) => res.sendFile(value), - headers: (req, res, value) => res.set(value), - body: (req, res, value) => res.send(value), - cookies: (req, res, cookies) => { - for (const key in cookies) { - res.cookie(key, cookies[key]) - } - }, -} diff --git a/src/properties.ts b/src/properties.ts new file mode 100644 index 0000000..3329c39 --- /dev/null +++ b/src/properties.ts @@ -0,0 +1,70 @@ +import { deepStrictEqual } from 'node:assert' +import { isIncluded, matchRegex } from './utils.js' +import type { NormalizedFixtureRequestType } from './fixtures.js' +import type { Request, Response } from 'express' + +export const REQUEST_PROPERTIES: ['headers', 'body', 'query', 'cookies'] = ['headers', 'body', 'query', 'cookies'] +export const RESPONSE_PROPERTIES: ['headers', 'cookies', 'filepath', 'body'] = [ + 'headers', + 'cookies', + 'filepath', + 'body', +] + +export function requestPropertyMatch( + request: Request, + match: NormalizedFixtureRequestType, + property: 'path' | 'method' | 'headers' | 'cookies' | 'query' | 'body', +) { + if (property === 'path' || property === 'method') { + const matchProperty = match[property] + + if (matchProperty === '*') { + return true + } + + const requestProperty = request[property] + + if (match.options?.[property]?.allowRegex) { + return matchRegex(matchProperty, requestProperty) + } + + return matchProperty === requestProperty + } + + let matchProperty = match[property] + let requestProperty = request[property] + const optionsProperty = match.options?.[property] || {} + + if (optionsProperty.strict) { + if (property !== 'body') { + matchProperty = matchProperty || {} + requestProperty = requestProperty || {} + } + + try { + deepStrictEqual(matchProperty, requestProperty) + return true + } catch (_) { + return false + } + } + + if (!matchProperty && property !== 'body') { + return true + } + + // @ts-ignore + return isIncluded(matchProperty, requestProperty, !!optionsProperty.allowRegex) +} + +export const useResponseProperties = { + filepath: (req: Request, res: Response, value: string) => res.sendFile(value), + headers: (req: Request, res: Response, value: { [key in string]: string } | null) => res.set(value), + body: (req: Request, res: Response, value: string | null | { [key in string]: string }) => res.send(value), + cookies: (req: Request, res: Response, cookies: { [key in string]: string } | null) => { + for (const key in cookies) { + res.cookie(key, cookies[key]) + } + }, +} diff --git a/src/service.ts b/src/service.ts new file mode 100644 index 0000000..a48f513 --- /dev/null +++ b/src/service.ts @@ -0,0 +1,130 @@ +import { + createFixtureStorage, + type FixtureStorageType, + registerFixture, + removeFixture, + removeFixtures, + validateFixture, +} from './fixtures.js' +import { + type ConfigurationType, + createConfiguration, + updateConfiguration, + validateConfiguration, +} from './configuration.js' + +function createError(status: number, message: string): [number, object] { + return [status, { message: `[FIXTURE SERVER ERROR ${status}]: ${message}` }] +} + +export type ServiceType = { + configuration: ConfigurationType + fixtureStorage: FixtureStorageType +} + +export function createService() { + return { + configuration: createConfiguration(), + fixtureStorage: createFixtureStorage(), + } +} + +export function resetService(service: ServiceType) { + service.configuration = createConfiguration() + service.fixtureStorage = createFixtureStorage() +} + +export function getServiceConfiguration({ configuration }: ServiceType): [number, object] { + return [200, configuration] +} + +export function updateServiceConfiguration( + { configuration }: ServiceType, + data: Partial, +): [number, object] { + const error = validateConfiguration(data) + + if (error) { + return createError(400, error.message) + } + + const { cors, headers, query, cookies } = data + updateConfiguration(configuration, cors, headers, query, cookies) + + return [200, configuration] +} + +export function deleteConfiguration(service: ServiceType): [number] { + service.configuration = createConfiguration() + + return [204] +} + +export function createServiceFixture( + { configuration, fixtureStorage }: ServiceType, + unsafeFixture: unknown, +): [number, object] { + const [fixture, validationError] = validateFixture(unsafeFixture, configuration) + + if (!fixture) { + return createError(400, validationError) + } + + const { conflictError, fixtureId } = registerFixture(fixtureStorage, fixture, configuration) + + if (conflictError) { + return createError(409, conflictError) + } + return [201, { id: fixtureId }] +} + +export function createServiceFixtures( + { configuration, fixtureStorage }: ServiceType, + unsafeFixtures: unknown[], +): [number, object] { + const fixtureIds: { id: string }[] = [] + + const cleanUpOnError = () => { + for (const { id } of fixtureIds) { + removeFixture(fixtureStorage, id) + } + } + + for (const unsafeFixture of unsafeFixtures) { + const [fixture, validationError] = validateFixture(unsafeFixture, configuration) + + if (!fixture) { + cleanUpOnError() + + return createError(400, validationError) + } + + const { conflictError, fixtureId } = registerFixture(fixtureStorage, fixture, configuration) + + if (conflictError) { + cleanUpOnError() + + return createError(409, conflictError) + } + + fixtureIds.push({ id: fixtureId }) + } + + return [201, fixtureIds] +} + +export function deleteServiceFixture({ fixtureStorage }: ServiceType, id: string): [number] { + removeFixture(fixtureStorage, id) + + return [204] +} + +export function deleteServiceFixtures({ fixtureStorage }: ServiceType): [number, object] { + removeFixtures(fixtureStorage) + + return [204, {}] +} + +export function hasServiceCors({ configuration }: ServiceType) { + return configuration.cors === '*' +} diff --git a/src/utils.js b/src/utils.ts similarity index 65% rename from src/utils.js rename to src/utils.ts index 473370b..2ef7cd0 100644 --- a/src/utils.js +++ b/src/utils.ts @@ -3,20 +3,20 @@ import { deepStrictEqual } from 'node:assert' export { isObjectEmpty } -function isObjectEmpty(object) { +function isObjectEmpty(object: object | null) { for (const key in object) { if (Object.prototype.hasOwnProperty.call(object, key)) return false } return true } -function isObject(object) { - return typeof object === 'object' && !Array.isArray(object) +function isObject(maybeObject: unknown): maybeObject is object { + return typeof maybeObject === 'object' && !Array.isArray(maybeObject) } const stringRegexp = /\/(.*)\/([gimuys]*)/ -function matchRegex(value, baseValue) { +function matchRegex(value: string, baseValue: string) { const matchRegExp = value.match(stringRegexp) if (matchRegExp) { @@ -32,7 +32,11 @@ function matchRegex(value, baseValue) { export { matchRegex } -export function isIncluded(object, base, allowRegex) { +export function isIncluded( + object: { [key in string]?: unknown } | undefined, + base: { [key in string]?: unknown }, + allowRegex: boolean, +) { for (const key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) { continue @@ -62,7 +66,8 @@ export function isIncluded(object, base, allowRegex) { return true } -export function sortObjectKeysRecurs(src) { +// @ts-ignore +export function sortObjectKeysRecurs(src: null | { [key: string]: unknown } | { [key: string]: unknown }[]) { if (Array.isArray(src)) { const out = [] @@ -74,11 +79,11 @@ export function sortObjectKeysRecurs(src) { } if (typeof src === 'object' && src !== null) { - const out = {} + const out = {} as { [key: string]: unknown } const sortedKeys = Object.keys(src).sort() for (const key of sortedKeys) { - out[key] = sortObjectKeysRecurs(src[key]) + out[key] = sortObjectKeysRecurs(src[key] as null | { [key: string]: unknown } | { [key: string]: unknown }[]) } return out @@ -87,6 +92,8 @@ export function sortObjectKeysRecurs(src) { return src } -export function hash(str) { +export function hash(str: string) { return crypto.createHash('sha1').update(str).digest('hex') } + +export type NonEmptyArray = [T, ...T[]] | [...T[], T] | [T, ...T[], T] diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0f23ade --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 20", + "_version": "20.1.0", + + "compilerOptions": { + "outDir": "./dist", + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "allowImportingTsExtensions": false, + "declaration": true, + "sourceMap": true, + }, + + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2a5820f..4d0092e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,6 +1972,65 @@ __metadata: languageName: node linkType: hard +"@types/body-parser@npm:*": + version: 1.19.5 + resolution: "@types/body-parser@npm:1.19.5" + dependencies: + "@types/connect": "npm:*" + "@types/node": "npm:*" + checksum: 10c0/aebeb200f25e8818d8cf39cd0209026750d77c9b85381cdd8deeb50913e4d18a1ebe4b74ca9b0b4d21952511eeaba5e9fbbf739b52731a2061e206ec60d568df + languageName: node + linkType: hard + +"@types/connect@npm:*": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c + languageName: node + linkType: hard + +"@types/cookie-parser@npm:^1.4.7": + version: 1.4.7 + resolution: "@types/cookie-parser@npm:1.4.7" + dependencies: + "@types/express": "npm:*" + checksum: 10c0/af37fea5399950e59ceb2e1f25c633f3df360c4f17e8b3f26418e672fe5c926a20993b86f8e1df72cfe2c4dc8967d9a18d3d78b5c6a5f751a297d0418e5690fa + languageName: node + linkType: hard + +"@types/cookiejar@npm:^2.1.5": + version: 2.1.5 + resolution: "@types/cookiejar@npm:2.1.5" + checksum: 10c0/af38c3d84aebb3ccc6e46fb6afeeaac80fb26e63a487dd4db5a8b87e6ad3d4b845ba1116b2ae90d6f886290a36200fa433d8b1f6fe19c47da6b81872ce9a2764 + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^5.0.0": + version: 5.0.0 + resolution: "@types/express-serve-static-core@npm:5.0.0" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: 10c0/671a67a5b367e19aa634dcd515364212490f10efb938fc1097082085a883ccb11c81ec96a3c2b5cc67d5756e5cb1ccbf1de192806f8193bb7de341994beb4ea6 + languageName: node + linkType: hard + +"@types/express@npm:*, @types/express@npm:^5.0.0": + version: 5.0.0 + resolution: "@types/express@npm:5.0.0" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^5.0.0" + "@types/qs": "npm:*" + "@types/serve-static": "npm:*" + checksum: 10c0/0d74b53aefa69c3b3817ee9b5145fd50d7dbac52a8986afc2d7500085c446656d0b6dc13158c04e2d9f18f4324d4d93b0452337c5ff73dd086dca3e4ff11f47b + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -1981,6 +2040,20 @@ __metadata: languageName: node linkType: hard +"@types/hapi__joi@npm:^17.1.14": + version: 17.1.14 + resolution: "@types/hapi__joi@npm:17.1.14" + checksum: 10c0/f9324de0c9ac37097b5bddc997402e299b7a9ab098a27c22f808d363c8218f94180023117b0b99fe187bffa1f6e473744b7417054a3737b3360035c20274d792 + languageName: node + linkType: hard + +"@types/http-errors@npm:*": + version: 2.0.4 + resolution: "@types/http-errors@npm:2.0.4" + checksum: 10c0/494670a57ad4062fee6c575047ad5782506dd35a6b9ed3894cea65830a94367bd84ba302eb3dde331871f6d70ca287bfedb1b2cf658e6132cd2cbd427ab56836 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.3 resolution: "@types/istanbul-lib-coverage@npm:2.0.3" @@ -2006,6 +2079,20 @@ __metadata: languageName: node linkType: hard +"@types/methods@npm:^1.1.4": + version: 1.1.4 + resolution: "@types/methods@npm:1.1.4" + checksum: 10c0/a78534d79c300718298bfff92facd07bf38429c66191f640c1db4c9cff1e36f819304298a96f7536b6512bfc398e5c3e6b831405e138cd774b88ad7be78d682a + languageName: node + linkType: hard + +"@types/mime@npm:^1": + version: 1.3.5 + resolution: "@types/mime@npm:1.3.5" + checksum: 10c0/c2ee31cd9b993804df33a694d5aa3fa536511a49f2e06eeab0b484fef59b4483777dbb9e42a4198a0809ffbf698081fdbca1e5c2218b82b91603dfab10a10fbc + languageName: node + linkType: hard + "@types/node@npm:*": version: 16.3.0 resolution: "@types/node@npm:16.3.0" @@ -2020,6 +2107,20 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:*": + version: 6.9.16 + resolution: "@types/qs@npm:6.9.16" + checksum: 10c0/a4e871b80fff623755e356fd1f225aea45ff7a29da30f99fddee1a05f4f5f33485b314ab5758145144ed45708f97e44595aa9a8368e9bbc083932f931b12dbb6 + languageName: node + linkType: hard + +"@types/range-parser@npm:*": + version: 1.2.7 + resolution: "@types/range-parser@npm:1.2.7" + checksum: 10c0/361bb3e964ec5133fa40644a0b942279ed5df1949f21321d77de79f48b728d39253e5ce0408c9c17e4e0fd95ca7899da36841686393b9f7a1e209916e9381a3c + languageName: node + linkType: hard + "@types/semver@npm:^7.5.5": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" @@ -2027,6 +2128,27 @@ __metadata: languageName: node linkType: hard +"@types/send@npm:*": + version: 0.17.4 + resolution: "@types/send@npm:0.17.4" + dependencies: + "@types/mime": "npm:^1" + "@types/node": "npm:*" + checksum: 10c0/7f17fa696cb83be0a104b04b424fdedc7eaba1c9a34b06027239aba513b398a0e2b7279778af521f516a397ced417c96960e5f50fcfce40c4bc4509fb1a5883c + languageName: node + linkType: hard + +"@types/serve-static@npm:*": + version: 1.15.7 + resolution: "@types/serve-static@npm:1.15.7" + dependencies: + "@types/http-errors": "npm:*" + "@types/node": "npm:*" + "@types/send": "npm:*" + checksum: 10c0/26ec864d3a626ea627f8b09c122b623499d2221bbf2f470127f4c9ebfe92bd8a6bb5157001372d4c4bd0dd37a1691620217d9dc4df5aa8f779f3fd996b1c60ae + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0": version: 2.0.1 resolution: "@types/stack-utils@npm:2.0.1" @@ -2034,6 +2156,28 @@ __metadata: languageName: node linkType: hard +"@types/superagent@npm:^8.1.0": + version: 8.1.9 + resolution: "@types/superagent@npm:8.1.9" + dependencies: + "@types/cookiejar": "npm:^2.1.5" + "@types/methods": "npm:^1.1.4" + "@types/node": "npm:*" + form-data: "npm:^4.0.0" + checksum: 10c0/12631f1d8b3a62e1f435bc885f6d64d1a2d1ae82b80f0c6d63d4d6372c40b6f1fee6b3da59ac18bb86250b1eb73583bf2d4b1f7882048c32468791c560c69b7c + languageName: node + linkType: hard + +"@types/supertest@npm:^6.0.2": + version: 6.0.2 + resolution: "@types/supertest@npm:6.0.2" + dependencies: + "@types/methods": "npm:^1.1.4" + "@types/superagent": "npm:^8.1.0" + checksum: 10c0/44a28f9b35b65800f4c7bcc23748e71c925098aa74ea504d14c98385c36d00de2a4f5aca15d7dc4514bc80533e0af21f985a4ab9f5f317c7266e9e75836aef39 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 20.2.1 resolution: "@types/yargs-parser@npm:20.2.1" @@ -2242,6 +2386,13 @@ __metadata: languageName: node linkType: hard +"async@npm:^3.2.3": + version: 3.2.6 + resolution: "async@npm:3.2.6" + checksum: 10c0/36484bb15ceddf07078688d95e27076379cc2f87b10c03b6dd8a83e89475a3c8df5848859dd06a4c95af1e4c16fc973de0171a77f18ea00be899aca2a4f85e70 + languageName: node + linkType: hard + "asynckit@npm:^0.4.0": version: 0.4.0 resolution: "asynckit@npm:0.4.0" @@ -2442,6 +2593,15 @@ __metadata: languageName: node linkType: hard +"bs-logger@npm:^0.2.6": + version: 0.2.6 + resolution: "bs-logger@npm:0.2.6" + dependencies: + fast-json-stable-stringify: "npm:2.x" + checksum: 10c0/80e89aaaed4b68e3374ce936f2eb097456a0dddbf11f75238dbd53140b1e39259f0d248a5089ed456f1158984f22191c3658d54a713982f676709fbe1a6fa5a0 + languageName: node + linkType: hard + "bser@npm:2.1.1": version: 2.1.1 resolution: "bser@npm:2.1.1" @@ -2584,6 +2744,16 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^4.0.2": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880 + languageName: node + linkType: hard + "chalk@npm:^5.3.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" @@ -3191,13 +3361,19 @@ __metadata: "@semantic-release/github": "npm:^11.0.0" "@semantic-release/npm": "npm:^12.0.1" "@semantic-release/release-notes-generator": "npm:^14.0.1" + "@types/cookie-parser": "npm:^1.4.7" + "@types/express": "npm:^5.0.0" + "@types/hapi__joi": "npm:^17.1.14" + "@types/supertest": "npm:^6.0.2" cookie-parser: "npm:1.4.7" express: "npm:4.21.1" jest: "npm:^29.7.0" semantic-release: "npm:^24.1.2" supertest: "npm:^7.0.0" + ts-jest: "npm:^29.2.5" + typescript: "npm:^5.6.3" bin: - dynamock: bin/dynamock.js + dynamock: ./dist/bin/dynamock.js languageName: unknown linkType: soft @@ -3215,6 +3391,17 @@ __metadata: languageName: node linkType: hard +"ejs@npm:^3.1.10": + version: 3.1.10 + resolution: "ejs@npm:3.1.10" + dependencies: + jake: "npm:^10.8.5" + bin: + ejs: bin/cli.js + checksum: 10c0/52eade9e68416ed04f7f92c492183340582a36482836b11eab97b159fcdcfdedc62233a1bf0bf5e5e1851c501f2dca0e2e9afd111db2599e4e7f53ee29429ae1 + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.4.477": version: 1.4.504 resolution: "electron-to-chromium@npm:1.4.504" @@ -3528,7 +3715,7 @@ __metadata: languageName: node linkType: hard -"fast-json-stable-stringify@npm:^2.1.0": +"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" checksum: 10c0/7f081eb0b8a64e0057b3bb03f974b3ef00135fbf36c1c710895cd9300f13c94ba809bb3a81cf4e1b03f6e5285610a61abbd7602d0652de423144dfee5a389c9b @@ -3585,6 +3772,15 @@ __metadata: languageName: node linkType: hard +"filelist@npm:^1.0.4": + version: 1.0.4 + resolution: "filelist@npm:1.0.4" + dependencies: + minimatch: "npm:^5.0.1" + checksum: 10c0/426b1de3944a3d153b053f1c0ebfd02dccd0308a4f9e832ad220707a6d1f1b3c9784d6cadf6b2f68f09a57565f63ebc7bcdc913ccf8012d834f472c46e596f41 + languageName: node + linkType: hard + "fill-range@npm:^7.1.1": version: 7.1.1 resolution: "fill-range@npm:7.1.1" @@ -4555,6 +4751,20 @@ __metadata: languageName: node linkType: hard +"jake@npm:^10.8.5": + version: 10.9.2 + resolution: "jake@npm:10.9.2" + dependencies: + async: "npm:^3.2.3" + chalk: "npm:^4.0.2" + filelist: "npm:^1.0.4" + minimatch: "npm:^3.1.2" + bin: + jake: bin/cli.js + checksum: 10c0/c4597b5ed9b6a908252feab296485a4f87cba9e26d6c20e0ca144fb69e0c40203d34a2efddb33b3d297b8bd59605e6c1f44f6221ca1e10e69175ecbf3ff5fe31 + languageName: node + linkType: hard + "java-properties@npm:^1.0.2": version: 1.0.2 resolution: "java-properties@npm:1.0.2" @@ -4926,7 +5136,7 @@ __metadata: languageName: node linkType: hard -"jest-util@npm:^29.7.0": +"jest-util@npm:^29.0.0, jest-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-util@npm:29.7.0" dependencies: @@ -5347,6 +5557,13 @@ __metadata: languageName: node linkType: hard +"lodash.memoize@npm:^4.1.2": + version: 4.1.2 + resolution: "lodash.memoize@npm:4.1.2" + checksum: 10c0/c8713e51eccc650422716a14cece1809cfe34bc5ab5e242b7f8b4e2241c2483697b971a604252807689b9dd69bfe3a98852e19a5b89d506b000b4187a1285df8 + languageName: node + linkType: hard + "lodash.uniqby@npm:^4.7.0": version: 4.7.0 resolution: "lodash.uniqby@npm:4.7.0" @@ -5386,6 +5603,13 @@ __metadata: languageName: node linkType: hard +"make-error@npm:^1.3.6": + version: 1.3.6 + resolution: "make-error@npm:1.3.6" + checksum: 10c0/171e458d86854c6b3fc46610cfacf0b45149ba043782558c6875d9f42f222124384ad0b468c92e996d815a8a2003817a710c0a160e49c1c394626f76fa45396f + languageName: node + linkType: hard + "make-fetch-happen@npm:^13.0.0, make-fetch-happen@npm:^13.0.1": version: 13.0.1 resolution: "make-fetch-happen@npm:13.0.1" @@ -5584,7 +5808,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1": +"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -5593,6 +5817,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^5.0.1": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10c0/3defdfd230914f22a8da203747c42ee3c405c39d4d37ffda284dac5e45b7e1f6c49aa8be606509002898e73091ff2a3bbfc59c2c6c71d4660609f63aa92f98e3 + languageName: node + linkType: hard + "minimatch@npm:^9.0.0, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": version: 9.0.5 resolution: "minimatch@npm:9.0.5" @@ -7719,6 +7952,43 @@ __metadata: languageName: node linkType: hard +"ts-jest@npm:^29.2.5": + version: 29.2.5 + resolution: "ts-jest@npm:29.2.5" + dependencies: + bs-logger: "npm:^0.2.6" + ejs: "npm:^3.1.10" + fast-json-stable-stringify: "npm:^2.1.0" + jest-util: "npm:^29.0.0" + json5: "npm:^2.2.3" + lodash.memoize: "npm:^4.1.2" + make-error: "npm:^1.3.6" + semver: "npm:^7.6.3" + yargs-parser: "npm:^21.1.1" + peerDependencies: + "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/transform": ^29.0.0 + "@jest/types": ^29.0.0 + babel-jest: ^29.0.0 + jest: ^29.0.0 + typescript: ">=4.3 <6" + peerDependenciesMeta: + "@babel/core": + optional: true + "@jest/transform": + optional: true + "@jest/types": + optional: true + babel-jest: + optional: true + esbuild: + optional: true + bin: + ts-jest: cli.js + checksum: 10c0/acb62d168faec073e64b20873b583974ba8acecdb94681164eb346cef82ade8fb481c5b979363e01a97ce4dd1e793baf64d9efd90720bc941ad7fc1c3d6f3f68 + languageName: node + linkType: hard + "tuf-js@npm:^2.2.1": version: 2.2.1 resolution: "tuf-js@npm:2.2.1" @@ -7786,6 +8056,26 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.6.3": + version: 5.6.3 + resolution: "typescript@npm:5.6.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/44f61d3fb15c35359bc60399cb8127c30bae554cd555b8e2b46d68fa79d680354b83320ad419ff1b81a0bdf324197b29affe6cc28988cd6a74d4ac60c94f9799 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.6.3#optional!builtin": + version: 5.6.3 + resolution: "typescript@patch:typescript@npm%3A5.6.3#optional!builtin::version=5.6.3&hash=8c6c40" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/7c9d2e07c81226d60435939618c91ec2ff0b75fbfa106eec3430f0fcf93a584bc6c73176676f532d78c3594fe28a54b36eb40b3d75593071a7ec91301533ace7 + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.13.10 resolution: "uglify-js@npm:3.13.10"