From a7655d3c201b167a8d9298e794133c5dd65c1418 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Wed, 3 Mar 2021 13:21:52 -0500 Subject: [PATCH] feat: refactor cy.intercept internals to generic event-based system (#15255) --- NOTES.md | 9 + .../integration/commands/net_stubbing_spec.ts | 44 ++-- .../driver/src/cy/net-stubbing/add-command.ts | 42 ++-- ...{request-complete.ts => after-response.ts} | 22 +- ...{request-received.ts => before-request.ts} | 100 +++++---- .../src/cy/net-stubbing/events/index.ts | 82 +++++-- .../{response-received.ts => response.ts} | 68 +++--- .../cy/net-stubbing/static-response-utils.ts | 19 +- packages/driver/src/cypress/events.ts | 2 + packages/net-stubbing/lib/external-types.ts | 44 ++-- packages/net-stubbing/lib/internal-types.ts | 73 +++---- .../net-stubbing/lib/server/driver-events.ts | 70 ++++-- packages/net-stubbing/lib/server/index.ts | 6 +- .../lib/server/intercept-error.ts | 34 --- .../lib/server/intercept-request.ts | 204 ------------------ .../lib/server/intercept-response.ts | 123 ----------- .../lib/server/intercepted-request.ts | 121 +++++++++++ .../lib/server/middleware/error.ts | 31 +++ .../lib/server/middleware/request.ts | 169 +++++++++++++++ .../lib/server/middleware/response.ts | 81 +++++++ .../net-stubbing/lib/server/route-matching.ts | 3 + packages/net-stubbing/lib/server/state.ts | 2 + packages/net-stubbing/lib/server/types.ts | 40 +--- packages/net-stubbing/lib/server/util.ts | 15 +- packages/net-stubbing/package.json | 1 + .../test/integration/net-stubbing.spec.ts | 90 ++++---- .../cypress/fixtures/errors/intercept_spec.ts | 43 ++++ .../integration/reporter.errors.spec.js | 20 ++ packages/runner/cypress/support/helpers.js | 3 + packages/server/lib/socket-base.ts | 1 - yarn.lock | 87 +++++--- 31 files changed, 926 insertions(+), 723 deletions(-) create mode 100644 NOTES.md rename packages/driver/src/cy/net-stubbing/events/{request-complete.ts => after-response.ts} (67%) rename packages/driver/src/cy/net-stubbing/events/{request-received.ts => before-request.ts} (74%) rename packages/driver/src/cy/net-stubbing/events/{response-received.ts => response.ts} (72%) delete mode 100644 packages/net-stubbing/lib/server/intercept-error.ts delete mode 100644 packages/net-stubbing/lib/server/intercept-request.ts delete mode 100644 packages/net-stubbing/lib/server/intercept-response.ts create mode 100644 packages/net-stubbing/lib/server/intercepted-request.ts create mode 100644 packages/net-stubbing/lib/server/middleware/error.ts create mode 100644 packages/net-stubbing/lib/server/middleware/request.ts create mode 100644 packages/net-stubbing/lib/server/middleware/response.ts create mode 100644 packages/runner/cypress/fixtures/errors/intercept_spec.ts diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000000..ddc59e7215 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,9 @@ +* New `RouteMatcher` option: `middleware: boolean` + * With `middleware: true`, will be called in the order they are defined and chained. + * With `middleware: true`, only dynamic handlers are supported - makes no sense to support `cy.intercept({ middleware: true }, staticResponse)` + * BREAKING CHANGE: `middleware: falsy` handlers will not be chained. For any given request, the most-recently-defined handler is always the one used. +* `req` is now an `EventEmitter` (regardless of `middleware` setting) +* Events on `req`: + * `request - ()` - Request will be sent outgoing. If the response has already been fulfilled by `req.reply`, this event will not be emitted. + * `before-response - (res)` - Response was received. Emitted before the response handler is run. + * `response - (res)` - Response will be sent to the browser. If a response has been supplied via `res.send`, this event will not be emitted. \ No newline at end of file diff --git a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts index 9fe2f41983..0c4ca4d28e 100644 --- a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts +++ b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts @@ -1,4 +1,4 @@ -describe('network stubbing', { retries: 2 }, function () { +describe('network stubbing', { retries: { runMode: 2, openMode: 0 } }, function () { const { $, _, sinon, state, Promise } = Cypress beforeEach(function () { @@ -997,8 +997,8 @@ describe('network stubbing', { retries: 2 }, function () { it('can delay and throttle a StaticResponse', function (done) { const payload = 'A'.repeat(10 * 1024) const throttleKbps = 10 - const delay = 250 - const expectedSeconds = payload.length / (1024 * throttleKbps) + delay / 1000 + const delayMs = 250 + const expectedSeconds = payload.length / (1024 * throttleKbps) + delayMs / 1000 cy.intercept('/timeout', (req) => { this.start = Date.now() @@ -1007,7 +1007,7 @@ describe('network stubbing', { retries: 2 }, function () { statusCode: 200, body: payload, throttleKbps, - delay, + delayMs, }) }).then(() => { return $.get('/timeout').then((responseText) => { @@ -1019,33 +1019,15 @@ describe('network stubbing', { retries: 2 }, function () { }) }) - it('can delay with deprecated delayMs param', function (done) { - const delay = 250 - - cy.intercept('/timeout', (req) => { - this.start = Date.now() - - req.reply({ - delay, - }) - }).then(() => { - return $.get('/timeout').then((responseText) => { - expect(Date.now() - this.start).to.be.closeTo(250 + 100, 100) - - done() - }) - }) - }) - // @see https://github.com/cypress-io/cypress/issues/14446 it('should delay the same amount on every response', () => { - const delay = 250 + const delayMs = 250 const testDelay = () => { const start = Date.now() return $.get('/timeout').then((responseText) => { - expect(Date.now() - start).to.be.closeTo(delay, 50) + expect(Date.now() - start).to.be.closeTo(delayMs, 50) expect(responseText).to.eq('foo') }) } @@ -1053,7 +1035,7 @@ describe('network stubbing', { retries: 2 }, function () { cy.intercept('/timeout', { statusCode: 200, body: 'foo', - delay, + delayMs, }).as('get') .then(() => testDelay()).wait('@get') .then(() => testDelay()).wait('@get') @@ -1636,15 +1618,15 @@ describe('network stubbing', { retries: 2 }, function () { const payload = 'A'.repeat(10 * 1024) const kbps = 20 let expectedSeconds = payload.length / (1024 * kbps) - const delay = 500 + const delayMs = 500 - expectedSeconds += delay / 1000 + expectedSeconds += delayMs / 1000 cy.intercept('/timeout', (req) => { req.reply((res) => { this.start = Date.now() - res.throttle(kbps).delay(delay).send({ + res.throttle(kbps).delay(delayMs).send({ statusCode: 200, body: payload, }) @@ -1880,8 +1862,8 @@ describe('network stubbing', { retries: 2 }, function () { it('can delay and throttle', function (done) { const payload = 'A'.repeat(10 * 1024) const throttleKbps = 50 - const delay = 50 - const expectedSeconds = payload.length / (1024 * throttleKbps) + delay / 1000 + const delayMs = 50 + const expectedSeconds = payload.length / (1024 * throttleKbps) + delayMs / 1000 cy.intercept('/timeout', (req) => { req.reply((res) => { @@ -1892,7 +1874,7 @@ describe('network stubbing', { retries: 2 }, function () { statusCode: 200, body: payload, throttleKbps, - delay, + delayMs, }) }) }).then(() => { diff --git a/packages/driver/src/cy/net-stubbing/add-command.ts b/packages/driver/src/cy/net-stubbing/add-command.ts index 262d38522c..9a87171718 100644 --- a/packages/driver/src/cy/net-stubbing/add-command.ts +++ b/packages/driver/src/cy/net-stubbing/add-command.ts @@ -10,7 +10,7 @@ import { DICT_STRING_MATCHER_FIELDS, AnnotatedRouteMatcherOptions, AnnotatedStringMatcher, - NetEventFrames, + NetEvent, StringMatcher, NumberMatcher, } from '@packages/net-stubbing/lib/types' @@ -217,31 +217,25 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, let staticResponse: StaticResponse | undefined = undefined let hasInterceptor = false - switch (true) { - case isHttpRequestInterceptor(handler): - hasInterceptor = true - break - case _.isUndefined(handler): - // user is doing something like cy.intercept('foo').as('foo') to wait on a URL - break - case _.isString(handler): - staticResponse = { body: handler } - break - case _.isObjectLike(handler): - if (!hasStaticResponseKeys(handler)) { - // the user has not supplied any of the StaticResponse keys, assume it's a JSON object - // that should become the body property - handler = { - body: handler, - } + if (isHttpRequestInterceptor(handler)) { + hasInterceptor = true + } else if (_.isString(handler)) { + staticResponse = { body: handler } + } else if (_.isObjectLike(handler)) { + if (!hasStaticResponseKeys(handler)) { + // the user has not supplied any of the StaticResponse keys, assume it's a JSON object + // that should become the body property + handler = { + body: handler, } + } - validateStaticResponse('cy.intercept', handler) + validateStaticResponse('cy.intercept', handler) - staticResponse = handler as StaticResponse - break - default: - return $errUtils.throwErrByPath('net_stubbing.intercept.invalid_handler', { args: { handler } }) + staticResponse = handler as StaticResponse + } else if (!_.isUndefined(handler)) { + // a handler was passed but we dunno what it's supposed to be + return $errUtils.throwErrByPath('net_stubbing.intercept.invalid_handler', { args: { handler } }) } const routeMatcher = annotateMatcherOptionsTypes(matcher) @@ -252,7 +246,7 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, routeMatcher.headers = lowercaseFieldNames(routeMatcher.headers) } - const frame: NetEventFrames.AddRoute = { + const frame: NetEvent.ToServer.AddRoute = { handlerId, hasInterceptor, routeMatcher, diff --git a/packages/driver/src/cy/net-stubbing/events/request-complete.ts b/packages/driver/src/cy/net-stubbing/events/after-response.ts similarity index 67% rename from packages/driver/src/cy/net-stubbing/events/request-complete.ts rename to packages/driver/src/cy/net-stubbing/events/after-response.ts index 26c5b5d3f4..e5aceca444 100644 --- a/packages/driver/src/cy/net-stubbing/events/request-complete.ts +++ b/packages/driver/src/cy/net-stubbing/events/after-response.ts @@ -1,20 +1,22 @@ import { get } from 'lodash' -import { NetEventFrames } from '@packages/net-stubbing/lib/types' +import { CyHttpMessages } from '@packages/net-stubbing/lib/types' import { errByPath, makeErrFromObj } from '../../../cypress/error_utils' -import { HandlerFn } from './' +import { HandlerFn } from '.' -export const onRequestComplete: HandlerFn = (Cypress, frame, { failCurrentTest, getRequest, getRoute }) => { +export const onAfterResponse: HandlerFn = (Cypress, frame, userHandler, { getRequest, getRoute }) => { const request = getRequest(frame.routeHandlerId, frame.requestId) + const { data } = frame + if (!request) { - return + return frame.data } - if (frame.error) { - let err = makeErrFromObj(frame.error) + if (data.error) { + let err = makeErrFromObj(data.error) // does this request have a responseHandler that has not run yet? const isAwaitingResponse = !!request.responseHandler && ['Received', 'Intercepted'].includes(request.state) - const isTimeoutError = frame.error.code && ['ESOCKETTIMEDOUT', 'ETIMEDOUT'].includes(frame.error.code) + const isTimeoutError = data.error.code && ['ESOCKETTIMEDOUT', 'ETIMEDOUT'].includes(data.error.code) if (isAwaitingResponse || isTimeoutError) { const errorName = isTimeoutError ? 'timeout' : 'network_error' @@ -34,14 +36,16 @@ export const onRequestComplete: HandlerFn = if (isAwaitingResponse) { // the user is implicitly expecting there to be a successful response from the server, so fail the test // since a network error has occured - return failCurrentTest(err) + throw err } - return + return frame.data } request.state = 'Complete' request.log.fireChangeEvent() request.log.end() + + return frame.data } diff --git a/packages/driver/src/cy/net-stubbing/events/request-received.ts b/packages/driver/src/cy/net-stubbing/events/before-request.ts similarity index 74% rename from packages/driver/src/cy/net-stubbing/events/request-received.ts rename to packages/driver/src/cy/net-stubbing/events/before-request.ts index 0428665ce2..778a9550c3 100644 --- a/packages/driver/src/cy/net-stubbing/events/request-received.ts +++ b/packages/driver/src/cy/net-stubbing/events/before-request.ts @@ -4,21 +4,20 @@ import { Route, Interception, CyHttpMessages, - StaticResponse, SERIALIZABLE_REQ_PROPS, - NetEventFrames, + Subscription, } from '../types' import { parseJsonBody } from './utils' import { validateStaticResponse, - getBackendStaticResponse, parseStaticResponseShorthand, } from '../static-response-utils' import $errUtils from '../../../cypress/error_utils' -import { HandlerFn } from './' +import { HandlerFn } from '.' import Bluebird from 'bluebird' +import { NetEvent } from '@packages/net-stubbing/lib/types' -export const onRequestReceived: HandlerFn = (Cypress, frame, { getRoute, emitNetEvent }) => { +export const onBeforeRequest: HandlerFn = (Cypress, frame, userHandler, { getRoute, emitNetEvent, sendStaticResponse }) => { function getRequestLog (route: Route, request: Omit) { return Cypress.log({ name: 'xhr', @@ -48,22 +47,35 @@ export const onRequestReceived: HandlerFn = } const route = getRoute(frame.routeHandlerId) - const { req, requestId, routeHandlerId } = frame + const { data: req, requestId, routeHandlerId } = frame parseJsonBody(req) - const request: Partial = { + const request: Interception = { id: requestId, routeHandlerId, request: req, state: 'Received', requestWaited: false, responseWaited: false, - } + subscriptions: [], + on (eventName, handler) { + const subscription: Subscription = { + id: _.uniqueId('Subscription'), + routeHandlerId, + eventName, + await: true, + } - const continueFrame: Partial = { - routeHandlerId, - requestId, + request.subscriptions.push({ + subscription, + handler, + }) + + emitNetEvent('subscribe', { requestId, subscription } as NetEvent.ToServer.Subscribe) + + return request + }, } let resolved = false @@ -93,17 +105,20 @@ export const onRequestReceived: HandlerFn = request.responseHandler = responseHandler // signals server to send a http:response:received - continueFrame.hasResponseHandler = true + request.on('response', responseHandler) + userReq.responseTimeout = userReq.responseTimeout || Cypress.config('responseTimeout') return sendContinueFrame() } if (!_.isUndefined(responseHandler)) { - // `replyHandler` is a StaticResponse + // `responseHandler` is a StaticResponse validateStaticResponse('req.reply', responseHandler) - continueFrame.staticResponse = getBackendStaticResponse(responseHandler as StaticResponse) + sendStaticResponse(requestId, responseHandler) + + return finishRequestStage(req) } return sendContinueFrame() @@ -123,6 +138,19 @@ export const onRequestReceived: HandlerFn = let continueSent = false + function finishRequestStage (req) { + if (request) { + request.request = _.cloneDeep(req) + + request.state = 'Intercepted' + request.log && request.log.fireChangeEvent() + } + } + + if (!route) { + return req + } + const sendContinueFrame = () => { if (continueSent) { throw new Error('sendContinueFrame called twice in handler') @@ -130,31 +158,23 @@ export const onRequestReceived: HandlerFn = continueSent = true - if (continueFrame) { - // copy changeable attributes of userReq to req in frame - // @ts-ignore - continueFrame.req = { - ..._.pick(userReq, SERIALIZABLE_REQ_PROPS), - } + // copy changeable attributes of userReq to req + _.merge(req, _.pick(userReq, SERIALIZABLE_REQ_PROPS)) - _.merge(request.request, continueFrame.req) + finishRequestStage(req) - if (_.isObject(continueFrame.req!.body)) { - continueFrame.req!.body = JSON.stringify(continueFrame.req!.body) - } - - emitNetEvent('http:request:continue', continueFrame) + if (_.isObject(req.body)) { + req.body = JSON.stringify(req.body) } - if (request) { - request.state = 'Intercepted' - request.log && request.log.fireChangeEvent() - } + resolve(req) } - if (!route) { - return sendContinueFrame() - } + let resolve: (changedData: CyHttpMessages.IncomingRequest) => void + + const promise: Promise = new Promise((_resolve) => { + resolve = _resolve + }) request.log = getRequestLog(route, request as Omit) @@ -162,24 +182,20 @@ export const onRequestReceived: HandlerFn = route.log.set('numResponses', (route.log.get('numResponses') || 0) + 1) route.requests[requestId] = request as Interception - if (frame.notificationOnly) { - return + if (!_.isFunction(userHandler)) { + // notification-only + return req } route.hitCount++ - if (!_.isFunction(route.handler)) { - return sendContinueFrame() - } - - const handler = route.handler as Function const timeout = Cypress.config('defaultCommandTimeout') const curTest = Cypress.state('test') // if a Promise is returned, wait for it to resolve. if req.reply() // has not been called, continue to the next interceptor return Bluebird.try(() => { - return handler(userReq) + return userHandler(userReq) }) .catch((err) => { $errUtils.throwErrByPath('net_stubbing.request_handling.cb_failed', { @@ -220,8 +236,8 @@ export const onRequestReceived: HandlerFn = if (!replyCalled) { // handler function resolved without resolving request, pass on - continueFrame.tryNextRoute = true sendContinueFrame() } }) + .return(promise) as any as Bluebird } diff --git a/packages/driver/src/cy/net-stubbing/events/index.ts b/packages/driver/src/cy/net-stubbing/events/index.ts index 7ee4431853..352f2584aa 100644 --- a/packages/driver/src/cy/net-stubbing/events/index.ts +++ b/packages/driver/src/cy/net-stubbing/events/index.ts @@ -1,21 +1,21 @@ -import { Route, Interception } from '../types' -import { NetEventFrames } from '@packages/net-stubbing/lib/types' -import { onRequestReceived } from './request-received' -import { onResponseReceived } from './response-received' -import { onRequestComplete } from './request-complete' +import { Route, Interception, StaticResponse, NetEvent } from '../types' +import { onBeforeRequest } from './before-request' +import { onResponse } from './response' +import { onAfterResponse } from './after-response' import Bluebird from 'bluebird' +import { getBackendStaticResponse } from '../static-response-utils' -export type HandlerFn = (Cypress: Cypress.Cypress, frame: Frame, opts: { +export type HandlerFn = (Cypress: Cypress.Cypress, frame: NetEvent.ToDriver.Event, userHandler: (data: D) => void | Promise, opts: { getRequest: (routeHandlerId: string, requestId: string) => Interception | undefined getRoute: (routeHandlerId: string) => Route | undefined emitNetEvent: (eventName: string, frame: any) => Promise - failCurrentTest: (err: Error) => void -}) => Promise | void + sendStaticResponse: (requestId: string, staticResponse: StaticResponse) => void +}) => Promise | D const netEventHandlers: { [eventName: string]: HandlerFn } = { - 'http:request:received': onRequestReceived, - 'http:response:received': onResponseReceived, - 'http:request:complete': onRequestComplete, + 'before:request': onBeforeRequest, + 'response': onResponse, + 'after:response': onAfterResponse, } export function registerEvents (Cypress: Cypress.Cypress, cy: Cypress.cy) { @@ -44,6 +44,13 @@ export function registerEvents (Cypress: Cypress.Cypress, cy: Cypress.cy) { }) } + function sendStaticResponse (requestId: string, staticResponse: StaticResponse) { + emitNetEvent('send:static:response', { + requestId, + staticResponse: getBackendStaticResponse(staticResponse), + }) + } + function failCurrentTest (err: Error) { // @ts-ignore cy.fail(err) @@ -55,16 +62,61 @@ export function registerEvents (Cypress: Cypress.Cypress, cy: Cypress.cy) { state('aliasedRequests', []) }) - Cypress.on('net:event', (eventName, frame: NetEventFrames.BaseHttp) => { - Bluebird.try(() => { + Cypress.on('net:event', (eventName, frame: NetEvent.ToDriver.Event) => { + Bluebird.try(async () => { const handler = netEventHandlers[eventName] - return handler(Cypress, frame, { + if (!handler) { + throw new Error(`received unknown net:event in driver: ${eventName}`) + } + + const emitResolved = (changedData: any) => { + return emitNetEvent('event:handler:resolved', { + eventId: frame.eventId, + changedData, + }) + } + + const route = getRoute(frame.routeHandlerId) + + if (!route) { + if (frame.subscription.await) { + // route not found, just resolve so the request can continue + emitResolved(frame.data) + } + + return + } + + const getUserHandler = () => { + if (eventName === 'before:request' && !frame.subscription.id) { + // users do not explicitly subscribe to the first `before:request` event (req handler) + return route && route.handler + } + + const request = getRequest(frame.routeHandlerId, frame.requestId) + + const subscription = request && request.subscriptions.find(({ subscription }) => { + return subscription.id === frame.subscription.id + }) + + return subscription && subscription.handler + } + + const userHandler = getUserHandler() + + const changedData = await handler(Cypress, frame, userHandler, { getRoute, getRequest, emitNetEvent, - failCurrentTest, + sendStaticResponse, }) + + if (!frame.subscription.await) { + return + } + + return emitResolved(changedData) }) .catch(failCurrentTest) }) diff --git a/packages/driver/src/cy/net-stubbing/events/response-received.ts b/packages/driver/src/cy/net-stubbing/events/response.ts similarity index 72% rename from packages/driver/src/cy/net-stubbing/events/response-received.ts rename to packages/driver/src/cy/net-stubbing/events/response.ts index a2e776a05d..99028839a6 100644 --- a/packages/driver/src/cy/net-stubbing/events/response-received.ts +++ b/packages/driver/src/cy/net-stubbing/events/response.ts @@ -3,21 +3,19 @@ import _ from 'lodash' import { CyHttpMessages, SERIALIZABLE_RES_PROPS, - NetEventFrames, } from '@packages/net-stubbing/lib/types' import { validateStaticResponse, parseStaticResponseShorthand, STATIC_RESPONSE_KEYS, - getBackendStaticResponse, } from '../static-response-utils' import $errUtils from '../../../cypress/error_utils' -import { HandlerFn } from './' +import { HandlerFn } from '.' import Bluebird from 'bluebird' import { parseJsonBody } from './utils' -export const onResponseReceived: HandlerFn = (Cypress, frame, { getRoute, getRequest, emitNetEvent }) => { - const { res, requestId, routeHandlerId } = frame +export const onResponse: HandlerFn = async (Cypress, frame, userHandler, { getRoute, getRequest, sendStaticResponse }) => { + const { data: res, requestId, routeHandlerId } = frame const request = getRequest(frame.routeHandlerId, frame.requestId) parseJsonBody(res) @@ -30,38 +28,24 @@ export const onResponseReceived: HandlerFn request.log.fireChangeEvent() - if (!request.responseHandler) { + if (!userHandler) { // this is notification-only, update the request with the response attributes and end request.response = res - return + return res } } - const continueFrame: NetEventFrames.HttpResponseContinue = { - routeHandlerId, - requestId, - } - - const sendContinueFrame = () => { - // copy changeable attributes of userRes to res in frame - // if the user is setting a StaticResponse, use that instead - // @ts-ignore - continueFrame.res = { - ..._.pick(continueFrame.staticResponse || userRes, SERIALIZABLE_RES_PROPS), - } - + const finishResponseStage = (res) => { if (request) { - request.response = _.clone(continueFrame.res) + request.response = _.cloneDeep(res) request.state = 'ResponseIntercepted' request.log.fireChangeEvent() } + } - if (_.isObject(continueFrame.res!.body)) { - continueFrame.res!.body = JSON.stringify(continueFrame.res!.body) - } - - emitNetEvent('http:response:continue', continueFrame) + if (!request) { + return res } const userRes: CyHttpMessages.IncomingHttpResponse = { @@ -91,32 +75,49 @@ export const onResponseReceived: HandlerFn _.defaults(_staticResponse.headers, res.headers) - continueFrame.staticResponse = getBackendStaticResponse(_staticResponse) + sendStaticResponse(requestId, _staticResponse) + + return finishResponseStage(_staticResponse) } return sendContinueFrame() }, - delay (delay) { - continueFrame.delay = delay + delay (delayMs) { + res.delayMs = delayMs return this }, throttle (throttleKbps) { - continueFrame.throttleKbps = throttleKbps + res.throttleKbps = throttleKbps return this }, } - if (!request) { - return sendContinueFrame() + const sendContinueFrame = () => { + // copy changeable attributes of userRes to res + _.merge(res, _.pick(userRes, SERIALIZABLE_RES_PROPS)) + + finishResponseStage(res) + + if (_.isObject(res.body)) { + res.body = JSON.stringify(res.body) + } + + resolve(_.cloneDeep(res)) } const timeout = Cypress.config('defaultCommandTimeout') const curTest = Cypress.state('test') + let resolve: (changedData: CyHttpMessages.IncomingResponse) => void + + const promise: Promise = new Promise((_resolve) => { + resolve = _resolve + }) + return Bluebird.try(() => { - return request.responseHandler!(userRes) + return userHandler!(userRes) }) .catch((err) => { $errUtils.throwErrByPath('net_stubbing.response_handling.cb_failed', { @@ -159,4 +160,5 @@ export const onResponseReceived: HandlerFn .finally(() => { resolved = true }) + .return(promise) } diff --git a/packages/driver/src/cy/net-stubbing/static-response-utils.ts b/packages/driver/src/cy/net-stubbing/static-response-utils.ts index 35572d0c7f..9a8dada922 100644 --- a/packages/driver/src/cy/net-stubbing/static-response-utils.ts +++ b/packages/driver/src/cy/net-stubbing/static-response-utils.ts @@ -8,14 +8,14 @@ import { import $errUtils from '../../cypress/error_utils' // user-facing StaticResponse only -export const STATIC_RESPONSE_KEYS: (keyof StaticResponse)[] = ['body', 'fixture', 'statusCode', 'headers', 'forceNetworkError', 'throttleKbps', 'delay', 'delayMs'] +export const STATIC_RESPONSE_KEYS: (keyof StaticResponse)[] = ['body', 'fixture', 'statusCode', 'headers', 'forceNetworkError', 'throttleKbps', 'delayMs'] export function validateStaticResponse (cmd: string, staticResponse: StaticResponse): void { const err = (message) => { $errUtils.throwErrByPath('net_stubbing.invalid_static_response', { args: { cmd, message, staticResponse } }) } - const { body, fixture, statusCode, headers, forceNetworkError, throttleKbps, delay, delayMs } = staticResponse + const { body, fixture, statusCode, headers, forceNetworkError, throttleKbps, delayMs } = staticResponse if (forceNetworkError && (body || statusCode || headers)) { err('`forceNetworkError`, if passed, must be the only option in the StaticResponse.') @@ -43,17 +43,9 @@ export function validateStaticResponse (cmd: string, staticResponse: StaticRespo err('`throttleKbps` must be a finite, positive number.') } - if (delayMs && delay) { - err('`delayMs` and `delay` cannot both be set.') - } - if (delayMs && (!_.isFinite(delayMs) || delayMs < 0)) { err('`delayMs` must be a finite, positive number.') } - - if (delay && (!_.isFinite(delay) || delay < 0)) { - err('`delay` must be a finite, positive number.') - } } export function parseStaticResponseShorthand (statusCodeOrBody: number | string | any, bodyOrHeaders: string | { [key: string]: string }, maybeHeaders?: { [key: string]: string }) { @@ -96,12 +88,7 @@ function getFixtureOpts (fixture: string): FixtureOpts { } export function getBackendStaticResponse (staticResponse: Readonly): BackendStaticResponse { - const backendStaticResponse: BackendStaticResponse = _.omit(staticResponse, 'body', 'fixture', 'delayMs') - - if (staticResponse.delayMs) { - // support deprecated `delayMs` usage - backendStaticResponse.delay = staticResponse.delayMs - } + const backendStaticResponse: BackendStaticResponse = _.omit(staticResponse, 'body', 'fixture') if (staticResponse.fixture) { backendStaticResponse.fixture = getFixtureOpts(staticResponse.fixture) diff --git a/packages/driver/src/cypress/events.ts b/packages/driver/src/cypress/events.ts index f33d1d9aff..5be0241eed 100644 --- a/packages/driver/src/cypress/events.ts +++ b/packages/driver/src/cypress/events.ts @@ -54,6 +54,7 @@ export function extend (obj): Events { // array of results return ret1.concat(ret2) case 'emitThen': + case 'emitThenSeries': return Bluebird.join(ret1, ret2, (a, a2) => { // array of results return a.concat(a2) @@ -87,6 +88,7 @@ export function extend (obj): Events { events.emitMap = map(_.map) events.emitThen = map(Bluebird.map) + events.emitThenSeries = map(Bluebird.mapSeries) // is our log enabled and have we not silenced // this specific object? diff --git a/packages/net-stubbing/lib/external-types.ts b/packages/net-stubbing/lib/external-types.ts index cb77458c36..081d85dcd3 100644 --- a/packages/net-stubbing/lib/external-types.ts +++ b/packages/net-stubbing/lib/external-types.ts @@ -68,7 +68,6 @@ type Method = | 'unlink' | 'unlock' | 'unsubscribe' - export namespace CyHttpMessages { export interface BaseMessage { body?: any @@ -81,6 +80,8 @@ export namespace CyHttpMessages { export type IncomingResponse = BaseMessage & { statusCode: number statusMessage: string + delayMs?: number + throttleKbps?: number } export type IncomingHttpResponse = IncomingResponse & { @@ -95,9 +96,9 @@ export namespace CyHttpMessages { */ send(): void /** - * Wait for `delay` milliseconds before sending the response to the client. + * Wait for `delayMs` milliseconds before sending the response to the client. */ - delay: (delay: number) => IncomingHttpResponse + delay: (delayMs: number) => IncomingHttpResponse /** * Serve the response at `throttleKbps` kilobytes per second. */ @@ -145,6 +146,10 @@ export namespace CyHttpMessages { */ redirect(location: string, statusCode?: number): void } + + export interface ResponseComplete { + error?: any + } } export interface DictMatcher { @@ -175,6 +180,16 @@ export type HttpResponseInterceptor = (res: CyHttpMessages.IncomingHttpResponse) */ export type NumberMatcher = number | number[] +/** + * Metadata for a subscription for an interception event. + */ +export interface Subscription { + id?: string + routeHandlerId: string + eventName: string + await: boolean +} + /** * Request/response cycle. */ @@ -182,7 +197,7 @@ export interface Interception { id: string routeHandlerId: string /* @internal */ - log: any + log?: any request: CyHttpMessages.IncomingRequest /** * Was `cy.wait()` used to wait on this request? @@ -203,6 +218,17 @@ export interface Interception { responseWaited: boolean /* @internal */ state: InterceptionState + /* @internal */ + subscriptions: Array<{ + subscription: Subscription + handler: (data: any) => Promise | void + }> + /* @internal */ + on(eventName: 'request', cb: () => void): Interception + /* @internal */ + on(eventName: 'before-response', cb: (res: CyHttpMessages.IncomingHttpResponse) => void): Interception + /* @internal */ + on(eventName: 'response', cb: (res: CyHttpMessages.IncomingHttpResponse) => void): Interception } export type InterceptionState = @@ -292,13 +318,7 @@ export type RouteHandler = string | StaticResponse | RouteHandlerController | ob /** * Describes a response that will be sent back to the browser to fulfill the request. */ -export type StaticResponse = GenericStaticResponse & { - /** - * Milliseconds to delay before the response is sent. - * @deprecated Use `delay` instead of `delayMs`. - */ - delayMs?: number -} +export type StaticResponse = GenericStaticResponse export interface GenericStaticResponse { /** @@ -332,7 +352,7 @@ export interface GenericStaticResponse { /** * Milliseconds to delay before the response is sent. */ - delay?: number + delayMs?: number } /** diff --git a/packages/net-stubbing/lib/internal-types.ts b/packages/net-stubbing/lib/internal-types.ts index 9de57f8d95..30aeff4c27 100644 --- a/packages/net-stubbing/lib/internal-types.ts +++ b/packages/net-stubbing/lib/internal-types.ts @@ -1,8 +1,8 @@ import * as _ from 'lodash' import { RouteMatcherOptionsGeneric, - CyHttpMessages, GenericStaticResponse, + Subscription, } from './external-types' export type FixtureOpts = { @@ -26,6 +26,8 @@ export const SERIALIZABLE_RES_PROPS = _.concat( SERIALIZABLE_REQ_PROPS, 'statusCode', 'statusMessage', + 'delayMs', + 'throttleKbps', ) export const DICT_STRING_MATCHER_FIELDS = ['headers', 'query'] @@ -45,56 +47,41 @@ export interface AnnotatedStringMatcher { */ export type AnnotatedRouteMatcherOptions = RouteMatcherOptionsGeneric -/** Types for messages between driver and server */ - -export declare namespace NetEventFrames { - export interface AddRoute { - routeMatcher: AnnotatedRouteMatcherOptions - staticResponse?: BackendStaticResponse - hasInterceptor: boolean - handlerId?: string - } - - interface BaseHttp { +export declare namespace NetEvent { + export interface Http { requestId: string routeHandlerId: string } - // fired when HTTP proxy receives headers + body of request - export interface HttpRequestReceived extends BaseHttp { - req: CyHttpMessages.IncomingRequest - /** - * Is the proxy expecting the driver to send `HttpRequestContinue`? - */ - notificationOnly: boolean + export namespace ToDriver { + export interface Event extends Http { + subscription: Subscription + eventId: string + data: D + } } - // fired when driver is done modifying request and wishes to pass control back to the proxy - export interface HttpRequestContinue extends BaseHttp { - req: CyHttpMessages.IncomingRequest - staticResponse?: BackendStaticResponse - hasResponseHandler?: boolean - tryNextRoute?: boolean - } + export namespace ToServer { + export interface AddRoute { + routeMatcher: AnnotatedRouteMatcherOptions + staticResponse?: BackendStaticResponse + hasInterceptor: boolean + handlerId?: string + } - // fired when a response is received and the driver has a req.reply callback registered - export interface HttpResponseReceived extends BaseHttp { - res: CyHttpMessages.IncomingResponse - } + export interface Subscribe { + requestId: string + subscription: Subscription + } - // fired when driver is done modifying response or driver callback completes, - // passes control back to proxy - export interface HttpResponseContinue extends BaseHttp { - res?: CyHttpMessages.IncomingResponse - staticResponse?: BackendStaticResponse - // Millisecond timestamp for when the response should continue - delay?: number - throttleKbps?: number - followRedirect?: boolean - } + export interface EventHandlerResolved { + eventId: string + changedData: any + } - // fired when a response has been sent completely by the server to an intercepted request - export interface HttpRequestComplete extends BaseHttp { - error?: Error & { code?: string } + export interface SendStaticResponse { + requestId: string + staticResponse: BackendStaticResponse + } } } diff --git a/packages/net-stubbing/lib/server/driver-events.ts b/packages/net-stubbing/lib/server/driver-events.ts index 6644bd2c18..b1e3e28473 100644 --- a/packages/net-stubbing/lib/server/driver-events.ts +++ b/packages/net-stubbing/lib/server/driver-events.ts @@ -7,20 +7,18 @@ import { } from './types' import { AnnotatedRouteMatcherOptions, - NetEventFrames, RouteMatcherOptions, + NetEvent, } from '../types' import { getAllStringMatcherFields, + sendStaticResponse as _sendStaticResponse, setResponseFromFixture, } from './util' -import { onRequestContinue } from './intercept-request' -import { onResponseContinue } from './intercept-response' -import CyServer from '@packages/server' const debug = Debug('cypress:net-stubbing:server:driver-events') -async function _onRouteAdded (state: NetStubbingState, getFixture: GetFixtureFn, options: NetEventFrames.AddRoute) { +async function onRouteAdded (state: NetStubbingState, getFixture: GetFixtureFn, options: NetEvent.ToServer.AddRoute) { const routeMatcher = _restoreMatcherOptionsTypes(options.routeMatcher) const { staticResponse } = options @@ -37,6 +35,51 @@ async function _onRouteAdded (state: NetStubbingState, getFixture: GetFixtureFn, state.routes.push(route) } +function getRequest (state: NetStubbingState, requestId: string) { + return Object.values(state.requests).find(({ id }) => { + return requestId === id + }) +} + +function subscribe (state: NetStubbingState, options: NetEvent.ToServer.Subscribe) { + const request = getRequest(state, options.requestId) + + if (!request) { + return + } + + // filter out any stub subscriptions that are no longer needed + _.remove(request.subscriptions, ({ eventName, routeHandlerId, id }) => { + return eventName === options.subscription.eventName && routeHandlerId === options.subscription.routeHandlerId && !id + }) + + request.subscriptions.push(options.subscription) +} + +function eventHandlerResolved (state: NetStubbingState, options: NetEvent.ToServer.EventHandlerResolved) { + const pendingEventHandler = state.pendingEventHandlers[options.eventId] + + if (!pendingEventHandler) { + return + } + + delete state.pendingEventHandlers[options.eventId] + + pendingEventHandler(options.changedData) +} + +async function sendStaticResponse (state: NetStubbingState, getFixture: GetFixtureFn, options: NetEvent.ToServer.SendStaticResponse) { + const request = getRequest(state, options.requestId) + + if (!request) { + return + } + + await setResponseFromFixture(getFixture, options.staticResponse) + + _sendStaticResponse(request, options.staticResponse) +} + export function _restoreMatcherOptionsTypes (options: AnnotatedRouteMatcherOptions) { const stringMatcherFields = getAllStringMatcherFields(options) @@ -72,24 +115,25 @@ export function _restoreMatcherOptionsTypes (options: AnnotatedRouteMatcherOptio type OnNetEventOpts = { eventName: string state: NetStubbingState - socket: CyServer.Socket getFixture: GetFixtureFn args: any[] - frame: NetEventFrames.AddRoute | NetEventFrames.HttpRequestContinue | NetEventFrames.HttpResponseContinue + frame: NetEvent.ToServer.AddRoute | NetEvent.ToServer.EventHandlerResolved | NetEvent.ToServer.Subscribe | NetEvent.ToServer.SendStaticResponse } export async function onNetEvent (opts: OnNetEventOpts): Promise { - const { state, socket, getFixture, args, eventName, frame } = opts + const { state, getFixture, args, eventName, frame } = opts debug('received driver event %o', { eventName, args }) switch (eventName) { case 'route:added': - return _onRouteAdded(state, getFixture, frame) - case 'http:request:continue': - return onRequestContinue(state, frame, socket) - case 'http:response:continue': - return onResponseContinue(state, frame) + return onRouteAdded(state, getFixture, frame) + case 'subscribe': + return subscribe(state, frame) + case 'event:handler:resolved': + return eventHandlerResolved(state, frame) + case 'send:static:response': + return sendStaticResponse(state, getFixture, frame) default: throw new Error(`Unrecognized net event: ${eventName}`) } diff --git a/packages/net-stubbing/lib/server/index.ts b/packages/net-stubbing/lib/server/index.ts index e119ef3d70..8a9f97277e 100644 --- a/packages/net-stubbing/lib/server/index.ts +++ b/packages/net-stubbing/lib/server/index.ts @@ -1,10 +1,10 @@ export { onNetEvent } from './driver-events' -export { InterceptError } from './intercept-error' +export { InterceptError } from './middleware/error' -export { InterceptRequest } from './intercept-request' +export { InterceptRequest } from './middleware/request' -export { InterceptResponse } from './intercept-response' +export { InterceptResponse } from './middleware/response' export { NetStubbingState } from './types' diff --git a/packages/net-stubbing/lib/server/intercept-error.ts b/packages/net-stubbing/lib/server/intercept-error.ts deleted file mode 100644 index 18397ec9f2..0000000000 --- a/packages/net-stubbing/lib/server/intercept-error.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Debug from 'debug' - -import { ErrorMiddleware } from '@packages/proxy' -import { NetEventFrames } from '../types' -import { emit } from './util' -import errors from '@packages/server/lib/errors' - -const debug = Debug('cypress:net-stubbing:server:intercept-error') - -export const InterceptError: ErrorMiddleware = function () { - const backendRequest = this.netStubbingState.requests[this.req.requestId] - - if (!backendRequest) { - // the original request was not intercepted, nothing to do - return this.next() - } - - debug('intercepting error %o', { req: this.req, backendRequest }) - - // this may get set back to `true` by another route - backendRequest.waitForResponseContinue = false - backendRequest.continueResponse = this.next - - const frame: NetEventFrames.HttpRequestComplete = { - routeHandlerId: backendRequest.route.handlerId!, - requestId: backendRequest.requestId, - // @ts-ignore - false positive when running type-check? - error: errors.clone(this.error), - } - - emit(this.socket, 'http:request:complete', frame) - - this.next() -} diff --git a/packages/net-stubbing/lib/server/intercept-request.ts b/packages/net-stubbing/lib/server/intercept-request.ts deleted file mode 100644 index afb709b4fe..0000000000 --- a/packages/net-stubbing/lib/server/intercept-request.ts +++ /dev/null @@ -1,204 +0,0 @@ -import _ from 'lodash' -import { concatStream } from '@packages/network' -import Debug from 'debug' -import url from 'url' - -import { - CypressIncomingRequest, - RequestMiddleware, -} from '@packages/proxy' -import { - BackendRoute, - BackendRequest, - NetStubbingState, -} from './types' -import { - CyHttpMessages, - NetEventFrames, - SERIALIZABLE_REQ_PROPS, -} from '../types' -import { getRouteForRequest, matchesRoutePreflight } from './route-matching' -import { - sendStaticResponse, - emit, - setResponseFromFixture, - setDefaultHeaders, -} from './util' -import CyServer from '@packages/server' - -const debug = Debug('cypress:net-stubbing:server:intercept-request') - -/** - * Called when a new request is received in the proxy layer. - */ -export const InterceptRequest: RequestMiddleware = function () { - if (matchesRoutePreflight(this.netStubbingState.routes, this.req)) { - // send positive CORS preflight response - return sendStaticResponse(this, { - statusCode: 204, - headers: { - 'access-control-max-age': '-1', - 'access-control-allow-credentials': 'true', - 'access-control-allow-origin': this.req.headers.origin || '*', - 'access-control-allow-methods': this.req.headers['access-control-request-method'] || '*', - 'access-control-allow-headers': this.req.headers['access-control-request-headers'] || '*', - }, - }) - } - - const route = getRouteForRequest(this.netStubbingState.routes, this.req) - - if (!route) { - // not intercepted, carry on normally... - return this.next() - } - - const requestId = _.uniqueId('interceptedRequest') - - debug('intercepting request %o', { requestId, route, req: _.pick(this.req, 'url') }) - - const request: BackendRequest = { - requestId, - route, - continueRequest: this.next, - onError: this.onError, - onResponse: (incomingRes, resStream) => { - setDefaultHeaders(this.req, incomingRes) - this.onResponse(incomingRes, resStream) - }, - req: this.req, - res: this.res, - } - - // attach requestId to the original req object for later use - this.req.requestId = requestId - - this.netStubbingState.requests[requestId] = request - - _interceptRequest(this.netStubbingState, request, route, this.socket) -} - -function _interceptRequest (state: NetStubbingState, request: BackendRequest, route: BackendRoute, socket: CyServer.Socket) { - const notificationOnly = !route.hasInterceptor - - const frame: NetEventFrames.HttpRequestReceived = { - routeHandlerId: route.handlerId!, - requestId: request.req.requestId, - req: _.extend(_.pick(request.req, SERIALIZABLE_REQ_PROPS), { - url: request.req.proxiedUrl, - }) as CyHttpMessages.IncomingRequest, - notificationOnly, - } - - request.res.once('finish', () => { - emit(socket, 'http:request:complete', { - requestId: request.requestId, - routeHandlerId: route.handlerId!, - }) - - debug('request/response finished, cleaning up %o', { requestId: request.requestId }) - delete state.requests[request.requestId] - }) - - const emitReceived = () => { - emit(socket, 'http:request:received', frame) - } - - const ensureBody = (cb: () => void) => { - if (frame.req.body) { - return cb() - } - - request.req.pipe(concatStream((reqBody) => { - const contentType = frame.req.headers['content-type'] - const isMultipart = contentType && contentType.includes('multipart/form-data') - - request.req.body = frame.req.body = isMultipart ? reqBody : reqBody.toString() - cb() - })) - } - - if (route.staticResponse) { - const { staticResponse } = route - - return ensureBody(() => { - emitReceived() - sendStaticResponse(request, staticResponse) - }) - } - - if (notificationOnly) { - return ensureBody(() => { - emitReceived() - - const nextRoute = getNextRoute(state, request.req, frame.routeHandlerId) - - if (!nextRoute) { - return request.continueRequest() - } - - _interceptRequest(state, request, nextRoute, socket) - }) - } - - ensureBody(emitReceived) -} - -/** - * If applicable, return the route that is next in line after `prevRouteHandlerId` to handle `req`. - */ -function getNextRoute (state: NetStubbingState, req: CypressIncomingRequest, prevRouteHandlerId: string): BackendRoute | undefined { - const prevRoute = _.find(state.routes, { handlerId: prevRouteHandlerId }) - - if (!prevRoute) { - return - } - - return getRouteForRequest(state.routes, req, prevRoute) -} - -export async function onRequestContinue (state: NetStubbingState, frame: NetEventFrames.HttpRequestContinue, socket: CyServer.Socket) { - const backendRequest = state.requests[frame.requestId] - - if (!backendRequest) { - debug('onRequestContinue received but no backendRequest exists %o', { frame }) - - return - } - - frame.req.url = url.resolve(backendRequest.req.proxiedUrl, frame.req.url) - - // modify the original paused request object using what the client returned - _.assign(backendRequest.req, _.pick(frame.req, SERIALIZABLE_REQ_PROPS)) - - // proxiedUrl is used to initialize the new request - backendRequest.req.proxiedUrl = frame.req.url - - // update problematic headers - // update content-length if available - if (backendRequest.req.headers['content-length'] && frame.req.body != null) { - backendRequest.req.headers['content-length'] = Buffer.from(frame.req.body).byteLength.toString() - } - - if (frame.hasResponseHandler) { - backendRequest.waitForResponseContinue = true - } - - if (frame.tryNextRoute) { - const nextRoute = getNextRoute(state, backendRequest.req, frame.routeHandlerId) - - if (!nextRoute) { - return backendRequest.continueRequest() - } - - return _interceptRequest(state, backendRequest, nextRoute, socket) - } - - if (frame.staticResponse) { - await setResponseFromFixture(backendRequest.route.getFixture, frame.staticResponse) - - return sendStaticResponse(backendRequest, frame.staticResponse) - } - - backendRequest.continueRequest() -} diff --git a/packages/net-stubbing/lib/server/intercept-response.ts b/packages/net-stubbing/lib/server/intercept-response.ts deleted file mode 100644 index 79d2aaf087..0000000000 --- a/packages/net-stubbing/lib/server/intercept-response.ts +++ /dev/null @@ -1,123 +0,0 @@ -import _ from 'lodash' -import { concatStream, httpUtils } from '@packages/network' -import Debug from 'debug' -import { Readable, PassThrough } from 'stream' - -import { - ResponseMiddleware, -} from '@packages/proxy' -import { - NetStubbingState, -} from './types' -import { - CyHttpMessages, - NetEventFrames, - SERIALIZABLE_RES_PROPS, -} from '../types' -import { - emit, - sendStaticResponse, - setResponseFromFixture, - getBodyStream, -} from './util' - -const debug = Debug('cypress:net-stubbing:server:intercept-response') - -export const InterceptResponse: ResponseMiddleware = function () { - const backendRequest = this.netStubbingState.requests[this.req.requestId] - - debug('InterceptResponse %o', { req: _.pick(this.req, 'url'), backendRequest }) - - if (!backendRequest) { - // original request was not intercepted, nothing to do - return this.next() - } - - backendRequest.incomingRes = this.incomingRes - - backendRequest.onResponse = (incomingRes, resStream) => { - this.incomingRes = incomingRes - - backendRequest.continueResponse!(resStream) - } - - backendRequest.continueResponse = (newResStream?: Readable) => { - if (newResStream) { - this.incomingResStream = newResStream.on('error', this.onError) - } - - this.next() - } - - const frame: NetEventFrames.HttpResponseReceived = { - routeHandlerId: backendRequest.route.handlerId!, - requestId: backendRequest.requestId, - res: _.extend(_.pick(this.incomingRes, SERIALIZABLE_RES_PROPS), { - url: this.req.proxiedUrl, - }) as CyHttpMessages.IncomingResponse, - } - - const res = frame.res as CyHttpMessages.IncomingResponse - - const emitReceived = () => { - emit(this.socket, 'http:response:received', frame) - } - - this.makeResStreamPlainText() - - new Promise((resolve) => { - if (httpUtils.responseMustHaveEmptyBody(this.req, this.incomingRes)) { - resolve('') - } else { - this.incomingResStream.pipe(concatStream((resBody) => { - resolve(resBody) - })) - } - }) - .then((body) => { - const pt = this.incomingResStream = new PassThrough() - - pt.end(body) - - res.body = String(body) - emitReceived() - - if (!backendRequest.waitForResponseContinue) { - this.next() - } - - // this may get set back to `true` by another route - backendRequest.waitForResponseContinue = false - }) -} - -export async function onResponseContinue (state: NetStubbingState, frame: NetEventFrames.HttpResponseContinue) { - const backendRequest = state.requests[frame.requestId] - - if (typeof backendRequest === 'undefined') { - return - } - - const { res } = backendRequest - - debug('_onResponseContinue %o', { backendRequest: _.omit(backendRequest, 'res.body'), frame: _.omit(frame, 'res.body') }) - - const throttleKbps = _.get(frame, 'staticResponse.throttleKbps') || frame.throttleKbps - const delay = _.get(frame, 'staticResponse.delay') || frame.delay - - if (frame.staticResponse) { - // replacing response with a staticResponse - await setResponseFromFixture(backendRequest.route.getFixture, frame.staticResponse) - - const staticResponse = _.chain(frame.staticResponse).clone().assign({ delay, throttleKbps }).value() - - return sendStaticResponse(backendRequest, staticResponse) - } - - // merge the changed response attributes with our response and continue - _.assign(res, _.pick(frame.res, SERIALIZABLE_RES_PROPS)) - - const bodyStream = getBodyStream(res.body, { throttleKbps, delay }) - - return backendRequest.continueResponse!(bodyStream) -} diff --git a/packages/net-stubbing/lib/server/intercepted-request.ts b/packages/net-stubbing/lib/server/intercepted-request.ts new file mode 100644 index 0000000000..21d3af756e --- /dev/null +++ b/packages/net-stubbing/lib/server/intercepted-request.ts @@ -0,0 +1,121 @@ +import _ from 'lodash' +import { IncomingMessage } from 'http' +import { Readable } from 'stream' +import { + CypressIncomingRequest, + CypressOutgoingResponse, +} from '@packages/proxy' +import { + NetEvent, + Subscription, +} from '../types' +import { NetStubbingState } from './types' +import { emit } from './util' +import CyServer from '@packages/server' + +export class InterceptedRequest { + id: string + onError: (err: Error) => void + /** + * A callback that can be used to make the request go outbound through the rest of the request proxy steps. + */ + continueRequest: Function + /** + * Finish the current request with a response. + */ + onResponse: (incomingRes: IncomingMessage, resStream: Readable) => void + /** + * A callback that can be used to send the response through the rest of the response proxy steps. + */ + continueResponse?: (newResStream?: Readable) => void + req: CypressIncomingRequest + res: CypressOutgoingResponse + incomingRes?: IncomingMessage + subscriptions: Subscription[] + state: NetStubbingState + socket: CyServer.Socket + + constructor (opts: Pick) { + this.id = _.uniqueId('interceptedRequest') + this.req = opts.req + this.res = opts.res + this.continueRequest = opts.continueRequest + this.onError = opts.onError + this.onResponse = opts.onResponse + this.subscriptions = opts.subscriptions + this.state = opts.state + this.socket = opts.socket + } + + /* + * Run all subscriptions for an event in order, awaiting responses if applicable. + * Resolves with the updated object, or the original object if no changes have been made. + */ + async handleSubscriptions ({ eventName, data, mergeChanges }: { + eventName: string + data: D + /* + * Given a `before` snapshot and an `after` snapshot, calculate the modified object. + */ + mergeChanges: (before: D, after: D) => D + }): Promise { + const handleSubscription = async (subscription: Subscription) => { + const eventId = _.uniqueId('event') + const eventFrame: NetEvent.ToDriver.Event = { + eventId, + subscription, + requestId: this.id, + routeHandlerId: subscription.routeHandlerId, + data, + } + + const _emit = () => emit(this.socket, eventName, eventFrame) + + if (!subscription.await) { + _emit() + + return data + } + + const p = new Promise((resolve) => { + this.state.pendingEventHandlers[eventId] = resolve + }) + + _emit() + + const changedData = await p + + return mergeChanges(data, changedData as any) + } + + let lastI = -1 + + const getNextSubscription = () => { + return _.find(this.subscriptions, (v, i) => { + if (i > lastI && v.eventName === eventName) { + lastI = i + + return v + } + + return + }) as Subscription | undefined + } + + const run = async () => { + const subscription = getNextSubscription() + + if (!subscription) { + return + } + + data = await handleSubscription(subscription) + + await run() + } + + await run() + + return data + } +} diff --git a/packages/net-stubbing/lib/server/middleware/error.ts b/packages/net-stubbing/lib/server/middleware/error.ts new file mode 100644 index 0000000000..eb6aa140fd --- /dev/null +++ b/packages/net-stubbing/lib/server/middleware/error.ts @@ -0,0 +1,31 @@ +import Debug from 'debug' + +import { ErrorMiddleware } from '@packages/proxy' +import { CyHttpMessages } from '../../types' +import _ from 'lodash' +import errors from '@packages/server/lib/errors' + +const debug = Debug('cypress:net-stubbing:server:intercept-error') + +export const InterceptError: ErrorMiddleware = function () { + const request = this.netStubbingState.requests[this.req.requestId] + + if (!request) { + // the original request was not intercepted, nothing to do + return this.next() + } + + debug('intercepting error %o', { req: this.req, request }) + + request.continueResponse = this.next + + request.handleSubscriptions({ + eventName: 'after:response', + data: { + error: errors.clone(this.error), + }, + mergeChanges: _.identity, + }) + + this.next() +} diff --git a/packages/net-stubbing/lib/server/middleware/request.ts b/packages/net-stubbing/lib/server/middleware/request.ts new file mode 100644 index 0000000000..8173621615 --- /dev/null +++ b/packages/net-stubbing/lib/server/middleware/request.ts @@ -0,0 +1,169 @@ +import _ from 'lodash' +import { concatStream } from '@packages/network' +import Debug from 'debug' +import url from 'url' +import { getEncoding } from 'istextorbinary' + +import { + RequestMiddleware, +} from '@packages/proxy' +import { + CyHttpMessages, + SERIALIZABLE_REQ_PROPS, +} from '../../types' +import { getRouteForRequest, matchesRoutePreflight } from '../route-matching' +import { + sendStaticResponse, + setDefaultHeaders, +} from '../util' +import { Subscription } from '../../external-types' +import { InterceptedRequest } from '../intercepted-request' + +const debug = Debug('cypress:net-stubbing:server:intercept-request') + +/** + * Called when a new request is received in the proxy layer. + */ +export const InterceptRequest: RequestMiddleware = async function () { + if (matchesRoutePreflight(this.netStubbingState.routes, this.req)) { + // send positive CORS preflight response + return sendStaticResponse(this, { + statusCode: 204, + headers: { + 'access-control-max-age': '-1', + 'access-control-allow-credentials': 'true', + 'access-control-allow-origin': this.req.headers.origin || '*', + 'access-control-allow-methods': this.req.headers['access-control-request-method'] || '*', + 'access-control-allow-headers': this.req.headers['access-control-request-headers'] || '*', + }, + }) + } + + let lastRoute + const subscriptions: Subscription[] = [] + + const addDefaultSubscriptions = (prevRoute?) => { + const route = getRouteForRequest(this.netStubbingState.routes, this.req, prevRoute) + + if (!route) { + return + } + + Array.prototype.push.apply(subscriptions, [{ + eventName: 'before:request', + // req.reply callback? + await: !!route.hasInterceptor, + routeHandlerId: route.handlerId, + }, { + eventName: 'response', + // notification-only + await: false, + routeHandlerId: route.handlerId, + }, { + eventName: 'after:response', + // notification-only + await: false, + routeHandlerId: route.handlerId, + }]) + + lastRoute = route + + addDefaultSubscriptions(route) + } + + addDefaultSubscriptions() + + if (!subscriptions.length) { + // not intercepted, carry on normally... + return this.next() + } + + const request = new InterceptedRequest({ + continueRequest: this.next, + onError: this.onError, + onResponse: (incomingRes, resStream) => { + setDefaultHeaders(this.req, incomingRes) + this.onResponse(incomingRes, resStream) + }, + req: this.req, + res: this.res, + socket: this.socket, + state: this.netStubbingState, + subscriptions, + }) + + debug('intercepting request %o', { requestId: request.id, req: _.pick(this.req, 'url') }) + + // attach requestId to the original req object for later use + this.req.requestId = request.id + + this.netStubbingState.requests[request.id] = request + + const req = _.extend(_.pick(request.req, SERIALIZABLE_REQ_PROPS), { + url: request.req.proxiedUrl, + }) as CyHttpMessages.IncomingRequest + + request.res.once('finish', async () => { + request.handleSubscriptions({ + eventName: 'after:response', + data: {}, + mergeChanges: _.identity, + }) + + debug('request/response finished, cleaning up %o', { requestId: request.id }) + delete this.netStubbingState.requests[request.id] + }) + + const ensureBody = () => { + return new Promise((resolve) => { + if (req.body) { + return resolve() + } + + request.req.pipe(concatStream((reqBody) => { + req.body = reqBody + resolve() + })) + }) + } + + await ensureBody() + + if (!_.isString(req.body) && !_.isBuffer(req.body)) { + throw new Error('req.body must be a string or a Buffer') + } + + if (getEncoding(req.body) !== 'binary') { + req.body = req.body.toString('utf8') + } + + request.req.body = req.body + + const mergeChanges = (before: CyHttpMessages.IncomingRequest, after: CyHttpMessages.IncomingRequest) => { + if (before.headers['content-length'] === after.headers['content-length']) { + // user did not purposely override content-length, let's set it + after.headers['content-length'] = String(Buffer.from(after.body).byteLength) + } + + // resolve and propagate any changes to the URL + request.req.proxiedUrl = after.url = url.resolve(request.req.proxiedUrl, after.url) + + return _.merge(before, _.pick(after, SERIALIZABLE_REQ_PROPS)) + } + + const modifiedReq = await request.handleSubscriptions({ + eventName: 'before:request', + data: req, + mergeChanges, + }) + + if (lastRoute.staticResponse) { + return sendStaticResponse(request, lastRoute.staticResponse) + } + + mergeChanges(req, modifiedReq) + // @ts-ignore + mergeChanges(request.req, req) + + return request.continueRequest() +} diff --git a/packages/net-stubbing/lib/server/middleware/response.ts b/packages/net-stubbing/lib/server/middleware/response.ts new file mode 100644 index 0000000000..d9512bd078 --- /dev/null +++ b/packages/net-stubbing/lib/server/middleware/response.ts @@ -0,0 +1,81 @@ +import _ from 'lodash' +import { concatStream, httpUtils } from '@packages/network' +import Debug from 'debug' +import { Readable } from 'stream' +import { getEncoding } from 'istextorbinary' + +import { + ResponseMiddleware, +} from '@packages/proxy' +import { + CyHttpMessages, + SERIALIZABLE_RES_PROPS, +} from '../../types' +import { + getBodyStream, +} from '../util' + +const debug = Debug('cypress:net-stubbing:server:intercept-response') + +export const InterceptResponse: ResponseMiddleware = async function () { + const request = this.netStubbingState.requests[this.req.requestId] + + debug('InterceptResponse %o', { req: _.pick(this.req, 'url'), request }) + + if (!request) { + // original request was not intercepted, nothing to do + return this.next() + } + + request.incomingRes = this.incomingRes + + request.onResponse = (incomingRes, resStream) => { + this.incomingRes = incomingRes + + request.continueResponse!(resStream) + } + + request.continueResponse = (newResStream?: Readable) => { + if (newResStream) { + this.incomingResStream = newResStream.on('error', this.onError) + } + + this.next() + } + + this.makeResStreamPlainText() + + const body: Buffer | string = await new Promise((resolve) => { + if (httpUtils.responseMustHaveEmptyBody(this.req, this.incomingRes)) { + resolve(Buffer.from('')) + } else { + this.incomingResStream.pipe(concatStream(resolve)) + } + }) + .then((buf) => { + return getEncoding(buf) !== 'binary' ? buf.toString('utf8') : buf + }) + + const res = _.extend(_.pick(this.incomingRes, SERIALIZABLE_RES_PROPS), { + url: this.req.proxiedUrl, + body, + }) as CyHttpMessages.IncomingResponse + + if (!_.isString(res.body) && !_.isBuffer(res.body)) { + throw new Error('res.body must be a string or a Buffer') + } + + const modifiedRes = await request.handleSubscriptions({ + eventName: 'response', + data: res, + mergeChanges: (before, after) => { + return _.merge(before, _.pick(after, SERIALIZABLE_RES_PROPS)) + }, + }) + + _.merge(request.res, modifiedRes) + + const bodyStream = getBodyStream(modifiedRes.body, _.pick(modifiedRes, ['throttleKbps', 'delayMs']) as any) + + return request.continueResponse!(bodyStream) +} diff --git a/packages/net-stubbing/lib/server/route-matching.ts b/packages/net-stubbing/lib/server/route-matching.ts index 203d78a050..5cf12ef1f5 100644 --- a/packages/net-stubbing/lib/server/route-matching.ts +++ b/packages/net-stubbing/lib/server/route-matching.ts @@ -124,6 +124,9 @@ export function _getMatchableForRequest (req: CypressIncomingRequest) { return matchable } +/** + * Try to match a `BackendRoute` to a request, optionally starting after `prevRoute`. + */ export function getRouteForRequest (routes: BackendRoute[], req: CypressIncomingRequest, prevRoute?: BackendRoute) { const possibleRoutes = prevRoute ? routes.slice(_.findIndex(routes, prevRoute) + 1) : routes diff --git a/packages/net-stubbing/lib/server/state.ts b/packages/net-stubbing/lib/server/state.ts index b5a6eefd00..b29b7cb35b 100644 --- a/packages/net-stubbing/lib/server/state.ts +++ b/packages/net-stubbing/lib/server/state.ts @@ -5,6 +5,7 @@ export function state (): NetStubbingState { return { requests: {}, routes: [], + pendingEventHandlers: {}, reset () { // clean up requests that are still pending for (const requestId in this.requests) { @@ -16,6 +17,7 @@ export function state (): NetStubbingState { res.destroy() } + this.pendingEventHandlers = {} this.requests = {} this.routes = [] }, diff --git a/packages/net-stubbing/lib/server/types.ts b/packages/net-stubbing/lib/server/types.ts index c5c82fd4d2..c49f5ab2a3 100644 --- a/packages/net-stubbing/lib/server/types.ts +++ b/packages/net-stubbing/lib/server/types.ts @@ -1,13 +1,10 @@ -import { IncomingMessage } from 'http' -import { Readable } from 'stream' -import { - CypressIncomingRequest, - CypressOutgoingResponse, -} from '@packages/proxy' import { RouteMatcherOptions, BackendStaticResponse, } from '../types' +import { + InterceptedRequest, +} from './intercepted-request' export type GetFixtureFn = (path: string, opts?: { encoding?: string | null }) => Promise @@ -19,35 +16,12 @@ export interface BackendRoute { getFixture: GetFixtureFn } -export interface BackendRequest { - requestId: string - /** - * The route that matched this request. - */ - route: BackendRoute - onError: (err: Error) => void - /** - * A callback that can be used to make the request go outbound. - */ - continueRequest: Function - /** - * A callback that can be used to send the response through the proxy. - */ - continueResponse?: (newResStream?: Readable) => void - onResponse?: (incomingRes: IncomingMessage, resStream: Readable) => void - req: CypressIncomingRequest - res: CypressOutgoingResponse - incomingRes?: IncomingMessage - /** - * Should we wait for the driver to allow the response to continue? - */ - waitForResponseContinue?: boolean -} - export interface NetStubbingState { - // map of request IDs to requests in flight + pendingEventHandlers: { + [eventId: string]: Function + } requests: { - [requestId: string]: BackendRequest + [requestId: string]: InterceptedRequest } routes: BackendRoute[] reset: () => void diff --git a/packages/net-stubbing/lib/server/util.ts b/packages/net-stubbing/lib/server/util.ts index f427ee1856..456e1bc5cb 100644 --- a/packages/net-stubbing/lib/server/util.ts +++ b/packages/net-stubbing/lib/server/util.ts @@ -11,13 +11,14 @@ import { import { Readable, PassThrough } from 'stream' import CyServer from '@packages/server' import { Socket } from 'net' -import { GetFixtureFn, BackendRequest } from './types' +import { GetFixtureFn } from './types' import ThrottleStream from 'throttle' import MimeTypes from 'mime-types' +import { CypressIncomingRequest } from '@packages/proxy' +import { InterceptedRequest } from './intercepted-request' // TODO: move this into net-stubbing once cy.route is removed import { parseContentType } from '@packages/server/lib/controllers/xhrs' -import { CypressIncomingRequest } from '@packages/proxy' const debug = Debug('cypress:net-stubbing:server:util') @@ -145,7 +146,7 @@ export async function setResponseFromFixture (getFixtureFn: GetFixtureFn, static * @param backendRequest BackendRequest object. * @param staticResponse BackendStaticResponse object. */ -export function sendStaticResponse (backendRequest: Pick, staticResponse: BackendStaticResponse) { +export function sendStaticResponse (backendRequest: Pick, staticResponse: BackendStaticResponse) { const { onError, onResponse } = backendRequest if (staticResponse.forceNetworkError) { @@ -165,13 +166,13 @@ export function sendStaticResponse (backendRequest: Pick { @@ -195,7 +196,7 @@ export function getBodyStream (body: Buffer | string | Readable | undefined, opt return writable.end() } - delay ? setTimeout(sendBody, delay) : sendBody() + delayMs ? setTimeout(sendBody, delayMs) : sendBody() return pt } diff --git a/packages/net-stubbing/package.json b/packages/net-stubbing/package.json index 2daeb7d772..7ad8d30500 100644 --- a/packages/net-stubbing/package.json +++ b/packages/net-stubbing/package.json @@ -11,6 +11,7 @@ "dependencies": { "@types/mime-types": "2.1.0", "is-html": "^2.0.0", + "istextorbinary": "5.12.0", "lodash": "4.17.15", "mime-types": "2.1.27", "minimatch": "^3.0.4", diff --git a/packages/proxy/test/integration/net-stubbing.spec.ts b/packages/proxy/test/integration/net-stubbing.spec.ts index ff26d8f964..bc2fba14e6 100644 --- a/packages/proxy/test/integration/net-stubbing.spec.ts +++ b/packages/proxy/test/integration/net-stubbing.spec.ts @@ -165,21 +165,18 @@ context('network stubbing', () => { }) socket.toDriver.callsFake((_, event, data) => { - if (event === 'http:request:received') { + if (event === 'before:request') { onNetEvent({ - eventName: 'http:request:continue', + eventName: 'send:static:response', + // @ts-ignore frame: { - routeHandlerId: '1', requestId: data.requestId, - req: data.req, staticResponse: { + ...data.data, body: 'replaced', }, - hasResponseHandler: false, - tryNextRoute: false, }, state: netStubbingState, - socket, getFixture, args: [], }) @@ -189,49 +186,19 @@ context('network stubbing', () => { return supertest(app) .get(`/http://localhost:${destinationPort}`) .then((res) => { + expect(res.text).to.eq('replaced') expect(res.headers).to.include({ 'access-control-allow-origin': '*', }) - - expect(res.text).to.eq('replaced') }) }) - it('does not modify multipart/form-data files', () => { + it('does not modify multipart/form-data files', async () => { + const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64') let sendContentLength = '' let receivedContentLength = '' let realContentLength = '' - netStubbingState.routes.push({ - handlerId: '1', - routeMatcher: { - url: '*', - }, - hasInterceptor: true, - getFixture, - }) - - socket.toDriver.callsFake((_, event, data) => { - if (event === 'http:request:received') { - sendContentLength = data.req.headers['content-length'] - onNetEvent({ - eventName: 'http:request:continue', - frame: { - routeHandlerId: '1', - requestId: data.requestId, - req: data.req, - res: data.res, - hasResponseHandler: false, - tryNextRoute: false, - }, - state: netStubbingState, - socket, - getFixture, - args: [], - }) - } - }) - destinationApp.post('/', (req, res) => { const chunks = [] @@ -250,12 +217,45 @@ context('network stubbing', () => { }) }) - return supertest(app) + // capture unintercepted content-length + await supertest(app) .post(`/http://localhost:${destinationPort}`) - .attach('file', Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64')) // 1 pixel png image - .then(() => { - expect(sendContentLength).to.eq(receivedContentLength) - expect(sendContentLength).to.eq(realContentLength) + .attach('file', png) + + netStubbingState.routes.push({ + handlerId: '1', + routeMatcher: { + url: '*', + }, + hasInterceptor: true, + getFixture, }) + + socket.toDriver.callsFake((_, event, data) => { + if (event === 'before:request') { + sendContentLength = data.data.headers['content-length'] + onNetEvent({ + eventName: 'send:static:response', + // @ts-ignore + frame: { + requestId: data.requestId, + staticResponse: { + ...data.data, + }, + }, + state: netStubbingState, + getFixture, + args: [], + }) + } + }) + + // capture content-length after intercepting + await supertest(app) + .post(`/http://localhost:${destinationPort}`) + .attach('file', png) + + expect(sendContentLength).to.eq(receivedContentLength) + expect(sendContentLength).to.eq(realContentLength) }) }) diff --git a/packages/runner/cypress/fixtures/errors/intercept_spec.ts b/packages/runner/cypress/fixtures/errors/intercept_spec.ts new file mode 100644 index 0000000000..8092bc8e08 --- /dev/null +++ b/packages/runner/cypress/fixtures/errors/intercept_spec.ts @@ -0,0 +1,43 @@ +import { SinonStub } from "sinon" + +describe('cy.intercept', () => { + const { $, sinon } = Cypress + + it('fails in req callback', () => { + cy.intercept('/json-content-type', () => { + expect('a').to.eq('b') + }) + .then(() => { + console.log('hi2') + Cypress.emit('net:event', 'before:request', { + eventId: '1', + // @ts-ignore + routeHandlerId: Object.keys(Cypress.state('routes'))[0], + subscription: { + await: true, + }, + data: {} + }) + const { $ } = Cypress + $.get('/json-content-type') + }) + }) + + it('fails in res callback', () => { + cy.intercept('/json-content-type', (req) => { + req.reply(() => { + expect('b').to.eq('c') + }) + }) + .then(() => $.get('/json-content-type')) + }) + + it('fails when erroneous response is received while awaiting response', () => { + cy.intercept('/fake', (req) => { + req.reply(() => { + expect('this should not be reached').to.eq('d') + }) + }) + .then(() => $.get('http://foo.invalid/fake')) + }) +}) \ No newline at end of file diff --git a/packages/runner/cypress/integration/reporter.errors.spec.js b/packages/runner/cypress/integration/reporter.errors.spec.js index 4b306fccfa..7848866c62 100644 --- a/packages/runner/cypress/integration/reporter.errors.spec.js +++ b/packages/runner/cypress/integration/reporter.errors.spec.js @@ -275,6 +275,26 @@ describe('errors ui', () => { }) }) + // FIXME: these cy.fail errors are propagating to window.top + describe.skip('cy.intercept', () => { + const file = 'intercept_spec.ts' + + verify.it('fails in req callback', { + file, + message: 'A request callback passed to cy.intercept() threw an error while intercepting a request', + }) + + verify.it('fails in res callback', { + file, + column: 1, + }) + + verify.it('fails when erroneous response is received while awaiting response', { + file, + column: 1, + }) + }) + describe('cy.route', () => { const file = 'route_spec.js' diff --git a/packages/runner/cypress/support/helpers.js b/packages/runner/cypress/support/helpers.js index fa4670f204..500f0934cb 100644 --- a/packages/runner/cypress/support/helpers.js +++ b/packages/runner/cypress/support/helpers.js @@ -250,6 +250,9 @@ function createCypress (defaultOptions = {}) { cb(opts.state) }) + .withArgs('backend:request', 'net') + .yieldsAsync({}) + .withArgs('backend:request', 'reset:server:state') .yieldsAsync({}) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 78ba2ba694..8e8a04f0ef 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -377,7 +377,6 @@ export class SocketBase { eventName: args[0], frame: args[1], state: options.netStubbingState, - socket: this, getFixture, args, }) diff --git a/yarn.lock b/yarn.lock index ec732ca0cc..5f1b88c41c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2916,7 +2916,7 @@ "@jest/types@^26.3.0", "@jest/types@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + resolved "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" @@ -5412,7 +5412,7 @@ "@types/cheerio@*", "@types/cheerio@0.22.21": version "0.22.21" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" + resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" integrity sha512-aGI3DfswwqgKPiEOTaiHV2ZPC9KEhprpgEbJnv0fZl3SGX0cGgEva1126dGrMC6AJM6v/aihlUgJn9M5DbDZ/Q== dependencies: "@types/node" "*" @@ -5507,7 +5507,7 @@ "@types/enzyme@*", "@types/enzyme@3.10.5": version "3.10.5" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" + resolved "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" integrity sha512-R+phe509UuUYy9Tk0YlSbipRpfVtIzb/9BHn5pTEtjJTF5LXvUjrIQcZvNyANNEyFrd2YGs196PniNT1fgvOQA== dependencies: "@types/cheerio" "*" @@ -9428,6 +9428,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +binaryextensions@^4.15.0: + version "4.15.0" + resolved "https://registry.npmjs.org/binaryextensions/-/binaryextensions-4.15.0.tgz#c63a502e0078ff1b0e9b00a9f74d3c2b0f8bd32e" + integrity sha512-MkUl3szxXolQ2scI1PM14WOT951KnaTNJ0eMKg7WzOI4kvSxyNo/Cygx4LOBNhwyINhAuSQpJW1rYD9aBSxGaw== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -13148,7 +13153,7 @@ debug@^3.0.0, debug@^3.0.1, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2 dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -14191,6 +14196,14 @@ ecstatic@^3.3.2: minimist "^1.1.0" url-join "^2.0.5" +editions@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/editions/-/editions-6.1.0.tgz#ba6c6cf9f4bb571d9e53ea34e771a602e5a66549" + integrity sha512-h6nWEyIocfgho9J3sTSuhU/WoFOu1hTX75rPBebNrbF38Y9QFDjCDizYXdikHTySW7Y3mSxli8bpDz9RAtc7rA== + dependencies: + errlop "^4.0.0" + version-range "^1.0.0" + editor@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742" @@ -14638,6 +14651,11 @@ err-code@^1.0.0: resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= +errlop@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/errlop/-/errlop-4.1.0.tgz#8e7b8f4f1bf0a6feafce4d14f0c0cf4bf5ef036b" + integrity sha512-vul6gGBuVt0M2TPi1/WrcL86+Hb3Q2Tpu3TME3sbVhZrYf7J1ZMHCodI25RQKCVurh56qTfvgM0p3w5cT4reSQ== + errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -19107,7 +19125,7 @@ import-local@2.0.0, import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -20362,6 +20380,15 @@ istanbul@0.4.5: which "^1.1.1" wordwrap "^1.0.0" +istextorbinary@5.12.0: + version "5.12.0" + resolved "https://registry.npmjs.org/istextorbinary/-/istextorbinary-5.12.0.tgz#2f84777838668fdf524c305a2363d6057aaeec84" + integrity sha512-wLDRWD7qpNTYubk04+q3en1+XZGS4vYWK0+SxNSXJLaITMMEK+J3o/TlOMyULeH1qozVZ9uUkKcyMA8odyxz8w== + dependencies: + binaryextensions "^4.15.0" + editions "^6.1.0" + textextensions "^5.11.0" + isurl@^1.0.0-alpha5: version "1.0.0" resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" @@ -22309,11 +22336,6 @@ lodash._basecreate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" integrity sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE= -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -22322,29 +22344,12 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= @@ -22547,11 +22552,6 @@ lodash.reduce@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs= -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= - lodash.set@4.3.2, lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" @@ -27737,7 +27737,7 @@ pretty-error@^2.0.2, pretty-error@^2.1.1: pretty-format@26.4.0, pretty-format@^23.0.1, pretty-format@^24.9.0, pretty-format@^26.6.2: version "26.4.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.4.0.tgz#c08073f531429e9e5024049446f42ecc9f933a3b" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.0.tgz#c08073f531429e9e5024049446f42ecc9f933a3b" integrity sha512-mEEwwpCseqrUtuMbrJG4b824877pM5xald3AkilJ47Po2YLr97/siejYQHqj2oDQBeJNbu+Q0qUuekJ8F0NAPg== dependencies: "@jest/types" "^26.3.0" @@ -31373,7 +31373,7 @@ socket.io-client@3.0.4: socket.io-parser@4.0.2, socket.io-parser@~3.3.0, socket.io-parser@~3.4.0, socket.io-parser@~4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.2.tgz#3d021a9c86671bb079e7c6c806db6a1d9b1bc780" + resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.2.tgz#3d021a9c86671bb079e7c6c806db6a1d9b1bc780" integrity sha512-Bs3IYHDivwf+bAAuW/8xwJgIiBNtlvnjYRc4PbXgniLmcP1BrakBoq/QhO24rgtgW7VZ7uAaswRGxutUnlAK7g== dependencies: "@types/component-emitter" "^1.2.10" @@ -33035,6 +33035,11 @@ text-table@0.2.0, text-table@^0.2.0, text-table@~0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +textextensions@^5.11.0: + version "5.12.0" + resolved "https://registry.npmjs.org/textextensions/-/textextensions-5.12.0.tgz#b908120b5c1bd4bb9eba41423d75b176011ab68a" + integrity sha512-IYogUDaP65IXboCiPPC0jTLLBzYlhhw2Y4b0a2trPgbHNGGGEfuHE6tds+yDcCf4mpNDaGISFzwSSezcXt+d6w== + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" @@ -34532,6 +34537,18 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +version-compare@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/version-compare/-/version-compare-1.1.0.tgz#7b3e67e7e6cec5c72d9c9e586f8854e419ade17c" + integrity sha512-zVKtPOJTC9x23lzS4+4D7J+drq80BXVYAmObnr5zqxxFVH7OffJ1lJlAS7LYsQNV56jx/wtbw0UV7XHLrvd6kQ== + +version-range@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/version-range/-/version-range-1.1.0.tgz#1c233064202ee742afc9d56e21da3b2e15260acf" + integrity sha512-R1Ggfg2EXamrnrV3TkZ6yBNgITDbclB3viwSjbZ3+eK0VVNK4ajkYJTnDz5N0bIMYDtK9MUBvXJUnKO5RWWJ6w== + dependencies: + version-compare "^1.0.0" + victory-area@^34.3.6: version "34.3.12" resolved "https://registry.yarnpkg.com/victory-area/-/victory-area-34.3.12.tgz#875e261aa67079fb0898c58848561578a5dac6f6"