From 537a7f5f498c1147f22e34991f7e49b3cb962581 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 14 May 2026 14:39:25 -0500 Subject: [PATCH 1/3] Add middleware for PATCH override support Introduce createPatchOverrideMiddleware in rest.js to centralize validation of X-HTTP-Method-Override for POST-to-PATCH requests. Replace duplicated inline checks in routes/patchSet.js, routes/patchUnset.js, and routes/patchUpdate.js with the new middleware (each using a route-specific statusMessage). Also standardize error handling by ending 405 responses with res.status(405).end() and export the new middleware from rest.js. --- rest.js | 19 ++++++++++++++++++- routes/patchSet.js | 15 ++++----------- routes/patchUnset.js | 15 ++++----------- routes/patchUpdate.js | 23 ++++++++--------------- 4 files changed, 34 insertions(+), 38 deletions(-) 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/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 From 0a7218106cc28496553a1ed4dd6bdc90bab6b5d2 Mon Sep 17 00:00:00 2001 From: cubap Date: Thu, 14 May 2026 15:59:48 -0500 Subject: [PATCH 2/3] End 405 responses and add route wrapper tests Replace patterns that set status and call next(res) with res.status(405).end() in route fallbacks (routes/query.js, routes/release.js, routes/search.js) to ensure the response is terminated immediately. Add route wrapper tests (routes/__tests__/route_wrappers.test.js) to validate method-override handling, unsupported-method fallbacks, client verify behavior, and API discovery. Include a test coverage inventory (test/coverage-inventory.json) to record current test/coverage state. --- routes/__tests__/route_wrappers.test.js | 270 ++++++++++++++++++++++++ routes/query.js | 3 +- routes/release.js | 3 +- routes/search.js | 8 +- test/coverage-inventory.json | 108 ++++++++++ 5 files changed, 383 insertions(+), 9 deletions(-) create mode 100644 routes/__tests__/route_wrappers.test.js create mode 100644 test/coverage-inventory.json diff --git a/routes/__tests__/route_wrappers.test.js b/routes/__tests__/route_wrappers.test.js new file mode 100644 index 00000000..e480e8bc --- /dev/null +++ b/routes/__tests__/route_wrappers.test.js @@ -0,0 +1,270 @@ +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 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 + }, + 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, []) +} + +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 /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', () => { + const fallbackLayer = getRoute(releaseRouter, '/:_id').stack.at(-1) + assert.ok(fallbackLayer, 'Expected fallback .all() layer for /:_id') + + const { res, nextCalls } = invokeLayer(fallbackLayer) + + assert.strictEqual(res.statusCode, 405) + assert.strictEqual(res.statusMessage, 'Improper request method for releasing, please use PATCH to release this object.') + assert.strictEqual(res.ended, true) + assert.deepStrictEqual(nextCalls, []) + }) +}) + +describe('api routes discovery', () => { + 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/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/test/coverage-inventory.json b/test/coverage-inventory.json new file mode 100644 index 00000000..d3f25b90 --- /dev/null +++ b/test/coverage-inventory.json @@ -0,0 +1,108 @@ +{ + "runner": { + "test": "node:test", + "coverage": "c8", + "bootstrap": "test/bootstrap.js", + "loader": "test/loader.js" + }, + "validation": { + "suite": { + "tests": 97, + "suites": 15, + "pass": 94, + "fail": 0, + "skip": 3 + }, + "coverage": { + "statements": 92.98, + "branches": 100, + "functions": 100, + "lines": 92.98 + } + }, + "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/create.js", + "lines": "12-14", + "statements": 82.35 + }, + { + "file": "routes/putUpdate.js", + "lines": "12-14", + "statements": 82.35 + }, + { + "file": "routes/overwrite.js", + "lines": "12-14", + "statements": 82.35 + }, + { + "file": "routes/index.js", + "lines": "6", + "statements": 88.88 + } + ] +} From 5d40b1fe130fb1ba235cf4d034fe402c70ca2513 Mon Sep 17 00:00:00 2001 From: cubap Date: Fri, 15 May 2026 12:14:59 -0500 Subject: [PATCH 3/3] End responses for unsupported-method handlers Replace next(res) with res.status(405).end() in multiple route fallbacks to ensure responses are terminated immediately (bulkCreate, bulkUpdate, create, delete, history, id, overwrite, putUpdate, since). Update route wrapper tests to import additional routers, add helper asserting fallback behavior on specific paths, and add tests for static/index handlers. Update test coverage inventory to reflect the new/updated tests. --- routes/__tests__/route_wrappers.test.js | 127 ++++++++++++++++++++++-- routes/bulkCreate.js | 3 +- routes/bulkUpdate.js | 3 +- routes/create.js | 3 +- routes/delete.js | 3 +- routes/history.js | 3 +- routes/id.js | 3 +- routes/overwrite.js | 3 +- routes/putUpdate.js | 3 +- routes/since.js | 3 +- test/coverage-inventory.json | 30 ++---- 11 files changed, 139 insertions(+), 45 deletions(-) diff --git a/routes/__tests__/route_wrappers.test.js b/routes/__tests__/route_wrappers.test.js index e480e8bc..3b14f86c 100644 --- a/routes/__tests__/route_wrappers.test.js +++ b/routes/__tests__/route_wrappers.test.js @@ -7,6 +7,17 @@ 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' @@ -42,6 +53,11 @@ function createResponse() { this.ended = true return this }, + sendFile(filePath, options) { + this.body = { filePath, options } + this.ended = true + return this + }, end() { this.ended = true return this @@ -100,6 +116,18 @@ function assertUnsupportedMethod(router, expectedMessage) { 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( @@ -207,6 +235,41 @@ describe('client route wrappers', () => { }) 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, @@ -234,19 +297,69 @@ describe('search, query, and release fallbacks', () => { }) it('rejects unsupported methods for /release/:id', () => { - const fallbackLayer = getRoute(releaseRouter, '/:_id').stack.at(-1) - assert.ok(fallbackLayer, 'Expected fallback .all() layer for /:_id') + assertUnsupportedMethodOnPath( + releaseRouter, + '/:_id', + 'Improper request method for releasing, please use PATCH to release this object.' + ) + }) - const { res, nextCalls } = invokeLayer(fallbackLayer) + it('rejects unsupported methods for /delete/:id', () => { + assertUnsupportedMethodOnPath( + deleteRouter, + '/:_id', + 'Improper request method for deleting, please use DELETE.' + ) + }) - assert.strictEqual(res.statusCode, 405) - assert.strictEqual(res.statusMessage, 'Improper request method for releasing, please use PATCH to release this object.') - assert.strictEqual(res.ended, true) - assert.deepStrictEqual(nextCalls, []) + 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) 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/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/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 index d3f25b90..01a838d3 100644 --- a/test/coverage-inventory.json +++ b/test/coverage-inventory.json @@ -7,17 +7,17 @@ }, "validation": { "suite": { - "tests": 97, + "tests": 108, "suites": 15, - "pass": 94, + "pass": 105, "fail": 0, "skip": 3 }, "coverage": { - "statements": 92.98, + "statements": 98.77, "branches": 100, "functions": 100, - "lines": 92.98 + "lines": 98.77 } }, "rootSuites": [ @@ -85,24 +85,14 @@ }, "notableCoverageGaps": [ { - "file": "routes/create.js", - "lines": "12-14", - "statements": 82.35 + "file": "routes/_gog_fragments_from_manuscript.js", + "lines": "10-12", + "statements": 80 }, { - "file": "routes/putUpdate.js", - "lines": "12-14", - "statements": 82.35 - }, - { - "file": "routes/overwrite.js", - "lines": "12-14", - "statements": 82.35 - }, - { - "file": "routes/index.js", - "lines": "6", - "statements": 88.88 + "file": "routes/_gog_glosses_from_manuscript.js", + "lines": "10-12", + "statements": 80 } ] }