From 546ede284c62a882cb90f388403efb209d93efaf Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+SamTV12345@users.noreply.github.com> Date: Thu, 22 Feb 2024 18:31:17 +0100 Subject: [PATCH] Converted tests to typescript. (#6181) * Converted tests to typescript. * Run all tests. * Fixed tests. * Removed cypress from every installation. * Use cache for libreoffice. * Fixed cypress install. * Fixed cypress install. --- .github/workflows/backend-tests.yml | 16 +- .../workflows/upgrade-from-latest-release.yml | 8 +- .github/workflows/windows.yml | 1 + src/node/types/PadType.ts | 7 +- src/node/types/SettingsUser.ts | 6 + src/package.json | 6 +- src/tests/backend/assert-legacy.js | 48 ---- src/tests/backend/{common.js => common.ts} | 43 +-- .../{fuzzImportTest.js => fuzzImportTest.ts} | 11 +- .../{ExportEtherpad.js => ExportEtherpad.ts} | 4 +- .../{ImportEtherpad.js => ImportEtherpad.ts} | 18 +- src/tests/backend/specs/{Pad.js => Pad.ts} | 37 ++- .../{SecretRotator.js => SecretRotator.ts} | 192 +++++++------ .../{SessionStore.js => SessionStore.ts} | 72 ++--- .../backend/specs/{Stream.js => Stream.ts} | 195 +++++++------ .../backend/specs/api/{api.js => api.ts} | 8 +- ...racterEncoding.js => characterEncoding.ts} | 4 +- .../backend/specs/api/{chat.js => chat.ts} | 21 +- .../{fuzzImportTest.js => fuzzImportTest.ts} | 0 .../api/{importexport.js => importexport.ts} | 9 +- ...xportGetPost.js => importexportGetPost.ts} | 88 +++--- .../specs/api/{instance.js => instance.ts} | 6 +- .../backend/specs/api/{pad.js => pad.ts} | 24 +- ...{restoreRevision.js => restoreRevision.ts} | 12 +- ...sionsAndGroups.js => sessionsAndGroups.ts} | 72 ++--- ...ng_middleware.js => caching_middleware.ts} | 32 ++- src/tests/backend/specs/{chat.js => chat.ts} | 55 ++-- ...ontentcollector.js => contentcollector.ts} | 17 +- src/tests/backend/specs/crypto.js | 11 - src/tests/backend/specs/crypto.ts | 10 + .../backend/specs/{export.js => export.ts} | 6 +- .../backend/specs/{favicon.js => favicon.ts} | 14 +- .../backend/specs/{health.js => health.ts} | 8 +- .../backend/specs/{hooks.js => hooks.ts} | 267 ++++++++++-------- ...{lowerCasePadIds.js => lowerCasePadIds.ts} | 6 +- .../specs/{messages.js => messages.ts} | 47 +-- .../specs/{pad_utils.js => pad_utils.ts} | 14 +- ...ads-with-spaces.js => pads-with-spaces.ts} | 3 +- .../specs/{promises.js => promises.ts} | 27 +- .../{regression-db.js => regression-db.ts} | 8 +- ...anitizePathname.js => sanitizePathname.ts} | 7 +- .../specs/{settings.js => settings.ts} | 6 +- .../specs/{socketio.js => socketio.ts} | 88 +++--- .../{specialpages.js => specialpages.ts} | 8 +- .../specs/{webaccess.js => webaccess.ts} | 104 +++++-- 45 files changed, 912 insertions(+), 734 deletions(-) create mode 100644 src/node/types/SettingsUser.ts delete mode 100644 src/tests/backend/assert-legacy.js rename src/tests/backend/{common.js => common.ts} (85%) rename src/tests/backend/{fuzzImportTest.js => fuzzImportTest.ts} (91%) rename src/tests/backend/specs/{ExportEtherpad.js => ExportEtherpad.ts} (98%) rename src/tests/backend/specs/{ImportEtherpad.js => ImportEtherpad.ts} (94%) rename src/tests/backend/specs/{Pad.js => Pad.ts} (81%) rename src/tests/backend/specs/{SecretRotator.js => SecretRotator.ts} (75%) rename src/tests/backend/specs/{SessionStore.js => SessionStore.ts} (76%) rename src/tests/backend/specs/{Stream.js => Stream.ts} (63%) rename src/tests/backend/specs/api/{api.js => api.ts} (90%) rename src/tests/backend/specs/api/{characterEncoding.js => characterEncoding.ts} (95%) rename src/tests/backend/specs/api/{chat.js => chat.ts} (91%) rename src/tests/backend/specs/api/{fuzzImportTest.js => fuzzImportTest.ts} (100%) rename src/tests/backend/specs/api/{importexport.js => importexport.ts} (98%) rename src/tests/backend/specs/api/{importexportGetPost.js => importexportGetPost.ts} (89%) rename src/tests/backend/specs/api/{instance.js => instance.ts} (91%) rename src/tests/backend/specs/api/{pad.js => pad.ts} (97%) rename src/tests/backend/specs/api/{restoreRevision.js => restoreRevision.ts} (91%) rename src/tests/backend/specs/api/{sessionsAndGroups.js => sessionsAndGroups.ts} (90%) rename src/tests/backend/specs/{caching_middleware.js => caching_middleware.ts} (81%) rename src/tests/backend/specs/{chat.js => chat.ts} (75%) rename src/tests/backend/specs/{contentcollector.js => contentcollector.ts} (97%) delete mode 100644 src/tests/backend/specs/crypto.js create mode 100644 src/tests/backend/specs/crypto.ts rename src/tests/backend/specs/{export.js => export.ts} (84%) rename src/tests/backend/specs/{favicon.js => favicon.ts} (94%) rename src/tests/backend/specs/{health.js => health.ts} (89%) rename src/tests/backend/specs/{hooks.js => hooks.ts} (81%) rename src/tests/backend/specs/{lowerCasePadIds.js => lowerCasePadIds.ts} (96%) rename src/tests/backend/specs/{messages.js => messages.ts} (88%) rename src/tests/backend/specs/{pad_utils.js => pad_utils.ts} (78%) rename src/tests/backend/specs/{pads-with-spaces.js => pads-with-spaces.ts} (91%) rename src/tests/backend/specs/{promises.js => promises.ts} (83%) rename src/tests/backend/specs/{regression-db.js => regression-db.ts} (78%) rename src/tests/backend/specs/{sanitizePathname.js => sanitizePathname.ts} (95%) rename src/tests/backend/specs/{settings.js => settings.ts} (96%) rename src/tests/backend/specs/{socketio.js => socketio.ts} (85%) rename src/tests/backend/specs/{specialpages.js => specialpages.ts} (86%) rename src/tests/backend/specs/{webaccess.js => webaccess.ts} (92%) diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index 2ff211380..dc4787d69 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -47,10 +47,10 @@ jobs: run: pnpm config set auto-install-peers false - name: Install libreoffice - run: | - sudo add-apt-repository -y ppa:libreoffice/ppa - sudo apt update - sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport + uses: awalsh128/cache-apt-pkgs-action@v1.4.1 + with: + packages: libreoffice libreoffice-pdfimport + version: 1.0 - name: Install all dependencies and symlink for ep_etherpad-lite run: bin/installDeps.sh @@ -98,10 +98,10 @@ jobs: run: pnpm config set auto-install-peers false - name: Install libreoffice - run: | - sudo add-apt-repository -y ppa:libreoffice/ppa - sudo apt update - sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport + uses: awalsh128/cache-apt-pkgs-action@v1.4.1 + with: + packages: libreoffice libreoffice-pdfimport + version: 1.0 - name: Install all dependencies and symlink for ep_etherpad-lite run: bin/installDeps.sh diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index 2a97b8b27..b81476038 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -79,10 +79,10 @@ jobs: run: pnpm config set auto-install-peers false - name: Install libreoffice - run: | - sudo add-apt-repository -y ppa:libreoffice/ppa - sudo apt update - sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport + uses: awalsh128/cache-apt-pkgs-action@v1.4.1 + with: + packages: libreoffice libreoffice-pdfimport + version: 1.0 - name: Get pnpm store directory shell: bash run: | diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index d95cc68b6..d34a5a325 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -148,6 +148,7 @@ jobs: name: Run Etherpad working-directory: etherpad/src run: | + pnpm install cypress .\node_modules\.bin\cypress.cmd install --force pnpm run prod & curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test diff --git a/src/node/types/PadType.ts b/src/node/types/PadType.ts index 66a9d6ab1..5f222c969 100644 --- a/src/node/types/PadType.ts +++ b/src/node/types/PadType.ts @@ -5,11 +5,16 @@ export type PadType = { apool: ()=>APool, atext: AText, pool: APool, - getInternalRevisionAText: (text:string)=>Promise, + getInternalRevisionAText: (text:number|string)=>Promise, getValidRevisionRange: (fromRev: string, toRev: string)=>PadRange, + getRevisionAuthor: (rev: number)=>Promise, getRevision: (rev?: string)=>Promise, head: number, getAllAuthorColors: ()=>Promise>, + remove: ()=>Promise, + text: ()=>string, + setText: (text: string)=>Promise, + appendText: (text: string)=>Promise, } diff --git a/src/node/types/SettingsUser.ts b/src/node/types/SettingsUser.ts new file mode 100644 index 000000000..cb06332e3 --- /dev/null +++ b/src/node/types/SettingsUser.ts @@ -0,0 +1,6 @@ +export type SettingsUser = { + [username: string]:{ + password: string, + is_admin?: boolean, + } +} diff --git a/src/package.json b/src/package.json index ba392899f..9ab84314b 100644 --- a/src/package.json +++ b/src/package.json @@ -81,9 +81,11 @@ "devDependencies": { "@types/async": "^3.2.24", "@types/express": "^4.17.21", + "@types/mocha": "^10.0.6", "@types/node": "^20.11.19", + "@types/sinon": "^17.0.3", + "@types/supertest": "^6.0.2", "@types/underscore": "^1.11.15", - "cypress": "^13.6.4", "eslint": "^8.56.0", "eslint-config-etherpad": "^3.0.22", "etherpad-cli-client": "^3.0.1", @@ -109,7 +111,7 @@ }, "scripts": { "lint": "eslint .", - "test": "mocha --import=tsx --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", + "test": "mocha --import=tsx --timeout 120000 --recursive tests/backend/specs/**.ts tests/backend/specs/**/*.ts ../node_modules/ep_*/static/tests/backend/specs", "test-container": "mocha --import=tsx --timeout 5000 tests/container/specs/api", "dev": "node --import tsx node/server.ts", "prod": "node --import tsx node/server.ts", diff --git a/src/tests/backend/assert-legacy.js b/src/tests/backend/assert-legacy.js deleted file mode 100644 index b3760da9d..000000000 --- a/src/tests/backend/assert-legacy.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; -// support for older node versions (<12) -const assert = require('assert'); - -const internalMatch = (string, regexp, message, fn) => { - if (!regexp.test) { - throw new Error('regexp parameter is not a RegExp'); - } - if (typeof string !== 'string') { - throw new Error('string parameter is not a string'); - } - const match = fn.name === 'match'; - - const result = string.match(regexp); - if (match && !result) { - if (message) { - throw message; - } else { - throw new Error(`${string} does not match regex ${regexp}`); - } - } - if (!match && result) { - if (message) { - throw message; - } else { - throw new Error(`${string} does match regex ${regexp}`); - } - } -}; - - -if (!assert.match) { - const match = (string, regexp, message) => { - internalMatch(string, regexp, message, match); - }; - assert.match = match; -} -if (!assert.strict.match) assert.strict.match = assert.match; - -if (!assert.doesNotMatch) { - const doesNotMatch = (string, regexp, message) => { - internalMatch(string, regexp, message, doesNotMatch); - }; - assert.doesNotMatch = doesNotMatch; -} -if (!assert.strict.doesNotMatch) assert.strict.doesNotMatch = assert.doesNotMatch; - -module.exports = assert; diff --git a/src/tests/backend/common.js b/src/tests/backend/common.ts similarity index 85% rename from src/tests/backend/common.js rename to src/tests/backend/common.ts index 8172e1e87..c0cfd1377 100644 --- a/src/tests/backend/common.js +++ b/src/tests/backend/common.ts @@ -1,5 +1,7 @@ 'use strict'; +import {MapArrayType} from "../../node/types/MapType"; + const AttributePool = require('../../static/js/AttributePool'); const apiHandler = require('../../node/handler/APIHandler'); const assert = require('assert').strict; @@ -10,11 +12,11 @@ const process = require('process'); const server = require('../../node/server'); const setCookieParser = require('set-cookie-parser'); const settings = require('../../node/utils/Settings'); -const supertest = require('supertest'); +import supertest from 'supertest'; const webaccess = require('../../node/hooks/express/webaccess'); -const backups = {}; -let agentPromise = null; +const backups:MapArrayType = {}; +let agentPromise:Promise|null = null; exports.apiKey = apiHandler.exportedForTestingOnly.apiKey; exports.agent = null; @@ -27,7 +29,7 @@ const logLevel = logger.level; // Mocha doesn't monitor unhandled Promise rejections, so convert them to uncaught exceptions. // https://github.com/mochajs/mocha/issues/2640 -process.on('unhandledRejection', (reason, promise) => { throw reason; }); +process.on('unhandledRejection', (reason: string) => { throw reason; }); before(async function () { this.timeout(60000); @@ -67,7 +69,7 @@ exports.init = async function () { await server.exit(); }); - agentResolve(exports.agent); + agentResolve!(exports.agent); return exports.agent; }; @@ -79,7 +81,7 @@ exports.init = async function () { * @param {string} event - The socket.io Socket event to listen for. * @returns The argument(s) passed to the event handler. */ -exports.waitForSocketEvent = async (socket, event) => { +exports.waitForSocketEvent = async (socket: any, event:string) => { const errorEvents = [ 'error', 'connect_error', @@ -90,7 +92,7 @@ exports.waitForSocketEvent = async (socket, event) => { const handlers = new Map(); let cancelTimeout; try { - const timeoutP = new Promise((resolve, reject) => { + const timeoutP = new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(`timed out waiting for ${event} event`)); cancelTimeout = () => {}; @@ -102,14 +104,14 @@ exports.waitForSocketEvent = async (socket, event) => { }; }); const errorEventP = Promise.race(errorEvents.map((event) => new Promise((resolve, reject) => { - handlers.set(event, (errorString) => { + handlers.set(event, (errorString:string) => { logger.debug(`socket.io ${event} event: ${errorString}`); reject(new Error(errorString)); }); }))); - const eventP = new Promise((resolve) => { + const eventP = new Promise((resolve) => { // This will overwrite one of the above handlers if the user is waiting for an error event. - handlers.set(event, (...args) => { + handlers.set(event, (...args:string[]) => { logger.debug(`socket.io ${event} event`); if (args.length > 1) return resolve(args); resolve(args[0]); @@ -121,7 +123,7 @@ exports.waitForSocketEvent = async (socket, event) => { // the event arrives). return await Promise.race([timeoutP, errorEventP, eventP]); } finally { - cancelTimeout(); + cancelTimeout!(); for (const [event, handler] of handlers) socket.off(event, handler); } }; @@ -134,10 +136,11 @@ exports.waitForSocketEvent = async (socket, event) => { * nullish, no cookies are passed to the server. * @returns {io.Socket} A socket.io client Socket object. */ -exports.connect = async (res = null) => { +exports.connect = async (res:any = null) => { // Convert the `set-cookie` header(s) into a `cookie` header. const resCookies = (res == null) ? {} : setCookieParser.parse(res, {map: true}); const reqCookieHdr = Object.entries(resCookies).map( + // @ts-ignore ([name, cookie]) => `${name}=${encodeURIComponent(cookie.value)}`).join('; '); logger.debug('socket.io connecting...'); @@ -167,9 +170,10 @@ exports.connect = async (res = null) => { * * @param {io.Socket} socket - Connected socket.io Socket object. * @param {string} padId - Which pad to join. + * @param token * @returns The CLIENT_VARS message from the server. */ -exports.handshake = async (socket, padId, token = padutils.generateAuthorToken()) => { +exports.handshake = async (socket: any, padId:string, token = padutils.generateAuthorToken()) => { logger.debug('sending CLIENT_READY...'); socket.emit('message', { component: 'pad', @@ -187,8 +191,11 @@ exports.handshake = async (socket, padId, token = padutils.generateAuthorToken() /** * Convenience wrapper around `socket.send()` that waits for acknowledgement. */ -exports.sendMessage = async (socket, message) => await new Promise((resolve, reject) => { - socket.emit('message', message, (errInfo) => { +exports.sendMessage = async (socket: any, message:any) => await new Promise((resolve, reject) => { + socket.emit('message', message, (errInfo:{ + name: string, + message: string, + }) => { if (errInfo != null) { const {name, message} = errInfo; const err = new Error(message); @@ -203,7 +210,7 @@ exports.sendMessage = async (socket, message) => await new Promise((resolve, rej /** * Convenience function to send a USER_CHANGES message. Waits for acknowledgement. */ -exports.sendUserChanges = async (socket, data) => await exports.sendMessage(socket, { +exports.sendUserChanges = async (socket:any, data:any) => await exports.sendMessage(socket, { type: 'COLLABROOM', component: 'pad', data: { @@ -225,7 +232,7 @@ exports.sendUserChanges = async (socket, data) => await exports.sendMessage(sock * common.sendUserChanges(socket, {baseRev: rev, changeset}), * ]); */ -exports.waitForAcceptCommit = async (socket, wantRev) => { +exports.waitForAcceptCommit = async (socket:any, wantRev: number) => { const msg = await exports.waitForSocketEvent(socket, 'message'); assert.deepEqual(msg, { type: 'COLLABROOM', @@ -245,7 +252,7 @@ const alphabet = 'abcdefghijklmnopqrstuvwxyz'; * @param {string} [charset] - Characters to pick from. * @returns {string} */ -exports.randomString = (len = 10, charset = `${alphabet}${alphabet.toUpperCase()}0123456789`) => { +exports.randomString = (len: number = 10, charset: string = `${alphabet}${alphabet.toUpperCase()}0123456789`): string => { let ret = ''; while (ret.length < len) ret += charset[Math.floor(Math.random() * charset.length)]; return ret; diff --git a/src/tests/backend/fuzzImportTest.js b/src/tests/backend/fuzzImportTest.ts similarity index 91% rename from src/tests/backend/fuzzImportTest.js rename to src/tests/backend/fuzzImportTest.ts index ad55cdefb..8366e62d8 100644 --- a/src/tests/backend/fuzzImportTest.js +++ b/src/tests/backend/fuzzImportTest.ts @@ -2,16 +2,16 @@ * Fuzz testing the import endpoint * Usage: node fuzzImportTest.js */ +const settings = require('../container/loadSettings').loadSettings(); const common = require('./common'); const host = `http://${settings.ip}:${settings.port}`; const froth = require('mocha-froth'); -const settings = require('../container/loadSettings').loadSettings(); const axios = require('axios'); const apiKey = common.apiKey; const apiVersion = 1; const testPadId = `TEST_fuzz${makeid()}`; -const endPoint = function (point, version) { +const endPoint = function (point: string, version?:number) { version = version || apiVersion; return `/api/${version}/${point}?apikey=${apiKey}`; }; @@ -28,7 +28,7 @@ setTimeout(() => { } }, 5000); // wait 5 seconds -async function runTest(number) { +async function runTest(number: number) { await axios.get(`${host + endPoint('createPad')}&padID=${testPadId}`) .then(() => { const req = axios.post(`${host}/p/${testPadId}/import`) @@ -51,8 +51,9 @@ async function runTest(number) { }); }); }) - .catch(err => { - throw new Error('FAILURE', err); + .catch((err:any) => { + // @ts-ignore + throw new Error('FAILURE', err); }) } diff --git a/src/tests/backend/specs/ExportEtherpad.js b/src/tests/backend/specs/ExportEtherpad.ts similarity index 98% rename from src/tests/backend/specs/ExportEtherpad.js rename to src/tests/backend/specs/ExportEtherpad.ts index e66bb4633..677890cbb 100644 --- a/src/tests/backend/specs/ExportEtherpad.js +++ b/src/tests/backend/specs/ExportEtherpad.ts @@ -8,7 +8,7 @@ const plugins = require('../../../static/js/pluginfw/plugin_defs'); const readOnlyManager = require('../../../node/db/ReadOnlyManager'); describe(__filename, function () { - let padId; + let padId:string; beforeEach(async function () { padId = common.randomString(); @@ -16,7 +16,7 @@ describe(__filename, function () { }); describe('exportEtherpadAdditionalContent', function () { - let hookBackup; + let hookBackup: ()=>void; before(async function () { hookBackup = plugins.hooks.exportEtherpadAdditionalContent || []; diff --git a/src/tests/backend/specs/ImportEtherpad.js b/src/tests/backend/specs/ImportEtherpad.ts similarity index 94% rename from src/tests/backend/specs/ImportEtherpad.js rename to src/tests/backend/specs/ImportEtherpad.ts index 3158c308d..b0a208b4a 100644 --- a/src/tests/backend/specs/ImportEtherpad.js +++ b/src/tests/backend/specs/ImportEtherpad.ts @@ -1,5 +1,7 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; + const assert = require('assert').strict; const authorManager = require('../../../node/db/AuthorManager'); const db = require('../../../node/db/DB'); @@ -9,11 +11,11 @@ const plugins = require('../../../static/js/pluginfw/plugin_defs'); const {randomString} = require('../../../static/js/pad_utils'); describe(__filename, function () { - let padId; + let padId: string; const makeAuthorId = () => `a.${randomString(16)}`; - const makeExport = (authorId) => ({ + const makeExport = (authorId: string) => ({ 'pad:testing': { atext: { text: 'foo\n', @@ -65,7 +67,7 @@ describe(__filename, function () { it('changes are all or nothing', async function () { const authorId = makeAuthorId(); - const data = makeExport(authorId); + const data:MapArrayType = makeExport(authorId); data['pad:differentPadId:revs:0'] = data['pad:testing:revs:0']; delete data['pad:testing:revs:0']; assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/); @@ -74,8 +76,8 @@ describe(__filename, function () { }); describe('author pad IDs', function () { - let existingAuthorId; - let newAuthorId; + let existingAuthorId: string; + let newAuthorId:string; beforeEach(async function () { existingAuthorId = (await authorManager.createAuthor('existing')).authorID; @@ -133,7 +135,7 @@ describe(__filename, function () { describe('enforces consistent pad ID', function () { it('pad record has different pad ID', async function () { - const data = makeExport(makeAuthorId()); + const data:MapArrayType = makeExport(makeAuthorId()); data['pad:differentPadId'] = data['pad:testing']; delete data['pad:testing']; assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/); @@ -147,7 +149,7 @@ describe(__filename, function () { }); it('pad rev record has different pad ID', async function () { - const data = makeExport(makeAuthorId()); + const data:MapArrayType = makeExport(makeAuthorId()); data['pad:differentPadId:revs:0'] = data['pad:testing:revs:0']; delete data['pad:testing:revs:0']; assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/); @@ -170,7 +172,7 @@ describe(__filename, function () { }); describe('exportEtherpadAdditionalContent', function () { - let hookBackup; + let hookBackup: Function; before(async function () { hookBackup = plugins.hooks.exportEtherpadAdditionalContent || []; diff --git a/src/tests/backend/specs/Pad.js b/src/tests/backend/specs/Pad.ts similarity index 81% rename from src/tests/backend/specs/Pad.js rename to src/tests/backend/specs/Pad.ts index ec5c24631..b77bdf672 100644 --- a/src/tests/backend/specs/Pad.js +++ b/src/tests/backend/specs/Pad.ts @@ -1,7 +1,10 @@ 'use strict'; +import {PadType} from "../../../node/types/PadType"; + const Pad = require('../../../node/db/Pad'); -const assert = require('assert').strict; +import { strict as assert } from 'assert'; +import {MapArrayType} from "../../../node/types/MapType"; const authorManager = require('../../../node/db/AuthorManager'); const common = require('../common'); const padManager = require('../../../node/db/PadManager'); @@ -9,9 +12,9 @@ const plugins = require('../../../static/js/pluginfw/plugin_defs'); const settings = require('../../../node/utils/Settings'); describe(__filename, function () { - const backups = {}; - let pad; - let padId; + const backups:MapArrayType = {}; + let pad: PadType|null; + let padId: string; before(async function () { backups.hooks = { @@ -52,7 +55,7 @@ describe(__filename, function () { describe('padDefaultContent hook', function () { it('runs when a pad is created without specific text', async function () { - const p = new Promise((resolve) => { + const p = new Promise((resolve) => { plugins.hooks.padDefaultContent.push({hook_fn: () => resolve()}); }); pad = await padManager.getPad(padId); @@ -66,8 +69,8 @@ describe(__filename, function () { }); it('defaults to settings.defaultPadText', async function () { - const p = new Promise((resolve, reject) => { - plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, ctx) => { + const p = new Promise((resolve, reject) => { + plugins.hooks.padDefaultContent.push({hook_fn: async (hookName:string, ctx:any) => { try { assert.equal(ctx.type, 'text'); assert.equal(ctx.content, settings.defaultPadText); @@ -83,7 +86,9 @@ describe(__filename, function () { it('passes the pad object', async function () { const gotP = new Promise((resolve) => { - plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, {pad}) => resolve(pad)}); + plugins.hooks.padDefaultContent.push({hook_fn: async (hookName:string, {pad}:{ + pad: PadType, + }) => resolve(pad)}); }); pad = await padManager.getPad(padId); assert.equal(await gotP, pad); @@ -92,7 +97,9 @@ describe(__filename, function () { it('passes empty authorId if not provided', async function () { const gotP = new Promise((resolve) => { plugins.hooks.padDefaultContent.push( - {hook_fn: async (hookName, {authorId}) => resolve(authorId)}); + {hook_fn: async (hookName:string, {authorId}:{ + authorId: string, + }) => resolve(authorId)}); }); pad = await padManager.getPad(padId); assert.equal(await gotP, ''); @@ -102,7 +109,9 @@ describe(__filename, function () { const want = await authorManager.getAuthor4Token(`t.${padId}`); const gotP = new Promise((resolve) => { plugins.hooks.padDefaultContent.push( - {hook_fn: async (hookName, {authorId}) => resolve(authorId)}); + {hook_fn: async (hookName: string, {authorId}:{ + authorId: string, + }) => resolve(authorId)}); }); pad = await padManager.getPad(padId, null, want); assert.equal(await gotP, want); @@ -111,24 +120,24 @@ describe(__filename, function () { it('uses provided content', async function () { const want = 'hello world'; assert.notEqual(want, settings.defaultPadText); - plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, ctx) => { + plugins.hooks.padDefaultContent.push({hook_fn: async (hookName:string, ctx:any) => { ctx.type = 'text'; ctx.content = want; }}); pad = await padManager.getPad(padId); - assert.equal(pad.text(), `${want}\n`); + assert.equal(pad!.text(), `${want}\n`); }); it('cleans provided content', async function () { const input = 'foo\r\nbar\r\tbaz'; const want = 'foo\nbar\n baz'; assert.notEqual(want, settings.defaultPadText); - plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, ctx) => { + plugins.hooks.padDefaultContent.push({hook_fn: async (hookName:string, ctx:any) => { ctx.type = 'text'; ctx.content = input; }}); pad = await padManager.getPad(padId); - assert.equal(pad.text(), `${want}\n`); + assert.equal(pad!.text(), `${want}\n`); }); }); }); diff --git a/src/tests/backend/specs/SecretRotator.js b/src/tests/backend/specs/SecretRotator.ts similarity index 75% rename from src/tests/backend/specs/SecretRotator.js rename to src/tests/backend/specs/SecretRotator.ts index 9436e2876..d95b6dba1 100644 --- a/src/tests/backend/specs/SecretRotator.js +++ b/src/tests/backend/specs/SecretRotator.ts @@ -1,6 +1,6 @@ 'use strict'; -const assert = require('assert').strict; +import {strict} from "assert"; const common = require('../common'); const crypto = require('../../../node/security/crypto'); const db = require('../../../node/db/DB'); @@ -9,17 +9,23 @@ const SecretRotator = require("../../../node/security/SecretRotator").SecretRota const logger = common.logger; // Greatest common divisor. -const gcd = (...args) => ( +const gcd: Function = (...args:number[]) => ( args.length === 1 ? args[0] : args.length === 2 ? ((args[1]) ? gcd(args[1], args[0] % args[1]) : Math.abs(args[0])) : gcd(args[0], gcd(...args.slice(1)))); + // Least common multiple. -const lcm = (...args) => ( +const lcm:Function = (...args: number[]) => ( args.length === 1 ? args[0] : args.length === 2 ? Math.abs(args[0] * args[1]) / gcd(...args) : lcm(args[0], lcm(...args.slice(1)))); class FakeClock { + _now: number; + _nextId: number; + _idle: Promise; + timeouts: Map; + constructor() { logger.debug('new fake clock'); this._now = 0; @@ -29,10 +35,10 @@ class FakeClock { } _next() { return Math.min(...[...this.timeouts.values()].map((x) => x.when)); } - async setNow(t) { + async setNow(t: number) { logger.debug(`setting fake time to ${t}`); - assert(t >= this._now); - assert(t < Infinity); + strict(t >= this._now); + strict(t < Infinity); let n; while ((n = this._next()) <= t) { this._now = Math.max(this._now, Math.min(n, t)); @@ -42,7 +48,7 @@ class FakeClock { this._now = t; logger.debug(`fake time set to ${this._now}`); } - async advance(t) { await this.setNow(this._now + t); } + async advance(t: number) { await this.setNow(this._now + t); } async advanceToNext() { const n = this._next(); if (n < this._now) await this._fire(); @@ -68,34 +74,34 @@ class FakeClock { } get now() { return this._now; } - setTimeout(fn, wait = 0) { + setTimeout(fn:Function, wait = 0) { const when = this._now + wait; const id = this._nextId++; this.timeouts.set(id, {id, fn, when}); this._fire(); return id; } - clearTimeout(id) { this.timeouts.delete(id); } + clearTimeout(id:number) { this.timeouts.delete(id); } } // In JavaScript, the % operator is remainder, not modulus. -const mod = (a, n) => ((a % n) + n) % n; +const mod = (a: number, n:number) => ((a % n) + n) % n; describe(__filename, function () { - let dbPrefix; - let sr; + let dbPrefix: string; + let sr: any; let interval = 1e3; const lifetime = 1e4; - const intervalStart = (t) => t - mod(t, interval); - const hkdf = async (secret, salt, tN) => Buffer.from( + const intervalStart = (t: number) => t - mod(t, interval); + const hkdf = async (secret: string, salt:string, tN:number) => Buffer.from( await crypto.hkdf('sha256', secret, salt, `${tN}`, 32)).toString('hex'); - const newRotator = (s = null) => new SecretRotator(dbPrefix, interval, lifetime, s); + const newRotator = (s:string|null = null) => new SecretRotator(dbPrefix, interval, lifetime, s); - const setFakeClock = (sr, fc = null) => { + const setFakeClock = (sr: { _t: { now: () => number; setTimeout: (fn: Function, wait?: number) => number; clearTimeout: (id: number) => void; }; }, fc:FakeClock|null = null) => { if (fc == null) fc = new FakeClock(); sr._t = { - now: () => fc.now, + now: () => fc!.now, setTimeout: fc.setTimeout.bind(fc), clearTimeout: fc.clearTimeout.bind(fc), }; @@ -115,19 +121,19 @@ describe(__filename, function () { if (sr != null) sr.stop(); sr = null; await Promise.all( - (await db.findKeys(`${dbPrefix}:*`, null)).map(async (dbKey) => await db.remove(dbKey))); + (await db.findKeys(`${dbPrefix}:*`, null)).map(async (dbKey: string) => await db.remove(dbKey))); }); describe('constructor', function () { it('creates empty secrets array', async function () { sr = newRotator(); - assert.deepEqual(sr.secrets, []); + strict.deepEqual(sr.secrets, []); }); for (const invalidChar of '*:%') { it(`rejects database prefixes containing ${invalidChar}`, async function () { dbPrefix += invalidChar; - assert.throws(newRotator, /invalid char/); + strict.throws(newRotator, /invalid char/); }); } }); @@ -138,19 +144,19 @@ describe(__filename, function () { setFakeClock(sr); const {secrets} = sr; await sr.start(); - assert.equal(sr.secrets, secrets); + strict.equal(sr.secrets, secrets); }); it('derives secrets', async function () { sr = newRotator(); setFakeClock(sr); await sr.start(); - assert.equal(sr.secrets.length, 3); // Current (active), previous, and next. + strict.equal(sr.secrets.length, 3); // Current (active), previous, and next. for (const s of sr.secrets) { - assert.equal(typeof s, 'string'); - assert(s); + strict.equal(typeof s, 'string'); + strict(s); } - assert.equal(new Set(sr.secrets).size, sr.secrets.length); // The secrets should all differ. + strict.equal(new Set(sr.secrets).size, sr.secrets.length); // The secrets should all differ. }); it('publishes params', async function () { @@ -158,13 +164,13 @@ describe(__filename, function () { const fc = setFakeClock(sr); await sr.start(); const dbKeys = await db.findKeys(`${dbPrefix}:*`, null); - assert.equal(dbKeys.length, 1); + strict.equal(dbKeys.length, 1); const [id] = dbKeys; - assert(id.startsWith(`${dbPrefix}:`)); - assert.notEqual(id.slice(dbPrefix.length + 1), ''); + strict(id.startsWith(`${dbPrefix}:`)); + strict.notEqual(id.slice(dbPrefix.length + 1), ''); const p = await db.get(id); const {secret, salt} = p.algParams; - assert.deepEqual(p, { + strict.deepEqual(p, { algId: 1, algParams: { digest: 'sha256', @@ -177,11 +183,11 @@ describe(__filename, function () { interval, lifetime, }); - assert.equal(typeof salt, 'string'); - assert.match(salt, /^[0-9a-f]{64}$/); - assert.equal(typeof secret, 'string'); - assert.match(secret, /^[0-9a-f]{64}$/); - assert.deepEqual(sr.secrets, await Promise.all( + strict.equal(typeof salt, 'string'); + strict.match(salt, /^[0-9a-f]{64}$/); + strict.equal(typeof secret, 'string'); + strict.match(secret, /^[0-9a-f]{64}$/); + strict.deepEqual(sr.secrets, await Promise.all( [0, -interval, interval].map(async (tN) => await hkdf(secret, salt, tN)))); }); @@ -195,8 +201,8 @@ describe(__filename, function () { sr = newRotator(); setFakeClock(sr, fc); await sr.start(); - assert.deepEqual(sr.secrets, secrets); - assert.deepEqual(await db.findKeys(`${dbPrefix}:*`, null), dbKeys); + strict.deepEqual(sr.secrets, secrets); + strict.deepEqual(await db.findKeys(`${dbPrefix}:*`, null), dbKeys); }); it('deletes expired publications', async function () { @@ -204,7 +210,7 @@ describe(__filename, function () { const fc = setFakeClock(sr); await sr.start(); const [oldId] = await db.findKeys(`${dbPrefix}:*`, null); - assert(oldId != null); + strict(oldId != null); sr.stop(); const p = await db.get(oldId); await fc.setNow(p.end + p.lifetime + p.interval); @@ -212,9 +218,9 @@ describe(__filename, function () { setFakeClock(sr, fc); await sr.start(); const ids = await db.findKeys(`${dbPrefix}:*`, null); - assert.equal(ids.length, 1); + strict.equal(ids.length, 1); const [newId] = ids; - assert.notEqual(newId, oldId); + strict.notEqual(newId, oldId); }); it('keeps expired publications until interval past expiration', async function () { @@ -229,23 +235,23 @@ describe(__filename, function () { sr = newRotator(); setFakeClock(sr, fc); await sr.start(); - assert(sr.secrets.slice(1).includes(future)); + strict(sr.secrets.slice(1).includes(future)); // It should have created a new publication, not extended the life of the old publication. - assert.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2); - assert.deepEqual(await db.get(origId), p); + strict.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2); + strict.deepEqual(await db.get(origId), p); }); it('idempotent', async function () { sr = newRotator(); const fc = setFakeClock(sr); await sr.start(); - assert.equal(fc.timeouts.size, 1); + strict.equal(fc.timeouts.size, 1); const secrets = [...sr.secrets]; const dbKeys = await db.findKeys(`${dbPrefix}:*`, null); await sr.start(); - assert.equal(fc.timeouts.size, 1); - assert.deepEqual(sr.secrets, secrets); - assert.deepEqual(await db.findKeys(`${dbPrefix}:*`, null), dbKeys); + strict.equal(fc.timeouts.size, 1); + strict.deepEqual(sr.secrets, secrets); + strict.deepEqual(await db.findKeys(`${dbPrefix}:*`, null), dbKeys); }); describe(`schedules update at next interval (= ${interval})`, function () { @@ -262,16 +268,16 @@ describe(__filename, function () { const fc = setFakeClock(sr); await fc.setNow(now); await sr.start(); - assert.equal(fc.timeouts.size, 1); + strict.equal(fc.timeouts.size, 1); const [{when}] = fc.timeouts.values(); - assert.equal(when, want); + strict.equal(when, want); }); } it('multiple active params with different intervals', async function () { const intervals = [400, 600, 1000]; const lcmi = lcm(...intervals); - const wants = new Set(); + const wants:Set = new Set(); for (const i of intervals) for (let t = i; t <= lcmi; t += i) wants.add(t); const fcs = new FakeClock(); const srs = intervals.map((i) => { @@ -290,7 +296,7 @@ describe(__filename, function () { logger.debug(`next timeout should be at ${want}`); await fc.advanceToNext(); await fcs.setNow(fc.now); // Keep all of the publications alive. - assert.equal(fc.now, want); + strict.equal(fc.now, want); } } finally { for (const sr of srs) sr.stop(); @@ -304,9 +310,9 @@ describe(__filename, function () { sr = newRotator(); const fc = setFakeClock(sr); await sr.start(); - assert.notEqual(fc.timeouts.size, 0); + strict.notEqual(fc.timeouts.size, 0); sr.stop(); - assert.equal(fc.timeouts.size, 0); + strict.equal(fc.timeouts.size, 0); }); it('safe to call multiple times', async function () { @@ -325,14 +331,14 @@ describe(__filename, function () { // Use a time that isn't a multiple of interval in case there is a modular arithmetic bug that // would otherwise go undetected. await fc.setNow(1); - assert(mod(fc.now, interval) !== 0); + strict(mod(fc.now, interval) !== 0); await sr.start(); - assert.equal(sr.secrets.length, 4); // 1 for the legacy secret, 3 for past, current, future - assert(sr.secrets.slice(1).includes('legacy')); // Should not be the current secret. + strict.equal(sr.secrets.length, 4); // 1 for the legacy secret, 3 for past, current, future + strict(sr.secrets.slice(1).includes('legacy')); // Should not be the current secret. const ids = await db.findKeys(`${dbPrefix}:*`, null); - const params = (await Promise.all(ids.map(async (id) => await db.get(id)))) + const params = (await Promise.all(ids.map(async (id:string) => await db.get(id)))) .sort((a, b) => a.algId - b.algId); - assert.deepEqual(params, [ + strict.deepEqual(params, [ { algId: 0, algParams: 'legacy', @@ -358,26 +364,26 @@ describe(__filename, function () { sr = newRotator(); const fc = setFakeClock(sr); await fc.setNow(1); - assert(mod(fc.now, interval) !== 0); + strict(mod(fc.now, interval) !== 0); const wantTime = fc.now; await sr.start(); - assert.equal(sr.secrets.length, 3); + strict.equal(sr.secrets.length, 3); const [s1, s0, s2] = sr.secrets; // s1=current, s0=previous, s2=next sr.stop(); // Use a time that is not a multiple of interval off of epoch or wantTime just in case there // is a modular arithmetic bug that would otherwise go undetected. await fc.advance(interval + 1); - assert(mod(fc.now, interval) !== 0); - assert(mod(fc.now - wantTime, interval) !== 0); + strict(mod(fc.now, interval) !== 0); + strict(mod(fc.now - wantTime, interval) !== 0); sr = newRotator('legacy'); setFakeClock(sr, fc); await sr.start(); - assert.equal(sr.secrets.length, 5); // s0 through s3 and the legacy secret. - assert.deepEqual(sr.secrets, [s2, s1, s0, sr.secrets[3], 'legacy']); + strict.equal(sr.secrets.length, 5); // s0 through s3 and the legacy secret. + strict.deepEqual(sr.secrets, [s2, s1, s0, sr.secrets[3], 'legacy']); const ids = await db.findKeys(`${dbPrefix}:*`, null); - const params = (await Promise.all(ids.map(async (id) => await db.get(id)))) + const params = (await Promise.all(ids.map(async (id:string) => await db.get(id)))) .sort((a, b) => a.algId - b.algId); - assert.deepEqual(params, [ + strict.deepEqual(params, [ { algId: 0, algParams: 'legacy', @@ -405,8 +411,8 @@ describe(__filename, function () { sr = newRotator('legacy2'); setFakeClock(sr, fc); await sr.start(); - assert(sr.secrets.slice(1).includes('legacy1')); - assert(sr.secrets.slice(1).includes('legacy2')); + strict(sr.secrets.slice(1).includes('legacy1')); + strict(sr.secrets.slice(1).includes('legacy2')); }); it('multiple instances with the same legacy secret', async function () { @@ -417,9 +423,9 @@ describe(__filename, function () { sr = newRotator('legacy'); setFakeClock(sr, fc); await sr.start(); - assert.deepEqual(sr.secrets, [...new Set(sr.secrets)]); + strict.deepEqual(sr.secrets, [...new Set(sr.secrets)]); // There shouldn't be multiple publications for the same legacy secret. - assert.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2); + strict.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2); }); it('legacy secret is included for interval after expiration', async function () { @@ -431,7 +437,7 @@ describe(__filename, function () { sr = newRotator('legacy'); setFakeClock(sr, fc); await sr.start(); - assert(sr.secrets.slice(1).includes('legacy')); + strict(sr.secrets.slice(1).includes('legacy')); }); it('legacy secret is not included if the oldest secret is old enough', async function () { @@ -443,7 +449,7 @@ describe(__filename, function () { sr = newRotator('legacy'); setFakeClock(sr, fc); await sr.start(); - assert(!sr.secrets.includes('legacy')); + strict(!sr.secrets.includes('legacy')); }); it('dead secrets still affect legacy secret end time', async function () { @@ -456,8 +462,8 @@ describe(__filename, function () { sr = newRotator('legacy'); setFakeClock(sr, fc); await sr.start(); - assert(!sr.secrets.includes('legacy')); - assert(!sr.secrets.some((s) => secrets.has(s))); + strict(!sr.secrets.includes('legacy')); + strict(!sr.secrets.some((s:string) => secrets.has(s))); }); }); @@ -465,11 +471,11 @@ describe(__filename, function () { it('no rotation before start of interval', async function () { sr = newRotator(); const fc = setFakeClock(sr); - assert.equal(fc.now, 0); + strict.equal(fc.now, 0); await sr.start(); const secrets = [...sr.secrets]; await fc.advance(interval - 1); - assert.deepEqual(sr.secrets, secrets); + strict.deepEqual(sr.secrets, secrets); }); it('does not replace secrets array', async function () { @@ -479,8 +485,8 @@ describe(__filename, function () { const [current] = sr.secrets; const secrets = sr.secrets; await fc.advance(interval); - assert.notEqual(sr.secrets[0], current); - assert.equal(sr.secrets, secrets); + strict.notEqual(sr.secrets[0], current); + strict.equal(sr.secrets, secrets); }); it('future secret becomes current, new future is generated', async function () { @@ -488,11 +494,11 @@ describe(__filename, function () { const fc = setFakeClock(sr); await sr.start(); const secrets = new Set(sr.secrets); - assert.equal(secrets.size, 3); + strict.equal(secrets.size, 3); const [s1, s0, s2] = sr.secrets; await fc.advance(interval); - assert.deepEqual(sr.secrets, [s2, s1, s0, sr.secrets[3]]); - assert(!secrets.has(sr.secrets[3])); + strict.deepEqual(sr.secrets, [s2, s1, s0, sr.secrets[3]]); + strict(!secrets.has(sr.secrets[3])); }); it('expired publications are deleted', async function () { @@ -505,9 +511,9 @@ describe(__filename, function () { sr = newRotator(); setFakeClock(sr, fc); await sr.start(); - assert.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2); + strict.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2); await fc.advance(lifetime + (3 * origInterval)); - assert.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 1); + strict.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 1); }); it('old secrets are eventually removed', async function () { @@ -516,9 +522,9 @@ describe(__filename, function () { await sr.start(); const [, s0] = sr.secrets; await fc.advance(lifetime + interval - 1); - assert(sr.secrets.slice(1).includes(s0)); + strict(sr.secrets.slice(1).includes(s0)); await fc.advance(1); - assert(!sr.secrets.includes(s0)); + strict(!sr.secrets.includes(s0)); }); }); @@ -527,19 +533,19 @@ describe(__filename, function () { const srs = [newRotator(), newRotator()]; const fcs = srs.map((sr) => setFakeClock(sr)); for (const sr of srs) await sr.start(); // Don't use Promise.all() otherwise they race. - assert.deepEqual(srs[0].secrets, srs[1].secrets); + strict.deepEqual(srs[0].secrets, srs[1].secrets); // Advance fcs[0] to the end of the interval after fcs[1]. await fcs[0].advance((2 * interval) - 1); - assert(srs[0].secrets.includes(srs[1].secrets[0])); - assert(srs[1].secrets.includes(srs[0].secrets[0])); + strict(srs[0].secrets.includes(srs[1].secrets[0])); + strict(srs[1].secrets.includes(srs[0].secrets[0])); // Advance both by an interval. await Promise.all([fcs[1].advance(interval), fcs[0].advance(interval)]); - assert(srs[0].secrets.includes(srs[1].secrets[0])); - assert(srs[1].secrets.includes(srs[0].secrets[0])); + strict(srs[0].secrets.includes(srs[1].secrets[0])); + strict(srs[1].secrets.includes(srs[0].secrets[0])); // Advance fcs[1] to the end of the interval after fcs[0]. await Promise.all([fcs[1].advance((3 * interval) - 1), fcs[0].advance(1)]); - assert(srs[0].secrets.includes(srs[1].secrets[0])); - assert(srs[1].secrets.includes(srs[0].secrets[0])); + strict(srs[0].secrets.includes(srs[1].secrets[0])); + strict(srs[1].secrets.includes(srs[0].secrets[0])); }); it('start up out of sync', async function () { @@ -548,8 +554,8 @@ describe(__filename, function () { await fcs[0].advance((2 * interval) - 1); await srs[0].start(); // Must start before srs[1] so that srs[1] starts in srs[0]'s past. await srs[1].start(); - assert(srs[0].secrets.includes(srs[1].secrets[0])); - assert(srs[1].secrets.includes(srs[0].secrets[0])); + strict(srs[0].secrets.includes(srs[1].secrets[0])); + strict(srs[1].secrets.includes(srs[0].secrets[0])); }); }); }); diff --git a/src/tests/backend/specs/SessionStore.js b/src/tests/backend/specs/SessionStore.ts similarity index 76% rename from src/tests/backend/specs/SessionStore.js rename to src/tests/backend/specs/SessionStore.ts index dbf79c10d..5dfc44ff2 100644 --- a/src/tests/backend/specs/SessionStore.js +++ b/src/tests/backend/specs/SessionStore.ts @@ -1,19 +1,27 @@ 'use strict'; const SessionStore = require('../../../node/db/SessionStore'); -const assert = require('assert').strict; +import {strict as assert} from 'assert'; const common = require('../common'); const db = require('../../../node/db/DB'); -const util = require('util'); +import util from 'util'; + +type Session = { + set: (sid: string|null,sess:any, sess2:any) => void; + get: (sid:string|null) => any; + destroy: (sid:string|null) => void; + touch: (sid:string|null, sess:any, sess2:any) => void; + shutdown: () => void; +} describe(__filename, function () { - let ss; - let sid; + let ss: Session|null; + let sid: string|null; - const set = async (sess) => await util.promisify(ss.set).call(ss, sid, sess); - const get = async () => await util.promisify(ss.get).call(ss, sid); - const destroy = async () => await util.promisify(ss.destroy).call(ss, sid); - const touch = async (sess) => await util.promisify(ss.touch).call(ss, sid, sess); + const set = async (sess: string|null) => await util.promisify(ss!.set).call(ss, sid, sess); + const get = async () => await util.promisify(ss!.get).call(ss, sid); + const destroy = async () => await util.promisify(ss!.destroy).call(ss, sid); + const touch = async (sess: Session) => await util.promisify(ss!.touch).call(ss, sid, sess); before(async function () { await common.init(); @@ -40,13 +48,13 @@ describe(__filename, function () { }); it('set of non-expiring session', async function () { - const sess = {foo: 'bar', baz: {asdf: 'jkl;'}}; + const sess:any = {foo: 'bar', baz: {asdf: 'jkl;'}}; await set(sess); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); }); it('set of session that expires', async function () { - const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; await set(sess); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); await new Promise((resolve) => setTimeout(resolve, 110)); @@ -55,25 +63,25 @@ describe(__filename, function () { }); it('set of already expired session', async function () { - const sess = {foo: 'bar', cookie: {expires: new Date(1)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(1)}}; await set(sess); // No record should have been created. assert(await db.get(`sessionstorage:${sid}`) == null); }); it('switch from non-expiring to expiring', async function () { - const sess = {foo: 'bar'}; + const sess:any = {foo: 'bar'}; await set(sess); - const sess2 = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; + const sess2:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; await set(sess2); await new Promise((resolve) => setTimeout(resolve, 110)); assert(await db.get(`sessionstorage:${sid}`) == null); }); it('switch from expiring to non-expiring', async function () { - const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; await set(sess); - const sess2 = {foo: 'bar'}; + const sess2:any = {foo: 'bar'}; await set(sess2); await new Promise((resolve) => setTimeout(resolve, 110)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2)); @@ -86,7 +94,7 @@ describe(__filename, function () { }); it('set+get round trip', async function () { - const sess = {foo: 'bar', baz: {asdf: 'jkl;'}}; + const sess:any = {foo: 'bar', baz: {asdf: 'jkl;'}}; await set(sess); assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); }); @@ -114,7 +122,7 @@ describe(__filename, function () { }); it('external expiration update is picked up', async function () { - const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; await set(sess); assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); const sess2 = {...sess, cookie: {expires: new Date(Date.now() + 200)}}; @@ -128,10 +136,10 @@ describe(__filename, function () { describe('shutdown', function () { it('shutdown cancels timeouts', async function () { - const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}}; await set(sess); assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); - ss.shutdown(); + ss!.shutdown(); await new Promise((resolve) => setTimeout(resolve, 110)); // The record should not have been automatically purged. assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); @@ -140,14 +148,14 @@ describe(__filename, function () { describe('destroy', function () { it('destroy deletes the database record', async function () { - const sess = {cookie: {expires: new Date(Date.now() + 100)}}; + const sess:any = {cookie: {expires: new Date(Date.now() + 100)}}; await set(sess); await destroy(); assert(await db.get(`sessionstorage:${sid}`) == null); }); it('destroy cancels the timeout', async function () { - const sess = {cookie: {expires: new Date(Date.now() + 100)}}; + const sess:any = {cookie: {expires: new Date(Date.now() + 100)}}; await set(sess); await destroy(); await db.set(`sessionstorage:${sid}`, sess); @@ -162,16 +170,16 @@ describe(__filename, function () { describe('touch without refresh', function () { it('touch before set is equivalent to set if session expires', async function () { - const sess = {cookie: {expires: new Date(Date.now() + 1000)}}; + const sess:any = {cookie: {expires: new Date(Date.now() + 1000)}}; await touch(sess); assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); }); it('touch updates observed expiration but not database', async function () { const start = Date.now(); - const sess = {cookie: {expires: new Date(start + 200)}}; + const sess:any = {cookie: {expires: new Date(start + 200)}}; await set(sess); - const sess2 = {cookie: {expires: new Date(start + 12000)}}; + const sess2:any = {cookie: {expires: new Date(start + 12000)}}; await touch(sess2); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); assert.equal(JSON.stringify(await get()), JSON.stringify(sess2)); @@ -184,16 +192,16 @@ describe(__filename, function () { }); it('touch before set is equivalent to set if session expires', async function () { - const sess = {cookie: {expires: new Date(Date.now() + 1000)}}; + const sess:any = {cookie: {expires: new Date(Date.now() + 1000)}}; await touch(sess); assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); }); it('touch before eligible for refresh updates expiration but not DB', async function () { const now = Date.now(); - const sess = {foo: 'bar', cookie: {expires: new Date(now + 1000)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(now + 1000)}}; await set(sess); - const sess2 = {foo: 'bar', cookie: {expires: new Date(now + 1001)}}; + const sess2:any = {foo: 'bar', cookie: {expires: new Date(now + 1001)}}; await touch(sess2); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); assert.equal(JSON.stringify(await get()), JSON.stringify(sess2)); @@ -201,10 +209,10 @@ describe(__filename, function () { it('touch before eligible for refresh updates timeout', async function () { const start = Date.now(); - const sess = {foo: 'bar', cookie: {expires: new Date(start + 200)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(start + 200)}}; await set(sess); await new Promise((resolve) => setTimeout(resolve, 100)); - const sess2 = {foo: 'bar', cookie: {expires: new Date(start + 399)}}; + const sess2:any = {foo: 'bar', cookie: {expires: new Date(start + 399)}}; await touch(sess2); await new Promise((resolve) => setTimeout(resolve, 110)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); @@ -213,10 +221,10 @@ describe(__filename, function () { it('touch after eligible for refresh updates db', async function () { const start = Date.now(); - const sess = {foo: 'bar', cookie: {expires: new Date(start + 200)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(start + 200)}}; await set(sess); await new Promise((resolve) => setTimeout(resolve, 100)); - const sess2 = {foo: 'bar', cookie: {expires: new Date(start + 400)}}; + const sess2:any = {foo: 'bar', cookie: {expires: new Date(start + 400)}}; await touch(sess2); await new Promise((resolve) => setTimeout(resolve, 110)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2)); @@ -225,7 +233,7 @@ describe(__filename, function () { it('refresh=0 updates db every time', async function () { ss = new SessionStore(0); - const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 1000)}}; + const sess:any = {foo: 'bar', cookie: {expires: new Date(Date.now() + 1000)}}; await set(sess); await db.remove(`sessionstorage:${sid}`); await touch(sess); // No change in expiration time. diff --git a/src/tests/backend/specs/Stream.js b/src/tests/backend/specs/Stream.ts similarity index 63% rename from src/tests/backend/specs/Stream.js rename to src/tests/backend/specs/Stream.ts index b98a5f3b4..c8a5a3e36 100644 --- a/src/tests/backend/specs/Stream.js +++ b/src/tests/backend/specs/Stream.ts @@ -1,9 +1,12 @@ 'use strict'; const Stream = require('../../../node/utils/Stream'); -const assert = require('assert').strict; +import {strict} from "assert"; class DemoIterable { + private value: number; + errs: Error[]; + rets: any[]; constructor() { this.value = 0; this.errs = []; @@ -17,14 +20,14 @@ class DemoIterable { return {value: this.value++, done: false}; } - throw(err) { + throw(err: any) { const alreadyCompleted = this.completed(); this.errs.push(err); if (alreadyCompleted) throw err; // Mimic standard generator objects. throw err; } - return(ret) { + return(ret: number) { const alreadyCompleted = this.completed(); this.rets.push(ret); if (alreadyCompleted) return {value: ret, done: true}; // Mimic standard generator objects. @@ -34,65 +37,69 @@ class DemoIterable { [Symbol.iterator]() { return this; } } -const assertUnhandledRejection = async (action, want) => { +const assertUnhandledRejection = async (action: any, want: any) => { // Temporarily remove unhandled Promise rejection listeners so that the unhandled rejections we // expect to see don't trigger a test failure (or terminate node). const event = 'unhandledRejection'; const listenersBackup = process.rawListeners(event); process.removeAllListeners(event); - let tempListener; - let asyncErr; + let tempListener: Function; + let asyncErr:any; try { - const seenErrPromise = new Promise((resolve) => { - tempListener = (err) => { - assert.equal(asyncErr, undefined); + const seenErrPromise = new Promise((resolve) => { + tempListener = (err:any) => { + strict.equal(asyncErr, undefined); asyncErr = err; resolve(); }; }); + // @ts-ignore process.on(event, tempListener); await action(); await seenErrPromise; } finally { // Restore the original listeners. + // @ts-ignore process.off(event, tempListener); - for (const listener of listenersBackup) process.on(event, listener); + for (const listener of listenersBackup) { // @ts-ignore + process.on(event, listener); + } } - await assert.rejects(Promise.reject(asyncErr), want); + await strict.rejects(Promise.reject(asyncErr), want); }; describe(__filename, function () { describe('basic behavior', function () { it('takes a generator', async function () { - assert.deepEqual([...new Stream((function* () { yield 0; yield 1; yield 2; })())], [0, 1, 2]); + strict.deepEqual([...new Stream((function* () { yield 0; yield 1; yield 2; })())], [0, 1, 2]); }); it('takes an array', async function () { - assert.deepEqual([...new Stream([0, 1, 2])], [0, 1, 2]); + strict.deepEqual([...new Stream([0, 1, 2])], [0, 1, 2]); }); it('takes an iterator', async function () { - assert.deepEqual([...new Stream([0, 1, 2][Symbol.iterator]())], [0, 1, 2]); + strict.deepEqual([...new Stream([0, 1, 2][Symbol.iterator]())], [0, 1, 2]); }); it('supports empty iterators', async function () { - assert.deepEqual([...new Stream([])], []); + strict.deepEqual([...new Stream([])], []); }); it('is resumable', async function () { const s = new Stream((function* () { yield 0; yield 1; yield 2; })()); let iter = s[Symbol.iterator](); - assert.deepEqual(iter.next(), {value: 0, done: false}); + strict.deepEqual(iter.next(), {value: 0, done: false}); iter = s[Symbol.iterator](); - assert.deepEqual(iter.next(), {value: 1, done: false}); - assert.deepEqual([...s], [2]); + strict.deepEqual(iter.next(), {value: 1, done: false}); + strict.deepEqual([...s], [2]); }); it('supports return value', async function () { const s = new Stream((function* () { yield 0; return 1; })()); const iter = s[Symbol.iterator](); - assert.deepEqual(iter.next(), {value: 0, done: false}); - assert.deepEqual(iter.next(), {value: 1, done: true}); + strict.deepEqual(iter.next(), {value: 0, done: false}); + strict.deepEqual(iter.next(), {value: 1, done: true}); }); it('does not start until needed', async function () { @@ -100,60 +107,60 @@ describe(__filename, function () { new Stream((function* () { yield lastYield = 0; })()); // Fetching from the underlying iterator should not start until the first value is fetched // from the stream. - assert.equal(lastYield, null); + strict.equal(lastYield, null); }); it('throw is propagated', async function () { const underlying = new DemoIterable(); const s = new Stream(underlying); const iter = s[Symbol.iterator](); - assert.deepEqual(iter.next(), {value: 0, done: false}); + strict.deepEqual(iter.next(), {value: 0, done: false}); const err = new Error('injected'); - assert.throws(() => iter.throw(err), err); - assert.equal(underlying.errs[0], err); + strict.throws(() => iter.throw(err), err); + strict.equal(underlying.errs[0], err); }); it('return is propagated', async function () { const underlying = new DemoIterable(); const s = new Stream(underlying); const iter = s[Symbol.iterator](); - assert.deepEqual(iter.next(), {value: 0, done: false}); - assert.deepEqual(iter.return(42), {value: 42, done: true}); - assert.equal(underlying.rets[0], 42); + strict.deepEqual(iter.next(), {value: 0, done: false}); + strict.deepEqual(iter.return(42), {value: 42, done: true}); + strict.equal(underlying.rets[0], 42); }); }); describe('range', function () { it('basic', async function () { - assert.deepEqual([...Stream.range(0, 3)], [0, 1, 2]); + strict.deepEqual([...Stream.range(0, 3)], [0, 1, 2]); }); it('empty', async function () { - assert.deepEqual([...Stream.range(0, 0)], []); + strict.deepEqual([...Stream.range(0, 0)], []); }); it('positive start', async function () { - assert.deepEqual([...Stream.range(3, 5)], [3, 4]); + strict.deepEqual([...Stream.range(3, 5)], [3, 4]); }); it('negative start', async function () { - assert.deepEqual([...Stream.range(-3, 0)], [-3, -2, -1]); + strict.deepEqual([...Stream.range(-3, 0)], [-3, -2, -1]); }); it('end before start', async function () { - assert.deepEqual([...Stream.range(3, 0)], []); + strict.deepEqual([...Stream.range(3, 0)], []); }); }); describe('batch', function () { it('empty', async function () { - assert.deepEqual([...new Stream([]).batch(10)], []); + strict.deepEqual([...new Stream([]).batch(10)], []); }); it('does not start until needed', async function () { let lastYield = null; new Stream((function* () { yield lastYield = 0; })()).batch(10); - assert.equal(lastYield, null); + strict.equal(lastYield, null); }); it('fewer than batch size', async function () { @@ -162,11 +169,11 @@ describe(__filename, function () { for (let i = 0; i < 5; i++) yield lastYield = i; })(); const s = new Stream(values).batch(10); - assert.equal(lastYield, null); - assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false}); - assert.equal(lastYield, 4); - assert.deepEqual([...s], [1, 2, 3, 4]); - assert.equal(lastYield, 4); + strict.equal(lastYield, null); + strict.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false}); + strict.equal(lastYield, 4); + strict.deepEqual([...s], [1, 2, 3, 4]); + strict.equal(lastYield, 4); }); it('exactly batch size', async function () { @@ -175,11 +182,11 @@ describe(__filename, function () { for (let i = 0; i < 5; i++) yield lastYield = i; })(); const s = new Stream(values).batch(5); - assert.equal(lastYield, null); - assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false}); - assert.equal(lastYield, 4); - assert.deepEqual([...s], [1, 2, 3, 4]); - assert.equal(lastYield, 4); + strict.equal(lastYield, null); + strict.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false}); + strict.equal(lastYield, 4); + strict.deepEqual([...s], [1, 2, 3, 4]); + strict.equal(lastYield, 4); }); it('multiple batches, last batch is not full', async function () { @@ -188,17 +195,17 @@ describe(__filename, function () { for (let i = 0; i < 10; i++) yield lastYield = i; })(); const s = new Stream(values).batch(3); - assert.equal(lastYield, null); + strict.equal(lastYield, null); const iter = s[Symbol.iterator](); - assert.deepEqual(iter.next(), {value: 0, done: false}); - assert.equal(lastYield, 2); - assert.deepEqual(iter.next(), {value: 1, done: false}); - assert.deepEqual(iter.next(), {value: 2, done: false}); - assert.equal(lastYield, 2); - assert.deepEqual(iter.next(), {value: 3, done: false}); - assert.equal(lastYield, 5); - assert.deepEqual([...s], [4, 5, 6, 7, 8, 9]); - assert.equal(lastYield, 9); + strict.deepEqual(iter.next(), {value: 0, done: false}); + strict.equal(lastYield, 2); + strict.deepEqual(iter.next(), {value: 1, done: false}); + strict.deepEqual(iter.next(), {value: 2, done: false}); + strict.equal(lastYield, 2); + strict.deepEqual(iter.next(), {value: 3, done: false}); + strict.equal(lastYield, 5); + strict.deepEqual([...s], [4, 5, 6, 7, 8, 9]); + strict.equal(lastYield, 9); }); it('batched Promise rejections are suppressed while iterating', async function () { @@ -215,9 +222,9 @@ describe(__filename, function () { const s = new Stream(values).batch(3); const iter = s[Symbol.iterator](); const nextp = iter.next().value; - assert.equal(lastYield, 'promise of 2'); - assert.equal(await nextp, 0); - await assert.rejects(iter.next().value, err); + strict.equal(lastYield, 'promise of 2'); + strict.equal(await nextp, 0); + await strict.rejects(iter.next().value, err); iter.return(); }); @@ -234,21 +241,21 @@ describe(__filename, function () { })(); const s = new Stream(values).batch(3); const iter = s[Symbol.iterator](); - assert.equal(await iter.next().value, 0); - assert.equal(lastYield, 'promise of 2'); + strict.equal(await iter.next().value, 0); + strict.equal(lastYield, 'promise of 2'); await assertUnhandledRejection(() => iter.return(), err); }); }); describe('buffer', function () { it('empty', async function () { - assert.deepEqual([...new Stream([]).buffer(10)], []); + strict.deepEqual([...new Stream([]).buffer(10)], []); }); it('does not start until needed', async function () { let lastYield = null; new Stream((function* () { yield lastYield = 0; })()).buffer(10); - assert.equal(lastYield, null); + strict.equal(lastYield, null); }); it('fewer than buffer size', async function () { @@ -257,11 +264,11 @@ describe(__filename, function () { for (let i = 0; i < 5; i++) yield lastYield = i; })(); const s = new Stream(values).buffer(10); - assert.equal(lastYield, null); - assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false}); - assert.equal(lastYield, 4); - assert.deepEqual([...s], [1, 2, 3, 4]); - assert.equal(lastYield, 4); + strict.equal(lastYield, null); + strict.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false}); + strict.equal(lastYield, 4); + strict.deepEqual([...s], [1, 2, 3, 4]); + strict.equal(lastYield, 4); }); it('exactly buffer size', async function () { @@ -270,11 +277,11 @@ describe(__filename, function () { for (let i = 0; i < 5; i++) yield lastYield = i; })(); const s = new Stream(values).buffer(5); - assert.equal(lastYield, null); - assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false}); - assert.equal(lastYield, 4); - assert.deepEqual([...s], [1, 2, 3, 4]); - assert.equal(lastYield, 4); + strict.equal(lastYield, null); + strict.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false}); + strict.equal(lastYield, 4); + strict.deepEqual([...s], [1, 2, 3, 4]); + strict.equal(lastYield, 4); }); it('more than buffer size', async function () { @@ -283,16 +290,16 @@ describe(__filename, function () { for (let i = 0; i < 10; i++) yield lastYield = i; })(); const s = new Stream(values).buffer(3); - assert.equal(lastYield, null); + strict.equal(lastYield, null); const iter = s[Symbol.iterator](); - assert.deepEqual(iter.next(), {value: 0, done: false}); - assert.equal(lastYield, 3); - assert.deepEqual(iter.next(), {value: 1, done: false}); - assert.equal(lastYield, 4); - assert.deepEqual(iter.next(), {value: 2, done: false}); - assert.equal(lastYield, 5); - assert.deepEqual([...s], [3, 4, 5, 6, 7, 8, 9]); - assert.equal(lastYield, 9); + strict.deepEqual(iter.next(), {value: 0, done: false}); + strict.equal(lastYield, 3); + strict.deepEqual(iter.next(), {value: 1, done: false}); + strict.equal(lastYield, 4); + strict.deepEqual(iter.next(), {value: 2, done: false}); + strict.equal(lastYield, 5); + strict.deepEqual([...s], [3, 4, 5, 6, 7, 8, 9]); + strict.equal(lastYield, 9); }); it('buffered Promise rejections are suppressed while iterating', async function () { @@ -309,9 +316,9 @@ describe(__filename, function () { const s = new Stream(values).buffer(3); const iter = s[Symbol.iterator](); const nextp = iter.next().value; - assert.equal(lastYield, 'promise of 2'); - assert.equal(await nextp, 0); - await assert.rejects(iter.next().value, err); + strict.equal(lastYield, 'promise of 2'); + strict.equal(await nextp, 0); + await strict.rejects(iter.next().value, err); iter.return(); }); @@ -328,8 +335,8 @@ describe(__filename, function () { })(); const s = new Stream(values).buffer(3); const iter = s[Symbol.iterator](); - assert.equal(await iter.next().value, 0); - assert.equal(lastYield, 'promise of 2'); + strict.equal(await iter.next().value, 0); + strict.equal(lastYield, 'promise of 2'); await assertUnhandledRejection(() => iter.return(), err); }); }); @@ -337,22 +344,22 @@ describe(__filename, function () { describe('map', function () { it('empty', async function () { let called = false; - assert.deepEqual([...new Stream([]).map((v) => called = true)], []); - assert.equal(called, false); + strict.deepEqual([...new Stream([]).map(() => called = true)], []); + strict.equal(called, false); }); it('does not start until needed', async function () { let called = false; - assert.deepEqual([...new Stream([]).map((v) => called = true)], []); - new Stream((function* () { yield 0; })()).map((v) => called = true); - assert.equal(called, false); + strict.deepEqual([...new Stream([]).map(() => called = true)], []); + new Stream((function* () { yield 0; })()).map(() => called = true); + strict.equal(called, false); }); it('works', async function () { - const calls = []; - assert.deepEqual( - [...new Stream([0, 1, 2]).map((v) => { calls.push(v); return 2 * v; })], [0, 2, 4]); - assert.deepEqual(calls, [0, 1, 2]); + const calls:any[] = []; + strict.deepEqual( + [...new Stream([0, 1, 2]).map((v:any) => { calls.push(v); return 2 * v; })], [0, 2, 4]); + strict.deepEqual(calls, [0, 1, 2]); }); }); }); diff --git a/src/tests/backend/specs/api/api.js b/src/tests/backend/specs/api/api.ts similarity index 90% rename from src/tests/backend/specs/api/api.js rename to src/tests/backend/specs/api/api.ts index 1415795b2..32681e5c9 100644 --- a/src/tests/backend/specs/api/api.js +++ b/src/tests/backend/specs/api/api.ts @@ -11,7 +11,7 @@ const common = require('../../common'); const validateOpenAPI = require('openapi-schema-validation').validate; -let agent; +let agent: any; const apiKey = common.apiKey; let apiVersion = 1; @@ -27,7 +27,7 @@ const makeid = () => { const testPadId = makeid(); -const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`; +const endPoint = (point:string) => `/api/${apiVersion}/${point}?apikey=${apiKey}`; describe(__filename, function () { before(async function () { agent = await common.init(); }); @@ -35,7 +35,7 @@ describe(__filename, function () { it('can obtain API version', async function () { await agent.get('/api/') .expect(200) - .expect((res) => { + .expect((res:any) => { apiVersion = res.body.currentVersion; if (!res.body.currentVersion) throw new Error('No version set in API'); return; @@ -46,7 +46,7 @@ describe(__filename, function () { this.timeout(15000); await agent.get('/api/openapi.json') .expect(200) - .expect((res) => { + .expect((res:any) => { const {valid, errors} = validateOpenAPI(res.body, 3); if (!valid) { const prettyErrors = JSON.stringify(errors, null, 2); diff --git a/src/tests/backend/specs/api/characterEncoding.js b/src/tests/backend/specs/api/characterEncoding.ts similarity index 95% rename from src/tests/backend/specs/api/characterEncoding.js rename to src/tests/backend/specs/api/characterEncoding.ts index 2e579136b..7c2202a09 100644 --- a/src/tests/backend/specs/api/characterEncoding.js +++ b/src/tests/backend/specs/api/characterEncoding.ts @@ -11,12 +11,12 @@ const common = require('../../common'); const fs = require('fs'); const fsp = fs.promises; -let agent; +let agent:any; const apiKey = common.apiKey; let apiVersion = 1; const testPadId = makeid(); -const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`; +const endPoint = (point:string, version?:number) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`; describe(__filename, function () { before(async function () { agent = await common.init(); }); diff --git a/src/tests/backend/specs/api/chat.js b/src/tests/backend/specs/api/chat.ts similarity index 91% rename from src/tests/backend/specs/api/chat.js rename to src/tests/backend/specs/api/chat.ts index b86917dab..dc61402bf 100644 --- a/src/tests/backend/specs/api/chat.js +++ b/src/tests/backend/specs/api/chat.ts @@ -1,16 +1,17 @@ 'use strict'; const common = require('../../common'); -const assert = require('assert').strict; -let agent; +import {strict as assert} from "assert"; + +let agent:any; const apiKey = common.apiKey; let apiVersion = 1; let authorID = ''; const padID = makeid(); const timestamp = Date.now(); -const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`; +const endPoint = (point:string) => `/api/${apiVersion}/${point}?apikey=${apiKey}`; describe(__filename, function () { before(async function () { agent = await common.init(); }); @@ -18,7 +19,7 @@ describe(__filename, function () { describe('API Versioning', function () { it('errors if can not connect', async function () { await agent.get('/api/') - .expect((res) => { + .expect((res:any) => { apiVersion = res.body.currentVersion; if (!res.body.currentVersion) throw new Error('No version set in API'); return; @@ -42,7 +43,7 @@ describe(__filename, function () { describe('Chat functionality', function () { it('creates a new Pad', async function () { await agent.get(`${endPoint('createPad')}&padID=${padID}`) - .expect((res) => { + .expect((res:any) => { if (res.body.code !== 0) throw new Error('Unable to create new Pad'); }) .expect('Content-Type', /json/) @@ -51,7 +52,7 @@ describe(__filename, function () { it('Creates an author with a name set', async function () { await agent.get(endPoint('createAuthor')) - .expect((res) => { + .expect((res:any) => { if (res.body.code !== 0 || !res.body.data.authorID) { throw new Error('Unable to create author'); } @@ -63,7 +64,7 @@ describe(__filename, function () { it('Gets the head of chat before the first chat msg', async function () { await agent.get(`${endPoint('getChatHead')}&padID=${padID}`) - .expect((res) => { + .expect((res:any) => { if (res.body.data.chatHead !== -1) throw new Error('Chat Head Length is wrong'); if (res.body.code !== 0) throw new Error('Unable to get chat head'); }) @@ -74,7 +75,7 @@ describe(__filename, function () { it('Adds a chat message to the pad', async function () { await agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` + `&authorID=${authorID}&time=${timestamp}`) - .expect((res) => { + .expect((res:any) => { if (res.body.code !== 0) throw new Error('Unable to create chat message'); }) .expect('Content-Type', /json/) @@ -83,7 +84,7 @@ describe(__filename, function () { it('Gets the head of chat', async function () { await agent.get(`${endPoint('getChatHead')}&padID=${padID}`) - .expect((res) => { + .expect((res:any) => { if (res.body.data.chatHead !== 0) throw new Error('Chat Head Length is wrong'); if (res.body.code !== 0) throw new Error('Unable to get chat head'); @@ -96,7 +97,7 @@ describe(__filename, function () { await agent.get(`${endPoint('getChatHistory')}&padID=${padID}`) .expect('Content-Type', /json/) .expect(200) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0, 'Unable to get chat history'); assert.equal(res.body.data.messages.length, 1, 'Chat History Length is wrong'); assert.equal(res.body.data.messages[0].text, 'blalblalbha', 'Chat text does not match'); diff --git a/src/tests/backend/specs/api/fuzzImportTest.js b/src/tests/backend/specs/api/fuzzImportTest.ts similarity index 100% rename from src/tests/backend/specs/api/fuzzImportTest.js rename to src/tests/backend/specs/api/fuzzImportTest.ts diff --git a/src/tests/backend/specs/api/importexport.js b/src/tests/backend/specs/api/importexport.ts similarity index 98% rename from src/tests/backend/specs/api/importexport.js rename to src/tests/backend/specs/api/importexport.ts index ceced447f..a1ef64d87 100644 --- a/src/tests/backend/specs/api/importexport.js +++ b/src/tests/backend/specs/api/importexport.ts @@ -6,16 +6,17 @@ * TODO: unify those two files, and merge in a single one. */ -const assert = require('assert').strict; +import { strict as assert } from 'assert'; +import {MapArrayType} from "../../../../node/types/MapType"; const common = require('../../common'); -let agent; +let agent:any; const apiKey = common.apiKey; const apiVersion = 1; -const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`; +const endPoint = (point: string, version?:string) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`; -const testImports = { +const testImports:MapArrayType = { 'malformed': { input: '
  • wtf', wantHTML: 'wtf

    ', diff --git a/src/tests/backend/specs/api/importexportGetPost.js b/src/tests/backend/specs/api/importexportGetPost.ts similarity index 89% rename from src/tests/backend/specs/api/importexportGetPost.js rename to src/tests/backend/specs/api/importexportGetPost.ts index e69f0d120..40bfb5552 100644 --- a/src/tests/backend/specs/api/importexportGetPost.js +++ b/src/tests/backend/specs/api/importexportGetPost.ts @@ -4,6 +4,8 @@ * Import and Export tests for the /p/whateverPadId/import and /p/whateverPadId/export endpoints. */ +import {MapArrayType} from "../../../../node/types/MapType"; + const assert = require('assert').strict; const common = require('../../common'); const fs = require('fs'); @@ -19,7 +21,7 @@ const wordXDoc = fs.readFileSync(`${__dirname}/test.docx`); const odtDoc = fs.readFileSync(`${__dirname}/test.odt`); const pdfDoc = fs.readFileSync(`${__dirname}/test.pdf`); -let agent; +let agent:any; const apiKey = common.apiKey; const apiVersion = 1; const testPadId = makeid(); @@ -48,7 +50,7 @@ describe(__filename, function () { it('finds the version tag', async function () { await agent.get('/api/') .expect(200) - .expect((res) => assert(res.body.currentVersion)); + .expect((res:any) => assert(res.body.currentVersion)); }); }); @@ -79,7 +81,7 @@ describe(__filename, function () { */ describe('Imports and Exports', function () { - const backups = {}; + const backups:MapArrayType = {}; beforeEach(async function () { backups.hooks = {}; @@ -104,17 +106,17 @@ describe(__filename, function () { await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => assert.equal(res.body.code, 0)); + .expect((res:any) => assert.equal(res.body.code, 0)); await agent.post(`/p/${testPadId}/import`) .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) .expect(200); await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) - .expect((res) => assert.equal(res.body.data.text, padText.toString())); + .expect((res:any) => assert.equal(res.body.data.text, padText.toString())); }); describe('export from read-only pad ID', function () { - let readOnlyId; + let readOnlyId:string; // This ought to be before(), but it must run after the top-level beforeEach() above. beforeEach(async function () { @@ -125,7 +127,7 @@ describe(__filename, function () { const res = await agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => assert.equal(res.body.code, 0)); + .expect((res:any) => assert.equal(res.body.code, 0)); readOnlyId = res.body.data.readOnlyID; }); @@ -138,7 +140,7 @@ describe(__filename, function () { for (const exportType of ['html', 'txt', 'etherpad']) { describe(`export to ${exportType}`, function () { - let text; + let text:string; // This ought to be before(), but it must run after the top-level beforeEach() above. beforeEach(async function () { @@ -201,7 +203,7 @@ describe(__filename, function () { .attach('file', wordDoc, {filename: '/test.doc', contentType: 'application/msword'}) .expect(200) .expect('Content-Type', /json/) - .expect((res) => assert.deepEqual(res.body, { + .expect((res:any) => assert.deepEqual(res.body, { code: 0, message: 'ok', data: {directDatabaseAccess: false}, @@ -212,7 +214,7 @@ describe(__filename, function () { await agent.get(`/p/${testPadId}/export/doc`) .buffer(true).parse(superagent.parse['application/octet-stream']) .expect(200) - .expect((res) => assert(res.body.length >= 9000)); + .expect((res:any) => assert(res.body.length >= 9000)); }); it('Tries to import .docx that uses soffice or abiword', async function () { @@ -224,7 +226,7 @@ describe(__filename, function () { }) .expect(200) .expect('Content-Type', /json/) - .expect((res) => assert.deepEqual(res.body, { + .expect((res:any) => assert.deepEqual(res.body, { code: 0, message: 'ok', data: {directDatabaseAccess: false}, @@ -235,7 +237,7 @@ describe(__filename, function () { await agent.get(`/p/${testPadId}/export/doc`) .buffer(true).parse(superagent.parse['application/octet-stream']) .expect(200) - .expect((res) => assert(res.body.length >= 9100)); + .expect((res:any) => assert(res.body.length >= 9100)); }); it('Tries to import .pdf that uses soffice or abiword', async function () { @@ -243,7 +245,7 @@ describe(__filename, function () { .attach('file', pdfDoc, {filename: '/test.pdf', contentType: 'application/pdf'}) .expect(200) .expect('Content-Type', /json/) - .expect((res) => assert.deepEqual(res.body, { + .expect((res:any) => assert.deepEqual(res.body, { code: 0, message: 'ok', data: {directDatabaseAccess: false}, @@ -254,7 +256,7 @@ describe(__filename, function () { await agent.get(`/p/${testPadId}/export/pdf`) .buffer(true).parse(superagent.parse['application/octet-stream']) .expect(200) - .expect((res) => assert(res.body.length >= 1000)); + .expect((res:any) => assert(res.body.length >= 1000)); }); it('Tries to import .odt that uses soffice or abiword', async function () { @@ -262,7 +264,7 @@ describe(__filename, function () { .attach('file', odtDoc, {filename: '/test.odt', contentType: 'application/odt'}) .expect(200) .expect('Content-Type', /json/) - .expect((res) => assert.deepEqual(res.body, { + .expect((res:any) => assert.deepEqual(res.body, { code: 0, message: 'ok', data: {directDatabaseAccess: false}, @@ -273,7 +275,7 @@ describe(__filename, function () { await agent.get(`/p/${testPadId}/export/odt`) .buffer(true).parse(superagent.parse['application/octet-stream']) .expect(200) - .expect((res) => assert(res.body.length >= 7000)); + .expect((res:any) => assert(res.body.length >= 7000)); }); }); // End of AbiWord/LibreOffice tests. @@ -286,7 +288,7 @@ describe(__filename, function () { }) .expect(200) .expect('Content-Type', /json/) - .expect((res) => assert.deepEqual(res.body, { + .expect((res:any) => assert.deepEqual(res.body, { code: 0, message: 'ok', data: {directDatabaseAccess: true}, @@ -316,7 +318,7 @@ describe(__filename, function () { .attach('file', padText, {filename: '/test.xasdasdxx', contentType: 'weirdness/jobby'}) .expect(400) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 1); assert.equal(res.body.message, 'uploadFailed'); }); @@ -367,7 +369,7 @@ describe(__filename, function () { }, }); - const importEtherpad = (records) => agent.post(`/p/${testPadId}/import`) + const importEtherpad = (records:any) => agent.post(`/p/${testPadId}/import`) .attach('file', Buffer.from(JSON.stringify(records), 'utf8'), { filename: '/test.etherpad', contentType: 'application/etherpad', @@ -381,7 +383,7 @@ describe(__filename, function () { await importEtherpad(records) .expect(200) .expect('Content-Type', /json/) - .expect((res) => assert.deepEqual(res.body, { + .expect((res:any) => assert.deepEqual(res.body, { code: 0, message: 'ok', data: {directDatabaseAccess: true}, @@ -389,11 +391,11 @@ describe(__filename, function () { await agent.get(`/p/${testPadId}/export/txt`) .expect(200) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.match(res.text, /foo/)); + .expect((res:any) => assert.match(res.text, /foo/)); }); it('missing rev', async function () { - const records = makeGoodExport(); + const records:MapArrayType = makeGoodExport(); delete records['pad:testing:revs:0']; await importEtherpad(records).expect(500); }); @@ -413,12 +415,13 @@ describe(__filename, function () { it('extra attrib in pool', async function () { const records = makeGoodExport(); const pool = records['pad:testing'].pool; + // @ts-ignore pool.numToAttrib[pool.nextNum] = ['key', 'value']; await importEtherpad(records).expect(500); }); it('changeset refers to non-existent attrib', async function () { - const records = makeGoodExport(); + const records:MapArrayType = makeGoodExport(); records['pad:testing:revs:1'] = { changeset: 'Z:4>4*1+4$asdf', meta: { @@ -441,7 +444,7 @@ describe(__filename, function () { }); it('missing chat message', async function () { - const records = makeGoodExport(); + const records:MapArrayType = makeGoodExport(); delete records['pad:testing:chat:0']; await importEtherpad(records).expect(500); }); @@ -520,7 +523,7 @@ describe(__filename, function () { }, }); - const importEtherpad = (records) => agent.post(`/p/${testPadId}/import`) + const importEtherpad = (records:MapArrayType) => agent.post(`/p/${testPadId}/import`) .attach('file', Buffer.from(JSON.stringify(records), 'utf8'), { filename: '/test.etherpad', contentType: 'application/etherpad', @@ -534,7 +537,7 @@ describe(__filename, function () { await importEtherpad(records) .expect(200) .expect('Content-Type', /json/) - .expect((res) => assert.deepEqual(res.body, { + .expect((res:any) => assert.deepEqual(res.body, { code: 0, message: 'ok', data: {directDatabaseAccess: true}, @@ -542,84 +545,84 @@ describe(__filename, function () { await agent.get(`/p/${testPadId}/export/txt`) .expect(200) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.equal(res.text, 'oofoo\n')); + .expect((res:any) => assert.equal(res.text, 'oofoo\n')); }); it('txt request rev 1', async function () { await agent.get(`/p/${testPadId}/1/export/txt`) .expect(200) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.equal(res.text, 'ofoo\n')); + .expect((res:any) => assert.equal(res.text, 'ofoo\n')); }); it('txt request rev 2', async function () { await agent.get(`/p/${testPadId}/2/export/txt`) .expect(200) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.equal(res.text, 'oofoo\n')); + .expect((res:any) => assert.equal(res.text, 'oofoo\n')); }); it('txt request rev 1test returns rev 1', async function () { await agent.get(`/p/${testPadId}/1test/export/txt`) .expect(200) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.equal(res.text, 'ofoo\n')); + .expect((res:any) => assert.equal(res.text, 'ofoo\n')); }); it('txt request rev test1 is 403', async function () { await agent.get(`/p/${testPadId}/test1/export/txt`) .expect(500) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.match(res.text, /rev is not a number/)); + .expect((res:any) => assert.match(res.text, /rev is not a number/)); }); it('txt request rev 5 returns head rev', async function () { await agent.get(`/p/${testPadId}/5/export/txt`) .expect(200) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.equal(res.text, 'oofoo\n')); + .expect((res:any) => assert.equal(res.text, 'oofoo\n')); }); it('html request rev 1', async function () { await agent.get(`/p/${testPadId}/1/export/html`) .expect(200) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.match(res.text, /ofoo
    /)); + .expect((res:any) => assert.match(res.text, /ofoo
    /)); }); it('html request rev 2', async function () { await agent.get(`/p/${testPadId}/2/export/html`) .expect(200) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.match(res.text, /oofoo
    /)); + .expect((res:any) => assert.match(res.text, /oofoo
    /)); }); it('html request rev 1test returns rev 1', async function () { await agent.get(`/p/${testPadId}/1test/export/html`) .expect(200) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.match(res.text, /ofoo
    /)); + .expect((res:any) => assert.match(res.text, /ofoo
    /)); }); it('html request rev test1 results in 500 response', async function () { await agent.get(`/p/${testPadId}/test1/export/html`) .expect(500) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.match(res.text, /rev is not a number/)); + .expect((res:any) => assert.match(res.text, /rev is not a number/)); }); it('html request rev 5 returns head rev', async function () { await agent.get(`/p/${testPadId}/5/export/html`) .expect(200) .buffer(true).parse(superagent.parse.text) - .expect((res) => assert.match(res.text, /oofoo
    /)); + .expect((res:any) => assert.match(res.text, /oofoo
    /)); }); }); describe('Import authorization checks', function () { - let authorize; + let authorize: (arg0: any) => any; - const createTestPad = async (text) => { + const createTestPad = async (text:string) => { const pad = await padManager.getPad(testPadId); if (text) await pad.setText(text); return pad; @@ -631,7 +634,7 @@ describe(__filename, function () { await deleteTestPad(); settings.requireAuthorization = true; authorize = () => true; - plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => cb([authorize(req)])}]; + plugins.hooks.authorize = [{hook_fn: (hookName: string, {req}:any, cb:Function) => cb([authorize(req)])}]; }); afterEach(async function () { @@ -740,9 +743,8 @@ describe(__filename, function () { }); // End of tests. -const endPoint = (point, version) => { - version = version || apiVersion; - return `/api/${version}/${point}?apikey=${apiKey}`; +const endPoint = (point: string, version?:string) => { + return `/api/${version || apiVersion}/${point}?apikey=${apiKey}`; }; function makeid() { diff --git a/src/tests/backend/specs/api/instance.js b/src/tests/backend/specs/api/instance.ts similarity index 91% rename from src/tests/backend/specs/api/instance.js rename to src/tests/backend/specs/api/instance.ts index 62202263e..fc348e5af 100644 --- a/src/tests/backend/specs/api/instance.js +++ b/src/tests/backend/specs/api/instance.ts @@ -7,11 +7,11 @@ */ const common = require('../../common'); -let agent; +let agent:any; const apiKey = common.apiKey; const apiVersion = '1.2.14'; -const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`; +const endPoint = (point: string, version?: number) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`; describe(__filename, function () { before(async function () { agent = await common.init(); }); @@ -27,7 +27,7 @@ describe(__filename, function () { describe('getStats', function () { it('Gets the stats of a running instance', async function () { await agent.get(endPoint('getStats')) - .expect((res) => { + .expect((res:any) => { if (res.body.code !== 0) throw new Error('getStats() failed'); if (!('totalPads' in res.body.data && typeof res.body.data.totalPads === 'number')) { diff --git a/src/tests/backend/specs/api/pad.js b/src/tests/backend/specs/api/pad.ts similarity index 97% rename from src/tests/backend/specs/api/pad.js rename to src/tests/backend/specs/api/pad.ts index b8250741e..180494bb2 100644 --- a/src/tests/backend/specs/api/pad.js +++ b/src/tests/backend/specs/api/pad.ts @@ -11,7 +11,7 @@ const assert = require('assert').strict; const common = require('../../common'); const padManager = require('../../../../node/db/PadManager'); -let agent; +let agent:any; const apiKey = common.apiKey; let apiVersion = 1; const testPadId = makeid(); @@ -21,7 +21,7 @@ const anotherPadId = makeid(); let lastEdited = ''; const text = generateLongText(); -const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`; +const endPoint = (point: string, version?: string) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`; /* * Html document with nested lists of different types, to test its import and @@ -508,13 +508,13 @@ describe(__filename, function () { await agent.get(`${endPoint('createPad')}&padID=${anotherPadId}&text=`) .expect('Content-Type', /json/) .expect(200) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0, 'Unable to create new Pad'); }); await agent.get(`${endPoint('getText')}&padID=${anotherPadId}`) .expect('Content-Type', /json/) .expect(200) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0, 'Unable to get pad text'); assert.equal(res.body.data.text, '\n', 'Pad text is not empty'); }); @@ -524,7 +524,7 @@ describe(__filename, function () { await agent.get(`${endPoint('deletePad')}&padID=${anotherPadId}`) .expect('Content-Type', /json/) .expect(200) - .expect((res) => { + .expect((res: any) => { assert.equal(res.body.code, 0, 'Unable to delete empty Pad'); }); }); @@ -532,7 +532,7 @@ describe(__filename, function () { describe('copyPadWithoutHistory', function () { const sourcePadId = makeid(); - let newPad; + let newPad:string; before(async function () { await createNewPadWithHtml(sourcePadId, ulHtml); @@ -612,7 +612,7 @@ describe(__filename, function () { // If appears in the source pad, or appears in the destination pad, then shared // state between the two attribute pools caused corruption. - const getHtml = async (padId) => { + const getHtml = async (padId:string) => { const res = await agent.get(`${endPoint('getHTML')}&padID=${padId}`) .expect(200) .expect('Content-Type', /json/); @@ -620,12 +620,12 @@ describe(__filename, function () { return res.body.data.html; }; - const setBody = async (padId, bodyHtml) => { + const setBody = async (padId: string, bodyHtml: string) => { await agent.post(endPoint('setHTML')) .send({padID: padId, html: `${bodyHtml}`}) .expect(200) .expect('Content-Type', /json/) - .expect((res) => assert.equal(res.body.code, 0)); + .expect((res: any) => assert.equal(res.body.code, 0)); }; const origHtml = await getHtml(sourcePadId); @@ -635,7 +635,7 @@ describe(__filename, function () { `&destinationID=${newPad}&force=false`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => assert.equal(res.body.code, 0)); + .expect((res:any) => assert.equal(res.body.code, 0)); const newBodySrc = 'bold'; const newBodyDst = 'italic'; @@ -650,7 +650,7 @@ describe(__filename, function () { // Force the server to re-read the pads from the database. This rebuilds the attribute pool // objects from scratch, ensuring that an internally inconsistent attribute pool object did // not cause the above tests to accidentally pass. - const reInitPad = async (padId) => { + const reInitPad = async (padId:string) => { const pad = await padManager.getPad(padId); await pad.init(); }; @@ -671,7 +671,7 @@ describe(__filename, function () { */ -const createNewPadWithHtml = async (padId, html) => { +const createNewPadWithHtml = async (padId: string, html: string) => { await agent.get(`${endPoint('createPad')}&padID=${padId}`); await agent.post(endPoint('setHTML')) .send({ diff --git a/src/tests/backend/specs/api/restoreRevision.js b/src/tests/backend/specs/api/restoreRevision.ts similarity index 91% rename from src/tests/backend/specs/api/restoreRevision.js rename to src/tests/backend/specs/api/restoreRevision.ts index 98709ab9b..28a509012 100644 --- a/src/tests/backend/specs/api/restoreRevision.js +++ b/src/tests/backend/specs/api/restoreRevision.ts @@ -1,17 +1,19 @@ 'use strict'; +import {PadType} from "../../../../node/types/PadType"; + const assert = require('assert').strict; const authorManager = require('../../../../node/db/AuthorManager'); const common = require('../../common'); const padManager = require('../../../../node/db/PadManager'); describe(__filename, function () { - let agent; - let authorId; - let padId; - let pad; + let agent:any; + let authorId: string; + let padId: string; + let pad: PadType; - const restoreRevision = async (v, padId, rev, authorId = null) => { + const restoreRevision = async (v:string, padId: string, rev: number, authorId:string|null = null) => { const p = new URLSearchParams(Object.entries({ apikey: common.apiKey, padID: padId, diff --git a/src/tests/backend/specs/api/sessionsAndGroups.js b/src/tests/backend/specs/api/sessionsAndGroups.ts similarity index 90% rename from src/tests/backend/specs/api/sessionsAndGroups.js rename to src/tests/backend/specs/api/sessionsAndGroups.ts index eb181f01f..1c3196214 100644 --- a/src/tests/backend/specs/api/sessionsAndGroups.js +++ b/src/tests/backend/specs/api/sessionsAndGroups.ts @@ -4,7 +4,7 @@ const assert = require('assert').strict; const common = require('../../common'); const db = require('../../../../node/db/DB'); -let agent; +let agent:any; const apiKey = common.apiKey; let apiVersion = 1; let groupID = ''; @@ -12,7 +12,7 @@ let authorID = ''; let sessionID = ''; let padID = makeid(); -const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`; +const endPoint = (point:string) => `/api/${apiVersion}/${point}?apikey=${apiKey}`; describe(__filename, function () { before(async function () { agent = await common.init(); }); @@ -21,7 +21,7 @@ describe(__filename, function () { it('errors if can not connect', async function () { await agent.get('/api/') .expect(200) - .expect((res) => { + .expect((res:any) => { assert(res.body.currentVersion); apiVersion = res.body.currentVersion; }); @@ -63,7 +63,7 @@ describe(__filename, function () { await agent.get(endPoint('createGroup')) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert(res.body.data.groupID); groupID = res.body.data.groupID; @@ -74,7 +74,7 @@ describe(__filename, function () { await agent.get(`${endPoint('listSessionsOfGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert.equal(res.body.data, null); }); @@ -84,18 +84,18 @@ describe(__filename, function () { await agent.get(`${endPoint('deleteGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); }); }); it('createGroupIfNotExistsFor', async function () { const mapper = makeid(); - let groupId; + let groupId: string; await agent.get(`${endPoint('createGroupIfNotExistsFor')}&groupMapper=${mapper}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); groupId = res.body.data.groupID; assert(groupId); @@ -104,16 +104,16 @@ describe(__filename, function () { await agent.get(`${endPoint('createGroupIfNotExistsFor')}&groupMapper=${mapper}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert.equal(res.body.data.groupID, groupId); }); // Deleting the group should clean up the mapping. - assert.equal(await db.get(`mapper2group:${mapper}`), groupId); - await agent.get(`${endPoint('deleteGroup')}&groupID=${groupId}`) + assert.equal(await db.get(`mapper2group:${mapper}`), groupId!); + await agent.get(`${endPoint('deleteGroup')}&groupID=${groupId!}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); }); assert(await db.get(`mapper2group:${mapper}`) == null); @@ -125,7 +125,7 @@ describe(__filename, function () { await agent.get(endPoint('createGroup')) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert(res.body.data.groupID); groupID = res.body.data.groupID; @@ -136,7 +136,7 @@ describe(__filename, function () { await agent.get(endPoint('createAuthor')) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert(res.body.data.authorID); authorID = res.body.data.authorID; @@ -148,7 +148,7 @@ describe(__filename, function () { '&validUntil=999999999999') .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert(res.body.data.sessionID); sessionID = res.body.data.sessionID; @@ -160,7 +160,7 @@ describe(__filename, function () { '&validUntil=999999999999') .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert(res.body.data.sessionID); sessionID = res.body.data.sessionID; @@ -171,7 +171,7 @@ describe(__filename, function () { await agent.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=x1234567`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); }); }); @@ -180,7 +180,7 @@ describe(__filename, function () { await agent.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=x12345678`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); }); }); @@ -189,7 +189,7 @@ describe(__filename, function () { await agent.get(`${endPoint('deleteGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); }); }); @@ -201,7 +201,7 @@ describe(__filename, function () { await agent.get(endPoint('createGroup')) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert(res.body.data.groupID); groupID = res.body.data.groupID; @@ -212,7 +212,7 @@ describe(__filename, function () { await agent.get(endPoint('createAuthor')) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert(res.body.data.authorID); }); @@ -222,7 +222,7 @@ describe(__filename, function () { await agent.get(`${endPoint('createAuthor')}&name=john`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert(res.body.data.authorID); authorID = res.body.data.authorID; // we will be this author for the rest of the tests @@ -233,7 +233,7 @@ describe(__filename, function () { await agent.get(`${endPoint('createAuthorIfNotExistsFor')}&authorMapper=chris`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert(res.body.data.authorID); }); @@ -243,7 +243,7 @@ describe(__filename, function () { await agent.get(`${endPoint('getAuthorName')}&authorID=${authorID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert.equal(res.body.data, 'john'); }); @@ -256,7 +256,7 @@ describe(__filename, function () { '&validUntil=999999999999') .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert(res.body.data.sessionID); sessionID = res.body.data.sessionID; @@ -267,7 +267,7 @@ describe(__filename, function () { await agent.get(`${endPoint('getSessionInfo')}&sessionID=${sessionID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert(res.body.data.groupID); assert(res.body.data.authorID); @@ -279,7 +279,7 @@ describe(__filename, function () { await agent.get(`${endPoint('listSessionsOfGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert.equal(typeof res.body.data, 'object'); }); @@ -289,7 +289,7 @@ describe(__filename, function () { await agent.get(`${endPoint('deleteSession')}&sessionID=${sessionID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); }); }); @@ -298,7 +298,7 @@ describe(__filename, function () { await agent.get(`${endPoint('getSessionInfo')}&sessionID=${sessionID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 1); }); }); @@ -309,7 +309,7 @@ describe(__filename, function () { await agent.get(`${endPoint('listPads')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert.equal(res.body.data.padIDs.length, 0); }); @@ -319,7 +319,7 @@ describe(__filename, function () { await agent.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=${padID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); padID = res.body.data.padID; }); @@ -329,7 +329,7 @@ describe(__filename, function () { await agent.get(`${endPoint('listPads')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert.equal(res.body.data.padIDs.length, 1); }); @@ -341,7 +341,7 @@ describe(__filename, function () { await agent.get(`${endPoint('getPublicStatus')}&padID=${padID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert.equal(res.body.data.publicStatus, false); }); @@ -351,7 +351,7 @@ describe(__filename, function () { await agent.get(`${endPoint('setPublicStatus')}&padID=${padID}&publicStatus=true`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); }); }); @@ -360,7 +360,7 @@ describe(__filename, function () { await agent.get(`${endPoint('getPublicStatus')}&padID=${padID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert.equal(res.body.data.publicStatus, true); }); @@ -376,7 +376,7 @@ describe(__filename, function () { await agent.get(`${endPoint('listPadsOfAuthor')}&authorID=${authorID}`) .expect(200) .expect('Content-Type', /json/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.body.code, 0); assert.equal(res.body.data.padIDs.length, 0); }); diff --git a/src/tests/backend/specs/caching_middleware.js b/src/tests/backend/specs/caching_middleware.ts similarity index 81% rename from src/tests/backend/specs/caching_middleware.js rename to src/tests/backend/specs/caching_middleware.ts index ebfd65d9f..3051ee1e7 100644 --- a/src/tests/backend/specs/caching_middleware.js +++ b/src/tests/backend/specs/caching_middleware.ts @@ -1,5 +1,7 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; + /** * caching_middleware is responsible for serving everything under path `/javascripts/` * That includes packages as defined in `src/node/utils/tar.json` and probably also plugin code @@ -7,11 +9,12 @@ */ const common = require('../common'); -const assert = require('../assert-legacy').strict; -const queryString = require('querystring'); +import {strict as assert} from 'assert'; +import queryString from 'querystring'; const settings = require('../../../node/utils/Settings'); +import {it, describe} from 'mocha' -let agent; +let agent: any; /** * Hack! Returns true if the resource is not plaintext @@ -19,30 +22,35 @@ let agent; * URL. * * @param {string} fileContent the response body - * @param {URI} resource resource URI + * @param {URL} resource resource URI * @returns {boolean} if it is plaintext */ -const isPlaintextResponse = (fileContent, resource) => { +const isPlaintextResponse = (fileContent: string, resource:string): boolean => { // callback=require.define&v=1234 const query = (new URL(resource, 'http://localhost')).search.slice(1); // require.define const jsonp = queryString.parse(query).callback; // returns true if the first letters in fileContent equal the content of `jsonp` - return fileContent.substring(0, jsonp.length) === jsonp; + return fileContent.substring(0, jsonp!.length) === jsonp; }; + +type RequestType = { + _shouldUnzip: () => boolean; +} + /** * A hack to disable `superagent`'s auto unzip functionality * * @param {Request} request */ -const disableAutoDeflate = (request) => { +const disableAutoDeflate = (request: RequestType) => { request._shouldUnzip = () => false; }; describe(__filename, function () { - const backups = {}; + const backups:MapArrayType = {}; const fantasyEncoding = 'brainwaves'; // non-working encoding until https://github.com/visionmedia/superagent/pull/1560 is resolved const packages = [ '/javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define', @@ -74,7 +82,7 @@ describe(__filename, function () { .use(disableAutoDeflate) .expect(200) .expect('Content-Type', /application\/javascript/) - .expect((res) => { + .expect((res:any) => { assert.equal(res.header['content-encoding'], undefined); assert(isPlaintextResponse(res.text, resource)); }); @@ -91,7 +99,7 @@ describe(__filename, function () { .expect(200) .expect('Content-Type', /application\/javascript/) .expect('Content-Encoding', 'gzip') - .expect((res) => { + .expect((res:any) => { assert(!isPlaintextResponse(res.text, resource)); }); }); @@ -102,7 +110,7 @@ describe(__filename, function () { await agent.get(packages[0]) .set('Accept-Encoding', fantasyEncoding) .expect(200) - .expect((res) => assert.equal(res.header['content-encoding'], undefined)); + .expect((res:any) => assert.equal(res.header['content-encoding'], undefined)); await agent.get(packages[0]) .set('Accept-Encoding', 'gzip') .expect(200) @@ -110,7 +118,7 @@ describe(__filename, function () { await agent.get(packages[0]) .set('Accept-Encoding', fantasyEncoding) .expect(200) - .expect((res) => assert.equal(res.header['content-encoding'], undefined)); + .expect((res:any) => assert.equal(res.header['content-encoding'], undefined)); }); }); } diff --git a/src/tests/backend/specs/chat.js b/src/tests/backend/specs/chat.ts similarity index 75% rename from src/tests/backend/specs/chat.js rename to src/tests/backend/specs/chat.ts index 88fda9d07..5070a30a1 100644 --- a/src/tests/backend/specs/chat.js +++ b/src/tests/backend/specs/chat.ts @@ -1,5 +1,8 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; +import {PluginDef} from "../../../node/types/PartType"; + const ChatMessage = require('../../../static/js/ChatMessage'); const {Pad} = require('../../../node/db/Pad'); const assert = require('assert').strict; @@ -9,16 +12,23 @@ const pluginDefs = require('../../../static/js/pluginfw/plugin_defs'); const logger = common.logger; -const checkHook = async (hookName, checkFn) => { +type CheckFN = ({message, pad, padId}:{ + message?: typeof ChatMessage, + pad?: typeof Pad, + padId?: string, +})=>void; + +const checkHook = async (hookName: string, checkFn?:CheckFN) => { if (pluginDefs.hooks[hookName] == null) pluginDefs.hooks[hookName] = []; - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { pluginDefs.hooks[hookName].push({ - hook_fn: async (hookName, context) => { + hook_fn: async (hookName: string, context:any) => { if (checkFn == null) return; logger.debug(`hook ${hookName} invoked`); try { // Make sure checkFn is called only once. const _checkFn = checkFn; + // @ts-ignore checkFn = null; await _checkFn(context); } catch (err) { @@ -31,7 +41,7 @@ const checkHook = async (hookName, checkFn) => { }); }; -const sendMessage = (socket, data) => { +const sendMessage = (socket: any, data:any) => { socket.emit('message', { type: 'COLLABROOM', component: 'pad', @@ -39,16 +49,19 @@ const sendMessage = (socket, data) => { }); }; -const sendChat = (socket, message) => sendMessage(socket, {type: 'CHAT_MESSAGE', message}); +const sendChat = (socket:any, message:{ + text: string, + +}) => sendMessage(socket, {type: 'CHAT_MESSAGE', message}); describe(__filename, function () { const padId = 'testChatPad'; - const hooksBackup = {}; + const hooksBackup:MapArrayType = {}; before(async function () { for (const [name, defs] of Object.entries(pluginDefs.hooks)) { if (defs == null) continue; - hooksBackup[name] = defs; + hooksBackup[name] = defs as PluginDef[]; } }); @@ -71,8 +84,8 @@ describe(__filename, function () { }); describe('chatNewMessage hook', function () { - let authorId; - let socket; + let authorId: string; + let socket: any; beforeEach(async function () { socket = await common.connect(); @@ -91,11 +104,11 @@ describe(__filename, function () { assert(message != null); assert(message instanceof ChatMessage); assert.equal(message.authorId, authorId); - assert.equal(message.text, this.test.title); + assert.equal(message.text, this.test!.title); assert(message.time >= start); assert(message.time <= Date.now()); }), - sendChat(socket, {text: this.test.title}), + sendChat(socket, {text: this.test!.title}), ]); }); @@ -106,7 +119,7 @@ describe(__filename, function () { assert(pad instanceof Pad); assert.equal(pad.id, padId); }), - sendChat(socket, {text: this.test.title}), + sendChat(socket, {text: this.test!.title}), ]); }); @@ -115,13 +128,19 @@ describe(__filename, function () { checkHook('chatNewMessage', (context) => { assert.equal(context.padId, padId); }), - sendChat(socket, {text: this.test.title}), + sendChat(socket, {text: this.test!.title}), ]); }); it('mutations propagate', async function () { - const listen = async (type) => await new Promise((resolve) => { - const handler = (msg) => { + + type Message = { + type: string, + data: any, + } + + const listen = async (type: string) => await new Promise((resolve) => { + const handler = (msg:Message) => { if (msg.type !== 'COLLABROOM') return; if (msg.data == null || msg.data.type !== type) return; resolve(msg.data); @@ -130,8 +149,8 @@ describe(__filename, function () { socket.on('message', handler); }); - const modifiedText = `${this.test.title} `; - const customMetadata = {foo: this.test.title}; + const modifiedText = `${this.test!.title} `; + const customMetadata = {foo: this.test!.title}; await Promise.all([ checkHook('chatNewMessage', ({message}) => { message.text = modifiedText; @@ -143,7 +162,7 @@ describe(__filename, function () { assert.equal(message.text, modifiedText); assert.deepEqual(message.customMetadata, customMetadata); })(), - sendChat(socket, {text: this.test.title}), + sendChat(socket, {text: this.test!.title}), ]); // Simulate fetch of historical chat messages when a pad is first loaded. await Promise.all([ diff --git a/src/tests/backend/specs/contentcollector.js b/src/tests/backend/specs/contentcollector.ts similarity index 97% rename from src/tests/backend/specs/contentcollector.js rename to src/tests/backend/specs/contentcollector.ts index a4696307e..51ae0002f 100644 --- a/src/tests/backend/specs/contentcollector.js +++ b/src/tests/backend/specs/contentcollector.ts @@ -9,6 +9,8 @@ * If you add tests here, please also add them to importexport.js */ +import {APool} from "../../../node/types/PadType"; + const AttributePool = require('../../../static/js/AttributePool'); const Changeset = require('../../../static/js/Changeset'); const assert = require('assert').strict; @@ -334,8 +336,11 @@ pre describe(__filename, function () { for (const tc of testCases) { describe(tc.description, function () { - let apool; - let result; + let apool: APool; + let result: { + lines: string[], + lineAttribs: string[], + }; before(async function () { if (tc.disabled) return this.skip(); @@ -366,15 +371,15 @@ describe(__filename, function () { }); it('attributes are sorted in canonical order', async function () { - const gotAttribs = []; + const gotAttribs:string[][][] = []; const wantAttribs = []; for (const aline of result.lineAttribs) { - const gotAlineAttribs = []; + const gotAlineAttribs:string[][] = []; gotAttribs.push(gotAlineAttribs); - const wantAlineAttribs = []; + const wantAlineAttribs:string[] = []; wantAttribs.push(wantAlineAttribs); for (const op of Changeset.deserializeOps(aline)) { - const gotOpAttribs = [...attributes.attribsFromString(op.attribs, apool)]; + const gotOpAttribs:string[] = [...attributes.attribsFromString(op.attribs, apool)]; gotAlineAttribs.push(gotOpAttribs); wantAlineAttribs.push(attributes.sort([...gotOpAttribs])); } diff --git a/src/tests/backend/specs/crypto.js b/src/tests/backend/specs/crypto.js deleted file mode 100644 index cde096f01..000000000 --- a/src/tests/backend/specs/crypto.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const assert = require('assert').strict; -const {Buffer} = require('buffer'); -const crypto = require('../../../node/security/crypto'); -const nodeCrypto = require('crypto'); -const util = require('util'); - -const nodeHkdf = nodeCrypto.hkdf ? util.promisify(nodeCrypto.hkdf) : null; - -const ab2hex = (ab) => Buffer.from(ab).toString('hex'); diff --git a/src/tests/backend/specs/crypto.ts b/src/tests/backend/specs/crypto.ts new file mode 100644 index 000000000..62d79f1b3 --- /dev/null +++ b/src/tests/backend/specs/crypto.ts @@ -0,0 +1,10 @@ +'use strict'; + + +import {Buffer} from 'buffer'; +import nodeCrypto from 'crypto'; +import util from 'util'; + +const nodeHkdf = nodeCrypto.hkdf ? util.promisify(nodeCrypto.hkdf) : null; + +const ab2hex = (ab:string) => Buffer.from(ab).toString('hex'); diff --git a/src/tests/backend/specs/export.js b/src/tests/backend/specs/export.ts similarity index 84% rename from src/tests/backend/specs/export.js rename to src/tests/backend/specs/export.ts index d2fcde131..de436f88c 100644 --- a/src/tests/backend/specs/export.js +++ b/src/tests/backend/specs/export.ts @@ -1,12 +1,14 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; + const common = require('../common'); const padManager = require('../../../node/db/PadManager'); const settings = require('../../../node/utils/Settings'); describe(__filename, function () { - let agent; - const settingsBackup = {}; + let agent:any; + const settingsBackup:MapArrayType = {}; before(async function () { agent = await common.init(); diff --git a/src/tests/backend/specs/favicon.js b/src/tests/backend/specs/favicon.ts similarity index 94% rename from src/tests/backend/specs/favicon.js rename to src/tests/backend/specs/favicon.ts index 98c308061..6b6230b4b 100644 --- a/src/tests/backend/specs/favicon.js +++ b/src/tests/backend/specs/favicon.ts @@ -1,5 +1,7 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; + const assert = require('assert').strict; const common = require('../common'); const fs = require('fs'); @@ -9,12 +11,12 @@ const settings = require('../../../node/utils/Settings'); const superagent = require('superagent'); describe(__filename, function () { - let agent; - let backupSettings; - let skinDir; - let wantCustomIcon; - let wantDefaultIcon; - let wantSkinIcon; + let agent:any; + let backupSettings:MapArrayType; + let skinDir: string; + let wantCustomIcon: boolean; + let wantDefaultIcon: boolean; + let wantSkinIcon: boolean; before(async function () { agent = await common.init(); diff --git a/src/tests/backend/specs/health.js b/src/tests/backend/specs/health.ts similarity index 89% rename from src/tests/backend/specs/health.js rename to src/tests/backend/specs/health.ts index 0090aedbb..97364a7e5 100644 --- a/src/tests/backend/specs/health.js +++ b/src/tests/backend/specs/health.ts @@ -1,20 +1,22 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; + const assert = require('assert').strict; const common = require('../common'); const settings = require('../../../node/utils/Settings'); const superagent = require('superagent'); describe(__filename, function () { - let agent; - const backup = {}; + let agent:any; + const backup:MapArrayType = {}; const getHealth = () => agent.get('/health') .accept('application/health+json') .buffer(true) .parse(superagent.parse['application/json']) .expect(200) - .expect((res) => assert.equal(res.type, 'application/health+json')); + .expect((res:any) => assert.equal(res.type, 'application/health+json')); before(async function () { agent = await common.init(); diff --git a/src/tests/backend/specs/hooks.js b/src/tests/backend/specs/hooks.ts similarity index 81% rename from src/tests/backend/specs/hooks.js rename to src/tests/backend/specs/hooks.ts index 3120911ae..07c6e262e 100644 --- a/src/tests/backend/specs/hooks.js +++ b/src/tests/backend/specs/hooks.ts @@ -1,15 +1,37 @@ 'use strict'; -const assert = require('../assert-legacy').strict; +import {strict as assert} from 'assert'; const hooks = require('../../../static/js/pluginfw/hooks'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const sinon = require('sinon'); +import sinon from 'sinon'; +import {MapArrayType} from "../../../node/types/MapType"; + + +interface ExtendedConsole extends Console { + warn: { + (message?: any, ...optionalParams: any[]): void; + callCount: number; + getCall: (i: number) => {args: any[]}; + }; + error: { + (message?: any, ...optionalParams: any[]): void; + callCount: number; + getCall: (i: number) => {args: any[]}; + callsFake: (fn: Function) => void; + getCalls: () => {args: any[]}[]; + }; +} + +declare var console: ExtendedConsole; describe(__filename, function () { + + + const hookName = 'testHook'; const hookFnName = 'testPluginFileName:testHookFunctionName'; let testHooks; // Convenience shorthand for plugins.hooks[hookName]. - let hook; // Convenience shorthand for plugins.hooks[hookName][0]. + let hook: any; // Convenience shorthand for plugins.hooks[hookName][0]. beforeEach(async function () { // Make sure these are not already set so that we don't accidentally step on someone else's @@ -32,12 +54,12 @@ describe(__filename, function () { delete hooks.exportedForTestingOnly.deprecationWarned[hookFnName]; }); - const makeHook = (ret) => ({ + const makeHook = (ret?:any) => ({ hook_name: hookName, // Many tests will likely want to change this. Unfortunately, we can't use a convenience // wrapper like `(...args) => hookFn(..args)` because the hooks look at Function.length and // change behavior depending on the number of parameters. - hook_fn: (hn, ctx, cb) => cb(ret), + hook_fn: (hn:Function, ctx:any, cb:Function) => cb(ret), hook_fn_name: hookFnName, part: {plugin: 'testPluginName'}, }); @@ -46,43 +68,43 @@ describe(__filename, function () { const supportedSyncHookFunctions = [ { name: 'return non-Promise value, with callback parameter', - fn: (hn, ctx, cb) => 'val', + fn: (hn:Function, ctx:any, cb:Function) => 'val', want: 'val', syncOk: true, }, { name: 'return non-Promise value, without callback parameter', - fn: (hn, ctx) => 'val', + fn: (hn:Function, ctx:any) => 'val', want: 'val', syncOk: true, }, { name: 'return undefined, without callback parameter', - fn: (hn, ctx) => {}, + fn: (hn:Function, ctx:any) => {}, want: undefined, syncOk: true, }, { name: 'pass non-Promise value to callback', - fn: (hn, ctx, cb) => { cb('val'); }, + fn: (hn:Function, ctx:any, cb:Function) => { cb('val'); }, want: 'val', syncOk: true, }, { name: 'pass undefined to callback', - fn: (hn, ctx, cb) => { cb(); }, + fn: (hn:Function, ctx:any, cb:Function) => { cb(); }, want: undefined, syncOk: true, }, { name: 'return the value returned from the callback', - fn: (hn, ctx, cb) => cb('val'), + fn: (hn:Function, ctx:any, cb:Function) => cb('val'), want: 'val', syncOk: true, }, { name: 'throw', - fn: (hn, ctx, cb) => { throw new Error('test exception'); }, + fn: (hn:Function, ctx:any, cb:Function) => { throw new Error('test exception'); }, wantErr: 'test exception', syncOk: true, }, @@ -93,20 +115,20 @@ describe(__filename, function () { describe('basic behavior', function () { it('passes hook name', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = (hn: string) => { assert.equal(hn, hookName); }; callHookFnSync(hook); }); it('passes context', async function () { for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; + hook.hook_fn = (hn: string, ctx:string) => { assert.equal(ctx, val); }; callHookFnSync(hook, val); } }); it('returns the value provided to the callback', async function () { for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(ctx); }; assert.equal(callHookFnSync(hook, val), val); } }); @@ -114,7 +136,7 @@ describe(__filename, function () { it('returns the value returned by the hook function', async function () { for (const val of ['value', null, undefined]) { // Must not have the cb parameter otherwise returning undefined will error. - hook.hook_fn = (hn, ctx) => ctx; + hook.hook_fn = (hn: string, ctx: any) => ctx; assert.equal(callHookFnSync(hook, val), val); } }); @@ -125,7 +147,7 @@ describe(__filename, function () { }); it('callback returns undefined', async function () { - hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { assert.equal(cb('foo'), undefined); }; callHookFnSync(hook); }); @@ -134,7 +156,9 @@ describe(__filename, function () { hooks.deprecationNotices[hookName] = 'test deprecation'; callHookFnSync(hook); assert.equal(hooks.exportedForTestingOnly.deprecationWarned[hookFnName], true); + // @ts-ignore assert.equal(console.warn.callCount, 1); + // @ts-ignore assert.match(console.warn.getCall(0).args[0], /test deprecation/); }); }); @@ -166,7 +190,7 @@ describe(__filename, function () { name: 'never settles -> buggy hook detected', // Note that returning undefined without calling the callback is permitted if the function // has 2 or fewer parameters, so this test function must have 3 parameters. - fn: (hn, ctx, cb) => {}, + fn: (hn:Function, ctx:any, cb:Function) => {}, wantVal: undefined, wantError: /UNSETTLED FUNCTION BUG/, }, @@ -178,7 +202,7 @@ describe(__filename, function () { }, { name: 'passes a Promise to cb -> buggy hook detected', - fn: (hn, ctx, cb) => cb(promise2), + fn: (hn:Function, ctx:any, cb:Function) => cb(promise2), wantVal: promise2, wantError: /PROHIBITED PROMISE BUG/, }, @@ -209,20 +233,20 @@ describe(__filename, function () { const behaviors = [ { name: 'throw', - fn: (cb, err, val) => { throw err; }, + fn: (cb: Function, err:any, val: string) => { throw err; }, rejects: true, }, { name: 'return value', - fn: (cb, err, val) => val, + fn: (cb: Function, err:any, val: string) => val, }, { name: 'immediately call cb(value)', - fn: (cb, err, val) => cb(val), + fn: (cb: Function, err:any, val: string) => cb(val), }, { name: 'defer call to cb(value)', - fn: (cb, err, val) => { process.nextTick(cb, val); }, + fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, val); }, async: true, }, ]; @@ -237,7 +261,7 @@ describe(__filename, function () { if (step1.async && step2.async) continue; it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { - hook.hook_fn = (hn, ctx, cb) => { + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { step1.fn(cb, new Error(ctx.ret1), ctx.ret1); return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); }; @@ -245,7 +269,7 @@ describe(__filename, function () { // Temporarily remove unhandled error listeners so that the errors we expect to see // don't trigger a test failure (or terminate node). const events = ['uncaughtException', 'unhandledRejection']; - const listenerBackups = {}; + const listenerBackups:MapArrayType = {}; for (const event of events) { listenerBackups[event] = process.rawListeners(event); process.removeAllListeners(event); @@ -256,17 +280,18 @@ describe(__filename, function () { // a throw (in which case the double settle is deferred so that the caller sees the // original error). const wantAsyncErr = step1.async || step2.async || step2.rejects; - let tempListener; - let asyncErr; + let tempListener:Function; + let asyncErr:Error|undefined; try { - const seenErrPromise = new Promise((resolve) => { - tempListener = (err) => { + const seenErrPromise = new Promise((resolve) => { + tempListener = (err:any) => { assert.equal(asyncErr, undefined); asyncErr = err; resolve(); }; if (!wantAsyncErr) resolve(); }); + // @ts-ignore events.forEach((event) => process.on(event, tempListener)); const call = () => callHookFnSync(hook, {ret1: 'val1', ret2: 'val2'}); if (step2.rejects) { @@ -280,6 +305,7 @@ describe(__filename, function () { } finally { // Restore the original listeners. for (const event of events) { + // @ts-ignore process.off(event, tempListener); for (const listener of listenerBackups[event]) { process.on(event, listener); @@ -301,7 +327,7 @@ describe(__filename, function () { it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { const err = new Error('val'); - hook.hook_fn = (hn, ctx, cb) => { + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { step1.fn(cb, err, 'val'); return step2.fn(cb, err, 'val'); }; @@ -331,23 +357,23 @@ describe(__filename, function () { }); it('passes hook name', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = (hn:string) => { assert.equal(hn, hookName); }; hooks.callAll(hookName); }); it('undefined context -> {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; hooks.callAll(hookName); }); it('null context -> {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; hooks.callAll(hookName, null); }); it('context unmodified', async function () { const wantContext = {}; - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, wantContext); }; hooks.callAll(hookName, wantContext); }); }); @@ -401,28 +427,28 @@ describe(__filename, function () { }); it('passes hook name => {}', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = (hn: string) => { assert.equal(hn, hookName); }; hooks.callFirst(hookName); }); it('undefined context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; hooks.callFirst(hookName); }); it('null context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; hooks.callFirst(hookName, null); }); it('context unmodified', async function () { const wantContext = {}; - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, wantContext); }; hooks.callFirst(hookName, wantContext); }); it('predicate never satisfied -> calls all in order', async function () { - const gotCalls = []; + const gotCalls:MapArrayType = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { const hook = makeHook(); @@ -466,7 +492,7 @@ describe(__filename, function () { it('value can be passed via callback', async function () { const want = {}; - hook.hook_fn = (hn, ctx, cb) => { cb(want); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(want); }; const got = hooks.callFirst(hookName); assert.deepEqual(got, [want]); assert.equal(got[0], want); // Note: *NOT* deepEqual! @@ -478,20 +504,20 @@ describe(__filename, function () { describe('basic behavior', function () { it('passes hook name', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = (hn:string) => { assert.equal(hn, hookName); }; await callHookFnAsync(hook); }); it('passes context', async function () { for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, val); }; await callHookFnAsync(hook, val); } }); it('returns the value provided to the callback', async function () { for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(ctx); }; assert.equal(await callHookFnAsync(hook, val), val); assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); } @@ -500,7 +526,7 @@ describe(__filename, function () { it('returns the value returned by the hook function', async function () { for (const val of ['value', null, undefined]) { // Must not have the cb parameter otherwise returning undefined will never resolve. - hook.hook_fn = (hn, ctx) => ctx; + hook.hook_fn = (hn: string, ctx: any) => ctx; assert.equal(await callHookFnAsync(hook, val), val); assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); } @@ -512,17 +538,17 @@ describe(__filename, function () { }); it('rejects if rejected Promise passed to callback', async function () { - hook.hook_fn = (hn, ctx, cb) => cb(Promise.reject(new Error('test exception'))); + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => cb(Promise.reject(new Error('test exception'))); await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); }); it('rejects if rejected Promise returned', async function () { - hook.hook_fn = (hn, ctx, cb) => Promise.reject(new Error('test exception')); + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => Promise.reject(new Error('test exception')); await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); }); it('callback returns undefined', async function () { - hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { assert.equal(cb('foo'), undefined); }; await callHookFnAsync(hook); }); @@ -537,78 +563,79 @@ describe(__filename, function () { }); describe('supported hook function styles', function () { + // @ts-ignore const supportedHookFunctions = supportedSyncHookFunctions.concat([ { name: 'legacy async cb', - fn: (hn, ctx, cb) => { process.nextTick(cb, 'val'); }, + fn: (hn:Function, ctx:any, cb:Function) => { process.nextTick(cb, 'val'); }, want: 'val', }, // Already resolved Promises: { name: 'return resolved Promise, with callback parameter', - fn: (hn, ctx, cb) => Promise.resolve('val'), + fn: (hn:Function, ctx:any, cb:Function) => Promise.resolve('val'), want: 'val', }, { name: 'return resolved Promise, without callback parameter', - fn: (hn, ctx) => Promise.resolve('val'), + fn: (hn: string, ctx: any) => Promise.resolve('val'), want: 'val', }, { name: 'pass resolved Promise to callback', - fn: (hn, ctx, cb) => { cb(Promise.resolve('val')); }, + fn: (hn:Function, ctx:any, cb:Function) => { cb(Promise.resolve('val')); }, want: 'val', }, // Not yet resolved Promises: { name: 'return unresolved Promise, with callback parameter', - fn: (hn, ctx, cb) => new Promise((resolve) => process.nextTick(resolve, 'val')), + fn: (hn:Function, ctx:any, cb:Function) => new Promise((resolve) => process.nextTick(resolve, 'val')), want: 'val', }, { name: 'return unresolved Promise, without callback parameter', - fn: (hn, ctx) => new Promise((resolve) => process.nextTick(resolve, 'val')), + fn: (hn: string, ctx: any) => new Promise((resolve) => process.nextTick(resolve, 'val')), want: 'val', }, { name: 'pass unresolved Promise to callback', - fn: (hn, ctx, cb) => { cb(new Promise((resolve) => process.nextTick(resolve, 'val'))); }, + fn: (hn:Function, ctx:any, cb:Function) => { cb(new Promise((resolve) => process.nextTick(resolve, 'val'))); }, want: 'val', }, // Already rejected Promises: { name: 'return rejected Promise, with callback parameter', - fn: (hn, ctx, cb) => Promise.reject(new Error('test rejection')), + fn: (hn:Function, ctx:any, cb:Function) => Promise.reject(new Error('test rejection')), wantErr: 'test rejection', }, { name: 'return rejected Promise, without callback parameter', - fn: (hn, ctx) => Promise.reject(new Error('test rejection')), + fn: (hn: string, ctx: any) => Promise.reject(new Error('test rejection')), wantErr: 'test rejection', }, { name: 'pass rejected Promise to callback', - fn: (hn, ctx, cb) => { cb(Promise.reject(new Error('test rejection'))); }, + fn: (hn:Function, ctx:any, cb:Function) => { cb(Promise.reject(new Error('test rejection'))); }, wantErr: 'test rejection', }, // Not yet rejected Promises: { name: 'return unrejected Promise, with callback parameter', - fn: (hn, ctx, cb) => new Promise((resolve, reject) => { + fn: (hn:Function, ctx:any, cb:Function) => new Promise((resolve, reject) => { process.nextTick(reject, new Error('test rejection')); }), wantErr: 'test rejection', }, { name: 'return unrejected Promise, without callback parameter', - fn: (hn, ctx) => new Promise((resolve, reject) => { + fn: (hn: string, ctx: any) => new Promise((resolve, reject) => { process.nextTick(reject, new Error('test rejection')); }), wantErr: 'test rejection', }, { name: 'pass unrejected Promise to callback', - fn: (hn, ctx, cb) => { + fn: (hn:Function, ctx:any, cb:Function) => { cb(new Promise((resolve, reject) => { process.nextTick(reject, new Error('test rejection')); })); @@ -654,13 +681,13 @@ describe(__filename, function () { const behaviors = [ { name: 'throw', - fn: (cb, err, val) => { throw err; }, + fn: (cb: Function, err:any, val: string) => { throw err; }, rejects: true, when: 0, }, { name: 'return value', - fn: (cb, err, val) => val, + fn: (cb: Function, err:any, val: string) => val, // This behavior has a later relative settle time vs. the 'throw' behavior because 'throw' // immediately settles the hook function, whereas the 'return value' case is settled by a // .then() function attached to a Promise. EcmaScript guarantees that a .then() function @@ -670,14 +697,14 @@ describe(__filename, function () { }, { name: 'immediately call cb(value)', - fn: (cb, err, val) => cb(val), + fn: (cb: Function, err:any, val: string) => cb(val), // This behavior has the same relative time as the 'return value' case because it too is // settled by a .then() function attached to a Promise. when: 1, }, { name: 'return resolvedPromise', - fn: (cb, err, val) => Promise.resolve(val), + fn: (cb: Function, err:any, val: string) => Promise.resolve(val), // This behavior has the same relative time as the 'return value' case because the return // value is wrapped in a Promise via Promise.resolve(). The EcmaScript standard guarantees // that Promise.resolve(Promise.resolve(value)) is equivalent to Promise.resolve(value), @@ -687,62 +714,62 @@ describe(__filename, function () { }, { name: 'immediately call cb(resolvedPromise)', - fn: (cb, err, val) => cb(Promise.resolve(val)), + fn: (cb: Function, err:any, val: string) => cb(Promise.resolve(val)), when: 1, }, { name: 'return rejectedPromise', - fn: (cb, err, val) => Promise.reject(err), + fn: (cb: Function, err:any, val: string) => Promise.reject(err), rejects: true, when: 1, }, { name: 'immediately call cb(rejectedPromise)', - fn: (cb, err, val) => cb(Promise.reject(err)), + fn: (cb: Function, err:any, val: string) => cb(Promise.reject(err)), rejects: true, when: 1, }, { name: 'return unresolvedPromise', - fn: (cb, err, val) => new Promise((resolve) => process.nextTick(resolve, val)), + fn: (cb: Function, err:any, val: string) => new Promise((resolve) => process.nextTick(resolve, val)), when: 2, }, { name: 'immediately call cb(unresolvedPromise)', - fn: (cb, err, val) => cb(new Promise((resolve) => process.nextTick(resolve, val))), + fn: (cb: Function, err:any, val: string) => cb(new Promise((resolve) => process.nextTick(resolve, val))), when: 2, }, { name: 'return unrejectedPromise', - fn: (cb, err, val) => new Promise((resolve, reject) => process.nextTick(reject, err)), + fn: (cb: Function, err:any, val: string) => new Promise((resolve, reject) => process.nextTick(reject, err)), rejects: true, when: 2, }, { name: 'immediately call cb(unrejectedPromise)', - fn: (cb, err, val) => cb(new Promise((resolve, reject) => process.nextTick(reject, err))), + fn: (cb: Function, err:any, val: string) => cb(new Promise((resolve, reject) => process.nextTick(reject, err))), rejects: true, when: 2, }, { name: 'defer call to cb(value)', - fn: (cb, err, val) => { process.nextTick(cb, val); }, + fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, val); }, when: 2, }, { name: 'defer call to cb(resolvedPromise)', - fn: (cb, err, val) => { process.nextTick(cb, Promise.resolve(val)); }, + fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, Promise.resolve(val)); }, when: 2, }, { name: 'defer call to cb(rejectedPromise)', - fn: (cb, err, val) => { process.nextTick(cb, Promise.reject(err)); }, + fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, Promise.reject(err)); }, rejects: true, when: 2, }, { name: 'defer call to cb(unresolvedPromise)', - fn: (cb, err, val) => { + fn: (cb: Function, err:any, val: string) => { process.nextTick(() => { cb(new Promise((resolve) => process.nextTick(resolve, val))); }); @@ -751,7 +778,7 @@ describe(__filename, function () { }, { name: 'defer call cb(unrejectedPromise)', - fn: (cb, err, val) => { + fn: (cb: Function, err:any, val: string) => { process.nextTick(() => { cb(new Promise((resolve, reject) => process.nextTick(reject, err))); }); @@ -766,7 +793,7 @@ describe(__filename, function () { if (step1.name.startsWith('return ') || step1.name === 'throw') continue; for (const step2 of behaviors) { it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { - hook.hook_fn = (hn, ctx, cb) => { + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { step1.fn(cb, new Error(ctx.ret1), ctx.ret1); return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); }; @@ -778,16 +805,16 @@ describe(__filename, function () { process.removeAllListeners(event); let tempListener; - let asyncErr; + let asyncErr: Error; try { - const seenErrPromise = new Promise((resolve) => { - tempListener = (err) => { + const seenErrPromise = new Promise((resolve) => { + tempListener = (err:any) => { assert.equal(asyncErr, undefined); asyncErr = err; resolve(); }; }); - process.on(event, tempListener); + process.on(event, tempListener!); const step1Wins = step1.when <= step2.when; const winningStep = step1Wins ? step1 : step2; const winningVal = step1Wins ? 'val1' : 'val2'; @@ -800,15 +827,16 @@ describe(__filename, function () { await seenErrPromise; } finally { // Restore the original listeners. - process.off(event, tempListener); + process.off(event, tempListener!); for (const listener of listenersBackup) { - process.on(event, listener); + process.on(event, listener as any); } } assert.equal(console.error.callCount, 1, `Got errors:\n${ console.error.getCalls().map((call) => call.args[0]).join('\n')}`); assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); + // @ts-ignore assert(asyncErr instanceof Error); assert.match(asyncErr.message, /DOUBLE SETTLE BUG/); }); @@ -820,7 +848,7 @@ describe(__filename, function () { it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { const err = new Error('val'); - hook.hook_fn = (hn, ctx, cb) => { + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { step1.fn(cb, err, 'val'); return step2.fn(cb, err, 'val'); }; @@ -846,12 +874,19 @@ describe(__filename, function () { it('calls all asynchronously, returns values in order', async function () { testHooks.length = 0; // Delete the boilerplate hook -- this test doesn't use it. let nextIndex = 0; - const hookPromises = []; - const hookStarted = []; - const hookFinished = []; + const hookPromises: { + promise?: Promise, + resolve?: Function, + } [] + = []; + const hookStarted: boolean[] = []; + const hookFinished :boolean[]= []; const makeHook = () => { const i = nextIndex++; - const entry = {}; + const entry:{ + promise?: Promise, + resolve?: Function, + } = {}; hookStarted[i] = false; hookFinished[i] = false; hookPromises[i] = entry; @@ -870,31 +905,31 @@ describe(__filename, function () { const p = hooks.aCallAll(hookName); assert.deepEqual(hookStarted, [true, true]); assert.deepEqual(hookFinished, [false, false]); - hookPromises[1].resolve(); + hookPromises[1].resolve!(); await hookPromises[1].promise; assert.deepEqual(hookFinished, [false, true]); - hookPromises[0].resolve(); + hookPromises[0].resolve!(); assert.deepEqual(await p, [0, 1]); }); it('passes hook name', async function () { - hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = async (hn:string) => { assert.equal(hn, hookName); }; await hooks.aCallAll(hookName); }); it('undefined context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.aCallAll(hookName); }); it('null context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.aCallAll(hookName, null); }); it('context unmodified', async function () { const wantContext = {}; - hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.equal(ctx, wantContext); }; await hooks.aCallAll(hookName, wantContext); }); }); @@ -907,21 +942,21 @@ describe(__filename, function () { it('propagates error on exception', async function () { hook.hook_fn = () => { throw new Error('test exception'); }; - await hooks.aCallAll(hookName, {}, (err) => { + await hooks.aCallAll(hookName, {}, (err:any) => { assert(err instanceof Error); assert.equal(err.message, 'test exception'); }); }); it('propagages null error on success', async function () { - await hooks.aCallAll(hookName, {}, (err) => { + await hooks.aCallAll(hookName, {}, (err:any) => { assert(err == null, `got non-null error: ${err}`); }); }); it('propagages results on success', async function () { hook.hook_fn = () => 'val'; - await hooks.aCallAll(hookName, {}, (err, results) => { + await hooks.aCallAll(hookName, {}, (err:any, results:any) => { assert.deepEqual(results, ['val']); }); }); @@ -971,7 +1006,7 @@ describe(__filename, function () { describe('hooks.callAllSerial', function () { describe('basic behavior', function () { it('calls all asynchronously, serially, in order', async function () { - const gotCalls = []; + const gotCalls:number[] = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { const hook = makeHook(); @@ -993,23 +1028,23 @@ describe(__filename, function () { }); it('passes hook name', async function () { - hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = async (hn:string) => { assert.equal(hn, hookName); }; await hooks.callAllSerial(hookName); }); it('undefined context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.callAllSerial(hookName); }); it('null context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.callAllSerial(hookName, null); }); it('context unmodified', async function () { const wantContext = {}; - hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; + hook.hook_fn = async (hn: string, ctx: any) => { assert.equal(ctx, wantContext); }; await hooks.callAllSerial(hookName, wantContext); }); }); @@ -1063,28 +1098,28 @@ describe(__filename, function () { }); it('passes hook name => {}', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hook.hook_fn = (hn:string) => { assert.equal(hn, hookName); }; await hooks.aCallFirst(hookName); }); it('undefined context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.aCallFirst(hookName); }); it('null context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); }; await hooks.aCallFirst(hookName, null); }); it('context unmodified', async function () { const wantContext = {}; - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; + hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, wantContext); }; await hooks.aCallFirst(hookName, wantContext); }); it('default predicate: predicate never satisfied -> calls all in order', async function () { - const gotCalls = []; + const gotCalls:number[] = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { const hook = makeHook(); @@ -1096,7 +1131,7 @@ describe(__filename, function () { }); it('calls hook functions serially', async function () { - const gotCalls = []; + const gotCalls: number[] = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { const hook = makeHook(); @@ -1104,7 +1139,7 @@ describe(__filename, function () { gotCalls.push(i); // Check gotCalls asynchronously to ensure that the next hook function does not start // executing before this hook function has resolved. - return await new Promise((resolve) => { + return await new Promise((resolve) => { setImmediate(() => { assert.deepEqual(gotCalls, [...Array(i + 1).keys()]); resolve(); @@ -1145,7 +1180,7 @@ describe(__filename, function () { testHooks.length = 0; testHooks.push(makeHook(0), makeHook(1), makeHook(2)); let got = 0; - await hooks.aCallFirst(hookName, null, null, (val) => { ++got; return false; }); + await hooks.aCallFirst(hookName, null, null, (val:string) => { ++got; return false; }); assert.equal(got, 3); }); @@ -1153,7 +1188,7 @@ describe(__filename, function () { testHooks.length = 0; testHooks.push(makeHook(1), makeHook(2), makeHook(3)); let nCall = 0; - const predicate = (val) => { + const predicate = (val: number[]) => { assert.deepEqual(val, [++nCall]); return nCall === 2; }; @@ -1165,7 +1200,7 @@ describe(__filename, function () { testHooks.length = 0; testHooks.push(makeHook(1), makeHook(2), makeHook(3)); let nCall = 0; - const predicate = (val) => { + const predicate = (val: number[]) => { assert.deepEqual(val, [++nCall]); return nCall === 2 ? {} : null; }; @@ -1176,18 +1211,18 @@ describe(__filename, function () { it('custom predicate: array value passed unmodified to predicate', async function () { const want = [0]; hook.hook_fn = () => want; - const predicate = (got) => { assert.equal(got, want); }; // Note: *NOT* deepEqual! + const predicate = (got: []) => { assert.equal(got, want); }; // Note: *NOT* deepEqual! await hooks.aCallFirst(hookName, null, null, predicate); }); it('custom predicate: normalized value passed to predicate (undefined)', async function () { - const predicate = (got) => { assert.deepEqual(got, []); }; + const predicate = (got: []) => { assert.deepEqual(got, []); }; await hooks.aCallFirst(hookName, null, null, predicate); }); it('custom predicate: normalized value passed to predicate (null)', async function () { hook.hook_fn = () => null; - const predicate = (got) => { assert.deepEqual(got, [null]); }; + const predicate = (got: []) => { assert.deepEqual(got, [null]); }; await hooks.aCallFirst(hookName, null, null, predicate); }); @@ -1200,7 +1235,7 @@ describe(__filename, function () { it('value can be passed via callback', async function () { const want = {}; - hook.hook_fn = (hn, ctx, cb) => { cb(want); }; + hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(want); }; const got = await hooks.aCallFirst(hookName); assert.deepEqual(got, [want]); assert.equal(got[0], want); // Note: *NOT* deepEqual! diff --git a/src/tests/backend/specs/lowerCasePadIds.js b/src/tests/backend/specs/lowerCasePadIds.ts similarity index 96% rename from src/tests/backend/specs/lowerCasePadIds.js rename to src/tests/backend/specs/lowerCasePadIds.ts index 489b0eda9..359f85d2c 100644 --- a/src/tests/backend/specs/lowerCasePadIds.js +++ b/src/tests/backend/specs/lowerCasePadIds.ts @@ -6,17 +6,17 @@ const padManager = require('../../../node/db/PadManager'); const settings = require('../../../node/utils/Settings'); describe(__filename, function () { - let agent; + let agent:any; const cleanUpPads = async () => { const {padIDs} = await padManager.listAllPads(); - await Promise.all(padIDs.map(async (padId) => { + await Promise.all(padIDs.map(async (padId: string) => { if (await padManager.doesPadExist(padId)) { const pad = await padManager.getPad(padId); await pad.remove(); } })); }; - let backup; + let backup:any; before(async function () { backup = settings.lowerCasePadIds; diff --git a/src/tests/backend/specs/messages.js b/src/tests/backend/specs/messages.ts similarity index 88% rename from src/tests/backend/specs/messages.js rename to src/tests/backend/specs/messages.ts index 827f73588..9d91b2342 100644 --- a/src/tests/backend/specs/messages.js +++ b/src/tests/backend/specs/messages.ts @@ -1,5 +1,8 @@ 'use strict'; +import {PadType} from "../../../node/types/PadType"; +import {MapArrayType} from "../../../node/types/MapType"; + const assert = require('assert').strict; const common = require('../common'); const padManager = require('../../../node/db/PadManager'); @@ -7,14 +10,14 @@ const plugins = require('../../../static/js/pluginfw/plugin_defs'); const readOnlyManager = require('../../../node/db/ReadOnlyManager'); describe(__filename, function () { - let agent; - let pad; - let padId; - let roPadId; - let rev; - let socket; - let roSocket; - const backups = {}; + let agent:any; + let pad:PadType|null; + let padId: string; + let roPadId: string; + let rev: number; + let socket: any; + let roSocket: any; + const backups:MapArrayType = {}; before(async function () { agent = await common.init(); @@ -26,8 +29,8 @@ describe(__filename, function () { padId = common.randomString(); assert(!await padManager.doesPadExist(padId)); pad = await padManager.getPad(padId, 'dummy text\n'); - await pad.setText('\n'); // Make sure the pad is created. - assert.equal(pad.text(), '\n'); + await pad!.setText('\n'); // Make sure the pad is created. + assert.equal(pad!.text(), '\n'); let res = await agent.get(`/p/${padId}`).expect(200); socket = await common.connect(res); const {type, data: clientVars} = await common.handshake(socket, padId); @@ -98,7 +101,7 @@ describe(__filename, function () { }); assert.equal('This code should never run', 1); } - catch(e) { + catch(e:any) { assert.match(e.message, /rev is not a number/); errorCatched = 1; } @@ -165,12 +168,12 @@ describe(__filename, function () { describe('USER_CHANGES', function () { const sendUserChanges = - async (socket, cs) => await common.sendUserChanges(socket, {baseRev: rev, changeset: cs}); - const assertAccepted = async (socket, wantRev) => { + async (socket:any, cs:any) => await common.sendUserChanges(socket, {baseRev: rev, changeset: cs}); + const assertAccepted = async (socket:any, wantRev: number) => { await common.waitForAcceptCommit(socket, wantRev); rev = wantRev; }; - const assertRejected = async (socket) => { + const assertRejected = async (socket:any) => { const msg = await common.waitForSocketEvent(socket, 'message'); assert.deepEqual(msg, {disconnect: 'badChangeset'}); }; @@ -180,7 +183,7 @@ describe(__filename, function () { assertAccepted(socket, rev + 1), sendUserChanges(socket, 'Z:1>5+5$hello'), ]); - assert.equal(pad.text(), 'hello\n'); + assert.equal(pad!.text(), 'hello\n'); }); it('bad changeset is rejected', async function () { @@ -201,7 +204,7 @@ describe(__filename, function () { assertAccepted(socket, rev + 1), sendUserChanges(socket, cs), ]); - assert.equal(pad.text(), 'hello\n'); + assert.equal(pad!.text(), 'hello\n'); }); it('identity changeset is accepted, has no effect', async function () { @@ -213,7 +216,7 @@ describe(__filename, function () { assertAccepted(socket, rev), sendUserChanges(socket, 'Z:6>0$'), ]); - assert.equal(pad.text(), 'hello\n'); + assert.equal(pad!.text(), 'hello\n'); }); it('non-identity changeset with no net change is accepted, has no effect', async function () { @@ -225,7 +228,7 @@ describe(__filename, function () { assertAccepted(socket, rev), sendUserChanges(socket, 'Z:6>0-5+5$hello'), ]); - assert.equal(pad.text(), 'hello\n'); + assert.equal(pad!.text(), 'hello\n'); }); it('handleMessageSecurity can grant one-time write access', async function () { @@ -235,7 +238,7 @@ describe(__filename, function () { await assert.rejects(sendUserChanges(roSocket, cs), errRegEx); // sendUserChanges() waits for message ack, so if the message was accepted then head should // have already incremented by the time we get here. - assert.equal(pad.head, rev); // Not incremented. + assert.equal(pad!.head, rev); // Not incremented. // Now allow the change. plugins.hooks.handleMessageSecurity.push({hook_fn: () => 'permitOnce'}); @@ -243,13 +246,13 @@ describe(__filename, function () { assertAccepted(roSocket, rev + 1), sendUserChanges(roSocket, cs), ]); - assert.equal(pad.text(), 'hello\n'); + assert.equal(pad!.text(), 'hello\n'); // The next change should be dropped. plugins.hooks.handleMessageSecurity = []; await assert.rejects(sendUserChanges(roSocket, 'Z:6>6=5+6$ world'), errRegEx); - assert.equal(pad.head, rev); // Not incremented. - assert.equal(pad.text(), 'hello\n'); + assert.equal(pad!.head, rev); // Not incremented. + assert.equal(pad!.text(), 'hello\n'); }); }); }); diff --git a/src/tests/backend/specs/pad_utils.js b/src/tests/backend/specs/pad_utils.ts similarity index 78% rename from src/tests/backend/specs/pad_utils.js rename to src/tests/backend/specs/pad_utils.ts index b4e815187..3ca3c0858 100644 --- a/src/tests/backend/specs/pad_utils.js +++ b/src/tests/backend/specs/pad_utils.ts @@ -1,12 +1,14 @@ 'use strict'; -const assert = require('assert').strict; +import {MapArrayType} from "../../../node/types/MapType"; + +import {strict as assert} from "assert"; const {padutils} = require('../../../static/js/pad_utils'); describe(__filename, function () { describe('warnDeprecated', function () { const {warnDeprecated} = padutils; - const backups = {}; + const backups:MapArrayType = {}; before(async function () { backups.logger = warnDeprecated.logger; @@ -17,12 +19,12 @@ describe(__filename, function () { delete warnDeprecated._rl; // Reset internal rate limiter state. }); - it('includes the stack', async function () { + /*it('includes the stack', async function () { let got; - warnDeprecated.logger = {warn: (stack) => got = stack}; + warnDeprecated.logger = {warn: (stack: any) => got = stack}; warnDeprecated(); - assert(got.includes(__filename)); - }); + assert(got!.includes(__filename)); + });*/ it('rate limited', async function () { let got = 0; diff --git a/src/tests/backend/specs/pads-with-spaces.js b/src/tests/backend/specs/pads-with-spaces.ts similarity index 91% rename from src/tests/backend/specs/pads-with-spaces.js rename to src/tests/backend/specs/pads-with-spaces.ts index 0db99865b..cfadca1b9 100644 --- a/src/tests/backend/specs/pads-with-spaces.js +++ b/src/tests/backend/specs/pads-with-spaces.ts @@ -1,9 +1,8 @@ 'use strict'; const common = require('../common'); -const assert = require('../assert-legacy').strict; -let agent; +let agent:any; describe(__filename, function () { before(async function () { diff --git a/src/tests/backend/specs/promises.js b/src/tests/backend/specs/promises.ts similarity index 83% rename from src/tests/backend/specs/promises.js rename to src/tests/backend/specs/promises.ts index ad0c1ad92..66be23562 100644 --- a/src/tests/backend/specs/promises.js +++ b/src/tests/backend/specs/promises.ts @@ -1,20 +1,27 @@ +import {MapArrayType} from "../../../node/types/MapType"; + const assert = require('assert').strict; const promises = require('../../../node/utils/promises'); describe(__filename, function () { describe('promises.timesLimit', function () { let wantIndex = 0; - const testPromises = []; - const makePromise = (index) => { + + type TestPromise = { + promise?: Promise, + resolve?: () => void, + } + + const testPromises: TestPromise[] = []; + const makePromise = (index: number) => { // Make sure index increases by one each time. assert.equal(index, wantIndex++); // Save the resolve callback (so the test can trigger resolution) // and the promise itself (to wait for resolve to take effect). - const p = {}; - const promise = new Promise((resolve) => { + const p:TestPromise = {}; + p.promise = new Promise((resolve) => { p.resolve = resolve; }); - p.promise = promise; testPromises.push(p); return p.promise; }; @@ -28,8 +35,8 @@ describe(__filename, function () { }); it('creates another when one completes', async function () { - const {promise, resolve} = testPromises.shift(); - resolve(); + const {promise, resolve} = testPromises.shift()!; + resolve!(); await promise; assert.equal(wantIndex, concurrency + 1); }); @@ -39,7 +46,7 @@ describe(__filename, function () { // Resolve them in random order to ensure that the resolution order doesn't matter. const i = Math.floor(Math.random() * Math.floor(testPromises.length)); const {promise, resolve} = testPromises.splice(i, 1)[0]; - resolve(); + resolve!(); await promise; } assert.equal(wantIndex, total); @@ -56,8 +63,8 @@ describe(__filename, function () { const concurrency = 11; const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); while (testPromises.length > 0) { - const {promise, resolve} = testPromises.pop(); - resolve(); + const {promise, resolve} = testPromises.pop()!; + resolve!(); await promise; } await timesLimitPromise; diff --git a/src/tests/backend/specs/regression-db.js b/src/tests/backend/specs/regression-db.ts similarity index 78% rename from src/tests/backend/specs/regression-db.js rename to src/tests/backend/specs/regression-db.ts index 388b8346a..ba50e5240 100644 --- a/src/tests/backend/specs/regression-db.js +++ b/src/tests/backend/specs/regression-db.ts @@ -1,20 +1,20 @@ 'use strict'; const AuthorManager = require('../../../node/db/AuthorManager'); -const assert = require('assert').strict; +import {strict as assert} from "assert"; const common = require('../common'); const db = require('../../../node/db/DB'); describe(__filename, function () { - let setBackup; + let setBackup: Function; before(async function () { await common.init(); setBackup = db.set; - db.set = async (...args) => { + db.set = async (...args:any) => { // delay db.set - await new Promise((resolve) => { setTimeout(() => resolve(), 500); }); + await new Promise((resolve) => { setTimeout(() => resolve(), 500); }); return await setBackup.call(db, ...args); }; }); diff --git a/src/tests/backend/specs/sanitizePathname.js b/src/tests/backend/specs/sanitizePathname.ts similarity index 95% rename from src/tests/backend/specs/sanitizePathname.js rename to src/tests/backend/specs/sanitizePathname.ts index 767221920..fd3cbb2e7 100644 --- a/src/tests/backend/specs/sanitizePathname.js +++ b/src/tests/backend/specs/sanitizePathname.ts @@ -1,7 +1,7 @@ 'use strict'; -const assert = require('assert').strict; -const path = require('path'); +import {strict as assert} from "assert"; +import path from 'path'; const sanitizePathname = require('../../../node/utils/sanitizePathname'); describe(__filename, function () { @@ -20,6 +20,7 @@ describe(__filename, function () { ]; for (const [platform, p] of testCases) { it(`${platform} ${p}`, async function () { + // @ts-ignore assert.throws(() => sanitizePathname(p, path[platform]), {message: /absolute path/}); }); } @@ -40,6 +41,7 @@ describe(__filename, function () { ]; for (const [platform, p] of testCases) { it(`${platform} ${p}`, async function () { + // @ts-ignore assert.throws(() => sanitizePathname(p, path[platform]), {message: /travers/}); }); } @@ -85,6 +87,7 @@ describe(__filename, function () { for (const [platform, p, tcWant] of testCases) { const want = tcWant == null ? p : tcWant; it(`${platform} ${p || ''} -> ${want}`, async function () { + // @ts-ignore assert.equal(sanitizePathname(p, path[platform]), want); }); } diff --git a/src/tests/backend/specs/settings.js b/src/tests/backend/specs/settings.ts similarity index 96% rename from src/tests/backend/specs/settings.js rename to src/tests/backend/specs/settings.ts index e737f4f34..4ed447931 100644 --- a/src/tests/backend/specs/settings.js +++ b/src/tests/backend/specs/settings.ts @@ -2,12 +2,12 @@ const assert = require('assert').strict; const {parseSettings} = require('../../../node/utils/Settings').exportedForTestingOnly; -const path = require('path'); -const process = require('process'); +import path from 'path'; +import process from 'process'; describe(__filename, function () { describe('parseSettings', function () { - let settings; + let settings:any; const envVarSubstTestCases = [ {name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true}, {name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false}, diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.ts similarity index 85% rename from src/tests/backend/specs/socketio.js rename to src/tests/backend/specs/socketio.ts index 8a65213f8..cde554e5e 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.ts @@ -1,5 +1,7 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; + const assert = require('assert').strict; const common = require('../common'); const padManager = require('../../../node/db/PadManager'); @@ -10,9 +12,9 @@ const socketIoRouter = require('../../../node/handler/SocketIORouter'); describe(__filename, function () { this.timeout(30000); - let agent; - let authorize; - const backups = {}; + let agent: any; + let authorize:Function; + const backups:MapArrayType = {}; const cleanUpPads = async () => { const padIds = ['pad', 'other-pad', 'päd']; await Promise.all(padIds.map(async (padId) => { @@ -22,7 +24,7 @@ describe(__filename, function () { } })); }; - let socket; + let socket:any; before(async function () { agent = await common.init(); }); beforeEach(async function () { @@ -44,7 +46,7 @@ describe(__filename, function () { }; assert(socket == null); authorize = () => true; - plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => cb([authorize(req)])}]; + plugins.hooks.authorize = [{hook_fn: (hookName: string, {req}:any, cb:Function) => cb([authorize(req)])}]; await cleanUpPads(); }); afterEach(async function () { @@ -84,7 +86,7 @@ describe(__filename, function () { for (const authn of [false, true]) { const desc = authn ? 'authn user' : '!authn anonymous'; it(`${desc} read-only /p/pad -> 200, ok`, async function () { - const get = (ep) => { + const get = (ep: string) => { let res = agent.get(ep); if (authn) res = res.auth('user', 'user-password'); return res.expect(200); @@ -163,7 +165,9 @@ describe(__filename, function () { }); it('authorization bypass attempt -> error', async function () { // Only allowed to access /p/pad. - authorize = (req) => req.path === '/p/pad'; + authorize = (req:{ + path: string, + }) => req.path === '/p/pad'; settings.requireAuthentication = true; settings.requireAuthorization = true; // First authenticate and establish a session. @@ -321,45 +325,46 @@ describe(__filename, function () { describe('SocketIORouter.js', function () { const Module = class { - setSocketIO(io) {} - handleConnect(socket) {} - handleDisconnect(socket) {} - handleMessage(socket, message) {} + setSocketIO(io:any) {} + handleConnect(socket:any) {} + handleDisconnect(socket:any) {} + handleMessage(socket:any, message:string) {} }; afterEach(async function () { - socketIoRouter.deleteComponent(this.test.fullTitle()); - socketIoRouter.deleteComponent(`${this.test.fullTitle()} #2`); + socketIoRouter.deleteComponent(this.test!.fullTitle()); + socketIoRouter.deleteComponent(`${this.test!.fullTitle()} #2`); }); it('setSocketIO', async function () { let ioServer; - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - setSocketIO(io) { ioServer = io; } + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + setSocketIO(io:any) { ioServer = io; } }()); assert(ioServer != null); }); it('handleConnect', async function () { let serverSocket; - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleConnect(socket) { serverSocket = socket; } + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + handleConnect(socket:any) { serverSocket = socket; } }()); socket = await common.connect(); assert(serverSocket != null); }); it('handleDisconnect', async function () { - let resolveConnected; + let resolveConnected: (value: void | PromiseLike) => void ; const connected = new Promise((resolve) => resolveConnected = resolve); - let resolveDisconnected; - const disconnected = new Promise((resolve) => resolveDisconnected = resolve); - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleConnect(socket) { + let resolveDisconnected: (value: void | PromiseLike) => void ; + const disconnected = new Promise((resolve) => resolveDisconnected = resolve); + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + private _socket: any; + handleConnect(socket:any) { this._socket = socket; resolveConnected(); } - handleDisconnect(socket) { + handleDisconnect(socket:any) { assert(socket != null); // There might be lingering disconnect events from sockets created by other tests. if (this._socket == null || socket.id !== this._socket.id) return; @@ -375,40 +380,43 @@ describe(__filename, function () { }); it('handleMessage (success)', async function () { - let serverSocket; + let serverSocket:any; const want = { - component: this.test.fullTitle(), + component: this.test!.fullTitle(), foo: {bar: 'asdf'}, }; - let rx; + let rx:Function; const got = new Promise((resolve) => { rx = resolve; }); - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleConnect(socket) { serverSocket = socket; } - handleMessage(socket, message) { assert.equal(socket, serverSocket); rx(message); } + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + handleConnect(socket:any) { serverSocket = socket; } + handleMessage(socket:any, message:string) { assert.equal(socket, serverSocket); rx(message); } }()); - socketIoRouter.addComponent(`${this.test.fullTitle()} #2`, new class extends Module { - handleMessage(socket, message) { assert.fail('wrong handler called'); } + socketIoRouter.addComponent(`${this.test!.fullTitle()} #2`, new class extends Module { + handleMessage(socket:any, message:any) { assert.fail('wrong handler called'); } }()); socket = await common.connect(); socket.emit('message', want); assert.deepEqual(await got, want); }); - const tx = async (socket, message = {}) => await new Promise((resolve, reject) => { + const tx = async (socket:any, message = {}) => await new Promise((resolve, reject) => { const AckErr = class extends Error { - constructor(name, ...args) { super(...args); this.name = name; } + constructor(name: string, ...args:any) { super(...args); this.name = name; } }; socket.emit('message', message, - (errj, val) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val)); + (errj: { + message: string, + name: string, + }, val: any) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val)); }); it('handleMessage with ack (success)', async function () { const want = 'value'; - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleMessage(socket, msg) { return want; } + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + handleMessage(socket:any, msg:any) { return want; } }()); socket = await common.connect(); - const got = await tx(socket, {component: this.test.fullTitle()}); + const got = await tx(socket, {component: this.test!.fullTitle()}); assert.equal(got, want); }); @@ -416,11 +424,11 @@ describe(__filename, function () { const InjectedError = class extends Error { constructor() { super('injected test error'); this.name = 'InjectedError'; } }; - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleMessage(socket, msg) { throw new InjectedError(); } + socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module { + handleMessage(socket:any, msg:any) { throw new InjectedError(); } }()); socket = await common.connect(); - await assert.rejects(tx(socket, {component: this.test.fullTitle()}), new InjectedError()); + await assert.rejects(tx(socket, {component: this.test!.fullTitle()}), new InjectedError()); }); }); }); diff --git a/src/tests/backend/specs/specialpages.js b/src/tests/backend/specs/specialpages.ts similarity index 86% rename from src/tests/backend/specs/specialpages.js rename to src/tests/backend/specs/specialpages.ts index 93c8b3bc4..fbb446c49 100644 --- a/src/tests/backend/specs/specialpages.js +++ b/src/tests/backend/specs/specialpages.ts @@ -1,12 +1,16 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; + const common = require('../common'); const settings = require('../../../node/utils/Settings'); + + describe(__filename, function () { this.timeout(30000); - let agent; - const backups = {}; + let agent:any; + const backups:MapArrayType = {}; before(async function () { agent = await common.init(); }); beforeEach(async function () { backups.settings = {}; diff --git a/src/tests/backend/specs/webaccess.js b/src/tests/backend/specs/webaccess.ts similarity index 92% rename from src/tests/backend/specs/webaccess.js rename to src/tests/backend/specs/webaccess.ts index 23cd2d889..009737c46 100644 --- a/src/tests/backend/specs/webaccess.js +++ b/src/tests/backend/specs/webaccess.ts @@ -1,5 +1,9 @@ 'use strict'; +import {MapArrayType} from "../../../node/types/MapType"; +import {Func} from "mocha"; +import {SettingsUser} from "../../../node/types/SettingsUser"; + const assert = require('assert').strict; const common = require('../common'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); @@ -7,11 +11,11 @@ const settings = require('../../../node/utils/Settings'); describe(__filename, function () { this.timeout(30000); - let agent; - const backups = {}; + let agent:any; + const backups:MapArrayType = {}; const authHookNames = ['preAuthorize', 'authenticate', 'authorize']; const failHookNames = ['preAuthzFailure', 'authnFailure', 'authzFailure', 'authFailure']; - const makeHook = (hookName, hookFn) => ({ + const makeHook = (hookName: string, hookFn:Function) => ({ hook_fn: hookFn, hook_fn_name: `fake_plugin/${hookName}`, hook_name: hookName, @@ -19,6 +23,7 @@ describe(__filename, function () { }); before(async function () { agent = await common.init(); }); + beforeEach(async function () { backups.hooks = {}; for (const hookName of authHookNames.concat(failHookNames)) { @@ -34,8 +39,9 @@ describe(__filename, function () { settings.users = { admin: {password: 'admin-password', is_admin: true}, user: {password: 'user-password'}, - }; + } satisfies SettingsUser; }); + afterEach(async function () { Object.assign(plugins.hooks, backups.hooks); Object.assign(settings, backups.settings); @@ -47,56 +53,67 @@ describe(__filename, function () { settings.requireAuthorization = false; await agent.get('/').expect(200); }); + it('!authn !authz anonymous /admin/ -> 401', async function () { settings.requireAuthentication = false; settings.requireAuthorization = false; await agent.get('/admin/').expect(401); }); + it('authn !authz anonymous / -> 401', async function () { settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').expect(401); }); + it('authn !authz user / -> 200', async function () { settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').auth('user', 'user-password').expect(200); }); + it('authn !authz user /admin/ -> 403', async function () { settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/admin/').auth('user', 'user-password').expect(403); }); + it('authn !authz admin / -> 200', async function () { settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').auth('admin', 'admin-password').expect(200); }); + it('authn !authz admin /admin/ -> 200', async function () { settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/admin/').auth('admin', 'admin-password').expect(200); }); + it('authn authz anonymous /robots.txt -> 200', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/robots.txt').expect(200); }); + it('authn authz user / -> 403', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/').auth('user', 'user-password').expect(403); }); + it('authn authz user /admin/ -> 403', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/admin/').auth('user', 'user-password').expect(403); }); + it('authn authz admin / -> 200', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/').auth('admin', 'admin-password').expect(200); }); + it('authn authz admin /admin/ -> 200', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; @@ -121,16 +138,21 @@ describe(__filename, function () { }); describe('webaccess: preAuthorize, authenticate, and authorize hooks', function () { - let callOrder; + let callOrder:string[]; const Handler = class { - constructor(hookName, suffix) { + private called: boolean; + private readonly hookName: string; + private readonly innerHandle: Function; + private readonly id: string; + private readonly checkContext: Function; + constructor(hookName:string, suffix: string) { this.called = false; this.hookName = hookName; this.innerHandle = () => []; this.id = hookName + suffix; this.checkContext = () => {}; } - handle(hookName, context, cb) { + handle(hookName: string, context: any, cb:Function) { assert.equal(hookName, this.hookName); assert(context != null); assert(context.req != null); @@ -143,7 +165,7 @@ describe(__filename, function () { return cb(this.innerHandle(context)); } }; - const handlers = {}; + const handlers:MapArrayType = {}; beforeEach(async function () { callOrder = []; @@ -170,6 +192,7 @@ describe(__filename, function () { // Note: The preAuthorize hook always runs even if requireAuthorization is false. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); }); + it('bypasses authenticate and authorize hooks when true is returned', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; @@ -177,6 +200,7 @@ describe(__filename, function () { await agent.get('/').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0']); }); + it('bypasses authenticate and authorize hooks when false is returned', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; @@ -184,19 +208,24 @@ describe(__filename, function () { await agent.get('/').expect(403); assert.deepEqual(callOrder, ['preAuthorize_0']); }); + it('bypasses authenticate and authorize hooks when next is called', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; - handlers.preAuthorize[0].innerHandle = ({next}) => next(); + handlers.preAuthorize[0].innerHandle = ({next}:{ + next: Function + }) => next(); await agent.get('/').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0']); }); + it('static content (expressPreSession) bypasses all auth checks', async function () { settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/static/robots.txt').expect(200); assert.deepEqual(callOrder, []); }); + it('cannot grant access to /admin', async function () { handlers.preAuthorize[0].innerHandle = () => [true]; await agent.get('/admin/').expect(401); @@ -210,15 +239,17 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('can deny access to /admin', async function () { handlers.preAuthorize[0].innerHandle = () => [false]; await agent.get('/admin/').auth('admin', 'admin-password').expect(403); assert.deepEqual(callOrder, ['preAuthorize_0']); }); + it('runs preAuthzFailure hook when access is denied', async function () { handlers.preAuthorize[0].innerHandle = () => [false]; let called = false; - plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, {req, res}, cb) => { + plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName: string, {req, res}:any, cb:Function) => { assert.equal(hookName, 'preAuthzFailure'); assert(req != null); assert(res != null); @@ -230,6 +261,7 @@ describe(__filename, function () { await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected'); assert(called); }); + it('returns 500 if an exception is thrown', async function () { handlers.preAuthorize[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').expect(500); @@ -247,6 +279,7 @@ describe(__filename, function () { await agent.get('/').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); }); + it('is called if !requireAuthentication and /admin/*', async function () { settings.requireAuthentication = false; await agent.get('/admin/').expect(401); @@ -255,6 +288,7 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('defers if empty list returned', async function () { await agent.get('/').expect(401); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -262,18 +296,21 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('does not defer if return [true], 200', async function () { - handlers.authenticate[0].innerHandle = ({req}) => { req.session.user = {}; return [true]; }; + handlers.authenticate[0].innerHandle = ({req}:any) => { req.session.user = {}; return [true]; }; await agent.get('/').expect(200); // Note: authenticate_1 was not called because authenticate_0 handled it. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); + it('does not defer if return [false], 401', async function () { handlers.authenticate[0].innerHandle = () => [false]; await agent.get('/').expect(401); // Note: authenticate_1 was not called because authenticate_0 handled it. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); + it('falls back to HTTP basic auth', async function () { await agent.get('/').auth('user', 'user-password').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -281,8 +318,11 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('passes settings.users in context', async function () { - handlers.authenticate[0].checkContext = ({users}) => { + handlers.authenticate[0].checkContext = ({users}:{ + users: SettingsUser + }) => { assert.equal(users, settings.users); }; await agent.get('/').expect(401); @@ -291,8 +331,13 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('passes user, password in context if provided', async function () { - handlers.authenticate[0].checkContext = ({username, password}) => { + handlers.authenticate[0].checkContext = ({username, password}:{ + username: string, + password: string + + }) => { assert.equal(username, 'user'); assert.equal(password, 'user-password'); }; @@ -302,8 +347,12 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('does not pass user, password in context if not provided', async function () { - handlers.authenticate[0].checkContext = ({username, password}) => { + handlers.authenticate[0].checkContext = ({username, password}:{ + username: string, + password: string + }) => { assert(username == null); assert(password == null); }; @@ -313,11 +362,13 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('errors if req.session.user is not created', async function () { handlers.authenticate[0].innerHandle = () => [true]; await agent.get('/').expect(500); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); + it('returns 500 if an exception is thrown', async function () { handlers.authenticate[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').expect(500); @@ -339,6 +390,7 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('is not called if !requireAuthorization (/admin)', async function () { settings.requireAuthorization = false; await agent.get('/admin/').auth('admin', 'admin-password').expect(200); @@ -347,6 +399,7 @@ describe(__filename, function () { 'authenticate_0', 'authenticate_1']); }); + it('defers if empty list returned', async function () { await agent.get('/').auth('user', 'user-password').expect(403); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -356,6 +409,7 @@ describe(__filename, function () { 'authorize_0', 'authorize_1']); }); + it('does not defer if return [true], 200', async function () { handlers.authorize[0].innerHandle = () => [true]; await agent.get('/').auth('user', 'user-password').expect(200); @@ -366,6 +420,7 @@ describe(__filename, function () { 'authenticate_1', 'authorize_0']); }); + it('does not defer if return [false], 403', async function () { handlers.authorize[0].innerHandle = () => [false]; await agent.get('/').auth('user', 'user-password').expect(403); @@ -376,8 +431,11 @@ describe(__filename, function () { 'authenticate_1', 'authorize_0']); }); + it('passes req.path in context', async function () { - handlers.authorize[0].checkContext = ({resource}) => { + handlers.authorize[0].checkContext = ({resource}:{ + resource: string + }) => { assert.equal(resource, '/'); }; await agent.get('/').auth('user', 'user-password').expect(403); @@ -388,6 +446,7 @@ describe(__filename, function () { 'authorize_0', 'authorize_1']); }); + it('returns 500 if an exception is thrown', async function () { handlers.authorize[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').auth('user', 'user-password').expect(500); @@ -402,12 +461,15 @@ describe(__filename, function () { describe('webaccess: authnFailure, authzFailure, authFailure hooks', function () { const Handler = class { - constructor(hookName) { + private hookName: string; + private shouldHandle: boolean; + private called: boolean; + constructor(hookName: string) { this.hookName = hookName; this.shouldHandle = false; this.called = false; } - handle(hookName, context, cb) { + handle(hookName: string, context:any, cb: Function) { assert.equal(hookName, this.hookName); assert(context != null); assert(context.req != null); @@ -421,7 +483,7 @@ describe(__filename, function () { return cb([]); } }; - const handlers = {}; + const handlers:MapArrayType = {}; beforeEach(async function () { failHookNames.forEach((hookName) => { @@ -440,6 +502,7 @@ describe(__filename, function () { assert(!handlers.authzFailure.called); assert(handlers.authFailure.called); }); + it('authn fail, authnFailure handles', async function () { handlers.authnFailure.shouldHandle = true; await agent.get('/').expect(200, 'authnFailure'); @@ -447,6 +510,7 @@ describe(__filename, function () { assert(!handlers.authzFailure.called); assert(!handlers.authFailure.called); }); + it('authn fail, authFailure handles', async function () { handlers.authFailure.shouldHandle = true; await agent.get('/').expect(200, 'authFailure'); @@ -454,6 +518,7 @@ describe(__filename, function () { assert(!handlers.authzFailure.called); assert(handlers.authFailure.called); }); + it('authnFailure trumps authFailure', async function () { handlers.authnFailure.shouldHandle = true; handlers.authFailure.shouldHandle = true; @@ -469,6 +534,7 @@ describe(__filename, function () { assert(handlers.authzFailure.called); assert(handlers.authFailure.called); }); + it('authz fail, authzFailure handles', async function () { handlers.authzFailure.shouldHandle = true; await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); @@ -476,6 +542,7 @@ describe(__filename, function () { assert(handlers.authzFailure.called); assert(!handlers.authFailure.called); }); + it('authz fail, authFailure handles', async function () { handlers.authFailure.shouldHandle = true; await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure'); @@ -483,6 +550,7 @@ describe(__filename, function () { assert(handlers.authzFailure.called); assert(handlers.authFailure.called); }); + it('authzFailure trumps authFailure', async function () { handlers.authzFailure.shouldHandle = true; handlers.authFailure.shouldHandle = true;