mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
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
This commit is contained in:
@@ -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
|
||||
@@ -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<any>} 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<Readable>}
|
||||
* @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<Response>}
|
||||
* @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<string>}
|
||||
* @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<string>}
|
||||
* @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,
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+84
-84
@@ -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')
|
||||
@@ -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 }
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.backend.json",
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"include": ["**/*.js", "**/*.cjs", "**/*.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,
|
||||
},
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user