From 05423f1a1d1c1ea2a4b1e02ef8ea21adf8b93d60 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Mon, 13 Jul 2026 13:45:44 +0100 Subject: [PATCH] Merge pull request #34344 from overleaf/copilot/convert-fetch-utils-to-es-modules Convert @overleaf/fetch-utils to TypeScript ESM with Vitest, enable noImplicitAny, fix downstream regressions, and align buildscript generation GitOrigin-RevId: e060152f056cbae31da0650fb22471525835bd5e --- libraries/fetch-utils/.mocharc.cjs | 13 -- libraries/fetch-utils/{index.js => index.ts} | 114 ++++++------ libraries/fetch-utils/package.json | 16 +- .../test/mocha-multi-reporters.cjs | 9 - libraries/fetch-utils/test/setup.js | 4 +- ...{FetchUtilsTests.js => FetchUtils.test.js} | 168 +++++++++--------- .../test/unit/helpers/TestServer.js | 12 +- libraries/fetch-utils/tsconfig.json | 3 + libraries/fetch-utils/vitest.config.ts | 25 +++ libraries/redis-wrapper/test/unit/src/test.js | 8 + yarn.lock | 4 +- 11 files changed, 193 insertions(+), 183 deletions(-) delete mode 100644 libraries/fetch-utils/.mocharc.cjs rename libraries/fetch-utils/{index.js => index.ts} (76%) delete mode 100644 libraries/fetch-utils/test/mocha-multi-reporters.cjs rename libraries/fetch-utils/test/unit/{FetchUtilsTests.js => FetchUtils.test.js} (69%) create mode 100644 libraries/fetch-utils/vitest.config.ts diff --git a/libraries/fetch-utils/.mocharc.cjs b/libraries/fetch-utils/.mocharc.cjs deleted file mode 100644 index bcb7a85d7e..0000000000 --- a/libraries/fetch-utils/.mocharc.cjs +++ /dev/null @@ -1,13 +0,0 @@ -let reporterOptions = {} -if (process.env.CI) { - reporterOptions = { - reporter: require.resolve('mocha-multi-reporters'), - 'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'], - } -} -const all = { - require: 'test/setup.js', - ...reporterOptions, -} - -module.exports = all diff --git a/libraries/fetch-utils/index.js b/libraries/fetch-utils/index.ts similarity index 76% rename from libraries/fetch-utils/index.js rename to libraries/fetch-utils/index.ts index 8a740fe7b0..ce1c06fdad 100644 --- a/libraries/fetch-utils/index.js +++ b/libraries/fetch-utils/index.ts @@ -1,34 +1,28 @@ -const _ = require('lodash') -const { Readable } = require('node:stream') -const OError = require('@overleaf/o-error') -const fetch = require('node-fetch') -const http = require('node:http') -const https = require('node:https') +import _ from 'lodash' +import { Readable } from 'node:stream' +import OError from '@overleaf/o-error' +import fetch from 'node-fetch' +import type { Response } from 'node-fetch' +import http from 'node:http' +import https from 'node:https' -let logger +let logger: { warn: (...args: any[]) => void } | undefined -function setLogger(loggerInstance) { +function setLogger(loggerInstance: { warn: (...args: any[]) => void }) { logger = loggerInstance } -/** - * @import { Response } from 'node-fetch' - */ - /** * Make a request and return the parsed JSON response. * - * @param {string | URL} url - request URL - * @param {any} [opts] - fetch options - * @return {Promise} the parsed JSON response * @throws {RequestFailedError} if the response has a failure status code */ -async function fetchJson(url, opts = {}) { +async function fetchJson(url: string | URL, opts: any = {}) { const { json } = await fetchJsonWithResponse(url, opts) return json } -async function fetchJsonWithResponse(url, opts = {}) { +async function fetchJsonWithResponse(url: string | URL, opts: any = {}) { const { fetchOpts, detachSignal } = parseOpts(opts, url) fetchOpts.headers = fetchOpts.headers ?? {} fetchOpts.headers.Accept = fetchOpts.headers.Accept ?? 'application/json' @@ -48,17 +42,14 @@ async function fetchJsonWithResponse(url, opts = {}) { * * If the response body is destroyed, the request is aborted. * - * @param {string | URL} url - request URL - * @param {any} [opts] - fetch options - * @return {Promise} * @throws {RequestFailedError} if the response has a failure status code */ -async function fetchStream(url, opts = {}) { +async function fetchStream(url: string | URL, opts: any = {}) { const { stream } = await fetchStreamWithResponse(url, opts) return stream } -async function fetchStreamWithResponse(url, opts = {}) { +async function fetchStreamWithResponse(url: string | URL, opts: any = {}) { const { fetchOpts, abortController, detachSignal } = parseOpts(opts, url) const response = await performRequest(url, fetchOpts, detachSignal) @@ -76,12 +67,9 @@ async function fetchStreamWithResponse(url, opts = {}) { /** * Make a request and discard the response. * - * @param {string | URL} url - request URL - * @param {any} [opts] - fetch options - * @return {Promise} * @throws {RequestFailedError} if the response has a failure status code */ -async function fetchNothing(url, opts = {}) { +async function fetchNothing(url: string | URL, opts: any = {}) { const { fetchOpts, detachSignal } = parseOpts(opts, url) const response = await performRequest(url, fetchOpts, detachSignal) if (!response.ok) { @@ -95,12 +83,9 @@ async function fetchNothing(url, opts = {}) { /** * Make a request and extract the redirect from the response. * - * @param {string | URL} url - request URL - * @param {any} [opts] - fetch options - * @return {Promise} * @throws {RequestFailedError} if the response has a non redirect status code or missing Location header */ -async function fetchRedirect(url, opts = {}) { +async function fetchRedirect(url: string | URL, opts: any = {}) { const { location } = await fetchRedirectWithResponse(url, opts) return location } @@ -108,12 +93,9 @@ async function fetchRedirect(url, opts = {}) { /** * Make a request and extract the redirect from the response. * - * @param {string | URL} url - request URL - * @param {object} opts - fetch options - * @return {Promise<{location: string, response: Response}>} * @throws {RequestFailedError} if the response has a non redirect status code or missing Location header */ -async function fetchRedirectWithResponse(url, opts = {}) { +async function fetchRedirectWithResponse(url: string | URL, opts: any = {}) { const { fetchOpts, detachSignal } = parseOpts(opts, url) fetchOpts.redirect = 'manual' const response = await performRequest(url, fetchOpts, detachSignal) @@ -137,17 +119,14 @@ async function fetchRedirectWithResponse(url, opts = {}) { /** * Make a request and return a string. * - * @param {string | URL} url - request URL - * @param {any} [opts] - fetch options - * @return {Promise} * @throws {RequestFailedError} if the response has a failure status code */ -async function fetchString(url, opts = {}) { +async function fetchString(url: string | URL, opts: any = {}) { const { body } = await fetchStringWithResponse(url, opts) return body } -async function fetchStringWithResponse(url, opts = {}) { +async function fetchStringWithResponse(url: string | URL, opts: any = {}) { const { fetchOpts, detachSignal } = parseOpts(opts, url) const response = await performRequest(url, fetchOpts, detachSignal) if (!response.ok) { @@ -159,7 +138,15 @@ async function fetchStringWithResponse(url, opts = {}) { } class RequestFailedError extends OError { - constructor(url, opts, response, body) { + response: Response + body?: string + + constructor( + url: string | URL, + opts: any, + response: Response, + body: string | null + ) { super('request failed', { url, method: opts.method ?? 'GET', @@ -173,7 +160,7 @@ class RequestFailedError extends OError { } } -function parseOpts(opts, url) { +function parseOpts(opts: any, url: string | URL) { const fetchOpts = _.omit(opts, ['json', 'signal', 'basicAuth']) if (opts.json) { setupJsonBody(fetchOpts, opts.json) @@ -188,7 +175,7 @@ function parseOpts(opts, url) { if (opts.signal) { detachSignal = abortOnSignal(abortController, opts.signal) } else { - let overTimeoutStart + let overTimeoutStart: bigint | undefined const stack = new Error().stack const timeout = setTimeout(() => { overTimeoutStart = process.hrtime.bigint() @@ -215,20 +202,20 @@ function parseOpts(opts, url) { return { fetchOpts, abortController, detachSignal } } -function setupJsonBody(fetchOpts, json) { +function setupJsonBody(fetchOpts: any, json: any) { fetchOpts.body = JSON.stringify(json) fetchOpts.headers = fetchOpts.headers ?? {} fetchOpts.headers['Content-Type'] = 'application/json' } -function setupBasicAuth(fetchOpts, basicAuth) { +function setupBasicAuth(fetchOpts: any, basicAuth: any) { fetchOpts.headers = fetchOpts.headers ?? {} fetchOpts.headers.Authorization = 'Basic ' + Buffer.from(`${basicAuth.user}:${basicAuth.password}`).toString('base64') } -function abortOnSignal(abortController, signal) { +function abortOnSignal(abortController: AbortController, signal: AbortSignal) { const listener = () => { abortController.abort(signal.reason) } @@ -241,7 +228,10 @@ function abortOnSignal(abortController, signal) { } } -function abortOnDestroyedRequest(abortController, stream) { +function abortOnDestroyedRequest( + abortController: AbortController, + stream: any +) { stream.on('close', () => { if (!stream.readableEnded) { abortController.abort() @@ -249,7 +239,10 @@ function abortOnDestroyedRequest(abortController, stream) { }) } -function abortOnDestroyedResponse(abortController, response) { +function abortOnDestroyedResponse( + abortController: AbortController, + response: Response +) { response.body.on('close', () => { if (!response.bodyUsed) { abortController.abort() @@ -257,11 +250,15 @@ function abortOnDestroyedResponse(abortController, response) { }) } -async function performRequest(url, fetchOpts, detachSignal) { +async function performRequest( + url: string | URL, + fetchOpts: any, + detachSignal: () => void +) { let response try { response = await fetch(url, fetchOpts) - } catch (err) { + } catch (err: any) { detachSignal() if (fetchOpts.body instanceof Readable) { fetchOpts.body.destroy() @@ -282,17 +279,14 @@ async function performRequest(url, fetchOpts, detachSignal) { return response } -async function discardResponseBody(response) { +async function discardResponseBody(response: Response) { // eslint-disable-next-line no-unused-vars for await (const chunk of response.body) { // discard the body } } -/** - * @param {Response} response - */ -async function maybeGetResponseBody(response) { +async function maybeGetResponseBody(response: Response) { try { return await response.text() } catch (err) { @@ -303,18 +297,18 @@ async function maybeGetResponseBody(response) { // Define custom http and https agents with support for connect timeouts class ConnectTimeoutError extends OError { - constructor(options) { + constructor(options: any) { super('connect timeout', options) } } -function withTimeout(createConnection, options, callback) { +function withTimeout(createConnection: any, options: any, callback: any) { if (options.connectTimeout) { // Wrap createConnection in a timeout const timer = setTimeout(() => { socket.destroy(new ConnectTimeoutError(options)) }, options.connectTimeout) - const socket = createConnection(options, (err, stream) => { + const socket = createConnection(options, (err: any, stream: any) => { clearTimeout(timer) callback(err, stream) }) @@ -326,17 +320,17 @@ function withTimeout(createConnection, options, callback) { } class CustomHttpAgent extends http.Agent { - createConnection(options, callback) { + createConnection(options: any, callback: any) { return withTimeout(super.createConnection.bind(this), options, callback) } } class CustomHttpsAgent extends https.Agent { - createConnection(options, callback) { + createConnection(options: any, callback: any) { return withTimeout(super.createConnection.bind(this), options, callback) } } -module.exports = { +export { fetchJson, fetchJsonWithResponse, fetchStream, diff --git a/libraries/fetch-utils/package.json b/libraries/fetch-utils/package.json index dce157ee98..c7d18f4995 100644 --- a/libraries/fetch-utils/package.json +++ b/libraries/fetch-utils/package.json @@ -2,13 +2,19 @@ "name": "@overleaf/fetch-utils", "version": "0.1.0", "description": "utilities for node-fetch", - "main": "index.js", + "main": "index.ts", + "exports": { + "import": "./index.ts", + "require": "./index.ts", + "default": "./index.ts" + }, + "type": "module", "scripts": { "test": "yarn run lint && yarn run types:check && yarn run test:unit", "lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 .", "lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .", "test:ci": "yarn run test:unit", - "test:unit": "mocha --exit test/**/*.{js,cjs}", + "test:unit": "vitest --config vitest.config.ts", "types:check": "tsc --noEmit" }, "author": "Overleaf (https://www.overleaf.com)", @@ -18,11 +24,9 @@ "chai": "^4.3.6", "chai-as-promised": "^7.1.1", "express": "4.22.1", - "mocha": "^11.1.0", - "mocha-junit-reporter": "^2.2.1", - "mocha-multi-reporters": "^1.5.1", "selfsigned": "^5.5.0", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "vitest": "4.1.5" }, "dependencies": { "@overleaf/o-error": "workspace:*", diff --git a/libraries/fetch-utils/test/mocha-multi-reporters.cjs b/libraries/fetch-utils/test/mocha-multi-reporters.cjs deleted file mode 100644 index 674ff0e1cd..0000000000 --- a/libraries/fetch-utils/test/mocha-multi-reporters.cjs +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - reporterEnabled: 'spec, mocha-junit-reporter', - mochaJunitReporterReporterOptions: { - mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`, - includePending: true, - jenkinsMode: true, - output: true, - }, -} diff --git a/libraries/fetch-utils/test/setup.js b/libraries/fetch-utils/test/setup.js index 09068185a8..eab39e0267 100644 --- a/libraries/fetch-utils/test/setup.js +++ b/libraries/fetch-utils/test/setup.js @@ -1,4 +1,4 @@ -const chai = require('chai') -const chaiAsPromised = require('chai-as-promised') +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' chai.use(chaiAsPromised) diff --git a/libraries/fetch-utils/test/unit/FetchUtilsTests.js b/libraries/fetch-utils/test/unit/FetchUtils.test.js similarity index 69% rename from libraries/fetch-utils/test/unit/FetchUtilsTests.js rename to libraries/fetch-utils/test/unit/FetchUtils.test.js index 8a3f45df1b..4f0a5fcd59 100644 --- a/libraries/fetch-utils/test/unit/FetchUtilsTests.js +++ b/libraries/fetch-utils/test/unit/FetchUtils.test.js @@ -1,13 +1,15 @@ -const { expect } = require('chai') -const fs = require('node:fs') -const events = require('node:events') -const { FetchError, AbortError } = require('node-fetch') -const { Readable } = require('node:stream') -const { pipeline } = require('node:stream/promises') -const { once } = require('node:events') -const { TestServer } = require('./helpers/TestServer') -const selfsigned = require('selfsigned') -const { +import { expect } from 'chai' +import dns from 'node:dns' +import { once } from 'node:events' +import events from 'node:events' +import fs from 'node:fs' +import { pipeline } from 'node:stream/promises' +import { Readable } from 'node:stream' +import { describe, beforeAll, beforeEach, afterAll, it } from 'vitest' +import { AbortError, FetchError } from 'node-fetch' +import selfsigned from 'selfsigned' +import { TestServer } from './helpers/TestServer.js' +import { fetchJson, fetchStream, fetchNothing, @@ -16,12 +18,14 @@ const { RequestFailedError, CustomHttpAgent, CustomHttpsAgent, -} = require('../..') +} from '../../index.ts' const HTTP_PORT = 30001 const HTTPS_PORT = 30002 -const dns = require('node:dns') +const url = pathname => `http://example.com:${HTTP_PORT}${pathname}` +const httpsUrl = pathname => `https://example.com:${HTTPS_PORT}${pathname}` + const _originalLookup = dns.lookup // Custom DNS resolver function dns.lookup = (hostname, options, callback) => { @@ -39,39 +43,38 @@ dns.lookup = (hostname, options, callback) => { } describe('fetch-utils', function () { + let server let PUBLIC_CERT - before(async function () { - this.server = new TestServer() + beforeAll(async function () { + server = new TestServer() const attrs = [{ name: 'commonName', value: 'example.com' }] const pems = await selfsigned.generate(attrs, { days: 365, keySize: 2048 }) const PRIVATE_KEY = pems.private PUBLIC_CERT = pems.cert - await this.server.start(HTTP_PORT, HTTPS_PORT, { + await server.start(HTTP_PORT, HTTPS_PORT, { key: PRIVATE_KEY, cert: PUBLIC_CERT, }) - this.url = path => `http://example.com:${HTTP_PORT}${path}` - this.httpsUrl = path => `https://example.com:${HTTPS_PORT}${path}` }) beforeEach(function () { - this.server.lastReq = undefined + server.lastReq = undefined }) - after(async function () { - await this.server.stop() + afterAll(async function () { + await server.stop() }) describe('fetchJson', function () { it('parses a JSON response', async function () { - const json = await fetchJson(this.url('/json/hello')) + const json = await fetchJson(url('/json/hello')) expect(json).to.deep.equal({ msg: 'hello' }) }) it('parses JSON in the request', async function () { - const json = await fetchJson(this.url('/json/add'), { + const json = await fetchJson(url('/json/add'), { method: 'POST', json: { a: 2, b: 3 }, }) @@ -79,7 +82,7 @@ describe('fetch-utils', function () { }) it('accepts stringified JSON as body', async function () { - const json = await fetchJson(this.url('/json/add'), { + const json = await fetchJson(url('/json/add'), { method: 'POST', body: JSON.stringify({ a: 2, b: 3 }), headers: { 'Content-Type': 'application/json' }, @@ -88,40 +91,37 @@ describe('fetch-utils', function () { }) it('throws a FetchError when the payload is not JSON', async function () { - await expect(fetchJson(this.url('/hello'))).to.be.rejectedWith(FetchError) + await expect(fetchJson(url('/hello'))).to.be.rejectedWith(FetchError) }) it('aborts the request if JSON parsing fails', async function () { - await expect(fetchJson(this.url('/large'))).to.be.rejectedWith(FetchError) - await expectRequestAborted(this.server.lastReq) + await expect(fetchJson(url('/large'))).to.be.rejectedWith(FetchError) + await expectRequestAborted(server.lastReq) }) it('handles errors when the payload is JSON', async function () { - await expect(fetchJson(this.url('/json/500'))).to.be.rejectedWith( + await expect(fetchJson(url('/json/500'))).to.be.rejectedWith( RequestFailedError ) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it('handles errors when the payload is not JSON', async function () { - await expect(fetchJson(this.url('/500'))).to.be.rejectedWith( + await expect(fetchJson(url('/500'))).to.be.rejectedWith( RequestFailedError ) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it('supports abort signals', async function () { await expect( - abortOnceReceived( - signal => fetchJson(this.url('/hang'), { signal }), - this.server - ) + abortOnceReceived(signal => fetchJson(url('/hang'), { signal }), server) ).to.be.rejectedWith(AbortError) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it('supports basic auth', async function () { - const json = await fetchJson(this.url('/json/basic-auth'), { + const json = await fetchJson(url('/json/basic-auth'), { basicAuth: { user: 'user', password: 'pass' }, }) expect(json).to.deep.equal({ key: 'verysecret' }) @@ -129,7 +129,7 @@ describe('fetch-utils', function () { it("destroys the request body if it doesn't get consumed", async function () { const stream = Readable.from(infiniteIterator()) - await fetchJson(this.url('/json/ignore-request'), { + await fetchJson(url('/json/ignore-request'), { method: 'POST', body: stream, }) @@ -139,57 +139,57 @@ describe('fetch-utils', function () { describe('fetchStream', function () { it('returns a stream', async function () { - const stream = await fetchStream(this.url('/large')) + const stream = await fetchStream(url('/large')) const text = await streamToString(stream) - expect(text).to.equal(this.server.largePayload) + expect(text).to.equal(server.largePayload) }) it('aborts the request when the stream is destroyed', async function () { - const stream = await fetchStream(this.url('/large')) + const stream = await fetchStream(url('/large')) stream.destroy() - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it('aborts the request when the request body is destroyed before transfer', async function () { const stream = Readable.from(infiniteIterator()) - const promise = fetchStream(this.url('/hang'), { + const promise = fetchStream(url('/hang'), { method: 'POST', body: stream, }) stream.destroy() await expect(promise).to.be.rejectedWith(AbortError) await wait(80) - expect(this.server.lastReq).to.be.undefined + expect(server.lastReq).to.be.undefined }) it('aborts the request when the request body is destroyed during transfer', async function () { const stream = Readable.from(infiniteIterator()) // Note: this test won't work on `/hang` - const promise = fetchStream(this.url('/sink'), { + const promise = fetchStream(url('/sink'), { method: 'POST', body: stream, }) - await once(this.server.events, 'request-received') + await once(server.events, 'request-received') stream.destroy() await expect(promise).to.be.rejectedWith(AbortError) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it('handles errors', async function () { - await expect(fetchStream(this.url('/500'))).to.be.rejectedWith( + await expect(fetchStream(url('/500'))).to.be.rejectedWith( RequestFailedError ) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it('supports abort signals', async function () { await expect( abortOnceReceived( - signal => fetchStream(this.url('/hang'), { signal }), - this.server + signal => fetchStream(url('/hang'), { signal }), + server ) ).to.be.rejectedWith(AbortError) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it('destroys the request body when an error occurs', async function () { @@ -197,12 +197,12 @@ describe('fetch-utils', function () { await expect( abortOnceReceived( signal => - fetchStream(this.url('/hang'), { + fetchStream(url('/hang'), { method: 'POST', body: stream, signal, }), - this.server + server ) ).to.be.rejectedWith(AbortError) expect(stream.destroyed).to.be.true @@ -211,7 +211,7 @@ describe('fetch-utils', function () { it('detaches from signal on success', async function () { const signal = AbortSignal.timeout(10_000) for (let i = 0; i < 20; i++) { - const s = await fetchStream(this.url('/hello'), { signal }) + const s = await fetchStream(url('/hello'), { signal }) expect(events.getEventListeners(signal, 'abort')).to.have.length(1) await pipeline(s, fs.createWriteStream('/dev/null')) expect(events.getEventListeners(signal, 'abort')).to.have.length(0) @@ -222,7 +222,7 @@ describe('fetch-utils', function () { const signal = AbortSignal.timeout(10_000) for (let i = 0; i < 20; i++) { try { - await fetchStream(this.url('/500'), { signal }) + await fetchStream(url('/500'), { signal }) } catch (err) { if (err instanceof RequestFailedError && err.response.status === 500) continue @@ -236,55 +236,55 @@ describe('fetch-utils', function () { describe('fetchNothing', function () { it('closes the connection', async function () { - await fetchNothing(this.url('/large')) - await expectRequestAborted(this.server.lastReq) + await fetchNothing(url('/large')) + await expectRequestAborted(server.lastReq) }) it('aborts the request when the request body is destroyed before transfer', async function () { const stream = Readable.from(infiniteIterator()) - const promise = fetchNothing(this.url('/hang'), { + const promise = fetchNothing(url('/hang'), { method: 'POST', body: stream, }) stream.destroy() await expect(promise).to.be.rejectedWith(AbortError) - expect(this.server.lastReq).to.be.undefined + expect(server.lastReq).to.be.undefined }) it('aborts the request when the request body is destroyed during transfer', async function () { const stream = Readable.from(infiniteIterator()) // Note: this test won't work on `/hang` - const promise = fetchNothing(this.url('/sink'), { + const promise = fetchNothing(url('/sink'), { method: 'POST', body: stream, }) - await once(this.server.events, 'request-received') + await once(server.events, 'request-received') stream.destroy() await expect(promise).to.be.rejectedWith(AbortError) await wait(80) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it("doesn't abort the request if the request body ends normally", async function () { const stream = Readable.from('hello there') - await fetchNothing(this.url('/sink'), { method: 'POST', body: stream }) + await fetchNothing(url('/sink'), { method: 'POST', body: stream }) }) it('handles errors', async function () { - await expect(fetchNothing(this.url('/500'))).to.be.rejectedWith( + await expect(fetchNothing(url('/500'))).to.be.rejectedWith( RequestFailedError ) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it('supports abort signals', async function () { await expect( abortOnceReceived( - signal => fetchNothing(this.url('/hang'), { signal }), - this.server + signal => fetchNothing(url('/hang'), { signal }), + server ) ).to.be.rejectedWith(AbortError) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it('destroys the request body when an error occurs', async function () { @@ -292,12 +292,12 @@ describe('fetch-utils', function () { await expect( abortOnceReceived( signal => - fetchNothing(this.url('/hang'), { + fetchNothing(url('/hang'), { method: 'POST', body: stream, signal, }), - this.server + server ) ).to.be.rejectedWith(AbortError) expect(stream.destroyed).to.be.true @@ -306,52 +306,52 @@ describe('fetch-utils', function () { describe('fetchString', function () { it('returns a string', async function () { - const body = await fetchString(this.url('/hello')) + const body = await fetchString(url('/hello')) expect(body).to.equal('hello') }) it('handles errors', async function () { - await expect(fetchString(this.url('/500'))).to.be.rejectedWith( + await expect(fetchString(url('/500'))).to.be.rejectedWith( RequestFailedError ) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) }) describe('fetchRedirect', function () { it('returns the immediate redirect', async function () { - const body = await fetchRedirect(this.url('/redirect/1')) - expect(body).to.equal(this.url('/redirect/2')) + const body = await fetchRedirect(url('/redirect/1')) + expect(body).to.equal(url('/redirect/2')) }) it('rejects status 200', async function () { - await expect(fetchRedirect(this.url('/hello'))).to.be.rejectedWith( + await expect(fetchRedirect(url('/hello'))).to.be.rejectedWith( RequestFailedError ) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it('rejects empty redirect', async function () { - await expect(fetchRedirect(this.url('/redirect/empty-location'))) + await expect(fetchRedirect(url('/redirect/empty-location'))) .to.be.rejectedWith(RequestFailedError) .and.eventually.have.property('cause') .and.to.have.property('message') .to.equal('missing Location response header on 3xx response') - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) it('handles errors', async function () { - await expect(fetchRedirect(this.url('/500'))).to.be.rejectedWith( + await expect(fetchRedirect(url('/500'))).to.be.rejectedWith( RequestFailedError ) - await expectRequestAborted(this.server.lastReq) + await expectRequestAborted(server.lastReq) }) }) describe('CustomHttpAgent', function () { it('makes an http request successfully', async function () { const agent = new CustomHttpAgent({ connectTimeout: 100 }) - const body = await fetchString(this.url('/hello'), { agent }) + const body = await fetchString(url('/hello'), { agent }) expect(body).to.equal('hello') }) @@ -372,7 +372,7 @@ describe('fetch-utils', function () { connectTimeout: 100, ca: PUBLIC_CERT, }) - const body = await fetchString(this.httpsUrl('/hello'), { agent }) + const body = await fetchString(httpsUrl('/hello'), { agent }) expect(body).to.equal('hello') }) @@ -380,7 +380,7 @@ describe('fetch-utils', function () { const agent = new CustomHttpsAgent({ connectTimeout: 100, }) - await expect(fetchString(this.httpsUrl('/hello'), { agent })) + await expect(fetchString(httpsUrl('/hello'), { agent })) .to.be.rejectedWith(FetchError) .and.eventually.have.property('code') .and.to.equal('DEPTH_ZERO_SELF_SIGNED_CERT') diff --git a/libraries/fetch-utils/test/unit/helpers/TestServer.js b/libraries/fetch-utils/test/unit/helpers/TestServer.js index 04bcac92d3..a151f72d71 100644 --- a/libraries/fetch-utils/test/unit/helpers/TestServer.js +++ b/libraries/fetch-utils/test/unit/helpers/TestServer.js @@ -1,8 +1,8 @@ -const express = require('express') -const { EventEmitter } = require('node:events') -const http = require('node:http') -const https = require('node:https') -const { promisify } = require('node:util') +import express from 'express' +import { EventEmitter } from 'node:events' +import http from 'node:http' +import https from 'node:https' +import { promisify } from 'node:util' class TestServer { constructor() { @@ -135,4 +135,4 @@ class TestServer { } } -module.exports = { TestServer } +export { TestServer } diff --git a/libraries/fetch-utils/tsconfig.json b/libraries/fetch-utils/tsconfig.json index 5c3833f9db..7dc569e499 100644 --- a/libraries/fetch-utils/tsconfig.json +++ b/libraries/fetch-utils/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.backend.json", + "compilerOptions": { + "noImplicitAny": true + }, "include": ["**/*.js", "**/*.cjs", "**/*.ts"] } diff --git a/libraries/fetch-utils/vitest.config.ts b/libraries/fetch-utils/vitest.config.ts new file mode 100644 index 0000000000..c19ff9733a --- /dev/null +++ b/libraries/fetch-utils/vitest.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, ViteUserConfig } from 'vitest/config' + +let reporterOptions: ViteUserConfig['test'] = {} +if (process.env.CI) { + reporterOptions = { + reporters: [ + 'default', + [ + 'junit', + { + classnameTemplate: `Unit tests.{filename}`, + }, + ], + ], + outputFile: 'reports/junit-vitest-unit.xml', + } +} +export default defineConfig({ + test: { + include: ['test/unit/**/*.test.{js,ts}'], + setupFiles: ['./test/setup.js'], + isolate: false, + ...reporterOptions, + }, +}) diff --git a/libraries/redis-wrapper/test/unit/src/test.js b/libraries/redis-wrapper/test/unit/src/test.js index a71dcf0230..b8a9fbeca7 100644 --- a/libraries/redis-wrapper/test/unit/src/test.js +++ b/libraries/redis-wrapper/test/unit/src/test.js @@ -16,6 +16,12 @@ const { expect } = require('chai') describe('index', function () { beforeEach(function () { let Cluster, IoRedis, ioredisConstructor + const loggerMock = { + error() {}, + warn() {}, + info() {}, + debug() {}, + } this.settings = {} this.ioredisConstructor = ioredisConstructor = sinon.stub() @@ -51,6 +57,7 @@ describe('index', function () { this.redis = SandboxedModule.require(modulePath, { requires: { ioredis: this.ioredis, + '@overleaf/logger': loggerMock, }, globals: { process, @@ -64,6 +71,7 @@ describe('index', function () { '@overleaf/metrics': { inc() {}, }, + '@overleaf/logger': loggerMock, }, globals: { process, diff --git a/yarn.lock b/yarn.lock index f25e2f198a..23fa9e14e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6841,12 +6841,10 @@ __metadata: chai-as-promised: "npm:^7.1.1" express: "npm:4.22.1" lodash: "npm:^4.18.1" - mocha: "npm:^11.1.0" - mocha-junit-reporter: "npm:^2.2.1" - mocha-multi-reporters: "npm:^1.5.1" node-fetch: "npm:^2.7.0" selfsigned: "npm:^5.5.0" typescript: "npm:^5.0.4" + vitest: "npm:4.1.5" languageName: unknown linkType: soft