diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e032c62e..fc3d886b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,7 +39,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 24 registry-url: 'https://registry.npmjs.org' - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87d622f6..b615cfb0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [ 20, 22 ] + node: [ 22, 24 ] env: FORCE_COLOR: 1 name: Node ${{ matrix.node }} diff --git a/.nvmrc b/.nvmrc index 8fdd954d..cabf43b5 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22 \ No newline at end of file +24 \ No newline at end of file diff --git a/packages/express-test/lib/Request.js b/packages/express-test/lib/Request.js index 50bbf5fc..ce05631c 100644 --- a/packages/express-test/lib/Request.js +++ b/packages/express-test/lib/Request.js @@ -1,8 +1,19 @@ -const {default: reqresnext} = require('reqresnext'); +const http = require('http'); +const {Writable, Readable} = require('stream'); +const express = require('express'); const {CookieAccessInfo} = require('cookiejar'); const {parse} = require('url'); const {isJSON, attachFile} = require('./utils'); -const {Readable} = require('stream'); + +class MockSocket extends Writable { + constructor() { + super(); + this.remoteAddress = '127.0.0.1'; + } + _write(chunk, encoding, callback) { + callback(); + } +} class RequestOptions { constructor({method, url, headers, body} = {}) { @@ -70,11 +81,8 @@ class Request { stream(readableStream) { this.reqOptions.body = undefined; - // reqresnext doesn't support streaming, which we need for file uploads. So we need to add some new methods. - // ExpressRequest inherits from http.IncomingMessage which inherits from stream.Readable - // It is that stream that is eventually read by middlewares and converted as JSON, as text, as form data etc. - // Currently the Express middlewares (and multer for file uploads) use the pipe method and/or the event listeners, so we don't need to override other methods - // If we need other methods, we only need to map them to the readable stream. + // Override stream methods on the request to delegate to the provided readable stream. + // Express middlewares (and multer for file uploads) use pipe/event listeners to consume the body. this.reqOptions.methodOverrides = { pipe: (destination) => { readableStream.pipe(destination); @@ -143,10 +151,64 @@ class Request { _getReqRes() { const {app, reqOptions} = this; - const {req, res} = reqresnext({...reqOptions, app}, {app}); + // Create proper Node.js req/res objects using built-in http module. + // MockSocket provides a writable stream so that res.end() properly emits 'finish'. + const socket = new MockSocket(); + const hasStreamOverrides = !!this.reqOptions.methodOverrides; + + // When streaming body data, the socket must appear readable so that + // body-parser's on-finished check doesn't skip the request as "already finished". + if (hasStreamOverrides) { + Object.defineProperty(socket, 'readable', {value: true}); + } + + const req = new http.IncomingMessage(socket); + req.method = reqOptions.method; + req.url = reqOptions.url; + req.headers = {}; + for (const key of Object.keys(reqOptions.headers)) { + req.headers[key.toLowerCase()] = reqOptions.headers[key]; + } + req.headers.host = req.headers.host || 'localhost'; + req.body = reqOptions.body; + req.app = app; + + // When body is pre-parsed (e.g. JSON object), mark it so body-parser skips parsing + if (reqOptions.body !== undefined) { + req._body = true; + } + + const res = new http.ServerResponse(req); + res.assignSocket(socket); + + // Apply Express prototypes so res.send(), res.json(), etc. are available + Object.setPrototypeOf(req, express.request); + Object.setPrototypeOf(res, express.response); + + res.req = req; + req.res = res; + res.app = app; + + // Track written body data for _buildResponse. + // Express calls res.write() then res.end() separately, so we must capture both. + let bodyChunks = []; + const originalWrite = res.write.bind(res); + res.write = function (chunk, encoding, cb) { + if (chunk !== null && chunk !== undefined) { + bodyChunks.push(Buffer.from(chunk, encoding)); + } + return originalWrite(chunk, encoding, cb); + }; + const originalEnd = res.end.bind(res); + res.end = function (chunk, encoding, cb) { + if (chunk !== null && chunk !== undefined) { + bodyChunks.push(Buffer.from(chunk, encoding)); + } + res.body = Buffer.concat(bodyChunks); + return originalEnd(chunk, encoding, cb); + }; - if (this.reqOptions.methodOverrides) { - // Copies all properties from original to copy, including getters and setters + if (hasStreamOverrides) { const props = Object.keys(this.reqOptions.methodOverrides); for (const prop of props) { const descriptor = Object.getOwnPropertyDescriptor(this.reqOptions.methodOverrides, prop); diff --git a/packages/express-test/package.json b/packages/express-test/package.json index 4efb5189..9edaaa95 100644 --- a/packages/express-test/package.json +++ b/packages/express-test/package.json @@ -30,11 +30,13 @@ "multer": "2.1.0", "sinon": "21.0.1" }, + "peerDependencies": { + "express": "^4.0.0 || ^5.0.0" + }, "dependencies": { "@tryghost/jest-snapshot": "^1.2.1", "cookiejar": "2.1.4", "form-data": "4.0.5", - "mime-types": "3.0.2", - "reqresnext": "1.7.0" + "mime-types": "3.0.2" } }