From 7df0ff33048f6a182517fb21223577fb1fe2e72a Mon Sep 17 00:00:00 2001 From: Michiel Date: Mon, 31 Aug 2026 09:32:59 +0000 Subject: [PATCH 01/15] You shall be TS tests! --- .eslintrc | 6 +- package.json | 7 +- ...t.js => LinkedDataFragmentsServer-test.ts} | 137 ++--- ...oller-test.js => AssetsController-test.ts} | 20 +- ...{Controller-test.js => Controller-test.ts} | 88 ++-- ...-test.js => DereferenceController-test.ts} | 22 +- ...ler-test.js => NotFoundController-test.ts} | 69 +-- ...{Datasource-test.js => Datasource-test.ts} | 84 +-- .../test/routers/DatasourceRouter-test.js | 106 ---- .../test/routers/DatasourceRouter-test.ts | 112 ++++ packages/core/test/routers/PageRouter-test.js | 172 ------- packages/core/test/routers/PageRouter-test.ts | 179 +++++++ .../test/views/{View-test.js => View-test.ts} | 70 ++- ...lection-test.js => ViewCollection-test.ts} | 62 ++- ...ce-test.js => CompositeDatasource-test.ts} | 75 +-- ...tasource-test.js => HdtDatasource-test.ts} | 47 +- ...ource-test.js => JsonLdDatasource-test.ts} | 26 +- ...atasource-test.js => N3Datasource-test.ts} | 26 +- ...asource-test.js => RdfaDatasource-test.ts} | 26 +- ...ource-test.js => SparqlDatasource-test.ts} | 85 +-- ...=> QuadPatternFragmentsController-test.ts} | 225 ++++---- .../test/routers/QuadPatternRouter-test.js | 475 ----------------- .../test/routers/QuadPatternRouter-test.ts | 482 ++++++++++++++++++ ...js => QuadPatternFragmentsRdfView-test.ts} | 76 +-- ...ller-test.js => SummaryController-test.ts} | 19 +- test/DummyServer.js | 21 - test/DummyServer.ts | 31 ++ test/test-helpers.js | 44 -- test/test-helpers.ts | 75 +++ tsconfig.typecheck.json | 17 + vitest.config.mts | 2 +- yarn.lock | 59 +-- 32 files changed, 1595 insertions(+), 1350 deletions(-) rename packages/core/test/{LinkedDataFragmentsServer-test.js => LinkedDataFragmentsServer-test.ts} (61%) rename packages/core/test/controllers/{AssetsController-test.js => AssetsController-test.ts} (82%) rename packages/core/test/controllers/{Controller-test.js => Controller-test.ts} (60%) rename packages/core/test/controllers/{DereferenceController-test.js => DereferenceController-test.ts} (73%) rename packages/core/test/controllers/{NotFoundController-test.js => NotFoundController-test.ts} (75%) rename packages/core/test/datasources/{Datasource-test.js => Datasource-test.ts} (79%) delete mode 100644 packages/core/test/routers/DatasourceRouter-test.js create mode 100644 packages/core/test/routers/DatasourceRouter-test.ts delete mode 100644 packages/core/test/routers/PageRouter-test.js create mode 100644 packages/core/test/routers/PageRouter-test.ts rename packages/core/test/views/{View-test.js => View-test.ts} (61%) rename packages/core/test/views/{ViewCollection-test.js => ViewCollection-test.ts} (71%) rename packages/datasource-composite/test/datasources/{CompositeDatasource-test.js => CompositeDatasource-test.ts} (69%) rename packages/datasource-hdt/test/datasources/{HdtDatasource-test.js => HdtDatasource-test.ts} (82%) rename packages/datasource-jsonld/test/datasources/{JsonLdDatasource-test.js => JsonLdDatasource-test.ts} (79%) rename packages/datasource-n3/test/datasources/{N3Datasource-test.js => N3Datasource-test.ts} (77%) rename packages/datasource-rdfa/test/datasources/{RdfaDatasource-test.js => RdfaDatasource-test.ts} (77%) rename packages/datasource-sparql/test/datasources/{SparqlDatasource-test.js => SparqlDatasource-test.ts} (84%) rename packages/feature-qpf/test/controllers/{QuadPatternFragmentsController-test.js => QuadPatternFragmentsController-test.ts} (61%) delete mode 100644 packages/feature-qpf/test/routers/QuadPatternRouter-test.js create mode 100644 packages/feature-qpf/test/routers/QuadPatternRouter-test.ts rename packages/feature-qpf/test/views/quadpatternfragments/{QuadPatternFragmentsRdfView-test.js => QuadPatternFragmentsRdfView-test.ts} (76%) rename packages/feature-summary/test/controllers/{SummaryController-test.js => SummaryController-test.ts} (84%) delete mode 100644 test/DummyServer.js create mode 100644 test/DummyServer.ts delete mode 100644 test/test-helpers.js create mode 100644 test/test-helpers.ts create mode 100644 tsconfig.typecheck.json diff --git a/.eslintrc b/.eslintrc index c2bd0e8b..e5949a14 100644 --- a/.eslintrc +++ b/.eslintrc @@ -193,7 +193,7 @@ overrides: [ { // Specific rules for test files - files: [ "packages/**/test/**/*-test.js" ], + files: [ "packages/**/test/**/*-test.ts" ], plugins: [ "eslint-plugin-promise" ], rules: { "import/no-extraneous-dependencies": "off", @@ -203,13 +203,13 @@ }, { // TypeScript source files - files: [ "packages/**/*.ts" ], + files: [ "packages/**/*.ts", "test/**/*.ts" ], parser: "@typescript-eslint/parser", extends: [ "plugin:@typescript-eslint/recommended-requiring-type-checking" ], parserOptions: { ecmaVersion: 9, sourceType: "module", - project: "./tsconfig.json" + project: "./tsconfig.typecheck.json" }, plugins: [ "@typescript-eslint" ], rules: { diff --git a/package.json b/package.json index 144091de..5dd7932e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@types/parse-cache-control": "^1.0.4", "@types/q": "^1.5.8", "@types/request": "^2.48.13", + "@types/supertest": "^7.2.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@vitest/coverage-v8": "^4.1.10", @@ -28,6 +29,10 @@ "typescript": "^5.9.3", "vitest": "^4.1.10" }, + "// resolutions": "pin @types/node: @types/supertest -> @types/superagent otherwise nests an old @types/node, breaking http.Server typing", + "resolutions": { + "@types/node": "^24.0.0" + }, "pre-commit": [ "lint", "test" @@ -37,7 +42,7 @@ "lint-changed": "lerna run lint --since HEAD", "test": "vitest run --coverage", "test-ci": "vitest run --coverage", - "typecheck": "tsc -p tsconfig.json --noEmit", + "typecheck": "tsc -p tsconfig.typecheck.json", "build": "tsc -p tsconfig.json", "lint": "eslint packages/*/bin/* packages/*/lib packages/*/test test --ext .js,.ts", "clean": "rm -rf ./node_modules && rm -rf ./packages/*/node_modules", diff --git a/packages/core/test/LinkedDataFragmentsServer-test.js b/packages/core/test/LinkedDataFragmentsServer-test.ts similarity index 61% rename from packages/core/test/LinkedDataFragmentsServer-test.js rename to packages/core/test/LinkedDataFragmentsServer-test.ts index e6d26994..8aade5ac 100644 --- a/packages/core/test/LinkedDataFragmentsServer-test.js +++ b/packages/core/test/LinkedDataFragmentsServer-test.ts @@ -1,31 +1,34 @@ /*! @license MIT ©2013-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest'; -let LinkedDataFragmentsServer = require('../lib/LinkedDataFragmentsServer').LinkedDataFragmentsServer; // changed to make tests pass, will be revised in follow up pr +import type { Mock } from 'vitest'; +import { LinkedDataFragmentsServer } from '../lib/LinkedDataFragmentsServer'; +import { Controller } from '../lib/controllers/Controller'; +import type { LdfRequestWithUrl, LdfResponse } from '../index'; -let request = require('supertest'); +import * as request from 'supertest'; describe('LinkedDataFragmentsServer', () => { describe('A LinkedDataFragmentsServer instance with one controller', () => { - let server, controller, client; + let server: ReturnType, controller: Controller, client: ReturnType; + let handleRequestSpy: Mock; beforeAll(() => { - controller = { - handleRequest: vi.fn((request, response, next) => { - switch (request.url) { - case '/handle': - response.end('body contents'); - break; - case '/error': - throw new Error('error message'); - default: - next(); - } - }), - }; + controller = new Controller(); + handleRequestSpy = vi.fn((request: LdfRequestWithUrl, response: LdfResponse, next: (error?: Error) => void) => { + switch (request.url) { + case '/handle': + response.end('body contents'); + break; + case '/error': + throw new Error('error message'); + default: + next(); + } + }); + controller.handleRequest = handleRequestSpy; server = new LinkedDataFragmentsServer({ controllers: [controller], log: vi.fn(), - protocol: 'http', response: { headers: { 'Access-Control-Allow-Origin': '*', @@ -36,7 +39,7 @@ describe('LinkedDataFragmentsServer', () => { client = request.agent(server); }); beforeEach(() => { - controller.handleRequest.mockClear(); + handleRequestSpy.mockClear(); }); it('should send the configured headers', async () => { @@ -47,7 +50,7 @@ describe('LinkedDataFragmentsServer', () => { it('should not allow POST requests', async () => { let response = await client.post('/'); - expect(controller.handleRequest).not.toHaveBeenCalled(); + expect(handleRequestSpy).not.toHaveBeenCalled(); expect(response).toHaveProperty('statusCode', 405); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'The HTTP method "POST" is not allowed; try "GET" instead.'); @@ -55,28 +58,28 @@ describe('LinkedDataFragmentsServer', () => { it('should send a body with GET requests', async () => { let response = await client.get('/handle'); - expect(controller.handleRequest).toHaveBeenCalledOnce(); + expect(handleRequestSpy).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 200); expect(response).toHaveProperty('text', 'body contents'); }); it('should not send a body with HEAD requests', async () => { let response = await client.head('/handle'); - expect(controller.handleRequest).toHaveBeenCalledOnce(); + expect(handleRequestSpy).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 200); expect(response.body).not.toHaveProperty('length'); }); it('should not send a body with OPTIONS requests', async () => { let response = await client.options('/handle'); - expect(controller.handleRequest).toHaveBeenCalledOnce(); + expect(handleRequestSpy).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 200); expect(response).toHaveProperty('text', ''); }); it('should error when the controller cannot handle the request', async () => { let response = await client.get('/unsupported'); - expect(controller.handleRequest).toHaveBeenCalledOnce(); + expect(handleRequestSpy).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 500); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'Application error: No controller for /unsupported\n'); @@ -84,7 +87,7 @@ describe('LinkedDataFragmentsServer', () => { it('should error when the controller errors', async () => { let response = await client.get('/error'); - expect(controller.handleRequest).toHaveBeenCalledOnce(); + expect(handleRequestSpy).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 500); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'Application error: error message\n'); @@ -92,51 +95,51 @@ describe('LinkedDataFragmentsServer', () => { }); describe('A LinkedDataFragmentsServer instance with two controllers', () => { - let server, controllerA, controllerB, client; + let server: ReturnType, controllerA: Controller, controllerB: Controller, client: ReturnType; + let handleRequestSpyA: Mock, handleRequestSpyB: Mock; beforeAll(() => { - controllerA = { - handleRequest: vi.fn((request, response, next) => { - switch (request.url) { - case '/handleA': - response.end('body contents A'); - break; - case '/errorA': - throw new Error('error message A'); - default: - next(); - } - }), - }; - controllerB = { - handleRequest: vi.fn((request, response, next) => { - switch (request.url) { - case '/handleB': - response.end('body contents B'); - break; - case '/errorB': - next(new Error('error message B')); - break; - default: - next(); - } - }), - }; + controllerA = new Controller(); + handleRequestSpyA = vi.fn((request: LdfRequestWithUrl, response: LdfResponse, next: (error?: Error) => void) => { + switch (request.url) { + case '/handleA': + response.end('body contents A'); + break; + case '/errorA': + throw new Error('error message A'); + default: + next(); + } + }); + controllerA.handleRequest = handleRequestSpyA; + controllerB = new Controller(); + handleRequestSpyB = vi.fn((request: LdfRequestWithUrl, response: LdfResponse, next: (error?: Error) => void) => { + switch (request.url) { + case '/handleB': + response.end('body contents B'); + break; + case '/errorB': + next(new Error('error message B')); + break; + default: + next(); + } + }); + controllerB.handleRequest = handleRequestSpyB; server = new LinkedDataFragmentsServer({ controllers: [controllerA, controllerB], - protocol: 'http', log: vi.fn(), }); client = request.agent(server); }); beforeEach(() => { - controllerA.handleRequest.mockClear(); - controllerB.handleRequest.mockClear(); + handleRequestSpyA.mockClear(); + handleRequestSpyB.mockClear(); }); it('should not allow POST requests', async () => { let response = await client.post('/'); - expect(controllerA.handleRequest).not.toHaveBeenCalled(); - expect(controllerB.handleRequest).not.toHaveBeenCalled(); + expect(handleRequestSpyA).not.toHaveBeenCalled(); + expect(handleRequestSpyB).not.toHaveBeenCalled(); expect(response).toHaveProperty('statusCode', 405); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'The HTTP method "POST" is not allowed; try "GET" instead.'); @@ -144,24 +147,24 @@ describe('LinkedDataFragmentsServer', () => { it('should use the first controller when it can handle the request', async () => { let response = await client.get('/handleA'); - expect(controllerA.handleRequest).toHaveBeenCalledOnce(); - expect(controllerB.handleRequest).not.toHaveBeenCalled(); + expect(handleRequestSpyA).toHaveBeenCalledOnce(); + expect(handleRequestSpyB).not.toHaveBeenCalled(); expect(response).toHaveProperty('statusCode', 200); expect(response).toHaveProperty('text', 'body contents A'); }); it('should use the second controller when the first cannot handle the request', async () => { let response = await client.get('/handleB'); - expect(controllerA.handleRequest).toHaveBeenCalledOnce(); - expect(controllerB.handleRequest).toHaveBeenCalledOnce(); + expect(handleRequestSpyA).toHaveBeenCalledOnce(); + expect(handleRequestSpyB).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 200); expect(response).toHaveProperty('text', 'body contents B'); }); it('should error when neither controller can handle the request', async () => { let response = await client.get('/unsupported'); - expect(controllerA.handleRequest).toHaveBeenCalledOnce(); - expect(controllerB.handleRequest).toHaveBeenCalledOnce(); + expect(handleRequestSpyA).toHaveBeenCalledOnce(); + expect(handleRequestSpyB).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 500); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'Application error: No controller for /unsupported\n'); @@ -169,8 +172,8 @@ describe('LinkedDataFragmentsServer', () => { it('should error when the first controller errors', async () => { let response = await client.get('/errorA'); - expect(controllerA.handleRequest).toHaveBeenCalledOnce(); - expect(controllerB.handleRequest).not.toHaveBeenCalled(); + expect(handleRequestSpyA).toHaveBeenCalledOnce(); + expect(handleRequestSpyB).not.toHaveBeenCalled(); expect(response).toHaveProperty('statusCode', 500); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'Application error: error message A\n'); @@ -178,8 +181,8 @@ describe('LinkedDataFragmentsServer', () => { it('should error when the second controller errors', async () => { let response = await client.get('/errorB'); - expect(controllerA.handleRequest).toHaveBeenCalledOnce(); - expect(controllerB.handleRequest).toHaveBeenCalledOnce(); + expect(handleRequestSpyA).toHaveBeenCalledOnce(); + expect(handleRequestSpyB).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 500); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'Application error: error message B\n'); diff --git a/packages/core/test/controllers/AssetsController-test.js b/packages/core/test/controllers/AssetsController-test.ts similarity index 82% rename from packages/core/test/controllers/AssetsController-test.js rename to packages/core/test/controllers/AssetsController-test.ts index 1f4f3e2d..4a623115 100644 --- a/packages/core/test/controllers/AssetsController-test.js +++ b/packages/core/test/controllers/AssetsController-test.ts @@ -1,12 +1,14 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll } from 'vitest'; -import { DummyServer } from '../../../../test/DummyServer'; -let AssetsController = require('../../lib/controllers/AssetsController').AssetsController; // changed to make tests pass, will be revised in follow up pr +import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { controllers } from '../../index'; -let request = require('supertest'), - fs = require('fs'), - path = require('path'); +import * as request from 'supertest'; +import * as fs from 'fs'; +import * as path from 'path'; + +const { AssetsController } = controllers; describe('AssetsController', () => { describe('The AssetsController module', () => { @@ -20,10 +22,10 @@ describe('AssetsController', () => { }); describe('An AssetsController instance', () => { - let controller, client; + let controller: InstanceType & Partial, client: ReturnType; beforeAll(() => { controller = new AssetsController(); - client = request.agent(new DummyServer(controller), {}); + client = request.agent(DummyServer(controller)); }); it('should correctly serve SVG assets', async () => { @@ -33,7 +35,7 @@ describe('AssetsController', () => { expect(response).toHaveProperty('statusCode', 200); expect(response.headers).toHaveProperty('content-type', 'image/svg+xml'); expect(response.headers).toHaveProperty('cache-control', 'public,max-age=1209600'); - expect(response.body.toString()).toBe(asset); + expect(String(response.body)).toBe(asset); }); it('should correctly serve CSS assets', async () => { @@ -53,7 +55,7 @@ describe('AssetsController', () => { expect(response).toHaveProperty('statusCode', 200); expect(response.headers).toHaveProperty('content-type', 'image/vnd.microsoft.icon'); expect(response.headers).toHaveProperty('cache-control', 'public,max-age=1209600'); - expect(response.body.toString()).toBe(asset); + expect(String(response.body)).toBe(asset); }); it('should hand over to the next controller if no asset with that name is found', async () => { diff --git a/packages/core/test/controllers/Controller-test.js b/packages/core/test/controllers/Controller-test.ts similarity index 60% rename from packages/core/test/controllers/Controller-test.js rename to packages/core/test/controllers/Controller-test.ts index 491d70ac..24388b61 100644 --- a/packages/core/test/controllers/Controller-test.js +++ b/packages/core/test/controllers/Controller-test.ts @@ -1,13 +1,23 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll, vi } from 'vitest'; -import { DummyServer } from '../../../../test/DummyServer'; -// changed to make tests pass, will be revised in follow up pr -let Controller = require('../../lib/controllers/Controller').Controller, - UrlData = require('../../lib/UrlData').UrlData; - -let http = require('http'), - request = require('supertest'); +import type { Mock } from 'vitest'; +import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { Controller } from '../../lib/controllers/Controller'; +import { UrlData } from '../../index'; +import type { LdfRequestWithUrl, LdfResponse, ViewSettings } from '../../index'; + +import * as http from 'http'; +import * as request from 'supertest'; + +// Controller marks _handleRequest protected so subclasses can implement it; +// this subclass widens it to public so this suite can spy on the base class's +// own behavior directly, the way its subclasses already do. +class TestableController extends Controller { + override _handleRequest(request: LdfRequestWithUrl, response: LdfResponse, next: (error?: Error) => void, settings?: ViewSettings): void { + return super._handleRequest(request, response, next, settings); + } +} describe('Controller', () => { describe('The Controller module', () => { @@ -21,27 +31,27 @@ describe('Controller', () => { }); describe('A Controller instance without baseURL', () => { - let controller, client; + let controller: TestableController & Partial, client: ReturnType, handleRequestSpy: Mock; beforeAll(() => { - controller = new Controller(); - vi.spyOn(controller, '_handleRequest'); - client = request.agent(new DummyServer(controller), {}); + controller = new TestableController(); + handleRequestSpy = vi.spyOn(controller, '_handleRequest'); + client = request.agent(DummyServer(controller)); }); describe('receiving a request', () => { beforeAll(() => client.get('/path?a=b')); it('should call _handleRequest with request, response and next', () => { - expect(controller._handleRequest).toHaveBeenCalledOnce(); - let args = controller._handleRequest.mock.calls[0]; + expect(handleRequestSpy).toHaveBeenCalledOnce(); + let args = handleRequestSpy.mock.calls[0]; expect(args[0]).toHaveProperty('url'); expect(args[1]).toBeInstanceOf(http.ServerResponse); expect(args[2]).toBeInstanceOf(Function); }); it('should extend _handleRequest with the original URL as parsedUrl property', () => { - expect(controller._handleRequest).toHaveBeenCalledOnce(); - let request = controller._handleRequest.mock.calls[0][0]; + expect(handleRequestSpy).toHaveBeenCalledOnce(); + let request = handleRequestSpy.mock.calls[0][0]; expect(request).toHaveProperty('parsedUrl'); expect(request.parsedUrl).toEqual({ protocol: 'http:', host: request.headers.host, hostname: undefined, port: undefined, @@ -57,11 +67,11 @@ describe('Controller', () => { }); describe('A Controller instance without baseURL using Forwarded header', () => { - let controller, client; + let controller: TestableController & Partial, client: ReturnType, handleRequestSpy: Mock; beforeAll(() => { - controller = new Controller({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); - vi.spyOn(controller, '_handleRequest'); - client = request.agent(new DummyServer(controller), {}); + controller = new TestableController({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); + handleRequestSpy = vi.spyOn(controller, '_handleRequest'); + client = request.agent(DummyServer(controller)); }); describe('receiving a request', () => { @@ -72,16 +82,16 @@ describe('Controller', () => { .set('Forwarded', 'proto=https;host="bar:8000"')); it('should call _handleRequest with request, response and next', () => { - expect(controller._handleRequest).toHaveBeenCalledOnce(); - let args = controller._handleRequest.mock.calls[0]; + expect(handleRequestSpy).toHaveBeenCalledOnce(); + let args = handleRequestSpy.mock.calls[0]; expect(args[0]).toHaveProperty('url'); expect(args[1]).toBeInstanceOf(http.ServerResponse); expect(args[2]).toBeInstanceOf(Function); }); it('should extend _handleRequest with the original URL as parsedUrl property', () => { - expect(controller._handleRequest).toHaveBeenCalledOnce(); - let request = controller._handleRequest.mock.calls[0][0]; + expect(handleRequestSpy).toHaveBeenCalledOnce(); + let request = handleRequestSpy.mock.calls[0][0]; expect(request).toHaveProperty('parsedUrl'); expect(request.parsedUrl).toEqual({ protocol: 'https:', host: 'bar:8000', hostname: 'example.org', port: '1234', @@ -97,11 +107,11 @@ describe('Controller', () => { }); describe('A Controller instance without baseURL using X-Forwarded-* headers', () => { - let controller, client; + let controller: TestableController & Partial, client: ReturnType, handleRequestSpy: Mock; beforeAll(() => { - controller = new Controller(); - vi.spyOn(controller, '_handleRequest'); - client = request.agent(new DummyServer(controller), {}); + controller = new TestableController(); + handleRequestSpy = vi.spyOn(controller, '_handleRequest'); + client = request.agent(DummyServer(controller)); }); describe('receiving a request', () => { @@ -111,16 +121,16 @@ describe('Controller', () => { .set('X-Forwarded-Proto', 'https')); it('should call _handleRequest with request, response and next', () => { - expect(controller._handleRequest).toHaveBeenCalledOnce(); - let args = controller._handleRequest.mock.calls[0]; + expect(handleRequestSpy).toHaveBeenCalledOnce(); + let args = handleRequestSpy.mock.calls[0]; expect(args[0]).toHaveProperty('url'); expect(args[1]).toBeInstanceOf(http.ServerResponse); expect(args[2]).toBeInstanceOf(Function); }); it('should extend _handleRequest with the original URL as parsedUrl property', () => { - expect(controller._handleRequest).toHaveBeenCalledOnce(); - let request = controller._handleRequest.mock.calls[0][0]; + expect(handleRequestSpy).toHaveBeenCalledOnce(); + let request = handleRequestSpy.mock.calls[0][0]; expect(request).toHaveProperty('parsedUrl'); expect(request.parsedUrl).toEqual({ protocol: 'https:', host: 'foo:5000', hostname: undefined, port: undefined, @@ -136,27 +146,27 @@ describe('Controller', () => { }); describe('A Controller instance with baseURL', () => { - let controller, client; + let controller: TestableController & Partial, client: ReturnType, handleRequestSpy: Mock; beforeAll(() => { - controller = new Controller({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); - vi.spyOn(controller, '_handleRequest'); - client = request.agent(new DummyServer(controller), {}); + controller = new TestableController({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); + handleRequestSpy = vi.spyOn(controller, '_handleRequest'); + client = request.agent(DummyServer(controller)); }); describe('receiving a request', () => { beforeAll(() => client.get('/path?a=b')); it('should call _handleRequest with request, response and next', () => { - expect(controller._handleRequest).toHaveBeenCalledOnce(); - let args = controller._handleRequest.mock.calls[0]; + expect(handleRequestSpy).toHaveBeenCalledOnce(); + let args = handleRequestSpy.mock.calls[0]; expect(args[0]).toHaveProperty('url'); expect(args[1]).toBeInstanceOf(http.ServerResponse); expect(args[2]).toBeInstanceOf(Function); }); it('should extend _handleRequest with the rebased URL as parsedUrl property', () => { - expect(controller._handleRequest).toHaveBeenCalledOnce(); - let request = controller._handleRequest.mock.calls[0][0]; + expect(handleRequestSpy).toHaveBeenCalledOnce(); + let request = handleRequestSpy.mock.calls[0][0]; expect(request).toHaveProperty('parsedUrl'); expect(request.parsedUrl).toEqual({ protocol: 'http:', host: 'example.org:1234', hostname: 'example.org', port: '1234', diff --git a/packages/core/test/controllers/DereferenceController-test.js b/packages/core/test/controllers/DereferenceController-test.ts similarity index 73% rename from packages/core/test/controllers/DereferenceController-test.js rename to packages/core/test/controllers/DereferenceController-test.ts index da3032ad..c9ee97cc 100644 --- a/packages/core/test/controllers/DereferenceController-test.js +++ b/packages/core/test/controllers/DereferenceController-test.ts @@ -1,10 +1,14 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll } from 'vitest'; -import { DummyServer } from '../../../../test/DummyServer'; -let DereferenceController = require('../../lib/controllers/DereferenceController').DeferenceController; // changed to make tests pass, will be revised in follow up pr +import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { controllers, datasources } from '../../index'; +import { DataFactory as dataFactory } from 'n3'; -let request = require('supertest'); +import * as request from 'supertest'; + +const { DereferenceController } = controllers; +const { Datasource } = datasources; describe('DereferenceController', () => { describe('The DereferenceController module', () => { @@ -18,14 +22,14 @@ describe('DereferenceController', () => { }); describe('A DereferenceController instance', () => { - let controller, client; + let controller: InstanceType & Partial, client: ReturnType; beforeAll(() => { - controller = new DereferenceController({ dereference: { '/resource/': { path: 'dbpedia/2014' } } }); - client = request.agent(new DummyServer(controller), {}); + controller = new DereferenceController({ dereference: { '/resource/': new Datasource({ dataFactory, path: 'dbpedia/2014' }) } }); + client = request.agent(DummyServer(controller)); }); describe('receiving a request for a dereferenced URL', () => { - let response; + let response: Awaited>; beforeAll(async () => { response = await client.get('/resource/Mickey_Mouse'); }); it('should not hand over to the next controller', () => { @@ -41,7 +45,7 @@ describe('DereferenceController', () => { }); it('should set the Location header correctly', () => { - let hostname = response.req.getHeader('Host'), + let hostname = String(response.request.req.getHeader('Host')), entityUrl = encodeURIComponent('http://' + hostname + '/resource/Mickey_Mouse'), expectedLocation = 'http://' + hostname + '/dbpedia/2014?subject=' + entityUrl; @@ -49,7 +53,7 @@ describe('DereferenceController', () => { }); it('should mention the desired location in the body', () => { - let hostname = response.req.getHeader('Host'), + let hostname = String(response.request.req.getHeader('Host')), entityUrl = encodeURIComponent('http://' + hostname + '/resource/Mickey_Mouse'), expectedLocation = 'http://' + hostname + '/dbpedia/2014?subject=' + entityUrl; diff --git a/packages/core/test/controllers/NotFoundController-test.js b/packages/core/test/controllers/NotFoundController-test.ts similarity index 75% rename from packages/core/test/controllers/NotFoundController-test.js rename to packages/core/test/controllers/NotFoundController-test.ts index de32100b..0c233dbb 100644 --- a/packages/core/test/controllers/NotFoundController-test.js +++ b/packages/core/test/controllers/NotFoundController-test.ts @@ -1,14 +1,17 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll, vi } from 'vitest'; -import { DummyServer } from '../../../../test/DummyServer'; -let NotFoundController = require('../../lib/controllers/NotFoundController').NotFoundController; +import type { Mock } from 'vitest'; +import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { controllers, views, datasources as coreDatasources, UrlData } from '../../index'; +import type { DatasourceRegistry } from '../../index'; -let request = require('supertest'), - dataFactory = require('n3').DataFactory; +import * as request from 'supertest'; +import { DataFactory as dataFactory } from 'n3'; -let NotFoundHtmlView = require('../../lib/views/notfound/NotFoundHtmlView').NotFoundHtmlView, - NotFoundRdfView = require('../../lib/views/notfound/NotFoundRdfView').NotFoundRdfView; +const { NotFoundController } = controllers; +const { NotFoundHtmlView, NotFoundRdfView } = views.notfound; +const { Datasource } = coreDatasources; describe('NotFoundController', () => { describe('The NotFoundController module', () => { @@ -22,14 +25,14 @@ describe('NotFoundController', () => { }); describe('A NotFoundController instance without views', () => { - let controller, client; + let controller: InstanceType & Partial, client: ReturnType; beforeAll(() => { controller = new NotFoundController(); - client = request.agent(new DummyServer(controller), {}); + client = request.agent(DummyServer(controller)); }); describe('receiving a request', () => { - let response; + let response: Awaited>; beforeAll(async () => { response = await client.get('/notfound'); }); @@ -57,23 +60,27 @@ describe('NotFoundController', () => { }); describe('A NotFoundController instance with HTML and RDF views', () => { - let controller, htmlView, rdfView, datasources, client; + let controller: InstanceType & Partial, + htmlView: InstanceType, rdfView: InstanceType, + htmlRenderSpy: Mock['render']>, + rdfRenderSpy: Mock['render']>, + datasources: DatasourceRegistry, client: ReturnType; beforeAll(() => { htmlView = new NotFoundHtmlView({ dataFactory }); rdfView = new NotFoundRdfView({ dataFactory }); - vi.spyOn(htmlView, 'render'); - vi.spyOn(rdfView, 'render'); - datasources = { a: { title: 'foo', url: 'http://example.org/foo#dataset' } }; + htmlRenderSpy = vi.spyOn(htmlView, 'render'); + rdfRenderSpy = vi.spyOn(rdfView, 'render'); + datasources = { a: new Datasource({ dataFactory, title: 'foo', path: 'foo', urlData: new UrlData({ baseURL: 'http://example.org/' }) }) }; controller = new NotFoundController({ views: [htmlView, rdfView], datasources: datasources }); - client = request.agent(new DummyServer(controller), {}); + client = request.agent(DummyServer(controller)); }); function resetAll() { - htmlView.render.mockClear(); - rdfView.render.mockClear(); + htmlRenderSpy.mockClear(); + rdfRenderSpy.mockClear(); } describe('receiving a request without Accept header', () => { - let response; + let response: Awaited>; beforeAll(async () => { resetAll(); response = await client.get('/notfound'); @@ -84,11 +91,11 @@ describe('NotFoundController', () => { }); it('should call the HTML view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); + expect(htmlRenderSpy).toHaveBeenCalledOnce(); }); it('should not call the RDF view', () => { - expect(rdfView.render).not.toHaveBeenCalled(); + expect(rdfRenderSpy).not.toHaveBeenCalled(); }); it('should have a 404 status', () => { @@ -109,7 +116,7 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of */*', () => { - let response; + let response: Awaited>; beforeAll(async () => { resetAll(); response = await client.get('/notfound').set('Accept', '*/*'); @@ -120,11 +127,11 @@ describe('NotFoundController', () => { }); it('should call the HTML view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); + expect(htmlRenderSpy).toHaveBeenCalledOnce(); }); it('should not call the RDF view', () => { - expect(rdfView.render).not.toHaveBeenCalled(); + expect(rdfRenderSpy).not.toHaveBeenCalled(); }); it('should have a 404 status', () => { @@ -145,7 +152,7 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of text/html', () => { - let response; + let response: Awaited>; beforeAll(async () => { resetAll(); response = await client.get('/notfound').set('Accept', 'text/html'); @@ -156,11 +163,11 @@ describe('NotFoundController', () => { }); it('should call the HTML view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); + expect(htmlRenderSpy).toHaveBeenCalledOnce(); }); it('should not call the RDF view', () => { - expect(rdfView.render).not.toHaveBeenCalled(); + expect(rdfRenderSpy).not.toHaveBeenCalled(); }); it('should have a 404 status', () => { @@ -181,7 +188,7 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of text/turtle', () => { - let response; + let response: Awaited>; beforeAll(async () => { resetAll(); response = await client.get('/notfound').set('Accept', 'text/turtle'); @@ -192,11 +199,11 @@ describe('NotFoundController', () => { }); it('should call the RDF view', () => { - expect(rdfView.render).toHaveBeenCalledOnce(); + expect(rdfRenderSpy).toHaveBeenCalledOnce(); }); it('should not call the HTML view', () => { - expect(htmlView.render).not.toHaveBeenCalled(); + expect(htmlRenderSpy).not.toHaveBeenCalled(); }); it('should have a 404 status', () => { @@ -218,7 +225,7 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of application/trig', () => { - let response; + let response: Awaited>; beforeAll(async () => { resetAll(); response = await client.get('/notfound').set('Accept', 'application/trig'); @@ -229,11 +236,11 @@ describe('NotFoundController', () => { }); it('should call the RDF view', () => { - expect(rdfView.render).toHaveBeenCalledOnce(); + expect(rdfRenderSpy).toHaveBeenCalledOnce(); }); it('should not call the HTML view', () => { - expect(htmlView.render).not.toHaveBeenCalled(); + expect(htmlRenderSpy).not.toHaveBeenCalled(); }); it('should have a 404 status', () => { diff --git a/packages/core/test/datasources/Datasource-test.js b/packages/core/test/datasources/Datasource-test.ts similarity index 79% rename from packages/core/test/datasources/Datasource-test.js rename to packages/core/test/datasources/Datasource-test.ts index fd085dc1..efcffe99 100644 --- a/packages/core/test/datasources/Datasource-test.js +++ b/packages/core/test/datasources/Datasource-test.ts @@ -1,17 +1,36 @@ /*! @license MIT ©2013-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest'; -const Datasource = require('../../lib/datasources/Datasource').Datasource; // changed to make tests pass, will be revised in follow up pr - -const EventEmitter = require('events'), - { once } = EventEmitter, - fs = require('fs'), - path = require('path'), - N3 = require('n3'); +import type { Mock } from 'vitest'; +import { Datasource } from '../../lib/datasources/Datasource'; +import { EventEmitter, once } from 'events'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as N3 from 'n3'; +import type { Quad } from 'rdf-js'; +import type { BufferedIterator } from 'asynciterator'; +import type { Pushable, Query } from '../../lib/types'; const exampleFile = path.join(__dirname, '../../../../test/assets/test.ttl'); const dataFactory = N3.DataFactory; +// Datasource marks these members protected so subclasses can use them internally; +// this subclass widens them to public so this suite can exercise the base class's +// own behavior directly, the way its subclasses already do. +class TestableDatasource extends Datasource { + override _fetch(options: { url: string } & Record): EventEmitter { + return super._fetch(options); + } + + override _initialize(): Promise { + return super._initialize(); + } + + override _executeQuery(query: Query, destination: BufferedIterator): void { + return super._executeQuery(query, destination); + } +} + describe('Datasource', () => { describe('The Datasource module', () => { it('should be a function', () => { @@ -28,7 +47,7 @@ describe('Datasource', () => { }); describe('A Datasource instance', () => { - let datasource = new Datasource({ dataFactory }); + let datasource = new TestableDatasource({ dataFactory }); datasource.initialize(); it('should not indicate support for any features', () => { @@ -44,7 +63,7 @@ describe('Datasource', () => { }); it('should throw an error when trying to execute an unsupported query', async () => { - let error = await new Promise((resolve) => datasource.select({ features: { a: true, b: true } }, resolve)); + let error = await new Promise((resolve) => datasource.select({ features: { a: true, b: true } }, resolve)); expect(error).toBeInstanceOf(Error); expect(error).toHaveProperty('message', 'The datasource does not support the given query'); }); @@ -71,20 +90,20 @@ describe('Datasource', () => { it('emits an error when the protocol is unknown', async () => { let result = datasource._fetch({ url: 'myprotocol:abc' }); - let [error] = await once(result, 'error'); + let [error]: Error[] = await once(result, 'error'); expect(error.message).toContain('Unknown protocol: myprotocol'); }); it('emits an error on the datasource when no error listener is attached to the result', async () => { let result = datasource._fetch({ url: exampleFile + 'notfound' }); result.on('data', () => {}); - let [error] = await once(datasource, 'error'); + let [error]: Error[] = await once(datasource, 'error'); expect(error.message).toContain('ENOENT: no such file or directory'); }); it('does not emit an error on the datasource when an error listener is attached to the result', async () => { let result = datasource._fetch({ url: exampleFile + 'notfound' }); - let [error] = await once(result, 'error'); + let [error]: Error[] = await once(result, 'error'); expect(error.message).toContain('ENOENT: no such file or directory'); }); }); @@ -96,14 +115,14 @@ describe('Datasource', () => { }); describe('when closed with a callback', () => { - it('should invoke the callback', () => new Promise((resolve) => datasource.close(resolve))); + it('should invoke the callback', () => new Promise((resolve) => datasource.close(resolve))); }); }); describe('A Datasource instance with an initializer', () => { - let datasource, initializedListener, errorListener, initResolver, initSpy; + let datasource: TestableDatasource, initializedListener: Mock, errorListener: Mock, initResolver: () => void, initSpy: Mock<() => Promise>; beforeAll(() => { - datasource = new Datasource({ dataFactory }); + datasource = new TestableDatasource({ dataFactory }); datasource._initialize = () => new Promise((resolve) => { initResolver = resolve; }); initSpy = vi.spyOn(datasource, '_initialize'); Object.defineProperty(datasource, 'supportedFeatures', { @@ -128,7 +147,7 @@ describe('Datasource', () => { }); it('should error when trying to query', async () => { - let error = await new Promise((resolve) => datasource.select({}, resolve)); + let error = await new Promise((resolve) => datasource.select({}, resolve)); expect(error).toHaveProperty('message', 'The datasource is not initialized yet'); }); }); @@ -155,16 +174,16 @@ describe('Datasource', () => { }); it('should allow querying', async () => { - let error = await new Promise((resolve) => datasource.select({}, resolve)); + let error = await new Promise((resolve) => datasource.select({}, resolve)); expect(error).toHaveProperty('message', '_executeQuery has not been implemented'); }); }); }); describe('A Datasource instance with an initializer that errors synchronously', () => { - let datasource, initializedListener, errorListener, error; + let datasource: TestableDatasource, initializedListener: Mock, errorListener: Mock, error: Error; beforeAll(() => { - datasource = new Datasource({ dataFactory }); + datasource = new TestableDatasource({ dataFactory }); error = new Error('initializer error'); datasource._initialize = () => { throw error; }; vi.spyOn(datasource, '_initialize'); @@ -194,9 +213,9 @@ describe('Datasource', () => { }); describe('A Datasource instance with an initializer that errors asynchronously', () => { - let datasource, initializedListener, errorListener, error; + let datasource: TestableDatasource, initializedListener: Mock, errorListener: Mock, error: Error; beforeAll(() => { - datasource = new Datasource({ dataFactory }); + datasource = new TestableDatasource({ dataFactory }); error = new Error('initializer error'); datasource._initialize = () => Promise.reject(error); vi.spyOn(datasource, '_initialize'); @@ -226,7 +245,7 @@ describe('Datasource', () => { }); describe('A derived Datasource instance', () => { - let datasource = new Datasource({ dataFactory }); + let datasource = new TestableDatasource({ dataFactory }); Object.defineProperty(datasource, 'supportedFeatures', { enumerable: true, value: { a: true, b: true, c: false }, @@ -270,7 +289,7 @@ describe('Datasource', () => { }); describe('A Datasource instance with a graph property', () => { - let datasource = new Datasource({ + let datasource = new TestableDatasource({ dataFactory, graph: 'http://example.org/#mygraph', }); @@ -279,22 +298,23 @@ describe('Datasource', () => { value: { custom: true }, }); datasource.initialize(); - datasource._executeQuery = vi.fn((query, destination) => { + const executeQuerySpy = vi.fn((query: Query, destination: Pushable) => { destination._push(dataFactory.quad(dataFactory.namedNode('s'), dataFactory.namedNode('p'), dataFactory.namedNode('o1'))); destination._push(dataFactory.quad(dataFactory.namedNode('s'), dataFactory.namedNode('p'), dataFactory.namedNode('o2'), dataFactory.defaultGraph())); destination._push(dataFactory.quad(dataFactory.namedNode('s'), dataFactory.namedNode('p'), dataFactory.namedNode('o3'), dataFactory.namedNode('g'))); destination.close(); }); + datasource._executeQuery = executeQuerySpy; beforeEach(() => { - datasource._executeQuery.mockClear(); + executeQuerySpy.mockClear(); }); it('should move triples in the default graph to the given graph', async () => { - let quads = await new Promise((resolve, reject) => { - let collected = []; + let quads = await new Promise((resolve, reject) => { + let collected: Quad[] = []; let result = datasource.select({ features: { custom: true } }, reject); - result.on('data', (q) => { collected.push(q); }); + result.on('data', (q: Quad) => { collected.push(q); }); result.on('end', () => { resolve(collected); }); }); expect(quads).toEqual([ @@ -309,8 +329,8 @@ describe('Datasource', () => { graph: dataFactory.namedNode('http://example.org/#mygraph'), features: { custom: true }, }); - expect(datasource._executeQuery.mock.calls[0][0].features).toEqual({ custom: true }), - datasource._executeQuery.mock.calls[0][0].graph.equals(dataFactory.defaultGraph()); + expect(executeQuerySpy.mock.calls[0][0].features).toEqual({ custom: true }), + executeQuerySpy.mock.calls[0][0].graph?.equals(dataFactory.defaultGraph()); }); it('should query the default graph as the empty graph', () => { @@ -318,8 +338,8 @@ describe('Datasource', () => { graph: dataFactory.defaultGraph(), features: { custom: true }, }); - expect(datasource._executeQuery.mock.calls[0][0].features).toEqual({ custom: true }), - datasource._executeQuery.mock.calls[0][0].graph.equals(dataFactory.namedNode('urn:ldf:emptyGraph')); + expect(executeQuerySpy.mock.calls[0][0].features).toEqual({ custom: true }), + executeQuerySpy.mock.calls[0][0].graph?.equals(dataFactory.namedNode('urn:ldf:emptyGraph')); }); }); }); diff --git a/packages/core/test/routers/DatasourceRouter-test.js b/packages/core/test/routers/DatasourceRouter-test.js deleted file mode 100644 index 9007716c..00000000 --- a/packages/core/test/routers/DatasourceRouter-test.js +++ /dev/null @@ -1,106 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ - -import { describe, it, expect } from 'vitest'; -import { extractQueryParams } from '../../../../test/test-helpers'; -let DatasourceRouter = require('../../lib/routers/DatasourceRouter').DatasourceRouter; // changed to make tests pass, will be revised in follow up pr - -describe('DatasourceRouter', () => { - describe('The DatasourceRouter module', () => { - it('should be a function', () => { - expect(typeof DatasourceRouter).toBe('function'); - }); - - it('should be a DatasourceRouter constructor', () => { - expect(new DatasourceRouter()).toBeInstanceOf(DatasourceRouter); - }); - }); - - describe('A DatasourceRouter instance', () => { - let router = new DatasourceRouter(); - - describe('extractUrlParams', () => { - describe('with an existing query', () => { - [ - [ - 'a root URL without trailing slash or query parameters', - 'http://example.org', - 'should extract the index datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/' }, - ], - [ - 'a root URL without query parameters', - 'http://example.org/', - 'should extract the index datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/' }, - ], - [ - 'a root URL with query parameters', - 'http://example.org/?a=b&c=d', - 'should extract the index datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/' }, - ], - [ - 'a URL with word characters without query parameters', - 'http://example.org/mydatasource', - 'should extract the datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/mydatasource' }, - ], - [ - 'a URL with word characters with query parameters', - 'http://example.org/mydatasource?a=b&c=d', - 'should extract the datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/mydatasource' }, - ], - [ - 'a URL with word and non-word characters without query parameters', - 'http://example.org/my/data-source', - 'should extract the datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/my/data-source' }, - ], - [ - 'a URL with word and non-word characters with query parameters', - 'http://example.org/my/data-source?a=b&c=d', - 'should extract the datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/my/data-source' }, - ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); - }); - }); - }); - - describe('A DatasourceRouter instance with a base URL', () => { - let router = new DatasourceRouter({ - urlData: { baseURLPath: '/my/base/' }, - }); - - describe('extractUrlParams', () => { - describe('with an existing query', () => { - [ - [ - 'a root URL', - 'http://example.org/my/base/', - 'should extract the index datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/' }, - ], - [ - 'a non-root URL', - 'http://example.org/my/base/other/path', - 'should extract the index datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/other/path' }, - ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); - }); - }); - }); -}); diff --git a/packages/core/test/routers/DatasourceRouter-test.ts b/packages/core/test/routers/DatasourceRouter-test.ts new file mode 100644 index 00000000..c9876b5f --- /dev/null +++ b/packages/core/test/routers/DatasourceRouter-test.ts @@ -0,0 +1,112 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ + +import { describe, it, expect } from 'vitest'; +import { extractQueryParams } from '../../../../test/test-helpers'; +import { DatasourceRouter } from '../../lib/routers/DatasourceRouter'; +import { UrlData } from '../../index'; +import type { Query } from '../../index'; + +describe('DatasourceRouter', () => { + describe('The DatasourceRouter module', () => { + it('should be a function', () => { + expect(typeof DatasourceRouter).toBe('function'); + }); + + it('should be a DatasourceRouter constructor', () => { + expect(new DatasourceRouter()).toBeInstanceOf(DatasourceRouter); + }); + }); + + describe('A DatasourceRouter instance', () => { + let router = new DatasourceRouter(); + + describe('extractUrlParams', () => { + describe('with an existing query', () => { + ( + [ + [ + 'a root URL without trailing slash or query parameters', + 'http://example.org', + 'should extract the index datasource', + { limit: 1 }, + { limit: 1, features: { datasource: true }, datasource: '/' }, + ], + [ + 'a root URL without query parameters', + 'http://example.org/', + 'should extract the index datasource', + { limit: 1 }, + { limit: 1, features: { datasource: true }, datasource: '/' }, + ], + [ + 'a root URL with query parameters', + 'http://example.org/?a=b&c=d', + 'should extract the index datasource', + { limit: 1 }, + { limit: 1, features: { datasource: true }, datasource: '/' }, + ], + [ + 'a URL with word characters without query parameters', + 'http://example.org/mydatasource', + 'should extract the datasource', + { limit: 1 }, + { limit: 1, features: { datasource: true }, datasource: '/mydatasource' }, + ], + [ + 'a URL with word characters with query parameters', + 'http://example.org/mydatasource?a=b&c=d', + 'should extract the datasource', + { limit: 1 }, + { limit: 1, features: { datasource: true }, datasource: '/mydatasource' }, + ], + [ + 'a URL with word and non-word characters without query parameters', + 'http://example.org/my/data-source', + 'should extract the datasource', + { limit: 1 }, + { limit: 1, features: { datasource: true }, datasource: '/my/data-source' }, + ], + [ + 'a URL with word and non-word characters with query parameters', + 'http://example.org/my/data-source?a=b&c=d', + 'should extract the datasource', + { limit: 1 }, + { limit: 1, features: { datasource: true }, datasource: '/my/data-source' }, + ], + ] as Array<[string, string, string, Query, Query]> + ) + .forEach((args) => { extractQueryParams(router, ...args); }); + }); + }); + }); + + describe('A DatasourceRouter instance with a base URL', () => { + let router = new DatasourceRouter({ + urlData: new UrlData({ baseURL: '/my/base' }), + }); + + describe('extractUrlParams', () => { + describe('with an existing query', () => { + ( + [ + [ + 'a root URL', + 'http://example.org/my/base/', + 'should extract the index datasource', + { limit: 1 }, + { limit: 1, features: { datasource: true }, datasource: '/' }, + ], + [ + 'a non-root URL', + 'http://example.org/my/base/other/path', + 'should extract the index datasource', + { limit: 1 }, + { limit: 1, features: { datasource: true }, datasource: '/other/path' }, + ], + ] as Array<[string, string, string, Query, Query]> + ) + .forEach((args) => { extractQueryParams(router, ...args); }); + }); + }); + }); +}); diff --git a/packages/core/test/routers/PageRouter-test.js b/packages/core/test/routers/PageRouter-test.js deleted file mode 100644 index 66e13811..00000000 --- a/packages/core/test/routers/PageRouter-test.js +++ /dev/null @@ -1,172 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ - -import { describe, it, expect } from 'vitest'; -import { extractQueryParams } from '../../../../test/test-helpers'; -let PageRouter = require('../../lib/routers/PageRouter').PageRouter; // changed to make tests pass, will be revised in follow up pr - -describe('PageRouter', () => { - describe('The PageRouter module', () => { - it('should be a function', () => { - expect(typeof PageRouter).toBe('function'); - }); - - it('should be a PageRouter constructor', () => { - expect(new PageRouter()).toBeInstanceOf(PageRouter); - }); - }); - - describe('A PageRouter instance', () => { - let router = new PageRouter(); - - describe('extractUrlParams', () => { - describe('with an existing query', () => { - [ - [ - 'a URL without query parameters', - 'http://example.org/', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with an empty page parameter', - 'http://example.org/?page=', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with a non-numerical page parameter', - 'http://example.org/?page=foo', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with -1 as page parameter', - 'http://example.org/?page=-1', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with 0 as page parameter', - 'http://example.org/?page=0', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with 1 as page parameter', - 'http://example.org/?page=1', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with 2 as page parameter', - 'http://example.org/?page=2', - 'should add the default page size as limit and set the offset', - { a: 1 }, - { a: 1, features: { limit: true, offset: true }, limit: 100, offset: 100 }, - ], - [ - 'a URL with 3 as page parameter', - 'http://example.org/?page=3', - 'should add the default page size as limit and set the offset', - { a: 1, features: { a: true, b: true } }, - { a: 1, features: { a: true, b: true, limit: true, offset: true }, limit: 100, offset: 200 }, - ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); - }); - }); - }); - - describe('A PageRouter instance with a given page size', () => { - let router = new PageRouter({ pageSize: 250 }); - - describe('extractUrlParams', () => { - describe('with an existing query', () => { - [ - [ - 'a URL without query parameters', - 'http://example.org/', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with an empty page parameter', - 'http://example.org/?page=', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with a non-numerical page parameter', - 'http://example.org/?page=foo', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with -1 as page parameter', - 'http://example.org/?page=-1', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with 0 as page parameter', - 'http://example.org/?page=0', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with 1 as page parameter', - 'http://example.org/?page=1', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with 2 as page parameter', - 'http://example.org/?page=2', - 'should add the page size as limit and set the offset', - { a: 1 }, - { a: 1, features: { limit: true, offset: true }, limit: 250, offset: 250 }, - ], - [ - 'a URL with 3 as page parameter', - 'http://example.org/?page=3', - 'should add the page size as limit and set the offset', - { a: 1, features: { a: true, b: true } }, - { a: 1, features: { a: true, b: true, limit: true, offset: true }, limit: 250, offset: 500 }, - ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); - }); - }); - }); - - describe('A PageRouter instance with an invalid page size', () => { - let router = new PageRouter({ pageSize: -1 }); - - describe('extractUrlParams', () => { - describe('with an existing query', () => { - [ - [ - 'a URL without query parameters', - 'http://example.org/', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); - }); - }); - }); -}); diff --git a/packages/core/test/routers/PageRouter-test.ts b/packages/core/test/routers/PageRouter-test.ts new file mode 100644 index 00000000..9e98af3a --- /dev/null +++ b/packages/core/test/routers/PageRouter-test.ts @@ -0,0 +1,179 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ + +import { describe, it, expect } from 'vitest'; +import { extractQueryParams } from '../../../../test/test-helpers'; +import { PageRouter } from '../../lib/routers/PageRouter'; +import type { Query } from '../../index'; + +describe('PageRouter', () => { + describe('The PageRouter module', () => { + it('should be a function', () => { + expect(typeof PageRouter).toBe('function'); + }); + + it('should be a PageRouter constructor', () => { + expect(new PageRouter()).toBeInstanceOf(PageRouter); + }); + }); + + describe('A PageRouter instance', () => { + let router = new PageRouter(); + + describe('extractUrlParams', () => { + describe('with an existing query', () => { + ( + [ + [ + 'a URL without query parameters', + 'http://example.org/', + 'should add the default page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with an empty page parameter', + 'http://example.org/?page=', + 'should add the default page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with a non-numerical page parameter', + 'http://example.org/?page=foo', + 'should add the default page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with -1 as page parameter', + 'http://example.org/?page=-1', + 'should add the default page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with 0 as page parameter', + 'http://example.org/?page=0', + 'should add the default page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with 1 as page parameter', + 'http://example.org/?page=1', + 'should add the default page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with 2 as page parameter', + 'http://example.org/?page=2', + 'should add the default page size as limit and set the offset', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true, offset: true }, limit: 100, offset: 100 }, + ], + [ + 'a URL with 3 as page parameter', + 'http://example.org/?page=3', + 'should add the default page size as limit and set the offset', + { patternString: 'x', features: { a: true, b: true } }, + { patternString: 'x', features: { a: true, b: true, limit: true, offset: true }, limit: 100, offset: 200 }, + ], + ] as Array<[string, string, string, Query, Query]> + ) + .forEach((args) => { extractQueryParams(router, ...args); }); + }); + }); + }); + + describe('A PageRouter instance with a given page size', () => { + let router = new PageRouter({ pageSize: 250 }); + + describe('extractUrlParams', () => { + describe('with an existing query', () => { + ( + [ + [ + 'a URL without query parameters', + 'http://example.org/', + 'should add the page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with an empty page parameter', + 'http://example.org/?page=', + 'should add the page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with a non-numerical page parameter', + 'http://example.org/?page=foo', + 'should add the page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with -1 as page parameter', + 'http://example.org/?page=-1', + 'should add the page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with 0 as page parameter', + 'http://example.org/?page=0', + 'should add the page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with 1 as page parameter', + 'http://example.org/?page=1', + 'should add the page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with 2 as page parameter', + 'http://example.org/?page=2', + 'should add the page size as limit and set the offset', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true, offset: true }, limit: 250, offset: 250 }, + ], + [ + 'a URL with 3 as page parameter', + 'http://example.org/?page=3', + 'should add the page size as limit and set the offset', + { patternString: 'x', features: { a: true, b: true } }, + { patternString: 'x', features: { a: true, b: true, limit: true, offset: true }, limit: 250, offset: 500 }, + ], + ] as Array<[string, string, string, Query, Query]> + ) + .forEach((args) => { extractQueryParams(router, ...args); }); + }); + }); + }); + + describe('A PageRouter instance with an invalid page size', () => { + let router = new PageRouter({ pageSize: -1 }); + + describe('extractUrlParams', () => { + describe('with an existing query', () => { + ( + [ + [ + 'a URL without query parameters', + 'http://example.org/', + 'should add the default page size as limit', + { patternString: 'x' }, + { patternString: 'x', features: { limit: true }, limit: 100 }, + ], + ] as Array<[string, string, string, Query, Query]> + ) + .forEach((args) => { extractQueryParams(router, ...args); }); + }); + }); + }); +}); diff --git a/packages/core/test/views/View-test.js b/packages/core/test/views/View-test.ts similarity index 61% rename from packages/core/test/views/View-test.js rename to packages/core/test/views/View-test.ts index 364362cd..d59d4cf2 100644 --- a/packages/core/test/views/View-test.js +++ b/packages/core/test/views/View-test.ts @@ -1,9 +1,28 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, vi } from 'vitest'; -// changed to make tests pass, will be revised in follow up pr -let View = require('../../lib/views/View').View, - resolve = require('path').resolve; +import { View } from '../../lib/views/View'; +import { resolve } from 'path'; +import { IncomingMessage, ServerResponse } from 'http'; +import { Socket } from 'net'; +import type { LdfRequest, LdfResponse, RenderDone, ViewSettings } from '../../index'; + +// View marks _render protected so subclasses can implement it; this subclass +// widens it to public so this suite can mock/spy on the base class's own +// behavior directly, the way its subclasses already do. +class TestableView extends View { + override _render(settings: ViewSettings, request: LdfRequest, response: LdfResponse, done: RenderDone): void { + return super._render(settings, request, response, done); + } +} + +function createRequest(): LdfRequest { + return new IncomingMessage(new Socket()); +} + +function createResponse(): LdfResponse { + return new ServerResponse(createRequest()); +} describe('View', () => { describe('The View module', () => { @@ -69,54 +88,59 @@ describe('View', () => { describe('without _render method', () => { it('should throw an error on calling render', () => { - let response = { getHeader: vi.fn() }; - expect(() => { new View().render(null, null, response); }) + let response = createResponse(); + response.getHeader = vi.fn(); + expect(() => { new View().render({}, createRequest(), response); }) .toThrow('The _render method is not yet implemented.'); }); }); describe('created without defaults', () => { it('should call _render with the given options', () => { - let view = new View(), - request = {}, response = { getHeader: vi.fn().mockReturnValue('text/html') }, + let view = new TestableView(), + request = createRequest(), response = createResponse(), options = { a: 'b' }; - view._render = vi.fn(); + response.getHeader = vi.fn().mockReturnValue('text/html'); + let renderSpy = vi.fn(); + view._render = renderSpy; view.render(options, request, response, noop); expect(response.getHeader).toHaveBeenCalledOnce(); expect(response.getHeader).toHaveBeenCalledWith('Content-Type'); - expect(view._render.mock.calls[0]).toHaveLength(4); - expect(view._render).toHaveBeenCalledOnce(); - expect(view._render.mock.calls[0][0]).toEqual({ + expect(renderSpy.mock.calls[0]).toHaveLength(4); + expect(renderSpy).toHaveBeenCalledOnce(); + expect(renderSpy.mock.calls[0][0]).toEqual({ a: 'b', contentType: 'text/html', viewPathBase: resolve(__dirname, '../../lib/views/base.html'), }); - expect(view._render.mock.calls[0][1]).toBe(request); - expect(view._render.mock.calls[0][2]).toBe(response); - expect(view._render.mock.calls[0][3]).toBeInstanceOf(Function); + expect(renderSpy.mock.calls[0][1]).toBe(request); + expect(renderSpy.mock.calls[0][2]).toBe(response); + expect(renderSpy.mock.calls[0][3]).toBeInstanceOf(Function); }); }); describe('created with defaults', () => { it('should call _render with the combined defaults and options', () => { - let view = new View(null, null, { c: 'd' }), - request = {}, response = { getHeader: vi.fn().mockReturnValue('text/html') }, + let view = new TestableView(undefined, undefined, { c: 'd' }), + request = createRequest(), response = createResponse(), options = { a: 'b' }; - view._render = vi.fn(); + response.getHeader = vi.fn().mockReturnValue('text/html'); + let renderSpy = vi.fn(); + view._render = renderSpy; view.render(options, request, response, noop); expect(response.getHeader).toHaveBeenCalledOnce(); expect(response.getHeader).toHaveBeenCalledWith('Content-Type'); - expect(view._render).toHaveBeenCalledOnce(); - expect(view._render.mock.calls[0]).toHaveLength(4); - expect(view._render.mock.calls[0][0]).toEqual({ + expect(renderSpy).toHaveBeenCalledOnce(); + expect(renderSpy.mock.calls[0]).toHaveLength(4); + expect(renderSpy.mock.calls[0][0]).toEqual({ a: 'b', c: 'd', contentType: 'text/html', viewPathBase: resolve(__dirname, '../../lib/views/base.html'), }); - expect(view._render.mock.calls[0][1]).toBe(request); - expect(view._render.mock.calls[0][2]).toBe(response); - expect(view._render.mock.calls[0][3]).toBeInstanceOf(Function); + expect(renderSpy.mock.calls[0][1]).toBe(request); + expect(renderSpy.mock.calls[0][2]).toBe(response); + expect(renderSpy.mock.calls[0][3]).toBeInstanceOf(Function); }); }); }); diff --git a/packages/core/test/views/ViewCollection-test.js b/packages/core/test/views/ViewCollection-test.ts similarity index 71% rename from packages/core/test/views/ViewCollection-test.js rename to packages/core/test/views/ViewCollection-test.ts index 6fd69bdc..a019b638 100644 --- a/packages/core/test/views/ViewCollection-test.js +++ b/packages/core/test/views/ViewCollection-test.ts @@ -1,9 +1,15 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll } from 'vitest'; -let ViewCollection = require('../../lib/views/ViewCollection').ViewCollection; // changed to make tests pass, will be revised in follow up pr +import { ViewCollection } from '../../lib/views/ViewCollection'; +import { View } from '../../lib/views/View'; +import { IncomingMessage } from 'http'; +import { Socket } from 'net'; +import type { LdfRequest } from '../../index'; -let View = require('../../lib/views/View').View; // changed to make tests pass, will be revised in follow up pr +function createRequest(): LdfRequest { + return new IncomingMessage(new Socket()); +} describe('ViewCollection', () => { describe('The ViewCollection module', () => { @@ -17,35 +23,35 @@ describe('ViewCollection', () => { }); describe('A ViewCollection instance without views', () => { - let viewCollection; + let viewCollection: ViewCollection; beforeAll(() => { viewCollection = new ViewCollection(); }); it('should throw an error when matching a view', () => { - expect(() => { viewCollection.matchView('Foo'); }) + expect(() => { viewCollection.matchView('Foo', createRequest()); }) .toThrow('No view named Foo found.'); }); }); describe('A ViewCollection instance with one view', () => { - let viewCollection, viewA; + let viewCollection: ViewCollection, viewA: View; beforeAll(() => { viewA = new View('MyView1', 'text/html,application/trig;q=0.7'); viewCollection = new ViewCollection([viewA]); }); it('should throw an error when matching a view with a non-existing type', () => { - expect(() => { viewCollection.matchView('Bar'); }) + expect(() => { viewCollection.matchView('Bar', createRequest()); }) .toThrow('No view named Bar found.'); }); describe('when a client requests HTML', () => { - let viewDetails, request, response; + let viewDetails: ReturnType, request: LdfRequest; beforeAll(() => { - request = { headers: { accept: 'text/html' } }; - response = {}; - viewDetails = viewCollection.matchView('MyView1', request, response); + request = createRequest(); + request.headers.accept = 'text/html'; + viewDetails = viewCollection.matchView('MyView1', request); }); it('should return a match for the view', () => { @@ -56,11 +62,11 @@ describe('ViewCollection', () => { }); describe('when a client requests TriG', () => { - let viewDetails, request, response; + let viewDetails: ReturnType, request: LdfRequest; beforeAll(() => { - request = { headers: { accept: 'application/trig' } }; - response = {}; - viewDetails = viewCollection.matchView('MyView1', request, response); + request = createRequest(); + request.headers.accept = 'application/trig'; + viewDetails = viewCollection.matchView('MyView1', request); }); it('should return a match for the view', () => { @@ -72,7 +78,7 @@ describe('ViewCollection', () => { }); describe('A ViewCollection instance with three views of two types', () => { - let viewCollection, viewA, viewB, viewC; + let viewCollection: ViewCollection, viewA: View, viewB: View, viewC: View; beforeAll(() => { viewA = new View('MyView1', 'text/html,application/trig;q=0.5'); viewB = new View('MyView1', 'text/html;q=1.0,application/trig'); @@ -81,16 +87,16 @@ describe('ViewCollection', () => { }); it('should throw an error when matching a view with a non-existing type', () => { - expect(() => { viewCollection.matchView('Bar'); }) + expect(() => { viewCollection.matchView('Bar', createRequest()); }) .toThrow('No view named Bar found.'); }); describe('when matching a request of one view type as HTML', () => { - let viewDetails, request, response; + let viewDetails: ReturnType, request: LdfRequest; beforeAll(() => { - request = { headers: { accept: 'text/html' } }; - response = {}; - viewDetails = viewCollection.matchView('MyView1', request, response); + request = createRequest(); + request.headers.accept = 'text/html'; + viewDetails = viewCollection.matchView('MyView1', request); }); it('should return a description of the best fitting view', () => { @@ -101,11 +107,11 @@ describe('ViewCollection', () => { }); describe('when matching a request of one view type as TriG', () => { - let viewDetails, request, response; + let viewDetails: ReturnType, request: LdfRequest; beforeAll(() => { - request = { headers: { accept: 'application/trig' } }; - response = {}; - viewDetails = viewCollection.matchView('MyView1', request, response); + request = createRequest(); + request.headers.accept = 'application/trig'; + viewDetails = viewCollection.matchView('MyView1', request); }); it('should return a description of the best fitting view', () => { @@ -116,11 +122,11 @@ describe('ViewCollection', () => { }); describe('when matching a request of another view type as HTML', () => { - let viewDetails, request, response; + let viewDetails: ReturnType, request: LdfRequest; beforeAll(() => { - request = { headers: { accept: 'text/html' } }; - response = {}; - viewDetails = viewCollection.matchView('MyView2', request, response); + request = createRequest(); + request.headers.accept = 'text/html'; + viewDetails = viewCollection.matchView('MyView2', request); }); it('should return a description of the other view', () => { diff --git a/packages/datasource-composite/test/datasources/CompositeDatasource-test.js b/packages/datasource-composite/test/datasources/CompositeDatasource-test.ts similarity index 69% rename from packages/datasource-composite/test/datasources/CompositeDatasource-test.js rename to packages/datasource-composite/test/datasources/CompositeDatasource-test.ts index a328dd0c..424f116d 100644 --- a/packages/datasource-composite/test/datasources/CompositeDatasource-test.js +++ b/packages/datasource-composite/test/datasources/CompositeDatasource-test.ts @@ -1,38 +1,47 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -let CompositeDatasource = require('../../').datasources.CompositeDatasource; - -let Datasource = require('@ldf/core').datasources.Datasource, - HdtDatasource = require('@ldf/datasource-hdt').datasources.HdtDatasource, - N3Datasource = require('@ldf/datasource-n3').datasources.N3Datasource, - path = require('path'), - dataFactory = require('n3').DataFactory; - -let EventEmitter = require('events'), - { once } = EventEmitter; +import { datasources as compositeDatasources } from '../../index'; +import { datasources as coreDatasources } from '@ldf/core'; +import { datasources as hdtDatasources } from '@ldf/datasource-hdt'; +import { datasources as n3Datasources } from '@ldf/datasource-n3'; +import type { DatasourceOptions, DatasourceRegistry, Query } from '@ldf/core'; +import type { Quad } from 'rdf-js'; +import * as path from 'path'; +import { DataFactory as dataFactory } from 'n3'; + +import { once } from 'events'; + +const { CompositeDatasource } = compositeDatasources; +const { Datasource } = coreDatasources; +const { HdtDatasource } = hdtDatasources; +const { N3Datasource } = n3Datasources; let exampleHdtFile = path.join(__dirname, '../../../../test/assets/test.hdt'); let exampleHdtFileWithBlanks = path.join(__dirname, '../../../../test/assets/test-blank.hdt'); let exampleTurtleUrl = 'file://' + path.join(__dirname, '../../../../test/assets/test.ttl'); let exampleTrigUrl = 'file://' + path.join(__dirname, '../../../../test/assets/test.trig'); +interface DatasourceReferenceConfig { + settings: DatasourceOptions; + datasourceType: new (options: DatasourceOptions) => InstanceType; + size: number; +} + describe('CompositeDatasource', () => { - let references = { - data0: { dataFactory, settings: { dataFactory, file: exampleHdtFile }, datasourceType: HdtDatasource, size: 132 }, - data1: { dataFactory, settings: { dataFactory, file: exampleHdtFileWithBlanks, graph: 'http://example.org/graph0' }, datasourceType: HdtDatasource, size: 6 }, - data2: { dataFactory, settings: { dataFactory, url: exampleTurtleUrl }, datasourceType: N3Datasource, size: 129 }, - data3: { dataFactory, settings: { dataFactory, url: exampleTrigUrl }, datasourceType: N3Datasource, size: 7 }, + let referenceConfigs: Record = { + data0: { settings: { dataFactory, file: exampleHdtFile }, datasourceType: HdtDatasource, size: 132 }, + data1: { settings: { dataFactory, file: exampleHdtFileWithBlanks, graph: 'http://example.org/graph0' }, datasourceType: HdtDatasource, size: 6 }, + data2: { settings: { dataFactory, url: exampleTurtleUrl }, datasourceType: N3Datasource, size: 129 }, + data3: { settings: { dataFactory, url: exampleTrigUrl }, datasourceType: N3Datasource, size: 7 }, }; - Object.keys(references).forEach((datasourceId) => { - let datasource = references[datasourceId]; - let DatasourceType = datasource.datasourceType; - let size = references[datasourceId].size; - references[datasourceId] = new DatasourceType(datasource.settings); - references[datasourceId].size = size; + let references: DatasourceRegistry = {}; + Object.keys(referenceConfigs).forEach((datasourceId) => { + let config = referenceConfigs[datasourceId]; + references[datasourceId] = new config.datasourceType(config.settings); }); - let totalSize = Object.keys(references).reduce((acc, key) => { - return acc + references[key].size; + let totalSize = Object.keys(referenceConfigs).reduce((acc, key) => { + return acc + referenceConfigs[key].size; }, 0); beforeAll(() => Promise.all(Object.keys(references).map(async (key) => { @@ -48,31 +57,31 @@ describe('CompositeDatasource', () => { it('should be an CompositeDatasource constructor', async () => { let instance = new CompositeDatasource({ references: references, dataFactory }); expect(instance).toBeInstanceOf(CompositeDatasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); it('should create CompositeDatasource objects', async () => { let instance = new CompositeDatasource({ references: references, dataFactory }); expect(instance).toBeInstanceOf(CompositeDatasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); it('should create Datasource objects', async () => { let instance = new CompositeDatasource({ references: references, dataFactory }); expect(instance).toBeInstanceOf(Datasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); }); describe('A CompositeDatasource instance for 4 Datasources', () => { - let datasource; + let datasource: InstanceType; function getDatasource() { return datasource; } beforeAll(async () => { datasource = new CompositeDatasource({ references: references, dataFactory }); datasource.initialize(); await once(datasource, 'initialized'); }); - afterAll(() => new Promise((resolve) => datasource.close(resolve))); + afterAll(() => new Promise((resolve) => datasource.close(resolve))); itShouldExecute(getDatasource, 'the empty query', @@ -161,14 +170,14 @@ describe('CompositeDatasource', () => { }); }); -function itShouldExecute(getDatasource, name, query, - expectedResultsCount, expectedTotalCount, expectedTriples) { +function itShouldExecute(getDatasource: () => InstanceType, name: string, query: Query, + expectedResultsCount: number, expectedTotalCount: number, expectedTriples?: Quad[]) { describe('executing ' + name, () => { - let resultsCount = 0, totalCount, triples = []; + let resultsCount = 0, totalCount: number | undefined, triples: Quad[] = []; beforeAll(async () => { let result = getDatasource().select(query); - result.getProperty('metadata', (metadata) => { totalCount = metadata.totalCount; }); - result.on('data', (triple) => { resultsCount++; expectedTriples && triples.push(triple); }); + result.getProperty('metadata', (metadata: { totalCount: number }) => { totalCount = metadata.totalCount; }); + result.on('data', (triple: Quad) => { resultsCount++; expectedTriples && triples.push(triple); }); await once(result, 'end'); }); diff --git a/packages/datasource-hdt/test/datasources/HdtDatasource-test.js b/packages/datasource-hdt/test/datasources/HdtDatasource-test.ts similarity index 82% rename from packages/datasource-hdt/test/datasources/HdtDatasource-test.js rename to packages/datasource-hdt/test/datasources/HdtDatasource-test.ts index 6f4aecdb..ff7b3baa 100644 --- a/packages/datasource-hdt/test/datasources/HdtDatasource-test.js +++ b/packages/datasource-hdt/test/datasources/HdtDatasource-test.ts @@ -1,14 +1,17 @@ /*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -let HdtDatasource = require('../../').datasources.HdtDatasource; - -let Datasource = require('@ldf/core').datasources.Datasource, - UrlData = require('@ldf/core').UrlData, - path = require('path'), - dataFactory = require('n3').DataFactory, - RdfString = require('rdf-string'), - { once } = require('events'); +import { datasources as hdtDatasources } from '../../index'; +import { datasources as coreDatasources, UrlData } from '@ldf/core'; +import type { Query } from '@ldf/core'; +import type { Quad } from 'rdf-js'; +import { stringQuadToQuad, type IStringQuad } from 'rdf-string'; +import * as path from 'path'; +import { DataFactory as dataFactory } from 'n3'; +import { once } from 'events'; + +const { HdtDatasource } = hdtDatasources; +const { Datasource } = coreDatasources; let exampleHdtFile = path.join(__dirname, '../../../../test/assets/test.hdt'); let exampleHdtFileWithBlanks = path.join(__dirname, '../../../../test/assets/test-blank.hdt'); @@ -23,26 +26,26 @@ describe('HdtDatasource', () => { let instance = new HdtDatasource({ dataFactory, file: exampleHdtFile }); instance.initialize(); expect(instance).toBeInstanceOf(HdtDatasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); it('should create Datasource objects', async () => { let instance = new HdtDatasource({ dataFactory, file: exampleHdtFile }); instance.initialize(); expect(instance).toBeInstanceOf(Datasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); }); describe('A HdtDatasource instance for an example HDT file', () => { - let datasource; + let datasource: InstanceType; function getDatasource() { return datasource; } beforeAll(async () => { datasource = new HdtDatasource({ dataFactory, file: exampleHdtFile }); datasource.initialize(); await once(datasource, 'initialized'); }); - afterAll(() => new Promise((resolve) => datasource.close(resolve))); + afterAll(() => new Promise((resolve) => datasource.close(resolve))); itShouldExecute(getDatasource, 'the empty query', @@ -96,14 +99,14 @@ describe('HdtDatasource', () => { }); describe('A HdtDatasource instance with blank nodes', () => { - let datasource; + let datasource: InstanceType; function getDatasource() { return datasource; } beforeAll(async () => { datasource = new HdtDatasource({ dataFactory, file: exampleHdtFileWithBlanks }); datasource.initialize(); await once(datasource, 'initialized'); }); - afterAll(() => new Promise((resolve) => datasource.close(resolve))); + afterAll(() => new Promise((resolve) => datasource.close(resolve))); itShouldExecute(getDatasource, 'the empty query', @@ -143,7 +146,7 @@ describe('HdtDatasource', () => { }); describe('A HdtDatasource instance with blank nodes and a blank node prefix', () => { - let datasource; + let datasource: InstanceType; function getDatasource() { return datasource; } beforeAll(async () => { datasource = new HdtDatasource({ @@ -154,7 +157,7 @@ describe('HdtDatasource', () => { datasource.initialize(); await once(datasource, 'initialized'); }); - afterAll(() => new Promise((resolve) => datasource.close(resolve))); + afterAll(() => new Promise((resolve) => datasource.close(resolve))); itShouldExecute(getDatasource, 'the empty query', @@ -194,14 +197,14 @@ describe('HdtDatasource', () => { }); }); -function itShouldExecute(getDatasource, name, query, - expectedResultsCount, expectedTotalCount, expectedTriples) { +function itShouldExecute(getDatasource: () => InstanceType, name: string, query: Query, + expectedResultsCount: number, expectedTotalCount: number, expectedTriples?: IStringQuad[]) { describe('executing ' + name, () => { - let resultsCount = 0, totalCount, triples = []; + let resultsCount = 0, totalCount: number | undefined, triples: Quad[] = []; beforeAll(async () => { let result = getDatasource().select(query); - result.getProperty('metadata', (metadata) => { totalCount = metadata.totalCount; }); - result.on('data', (triple) => { resultsCount++; expectedTriples && triples.push(triple); }); + result.getProperty('metadata', (metadata: { totalCount: number }) => { totalCount = metadata.totalCount; }); + result.on('data', (triple: Quad) => { resultsCount++; expectedTriples && triples.push(triple); }); await once(result, 'end'); }); @@ -217,7 +220,7 @@ function itShouldExecute(getDatasource, name, query, it('should emit the expected triples', () => { expect(triples.length).toBe(expectedTriples.length); for (let i = 0; i < expectedTriples.length; i++) - expect(triples[i]).toEqual(RdfString.stringQuadToQuad(expectedTriples[i], dataFactory)); + expect(triples[i]).toEqual(stringQuadToQuad(expectedTriples[i], dataFactory)); }); } }); diff --git a/packages/datasource-jsonld/test/datasources/JsonLdDatasource-test.js b/packages/datasource-jsonld/test/datasources/JsonLdDatasource-test.ts similarity index 79% rename from packages/datasource-jsonld/test/datasources/JsonLdDatasource-test.js rename to packages/datasource-jsonld/test/datasources/JsonLdDatasource-test.ts index 73191087..77e613d0 100644 --- a/packages/datasource-jsonld/test/datasources/JsonLdDatasource-test.js +++ b/packages/datasource-jsonld/test/datasources/JsonLdDatasource-test.ts @@ -2,11 +2,15 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { once } from 'events'; -let JsonLdDatasource = require('../../').datasources.JsonLdDatasource; +import { datasources as jsonLdDatasources } from '../../index'; +import { datasources as coreDatasources } from '@ldf/core'; +import type { Query } from '@ldf/core'; +import type { Quad } from 'rdf-js'; +import * as path from 'path'; +import { DataFactory as dataFactory } from 'n3'; -let Datasource = require('@ldf/core').datasources.Datasource, - path = require('path'), - dataFactory = require('n3').DataFactory; +const { JsonLdDatasource } = jsonLdDatasources; +const { Datasource } = coreDatasources; let exampleJsonLdUrl = 'file://' + path.join(__dirname, '../../../../test/assets/test.jsonld'); @@ -19,13 +23,13 @@ describe('JsonLdDatasource', () => { it('should be a JsonLdDatasource constructor', async () => { let instance = new JsonLdDatasource({ dataFactory, url: exampleJsonLdUrl }); expect(instance).toBeInstanceOf(JsonLdDatasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); it('should create Datasource objects', async () => { let instance = new JsonLdDatasource({ dataFactory, url: exampleJsonLdUrl }); expect(instance).toBeInstanceOf(Datasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); }); @@ -35,7 +39,7 @@ describe('JsonLdDatasource', () => { datasource.initialize(); await once(datasource, 'initialized'); }); - afterAll(() => new Promise((resolve) => datasource.close(resolve))); + afterAll(() => new Promise((resolve) => datasource.close(resolve))); itShouldExecute(datasource, 'the empty query', @@ -94,13 +98,13 @@ describe('JsonLdDatasource', () => { }); }); -function itShouldExecute(datasource, name, query, expectedResultsCount, expectedTotalCount) { +function itShouldExecute(datasource: InstanceType, name: string, query: Query, expectedResultsCount: number, expectedTotalCount: number) { describe('executing ' + name, () => { - let resultsCount = 0, totalCount; + let resultsCount = 0, totalCount: number | undefined; beforeAll(async () => { let result = datasource.select(query); - result.getProperty('metadata', (metadata) => { totalCount = metadata.totalCount; }); - result.on('data', (triple) => { resultsCount++; }); + result.getProperty('metadata', (metadata: { totalCount: number }) => { totalCount = metadata.totalCount; }); + result.on('data', (_triple: Quad) => { resultsCount++; }); await once(result, 'end'); }); diff --git a/packages/datasource-n3/test/datasources/N3Datasource-test.js b/packages/datasource-n3/test/datasources/N3Datasource-test.ts similarity index 77% rename from packages/datasource-n3/test/datasources/N3Datasource-test.js rename to packages/datasource-n3/test/datasources/N3Datasource-test.ts index bfe8fba2..a2cf5db9 100644 --- a/packages/datasource-n3/test/datasources/N3Datasource-test.js +++ b/packages/datasource-n3/test/datasources/N3Datasource-test.ts @@ -2,11 +2,15 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { once } from 'events'; -let N3Datasource = require('../../').datasources.N3Datasource; +import { datasources as n3Datasources } from '../../index'; +import { datasources as coreDatasources } from '@ldf/core'; +import type { Query } from '@ldf/core'; +import type { Quad } from 'rdf-js'; +import * as path from 'path'; +import { DataFactory as dataFactory } from 'n3'; -let Datasource = require('@ldf/core').datasources.Datasource, - path = require('path'), - dataFactory = require('n3').DataFactory; +const { N3Datasource } = n3Datasources; +const { Datasource } = coreDatasources; let exampleTurtleUrl = 'file://' + path.join(__dirname, '../../../../test/assets/test.ttl'); @@ -19,13 +23,13 @@ describe('N3Datasource', () => { it('should be a N3Datasource constructor', async () => { let instance = new N3Datasource({ dataFactory, url: exampleTurtleUrl }); expect(instance).toBeInstanceOf(N3Datasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); it('should create Datasource objects', async () => { let instance = new N3Datasource({ dataFactory, url: exampleTurtleUrl }); expect(instance).toBeInstanceOf(Datasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); }); @@ -35,7 +39,7 @@ describe('N3Datasource', () => { datasource.initialize(); await once(datasource, 'initialized'); }); - afterAll(() => new Promise((resolve) => datasource.close(resolve))); + afterAll(() => new Promise((resolve) => datasource.close(resolve))); itShouldExecute(datasource, 'the empty query', @@ -84,13 +88,13 @@ describe('N3Datasource', () => { }); }); -function itShouldExecute(datasource, name, query, expectedResultsCount, expectedTotalCount) { +function itShouldExecute(datasource: InstanceType, name: string, query: Query, expectedResultsCount: number, expectedTotalCount: number) { describe('executing ' + name, () => { - let resultsCount = 0, totalCount; + let resultsCount = 0, totalCount: number | undefined; beforeAll(async () => { let result = datasource.select(query); - result.getProperty('metadata', (metadata) => { totalCount = metadata.totalCount; }); - result.on('data', (triple) => { resultsCount++; }); + result.getProperty('metadata', (metadata: { totalCount: number }) => { totalCount = metadata.totalCount; }); + result.on('data', (_triple: Quad) => { resultsCount++; }); await once(result, 'end'); }); diff --git a/packages/datasource-rdfa/test/datasources/RdfaDatasource-test.js b/packages/datasource-rdfa/test/datasources/RdfaDatasource-test.ts similarity index 77% rename from packages/datasource-rdfa/test/datasources/RdfaDatasource-test.js rename to packages/datasource-rdfa/test/datasources/RdfaDatasource-test.ts index e54903ff..06f64191 100644 --- a/packages/datasource-rdfa/test/datasources/RdfaDatasource-test.js +++ b/packages/datasource-rdfa/test/datasources/RdfaDatasource-test.ts @@ -2,11 +2,15 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { once } from 'events'; -let RdfaDatasource = require('../../').datasources.RdfaDatasource; +import { datasources as rdfaDatasources } from '../../index'; +import { datasources as coreDatasources } from '@ldf/core'; +import type { Query } from '@ldf/core'; +import type { Quad } from 'rdf-js'; +import * as path from 'path'; +import { DataFactory as dataFactory } from 'n3'; -let Datasource = require('@ldf/core').datasources.Datasource, - path = require('path'), - dataFactory = require('n3').DataFactory; +const { RdfaDatasource } = rdfaDatasources; +const { Datasource } = coreDatasources; let exampleRdfaUrl = 'file://' + path.join(__dirname, '../../../../test/assets/test.html'); @@ -19,13 +23,13 @@ describe('RdfaDatasource', () => { it('should be a RdfaDatasource constructor', async () => { let instance = new RdfaDatasource({ dataFactory, url: exampleRdfaUrl }); expect(instance).toBeInstanceOf(RdfaDatasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); it('should create Datasource objects', async () => { let instance = new RdfaDatasource({ dataFactory, url: exampleRdfaUrl }); expect(instance).toBeInstanceOf(Datasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); }); @@ -35,7 +39,7 @@ describe('RdfaDatasource', () => { datasource.initialize(); await once(datasource, 'initialized'); }); - afterAll(() => new Promise((resolve) => datasource.close(resolve))); + afterAll(() => new Promise((resolve) => datasource.close(resolve))); itShouldExecute(datasource, 'the empty query', @@ -84,13 +88,13 @@ describe('RdfaDatasource', () => { }); }); -function itShouldExecute(datasource, name, query, expectedResultsCount, expectedTotalCount) { +function itShouldExecute(datasource: InstanceType, name: string, query: Query, expectedResultsCount: number, expectedTotalCount: number) { describe('executing ' + name, () => { - let resultsCount = 0, totalCount; + let resultsCount = 0, totalCount: number | undefined; beforeAll(async () => { let result = datasource.select(query); - result.getProperty('metadata', (metadata) => { totalCount = metadata.totalCount; }); - result.on('data', (triple) => { resultsCount++; }); + result.getProperty('metadata', (metadata: { totalCount: number }) => { totalCount = metadata.totalCount; }); + result.on('data', (_triple: Quad) => { resultsCount++; }); await once(result, 'end'); }); diff --git a/packages/datasource-sparql/test/datasources/SparqlDatasource-test.js b/packages/datasource-sparql/test/datasources/SparqlDatasource-test.ts similarity index 84% rename from packages/datasource-sparql/test/datasources/SparqlDatasource-test.js rename to packages/datasource-sparql/test/datasources/SparqlDatasource-test.ts index b39dcdcb..42207171 100644 --- a/packages/datasource-sparql/test/datasources/SparqlDatasource-test.js +++ b/packages/datasource-sparql/test/datasources/SparqlDatasource-test.ts @@ -1,28 +1,49 @@ /*! @license MIT ©2013-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll, vi } from 'vitest'; +import type { Mock } from 'vitest'; import { createHttpResponse, streamLength } from '../../../../test/test-helpers'; import { once } from 'events'; -let SparqlDatasource = require('../../').datasources.SparqlDatasource; +import { datasources as sparqlDatasources } from '../../index'; +import { datasources as coreDatasources } from '@ldf/core'; +import type { Quad } from 'rdf-js'; +import type { Query } from '@ldf/core'; +import type { AsyncIterator } from 'asynciterator'; +import * as fs from 'fs'; +import * as path from 'path'; +import { parse as parseUrl } from 'url'; +import { DataFactory as dataFactory } from 'n3'; + +const { SparqlDatasource } = sparqlDatasources; +const { Datasource } = coreDatasources; + +type HttpResponse = ReturnType; + +let jsonResult = fs.readFileSync(path.join(__dirname, '../../../../test/assets/sparql-quads-response.json'), 'utf8'); +let countResult = '"c"\n12345678\n'; -let Datasource = require('@ldf/core').datasources.Datasource, - fs = require('fs'), - path = require('path'), - URL = require('url'), - dataFactory = require('n3').DataFactory; +interface RequestOptions { + url: string; + headers?: Record; + timeout?: number; +} +type RequestCallback = (error?: { message: string }) => void; -let jsonResult = fs.readFileSync(path.join(__dirname, '../../../../test/assets/sparql-quads-response.json')); -let countResult = '"c"\n12345678\n'; +interface RequestStub extends Mock<(options: RequestOptions, callback?: RequestCallback) => HttpResponse> { + onFirstCall(value: HttpResponse): void; + onSecondCall(value: HttpResponse): void; +} // Mimics sinon's onFirstCall/onSecondCall: each configured return value stays // tied to that call index until overridden, and survives mockClear (unlike // mockReturnValueOnce, which is consumed after a single use). -function createRequestStub() { - let responses = []; - let request = vi.fn(() => responses[request.mock.calls.length - 1]); - request.onFirstCall = (value) => { responses[0] = value; }; - request.onSecondCall = (value) => { responses[1] = value; }; - return request; +function createRequestStub(): RequestStub { + let responses: HttpResponse[] = []; + let fn = vi.fn(() => responses[fn.mock.calls.length - 1]); + return Object.assign(fn, { + onFirstCall: (value: HttpResponse) => { responses[0] = value; }, + onSecondCall: (value: HttpResponse) => { responses[1] = value; }, + }); } describe('SparqlDatasource', () => { @@ -68,7 +89,7 @@ describe('SparqlDatasource', () => { }); it('should throw an error when trying to execute an unsupported query', async () => { - let error = await new Promise((resolve) => datasource.select({ features: { a: true, b: true } }, resolve)); + let error = await new Promise((resolve) => datasource.select({ features: { a: true, b: true } }, resolve)); expect(error).toBeInstanceOf(Error); expect(error).toHaveProperty('message', 'The datasource does not support the given query'); }); @@ -207,7 +228,7 @@ describe('SparqlDatasource', () => { null /* count should be cached, since this pattern already occurred above */); describe('when invalid JSON is returned in response to the data query', () => { - let result, error; + let result: AsyncIterator, error: Error; beforeAll(async () => { request.mockClear(); request.onFirstCall(createHttpResponse('invalid', 'application/sparql-results+json')); @@ -223,7 +244,7 @@ describe('SparqlDatasource', () => { }); describe('when invalid JSON is returned in response to the count query', () => { - let result, error; + let result: AsyncIterator, error: Error; beforeAll(async () => { request.mockClear(); request.onFirstCall(createHttpResponse(jsonResult, 'application/sparql-results+json')); @@ -239,13 +260,13 @@ describe('SparqlDatasource', () => { }); describe('when the data query request errors', () => { - let result, error; + let result: AsyncIterator, error: Error; beforeAll(async () => { request.mockClear(); let query = { subject: dataFactory.namedNode('abcde'), features: { quadPattern: true } }; result = datasource.select(query); let errorEvent = once(result, 'error'); - request.mock.calls[0][1](new Error('query response error')); + request.mock.calls[0][1]?.(new Error('query response error')); [error] = await errorEvent; }); @@ -255,14 +276,16 @@ describe('SparqlDatasource', () => { }); describe('when the count query request errors', () => { - let totalCount; + let totalCount: number | undefined; beforeAll(async () => { request.mockClear(); let query = { subject: dataFactory.namedNode('abcdef'), features: { quadPattern: true } }; let result = datasource.select(query); - request.mock.results[1].value.emit('error', new Error()); - await new Promise((resolve) => { - result.getProperty('metadata', (metadata) => { totalCount = metadata.totalCount; resolve(); }); + let secondCallResult = request.mock.results[1]; + if (secondCallResult.type === 'return') + secondCallResult.value.emit('error', new Error()); + await new Promise((resolve) => { + result.getProperty('metadata', (metadata: { totalCount: number }) => { totalCount = metadata.totalCount; resolve(); }); }); }); @@ -309,31 +332,31 @@ describe('SparqlDatasource', () => { }); }); -function itShouldExecute(datasource, request, name, query, constructQuery, countQuery) { +function itShouldExecute(datasource: InstanceType, request: RequestStub, name: string, query: Query, constructQuery: string, countQuery: string | null) { describe('executing ' + name, () => { - let result, totalCount; + let result: AsyncIterator, totalCount: number | undefined; beforeAll(async () => { request.mockClear(); request.onFirstCall(createHttpResponse(jsonResult, 'application/sparql-results+json')); request.onSecondCall(createHttpResponse(countResult, 'text/csv')); result = datasource.select(query); - await new Promise((resolve) => { - result.getProperty('metadata', (metadata) => { totalCount = metadata.totalCount; resolve(); }); + await new Promise((resolve) => { + result.getProperty('metadata', (metadata: { totalCount: number }) => { totalCount = metadata.totalCount; resolve(); }); }); }); it('should request a matching CONSTRUCT query', () => { expect(request).toHaveBeenCalled(); - let url = URL.parse(request.mock.calls[0][0].url, true); - expect(url.protocol + '//' + url.host + url.pathname).toBe('http://ex.org/sparql'); + let url = parseUrl(request.mock.calls[0][0].url, true); + expect(`${String(url.protocol)}//${String(url.host)}${String(url.pathname)}`).toBe('http://ex.org/sparql'); expect(url.query.query).toBe(constructQuery); }); if (countQuery) { it('should request a matching COUNT query', () => { expect(request).toHaveBeenCalledTimes(2); - let url = URL.parse(request.mock.calls[1][0].url, true); - expect(url.protocol + '//' + url.host + url.pathname).toBe('http://ex.org/sparql'); + let url = parseUrl(request.mock.calls[1][0].url, true); + expect(`${String(url.protocol)}//${String(url.host)}${String(url.pathname)}`).toBe('http://ex.org/sparql'); expect(url.query.query).toBe(countQuery); }); } diff --git a/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.js b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts similarity index 61% rename from packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.js rename to packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts index 97d463e8..bf1237aa 100644 --- a/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.js +++ b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts @@ -1,16 +1,24 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll, vi } from 'vitest'; -import { DummyServer } from '../../../../test/DummyServer'; -let QuadPatternFragmentsController = require('../../').controllers.QuadPatternFragmentsController; +import type { Mock } from 'vitest'; +import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { controllers, views } from '../../index'; +import { datasources as coreDatasources, UrlData } from '@ldf/core'; +import type { DatasourceRegistry, Query, QueryFeatures, RouterRequest } from '@ldf/core'; +import type { Quad } from 'rdf-js'; +import { empty } from 'asynciterator'; +import type { AsyncIterator } from 'asynciterator'; -let request = require('supertest'), - http = require('http'); +import * as http from 'http'; +import * as request from 'supertest'; +import { DataFactory as dataFactory } from 'n3'; -let QuadPatternFragmentsHtmlView = require('../../').views.quadpatternfragments.QuadPatternFragmentsHtmlView, - QuadPatternFragmentsRdfView = require('../../').views.quadpatternfragments.QuadPatternFragmentsRdfView, - UrlData = require('@ldf/core').UrlData, - dataFactory = require('n3').DataFactory; +const { QuadPatternFragmentsController } = controllers; +const { QuadPatternFragmentsHtmlView, QuadPatternFragmentsRdfView } = views.quadpatternfragments; +const { Datasource } = coreDatasources; + +type MutableQuery = Query & { features: QueryFeatures }; describe('QuadPatternFragmentsController', () => { describe('The QuadPatternFragmentsController module', () => { @@ -24,27 +32,40 @@ describe('QuadPatternFragmentsController', () => { }); describe('A QuadPatternFragmentsController instance with 3 routers', () => { - let controller, client, routerA, routerB, routerC, datasource, datasources, view, prefixes; + let controller: InstanceType & Partial, + client: ReturnType, + routerA: { extractQueryParams: Mock<(request: RouterRequest, query: Query) => void> }, + routerB: { extractQueryParams: Mock<(request: RouterRequest, query: Query) => void> }, + routerC: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, + datasource: InstanceType, + supportsQuerySpy: Mock<(query: Query) => boolean>, + selectSpy: Mock<(query: Query) => AsyncIterator>, + selectResult: AsyncIterator, + datasources: DatasourceRegistry, + view: InstanceType, + renderSpy: Mock['render']>, + prefixes: Record; beforeAll(() => { routerA = { extractQueryParams: vi.fn() }; routerB = { extractQueryParams: vi.fn(() => { throw new Error('second router error'); }) }; routerC = { - extractQueryParams: vi.fn((request, query) => { + extractQueryParams: vi.fn((request: RouterRequest, query: MutableQuery) => { query.features.datasource = true; query.features.other = true; query.datasource = '/my-datasource'; - query.other = 'other'; }), }; - datasource = { - title: 'My data', - supportsQuery: vi.fn().mockReturnValue(true), - select: vi.fn().mockReturnValue({ stream: 'items' }), - supportedFeatures: { quadPattern: true }, - }; + datasource = new Datasource({ dataFactory, title: 'My data' }); + supportsQuerySpy = vi.fn().mockReturnValue(true); + datasource.supportsQuery = supportsQuerySpy; + selectResult = empty(); + selectResult.setProperty('metadata', {}); + selectSpy = vi.fn().mockReturnValue(selectResult); + datasource.select = selectSpy; + datasource.supportedFeatures = { quadPattern: true }; datasources = { 'my-datasource': datasource }; - view = new QuadPatternFragmentsRdfView({ dataFactory }), - vi.spyOn(view, 'render'); + view = new QuadPatternFragmentsRdfView({ dataFactory }); + renderSpy = vi.spyOn(view, 'render'); prefixes = { a: 'a' }; controller = new QuadPatternFragmentsController({ urlData: new UrlData({ baseURL: 'https://example.org/base/?bar=foo' }), @@ -53,14 +74,14 @@ describe('QuadPatternFragmentsController', () => { views: [view], prefixes: prefixes, }); - client = request.agent(new DummyServer(controller), {}); + client = request.agent(DummyServer(controller)); }); function resetAll() { routerA.extractQueryParams.mockClear(); routerB.extractQueryParams.mockClear(); routerC.extractQueryParams.mockClear(); - datasource.supportsQuery.mockClear(); - datasource.select.mockClear(); + supportsQuerySpy.mockClear(); + selectSpy.mockClear(); } describe('receiving a request for a fragment', () => { @@ -77,7 +98,7 @@ describe('QuadPatternFragmentsController', () => { expect(args[0].url).toHaveProperty('path', '/my-datasource?a=b&c=d'); expect(args[0].url).toHaveProperty('pathname', '/my-datasource'); expect(args[0].url).toHaveProperty('query'); - expect(args[0].url.query).toEqual({ a: 'b', c: 'd' }); + expect(args[0].url?.query).toEqual({ a: 'b', c: 'd' }); expect(typeof args[1]).toBe('object'); expect(args[1]).toHaveProperty('features'); @@ -104,19 +125,19 @@ describe('QuadPatternFragmentsController', () => { it('should verify whether the data source supports the query', () => { let query = routerC.extractQueryParams.mock.calls[0][1]; - expect(datasource.supportsQuery).toHaveBeenCalledOnce(); - expect(datasource.supportsQuery).toHaveBeenCalledWith(query); + expect(supportsQuerySpy).toHaveBeenCalledOnce(); + expect(supportsQuerySpy).toHaveBeenCalledWith(query); }); it('should send the query to the right data source', () => { let query = routerC.extractQueryParams.mock.calls[0][1]; - expect(datasource.select).toHaveBeenCalledOnce(); - expect(datasource.select.mock.calls[0][0]).toBe(query); + expect(selectSpy).toHaveBeenCalledOnce(); + expect(selectSpy.mock.calls[0][0]).toBe(query); }); it('should pass the query result to the output view', () => { - expect(view.render).toHaveBeenCalledOnce(); - let args = view.render.mock.calls[0]; + expect(renderSpy).toHaveBeenCalledOnce(); + let args = renderSpy.mock.calls[0]; expect(typeof args[0]).toBe('object'); // settings expect(args[1]).toBeInstanceOf(http.IncomingMessage); @@ -124,9 +145,9 @@ describe('QuadPatternFragmentsController', () => { }); it('should pass the correct settings to the view', () => { - expect(view.render).toHaveBeenCalledOnce(); + expect(renderSpy).toHaveBeenCalledOnce(); let query = routerC.extractQueryParams.mock.calls[0][1]; - let settings = view.render.mock.calls[0][0]; + let settings = renderSpy.mock.calls[0][0]; expect(settings.datasource).toHaveProperty('title', 'My data'); expect(settings.datasource).toHaveProperty('index', 'https://example.org/#dataset'); @@ -140,9 +161,7 @@ describe('QuadPatternFragmentsController', () => { nextPageUrl: 'https://example.org/my-datasource?a=b&c=d&page=2', previousPageUrl: null, }); - expect(settings.results).toEqual({ - stream: 'items', - }); + expect(settings.results).toBe(selectResult); expect(settings.prefixes).toEqual(prefixes); expect(settings.query).toEqual(query); expect(settings.datasources).toEqual({ '/my-datasource': datasource }); @@ -153,68 +172,69 @@ describe('QuadPatternFragmentsController', () => { describe('receiving a request for an unsupported fragment', () => { beforeAll(async () => { resetAll(); - datasource.supportsQuery = vi.fn().mockReturnValue(false); + supportsQuerySpy = vi.fn().mockReturnValue(false); + datasource.supportsQuery = supportsQuerySpy; await client.get('/my-datasource?a=b&c=d'); }); it('should verify whether the data source supports the query', () => { let query = routerC.extractQueryParams.mock.calls[0][1]; - expect(datasource.supportsQuery).toHaveBeenCalledOnce(); - expect(datasource.supportsQuery).toHaveBeenCalledWith(query); + expect(supportsQuerySpy).toHaveBeenCalledOnce(); + expect(supportsQuerySpy).toHaveBeenCalledWith(query); }); it('should not send the query to the data source', () => { - expect(datasource.select).not.toHaveBeenCalled(); + expect(selectSpy).not.toHaveBeenCalled(); }); }); }); describe('A QuadPatternFragmentsController instance with 2 views', () => { - let controller, client, htmlView, rdfView; + let controller: InstanceType & Partial, + client: ReturnType, + htmlView: InstanceType, rdfView: InstanceType, + htmlRenderSpy: Mock['render']>, + rdfRenderSpy: Mock['render']>; beforeAll(() => { - let datasource = { - supportsQuery: vi.fn().mockReturnValue(true), - select: vi.fn().mockReturnValue({ - // Mocks AsyncIterator's own on(event, callback) signature. - // eslint-disable-next-line promise/prefer-await-to-callbacks - on: function (event, callback) { - if (event === 'end' || event === 'metadata') - setImmediate(callback, {}); - }, - }), - supportedFeatures: { triplePattern: true }, - }; + let datasource = new Datasource({ dataFactory }); + datasource.supportsQuery = vi.fn().mockReturnValue(true); + datasource.select = vi.fn(() => { + let it = empty(); + it.setProperty('metadata', {}); + return it; + }); + datasource.supportedFeatures = { triplePattern: true }; let router = { - extractQueryParams: function (request, query) { + extractQueryParams: function (request: RouterRequest, query: MutableQuery) { query.features.datasource = true; query.datasource = '/my-datasource'; }, }; htmlView = new QuadPatternFragmentsHtmlView(); rdfView = new QuadPatternFragmentsRdfView({ dataFactory }); - vi.spyOn(htmlView, 'render'); - vi.spyOn(rdfView, 'render'); + htmlRenderSpy = vi.spyOn(htmlView, 'render'); + rdfRenderSpy = vi.spyOn(rdfView, 'render'); controller = new QuadPatternFragmentsController({ routers: [router], datasources: { 'my-datasource': datasource }, views: [htmlView, rdfView], }); - client = request.agent(new DummyServer(controller), {}); + client = request.agent(DummyServer(controller)); }); function resetAll() { - htmlView.render.mockClear(); - rdfView.render.mockClear(); + htmlRenderSpy.mockClear(); + rdfRenderSpy.mockClear(); } describe('receiving a request without Accept header', () => { - let response; + let response: Awaited>; beforeAll(async () => { resetAll(); response = await client.get('/my-datasource'); }); it('should call the default view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); + expect(htmlRenderSpy).toHaveBeenCalledOnce(); }); it('should set the text/html content type', () => { @@ -227,14 +247,14 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of */*', () => { - let response; + let response: Awaited>; beforeAll(async () => { resetAll(); response = await client.get('/my-datasource').set('Accept', '*/*'); }); it('should call the HTML view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); + expect(htmlRenderSpy).toHaveBeenCalledOnce(); }); it('should set the text/html content type', () => { @@ -247,14 +267,14 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/html', () => { - let response; + let response: Awaited>; beforeAll(async () => { resetAll(); response = await client.get('/my-datasource').set('Accept', 'text/html'); }); it('should call the HTML view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); + expect(htmlRenderSpy).toHaveBeenCalledOnce(); }); it('should set the text/html content type', () => { @@ -267,14 +287,14 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/turtle', () => { - let response; + let response: Awaited>; beforeAll(async () => { resetAll(); response = await client.get('/my-datasource').set('Accept', 'text/turtle'); }); it('should call the Turtle view', () => { - expect(rdfView.render).toHaveBeenCalledOnce(); + expect(rdfRenderSpy).toHaveBeenCalledOnce(); }); it('should set the text/turtle content type', () => { @@ -287,14 +307,14 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/n3', () => { - let response; + let response: Awaited>; beforeAll(async () => { resetAll(); response = await client.get('/my-datasource').set('Accept', 'text/n3'); }); it('should call the Turtle view', () => { - expect(rdfView.render).toHaveBeenCalledOnce(); + expect(rdfRenderSpy).toHaveBeenCalledOnce(); }); it('should set the text/n3 content type', () => { @@ -308,15 +328,14 @@ describe('QuadPatternFragmentsController', () => { }); describe('A QuadPatternFragmentsController instance without matching view', () => { - let controller, client; + let controller: InstanceType & Partial, client: ReturnType; beforeAll(() => { - let datasource = { - supportsQuery: vi.fn().mockReturnValue(true), - select: vi.fn(), - supportedFeatures: { triplePattern: true }, - }; + let datasource = new Datasource({ dataFactory }); + datasource.supportsQuery = vi.fn().mockReturnValue(true); + datasource.select = vi.fn(() => empty()); + datasource.supportedFeatures = { triplePattern: true }; let router = { - extractQueryParams: function (request, query) { + extractQueryParams: function (request: RouterRequest, query: MutableQuery) { query.features.datasource = true; query.datasource = '/my-datasource'; }, @@ -325,11 +344,11 @@ describe('QuadPatternFragmentsController', () => { routers: [router], datasources: { 'my-datasource': datasource }, }); - client = request.agent(new DummyServer(controller), {}); + client = request.agent(DummyServer(controller)); }); describe('receiving a request without Accept header', () => { - let response; + let response: Awaited>; beforeAll(async () => { response = await client.get('/my-datasource'); }); @@ -348,7 +367,7 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/html', () => { - let response; + let response: Awaited>; beforeAll(async () => { response = await client.get('/my-datasource').set('Accept', 'text/html'); }); @@ -368,27 +387,29 @@ describe('QuadPatternFragmentsController', () => { }); describe('A QuadPatternFragmentsController instance with a datasource that synchronously errors', () => { - let controller, client, router, datasource, error, view; + let controller: InstanceType & Partial, + client: ReturnType, + router: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, + datasource: InstanceType, error: Error, view: InstanceType; beforeAll(() => { router = { - extractQueryParams: vi.fn((request, query) => { + extractQueryParams: vi.fn((request: RouterRequest, query: MutableQuery) => { query.features.datasource = true; query.datasource = '/my-datasource'; }), }; - error = new Error('datasource error'), - datasource = { - supportsQuery: vi.fn().mockReturnValue(true), - select: vi.fn(() => { throw error; }), - supportedFeatures: { triplePattern: true }, - }; - view = new QuadPatternFragmentsRdfView({ dataFactory }), + error = new Error('datasource error'); + datasource = new Datasource({ dataFactory }); + datasource.supportsQuery = vi.fn().mockReturnValue(true); + datasource.select = vi.fn(() => { throw error; }); + datasource.supportedFeatures = { triplePattern: true }; + view = new QuadPatternFragmentsRdfView({ dataFactory }); controller = new QuadPatternFragmentsController({ routers: [router], views: [view], datasources: { '/my-datasource': datasource }, }); - client = request.agent(new DummyServer(controller), {}); + client = request.agent(DummyServer(controller)); }); function resetAll() { router.extractQueryParams.mockClear(); @@ -407,30 +428,36 @@ describe('QuadPatternFragmentsController', () => { }); describe('A QuadPatternFragmentsController instance with a datasource that asynchronously errors', () => { - let controller, client, router, datasource, error, view; + let controller: InstanceType & Partial, + client: ReturnType, + router: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, + datasource: InstanceType, error: Error, view: InstanceType; beforeAll(() => { router = { - extractQueryParams: vi.fn((request, query) => { + extractQueryParams: vi.fn((request: RouterRequest, query: MutableQuery) => { query.features.datasource = true; query.datasource = '/my-datasource'; }), }; - error = new Error('datasource error'), - datasource = { - supportsQuery: vi.fn().mockReturnValue(true), - // Mocks Datasource.select's own callback-based signature. - // eslint-disable-next-line promise/prefer-await-to-callbacks - select: function (query, callback) { setImmediate(callback.bind(null, error)); }, - supportedFeatures: { triplePattern: true }, - }; - view = new QuadPatternFragmentsRdfView({ dataFactory }), + error = new Error('datasource error'); + datasource = new Datasource({ dataFactory }); + datasource.supportsQuery = vi.fn().mockReturnValue(true); + // Mocks Datasource.select's own callback-based signature. + /* eslint-disable promise/prefer-await-to-callbacks */ + datasource.select = vi.fn((query: Query, callback?: (error?: Error) => void) => { + setImmediate(() => callback?.(error)); + return empty(); + }); + /* eslint-enable promise/prefer-await-to-callbacks */ + datasource.supportedFeatures = { triplePattern: true }; + view = new QuadPatternFragmentsRdfView({ dataFactory }); view.render = vi.fn(); // avoid writing a partial body controller = new QuadPatternFragmentsController({ routers: [router], views: [view], datasources: { 'my-datasource': datasource }, }); - client = request.agent(new DummyServer(controller), {}); + client = request.agent(DummyServer(controller)); }); function resetAll() { router.extractQueryParams.mockClear(); diff --git a/packages/feature-qpf/test/routers/QuadPatternRouter-test.js b/packages/feature-qpf/test/routers/QuadPatternRouter-test.js deleted file mode 100644 index 5b419f3d..00000000 --- a/packages/feature-qpf/test/routers/QuadPatternRouter-test.js +++ /dev/null @@ -1,475 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ - -import { describe, it, expect } from 'vitest'; -import { extractQueryParams } from '../../../../test/test-helpers'; -let QuadPatternRouter = require('../../').routers.QuadPatternRouter; -const dataFactory = require('n3').DataFactory; - -describe('QuadPatternRouter', () => { - describe('The QuadPatternRouter module', () => { - it('should be a function', () => { - expect(typeof QuadPatternRouter).toBe('function'); - }); - - it('should be a QuadPatternRouter constructor', () => { - expect(new QuadPatternRouter({})).toBeInstanceOf(QuadPatternRouter); - }); - }); - - describe('A QuadPatternRouter instance', () => { - let router = new QuadPatternRouter({ dataFactory }); - - describe('extractUrlParams', () => { - describe('with an existing query', () => { - [ - [ - 'a URL without query parameters', - 'http://example.org/', - 'should not change the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an empty subject parameter', - 'http://example.org/?subject=', - 'should not add the subject to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI subject parameter', - 'http://example.org/?subject=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with an IRI subject parameter in angular brackets', - 'http://example.org/?subject=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a variable subject parameter', - 'http://example.org/?subject=%3Ffoo', - 'should not add the subject to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a blank subject parameter', - 'http://example.org/?subject=_:foo', - 'should not add the subject to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a literal subject parameter', - 'http://example.org/?subject=%22foo%22', - 'should not add the subject to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an empty predicate parameter', - 'http://example.org/?predicate=', - 'should not add the predicate to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI predicate parameter', - 'http://example.org/?predicate=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the predicate to the query', - { a: 1, features: { a: true } }, - { a: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with an IRI predicate parameter in angular brackets', - 'http://example.org/?predicate=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the predicate to the query', - { a: 1, features: { a: true } }, - { a: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a variable predicate parameter', - 'http://example.org/?predicate=%3Ffoo', - 'should not add the predicate to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a blank predicate parameter', - 'http://example.org/?predicate=_:foo', - 'should not add the predicate to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a literal predicate parameter', - 'http://example.org/?predicate=%22foo%22', - 'should not add the predicate to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an empty object parameter', - 'http://example.org/?object=', - 'should not add the object to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI object parameter', - 'http://example.org/?object=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with an IRI object parameter in angular brackets', - 'http://example.org/?object=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a variable object parameter', - 'http://example.org/?object=%3Ffoo', - 'should not add the object to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a blank object parameter', - 'http://example.org/?object=_:foo', - 'should not add the object to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a literal object parameter', - 'http://example.org/?object=%22foo%22', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo') }, - ], - [ - 'a URL with a language literal object parameter', - 'http://example.org/?object=%22foo%22@nl-be', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', 'nl-be') }, - ], - [ - 'a URL with a typed literal object parameter', - 'http://example.org/?object=%22foo%22%5E%5Ehttp%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, - ], - [ - 'a URL with a typed literal object parameter in angular brackets', - 'http://example.org/?object=%22foo%22%5E%5E%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, - ], - [ - 'a URL with an empty graph parameter', - 'http://example.org/?graph=', - 'should not add the graph to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI graph parameter', - 'http://example.org/?graph=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with an IRI graph parameter in angular brackets', - 'http://example.org/?graph=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a variable graph parameter', - 'http://example.org/?graph=%3Ffoo', - 'should not add the graph to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a blank graph parameter', - 'http://example.org/?graph=_:foo', - 'should not add the graph to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a literal graph parameter', - 'http://example.org/?graph=%22foo%22', - 'should not add the graph to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'the default graph as graph parameter', - 'http://example.org/?graph=urn%3Aldf%3AdefaultGraph', - 'should add the default graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.defaultGraph() }, - ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); - }); - }); - }); - - describe('A QuadPatternRouter instance with prefixes', () => { - let router = new QuadPatternRouter({ - prefixes: { - foo: 'http://example.org/foo#', - http: 'http://www.w3.org/2011/http#', - }, - dataFactory, - }); - - describe('extractUrlParams', () => { - describe('with an existing query', () => { - [ - [ - 'a URL without query parameters', - 'http://example.org/', - 'should not change the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an empty subject parameter', - 'http://example.org/?subject=', - 'should not add the subject to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI subject parameter', - 'http://example.org/?subject=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name subject parameter', - 'http://example.org/?subject=foo%3Abar', - 'should add the expanded subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name subject parameter with the "http" prefix', - 'http://example.org/?subject=http%3AConnection', - 'should add the expanded subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, - ], - [ - 'a URL with a prefixed name subject parameter with an unknown prefix', - 'http://example.org/?subject=bar%3Afoo', - 'should add the non-expanded subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('bar:foo') }, - ], - [ - 'a URL with an IRI subject parameter in angular brackets', - 'http://example.org/?subject=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the non-expanded subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name subject parameter in angular brackets', - 'http://example.org/?subject=%3Cfoo%3Abar%3E', - 'should add the non-expanded subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('foo:bar') }, - ], - [ - 'a URL with an empty predicate parameter', - 'http://example.org/?predicate=', - 'should not add the predicate to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI predicate parameter', - 'http://example.org/?predicate=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name predicate parameter', - 'http://example.org/?predicate=foo%3Abar', - 'should add the expanded predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name predicate parameter with the "http" prefix', - 'http://example.org/?predicate=http%3Aauthority', - 'should add the expanded predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://www.w3.org/2011/http#authority') }, - ], - [ - 'a URL with a prefixed name predicate parameter with an unknown prefix', - 'http://example.org/?predicate=bar%3Afoo', - 'should add the non-expanded predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('bar:foo') }, - ], - [ - 'a URL with an IRI predicate parameter in angular brackets', - 'http://example.org/?predicate=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the non-expanded predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name predicate parameter in angular brackets', - 'http://example.org/?predicate=%3Cfoo%3Abar%3E', - 'should add the non-expanded predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('foo:bar') }, - ], - [ - 'a URL with an empty object parameter', - 'http://example.org/?object=', - 'should not add the object to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI object parameter', - 'http://example.org/?object=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name object parameter', - 'http://example.org/?object=foo%3Abar', - 'should add the expanded object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name object parameter with the "http" prefix', - 'http://example.org/?object=http%3AConnection', - 'should add the expanded object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, - ], - [ - 'a URL with a prefixed name object parameter with an unknown prefix', - 'http://example.org/?object=bar%3Afoo', - 'should add the non-expanded object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('bar:foo') }, - ], - [ - 'a URL with an IRI object parameter in angular brackets', - 'http://example.org/?object=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the non-expanded object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name object parameter in angular brackets', - 'http://example.org/?object=%3Cfoo%3Abar%3E', - 'should add the non-expanded object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('foo:bar') }, - ], - [ - 'a URL with a typed literal object parameter', - 'http://example.org/?object=%22foo%22%5E%5Ehttp%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, - ], - [ - 'a URL with a typed literal object parameter in angular brackets', - 'http://example.org/?object=%22foo%22%5E%5E%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, - ], - [ - 'a URL with a prefixed literal object parameter', - 'http://example.org/?object=%22foo%22%5E%5Efoo%3Abar', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, - ], - [ - 'a URL with an empty graph parameter', - 'http://example.org/?graph=', - 'should not add the graph to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI graph parameter', - 'http://example.org/?graph=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name graph parameter', - 'http://example.org/?graph=foo%3Abar', - 'should add the expanded graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name graph parameter with the "http" prefix', - 'http://example.org/?graph=http%3AConnection', - 'should add the expanded graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, - ], - [ - 'a URL with a prefixed name graph parameter with an unknown prefix', - 'http://example.org/?graph=bar%3Afoo', - 'should add the non-expanded graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('bar:foo') }, - ], - [ - 'a URL with an IRI graph parameter in angular brackets', - 'http://example.org/?graph=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the non-expanded graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name graph parameter in angular brackets', - 'http://example.org/?graph=%3Cfoo%3Abar%3E', - 'should add the non-expanded graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('foo:bar') }, - ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); - }); - }); - }); -}); diff --git a/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts b/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts new file mode 100644 index 00000000..c23edf17 --- /dev/null +++ b/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts @@ -0,0 +1,482 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ + +import { describe, it, expect } from 'vitest'; +import { extractQueryParams } from '../../../../test/test-helpers'; +import { routers } from '../../index'; +import type { Query } from '@ldf/core'; +import { DataFactory as dataFactory } from 'n3'; + +const { QuadPatternRouter } = routers; + +describe('QuadPatternRouter', () => { + describe('The QuadPatternRouter module', () => { + it('should be a function', () => { + expect(typeof QuadPatternRouter).toBe('function'); + }); + + it('should be a QuadPatternRouter constructor', () => { + expect(new QuadPatternRouter({})).toBeInstanceOf(QuadPatternRouter); + }); + }); + + describe('A QuadPatternRouter instance', () => { + let router = new QuadPatternRouter({ dataFactory }); + + describe('extractUrlParams', () => { + describe('with an existing query', () => { + ( + [ + [ + 'a URL without query parameters', + 'http://example.org/', + 'should not change the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an empty subject parameter', + 'http://example.org/?subject=', + 'should not add the subject to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an IRI subject parameter', + 'http://example.org/?subject=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the subject to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with an IRI subject parameter in angular brackets', + 'http://example.org/?subject=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the subject to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a variable subject parameter', + 'http://example.org/?subject=%3Ffoo', + 'should not add the subject to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with a blank subject parameter', + 'http://example.org/?subject=_:foo', + 'should not add the subject to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with a literal subject parameter', + 'http://example.org/?subject=%22foo%22', + 'should not add the subject to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an empty predicate parameter', + 'http://example.org/?predicate=', + 'should not add the predicate to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an IRI predicate parameter', + 'http://example.org/?predicate=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the predicate to the query', + { limit: 1, features: { a: true } }, + { limit: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with an IRI predicate parameter in angular brackets', + 'http://example.org/?predicate=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the predicate to the query', + { limit: 1, features: { a: true } }, + { limit: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a variable predicate parameter', + 'http://example.org/?predicate=%3Ffoo', + 'should not add the predicate to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with a blank predicate parameter', + 'http://example.org/?predicate=_:foo', + 'should not add the predicate to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with a literal predicate parameter', + 'http://example.org/?predicate=%22foo%22', + 'should not add the predicate to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an empty object parameter', + 'http://example.org/?object=', + 'should not add the object to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an IRI object parameter', + 'http://example.org/?object=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with an IRI object parameter in angular brackets', + 'http://example.org/?object=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a variable object parameter', + 'http://example.org/?object=%3Ffoo', + 'should not add the object to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with a blank object parameter', + 'http://example.org/?object=_:foo', + 'should not add the object to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with a literal object parameter', + 'http://example.org/?object=%22foo%22', + 'should add the object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo') }, + ], + [ + 'a URL with a language literal object parameter', + 'http://example.org/?object=%22foo%22@nl-be', + 'should add the object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', 'nl-be') }, + ], + [ + 'a URL with a typed literal object parameter', + 'http://example.org/?object=%22foo%22%5E%5Ehttp%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + ], + [ + 'a URL with a typed literal object parameter in angular brackets', + 'http://example.org/?object=%22foo%22%5E%5E%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + ], + [ + 'a URL with an empty graph parameter', + 'http://example.org/?graph=', + 'should not add the graph to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an IRI graph parameter', + 'http://example.org/?graph=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the graph to the query', + { limit: 1 }, + { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with an IRI graph parameter in angular brackets', + 'http://example.org/?graph=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the graph to the query', + { limit: 1 }, + { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a variable graph parameter', + 'http://example.org/?graph=%3Ffoo', + 'should not add the graph to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with a blank graph parameter', + 'http://example.org/?graph=_:foo', + 'should not add the graph to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with a literal graph parameter', + 'http://example.org/?graph=%22foo%22', + 'should not add the graph to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'the default graph as graph parameter', + 'http://example.org/?graph=urn%3Aldf%3AdefaultGraph', + 'should add the default graph to the query', + { limit: 1 }, + { limit: 1, features: { quadPattern: true }, graph: dataFactory.defaultGraph() }, + ], + ] as Array<[string, string, string, Query, Query]> + ) + .forEach((args) => { extractQueryParams(router, ...args); }); + }); + }); + }); + + describe('A QuadPatternRouter instance with prefixes', () => { + let router = new QuadPatternRouter({ + prefixes: { + foo: 'http://example.org/foo#', + http: 'http://www.w3.org/2011/http#', + }, + dataFactory, + }); + + describe('extractUrlParams', () => { + describe('with an existing query', () => { + ( + [ + [ + 'a URL without query parameters', + 'http://example.org/', + 'should not change the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an empty subject parameter', + 'http://example.org/?subject=', + 'should not add the subject to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an IRI subject parameter', + 'http://example.org/?subject=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the subject to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name subject parameter', + 'http://example.org/?subject=foo%3Abar', + 'should add the expanded subject to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name subject parameter with the "http" prefix', + 'http://example.org/?subject=http%3AConnection', + 'should add the expanded subject to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, + ], + [ + 'a URL with a prefixed name subject parameter with an unknown prefix', + 'http://example.org/?subject=bar%3Afoo', + 'should add the non-expanded subject to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('bar:foo') }, + ], + [ + 'a URL with an IRI subject parameter in angular brackets', + 'http://example.org/?subject=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the non-expanded subject to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name subject parameter in angular brackets', + 'http://example.org/?subject=%3Cfoo%3Abar%3E', + 'should add the non-expanded subject to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('foo:bar') }, + ], + [ + 'a URL with an empty predicate parameter', + 'http://example.org/?predicate=', + 'should not add the predicate to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an IRI predicate parameter', + 'http://example.org/?predicate=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the predicate to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name predicate parameter', + 'http://example.org/?predicate=foo%3Abar', + 'should add the expanded predicate to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name predicate parameter with the "http" prefix', + 'http://example.org/?predicate=http%3Aauthority', + 'should add the expanded predicate to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://www.w3.org/2011/http#authority') }, + ], + [ + 'a URL with a prefixed name predicate parameter with an unknown prefix', + 'http://example.org/?predicate=bar%3Afoo', + 'should add the non-expanded predicate to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('bar:foo') }, + ], + [ + 'a URL with an IRI predicate parameter in angular brackets', + 'http://example.org/?predicate=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the non-expanded predicate to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name predicate parameter in angular brackets', + 'http://example.org/?predicate=%3Cfoo%3Abar%3E', + 'should add the non-expanded predicate to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('foo:bar') }, + ], + [ + 'a URL with an empty object parameter', + 'http://example.org/?object=', + 'should not add the object to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an IRI object parameter', + 'http://example.org/?object=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name object parameter', + 'http://example.org/?object=foo%3Abar', + 'should add the expanded object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name object parameter with the "http" prefix', + 'http://example.org/?object=http%3AConnection', + 'should add the expanded object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, + ], + [ + 'a URL with a prefixed name object parameter with an unknown prefix', + 'http://example.org/?object=bar%3Afoo', + 'should add the non-expanded object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('bar:foo') }, + ], + [ + 'a URL with an IRI object parameter in angular brackets', + 'http://example.org/?object=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the non-expanded object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name object parameter in angular brackets', + 'http://example.org/?object=%3Cfoo%3Abar%3E', + 'should add the non-expanded object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('foo:bar') }, + ], + [ + 'a URL with a typed literal object parameter', + 'http://example.org/?object=%22foo%22%5E%5Ehttp%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + ], + [ + 'a URL with a typed literal object parameter in angular brackets', + 'http://example.org/?object=%22foo%22%5E%5E%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + ], + [ + 'a URL with a prefixed literal object parameter', + 'http://example.org/?object=%22foo%22%5E%5Efoo%3Abar', + 'should add the object to the query', + { limit: 1 }, + { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + ], + [ + 'a URL with an empty graph parameter', + 'http://example.org/?graph=', + 'should not add the graph to the query', + { limit: 1 }, + { limit: 1 }, + ], + [ + 'a URL with an IRI graph parameter', + 'http://example.org/?graph=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the graph to the query', + { limit: 1 }, + { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name graph parameter', + 'http://example.org/?graph=foo%3Abar', + 'should add the expanded graph to the query', + { limit: 1 }, + { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name graph parameter with the "http" prefix', + 'http://example.org/?graph=http%3AConnection', + 'should add the expanded graph to the query', + { limit: 1 }, + { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, + ], + [ + 'a URL with a prefixed name graph parameter with an unknown prefix', + 'http://example.org/?graph=bar%3Afoo', + 'should add the non-expanded graph to the query', + { limit: 1 }, + { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('bar:foo') }, + ], + [ + 'a URL with an IRI graph parameter in angular brackets', + 'http://example.org/?graph=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the non-expanded graph to the query', + { limit: 1 }, + { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name graph parameter in angular brackets', + 'http://example.org/?graph=%3Cfoo%3Abar%3E', + 'should add the non-expanded graph to the query', + { limit: 1 }, + { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('foo:bar') }, + ], + ] as Array<[string, string, string, Query, Query]> + ) + .forEach((args) => { extractQueryParams(router, ...args); }); + }); + }); + }); +}); diff --git a/packages/feature-qpf/test/views/quadpatternfragments/QuadPatternFragmentsRdfView-test.js b/packages/feature-qpf/test/views/quadpatternfragments/QuadPatternFragmentsRdfView-test.ts similarity index 76% rename from packages/feature-qpf/test/views/quadpatternfragments/QuadPatternFragmentsRdfView-test.js rename to packages/feature-qpf/test/views/quadpatternfragments/QuadPatternFragmentsRdfView-test.ts index 642d7405..d4812d7f 100644 --- a/packages/feature-qpf/test/views/quadpatternfragments/QuadPatternFragmentsRdfView-test.js +++ b/packages/feature-qpf/test/views/quadpatternfragments/QuadPatternFragmentsRdfView-test.ts @@ -2,15 +2,24 @@ import { describe, it, expect, beforeAll, vi } from 'vitest'; import { createStreamCapture } from '../../../../../test/test-helpers'; -let QuadPatternFragmentsRdfView = require('../../../').views.quadpatternfragments.QuadPatternFragmentsRdfView; +import { views } from '../../../index'; -let _ = require('lodash'), - fs = require('fs'), - path = require('path'), - AsyncIterator = require('asynciterator'), - N3 = require('n3'); +import * as _ from 'lodash'; +import * as fs from 'fs'; +import * as path from 'path'; +import { empty, fromArray, TransformIterator } from 'asynciterator'; +import type { AsyncIterator } from 'asynciterator'; +import type { Quad } from 'rdf-js'; +import { DataFactory as dataFactory } from 'n3'; +import { IncomingMessage } from 'http'; +import { Socket } from 'net'; +import type { LdfRequest } from '../../../../core/index'; -const dataFactory = N3.DataFactory; +const { QuadPatternFragmentsRdfView } = views.quadpatternfragments; + +function createRequest(): LdfRequest { + return new IncomingMessage(new Socket()); +} describe('QuadPatternFragmentsRdfView', () => { describe('The QuadPatternFragmentsRdfView module', () => { @@ -25,7 +34,7 @@ describe('QuadPatternFragmentsRdfView', () => { describe('A QuadPatternFragmentsRdfView instance', () => { let view = new QuadPatternFragmentsRdfView({ dataFactory }); - let settings = { + let settings: { results?: AsyncIterator; [key: string]: unknown } = { datasource: { title: 'My data', index: 'http://ex.org/#dataset', @@ -63,18 +72,18 @@ describe('QuadPatternFragmentsRdfView', () => { }, (extension, format) => { describe('when render is called for ' + format, () => { - function readAsset(name) { + function readAsset(name: string) { let file = path.join(__dirname, '../../../../../test/assets/', name + '.' + extension); return fs.readFileSync(file, 'utf8'); } describe('with an empty triple stream', () => { - let results = AsyncIterator.empty(); + let results = empty(); let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { + beforeAll(() => new Promise((resolve) => { settings.results = results; response.getHeader = vi.fn().mockReturnValue(format); - view.render(settings, {}, response, resolve); + view.render(settings, createRequest(), response, resolve); results.setProperty('metadata', { totalCount: 1234 }); })); @@ -84,18 +93,19 @@ describe('QuadPatternFragmentsRdfView', () => { }); describe('with a non-empty triple stream that writes metadata first', () => { - let results = AsyncIterator.fromArray([ + let results = fromArray([ dataFactory.quad(dataFactory.namedNode('a'), dataFactory.namedNode('b'), dataFactory.namedNode('c'), dataFactory.defaultGraph()), dataFactory.quad(dataFactory.namedNode('a'), dataFactory.namedNode('d'), dataFactory.namedNode('e'), dataFactory.defaultGraph()), dataFactory.quad(dataFactory.namedNode('f'), dataFactory.namedNode('g'), dataFactory.namedNode('h'), dataFactory.defaultGraph()), ]); let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { - settings.results = new AsyncIterator.TransformIterator(); + beforeAll(() => new Promise((resolve) => { + let transform = new TransformIterator(); + settings.results = transform; response.getHeader = vi.fn().mockReturnValue(format); - view.render(settings, {}, response, resolve); - settings.results.setProperty('metadata', { totalCount: 1234 }); - settings.results.source = results; + view.render(settings, createRequest(), response, resolve); + transform.setProperty('metadata', { totalCount: 1234 }); + transform.source = results; })); it('should write data and metadata', () => { @@ -104,16 +114,16 @@ describe('QuadPatternFragmentsRdfView', () => { }); describe('with a non-empty triple stream that writes metadata afterwards', () => { - let results = AsyncIterator.fromArray([ + let results = fromArray([ dataFactory.quad(dataFactory.namedNode('a'), dataFactory.namedNode('b'), dataFactory.namedNode('c'), dataFactory.defaultGraph()), dataFactory.quad(dataFactory.namedNode('a'), dataFactory.namedNode('d'), dataFactory.namedNode('e'), dataFactory.defaultGraph()), dataFactory.quad(dataFactory.namedNode('f'), dataFactory.namedNode('g'), dataFactory.namedNode('h'), dataFactory.defaultGraph()), ]); let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { + beforeAll(() => new Promise((resolve) => { settings.results = results; response.getHeader = vi.fn().mockReturnValue(format); - view.render(settings, {}, response, resolve); + view.render(settings, createRequest(), response, resolve); setImmediate(() => { results.setProperty('metadata', { totalCount: 1234 }); }); @@ -125,8 +135,8 @@ describe('QuadPatternFragmentsRdfView', () => { }); describe('with a query with a limit but no offset', () => { - let results = AsyncIterator.empty(); - let settings = { + let results = empty(); + let settings: { results?: AsyncIterator; [key: string]: unknown } = { datasource: { }, fragment: { pageUrl: 'mypage', @@ -137,10 +147,10 @@ describe('QuadPatternFragmentsRdfView', () => { query: { limit: 100 }, }; let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { + beforeAll(() => new Promise((resolve) => { settings.results = results; response.getHeader = vi.fn().mockReturnValue(format); - view.render(settings, {}, response, resolve); + view.render(settings, createRequest(), response, resolve); results.setProperty('metadata', { totalCount: 1234 }); })); @@ -158,8 +168,8 @@ describe('QuadPatternFragmentsRdfView', () => { }); describe('with a query with a limit and offset before the end', () => { - let results = AsyncIterator.empty(); - let settings = { + let results = empty(); + let settings: { results?: AsyncIterator; [key: string]: unknown } = { datasource: { }, fragment: { pageUrl: 'mypage', @@ -170,10 +180,10 @@ describe('QuadPatternFragmentsRdfView', () => { query: { limit: 100, offset: 1133 }, }; let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { + beforeAll(() => new Promise((resolve) => { settings.results = results; response.getHeader = vi.fn().mockReturnValue(format); - view.render(settings, {}, response, resolve); + view.render(settings, createRequest(), response, resolve); results.setProperty('metadata', { totalCount: 1234 }); })); @@ -191,8 +201,8 @@ describe('QuadPatternFragmentsRdfView', () => { }); describe('with a query with a limit and offset past the end', () => { - let results = AsyncIterator.empty(); - let settings = { + let results = empty(); + let settings: { results?: AsyncIterator; [key: string]: unknown } = { datasource: { }, fragment: { pageUrl: 'mypage', @@ -203,10 +213,10 @@ describe('QuadPatternFragmentsRdfView', () => { query: { limit: 100, offset: 1135 }, }; let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { + beforeAll(() => new Promise((resolve) => { settings.results = results; response.getHeader = vi.fn().mockReturnValue(format); - view.render(settings, {}, response, resolve); + view.render(settings, createRequest(), response, resolve); results.setProperty('metadata', { totalCount: 1234 }); })); diff --git a/packages/feature-summary/test/controllers/SummaryController-test.js b/packages/feature-summary/test/controllers/SummaryController-test.ts similarity index 84% rename from packages/feature-summary/test/controllers/SummaryController-test.js rename to packages/feature-summary/test/controllers/SummaryController-test.ts index 4a1f5e1e..2a451194 100644 --- a/packages/feature-summary/test/controllers/SummaryController-test.js +++ b/packages/feature-summary/test/controllers/SummaryController-test.ts @@ -1,16 +1,17 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll } from 'vitest'; -import { DummyServer } from '../../../../test/DummyServer'; -let SummaryController = require('../../lib/controllers/SummaryController').SummaryController; // changed to make tests pass, will be revised in follow up pr +import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { controllers, views } from '../../index'; -let request = require('supertest'), - fs = require('fs'), - path = require('path'); +import * as request from 'supertest'; +import * as fs from 'fs'; +import * as path from 'path'; -let SummaryRdfView = require('../../lib/views/summary/SummaryRdfView.js').SummaryRdfView; // changed to make tests pass, will be revised in follow up pr +import { DataFactory as dataFactory } from 'n3'; -let dataFactory = require('n3').DataFactory; +const { SummaryController } = controllers; +const { SummaryRdfView } = views.summary; describe('SummaryController', () => { describe('The SummaryController module', () => { @@ -28,7 +29,7 @@ describe('SummaryController', () => { }); describe('An SummaryController instance', () => { - let controller, client; + let controller: InstanceType & Partial, client: ReturnType; beforeAll(() => { controller = new SummaryController({ views: [new SummaryRdfView({ dataFactory })], @@ -38,7 +39,7 @@ describe('SummaryController', () => { rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', }, }); - client = request.agent(new DummyServer(controller), {}); + client = request.agent(DummyServer(controller)); }); it('should correctly serve summary in Turtle', async () => { diff --git a/test/DummyServer.js b/test/DummyServer.js deleted file mode 100644 index 23d39f37..00000000 --- a/test/DummyServer.js +++ /dev/null @@ -1,21 +0,0 @@ -/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ - -import * as http from 'http'; -import { vi } from 'vitest'; - -/* Dummy server that emulates LinkedDataFragmentsServer */ -export function DummyServer(controller) { - const server = http.createServer(); - server.on('request', (request, response) => { - // End the response if the controller did not handle the request - controller.next = vi.fn((error) => { - controller.error = error; - if (!response.headersSent) - response.writeHead(error ? 500 : 200); - response.end(error && error.message || ''); - }); - try { controller.result = controller.handleRequest(request, response, controller.next); } - catch (error) { controller.next(error); } - }); - return server; -} diff --git a/test/DummyServer.ts b/test/DummyServer.ts new file mode 100644 index 00000000..b8c2edbb --- /dev/null +++ b/test/DummyServer.ts @@ -0,0 +1,31 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ + +import * as http from 'http'; +import { vi, type Mock } from 'vitest'; +import type { Controller } from '@ldf/core/lib/controllers/Controller'; +import type { LdfRequest, LdfRequestWithUrl, LdfResponse } from '@ldf/core'; + +// The bookkeeping DummyServer attaches to a controller so a test can inspect +// how the request was ultimately handled +export interface SpiedController { + next: Mock<(error?: Error) => void>; + error?: Error; +} + +/* Dummy server that emulates LinkedDataFragmentsServer */ +export function DummyServer(controller: Controller & Partial): http.Server { + const server = http.createServer(); + server.on('request', (request: LdfRequest, response: LdfResponse) => { + // End the response if the controller did not handle the request + const next: Mock<(error?: Error) => void> = vi.fn((error?: Error) => { + controller.error = error; + if (!response.headersSent) + response.writeHead(error ? 500 : 200); + response.end(error && error.message || ''); + }); + controller.next = next; + try { controller.handleRequest(request as LdfRequestWithUrl, response, next); } + catch (error) { next(error as Error); } + }); + return server; +} diff --git a/test/test-helpers.js b/test/test-helpers.js deleted file mode 100644 index c5318730..00000000 --- a/test/test-helpers.js +++ /dev/null @@ -1,44 +0,0 @@ -/*! @license MIT ©2013-2016 Ruben Verborgh, Ghent University - imec */ -import { it, expect } from 'vitest'; -import { parse as parseUrl } from 'url'; -import { Readable, Writable } from 'stream'; -import { once } from 'events'; - -// Generates an `it` block that verifies a router's extractQueryParams behavior -export function extractQueryParams(router, description, url, intent, query, expectedQuery) { - it(description + ' ' + intent, () => { - const result = router.extractQueryParams({ url: parseUrl(url, true) }, query); - expect(result, 'should not return anything').toBeUndefined(); - expect(query, 'should match the expected query').toEqual(expectedQuery); - }); -} - -// Creates a dummy HTTP response -export function createHttpResponse(contents, contentType) { - const response = new Readable(); - response._read = () => {}; - response.statusCode = 200; - response.headers = { 'content-type': contentType }; - response.abort = () => { response.aborted = true; }; - setImmediate(() => { response.push(contents); response.push(null); }); - return response; -} - -// Creates an in-memory stream -export function createStreamCapture() { - const stream = new Writable({ objectMode: true }); - stream.buffer = ''; - stream._write = (chunk, encoding, callback) => { - stream.buffer += chunk; - callback && callback(); - }; - return stream; -} - -// Counts the elements in a stream and resolves once it ends -export async function streamLength(stream) { - let length = 0; - stream.on('data', () => { length++; }); - await once(stream, 'end'); - return length; -} diff --git a/test/test-helpers.ts b/test/test-helpers.ts new file mode 100644 index 00000000..3fd9fb85 --- /dev/null +++ b/test/test-helpers.ts @@ -0,0 +1,75 @@ +/*! @license MIT ©2013-2016 Ruben Verborgh, Ghent University - imec */ +import { it, expect } from 'vitest'; +import { parse as parseUrl } from 'url'; +import { Readable } from 'stream'; +import { IncomingMessage, ServerResponse } from 'http'; +import { Socket } from 'net'; +import { EventEmitter, once } from 'events'; +import type { Query, RouterRequest } from '../packages/core/lib/types'; + +// A router as accepted by extractQueryParams; DatasourceRouter and PageRouter both satisfy this +interface QueryParamRouter { + extractQueryParams(request: RouterRequest, query: Query): void; +} + +// Generates an `it` block that verifies a router's extractQueryParams behavior +export function extractQueryParams(router: QueryParamRouter, description: string, url: string, intent: string, query: Query, expectedQuery: Query) { + it(description + ' ' + intent, () => { + const parsed = parseUrl(url, true); + const result = router.extractQueryParams({ url: { pathname: parsed.pathname ?? undefined, query: parsed.query } }, query); + expect(result, 'should not return anything').toBeUndefined(); + expect(query, 'should match the expected query').toEqual(expectedQuery); + }); +} + +// A dummy HTTP response, as returned by createHttpResponse +class HttpResponse extends Readable { + statusCode = 200; + headers: Record; + aborted = false; + + constructor(contentType: string) { + super(); + this.headers = { 'content-type': contentType }; + } + + override _read(): void {} + + abort(): void { + this.aborted = true; + } +} + +// Creates a dummy HTTP response +export function createHttpResponse(contents: string, contentType: string): HttpResponse { + const response = new HttpResponse(contentType); + setImmediate(() => { response.push(contents); response.push(null); }); + return response; +} + +// An in-memory HTTP response, as returned by createStreamCapture +class StreamCapture extends ServerResponse { + buffer = ''; + + constructor() { + super(new IncomingMessage(new Socket())); + } + + override write(chunk: unknown): boolean { + this.buffer += chunk; + return true; + } +} + +// Creates an in-memory HTTP response that captures everything written to it +export function createStreamCapture(): StreamCapture { + return new StreamCapture(); +} + +// Counts the elements in a stream and resolves once it ends +export async function streamLength(stream: EventEmitter): Promise { + let length = 0; + stream.on('data', () => { length++; }); + await once(stream, 'end'); + return length; +} diff --git a/tsconfig.typecheck.json b/tsconfig.typecheck.json new file mode 100644 index 00000000..8038e5a5 --- /dev/null +++ b/tsconfig.typecheck.json @@ -0,0 +1,17 @@ +{ + "// why": "separate from tsconfig.json so 'yarn build' (tsc -p tsconfig.json) never emits compiled .js for test files; this config adds test dirs and is noEmit-only, used by 'yarn typecheck' and ESLint's type-aware parserOptions.project", + "// skipLibCheck": "vitest's own .d.ts files reference package-exports-only subpaths (e.g. @vitest/utils/display) that this project's module/moduleResolution can't follow; skipLibCheck avoids that without changing module/moduleResolution project-wide (which broke asynciterator resolution and import-equals syntax in existing lib code when tried)", + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "skipLibCheck": true + }, + "include": [ + "packages/*/index.ts", + "packages/*/lib/**/*", + "packages/*/bin/**/*", + "packages/*/test/**/*.ts", + "test/**/*.ts", + "types/**/*.d.ts" + ] +} diff --git a/vitest.config.mts b/vitest.config.mts index 92ed70a4..bf13c2fc 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -15,7 +15,7 @@ export default defineConfig({ ], }, test: { - include: ['packages/*/test/**/*-test.js'], + include: ['packages/*/test/**/*-test.ts'], environment: 'node', testTimeout: 5000, setupFiles: ['./test/vitest-setup.js'], diff --git a/yarn.lock b/yarn.lock index 8673c3db..9bc3b1e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1318,6 +1318,11 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.5.tgz#3a3510c4e3661f7707c5ae9c67d726986e6e147d" integrity sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg== +"@types/cookiejar@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + "@types/deep-eql@*": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" @@ -1353,6 +1358,11 @@ resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef" integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw== +"@types/methods@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + "@types/mime@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" @@ -1376,29 +1386,7 @@ "@types/node" "*" "@types/rdf-js" "*" -"@types/node@*": - version "13.7.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.6.tgz#cb734a7c191472ae6a2b3a502b4dfffcea974113" - integrity sha512-eyK7MWD0R1HqVTp+PtwRgFeIsemzuj4gBFSQxfPHY5iMjS7474e5wq+VFgTcdpyHeNxyKSaetYAjdMLJlKoWqA== - -"@types/node@^13.1.0": - version "13.11.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" - integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== - -"@types/node@^14.14.7": - version "14.14.20" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.20.tgz#f7974863edd21d1f8a494a73e8e2b3658615c340" - integrity sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A== - -"@types/node@^18.0.0": - version "18.19.54" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.54.tgz#f1048dc083f81b242640f04f18fb3e4ccf13fcdb" - integrity sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw== - dependencies: - undici-types "~5.26.4" - -"@types/node@^24.0.0": +"@types/node@*", "@types/node@^13.1.0", "@types/node@^14.14.7", "@types/node@^18.0.0", "@types/node@^24.0.0": version "24.13.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-24.13.3.tgz#49f18bd3c647866dcda51a0756c145e14590ce16" integrity sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q== @@ -1480,6 +1468,24 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== +"@types/superagent@^8.1.0": + version "8.1.11" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.11.tgz#14da75aa2f916dcdd6fb2a90a8fb24a9a1a86d08" + integrity sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw== + dependencies: + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" + "@types/node" "*" + form-data "^4.0.0" + +"@types/supertest@^7.2.1": + version "7.2.1" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-7.2.1.tgz#165e99f10fd652027cf1eaa74b55081b6daa0965" + integrity sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw== + dependencies: + "@types/methods" "^1.1.4" + "@types/superagent" "^8.1.0" + "@types/tough-cookie@*": version "4.0.5" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" @@ -3499,7 +3505,7 @@ form-data@^2.5.5: mime-types "^2.1.35" safe-buffer "^5.2.1" -form-data@^4.0.5: +form-data@^4.0.0, form-data@^4.0.5: version "4.0.6" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827" integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ== @@ -8013,11 +8019,6 @@ unbox-primitive@^1.1.0: has-symbols "^1.1.0" which-boxed-primitive "^1.1.1" -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - undici-types@~7.18.0: version "7.18.2" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9" From 1cb93ae36fbcb4285925a24d8978174146734204 Mon Sep 17 00:00:00 2001 From: Michiel Date: Mon, 31 Aug 2026 09:45:03 +0000 Subject: [PATCH 02/15] remove unncessary pin added --- package.json | 4 ---- yarn.lock | 24 +++++++++++++++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5dd7932e..2b719ebd 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,6 @@ "typescript": "^5.9.3", "vitest": "^4.1.10" }, - "// resolutions": "pin @types/node: @types/supertest -> @types/superagent otherwise nests an old @types/node, breaking http.Server typing", - "resolutions": { - "@types/node": "^24.0.0" - }, "pre-commit": [ "lint", "test" diff --git a/yarn.lock b/yarn.lock index 9bc3b1e6..89dbe559 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1386,13 +1386,30 @@ "@types/node" "*" "@types/rdf-js" "*" -"@types/node@*", "@types/node@^13.1.0", "@types/node@^14.14.7", "@types/node@^18.0.0", "@types/node@^24.0.0": +"@types/node@*", "@types/node@^24.0.0": version "24.13.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-24.13.3.tgz#49f18bd3c647866dcda51a0756c145e14590ce16" integrity sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q== dependencies: undici-types "~7.18.0" +"@types/node@^13.1.0": + version "13.13.52" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.52.tgz#03c13be70b9031baaed79481c0c0cfb0045e53f7" + integrity sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ== + +"@types/node@^14.14.7": + version "14.18.63" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" + integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== + +"@types/node@^18.0.0": + version "18.19.130" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.130.tgz#da4c6324793a79defb7a62cba3947ec5add00d59" + integrity sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -8019,6 +8036,11 @@ unbox-primitive@^1.1.0: has-symbols "^1.1.0" which-boxed-primitive "^1.1.1" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + undici-types@~7.18.0: version "7.18.2" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9" From 0cd4a608fef07b46c5d507c4a47d5be0cbd2ec39 Mon Sep 17 00:00:00 2001 From: Michiel Date: Mon, 31 Aug 2026 11:35:29 +0000 Subject: [PATCH 03/15] LDTFS small refactor --- .../test/LinkedDataFragmentsServer-test.ts | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/core/test/LinkedDataFragmentsServer-test.ts b/packages/core/test/LinkedDataFragmentsServer-test.ts index 8aade5ac..aebeb83b 100644 --- a/packages/core/test/LinkedDataFragmentsServer-test.ts +++ b/packages/core/test/LinkedDataFragmentsServer-test.ts @@ -11,6 +11,8 @@ import * as request from 'supertest'; describe('LinkedDataFragmentsServer', () => { describe('A LinkedDataFragmentsServer instance with one controller', () => { let server: ReturnType, controller: Controller, client: ReturnType; + // Kept separate so .mockClear() has a Mock-typed target. controller.handleRequest's + // declared type is just Controller's plain function signature, not Mock. let handleRequestSpy: Mock; beforeAll(() => { controller = new Controller(); @@ -50,7 +52,7 @@ describe('LinkedDataFragmentsServer', () => { it('should not allow POST requests', async () => { let response = await client.post('/'); - expect(handleRequestSpy).not.toHaveBeenCalled(); + expect(controller.handleRequest).not.toHaveBeenCalled(); expect(response).toHaveProperty('statusCode', 405); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'The HTTP method "POST" is not allowed; try "GET" instead.'); @@ -58,28 +60,28 @@ describe('LinkedDataFragmentsServer', () => { it('should send a body with GET requests', async () => { let response = await client.get('/handle'); - expect(handleRequestSpy).toHaveBeenCalledOnce(); + expect(controller.handleRequest).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 200); expect(response).toHaveProperty('text', 'body contents'); }); it('should not send a body with HEAD requests', async () => { let response = await client.head('/handle'); - expect(handleRequestSpy).toHaveBeenCalledOnce(); + expect(controller.handleRequest).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 200); expect(response.body).not.toHaveProperty('length'); }); it('should not send a body with OPTIONS requests', async () => { let response = await client.options('/handle'); - expect(handleRequestSpy).toHaveBeenCalledOnce(); + expect(controller.handleRequest).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 200); expect(response).toHaveProperty('text', ''); }); it('should error when the controller cannot handle the request', async () => { let response = await client.get('/unsupported'); - expect(handleRequestSpy).toHaveBeenCalledOnce(); + expect(controller.handleRequest).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 500); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'Application error: No controller for /unsupported\n'); @@ -87,7 +89,7 @@ describe('LinkedDataFragmentsServer', () => { it('should error when the controller errors', async () => { let response = await client.get('/error'); - expect(handleRequestSpy).toHaveBeenCalledOnce(); + expect(controller.handleRequest).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 500); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'Application error: error message\n'); @@ -96,6 +98,8 @@ describe('LinkedDataFragmentsServer', () => { describe('A LinkedDataFragmentsServer instance with two controllers', () => { let server: ReturnType, controllerA: Controller, controllerB: Controller, client: ReturnType; + // Kept separate so .mockClear() has a Mock-typed target. controllerX.handleRequest's + // declared type is just Controller's plain function signature, not Mock. let handleRequestSpyA: Mock, handleRequestSpyB: Mock; beforeAll(() => { controllerA = new Controller(); @@ -138,8 +142,8 @@ describe('LinkedDataFragmentsServer', () => { it('should not allow POST requests', async () => { let response = await client.post('/'); - expect(handleRequestSpyA).not.toHaveBeenCalled(); - expect(handleRequestSpyB).not.toHaveBeenCalled(); + expect(controllerA.handleRequest).not.toHaveBeenCalled(); + expect(controllerB.handleRequest).not.toHaveBeenCalled(); expect(response).toHaveProperty('statusCode', 405); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'The HTTP method "POST" is not allowed; try "GET" instead.'); @@ -147,24 +151,24 @@ describe('LinkedDataFragmentsServer', () => { it('should use the first controller when it can handle the request', async () => { let response = await client.get('/handleA'); - expect(handleRequestSpyA).toHaveBeenCalledOnce(); - expect(handleRequestSpyB).not.toHaveBeenCalled(); + expect(controllerA.handleRequest).toHaveBeenCalledOnce(); + expect(controllerB.handleRequest).not.toHaveBeenCalled(); expect(response).toHaveProperty('statusCode', 200); expect(response).toHaveProperty('text', 'body contents A'); }); it('should use the second controller when the first cannot handle the request', async () => { let response = await client.get('/handleB'); - expect(handleRequestSpyA).toHaveBeenCalledOnce(); - expect(handleRequestSpyB).toHaveBeenCalledOnce(); + expect(controllerA.handleRequest).toHaveBeenCalledOnce(); + expect(controllerB.handleRequest).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 200); expect(response).toHaveProperty('text', 'body contents B'); }); it('should error when neither controller can handle the request', async () => { let response = await client.get('/unsupported'); - expect(handleRequestSpyA).toHaveBeenCalledOnce(); - expect(handleRequestSpyB).toHaveBeenCalledOnce(); + expect(controllerA.handleRequest).toHaveBeenCalledOnce(); + expect(controllerB.handleRequest).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 500); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'Application error: No controller for /unsupported\n'); @@ -172,8 +176,8 @@ describe('LinkedDataFragmentsServer', () => { it('should error when the first controller errors', async () => { let response = await client.get('/errorA'); - expect(handleRequestSpyA).toHaveBeenCalledOnce(); - expect(handleRequestSpyB).not.toHaveBeenCalled(); + expect(controllerA.handleRequest).toHaveBeenCalledOnce(); + expect(controllerB.handleRequest).not.toHaveBeenCalled(); expect(response).toHaveProperty('statusCode', 500); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'Application error: error message A\n'); @@ -181,8 +185,8 @@ describe('LinkedDataFragmentsServer', () => { it('should error when the second controller errors', async () => { let response = await client.get('/errorB'); - expect(handleRequestSpyA).toHaveBeenCalledOnce(); - expect(handleRequestSpyB).toHaveBeenCalledOnce(); + expect(controllerA.handleRequest).toHaveBeenCalledOnce(); + expect(controllerB.handleRequest).toHaveBeenCalledOnce(); expect(response).toHaveProperty('statusCode', 500); expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); expect(response).toHaveProperty('text', 'Application error: error message B\n'); From e785c00c345f7ef52f717818567f979f78c50105 Mon Sep 17 00:00:00 2001 From: Michiel Date: Mon, 31 Aug 2026 11:56:03 +0000 Subject: [PATCH 04/15] make the controller-test a bit more readable --- .../core/test/controllers/Controller-test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/core/test/controllers/Controller-test.ts b/packages/core/test/controllers/Controller-test.ts index 24388b61..87cdbbd4 100644 --- a/packages/core/test/controllers/Controller-test.ts +++ b/packages/core/test/controllers/Controller-test.ts @@ -31,7 +31,9 @@ describe('Controller', () => { }); describe('A Controller instance without baseURL', () => { - let controller: TestableController & Partial, client: ReturnType, handleRequestSpy: Mock; + let controller: TestableController & Partial; + let client: ReturnType; + let handleRequestSpy: Mock; beforeAll(() => { controller = new TestableController(); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); @@ -67,7 +69,9 @@ describe('Controller', () => { }); describe('A Controller instance without baseURL using Forwarded header', () => { - let controller: TestableController & Partial, client: ReturnType, handleRequestSpy: Mock; + let controller: TestableController & Partial; + let client: ReturnType; + let handleRequestSpy: Mock; beforeAll(() => { controller = new TestableController({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); @@ -107,7 +111,9 @@ describe('Controller', () => { }); describe('A Controller instance without baseURL using X-Forwarded-* headers', () => { - let controller: TestableController & Partial, client: ReturnType, handleRequestSpy: Mock; + let controller: TestableController & Partial; + let client: ReturnType; + let handleRequestSpy: Mock; beforeAll(() => { controller = new TestableController(); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); @@ -146,7 +152,9 @@ describe('Controller', () => { }); describe('A Controller instance with baseURL', () => { - let controller: TestableController & Partial, client: ReturnType, handleRequestSpy: Mock; + let controller: TestableController & Partial; + let client: ReturnType; + let handleRequestSpy: Mock; beforeAll(() => { controller = new TestableController({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); From e017c89615a0f80e8b1fe0b4be7abfa131f976db Mon Sep 17 00:00:00 2001 From: Michiel Date: Mon, 31 Aug 2026 12:04:25 +0000 Subject: [PATCH 05/15] refactor of some AI-changes --- .../test/LinkedDataFragmentsServer-test.ts | 4 ++-- .../test/controllers/AssetsController-test.ts | 2 +- .../core/test/controllers/Controller-test.ts | 8 +++---- .../controllers/DereferenceController-test.ts | 4 ++-- .../controllers/NotFoundController-test.ts | 16 ++++++------- .../QuadPatternFragmentsController-test.ts | 24 +++++++++---------- .../controllers/SummaryController-test.ts | 2 +- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/core/test/LinkedDataFragmentsServer-test.ts b/packages/core/test/LinkedDataFragmentsServer-test.ts index aebeb83b..46dfbc00 100644 --- a/packages/core/test/LinkedDataFragmentsServer-test.ts +++ b/packages/core/test/LinkedDataFragmentsServer-test.ts @@ -10,7 +10,7 @@ import * as request from 'supertest'; describe('LinkedDataFragmentsServer', () => { describe('A LinkedDataFragmentsServer instance with one controller', () => { - let server: ReturnType, controller: Controller, client: ReturnType; + let server: ReturnType, controller: Controller, client: request.Agent; // Kept separate so .mockClear() has a Mock-typed target. controller.handleRequest's // declared type is just Controller's plain function signature, not Mock. let handleRequestSpy: Mock; @@ -97,7 +97,7 @@ describe('LinkedDataFragmentsServer', () => { }); describe('A LinkedDataFragmentsServer instance with two controllers', () => { - let server: ReturnType, controllerA: Controller, controllerB: Controller, client: ReturnType; + let server: ReturnType, controllerA: Controller, controllerB: Controller, client: request.Agent; // Kept separate so .mockClear() has a Mock-typed target. controllerX.handleRequest's // declared type is just Controller's plain function signature, not Mock. let handleRequestSpyA: Mock, handleRequestSpyB: Mock; diff --git a/packages/core/test/controllers/AssetsController-test.ts b/packages/core/test/controllers/AssetsController-test.ts index 4a623115..2da7c5bf 100644 --- a/packages/core/test/controllers/AssetsController-test.ts +++ b/packages/core/test/controllers/AssetsController-test.ts @@ -22,7 +22,7 @@ describe('AssetsController', () => { }); describe('An AssetsController instance', () => { - let controller: InstanceType & Partial, client: ReturnType; + let controller: InstanceType & Partial, client: request.Agent; beforeAll(() => { controller = new AssetsController(); client = request.agent(DummyServer(controller)); diff --git a/packages/core/test/controllers/Controller-test.ts b/packages/core/test/controllers/Controller-test.ts index 87cdbbd4..4448db92 100644 --- a/packages/core/test/controllers/Controller-test.ts +++ b/packages/core/test/controllers/Controller-test.ts @@ -32,7 +32,7 @@ describe('Controller', () => { describe('A Controller instance without baseURL', () => { let controller: TestableController & Partial; - let client: ReturnType; + let client: request.Agent; let handleRequestSpy: Mock; beforeAll(() => { controller = new TestableController(); @@ -70,7 +70,7 @@ describe('Controller', () => { describe('A Controller instance without baseURL using Forwarded header', () => { let controller: TestableController & Partial; - let client: ReturnType; + let client: request.Agent; let handleRequestSpy: Mock; beforeAll(() => { controller = new TestableController({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); @@ -112,7 +112,7 @@ describe('Controller', () => { describe('A Controller instance without baseURL using X-Forwarded-* headers', () => { let controller: TestableController & Partial; - let client: ReturnType; + let client: request.Agent; let handleRequestSpy: Mock; beforeAll(() => { controller = new TestableController(); @@ -153,7 +153,7 @@ describe('Controller', () => { describe('A Controller instance with baseURL', () => { let controller: TestableController & Partial; - let client: ReturnType; + let client: request.Agent; let handleRequestSpy: Mock; beforeAll(() => { controller = new TestableController({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); diff --git a/packages/core/test/controllers/DereferenceController-test.ts b/packages/core/test/controllers/DereferenceController-test.ts index c9ee97cc..b6a680ee 100644 --- a/packages/core/test/controllers/DereferenceController-test.ts +++ b/packages/core/test/controllers/DereferenceController-test.ts @@ -22,14 +22,14 @@ describe('DereferenceController', () => { }); describe('A DereferenceController instance', () => { - let controller: InstanceType & Partial, client: ReturnType; + let controller: InstanceType & Partial, client: request.Agent; beforeAll(() => { controller = new DereferenceController({ dereference: { '/resource/': new Datasource({ dataFactory, path: 'dbpedia/2014' }) } }); client = request.agent(DummyServer(controller)); }); describe('receiving a request for a dereferenced URL', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { response = await client.get('/resource/Mickey_Mouse'); }); it('should not hand over to the next controller', () => { diff --git a/packages/core/test/controllers/NotFoundController-test.ts b/packages/core/test/controllers/NotFoundController-test.ts index 0c233dbb..99da8872 100644 --- a/packages/core/test/controllers/NotFoundController-test.ts +++ b/packages/core/test/controllers/NotFoundController-test.ts @@ -25,14 +25,14 @@ describe('NotFoundController', () => { }); describe('A NotFoundController instance without views', () => { - let controller: InstanceType & Partial, client: ReturnType; + let controller: InstanceType & Partial, client: request.Agent; beforeAll(() => { controller = new NotFoundController(); client = request.agent(DummyServer(controller)); }); describe('receiving a request', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { response = await client.get('/notfound'); }); @@ -64,7 +64,7 @@ describe('NotFoundController', () => { htmlView: InstanceType, rdfView: InstanceType, htmlRenderSpy: Mock['render']>, rdfRenderSpy: Mock['render']>, - datasources: DatasourceRegistry, client: ReturnType; + datasources: DatasourceRegistry, client: request.Agent; beforeAll(() => { htmlView = new NotFoundHtmlView({ dataFactory }); rdfView = new NotFoundRdfView({ dataFactory }); @@ -80,7 +80,7 @@ describe('NotFoundController', () => { } describe('receiving a request without Accept header', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { resetAll(); response = await client.get('/notfound'); @@ -116,7 +116,7 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of */*', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { resetAll(); response = await client.get('/notfound').set('Accept', '*/*'); @@ -152,7 +152,7 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of text/html', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { resetAll(); response = await client.get('/notfound').set('Accept', 'text/html'); @@ -188,7 +188,7 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of text/turtle', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { resetAll(); response = await client.get('/notfound').set('Accept', 'text/turtle'); @@ -225,7 +225,7 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of application/trig', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { resetAll(); response = await client.get('/notfound').set('Accept', 'application/trig'); diff --git a/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts index bf1237aa..0710aa98 100644 --- a/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts +++ b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts @@ -33,7 +33,7 @@ describe('QuadPatternFragmentsController', () => { describe('A QuadPatternFragmentsController instance with 3 routers', () => { let controller: InstanceType & Partial, - client: ReturnType, + client: request.Agent, routerA: { extractQueryParams: Mock<(request: RouterRequest, query: Query) => void> }, routerB: { extractQueryParams: Mock<(request: RouterRequest, query: Query) => void> }, routerC: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, @@ -191,7 +191,7 @@ describe('QuadPatternFragmentsController', () => { describe('A QuadPatternFragmentsController instance with 2 views', () => { let controller: InstanceType & Partial, - client: ReturnType, + client: request.Agent, htmlView: InstanceType, rdfView: InstanceType, htmlRenderSpy: Mock['render']>, rdfRenderSpy: Mock['render']>; @@ -227,7 +227,7 @@ describe('QuadPatternFragmentsController', () => { } describe('receiving a request without Accept header', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { resetAll(); response = await client.get('/my-datasource'); @@ -247,7 +247,7 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of */*', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { resetAll(); response = await client.get('/my-datasource').set('Accept', '*/*'); @@ -267,7 +267,7 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/html', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { resetAll(); response = await client.get('/my-datasource').set('Accept', 'text/html'); @@ -287,7 +287,7 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/turtle', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { resetAll(); response = await client.get('/my-datasource').set('Accept', 'text/turtle'); @@ -307,7 +307,7 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/n3', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { resetAll(); response = await client.get('/my-datasource').set('Accept', 'text/n3'); @@ -328,7 +328,7 @@ describe('QuadPatternFragmentsController', () => { }); describe('A QuadPatternFragmentsController instance without matching view', () => { - let controller: InstanceType & Partial, client: ReturnType; + let controller: InstanceType & Partial, client: request.Agent; beforeAll(() => { let datasource = new Datasource({ dataFactory }); datasource.supportsQuery = vi.fn().mockReturnValue(true); @@ -348,7 +348,7 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request without Accept header', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { response = await client.get('/my-datasource'); }); @@ -367,7 +367,7 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/html', () => { - let response: Awaited>; + let response: request.Response; beforeAll(async () => { response = await client.get('/my-datasource').set('Accept', 'text/html'); }); @@ -388,7 +388,7 @@ describe('QuadPatternFragmentsController', () => { describe('A QuadPatternFragmentsController instance with a datasource that synchronously errors', () => { let controller: InstanceType & Partial, - client: ReturnType, + client: request.Agent, router: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, datasource: InstanceType, error: Error, view: InstanceType; beforeAll(() => { @@ -429,7 +429,7 @@ describe('QuadPatternFragmentsController', () => { describe('A QuadPatternFragmentsController instance with a datasource that asynchronously errors', () => { let controller: InstanceType & Partial, - client: ReturnType, + client: request.Agent, router: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, datasource: InstanceType, error: Error, view: InstanceType; beforeAll(() => { diff --git a/packages/feature-summary/test/controllers/SummaryController-test.ts b/packages/feature-summary/test/controllers/SummaryController-test.ts index 2a451194..c3a76468 100644 --- a/packages/feature-summary/test/controllers/SummaryController-test.ts +++ b/packages/feature-summary/test/controllers/SummaryController-test.ts @@ -29,7 +29,7 @@ describe('SummaryController', () => { }); describe('An SummaryController instance', () => { - let controller: InstanceType & Partial, client: ReturnType; + let controller: InstanceType & Partial, client: request.Agent; beforeAll(() => { controller = new SummaryController({ views: [new SummaryRdfView({ dataFactory })], From 1170bdfaf5a82f4a9414e26112056ba8c1d07d55 Mon Sep 17 00:00:00 2001 From: Michiel Date: Mon, 31 Aug 2026 12:27:02 +0000 Subject: [PATCH 06/15] trying to get it inline diff... --- .../test/routers/DatasourceRouter-test.ts | 44 +-- packages/core/test/routers/PageRouter-test.ts | 78 +++--- .../test/routers/QuadPatternRouter-test.ts | 252 +++++++++--------- 3 files changed, 193 insertions(+), 181 deletions(-) diff --git a/packages/core/test/routers/DatasourceRouter-test.ts b/packages/core/test/routers/DatasourceRouter-test.ts index c9876b5f..8d0322c9 100644 --- a/packages/core/test/routers/DatasourceRouter-test.ts +++ b/packages/core/test/routers/DatasourceRouter-test.ts @@ -6,6 +6,10 @@ import { DatasourceRouter } from '../../lib/routers/DatasourceRouter'; import { UrlData } from '../../index'; import type { Query } from '../../index'; +// Query has no index signature; this lets the test tables below use an +// arbitrary 'a' field as a stand-in for "pre-existing data that should survive". +type TestQuery = Query & { a?: number }; + describe('DatasourceRouter', () => { describe('The DatasourceRouter module', () => { it('should be a function', () => { @@ -28,52 +32,52 @@ describe('DatasourceRouter', () => { 'a root URL without trailing slash or query parameters', 'http://example.org', 'should extract the index datasource', - { limit: 1 }, - { limit: 1, features: { datasource: true }, datasource: '/' }, + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/' }, ], [ 'a root URL without query parameters', 'http://example.org/', 'should extract the index datasource', - { limit: 1 }, - { limit: 1, features: { datasource: true }, datasource: '/' }, + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/' }, ], [ 'a root URL with query parameters', 'http://example.org/?a=b&c=d', 'should extract the index datasource', - { limit: 1 }, - { limit: 1, features: { datasource: true }, datasource: '/' }, + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/' }, ], [ 'a URL with word characters without query parameters', 'http://example.org/mydatasource', 'should extract the datasource', - { limit: 1 }, - { limit: 1, features: { datasource: true }, datasource: '/mydatasource' }, + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/mydatasource' }, ], [ 'a URL with word characters with query parameters', 'http://example.org/mydatasource?a=b&c=d', 'should extract the datasource', - { limit: 1 }, - { limit: 1, features: { datasource: true }, datasource: '/mydatasource' }, + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/mydatasource' }, ], [ 'a URL with word and non-word characters without query parameters', 'http://example.org/my/data-source', 'should extract the datasource', - { limit: 1 }, - { limit: 1, features: { datasource: true }, datasource: '/my/data-source' }, + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/my/data-source' }, ], [ 'a URL with word and non-word characters with query parameters', 'http://example.org/my/data-source?a=b&c=d', 'should extract the datasource', - { limit: 1 }, - { limit: 1, features: { datasource: true }, datasource: '/my/data-source' }, + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/my/data-source' }, ], - ] as Array<[string, string, string, Query, Query]> + ] as Array<[string, string, string, TestQuery, TestQuery]> ) .forEach((args) => { extractQueryParams(router, ...args); }); }); @@ -93,17 +97,17 @@ describe('DatasourceRouter', () => { 'a root URL', 'http://example.org/my/base/', 'should extract the index datasource', - { limit: 1 }, - { limit: 1, features: { datasource: true }, datasource: '/' }, + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/' }, ], [ 'a non-root URL', 'http://example.org/my/base/other/path', 'should extract the index datasource', - { limit: 1 }, - { limit: 1, features: { datasource: true }, datasource: '/other/path' }, + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/other/path' }, ], - ] as Array<[string, string, string, Query, Query]> + ] as Array<[string, string, string, TestQuery, TestQuery]> ) .forEach((args) => { extractQueryParams(router, ...args); }); }); diff --git a/packages/core/test/routers/PageRouter-test.ts b/packages/core/test/routers/PageRouter-test.ts index 9e98af3a..27109fa5 100644 --- a/packages/core/test/routers/PageRouter-test.ts +++ b/packages/core/test/routers/PageRouter-test.ts @@ -5,6 +5,10 @@ import { extractQueryParams } from '../../../../test/test-helpers'; import { PageRouter } from '../../lib/routers/PageRouter'; import type { Query } from '../../index'; +// Query has no index signature; this lets the test tables below use an +// arbitrary 'a' field as a stand-in for "pre-existing data that should survive". +type TestQuery = Query & { a?: number }; + describe('PageRouter', () => { describe('The PageRouter module', () => { it('should be a function', () => { @@ -27,59 +31,59 @@ describe('PageRouter', () => { 'a URL without query parameters', 'http://example.org/', 'should add the default page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 100 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, ], [ 'a URL with an empty page parameter', 'http://example.org/?page=', 'should add the default page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 100 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, ], [ 'a URL with a non-numerical page parameter', 'http://example.org/?page=foo', 'should add the default page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 100 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, ], [ 'a URL with -1 as page parameter', 'http://example.org/?page=-1', 'should add the default page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 100 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, ], [ 'a URL with 0 as page parameter', 'http://example.org/?page=0', 'should add the default page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 100 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, ], [ 'a URL with 1 as page parameter', 'http://example.org/?page=1', 'should add the default page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 100 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, ], [ 'a URL with 2 as page parameter', 'http://example.org/?page=2', 'should add the default page size as limit and set the offset', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true, offset: true }, limit: 100, offset: 100 }, + { a: 1 }, + { a: 1, features: { limit: true, offset: true }, limit: 100, offset: 100 }, ], [ 'a URL with 3 as page parameter', 'http://example.org/?page=3', 'should add the default page size as limit and set the offset', - { patternString: 'x', features: { a: true, b: true } }, - { patternString: 'x', features: { a: true, b: true, limit: true, offset: true }, limit: 100, offset: 200 }, + { a: 1, features: { a: true, b: true } }, + { a: 1, features: { a: true, b: true, limit: true, offset: true }, limit: 100, offset: 200 }, ], - ] as Array<[string, string, string, Query, Query]> + ] as Array<[string, string, string, TestQuery, TestQuery]> ) .forEach((args) => { extractQueryParams(router, ...args); }); }); @@ -97,59 +101,59 @@ describe('PageRouter', () => { 'a URL without query parameters', 'http://example.org/', 'should add the page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 250 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, ], [ 'a URL with an empty page parameter', 'http://example.org/?page=', 'should add the page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 250 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, ], [ 'a URL with a non-numerical page parameter', 'http://example.org/?page=foo', 'should add the page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 250 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, ], [ 'a URL with -1 as page parameter', 'http://example.org/?page=-1', 'should add the page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 250 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, ], [ 'a URL with 0 as page parameter', 'http://example.org/?page=0', 'should add the page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 250 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, ], [ 'a URL with 1 as page parameter', 'http://example.org/?page=1', 'should add the page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 250 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, ], [ 'a URL with 2 as page parameter', 'http://example.org/?page=2', 'should add the page size as limit and set the offset', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true, offset: true }, limit: 250, offset: 250 }, + { a: 1 }, + { a: 1, features: { limit: true, offset: true }, limit: 250, offset: 250 }, ], [ 'a URL with 3 as page parameter', 'http://example.org/?page=3', 'should add the page size as limit and set the offset', - { patternString: 'x', features: { a: true, b: true } }, - { patternString: 'x', features: { a: true, b: true, limit: true, offset: true }, limit: 250, offset: 500 }, + { a: 1, features: { a: true, b: true } }, + { a: 1, features: { a: true, b: true, limit: true, offset: true }, limit: 250, offset: 500 }, ], - ] as Array<[string, string, string, Query, Query]> + ] as Array<[string, string, string, TestQuery, TestQuery]> ) .forEach((args) => { extractQueryParams(router, ...args); }); }); @@ -167,10 +171,10 @@ describe('PageRouter', () => { 'a URL without query parameters', 'http://example.org/', 'should add the default page size as limit', - { patternString: 'x' }, - { patternString: 'x', features: { limit: true }, limit: 100 }, + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, ], - ] as Array<[string, string, string, Query, Query]> + ] as Array<[string, string, string, TestQuery, TestQuery]> ) .forEach((args) => { extractQueryParams(router, ...args); }); }); diff --git a/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts b/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts index c23edf17..2f5ae363 100644 --- a/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts +++ b/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts @@ -4,6 +4,10 @@ import { describe, it, expect } from 'vitest'; import { extractQueryParams } from '../../../../test/test-helpers'; import { routers } from '../../index'; import type { Query } from '@ldf/core'; + +// Query has no index signature; this lets the test tables below use an +// arbitrary 'a' field as a stand-in for "pre-existing data that should survive". +type TestQuery = Query & { a?: number }; import { DataFactory as dataFactory } from 'n3'; const { QuadPatternRouter } = routers; @@ -30,206 +34,206 @@ describe('QuadPatternRouter', () => { 'a URL without query parameters', 'http://example.org/', 'should not change the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an empty subject parameter', 'http://example.org/?subject=', 'should not add the subject to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an IRI subject parameter', 'http://example.org/?subject=http%3A%2F%2Fexample.org%2Ffoo%23bar', 'should add the subject to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with an IRI subject parameter in angular brackets', 'http://example.org/?subject=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', 'should add the subject to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a variable subject parameter', 'http://example.org/?subject=%3Ffoo', 'should not add the subject to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with a blank subject parameter', 'http://example.org/?subject=_:foo', 'should not add the subject to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with a literal subject parameter', 'http://example.org/?subject=%22foo%22', 'should not add the subject to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an empty predicate parameter', 'http://example.org/?predicate=', 'should not add the predicate to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an IRI predicate parameter', 'http://example.org/?predicate=http%3A%2F%2Fexample.org%2Ffoo%23bar', 'should add the predicate to the query', - { limit: 1, features: { a: true } }, - { limit: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1, features: { a: true } }, + { a: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with an IRI predicate parameter in angular brackets', 'http://example.org/?predicate=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', 'should add the predicate to the query', - { limit: 1, features: { a: true } }, - { limit: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1, features: { a: true } }, + { a: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a variable predicate parameter', 'http://example.org/?predicate=%3Ffoo', 'should not add the predicate to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with a blank predicate parameter', 'http://example.org/?predicate=_:foo', 'should not add the predicate to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with a literal predicate parameter', 'http://example.org/?predicate=%22foo%22', 'should not add the predicate to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an empty object parameter', 'http://example.org/?object=', 'should not add the object to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an IRI object parameter', 'http://example.org/?object=http%3A%2F%2Fexample.org%2Ffoo%23bar', 'should add the object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with an IRI object parameter in angular brackets', 'http://example.org/?object=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', 'should add the object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a variable object parameter', 'http://example.org/?object=%3Ffoo', 'should not add the object to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with a blank object parameter', 'http://example.org/?object=_:foo', 'should not add the object to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with a literal object parameter', 'http://example.org/?object=%22foo%22', 'should add the object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo') }, ], [ 'a URL with a language literal object parameter', 'http://example.org/?object=%22foo%22@nl-be', 'should add the object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', 'nl-be') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', 'nl-be') }, ], [ 'a URL with a typed literal object parameter', 'http://example.org/?object=%22foo%22%5E%5Ehttp%3A%2F%2Fexample.org%2Ffoo%23bar', 'should add the object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, ], [ 'a URL with a typed literal object parameter in angular brackets', 'http://example.org/?object=%22foo%22%5E%5E%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', 'should add the object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, ], [ 'a URL with an empty graph parameter', 'http://example.org/?graph=', 'should not add the graph to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an IRI graph parameter', 'http://example.org/?graph=http%3A%2F%2Fexample.org%2Ffoo%23bar', 'should add the graph to the query', - { limit: 1 }, - { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with an IRI graph parameter in angular brackets', 'http://example.org/?graph=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', 'should add the graph to the query', - { limit: 1 }, - { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a variable graph parameter', 'http://example.org/?graph=%3Ffoo', 'should not add the graph to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with a blank graph parameter', 'http://example.org/?graph=_:foo', 'should not add the graph to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with a literal graph parameter', 'http://example.org/?graph=%22foo%22', 'should not add the graph to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'the default graph as graph parameter', 'http://example.org/?graph=urn%3Aldf%3AdefaultGraph', 'should add the default graph to the query', - { limit: 1 }, - { limit: 1, features: { quadPattern: true }, graph: dataFactory.defaultGraph() }, + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.defaultGraph() }, ], - ] as Array<[string, string, string, Query, Query]> + ] as Array<[string, string, string, TestQuery, TestQuery]> ) .forEach((args) => { extractQueryParams(router, ...args); }); }); @@ -253,227 +257,227 @@ describe('QuadPatternRouter', () => { 'a URL without query parameters', 'http://example.org/', 'should not change the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an empty subject parameter', 'http://example.org/?subject=', 'should not add the subject to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an IRI subject parameter', 'http://example.org/?subject=http%3A%2F%2Fexample.org%2Ffoo%23bar', 'should add the subject to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name subject parameter', 'http://example.org/?subject=foo%3Abar', 'should add the expanded subject to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name subject parameter with the "http" prefix', 'http://example.org/?subject=http%3AConnection', 'should add the expanded subject to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, ], [ 'a URL with a prefixed name subject parameter with an unknown prefix', 'http://example.org/?subject=bar%3Afoo', 'should add the non-expanded subject to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('bar:foo') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('bar:foo') }, ], [ 'a URL with an IRI subject parameter in angular brackets', 'http://example.org/?subject=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', 'should add the non-expanded subject to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name subject parameter in angular brackets', 'http://example.org/?subject=%3Cfoo%3Abar%3E', 'should add the non-expanded subject to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('foo:bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('foo:bar') }, ], [ 'a URL with an empty predicate parameter', 'http://example.org/?predicate=', 'should not add the predicate to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an IRI predicate parameter', 'http://example.org/?predicate=http%3A%2F%2Fexample.org%2Ffoo%23bar', 'should add the predicate to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name predicate parameter', 'http://example.org/?predicate=foo%3Abar', 'should add the expanded predicate to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name predicate parameter with the "http" prefix', 'http://example.org/?predicate=http%3Aauthority', 'should add the expanded predicate to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://www.w3.org/2011/http#authority') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://www.w3.org/2011/http#authority') }, ], [ 'a URL with a prefixed name predicate parameter with an unknown prefix', 'http://example.org/?predicate=bar%3Afoo', 'should add the non-expanded predicate to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('bar:foo') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('bar:foo') }, ], [ 'a URL with an IRI predicate parameter in angular brackets', 'http://example.org/?predicate=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', 'should add the non-expanded predicate to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name predicate parameter in angular brackets', 'http://example.org/?predicate=%3Cfoo%3Abar%3E', 'should add the non-expanded predicate to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('foo:bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('foo:bar') }, ], [ 'a URL with an empty object parameter', 'http://example.org/?object=', 'should not add the object to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an IRI object parameter', 'http://example.org/?object=http%3A%2F%2Fexample.org%2Ffoo%23bar', 'should add the object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name object parameter', 'http://example.org/?object=foo%3Abar', 'should add the expanded object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name object parameter with the "http" prefix', 'http://example.org/?object=http%3AConnection', 'should add the expanded object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, ], [ 'a URL with a prefixed name object parameter with an unknown prefix', 'http://example.org/?object=bar%3Afoo', 'should add the non-expanded object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('bar:foo') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('bar:foo') }, ], [ 'a URL with an IRI object parameter in angular brackets', 'http://example.org/?object=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', 'should add the non-expanded object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name object parameter in angular brackets', 'http://example.org/?object=%3Cfoo%3Abar%3E', 'should add the non-expanded object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.namedNode('foo:bar') }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('foo:bar') }, ], [ 'a URL with a typed literal object parameter', 'http://example.org/?object=%22foo%22%5E%5Ehttp%3A%2F%2Fexample.org%2Ffoo%23bar', 'should add the object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, ], [ 'a URL with a typed literal object parameter in angular brackets', 'http://example.org/?object=%22foo%22%5E%5E%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', 'should add the object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, ], [ 'a URL with a prefixed literal object parameter', 'http://example.org/?object=%22foo%22%5E%5Efoo%3Abar', 'should add the object to the query', - { limit: 1 }, - { limit: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, ], [ 'a URL with an empty graph parameter', 'http://example.org/?graph=', 'should not add the graph to the query', - { limit: 1 }, - { limit: 1 }, + { a: 1 }, + { a: 1 }, ], [ 'a URL with an IRI graph parameter', 'http://example.org/?graph=http%3A%2F%2Fexample.org%2Ffoo%23bar', 'should add the graph to the query', - { limit: 1 }, - { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name graph parameter', 'http://example.org/?graph=foo%3Abar', 'should add the expanded graph to the query', - { limit: 1 }, - { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name graph parameter with the "http" prefix', 'http://example.org/?graph=http%3AConnection', 'should add the expanded graph to the query', - { limit: 1 }, - { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, ], [ 'a URL with a prefixed name graph parameter with an unknown prefix', 'http://example.org/?graph=bar%3Afoo', 'should add the non-expanded graph to the query', - { limit: 1 }, - { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('bar:foo') }, + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('bar:foo') }, ], [ 'a URL with an IRI graph parameter in angular brackets', 'http://example.org/?graph=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', 'should add the non-expanded graph to the query', - { limit: 1 }, - { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, ], [ 'a URL with a prefixed name graph parameter in angular brackets', 'http://example.org/?graph=%3Cfoo%3Abar%3E', 'should add the non-expanded graph to the query', - { limit: 1 }, - { limit: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('foo:bar') }, + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('foo:bar') }, ], - ] as Array<[string, string, string, Query, Query]> + ] as Array<[string, string, string, TestQuery, TestQuery]> ) .forEach((args) => { extractQueryParams(router, ...args); }); }); From 7acecf0da8805074f61984085440a0f5b3685c34 Mon Sep 17 00:00:00 2001 From: Michiel Date: Mon, 31 Aug 2026 12:39:12 +0000 Subject: [PATCH 07/15] lil test --- .../test/routers/DatasourceRouter-test.ts | 138 ++- packages/core/test/routers/PageRouter-test.ts | 262 +++--- .../test/routers/QuadPatternRouter-test.ts | 870 +++++++++--------- test/DummyServer.ts | 7 +- 4 files changed, 631 insertions(+), 646 deletions(-) diff --git a/packages/core/test/routers/DatasourceRouter-test.ts b/packages/core/test/routers/DatasourceRouter-test.ts index 8d0322c9..f1bcf0c9 100644 --- a/packages/core/test/routers/DatasourceRouter-test.ts +++ b/packages/core/test/routers/DatasourceRouter-test.ts @@ -26,60 +26,58 @@ describe('DatasourceRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - ( + const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ [ - [ - 'a root URL without trailing slash or query parameters', - 'http://example.org', - 'should extract the index datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/' }, - ], - [ - 'a root URL without query parameters', - 'http://example.org/', - 'should extract the index datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/' }, - ], - [ - 'a root URL with query parameters', - 'http://example.org/?a=b&c=d', - 'should extract the index datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/' }, - ], - [ - 'a URL with word characters without query parameters', - 'http://example.org/mydatasource', - 'should extract the datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/mydatasource' }, - ], - [ - 'a URL with word characters with query parameters', - 'http://example.org/mydatasource?a=b&c=d', - 'should extract the datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/mydatasource' }, - ], - [ - 'a URL with word and non-word characters without query parameters', - 'http://example.org/my/data-source', - 'should extract the datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/my/data-source' }, - ], - [ - 'a URL with word and non-word characters with query parameters', - 'http://example.org/my/data-source?a=b&c=d', - 'should extract the datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/my/data-source' }, - ], - ] as Array<[string, string, string, TestQuery, TestQuery]> - ) - .forEach((args) => { extractQueryParams(router, ...args); }); + 'a root URL without trailing slash or query parameters', + 'http://example.org', + 'should extract the index datasource', + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/' }, + ], + [ + 'a root URL without query parameters', + 'http://example.org/', + 'should extract the index datasource', + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/' }, + ], + [ + 'a root URL with query parameters', + 'http://example.org/?a=b&c=d', + 'should extract the index datasource', + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/' }, + ], + [ + 'a URL with word characters without query parameters', + 'http://example.org/mydatasource', + 'should extract the datasource', + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/mydatasource' }, + ], + [ + 'a URL with word characters with query parameters', + 'http://example.org/mydatasource?a=b&c=d', + 'should extract the datasource', + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/mydatasource' }, + ], + [ + 'a URL with word and non-word characters without query parameters', + 'http://example.org/my/data-source', + 'should extract the datasource', + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/my/data-source' }, + ], + [ + 'a URL with word and non-word characters with query parameters', + 'http://example.org/my/data-source?a=b&c=d', + 'should extract the datasource', + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/my/data-source' }, + ], + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); @@ -91,25 +89,23 @@ describe('DatasourceRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - ( + const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + [ + 'a root URL', + 'http://example.org/my/base/', + 'should extract the index datasource', + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/' }, + ], [ - [ - 'a root URL', - 'http://example.org/my/base/', - 'should extract the index datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/' }, - ], - [ - 'a non-root URL', - 'http://example.org/my/base/other/path', - 'should extract the index datasource', - { a: 1 }, - { a: 1, features: { datasource: true }, datasource: '/other/path' }, - ], - ] as Array<[string, string, string, TestQuery, TestQuery]> - ) - .forEach((args) => { extractQueryParams(router, ...args); }); + 'a non-root URL', + 'http://example.org/my/base/other/path', + 'should extract the index datasource', + { a: 1 }, + { a: 1, features: { datasource: true }, datasource: '/other/path' }, + ], + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); diff --git a/packages/core/test/routers/PageRouter-test.ts b/packages/core/test/routers/PageRouter-test.ts index 27109fa5..b267bfc1 100644 --- a/packages/core/test/routers/PageRouter-test.ts +++ b/packages/core/test/routers/PageRouter-test.ts @@ -25,67 +25,65 @@ describe('PageRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - ( - [ - [ - 'a URL without query parameters', - 'http://example.org/', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with an empty page parameter', - 'http://example.org/?page=', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with a non-numerical page parameter', - 'http://example.org/?page=foo', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with -1 as page parameter', - 'http://example.org/?page=-1', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with 0 as page parameter', - 'http://example.org/?page=0', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with 1 as page parameter', - 'http://example.org/?page=1', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - [ - 'a URL with 2 as page parameter', - 'http://example.org/?page=2', - 'should add the default page size as limit and set the offset', - { a: 1 }, - { a: 1, features: { limit: true, offset: true }, limit: 100, offset: 100 }, - ], - [ - 'a URL with 3 as page parameter', - 'http://example.org/?page=3', - 'should add the default page size as limit and set the offset', - { a: 1, features: { a: true, b: true } }, - { a: 1, features: { a: true, b: true, limit: true, offset: true }, limit: 100, offset: 200 }, - ], - ] as Array<[string, string, string, TestQuery, TestQuery]> - ) - .forEach((args) => { extractQueryParams(router, ...args); }); + const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + [ + 'a URL without query parameters', + 'http://example.org/', + 'should add the default page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with an empty page parameter', + 'http://example.org/?page=', + 'should add the default page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with a non-numerical page parameter', + 'http://example.org/?page=foo', + 'should add the default page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with -1 as page parameter', + 'http://example.org/?page=-1', + 'should add the default page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with 0 as page parameter', + 'http://example.org/?page=0', + 'should add the default page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with 1 as page parameter', + 'http://example.org/?page=1', + 'should add the default page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, + ], + [ + 'a URL with 2 as page parameter', + 'http://example.org/?page=2', + 'should add the default page size as limit and set the offset', + { a: 1 }, + { a: 1, features: { limit: true, offset: true }, limit: 100, offset: 100 }, + ], + [ + 'a URL with 3 as page parameter', + 'http://example.org/?page=3', + 'should add the default page size as limit and set the offset', + { a: 1, features: { a: true, b: true } }, + { a: 1, features: { a: true, b: true, limit: true, offset: true }, limit: 100, offset: 200 }, + ], + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); @@ -95,67 +93,65 @@ describe('PageRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - ( - [ - [ - 'a URL without query parameters', - 'http://example.org/', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with an empty page parameter', - 'http://example.org/?page=', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with a non-numerical page parameter', - 'http://example.org/?page=foo', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with -1 as page parameter', - 'http://example.org/?page=-1', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with 0 as page parameter', - 'http://example.org/?page=0', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with 1 as page parameter', - 'http://example.org/?page=1', - 'should add the page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 250 }, - ], - [ - 'a URL with 2 as page parameter', - 'http://example.org/?page=2', - 'should add the page size as limit and set the offset', - { a: 1 }, - { a: 1, features: { limit: true, offset: true }, limit: 250, offset: 250 }, - ], - [ - 'a URL with 3 as page parameter', - 'http://example.org/?page=3', - 'should add the page size as limit and set the offset', - { a: 1, features: { a: true, b: true } }, - { a: 1, features: { a: true, b: true, limit: true, offset: true }, limit: 250, offset: 500 }, - ], - ] as Array<[string, string, string, TestQuery, TestQuery]> - ) - .forEach((args) => { extractQueryParams(router, ...args); }); + const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + [ + 'a URL without query parameters', + 'http://example.org/', + 'should add the page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with an empty page parameter', + 'http://example.org/?page=', + 'should add the page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with a non-numerical page parameter', + 'http://example.org/?page=foo', + 'should add the page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with -1 as page parameter', + 'http://example.org/?page=-1', + 'should add the page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with 0 as page parameter', + 'http://example.org/?page=0', + 'should add the page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with 1 as page parameter', + 'http://example.org/?page=1', + 'should add the page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 250 }, + ], + [ + 'a URL with 2 as page parameter', + 'http://example.org/?page=2', + 'should add the page size as limit and set the offset', + { a: 1 }, + { a: 1, features: { limit: true, offset: true }, limit: 250, offset: 250 }, + ], + [ + 'a URL with 3 as page parameter', + 'http://example.org/?page=3', + 'should add the page size as limit and set the offset', + { a: 1, features: { a: true, b: true } }, + { a: 1, features: { a: true, b: true, limit: true, offset: true }, limit: 250, offset: 500 }, + ], + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); @@ -165,18 +161,16 @@ describe('PageRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - ( - [ - [ - 'a URL without query parameters', - 'http://example.org/', - 'should add the default page size as limit', - { a: 1 }, - { a: 1, features: { limit: true }, limit: 100 }, - ], - ] as Array<[string, string, string, TestQuery, TestQuery]> - ) - .forEach((args) => { extractQueryParams(router, ...args); }); + const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + [ + 'a URL without query parameters', + 'http://example.org/', + 'should add the default page size as limit', + { a: 1 }, + { a: 1, features: { limit: true }, limit: 100 }, + ], + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); diff --git a/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts b/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts index 2f5ae363..ce637e47 100644 --- a/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts +++ b/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts @@ -28,214 +28,212 @@ describe('QuadPatternRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - ( - [ - [ - 'a URL without query parameters', - 'http://example.org/', - 'should not change the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an empty subject parameter', - 'http://example.org/?subject=', - 'should not add the subject to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI subject parameter', - 'http://example.org/?subject=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with an IRI subject parameter in angular brackets', - 'http://example.org/?subject=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a variable subject parameter', - 'http://example.org/?subject=%3Ffoo', - 'should not add the subject to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a blank subject parameter', - 'http://example.org/?subject=_:foo', - 'should not add the subject to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a literal subject parameter', - 'http://example.org/?subject=%22foo%22', - 'should not add the subject to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an empty predicate parameter', - 'http://example.org/?predicate=', - 'should not add the predicate to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI predicate parameter', - 'http://example.org/?predicate=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the predicate to the query', - { a: 1, features: { a: true } }, - { a: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with an IRI predicate parameter in angular brackets', - 'http://example.org/?predicate=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the predicate to the query', - { a: 1, features: { a: true } }, - { a: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a variable predicate parameter', - 'http://example.org/?predicate=%3Ffoo', - 'should not add the predicate to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a blank predicate parameter', - 'http://example.org/?predicate=_:foo', - 'should not add the predicate to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a literal predicate parameter', - 'http://example.org/?predicate=%22foo%22', - 'should not add the predicate to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an empty object parameter', - 'http://example.org/?object=', - 'should not add the object to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI object parameter', - 'http://example.org/?object=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with an IRI object parameter in angular brackets', - 'http://example.org/?object=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a variable object parameter', - 'http://example.org/?object=%3Ffoo', - 'should not add the object to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a blank object parameter', - 'http://example.org/?object=_:foo', - 'should not add the object to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a literal object parameter', - 'http://example.org/?object=%22foo%22', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo') }, - ], - [ - 'a URL with a language literal object parameter', - 'http://example.org/?object=%22foo%22@nl-be', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', 'nl-be') }, - ], - [ - 'a URL with a typed literal object parameter', - 'http://example.org/?object=%22foo%22%5E%5Ehttp%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, - ], - [ - 'a URL with a typed literal object parameter in angular brackets', - 'http://example.org/?object=%22foo%22%5E%5E%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, - ], - [ - 'a URL with an empty graph parameter', - 'http://example.org/?graph=', - 'should not add the graph to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI graph parameter', - 'http://example.org/?graph=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with an IRI graph parameter in angular brackets', - 'http://example.org/?graph=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a variable graph parameter', - 'http://example.org/?graph=%3Ffoo', - 'should not add the graph to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a blank graph parameter', - 'http://example.org/?graph=_:foo', - 'should not add the graph to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with a literal graph parameter', - 'http://example.org/?graph=%22foo%22', - 'should not add the graph to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'the default graph as graph parameter', - 'http://example.org/?graph=urn%3Aldf%3AdefaultGraph', - 'should add the default graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.defaultGraph() }, - ], - ] as Array<[string, string, string, TestQuery, TestQuery]> - ) - .forEach((args) => { extractQueryParams(router, ...args); }); + const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + [ + 'a URL without query parameters', + 'http://example.org/', + 'should not change the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an empty subject parameter', + 'http://example.org/?subject=', + 'should not add the subject to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an IRI subject parameter', + 'http://example.org/?subject=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the subject to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with an IRI subject parameter in angular brackets', + 'http://example.org/?subject=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the subject to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a variable subject parameter', + 'http://example.org/?subject=%3Ffoo', + 'should not add the subject to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with a blank subject parameter', + 'http://example.org/?subject=_:foo', + 'should not add the subject to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with a literal subject parameter', + 'http://example.org/?subject=%22foo%22', + 'should not add the subject to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an empty predicate parameter', + 'http://example.org/?predicate=', + 'should not add the predicate to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an IRI predicate parameter', + 'http://example.org/?predicate=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the predicate to the query', + { a: 1, features: { a: true } }, + { a: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with an IRI predicate parameter in angular brackets', + 'http://example.org/?predicate=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the predicate to the query', + { a: 1, features: { a: true } }, + { a: 1, features: { a: true, triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a variable predicate parameter', + 'http://example.org/?predicate=%3Ffoo', + 'should not add the predicate to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with a blank predicate parameter', + 'http://example.org/?predicate=_:foo', + 'should not add the predicate to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with a literal predicate parameter', + 'http://example.org/?predicate=%22foo%22', + 'should not add the predicate to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an empty object parameter', + 'http://example.org/?object=', + 'should not add the object to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an IRI object parameter', + 'http://example.org/?object=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with an IRI object parameter in angular brackets', + 'http://example.org/?object=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a variable object parameter', + 'http://example.org/?object=%3Ffoo', + 'should not add the object to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with a blank object parameter', + 'http://example.org/?object=_:foo', + 'should not add the object to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with a literal object parameter', + 'http://example.org/?object=%22foo%22', + 'should add the object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo') }, + ], + [ + 'a URL with a language literal object parameter', + 'http://example.org/?object=%22foo%22@nl-be', + 'should add the object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', 'nl-be') }, + ], + [ + 'a URL with a typed literal object parameter', + 'http://example.org/?object=%22foo%22%5E%5Ehttp%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + ], + [ + 'a URL with a typed literal object parameter in angular brackets', + 'http://example.org/?object=%22foo%22%5E%5E%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + ], + [ + 'a URL with an empty graph parameter', + 'http://example.org/?graph=', + 'should not add the graph to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an IRI graph parameter', + 'http://example.org/?graph=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the graph to the query', + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with an IRI graph parameter in angular brackets', + 'http://example.org/?graph=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the graph to the query', + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a variable graph parameter', + 'http://example.org/?graph=%3Ffoo', + 'should not add the graph to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with a blank graph parameter', + 'http://example.org/?graph=_:foo', + 'should not add the graph to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with a literal graph parameter', + 'http://example.org/?graph=%22foo%22', + 'should not add the graph to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'the default graph as graph parameter', + 'http://example.org/?graph=urn%3Aldf%3AdefaultGraph', + 'should add the default graph to the query', + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.defaultGraph() }, + ], + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); @@ -251,235 +249,233 @@ describe('QuadPatternRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - ( - [ - [ - 'a URL without query parameters', - 'http://example.org/', - 'should not change the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an empty subject parameter', - 'http://example.org/?subject=', - 'should not add the subject to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI subject parameter', - 'http://example.org/?subject=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name subject parameter', - 'http://example.org/?subject=foo%3Abar', - 'should add the expanded subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name subject parameter with the "http" prefix', - 'http://example.org/?subject=http%3AConnection', - 'should add the expanded subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, - ], - [ - 'a URL with a prefixed name subject parameter with an unknown prefix', - 'http://example.org/?subject=bar%3Afoo', - 'should add the non-expanded subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('bar:foo') }, - ], - [ - 'a URL with an IRI subject parameter in angular brackets', - 'http://example.org/?subject=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the non-expanded subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name subject parameter in angular brackets', - 'http://example.org/?subject=%3Cfoo%3Abar%3E', - 'should add the non-expanded subject to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('foo:bar') }, - ], - [ - 'a URL with an empty predicate parameter', - 'http://example.org/?predicate=', - 'should not add the predicate to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI predicate parameter', - 'http://example.org/?predicate=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name predicate parameter', - 'http://example.org/?predicate=foo%3Abar', - 'should add the expanded predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name predicate parameter with the "http" prefix', - 'http://example.org/?predicate=http%3Aauthority', - 'should add the expanded predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://www.w3.org/2011/http#authority') }, - ], - [ - 'a URL with a prefixed name predicate parameter with an unknown prefix', - 'http://example.org/?predicate=bar%3Afoo', - 'should add the non-expanded predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('bar:foo') }, - ], - [ - 'a URL with an IRI predicate parameter in angular brackets', - 'http://example.org/?predicate=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the non-expanded predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name predicate parameter in angular brackets', - 'http://example.org/?predicate=%3Cfoo%3Abar%3E', - 'should add the non-expanded predicate to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('foo:bar') }, - ], - [ - 'a URL with an empty object parameter', - 'http://example.org/?object=', - 'should not add the object to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI object parameter', - 'http://example.org/?object=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name object parameter', - 'http://example.org/?object=foo%3Abar', - 'should add the expanded object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name object parameter with the "http" prefix', - 'http://example.org/?object=http%3AConnection', - 'should add the expanded object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, - ], - [ - 'a URL with a prefixed name object parameter with an unknown prefix', - 'http://example.org/?object=bar%3Afoo', - 'should add the non-expanded object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('bar:foo') }, - ], - [ - 'a URL with an IRI object parameter in angular brackets', - 'http://example.org/?object=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the non-expanded object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name object parameter in angular brackets', - 'http://example.org/?object=%3Cfoo%3Abar%3E', - 'should add the non-expanded object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('foo:bar') }, - ], - [ - 'a URL with a typed literal object parameter', - 'http://example.org/?object=%22foo%22%5E%5Ehttp%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, - ], - [ - 'a URL with a typed literal object parameter in angular brackets', - 'http://example.org/?object=%22foo%22%5E%5E%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, - ], - [ - 'a URL with a prefixed literal object parameter', - 'http://example.org/?object=%22foo%22%5E%5Efoo%3Abar', - 'should add the object to the query', - { a: 1 }, - { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, - ], - [ - 'a URL with an empty graph parameter', - 'http://example.org/?graph=', - 'should not add the graph to the query', - { a: 1 }, - { a: 1 }, - ], - [ - 'a URL with an IRI graph parameter', - 'http://example.org/?graph=http%3A%2F%2Fexample.org%2Ffoo%23bar', - 'should add the graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name graph parameter', - 'http://example.org/?graph=foo%3Abar', - 'should add the expanded graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name graph parameter with the "http" prefix', - 'http://example.org/?graph=http%3AConnection', - 'should add the expanded graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, - ], - [ - 'a URL with a prefixed name graph parameter with an unknown prefix', - 'http://example.org/?graph=bar%3Afoo', - 'should add the non-expanded graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('bar:foo') }, - ], - [ - 'a URL with an IRI graph parameter in angular brackets', - 'http://example.org/?graph=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', - 'should add the non-expanded graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, - ], - [ - 'a URL with a prefixed name graph parameter in angular brackets', - 'http://example.org/?graph=%3Cfoo%3Abar%3E', - 'should add the non-expanded graph to the query', - { a: 1 }, - { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('foo:bar') }, - ], - ] as Array<[string, string, string, TestQuery, TestQuery]> - ) - .forEach((args) => { extractQueryParams(router, ...args); }); + const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + [ + 'a URL without query parameters', + 'http://example.org/', + 'should not change the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an empty subject parameter', + 'http://example.org/?subject=', + 'should not add the subject to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an IRI subject parameter', + 'http://example.org/?subject=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the subject to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name subject parameter', + 'http://example.org/?subject=foo%3Abar', + 'should add the expanded subject to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name subject parameter with the "http" prefix', + 'http://example.org/?subject=http%3AConnection', + 'should add the expanded subject to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, + ], + [ + 'a URL with a prefixed name subject parameter with an unknown prefix', + 'http://example.org/?subject=bar%3Afoo', + 'should add the non-expanded subject to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('bar:foo') }, + ], + [ + 'a URL with an IRI subject parameter in angular brackets', + 'http://example.org/?subject=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the non-expanded subject to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name subject parameter in angular brackets', + 'http://example.org/?subject=%3Cfoo%3Abar%3E', + 'should add the non-expanded subject to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, subject: dataFactory.namedNode('foo:bar') }, + ], + [ + 'a URL with an empty predicate parameter', + 'http://example.org/?predicate=', + 'should not add the predicate to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an IRI predicate parameter', + 'http://example.org/?predicate=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the predicate to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name predicate parameter', + 'http://example.org/?predicate=foo%3Abar', + 'should add the expanded predicate to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name predicate parameter with the "http" prefix', + 'http://example.org/?predicate=http%3Aauthority', + 'should add the expanded predicate to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://www.w3.org/2011/http#authority') }, + ], + [ + 'a URL with a prefixed name predicate parameter with an unknown prefix', + 'http://example.org/?predicate=bar%3Afoo', + 'should add the non-expanded predicate to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('bar:foo') }, + ], + [ + 'a URL with an IRI predicate parameter in angular brackets', + 'http://example.org/?predicate=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the non-expanded predicate to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name predicate parameter in angular brackets', + 'http://example.org/?predicate=%3Cfoo%3Abar%3E', + 'should add the non-expanded predicate to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, predicate: dataFactory.namedNode('foo:bar') }, + ], + [ + 'a URL with an empty object parameter', + 'http://example.org/?object=', + 'should not add the object to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an IRI object parameter', + 'http://example.org/?object=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name object parameter', + 'http://example.org/?object=foo%3Abar', + 'should add the expanded object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name object parameter with the "http" prefix', + 'http://example.org/?object=http%3AConnection', + 'should add the expanded object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, + ], + [ + 'a URL with a prefixed name object parameter with an unknown prefix', + 'http://example.org/?object=bar%3Afoo', + 'should add the non-expanded object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('bar:foo') }, + ], + [ + 'a URL with an IRI object parameter in angular brackets', + 'http://example.org/?object=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the non-expanded object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name object parameter in angular brackets', + 'http://example.org/?object=%3Cfoo%3Abar%3E', + 'should add the non-expanded object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.namedNode('foo:bar') }, + ], + [ + 'a URL with a typed literal object parameter', + 'http://example.org/?object=%22foo%22%5E%5Ehttp%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + ], + [ + 'a URL with a typed literal object parameter in angular brackets', + 'http://example.org/?object=%22foo%22%5E%5E%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + ], + [ + 'a URL with a prefixed literal object parameter', + 'http://example.org/?object=%22foo%22%5E%5Efoo%3Abar', + 'should add the object to the query', + { a: 1 }, + { a: 1, features: { triplePattern: true }, object: dataFactory.literal('foo', dataFactory.namedNode('http://example.org/foo#bar')) }, + ], + [ + 'a URL with an empty graph parameter', + 'http://example.org/?graph=', + 'should not add the graph to the query', + { a: 1 }, + { a: 1 }, + ], + [ + 'a URL with an IRI graph parameter', + 'http://example.org/?graph=http%3A%2F%2Fexample.org%2Ffoo%23bar', + 'should add the graph to the query', + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name graph parameter', + 'http://example.org/?graph=foo%3Abar', + 'should add the expanded graph to the query', + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name graph parameter with the "http" prefix', + 'http://example.org/?graph=http%3AConnection', + 'should add the expanded graph to the query', + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://www.w3.org/2011/http#Connection') }, + ], + [ + 'a URL with a prefixed name graph parameter with an unknown prefix', + 'http://example.org/?graph=bar%3Afoo', + 'should add the non-expanded graph to the query', + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('bar:foo') }, + ], + [ + 'a URL with an IRI graph parameter in angular brackets', + 'http://example.org/?graph=%3Chttp%3A%2F%2Fexample.org%2Ffoo%23bar%3E', + 'should add the non-expanded graph to the query', + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('http://example.org/foo#bar') }, + ], + [ + 'a URL with a prefixed name graph parameter in angular brackets', + 'http://example.org/?graph=%3Cfoo%3Abar%3E', + 'should add the non-expanded graph to the query', + { a: 1 }, + { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('foo:bar') }, + ], + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); diff --git a/test/DummyServer.ts b/test/DummyServer.ts index b8c2edbb..afb6f688 100644 --- a/test/DummyServer.ts +++ b/test/DummyServer.ts @@ -17,15 +17,14 @@ export function DummyServer(controller: Controller & Partial): const server = http.createServer(); server.on('request', (request: LdfRequest, response: LdfResponse) => { // End the response if the controller did not handle the request - const next: Mock<(error?: Error) => void> = vi.fn((error?: Error) => { + controller.next = vi.fn((error?: Error) => { controller.error = error; if (!response.headersSent) response.writeHead(error ? 500 : 200); response.end(error && error.message || ''); }); - controller.next = next; - try { controller.handleRequest(request as LdfRequestWithUrl, response, next); } - catch (error) { next(error as Error); } + try { controller.handleRequest(request as LdfRequestWithUrl, response, controller.next); } + catch (error) { controller.next(error as Error); } }); return server; } From 7b0aa1c8ca6af41ed8735e82bd36a011225d8457 Mon Sep 17 00:00:00 2001 From: Michiel Date: Mon, 31 Aug 2026 13:19:19 +0000 Subject: [PATCH 08/15] adding some Types... --- packages/core/test/routers/DatasourceRouter-test.ts | 5 +++-- packages/core/test/routers/PageRouter-test.ts | 7 ++++--- .../test/datasources/HdtDatasource-test.ts | 10 +++++----- .../test/datasources/SparqlDatasource-test.ts | 2 +- .../test/routers/QuadPatternRouter-test.ts | 5 +++-- .../QuadPatternFragmentsRdfView-test.ts | 12 ++++++------ 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/core/test/routers/DatasourceRouter-test.ts b/packages/core/test/routers/DatasourceRouter-test.ts index f1bcf0c9..3aed4fe2 100644 --- a/packages/core/test/routers/DatasourceRouter-test.ts +++ b/packages/core/test/routers/DatasourceRouter-test.ts @@ -9,6 +9,7 @@ import type { Query } from '../../index'; // Query has no index signature; this lets the test tables below use an // arbitrary 'a' field as a stand-in for "pre-existing data that should survive". type TestQuery = Query & { a?: number }; +type QueryParamsTestCase = [string, string, string, TestQuery, TestQuery]; describe('DatasourceRouter', () => { describe('The DatasourceRouter module', () => { @@ -26,7 +27,7 @@ describe('DatasourceRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + const rows: QueryParamsTestCase[] = [ [ 'a root URL without trailing slash or query parameters', 'http://example.org', @@ -89,7 +90,7 @@ describe('DatasourceRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + const rows: QueryParamsTestCase[] = [ [ 'a root URL', 'http://example.org/my/base/', diff --git a/packages/core/test/routers/PageRouter-test.ts b/packages/core/test/routers/PageRouter-test.ts index b267bfc1..3c85d976 100644 --- a/packages/core/test/routers/PageRouter-test.ts +++ b/packages/core/test/routers/PageRouter-test.ts @@ -8,6 +8,7 @@ import type { Query } from '../../index'; // Query has no index signature; this lets the test tables below use an // arbitrary 'a' field as a stand-in for "pre-existing data that should survive". type TestQuery = Query & { a?: number }; +type QueryParamsTestCase = [string, string, string, TestQuery, TestQuery]; describe('PageRouter', () => { describe('The PageRouter module', () => { @@ -25,7 +26,7 @@ describe('PageRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + const rows: QueryParamsTestCase[] = [ [ 'a URL without query parameters', 'http://example.org/', @@ -93,7 +94,7 @@ describe('PageRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + const rows: QueryParamsTestCase[] = [ [ 'a URL without query parameters', 'http://example.org/', @@ -161,7 +162,7 @@ describe('PageRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + const rows: QueryParamsTestCase[] = [ [ 'a URL without query parameters', 'http://example.org/', diff --git a/packages/datasource-hdt/test/datasources/HdtDatasource-test.ts b/packages/datasource-hdt/test/datasources/HdtDatasource-test.ts index ff7b3baa..00898c1a 100644 --- a/packages/datasource-hdt/test/datasources/HdtDatasource-test.ts +++ b/packages/datasource-hdt/test/datasources/HdtDatasource-test.ts @@ -26,14 +26,14 @@ describe('HdtDatasource', () => { let instance = new HdtDatasource({ dataFactory, file: exampleHdtFile }); instance.initialize(); expect(instance).toBeInstanceOf(HdtDatasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); it('should create Datasource objects', async () => { let instance = new HdtDatasource({ dataFactory, file: exampleHdtFile }); instance.initialize(); expect(instance).toBeInstanceOf(Datasource); - await new Promise((resolve) => instance.close(resolve)); + await new Promise((resolve) => instance.close(resolve)); }); }); @@ -45,7 +45,7 @@ describe('HdtDatasource', () => { datasource.initialize(); await once(datasource, 'initialized'); }); - afterAll(() => new Promise((resolve) => datasource.close(resolve))); + afterAll(() => new Promise((resolve) => datasource.close(resolve))); itShouldExecute(getDatasource, 'the empty query', @@ -106,7 +106,7 @@ describe('HdtDatasource', () => { datasource.initialize(); await once(datasource, 'initialized'); }); - afterAll(() => new Promise((resolve) => datasource.close(resolve))); + afterAll(() => new Promise((resolve) => datasource.close(resolve))); itShouldExecute(getDatasource, 'the empty query', @@ -157,7 +157,7 @@ describe('HdtDatasource', () => { datasource.initialize(); await once(datasource, 'initialized'); }); - afterAll(() => new Promise((resolve) => datasource.close(resolve))); + afterAll(() => new Promise((resolve) => datasource.close(resolve))); itShouldExecute(getDatasource, 'the empty query', diff --git a/packages/datasource-sparql/test/datasources/SparqlDatasource-test.ts b/packages/datasource-sparql/test/datasources/SparqlDatasource-test.ts index 42207171..5788dd67 100644 --- a/packages/datasource-sparql/test/datasources/SparqlDatasource-test.ts +++ b/packages/datasource-sparql/test/datasources/SparqlDatasource-test.ts @@ -266,7 +266,7 @@ describe('SparqlDatasource', () => { let query = { subject: dataFactory.namedNode('abcde'), features: { quadPattern: true } }; result = datasource.select(query); let errorEvent = once(result, 'error'); - request.mock.calls[0][1]?.(new Error('query response error')); + (request.mock.calls[0][1] as RequestCallback)(new Error('query response error')); [error] = await errorEvent; }); diff --git a/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts b/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts index ce637e47..760f77d0 100644 --- a/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts +++ b/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts @@ -8,6 +8,7 @@ import type { Query } from '@ldf/core'; // Query has no index signature; this lets the test tables below use an // arbitrary 'a' field as a stand-in for "pre-existing data that should survive". type TestQuery = Query & { a?: number }; +type QueryParamsTestCase = [string, string, string, TestQuery, TestQuery]; import { DataFactory as dataFactory } from 'n3'; const { QuadPatternRouter } = routers; @@ -28,7 +29,7 @@ describe('QuadPatternRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + const rows: QueryParamsTestCase[] = [ [ 'a URL without query parameters', 'http://example.org/', @@ -249,7 +250,7 @@ describe('QuadPatternRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - const rows: Array<[string, string, string, TestQuery, TestQuery]> = [ + const rows: QueryParamsTestCase[] = [ [ 'a URL without query parameters', 'http://example.org/', diff --git a/packages/feature-qpf/test/views/quadpatternfragments/QuadPatternFragmentsRdfView-test.ts b/packages/feature-qpf/test/views/quadpatternfragments/QuadPatternFragmentsRdfView-test.ts index d4812d7f..262e55c8 100644 --- a/packages/feature-qpf/test/views/quadpatternfragments/QuadPatternFragmentsRdfView-test.ts +++ b/packages/feature-qpf/test/views/quadpatternfragments/QuadPatternFragmentsRdfView-test.ts @@ -80,7 +80,7 @@ describe('QuadPatternFragmentsRdfView', () => { describe('with an empty triple stream', () => { let results = empty(); let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { + beforeAll(() => new Promise((resolve) => { settings.results = results; response.getHeader = vi.fn().mockReturnValue(format); view.render(settings, createRequest(), response, resolve); @@ -99,7 +99,7 @@ describe('QuadPatternFragmentsRdfView', () => { dataFactory.quad(dataFactory.namedNode('f'), dataFactory.namedNode('g'), dataFactory.namedNode('h'), dataFactory.defaultGraph()), ]); let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { + beforeAll(() => new Promise((resolve) => { let transform = new TransformIterator(); settings.results = transform; response.getHeader = vi.fn().mockReturnValue(format); @@ -120,7 +120,7 @@ describe('QuadPatternFragmentsRdfView', () => { dataFactory.quad(dataFactory.namedNode('f'), dataFactory.namedNode('g'), dataFactory.namedNode('h'), dataFactory.defaultGraph()), ]); let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { + beforeAll(() => new Promise((resolve) => { settings.results = results; response.getHeader = vi.fn().mockReturnValue(format); view.render(settings, createRequest(), response, resolve); @@ -147,7 +147,7 @@ describe('QuadPatternFragmentsRdfView', () => { query: { limit: 100 }, }; let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { + beforeAll(() => new Promise((resolve) => { settings.results = results; response.getHeader = vi.fn().mockReturnValue(format); view.render(settings, createRequest(), response, resolve); @@ -180,7 +180,7 @@ describe('QuadPatternFragmentsRdfView', () => { query: { limit: 100, offset: 1133 }, }; let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { + beforeAll(() => new Promise((resolve) => { settings.results = results; response.getHeader = vi.fn().mockReturnValue(format); view.render(settings, createRequest(), response, resolve); @@ -213,7 +213,7 @@ describe('QuadPatternFragmentsRdfView', () => { query: { limit: 100, offset: 1135 }, }; let response = createStreamCapture(); - beforeAll(() => new Promise((resolve) => { + beforeAll(() => new Promise((resolve) => { settings.results = results; response.getHeader = vi.fn().mockReturnValue(format); view.render(settings, createRequest(), response, resolve); From 395f7f2e9b7ba4c5375d83b753af6669728ff24f Mon Sep 17 00:00:00 2001 From: Michiel Date: Mon, 31 Aug 2026 14:24:12 +0000 Subject: [PATCH 09/15] another batch of improvements --- .eslintrc | 2 +- package.json | 2 +- .../core/test/controllers/Controller-test.ts | 6 +++--- .../core/test/datasources/Datasource-test.ts | 10 +++++----- packages/core/test/views/View-test.ts | 2 +- .../QuadPatternFragmentsController-test.ts | 7 ++----- tsconfig.json | 4 +++- tsconfig.typecheck.json | 17 ----------------- 8 files changed, 16 insertions(+), 34 deletions(-) delete mode 100644 tsconfig.typecheck.json diff --git a/.eslintrc b/.eslintrc index e5949a14..9132246a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -209,7 +209,7 @@ parserOptions: { ecmaVersion: 9, sourceType: "module", - project: "./tsconfig.typecheck.json" + project: "./tsconfig.json" }, plugins: [ "@typescript-eslint" ], rules: { diff --git a/package.json b/package.json index 2b719ebd..2028793a 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "lint-changed": "lerna run lint --since HEAD", "test": "vitest run --coverage", "test-ci": "vitest run --coverage", - "typecheck": "tsc -p tsconfig.typecheck.json", + "typecheck": "tsc -p tsconfig.json --noEmit", "build": "tsc -p tsconfig.json", "lint": "eslint packages/*/bin/* packages/*/lib packages/*/test test --ext .js,.ts", "clean": "rm -rf ./node_modules && rm -rf ./packages/*/node_modules", diff --git a/packages/core/test/controllers/Controller-test.ts b/packages/core/test/controllers/Controller-test.ts index 4448db92..0248770a 100644 --- a/packages/core/test/controllers/Controller-test.ts +++ b/packages/core/test/controllers/Controller-test.ts @@ -10,9 +10,9 @@ import type { LdfRequestWithUrl, LdfResponse, ViewSettings } from '../../index'; import * as http from 'http'; import * as request from 'supertest'; -// Controller marks _handleRequest protected so subclasses can implement it; -// this subclass widens it to public so this suite can spy on the base class's -// own behavior directly, the way its subclasses already do. +// Controller marks _handleRequest protected so subclasses can implement it. This +// subclass widens it to public so this suite can spy on the base class's own +// behavior directly, the way its subclasses already do. class TestableController extends Controller { override _handleRequest(request: LdfRequestWithUrl, response: LdfResponse, next: (error?: Error) => void, settings?: ViewSettings): void { return super._handleRequest(request, response, next, settings); diff --git a/packages/core/test/datasources/Datasource-test.ts b/packages/core/test/datasources/Datasource-test.ts index efcffe99..e18dbe8b 100644 --- a/packages/core/test/datasources/Datasource-test.ts +++ b/packages/core/test/datasources/Datasource-test.ts @@ -7,15 +7,15 @@ import { EventEmitter, once } from 'events'; import * as fs from 'fs'; import * as path from 'path'; import * as N3 from 'n3'; -import type { Quad } from 'rdf-js'; +import type { Quad, Term } from 'rdf-js'; import type { BufferedIterator } from 'asynciterator'; import type { Pushable, Query } from '../../lib/types'; const exampleFile = path.join(__dirname, '../../../../test/assets/test.ttl'); const dataFactory = N3.DataFactory; -// Datasource marks these members protected so subclasses can use them internally; -// this subclass widens them to public so this suite can exercise the base class's +// Datasource marks these members protected so subclasses can use them internally. +// This subclass widens them to public so this suite can exercise the base class's // own behavior directly, the way its subclasses already do. class TestableDatasource extends Datasource { override _fetch(options: { url: string } & Record): EventEmitter { @@ -330,7 +330,7 @@ describe('Datasource', () => { features: { custom: true }, }); expect(executeQuerySpy.mock.calls[0][0].features).toEqual({ custom: true }), - executeQuerySpy.mock.calls[0][0].graph?.equals(dataFactory.defaultGraph()); + (executeQuerySpy.mock.calls[0][0].graph as Term).equals(dataFactory.defaultGraph()); }); it('should query the default graph as the empty graph', () => { @@ -339,7 +339,7 @@ describe('Datasource', () => { features: { custom: true }, }); expect(executeQuerySpy.mock.calls[0][0].features).toEqual({ custom: true }), - executeQuerySpy.mock.calls[0][0].graph?.equals(dataFactory.namedNode('urn:ldf:emptyGraph')); + (executeQuerySpy.mock.calls[0][0].graph as Term).equals(dataFactory.namedNode('urn:ldf:emptyGraph')); }); }); }); diff --git a/packages/core/test/views/View-test.ts b/packages/core/test/views/View-test.ts index d59d4cf2..4f2dd834 100644 --- a/packages/core/test/views/View-test.ts +++ b/packages/core/test/views/View-test.ts @@ -7,7 +7,7 @@ import { IncomingMessage, ServerResponse } from 'http'; import { Socket } from 'net'; import type { LdfRequest, LdfResponse, RenderDone, ViewSettings } from '../../index'; -// View marks _render protected so subclasses can implement it; this subclass +// View marks _render protected so subclasses can implement it. This subclass // widens it to public so this suite can mock/spy on the base class's own // behavior directly, the way its subclasses already do. class TestableView extends View { diff --git a/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts index 0710aa98..40228d44 100644 --- a/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts +++ b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts @@ -442,13 +442,10 @@ describe('QuadPatternFragmentsController', () => { error = new Error('datasource error'); datasource = new Datasource({ dataFactory }); datasource.supportsQuery = vi.fn().mockReturnValue(true); - // Mocks Datasource.select's own callback-based signature. - /* eslint-disable promise/prefer-await-to-callbacks */ - datasource.select = vi.fn((query: Query, callback?: (error?: Error) => void) => { - setImmediate(() => callback?.(error)); + datasource.select = vi.fn((query: Query, onError?: (error?: Error) => void) => { + setImmediate(() => onError?.(error)); return empty(); }); - /* eslint-enable promise/prefer-await-to-callbacks */ datasource.supportedFeatures = { triplePattern: true }; view = new QuadPatternFragmentsRdfView({ dataFactory }); view.render = vi.fn(); // avoid writing a partial body diff --git a/tsconfig.json b/tsconfig.json index ad063175..1de65cca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,12 +15,14 @@ "preserveConstEnums": true, "removeComments": false, "sourceMap": true, - "skipLibCheck": false + "skipLibCheck": true }, "include": [ "packages/*/index.ts", "packages/*/lib/**/*", "packages/*/bin/**/*", + "packages/*/test/**/*.ts", + "test/**/*.ts", "types/**/*.d.ts" ], "exclude": [ diff --git a/tsconfig.typecheck.json b/tsconfig.typecheck.json deleted file mode 100644 index 8038e5a5..00000000 --- a/tsconfig.typecheck.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "// why": "separate from tsconfig.json so 'yarn build' (tsc -p tsconfig.json) never emits compiled .js for test files; this config adds test dirs and is noEmit-only, used by 'yarn typecheck' and ESLint's type-aware parserOptions.project", - "// skipLibCheck": "vitest's own .d.ts files reference package-exports-only subpaths (e.g. @vitest/utils/display) that this project's module/moduleResolution can't follow; skipLibCheck avoids that without changing module/moduleResolution project-wide (which broke asynciterator resolution and import-equals syntax in existing lib code when tried)", - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": true, - "skipLibCheck": true - }, - "include": [ - "packages/*/index.ts", - "packages/*/lib/**/*", - "packages/*/bin/**/*", - "packages/*/test/**/*.ts", - "test/**/*.ts", - "types/**/*.d.ts" - ] -} From 09b4f4460f7d27ce09fdfd950db7364174a4f09b Mon Sep 17 00:00:00 2001 From: Michiel Date: Tue, 1 Sep 2026 08:28:25 +0000 Subject: [PATCH 10/15] We on go --- .../test/LinkedDataFragmentsServer-test.ts | 113 +++++++++--------- .../test/controllers/AssetsController-test.ts | 42 +++---- .../core/test/controllers/Controller-test.ts | 52 ++++---- .../controllers/DereferenceController-test.ts | 33 ++--- .../controllers/NotFoundController-test.ts | 96 ++++++++------- .../QuadPatternFragmentsController-test.ts | 100 ++++++++-------- .../controllers/SummaryController-test.ts | 42 +++---- test/test-helpers.ts | 11 +- tsconfig.json | 5 +- 9 files changed, 257 insertions(+), 237 deletions(-) diff --git a/packages/core/test/LinkedDataFragmentsServer-test.ts b/packages/core/test/LinkedDataFragmentsServer-test.ts index 46dfbc00..30339001 100644 --- a/packages/core/test/LinkedDataFragmentsServer-test.ts +++ b/packages/core/test/LinkedDataFragmentsServer-test.ts @@ -5,16 +5,15 @@ import type { Mock } from 'vitest'; import { LinkedDataFragmentsServer } from '../lib/LinkedDataFragmentsServer'; import { Controller } from '../lib/controllers/Controller'; import type { LdfRequestWithUrl, LdfResponse } from '../index'; - -import * as request from 'supertest'; +import { listen } from '../../../test/test-helpers'; describe('LinkedDataFragmentsServer', () => { describe('A LinkedDataFragmentsServer instance with one controller', () => { - let server: ReturnType, controller: Controller, client: request.Agent; + let baseUrl: string, controller: Controller; // Kept separate so .mockClear() has a Mock-typed target. controller.handleRequest's // declared type is just Controller's plain function signature, not Mock. let handleRequestSpy: Mock; - beforeAll(() => { + beforeAll(async () => { controller = new Controller(); handleRequestSpy = vi.fn((request: LdfRequestWithUrl, response: LdfResponse, next: (error?: Error) => void) => { switch (request.url) { @@ -28,7 +27,7 @@ describe('LinkedDataFragmentsServer', () => { } }); controller.handleRequest = handleRequestSpy; - server = new LinkedDataFragmentsServer({ + baseUrl = await listen(new LinkedDataFragmentsServer({ controllers: [controller], log: vi.fn(), response: { @@ -37,71 +36,70 @@ describe('LinkedDataFragmentsServer', () => { 'My-Header': 'value', }, }, - }); - client = request.agent(server); + })); }); beforeEach(() => { handleRequestSpy.mockClear(); }); it('should send the configured headers', async () => { - let response = await client.head('/'); - expect(response.headers).toHaveProperty('access-control-allow-origin', '*'); - expect(response.headers).toHaveProperty('my-header', 'value'); + let response = await fetch(baseUrl + '/', { method: 'HEAD' }); + expect(response.headers.get('access-control-allow-origin')).toBe('*'); + expect(response.headers.get('my-header')).toBe('value'); }); it('should not allow POST requests', async () => { - let response = await client.post('/'); + let response = await fetch(baseUrl + '/', { method: 'POST' }); expect(controller.handleRequest).not.toHaveBeenCalled(); - expect(response).toHaveProperty('statusCode', 405); - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); - expect(response).toHaveProperty('text', 'The HTTP method "POST" is not allowed; try "GET" instead.'); + expect(response.status).toBe(405); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); + expect(await response.text()).toBe('The HTTP method "POST" is not allowed; try "GET" instead.'); }); it('should send a body with GET requests', async () => { - let response = await client.get('/handle'); + let response = await fetch(baseUrl + '/handle'); expect(controller.handleRequest).toHaveBeenCalledOnce(); - expect(response).toHaveProperty('statusCode', 200); - expect(response).toHaveProperty('text', 'body contents'); + expect(response.status).toBe(200); + expect(await response.text()).toBe('body contents'); }); it('should not send a body with HEAD requests', async () => { - let response = await client.head('/handle'); + let response = await fetch(baseUrl + '/handle', { method: 'HEAD' }); expect(controller.handleRequest).toHaveBeenCalledOnce(); - expect(response).toHaveProperty('statusCode', 200); - expect(response.body).not.toHaveProperty('length'); + expect(response.status).toBe(200); + expect(await response.text()).toBe(''); }); it('should not send a body with OPTIONS requests', async () => { - let response = await client.options('/handle'); + let response = await fetch(baseUrl + '/handle', { method: 'OPTIONS' }); expect(controller.handleRequest).toHaveBeenCalledOnce(); - expect(response).toHaveProperty('statusCode', 200); - expect(response).toHaveProperty('text', ''); + expect(response.status).toBe(200); + expect(await response.text()).toBe(''); }); it('should error when the controller cannot handle the request', async () => { - let response = await client.get('/unsupported'); + let response = await fetch(baseUrl + '/unsupported'); expect(controller.handleRequest).toHaveBeenCalledOnce(); - expect(response).toHaveProperty('statusCode', 500); - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); - expect(response).toHaveProperty('text', 'Application error: No controller for /unsupported\n'); + expect(response.status).toBe(500); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); + expect(await response.text()).toBe('Application error: No controller for /unsupported\n'); }); it('should error when the controller errors', async () => { - let response = await client.get('/error'); + let response = await fetch(baseUrl + '/error'); expect(controller.handleRequest).toHaveBeenCalledOnce(); - expect(response).toHaveProperty('statusCode', 500); - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); - expect(response).toHaveProperty('text', 'Application error: error message\n'); + expect(response.status).toBe(500); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); + expect(await response.text()).toBe('Application error: error message\n'); }); }); describe('A LinkedDataFragmentsServer instance with two controllers', () => { - let server: ReturnType, controllerA: Controller, controllerB: Controller, client: request.Agent; + let baseUrl: string, controllerA: Controller, controllerB: Controller; // Kept separate so .mockClear() has a Mock-typed target. controllerX.handleRequest's // declared type is just Controller's plain function signature, not Mock. let handleRequestSpyA: Mock, handleRequestSpyB: Mock; - beforeAll(() => { + beforeAll(async () => { controllerA = new Controller(); handleRequestSpyA = vi.fn((request: LdfRequestWithUrl, response: LdfResponse, next: (error?: Error) => void) => { switch (request.url) { @@ -129,11 +127,10 @@ describe('LinkedDataFragmentsServer', () => { } }); controllerB.handleRequest = handleRequestSpyB; - server = new LinkedDataFragmentsServer({ + baseUrl = await listen(new LinkedDataFragmentsServer({ controllers: [controllerA, controllerB], log: vi.fn(), - }); - client = request.agent(server); + })); }); beforeEach(() => { handleRequestSpyA.mockClear(); @@ -141,55 +138,55 @@ describe('LinkedDataFragmentsServer', () => { }); it('should not allow POST requests', async () => { - let response = await client.post('/'); + let response = await fetch(baseUrl + '/', { method: 'POST' }); expect(controllerA.handleRequest).not.toHaveBeenCalled(); expect(controllerB.handleRequest).not.toHaveBeenCalled(); - expect(response).toHaveProperty('statusCode', 405); - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); - expect(response).toHaveProperty('text', 'The HTTP method "POST" is not allowed; try "GET" instead.'); + expect(response.status).toBe(405); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); + expect(await response.text()).toBe('The HTTP method "POST" is not allowed; try "GET" instead.'); }); it('should use the first controller when it can handle the request', async () => { - let response = await client.get('/handleA'); + let response = await fetch(baseUrl + '/handleA'); expect(controllerA.handleRequest).toHaveBeenCalledOnce(); expect(controllerB.handleRequest).not.toHaveBeenCalled(); - expect(response).toHaveProperty('statusCode', 200); - expect(response).toHaveProperty('text', 'body contents A'); + expect(response.status).toBe(200); + expect(await response.text()).toBe('body contents A'); }); it('should use the second controller when the first cannot handle the request', async () => { - let response = await client.get('/handleB'); + let response = await fetch(baseUrl + '/handleB'); expect(controllerA.handleRequest).toHaveBeenCalledOnce(); expect(controllerB.handleRequest).toHaveBeenCalledOnce(); - expect(response).toHaveProperty('statusCode', 200); - expect(response).toHaveProperty('text', 'body contents B'); + expect(response.status).toBe(200); + expect(await response.text()).toBe('body contents B'); }); it('should error when neither controller can handle the request', async () => { - let response = await client.get('/unsupported'); + let response = await fetch(baseUrl + '/unsupported'); expect(controllerA.handleRequest).toHaveBeenCalledOnce(); expect(controllerB.handleRequest).toHaveBeenCalledOnce(); - expect(response).toHaveProperty('statusCode', 500); - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); - expect(response).toHaveProperty('text', 'Application error: No controller for /unsupported\n'); + expect(response.status).toBe(500); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); + expect(await response.text()).toBe('Application error: No controller for /unsupported\n'); }); it('should error when the first controller errors', async () => { - let response = await client.get('/errorA'); + let response = await fetch(baseUrl + '/errorA'); expect(controllerA.handleRequest).toHaveBeenCalledOnce(); expect(controllerB.handleRequest).not.toHaveBeenCalled(); - expect(response).toHaveProperty('statusCode', 500); - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); - expect(response).toHaveProperty('text', 'Application error: error message A\n'); + expect(response.status).toBe(500); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); + expect(await response.text()).toBe('Application error: error message A\n'); }); it('should error when the second controller errors', async () => { - let response = await client.get('/errorB'); + let response = await fetch(baseUrl + '/errorB'); expect(controllerA.handleRequest).toHaveBeenCalledOnce(); expect(controllerB.handleRequest).toHaveBeenCalledOnce(); - expect(response).toHaveProperty('statusCode', 500); - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); - expect(response).toHaveProperty('text', 'Application error: error message B\n'); + expect(response.status).toBe(500); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); + expect(await response.text()).toBe('Application error: error message B\n'); }); }); }); diff --git a/packages/core/test/controllers/AssetsController-test.ts b/packages/core/test/controllers/AssetsController-test.ts index 2da7c5bf..65bceb14 100644 --- a/packages/core/test/controllers/AssetsController-test.ts +++ b/packages/core/test/controllers/AssetsController-test.ts @@ -2,9 +2,9 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { listen } from '../../../../test/test-helpers'; import { controllers } from '../../index'; -import * as request from 'supertest'; import * as fs from 'fs'; import * as path from 'path'; @@ -22,49 +22,49 @@ describe('AssetsController', () => { }); describe('An AssetsController instance', () => { - let controller: InstanceType & Partial, client: request.Agent; - beforeAll(() => { + let controller: InstanceType & Partial, baseUrl: string; + beforeAll(async () => { controller = new AssetsController(); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); it('should correctly serve SVG assets', async () => { - let response = await client.get('/assets/images/logo'); + let response = await fetch(baseUrl + '/assets/images/logo'); let asset = fs.readFileSync(path.join(__dirname, '/../../assets/images/logo.svg'), 'utf8'); expect(controller.next).not.toHaveBeenCalled(); - expect(response).toHaveProperty('statusCode', 200); - expect(response.headers).toHaveProperty('content-type', 'image/svg+xml'); - expect(response.headers).toHaveProperty('cache-control', 'public,max-age=1209600'); - expect(String(response.body)).toBe(asset); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('image/svg+xml'); + expect(response.headers.get('cache-control')).toBe('public,max-age=1209600'); + expect(await response.text()).toBe(asset); }); it('should correctly serve CSS assets', async () => { - let response = await client.get('/assets/styles/ldf-server'); + let response = await fetch(baseUrl + '/assets/styles/ldf-server'); let asset = fs.readFileSync(path.join(__dirname, '/../../assets/styles/ldf-server.css'), 'utf8'); expect(controller.next).not.toHaveBeenCalled(); - expect(response).toHaveProperty('statusCode', 200); - expect(response.headers).toHaveProperty('content-type', 'text/css;charset=utf-8'); - expect(response.headers).toHaveProperty('cache-control', 'public,max-age=1209600'); - expect(response).toHaveProperty('text', asset); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('text/css;charset=utf-8'); + expect(response.headers.get('cache-control')).toBe('public,max-age=1209600'); + expect(await response.text()).toBe(asset); }); it('should correctly serve ICO assets', async () => { - let response = await client.get('/favicon.ico'); + let response = await fetch(baseUrl + '/favicon.ico'); let asset = fs.readFileSync(path.join(__dirname, '/../../assets/favicon.ico'), 'utf8'); expect(controller.next).not.toHaveBeenCalled(); - expect(response).toHaveProperty('statusCode', 200); - expect(response.headers).toHaveProperty('content-type', 'image/vnd.microsoft.icon'); - expect(response.headers).toHaveProperty('cache-control', 'public,max-age=1209600'); - expect(String(response.body)).toBe(asset); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('image/vnd.microsoft.icon'); + expect(response.headers.get('cache-control')).toBe('public,max-age=1209600'); + expect(await response.text()).toBe(asset); }); it('should hand over to the next controller if no asset with that name is found', async () => { - await client.get('/assets/unknown'); + await fetch(baseUrl + '/assets/unknown'); expect(controller.next).toHaveBeenCalledOnce(); }); it('should hand over to the next controller for non-asset paths', async () => { - await client.get('/other'); + await fetch(baseUrl + '/other'); expect(controller.next).toHaveBeenCalledOnce(); }); }); diff --git a/packages/core/test/controllers/Controller-test.ts b/packages/core/test/controllers/Controller-test.ts index 0248770a..5b52160e 100644 --- a/packages/core/test/controllers/Controller-test.ts +++ b/packages/core/test/controllers/Controller-test.ts @@ -3,12 +3,12 @@ import { describe, it, expect, beforeAll, vi } from 'vitest'; import type { Mock } from 'vitest'; import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { listen } from '../../../../test/test-helpers'; import { Controller } from '../../lib/controllers/Controller'; import { UrlData } from '../../index'; import type { LdfRequestWithUrl, LdfResponse, ViewSettings } from '../../index'; import * as http from 'http'; -import * as request from 'supertest'; // Controller marks _handleRequest protected so subclasses can implement it. This // subclass widens it to public so this suite can spy on the base class's own @@ -32,16 +32,16 @@ describe('Controller', () => { describe('A Controller instance without baseURL', () => { let controller: TestableController & Partial; - let client: request.Agent; + let baseUrl: string; let handleRequestSpy: Mock; - beforeAll(() => { + beforeAll(async () => { controller = new TestableController(); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); describe('receiving a request', () => { - beforeAll(() => client.get('/path?a=b')); + beforeAll(() => fetch(baseUrl + '/path?a=b')); it('should call _handleRequest with request, response and next', () => { expect(handleRequestSpy).toHaveBeenCalledOnce(); @@ -70,20 +70,22 @@ describe('Controller', () => { describe('A Controller instance without baseURL using Forwarded header', () => { let controller: TestableController & Partial; - let client: request.Agent; + let baseUrl: string; let handleRequestSpy: Mock; - beforeAll(() => { + beforeAll(async () => { controller = new TestableController({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); describe('receiving a request', () => { - beforeAll(() => client - .get('/path?a=b') - .set('X-Forwarded-Host', 'foo:5000') - // NOTE: the priority will go to the Forwarded header over the X-Forwarded-Host header - .set('Forwarded', 'proto=https;host="bar:8000"')); + beforeAll(() => fetch(baseUrl + '/path?a=b', { + headers: { + 'X-Forwarded-Host': 'foo:5000', + // NOTE: the priority will go to the Forwarded header over the X-Forwarded-Host header + 'Forwarded': 'proto=https;host="bar:8000"', + }, + })); it('should call _handleRequest with request, response and next', () => { expect(handleRequestSpy).toHaveBeenCalledOnce(); @@ -112,19 +114,21 @@ describe('Controller', () => { describe('A Controller instance without baseURL using X-Forwarded-* headers', () => { let controller: TestableController & Partial; - let client: request.Agent; + let baseUrl: string; let handleRequestSpy: Mock; - beforeAll(() => { + beforeAll(async () => { controller = new TestableController(); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); describe('receiving a request', () => { - beforeAll(() => client - .get('/path?a=b') - .set('X-Forwarded-Host', 'foo:5000') - .set('X-Forwarded-Proto', 'https')); + beforeAll(() => fetch(baseUrl + '/path?a=b', { + headers: { + 'X-Forwarded-Host': 'foo:5000', + 'X-Forwarded-Proto': 'https', + }, + })); it('should call _handleRequest with request, response and next', () => { expect(handleRequestSpy).toHaveBeenCalledOnce(); @@ -153,16 +157,16 @@ describe('Controller', () => { describe('A Controller instance with baseURL', () => { let controller: TestableController & Partial; - let client: request.Agent; + let baseUrl: string; let handleRequestSpy: Mock; - beforeAll(() => { + beforeAll(async () => { controller = new TestableController({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); describe('receiving a request', () => { - beforeAll(() => client.get('/path?a=b')); + beforeAll(() => fetch(baseUrl + '/path?a=b')); it('should call _handleRequest with request, response and next', () => { expect(handleRequestSpy).toHaveBeenCalledOnce(); diff --git a/packages/core/test/controllers/DereferenceController-test.ts b/packages/core/test/controllers/DereferenceController-test.ts index b6a680ee..796e51a3 100644 --- a/packages/core/test/controllers/DereferenceController-test.ts +++ b/packages/core/test/controllers/DereferenceController-test.ts @@ -2,11 +2,10 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { listen } from '../../../../test/test-helpers'; import { controllers, datasources } from '../../index'; import { DataFactory as dataFactory } from 'n3'; -import * as request from 'supertest'; - const { DereferenceController } = controllers; const { Datasource } = datasources; @@ -22,47 +21,49 @@ describe('DereferenceController', () => { }); describe('A DereferenceController instance', () => { - let controller: InstanceType & Partial, client: request.Agent; - beforeAll(() => { + let controller: InstanceType & Partial, baseUrl: string, hostname: string; + beforeAll(async () => { controller = new DereferenceController({ dereference: { '/resource/': new Datasource({ dataFactory, path: 'dbpedia/2014' }) } }); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); + hostname = new URL(baseUrl).host; }); describe('receiving a request for a dereferenced URL', () => { - let response: request.Response; - beforeAll(async () => { response = await client.get('/resource/Mickey_Mouse'); }); + let response: Response, responseText: string; + beforeAll(async () => { + response = await fetch(baseUrl + '/resource/Mickey_Mouse', { redirect: 'manual' }); + responseText = await response.text(); + }); it('should not hand over to the next controller', () => { expect(controller.next).not.toHaveBeenCalled(); }); it('should set the status code to 303', () => { - expect(response).toHaveProperty('statusCode', 303); + expect(response.status).toBe(303); }); it('should set the text/plain content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); }); it('should set the Location header correctly', () => { - let hostname = String(response.request.req.getHeader('Host')), - entityUrl = encodeURIComponent('http://' + hostname + '/resource/Mickey_Mouse'), + let entityUrl = encodeURIComponent('http://' + hostname + '/resource/Mickey_Mouse'), expectedLocation = 'http://' + hostname + '/dbpedia/2014?subject=' + entityUrl; - expect(response.headers).toHaveProperty('location', expectedLocation); + expect(response.headers.get('location')).toBe(expectedLocation); }); it('should mention the desired location in the body', () => { - let hostname = String(response.request.req.getHeader('Host')), - entityUrl = encodeURIComponent('http://' + hostname + '/resource/Mickey_Mouse'), + let entityUrl = encodeURIComponent('http://' + hostname + '/resource/Mickey_Mouse'), expectedLocation = 'http://' + hostname + '/dbpedia/2014?subject=' + entityUrl; - expect(response.text).toContain(expectedLocation); + expect(responseText).toContain(expectedLocation); }); }); describe('receiving a request for a non-defererenced URL', () => { - beforeAll(() => client.get('/otherresource/Mickey_Mouse')); + beforeAll(() => fetch(baseUrl + '/otherresource/Mickey_Mouse')); it('should hand over to the next controller', () => { expect(controller.next).toHaveBeenCalledOnce(); diff --git a/packages/core/test/controllers/NotFoundController-test.ts b/packages/core/test/controllers/NotFoundController-test.ts index 99da8872..16404bce 100644 --- a/packages/core/test/controllers/NotFoundController-test.ts +++ b/packages/core/test/controllers/NotFoundController-test.ts @@ -3,10 +3,10 @@ import { describe, it, expect, beforeAll, vi } from 'vitest'; import type { Mock } from 'vitest'; import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { listen } from '../../../../test/test-helpers'; import { controllers, views, datasources as coreDatasources, UrlData } from '../../index'; import type { DatasourceRegistry } from '../../index'; -import * as request from 'supertest'; import { DataFactory as dataFactory } from 'n3'; const { NotFoundController } = controllers; @@ -25,16 +25,17 @@ describe('NotFoundController', () => { }); describe('A NotFoundController instance without views', () => { - let controller: InstanceType & Partial, client: request.Agent; - beforeAll(() => { + let controller: InstanceType & Partial, baseUrl: string; + beforeAll(async () => { controller = new NotFoundController(); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); describe('receiving a request', () => { - let response: request.Response; + let response: Response, responseText: string; beforeAll(async () => { - response = await client.get('/notfound'); + response = await fetch(baseUrl + '/notfound'); + responseText = await response.text(); }); it('should not hand over to the next controller', () => { @@ -42,19 +43,19 @@ describe('NotFoundController', () => { }); it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); + expect(response.status).toBe(404); }); it('should set the text/plain content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); it('should send a textual error body', () => { - expect(response).toHaveProperty('text', '/notfound not found\n'); + expect(responseText).toBe('/notfound not found\n'); }); }); }); @@ -64,15 +65,15 @@ describe('NotFoundController', () => { htmlView: InstanceType, rdfView: InstanceType, htmlRenderSpy: Mock['render']>, rdfRenderSpy: Mock['render']>, - datasources: DatasourceRegistry, client: request.Agent; - beforeAll(() => { + datasources: DatasourceRegistry, baseUrl: string; + beforeAll(async () => { htmlView = new NotFoundHtmlView({ dataFactory }); rdfView = new NotFoundRdfView({ dataFactory }); htmlRenderSpy = vi.spyOn(htmlView, 'render'); rdfRenderSpy = vi.spyOn(rdfView, 'render'); datasources = { a: new Datasource({ dataFactory, title: 'foo', path: 'foo', urlData: new UrlData({ baseURL: 'http://example.org/' }) }) }; controller = new NotFoundController({ views: [htmlView, rdfView], datasources: datasources }); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); function resetAll() { htmlRenderSpy.mockClear(); @@ -80,10 +81,11 @@ describe('NotFoundController', () => { } describe('receiving a request without Accept header', () => { - let response: request.Response; + let response: Response, responseText: string; beforeAll(async () => { resetAll(); - response = await client.get('/notfound'); + response = await fetch(baseUrl + '/notfound'); + responseText = await response.text(); }); it('should not hand over to the next controller', () => { @@ -99,27 +101,28 @@ describe('NotFoundController', () => { }); it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); + expect(response.status).toBe(404); }); it('should set the text/html content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/html;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/html;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); it('should send an HTML error body', () => { - expect(response.text).toContain('No resource with URL /notfound was found.'); + expect(responseText).toContain('No resource with URL /notfound was found.'); }); }); describe('receiving a request with an Accept header of */*', () => { - let response: request.Response; + let response: Response, responseText: string; beforeAll(async () => { resetAll(); - response = await client.get('/notfound').set('Accept', '*/*'); + response = await fetch(baseUrl + '/notfound', { headers: { Accept: '*/*' } }); + responseText = await response.text(); }); it('should not hand over to the next controller', () => { @@ -135,27 +138,28 @@ describe('NotFoundController', () => { }); it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); + expect(response.status).toBe(404); }); it('should set the text/html content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/html;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/html;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); it('should send an HTML error body', () => { - expect(response.text).toContain('No resource with URL /notfound was found.'); + expect(responseText).toContain('No resource with URL /notfound was found.'); }); }); describe('receiving a request with an Accept header of text/html', () => { - let response: request.Response; + let response: Response, responseText: string; beforeAll(async () => { resetAll(); - response = await client.get('/notfound').set('Accept', 'text/html'); + response = await fetch(baseUrl + '/notfound', { headers: { Accept: 'text/html' } }); + responseText = await response.text(); }); it('should not hand over to the next controller', () => { @@ -171,27 +175,28 @@ describe('NotFoundController', () => { }); it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); + expect(response.status).toBe(404); }); it('should set the text/html content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/html;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/html;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); it('should send an HTML error body', () => { - expect(response.text).toContain('No resource with URL /notfound was found.'); + expect(responseText).toContain('No resource with URL /notfound was found.'); }); }); describe('receiving a request with an Accept header of text/turtle', () => { - let response: request.Response; + let response: Response, responseText: string; beforeAll(async () => { resetAll(); - response = await client.get('/notfound').set('Accept', 'text/turtle'); + response = await fetch(baseUrl + '/notfound', { headers: { Accept: 'text/turtle' } }); + responseText = await response.text(); }); it('should not hand over to the next controller', () => { @@ -207,28 +212,29 @@ describe('NotFoundController', () => { }); it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); + expect(response.status).toBe(404); }); it('should set the text/turtle content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/turtle;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/turtle;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); it('should send a Turtle error body', () => { - expect(response.text).toContain(' a '); - expect(response.text).not.toContain('<#metadata> <>.'); + expect(responseText).toContain(' a '); + expect(responseText).not.toContain('<#metadata> <>.'); }); }); describe('receiving a request with an Accept header of application/trig', () => { - let response: request.Response; + let response: Response, responseText: string; beforeAll(async () => { resetAll(); - response = await client.get('/notfound').set('Accept', 'application/trig'); + response = await fetch(baseUrl + '/notfound', { headers: { Accept: 'application/trig' } }); + responseText = await response.text(); }); it('should not hand over to the next controller', () => { @@ -244,20 +250,20 @@ describe('NotFoundController', () => { }); it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); + expect(response.status).toBe(404); }); it('should set the text/html content type', () => { - expect(response.headers).toHaveProperty('content-type', 'application/trig;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('application/trig;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); it('should send a TriG error body', () => { - expect(response.text).toContain(' a '); - expect(response.text).toContain('<#metadata> <>.'); + expect(responseText).toContain(' a '); + expect(responseText).toContain('<#metadata> <>.'); }); }); }); diff --git a/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts index 40228d44..4e35bf85 100644 --- a/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts +++ b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeAll, vi } from 'vitest'; import type { Mock } from 'vitest'; import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { listen } from '../../../../test/test-helpers'; import { controllers, views } from '../../index'; import { datasources as coreDatasources, UrlData } from '@ldf/core'; import type { DatasourceRegistry, Query, QueryFeatures, RouterRequest } from '@ldf/core'; @@ -11,7 +12,6 @@ import { empty } from 'asynciterator'; import type { AsyncIterator } from 'asynciterator'; import * as http from 'http'; -import * as request from 'supertest'; import { DataFactory as dataFactory } from 'n3'; const { QuadPatternFragmentsController } = controllers; @@ -33,7 +33,7 @@ describe('QuadPatternFragmentsController', () => { describe('A QuadPatternFragmentsController instance with 3 routers', () => { let controller: InstanceType & Partial, - client: request.Agent, + baseUrl: string, routerA: { extractQueryParams: Mock<(request: RouterRequest, query: Query) => void> }, routerB: { extractQueryParams: Mock<(request: RouterRequest, query: Query) => void> }, routerC: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, @@ -45,7 +45,7 @@ describe('QuadPatternFragmentsController', () => { view: InstanceType, renderSpy: Mock['render']>, prefixes: Record; - beforeAll(() => { + beforeAll(async () => { routerA = { extractQueryParams: vi.fn() }; routerB = { extractQueryParams: vi.fn(() => { throw new Error('second router error'); }) }; routerC = { @@ -74,7 +74,7 @@ describe('QuadPatternFragmentsController', () => { views: [view], prefixes: prefixes, }); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); function resetAll() { routerA.extractQueryParams.mockClear(); @@ -87,7 +87,7 @@ describe('QuadPatternFragmentsController', () => { describe('receiving a request for a fragment', () => { beforeAll(async () => { resetAll(); - await client.get('/my-datasource?a=b&c=d'); + await fetch(baseUrl + '/my-datasource?a=b&c=d'); }); it('should call the first router with the request and an empty query', () => { @@ -174,7 +174,7 @@ describe('QuadPatternFragmentsController', () => { resetAll(); supportsQuerySpy = vi.fn().mockReturnValue(false); datasource.supportsQuery = supportsQuerySpy; - await client.get('/my-datasource?a=b&c=d'); + await fetch(baseUrl + '/my-datasource?a=b&c=d'); }); it('should verify whether the data source supports the query', () => { @@ -191,11 +191,11 @@ describe('QuadPatternFragmentsController', () => { describe('A QuadPatternFragmentsController instance with 2 views', () => { let controller: InstanceType & Partial, - client: request.Agent, + baseUrl: string, htmlView: InstanceType, rdfView: InstanceType, htmlRenderSpy: Mock['render']>, rdfRenderSpy: Mock['render']>; - beforeAll(() => { + beforeAll(async () => { let datasource = new Datasource({ dataFactory }); datasource.supportsQuery = vi.fn().mockReturnValue(true); datasource.select = vi.fn(() => { @@ -219,7 +219,7 @@ describe('QuadPatternFragmentsController', () => { datasources: { 'my-datasource': datasource }, views: [htmlView, rdfView], }); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); function resetAll() { htmlRenderSpy.mockClear(); @@ -227,10 +227,10 @@ describe('QuadPatternFragmentsController', () => { } describe('receiving a request without Accept header', () => { - let response: request.Response; + let response: Response; beforeAll(async () => { resetAll(); - response = await client.get('/my-datasource'); + response = await fetch(baseUrl + '/my-datasource'); }); it('should call the default view', () => { @@ -238,19 +238,19 @@ describe('QuadPatternFragmentsController', () => { }); it('should set the text/html content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/html;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/html;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); }); describe('receiving a request with an Accept header of */*', () => { - let response: request.Response; + let response: Response; beforeAll(async () => { resetAll(); - response = await client.get('/my-datasource').set('Accept', '*/*'); + response = await fetch(baseUrl + '/my-datasource', { headers: { Accept: '*/*' } }); }); it('should call the HTML view', () => { @@ -258,19 +258,19 @@ describe('QuadPatternFragmentsController', () => { }); it('should set the text/html content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/html;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/html;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); }); describe('receiving a request with an Accept header of text/html', () => { - let response: request.Response; + let response: Response; beforeAll(async () => { resetAll(); - response = await client.get('/my-datasource').set('Accept', 'text/html'); + response = await fetch(baseUrl + '/my-datasource', { headers: { Accept: 'text/html' } }); }); it('should call the HTML view', () => { @@ -278,19 +278,19 @@ describe('QuadPatternFragmentsController', () => { }); it('should set the text/html content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/html;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/html;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); }); describe('receiving a request with an Accept header of text/turtle', () => { - let response: request.Response; + let response: Response; beforeAll(async () => { resetAll(); - response = await client.get('/my-datasource').set('Accept', 'text/turtle'); + response = await fetch(baseUrl + '/my-datasource', { headers: { Accept: 'text/turtle' } }); }); it('should call the Turtle view', () => { @@ -298,19 +298,19 @@ describe('QuadPatternFragmentsController', () => { }); it('should set the text/turtle content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/turtle;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/turtle;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); }); describe('receiving a request with an Accept header of text/n3', () => { - let response: request.Response; + let response: Response; beforeAll(async () => { resetAll(); - response = await client.get('/my-datasource').set('Accept', 'text/n3'); + response = await fetch(baseUrl + '/my-datasource', { headers: { Accept: 'text/n3' } }); }); it('should call the Turtle view', () => { @@ -318,18 +318,18 @@ describe('QuadPatternFragmentsController', () => { }); it('should set the text/n3 content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/n3;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/n3;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); }); }); describe('A QuadPatternFragmentsController instance without matching view', () => { - let controller: InstanceType & Partial, client: request.Agent; - beforeAll(() => { + let controller: InstanceType & Partial, baseUrl: string; + beforeAll(async () => { let datasource = new Datasource({ dataFactory }); datasource.supportsQuery = vi.fn().mockReturnValue(true); datasource.select = vi.fn(() => empty()); @@ -344,54 +344,54 @@ describe('QuadPatternFragmentsController', () => { routers: [router], datasources: { 'my-datasource': datasource }, }); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); describe('receiving a request without Accept header', () => { - let response: request.Response; + let response: Response; beforeAll(async () => { - response = await client.get('/my-datasource'); + response = await fetch(baseUrl + '/my-datasource'); }); it('should return status code 406', () => { - expect(response).toHaveProperty('statusCode', 406); + expect(response.status).toBe(406); }); it('should set the text/plain content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); }); describe('receiving a request with an Accept header of text/html', () => { - let response: request.Response; + let response: Response; beforeAll(async () => { - response = await client.get('/my-datasource').set('Accept', 'text/html'); + response = await fetch(baseUrl + '/my-datasource', { headers: { Accept: 'text/html' } }); }); it('should return status code 406', () => { - expect(response).toHaveProperty('statusCode', 406); + expect(response.status).toBe(406); }); it('should set the text/plain content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); }); it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); + expect(response.headers.get('vary')).toBe('Accept'); }); }); }); describe('A QuadPatternFragmentsController instance with a datasource that synchronously errors', () => { let controller: InstanceType & Partial, - client: request.Agent, + baseUrl: string, router: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, datasource: InstanceType, error: Error, view: InstanceType; - beforeAll(() => { + beforeAll(async () => { router = { extractQueryParams: vi.fn((request: RouterRequest, query: MutableQuery) => { query.features.datasource = true; @@ -409,7 +409,7 @@ describe('QuadPatternFragmentsController', () => { views: [view], datasources: { '/my-datasource': datasource }, }); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); function resetAll() { router.extractQueryParams.mockClear(); @@ -418,7 +418,7 @@ describe('QuadPatternFragmentsController', () => { describe('receiving a request for a fragment', () => { beforeAll(async () => { resetAll(); - await client.get('/my-datasource?a=b&c=d'); + await fetch(baseUrl + '/my-datasource?a=b&c=d'); }); it('should emit the error', () => { @@ -429,10 +429,10 @@ describe('QuadPatternFragmentsController', () => { describe('A QuadPatternFragmentsController instance with a datasource that asynchronously errors', () => { let controller: InstanceType & Partial, - client: request.Agent, + baseUrl: string, router: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, datasource: InstanceType, error: Error, view: InstanceType; - beforeAll(() => { + beforeAll(async () => { router = { extractQueryParams: vi.fn((request: RouterRequest, query: MutableQuery) => { query.features.datasource = true; @@ -454,7 +454,7 @@ describe('QuadPatternFragmentsController', () => { views: [view], datasources: { 'my-datasource': datasource }, }); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); function resetAll() { router.extractQueryParams.mockClear(); @@ -463,7 +463,7 @@ describe('QuadPatternFragmentsController', () => { describe('receiving a request for a fragment', () => { beforeAll(async () => { resetAll(); - await client.get('/my-datasource?a=b&c=d'); + await fetch(baseUrl + '/my-datasource?a=b&c=d'); }); it('should emit the error', () => { diff --git a/packages/feature-summary/test/controllers/SummaryController-test.ts b/packages/feature-summary/test/controllers/SummaryController-test.ts index c3a76468..6623178f 100644 --- a/packages/feature-summary/test/controllers/SummaryController-test.ts +++ b/packages/feature-summary/test/controllers/SummaryController-test.ts @@ -2,9 +2,9 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; +import { listen } from '../../../../test/test-helpers'; import { controllers, views } from '../../index'; -import * as request from 'supertest'; import * as fs from 'fs'; import * as path from 'path'; @@ -29,8 +29,8 @@ describe('SummaryController', () => { }); describe('An SummaryController instance', () => { - let controller: InstanceType & Partial, client: request.Agent; - beforeAll(() => { + let controller: InstanceType & Partial, baseUrl: string; + beforeAll(async () => { controller = new SummaryController({ views: [new SummaryRdfView({ dataFactory })], summaries: { dir: path.join(__dirname, '/../../../../test/assets') }, @@ -39,46 +39,46 @@ describe('SummaryController', () => { rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', }, }); - client = request.agent(DummyServer(controller)); + baseUrl = await listen(DummyServer(controller)); }); it('should correctly serve summary in Turtle', async () => { - let response = await client.get('/summaries/summary').set('Accept', 'text/turtle'); + let response = await fetch(baseUrl + '/summaries/summary', { headers: { Accept: 'text/turtle' } }); let summary = fs.readFileSync(path.join(__dirname, '/../../../../test/assets/summary.ttl'), 'utf8'); expect(controller.next).not.toHaveBeenCalled(); - expect(response).toHaveProperty('statusCode', 200); - expect(response.headers).toHaveProperty('content-type', 'text/turtle;charset=utf-8'); - expect(response.headers).toHaveProperty('cache-control', 'public,max-age=604800'); - expect(response.text).toBe(summary); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('text/turtle;charset=utf-8'); + expect(response.headers.get('cache-control')).toBe('public,max-age=604800'); + expect(await response.text()).toBe(summary); }); it('should correctly serve summary in Trig', async () => { - let response = await client.get('/summaries/summary'); + let response = await fetch(baseUrl + '/summaries/summary'); let summary = fs.readFileSync(path.join(__dirname, '/../../../../test/assets/summary.ttl'), 'utf8'); expect(controller.next).not.toHaveBeenCalled(); - expect(response).toHaveProperty('statusCode', 200); - expect(response.headers).toHaveProperty('content-type', 'application/trig;charset=utf-8'); - expect(response.headers).toHaveProperty('cache-control', 'public,max-age=604800'); - expect(response.text).toBe(summary); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/trig;charset=utf-8'); + expect(response.headers.get('cache-control')).toBe('public,max-age=604800'); + expect(await response.text()).toBe(summary); }); it('should correctly serve summary in ntriples', async () => { - let response = await client.get('/summaries/summary').set('Accept', 'application/n-triples'); + let response = await fetch(baseUrl + '/summaries/summary', { headers: { Accept: 'application/n-triples' } }); let summary = fs.readFileSync(path.join(__dirname, '/../../../../test/assets/summary.nt'), 'utf8'); expect(controller.next).not.toHaveBeenCalled(); - expect(response).toHaveProperty('statusCode', 200); - expect(response.headers).toHaveProperty('content-type', 'application/n-triples;charset=utf-8'); - expect(response.headers).toHaveProperty('cache-control', 'public,max-age=604800'); - expect(response.text).toBe(summary); + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/n-triples;charset=utf-8'); + expect(response.headers.get('cache-control')).toBe('public,max-age=604800'); + expect(await response.text()).toBe(summary); }); it('should hand over to the next controller if no summary with that name is found', async () => { - await client.get('/summaries/unknown'); + await fetch(baseUrl + '/summaries/unknown'); expect(controller.next).toHaveBeenCalledOnce(); }); it('should hand over to the next controller for non-summary paths', async () => { - await client.get('/other'); + await fetch(baseUrl + '/other'); expect(controller.next).toHaveBeenCalledOnce(); }); }); diff --git a/test/test-helpers.ts b/test/test-helpers.ts index 3fd9fb85..71cbbad0 100644 --- a/test/test-helpers.ts +++ b/test/test-helpers.ts @@ -2,11 +2,20 @@ import { it, expect } from 'vitest'; import { parse as parseUrl } from 'url'; import { Readable } from 'stream'; -import { IncomingMessage, ServerResponse } from 'http'; +import { IncomingMessage, ServerResponse, type Server } from 'http'; import { Socket } from 'net'; import { EventEmitter, once } from 'events'; import type { Query, RouterRequest } from '../packages/core/lib/types'; +// Starts the given server on an ephemeral port and resolves with its base URL +export async function listen(server: Server): Promise { + await new Promise((resolve) => server.listen(0, resolve)); + const address = server.address(); + if (address === null || typeof address === 'string') + throw new Error('Expected the server to report a network address'); + return `http://localhost:${address.port}`; +} + // A router as accepted by extractQueryParams; DatasourceRouter and PageRouter both satisfy this interface QueryParamRouter { extractQueryParams(request: RouterRequest, query: Query): void; diff --git a/tsconfig.json b/tsconfig.json index 1de65cca..613b9514 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,10 @@ "preserveConstEnums": true, "removeComments": false, "sourceMap": true, - "skipLibCheck": true + // false means `yarn typecheck` fails on vitest/vite's own unresolvable .d.ts + // subpath imports (moduleResolution can't follow their package.json "exports"), + // unrelated to our code. `yarn lint` is unaffected. + "skipLibCheck": false }, "include": [ "packages/*/index.ts", From 07d5a9dc5c2e7f3672e884b5c1af577e91c23578 Mon Sep 17 00:00:00 2001 From: Michiel Date: Tue, 1 Sep 2026 08:30:22 +0000 Subject: [PATCH 11/15] We on go v2 --- tsconfig.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 613b9514..40e6e5a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,9 +15,6 @@ "preserveConstEnums": true, "removeComments": false, "sourceMap": true, - // false means `yarn typecheck` fails on vitest/vite's own unresolvable .d.ts - // subpath imports (moduleResolution can't follow their package.json "exports"), - // unrelated to our code. `yarn lint` is unaffected. "skipLibCheck": false }, "include": [ From 4ae54642ae00d678953a22e222e89a13c20794e9 Mon Sep 17 00:00:00 2001 From: Michiel Date: Tue, 1 Sep 2026 11:22:55 +0000 Subject: [PATCH 12/15] let that async in --- .github/workflows/ci.yml | 1 + package.json | 4 ++-- packages/core/package.json | 2 +- tsconfig.build.json | 10 ++++++++++ tsconfig.typecheck.json | 8 ++++++++ yarn.lock | 15 +++++++++++---- 6 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 tsconfig.build.json create mode 100644 tsconfig.typecheck.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45b43ddb..e19573bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: with: node-version: 24.x - run: yarn install + - run: yarn run typecheck - run: yarn run lint test: diff --git a/package.json b/package.json index 2028793a..055a6217 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "lint-changed": "lerna run lint --since HEAD", "test": "vitest run --coverage", "test-ci": "vitest run --coverage", - "typecheck": "tsc -p tsconfig.json --noEmit", - "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.typecheck.json --noEmit", + "build": "tsc -p tsconfig.build.json", "lint": "eslint packages/*/bin/* packages/*/lib packages/*/test test --ext .js,.ts", "clean": "rm -rf ./node_modules && rm -rf ./packages/*/node_modules", "dedupe": "npx yarn-deduplicate yarn.lock --scopes", diff --git a/packages/core/package.json b/packages/core/package.json index 9d635f05..b015b310 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,7 +25,7 @@ "lint": "eslint bin/* lib test" }, "dependencies": { - "asynciterator": "^3.0.0", + "asynciterator": "^3.10.0", "componentsjs": "^4.0.4", "forwarded-parse": "^2.1.0", "jsonld-streaming-serializer": "^3.0.0", diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..4c857d28 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,10 @@ +// Used by "build"/postinstall; excludes tests so it never needs vitest's types. +{ + "extends": "./tsconfig.json", + "include": [ + "packages/*/index.ts", + "packages/*/lib/**/*", + "packages/*/bin/**/*", + "types/**/*.d.ts" + ] +} diff --git a/tsconfig.typecheck.json b/tsconfig.typecheck.json new file mode 100644 index 00000000..2c34bf09 --- /dev/null +++ b/tsconfig.typecheck.json @@ -0,0 +1,8 @@ +// Used by "typecheck"; needs exports-aware resolution for vitest's types. +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "preserve", + "moduleResolution": "bundler" + } +} diff --git a/yarn.lock b/yarn.lock index 89dbe559..d0a7fb15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1984,10 +1984,12 @@ async@^3.1.0: resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== -asynciterator@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/asynciterator/-/asynciterator-3.0.0.tgz#0dd253bb8cc314469a5644eea1f88596fc16ffda" - integrity sha512-8aq8lNFDG47H/LlEFHLze17T+zjsHP7yscvEdyWh8MYBEI9CkpBbKuFx6FT4dmMVsBZ0D1LUq8aIh8+vHd/74Q== +asynciterator@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/asynciterator/-/asynciterator-3.10.0.tgz#b9baade3792078235dbb6b35f6d28e88db76c5ce" + integrity sha512-eDOBoUf2m+4ht0ETVn2SCfuBIZZ6UWyyQbP++LRPKoK7PmrCQq37pJ6vRvyef4o1Pn+CwWnzMlkXxGdh/krVIw== + dependencies: + tiny-set-immediate "^1.0.2" asynckit@^0.4.0: version "0.4.0" @@ -7803,6 +7805,11 @@ through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +tiny-set-immediate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tiny-set-immediate/-/tiny-set-immediate-1.0.2.tgz#4abcc1cd597e9cdcd6515c3c0a18c8ef4d98f292" + integrity sha512-EVbaM4zXFWS4CIqVoPzY7XIioQ5LU1p49AHizwPO1KyFyp/gxy5SA8mDmfDVl/2WLQiHgUL+esO6Ig+KhpUxUw== + tinybench@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" From dd77110c533f4b981a1c298bdf24e6f078bd812f Mon Sep 17 00:00:00 2001 From: Michiel Date: Tue, 1 Sep 2026 13:09:02 +0000 Subject: [PATCH 13/15] remove comments --- tsconfig.build.json | 1 - tsconfig.typecheck.json | 1 - 2 files changed, 2 deletions(-) diff --git a/tsconfig.build.json b/tsconfig.build.json index 4c857d28..3e0e381d 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,3 @@ -// Used by "build"/postinstall; excludes tests so it never needs vitest's types. { "extends": "./tsconfig.json", "include": [ diff --git a/tsconfig.typecheck.json b/tsconfig.typecheck.json index 2c34bf09..720528b6 100644 --- a/tsconfig.typecheck.json +++ b/tsconfig.typecheck.json @@ -1,4 +1,3 @@ -// Used by "typecheck"; needs exports-aware resolution for vitest's types. { "extends": "./tsconfig.json", "compilerOptions": { From 73f412f9994fd5695efa4b62efc53c25e5b6c9f1 Mon Sep 17 00:00:00 2001 From: Michiel Date: Wed, 9 Sep 2026 14:15:27 +0000 Subject: [PATCH 14/15] Address review feedback: fix CI typecheck, remove ephemeral ports, drop InstanceType - Fold typecheck into postinstall, drop separate CI step (per review) - Migrate controller tests off real ports to light-my-request injection, keep listen() for LinkedDataFragmentsServer-test.ts (relies on response 'error' events as a recoverable signal, incompatible with light-my-request) - Make DummyServer a class exposing next/error directly, drop SpiedController/Partial workaround - Replace InstanceType & Partial patterns with direct class imports across controller test files - Remove unused supertest/@types/supertest dependency --- .github/workflows/ci.yml | 1 - package.json | 5 +- .../test/controllers/AssetsController-test.ts | 34 ++-- .../core/test/controllers/Controller-test.ts | 52 +++--- .../controllers/DereferenceController-test.ts | 26 +-- .../controllers/NotFoundController-test.ts | 67 ++++---- .../QuadPatternFragmentsController-test.ts | 105 ++++++------ .../controllers/SummaryController-test.ts | 34 ++-- test/DummyServer.ts | 37 +++-- test/test-helpers.ts | 38 ++++- yarn.lock | 150 +++--------------- 11 files changed, 242 insertions(+), 307 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e19573bb..45b43ddb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,6 @@ jobs: with: node-version: 24.x - run: yarn install - - run: yarn run typecheck - run: yarn run lint test: diff --git a/package.json b/package.json index 164e7d89..31826147 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "@types/parse-cache-control": "^1.0.4", "@types/q": "^1.5.8", "@types/request": "^2.48.13", - "@types/supertest": "^7.2.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@vitest/coverage-v8": "^4.1.10", @@ -21,11 +20,11 @@ "eslint-plugin-import": "^2.22.0", "eslint-plugin-promise": "^7.3.0", "lerna": "^4.0.0", + "light-my-request": "^6.6.0", "manual-git-changelog": "^1.0.1", "node-gyp": "^11.0.0", "pre-commit": "^2.0.0", "rdf-object": "^1.14.0", - "supertest": "^7.0.0", "typescript": "^5.9.3", "vitest": "^4.1.10" }, @@ -45,7 +44,7 @@ "dedupe": "npx yarn-deduplicate yarn.lock --scopes", "publish": "lerna publish", "publish-bare": "lerna exec -- npm publish --silent", - "postinstall": "lerna run prepare && yarn run build", + "postinstall": "lerna run prepare && yarn run build && yarn run typecheck", "version": "manual-git-changelog onversion" } } diff --git a/packages/core/test/controllers/AssetsController-test.ts b/packages/core/test/controllers/AssetsController-test.ts index 65bceb14..bdca47ff 100644 --- a/packages/core/test/controllers/AssetsController-test.ts +++ b/packages/core/test/controllers/AssetsController-test.ts @@ -1,15 +1,13 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll } from 'vitest'; -import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; -import { listen } from '../../../../test/test-helpers'; -import { controllers } from '../../index'; +import { DummyServer } from '../../../../test/DummyServer'; +import { request } from '../../../../test/test-helpers'; +import { AssetsController } from '../../lib/controllers/AssetsController'; import * as fs from 'fs'; import * as path from 'path'; -const { AssetsController } = controllers; - describe('AssetsController', () => { describe('The AssetsController module', () => { it('should be a function', () => { @@ -22,16 +20,16 @@ describe('AssetsController', () => { }); describe('An AssetsController instance', () => { - let controller: InstanceType & Partial, baseUrl: string; - beforeAll(async () => { + let controller: AssetsController, server: DummyServer; + beforeAll(() => { controller = new AssetsController(); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); it('should correctly serve SVG assets', async () => { - let response = await fetch(baseUrl + '/assets/images/logo'); + let response = await request(server, '/assets/images/logo'); let asset = fs.readFileSync(path.join(__dirname, '/../../assets/images/logo.svg'), 'utf8'); - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); expect(response.status).toBe(200); expect(response.headers.get('content-type')).toBe('image/svg+xml'); expect(response.headers.get('cache-control')).toBe('public,max-age=1209600'); @@ -39,9 +37,9 @@ describe('AssetsController', () => { }); it('should correctly serve CSS assets', async () => { - let response = await fetch(baseUrl + '/assets/styles/ldf-server'); + let response = await request(server, '/assets/styles/ldf-server'); let asset = fs.readFileSync(path.join(__dirname, '/../../assets/styles/ldf-server.css'), 'utf8'); - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); expect(response.status).toBe(200); expect(response.headers.get('content-type')).toBe('text/css;charset=utf-8'); expect(response.headers.get('cache-control')).toBe('public,max-age=1209600'); @@ -49,9 +47,9 @@ describe('AssetsController', () => { }); it('should correctly serve ICO assets', async () => { - let response = await fetch(baseUrl + '/favicon.ico'); + let response = await request(server, '/favicon.ico'); let asset = fs.readFileSync(path.join(__dirname, '/../../assets/favicon.ico'), 'utf8'); - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); expect(response.status).toBe(200); expect(response.headers.get('content-type')).toBe('image/vnd.microsoft.icon'); expect(response.headers.get('cache-control')).toBe('public,max-age=1209600'); @@ -59,13 +57,13 @@ describe('AssetsController', () => { }); it('should hand over to the next controller if no asset with that name is found', async () => { - await fetch(baseUrl + '/assets/unknown'); - expect(controller.next).toHaveBeenCalledOnce(); + await request(server, '/assets/unknown'); + expect(server.next).toHaveBeenCalledOnce(); }); it('should hand over to the next controller for non-asset paths', async () => { - await fetch(baseUrl + '/other'); - expect(controller.next).toHaveBeenCalledOnce(); + await request(server, '/other'); + expect(server.next).toHaveBeenCalledOnce(); }); }); }); diff --git a/packages/core/test/controllers/Controller-test.ts b/packages/core/test/controllers/Controller-test.ts index 5b52160e..015e9586 100644 --- a/packages/core/test/controllers/Controller-test.ts +++ b/packages/core/test/controllers/Controller-test.ts @@ -2,8 +2,8 @@ import { describe, it, expect, beforeAll, vi } from 'vitest'; import type { Mock } from 'vitest'; -import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; -import { listen } from '../../../../test/test-helpers'; +import { DummyServer } from '../../../../test/DummyServer'; +import { request } from '../../../../test/test-helpers'; import { Controller } from '../../lib/controllers/Controller'; import { UrlData } from '../../index'; import type { LdfRequestWithUrl, LdfResponse, ViewSettings } from '../../index'; @@ -31,17 +31,17 @@ describe('Controller', () => { }); describe('A Controller instance without baseURL', () => { - let controller: TestableController & Partial; - let baseUrl: string; + let controller: TestableController; + let server: DummyServer; let handleRequestSpy: Mock; - beforeAll(async () => { + beforeAll(() => { controller = new TestableController(); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); describe('receiving a request', () => { - beforeAll(() => fetch(baseUrl + '/path?a=b')); + beforeAll(() => request(server, '/path?a=b')); it('should call _handleRequest with request, response and next', () => { expect(handleRequestSpy).toHaveBeenCalledOnce(); @@ -63,23 +63,23 @@ describe('Controller', () => { }); it('should hand over to the next controller', () => { - expect(controller.next).toHaveBeenCalledOnce(); + expect(server.next).toHaveBeenCalledOnce(); }); }); }); describe('A Controller instance without baseURL using Forwarded header', () => { - let controller: TestableController & Partial; - let baseUrl: string; + let controller: TestableController; + let server: DummyServer; let handleRequestSpy: Mock; - beforeAll(async () => { + beforeAll(() => { controller = new TestableController({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); describe('receiving a request', () => { - beforeAll(() => fetch(baseUrl + '/path?a=b', { + beforeAll(() => request(server, '/path?a=b', { headers: { 'X-Forwarded-Host': 'foo:5000', // NOTE: the priority will go to the Forwarded header over the X-Forwarded-Host header @@ -107,23 +107,23 @@ describe('Controller', () => { }); it('should hand over to the next controller', () => { - expect(controller.next).toHaveBeenCalledOnce(); + expect(server.next).toHaveBeenCalledOnce(); }); }); }); describe('A Controller instance without baseURL using X-Forwarded-* headers', () => { - let controller: TestableController & Partial; - let baseUrl: string; + let controller: TestableController; + let server: DummyServer; let handleRequestSpy: Mock; - beforeAll(async () => { + beforeAll(() => { controller = new TestableController(); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); describe('receiving a request', () => { - beforeAll(() => fetch(baseUrl + '/path?a=b', { + beforeAll(() => request(server, '/path?a=b', { headers: { 'X-Forwarded-Host': 'foo:5000', 'X-Forwarded-Proto': 'https', @@ -150,23 +150,23 @@ describe('Controller', () => { }); it('should hand over to the next controller', () => { - expect(controller.next).toHaveBeenCalledOnce(); + expect(server.next).toHaveBeenCalledOnce(); }); }); }); describe('A Controller instance with baseURL', () => { - let controller: TestableController & Partial; - let baseUrl: string; + let controller: TestableController; + let server: DummyServer; let handleRequestSpy: Mock; - beforeAll(async () => { + beforeAll(() => { controller = new TestableController({ urlData: new UrlData({ baseURL: 'http://example.org:1234/base?c=d#f' }) }); handleRequestSpy = vi.spyOn(controller, '_handleRequest'); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); describe('receiving a request', () => { - beforeAll(() => fetch(baseUrl + '/path?a=b')); + beforeAll(() => request(server, '/path?a=b')); it('should call _handleRequest with request, response and next', () => { expect(handleRequestSpy).toHaveBeenCalledOnce(); @@ -188,7 +188,7 @@ describe('Controller', () => { }); it('should hand over to the next controller', () => { - expect(controller.next).toHaveBeenCalledOnce(); + expect(server.next).toHaveBeenCalledOnce(); }); }); }); diff --git a/packages/core/test/controllers/DereferenceController-test.ts b/packages/core/test/controllers/DereferenceController-test.ts index 796e51a3..d939f2f7 100644 --- a/packages/core/test/controllers/DereferenceController-test.ts +++ b/packages/core/test/controllers/DereferenceController-test.ts @@ -1,12 +1,12 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll } from 'vitest'; -import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; -import { listen } from '../../../../test/test-helpers'; -import { controllers, datasources } from '../../index'; +import { DummyServer } from '../../../../test/DummyServer'; +import { request, type FetchLikeResponse } from '../../../../test/test-helpers'; +import { datasources } from '../../index'; +import { DeferenceController as DereferenceController } from '../../lib/controllers/DereferenceController'; import { DataFactory as dataFactory } from 'n3'; -const { DereferenceController } = controllers; const { Datasource } = datasources; describe('DereferenceController', () => { @@ -21,22 +21,22 @@ describe('DereferenceController', () => { }); describe('A DereferenceController instance', () => { - let controller: InstanceType & Partial, baseUrl: string, hostname: string; - beforeAll(async () => { + let controller: DereferenceController, server: DummyServer; + const hostname = 'localhost:80'; + beforeAll(() => { controller = new DereferenceController({ dereference: { '/resource/': new Datasource({ dataFactory, path: 'dbpedia/2014' }) } }); - baseUrl = await listen(DummyServer(controller)); - hostname = new URL(baseUrl).host; + server = new DummyServer(controller); }); describe('receiving a request for a dereferenced URL', () => { - let response: Response, responseText: string; + let response: FetchLikeResponse, responseText: string; beforeAll(async () => { - response = await fetch(baseUrl + '/resource/Mickey_Mouse', { redirect: 'manual' }); + response = await request(server, '/resource/Mickey_Mouse'); responseText = await response.text(); }); it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); }); it('should set the status code to 303', () => { @@ -63,10 +63,10 @@ describe('DereferenceController', () => { }); describe('receiving a request for a non-defererenced URL', () => { - beforeAll(() => fetch(baseUrl + '/otherresource/Mickey_Mouse')); + beforeAll(() => request(server, '/otherresource/Mickey_Mouse')); it('should hand over to the next controller', () => { - expect(controller.next).toHaveBeenCalledOnce(); + expect(server.next).toHaveBeenCalledOnce(); }); }); }); diff --git a/packages/core/test/controllers/NotFoundController-test.ts b/packages/core/test/controllers/NotFoundController-test.ts index 16404bce..2e528e54 100644 --- a/packages/core/test/controllers/NotFoundController-test.ts +++ b/packages/core/test/controllers/NotFoundController-test.ts @@ -2,15 +2,16 @@ import { describe, it, expect, beforeAll, vi } from 'vitest'; import type { Mock } from 'vitest'; -import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; -import { listen } from '../../../../test/test-helpers'; -import { controllers, views, datasources as coreDatasources, UrlData } from '../../index'; +import { DummyServer } from '../../../../test/DummyServer'; +import { request, type FetchLikeResponse } from '../../../../test/test-helpers'; +import { datasources as coreDatasources, UrlData } from '../../index'; +import { NotFoundController } from '../../lib/controllers/NotFoundController'; +import { NotFoundHtmlView } from '../../lib/views/notfound/NotFoundHtmlView'; +import { NotFoundRdfView } from '../../lib/views/notfound/NotFoundRdfView'; import type { DatasourceRegistry } from '../../index'; import { DataFactory as dataFactory } from 'n3'; -const { NotFoundController } = controllers; -const { NotFoundHtmlView, NotFoundRdfView } = views.notfound; const { Datasource } = coreDatasources; describe('NotFoundController', () => { @@ -25,21 +26,21 @@ describe('NotFoundController', () => { }); describe('A NotFoundController instance without views', () => { - let controller: InstanceType & Partial, baseUrl: string; - beforeAll(async () => { + let controller: NotFoundController, server: DummyServer; + beforeAll(() => { controller = new NotFoundController(); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); describe('receiving a request', () => { - let response: Response, responseText: string; + let response: FetchLikeResponse, responseText: string; beforeAll(async () => { - response = await fetch(baseUrl + '/notfound'); + response = await request(server, '/notfound'); responseText = await response.text(); }); it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); }); it('should have a 404 status', () => { @@ -61,19 +62,19 @@ describe('NotFoundController', () => { }); describe('A NotFoundController instance with HTML and RDF views', () => { - let controller: InstanceType & Partial, - htmlView: InstanceType, rdfView: InstanceType, - htmlRenderSpy: Mock['render']>, - rdfRenderSpy: Mock['render']>, - datasources: DatasourceRegistry, baseUrl: string; - beforeAll(async () => { + let controller: NotFoundController, + htmlView: NotFoundHtmlView, rdfView: NotFoundRdfView, + htmlRenderSpy: Mock, + rdfRenderSpy: Mock, + datasources: DatasourceRegistry, server: DummyServer; + beforeAll(() => { htmlView = new NotFoundHtmlView({ dataFactory }); rdfView = new NotFoundRdfView({ dataFactory }); htmlRenderSpy = vi.spyOn(htmlView, 'render'); rdfRenderSpy = vi.spyOn(rdfView, 'render'); datasources = { a: new Datasource({ dataFactory, title: 'foo', path: 'foo', urlData: new UrlData({ baseURL: 'http://example.org/' }) }) }; controller = new NotFoundController({ views: [htmlView, rdfView], datasources: datasources }); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); function resetAll() { htmlRenderSpy.mockClear(); @@ -81,15 +82,15 @@ describe('NotFoundController', () => { } describe('receiving a request without Accept header', () => { - let response: Response, responseText: string; + let response: FetchLikeResponse, responseText: string; beforeAll(async () => { resetAll(); - response = await fetch(baseUrl + '/notfound'); + response = await request(server, '/notfound'); responseText = await response.text(); }); it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); }); it('should call the HTML view', () => { @@ -118,15 +119,15 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of */*', () => { - let response: Response, responseText: string; + let response: FetchLikeResponse, responseText: string; beforeAll(async () => { resetAll(); - response = await fetch(baseUrl + '/notfound', { headers: { Accept: '*/*' } }); + response = await request(server, '/notfound', { headers: { Accept: '*/*' } }); responseText = await response.text(); }); it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); }); it('should call the HTML view', () => { @@ -155,15 +156,15 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of text/html', () => { - let response: Response, responseText: string; + let response: FetchLikeResponse, responseText: string; beforeAll(async () => { resetAll(); - response = await fetch(baseUrl + '/notfound', { headers: { Accept: 'text/html' } }); + response = await request(server, '/notfound', { headers: { Accept: 'text/html' } }); responseText = await response.text(); }); it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); }); it('should call the HTML view', () => { @@ -192,15 +193,15 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of text/turtle', () => { - let response: Response, responseText: string; + let response: FetchLikeResponse, responseText: string; beforeAll(async () => { resetAll(); - response = await fetch(baseUrl + '/notfound', { headers: { Accept: 'text/turtle' } }); + response = await request(server, '/notfound', { headers: { Accept: 'text/turtle' } }); responseText = await response.text(); }); it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); }); it('should call the RDF view', () => { @@ -230,15 +231,15 @@ describe('NotFoundController', () => { }); describe('receiving a request with an Accept header of application/trig', () => { - let response: Response, responseText: string; + let response: FetchLikeResponse, responseText: string; beforeAll(async () => { resetAll(); - response = await fetch(baseUrl + '/notfound', { headers: { Accept: 'application/trig' } }); + response = await request(server, '/notfound', { headers: { Accept: 'application/trig' } }); responseText = await response.text(); }); it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); }); it('should call the RDF view', () => { diff --git a/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts index 4e35bf85..99842049 100644 --- a/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts +++ b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts @@ -2,10 +2,13 @@ import { describe, it, expect, beforeAll, vi } from 'vitest'; import type { Mock } from 'vitest'; -import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; -import { listen } from '../../../../test/test-helpers'; -import { controllers, views } from '../../index'; +import { DummyServer } from '../../../../test/DummyServer'; +import { request, type FetchLikeResponse } from '../../../../test/test-helpers'; +import { QuadPatternFragmentsController } from '../../lib/controllers/QuadPatternFragmentsController'; +import { QuadPatternFragmentsHtmlView } from '../../lib/views/quadpatternfragments/QuadPatternFragmentsHtmlView'; +import { QuadPatternFragmentsRdfView } from '../../lib/views/quadpatternfragments/QuadPatternFragmentsRdfView'; import { datasources as coreDatasources, UrlData } from '@ldf/core'; +import type { Datasource as DatasourceType } from '@ldf/core/lib/datasources/Datasource'; import type { DatasourceRegistry, Query, QueryFeatures, RouterRequest } from '@ldf/core'; import type { Quad } from 'rdf-js'; import { empty } from 'asynciterator'; @@ -14,8 +17,6 @@ import type { AsyncIterator } from 'asynciterator'; import * as http from 'http'; import { DataFactory as dataFactory } from 'n3'; -const { QuadPatternFragmentsController } = controllers; -const { QuadPatternFragmentsHtmlView, QuadPatternFragmentsRdfView } = views.quadpatternfragments; const { Datasource } = coreDatasources; type MutableQuery = Query & { features: QueryFeatures }; @@ -32,20 +33,20 @@ describe('QuadPatternFragmentsController', () => { }); describe('A QuadPatternFragmentsController instance with 3 routers', () => { - let controller: InstanceType & Partial, - baseUrl: string, + let controller: QuadPatternFragmentsController, + server: DummyServer, routerA: { extractQueryParams: Mock<(request: RouterRequest, query: Query) => void> }, routerB: { extractQueryParams: Mock<(request: RouterRequest, query: Query) => void> }, routerC: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, - datasource: InstanceType, + datasource: DatasourceType, supportsQuerySpy: Mock<(query: Query) => boolean>, selectSpy: Mock<(query: Query) => AsyncIterator>, selectResult: AsyncIterator, datasources: DatasourceRegistry, - view: InstanceType, - renderSpy: Mock['render']>, + view: QuadPatternFragmentsRdfView, + renderSpy: Mock, prefixes: Record; - beforeAll(async () => { + beforeAll(() => { routerA = { extractQueryParams: vi.fn() }; routerB = { extractQueryParams: vi.fn(() => { throw new Error('second router error'); }) }; routerC = { @@ -74,7 +75,7 @@ describe('QuadPatternFragmentsController', () => { views: [view], prefixes: prefixes, }); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); function resetAll() { routerA.extractQueryParams.mockClear(); @@ -87,7 +88,7 @@ describe('QuadPatternFragmentsController', () => { describe('receiving a request for a fragment', () => { beforeAll(async () => { resetAll(); - await fetch(baseUrl + '/my-datasource?a=b&c=d'); + await request(server, '/my-datasource?a=b&c=d'); }); it('should call the first router with the request and an empty query', () => { @@ -174,7 +175,7 @@ describe('QuadPatternFragmentsController', () => { resetAll(); supportsQuerySpy = vi.fn().mockReturnValue(false); datasource.supportsQuery = supportsQuerySpy; - await fetch(baseUrl + '/my-datasource?a=b&c=d'); + await request(server, '/my-datasource?a=b&c=d'); }); it('should verify whether the data source supports the query', () => { @@ -190,12 +191,12 @@ describe('QuadPatternFragmentsController', () => { }); describe('A QuadPatternFragmentsController instance with 2 views', () => { - let controller: InstanceType & Partial, - baseUrl: string, - htmlView: InstanceType, rdfView: InstanceType, - htmlRenderSpy: Mock['render']>, - rdfRenderSpy: Mock['render']>; - beforeAll(async () => { + let controller: QuadPatternFragmentsController, + server: DummyServer, + htmlView: QuadPatternFragmentsHtmlView, rdfView: QuadPatternFragmentsRdfView, + htmlRenderSpy: Mock, + rdfRenderSpy: Mock; + beforeAll(() => { let datasource = new Datasource({ dataFactory }); datasource.supportsQuery = vi.fn().mockReturnValue(true); datasource.select = vi.fn(() => { @@ -219,7 +220,7 @@ describe('QuadPatternFragmentsController', () => { datasources: { 'my-datasource': datasource }, views: [htmlView, rdfView], }); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); function resetAll() { htmlRenderSpy.mockClear(); @@ -227,10 +228,10 @@ describe('QuadPatternFragmentsController', () => { } describe('receiving a request without Accept header', () => { - let response: Response; + let response: FetchLikeResponse; beforeAll(async () => { resetAll(); - response = await fetch(baseUrl + '/my-datasource'); + response = await request(server, '/my-datasource'); }); it('should call the default view', () => { @@ -247,10 +248,10 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of */*', () => { - let response: Response; + let response: FetchLikeResponse; beforeAll(async () => { resetAll(); - response = await fetch(baseUrl + '/my-datasource', { headers: { Accept: '*/*' } }); + response = await request(server, '/my-datasource', { headers: { Accept: '*/*' } }); }); it('should call the HTML view', () => { @@ -267,10 +268,10 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/html', () => { - let response: Response; + let response: FetchLikeResponse; beforeAll(async () => { resetAll(); - response = await fetch(baseUrl + '/my-datasource', { headers: { Accept: 'text/html' } }); + response = await request(server, '/my-datasource', { headers: { Accept: 'text/html' } }); }); it('should call the HTML view', () => { @@ -287,10 +288,10 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/turtle', () => { - let response: Response; + let response: FetchLikeResponse; beforeAll(async () => { resetAll(); - response = await fetch(baseUrl + '/my-datasource', { headers: { Accept: 'text/turtle' } }); + response = await request(server, '/my-datasource', { headers: { Accept: 'text/turtle' } }); }); it('should call the Turtle view', () => { @@ -307,10 +308,10 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/n3', () => { - let response: Response; + let response: FetchLikeResponse; beforeAll(async () => { resetAll(); - response = await fetch(baseUrl + '/my-datasource', { headers: { Accept: 'text/n3' } }); + response = await request(server, '/my-datasource', { headers: { Accept: 'text/n3' } }); }); it('should call the Turtle view', () => { @@ -328,8 +329,8 @@ describe('QuadPatternFragmentsController', () => { }); describe('A QuadPatternFragmentsController instance without matching view', () => { - let controller: InstanceType & Partial, baseUrl: string; - beforeAll(async () => { + let controller: QuadPatternFragmentsController, server: DummyServer; + beforeAll(() => { let datasource = new Datasource({ dataFactory }); datasource.supportsQuery = vi.fn().mockReturnValue(true); datasource.select = vi.fn(() => empty()); @@ -344,13 +345,13 @@ describe('QuadPatternFragmentsController', () => { routers: [router], datasources: { 'my-datasource': datasource }, }); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); describe('receiving a request without Accept header', () => { - let response: Response; + let response: FetchLikeResponse; beforeAll(async () => { - response = await fetch(baseUrl + '/my-datasource'); + response = await request(server, '/my-datasource'); }); it('should return status code 406', () => { @@ -367,9 +368,9 @@ describe('QuadPatternFragmentsController', () => { }); describe('receiving a request with an Accept header of text/html', () => { - let response: Response; + let response: FetchLikeResponse; beforeAll(async () => { - response = await fetch(baseUrl + '/my-datasource', { headers: { Accept: 'text/html' } }); + response = await request(server, '/my-datasource', { headers: { Accept: 'text/html' } }); }); it('should return status code 406', () => { @@ -387,11 +388,11 @@ describe('QuadPatternFragmentsController', () => { }); describe('A QuadPatternFragmentsController instance with a datasource that synchronously errors', () => { - let controller: InstanceType & Partial, - baseUrl: string, + let controller: QuadPatternFragmentsController, + server: DummyServer, router: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, - datasource: InstanceType, error: Error, view: InstanceType; - beforeAll(async () => { + datasource: DatasourceType, error: Error, view: QuadPatternFragmentsRdfView; + beforeAll(() => { router = { extractQueryParams: vi.fn((request: RouterRequest, query: MutableQuery) => { query.features.datasource = true; @@ -409,7 +410,7 @@ describe('QuadPatternFragmentsController', () => { views: [view], datasources: { '/my-datasource': datasource }, }); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); function resetAll() { router.extractQueryParams.mockClear(); @@ -418,21 +419,21 @@ describe('QuadPatternFragmentsController', () => { describe('receiving a request for a fragment', () => { beforeAll(async () => { resetAll(); - await fetch(baseUrl + '/my-datasource?a=b&c=d'); + await request(server, '/my-datasource?a=b&c=d'); }); it('should emit the error', () => { - expect(controller.error).toBe(error); + expect(server.error).toBe(error); }); }); }); describe('A QuadPatternFragmentsController instance with a datasource that asynchronously errors', () => { - let controller: InstanceType & Partial, - baseUrl: string, + let controller: QuadPatternFragmentsController, + server: DummyServer, router: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, - datasource: InstanceType, error: Error, view: InstanceType; - beforeAll(async () => { + datasource: DatasourceType, error: Error, view: QuadPatternFragmentsRdfView; + beforeAll(() => { router = { extractQueryParams: vi.fn((request: RouterRequest, query: MutableQuery) => { query.features.datasource = true; @@ -454,7 +455,7 @@ describe('QuadPatternFragmentsController', () => { views: [view], datasources: { 'my-datasource': datasource }, }); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); function resetAll() { router.extractQueryParams.mockClear(); @@ -463,11 +464,11 @@ describe('QuadPatternFragmentsController', () => { describe('receiving a request for a fragment', () => { beforeAll(async () => { resetAll(); - await fetch(baseUrl + '/my-datasource?a=b&c=d'); + await request(server, '/my-datasource?a=b&c=d'); }); it('should emit the error', () => { - expect(controller.error).toBe(error); + expect(server.error).toBe(error); }); }); }); diff --git a/packages/feature-summary/test/controllers/SummaryController-test.ts b/packages/feature-summary/test/controllers/SummaryController-test.ts index 6623178f..5662c9d5 100644 --- a/packages/feature-summary/test/controllers/SummaryController-test.ts +++ b/packages/feature-summary/test/controllers/SummaryController-test.ts @@ -1,16 +1,16 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll } from 'vitest'; -import { DummyServer, type SpiedController } from '../../../../test/DummyServer'; -import { listen } from '../../../../test/test-helpers'; -import { controllers, views } from '../../index'; +import { DummyServer } from '../../../../test/DummyServer'; +import { request } from '../../../../test/test-helpers'; +import { views } from '../../index'; +import { SummaryController } from '../../lib/controllers/SummaryController'; import * as fs from 'fs'; import * as path from 'path'; import { DataFactory as dataFactory } from 'n3'; -const { SummaryController } = controllers; const { SummaryRdfView } = views.summary; describe('SummaryController', () => { @@ -29,8 +29,8 @@ describe('SummaryController', () => { }); describe('An SummaryController instance', () => { - let controller: InstanceType & Partial, baseUrl: string; - beforeAll(async () => { + let controller: SummaryController, server: DummyServer; + beforeAll(() => { controller = new SummaryController({ views: [new SummaryRdfView({ dataFactory })], summaries: { dir: path.join(__dirname, '/../../../../test/assets') }, @@ -39,13 +39,13 @@ describe('SummaryController', () => { rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', }, }); - baseUrl = await listen(DummyServer(controller)); + server = new DummyServer(controller); }); it('should correctly serve summary in Turtle', async () => { - let response = await fetch(baseUrl + '/summaries/summary', { headers: { Accept: 'text/turtle' } }); + let response = await request(server, '/summaries/summary', { headers: { Accept: 'text/turtle' } }); let summary = fs.readFileSync(path.join(__dirname, '/../../../../test/assets/summary.ttl'), 'utf8'); - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); expect(response.status).toBe(200); expect(response.headers.get('content-type')).toBe('text/turtle;charset=utf-8'); expect(response.headers.get('cache-control')).toBe('public,max-age=604800'); @@ -53,9 +53,9 @@ describe('SummaryController', () => { }); it('should correctly serve summary in Trig', async () => { - let response = await fetch(baseUrl + '/summaries/summary'); + let response = await request(server, '/summaries/summary'); let summary = fs.readFileSync(path.join(__dirname, '/../../../../test/assets/summary.ttl'), 'utf8'); - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); expect(response.status).toBe(200); expect(response.headers.get('content-type')).toBe('application/trig;charset=utf-8'); expect(response.headers.get('cache-control')).toBe('public,max-age=604800'); @@ -63,9 +63,9 @@ describe('SummaryController', () => { }); it('should correctly serve summary in ntriples', async () => { - let response = await fetch(baseUrl + '/summaries/summary', { headers: { Accept: 'application/n-triples' } }); + let response = await request(server, '/summaries/summary', { headers: { Accept: 'application/n-triples' } }); let summary = fs.readFileSync(path.join(__dirname, '/../../../../test/assets/summary.nt'), 'utf8'); - expect(controller.next).not.toHaveBeenCalled(); + expect(server.next).not.toHaveBeenCalled(); expect(response.status).toBe(200); expect(response.headers.get('content-type')).toBe('application/n-triples;charset=utf-8'); expect(response.headers.get('cache-control')).toBe('public,max-age=604800'); @@ -73,13 +73,13 @@ describe('SummaryController', () => { }); it('should hand over to the next controller if no summary with that name is found', async () => { - await fetch(baseUrl + '/summaries/unknown'); - expect(controller.next).toHaveBeenCalledOnce(); + await request(server, '/summaries/unknown'); + expect(server.next).toHaveBeenCalledOnce(); }); it('should hand over to the next controller for non-summary paths', async () => { - await fetch(baseUrl + '/other'); - expect(controller.next).toHaveBeenCalledOnce(); + await request(server, '/other'); + expect(server.next).toHaveBeenCalledOnce(); }); }); }); diff --git a/test/DummyServer.ts b/test/DummyServer.ts index afb6f688..d4f44dc2 100644 --- a/test/DummyServer.ts +++ b/test/DummyServer.ts @@ -5,26 +5,25 @@ import { vi, type Mock } from 'vitest'; import type { Controller } from '@ldf/core/lib/controllers/Controller'; import type { LdfRequest, LdfRequestWithUrl, LdfResponse } from '@ldf/core'; -// The bookkeeping DummyServer attaches to a controller so a test can inspect -// how the request was ultimately handled -export interface SpiedController { - next: Mock<(error?: Error) => void>; +/* Dummy server that emulates LinkedDataFragmentsServer, tracking how the + controller ultimately handled each request */ +export class DummyServer extends http.Server { + // Only set once a request has been handled + next?: Mock<(error?: Error) => void>; error?: Error; -} -/* Dummy server that emulates LinkedDataFragmentsServer */ -export function DummyServer(controller: Controller & Partial): http.Server { - const server = http.createServer(); - server.on('request', (request: LdfRequest, response: LdfResponse) => { - // End the response if the controller did not handle the request - controller.next = vi.fn((error?: Error) => { - controller.error = error; - if (!response.headersSent) - response.writeHead(error ? 500 : 200); - response.end(error && error.message || ''); + constructor(controller: Controller) { + super(); + this.on('request', (request: LdfRequest, response: LdfResponse) => { + // End the response if the controller did not handle the request + this.next = vi.fn((error?: Error) => { + this.error = error; + if (!response.headersSent) + response.writeHead(error ? 500 : 200); + response.end(error && error.message || ''); + }); + try { controller.handleRequest(request as LdfRequestWithUrl, response, this.next); } + catch (error) { this.next(error as Error); } }); - try { controller.handleRequest(request as LdfRequestWithUrl, response, controller.next); } - catch (error) { controller.next(error as Error); } - }); - return server; + } } diff --git a/test/test-helpers.ts b/test/test-helpers.ts index 71cbbad0..26f1348a 100644 --- a/test/test-helpers.ts +++ b/test/test-helpers.ts @@ -5,9 +5,26 @@ import { Readable } from 'stream'; import { IncomingMessage, ServerResponse, type Server } from 'http'; import { Socket } from 'net'; import { EventEmitter, once } from 'events'; +import inject = require('light-my-request'); import type { Query, RouterRequest } from '../packages/core/lib/types'; -// Starts the given server on an ephemeral port and resolves with its base URL +type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS'; + +export interface FetchLikeInit { + method?: HttpMethod; + headers?: Record; +} + +export interface FetchLikeResponse { + status: number; + headers: { get(name: string): string | null }; + text(): Promise; +} + +// Starts the given server on an ephemeral port and resolves with its base URL. +// Only needed for servers whose error handling relies on emitting 'error' on the +// response as a recoverable signal (LinkedDataFragmentsServer): light-my-request's +// `request()` below treats any such 'error' event as fatal to the whole injected exchange. export async function listen(server: Server): Promise { await new Promise((resolve) => server.listen(0, resolve)); const address = server.address(); @@ -16,6 +33,25 @@ export async function listen(server: Server): Promise { return `http://localhost:${address.port}`; } +export async function request(server: Server, path: string, init: FetchLikeInit = {}): Promise { + const response = await inject((req, res) => server.emit('request', req, res), { + url: path, + method: init.method, + headers: init.headers, + Request: IncomingMessage, + }); + return { + status: response.statusCode, + headers: { + get(name: string) { + const value = response.headers[name.toLowerCase()]; + return Array.isArray(value) ? value.join(', ') : value === undefined ? null : String(value); + }, + }, + text: () => Promise.resolve(response.payload), + }; +} + // A router as accepted by extractQueryParams; DatasourceRouter and PageRouter both satisfy this interface QueryParamRouter { extractQueryParams(request: RouterRequest, query: Query): void; diff --git a/yarn.lock b/yarn.lock index 29700ec2..eac673c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -962,11 +962,6 @@ npmlog "^4.1.2" write-file-atomic "^3.0.3" -"@noble/hashes@^1.1.5": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" - integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1182,13 +1177,6 @@ resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.144.0.tgz#7dfbfbfbbb9c24d4abeb6f9856a1eca02aff6468" integrity sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg== -"@paralleldrive/cuid2@^2.2.2": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz#3d62ea9e7be867d3fa94b9897fab5b0ae187d784" - integrity sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw== - dependencies: - "@noble/hashes" "^1.1.5" - "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -1318,11 +1306,6 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.5.tgz#3a3510c4e3661f7707c5ae9c67d726986e6e147d" integrity sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg== -"@types/cookiejar@^2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" - integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== - "@types/deep-eql@*": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" @@ -1358,11 +1341,6 @@ resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef" integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw== -"@types/methods@^1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" - integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== - "@types/mime@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" @@ -1485,24 +1463,6 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== -"@types/superagent@^8.1.0": - version "8.1.11" - resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.11.tgz#14da75aa2f916dcdd6fb2a90a8fb24a9a1a86d08" - integrity sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw== - dependencies: - "@types/cookiejar" "^2.1.5" - "@types/methods" "^1.1.4" - "@types/node" "*" - form-data "^4.0.0" - -"@types/supertest@^7.2.1": - version "7.2.1" - resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-7.2.1.tgz#165e99f10fd652027cf1eaa74b55081b6daa0965" - integrity sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw== - dependencies: - "@types/methods" "^1.1.4" - "@types/superagent" "^8.1.0" - "@types/tough-cookie@*": version "4.0.5" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" @@ -2389,11 +2349,6 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" -component-emitter@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" - integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== - componentsjs@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/componentsjs/-/componentsjs-4.0.4.tgz#6a9a7106eaac08726c69b4193cbb329104ef1ab3" @@ -2533,15 +2488,10 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -cookie-signature@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" - integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== - -cookiejar@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" - integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== +cookie@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -2667,7 +2617,7 @@ debug@^2.6.9: dependencies: ms "2.0.0" -debug@^4.0.1, debug@^4.3.3, debug@^4.3.4, debug@^4.3.7: +debug@^4.0.1, debug@^4.3.3, debug@^4.3.4: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -2776,7 +2726,7 @@ detect-libc@^2.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== -dezalgo@^1.0.0, dezalgo@^1.0.4: +dezalgo@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== @@ -3381,11 +3331,6 @@ fast-safe-stringify@^2.0.4: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== -fast-safe-stringify@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" - integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== - fast-uri@^3.0.1: version "3.1.4" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.4.tgz#3b3daf9ce68f41f956df0b505132c0cfce9ec7af" @@ -3505,17 +3450,6 @@ form-data@^2.5.5: mime-types "^2.1.35" safe-buffer "^5.2.1" -form-data@^4.0.0, form-data@^4.0.5: - version "4.0.6" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827" - integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - es-set-tostringtag "^2.1.0" - hasown "^2.0.4" - mime-types "^2.1.35" - form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -3525,15 +3459,6 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -formidable@^3.5.4: - version "3.5.4" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.4.tgz#ac9a593b951e829b3298f21aa9a2243932f32ed9" - integrity sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug== - dependencies: - "@paralleldrive/cuid2" "^2.2.2" - dezalgo "^1.0.4" - once "^1.4.0" - forwarded-parse@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.0.tgz#1ae9d7a4be3af884f74d936d856f7d8c6abd0439" @@ -4851,6 +4776,15 @@ libnpmpublish@^4.0.0: semver "^7.1.3" ssri "^8.0.1" +light-my-request@^6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-6.6.0.tgz#c9448772323f65f33720fb5979c7841f14060add" + integrity sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A== + dependencies: + cookie "^1.0.1" + process-warning "^4.0.0" + set-cookie-parser "^2.6.0" + lightningcss-android-arm64@1.33.0: version "1.33.0" resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz#9a6841f88ae50fc83502903892b41af41bc2b907" @@ -5262,11 +5196,6 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - microdata-rdf-streaming-parser@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/microdata-rdf-streaming-parser/-/microdata-rdf-streaming-parser-1.1.0.tgz#1807cfc9b44e739defac7be879f89545cb4dfe59" @@ -5309,11 +5238,6 @@ mime-types@^2.1.35: dependencies: mime-db "1.52.0" -mime@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" - integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== - mime@^2.4.4: version "2.4.4" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" @@ -6323,6 +6247,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process-warning@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.1.tgz#5c1db66007c67c756e4e09eb170cdece15da32fb" + integrity sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -6405,14 +6334,6 @@ qejs@^3.0.5: dependencies: q "0.8.x" -qs@^6.14.1: - version "6.15.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.3.tgz#76852132a58ed5c7c0ef67e4441b9bb5d6061b3b" - integrity sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A== - dependencies: - es-define-property "^1.0.1" - side-channel "^1.1.1" - qs@^6.9.4: version "6.9.4" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" @@ -7063,6 +6984,11 @@ set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-cookie-parser@^2.6.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" + integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== + set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -7142,7 +7068,7 @@ side-channel-weakmap@^1.0.2: object-inspect "^1.13.3" side-channel-map "^1.0.1" -side-channel@^1.1.0, side-channel@^1.1.1: +side-channel@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.1.tgz#ea02c62e05dc4bea67d4442f0fb71ee192f8e0ab" integrity sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ== @@ -7604,30 +7530,6 @@ strong-log-transformer@^2.1.0: minimist "^1.2.0" through "^2.3.4" -superagent@^10.3.0: - version "10.3.0" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-10.3.0.tgz#ff1e39e7976b63f8084291d65f5bfbbbbd156989" - integrity sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ== - dependencies: - component-emitter "^1.3.1" - cookiejar "^2.1.4" - debug "^4.3.7" - fast-safe-stringify "^2.1.1" - form-data "^4.0.5" - formidable "^3.5.4" - methods "^1.1.2" - mime "2.6.0" - qs "^6.14.1" - -supertest@^7.0.0: - version "7.2.2" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-7.2.2.tgz#dac3ee25a2aa59942a7f641e50c838a7c8819204" - integrity sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA== - dependencies: - cookie-signature "^1.2.2" - methods "^1.1.2" - superagent "^10.3.0" - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" From 6ea8a6d670b2c4f35a3fcdd3ee16685a9ddbd804 Mon Sep 17 00:00:00 2001 From: Michiel Date: Wed, 9 Sep 2026 17:14:36 +0000 Subject: [PATCH 15/15] Address more review feedback: centralize Router type, fix pathname typing, narrow HttpResponse - Add Router interface to core/lib/types.ts, have DatasourceRouter, PageRouter and QuadPatternRouter implement it explicitly instead of each router-consuming file declaring its own local duck-typed interface - Widen RouterRequest.url.pathname to string | null, matching Node's own url.parse()/UrlObject convention, removing the need for callers to coerce null to undefined - Apply reviewer's suggested { url: parsed } simplification in test-helpers.ts, now unblocked by the pathname widening above - Make test-helpers.ts's HttpResponse extend IncomingMessage instead of Readable, mirroring the existing StreamCapture/ServerResponse pattern --- packages/core/lib/routers/DatasourceRouter.ts | 4 ++-- packages/core/lib/routers/PageRouter.ts | 4 ++-- packages/core/lib/types.ts | 7 +++++- .../QuadPatternFragmentsController.ts | 6 +---- .../lib/routers/QuadPatternRouter.ts | 4 ++-- test/test-helpers.ts | 24 +++++-------------- 6 files changed, 19 insertions(+), 30 deletions(-) diff --git a/packages/core/lib/routers/DatasourceRouter.ts b/packages/core/lib/routers/DatasourceRouter.ts index 18393316..1416e98c 100644 --- a/packages/core/lib/routers/DatasourceRouter.ts +++ b/packages/core/lib/routers/DatasourceRouter.ts @@ -2,10 +2,10 @@ /* A DatasourceRouter routes URLs to data sources. */ import { UrlData } from '../UrlData'; -import type { Query, RouterRequest } from '../types'; +import type { Query, Router, RouterRequest } from '../types'; // Creates a new DatasourceRouter -export class DatasourceRouter { +export class DatasourceRouter implements Router { protected _baseLength: number; constructor(options?: { urlData?: UrlData }) { diff --git a/packages/core/lib/routers/PageRouter.ts b/packages/core/lib/routers/PageRouter.ts index 0886e035..0929cee3 100644 --- a/packages/core/lib/routers/PageRouter.ts +++ b/packages/core/lib/routers/PageRouter.ts @@ -1,10 +1,10 @@ /*! @license MIT ©2014-2016 Ruben Verborgh, Ghent University - imec */ /* A PageRouter routes page numbers to offsets */ -import type { Query, RouterRequest } from '../types'; +import type { Query, Router, RouterRequest } from '../types'; // Creates a new PageRouter with the given page size, which defaults to 100. -export class PageRouter { +export class PageRouter implements Router { pageSize: number; constructor(config?: { pageSize?: number }) { diff --git a/packages/core/lib/types.ts b/packages/core/lib/types.ts index 9347ec75..fcdedab9 100644 --- a/packages/core/lib/types.ts +++ b/packages/core/lib/types.ts @@ -93,10 +93,15 @@ export interface LdfResponse extends ServerResponse { // The (already-parsed) request shape routers' extractQueryParams receives — // note this is distinct from LdfRequest: callers pass { url: request.parsedUrl, headers }. export interface RouterRequest { - url?: { pathname?: string; query?: ParsedUrlQuery }; + url?: { pathname?: string | null; query?: ParsedUrlQuery }; headers?: IncomingHttpHeaders; } +// A router extracts query parameters from a (partial) request into a query object +export interface Router { + extractQueryParams(request: RouterRequest, query: Query): void; +} + // Options accepted by the Controller base class (and its subclasses) constructor export interface ControllerOptions { urlData?: UrlData; diff --git a/packages/feature-qpf/lib/controllers/QuadPatternFragmentsController.ts b/packages/feature-qpf/lib/controllers/QuadPatternFragmentsController.ts index 5842c671..54b986a2 100644 --- a/packages/feature-qpf/lib/controllers/QuadPatternFragmentsController.ts +++ b/packages/feature-qpf/lib/controllers/QuadPatternFragmentsController.ts @@ -5,14 +5,10 @@ import { Controller } from '@ldf/core/lib/controllers/Controller'; import * as url from 'url'; import * as _ from 'lodash'; import type { ParsedUrlQuery } from 'querystring'; -import type { ControllerOptions, LdfRequestWithUrl, LdfResponse, Query, QueryFeatures, RouterRequest, ViewSettings } from '@ldf/core'; +import type { ControllerOptions, LdfRequestWithUrl, LdfResponse, Query, QueryFeatures, Router, RouterRequest, ViewSettings } from '@ldf/core'; import type { Datasource } from '@ldf/core/lib/datasources/Datasource'; import type { FragmentInfo } from '../views/quadpatternfragments/QuadPatternFragmentsRdfView'; -interface Router { - extractQueryParams(request: RouterRequest, query: Query): void; -} - interface QuadPatternFragmentsControllerOptions extends ControllerOptions { routers?: Router[]; extensions?: Controller[]; diff --git a/packages/feature-qpf/lib/routers/QuadPatternRouter.ts b/packages/feature-qpf/lib/routers/QuadPatternRouter.ts index 0055fd9e..a6aead50 100644 --- a/packages/feature-qpf/lib/routers/QuadPatternRouter.ts +++ b/packages/feature-qpf/lib/routers/QuadPatternRouter.ts @@ -3,7 +3,7 @@ import { stringToTerm } from 'rdf-string'; import type { DataFactory, Term } from 'rdf-js'; -import type { Query, RouterRequest } from '@ldf/core'; +import type { Query, Router, RouterRequest } from '@ldf/core'; let iriMatcher = /^(][^"<>]*)>?$/; let literalMatcher = /^("[^]*")(?:|\^\^]+)>?|@[a-z0-9\-]+)$/i; @@ -20,7 +20,7 @@ interface QuadPatternRouterConfig { } // Creates a new QuadPatternRouter -export class QuadPatternRouter { +export class QuadPatternRouter implements Router { protected _prefixes: Record; dataFactory?: DataFactory; diff --git a/test/test-helpers.ts b/test/test-helpers.ts index 26f1348a..dd6fd8c5 100644 --- a/test/test-helpers.ts +++ b/test/test-helpers.ts @@ -1,12 +1,11 @@ /*! @license MIT ©2013-2016 Ruben Verborgh, Ghent University - imec */ import { it, expect } from 'vitest'; import { parse as parseUrl } from 'url'; -import { Readable } from 'stream'; import { IncomingMessage, ServerResponse, type Server } from 'http'; import { Socket } from 'net'; import { EventEmitter, once } from 'events'; import inject = require('light-my-request'); -import type { Query, RouterRequest } from '../packages/core/lib/types'; +import type { Query, Router } from '../packages/core/lib/types'; type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS'; @@ -52,37 +51,26 @@ export async function request(server: Server, path: string, init: FetchLikeInit }; } -// A router as accepted by extractQueryParams; DatasourceRouter and PageRouter both satisfy this -interface QueryParamRouter { - extractQueryParams(request: RouterRequest, query: Query): void; -} - // Generates an `it` block that verifies a router's extractQueryParams behavior -export function extractQueryParams(router: QueryParamRouter, description: string, url: string, intent: string, query: Query, expectedQuery: Query) { +export function extractQueryParams(router: Router, description: string, url: string, intent: string, query: Query, expectedQuery: Query) { it(description + ' ' + intent, () => { const parsed = parseUrl(url, true); - const result = router.extractQueryParams({ url: { pathname: parsed.pathname ?? undefined, query: parsed.query } }, query); + const result = router.extractQueryParams({ url: parsed }, query); expect(result, 'should not return anything').toBeUndefined(); expect(query, 'should match the expected query').toEqual(expectedQuery); }); } // A dummy HTTP response, as returned by createHttpResponse -class HttpResponse extends Readable { - statusCode = 200; - headers: Record; - aborted = false; +class HttpResponse extends IncomingMessage { + override statusCode = 200; constructor(contentType: string) { - super(); + super(new Socket()); this.headers = { 'content-type': contentType }; } override _read(): void {} - - abort(): void { - this.aborted = true; - } } // Creates a dummy HTTP response