diff --git a/.eslintrc b/.eslintrc index c2bd0e8b..9132246a 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,7 +203,7 @@ }, { // TypeScript source files - files: [ "packages/**/*.ts" ], + files: [ "packages/**/*.ts", "test/**/*.ts" ], parser: "@typescript-eslint/parser", extends: [ "plugin:@typescript-eslint/recommended-requiring-type-checking" ], parserOptions: { diff --git a/package.json b/package.json index 4d82df05..31826147 100644 --- a/package.json +++ b/package.json @@ -20,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" }, @@ -37,14 +37,14 @@ "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", "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/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/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/packages/core/test/LinkedDataFragmentsServer-test.js b/packages/core/test/LinkedDataFragmentsServer-test.js deleted file mode 100644 index e6d26994..00000000 --- a/packages/core/test/LinkedDataFragmentsServer-test.js +++ /dev/null @@ -1,188 +0,0 @@ -/*! @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 - -let request = require('supertest'); - -describe('LinkedDataFragmentsServer', () => { - describe('A LinkedDataFragmentsServer instance with one controller', () => { - let server, controller, client; - 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(); - } - }), - }; - server = new LinkedDataFragmentsServer({ - controllers: [controller], - log: vi.fn(), - protocol: 'http', - response: { - headers: { - 'Access-Control-Allow-Origin': '*', - 'My-Header': 'value', - }, - }, - }); - client = request.agent(server); - }); - beforeEach(() => { - controller.handleRequest.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'); - }); - - it('should not allow POST requests', async () => { - let response = await client.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.'); - }); - - it('should send a body with GET requests', async () => { - let response = await client.get('/handle'); - 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(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(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(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'); - }); - - it('should error when the controller errors', async () => { - let response = await client.get('/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'); - }); - }); - - describe('A LinkedDataFragmentsServer instance with two controllers', () => { - let server, controllerA, controllerB, client; - 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(); - } - }), - }; - server = new LinkedDataFragmentsServer({ - controllers: [controllerA, controllerB], - protocol: 'http', - log: vi.fn(), - }); - client = request.agent(server); - }); - beforeEach(() => { - controllerA.handleRequest.mockClear(); - controllerB.handleRequest.mockClear(); - }); - - it('should not allow POST requests', async () => { - let response = await client.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.'); - }); - - 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(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(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(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'); - }); - - 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(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'); - }); - - it('should error when the second controller errors', async () => { - let response = await client.get('/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'); - }); - }); -}); diff --git a/packages/core/test/LinkedDataFragmentsServer-test.ts b/packages/core/test/LinkedDataFragmentsServer-test.ts new file mode 100644 index 00000000..30339001 --- /dev/null +++ b/packages/core/test/LinkedDataFragmentsServer-test.ts @@ -0,0 +1,192 @@ +/*! @license MIT ©2013-2016 Ruben Verborgh, Ghent University - imec */ + +import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest'; +import type { Mock } from 'vitest'; +import { LinkedDataFragmentsServer } from '../lib/LinkedDataFragmentsServer'; +import { Controller } from '../lib/controllers/Controller'; +import type { LdfRequestWithUrl, LdfResponse } from '../index'; +import { listen } from '../../../test/test-helpers'; + +describe('LinkedDataFragmentsServer', () => { + describe('A LinkedDataFragmentsServer instance with one controller', () => { + 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(async () => { + 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; + baseUrl = await listen(new LinkedDataFragmentsServer({ + controllers: [controller], + log: vi.fn(), + response: { + headers: { + 'Access-Control-Allow-Origin': '*', + 'My-Header': 'value', + }, + }, + })); + }); + beforeEach(() => { + handleRequestSpy.mockClear(); + }); + + it('should send the configured headers', async () => { + 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 fetch(baseUrl + '/', { method: 'POST' }); + expect(controller.handleRequest).not.toHaveBeenCalled(); + 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 fetch(baseUrl + '/handle'); + expect(controller.handleRequest).toHaveBeenCalledOnce(); + 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 fetch(baseUrl + '/handle', { method: 'HEAD' }); + expect(controller.handleRequest).toHaveBeenCalledOnce(); + expect(response.status).toBe(200); + expect(await response.text()).toBe(''); + }); + + it('should not send a body with OPTIONS requests', async () => { + let response = await fetch(baseUrl + '/handle', { method: 'OPTIONS' }); + expect(controller.handleRequest).toHaveBeenCalledOnce(); + expect(response.status).toBe(200); + expect(await response.text()).toBe(''); + }); + + it('should error when the controller cannot handle the request', async () => { + let response = await fetch(baseUrl + '/unsupported'); + expect(controller.handleRequest).toHaveBeenCalledOnce(); + 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 fetch(baseUrl + '/error'); + expect(controller.handleRequest).toHaveBeenCalledOnce(); + 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 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(async () => { + 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; + baseUrl = await listen(new LinkedDataFragmentsServer({ + controllers: [controllerA, controllerB], + log: vi.fn(), + })); + }); + beforeEach(() => { + handleRequestSpyA.mockClear(); + handleRequestSpyB.mockClear(); + }); + + it('should not allow POST requests', async () => { + let response = await fetch(baseUrl + '/', { method: 'POST' }); + expect(controllerA.handleRequest).not.toHaveBeenCalled(); + expect(controllerB.handleRequest).not.toHaveBeenCalled(); + 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 fetch(baseUrl + '/handleA'); + expect(controllerA.handleRequest).toHaveBeenCalledOnce(); + expect(controllerB.handleRequest).not.toHaveBeenCalled(); + 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 fetch(baseUrl + '/handleB'); + expect(controllerA.handleRequest).toHaveBeenCalledOnce(); + expect(controllerB.handleRequest).toHaveBeenCalledOnce(); + 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 fetch(baseUrl + '/unsupported'); + expect(controllerA.handleRequest).toHaveBeenCalledOnce(); + expect(controllerB.handleRequest).toHaveBeenCalledOnce(); + 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 fetch(baseUrl + '/errorA'); + expect(controllerA.handleRequest).toHaveBeenCalledOnce(); + expect(controllerB.handleRequest).not.toHaveBeenCalled(); + 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 fetch(baseUrl + '/errorB'); + expect(controllerA.handleRequest).toHaveBeenCalledOnce(); + expect(controllerB.handleRequest).toHaveBeenCalledOnce(); + 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.js b/packages/core/test/controllers/AssetsController-test.js deleted file mode 100644 index 1f4f3e2d..00000000 --- a/packages/core/test/controllers/AssetsController-test.js +++ /dev/null @@ -1,69 +0,0 @@ -/*! @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 - -let request = require('supertest'), - fs = require('fs'), - path = require('path'); - -describe('AssetsController', () => { - describe('The AssetsController module', () => { - it('should be a function', () => { - expect(typeof AssetsController).toBe('function'); - }); - - it('should be an AssetsController constructor', () => { - expect(new AssetsController()).toBeInstanceOf(AssetsController); - }); - }); - - describe('An AssetsController instance', () => { - let controller, client; - beforeAll(() => { - controller = new AssetsController(); - client = request.agent(new DummyServer(controller), {}); - }); - - it('should correctly serve SVG assets', async () => { - let response = await client.get('/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(response.body.toString()).toBe(asset); - }); - - it('should correctly serve CSS assets', async () => { - let response = await client.get('/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); - }); - - it('should correctly serve ICO assets', async () => { - let response = await client.get('/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(response.body.toString()).toBe(asset); - }); - - it('should hand over to the next controller if no asset with that name is found', async () => { - await client.get('/assets/unknown'); - expect(controller.next).toHaveBeenCalledOnce(); - }); - - it('should hand over to the next controller for non-asset paths', async () => { - await client.get('/other'); - expect(controller.next).toHaveBeenCalledOnce(); - }); - }); -}); diff --git a/packages/core/test/controllers/AssetsController-test.ts b/packages/core/test/controllers/AssetsController-test.ts new file mode 100644 index 00000000..bdca47ff --- /dev/null +++ b/packages/core/test/controllers/AssetsController-test.ts @@ -0,0 +1,69 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ + +import { describe, it, expect, beforeAll } from 'vitest'; +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'; + +describe('AssetsController', () => { + describe('The AssetsController module', () => { + it('should be a function', () => { + expect(typeof AssetsController).toBe('function'); + }); + + it('should be an AssetsController constructor', () => { + expect(new AssetsController()).toBeInstanceOf(AssetsController); + }); + }); + + describe('An AssetsController instance', () => { + let controller: AssetsController, server: DummyServer; + beforeAll(() => { + controller = new AssetsController(); + server = new DummyServer(controller); + }); + + it('should correctly serve SVG assets', async () => { + let response = await request(server, '/assets/images/logo'); + let asset = fs.readFileSync(path.join(__dirname, '/../../assets/images/logo.svg'), 'utf8'); + 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'); + expect(await response.text()).toBe(asset); + }); + + it('should correctly serve CSS assets', async () => { + let response = await request(server, '/assets/styles/ldf-server'); + let asset = fs.readFileSync(path.join(__dirname, '/../../assets/styles/ldf-server.css'), 'utf8'); + 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'); + expect(await response.text()).toBe(asset); + }); + + it('should correctly serve ICO assets', async () => { + let response = await request(server, '/favicon.ico'); + let asset = fs.readFileSync(path.join(__dirname, '/../../assets/favicon.ico'), 'utf8'); + 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'); + expect(await response.text()).toBe(asset); + }); + + it('should hand over to the next controller if no asset with that name is found', async () => { + await request(server, '/assets/unknown'); + expect(server.next).toHaveBeenCalledOnce(); + }); + + it('should hand over to the next controller for non-asset paths', async () => { + await request(server, '/other'); + expect(server.next).toHaveBeenCalledOnce(); + }); + }); +}); diff --git a/packages/core/test/controllers/Controller-test.js b/packages/core/test/controllers/Controller-test.ts similarity index 53% rename from packages/core/test/controllers/Controller-test.js rename to packages/core/test/controllers/Controller-test.ts index 491d70ac..015e9586 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 type { Mock } 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 { request } 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'; + +// 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,29 @@ describe('Controller', () => { }); describe('A Controller instance without baseURL', () => { - let controller, client; + let controller: TestableController; + let server: DummyServer; + let handleRequestSpy: Mock; beforeAll(() => { - controller = new Controller(); - vi.spyOn(controller, '_handleRequest'); - client = request.agent(new DummyServer(controller), {}); + controller = new TestableController(); + handleRequestSpy = vi.spyOn(controller, '_handleRequest'); + server = new DummyServer(controller); }); describe('receiving a request', () => { - beforeAll(() => client.get('/path?a=b')); + beforeAll(() => request(server, '/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, @@ -51,37 +63,41 @@ 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, client; + let controller: TestableController; + let server: DummyServer; + let 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'); + server = new 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(() => 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 + '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', @@ -91,36 +107,40 @@ 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, client; + let controller: TestableController; + let server: DummyServer; + let handleRequestSpy: Mock; beforeAll(() => { - controller = new Controller(); - vi.spyOn(controller, '_handleRequest'); - client = request.agent(new DummyServer(controller), {}); + controller = new TestableController(); + handleRequestSpy = vi.spyOn(controller, '_handleRequest'); + server = new DummyServer(controller); }); describe('receiving a request', () => { - beforeAll(() => client - .get('/path?a=b') - .set('X-Forwarded-Host', 'foo:5000') - .set('X-Forwarded-Proto', 'https')); + beforeAll(() => request(server, '/path?a=b', { + headers: { + 'X-Forwarded-Host': 'foo:5000', + '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, @@ -130,33 +150,35 @@ 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, client; + let controller: TestableController; + let server: DummyServer; + let 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'); + server = new DummyServer(controller); }); describe('receiving a request', () => { - beforeAll(() => client.get('/path?a=b')); + beforeAll(() => request(server, '/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', @@ -166,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.js b/packages/core/test/controllers/DereferenceController-test.ts similarity index 54% rename from packages/core/test/controllers/DereferenceController-test.js rename to packages/core/test/controllers/DereferenceController-test.ts index da3032ad..d939f2f7 100644 --- a/packages/core/test/controllers/DereferenceController-test.js +++ b/packages/core/test/controllers/DereferenceController-test.ts @@ -2,9 +2,12 @@ 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 { 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'; -let request = require('supertest'); +const { Datasource } = datasources; describe('DereferenceController', () => { describe('The DereferenceController module', () => { @@ -18,50 +21,52 @@ describe('DereferenceController', () => { }); describe('A DereferenceController instance', () => { - let controller, client; + let controller: DereferenceController, server: DummyServer; + const hostname = 'localhost:80'; 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' }) } }); + server = new DummyServer(controller); }); describe('receiving a request for a dereferenced URL', () => { - let response; - beforeAll(async () => { response = await client.get('/resource/Mickey_Mouse'); }); + let response: FetchLikeResponse, responseText: string; + beforeAll(async () => { + 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', () => { - 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 = response.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 = response.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(() => 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.js b/packages/core/test/controllers/NotFoundController-test.js deleted file mode 100644 index de32100b..00000000 --- a/packages/core/test/controllers/NotFoundController-test.js +++ /dev/null @@ -1,257 +0,0 @@ -/*! @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; - -let request = require('supertest'), - dataFactory = require('n3').DataFactory; - -let NotFoundHtmlView = require('../../lib/views/notfound/NotFoundHtmlView').NotFoundHtmlView, - NotFoundRdfView = require('../../lib/views/notfound/NotFoundRdfView').NotFoundRdfView; - -describe('NotFoundController', () => { - describe('The NotFoundController module', () => { - it('should be a function', () => { - expect(typeof NotFoundController).toBe('function'); - }); - - it('should be a NotFoundController constructor', () => { - expect(new NotFoundController()).toBeInstanceOf(NotFoundController); - }); - }); - - describe('A NotFoundController instance without views', () => { - let controller, client; - beforeAll(() => { - controller = new NotFoundController(); - client = request.agent(new DummyServer(controller), {}); - }); - - describe('receiving a request', () => { - let response; - beforeAll(async () => { - response = await client.get('/notfound'); - }); - - it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); - }); - - it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); - }); - - it('should set the text/plain content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/plain;charset=utf-8'); - }); - - it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); - }); - - it('should send a textual error body', () => { - expect(response).toHaveProperty('text', '/notfound not found\n'); - }); - }); - }); - - describe('A NotFoundController instance with HTML and RDF views', () => { - let controller, htmlView, rdfView, datasources, client; - 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' } }; - controller = new NotFoundController({ views: [htmlView, rdfView], datasources: datasources }); - client = request.agent(new DummyServer(controller), {}); - }); - function resetAll() { - htmlView.render.mockClear(); - rdfView.render.mockClear(); - } - - describe('receiving a request without Accept header', () => { - let response; - beforeAll(async () => { - resetAll(); - response = await client.get('/notfound'); - }); - - it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); - }); - - it('should call the HTML view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); - }); - - it('should not call the RDF view', () => { - expect(rdfView.render).not.toHaveBeenCalled(); - }); - - it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); - }); - - it('should set the text/html content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/html;charset=utf-8'); - }); - - it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); - }); - - it('should send an HTML error body', () => { - expect(response.text).toContain('No resource with URL /notfound was found.'); - }); - }); - - describe('receiving a request with an Accept header of */*', () => { - let response; - beforeAll(async () => { - resetAll(); - response = await client.get('/notfound').set('Accept', '*/*'); - }); - - it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); - }); - - it('should call the HTML view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); - }); - - it('should not call the RDF view', () => { - expect(rdfView.render).not.toHaveBeenCalled(); - }); - - it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); - }); - - it('should set the text/html content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/html;charset=utf-8'); - }); - - it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); - }); - - it('should send an HTML error body', () => { - expect(response.text).toContain('No resource with URL /notfound was found.'); - }); - }); - - describe('receiving a request with an Accept header of text/html', () => { - let response; - beforeAll(async () => { - resetAll(); - response = await client.get('/notfound').set('Accept', 'text/html'); - }); - - it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); - }); - - it('should call the HTML view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); - }); - - it('should not call the RDF view', () => { - expect(rdfView.render).not.toHaveBeenCalled(); - }); - - it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); - }); - - it('should set the text/html content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/html;charset=utf-8'); - }); - - it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); - }); - - it('should send an HTML error body', () => { - expect(response.text).toContain('No resource with URL /notfound was found.'); - }); - }); - - describe('receiving a request with an Accept header of text/turtle', () => { - let response; - beforeAll(async () => { - resetAll(); - response = await client.get('/notfound').set('Accept', 'text/turtle'); - }); - - it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); - }); - - it('should call the RDF view', () => { - expect(rdfView.render).toHaveBeenCalledOnce(); - }); - - it('should not call the HTML view', () => { - expect(htmlView.render).not.toHaveBeenCalled(); - }); - - it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); - }); - - it('should set the text/turtle content type', () => { - expect(response.headers).toHaveProperty('content-type', 'text/turtle;charset=utf-8'); - }); - - it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); - }); - - it('should send a Turtle error body', () => { - expect(response.text).toContain(' a '); - expect(response.text).not.toContain('<#metadata> <>.'); - }); - }); - - describe('receiving a request with an Accept header of application/trig', () => { - let response; - beforeAll(async () => { - resetAll(); - response = await client.get('/notfound').set('Accept', 'application/trig'); - }); - - it('should not hand over to the next controller', () => { - expect(controller.next).not.toHaveBeenCalled(); - }); - - it('should call the RDF view', () => { - expect(rdfView.render).toHaveBeenCalledOnce(); - }); - - it('should not call the HTML view', () => { - expect(htmlView.render).not.toHaveBeenCalled(); - }); - - it('should have a 404 status', () => { - expect(response).toHaveProperty('statusCode', 404); - }); - - it('should set the text/html content type', () => { - expect(response.headers).toHaveProperty('content-type', 'application/trig;charset=utf-8'); - }); - - it('should indicate Accept in the Vary header', () => { - expect(response.headers).toHaveProperty('vary', 'Accept'); - }); - - it('should send a TriG error body', () => { - expect(response.text).toContain(' a '); - expect(response.text).toContain('<#metadata> <>.'); - }); - }); - }); -}); diff --git a/packages/core/test/controllers/NotFoundController-test.ts b/packages/core/test/controllers/NotFoundController-test.ts new file mode 100644 index 00000000..2e528e54 --- /dev/null +++ b/packages/core/test/controllers/NotFoundController-test.ts @@ -0,0 +1,271 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ + +import { describe, it, expect, beforeAll, vi } from 'vitest'; +import type { Mock } from 'vitest'; +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 { Datasource } = coreDatasources; + +describe('NotFoundController', () => { + describe('The NotFoundController module', () => { + it('should be a function', () => { + expect(typeof NotFoundController).toBe('function'); + }); + + it('should be a NotFoundController constructor', () => { + expect(new NotFoundController()).toBeInstanceOf(NotFoundController); + }); + }); + + describe('A NotFoundController instance without views', () => { + let controller: NotFoundController, server: DummyServer; + beforeAll(() => { + controller = new NotFoundController(); + server = new DummyServer(controller); + }); + + describe('receiving a request', () => { + let response: FetchLikeResponse, responseText: string; + beforeAll(async () => { + response = await request(server, '/notfound'); + responseText = await response.text(); + }); + + it('should not hand over to the next controller', () => { + expect(server.next).not.toHaveBeenCalled(); + }); + + it('should have a 404 status', () => { + expect(response.status).toBe(404); + }); + + it('should set the text/plain content type', () => { + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); + }); + + it('should indicate Accept in the Vary header', () => { + expect(response.headers.get('vary')).toBe('Accept'); + }); + + it('should send a textual error body', () => { + expect(responseText).toBe('/notfound not found\n'); + }); + }); + }); + + describe('A NotFoundController instance with HTML and RDF views', () => { + 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 }); + server = new DummyServer(controller); + }); + function resetAll() { + htmlRenderSpy.mockClear(); + rdfRenderSpy.mockClear(); + } + + describe('receiving a request without Accept header', () => { + let response: FetchLikeResponse, responseText: string; + beforeAll(async () => { + resetAll(); + response = await request(server, '/notfound'); + responseText = await response.text(); + }); + + it('should not hand over to the next controller', () => { + expect(server.next).not.toHaveBeenCalled(); + }); + + it('should call the HTML view', () => { + expect(htmlRenderSpy).toHaveBeenCalledOnce(); + }); + + it('should not call the RDF view', () => { + expect(rdfRenderSpy).not.toHaveBeenCalled(); + }); + + it('should have a 404 status', () => { + expect(response.status).toBe(404); + }); + + it('should set the text/html content type', () => { + expect(response.headers.get('content-type')).toBe('text/html;charset=utf-8'); + }); + + it('should indicate Accept in the Vary header', () => { + expect(response.headers.get('vary')).toBe('Accept'); + }); + + it('should send an HTML error body', () => { + expect(responseText).toContain('No resource with URL /notfound was found.'); + }); + }); + + describe('receiving a request with an Accept header of */*', () => { + let response: FetchLikeResponse, responseText: string; + beforeAll(async () => { + resetAll(); + response = await request(server, '/notfound', { headers: { Accept: '*/*' } }); + responseText = await response.text(); + }); + + it('should not hand over to the next controller', () => { + expect(server.next).not.toHaveBeenCalled(); + }); + + it('should call the HTML view', () => { + expect(htmlRenderSpy).toHaveBeenCalledOnce(); + }); + + it('should not call the RDF view', () => { + expect(rdfRenderSpy).not.toHaveBeenCalled(); + }); + + it('should have a 404 status', () => { + expect(response.status).toBe(404); + }); + + it('should set the text/html content type', () => { + expect(response.headers.get('content-type')).toBe('text/html;charset=utf-8'); + }); + + it('should indicate Accept in the Vary header', () => { + expect(response.headers.get('vary')).toBe('Accept'); + }); + + it('should send an HTML error body', () => { + expect(responseText).toContain('No resource with URL /notfound was found.'); + }); + }); + + describe('receiving a request with an Accept header of text/html', () => { + let response: FetchLikeResponse, responseText: string; + beforeAll(async () => { + resetAll(); + response = await request(server, '/notfound', { headers: { Accept: 'text/html' } }); + responseText = await response.text(); + }); + + it('should not hand over to the next controller', () => { + expect(server.next).not.toHaveBeenCalled(); + }); + + it('should call the HTML view', () => { + expect(htmlRenderSpy).toHaveBeenCalledOnce(); + }); + + it('should not call the RDF view', () => { + expect(rdfRenderSpy).not.toHaveBeenCalled(); + }); + + it('should have a 404 status', () => { + expect(response.status).toBe(404); + }); + + it('should set the text/html content type', () => { + expect(response.headers.get('content-type')).toBe('text/html;charset=utf-8'); + }); + + it('should indicate Accept in the Vary header', () => { + expect(response.headers.get('vary')).toBe('Accept'); + }); + + it('should send an HTML error body', () => { + expect(responseText).toContain('No resource with URL /notfound was found.'); + }); + }); + + describe('receiving a request with an Accept header of text/turtle', () => { + let response: FetchLikeResponse, responseText: string; + beforeAll(async () => { + resetAll(); + response = await request(server, '/notfound', { headers: { Accept: 'text/turtle' } }); + responseText = await response.text(); + }); + + it('should not hand over to the next controller', () => { + expect(server.next).not.toHaveBeenCalled(); + }); + + it('should call the RDF view', () => { + expect(rdfRenderSpy).toHaveBeenCalledOnce(); + }); + + it('should not call the HTML view', () => { + expect(htmlRenderSpy).not.toHaveBeenCalled(); + }); + + it('should have a 404 status', () => { + expect(response.status).toBe(404); + }); + + it('should set the text/turtle content type', () => { + expect(response.headers.get('content-type')).toBe('text/turtle;charset=utf-8'); + }); + + it('should indicate Accept in the Vary header', () => { + expect(response.headers.get('vary')).toBe('Accept'); + }); + + it('should send a Turtle error body', () => { + expect(responseText).toContain(' a '); + expect(responseText).not.toContain('<#metadata> <>.'); + }); + }); + + describe('receiving a request with an Accept header of application/trig', () => { + let response: FetchLikeResponse, responseText: string; + beforeAll(async () => { + resetAll(); + response = await request(server, '/notfound', { headers: { Accept: 'application/trig' } }); + responseText = await response.text(); + }); + + it('should not hand over to the next controller', () => { + expect(server.next).not.toHaveBeenCalled(); + }); + + it('should call the RDF view', () => { + expect(rdfRenderSpy).toHaveBeenCalledOnce(); + }); + + it('should not call the HTML view', () => { + expect(htmlRenderSpy).not.toHaveBeenCalled(); + }); + + it('should have a 404 status', () => { + expect(response.status).toBe(404); + }); + + it('should set the text/html content type', () => { + expect(response.headers.get('content-type')).toBe('application/trig;charset=utf-8'); + }); + + it('should indicate Accept in the Vary header', () => { + expect(response.headers.get('vary')).toBe('Accept'); + }); + + it('should send a TriG error body', () => { + expect(responseText).toContain(' a '); + expect(responseText).toContain('<#metadata> <>.'); + }); + }); + }); +}); diff --git a/packages/core/test/datasources/Datasource-test.js b/packages/core/test/datasources/Datasource-test.ts similarity index 78% rename from packages/core/test/datasources/Datasource-test.js rename to packages/core/test/datasources/Datasource-test.ts index fd085dc1..e18dbe8b 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, 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 +// 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 as Term).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 as Term).equals(dataFactory.namedNode('urn:ldf:emptyGraph')); }); }); }); diff --git a/packages/core/test/routers/DatasourceRouter-test.js b/packages/core/test/routers/DatasourceRouter-test.ts similarity index 82% rename from packages/core/test/routers/DatasourceRouter-test.js rename to packages/core/test/routers/DatasourceRouter-test.ts index 9007716c..3aed4fe2 100644 --- a/packages/core/test/routers/DatasourceRouter-test.js +++ b/packages/core/test/routers/DatasourceRouter-test.ts @@ -2,7 +2,14 @@ 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 +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 }; +type QueryParamsTestCase = [string, string, string, TestQuery, TestQuery]; describe('DatasourceRouter', () => { describe('The DatasourceRouter module', () => { @@ -20,7 +27,7 @@ describe('DatasourceRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - [ + const rows: QueryParamsTestCase[] = [ [ 'a root URL without trailing slash or query parameters', 'http://example.org', @@ -70,20 +77,20 @@ describe('DatasourceRouter', () => { { a: 1 }, { a: 1, features: { datasource: true }, datasource: '/my/data-source' }, ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); describe('A DatasourceRouter instance with a base URL', () => { let router = new DatasourceRouter({ - urlData: { baseURLPath: '/my/base/' }, + urlData: new UrlData({ baseURL: '/my/base' }), }); describe('extractUrlParams', () => { describe('with an existing query', () => { - [ + const rows: QueryParamsTestCase[] = [ [ 'a root URL', 'http://example.org/my/base/', @@ -98,8 +105,8 @@ describe('DatasourceRouter', () => { { a: 1 }, { a: 1, features: { datasource: true }, datasource: '/other/path' }, ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); diff --git a/packages/core/test/routers/PageRouter-test.js b/packages/core/test/routers/PageRouter-test.ts similarity index 88% rename from packages/core/test/routers/PageRouter-test.js rename to packages/core/test/routers/PageRouter-test.ts index 66e13811..3c85d976 100644 --- a/packages/core/test/routers/PageRouter-test.js +++ b/packages/core/test/routers/PageRouter-test.ts @@ -2,7 +2,13 @@ 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 +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 }; +type QueryParamsTestCase = [string, string, string, TestQuery, TestQuery]; describe('PageRouter', () => { describe('The PageRouter module', () => { @@ -20,7 +26,7 @@ describe('PageRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - [ + const rows: QueryParamsTestCase[] = [ [ 'a URL without query parameters', 'http://example.org/', @@ -77,8 +83,8 @@ describe('PageRouter', () => { { 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); }); + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); @@ -88,7 +94,7 @@ describe('PageRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - [ + const rows: QueryParamsTestCase[] = [ [ 'a URL without query parameters', 'http://example.org/', @@ -145,8 +151,8 @@ describe('PageRouter', () => { { 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); }); + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); @@ -156,7 +162,7 @@ describe('PageRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - [ + const rows: QueryParamsTestCase[] = [ [ 'a URL without query parameters', 'http://example.org/', @@ -164,8 +170,8 @@ describe('PageRouter', () => { { a: 1 }, { a: 1, features: { limit: true }, limit: 100 }, ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); + ]; + rows.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..4f2dd834 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..00898c1a 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..5788dd67 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] as RequestCallback)(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/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/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.js b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts similarity index 53% rename from packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.js rename to packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts index 97d463e8..99842049 100644 --- a/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.js +++ b/packages/feature-qpf/test/controllers/QuadPatternFragmentsController-test.ts @@ -1,16 +1,25 @@ /*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ import { describe, it, expect, beforeAll, vi } from 'vitest'; +import type { Mock } from 'vitest'; import { DummyServer } from '../../../../test/DummyServer'; -let QuadPatternFragmentsController = require('../../').controllers.QuadPatternFragmentsController; +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'; +import type { AsyncIterator } from 'asynciterator'; -let request = require('supertest'), - http = require('http'); +import * as http from 'http'; +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 { Datasource } = coreDatasources; + +type MutableQuery = Query & { features: QueryFeatures }; describe('QuadPatternFragmentsController', () => { describe('The QuadPatternFragmentsController module', () => { @@ -24,27 +33,40 @@ describe('QuadPatternFragmentsController', () => { }); describe('A QuadPatternFragmentsController instance with 3 routers', () => { - let controller, client, routerA, routerB, routerC, datasource, datasources, view, prefixes; + 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: DatasourceType, + supportsQuerySpy: Mock<(query: Query) => boolean>, + selectSpy: Mock<(query: Query) => AsyncIterator>, + selectResult: AsyncIterator, + datasources: DatasourceRegistry, + view: QuadPatternFragmentsRdfView, + renderSpy: Mock, + 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,20 +75,20 @@ describe('QuadPatternFragmentsController', () => { views: [view], prefixes: prefixes, }); - client = request.agent(new DummyServer(controller), {}); + server = new 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', () => { beforeAll(async () => { resetAll(); - await client.get('/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', () => { @@ -77,7 +99,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 +126,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 +146,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 +162,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,170 +173,170 @@ describe('QuadPatternFragmentsController', () => { describe('receiving a request for an unsupported fragment', () => { beforeAll(async () => { resetAll(); - datasource.supportsQuery = vi.fn().mockReturnValue(false); - await client.get('/my-datasource?a=b&c=d'); + supportsQuerySpy = vi.fn().mockReturnValue(false); + datasource.supportsQuery = supportsQuerySpy; + await request(server, '/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: QuadPatternFragmentsController, + server: DummyServer, + htmlView: QuadPatternFragmentsHtmlView, rdfView: QuadPatternFragmentsRdfView, + htmlRenderSpy: Mock, + rdfRenderSpy: Mock; 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), {}); + server = new 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: FetchLikeResponse; beforeAll(async () => { resetAll(); - response = await client.get('/my-datasource'); + response = await request(server, '/my-datasource'); }); it('should call the default view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); + expect(htmlRenderSpy).toHaveBeenCalledOnce(); }); 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; + let response: FetchLikeResponse; beforeAll(async () => { resetAll(); - response = await client.get('/my-datasource').set('Accept', '*/*'); + response = await request(server, '/my-datasource', { headers: { Accept: '*/*' } }); }); it('should call the HTML view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); + expect(htmlRenderSpy).toHaveBeenCalledOnce(); }); 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; + let response: FetchLikeResponse; beforeAll(async () => { resetAll(); - response = await client.get('/my-datasource').set('Accept', 'text/html'); + response = await request(server, '/my-datasource', { headers: { Accept: 'text/html' } }); }); it('should call the HTML view', () => { - expect(htmlView.render).toHaveBeenCalledOnce(); + expect(htmlRenderSpy).toHaveBeenCalledOnce(); }); 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; + let response: FetchLikeResponse; beforeAll(async () => { resetAll(); - response = await client.get('/my-datasource').set('Accept', 'text/turtle'); + response = await request(server, '/my-datasource', { headers: { Accept: 'text/turtle' } }); }); it('should call the Turtle view', () => { - expect(rdfView.render).toHaveBeenCalledOnce(); + expect(rdfRenderSpy).toHaveBeenCalledOnce(); }); 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; + let response: FetchLikeResponse; beforeAll(async () => { resetAll(); - response = await client.get('/my-datasource').set('Accept', 'text/n3'); + response = await request(server, '/my-datasource', { headers: { Accept: 'text/n3' } }); }); it('should call the Turtle view', () => { - expect(rdfView.render).toHaveBeenCalledOnce(); + expect(rdfRenderSpy).toHaveBeenCalledOnce(); }); 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, client; + let controller: QuadPatternFragmentsController, server: DummyServer; 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,70 +345,72 @@ describe('QuadPatternFragmentsController', () => { routers: [router], datasources: { 'my-datasource': datasource }, }); - client = request.agent(new DummyServer(controller), {}); + server = new DummyServer(controller); }); describe('receiving a request without Accept header', () => { - let response; + let response: FetchLikeResponse; beforeAll(async () => { - response = await client.get('/my-datasource'); + response = await request(server, '/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; + let response: FetchLikeResponse; beforeAll(async () => { - response = await client.get('/my-datasource').set('Accept', 'text/html'); + response = await request(server, '/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, client, router, datasource, error, view; + let controller: QuadPatternFragmentsController, + server: DummyServer, + router: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, + datasource: DatasourceType, error: Error, view: QuadPatternFragmentsRdfView; 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), {}); + server = new DummyServer(controller); }); function resetAll() { router.extractQueryParams.mockClear(); @@ -397,40 +419,43 @@ describe('QuadPatternFragmentsController', () => { describe('receiving a request for a fragment', () => { beforeAll(async () => { resetAll(); - await client.get('/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, client, router, datasource, error, view; + let controller: QuadPatternFragmentsController, + server: DummyServer, + router: { extractQueryParams: Mock<(request: RouterRequest, query: MutableQuery) => void> }, + datasource: DatasourceType, error: Error, view: QuadPatternFragmentsRdfView; 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); + datasource.select = vi.fn((query: Query, onError?: (error?: Error) => void) => { + setImmediate(() => onError?.(error)); + return empty(); + }); + 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), {}); + server = new DummyServer(controller); }); function resetAll() { router.extractQueryParams.mockClear(); @@ -439,11 +464,11 @@ describe('QuadPatternFragmentsController', () => { describe('receiving a request for a fragment', () => { beforeAll(async () => { resetAll(); - await client.get('/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-qpf/test/routers/QuadPatternRouter-test.js b/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts similarity index 96% rename from packages/feature-qpf/test/routers/QuadPatternRouter-test.js rename to packages/feature-qpf/test/routers/QuadPatternRouter-test.ts index 5b419f3d..760f77d0 100644 --- a/packages/feature-qpf/test/routers/QuadPatternRouter-test.js +++ b/packages/feature-qpf/test/routers/QuadPatternRouter-test.ts @@ -2,8 +2,16 @@ import { describe, it, expect } from 'vitest'; import { extractQueryParams } from '../../../../test/test-helpers'; -let QuadPatternRouter = require('../../').routers.QuadPatternRouter; -const dataFactory = require('n3').DataFactory; +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 }; +type QueryParamsTestCase = [string, string, string, TestQuery, TestQuery]; +import { DataFactory as dataFactory } from 'n3'; + +const { QuadPatternRouter } = routers; describe('QuadPatternRouter', () => { describe('The QuadPatternRouter module', () => { @@ -21,7 +29,7 @@ describe('QuadPatternRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - [ + const rows: QueryParamsTestCase[] = [ [ 'a URL without query parameters', 'http://example.org/', @@ -225,8 +233,8 @@ describe('QuadPatternRouter', () => { { a: 1 }, { a: 1, features: { quadPattern: true }, graph: dataFactory.defaultGraph() }, ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); + ]; + rows.forEach((args) => { extractQueryParams(router, ...args); }); }); }); }); @@ -242,7 +250,7 @@ describe('QuadPatternRouter', () => { describe('extractUrlParams', () => { describe('with an existing query', () => { - [ + const rows: QueryParamsTestCase[] = [ [ 'a URL without query parameters', 'http://example.org/', @@ -467,8 +475,8 @@ describe('QuadPatternRouter', () => { { a: 1 }, { a: 1, features: { quadPattern: true }, graph: dataFactory.namedNode('foo:bar') }, ], - ] - .forEach((args) => { extractQueryParams(router, ...args); }); + ]; + rows.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..262e55c8 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.js deleted file mode 100644 index 4a1f5e1e..00000000 --- a/packages/feature-summary/test/controllers/SummaryController-test.js +++ /dev/null @@ -1,84 +0,0 @@ -/*! @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 - -let request = require('supertest'), - fs = require('fs'), - path = require('path'); - -let SummaryRdfView = require('../../lib/views/summary/SummaryRdfView.js').SummaryRdfView; // changed to make tests pass, will be revised in follow up pr - -let dataFactory = require('n3').DataFactory; - -describe('SummaryController', () => { - describe('The SummaryController module', () => { - it('should be a function', () => { - expect(typeof SummaryController).toBe('function'); - }); - - it('should be an SummaryController constructor', () => { - expect(new SummaryController()).toBeInstanceOf(SummaryController); - }); - - it('should create new SummaryController objects', () => { - expect(new SummaryController()).toBeInstanceOf(SummaryController); - }); - }); - - describe('An SummaryController instance', () => { - let controller, client; - beforeAll(() => { - controller = new SummaryController({ - views: [new SummaryRdfView({ dataFactory })], - summaries: { dir: path.join(__dirname, '/../../../../test/assets') }, - prefixes: { - ds: 'http://semweb.mmlab.be/ns/datasummaries#', - rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - }, - }); - client = request.agent(new DummyServer(controller), {}); - }); - - it('should correctly serve summary in Turtle', async () => { - let response = await client.get('/summaries/summary').set('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); - }); - - it('should correctly serve summary in Trig', async () => { - let response = await client.get('/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); - }); - - it('should correctly serve summary in ntriples', async () => { - let response = await client.get('/summaries/summary').set('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); - }); - - it('should hand over to the next controller if no summary with that name is found', async () => { - await client.get('/summaries/unknown'); - expect(controller.next).toHaveBeenCalledOnce(); - }); - - it('should hand over to the next controller for non-summary paths', async () => { - await client.get('/other'); - expect(controller.next).toHaveBeenCalledOnce(); - }); - }); -}); diff --git a/packages/feature-summary/test/controllers/SummaryController-test.ts b/packages/feature-summary/test/controllers/SummaryController-test.ts new file mode 100644 index 00000000..5662c9d5 --- /dev/null +++ b/packages/feature-summary/test/controllers/SummaryController-test.ts @@ -0,0 +1,85 @@ +/*! @license MIT ©2015-2016 Ruben Verborgh, Ghent University - imec */ + +import { describe, it, expect, beforeAll } from 'vitest'; +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 { SummaryRdfView } = views.summary; + +describe('SummaryController', () => { + describe('The SummaryController module', () => { + it('should be a function', () => { + expect(typeof SummaryController).toBe('function'); + }); + + it('should be an SummaryController constructor', () => { + expect(new SummaryController()).toBeInstanceOf(SummaryController); + }); + + it('should create new SummaryController objects', () => { + expect(new SummaryController()).toBeInstanceOf(SummaryController); + }); + }); + + describe('An SummaryController instance', () => { + let controller: SummaryController, server: DummyServer; + beforeAll(() => { + controller = new SummaryController({ + views: [new SummaryRdfView({ dataFactory })], + summaries: { dir: path.join(__dirname, '/../../../../test/assets') }, + prefixes: { + ds: 'http://semweb.mmlab.be/ns/datasummaries#', + rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + }, + }); + server = new DummyServer(controller); + }); + + it('should correctly serve summary in Turtle', async () => { + let response = await request(server, '/summaries/summary', { headers: { Accept: 'text/turtle' } }); + let summary = fs.readFileSync(path.join(__dirname, '/../../../../test/assets/summary.ttl'), 'utf8'); + 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'); + expect(await response.text()).toBe(summary); + }); + + it('should correctly serve summary in Trig', async () => { + let response = await request(server, '/summaries/summary'); + let summary = fs.readFileSync(path.join(__dirname, '/../../../../test/assets/summary.ttl'), 'utf8'); + 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'); + expect(await response.text()).toBe(summary); + }); + + it('should correctly serve summary in ntriples', async () => { + 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(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'); + expect(await response.text()).toBe(summary); + }); + + it('should hand over to the next controller if no summary with that name is found', async () => { + await request(server, '/summaries/unknown'); + expect(server.next).toHaveBeenCalledOnce(); + }); + + it('should hand over to the next controller for non-summary paths', async () => { + await request(server, '/other'); + expect(server.next).toHaveBeenCalledOnce(); + }); + }); +}); 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..d4f44dc2 --- /dev/null +++ b/test/DummyServer.ts @@ -0,0 +1,29 @@ +/*! @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'; + +/* 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; + + 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); } + }); + } +} 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..dd6fd8c5 --- /dev/null +++ b/test/test-helpers.ts @@ -0,0 +1,108 @@ +/*! @license MIT ©2013-2016 Ruben Verborgh, Ghent University - imec */ +import { it, expect } from 'vitest'; +import { parse as parseUrl } from 'url'; +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, Router } from '../packages/core/lib/types'; + +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(); + if (address === null || typeof address === 'string') + throw new Error('Expected the server to report a network address'); + 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), + }; +} + +// Generates an `it` block that verifies a router's extractQueryParams behavior +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: 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 IncomingMessage { + override statusCode = 200; + + constructor(contentType: string) { + super(new Socket()); + this.headers = { 'content-type': contentType }; + } + + override _read(): void {} +} + +// 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.build.json b/tsconfig.build.json new file mode 100644 index 00000000..3e0e381d --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "packages/*/index.ts", + "packages/*/lib/**/*", + "packages/*/bin/**/*", + "types/**/*.d.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json index ad063175..40e6e5a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,8 @@ "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 new file mode 100644 index 00000000..720528b6 --- /dev/null +++ b/tsconfig.typecheck.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "preserve", + "moduleResolution": "bundler" + } +} 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 8ab33914..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" @@ -1376,35 +1364,30 @@ "@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@*", "@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.11.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" - integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== + 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.14.20" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.20.tgz#f7974863edd21d1f8a494a73e8e2b3658615c340" - integrity sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A== + 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.54" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.54.tgz#f1048dc083f81b242640f04f18fb3e4ccf13fcdb" - integrity sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw== + 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/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/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" @@ -1961,10 +1944,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" @@ -2364,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" @@ -2508,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" @@ -2642,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== @@ -2751,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== @@ -3356,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" @@ -3480,17 +3450,6 @@ form-data@^2.5.5: mime-types "^2.1.35" safe-buffer "^5.2.1" -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" @@ -3500,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" @@ -4826,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" @@ -5237,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" @@ -5284,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" @@ -6298,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" @@ -6380,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" @@ -7038,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" @@ -7117,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== @@ -7579,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" @@ -7722,6 +7649,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"