@revoloo/cypress6
Version:
Cypress.io end to end testing tool
205 lines (161 loc) • 5.9 kB
text/typescript
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()
}