mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-11 17:30:02 -06:00
chore: continuing with request to axios changes (#31915)
This commit is contained in:
108
packages/server/lib/cloud/api/axios_middleware/encryption.ts
Normal file
108
packages/server/lib/cloud/api/axios_middleware/encryption.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
||||
import * as enc from '../../encryption'
|
||||
import { PUBLIC_KEY_VERSION } from '../../constants'
|
||||
import crypto, { KeyObject } from 'crypto'
|
||||
import { DecryptionError } from '../cloud_request_errors'
|
||||
import axios from 'axios'
|
||||
|
||||
let encryptionKey: KeyObject
|
||||
|
||||
declare module 'axios' {
|
||||
interface AxiosRequestConfig {
|
||||
encrypt?: 'always' | 'signed' | boolean
|
||||
}
|
||||
}
|
||||
|
||||
const encryptRequest = async (req: InternalAxiosRequestConfig) => {
|
||||
if (!req.data) {
|
||||
throw new Error(`Cannot issue encrypted request to ${req.url} without request body`)
|
||||
}
|
||||
|
||||
encryptionKey ??= crypto.createSecretKey(Uint8Array.from(crypto.randomBytes(32)))
|
||||
|
||||
const { jwe } = await enc.encryptRequest({ body: req.data }, { secretKey: encryptionKey })
|
||||
|
||||
req.headers.set('x-cypress-encrypted', PUBLIC_KEY_VERSION)
|
||||
req.data = jwe
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
const signRequest = (req: InternalAxiosRequestConfig) => {
|
||||
req.headers.set('x-cypress-signature', PUBLIC_KEY_VERSION)
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
const maybeDecryptResponse = async (res: AxiosResponse) => {
|
||||
if (!res.config.encrypt) {
|
||||
return res
|
||||
}
|
||||
|
||||
if (res.config.encrypt === 'always' || res.headers['x-cypress-encrypted']) {
|
||||
try {
|
||||
res.data = await enc.decryptResponse(res.data, encryptionKey)
|
||||
} catch (e) {
|
||||
throw new DecryptionError(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
const maybeDecryptErrorResponse = async (err: AxiosError<any> | Error & { error?: any, statusCode: number, isApiError?: boolean }) => {
|
||||
if (axios.isAxiosError(err) && err.response?.data) {
|
||||
if (err.config?.encrypt === 'always' || err.response?.headers['x-cypress-encrypted']) {
|
||||
try {
|
||||
if (err.response.data) {
|
||||
err.response.data = await enc.decryptResponse(err.response.data, encryptionKey)
|
||||
}
|
||||
} catch (e) {
|
||||
if (err.status && err.status >= 500 || err.status === 404) {
|
||||
throw err
|
||||
}
|
||||
|
||||
throw new DecryptionError(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
const maybeVerifyResponseSignature = (res: AxiosResponse) => {
|
||||
if (res.config.encrypt === 'signed' && !res.headers['x-cypress-signature']) {
|
||||
throw new Error(`Expected signed response for ${res.config.url }`)
|
||||
}
|
||||
|
||||
if (res.headers['x-cypress-signature']) {
|
||||
const dataString = typeof res.data === 'string' ? res.data : JSON.stringify(res.data)
|
||||
const verified = enc.verifySignature(dataString, res.headers['x-cypress-signature'])
|
||||
|
||||
if (!verified) {
|
||||
throw new Error(`Unable to verify response signature for ${res.config.url}`)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Always = req & res MUST be encrypted
|
||||
// true = req MUST be encrypted, res MAY be encrypted, signified by header
|
||||
// signed = verify signature of the response body
|
||||
export const installEncryption = (axios: AxiosInstance) => {
|
||||
axios.interceptors.request.use(encryptRequest, undefined, {
|
||||
runWhen (config) {
|
||||
return config.encrypt === true || config.encrypt === 'always'
|
||||
},
|
||||
})
|
||||
|
||||
axios.interceptors.request.use(signRequest, undefined, {
|
||||
runWhen (config) {
|
||||
return config.encrypt === 'signed'
|
||||
},
|
||||
})
|
||||
|
||||
axios.interceptors.response.use(maybeDecryptResponse, maybeDecryptErrorResponse)
|
||||
axios.interceptors.response.use(maybeVerifyResponseSignature)
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
* The axios Cloud instance should not be used.
|
||||
*/
|
||||
import os from 'os'
|
||||
|
||||
import followRedirects from 'follow-redirects'
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import pkg from '@packages/root'
|
||||
@@ -11,19 +10,43 @@ import agent from '@packages/network/lib/agent'
|
||||
import app_config from '../../../config/app.json'
|
||||
import { installErrorTransform } from './axios_middleware/transform_error'
|
||||
import { installLogging } from './axios_middleware/logging'
|
||||
import { installEncryption } from './axios_middleware/encryption'
|
||||
|
||||
// initialized with an export for testing purposes
|
||||
export const _create = (options: { baseURL?: string } = {}): AxiosInstance => {
|
||||
export interface CreateCloudRequestOptions {
|
||||
/**
|
||||
* The baseURL for all requests for this Cloud Request instance
|
||||
*/
|
||||
baseURL?: string
|
||||
/**
|
||||
* Additional headers for the Cloud Request
|
||||
*/
|
||||
additionalHeaders?: Record<string, string>
|
||||
/**
|
||||
* Whether to include the default logging middleware
|
||||
* @default true
|
||||
*/
|
||||
enableLogging?: boolean
|
||||
/**
|
||||
* Whether to include the default error transformation
|
||||
* @default true
|
||||
*/
|
||||
enableErrorTransform?: boolean
|
||||
}
|
||||
|
||||
// Allows us to create customized Cloud Request instances w/ different baseURL & encryption configuration
|
||||
export const createCloudRequest = (options: CreateCloudRequestOptions = {}): AxiosInstance => {
|
||||
const cfgKey = process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'development'
|
||||
const { baseURL = app_config[cfgKey].api_url, enableLogging = true, enableErrorTransform = true } = options
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: options.baseURL ?? app_config[cfgKey].api_url,
|
||||
baseURL,
|
||||
httpAgent: agent,
|
||||
httpsAgent: agent,
|
||||
headers: {
|
||||
'x-os-name': os.platform(),
|
||||
'x-cypress-version': pkg.version,
|
||||
'User-Agent': `cypress/${pkg.version}`,
|
||||
...options.additionalHeaders,
|
||||
},
|
||||
transport: {
|
||||
// https://github.com/axios/axios/issues/6313#issue-2198831362
|
||||
@@ -43,13 +66,22 @@ export const _create = (options: { baseURL?: string } = {}): AxiosInstance => {
|
||||
},
|
||||
})
|
||||
|
||||
installLogging(instance)
|
||||
installErrorTransform(instance)
|
||||
installEncryption(instance)
|
||||
|
||||
if (enableLogging) {
|
||||
installLogging(instance)
|
||||
}
|
||||
|
||||
if (enableErrorTransform) {
|
||||
installErrorTransform(instance)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
export const CloudRequest = _create()
|
||||
export const CloudRequest = createCloudRequest()
|
||||
|
||||
export type TCloudReqest = ReturnType<typeof createCloudRequest>
|
||||
|
||||
export const isRetryableCloudError = (error: unknown) => {
|
||||
// setting this env via mocha's beforeEach coerces this to a string, even if it's a boolean
|
||||
|
||||
8
packages/server/lib/cloud/api/cloud_request_errors.ts
Normal file
8
packages/server/lib/cloud/api/cloud_request_errors.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export class DecryptionError extends Error {
|
||||
isDecryptionError = true
|
||||
|
||||
constructor (message: string) {
|
||||
super(message)
|
||||
this.name = 'DecryptionError'
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import { PUBLIC_KEY_VERSION } from '../constants'
|
||||
import type { CreateInstanceRequestBody, CreateInstanceResponse } from './create_instance'
|
||||
|
||||
import { transformError } from './axios_middleware/transform_error'
|
||||
import { DecryptionError } from './cloud_request_errors'
|
||||
|
||||
const THIRTY_SECONDS = humanInterval('30 seconds')
|
||||
const SIXTY_SECONDS = humanInterval('60 seconds')
|
||||
@@ -57,15 +58,6 @@ let responseCache = {}
|
||||
|
||||
const CAPTURE_ERRORS = !process.env.CYPRESS_LOCAL_PROTOCOL_PATH
|
||||
|
||||
class DecryptionError extends Error {
|
||||
isDecryptionError = true
|
||||
|
||||
constructor (message: string) {
|
||||
super(message)
|
||||
this.name = 'DecryptionError'
|
||||
}
|
||||
}
|
||||
|
||||
export interface CypressRequestOptions extends OptionsWithUrl {
|
||||
encrypt?: boolean | 'always' | 'signed'
|
||||
method: string
|
||||
|
||||
@@ -69,12 +69,14 @@ export function verifySignatureFromFile (file: string, signature: string, public
|
||||
// in the jose library (https://github.com/panva/jose/blob/main/src/jwe/general/encrypt.ts),
|
||||
// but allows us to keep track of the encrypting key locally, to optionally use it for decryption
|
||||
// of encrypted payloads coming back in the response body.
|
||||
export async function encryptRequest (params: CypressRequestOptions, publicKey?: crypto.KeyObject): Promise<EncryptRequestData> {
|
||||
const key = publicKey || getPublicKey()
|
||||
export async function encryptRequest (params: Pick<CypressRequestOptions, 'body'>, options: {
|
||||
publicKey?: crypto.KeyObject
|
||||
secretKey?: crypto.KeyObject
|
||||
} = {}): Promise<EncryptRequestData> {
|
||||
const { publicKey = getPublicKey(), secretKey = crypto.createSecretKey(crypto.randomBytes(32)) } = options
|
||||
const header = base64Url(JSON.stringify({ alg: 'RSA-OAEP', enc: 'A256GCM', zip: 'DEF' }))
|
||||
const deflated = await deflateRaw(JSON.stringify(params.body))
|
||||
const iv = crypto.randomBytes(12)
|
||||
const secretKey = crypto.createSecretKey(crypto.randomBytes(32))
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', secretKey, iv, { authTagLength: 16 })
|
||||
const aad = new TextEncoder().encode(header)
|
||||
|
||||
@@ -95,7 +97,7 @@ export async function encryptRequest (params: CypressRequestOptions, publicKey?:
|
||||
ciphertext: base64Url(encrypted),
|
||||
recipients: [
|
||||
{
|
||||
encrypted_key: base64Url(crypto.publicEncrypt(key, secretKey.export())),
|
||||
encrypted_key: base64Url(crypto.publicEncrypt(publicKey, secretKey.export())),
|
||||
},
|
||||
],
|
||||
tag: base64Url(cipher.getAuthTag()),
|
||||
|
||||
@@ -55,7 +55,7 @@ const decryptReqBodyAndRespond = ({ reqBody, resBody }, fn) => {
|
||||
expect(params.body).to.deep.eq(reqBody)
|
||||
}
|
||||
|
||||
const { secretKey, jwe } = await encryptRequest(params, publicKey)
|
||||
const { secretKey, jwe } = await encryptRequest(params, { publicKey })
|
||||
|
||||
if (fn) {
|
||||
encryption.encryptRequest.restore()
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import crypto from 'crypto'
|
||||
import { expect } from 'chai'
|
||||
import fs from 'fs'
|
||||
|
||||
import { DestroyableProxy, fakeServer } from './utils/fake_proxy_server'
|
||||
import bodyParser from 'body-parser'
|
||||
import { TEST_PRIVATE } from '@tooling/system-tests/lib/protocol-stubs/protocolStubResponse'
|
||||
import { createCloudRequest, TCloudReqest } from '../../../../lib/cloud/api/cloud_request'
|
||||
import * as jose from 'jose'
|
||||
import dedent from 'dedent'
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
unwrappedSecretKey(): crypto.KeyObject
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('CloudRequest Encryption', () => {
|
||||
let fakeEncryptionServer: DestroyableProxy
|
||||
const app = express()
|
||||
|
||||
let requests: express.Request[] = []
|
||||
|
||||
const encryptBody = async (req: express.Request, res: express.Response, body: object) => {
|
||||
const enc = new jose.GeneralEncrypt(Buffer.from(JSON.stringify(body)))
|
||||
|
||||
enc
|
||||
.setProtectedHeader({ alg: 'A256GCMKW', enc: 'A256GCM', zip: 'DEF' })
|
||||
.addRecipient(req.unwrappedSecretKey())
|
||||
|
||||
res.header('x-cypress-encrypted', 'true')
|
||||
|
||||
return await enc.encrypt()
|
||||
}
|
||||
|
||||
app.use(bodyParser.json())
|
||||
app.use((req, res, next) => {
|
||||
requests.push(req)
|
||||
if (req.headers['x-cypress-encrypted']) {
|
||||
const jwe = req.body
|
||||
|
||||
req.unwrappedSecretKey = () => {
|
||||
return crypto.createSecretKey(
|
||||
crypto.privateDecrypt(
|
||||
TEST_PRIVATE,
|
||||
Buffer.from(jwe.recipients[0].encrypted_key, 'base64url'),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return jose.generalDecrypt(jwe, TEST_PRIVATE).then(({ plaintext }) => Buffer.from(plaintext).toString('utf8')).then((body) => {
|
||||
req.body = JSON.parse(body)
|
||||
next()
|
||||
}).catch(next)
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
function signResponse (req: Request, res: Response, val: Buffer | string) {
|
||||
if (req.headers['x-cypress-signature']) {
|
||||
const sign = crypto.createSign('sha256', {
|
||||
defaultEncoding: 'base64',
|
||||
})
|
||||
|
||||
sign.update(val).end()
|
||||
const signature = sign.sign(TEST_PRIVATE, 'base64')
|
||||
|
||||
res.setHeader('x-cypress-signature', signature)
|
||||
}
|
||||
|
||||
res.write(val)
|
||||
res.end()
|
||||
}
|
||||
|
||||
function invalidSignResponse (req: Request, res: Response, val: Buffer | string) {
|
||||
const hash = crypto.createHash('sha256', {
|
||||
defaultEncoding: 'base64',
|
||||
})
|
||||
|
||||
hash.update(val).end()
|
||||
res.setHeader('x-cypress-signature', hash.digest('base64'))
|
||||
res.write(val)
|
||||
res.end()
|
||||
}
|
||||
|
||||
app.get('/ping', (req, res) => res.json({ pong: 'true' }))
|
||||
|
||||
app.get('/signed', async (req, res) => {
|
||||
const buffer = fs.readFileSync(__filename)
|
||||
|
||||
return signResponse(req, res, buffer)
|
||||
})
|
||||
|
||||
app.get('/invalid-signing', async (req, res) => {
|
||||
const buffer = fs.readFileSync(__filename)
|
||||
|
||||
return invalidSignResponse(req, res, buffer)
|
||||
})
|
||||
|
||||
app.post('/signed-post', async (req, res) => {
|
||||
return signResponse(req, res, JSON.stringify(req.body))
|
||||
})
|
||||
|
||||
app.post('/invalid-signed-post', async (req, res) => {
|
||||
return invalidSignResponse(req, res, JSON.stringify(req.body))
|
||||
})
|
||||
|
||||
app.post('/', async (req, res) => {
|
||||
return res.json(await encryptBody(req, res, req.body))
|
||||
})
|
||||
|
||||
app.post('/error', async (req, res) => {
|
||||
return res.status(400).json(await encryptBody(req, res, {
|
||||
error: 'Some Error',
|
||||
}))
|
||||
})
|
||||
|
||||
let TestReq: TCloudReqest
|
||||
|
||||
before(async () => {
|
||||
fakeEncryptionServer = await fakeServer({}, app)
|
||||
TestReq = createCloudRequest({ baseURL: fakeEncryptionServer.baseUrl })
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
requests = []
|
||||
})
|
||||
|
||||
after(() => fakeEncryptionServer.teardown())
|
||||
|
||||
it('cannot issue encryption request without body', async () => {
|
||||
try {
|
||||
await TestReq.get('/foo', {
|
||||
encrypt: true,
|
||||
})
|
||||
|
||||
throw new Error('Unreachable')
|
||||
} catch (e) {
|
||||
expect(e.message).to.eq('Cannot issue encrypted request to /foo without request body')
|
||||
}
|
||||
})
|
||||
|
||||
it('verifies the signed response', async () => {
|
||||
// Good
|
||||
const data = await TestReq.get('/signed', { encrypt: 'signed' }).then((d) => d.data)
|
||||
|
||||
expect(data).to.equal(fs.readFileSync(__filename, 'utf8'))
|
||||
|
||||
// Bad
|
||||
try {
|
||||
await TestReq.get('/invalid-signing', { encrypt: 'signed' })
|
||||
throw new Error('Unreachable')
|
||||
} catch (e) {
|
||||
expect(e.message).to.equal('Unable to verify response signature for /invalid-signing')
|
||||
}
|
||||
})
|
||||
|
||||
it('enforces a response signature on signed requests', async () => {
|
||||
try {
|
||||
await TestReq.get('/ping', { encrypt: 'signed' })
|
||||
throw new Error('Unreachable')
|
||||
} catch (e) {
|
||||
expect(e.message).to.equal('Expected signed response for /ping')
|
||||
}
|
||||
})
|
||||
|
||||
it('encrypts requests', async () => {
|
||||
const dataObj = (v: number) => {
|
||||
return {
|
||||
foo: {
|
||||
bar: v,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const [res, res2, res3] = await Promise.all([
|
||||
TestReq.post('/', dataObj(1), { encrypt: 'always' }),
|
||||
TestReq.post('/', dataObj(2), { encrypt: 'always' }),
|
||||
TestReq.post('/', dataObj(3), { encrypt: 'always' }),
|
||||
])
|
||||
|
||||
expect(res.data).to.eql(dataObj(1))
|
||||
expect(res2.data).to.eql(dataObj(2))
|
||||
expect(res3.data).to.eql(dataObj(3))
|
||||
})
|
||||
|
||||
it('decrypts errors', async () => {
|
||||
try {
|
||||
await TestReq.post('/error', {
|
||||
foo: true,
|
||||
}, { encrypt: 'always' })
|
||||
|
||||
throw new Error('Unreachable')
|
||||
} catch (e) {
|
||||
expect(e.isApiError).to.be.true
|
||||
|
||||
expect(e.message).to.equal(dedent`
|
||||
400
|
||||
|
||||
{
|
||||
"error": "Some Error"
|
||||
}
|
||||
`)
|
||||
}
|
||||
})
|
||||
|
||||
it('supports a signed response on encrypted requests', async () => {
|
||||
// Good
|
||||
const data = await TestReq.post('/signed-post', {
|
||||
foo: 'bar',
|
||||
}, { encrypt: 'signed' }).then((d) => d.data)
|
||||
|
||||
expect(data).to.eql({ foo: 'bar' })
|
||||
|
||||
// Bad
|
||||
try {
|
||||
await TestReq.post('/invalid-signed-post', {}, {
|
||||
encrypt: 'signed',
|
||||
})
|
||||
|
||||
throw new Error('Unreachable')
|
||||
} catch (e) {
|
||||
expect(e.message).to.equal('Unable to verify response signature for /invalid-signed-post')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -4,13 +4,16 @@ import sinonChai from 'sinon-chai'
|
||||
import chai, { expect } from 'chai'
|
||||
import agent from '@packages/network/lib/agent'
|
||||
import axios, { CreateAxiosDefaults, AxiosInstance } from 'axios'
|
||||
import { _create } from '../../../../lib/cloud/api/cloud_request'
|
||||
import debugLib from 'debug'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import { createCloudRequest } from '../../../../lib/cloud/api/cloud_request'
|
||||
import cloudApi from '../../../../lib/cloud/api'
|
||||
import app_config from '../../../../config/app.json'
|
||||
import os from 'os'
|
||||
import pkg from '@packages/root'
|
||||
import { transformError } from '../../../../lib/cloud/api/axios_middleware/transform_error'
|
||||
import { DestroyableProxy, fakeServer, fakeProxy } from './utils/fake_proxy_server'
|
||||
import dedent from 'dedent'
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
@@ -30,7 +33,7 @@ describe('CloudRequest', () => {
|
||||
}
|
||||
|
||||
it('instantiates with network combined agent', () => {
|
||||
_create()
|
||||
createCloudRequest()
|
||||
const cfg = getCreatedConfig()
|
||||
|
||||
expect(cfg.httpAgent).to.eq(agent)
|
||||
@@ -132,7 +135,7 @@ describe('CloudRequest', () => {
|
||||
}
|
||||
|
||||
if (adapter === 'Axios') {
|
||||
const CloudReq = _create({ baseURL: targetServer.baseUrl })
|
||||
const CloudReq = createCloudRequest({ baseURL: targetServer.baseUrl })
|
||||
|
||||
return CloudReq[method](`/ping`, {}).then((r) => r.data)
|
||||
}
|
||||
@@ -150,14 +153,14 @@ describe('CloudRequest', () => {
|
||||
}
|
||||
|
||||
it('does a basic request', async () => {
|
||||
const CloudReq = _create({ baseURL: fakeHttpUpstream.baseUrl })
|
||||
const CloudReq = createCloudRequest({ baseURL: fakeHttpUpstream.baseUrl })
|
||||
|
||||
expect(await CloudReq.get('/ping').then((r) => r.data)).to.eql('OK')
|
||||
expect(fakeHttpUpstream.requests[0].rawHeaders).to.not.contain('Proxy-Authorization')
|
||||
})
|
||||
|
||||
it('retains Proxy-Authorization for non-proxied requests', async () => {
|
||||
const CloudReq = _create({ baseURL: fakeHttpUpstream.baseUrl })
|
||||
const CloudReq = createCloudRequest({ baseURL: fakeHttpUpstream.baseUrl })
|
||||
|
||||
expect(await CloudReq.get('/ping', {
|
||||
headers: {
|
||||
@@ -310,6 +313,81 @@ describe('CloudRequest', () => {
|
||||
}
|
||||
})
|
||||
|
||||
describe('createCloudRequest', () => {
|
||||
let fakeApp: DestroyableProxy
|
||||
|
||||
before(async () => {
|
||||
fakeApp = await fakeServer({})
|
||||
})
|
||||
|
||||
after(() => fakeApp.teardown())
|
||||
|
||||
let wasEnabled: string
|
||||
|
||||
beforeEach(() => {
|
||||
wasEnabled = debugLib.disable()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
debugLib.enable(wasEnabled)
|
||||
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('can skip installing logging', async () => {
|
||||
debugLib.enable('cypress:server:cloud:api')
|
||||
|
||||
const CloudRequest = createCloudRequest({ baseURL: fakeApp.baseUrl })
|
||||
|
||||
const logSpy = sinon.stub(process.stderr, 'write')
|
||||
|
||||
await CloudRequest.get('/ping')
|
||||
const debugCalls = logSpy.getCalls().flatMap((c) => stripAnsi(String(c.args[0])).trim().replace(/\+(\d+)ms$/, '+?ms'))
|
||||
|
||||
expect(debugCalls).to.eql([
|
||||
'cypress:server:cloud:api get /ping +?ms',
|
||||
'cypress:server:cloud:api get /ping Success: 200 OK -> \n cypress:server:cloud:api Response: \'OK\' +?ms',
|
||||
])
|
||||
|
||||
logSpy.reset()
|
||||
|
||||
const CloudRequestNoLogs = createCloudRequest({ baseURL: fakeApp.baseUrl, enableLogging: false })
|
||||
|
||||
await CloudRequestNoLogs.get('/ping')
|
||||
expect(logSpy.getCalls()).to.eql([])
|
||||
})
|
||||
|
||||
it('can skip installing the error transform', async () => {
|
||||
const CloudRequest = createCloudRequest({ baseURL: fakeApp.baseUrl })
|
||||
|
||||
// Installed
|
||||
try {
|
||||
await CloudRequest.get('/error')
|
||||
throw new Error('Unreachable')
|
||||
} catch (e) {
|
||||
expect(e.isApiError).to.eql(true)
|
||||
expect(e.message).to.equal(dedent`
|
||||
404
|
||||
|
||||
{
|
||||
"ok": false
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
const CloudRequestNoError = createCloudRequest({ baseURL: fakeApp.baseUrl, enableErrorTransform: false })
|
||||
|
||||
// Not Installed
|
||||
try {
|
||||
await CloudRequestNoError.get('/error')
|
||||
throw new Error('Unreachable')
|
||||
} catch (e) {
|
||||
expect(e.isApiError).to.eql(undefined)
|
||||
expect(e.response.data).to.eql({ ok: false })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('headers', () => {
|
||||
const platform = 'sunos'
|
||||
const version = '0.0.0'
|
||||
@@ -328,7 +406,7 @@ describe('CloudRequest', () => {
|
||||
})
|
||||
|
||||
it('sets exepcted platform, version, and user-agent headers', () => {
|
||||
_create()
|
||||
createCloudRequest()
|
||||
const cfg = getCreatedConfig()
|
||||
|
||||
expect(cfg.headers).to.have.property('x-os-name', platform)
|
||||
@@ -358,7 +436,7 @@ describe('CloudRequest', () => {
|
||||
|
||||
;(axios.create as sinon.SinonStub).returns(stubbedAxiosInstance)
|
||||
|
||||
_create()
|
||||
createCloudRequest()
|
||||
})
|
||||
|
||||
it('registers error transformation interceptor', () => {
|
||||
@@ -388,7 +466,7 @@ describe('CloudRequest', () => {
|
||||
})
|
||||
|
||||
it('sets to the value defined in app config', () => {
|
||||
_create()
|
||||
createCloudRequest()
|
||||
const cfg = getCreatedConfig()
|
||||
|
||||
expect(cfg.baseURL).to.eq(app_config[env ?? 'development']?.api_url)
|
||||
@@ -416,7 +494,7 @@ describe('CloudRequest', () => {
|
||||
})
|
||||
|
||||
it('sets to the value defined in app config', () => {
|
||||
_create()
|
||||
createCloudRequest()
|
||||
const cfg = getCreatedConfig()
|
||||
|
||||
expect(cfg.baseURL).to.eq(app_config[env ?? 'development']?.api_url)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
import http from 'http'
|
||||
import { AddressInfo } from 'net'
|
||||
import express from 'express'
|
||||
import express, { Application } from 'express'
|
||||
import Promise from 'bluebird'
|
||||
import debugLib from 'debug'
|
||||
import DebuggingProxy from '@cypress/debugging-proxy'
|
||||
@@ -23,6 +23,10 @@ app.post('/ping', (req, res) => {
|
||||
res.json({ ok: true, auth: req.headers['authorization'] })
|
||||
})
|
||||
|
||||
app.get('/error', (req, res) => {
|
||||
res.status(404).json({ ok: false })
|
||||
})
|
||||
|
||||
interface DestroyableProxyOptions {
|
||||
keepRequests?: boolean
|
||||
auth?: {
|
||||
@@ -91,7 +95,7 @@ interface FakeProxyOptions {
|
||||
}
|
||||
}
|
||||
|
||||
export async function fakeServer (opts: FakeServerOptions) {
|
||||
export async function fakeServer (opts: FakeServerOptions, serverApp: Application = app) {
|
||||
const port = await getPort()
|
||||
const server = new DestroyableProxy({
|
||||
auth: opts.auth,
|
||||
@@ -111,7 +115,7 @@ export async function fakeServer (opts: FakeServerOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
app(req, res)
|
||||
serverApp(req, res)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('encryption', () => {
|
||||
const { jwe, secretKey } = await encryption.encryptRequest({
|
||||
encrypt: true,
|
||||
body: TEST_BODY,
|
||||
}, publicKey)
|
||||
}, { publicKey })
|
||||
|
||||
const { plaintext } = await jose.generalDecrypt(jwe, privateKey)
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('encryption', () => {
|
||||
const { jwe, secretKey } = await encryption.encryptRequest({
|
||||
encrypt: true,
|
||||
body: TEST_BODY,
|
||||
}, publicKey)
|
||||
}, { publicKey })
|
||||
|
||||
const RESPONSE_BODY = { runId: 123 }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user