diff --git a/rest.js b/rest.js index bff6df47..a13d8a7c 100644 --- a/rest.js +++ b/rest.js @@ -26,6 +26,23 @@ const checkPatchOverrideSupport = function (req, res) { return undefined !== override && override === "PATCH" } +/** + * Creates middleware to validate PATCH override support for POST requests. + * Returns 405 if the request does not have proper X-HTTP-Method-Override header. + * + * @param {string} message - Error message to send if validation fails + * @returns {Function} Express middleware function + */ +const createPatchOverrideMiddleware = (message) => { + return (req, res, next) => { + if (!checkPatchOverrideSupport(req, res)) { + res.statusMessage = message + return res.status(405).end() + } + next() + } +} + /** * Detects multiple MIME types smuggled into a single Content-Type header. * The following are the cases that should result in a 415 (not a 500) @@ -210,4 +227,4 @@ It may not have completed at all, and most likely did not complete successfully. res.status(error.status).send(error.message) } -export default { checkPatchOverrideSupport, verifyJsonContentType, verifyEitherContentType, messenger } +export default { checkPatchOverrideSupport, createPatchOverrideMiddleware, verifyJsonContentType, verifyEitherContentType, messenger } diff --git a/routes/__tests__/route_wrappers.test.js b/routes/__tests__/route_wrappers.test.js new file mode 100644 index 00000000..3b14f86c --- /dev/null +++ b/routes/__tests__/route_wrappers.test.js @@ -0,0 +1,383 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import express from 'express' +import request from 'supertest' + +import patchSetRouter from '../patchSet.js' +import patchUnsetRouter from '../patchUnset.js' +import patchUpdateRouter from '../patchUpdate.js' +import clientRouter from '../client.js' +import createRouter from '../create.js' +import bulkCreateRouter from '../bulkCreate.js' +import bulkUpdateRouter from '../bulkUpdate.js' +import deleteRouter from '../delete.js' +import historyRouter from '../history.js' +import idRouter from '../id.js' +import overwriteRouter from '../overwrite.js' +import staticRouter from '../static.js' +import sinceRouter from '../since.js' +import updateRouter from '../putUpdate.js' +import indexRouter from '../index.js' +import searchRouter from '../search.js' +import queryRouter from '../query.js' +import releaseRouter from '../release.js' +import apiRoutesRouter from '../api-routes.js' + +function getRoute(router, path) { + const routeLayer = router.stack.find(layer => layer.route?.path === path) + assert.ok(routeLayer, `Expected route for path '${path}'`) + return routeLayer.route +} + +function getMethodLayers(router, path, method) { + return getRoute(router, path).stack.filter(layer => layer.method === method) +} + +function createResponse() { + return { + headers: {}, + statusCode: undefined, + statusMessage: undefined, + body: undefined, + ended: false, + set(name, value) { + this.headers[name] = value + return this + }, + status(code) { + this.statusCode = code + return this + }, + send(value) { + this.body = value + this.ended = true + return this + }, + sendFile(filePath, options) { + this.body = { filePath, options } + this.ended = true + return this + }, + end() { + this.ended = true + return this + } + } +} + +function invokeLayer(layer, req = {}, res = createResponse()) { + const nextCalls = [] + layer.handle(req, res, arg => nextCalls.push(arg)) + return { res, nextCalls } +} + +function getOverrideLayer(router) { + const postLayers = getMethodLayers(router, '/', 'post') + const overrideLayer = postLayers.at(-2) + assert.ok(overrideLayer, 'Expected override middleware layer') + return overrideLayer +} + +function assertInvalidOverride(router, expectedMessage) { + const { res, nextCalls } = invokeLayer(getOverrideLayer(router), { + header() { + return undefined + } + }) + + assert.strictEqual(res.statusCode, 405) + assert.strictEqual(res.statusMessage, expectedMessage) + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) +} + +function assertValidOverride(router) { + const { res, nextCalls } = invokeLayer(getOverrideLayer(router), { + header(name) { + return name === 'X-HTTP-Method-Override' ? 'PATCH' : undefined + } + }) + + assert.strictEqual(res.statusCode, undefined) + assert.strictEqual(res.ended, false) + assert.strictEqual(nextCalls.length, 1) + assert.strictEqual(nextCalls[0], undefined) +} + +function assertUnsupportedMethod(router, expectedMessage) { + const fallbackLayer = getRoute(router, '/').stack.at(-1) + assert.ok(fallbackLayer, 'Expected fallback .all() layer') + + const { res, nextCalls } = invokeLayer(fallbackLayer) + + assert.strictEqual(res.statusCode, 405) + assert.strictEqual(res.statusMessage, expectedMessage) + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) +} + +function assertUnsupportedMethodOnPath(router, path, expectedMessage) { + const fallbackLayer = getRoute(router, path).stack.at(-1) + assert.ok(fallbackLayer, `Expected fallback .all() layer for '${path}'`) + + const { res, nextCalls } = invokeLayer(fallbackLayer) + + assert.strictEqual(res.statusCode, 405) + assert.strictEqual(res.statusMessage, expectedMessage) + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) +} + +describe('patch route wrappers', () => { + it('rejects POST /set requests without PATCH override', () => { + assertInvalidOverride( + patchSetRouter, + 'Improper request method for updating, please use PATCH to add new keys to this object.' + ) + }) + + it('passes POST /set requests with PATCH override to the next handler', () => { + assertValidOverride(patchSetRouter) + }) + + it('rejects unsupported methods for /set', () => { + assertUnsupportedMethod( + patchSetRouter, + 'Improper request method for updating, please use PATCH to add new keys to this object.' + ) + }) + + it('rejects POST /unset requests without PATCH override', () => { + assertInvalidOverride( + patchUnsetRouter, + 'Improper request method for updating, please use PATCH to remove keys from this object.' + ) + }) + + it('passes POST /unset requests with PATCH override to the next handler', () => { + assertValidOverride(patchUnsetRouter) + }) + + it('rejects unsupported methods for /unset', () => { + assertUnsupportedMethod( + patchUnsetRouter, + 'Improper request method for updating, please use PATCH to remove keys from this object.' + ) + }) + + it('rejects POST /patch requests without PATCH override', () => { + assertInvalidOverride( + patchUpdateRouter, + 'Improper request method for updating, please use PATCH to alter the existing keys this object.' + ) + }) + + it('passes POST /patch requests with PATCH override to the next handler', () => { + assertValidOverride(patchUpdateRouter) + }) + + it('rejects unsupported methods for /patch', () => { + assertUnsupportedMethod( + patchUpdateRouter, + 'Improper request method for updating, please use PATCH to alter existing keys on this object.' + ) + }) +}) + +describe('client route wrappers', () => { + it('builds the Auth0 registration URL with the expected query params', async () => { + const audience = process.env.AUDIENCE + const clientId = process.env.CLIENT_ID + const rerumPrefix = process.env.RERUM_PREFIX + + process.env.AUDIENCE = 'https://example.org/audience' + process.env.CLIENT_ID = 'client-123' + process.env.RERUM_PREFIX = 'https://example.org/rerum' + + const app = express() + app.use('/client', clientRouter) + + try { + const response = await request(app).get('/client/register') + + assert.strictEqual(response.statusCode, 200) + const registrationUrl = new URL(response.text) + assert.strictEqual(registrationUrl.origin + registrationUrl.pathname, 'https://cubap.auth0.com/authorize') + assert.strictEqual(registrationUrl.searchParams.get('audience'), 'https://example.org/audience') + assert.strictEqual(registrationUrl.searchParams.get('scope'), 'offline_access') + assert.strictEqual(registrationUrl.searchParams.get('response_type'), 'code') + assert.strictEqual(registrationUrl.searchParams.get('client_id'), 'client-123') + assert.strictEqual(registrationUrl.searchParams.get('redirect_uri'), 'https://example.org/rerum') + assert.strictEqual(registrationUrl.searchParams.get('state'), 'register') + } + finally { + process.env.AUDIENCE = audience + process.env.CLIENT_ID = clientId + process.env.RERUM_PREFIX = rerumPrefix + } + }) + + it('returns a plain-text success response from the verified token handler', () => { + const verifyHandler = getMethodLayers(clientRouter, '/verify', 'get').at(-1) + assert.ok(verifyHandler, 'Expected verify handler after auth middleware') + + const { res, nextCalls } = invokeLayer(verifyHandler, { + user: { + 'http://store.rerum.io/agent': 'https://store.rerum.io/v1/id/test-agent' + } + }) + + assert.strictEqual(res.headers['Content-Type'], 'text/plain') + assert.strictEqual(res.statusCode, 200) + assert.strictEqual(res.body, 'The token was verified by Auth0') + assert.deepStrictEqual(nextCalls, []) + }) +}) + +describe('search, query, and release fallbacks', () => { + it('rejects unsupported methods for /bulkCreate', () => { + assertUnsupportedMethod( + bulkCreateRouter, + 'Improper request method for creating, please use POST.' + ) + }) + + it('rejects unsupported methods for /bulkUpdate', () => { + assertUnsupportedMethod( + bulkUpdateRouter, + 'Improper request method for creating, please use PUT.' + ) + }) + + it('rejects unsupported methods for /create', () => { + assertUnsupportedMethod( + createRouter, + 'Improper request method for creating, please use POST.' + ) + }) + + it('rejects unsupported methods for /update', () => { + assertUnsupportedMethod( + updateRouter, + 'Improper request method for updating, please use PUT to update this object.' + ) + }) + + it('rejects unsupported methods for /overwrite', () => { + assertUnsupportedMethod( + overwriteRouter, + 'Improper request method for overwriting, please use PUT to overwrite this object.' + ) + }) + + it('rejects unsupported methods for /search', () => { + assertUnsupportedMethod( + searchRouter, + 'Improper request method for search. Please use POST.' + ) + }) + + it('rejects unsupported methods for /search/phrase', () => { + const fallbackLayer = getRoute(searchRouter, '/phrase').stack.at(-1) + assert.ok(fallbackLayer, 'Expected fallback .all() layer for /phrase') + + const { res, nextCalls } = invokeLayer(fallbackLayer) + + assert.strictEqual(res.statusCode, 405) + assert.strictEqual(res.statusMessage, 'Improper request method for search. Please use POST.') + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) + }) + + it('rejects unsupported methods for /query', () => { + assertUnsupportedMethod( + queryRouter, + 'Improper request method for requesting objects with matching properties. Please use POST.' + ) + }) + + it('rejects unsupported methods for /release/:id', () => { + assertUnsupportedMethodOnPath( + releaseRouter, + '/:_id', + 'Improper request method for releasing, please use PATCH to release this object.' + ) + }) + + it('rejects unsupported methods for /delete/:id', () => { + assertUnsupportedMethodOnPath( + deleteRouter, + '/:_id', + 'Improper request method for deleting, please use DELETE.' + ) + }) + + it('rejects unsupported methods for /history/:id', () => { + assertUnsupportedMethodOnPath( + historyRouter, + '/:_id', + 'Improper request method, please use GET.' + ) + }) + + it('rejects unsupported methods for /id/:id', () => { + assertUnsupportedMethodOnPath( + idRouter, + '/:_id', + 'Improper request method, please use GET.' + ) + }) + + it('rejects unsupported methods for /since/:id', () => { + assertUnsupportedMethodOnPath( + sinceRouter, + '/:_id', + 'Improper request method, please use GET.' + ) + }) +}) + +describe('api routes discovery', () => { + it('directly serves the welcome page from the static route handler', () => { + const handler = getMethodLayers(staticRouter, '/', 'get').at(-1) + assert.ok(handler, 'Expected static router GET handler') + + const { res, nextCalls } = invokeLayer(handler) + + assert.deepStrictEqual(res.body, { filePath: 'index.html', options: undefined }) + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) + }) + + it('serves the public index page from GET /', async () => { + const app = express() + app.use(indexRouter) + + const response = await request(app).get('/') + + assert.strictEqual(response.statusCode, 200) + assert.match(response.headers['content-type'], /^text\/html/) + assert.match(response.text, /RERUM/i) + }) + + it('returns the advertised endpoint map for GET /api', async () => { + const app = express() + app.use(apiRoutesRouter) + + const response = await request(app).get('/api') + + assert.strictEqual(response.statusCode, 200) + assert.strictEqual(response.body.message, 'Welcome to v1 in nodeJS! Below are the available endpoints, used like /v1/api/{endpoint}') + assert.deepStrictEqual(response.body.endpoints, { + '/create': 'POST - Create a new object.', + '/update': 'PUT - Update the body an existing object.', + '/patch': 'PATCH - Update the properties of an existing object.', + '/set': 'PATCH - Update the body an existing object by adding a new property.', + '/unset': 'PATCH - Update the body an existing object by removing an existing property.', + '/delete': 'DELETE - Mark an object as deleted.', + '/query': 'POST - Supply a JSON object to match on, and query the db for an array of matches.', + '/release': 'POST - Lock a JSON object from changes and guarantee the content and URI.', + '/overwrite': 'POST - Update a specific document in place, overwriting the existing body.' + }) + }) +}) diff --git a/routes/bulkCreate.js b/routes/bulkCreate.js index b4cb49f4..3b5286ae 100644 --- a/routes/bulkCreate.js +++ b/routes/bulkCreate.js @@ -11,8 +11,7 @@ router.route('/') .post(auth.checkJwt, rest.verifyJsonContentType, controller.bulkCreate) .all((req, res, next) => { res.statusMessage = 'Improper request method for creating, please use POST.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/bulkUpdate.js b/routes/bulkUpdate.js index 293cd113..c22dcfb8 100644 --- a/routes/bulkUpdate.js +++ b/routes/bulkUpdate.js @@ -11,8 +11,7 @@ router.route('/') .put(auth.checkJwt, rest.verifyJsonContentType, controller.bulkUpdate) .all((req, res, next) => { res.statusMessage = 'Improper request method for creating, please use PUT.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/create.js b/routes/create.js index e015d129..9a879e58 100644 --- a/routes/create.js +++ b/routes/create.js @@ -10,8 +10,7 @@ router.route('/') .post(auth.checkJwt, rest.verifyJsonContentType, controller.create) .all((req, res, next) => { res.statusMessage = 'Improper request method for creating, please use POST.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/delete.js b/routes/delete.js index 5254740e..f2f63565 100644 --- a/routes/delete.js +++ b/routes/delete.js @@ -8,8 +8,7 @@ router.route('/:_id') .delete(auth.checkJwt, deleteObj) .all((req, res, next) => { res.statusMessage = 'Improper request method for deleting, please use DELETE.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/history.js b/routes/history.js index 06470da0..9abd6dd9 100644 --- a/routes/history.js +++ b/routes/history.js @@ -8,8 +8,7 @@ router.route('/:_id') .head(controller.historyHeadRequest) .all((req, res, next) => { res.statusMessage = 'Improper request method, please use GET.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/id.js b/routes/id.js index 3c2e8988..0e3d8d70 100644 --- a/routes/id.js +++ b/routes/id.js @@ -8,8 +8,7 @@ router.route('/:_id') .head(controller.idHeadRequest) .all((req, res, next) => { res.statusMessage = 'Improper request method, please use GET.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/overwrite.js b/routes/overwrite.js index edb8ed06..657a4232 100644 --- a/routes/overwrite.js +++ b/routes/overwrite.js @@ -10,8 +10,7 @@ router.route('/') .put(auth.checkJwt, rest.verifyJsonContentType, controller.overwrite) .all((req, res, next) => { res.statusMessage = 'Improper request method for overwriting, please use PUT to overwrite this object.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/patchSet.js b/routes/patchSet.js index 56db7e66..c5c41bb8 100644 --- a/routes/patchSet.js +++ b/routes/patchSet.js @@ -5,21 +5,14 @@ import controller from '../db-controller.js' import auth from '../auth/index.js' import rest from '../rest.js' +const checkPatchOverride = rest.createPatchOverrideMiddleware('Improper request method for updating, please use PATCH to add new keys to this object.') + router.route('/') .patch(auth.checkJwt, rest.verifyJsonContentType, controller.patchSet) - .post(auth.checkJwt, rest.verifyJsonContentType, (req, res, next) => { - if (!rest.checkPatchOverrideSupport(req, res)) { - res.statusMessage = 'Improper request method for updating, please use PATCH to add new keys to this object.' - res.status(405) - next(res) - return - } - controller.patchSet(req, res, next) - }) + .post(auth.checkJwt, rest.verifyJsonContentType, checkPatchOverride, controller.patchSet) .all((req, res, next) => { res.statusMessage = 'Improper request method for updating, please use PATCH to add new keys to this object.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/patchUnset.js b/routes/patchUnset.js index 6def00ae..da9ced82 100644 --- a/routes/patchUnset.js +++ b/routes/patchUnset.js @@ -5,21 +5,14 @@ import controller from '../db-controller.js' import auth from '../auth/index.js' import rest from '../rest.js' +const checkPatchOverride = rest.createPatchOverrideMiddleware('Improper request method for updating, please use PATCH to remove keys from this object.') + router.route('/') .patch(auth.checkJwt, rest.verifyJsonContentType, controller.patchUnset) - .post(auth.checkJwt, rest.verifyJsonContentType, (req, res, next) => { - if (!rest.checkPatchOverrideSupport(req, res)) { - res.statusMessage = 'Improper request method for updating, please use PATCH to remove keys from this object.' - res.status(405) - next(res) - return - } - controller.patchUnset(req, res, next) - }) + .post(auth.checkJwt, rest.verifyJsonContentType, checkPatchOverride, controller.patchUnset) .all((req, res, next) => { res.statusMessage = 'Improper request method for updating, please use PATCH to remove keys from this object.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/patchUpdate.js b/routes/patchUpdate.js index 7c4918f1..5a9731f5 100644 --- a/routes/patchUpdate.js +++ b/routes/patchUpdate.js @@ -6,21 +6,14 @@ import controller from '../db-controller.js' import rest from '../rest.js' import auth from '../auth/index.js' +const checkPatchOverride = rest.createPatchOverrideMiddleware('Improper request method for updating, please use PATCH to alter the existing keys this object.') + router.route('/') - .patch(auth.checkJwt, rest.verifyJsonContentType, controller.patchUpdate) - .post(auth.checkJwt, rest.verifyJsonContentType, (req, res, next) => { - if (!rest.checkPatchOverrideSupport(req, res)) { - res.statusMessage = 'Improper request method for updating, please use PATCH to alter the existing keys this object.' - res.status(405) - next(res) - return - } - controller.patchUpdate(req, res, next) - }) - .all((req, res, next) => { - res.statusMessage = 'Improper request method for updating, please use PATCH to alter existing keys on this object.' - res.status(405) - next(res) - }) + .patch(auth.checkJwt, rest.verifyJsonContentType, controller.patchUpdate) + .post(auth.checkJwt, rest.verifyJsonContentType, checkPatchOverride, controller.patchUpdate) + .all((req, res, next) => { + res.statusMessage = 'Improper request method for updating, please use PATCH to alter existing keys on this object.' + res.status(405).end() + }) export default router diff --git a/routes/putUpdate.js b/routes/putUpdate.js index 88cc93f4..cca93137 100644 --- a/routes/putUpdate.js +++ b/routes/putUpdate.js @@ -10,8 +10,7 @@ router.route('/') .put(auth.checkJwt, rest.verifyJsonContentType, controller.putUpdate) .all((req, res, next) => { res.statusMessage = 'Improper request method for updating, please use PUT to update this object.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/query.js b/routes/query.js index 5be0c5ba..b9882b82 100644 --- a/routes/query.js +++ b/routes/query.js @@ -9,8 +9,7 @@ router.route('/') .head(controller.queryHeadRequest) .all((req, res, next) => { res.statusMessage = 'Improper request method for requesting objects with matching properties. Please use POST.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/release.js b/routes/release.js index 870c0d88..3a90ac15 100644 --- a/routes/release.js +++ b/routes/release.js @@ -9,8 +9,7 @@ router.route('/:_id') .patch(auth.checkJwt, controller.release) .all((req, res, next) => { res.statusMessage = 'Improper request method for releasing, please use PATCH to release this object.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/routes/search.js b/routes/search.js index 9b9948ca..e9c3bd51 100644 --- a/routes/search.js +++ b/routes/search.js @@ -7,19 +7,17 @@ router.route('/') .post(rest.verifyEitherContentType, controller.searchAsWords) .all((req, res, next) => { res.statusMessage = 'Improper request method for search. Please use POST.' - res.status(405) - next(res) + res.status(405).end() }) router.route('/phrase') .post(rest.verifyEitherContentType, controller.searchAsPhrase) .all((req, res, next) => { res.statusMessage = 'Improper request method for search. Please use POST.' - res.status(405) - next(res) + res.status(405).end() }) // Note that there are more search functions available in the controller, such as controller.searchFuzzily // They can be used through additional endpoints here when we are ready. -export default router \ No newline at end of file +export default router diff --git a/routes/since.js b/routes/since.js index e0f7a841..c328fd5b 100644 --- a/routes/since.js +++ b/routes/since.js @@ -8,8 +8,7 @@ router.route('/:_id') .head(controller.sinceHeadRequest) .all((req, res, next) => { res.statusMessage = 'Improper request method, please use GET.' - res.status(405) - next(res) + res.status(405).end() }) export default router diff --git a/test/coverage-inventory.json b/test/coverage-inventory.json new file mode 100644 index 00000000..01a838d3 --- /dev/null +++ b/test/coverage-inventory.json @@ -0,0 +1,98 @@ +{ + "runner": { + "test": "node:test", + "coverage": "c8", + "bootstrap": "test/bootstrap.js", + "loader": "test/loader.js" + }, + "validation": { + "suite": { + "tests": 108, + "suites": 15, + "pass": 105, + "fail": 0, + "skip": 3 + }, + "coverage": { + "statements": 98.77, + "branches": 100, + "functions": 100, + "lines": 98.77 + } + }, + "rootSuites": [ + "__tests__/core_provider_contract.test.js", + "__tests__/openapi_sync_artifacts.test.js", + "__tests__/provider_sync_artifacts.test.js", + "__tests__/routes_mounted.test.js" + ], + "nativeSuites": { + "added": [ + "auth/__tests__/token.test.js", + "routes/__tests__/overwrite.test.js", + "routes/__tests__/route_wrappers.test.js" + ], + "migrated": [ + "routes/__tests__/bulkCreate.test.js", + "routes/__tests__/bulkUpdate.test.js", + "routes/__tests__/contentType.test.js", + "routes/__tests__/create.test.js", + "routes/__tests__/delete.test.js", + "routes/__tests__/history.test.js", + "routes/__tests__/id.test.js", + "routes/__tests__/idNegotiation.test.js", + "routes/__tests__/patch.test.js", + "routes/__tests__/query.test.js", + "routes/__tests__/release.test.js", + "routes/__tests__/set.test.js", + "routes/__tests__/since.test.js", + "routes/__tests__/unset.test.js", + "routes/__tests__/update.test.js" + ], + "removed": [ + "auth/__tests__/token.test.txt", + "routes/__tests__/client.test.txt", + "routes/__tests__/compatability.test.txt", + "routes/__tests__/crud_routes_function.txt", + "routes/__tests__/overwrite-optimistic-locking.test.txt", + "routes/__tests__/overwrite.test.txt" + ] + }, + "harnessCleanup": { + "removed": [ + "jest.config.js", + "test/jest-globals.js", + "test/register-globals.js", + "test/setup.js", + "test/tiers.js", + "test/utils.js", + "test/mocks/db.js" + ], + "kept": [ + "database/__mocks__/index.js", + "test/bootstrap.js", + "test/loader.js" + ] + }, + "coverageScope": { + "included": [ + "db-controller.js", + "routes/**/*.js" + ], + "excluded": [ + "**/__tests__/**" + ] + }, + "notableCoverageGaps": [ + { + "file": "routes/_gog_fragments_from_manuscript.js", + "lines": "10-12", + "statements": 80 + }, + { + "file": "routes/_gog_glosses_from_manuscript.js", + "lines": "10-12", + "statements": 80 + } + ] +}