Skip to content
154 changes: 91 additions & 63 deletions __tests__/routes_mounted.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,111 +6,139 @@
*/

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

/**
* 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)
if (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
}

describe('Check to see that all expected top level route patterns exist.', () => {

it('/v1 -- mounted ', () => {
expect(routeExists(app_stack, '/v1')).toBe(true)
it('/v1 -- mounted ', async () => {
const response = await request(app).get('/v1')
expect(response.statusCode).not.toBe(404)
})

it('/client -- mounted ', () => {
expect(routeExists(app_stack, '/client')).toBe(true)
it('/client -- mounted ', async () => {
const response = await request(app).get('/client/register')
expect(response.statusCode).not.toBe(404)
})

it('/v1/id/{_id} -- mounted', () => {
expect(routeExists(api_stack, '/id')).toBe(true)
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', () => {
expect(routeExists(api_stack, '/since')).toBe(true)
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('/v1/history/{_id} -- mounted', () => {
expect(routeExists(api_stack, '/history')).toBe(true)
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('/v1/api/query -- mounted ', () => {
expect(routeExists(api_stack, '/api/query')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/create')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/bulkCreate')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/update')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/bulkUpdate')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/overwrite')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/patch')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/set')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/unset')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/delete')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/release')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/search')).toBe(true)
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 ', () => {
expect(routeExists(api_stack, '/api/search/phrase')).toBe(true)
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)
})

})
Expand Down Expand Up @@ -142,4 +170,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()
})
})
})
4 changes: 2 additions & 2 deletions controllers/bulk.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
9 changes: 4 additions & 5 deletions controllers/crud.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 = structuredClone(req.body)
let rerumProp = { "__rerum": utils.configureRerumOptions(generatorAgent, provided, false, false)["__rerum"] }
if(slug){
rerumProp.__rerum.slug = slug
Expand All @@ -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 = structuredClone(newObject)
res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"])
res.status(201)
res.json(newObject)
Expand All @@ -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 = {
Expand Down
10 changes: 5 additions & 5 deletions controllers/delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,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}`,
Expand All @@ -57,7 +57,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 = {
Expand Down Expand Up @@ -121,7 +121,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.
Expand Down Expand Up @@ -154,7 +154,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"])
Expand Down Expand Up @@ -193,7 +193,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) {
Expand Down
10 changes: 4 additions & 6 deletions controllers/gog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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") {
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -389,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)
Expand Down
12 changes: 6 additions & 6 deletions controllers/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -112,12 +112,12 @@ 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()
if (matches.length) {
const size = Buffer.byteLength(JSON.stringify(matches))
res.set("Content-Length", size)
res.sendStatus(200)
const matchCount = await db.countDocuments(props, { limit, skip })
if (matchCount > 0) {
res.set("Content-Length", 0)
res.status(200).end()
return
}
let err = {
Expand Down
4 changes: 2 additions & 2 deletions controllers/overwrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading