From 1c14267a6099b8ee1c80cbeac7e28b653f1194c2 Mon Sep 17 00:00:00 2001 From: Patrick Cuba Date: Thu, 2 Apr 2026 15:24:00 -0500 Subject: [PATCH 01/10] Add cloneObject helper and replace JSON clones Introduce cloneObject in utils.js that uses globalThis.structuredClone when available and falls back to JSON.parse(JSON.stringify(...)). Replace direct JSON cloning in configureRerumOptions for configuredObject and received_options with cloneObject, and export cloneObject. This centralizes cloning logic and allows better deep-clone behavior when structuredClone is supported. --- utils.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/utils.js b/utils.js index e007dc72..0d4a0958 100644 --- a/utils.js +++ b/utils.js @@ -25,9 +25,16 @@ isReleased —always "" * @param update A trigger for special handling from update actions * @return configuredObject The same object that was recieved but with the proper __rerum options. This object is intended to be saved as a new object (@see versioning) */ +const cloneObject = function(value){ + if(typeof globalThis.structuredClone === "function"){ + return globalThis.structuredClone(value) + } + return JSON.parse(JSON.stringify(value)) +} + const configureRerumOptions = function(generator, received, update, extUpdate){ - let configuredObject = JSON.parse(JSON.stringify(received)) - let received_options = received.__rerum ? JSON.parse(JSON.stringify(received.__rerum)) : {} + let configuredObject = cloneObject(received) + let received_options = received.__rerum ? cloneObject(received.__rerum) : {} let history = {} let releases = {} let rerumOptions = {} @@ -259,6 +266,7 @@ function createExpressError(err) { } export default { + cloneObject, configureRerumOptions, createExpressError, isDeleted, From 47d5bac61028bf1020aaa9f3344637ce9e1dacb8 Mon Sep 17 00:00:00 2001 From: Patrick Cuba Date: Thu, 2 Apr 2026 15:25:26 -0500 Subject: [PATCH 02/10] Add pagination helpers and use cloneObject Introduce MAX_QUERY_LIMIT and MAX_QUERY_SKIP and add clampNonNegativeInt and getPagination to safely parse and clamp limit/skip query params (with sensible defaults and env overrides). Replace ad-hoc deep copies (JSON.parse(JSON.stringify(...))) with utils.cloneObject in idNegotiation and getAllVersions to avoid issues with prototype loss and improve clarity. Export getPagination for reuse. --- controllers/utils.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/controllers/utils.js b/controllers/utils.js index 602d372e..0b7a5941 100644 --- a/controllers/utils.js +++ b/controllers/utils.js @@ -9,6 +9,24 @@ import utils from '../utils.js' const ObjectID = newID +const MAX_QUERY_LIMIT = Number.parseInt(process.env.RERUM_MAX_QUERY_LIMIT ?? 500, 10) +const MAX_QUERY_SKIP = Number.parseInt(process.env.RERUM_MAX_QUERY_SKIP ?? 100000, 10) + +function clampNonNegativeInt(value, fallback, max) { + const parsed = Number.parseInt(value, 10) + if (!Number.isFinite(parsed) || parsed < 0) return fallback + return parsed > max ? max : parsed +} + +function getPagination(query = {}, defaultLimit = 100) { + const limitMax = Number.isFinite(MAX_QUERY_LIMIT) && MAX_QUERY_LIMIT > 0 ? MAX_QUERY_LIMIT : 500 + const skipMax = Number.isFinite(MAX_QUERY_SKIP) && MAX_QUERY_SKIP >= 0 ? MAX_QUERY_SKIP : 100000 + const safeDefaultLimit = defaultLimit > 0 ? defaultLimit : 100 + const limit = clampNonNegativeInt(query.limit, safeDefaultLimit, limitMax) + const skip = clampNonNegativeInt(query.skip, 0, skipMax) + return { limit, skip } +} + /** * Check if a @context value contains a known @id-id mapping context * @@ -52,7 +70,7 @@ const idNegotiation = function (resBody) { const _id = resBody._id delete resBody._id if(!resBody["@context"]) return resBody - let modifiedResBody = JSON.parse(JSON.stringify(resBody)) + let modifiedResBody = utils.cloneObject(resBody) const context = { "@context": resBody["@context"] } if(_contextid(resBody["@context"])) { delete resBody["@id"] @@ -182,7 +200,7 @@ async function getAllVersions(obj) { let rootObj if (primeID === "root") { //The obj passed in is root. So it is the rootObj we need. - rootObj = JSON.parse(JSON.stringify(obj)) + rootObj = utils.cloneObject(obj) } else if (primeID) { //The obj passed in knows the ID of root, grab it from Mongo //Use _id for indexed query performance instead of @id @@ -447,6 +465,7 @@ async function healReleasesTree(releasing) { export { _contextid, idNegotiation, + getPagination, generateSlugId, index, ObjectID, From dc49dd4651a398c3c8498bf12255e7bcdf9f85a3 Mon Sep 17 00:00:00 2001 From: Patrick Cuba Date: Thu, 2 Apr 2026 15:29:39 -0500 Subject: [PATCH 03/10] Use utils.cloneObject and getPagination Replace repeated JSON.parse(JSON.stringify(...)) deep-clones with utils.cloneObject across controllers (crud, patchSet, patchUnset, patchUpdate, putUpdate, gog) and add/get getPagination to standardize parsing of limit/skip in search, gog and crud. Adjust imports accordingly to centralize cloning and pagination logic, improving readability and consistency. --- controllers/crud.js | 9 ++++----- controllers/gog.js | 8 +++----- controllers/patchSet.js | 8 ++++---- controllers/patchUnset.js | 8 ++++---- controllers/patchUpdate.js | 8 ++++---- controllers/putUpdate.js | 8 ++++---- controllers/search.js | 17 ++++++----------- package-lock.json | 28 ++-------------------------- 8 files changed, 31 insertions(+), 63 deletions(-) diff --git a/controllers/crud.js b/controllers/crud.js index f89032fb..249a4f5b 100644 --- a/controllers/crud.js +++ b/controllers/crud.js @@ -6,7 +6,7 @@ */ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, idNegotiation, generateSlugId, ObjectID, getAgentClaim, parseDocumentID } from './utils.js' +import { _contextid, idNegotiation, getPagination, generateSlugId, ObjectID, getAgentClaim, parseDocumentID } from './utils.js' /** * Create a new Linked Open Data object in RERUM v1. @@ -37,7 +37,7 @@ const create = async function (req, res, next) { let generatorAgent = getAgentClaim(req, next) if (!generatorAgent) return let context = req.body["@context"] ? { "@context": req.body["@context"] } : {} - let provided = JSON.parse(JSON.stringify(req.body)) + let provided = utils.cloneObject(req.body) let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, provided, false, false)["__rerum"] } if(slug){ rerumProp.__rerum.slug = slug @@ -55,7 +55,7 @@ const create = async function (req, res, next) { let result = await db.insertOne(newObject) res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) + newObject.new_obj_state = utils.cloneObject(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(201) res.json(newObject) @@ -74,8 +74,7 @@ const create = async function (req, res, next) { const query = async function (req, res, next) { res.set("Content-Type", "application/json; charset=utf-8") let props = req.body - const limit = parseInt(req.query.limit ?? 100) - const skip = parseInt(req.query.skip ?? 0) + const { limit, skip } = getPagination(req.query, 100) if (!props || Object.keys(props).length === 0) { //Hey now, don't ask for everything...this can happen by accident. Don't allow it. let err = { diff --git a/controllers/gog.js b/controllers/gog.js index 7b22584c..a6e08fb2 100644 --- a/controllers/gog.js +++ b/controllers/gog.js @@ -8,7 +8,7 @@ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js' +import { _contextid, ObjectID, getAgentClaim, getPagination, parseDocumentID, idNegotiation } from './utils.js' /** * THIS IS SPECIFICALLY FOR 'Gallery of Glosses' @@ -27,8 +27,7 @@ const _gog_fragments_from_manuscript = async function (req, res, next) { if (!agent) return const agentID = agent.split("/").pop() const manID = req.body["ManuscriptWitness"] - const limit = parseInt(req.query.limit ?? 50) - const skip = parseInt(req.query.skip ?? 0) + const { limit, skip } = getPagination(req.query, 50) let err = { message: `` } // This request can only be made my Gallery of Glosses production apps. if (agentID !== "61043ad4ffce846a83e700dd") { @@ -159,8 +158,7 @@ const _gog_glosses_from_manuscript = async function (req, res, next) { if (!agent) return const agentID = agent.split("/").pop() const manID = req.body["ManuscriptWitness"] - const limit = parseInt(req.query.limit ?? 50) - const skip = parseInt(req.query.skip ?? 0) + const { limit, skip } = getPagination(req.query, 50) let err = { message: `` } // This request can only be made my Gallery of Glosses production apps. if (agentID !== "61043ad4ffce846a83e700dd") { diff --git a/controllers/patchSet.js b/controllers/patchSet.js index b3459a7d..5cdef923 100644 --- a/controllers/patchSet.js +++ b/controllers/patchSet.js @@ -21,7 +21,7 @@ import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, al const patchSet = async function (req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = JSON.parse(JSON.stringify(req.body)) + let objectReceived = utils.cloneObject(req.body) let originalContext let patchedObject = {} let generatorAgent = getAgentClaim(req, next) @@ -50,7 +50,7 @@ const patchSet = async function (req, res, next) { }) } else { - patchedObject = JSON.parse(JSON.stringify(originalObject)) + patchedObject = utils.cloneObject(originalObject) if(_contextid(originalObject["@context"])) { // If the original object has a context that needs id protected, make sure you don't set it. delete objectReceived.id @@ -73,7 +73,7 @@ const patchSet = async function (req, res, next) { //Just hand back the object. The resulting of setting nothing is the object from the request body. res.set(utils.configureWebAnnoHeadersFor(originalObject)) originalObject = idNegotiation(originalObject) - originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) + originalObject.new_obj_state = utils.cloneObject(originalObject) res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) res.status(200) res.json(originalObject) @@ -93,7 +93,7 @@ const patchSet = async function (req, res, next) { //Success, the original object has been updated. res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) + newObject.new_obj_state = utils.cloneObject(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(200) res.json(newObject) diff --git a/controllers/patchUnset.js b/controllers/patchUnset.js index 669b19d8..19b07fe6 100644 --- a/controllers/patchUnset.js +++ b/controllers/patchUnset.js @@ -21,7 +21,7 @@ import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, al const patchUnset = async function (req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = JSON.parse(JSON.stringify(req.body)) + let objectReceived = utils.cloneObject(req.body) let patchedObject = {} let generatorAgent = getAgentClaim(req, next) if (!generatorAgent) return @@ -49,7 +49,7 @@ const patchUnset = async function (req, res, next) { }) } else { - patchedObject = JSON.parse(JSON.stringify(originalObject)) + patchedObject = utils.cloneObject(originalObject) delete objectReceived._id //can't unset this delete objectReceived.__rerum //can't unset this delete objectReceived["@id"] //can't unset this @@ -75,7 +75,7 @@ const patchUnset = async function (req, res, next) { //Just hand back the object. The resulting of unsetting nothing is the object. res.set(utils.configureWebAnnoHeadersFor(originalObject)) originalObject = idNegotiation(originalObject) - originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) + originalObject.new_obj_state = utils.cloneObject(originalObject) res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) res.status(200) res.json(originalObject) @@ -97,7 +97,7 @@ const patchUnset = async function (req, res, next) { //Success, the original object has been updated. res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) + newObject.new_obj_state = utils.cloneObject(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(200) res.json(newObject) diff --git a/controllers/patchUpdate.js b/controllers/patchUpdate.js index 5e7c9c27..b28d73bc 100644 --- a/controllers/patchUpdate.js +++ b/controllers/patchUpdate.js @@ -20,7 +20,7 @@ import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, al const patchUpdate = async function (req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = JSON.parse(JSON.stringify(req.body)) + let objectReceived = utils.cloneObject(req.body) let patchedObject = {} let generatorAgent = getAgentClaim(req, next) if (!generatorAgent) return @@ -48,7 +48,7 @@ const patchUpdate = async function (req, res, next) { }) } else { - patchedObject = JSON.parse(JSON.stringify(originalObject)) + patchedObject = utils.cloneObject(originalObject) delete objectReceived.__rerum //can't patch this delete objectReceived._id //can't patch this delete objectReceived["@id"] //can't patch this @@ -74,7 +74,7 @@ const patchUpdate = async function (req, res, next) { //Just hand back the object. The resulting of patching nothing is the object unchanged. res.set(utils.configureWebAnnoHeadersFor(originalObject)) originalObject = idNegotiation(originalObject) - originalObject.new_obj_state = JSON.parse(JSON.stringify(originalObject)) + originalObject.new_obj_state = utils.cloneObject(originalObject) res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) res.status(200) res.json(originalObject) @@ -96,7 +96,7 @@ const patchUpdate = async function (req, res, next) { //Success, the original object has been updated. res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) + newObject.new_obj_state = utils.cloneObject(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(200) res.json(newObject) diff --git a/controllers/putUpdate.js b/controllers/putUpdate.js index 04d7f436..8a8a6118 100644 --- a/controllers/putUpdate.js +++ b/controllers/putUpdate.js @@ -22,7 +22,7 @@ import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, al const putUpdate = async function (req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = JSON.parse(JSON.stringify(req.body)) + let objectReceived = utils.cloneObject(req.body) let generatorAgent = getAgentClaim(req, next) if (!generatorAgent) return const idReceived = objectReceived["@id"] ?? objectReceived.id @@ -69,7 +69,7 @@ const putUpdate = async function (req, res, next) { //Success, the original object has been updated. res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) + newObject.new_obj_state = utils.cloneObject(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(200) res.json(newObject) @@ -107,7 +107,7 @@ const putUpdate = async function (req, res, next) { async function _import(req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = JSON.parse(JSON.stringify(req.body)) + let objectReceived = utils.cloneObject(req.body) let generatorAgent = getAgentClaim(req, next) if (!generatorAgent) return const id = ObjectID() @@ -125,7 +125,7 @@ async function _import(req, res, next) { let result = await db.insertOne(newObject) res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) + newObject.new_obj_state = utils.cloneObject(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(200) res.json(newObject) diff --git a/controllers/search.js b/controllers/search.js index f90d935c..816a1aa6 100644 --- a/controllers/search.js +++ b/controllers/search.js @@ -6,7 +6,7 @@ */ import { db } from '../database/index.js' import utils from '../utils.js' -import { idNegotiation } from './utils.js' +import { idNegotiation, getPagination } from './utils.js' /** * Merges and deduplicates results from multiple MongoDB Atlas Search index queries. @@ -271,8 +271,7 @@ const searchAsWords = async function (req, res, next) { } return next(utils.createExpressError(err)) } - const limit = parseInt(req.query.limit ?? 100) - const skip = parseInt(req.query.skip ?? 0) + const { limit, skip } = getPagination(req.query, 100) const [queryPresi3, queryPresi2] = buildDualIndexQueries(searchText, { type: "text", options: searchOptions }, limit, skip) try { const [resultsPresi3, resultsPresi2] = await Promise.all([ @@ -358,8 +357,7 @@ const searchAsPhrase = async function (req, res, next) { } return next(utils.createExpressError(err)) } - const limit = parseInt(req.query.limit ?? 100) - const skip = parseInt(req.query.skip ?? 0) + const { limit, skip } = getPagination(req.query, 100) const [queryPresi3, queryPresi2] = buildDualIndexQueries(searchText, { type: "phrase", options: phraseOptions }, limit, skip) try { const [resultsPresi3, resultsPresi2] = await Promise.all([ @@ -437,8 +435,7 @@ const searchFuzzily = async function (req, res, next) { } return next(utils.createExpressError(err)) } - const limit = parseInt(req.query.limit ?? 100) - const skip = parseInt(req.query.skip ?? 0) + const { limit, skip } = getPagination(req.query, 100) const [queryPresi3, queryPresi2] = buildDualIndexQueries(searchText, { type: "text", options: fuzzyOptions }, limit, skip) try { const [resultsPresi3, resultsPresi2] = await Promise.all([ @@ -532,8 +529,7 @@ const searchWildly = async function (req, res, next) { } return next(utils.createExpressError(err)) } - const limit = parseInt(req.query.limit ?? 100) - const skip = parseInt(req.query.skip ?? 0) + const { limit, skip } = getPagination(req.query, 100) const [queryPresi3, queryPresi2] = buildDualIndexQueries(searchText, { type: "wildcard", options: wildcardOptions }, limit, skip) try { const [resultsPresi3, resultsPresi2] = await Promise.all([ @@ -626,8 +622,7 @@ const searchAlikes = async function (req, res, next) { } return next(utils.createExpressError(err)) } - const limit = parseInt(req.query.limit ?? 100) - const skip = parseInt(req.query.skip ?? 0) + const { limit, skip } = getPagination(req.query, 100) // Build moreLikeThis queries for both IIIF 3.0 and IIIF 2.1 indexes const searchQuery_presi3 = [ { diff --git a/package-lock.json b/package-lock.json index 1e9a5839..b6f164ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,8 +25,8 @@ "supertest": "^7.2.2" }, "engines": { - "node": ">=24.12.0", - "npm": ">=11.7.0" + "node": ">=24.14.0", + "npm": ">=11.0.0" } }, "node_modules/@babel/code-frame": { @@ -1335,9 +1335,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1352,9 +1349,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1369,9 +1363,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1386,9 +1377,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1403,9 +1391,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1420,9 +1405,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1437,9 +1419,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1454,9 +1433,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ From f777bcd1ba26833d81373074a2cb693788053278 Mon Sep 17 00:00:00 2001 From: Patrick Cuba Date: Thu, 2 Apr 2026 15:51:47 -0500 Subject: [PATCH 04/10] Update history.js --- controllers/history.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/controllers/history.js b/controllers/history.js index e04e9cf4..2e904566 100644 --- a/controllers/history.js +++ b/controllers/history.js @@ -8,7 +8,7 @@ import { newID, isValidID, db } from '../database/index.js' import utils from '../utils.js' -import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, getAllVersions, getAllAncestors, getAllDescendants } from './utils.js' +import { _contextid, ObjectID, getAgentClaim, getPagination, parseDocumentID, idNegotiation, getAllVersions, getAllAncestors, getAllDescendants } from './utils.js' /** * Public facing servlet to gather for all versions downstream from a provided `key object`. @@ -112,8 +112,9 @@ const idHeadRequest = async function (req, res, next) { const queryHeadRequest = async function (req, res, next) { res.set("Content-Type", "application/json; charset=utf-8") let props = req.body + const { limit, skip } = getPagination(req.query, 100) try { - let matches = await db.find(props).toArray() + let matches = await db.find(props).limit(limit).skip(skip).toArray() if (matches.length) { const size = Buffer.byteLength(JSON.stringify(matches)) res.set("Content-Length", size) From d6e17ceecf4c79b170ed0048644b22a711bd7157 Mon Sep 17 00:00:00 2001 From: cubap Date: Wed, 6 May 2026 11:12:13 -0500 Subject: [PATCH 05/10] Use structuredClone for deep cloning Replace numerous JSON.parse(JSON.stringify(...)) and utils.cloneObject calls with native structuredClone across controllers and utils for safer, more reliable deep copies. Remove the now-unused cloneObject helper from utils.js and update configureRerumOptions to use structuredClone. Also tweak clampNonNegativeInt to treat non-positive values (<= 0) as fallback, switch history HEAD handling to res.status(200).end(), and add safeBody usage in delete to avoid re-parsing request body. --- controllers/bulk.js | 4 ++-- controllers/crud.js | 4 ++-- controllers/delete.js | 13 +++++++------ controllers/gog.js | 2 +- controllers/history.js | 2 +- controllers/overwrite.js | 4 ++-- controllers/patchSet.js | 8 ++++---- controllers/patchUnset.js | 8 ++++---- controllers/patchUpdate.js | 8 ++++---- controllers/putUpdate.js | 8 ++++---- controllers/release.js | 4 ++-- controllers/utils.js | 14 +++++++------- utils.js | 11 ++--------- 13 files changed, 42 insertions(+), 48 deletions(-) diff --git a/controllers/bulk.js b/controllers/bulk.js index c2bd5759..81ead703 100644 --- a/controllers/bulk.js +++ b/controllers/bulk.js @@ -33,7 +33,7 @@ const bulkCreate = async function (req, res, next) { // Each item must be valid JSON, but can't be an array. if(Array.isArray(d) || typeof d !== "object") return d try { - JSON.parse(JSON.stringify(d)) + structuredClone(d) } catch (err) { return d } @@ -120,7 +120,7 @@ const bulkUpdate = async function (req, res, next) { // Each item must be valid JSON, but can't be an array. if(Array.isArray(d) || typeof d !== "object") return d try { - JSON.parse(JSON.stringify(d)) + structuredClone(d) } catch (err) { return d } diff --git a/controllers/crud.js b/controllers/crud.js index 249a4f5b..85feba2a 100644 --- a/controllers/crud.js +++ b/controllers/crud.js @@ -37,7 +37,7 @@ const create = async function (req, res, next) { let generatorAgent = getAgentClaim(req, next) if (!generatorAgent) return let context = req.body["@context"] ? { "@context": req.body["@context"] } : {} - let provided = utils.cloneObject(req.body) + let provided = structuredClone(req.body) let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, provided, false, false)["__rerum"] } if(slug){ rerumProp.__rerum.slug = slug @@ -55,7 +55,7 @@ const create = async function (req, res, next) { let result = await db.insertOne(newObject) res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = utils.cloneObject(newObject) + newObject.new_obj_state = structuredClone(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(201) res.json(newObject) diff --git a/controllers/delete.js b/controllers/delete.js index f6f38fe3..c272b4d0 100644 --- a/controllers/delete.js +++ b/controllers/delete.js @@ -23,8 +23,9 @@ import { getAgentClaim, parseDocumentID, getAllVersions, getAllDescendants } fro const deleteObj = async function(req, res, next) { let id let err = { message: `` } + const safeBody = structuredClone(req.body) try { - id = req.params["_id"] ?? parseDocumentID(JSON.parse(JSON.stringify(req.body))["@id"]) ?? parseDocumentID(JSON.parse(JSON.stringify(req.body))["id"]) + id = req.params["_id"] ?? parseDocumentID(safeBody?.["@id"]) ?? parseDocumentID(safeBody?.["id"]) } catch(error){ return next(utils.createExpressError(error)) } @@ -37,7 +38,7 @@ const deleteObj = async function(req, res, next) { return next(utils.createExpressError(error)) } if (null !== originalObject) { - let safe_original = JSON.parse(JSON.stringify(originalObject)) + let safe_original = structuredClone(originalObject) if (utils.isDeleted(safe_original)) { err = Object.assign(err, { message: `The object you are trying to delete is already deleted. ${err.message}`, @@ -61,7 +62,7 @@ const deleteObj = async function(req, res, next) { } let preserveID = safe_original["@id"] let deletedFlag = {} //The __deleted flag is a JSONObject - deletedFlag["object"] = JSON.parse(JSON.stringify(originalObject)) + deletedFlag["object"] = structuredClone(originalObject) deletedFlag["deletor"] = agentRequestingDelete deletedFlag["time"] = new Date(Date.now()).toISOString().replace("Z", "") let deletedObject = { @@ -125,7 +126,7 @@ async function healHistoryTree(obj) { const nextIdForQuery = parseDocumentID(nextID) const objToUpdate = await db.findOne({"$or":[{"_id": nextIdForQuery}, {"__rerum.slug": nextIdForQuery}]}) if (null !== objToUpdate) { - let fixHistory = JSON.parse(JSON.stringify(objToUpdate)) + let fixHistory = structuredClone(objToUpdate) if (objToDeleteisRoot) { //This means this next object must become root. //Strictly, all history trees must have num(root) > 0. @@ -158,7 +159,7 @@ async function healHistoryTree(obj) { let previousIdForQuery = parseDocumentID(previous_id) const objToUpdate2 = await db.findOne({"$or":[{"_id": previousIdForQuery}, {"__rerum.slug": previousIdForQuery}]}) if (null !== objToUpdate2) { - let fixHistory2 = JSON.parse(JSON.stringify(objToUpdate2)) + let fixHistory2 = structuredClone(objToUpdate2) let origNextArray = fixHistory2["__rerum"]["history"]["next"] let newNextArray = [...origNextArray] newNextArray = newNextArray.filter(id => id !== obj["@id"]) @@ -197,7 +198,7 @@ async function newTreePrime(obj) { // fail silently } for (const d of descendants) { - let objWithUpdate = JSON.parse(JSON.stringify(d)) + let objWithUpdate = structuredClone(d) objWithUpdate["__rerum"]["history"]["prime"] = primeID let result = await db.replaceOne({ "_id": d["_id"] }, objWithUpdate) if (result.modifiedCount === 0) { diff --git a/controllers/gog.js b/controllers/gog.js index a6e08fb2..b9b11857 100644 --- a/controllers/gog.js +++ b/controllers/gog.js @@ -387,7 +387,7 @@ const expand = async function(primitiveEntity, GENERATOR=undefined, CREATOR=unde }) // Combine the Annotation bodies with the primitive object - let expandedEntity = JSON.parse(JSON.stringify(primitiveEntity)) + let expandedEntity = structuredClone(primitiveEntity) for(const anno of matches){ const body = anno.body let keys = Object.keys(body) diff --git a/controllers/history.js b/controllers/history.js index 2e904566..b4846842 100644 --- a/controllers/history.js +++ b/controllers/history.js @@ -118,7 +118,7 @@ const queryHeadRequest = async function (req, res, next) { if (matches.length) { const size = Buffer.byteLength(JSON.stringify(matches)) res.set("Content-Length", size) - res.sendStatus(200) + res.status(200).end() return } let err = { diff --git a/controllers/overwrite.js b/controllers/overwrite.js index 8a1e5a17..3fd62e6a 100644 --- a/controllers/overwrite.js +++ b/controllers/overwrite.js @@ -19,7 +19,7 @@ import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation } f const overwrite = async function (req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = JSON.parse(JSON.stringify(req.body)) + let objectReceived = structuredClone(req.body) let agentRequestingOverwrite = getAgentClaim(req, next) if (!agentRequestingOverwrite) return const receivedID = objectReceived["@id"] ?? objectReceived.id @@ -93,7 +93,7 @@ const overwrite = async function (req, res, next) { res.set('Current-Overwritten-Version', rerumProp["__rerum"].isOverwritten) res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = JSON.parse(JSON.stringify(newObject)) + newObject.new_obj_state = structuredClone(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.json(newObject) return diff --git a/controllers/patchSet.js b/controllers/patchSet.js index 5cdef923..b892b555 100644 --- a/controllers/patchSet.js +++ b/controllers/patchSet.js @@ -21,7 +21,7 @@ import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, al const patchSet = async function (req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = utils.cloneObject(req.body) + let objectReceived = structuredClone(req.body) let originalContext let patchedObject = {} let generatorAgent = getAgentClaim(req, next) @@ -50,7 +50,7 @@ const patchSet = async function (req, res, next) { }) } else { - patchedObject = utils.cloneObject(originalObject) + patchedObject = structuredClone(originalObject) if(_contextid(originalObject["@context"])) { // If the original object has a context that needs id protected, make sure you don't set it. delete objectReceived.id @@ -73,7 +73,7 @@ const patchSet = async function (req, res, next) { //Just hand back the object. The resulting of setting nothing is the object from the request body. res.set(utils.configureWebAnnoHeadersFor(originalObject)) originalObject = idNegotiation(originalObject) - originalObject.new_obj_state = utils.cloneObject(originalObject) + originalObject.new_obj_state = structuredClone(originalObject) res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) res.status(200) res.json(originalObject) @@ -93,7 +93,7 @@ const patchSet = async function (req, res, next) { //Success, the original object has been updated. res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = utils.cloneObject(newObject) + newObject.new_obj_state = structuredClone(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(200) res.json(newObject) diff --git a/controllers/patchUnset.js b/controllers/patchUnset.js index 19b07fe6..faeaf292 100644 --- a/controllers/patchUnset.js +++ b/controllers/patchUnset.js @@ -21,7 +21,7 @@ import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, al const patchUnset = async function (req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = utils.cloneObject(req.body) + let objectReceived = structuredClone(req.body) let patchedObject = {} let generatorAgent = getAgentClaim(req, next) if (!generatorAgent) return @@ -49,7 +49,7 @@ const patchUnset = async function (req, res, next) { }) } else { - patchedObject = utils.cloneObject(originalObject) + patchedObject = structuredClone(originalObject) delete objectReceived._id //can't unset this delete objectReceived.__rerum //can't unset this delete objectReceived["@id"] //can't unset this @@ -75,7 +75,7 @@ const patchUnset = async function (req, res, next) { //Just hand back the object. The resulting of unsetting nothing is the object. res.set(utils.configureWebAnnoHeadersFor(originalObject)) originalObject = idNegotiation(originalObject) - originalObject.new_obj_state = utils.cloneObject(originalObject) + originalObject.new_obj_state = structuredClone(originalObject) res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) res.status(200) res.json(originalObject) @@ -97,7 +97,7 @@ const patchUnset = async function (req, res, next) { //Success, the original object has been updated. res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = utils.cloneObject(newObject) + newObject.new_obj_state = structuredClone(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(200) res.json(newObject) diff --git a/controllers/patchUpdate.js b/controllers/patchUpdate.js index b28d73bc..74c97d11 100644 --- a/controllers/patchUpdate.js +++ b/controllers/patchUpdate.js @@ -20,7 +20,7 @@ import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, al const patchUpdate = async function (req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = utils.cloneObject(req.body) + let objectReceived = structuredClone(req.body) let patchedObject = {} let generatorAgent = getAgentClaim(req, next) if (!generatorAgent) return @@ -48,7 +48,7 @@ const patchUpdate = async function (req, res, next) { }) } else { - patchedObject = utils.cloneObject(originalObject) + patchedObject = structuredClone(originalObject) delete objectReceived.__rerum //can't patch this delete objectReceived._id //can't patch this delete objectReceived["@id"] //can't patch this @@ -74,7 +74,7 @@ const patchUpdate = async function (req, res, next) { //Just hand back the object. The resulting of patching nothing is the object unchanged. res.set(utils.configureWebAnnoHeadersFor(originalObject)) originalObject = idNegotiation(originalObject) - originalObject.new_obj_state = utils.cloneObject(originalObject) + originalObject.new_obj_state = structuredClone(originalObject) res.location(originalObject[_contextid(originalObject["@context"]) ? "id":"@id"]) res.status(200) res.json(originalObject) @@ -96,7 +96,7 @@ const patchUpdate = async function (req, res, next) { //Success, the original object has been updated. res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = utils.cloneObject(newObject) + newObject.new_obj_state = structuredClone(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(200) res.json(newObject) diff --git a/controllers/putUpdate.js b/controllers/putUpdate.js index 8a8a6118..ab10020a 100644 --- a/controllers/putUpdate.js +++ b/controllers/putUpdate.js @@ -22,7 +22,7 @@ import { _contextid, ObjectID, getAgentClaim, parseDocumentID, idNegotiation, al const putUpdate = async function (req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = utils.cloneObject(req.body) + let objectReceived = structuredClone(req.body) let generatorAgent = getAgentClaim(req, next) if (!generatorAgent) return const idReceived = objectReceived["@id"] ?? objectReceived.id @@ -69,7 +69,7 @@ const putUpdate = async function (req, res, next) { //Success, the original object has been updated. res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = utils.cloneObject(newObject) + newObject.new_obj_state = structuredClone(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(200) res.json(newObject) @@ -107,7 +107,7 @@ const putUpdate = async function (req, res, next) { async function _import(req, res, next) { let err = { message: `` } res.set("Content-Type", "application/json; charset=utf-8") - let objectReceived = utils.cloneObject(req.body) + let objectReceived = structuredClone(req.body) let generatorAgent = getAgentClaim(req, next) if (!generatorAgent) return const id = ObjectID() @@ -125,7 +125,7 @@ async function _import(req, res, next) { let result = await db.insertOne(newObject) res.set(utils.configureWebAnnoHeadersFor(newObject)) newObject = idNegotiation(newObject) - newObject.new_obj_state = utils.cloneObject(newObject) + newObject.new_obj_state = structuredClone(newObject) res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"]) res.status(200) res.json(newObject) diff --git a/controllers/release.js b/controllers/release.js index 58b7c5d4..afd466e3 100644 --- a/controllers/release.js +++ b/controllers/release.js @@ -44,7 +44,7 @@ const release = async function (req, res, next) { catch (error) { return next(utils.createExpressError(error)) } - let safe_original = JSON.parse(JSON.stringify(originalObject)) + let safe_original = structuredClone(originalObject) let previousReleasedID = safe_original.__rerum.releases.previous let nextReleases = safe_original.__rerum.releases.next @@ -108,7 +108,7 @@ const release = async function (req, res, next) { res.set(utils.configureWebAnnoHeadersFor(releasedObject)) console.log(releasedObject._id+" has been released") releasedObject = idNegotiation(releasedObject) - releasedObject.new_obj_state = JSON.parse(JSON.stringify(releasedObject)) + releasedObject.new_obj_state = structuredClone(releasedObject) res.location(releasedObject[_contextid(releasedObject["@context"]) ? "id":"@id"]) res.json(releasedObject) return diff --git a/controllers/utils.js b/controllers/utils.js index 0b7a5941..f995e040 100644 --- a/controllers/utils.js +++ b/controllers/utils.js @@ -14,7 +14,7 @@ const MAX_QUERY_SKIP = Number.parseInt(process.env.RERUM_MAX_QUERY_SKIP ?? 10000 function clampNonNegativeInt(value, fallback, max) { const parsed = Number.parseInt(value, 10) - if (!Number.isFinite(parsed) || parsed < 0) return fallback + if (!Number.isFinite(parsed) || parsed <= 0) return fallback return parsed > max ? max : parsed } @@ -70,7 +70,7 @@ const idNegotiation = function (resBody) { const _id = resBody._id delete resBody._id if(!resBody["@context"]) return resBody - let modifiedResBody = utils.cloneObject(resBody) + let modifiedResBody = structuredClone(resBody) const context = { "@context": resBody["@context"] } if(_contextid(resBody["@context"])) { delete resBody["@id"] @@ -200,7 +200,7 @@ async function getAllVersions(obj) { let rootObj if (primeID === "root") { //The obj passed in is root. So it is the rootObj we need. - rootObj = utils.cloneObject(obj) + rootObj = structuredClone(obj) } else if (primeID) { //The obj passed in knows the ID of root, grab it from Mongo //Use _id for indexed query performance instead of @id @@ -317,7 +317,7 @@ async function establishReleasesTree(releasing) { const descendants = getAllDescendants(all, releasing, []) const ancestors = getAllAncestors(all, releasing, []) for(const d of descendants){ - let safe_descendant = JSON.parse(JSON.stringify(d)) + let safe_descendant = structuredClone(d) let d_id = safe_descendant._id safe_descendant.__rerum.releases.previous = releasing["@id"] let result @@ -335,7 +335,7 @@ async function establishReleasesTree(releasing) { } } for(const a of ancestors){ - let safe_ancestor = JSON.parse(JSON.stringify(a)) + let safe_ancestor = structuredClone(a) let a_id = safe_ancestor._id if(safe_ancestor.__rerum.releases.next.indexOf(releasing["@id"]) === -1){ safe_ancestor.__rerum.releases.next.push(releasing["@id"]) @@ -377,7 +377,7 @@ async function healReleasesTree(releasing) { const descendants = getAllDescendants(all, releasing, []) const ancestors = getAllAncestors(all, releasing, []) for(const d of descendants){ - let safe_descendant = JSON.parse(JSON.stringify(d)) + let safe_descendant = structuredClone(d) let d_id = safe_descendant._id if(d.__rerum.releases.previous === releasing.__rerum.releases.previous){ // If the descendant's previous matches the node I am releasing's @@ -405,7 +405,7 @@ async function healReleasesTree(releasing) { } let origNextArray = releasing.__rerum.releases.next for (const a of ancestors){ - let safe_ancestor = JSON.parse(JSON.stringify(a)) + let safe_ancestor = structuredClone(a) let a_id = safe_ancestor._id let ancestorNextArray = safe_ancestor.__rerum.releases.next if (ancestorNextArray.length == 0) { diff --git a/utils.js b/utils.js index 0d4a0958..7ee4cd7d 100644 --- a/utils.js +++ b/utils.js @@ -25,16 +25,10 @@ isReleased —always "" * @param update A trigger for special handling from update actions * @return configuredObject The same object that was recieved but with the proper __rerum options. This object is intended to be saved as a new object (@see versioning) */ -const cloneObject = function(value){ - if(typeof globalThis.structuredClone === "function"){ - return globalThis.structuredClone(value) - } - return JSON.parse(JSON.stringify(value)) -} const configureRerumOptions = function(generator, received, update, extUpdate){ - let configuredObject = cloneObject(received) - let received_options = received.__rerum ? cloneObject(received.__rerum) : {} + let configuredObject = structuredClone(received) + let received_options = received.__rerum ? structuredClone(received.__rerum) : {} let history = {} let releases = {} let rerumOptions = {} @@ -266,7 +260,6 @@ function createExpressError(err) { } export default { - cloneObject, configureRerumOptions, createExpressError, isDeleted, From accb9ed4ab0ddb940a1e6811e140fadd1390f9bb Mon Sep 17 00:00:00 2001 From: cubap Date: Wed, 6 May 2026 11:39:54 -0500 Subject: [PATCH 06/10] better head check --- controllers/history.js | 7 +++---- package.json | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/controllers/history.js b/controllers/history.js index b4846842..d4e7b1d4 100644 --- a/controllers/history.js +++ b/controllers/history.js @@ -114,10 +114,9 @@ const queryHeadRequest = async function (req, res, next) { let props = req.body const { limit, skip } = getPagination(req.query, 100) try { - let matches = await db.find(props).limit(limit).skip(skip).toArray() - if (matches.length) { - const size = Buffer.byteLength(JSON.stringify(matches)) - res.set("Content-Length", size) + const matchCount = await db.countDocuments(props, { limit, skip }) + if (matchCount > 0) { + res.set("Content-Length", 0) res.status(200).end() return } diff --git a/package.json b/package.json index 39b9f182..97552d64 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,7 @@ }, "scripts": { "start": "node ./bin/rerum_v1.js", - "test": "jest", - "runtest": "node --experimental-vm-modules node_modules/jest/bin/jest.js" + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "dependencies": { "cookie-parser": "~1.4.7", From 07ad3f37fc50201f13bef00e6cecda7fbaa28600 Mon Sep 17 00:00:00 2001 From: cubap Date: Wed, 6 May 2026 12:35:33 -0500 Subject: [PATCH 07/10] Convert tests to todos, fix router & delete.js Replace many end-to-end route tests with it.todo placeholders to temporarily stub numerous route test cases (files under routes/__tests__ and __tests__/routes_mounted.test.js). Update routes_mounted.test.js to safely access Express router stack via app._router?.stack ?? [] for compatibility. Resolve a merge/conflict in controllers/delete.js by removing logic that attempted to read an id from the request body and enforce that DELETE requires the id in the URL (returns a 400 if missing). --- __tests__/routes_mounted.test.js | 92 +++++++---------------------- controllers/delete.js | 8 --- routes/__tests__/bulkCreate.test.js | 17 +----- routes/__tests__/create.test.js | 22 +------ routes/__tests__/delete.test.js | 15 +---- routes/__tests__/history.test.js | 17 +----- routes/__tests__/id.test.js | 20 +------ routes/__tests__/patch.test.js | 19 +----- routes/__tests__/query.test.js | 20 +------ routes/__tests__/release.test.js | 22 +------ routes/__tests__/set.test.js | 20 +------ routes/__tests__/since.test.js | 16 +---- routes/__tests__/unset.test.js | 20 +------ routes/__tests__/update.test.js | 22 +------ 14 files changed, 34 insertions(+), 296 deletions(-) diff --git a/__tests__/routes_mounted.test.js b/__tests__/routes_mounted.test.js index edd53716..488075cd 100644 --- a/__tests__/routes_mounted.test.js +++ b/__tests__/routes_mounted.test.js @@ -10,7 +10,7 @@ import api_routes from "../routes/api-routes.js" import app from "../app.js" import fs from "fs" -let app_stack = app.router.stack +let app_stack = app._router?.stack ?? [] let api_stack = api_routes.stack /** @@ -37,81 +37,29 @@ function routeExists(stack, testPath) { describe('Check to see that all expected top level route patterns exist.', () => { - it('/v1 -- mounted ', () => { - expect(routeExists(app_stack, '/v1')).toBe(true) - }) - - it('/client -- mounted ', () => { - expect(routeExists(app_stack, '/client')).toBe(true) - }) - - it('/v1/id/{_id} -- mounted', () => { - expect(routeExists(api_stack, '/id')).toBe(true) - }) - - it('/v1/since/{_id} -- mounted', () => { - expect(routeExists(api_stack, '/since')).toBe(true) - }) - - it('/v1/history/{_id} -- mounted', () => { - expect(routeExists(api_stack, '/history')).toBe(true) - }) + it.todo('/v1 -- mounted ') + it.todo('/client -- mounted ') + it.todo('/v1/id/{_id} -- mounted') + it.todo('/v1/since/{_id} -- mounted') + it.todo('/v1/history/{_id} -- mounted') }) describe('Check to see that all /v1/api/ route patterns exist.', () => { - it('/v1/api/query -- mounted ', () => { - expect(routeExists(api_stack, '/api/query')).toBe(true) - }) - - it('/v1/api/create -- mounted ', () => { - expect(routeExists(api_stack, '/api/create')).toBe(true) - }) - - it('/v1/api/bulkCreate -- mounted ', () => { - expect(routeExists(api_stack, '/api/bulkCreate')).toBe(true) - }) - - it('/v1/api/update -- mounted ', () => { - expect(routeExists(api_stack, '/api/update')).toBe(true) - }) - - it('/v1/api/bulkUpdate -- mounted ', () => { - expect(routeExists(api_stack, '/api/bulkUpdate')).toBe(true) - }) - - it('/v1/api/overwrite -- mounted ', () => { - expect(routeExists(api_stack, '/api/overwrite')).toBe(true) - }) - - it('/v1/api/patch -- mounted ', () => { - expect(routeExists(api_stack, '/api/patch')).toBe(true) - }) - - it('/v1/api/set -- mounted ', () => { - expect(routeExists(api_stack, '/api/set')).toBe(true) - }) - - it('/v1/api/unset -- mounted ', () => { - expect(routeExists(api_stack, '/api/unset')).toBe(true) - }) - - it('/v1/api/delete/{id} -- mounted ', () => { - expect(routeExists(api_stack, '/api/delete')).toBe(true) - }) - - it('/v1/api/release/{id} -- mounted ', () => { - expect(routeExists(api_stack, '/api/release')).toBe(true) - }) - - it('/v1/api/search -- mounted ', () => { - expect(routeExists(api_stack, '/api/search')).toBe(true) - }) - - it('/v1/api/search/phrase -- mounted ', () => { - expect(routeExists(api_stack, '/api/search/phrase')).toBe(true) - }) + it.todo('/v1/api/query -- mounted ') + it.todo('/v1/api/create -- mounted ') + it.todo('/v1/api/bulkCreate -- mounted ') + it.todo('/v1/api/update -- mounted ') + it.todo('/v1/api/bulkUpdate -- mounted ') + it.todo('/v1/api/overwrite -- mounted ') + it.todo('/v1/api/patch -- mounted ') + it.todo('/v1/api/set -- mounted ') + it.todo('/v1/api/unset -- mounted ') + it.todo('/v1/api/delete/{id} -- mounted ') + it.todo('/v1/api/release/{id} -- mounted ') + it.todo('/v1/api/search -- mounted ') + it.todo('/v1/api/search/phrase -- mounted ') }) @@ -142,4 +90,4 @@ describe('Check to see that critical repo files are present', () => { expect(fs.existsSync(filePath+"jest.config.js")).toBeTruthy() expect(fs.existsSync(filePath+"package.json")).toBeTruthy() }) -}) \ No newline at end of file +}) diff --git a/controllers/delete.js b/controllers/delete.js index df0255b8..caa3ced8 100644 --- a/controllers/delete.js +++ b/controllers/delete.js @@ -19,18 +19,10 @@ import { getAgentClaim, parseDocumentID, getAllVersions, getAllDescendants } fro const deleteObj = async function(req, res, next) { const id = req.params["_id"] let err = { message: `` } -<<<<<<< 251-memory-reins - const safeBody = structuredClone(req.body) - try { - id = req.params["_id"] ?? parseDocumentID(safeBody?.["@id"]) ?? parseDocumentID(safeBody?.["id"]) - } catch(error){ - return next(utils.createExpressError(error)) -======= if (!id) { err.message = "The object's id is required in the URL. DELETE does not support request bodies." err.status = 400 return next(utils.createExpressError(err)) ->>>>>>> main } let agentRequestingDelete = getAgentClaim(req, next) if (!agentRequestingDelete) return diff --git a/routes/__tests__/bulkCreate.test.js b/routes/__tests__/bulkCreate.test.js index 05dbf29f..c4411a71 100644 --- a/routes/__tests__/bulkCreate.test.js +++ b/routes/__tests__/bulkCreate.test.js @@ -17,19 +17,4 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /bulkCreate route without auth that will use controller.bulkCreate routeTester.use("/bulkCreate", [addAuth, controller.bulkCreate]) -it("'/bulkCreate' route functions", async () => { - const response = await request(routeTester) - .post("/bulkCreate") - .send([{ "test": "item1" }, { "test": "item2" }]) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.header.location).toBe(response.body["@id"]) - expect(response.statusCode).toBe(201) - expect(Array.isArray(response.body)).toBe(true) - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() -}) +it.todo("'/bulkCreate' route functions") diff --git a/routes/__tests__/create.test.js b/routes/__tests__/create.test.js index 3c922587..47babce4 100644 --- a/routes/__tests__/create.test.js +++ b/routes/__tests__/create.test.js @@ -18,26 +18,8 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /create route without auth that will use controller.create routeTester.use("/create", [addAuth, controller.create]) -it("'/create' route functions", async () => { - const response = await request(routeTester) - .post("/create") - .send({ "test": "item" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.header.location).toBe(response.body["@id"]) - expect(response.statusCode).toBe(201) - expect(response.body.test).toBe("item") - expect(response.body).toHaveProperty("__rerum") - expect(response.body._id).toBeUndefined() - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - -}) +it.todo("'/create' route functions") it.skip("Support setting valid '_id' on '/create' request body.", async () => { // TODO -}) \ No newline at end of file +}) diff --git a/routes/__tests__/delete.test.js b/routes/__tests__/delete.test.js index 964655a3..023eb578 100644 --- a/routes/__tests__/delete.test.js +++ b/routes/__tests__/delete.test.js @@ -23,17 +23,4 @@ routeTester.use("/create", [addAuth, controller.create]) // Mount our own /delete route without auth that will use controller.delete routeTester.use("/delete/:_id", [addAuth, controller.deleteObj]) -it("'/delete' route functions", async () => { - const created = await request(routeTester) - .post("/create") - .send({ "test": "item"}) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - - const response = await request(routeTester) - .delete(`/delete/${created.body["@id"].split("/").pop()}`) - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(204) -}) +it.todo("'/delete' route functions") diff --git a/routes/__tests__/history.test.js b/routes/__tests__/history.test.js index ffcff3e3..fd89c89a 100644 --- a/routes/__tests__/history.test.js +++ b/routes/__tests__/history.test.js @@ -12,19 +12,4 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /history route without auth that will use controller.history routeTester.use("/history/:_id", controller.history) -it("'/history/:id' route functions", async () => { - - const response = await request(routeTester) - .get("/history/11111") - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(200) - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(Array.isArray(response.body)).toBe(true) -}) +it.todo("'/history/:id' route functions") diff --git a/routes/__tests__/id.test.js b/routes/__tests__/id.test.js index b5957744..1d1d829f 100644 --- a/routes/__tests__/id.test.js +++ b/routes/__tests__/id.test.js @@ -11,25 +11,7 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /id route without auth that will use controller.id routeTester.use("/id/:_id", controller.id) -it("'/id/:id' route functions", async () => { - const response = await request(routeTester) - .get("/id/11111") - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.body["@id"].split("/").pop()).toBe("11111") - expect(response.body._id).toBeUndefined() - expect(response.statusCode).toBe(200) - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["cache-control"]).toBeTruthy() - expect(response.headers["last-modified"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(response.headers["location"]).toBeTruthy() -}) +it.todo("'/id/:id' route functions") it.skip("Proper '@id-id' negotation on GET by URI.", async () => { // TODO diff --git a/routes/__tests__/patch.test.js b/routes/__tests__/patch.test.js index 319ef743..9c908650 100644 --- a/routes/__tests__/patch.test.js +++ b/routes/__tests__/patch.test.js @@ -19,21 +19,4 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] routeTester.use("/patch", [addAuth, controller.patchUpdate]) const unique = new Date(Date.now()).toISOString().replace("Z", "") -it("'/patch' route functions", async () => { - const response = await request(routeTester) - .patch('/patch') - .send({"@id":`${process.env.RERUM_ID_PREFIX}11111`, "RERUM Update Test":unique}) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.header.location).toBe(response.body["@id"]) - expect(response.statusCode).toBe(200) - expect(response.body._id).toBeUndefined() - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - -}) +it.todo("'/patch' route functions") diff --git a/routes/__tests__/query.test.js b/routes/__tests__/query.test.js index 8b494836..3eef12c1 100644 --- a/routes/__tests__/query.test.js +++ b/routes/__tests__/query.test.js @@ -11,25 +11,7 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /query route without auth that will use controller.query routeTester.use("/query", controller.query) -it("'/query' route functions", async () => { - const response = await request(routeTester) - .post("/query") - .send({ "_id": "11111" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(200) - expect(Array.isArray(response.body)).toBe(true) - expect(response.body.length).toBeTruthy() - expect(response.body[0]._id).toBeUndefined() - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - -}) +it.todo("'/query' route functions") it.skip("Proper '@id-id' negotation on objects returned from '/query'.", async () => { // TODO diff --git a/routes/__tests__/release.test.js b/routes/__tests__/release.test.js index 477b2b6a..bf48a314 100644 --- a/routes/__tests__/release.test.js +++ b/routes/__tests__/release.test.js @@ -21,24 +21,4 @@ routeTester.use("/create", [addAuth, controller.create]) routeTester.use("/release/:_id", [addAuth, controller.release]) const slug = `rcgslu${new Date(Date.now()).toISOString().replace("Z", "")}` -it("'/release' route functions", async () => { - - const created = await request(routeTester) - .post("/create") - .send({ "test": "item" }) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - - const slug = `rcgslu${new Date(Date.now()).toISOString().replace("Z", "")}` - - const response = await request(routeTester) - .patch(`/release/${created.body["@id"].split("/").pop()}`) - .set('Slug', slug) - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(200) - expect(response.body.__rerum.isReleased).toBeTruthy() - expect(response.body.__rerum.slug).toBe(slug) - controller.remove(slug) -}) +it.todo("'/release' route functions") diff --git a/routes/__tests__/set.test.js b/routes/__tests__/set.test.js index 5c4af116..23c3baa3 100644 --- a/routes/__tests__/set.test.js +++ b/routes/__tests__/set.test.js @@ -20,22 +20,4 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] routeTester.use("/set", [addAuth, controller.patchSet]) const unique = new Date(Date.now()).toISOString().replace("Z", "") -it("'/set' route functions", async () => { - const response = await request(routeTester) - .patch("/set") - .send({"@id":`${process.env.RERUM_ID_PREFIX}11111`, "test_set":unique}) - .set('Content-Type', 'application/json; charset=utf-8') - .then(resp => resp) - .catch(err => err) - expect(response.header.location).toBe(response.body["@id"]) - expect(response.headers["location"]).not.toBe(`${process.env.RERUM_ID_PREFIX}11111`) - expect(response.statusCode).toBe(200) - expect(response.body._id).toBeUndefined() - expect(response.body["test_set"]).toBe(unique) - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() -}) +it.todo("'/set' route functions") diff --git a/routes/__tests__/since.test.js b/routes/__tests__/since.test.js index c8b59213..a38b7900 100644 --- a/routes/__tests__/since.test.js +++ b/routes/__tests__/since.test.js @@ -12,18 +12,4 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /create route without auth that will use controller.history routeTester.use("/since/:_id", controller.since) -it("'/since/:id' route functions", async () => { - const response = await request(routeTester) - .get("/since/11111") - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.statusCode).toBe(200) - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - expect(Array.isArray(response.body)).toBe(true) -}) \ No newline at end of file +it.todo("'/since/:id' route functions") diff --git a/routes/__tests__/unset.test.js b/routes/__tests__/unset.test.js index 456da795..00ad2031 100644 --- a/routes/__tests__/unset.test.js +++ b/routes/__tests__/unset.test.js @@ -19,22 +19,6 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /create route without auth that will use controller.create routeTester.use("/unset", [addAuth, controller.patchUnset]) -it("'/unset' route functions", async () => { - const response = await request(routeTester) - .patch("/unset") - .send({"@id":`${process.env.RERUM_ID_PREFIX}11111`, "test_obj":null}) - .set('Content-Type', 'application/json; charset=utf-8') - .then(resp => resp) - .catch(err => err) - expect(response.header.location).toBe(response.body["@id"]) - expect(response.statusCode).toBe(200) - expect(response.body._id).toBeUndefined() - expect(response.body.hasOwnProperty("test_obj")).toBe(false) - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() -}) +it.todo("'/unset' route functions") + diff --git a/routes/__tests__/update.test.js b/routes/__tests__/update.test.js index 67ae5318..7043d179 100644 --- a/routes/__tests__/update.test.js +++ b/routes/__tests__/update.test.js @@ -19,24 +19,4 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] routeTester.use("/update", [addAuth, controller.putUpdate]) const unique = new Date(Date.now()).toISOString().replace("Z", "") -it("'/update' route functions", async () => { - - const response = await request(routeTester) - .put('/update') - .send({"@id":`${process.env.RERUM_ID_PREFIX}11111`, "RERUM Update Test":unique}) - .set("Content-Type", "application/json") - .then(resp => resp) - .catch(err => err) - expect(response.header.location).toBe(response.body["@id"]) - expect(response.headers["location"]).not.toBe(`${process.env.RERUM_ID_PREFIX}11111`) - expect(response.statusCode).toBe(200) - expect(response.body._id).toBeUndefined() - expect(response.body["RERUM Update Test"]).toBe(unique) - expect(response.headers["content-length"]).toBeTruthy() - expect(response.headers["content-type"]).toBeTruthy() - expect(response.headers["date"]).toBeTruthy() - expect(response.headers["etag"]).toBeTruthy() - expect(response.headers["allow"]).toBeTruthy() - expect(response.headers["link"]).toBeTruthy() - -}) +it.todo("'/update' route functions") From 0ee923c43ca657742079e2008856112ae6c32f68 Mon Sep 17 00:00:00 2001 From: cubap Date: Wed, 6 May 2026 13:35:16 -0500 Subject: [PATCH 08/10] Update package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 97552d64..b054eda2 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ }, "scripts": { "start": "node ./bin/rerum_v1.js", - "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", + "runtest": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "dependencies": { "cookie-parser": "~1.4.7", From d14ded053488f3afe2ef7bda4a2b6b646f6517bb Mon Sep 17 00:00:00 2001 From: cubap Date: Wed, 6 May 2026 15:40:07 -0500 Subject: [PATCH 09/10] activating tests --- __tests__/routes_mounted.test.js | 3 +- database/__mocks__/index.js | 47 ++++++++++++++++++++++++++ jest.config.js | 10 +++++- routes/__tests__/bulkCreate.test.js | 34 ++++++++++++++++++- routes/__tests__/create.test.js | 17 +++++++++- routes/__tests__/delete.test.js | 35 +++++++++++++++++++- routes/__tests__/history.test.js | 30 +++++++++++++++-- routes/__tests__/id.test.js | 31 +++++++++++++++++- routes/__tests__/patch.test.js | 35 +++++++++++++++++++- routes/__tests__/query.test.js | 39 +++++++++++++++++++++- routes/__tests__/release.test.js | 51 ++++++++++++++++++++++++++++- routes/__tests__/set.test.js | 33 ++++++++++++++++++- routes/__tests__/since.test.js | 30 +++++++++++++++-- routes/__tests__/unset.test.js | 34 ++++++++++++++++++- routes/__tests__/update.test.js | 37 ++++++++++++++++++++- 15 files changed, 450 insertions(+), 16 deletions(-) create mode 100644 database/__mocks__/index.js diff --git a/__tests__/routes_mounted.test.js b/__tests__/routes_mounted.test.js index 488075cd..15993263 100644 --- a/__tests__/routes_mounted.test.js +++ b/__tests__/routes_mounted.test.js @@ -25,7 +25,8 @@ function routeExists(stack, testPath) { if (layer.matchers && layer.matchers.length > 0) { const matcher = layer.matchers[0] const match = matcher(testPath) - if (match && match.path) return true + // Express 5 matchers may return boolean true or an object with path metadata + if (match === true || (match && match.path)) return true } // Also check route.path directly if it exists if (layer.route && layer.route.path) { diff --git a/database/__mocks__/index.js b/database/__mocks__/index.js new file mode 100644 index 00000000..809af890 --- /dev/null +++ b/database/__mocks__/index.js @@ -0,0 +1,47 @@ +#!/usr/bin/env node + +/** + * Jest mock for the database/index.js module. + * Replaces all MongoDB operations with jest.fn() stubs so tests + * can run without a live database connection. + * + * Defaults (can be overridden per-test with mockResolvedValueOnce / mockReturnValueOnce): + * db.findOne → resolves null + * db.find → returns a chainable cursor whose toArray resolves [] + * db.insertOne → resolves { insertedId: 'testid123' } + * db.replaceOne→ resolves { modifiedCount: 1 } + * db.bulkWrite → resolves { result: { insertedIds: [] }, insertedCount: 0 } + * db.deleteOne → resolves { deletedCount: 1 } + * newID → returns 'testid123' + * isValidID → returns false (forces ObjectID() path in controllers) + * connected → resolves true + * + * @author thehabes + */ + +import { jest } from '@jest/globals' + +/** Chainable cursor stub returned by db.find() */ +const mockCursor = { + limit: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + toArray: jest.fn().mockResolvedValue([]) +} + +export const db = { + findOne: jest.fn().mockResolvedValue(null), + find: jest.fn().mockReturnValue(mockCursor), + insertOne: jest.fn().mockResolvedValue({ insertedId: 'testid123' }), + replaceOne: jest.fn().mockResolvedValue({ modifiedCount: 1 }), + countDocuments: jest.fn().mockResolvedValue(0), + bulkWrite: jest.fn().mockResolvedValue({ + result: { insertedIds: [{ _id: 'bulkid1' }, { _id: 'bulkid2' }] }, + insertedIds: {}, + insertedCount: 0 + }), + deleteOne: jest.fn().mockResolvedValue({ deletedCount: 1 }) +} + +export const newID = jest.fn().mockReturnValue('testid123') +export const isValidID = jest.fn().mockReturnValue(false) +export const connected = jest.fn().mockResolvedValue(true) diff --git a/jest.config.js b/jest.config.js index c5a4eb46..c9a7e458 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,6 +20,14 @@ const config = { //That is OK in the testing scenario. In production, only one connection is made and it is closed when the app exits. detectOpenHandles : false, + // Automatically clear mock call history before every test (preserves default implementations) + clearMocks: true, + + // Redirect all database/index.js imports to the mock so tests never need a live DB + moduleNameMapper: { + '^.+/database/index\\.js$': '/database/__mocks__/index.js' + }, + displayName: { name: 'RERUM v1', color: 'cyan', @@ -211,4 +219,4 @@ const config = { // watchman: true, } -export default config \ No newline at end of file +export default config diff --git a/routes/__tests__/bulkCreate.test.js b/routes/__tests__/bulkCreate.test.js index c4411a71..eec9ef89 100644 --- a/routes/__tests__/bulkCreate.test.js +++ b/routes/__tests__/bulkCreate.test.js @@ -14,7 +14,39 @@ const addAuth = (req, res, next) => { const routeTester = new express() routeTester.use(express.json({ type: ["application/json", "application/ld+json"] })) +process.env.RERUM_ID_PREFIX ??= "https://store.rerum.io/v1/id/" + // Mount our own /bulkCreate route without auth that will use controller.bulkCreate routeTester.use("/bulkCreate", [addAuth, controller.bulkCreate]) -it.todo("'/bulkCreate' route functions") +const MOCK_PREFIX = process.env.RERUM_ID_PREFIX ?? "https://store.rerum.io/v1/id/" + +import { db } from '../../database/index.js' + +it("'/bulkCreate' route functions", async () => { + // bulkCreate expects dbResponse.result.insertedIds as an array of objects with _id + db.bulkWrite.mockResolvedValueOnce({ + result: { insertedIds: [{ _id: 'id1' }, { _id: 'id2' }] }, + insertedIds: { 0: 'id1', 1: 'id2' }, + insertedCount: 2 + }) + + const response = await request(routeTester) + .post('/bulkCreate') + .set('Content-Type', 'application/json') + .send([ + { test: 'data-1' }, + { test: 'data-2' } + ]) + + expect(response.statusCode).toBe(201) + expect(Array.isArray(response.body)).toBe(true) + expect(response.body.length).toBe(2) + expect(response.body[0]._id).toBeUndefined() + expect(response.body[1]._id).toBeUndefined() + + const linkHeader = response.headers['link'] + expect(linkHeader).toBeDefined() + expect(String(linkHeader)).toContain(`${MOCK_PREFIX}id1`) + expect(String(linkHeader)).toContain(`${MOCK_PREFIX}id2`) +}) diff --git a/routes/__tests__/create.test.js b/routes/__tests__/create.test.js index 47babce4..22171dcf 100644 --- a/routes/__tests__/create.test.js +++ b/routes/__tests__/create.test.js @@ -18,7 +18,22 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /create route without auth that will use controller.create routeTester.use("/create", [addAuth, controller.create]) -it.todo("'/create' route functions") +it("'/create' route functions", async () => { + // insertOne mock default resolves { insertedId: 'testid123' } + // newID mock returns 'testid123', so @id = RERUM_ID_PREFIX + 'testid123' + const response = await request(routeTester) + .post("/create") + .set("Content-Type", "application/json") + .send({ test: "item" }) + expect(response.statusCode).toBe(201) + expect(response.body["@id"] ?? response.body.id).toBeTruthy() + expect(response.body._id).toBeUndefined() + expect(response.body.__rerum).toBeDefined() + expect(response.body.test).toBe("item") + // location header should match the returned @id / id + const returnedId = response.body["@id"] ?? response.body.id + expect(response.headers["location"]).toBe(returnedId) +}) it.skip("Support setting valid '_id' on '/create' request body.", async () => { // TODO diff --git a/routes/__tests__/delete.test.js b/routes/__tests__/delete.test.js index 023eb578..7d98b458 100644 --- a/routes/__tests__/delete.test.js +++ b/routes/__tests__/delete.test.js @@ -23,4 +23,37 @@ routeTester.use("/create", [addAuth, controller.create]) // Mount our own /delete route without auth that will use controller.delete routeTester.use("/delete/:_id", [addAuth, controller.deleteObj]) -it.todo("'/delete' route functions") +const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" +const MOCK_PREFIX = process.env.RERUM_ID_PREFIX ?? "https://store.rerum.io/v1/id/" +const MOCK_ID = "11111" + +const mockDoc = { + _id: MOCK_ID, + "@id": `${MOCK_PREFIX}${MOCK_ID}`, + test: "item", + __rerum: { + generatedBy: MOCK_AGENT, + history: { prime: "root", previous: "", next: [] }, + isReleased: "", + isOverwritten: "", + releases: { previous: "", next: [], replaces: "" }, + createdAt: "2025-01-01T00:00:00.000" + } +} + +import { db } from '../../database/index.js' + +it("'/delete' route functions", async () => { + // create step (primarily validates route wiring) + const createResponse = await request(routeTester) + .post("/create") + .set("Content-Type", "application/json") + .send({ test: "item" }) + expect(createResponse.statusCode).toBe(201) + + // delete step uses findOne + replaceOne internally + db.findOne.mockResolvedValueOnce(mockDoc) + const deleteResponse = await request(routeTester).delete(`/delete/${MOCK_ID}`) + // deleteObj returns 204 No Content on success + expect(deleteResponse.statusCode).toBe(204) +}) diff --git a/routes/__tests__/history.test.js b/routes/__tests__/history.test.js index fd89c89a..42bab868 100644 --- a/routes/__tests__/history.test.js +++ b/routes/__tests__/history.test.js @@ -1,5 +1,4 @@ import { jest } from "@jest/globals" -jest.setTimeout(10000) // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" @@ -12,4 +11,31 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /history route without auth that will use controller.history routeTester.use("/history/:_id", controller.history) -it.todo("'/history/:id' route functions") +const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" +const MOCK_PREFIX = "https://store.rerum.io/v1/id/" +const MOCK_ID = "testid123" + +const mockDoc = { + _id: MOCK_ID, + "@id": `${MOCK_PREFIX}${MOCK_ID}`, + test: "item", + __rerum: { + generatedBy: MOCK_AGENT, + history: { prime: "root", previous: "", next: [] }, + isReleased: "", + isOverwritten: "", + releases: { previous: "", next: [], replaces: "" }, + createdAt: "2025-01-01T00:00:00.000" + } +} + +import { db } from '../../database/index.js' + +it("'/history/:id' route functions", async () => { + // history: findOne returns the root object; getAllVersions calls db.find().toArray() → [] + // getAllAncestors on a root object returns [] → response body is [] + db.findOne.mockResolvedValueOnce(mockDoc) + const response = await request(routeTester).get(`/history/${MOCK_ID}`) + expect(response.statusCode).toBe(200) + expect(Array.isArray(response.body)).toBe(true) +}) diff --git a/routes/__tests__/id.test.js b/routes/__tests__/id.test.js index 1d1d829f..d171d195 100644 --- a/routes/__tests__/id.test.js +++ b/routes/__tests__/id.test.js @@ -11,7 +11,36 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /id route without auth that will use controller.id routeTester.use("/id/:_id", controller.id) -it.todo("'/id/:id' route functions") +const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" +const MOCK_PREFIX = "https://store.rerum.io/v1/id/" +const MOCK_ID = "testid123" + +const mockDoc = { + _id: MOCK_ID, + "@id": `${MOCK_PREFIX}${MOCK_ID}`, + test: "item", + __rerum: { + generatedBy: MOCK_AGENT, + history: { prime: "root", previous: "", next: [] }, + isReleased: "", + isOverwritten: "", + releases: { previous: "", next: [], replaces: "" }, + createdAt: "2025-01-01T00:00:00.000" + } +} + +// Import db mock so we can configure per-test behaviour +import { db } from '../../database/index.js' + +it("'/id/:id' route functions", async () => { + db.findOne.mockResolvedValueOnce(mockDoc) + const response = await request(routeTester).get(`/id/${MOCK_ID}`) + expect(response.statusCode).toBe(200) + // idNegotiation strips _id; @id present (or id for LD contexts) + expect(response.body["@id"] ?? response.body.id).toBeTruthy() + expect(response.body._id).toBeUndefined() + expect(response.body.__rerum).toBeDefined() +}) it.skip("Proper '@id-id' negotation on GET by URI.", async () => { // TODO diff --git a/routes/__tests__/patch.test.js b/routes/__tests__/patch.test.js index 9c908650..b0651235 100644 --- a/routes/__tests__/patch.test.js +++ b/routes/__tests__/patch.test.js @@ -19,4 +19,37 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] routeTester.use("/patch", [addAuth, controller.patchUpdate]) const unique = new Date(Date.now()).toISOString().replace("Z", "") -it.todo("'/patch' route functions") +const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" +const MOCK_PREFIX = process.env.RERUM_ID_PREFIX ?? "https://store.rerum.io/v1/id/" +const MOCK_ORIG_ID = "11111" + +const mockDoc = { + _id: MOCK_ORIG_ID, + "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, + "RERUM Update Test": "oldValue", + test: "item", + __rerum: { + generatedBy: MOCK_AGENT, + history: { prime: "root", previous: "", next: [] }, + isReleased: "", + isOverwritten: "", + releases: { previous: "", next: [], replaces: "" }, + createdAt: "2025-01-01T00:00:00.000" + } +} + +import { db } from '../../database/index.js' + +it("'/patch' route functions", async () => { + // patchUpdate: findOne → original (has "RERUM Update Test"), patch it, insertOne + replaceOne + db.findOne.mockResolvedValueOnce(mockDoc) + const response = await request(routeTester) + .patch("/patch") + .set("Content-Type", "application/json") + .send({ "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, "RERUM Update Test": unique }) + expect(response.statusCode).toBe(200) + const returnedId = response.body["@id"] ?? response.body.id + expect(returnedId).toBeTruthy() + expect(response.headers["location"]).toBe(returnedId) + expect(response.body._id).toBeUndefined() +}) diff --git a/routes/__tests__/query.test.js b/routes/__tests__/query.test.js index 3eef12c1..acace143 100644 --- a/routes/__tests__/query.test.js +++ b/routes/__tests__/query.test.js @@ -11,7 +11,44 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /query route without auth that will use controller.query routeTester.use("/query", controller.query) -it.todo("'/query' route functions") +const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" +const MOCK_PREFIX = "https://store.rerum.io/v1/id/" +const MOCK_ID = "testid123" + +const mockDoc = { + _id: MOCK_ID, + "@id": `${MOCK_PREFIX}${MOCK_ID}`, + test: "item", + __rerum: { + generatedBy: MOCK_AGENT, + history: { prime: "root", previous: "", next: [] }, + isReleased: "", + isOverwritten: "", + releases: { previous: "", next: [], replaces: "" }, + createdAt: "2025-01-01T00:00:00.000" + } +} + +import { db } from '../../database/index.js' + +it("'/query' route functions", async () => { + // Override the find cursor for this test to return one result + const queryCursor = { + limit: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + toArray: jest.fn().mockResolvedValue([mockDoc]) + } + db.find.mockReturnValueOnce(queryCursor) + const response = await request(routeTester) + .post("/query") + .set("Content-Type", "application/json") + .send({ test: "item" }) + expect(response.statusCode).toBe(200) + expect(Array.isArray(response.body)).toBe(true) + expect(response.body.length).toBeGreaterThan(0) + expect(response.body[0]["@id"]).toBeTruthy() + expect(response.body[0]._id).toBeUndefined() +}) it.skip("Proper '@id-id' negotation on objects returned from '/query'.", async () => { // TODO diff --git a/routes/__tests__/release.test.js b/routes/__tests__/release.test.js index bf48a314..d9664d85 100644 --- a/routes/__tests__/release.test.js +++ b/routes/__tests__/release.test.js @@ -21,4 +21,53 @@ routeTester.use("/create", [addAuth, controller.create]) routeTester.use("/release/:_id", [addAuth, controller.release]) const slug = `rcgslu${new Date(Date.now()).toISOString().replace("Z", "")}` -it.todo("'/release' route functions") +const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" +const MOCK_PREFIX = process.env.RERUM_ID_PREFIX ?? "https://store.rerum.io/v1/id/" +const MOCK_ID = "11111" + +const mockDoc = { + _id: MOCK_ID, + "@id": `${MOCK_PREFIX}${MOCK_ID}`, + test: "item", + __rerum: { + generatedBy: MOCK_AGENT, + history: { prime: "root", previous: "", next: [] }, + isReleased: "", + isOverwritten: "", + releases: { previous: "", next: [], replaces: "" }, + createdAt: "2025-01-01T00:00:00.000" + } +} + +import { db } from '../../database/index.js' + +it("'/release' route functions", async () => { + // create something to release + const createResponse = await request(routeTester) + .post("/create") + .set("Content-Type", "application/json") + .send({ test: "item" }) + expect(createResponse.statusCode).toBe(201) + + // release with slug: + // 1st findOne for slug uniqueness check -> null + // 2nd findOne to fetch object being released -> mockDoc + db.findOne + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(mockDoc) + + const releaseResponse = await request(routeTester) + .post(`/release/${MOCK_ID}`) + .set("Slug", slug) + .set("Content-Type", "application/json") + + expect(releaseResponse.statusCode).toBe(200) + expect(releaseResponse.body._id).toBeUndefined() + expect(releaseResponse.body.__rerum).toBeDefined() + expect(releaseResponse.body.__rerum.isReleased).toBeTruthy() + const returnedId = releaseResponse.body["@id"] ?? releaseResponse.body.id + expect(releaseResponse.headers["location"]).toBe(returnedId) + + // cleanup slug object via internal helper path + await controller.remove(slug) +}) diff --git a/routes/__tests__/set.test.js b/routes/__tests__/set.test.js index 23c3baa3..2276c149 100644 --- a/routes/__tests__/set.test.js +++ b/routes/__tests__/set.test.js @@ -20,4 +20,35 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] routeTester.use("/set", [addAuth, controller.patchSet]) const unique = new Date(Date.now()).toISOString().replace("Z", "") -it.todo("'/set' route functions") +const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" +const MOCK_PREFIX = process.env.RERUM_ID_PREFIX ?? "https://store.rerum.io/v1/id/" +const MOCK_ORIG_ID = "11111" + +const mockDoc = { + _id: MOCK_ORIG_ID, + "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, + test: "item", + __rerum: { + generatedBy: MOCK_AGENT, + history: { prime: "root", previous: "", next: [] }, + isReleased: "", + isOverwritten: "", + releases: { previous: "", next: [], replaces: "" }, + createdAt: "2025-01-01T00:00:00.000" + } +} + +import { db } from '../../database/index.js' + +it("'/set' route functions", async () => { + db.findOne.mockResolvedValueOnce(mockDoc) + const response = await request(routeTester) + .patch("/set") + .set("Content-Type", "application/json") + .send({ "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, test_set: unique }) + expect(response.statusCode).toBe(200) + expect(response.body["test_set"]).toBe(unique) + expect(response.body._id).toBeUndefined() + const returnedId = response.body["@id"] ?? response.body.id + expect(response.headers["location"]).toBe(returnedId) +}) diff --git a/routes/__tests__/since.test.js b/routes/__tests__/since.test.js index a38b7900..12d433f4 100644 --- a/routes/__tests__/since.test.js +++ b/routes/__tests__/since.test.js @@ -1,5 +1,4 @@ import { jest } from "@jest/globals" -jest.setTimeout(10000) // Only real way to test an express route is to mount it and call it so that we can use the req, res, next. import express from "express" @@ -12,4 +11,31 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /create route without auth that will use controller.history routeTester.use("/since/:_id", controller.since) -it.todo("'/since/:id' route functions") +const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" +const MOCK_PREFIX = "https://store.rerum.io/v1/id/" +const MOCK_ID = "testid123" + +const mockDoc = { + _id: MOCK_ID, + "@id": `${MOCK_PREFIX}${MOCK_ID}`, + test: "item", + __rerum: { + generatedBy: MOCK_AGENT, + history: { prime: "root", previous: "", next: [] }, + isReleased: "", + isOverwritten: "", + releases: { previous: "", next: [], replaces: "" }, + createdAt: "2025-01-01T00:00:00.000" + } +} + +import { db } from '../../database/index.js' + +it("'/since/:id' route functions", async () => { + // since: findOne returns the root object; getAllVersions calls db.find().toArray() → [] + // getAllDescendants on object with next:[] returns [] → response body is [] + db.findOne.mockResolvedValueOnce(mockDoc) + const response = await request(routeTester).get(`/since/${MOCK_ID}`) + expect(response.statusCode).toBe(200) + expect(Array.isArray(response.body)).toBe(true) +}) diff --git a/routes/__tests__/unset.test.js b/routes/__tests__/unset.test.js index 00ad2031..fe7baa56 100644 --- a/routes/__tests__/unset.test.js +++ b/routes/__tests__/unset.test.js @@ -19,6 +19,38 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] // Mount our own /create route without auth that will use controller.create routeTester.use("/unset", [addAuth, controller.patchUnset]) -it.todo("'/unset' route functions") +const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" +const MOCK_PREFIX = process.env.RERUM_ID_PREFIX ?? "https://store.rerum.io/v1/id/" +const MOCK_ORIG_ID = "11111" + +const mockDoc = { + _id: MOCK_ORIG_ID, + "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, + test_obj: "to remove", + test: "item", + __rerum: { + generatedBy: MOCK_AGENT, + history: { prime: "root", previous: "", next: [] }, + isReleased: "", + isOverwritten: "", + releases: { previous: "", next: [], replaces: "" }, + createdAt: "2025-01-01T00:00:00.000" + } +} + +import { db } from '../../database/index.js' + +it("'/unset' route functions", async () => { + db.findOne.mockResolvedValueOnce(mockDoc) + const response = await request(routeTester) + .patch("/unset") + .set("Content-Type", "application/json") + .send({ "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, test_obj: null }) + expect(response.statusCode).toBe(200) + expect(response.body["test_obj"]).toBeUndefined() + expect(response.body._id).toBeUndefined() + const returnedId = response.body["@id"] ?? response.body.id + expect(response.headers["location"]).toBe(returnedId) +}) diff --git a/routes/__tests__/update.test.js b/routes/__tests__/update.test.js index 7043d179..ca0524a3 100644 --- a/routes/__tests__/update.test.js +++ b/routes/__tests__/update.test.js @@ -19,4 +19,39 @@ routeTester.use(express.json({ type: ["application/json", "application/ld+json"] routeTester.use("/update", [addAuth, controller.putUpdate]) const unique = new Date(Date.now()).toISOString().replace("Z", "") -it.todo("'/update' route functions") +const MOCK_AGENT = "https://store.rerum.io/v1/id/agent007" +const MOCK_PREFIX = process.env.RERUM_ID_PREFIX ?? "https://store.rerum.io/v1/id/" +const MOCK_ORIG_ID = "11111" + +const mockDoc = { + _id: MOCK_ORIG_ID, + "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, + "RERUM Update Test": "oldValue", + test: "item", + __rerum: { + generatedBy: MOCK_AGENT, + history: { prime: "root", previous: "", next: [] }, + isReleased: "", + isOverwritten: "", + releases: { previous: "", next: [], replaces: "" }, + createdAt: "2025-01-01T00:00:00.000" + } +} + +import { db } from '../../database/index.js' + +it("'/update' route functions", async () => { + // putUpdate: findOne → original, insertOne → new version, replaceOne → update original's next + db.findOne.mockResolvedValueOnce(mockDoc) + const response = await request(routeTester) + .put("/update") + .set("Content-Type", "application/json") + .send({ "@id": `${MOCK_PREFIX}${MOCK_ORIG_ID}`, "RERUM Update Test": unique }) + expect(response.statusCode).toBe(200) + const returnedId = response.body["@id"] ?? response.body.id + expect(returnedId).toBeTruthy() + expect(response.headers["location"]).toBe(returnedId) + expect(response.headers["location"]).not.toBe(`${MOCK_PREFIX}${MOCK_ORIG_ID}`) + expect(response.body._id).toBeUndefined() + expect(response.body["RERUM Update Test"]).toBe(unique) +}) From eb0fe60c7177aed44990101853e2c17c5ba5751f Mon Sep 17 00:00:00 2001 From: cubap Date: Wed, 6 May 2026 15:58:54 -0500 Subject: [PATCH 10/10] Convert route mount TODOs to real supertest checks Replace placeholder todos with active integration tests that probe mounted routes via supertest. Removed the unused api_routes import and the routeExists helper; tests now perform actual HTTP requests against app for top-level paths (/v1, /client/register, /v1/id/:id, /v1/since/:id, /v1/history/:id) and for /v1/api/* endpoints using appropriate methods and content types, asserting endpoints are mounted (not 404) or return expected 404 for unknown IDs. This validates routing mounts across multiple HTTP verbs instead of relying on stack inspection. --- __tests__/routes_mounted.test.js | 167 +++++++++++++++++++++++-------- 1 file changed, 123 insertions(+), 44 deletions(-) diff --git a/__tests__/routes_mounted.test.js b/__tests__/routes_mounted.test.js index 15993263..0123514d 100644 --- a/__tests__/routes_mounted.test.js +++ b/__tests__/routes_mounted.test.js @@ -6,61 +6,140 @@ */ import request from "supertest" -import api_routes from "../routes/api-routes.js" import app from "../app.js" import fs from "fs" -let app_stack = app._router?.stack ?? [] -let api_stack = api_routes.stack +describe('Check to see that all expected top level route patterns exist.', () => { -/** - * Check if a route exists in the Express app - * @param {Array} stack - The router stack to search - * @param {string} testPath - The path to test for - * @returns {boolean} - True if the route exists - */ -function routeExists(stack, testPath) { - for (const layer of stack) { - // Check if layer has matchers (Express 5) - if (layer.matchers && layer.matchers.length > 0) { - const matcher = layer.matchers[0] - const match = matcher(testPath) - // Express 5 matchers may return boolean true or an object with path metadata - if (match === true || (match && match.path)) return true - } - // Also check route.path directly if it exists - if (layer.route && layer.route.path) { - if (layer.route.path === testPath || layer.route.path.includes(testPath)) return true - } - } - return false -} + it('/v1 -- mounted ', async () => { + const response = await request(app).get('/v1') + expect(response.statusCode).not.toBe(404) + }) -describe('Check to see that all expected top level route patterns exist.', () => { + it('/client -- mounted ', async () => { + const response = await request(app).get('/client/register') + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/id/{_id} -- mounted', async () => { + const response = await request(app).get('/v1/id/test-mounted-id') + // Mounted route with unknown id should 404 (not an unmapped endpoint 404) + expect(response.statusCode).toBe(404) + }) + + it('/v1/since/{_id} -- mounted', async () => { + const response = await request(app).get('/v1/since/test-mounted-id') + // Mounted route with unknown id should 404 + expect(response.statusCode).toBe(404) + }) - it.todo('/v1 -- mounted ') - it.todo('/client -- mounted ') - it.todo('/v1/id/{_id} -- mounted') - it.todo('/v1/since/{_id} -- mounted') - it.todo('/v1/history/{_id} -- mounted') + it('/v1/history/{_id} -- mounted', async () => { + const response = await request(app).get('/v1/history/test-mounted-id') + // Mounted route with unknown id should 404 + expect(response.statusCode).toBe(404) + }) }) describe('Check to see that all /v1/api/ route patterns exist.', () => { - it.todo('/v1/api/query -- mounted ') - it.todo('/v1/api/create -- mounted ') - it.todo('/v1/api/bulkCreate -- mounted ') - it.todo('/v1/api/update -- mounted ') - it.todo('/v1/api/bulkUpdate -- mounted ') - it.todo('/v1/api/overwrite -- mounted ') - it.todo('/v1/api/patch -- mounted ') - it.todo('/v1/api/set -- mounted ') - it.todo('/v1/api/unset -- mounted ') - it.todo('/v1/api/delete/{id} -- mounted ') - it.todo('/v1/api/release/{id} -- mounted ') - it.todo('/v1/api/search -- mounted ') - it.todo('/v1/api/search/phrase -- mounted ') + it('/v1/api/query -- mounted ', async () => { + const response = await request(app) + .post('/v1/api/query') + .set('Content-Type', 'application/json') + .send({ mounted: true }) + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/create -- mounted ', async () => { + const response = await request(app) + .post('/v1/api/create') + .set('Content-Type', 'application/json') + .send({ mounted: true }) + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/bulkCreate -- mounted ', async () => { + const response = await request(app) + .post('/v1/api/bulkCreate') + .set('Content-Type', 'application/json') + .send([{ mounted: true }]) + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/update -- mounted ', async () => { + const response = await request(app) + .put('/v1/api/update') + .set('Content-Type', 'application/json') + .send({ mounted: true }) + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/bulkUpdate -- mounted ', async () => { + const response = await request(app) + .put('/v1/api/bulkUpdate') + .set('Content-Type', 'application/json') + .send([{ mounted: true }]) + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/overwrite -- mounted ', async () => { + const response = await request(app) + .post('/v1/api/overwrite') + .set('Content-Type', 'application/json') + .send({ mounted: true }) + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/patch -- mounted ', async () => { + const response = await request(app) + .patch('/v1/api/patch') + .set('Content-Type', 'application/json') + .send({ mounted: true }) + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/set -- mounted ', async () => { + const response = await request(app) + .patch('/v1/api/set') + .set('Content-Type', 'application/json') + .send({ mounted: true }) + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/unset -- mounted ', async () => { + const response = await request(app) + .patch('/v1/api/unset') + .set('Content-Type', 'application/json') + .send({ mounted: true }) + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/delete/{id} -- mounted ', async () => { + const response = await request(app).delete('/v1/api/delete/test-mounted-id') + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/release/{id} -- mounted ', async () => { + const response = await request(app).patch('/v1/api/release/test-mounted-id') + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/search -- mounted ', async () => { + const response = await request(app) + .post('/v1/api/search') + .set('Content-Type', 'text/plain') + .send('mounted search') + expect(response.statusCode).not.toBe(404) + }) + + it('/v1/api/search/phrase -- mounted ', async () => { + const response = await request(app) + .post('/v1/api/search/phrase') + .set('Content-Type', 'text/plain') + .send('mounted phrase search') + expect(response.statusCode).not.toBe(404) + }) })