feat: refactor cy.intercept internals to generic event-based system (#15255)

This commit is contained in:
Zach Bloomquist
2021-03-03 13:21:52 -05:00
committed by GitHub
parent 4f63851029
commit a7655d3c20
31 changed files with 926 additions and 723 deletions
+9
View File
@@ -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.
@@ -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(() => {
@@ -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: <string>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', <StaticResponse>handler)
validateStaticResponse('cy.intercept', <StaticResponse>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,
@@ -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<NetEventFrames.HttpRequestComplete> = (Cypress, frame, { failCurrentTest, getRequest, getRoute }) => {
export const onAfterResponse: HandlerFn<CyHttpMessages.ResponseComplete> = (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<NetEventFrames.HttpRequestComplete> =
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
}
@@ -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<NetEventFrames.HttpRequestReceived> = (Cypress, frame, { getRoute, emitNetEvent }) => {
export const onBeforeRequest: HandlerFn<CyHttpMessages.IncomingRequest> = (Cypress, frame, userHandler, { getRoute, emitNetEvent, sendStaticResponse }) => {
function getRequestLog (route: Route, request: Omit<Interception, 'log'>) {
return Cypress.log({
name: 'xhr',
@@ -48,22 +47,35 @@ export const onRequestReceived: HandlerFn<NetEventFrames.HttpRequestReceived> =
}
const route = getRoute(frame.routeHandlerId)
const { req, requestId, routeHandlerId } = frame
const { data: req, requestId, routeHandlerId } = frame
parseJsonBody(req)
const request: Partial<Interception> = {
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<NetEventFrames.HttpRequestContinue> = {
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<NetEventFrames.HttpRequestReceived> =
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<NetEventFrames.HttpRequestReceived> =
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<NetEventFrames.HttpRequestReceived> =
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<CyHttpMessages.IncomingRequest> = new Promise((_resolve) => {
resolve = _resolve
})
request.log = getRequestLog(route, request as Omit<Interception, 'log'>)
@@ -162,24 +182,20 @@ export const onRequestReceived: HandlerFn<NetEventFrames.HttpRequestReceived> =
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<NetEventFrames.HttpRequestReceived> =
if (!replyCalled) {
// handler function resolved without resolving request, pass on
continueFrame.tryNextRoute = true
sendContinueFrame()
}
})
.return(promise) as any as Bluebird<CyHttpMessages.IncomingRequest>
}
@@ -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<Frame extends NetEventFrames.BaseHttp> = (Cypress: Cypress.Cypress, frame: Frame, opts: {
export type HandlerFn<D> = (Cypress: Cypress.Cypress, frame: NetEvent.ToDriver.Event<D>, userHandler: (data: D) => void | Promise<void>, opts: {
getRequest: (routeHandlerId: string, requestId: string) => Interception | undefined
getRoute: (routeHandlerId: string) => Route | undefined
emitNetEvent: (eventName: string, frame: any) => Promise<void>
failCurrentTest: (err: Error) => void
}) => Promise<void> | void
sendStaticResponse: (requestId: string, staticResponse: StaticResponse) => void
}) => Promise<D> | D
const netEventHandlers: { [eventName: string]: HandlerFn<any> } = {
'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<any>) => {
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)
})
@@ -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<NetEventFrames.HttpResponseReceived> = (Cypress, frame, { getRoute, getRequest, emitNetEvent }) => {
const { res, requestId, routeHandlerId } = frame
export const onResponse: HandlerFn<CyHttpMessages.IncomingResponse> = 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<NetEventFrames.HttpResponseReceived>
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<NetEventFrames.HttpResponseReceived>
_.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<CyHttpMessages.IncomingResponse> = 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<NetEventFrames.HttpResponseReceived>
.finally(() => {
resolved = true
})
.return(promise)
}
@@ -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<StaticResponse>): 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)
+2
View File
@@ -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?
+32 -12
View File
@@ -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<T> {
@@ -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> | 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<string, string | object> & {
/**
* Milliseconds to delay before the response is sent.
* @deprecated Use `delay` instead of `delayMs`.
*/
delayMs?: number
}
export type StaticResponse = GenericStaticResponse<string, string | object>
export interface GenericStaticResponse<Fixture, Body> {
/**
@@ -332,7 +352,7 @@ export interface GenericStaticResponse<Fixture, Body> {
/**
* Milliseconds to delay before the response is sent.
*/
delay?: number
delayMs?: number
}
/**
+30 -43
View File
@@ -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<AnnotatedStringMatcher>
/** 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<D> 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
}
}
}
@@ -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<any> {
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, <NetEventFrames.AddRoute>frame)
case 'http:request:continue':
return onRequestContinue(state, <NetEventFrames.HttpRequestContinue>frame, socket)
case 'http:response:continue':
return onResponseContinue(state, <NetEventFrames.HttpResponseContinue>frame)
return onRouteAdded(state, getFixture, <NetEvent.ToServer.AddRoute>frame)
case 'subscribe':
return subscribe(state, <NetEvent.ToServer.Subscribe>frame)
case 'event:handler:resolved':
return eventHandlerResolved(state, <NetEvent.ToServer.EventHandlerResolved>frame)
case 'send:static:response':
return sendStaticResponse(state, getFixture, <NetEvent.ToServer.SendStaticResponse>frame)
default:
throw new Error(`Unrecognized net event: ${eventName}`)
}
+3 -3
View File
@@ -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'
@@ -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()
}
@@ -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()
}
@@ -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)
}
@@ -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<InterceptedRequest, 'req' | 'res' | 'continueRequest' | 'onError' | 'onResponse' | 'subscriptions' | 'state' | 'socket'>) {
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<D> ({ 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<D> {
const handleSubscription = async (subscription: Subscription) => {
const eventId = _.uniqueId('event')
const eventFrame: NetEvent.ToDriver.Event<any> = {
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
}
}
@@ -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<CyHttpMessages.ResponseComplete>({
eventName: 'after:response',
data: {
error: errors.clone(this.error),
},
mergeChanges: _.identity,
})
this.next()
}
@@ -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<CyHttpMessages.ResponseComplete>({
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<void>((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<CyHttpMessages.IncomingRequest>({
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()
}
@@ -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<Buffer>((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<CyHttpMessages.IncomingResponse>({
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)
}
@@ -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
@@ -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 = []
},
+7 -33
View File
@@ -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<any>
@@ -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
+8 -7
View File
@@ -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<BackendRequest, 'onError' | 'onResponse'>, staticResponse: BackendStaticResponse) {
export function sendStaticResponse (backendRequest: Pick<InterceptedRequest, 'onError' | 'onResponse'>, staticResponse: BackendStaticResponse) {
const { onError, onResponse } = backendRequest
if (staticResponse.forceNetworkError) {
@@ -165,13 +166,13 @@ export function sendStaticResponse (backendRequest: Pick<BackendRequest, 'onErro
body,
})
const bodyStream = getBodyStream(body, _.pick(staticResponse, 'throttleKbps', 'delay'))
const bodyStream = getBodyStream(body, _.pick(staticResponse, 'throttleKbps', 'delayMs'))
onResponse!(incomingRes, bodyStream)
}
export function getBodyStream (body: Buffer | string | Readable | undefined, options: { delay?: number, throttleKbps?: number }): Readable {
const { delay, throttleKbps } = options
export function getBodyStream (body: Buffer | string | Readable | undefined, options: { delayMs?: number, throttleKbps?: number }): Readable {
const { delayMs, throttleKbps } = options
const pt = new PassThrough()
const sendBody = () => {
@@ -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
}
+1
View File
@@ -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",
@@ -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)
})
})
@@ -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'))
})
})
@@ -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'
@@ -250,6 +250,9 @@ function createCypress (defaultOptions = {}) {
cb(opts.state)
})
.withArgs('backend:request', 'net')
.yieldsAsync({})
.withArgs('backend:request', 'reset:server:state')
.yieldsAsync({})
-1
View File
@@ -377,7 +377,6 @@ export class SocketBase {
eventName: args[0],
frame: args[1],
state: options.netStubbingState,
socket: this,
getFixture,
args,
})
+52 -35
View File
@@ -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"