mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-07 23:40:21 -05:00
feat: refactor cy.intercept internals to generic event-based system (#15255)
This commit is contained in:
@@ -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,
|
||||
|
||||
+13
-9
@@ -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
|
||||
}
|
||||
+58
-42
@@ -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)
|
||||
})
|
||||
|
||||
+35
-33
@@ -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)
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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({})
|
||||
|
||||
|
||||
@@ -377,7 +377,6 @@ export class SocketBase {
|
||||
eventName: args[0],
|
||||
frame: args[1],
|
||||
state: options.netStubbingState,
|
||||
socket: this,
|
||||
getFixture,
|
||||
args,
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user