From 2e5994735990bab462f4e66bfcfd6bd3ddf67aa1 Mon Sep 17 00:00:00 2001 From: Ludovic Muller Date: Wed, 27 Nov 2024 17:27:11 +0100 Subject: [PATCH 1/6] sp: use node:test --- package-lock.json | 131 ------------------ packages/sparql-proxy/index.js | 21 ++- .../lib/fetchServiceDescription.js | 4 +- .../lib/serviceDescriptionWorker.js | 39 +++--- packages/sparql-proxy/lib/utils.js | 37 +++++ packages/sparql-proxy/package.json | 9 +- .../sparql-proxy/test/ReplaceStream.test.js | 104 +++++++------- packages/sparql-proxy/test/index.test.js | 43 ++---- packages/sparql-proxy/test/mocha_setup.js | 4 - .../test/support/failingWorker.js | 7 +- .../sparql-proxy/test/support/workerDouble.js | 12 +- packages/sparql-proxy/test/utils.test.js | 96 +++++++++++++ 12 files changed, 237 insertions(+), 270 deletions(-) create mode 100644 packages/sparql-proxy/lib/utils.js delete mode 100644 packages/sparql-proxy/test/mocha_setup.js create mode 100644 packages/sparql-proxy/test/utils.test.js diff --git a/package-lock.json b/package-lock.json index 0d670d63..ade33244 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2321,55 +2321,6 @@ "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", "license": "MIT" }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/commons/node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", - "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "lodash.get": "^4.4.2", - "type-detect": "^4.1.0" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", - "dev": true, - "license": "(Unlicense OR Apache-2.0)" - }, "node_modules/@streamparser/json": { "version": "0.0.20", "resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.20.tgz", @@ -9780,13 +9731,6 @@ "node": ">=4.0" } }, - "node_modules/just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true, - "license": "MIT" - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -10134,13 +10078,6 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -11750,20 +11687,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", - "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.1", - "@sinonjs/text-encoding": "^0.7.3", - "just-extend": "^6.2.0", - "path-to-regexp": "^8.1.0" - } - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -12409,16 +12332,6 @@ "node": "20 || >=22" } }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -13832,46 +13745,6 @@ "node": ">=10" } }, - "node_modules/sinon": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", - "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.2", - "@sinonjs/samsam": "^8.0.1", - "diff": "^7.0.0", - "nise": "^6.1.1", - "supports-color": "^7.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/sinon-chai": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-4.0.0.tgz", - "integrity": "sha512-cWqO7O2I4XfJDWyWElAQ9D/dtdh5Mo0RHndsfiiYyjWnlPzBJdIvjCVURO4EjyYaC3BjV+ISNXCfTXPXTEIEWA==", - "dev": true, - "license": "(BSD-2-Clause OR WTFPL)", - "peerDependencies": { - "chai": "^5.0.0", - "sinon": ">=4.0.0" - } - }, - "node_modules/sinon/node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -16450,11 +16323,7 @@ }, "devDependencies": { "@types/node": "^22.9.3", - "chai": "^5.1.2", - "mocha": "^10.8.2", "rimraf": "^6.0.1", - "sinon": "^19.0.2", - "sinon-chai": "^4.0.0", "typescript": "^5.6.3" } }, diff --git a/packages/sparql-proxy/index.js b/packages/sparql-proxy/index.js index 428cac5c..43a0be06 100644 --- a/packages/sparql-proxy/index.js +++ b/packages/sparql-proxy/index.js @@ -8,11 +8,13 @@ import { Worker } from 'node:worker_threads' import { sparqlGetRewriteConfiguration } from 'trifid-core' import rdf from '@zazuko/env-node' import ReplaceStream from './lib/ReplaceStream.js' +import { authBasicHeader, objectLength } from './lib/utils.js' const defaultConfiguration = { endpointUrl: '', username: '', password: '', + endpoints: {}, datasetBaseUrl: '', allowRewriteToggle: true, // Allow the user to toggle the rewrite configuration using the `rewrite` query parameter. rewrite: false, // Rewrite by default @@ -25,23 +27,18 @@ const defaultConfiguration = { serviceDescriptionFormat: undefined, // override the accept header for the service description request. by default, will use content negotiation using formats `@zazuko/env-node` can parse } -/** - * Generate the value for the Authorization header for basic authentication. - * - * @param {string} user The username. - * @param {string} password The password of that user. - * @returns {string} The value of the Authorization header to use. - */ -const authBasicHeader = (user, password) => { - const base64String = Buffer.from(`${user}:${password}`).toString('base64') - return `Basic ${base64String}` -} - /** @type {import('trifid-core/types').TrifidPlugin} */ const factory = async (trifid) => { const { logger, config, trifidEvents } = trifid const options = { ...defaultConfiguration, ...config } + let dynamicEndpoints = false + + if (objectLength(options.endpoints) > 0) { + // Support for multiple endpoints + dynamicEndpoints = true + } + if (!options.endpointUrl) { throw Error('Missing endpointUrl parameter') } diff --git a/packages/sparql-proxy/lib/fetchServiceDescription.js b/packages/sparql-proxy/lib/fetchServiceDescription.js index 3cd7fbc7..87934148 100644 --- a/packages/sparql-proxy/lib/fetchServiceDescription.js +++ b/packages/sparql-proxy/lib/fetchServiceDescription.js @@ -10,6 +10,4 @@ const fetchServiceDescription = async (endpointUrl, { format, authorization }) = return response.dataset() } -export default { - fetchServiceDescription, -} +export default { fetchServiceDescription } diff --git a/packages/sparql-proxy/lib/serviceDescriptionWorker.js b/packages/sparql-proxy/lib/serviceDescriptionWorker.js index a74d122d..1f81e510 100644 --- a/packages/sparql-proxy/lib/serviceDescriptionWorker.js +++ b/packages/sparql-proxy/lib/serviceDescriptionWorker.js @@ -1,7 +1,25 @@ import { parentPort } from 'node:worker_threads' import rdf from '@zazuko/env-node' import sd from '@vocabulary/sd' -import fetch from './fetchServiceDescription.js' +import fsd from './fetchServiceDescription.js' + +const serviceDescriptionVocab = rdf.clownface({ + dataset: rdf.dataset(sd({ factory: rdf })), +}) + +const allowedProperties = rdf.termSet( + serviceDescriptionVocab + .has(rdf.ns.rdf.type, rdf.ns.rdf.Property) + .terms, +) + +const cbd = ({ level, quad: { predicate, subject } }) => { + if (level === 0) { + return !predicate.equals(rdf.ns.sd.endpoint) && allowedProperties.has(predicate) + } + + return subject.termType === 'BlankNode' +} parentPort.once('message', async (message) => { const { type, data: { endpointUrl, serviceDescriptionTimeout, ...rest } } = message @@ -13,7 +31,7 @@ parentPort.once('message', async (message) => { }, serviceDescriptionTimeout) try { - const dataset = await fetch.fetchServiceDescription(endpointUrl, { + const dataset = await fsd.fetchServiceDescription(endpointUrl, { format: rest.serviceDescriptionFormat, authorization: rest.authorizationHeader, }) @@ -42,20 +60,3 @@ parentPort.once('message', async (message) => { } } }) - -const serviceDescriptionVocab = rdf.clownface({ - dataset: rdf.dataset(sd({ factory: rdf })), -}) -const allowedProperties = rdf.termSet( - serviceDescriptionVocab - .has(rdf.ns.rdf.type, rdf.ns.rdf.Property) - .terms, -) - -const cbd = ({ level, quad: { predicate, subject } }) => { - if (level === 0) { - return !predicate.equals(rdf.ns.sd.endpoint) && allowedProperties.has(predicate) - } - - return subject.termType === 'BlankNode' -} diff --git a/packages/sparql-proxy/lib/utils.js b/packages/sparql-proxy/lib/utils.js new file mode 100644 index 00000000..215d0ead --- /dev/null +++ b/packages/sparql-proxy/lib/utils.js @@ -0,0 +1,37 @@ +/** + * Generate the value for the Authorization header for basic authentication. + * + * @param {string} user The username. + * @param {string} password The password of that user. + * @returns {string} The value of the Authorization header to use. + */ +export const authBasicHeader = (user, password) => { + const base64String = Buffer.from(`${user}:${password}`).toString('base64') + return `Basic ${base64String}` +} + +/** + * Assert that the value is an object. + * Return the value if it is an object, otherwise return false. + * + * @param {any} value The value to assert. + * @returns {Object | false} The value, if it is an object, otherwise false. + */ +export const assertObject = (value) => { + if (value === null || typeof value !== 'object') { + return false + } + return value +} + +/** + * Return the length of an object. + * @param {any} obj The object to get the length of. + * @returns {number} The length of the object. + */ +export const objectLength = (obj) => { + if (!assertObject(obj)) { + return 0 + } + return Object.keys(obj).length +} diff --git a/packages/sparql-proxy/package.json b/packages/sparql-proxy/package.json index fb35e9e9..91d09fd0 100644 --- a/packages/sparql-proxy/package.json +++ b/packages/sparql-proxy/package.json @@ -7,7 +7,7 @@ "types": "dist/index.d.ts", "homepage": "https://github.com/zazuko/trifid", "scripts": { - "test": "c8 --all --reporter lcovonly --reporter text mocha", + "test": "c8 --all --reporter lcovonly --reporter text node --test **/*.test.js", "clean": "rimraf dist/", "prebuild": "npm run clean", "build": "tsc", @@ -43,18 +43,11 @@ }, "devDependencies": { "@types/node": "^22.9.3", - "chai": "^5.1.2", - "mocha": "^10.8.2", "rimraf": "^6.0.1", - "sinon": "^19.0.2", - "sinon-chai": "^4.0.0", "typescript": "^5.6.3" }, "publishConfig": { "access": "public", "provenance": true - }, - "mocha": { - "require": "test/mocha_setup.js" } } diff --git a/packages/sparql-proxy/test/ReplaceStream.test.js b/packages/sparql-proxy/test/ReplaceStream.test.js index 9f69d0b5..13b6d77a 100644 --- a/packages/sparql-proxy/test/ReplaceStream.test.js +++ b/packages/sparql-proxy/test/ReplaceStream.test.js @@ -1,137 +1,135 @@ import { PassThrough } from 'node:stream' -import { expect } from 'chai' -import { describe, it } from 'mocha' +import { describe, it } from 'node:test' +import { equal } from 'node:assert' + import ReplaceStream from '../lib/ReplaceStream.js' describe('ReplaceStream', () => { - it('should replace a single occurrence of a string', (done) => { + it('should replace a single occurrence of a string', async () => { const replaceStream = new ReplaceStream('old', 'new') const input = new PassThrough() const output = new PassThrough() - const receivedData = [] input.pipe(replaceStream).pipe(output) - output.setEncoding('utf8') output.on('data', (data) => { receivedData.push(data) }) - output.on('end', () => { - expect(receivedData.join('')).to.equal('new string') - done() + await new Promise((resolve) => { + output.on('end', () => { + equal(receivedData.join(''), 'new string') + resolve() + }) + input.write('old string') + input.end() }) - - input.write('old string') - input.end() }) - it('should replace multiple occurrences of a string', (done) => { + it('should replace multiple occurrences of a string', async () => { const replaceStream = new ReplaceStream('old', 'new') const input = new PassThrough() const output = new PassThrough() - const receivedData = [] input.pipe(replaceStream).pipe(output) - output.setEncoding('utf8') output.on('data', (data) => { receivedData.push(data) }) - output.on('end', () => { - expect(receivedData.join('')).to.equal('new string with new value') - done() + await new Promise((resolve) => { + output.on('end', () => { + equal(receivedData.join(''), 'new string with new value') + resolve() + }) + input.write('old string with old value') + input.end() }) - - input.write('old string with old value') - input.end() }) - it('should replace strings that span across chunks', (done) => { + it('should replace strings that span across chunks', async () => { const replaceStream = new ReplaceStream('old', 'new') const input = new PassThrough() const output = new PassThrough() - const receivedData = [] input.pipe(replaceStream).pipe(output) - output.setEncoding('utf8') output.on('data', (data) => { receivedData.push(data) }) - output.on('end', () => { - expect(receivedData.join('')).to.equal('new and new') - done() + await new Promise((resolve) => { + output.on('end', () => { + equal(receivedData.join(''), 'new and new') + resolve() + }) + input.write('ol') + input.write('d and ol') + input.write('d') + input.end() }) - - input.write('ol') - input.write('d and ol') - input.write('d') - input.end() }) - it('should handle no occurrences of the string', (done) => { + it('should handle no occurrences of the string', async () => { const replaceStream = new ReplaceStream('old', 'new') const input = new PassThrough() const output = new PassThrough() input.pipe(replaceStream).pipe(output) - output.setEncoding('utf8') output.on('data', (data) => { - expect(data).to.equal('no match here') + equal(data, 'no match here') }) - output.on('end', done) - - input.write('no match here') - input.end() + await new Promise((resolve) => { + output.on('end', resolve) + input.write('no match here') + input.end() + }) }) - it('should handle empty input', (done) => { + it('should handle empty input', async () => { const replaceStream = new ReplaceStream('old', 'new') const input = new PassThrough() const output = new PassThrough() input.pipe(replaceStream).pipe(output) - output.setEncoding('utf8') output.on('data', (data) => { - expect(data).to.equal('') + equal(data, '') }) - output.on('end', done) - - input.end() + await new Promise((resolve) => { + output.on('end', resolve) + input.end() + }) }) - it('should handle large input with multiple replacements', (done) => { + it('should handle large input with multiple replacements', async () => { const replaceStream = new ReplaceStream('old', 'new') const input = new PassThrough() const output = new PassThrough() + let result = '' const largeInput = 'old '.repeat(1000) const expectedOutput = 'new '.repeat(1000) input.pipe(replaceStream).pipe(output) - - let result = '' output.setEncoding('utf8') output.on('data', (data) => { result += data }) - output.on('end', () => { - expect(result).to.equal(expectedOutput) - done() + await new Promise((resolve) => { + output.on('end', () => { + equal(result, expectedOutput) + resolve() + }) + input.write(largeInput) + input.end() }) - - input.write(largeInput) - input.end() }) }) diff --git a/packages/sparql-proxy/test/index.test.js b/packages/sparql-proxy/test/index.test.js index e4014650..fe600e21 100644 --- a/packages/sparql-proxy/test/index.test.js +++ b/packages/sparql-proxy/test/index.test.js @@ -1,7 +1,7 @@ -import { expect } from 'chai' +import { describe, it, beforeEach, afterEach } from 'node:test' +import { equal, deepEqual, match, ok } from 'node:assert' import trifidCore, { getListenerURL } from 'trifid-core' import rdf from '@zazuko/env-node' -import { stub, restore } from 'sinon' import sparqlProxy from '../index.js' describe('sparql-proxy', () => { @@ -12,7 +12,7 @@ describe('sparql-proxy', () => { const server = await trifidCore({ server: { listener: { - port: 4242, + port: 0, }, logLevel: 'warn', }, @@ -45,19 +45,11 @@ describe('sparql-proxy', () => { await startTrifid() }) - context('requesting service description', () => { + describe('requesting service description', () => { const forwardedProperties = rdf.termMap([ [rdf.ns.sd.feature], ]) - beforeEach(() => { - stub(global, 'fetch') - }) - - afterEach(() => { - restore() - }) - it('does not serve Service Description when there are any query string', async () => { // given const url = await startTrifid({ @@ -68,7 +60,7 @@ describe('sparql-proxy', () => { const response = await rdf.fetch(`${url}/query?foo=bar`) // then - expect(response.headers.get('content-type')).to.match(/^(text\/plain|application\/json).*/) + match(response.headers.get('content-type'), /^(text\/plain|text\/html|application\/json).*/) }) for (const [property] of forwardedProperties) { @@ -84,7 +76,7 @@ describe('sparql-proxy', () => { // then const service = rdf.clownface({ dataset }).has(rdf.ns.sd.endpoint) - expect(service.out(property).values).to.have.property('length').greaterThan(0) + ok(service.out(property).values.length > 0) }) } @@ -99,7 +91,7 @@ describe('sparql-proxy', () => { // then const service = rdf.clownface({ dataset }).has(rdf.ns.sd.endpoint) - expect(service.out(rdf.ns.sd.endpoint).term).to.deep.eq(rdf.namedNode(`${url}/${path}`)) + deepEqual(service.out(rdf.ns.sd.endpoint).term, rdf.namedNode(`${url}/${path}`)) } }) @@ -115,7 +107,7 @@ describe('sparql-proxy', () => { // then const service = rdf.clownface({ dataset }).has(rdf.ns.sd.endpoint) - expect(service.out(rdf.ns.sd.endpoint).term).to.deep.eq(rdf.namedNode(`${url}/query`)) + deepEqual(service.out(rdf.ns.sd.endpoint).term, rdf.namedNode(`${url}/query`)) }) it('serves minimal description if original service fails', async () => { @@ -130,7 +122,7 @@ describe('sparql-proxy', () => { // then const service = rdf.clownface({ dataset }).has(rdf.ns.sd.endpoint) - expect(service.out(rdf.ns.sd.endpoint).term).to.deep.eq(rdf.namedNode(`${url}/query`)) + deepEqual(service.out(rdf.ns.sd.endpoint).term, rdf.namedNode(`${url}/query`)) }) for (const property of [rdf.namedNode('http://example.org/foo', rdf.ns.sd.nonStandardProp)]) { @@ -144,7 +136,7 @@ describe('sparql-proxy', () => { // then const service = rdf.clownface({ dataset }).has(rdf.ns.sd.endpoint) - expect(service.out(property).terms).to.have.length(0) + equal(service.out(property).terms.length, 0) }) } @@ -163,18 +155,7 @@ describe('sparql-proxy', () => { .out(rdf.ns.sd.namedGraph) .out(rdf.ns.sd.graph) .out(rdf.ns._void.triples) - expect(actual.term).to.deep.eq(rdf.literal('2000', rdf.ns.xsd.integer)) - }) - - it('serves service description from memory', async () => { - // given - const url = await startTrifid() - - // when - await rdf.fetch(`${url}/query`) - - // then - expect(fetch).not.to.have.been.called + deepEqual(actual.term, rdf.literal('2000', rdf.ns.xsd.integer)) }) it('respects content negotiation', async () => { @@ -189,7 +170,7 @@ describe('sparql-proxy', () => { }) // then - expect(response.headers.get('content-type')).to.eq('text/turtle') + equal(response.headers.get('content-type'), 'text/turtle') }) }) }) diff --git a/packages/sparql-proxy/test/mocha_setup.js b/packages/sparql-proxy/test/mocha_setup.js deleted file mode 100644 index 01d0a6e2..00000000 --- a/packages/sparql-proxy/test/mocha_setup.js +++ /dev/null @@ -1,4 +0,0 @@ -import * as chai from 'chai' -import sinonChai from 'sinon-chai' - -chai.use(sinonChai) diff --git a/packages/sparql-proxy/test/support/failingWorker.js b/packages/sparql-proxy/test/support/failingWorker.js index a421c6d8..803b4547 100644 --- a/packages/sparql-proxy/test/support/failingWorker.js +++ b/packages/sparql-proxy/test/support/failingWorker.js @@ -1,6 +1,7 @@ -import { stub } from 'sinon' -import fetch from '../../lib/fetchServiceDescription.js' +import fsd from '../../lib/fetchServiceDescription.js' -fetch.fetchServiceDescription = stub().rejects(new Error('Failed to fetch service description')) +fsd.fetchServiceDescription = async (_endpointUrl, _opts) => { + throw new Error('Failed to fetch service description') +} import('../../lib/serviceDescriptionWorker.js') diff --git a/packages/sparql-proxy/test/support/workerDouble.js b/packages/sparql-proxy/test/support/workerDouble.js index 5498e758..0bd91bf2 100644 --- a/packages/sparql-proxy/test/support/workerDouble.js +++ b/packages/sparql-proxy/test/support/workerDouble.js @@ -1,12 +1,12 @@ -import * as url from 'node:url' -import { stub } from 'sinon' +import { fileURLToPath } from 'node:url' import rdf from '@zazuko/env-node' -import fetch from '../../lib/fetchServiceDescription.js' -const serviceDescriptionPath = url.fileURLToPath(new URL('./serviceDescription.ttl', import.meta.url)) +import fsd from '../../lib/fetchServiceDescription.js' -fetch.fetchServiceDescription = stub().callsFake(async () => { +const serviceDescriptionPath = fileURLToPath(new URL('./serviceDescription.ttl', import.meta.url)) + +fsd.fetchServiceDescription = async (_endpointUrl, _opts) => { return rdf.dataset().import(rdf.fromFile(serviceDescriptionPath)) -}) +} import('../../lib/serviceDescriptionWorker.js') diff --git a/packages/sparql-proxy/test/utils.test.js b/packages/sparql-proxy/test/utils.test.js new file mode 100644 index 00000000..aa673091 --- /dev/null +++ b/packages/sparql-proxy/test/utils.test.js @@ -0,0 +1,96 @@ +import { describe, it } from 'node:test' +import { equal, deepEqual } from 'node:assert' + +import { authBasicHeader, assertObject, objectLength } from '../lib/utils.js' + +describe('utils', () => { + describe('authBasicHeader', () => { + it('should return the expected value', () => { + equal(authBasicHeader('user', 'password'), 'Basic dXNlcjpwYXNzd29yZA==') + }) + }) + + describe('assertObject', () => { + it('should return false for null', () => { + equal(assertObject(null), false) + }) + + it('should return false for a string', () => { + equal(assertObject('string'), false) + }) + + it('should return false for a number', () => { + equal(assertObject(42), false) + }) + + it('should return false for an array', () => { + equal(assertObject([]), false) + }) + + it('should return false for a function', () => { + equal(assertObject(() => { }), false) + }) + + it('should return false for a boolean', () => { + equal(assertObject(true), false) + }) + + it('should return false for undefined', () => { + equal(assertObject(undefined), false) + }) + + it('should return an object for an empty object', () => { + deepEqual(assertObject({}), {}) + }) + + it('should return an object for an object with properties', () => { + deepEqual(assertObject({ foo: 'bar' }), { foo: 'bar' }) + }) + }) + + describe('objectLength', () => { + it('should return 0 for null', () => { + equal(objectLength(null), 0) + }) + + it('should return 0 for a string', () => { + equal(objectLength('string'), 0) + }) + + it('should return 0 for a number', () => { + equal(objectLength(42), 0) + }) + + it('should return 0 for an array', () => { + equal(objectLength([]), 0) + }) + + it('should return 0 for a function', () => { + equal(objectLength(() => { }), 0) + }) + + it('should return 0 for a boolean', () => { + equal(objectLength(true), 0) + }) + + it('should return 0 for undefined', () => { + equal(objectLength(undefined), 0) + }) + + it('should return 0 for an empty object', () => { + equal(objectLength({}), 0) + }) + + it('should return 1 for an object with one property', () => { + equal(objectLength({ foo: 'bar' }), 1) + }) + + it('should return 2 for an object with two properties', () => { + equal(objectLength({ one: '1', two: '2' }), 2) + }) + + it('should return 3 for an object with three properties', () => { + equal(objectLength({ one: '1', two: '2', three: '2' }), 3) + }) + }) +}) From dbd8574ac5e6d3b93103859ab0928bff7730630f Mon Sep 17 00:00:00 2001 From: Ludovic Muller Date: Wed, 27 Nov 2024 18:01:58 +0100 Subject: [PATCH 2/6] core: use node:test --- package-lock.json | 412 +------------------- packages/core/package.json | 4 +- packages/core/test/config.handler.test.js | 49 +-- packages/core/test/config.test.js | 142 +++---- packages/core/test/plugins/health.test.js | 4 +- packages/core/test/plugins/redirect.test.js | 5 +- packages/core/test/plugins/static.test.js | 4 +- packages/core/test/plugins/throw.test.js | 4 +- packages/core/test/plugins/view.test.js | 8 +- packages/core/test/resolvers.test.js | 158 ++++---- 10 files changed, 202 insertions(+), 588 deletions(-) diff --git a/package-lock.json b/package-lock.json index ade33244..a3afb4ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4399,13 +4399,6 @@ "node": ">=8" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" - }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -4598,19 +4591,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/canonicalize": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-1.0.8.tgz", @@ -4660,6 +4640,7 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5788,19 +5769,6 @@ } } }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -5937,16 +5905,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -6393,6 +6351,7 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -7728,16 +7687,6 @@ "node": ">=8" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -8511,16 +8460,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/help-me": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", @@ -9232,19 +9171,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -10092,23 +10018,6 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -11333,268 +11242,6 @@ "obliterator": "^2.0.1" } }, - "node_modules/mocha": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", - "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^8.1.0", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/mocha/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/mocha/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/mocha/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/mocha/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -12816,16 +12463,6 @@ "dev": true, "license": "ISC" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/rbush": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", @@ -13608,16 +13245,6 @@ "node": ">=10" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -15435,13 +15062,6 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "license": "MIT" }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -16024,32 +15644,6 @@ "node": ">=12" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser/node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -16173,8 +15767,6 @@ "@rdfjs/types": "^1.1.2", "@types/node": "^22.9.3", "c8": "^10.1.2", - "chai": "^5.1.2", - "mocha": "^10.8.2", "nodemon": "^3.1.7", "rimraf": "^6.0.1", "typescript": "^5.6.3" diff --git a/packages/core/package.json b/packages/core/package.json index 7b75cc9d..2db44001 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -14,7 +14,7 @@ "url": "https://github.com/zazuko/trifid/issues" }, "scripts": { - "test": "c8 --all --reporter=lcov --reporter=text mocha test/**.test.js test/**/*.test.js", + "test": "c8 --all --reporter=lcov --reporter=text node --test test/**.test.js test/**/*.test.js", "watch": "nodemon server.js", "typings": "tsc", "clean": "rimraf *.tgz dist/", @@ -77,8 +77,6 @@ "@rdfjs/types": "^1.1.2", "@types/node": "^22.9.3", "c8": "^10.1.2", - "chai": "^5.1.2", - "mocha": "^10.8.2", "nodemon": "^3.1.7", "rimraf": "^6.0.1", "typescript": "^5.6.3" diff --git a/packages/core/test/config.handler.test.js b/packages/core/test/config.handler.test.js index b05d0506..14377c83 100644 --- a/packages/core/test/config.handler.test.js +++ b/packages/core/test/config.handler.test.js @@ -1,11 +1,10 @@ // @ts-check +import { describe, it } from 'node:test' +import { equal, ok, doesNotThrow } from 'node:assert' import { dirname } from 'node:path' import { fileURLToPath } from 'node:url' -import { describe, it } from 'mocha' -import { expect } from 'chai' - import handler from '../lib/config/handler.js' import { fileCallback } from '../lib/resolvers.js' @@ -42,9 +41,9 @@ const assertResolution = (promise) => { describe('config handler', () => { it('should not throw when loading an empty configuration file', () => { const currentDir = dirname(fileURLToPath(import.meta.url)) - return expect( - handler(fileCallback(currentDir)('./support/empty.json')), - ).to.not.throw + doesNotThrow( + () => handler(fileCallback(currentDir)('./support/empty.json')), + ) }) it('should not throw when loading an empty configuration', () => { @@ -123,15 +122,16 @@ describe('config handler', () => { const config = await handler( fileCallback(currentDir)('./support/chain/chain1.json'), ) - expect(config.globals).to.not.be.undefined - expect(config.globals.value3).to.not.be.undefined - expect(config.globals.value3).to.equal('chain3') - expect(config.globals.value2).to.not.be.undefined - expect(config.globals.value2).to.equal('chain2') - expect(config.globals.value1).to.not.be.undefined - expect(config.globals.value1).to.equal('chain1') - expect(config.globals.value).to.not.be.undefined - expect(config.globals.value).to.equal('chain1') + + ok(config.globals !== undefined) + ok(config.globals.value3 !== undefined) + equal(config.globals.value3, 'chain3') + ok(config.globals.value2 !== undefined) + equal(config.globals.value2, 'chain2') + ok(config.globals.value1 !== undefined) + equal(config.globals.value1, 'chain1') + ok(config.globals.value !== undefined) + equal(config.globals.value, 'chain1') }) it('simple check using the file resolver should work', () => { @@ -146,15 +146,16 @@ describe('config handler', () => { const config = await handler( fileCallback(currentDir)('./support/chain-file/chain1.json'), ) - expect(config.globals).to.not.be.undefined - expect(config.globals.value3).to.not.be.undefined - expect(config.globals.value3).to.equal('chain3') - expect(config.globals.value2).to.not.be.undefined - expect(config.globals.value2).to.equal('chain2') - expect(config.globals.value1).to.not.be.undefined - expect(config.globals.value1).to.equal('chain1') - expect(config.globals.value).to.not.be.undefined - expect(config.globals.value).to.equal('chain1') + + ok(config.globals !== undefined) + ok(config.globals.value3 !== undefined) + equal(config.globals.value3, 'chain3') + ok(config.globals.value2 !== undefined) + equal(config.globals.value2, 'chain2') + ok(config.globals.value1 !== undefined) + equal(config.globals.value1, 'chain1') + ok(config.globals.value !== undefined) + equal(config.globals.value, 'chain1') }) it('should throw in case of infinite loop', () => { diff --git a/packages/core/test/config.test.js b/packages/core/test/config.test.js index 6d4a0622..6e475f1d 100644 --- a/packages/core/test/config.test.js +++ b/packages/core/test/config.test.js @@ -1,96 +1,96 @@ // @ts-check -import { describe, it } from 'mocha' -import { expect } from 'chai' +import { describe, it } from 'node:test' +import { throws, doesNotThrow } from 'node:assert' import parser from '../lib/config/parser.js' describe('config', () => { it('should not throw if the configuration is empty', () => { - expect(() => parser()).to.not.throw() - expect(() => parser({})).to.not.throw() + doesNotThrow(() => parser()) + doesNotThrow(() => parser({})) }) it('sould throw if we add some non-supported fields', () => { - // @ts-ignore (this is what we want to test) - expect(() => parser({ thisFieldIsNotSupported: true })).to.throw() + // @ts-expect-error + throws(() => parser({ thisFieldIsNotSupported: true })) }) it('should not throw if supported properties are empty', () => { - expect(() => + doesNotThrow(() => parser({ extends: [], globals: {}, server: {}, plugins: {}, }), - ).to.not.throw() + ) }) it('should not throw on valid values for extends', () => { - expect(() => + doesNotThrow(() => parser({ extends: [], }), - ).to.not.throw() + ) - expect(() => + doesNotThrow(() => parser({ extends: ['path'], }), - ).to.not.throw() + ) - expect(() => + doesNotThrow(() => parser({ extends: ['path1', 'path2', 'path3'], }), - ).to.not.throw() + ) }) it('should throw on invalid values for extends', () => { // this is a string instead of an array of strings - expect(() => { + throws(() => { parser({ - // @ts-ignore (this is what we want to test) + // @ts-expect-error extends: 'this is a string instead of an array', }) - }).to.throw() + }) // this is not an array of strings, but an array of integers - expect(() => { + throws(() => { parser({ - // @ts-ignore (this is what we want to test) + // @ts-expect-error extends: [1, 2, 3], }) - }).to.throw() + }) }) it('should not throw on valid values for server', () => { - expect(() => { + doesNotThrow(() => { parser({ server: {}, }) - }).to.not.throw() + }) - expect(() => { + doesNotThrow(() => { parser({ server: { listener: {}, options: {}, }, }) - }).to.not.throw() + }) - expect(() => { + doesNotThrow(() => { parser({ server: { listener: {}, options: {}, }, }) - }).to.not.throw() + }) - expect(() => { + doesNotThrow(() => { parser({ server: { listener: { @@ -99,9 +99,9 @@ describe('config', () => { options: {}, }, }) - }).to.not.throw() + }) - expect(() => { + doesNotThrow(() => { parser({ server: { listener: { @@ -112,32 +112,32 @@ describe('config', () => { }, }, }) - }).to.not.throw() + }) }) it('should throw on invalid values for server', () => { // this is a string instead of an object - expect(() => { + throws(() => { parser({ - // @ts-ignore (this is what we want to test) + // @ts-expect-error server: 'this is a string instead of an object', }) - }).to.throw() + }) // unsupported field - expect(() => { + throws(() => { parser({ server: { listener: {}, options: {}, - // @ts-ignore (this is what we want to test) + // @ts-expect-error unsupportedField: true, }, }) - }).to.throw() + }) // invalid port number - expect(() => { + throws(() => { parser({ server: { listener: { @@ -146,49 +146,49 @@ describe('config', () => { options: {}, }, }) - }).to.throw() + }) // unsupported listener property - expect(() => { + throws(() => { parser({ server: { listener: { port: 8080, - // @ts-ignore (this is what we want to test) + // @ts-expect-error unsupportedField: true, }, options: {}, }, }) - }).to.throw() + }) }) it('should not throw on valid values for globals', () => { - expect(() => { + doesNotThrow(() => { parser({ globals: {}, }) - }).to.not.throw() + }) - expect(() => { + doesNotThrow(() => { parser({ globals: { foo: 'bar', }, }) - }).to.not.throw() + }) - expect(() => { + doesNotThrow(() => { parser({ globals: { foo: 'bar', jon: 'doe', }, }) - }).to.not.throw() + }) // multi-level globals - expect(() => { + doesNotThrow(() => { parser({ globals: { foo: { @@ -196,27 +196,27 @@ describe('config', () => { }, }, }) - }).to.not.throw() + }) }) it('should throw on invalid values for globals', () => { // this is a string instead of an object - expect(() => { + throws(() => { parser({ - // @ts-ignore (this is what we want to test) + // @ts-expect-error globals: 'this is a string instead of an object', }) - }).to.throw() + }) }) it('should not throw on valid values for plugins', () => { - expect(() => { + doesNotThrow(() => { parser({ plugins: {}, }) - }).to.not.throw() + }) - expect(() => { + doesNotThrow(() => { parser({ plugins: { module: { @@ -225,9 +225,9 @@ describe('config', () => { }, }, }) - }).to.not.throw() + }) - expect(() => { + doesNotThrow(() => { parser({ plugins: { module: { @@ -239,9 +239,9 @@ describe('config', () => { }, }, }) - }).to.not.throw() + }) - expect(() => { + doesNotThrow(() => { parser({ plugins: { module: { @@ -254,10 +254,10 @@ describe('config', () => { }, }, }) - }).to.not.throw() + }) // allow complex config object - expect(() => { + doesNotThrow(() => { parser({ plugins: { module: { @@ -271,34 +271,34 @@ describe('config', () => { }, }, }) - }).to.not.throw() + }) }) describe('should throw on invalid values for plugins', () => { it('should throw if plugins is a string', () => { - return expect(() => { + throws(() => { parser({ - // @ts-ignore (this is what we want to test) + // @ts-expect-error plugins: 'this is a string instead of an object', }) - }).to.throw() + }) }) it('should throw if plugins is not an object of plugins', () => { - return expect(() => { + throws(() => { parser({ plugins: { - // @ts-ignore (this is what we want to test) + // @ts-expect-error order: 42, - // @ts-ignore (this is what we want to test) + // @ts-expect-error name: 'module', }, }) - }).to.throw() + }) }) it('should throw if the "module" property is missing', () => { - expect(() => { + throws(() => { parser({ plugins: { module: { @@ -306,7 +306,7 @@ describe('config', () => { }, }, }) - }).to.throw() + }) }) }) }) diff --git a/packages/core/test/plugins/health.test.js b/packages/core/test/plugins/health.test.js index a6a1cdce..ef50fb2b 100644 --- a/packages/core/test/plugins/health.test.js +++ b/packages/core/test/plugins/health.test.js @@ -1,8 +1,8 @@ // @ts-check +import { describe, it } from 'node:test' import { strictEqual } from 'node:assert' -import { describe, it } from 'mocha' import trifidCore, { getListenerURL } from '../../index.js' import healthPlugin from '../../plugins/health.js' @@ -11,7 +11,7 @@ const createTrifidInstance = async () => { return await trifidCore({ server: { listener: { - port: 4242, + port: 0, }, logLevel: 'warn', }, diff --git a/packages/core/test/plugins/redirect.test.js b/packages/core/test/plugins/redirect.test.js index d6e05323..bcfe2029 100644 --- a/packages/core/test/plugins/redirect.test.js +++ b/packages/core/test/plugins/redirect.test.js @@ -1,8 +1,8 @@ // @ts-check +import { describe, it } from 'node:test' import { strictEqual } from 'node:assert' -import { describe, it } from 'mocha' import trifidCore, { getListenerURL, assertRejection } from '../../index.js' import redirectPlugin from '../../plugins/redirect.js' @@ -11,7 +11,7 @@ const createTrifidInstance = async (config) => { return await trifidCore({ server: { listener: { - port: 4242, + port: 0, }, logLevel: 'warn', }, @@ -26,6 +26,7 @@ const createTrifidInstance = async (config) => { describe('redirect plugin', () => { it('should throw if the target parameter is not set', () => { + // @ts-expect-error assertRejection(redirectPlugin({ config: {}, logger: { debug: (/** @type {any} */ _) => { } } })) }) diff --git a/packages/core/test/plugins/static.test.js b/packages/core/test/plugins/static.test.js index 765a80a4..8a14568a 100644 --- a/packages/core/test/plugins/static.test.js +++ b/packages/core/test/plugins/static.test.js @@ -1,10 +1,10 @@ // @ts-check +import { describe, it } from 'node:test' import { strictEqual } from 'node:assert' import { dirname } from 'node:path' import { fileURLToPath } from 'node:url' -import { describe, it } from 'mocha' import trifidCore, { getListenerURL, assertRejection } from '../../index.js' import staticPlugin from '../../plugins/static.js' @@ -15,7 +15,7 @@ const createTrifidInstance = async (config, paths) => { return await trifidCore({ server: { listener: { - port: 4242, + port: 0, }, logLevel: 'warn', }, diff --git a/packages/core/test/plugins/throw.test.js b/packages/core/test/plugins/throw.test.js index 1b880d54..08f1a3ba 100644 --- a/packages/core/test/plugins/throw.test.js +++ b/packages/core/test/plugins/throw.test.js @@ -1,8 +1,8 @@ // @ts-check +import { describe, it } from 'node:test' import { strictEqual } from 'node:assert' -import { describe, it } from 'mocha' import trifidCore, { getListenerURL } from '../../index.js' import throwPlugin from '../../plugins/throw.js' @@ -11,7 +11,7 @@ const createTrifidInstance = async (config) => { return await trifidCore({ server: { listener: { - port: 4242, + port: 0, }, logLevel: 'warn', }, diff --git a/packages/core/test/plugins/view.test.js b/packages/core/test/plugins/view.test.js index 9295e8b3..e9a7abb9 100644 --- a/packages/core/test/plugins/view.test.js +++ b/packages/core/test/plugins/view.test.js @@ -1,10 +1,10 @@ // @ts-check +import { describe, it } from 'node:test' import { strictEqual, match } from 'node:assert' - import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' -import { describe, it } from 'mocha' + import trifidCore, { assertRejection, getListenerURL } from '../../index.js' import viewPlugin from '../../plugins/view.js' @@ -15,7 +15,7 @@ const createTrifidInstance = async (config) => { return await trifidCore({ server: { listener: { - port: 4242, + port: 0, }, logLevel: 'warn', }, @@ -30,11 +30,13 @@ const createTrifidInstance = async (config) => { describe('view plugin', () => { it('should throw if the path parameter is not set', () => { + // @ts-expect-error assertRejection(viewPlugin({})) }) it('should throw if the path parameter is set to an empty string', () => { assertRejection(viewPlugin({ + // @ts-expect-error path: '', })) }) diff --git a/packages/core/test/resolvers.test.js b/packages/core/test/resolvers.test.js index 056653ca..43315bfa 100644 --- a/packages/core/test/resolvers.test.js +++ b/packages/core/test/resolvers.test.js @@ -1,7 +1,7 @@ // @ts-check -import { describe, it } from 'mocha' -import { expect } from 'chai' +import { describe, it } from 'node:test' +import { equal } from 'node:assert' import { cwdCallback, @@ -17,168 +17,188 @@ describe('resolvers', () => { it('should be able to resolve an environment variable', () => { process.env.TEST_VARIABLE = 'test' - expect(envCallback('TEST_VARIABLE')).to.equal('test') + equal(envCallback('TEST_VARIABLE'), 'test') delete process.env.TEST_VARIABLE }) it('should return an empty string on non-existant environment variables', () => { delete process.env.TEST_VARIABLE - expect(envCallback('TEST_VARIABLE')).to.equal('') + equal(envCallback('TEST_VARIABLE'), '') }) it('env should not resolve to anything if it is another prefix', () => { - expect(envResolver('something:TEST_VARIABLE')).to.equal( + equal( + envResolver('something:TEST_VARIABLE'), 'something:TEST_VARIABLE', ) }) it('env should resolve with the right prefix', () => { process.env.TEST_VARIABLE = 'test' - expect(envResolver('env:TEST_VARIABLE')).to.equal('test') + equal(envResolver('env:TEST_VARIABLE'), 'test') delete process.env.TEST_VARIABLE }) it('env should resolve to empty string for non-existant variable with the right prefix', () => { delete process.env.TEST_VARIABLE - expect(envResolver('env:TEST_VARIABLE')).to.equal('') + equal(envResolver('env:TEST_VARIABLE'), '') }) // Current working directory resolver it('should return the current working directory', () => { - expect(cwdCallback('.')).to.equal(process.cwd()) + equal(cwdCallback('.'), process.cwd()) }) it('cwd should be able to resolve paths', () => { - expect(cwdCallback('./test.js')).to.equal(`${process.cwd()}/test.js`) - expect(cwdCallback('test.js')).to.equal(`${process.cwd()}/test.js`) - expect(cwdCallback('././././test.js')).to.equal(`${process.cwd()}/test.js`) - expect(cwdCallback('./a/.././test.js')).to.equal(`${process.cwd()}/test.js`) - expect(cwdCallback('/test.js')).to.equal('/test.js') - expect(cwdCallback('/a/b/c/test.js')).to.equal('/a/b/c/test.js') + equal(cwdCallback('./test.js'), `${process.cwd()}/test.js`) + equal(cwdCallback('test.js'), `${process.cwd()}/test.js`) + equal(cwdCallback('././././test.js'), `${process.cwd()}/test.js`) + equal(cwdCallback('./a/.././test.js'), `${process.cwd()}/test.js`) + equal(cwdCallback('/test.js'), '/test.js') + equal(cwdCallback('/a/b/c/test.js'), '/a/b/c/test.js') }) it('cwd resolver should not resolve on other prefix', () => { - expect(cwdResolver('something:test.js')).to.equal('something:test.js') + equal(cwdResolver('something:test.js'), 'something:test.js') }) it('cwd resolver should resolve on the cwd prefix', () => { - expect(cwdResolver('cwd:test.js')).to.equal(`${process.cwd()}/test.js`) + equal(cwdResolver('cwd:test.js'), `${process.cwd()}/test.js`) }) it('cwd resolver should give the same results than the callback', () => { - expect(cwdResolver('cwd:.')).to.equal(process.cwd()) - expect(cwdResolver('cwd:./test.js')).to.equal(`${process.cwd()}/test.js`) - expect(cwdResolver('cwd:test.js')).to.equal(`${process.cwd()}/test.js`) - expect(cwdResolver('cwd:././././test.js')).to.equal( + equal(cwdResolver('cwd:.'), process.cwd()) + equal(cwdResolver('cwd:./test.js'), `${process.cwd()}/test.js`) + equal(cwdResolver('cwd:test.js'), `${process.cwd()}/test.js`) + equal( + cwdResolver('cwd:././././test.js'), `${process.cwd()}/test.js`, ) - expect(cwdResolver('cwd:./a/.././test.js')).to.equal( + equal( + cwdResolver('cwd:./a/.././test.js'), `${process.cwd()}/test.js`, ) - expect(cwdResolver('cwd:/test.js')).to.equal('/test.js') - expect(cwdResolver('cwd:/a/b/c/test.js')).to.equal('/a/b/c/test.js') + equal(cwdResolver('cwd:/test.js'), '/test.js') + equal(cwdResolver('cwd:/a/b/c/test.js'), '/a/b/c/test.js') }) // File resolver it('file callback should behave the same as cwd if no base is defined', () => { - expect(fileCallback()('.')).to.equal(process.cwd()) - expect(fileCallback()('./test.js')).to.equal(`${process.cwd()}/test.js`) - expect(fileCallback()('test.js')).to.equal(`${process.cwd()}/test.js`) - expect(fileCallback()('././././test.js')).to.equal( + equal(fileCallback()('.'), process.cwd()) + equal(fileCallback()('./test.js'), `${process.cwd()}/test.js`) + equal(fileCallback()('test.js'), `${process.cwd()}/test.js`) + equal( + fileCallback()('././././test.js'), `${process.cwd()}/test.js`, ) - expect(fileCallback()('./a/.././test.js')).to.equal( + equal( + fileCallback()('./a/.././test.js'), `${process.cwd()}/test.js`, ) - expect(fileCallback()('/test.js')).to.equal('/test.js') - expect(fileCallback()('/a/b/c/test.js')).to.equal('/a/b/c/test.js') + equal(fileCallback()('/test.js'), '/test.js') + equal(fileCallback()('/a/b/c/test.js'), '/a/b/c/test.js') // test with explicit 'undefined' base - expect(fileCallback(undefined)('.')).to.equal(process.cwd()) - expect(fileCallback(undefined)('./test.js')).to.equal( + equal(fileCallback(undefined)('.'), process.cwd()) + equal( + fileCallback(undefined)('./test.js'), `${process.cwd()}/test.js`, ) - expect(fileCallback(undefined)('test.js')).to.equal( + equal( + fileCallback(undefined)('test.js'), `${process.cwd()}/test.js`, ) - expect(fileCallback(undefined)('././././test.js')).to.equal( + equal( + fileCallback(undefined)('././././test.js'), `${process.cwd()}/test.js`, ) - expect(fileCallback(undefined)('./a/.././test.js')).to.equal( + equal( + fileCallback(undefined)('./a/.././test.js'), `${process.cwd()}/test.js`, ) - expect(fileCallback(undefined)('/test.js')).to.equal('/test.js') - expect(fileCallback(undefined)('/a/b/c/test.js')).to.equal('/a/b/c/test.js') + equal(fileCallback(undefined)('/test.js'), '/test.js') + equal(fileCallback(undefined)('/a/b/c/test.js'), '/a/b/c/test.js') }) it('file callback should resolve as expected with the specified base', () => { - expect(fileCallback('/path/test')('.')).to.equal('/path/test') - expect(fileCallback('/path/test')('..')).to.equal('/path') + equal(fileCallback('/path/test')('.'), '/path/test') + equal(fileCallback('/path/test')('..'), '/path') // note the '/' at the end - expect(fileCallback('/path/test')('../')).to.equal('/path/') - - expect(fileCallback('/path/test')('../..')).to.equal('/') - expect(fileCallback('/path/test')('../../')).to.equal('/') - expect(fileCallback('/path/test')('../../..')).to.equal('/') - expect(fileCallback('/path/test')('../../../')).to.equal('/') - expect(fileCallback('/path/test')('./test.js')).to.equal( + equal(fileCallback('/path/test')('../'), '/path/') + + equal(fileCallback('/path/test')('../..'), '/') + equal(fileCallback('/path/test')('../../'), '/') + equal(fileCallback('/path/test')('../../..'), '/') + equal(fileCallback('/path/test')('../../../'), '/') + equal( + fileCallback('/path/test')('./test.js'), '/path/test/test.js', ) - expect(fileCallback('/path/test')('test.js')).to.equal('/path/test/test.js') - expect(fileCallback('/path/test')('././././test.js')).to.equal( + equal(fileCallback('/path/test')('test.js'), '/path/test/test.js') + equal( + fileCallback('/path/test')('././././test.js'), '/path/test/test.js', ) - expect(fileCallback('/path/test')('./a/.././test.js')).to.equal( + equal( + fileCallback('/path/test')('./a/.././test.js'), '/path/test/test.js', ) - expect(fileCallback('/path/test')('/test.js')).to.equal('/test.js') - expect(fileCallback('/path/test')('/a/b/c/test.js')).to.equal( + equal(fileCallback('/path/test')('/test.js'), '/test.js') + equal( + fileCallback('/path/test')('/a/b/c/test.js'), '/a/b/c/test.js', ) }) it('file resolver should not resolve on other prefix', () => { - expect(fileResolver('something:test.js')).to.equal('something:test.js') + equal(fileResolver('something:test.js'), 'something:test.js') }) it('file resolver should resolve on the file prefix', () => { - expect(fileResolver('file:test.js')).to.equal(`${process.cwd()}/test.js`) - expect(fileResolver('file:test.js', undefined)).to.equal( + equal(fileResolver('file:test.js'), `${process.cwd()}/test.js`) + equal( + fileResolver('file:test.js', undefined), `${process.cwd()}/test.js`, ) - expect(fileResolver('file:test.js', '/path/test')).to.equal( + equal( + fileResolver('file:test.js', '/path/test'), '/path/test/test.js', ) }) it('file resolver should behave the same as the file callback', () => { - expect(fileResolver('file:.', '/path/test')).to.equal('/path/test') - expect(fileResolver('file:..', '/path/test')).to.equal('/path') + equal(fileResolver('file:.', '/path/test'), '/path/test') + equal(fileResolver('file:..', '/path/test'), '/path') // note the '/' at the end - expect(fileResolver('file:../', '/path/test')).to.equal('/path/') - - expect(fileResolver('file:../..', '/path/test')).to.equal('/') - expect(fileResolver('file:../../', '/path/test')).to.equal('/') - expect(fileResolver('file:../../..', '/path/test')).to.equal('/') - expect(fileResolver('file:../../../', '/path/test')).to.equal('/') - expect(fileResolver('file:./test.js', '/path/test')).to.equal( + equal(fileResolver('file:../', '/path/test'), '/path/') + + equal(fileResolver('file:../..', '/path/test'), '/') + equal(fileResolver('file:../../', '/path/test'), '/') + equal(fileResolver('file:../../..', '/path/test'), '/') + equal(fileResolver('file:../../../', '/path/test'), '/') + equal( + fileResolver('file:./test.js', '/path/test'), '/path/test/test.js', ) - expect(fileResolver('file:test.js', '/path/test')).to.equal( + equal( + fileResolver('file:test.js', '/path/test'), '/path/test/test.js', ) - expect(fileResolver('file:././././test.js', '/path/test')).to.equal( + equal( + fileResolver('file:././././test.js', '/path/test'), '/path/test/test.js', ) - expect(fileResolver('file:./a/.././test.js', '/path/test')).to.equal( + equal( + fileResolver('file:./a/.././test.js', '/path/test'), '/path/test/test.js', ) - expect(fileResolver('file:/test.js', '/path/test')).to.equal('/test.js') - expect(fileResolver('file:/a/b/c/test.js', '/path/test')).to.equal( + equal(fileResolver('file:/test.js', '/path/test'), '/test.js') + equal( + fileResolver('file:/a/b/c/test.js', '/path/test'), '/a/b/c/test.js', ) }) From d3bc56fa02737a8ff2febf7ac8404e9ee1578184 Mon Sep 17 00:00:00 2001 From: Ludovic Muller Date: Wed, 27 Nov 2024 19:32:39 +0100 Subject: [PATCH 3/6] sparql-proxy: add support for multiple endpoints --- .changeset/tall-plums-type.md | 5 ++ packages/sparql-proxy/README.md | 13 ++++ packages/sparql-proxy/index.js | 111 ++++++++++++++++++++++++++------ 3 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 .changeset/tall-plums-type.md diff --git a/.changeset/tall-plums-type.md b/.changeset/tall-plums-type.md new file mode 100644 index 00000000..2068717e --- /dev/null +++ b/.changeset/tall-plums-type.md @@ -0,0 +1,5 @@ +--- +"@zazuko/trifid-plugin-sparql-proxy": minor +--- + +Add support for multiple endpoints diff --git a/packages/sparql-proxy/README.md b/packages/sparql-proxy/README.md index 12ccb9a6..3c70ce49 100644 --- a/packages/sparql-proxy/README.md +++ b/packages/sparql-proxy/README.md @@ -24,6 +24,19 @@ plugins: username: admin password: secret + # # In case you want to add support for multiple endpoints + # # Define the endpoints in the following way (the "default" endpoint is required) + # # The default endpoint value will override the endpointUrl, username and password values + # endpoints: + # default: + # endpointUrl: https://example.com/query + # username: admin1 + # password: secret2 + # other: + # endpointUrl: https://example.com/other-query + # username: admin2 + # password: secret2 + # Rewriting configuration allowRewriteToggle: true # Allow the user to toggle the rewrite configuration using the `rewrite` query parameter, even if `rewrite` is set to false rewrite: false # Rewrite by default diff --git a/packages/sparql-proxy/index.js b/packages/sparql-proxy/index.js index 43a0be06..6892cea3 100644 --- a/packages/sparql-proxy/index.js +++ b/packages/sparql-proxy/index.js @@ -27,20 +27,39 @@ const defaultConfiguration = { serviceDescriptionFormat: undefined, // override the accept header for the service description request. by default, will use content negotiation using formats `@zazuko/env-node` can parse } +const oneMonthMilliseconds = 60 * 60 * 24 * 30 * 1000 +const DEFAULT_ENDPOINT_NAME = 'default' + /** @type {import('trifid-core/types').TrifidPlugin} */ const factory = async (trifid) => { const { logger, config, trifidEvents } = trifid + const endpoints = new Map() + const options = { ...defaultConfiguration, ...config } let dynamicEndpoints = false if (objectLength(options.endpoints) > 0) { + // Check if the default endpoint is defined + if (!Object.hasOwnProperty.call(options.endpoints, DEFAULT_ENDPOINT_NAME)) { + throw Error('Missing default endpoint in the endpoints configuration') + } + + // Override default values with the default endpoint values + options.endpointUrl = options.endpoints.default.url || '' + options.username = options.endpoints.default.username || '' + options.password = options.endpoints.default.password || '' + // Support for multiple endpoints dynamicEndpoints = true } if (!options.endpointUrl) { - throw Error('Missing endpointUrl parameter') + throw Error( + dynamicEndpoints + ? `Missing endpoints.${DEFAULT_ENDPOINT_NAME}.url parameter` + : 'Missing endpointUrl parameter', + ) } let authorizationHeader @@ -53,6 +72,48 @@ const factory = async (trifid) => { const rewriteConfigValue = options.rewrite const rewriteConfig = sparqlGetRewriteConfiguration(rewriteConfigValue, datasetBaseUrl) + endpoints.set(DEFAULT_ENDPOINT_NAME, { + endpointUrl: options.endpointUrl, + username: options.username, + password: options.password, + authorizationHeader, + datasetBaseUrl, + allowRewriteToggle, + rewriteConfigValue, + rewriteConfig, + }) + + if (dynamicEndpoints) { + for (const [endpointName, endpointConfig] of Object.entries(options.endpoints)) { + if (endpointName === DEFAULT_ENDPOINT_NAME) { + continue + } + + if (!endpointConfig.url) { + throw Error(`Missing endpoints.${endpointName}.url parameter`) + } + + let endpointAuthorizationHeader + if (endpointConfig.username && endpointConfig.password) { + endpointAuthorizationHeader = authBasicHeader(endpointConfig.username, endpointConfig.password) + } + + const endpointDatasetBaseUrl = endpointConfig.datasetBaseUrl || datasetBaseUrl + const endpointRewriteConfigValue = endpointConfig.rewrite ?? rewriteConfigValue + + endpoints.set(endpointName, { + endpointUrl: endpointConfig.url || '', + username: endpointConfig.username || '', + password: endpointConfig.password || '', + authorizationHeader: endpointAuthorizationHeader, + datasetBaseUrl: endpointDatasetBaseUrl, + allowRewriteToggle: endpointConfig.allowRewriteToggle ?? allowRewriteToggle, + rewriteConfigValue: endpointRewriteConfigValue, + rewriteConfig: sparqlGetRewriteConfiguration(endpointRewriteConfigValue, endpointDatasetBaseUrl), + }) + } + } + const queryLogLevel = options.queryLogLevel if (!logger[queryLogLevel]) { throw Error(`Invalid queryLogLevel: ${queryLogLevel}`) @@ -124,6 +185,7 @@ const factory = async (trifid) => { * @property {string} [query] The SPARQL query. * @property {string} [rewrite] Should the query and the results be rewritten? * @property {string} [format] The format of the results. + * @property {string} [endpoint] The name of the endpoint to use (default: DEFAULT_ENDPOINT_NAME). */ /** @@ -134,10 +196,22 @@ const factory = async (trifid) => { /** * Route handler. - * @param {import('fastify').FastifyRequest<{ Querystring: QueryString, Body: RequestBody | string }> & { accepts: () => { type: (types: string[]) => string[] | string | false }}} request Request. - * @param {import('fastify').FastifyReply} reply Reply. + * @param {import('fastify').FastifyRequest<{ Querystring: QueryString, Body: RequestBody | string }> & { cookies: { endpointName?: string }, accepts: () => { type: (types: string[]) => string[] | string | false }}} request Request. + * @param {import('fastify').FastifyReply & { setCookie: (name: string, value: string, opts?: any) => {}}} reply Reply. */ const handler = async (request, reply) => { + const savedEndpointName = request.cookies.endpointName || DEFAULT_ENDPOINT_NAME + const endpointName = request.query.endpoint || savedEndpointName + + // TODO: only set the cookie if the endpointName is different from the saved one and if it is not the default one + reply.setCookie('endpointName', endpointName, { maxAge: oneMonthMilliseconds, path: '/' }) + + const endpoint = endpoints.get(endpointName) + if (!endpoint) { + return reply.callNotFound() + } + logger.debug(`Using endpoint: ${endpointName}`) + let requestPort = '' if (request.port) { requestPort = `:${request.port}` @@ -151,6 +225,13 @@ const factory = async (trifid) => { fullUrlObject.searchParams.forEach((_value, key) => fullUrlObject.searchParams.delete(key)) const iriUrlString = fullUrlObject.toString() + // Enforce non-trailing slash + if (fullUrlPathname.slice(-1) === '/') { + reply.redirect(`${fullUrlPathname.slice(0, -1)}`) + return reply + } + + // Handle Service Description request if (Object.keys(request.query).length === 0 && request.method === 'GET') { const dataset = rdf.dataset(await serviceDescription) rdf.clownface({ dataset }) @@ -172,28 +253,20 @@ const factory = async (trifid) => { return reply } - // Enforce non-trailing slash - if (fullUrlPathname.slice(-1) === '/') { - reply.redirect(`${fullUrlPathname.slice(0, -1)}`) - return reply - } - - let currentRewriteConfig = rewriteConfig - if (allowRewriteToggle) { - let rewriteConfigValueFromQuery = rewriteConfigValue + let currentRewriteConfig = endpoint.rewriteConfig + if (endpoint.allowRewriteToggle) { + let rewriteConfigValueFromQuery = endpoint.rewriteConfigValue if (`${request.query.rewrite}` === 'false') { rewriteConfigValueFromQuery = false } else if (`${request.query.rewrite}` === 'true') { rewriteConfigValueFromQuery = true - } else { - rewriteConfigValueFromQuery = rewriteConfigValue } - currentRewriteConfig = sparqlGetRewriteConfiguration(rewriteConfigValueFromQuery, datasetBaseUrl) + currentRewriteConfig = sparqlGetRewriteConfiguration(rewriteConfigValueFromQuery, endpoint.datasetBaseUrl) } const { rewrite: rewriteValue, iriOrigin } = currentRewriteConfig const rewriteResponse = rewriteValue ? { - origin: datasetBaseUrl, + origin: endpoint.datasetBaseUrl, replacement: iriOrigin(iriUrlString), } : false @@ -239,12 +312,12 @@ const factory = async (trifid) => { 'Content-Type': 'application/x-www-form-urlencoded', Accept: acceptHeader, } - if (authorizationHeader) { - headers.Authorization = authorizationHeader + if (endpoint.authorizationHeader) { + headers.Authorization = endpoint.authorizationHeader } const start = performance.now() - let response = await fetch(options.endpointUrl, { + let response = await fetch(endpoint.endpointUrl, { method: 'POST', headers, body: new URLSearchParams({ query }), From ad03be1abda88078b16ab5871c5321a15c0cb462 Mon Sep 17 00:00:00 2001 From: Ludovic Muller Date: Wed, 27 Nov 2024 19:38:03 +0100 Subject: [PATCH 4/6] sparql-proxy: fix tests --- packages/sparql-proxy/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/sparql-proxy/index.js b/packages/sparql-proxy/index.js index 6892cea3..b2d26bbc 100644 --- a/packages/sparql-proxy/index.js +++ b/packages/sparql-proxy/index.js @@ -225,12 +225,6 @@ const factory = async (trifid) => { fullUrlObject.searchParams.forEach((_value, key) => fullUrlObject.searchParams.delete(key)) const iriUrlString = fullUrlObject.toString() - // Enforce non-trailing slash - if (fullUrlPathname.slice(-1) === '/') { - reply.redirect(`${fullUrlPathname.slice(0, -1)}`) - return reply - } - // Handle Service Description request if (Object.keys(request.query).length === 0 && request.method === 'GET') { const dataset = rdf.dataset(await serviceDescription) @@ -253,6 +247,12 @@ const factory = async (trifid) => { return reply } + // Enforce non-trailing slash + if (fullUrlPathname.slice(-1) === '/') { + reply.redirect(`${fullUrlPathname.slice(0, -1)}`) + return reply + } + let currentRewriteConfig = endpoint.rewriteConfig if (endpoint.allowRewriteToggle) { let rewriteConfigValueFromQuery = endpoint.rewriteConfigValue From b53af4e768f714211b3541f1252f94a9e3e825fb Mon Sep 17 00:00:00 2001 From: Ludovic Muller Date: Tue, 3 Dec 2024 13:22:35 +0100 Subject: [PATCH 5/6] entity-renderer: add allowEndpointSwitch option --- .changeset/loud-guests-attack.md | 5 +++++ packages/entity-renderer/README.md | 4 ++++ packages/entity-renderer/index.js | 15 ++++++++++++--- packages/sparql-proxy/index.js | 11 ++++++++--- 4 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 .changeset/loud-guests-attack.md diff --git a/.changeset/loud-guests-attack.md b/.changeset/loud-guests-attack.md new file mode 100644 index 00000000..a35bef67 --- /dev/null +++ b/.changeset/loud-guests-attack.md @@ -0,0 +1,5 @@ +--- +"@zazuko/trifid-entity-renderer": minor +--- + +Add `allowEndpointSwitch` configuration option in order to allow the use of another whitelisted endpoint by the sparql-proxy plugin. diff --git a/packages/entity-renderer/README.md b/packages/entity-renderer/README.md index b1ece937..1e823929 100644 --- a/packages/entity-renderer/README.md +++ b/packages/entity-renderer/README.md @@ -98,6 +98,10 @@ The default redirect query supports `http://www.w3.org/2011/http#` and `http://w If enabled, the user can still disable this behavior by either: - setting the `disableSchemaUrlRedirect` query parameter to `true` - setting the `x-disable-schema-url-redirect` header to `true` +- `allowEndpointSwitch`: If set to `true`, the plugin will allow the user to switch the endpoint by setting the `endpoint` query parameter. + This will inject a `endpointName` cookie while querying the SPARQL endpoint and is meant to be used with the `sparql-proxy` Trifid plugin. + The default value is `false`. + This option is experimental and might change or be removed in the future. ## Run an example instance diff --git a/packages/entity-renderer/index.js b/packages/entity-renderer/index.js index 399e2720..3c1b41a7 100644 --- a/packages/entity-renderer/index.js +++ b/packages/entity-renderer/index.js @@ -11,6 +11,8 @@ import { createMetadataProvider } from './renderer/metadata.js' const currentDir = dirname(fileURLToPath(import.meta.url)) +const DEFAULT_ENDPOINT_NAME = 'default' + const getAcceptHeader = (req) => { const queryStringValue = req.query.format @@ -104,6 +106,7 @@ const defaultConfiguration = { `, followRedirects: false, enableSchemaUrlRedirect: false, // Experimental + allowEndpointSwitch: false, // Experimental } const fixContentTypeHeader = (contentType) => { @@ -116,7 +119,8 @@ const factory = async (trifid) => { const entityRenderer = createEntityRenderer({ options: config, logger, query }) const metadataProvider = createMetadataProvider({ options: config }) - const { path, ignorePaths, rewrite: rewriteConfigValue, datasetBaseUrl } = config + const { path, ignorePaths, rewrite: rewriteConfigValue, datasetBaseUrl, allowEndpointSwitch: allowEndpointSwitchConfigValue } = config + const allowEndpointSwitch = `${allowEndpointSwitchConfigValue}` === 'true' const entityTemplatePath = path || `${currentDir}/views/render.hbs` const rewriteConfig = sparqlGetRewriteConfiguration(rewriteConfigValue, datasetBaseUrl) const { rewrite: rewriteValue, replaceIri, iriOrigin } = rewriteConfig @@ -171,9 +175,13 @@ const factory = async (trifid) => { return reply } - // To avoid any languge issues, we will forward the i18n cookie to the SPARQL endpoint + // Get the endpoint name from the query parameter or from the cookie (if allowed) + const savedEndpointName = request.cookies.endpointName || DEFAULT_ENDPOINT_NAME + const endpointName = request.query.endpoint || savedEndpointName + + // To avoid any languge issues, we will forward the i18n cookie to the SPARQL endpoint + add the endpointName cookie const queryHeaders = { - cookie: `i18n=${request.session.get('currentLanguage') || 'en'}; Path=/; SameSite=Lax; Secure; HttpOnly`, + cookie: `i18n=${request.session.get('currentLanguage') || 'en'}${allowEndpointSwitch && endpointName !== DEFAULT_ENDPOINT_NAME ? `; endpointName=${endpointName}` : ''}`, } // Get the expected format from the Accept header or from the `format` query parameter @@ -220,6 +228,7 @@ const factory = async (trifid) => { ask: false, select: true, // Force the parsing of the response rewriteResponse, + headers: queryHeaders, }) if (redirect.length > 0) { const entityRedirect = redirect[0] diff --git a/packages/sparql-proxy/index.js b/packages/sparql-proxy/index.js index b2d26bbc..bcc4f648 100644 --- a/packages/sparql-proxy/index.js +++ b/packages/sparql-proxy/index.js @@ -197,14 +197,19 @@ const factory = async (trifid) => { /** * Route handler. * @param {import('fastify').FastifyRequest<{ Querystring: QueryString, Body: RequestBody | string }> & { cookies: { endpointName?: string }, accepts: () => { type: (types: string[]) => string[] | string | false }}} request Request. - * @param {import('fastify').FastifyReply & { setCookie: (name: string, value: string, opts?: any) => {}}} reply Reply. + * @param {import('fastify').FastifyReply & { setCookie: (name: string, value: string, opts?: any) => {}, clearCookie: (name: string, opts?: any) => {}}} reply Reply. */ const handler = async (request, reply) => { const savedEndpointName = request.cookies.endpointName || DEFAULT_ENDPOINT_NAME const endpointName = request.query.endpoint || savedEndpointName - // TODO: only set the cookie if the endpointName is different from the saved one and if it is not the default one - reply.setCookie('endpointName', endpointName, { maxAge: oneMonthMilliseconds, path: '/' }) + // Only set the cookie if the endpoint name has changed and if it's not the default endpoint + if (request.cookies.endpointName !== endpointName && endpointName !== DEFAULT_ENDPOINT_NAME) { + reply.setCookie('endpointName', endpointName, { maxAge: oneMonthMilliseconds, path: '/' }) + // Clear the cookie if the endpoint name is the default one + } else if (endpointName === DEFAULT_ENDPOINT_NAME && request.cookies.endpointName !== undefined) { + reply.clearCookie('endpointName', { path: '/' }) + } const endpoint = endpoints.get(endpointName) if (!endpoint) { From a989dcdbd9550b21f97d4fe92ecb6d0d5402a098 Mon Sep 17 00:00:00 2001 From: Ludovic Muller Date: Tue, 3 Dec 2024 13:37:22 +0100 Subject: [PATCH 6/6] chore: only allow alnum- chars for cookies --- packages/entity-renderer/index.js | 7 +++++-- packages/sparql-proxy/index.js | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/entity-renderer/index.js b/packages/entity-renderer/index.js index 3c1b41a7..4bfb716c 100644 --- a/packages/entity-renderer/index.js +++ b/packages/entity-renderer/index.js @@ -177,11 +177,14 @@ const factory = async (trifid) => { // Get the endpoint name from the query parameter or from the cookie (if allowed) const savedEndpointName = request.cookies.endpointName || DEFAULT_ENDPOINT_NAME - const endpointName = request.query.endpoint || savedEndpointName + let endpointName = request.query.endpoint || savedEndpointName + endpointName = endpointName.replace(/[^a-z0-9-]/gi, '') + + const i18nValue = `${request.session.get('currentLanguage') || 'en'}`.replace(/[^a-z0-9-]/gi, '') // To avoid any languge issues, we will forward the i18n cookie to the SPARQL endpoint + add the endpointName cookie const queryHeaders = { - cookie: `i18n=${request.session.get('currentLanguage') || 'en'}${allowEndpointSwitch && endpointName !== DEFAULT_ENDPOINT_NAME ? `; endpointName=${endpointName}` : ''}`, + cookie: `i18n=${i18nValue}${allowEndpointSwitch && endpointName !== DEFAULT_ENDPOINT_NAME ? `; endpointName=${endpointName}` : ''}`, } // Get the expected format from the Accept header or from the `format` query parameter diff --git a/packages/sparql-proxy/index.js b/packages/sparql-proxy/index.js index bcc4f648..6b79ef1a 100644 --- a/packages/sparql-proxy/index.js +++ b/packages/sparql-proxy/index.js @@ -201,7 +201,8 @@ const factory = async (trifid) => { */ const handler = async (request, reply) => { const savedEndpointName = request.cookies.endpointName || DEFAULT_ENDPOINT_NAME - const endpointName = request.query.endpoint || savedEndpointName + let endpointName = request.query.endpoint || savedEndpointName + endpointName = endpointName.replace(/[^a-z0-9-]/gi, '') // Only set the cookie if the endpoint name has changed and if it's not the default endpoint if (request.cookies.endpointName !== endpointName && endpointName !== DEFAULT_ENDPOINT_NAME) {