mirror of
https://github.com/cypress-io/cypress.git
synced 2025-12-30 19:19:53 -06:00
* chore(deps): update dependency chai to v3 * empty commit * update some tests to new syntax * update tests + bump cache * update system test expect * update more chai usage * update more chai usage * cleanup * fix some leftover empty functions --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jennifer Shehane <jennifer@cypress.io> Co-authored-by: Jennifer Shehane <shehane.jennifer@gmail.com>
1588 lines
44 KiB
JavaScript
1588 lines
44 KiB
JavaScript
const crypto = require('crypto')
|
|
const jose = require('jose')
|
|
const base64Url = require('base64url')
|
|
const stealthyRequire = require('stealthy-require')
|
|
|
|
require('../../../spec_helper')
|
|
|
|
const _ = require('lodash')
|
|
const os = require('os')
|
|
const encryption = require('../../../../lib/cloud/encryption')
|
|
|
|
const {
|
|
agent,
|
|
} = require('@packages/network')
|
|
const pkg = require('@packages/root')
|
|
const api = require('../../../../lib/cloud/api').default
|
|
const cache = require('../../../../lib/cache').cache
|
|
const errors = require('../../../../lib/errors')
|
|
const machineId = require('../../../../lib/cloud/machine_id')
|
|
const Promise = require('bluebird')
|
|
|
|
const API_BASEURL = 'http://localhost:1234'
|
|
const API_PROD_BASEURL = 'https://api.cypress.io'
|
|
const API_PROD_PROXY_BASEURL = 'https://api-proxy.cypress.io'
|
|
const CLOUD_BASEURL = 'http://localhost:3000'
|
|
const AUTH_URLS = {
|
|
'dashboardAuthUrl': 'http://localhost:3000/test-runner.html',
|
|
'dashboardLogoutUrl': 'http://localhost:3000/logout',
|
|
}
|
|
|
|
const {
|
|
PROTOCOL_STUB_VALID,
|
|
} = require('@tooling/system-tests/lib/protocol-stubs/protocolStubResponse')
|
|
|
|
const makeError = (details = {}) => {
|
|
return _.extend(new Error(details.message || 'Some error'), details)
|
|
}
|
|
|
|
const OS_PLATFORM = 'linux'
|
|
|
|
const encryptRequest = encryption.encryptRequest
|
|
|
|
const decryptReqBodyAndRespond = ({ reqBody, resBody }, fn) => {
|
|
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
|
|
modulusLength: 2048,
|
|
})
|
|
|
|
/**
|
|
* @type {crypto.KeyObject}
|
|
*/
|
|
let _secretKey
|
|
|
|
sinon.stub(encryption, 'encryptRequest').callsFake(async (params) => {
|
|
if (reqBody) {
|
|
expect(params.body).to.deep.eq(reqBody)
|
|
}
|
|
|
|
const { secretKey, jwe } = await encryptRequest(params, publicKey)
|
|
|
|
if (fn) {
|
|
encryption.encryptRequest.restore()
|
|
}
|
|
|
|
_secretKey = secretKey
|
|
|
|
return { secretKey, jwe }
|
|
})
|
|
|
|
return async (uri, encReqBody) => {
|
|
const decryptedSecretKey = crypto.createSecretKey(
|
|
crypto.privateDecrypt(
|
|
privateKey,
|
|
Buffer.from(base64Url.toBase64(encReqBody.recipients[0].encrypted_key), 'base64'),
|
|
),
|
|
)
|
|
|
|
expect(_secretKey.export().toString('utf8')).to.eq(decryptedSecretKey.export().toString('utf8'))
|
|
|
|
const enc = new jose.GeneralEncrypt(
|
|
Buffer.from(JSON.stringify(resBody)),
|
|
)
|
|
|
|
enc.setProtectedHeader({ alg: 'A256GCMKW', enc: 'A256GCM', zip: 'DEF' }).addRecipient(decryptedSecretKey)
|
|
|
|
const jweResponse = await enc.encrypt()
|
|
|
|
fn && fn()
|
|
|
|
return jweResponse
|
|
}
|
|
}
|
|
|
|
const preflightNock = (baseUrl) => {
|
|
return nock(baseUrl)
|
|
.matchHeader('x-route-version', '1')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/preflight')
|
|
}
|
|
|
|
describe('lib/cloud/api', () => {
|
|
beforeEach(() => {
|
|
api.setPreflightResult({ encrypt: false })
|
|
|
|
preflightNock(API_BASEURL)
|
|
.reply(200, decryptReqBodyAndRespond({
|
|
resBody: {
|
|
encrypt: false,
|
|
apiUrl: `${API_BASEURL}/`,
|
|
},
|
|
}))
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '2')
|
|
.get('/auth')
|
|
.reply(200, AUTH_URLS)
|
|
|
|
api.clearCache()
|
|
sinon.stub(os, 'platform').returns(OS_PLATFORM)
|
|
|
|
if (this.oldEnv) {
|
|
process.env = this.oldEnv
|
|
}
|
|
|
|
this.oldEnv = Object.assign({}, process.env)
|
|
|
|
process.env.DISABLE_API_RETRIES = true
|
|
|
|
return sinon.stub(cache, 'getUser').resolves({
|
|
name: 'foo bar',
|
|
email: 'foo@bar',
|
|
//authToken: 'auth-token-123'
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
api.resetPreflightResult()
|
|
sinon.restore()
|
|
})
|
|
|
|
context('.rp', () => {
|
|
beforeEach(() => {
|
|
sinon.spy(agent, 'addRequest')
|
|
|
|
return nock.enableNetConnect()
|
|
}) // nock will prevent requests from reaching the agent
|
|
|
|
it('makes calls using the correct agent', () => {
|
|
nock.cleanAll()
|
|
|
|
return api.ping()
|
|
.thenThrow()
|
|
.catch(() => {
|
|
expect(agent.addRequest).to.be.calledOnce
|
|
|
|
expect(agent.addRequest).to.be.calledWithMatch(sinon.match.any, {
|
|
href: 'http://localhost:1234/ping',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('sets rejectUnauthorized on the request', () => {
|
|
nock.cleanAll()
|
|
|
|
return api.ping()
|
|
.thenThrow()
|
|
.catch(() => {
|
|
expect(agent.addRequest).to.be.calledOnce
|
|
|
|
expect(agent.addRequest).to.be.calledWithMatch(sinon.match.any, {
|
|
rejectUnauthorized: true,
|
|
})
|
|
})
|
|
})
|
|
|
|
context('with a proxy defined', () => {
|
|
beforeEach(function () {
|
|
nock.cleanAll()
|
|
})
|
|
|
|
it('makes calls using the correct agent', () => {
|
|
process.env.HTTP_PROXY = (process.env.HTTPS_PROXY = 'http://foo.invalid:1234')
|
|
process.env.NO_PROXY = ''
|
|
|
|
return api.ping()
|
|
.thenThrow()
|
|
.catch(() => {
|
|
expect(agent.addRequest).to.be.calledOnce
|
|
|
|
expect(agent.addRequest).to.be.calledWithMatch(sinon.match.any, {
|
|
href: 'http://localhost:1234/ping',
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.ping', () => {
|
|
it('GET /ping', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.get('/ping')
|
|
.reply(200, 'OK')
|
|
|
|
return api.ping()
|
|
.then((resp) => {
|
|
expect(resp).to.eq('OK')
|
|
})
|
|
})
|
|
|
|
it('tags errors', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/ping')
|
|
.reply(500, {})
|
|
|
|
return api.ping()
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
expect(err).to.have.property('isApiError', true)
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.sendPreflight', () => {
|
|
let prodApi
|
|
|
|
beforeEach(function () {
|
|
this.timeout(30000)
|
|
|
|
nock.cleanAll()
|
|
sinon.restore()
|
|
sinon.stub(os, 'platform').returns(OS_PLATFORM)
|
|
|
|
process.env.CYPRESS_CONFIG_ENV = 'production'
|
|
process.env.CYPRESS_API_URL = 'https://some.server.com'
|
|
|
|
if (!prodApi) {
|
|
prodApi = stealthyRequire(require.cache, () => {
|
|
return require('../../../../lib/cloud/api').default
|
|
}, () => {
|
|
require('../../../../lib/cloud/encryption')
|
|
}, module)
|
|
}
|
|
|
|
prodApi.resetPreflightResult()
|
|
})
|
|
|
|
it('POST /preflight to proxy. returns encryption', () => {
|
|
preflightNock(API_PROD_PROXY_BASEURL)
|
|
.reply(200, decryptReqBodyAndRespond({
|
|
reqBody: {
|
|
envUrl: 'https://some.server.com',
|
|
dependencies: {},
|
|
errors: [],
|
|
apiUrl: 'https://api.cypress.io/',
|
|
projectId: 'abc123',
|
|
},
|
|
resBody: {
|
|
encrypt: true,
|
|
apiUrl: `${API_PROD_BASEURL}/`,
|
|
},
|
|
}))
|
|
|
|
return prodApi.sendPreflight({ projectId: 'abc123' })
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
|
|
})
|
|
})
|
|
|
|
it('POST /preflight to proxy, and then api on response status code failure. returns encryption', () => {
|
|
const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
|
|
.reply(500)
|
|
|
|
const scopeApi = preflightNock(API_PROD_BASEURL)
|
|
.reply(200, decryptReqBodyAndRespond({
|
|
reqBody: {
|
|
envUrl: 'https://some.server.com',
|
|
dependencies: {},
|
|
errors: [],
|
|
apiUrl: 'https://api.cypress.io/',
|
|
projectId: 'abc123',
|
|
},
|
|
resBody: {
|
|
encrypt: true,
|
|
apiUrl: `${API_PROD_BASEURL}/`,
|
|
},
|
|
}))
|
|
|
|
return prodApi.sendPreflight({ projectId: 'abc123' })
|
|
.then((ret) => {
|
|
scopeProxy.done()
|
|
scopeApi.done()
|
|
expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
|
|
})
|
|
})
|
|
|
|
it('POST /preflight to proxy, and then api on network failure. returns encryption', () => {
|
|
const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
|
|
.replyWithError('some request error')
|
|
|
|
const scopeApi = preflightNock(API_PROD_BASEURL)
|
|
.reply(200, decryptReqBodyAndRespond({
|
|
reqBody: {
|
|
envUrl: 'https://some.server.com',
|
|
dependencies: {},
|
|
errors: [],
|
|
apiUrl: 'https://api.cypress.io/',
|
|
projectId: 'abc123',
|
|
},
|
|
resBody: {
|
|
encrypt: true,
|
|
apiUrl: `${API_PROD_BASEURL}/`,
|
|
},
|
|
}))
|
|
|
|
return prodApi.sendPreflight({ projectId: 'abc123' })
|
|
.then((ret) => {
|
|
scopeProxy.done()
|
|
scopeApi.done()
|
|
expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
|
|
})
|
|
})
|
|
|
|
it('sets timeout to 5 seconds when no CYPRESS_INITIAL_PREFLIGHT_TIMEOUT env is set', () => {
|
|
sinon.stub(api.rp, 'post').resolves({})
|
|
|
|
return api.sendPreflight({})
|
|
.then(() => {
|
|
expect(api.rp.post).to.be.calledWithMatch({ timeout: 5000 })
|
|
})
|
|
})
|
|
|
|
describe('when CYPRESS_INITIAL_PREFLIGHT_TIMEOUT env is set to a negative number', () => {
|
|
const configuredTimeout = -1
|
|
let prevEnv
|
|
|
|
beforeEach(() => {
|
|
prevEnv = process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT
|
|
process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT = configuredTimeout
|
|
})
|
|
|
|
afterEach(() => {
|
|
process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT = prevEnv
|
|
})
|
|
|
|
it('skips the no-agent preflight request', () => {
|
|
preflightNock(API_PROD_PROXY_BASEURL)
|
|
.replyWithError('should not be called')
|
|
|
|
preflightNock(API_PROD_BASEURL)
|
|
.reply(200, decryptReqBodyAndRespond({
|
|
reqBody: {
|
|
envUrl: 'https://some.server.com',
|
|
dependencies: {},
|
|
errors: [],
|
|
apiUrl: 'https://api.cypress.io/',
|
|
projectId: 'abc123',
|
|
},
|
|
resBody: {
|
|
encrypt: true,
|
|
apiUrl: `${API_PROD_BASEURL}/`,
|
|
},
|
|
}))
|
|
|
|
return prodApi.sendPreflight({ projectId: 'abc123' })
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when CYPRESS_INITIAL_PREFLIGHT_TIMEOUT env is set to a positive number', () => {
|
|
const configuredTimeout = 10000
|
|
let prevEnv
|
|
|
|
beforeEach(() => {
|
|
prevEnv = process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT
|
|
process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT = configuredTimeout
|
|
})
|
|
|
|
afterEach(() => {
|
|
process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT = prevEnv
|
|
api.rp.post.restore()
|
|
})
|
|
|
|
it('makes the initial request with the number set in the env', () => {
|
|
sinon.stub(api.rp, 'post').resolves({})
|
|
|
|
return api.sendPreflight({})
|
|
.then(() => {
|
|
expect(api.rp.post).to.be.calledWithMatch({ timeout: configuredTimeout })
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('errors', () => {
|
|
it('[F1] POST /preflight TimeoutError', () => {
|
|
preflightNock(API_BASEURL)
|
|
.times(2)
|
|
.delayConnection(5000)
|
|
.reply(200, {})
|
|
|
|
return api.sendPreflight({
|
|
timeout: 100,
|
|
})
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
expect(err.message).to.eq('Error: ESOCKETTIMEDOUT')
|
|
})
|
|
})
|
|
|
|
it('[F1] POST /preflight RequestError', () => {
|
|
const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
|
|
.replyWithError('first request error')
|
|
|
|
const scopeApi = preflightNock(API_PROD_BASEURL)
|
|
.replyWithError('2nd request error')
|
|
|
|
return prodApi.sendPreflight({ projectId: 'abc123' })
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
scopeProxy.done()
|
|
scopeApi.done()
|
|
|
|
expect(err).not.to.have.property('statusCode')
|
|
expect(err).to.contain({
|
|
name: 'RequestError',
|
|
message: 'Error: 2nd request error',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('[F1] POST /preflight statusCode >= 500', () => {
|
|
const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
|
|
.reply(500)
|
|
|
|
const scopeApi = preflightNock(API_PROD_BASEURL)
|
|
.reply(500)
|
|
|
|
return prodApi.sendPreflight({ projectId: 'abc123' })
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
scopeProxy.done()
|
|
scopeApi.done()
|
|
|
|
expect(err).to.contain({
|
|
name: 'StatusCodeError',
|
|
statusCode: 500,
|
|
})
|
|
})
|
|
})
|
|
|
|
it('[F2] POST /preflight statusCode = 404', () => {
|
|
const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
|
|
.reply(404)
|
|
|
|
const scopeApi = preflightNock(API_PROD_BASEURL)
|
|
.reply(404, '<html>404 not found</html>', {
|
|
'Content-Type': 'text/html',
|
|
})
|
|
|
|
return prodApi.sendPreflight({ projectId: 'abc123' })
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
scopeProxy.done()
|
|
scopeApi.done()
|
|
|
|
expect(err).to.contain({
|
|
name: 'StatusCodeError',
|
|
statusCode: 404,
|
|
})
|
|
})
|
|
})
|
|
|
|
it('[F3] POST /preflight statusCode = 422 but decrypt error', () => {
|
|
const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
|
|
.reply(422, { data: 'very encrypted and secure string' })
|
|
|
|
const scopeApi = preflightNock(API_PROD_BASEURL)
|
|
.reply(422, { data: 'very encrypted and secure string' })
|
|
|
|
return prodApi.sendPreflight({ projectId: 'abc123' })
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
scopeProxy.done()
|
|
scopeApi.done()
|
|
|
|
expect(err).not.to.have.property('statusCode')
|
|
expect(err).to.have.property('name', 'DecryptionError')
|
|
expect(err).to.have.property('message', 'JWE Recipients missing or incorrect type')
|
|
})
|
|
})
|
|
|
|
it('[F3] POST /preflight statusCode = 200 but decrypt error', () => {
|
|
const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
|
|
.reply(200, { data: 'very encrypted and secure string' })
|
|
|
|
const scopeApi = preflightNock(API_PROD_BASEURL)
|
|
.reply(201, 'very encrypted and secure string')
|
|
|
|
return prodApi.sendPreflight({ projectId: 'abc123' })
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
scopeProxy.done()
|
|
scopeApi.done()
|
|
|
|
expect(err).not.to.have.property('statusCode')
|
|
expect(err).to.have.property('name', 'DecryptionError')
|
|
expect(err).to.have.property('message', 'General JWE must be an object')
|
|
})
|
|
})
|
|
|
|
it('[F3] POST /preflight statusCode = 201 but no body', () => {
|
|
const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
|
|
.reply(200)
|
|
|
|
const scopeApi = preflightNock(API_PROD_BASEURL)
|
|
.reply(201)
|
|
|
|
return prodApi.sendPreflight({ projectId: 'abc123' })
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
scopeProxy.done()
|
|
scopeApi.done()
|
|
|
|
expect(err).not.to.have.property('statusCode')
|
|
expect(err).to.have.property('name', 'DecryptionError')
|
|
expect(err).to.have.property('message', 'General JWE must be an object')
|
|
})
|
|
})
|
|
|
|
it('[F4] POST /preflight statusCode = 412 valid decryption', () => {
|
|
const scopeProxy = preflightNock(API_PROD_PROXY_BASEURL)
|
|
.reply(412, decryptReqBodyAndRespond({
|
|
reqBody: {
|
|
envUrl: 'https://some.server.com',
|
|
dependencies: {},
|
|
errors: [],
|
|
apiUrl: 'https://api.cypress.io/',
|
|
projectId: 'abc123',
|
|
},
|
|
resBody: {
|
|
message: 'Recording is not working',
|
|
errors: [
|
|
'attempted to send invalid data',
|
|
],
|
|
object: {
|
|
projectId: 'cy12345',
|
|
},
|
|
},
|
|
}))
|
|
|
|
const scopeApi = preflightNock(API_PROD_BASEURL)
|
|
.reply(200)
|
|
|
|
return prodApi.sendPreflight({ projectId: 'abc123' })
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
scopeProxy.done()
|
|
expect(scopeApi.isDone()).to.be.false
|
|
|
|
expect(err).to.contain({
|
|
name: 'StatusCodeError',
|
|
message: '412 - {"message":"Recording is not working","errors":["attempted to send invalid data"],"object":{"projectId":"cy12345"}}',
|
|
statusCode: 412,
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.createRun', () => {
|
|
beforeEach(function () {
|
|
this.protocolManager = {
|
|
prepareAndSetupProtocol: sinon.stub(),
|
|
}
|
|
|
|
this.buildProps = {
|
|
group: null,
|
|
parallel: null,
|
|
ciBuildId: null,
|
|
projectId: 'id-123',
|
|
recordKey: 'token-123',
|
|
testingType: 'e2e',
|
|
ci: {
|
|
provider: 'circle',
|
|
buildNumber: '987',
|
|
params: { foo: 'bar' },
|
|
},
|
|
platform: {},
|
|
commit: {
|
|
sha: 'sha',
|
|
branch: 'master',
|
|
authorName: 'brian',
|
|
authorEmail: 'brian@cypress.io',
|
|
message: 'such hax',
|
|
remoteOrigin: 'https://github.com/foo/bar.git',
|
|
},
|
|
specs: ['foo.js', 'bar.js'],
|
|
runnerCapabilities: {
|
|
'protocolMountVersion': 2,
|
|
'dynamicSpecsInSerialMode': true,
|
|
'skipSpecAction': true,
|
|
},
|
|
}
|
|
})
|
|
|
|
it('POST /runs + returns runId', function () {
|
|
nock(API_BASEURL)
|
|
.get('/capture-protocol/script/protocolStub.js')
|
|
.reply(200, PROTOCOL_STUB_VALID.compressed, {
|
|
'x-cypress-signature': PROTOCOL_STUB_VALID.sign,
|
|
'Content-Encoding': 'gzip',
|
|
})
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '4')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/runs', this.buildProps)
|
|
.reply(200, {
|
|
runId: 'new-run-id-123',
|
|
capture: {
|
|
url: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
|
},
|
|
})
|
|
|
|
const protocolManager = this.protocolManager
|
|
const project = {
|
|
set protocolManager (val) {
|
|
// don't override with the setter so that the protocol manager is always the same
|
|
},
|
|
get protocolManager () {
|
|
return protocolManager
|
|
},
|
|
getConfig: () => {
|
|
return {
|
|
port: 1234,
|
|
devServerPublicPathRoute: '/dev-server',
|
|
proxyUrl: 'http://localhost:1234',
|
|
namespace: '__cypress',
|
|
}
|
|
},
|
|
get configDebugData () {
|
|
return {
|
|
filePreprocessorHandlerText: 'function () {}',
|
|
}
|
|
},
|
|
}
|
|
|
|
return api.createRun({
|
|
...this.buildProps,
|
|
project,
|
|
})
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq({
|
|
runId: 'new-run-id-123',
|
|
capture: {
|
|
url: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
|
},
|
|
})
|
|
|
|
expect(this.protocolManager.prepareAndSetupProtocol).to.be.calledWith(
|
|
PROTOCOL_STUB_VALID.value,
|
|
{
|
|
runId: 'new-run-id-123',
|
|
testingType: 'e2e',
|
|
mountVersion: 2,
|
|
projectId: 'id-123',
|
|
cloudApi: {
|
|
url: 'http://localhost:1234/',
|
|
retryWithBackoff: api.retryWithBackoff,
|
|
requestPromise: api.rp,
|
|
},
|
|
projectConfig: {
|
|
port: 1234,
|
|
devServerPublicPathRoute: '/dev-server',
|
|
proxyUrl: 'http://localhost:1234',
|
|
namespace: '__cypress',
|
|
},
|
|
debugData: {
|
|
filePreprocessorHandlerText: 'function () {}',
|
|
},
|
|
mode: 'record',
|
|
},
|
|
)
|
|
})
|
|
})
|
|
|
|
it('POST /runs + returns runId with encryption', function () {
|
|
nock.cleanAll()
|
|
sinon.restore()
|
|
sinon.stub(os, 'platform').returns(OS_PLATFORM)
|
|
|
|
nock(API_BASEURL)
|
|
.get('/capture-protocol/script/protocolStub.js')
|
|
.reply(200, PROTOCOL_STUB_VALID.compressed, {
|
|
'x-cypress-signature': PROTOCOL_STUB_VALID.sign,
|
|
'Content-Encoding': 'gzip',
|
|
})
|
|
|
|
preflightNock(API_BASEURL)
|
|
.reply(200, decryptReqBodyAndRespond({
|
|
resBody: {
|
|
encrypt: true,
|
|
apiUrl: `${API_BASEURL}/`,
|
|
},
|
|
}, () => {
|
|
nock(API_BASEURL)
|
|
.defaultReplyHeaders({ 'x-cypress-encrypted': 'true' })
|
|
.matchHeader('x-route-version', '4')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/runs')
|
|
.reply(200, decryptReqBodyAndRespond({
|
|
reqBody: this.buildProps,
|
|
resBody: {
|
|
runId: 'new-run-id-123',
|
|
capture: {
|
|
url: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
|
},
|
|
},
|
|
}))
|
|
}))
|
|
|
|
const protocolManager = this.protocolManager
|
|
const project = {
|
|
set protocolManager (val) {
|
|
// don't override with the setter so that the protocol manager is always the same
|
|
},
|
|
get protocolManager () {
|
|
return protocolManager
|
|
},
|
|
getConfig: () => {
|
|
return {
|
|
port: 1234,
|
|
devServerPublicPathRoute: '/dev-server',
|
|
proxyUrl: 'http://localhost:1234',
|
|
namespace: '__cypress',
|
|
}
|
|
},
|
|
configDebugData: {
|
|
filePreprocessorHandlerText: 'function () {}',
|
|
},
|
|
}
|
|
|
|
return api.createRun({
|
|
...this.buildProps,
|
|
project,
|
|
})
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq({
|
|
runId: 'new-run-id-123',
|
|
capture: {
|
|
url: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
|
},
|
|
})
|
|
|
|
expect(this.protocolManager.prepareAndSetupProtocol).to.be.calledWith(
|
|
PROTOCOL_STUB_VALID.value,
|
|
{
|
|
runId: 'new-run-id-123',
|
|
testingType: 'e2e',
|
|
mountVersion: 2,
|
|
projectId: 'id-123',
|
|
cloudApi: {
|
|
url: 'http://localhost:1234/',
|
|
retryWithBackoff: api.retryWithBackoff,
|
|
requestPromise: api.rp,
|
|
},
|
|
projectConfig: {
|
|
port: 1234,
|
|
devServerPublicPathRoute: '/dev-server',
|
|
proxyUrl: 'http://localhost:1234',
|
|
namespace: '__cypress',
|
|
},
|
|
debugData: {
|
|
filePreprocessorHandlerText: 'function () {}',
|
|
},
|
|
mode: 'record',
|
|
},
|
|
)
|
|
})
|
|
})
|
|
|
|
it('POST /runs does not call prepareAndSetupProtocol with invalid signature', function () {
|
|
nock(API_BASEURL)
|
|
.get('/capture-protocol/script/protocolStub.js')
|
|
.reply(200, PROTOCOL_STUB_VALID.compressed, {
|
|
'x-cypress-signature': 'invalid',
|
|
'Content-Encoding': 'gzip',
|
|
})
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '4')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/runs', this.buildProps)
|
|
.reply(200, {
|
|
runId: 'new-run-id-123',
|
|
capture: {
|
|
url: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
|
},
|
|
})
|
|
|
|
const protocolManager = this.protocolManager
|
|
const project = {
|
|
set protocolManager (val) {
|
|
// don't override with the setter so that the protocol manager is always the same
|
|
},
|
|
get protocolManager () {
|
|
return protocolManager
|
|
},
|
|
}
|
|
|
|
return api.createRun({
|
|
...this.buildProps,
|
|
project,
|
|
})
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq({
|
|
runId: 'new-run-id-123',
|
|
capture: {
|
|
url: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
|
},
|
|
})
|
|
|
|
expect(this.protocolManager.prepareAndSetupProtocol).not.to.be.called
|
|
})
|
|
})
|
|
|
|
it('POST /runs failure formatting', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '4')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/runs', this.buildProps)
|
|
.reply(422, {
|
|
errors: {
|
|
runId: ['is required'],
|
|
},
|
|
})
|
|
|
|
return api.createRun({
|
|
...this.buildProps,
|
|
protocolManager: this.protocolManager,
|
|
})
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq(`\
|
|
422
|
|
|
|
{
|
|
"errors": {
|
|
"runId": [
|
|
"is required"
|
|
]
|
|
}
|
|
}\
|
|
`)
|
|
|
|
expect(this.protocolManager.prepareAndSetupProtocol).not.to.be.called
|
|
})
|
|
})
|
|
|
|
it('handles timeouts', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '4')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/runs')
|
|
.delayConnection(5000)
|
|
.reply(200, {})
|
|
|
|
return api.createRun({
|
|
timeout: 100,
|
|
})
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq('Error: ESOCKETTIMEDOUT')
|
|
})
|
|
})
|
|
|
|
it('sets timeout to 10 seconds', () => {
|
|
sinon.stub(api.rp, 'post').resolves({ runId: 'foo' })
|
|
|
|
const protocolManager = this.protocolManager
|
|
const project = {
|
|
set protocolManager (val) {
|
|
// don't override with the setter so that the protocol manager is always the same
|
|
},
|
|
get protocolManager () {
|
|
return protocolManager
|
|
},
|
|
}
|
|
|
|
return api.createRun({ project })
|
|
.then(() => {
|
|
expect(api.rp.post).to.be.calledWithMatch({ timeout: 60000 })
|
|
})
|
|
})
|
|
|
|
it('tags errors', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '4')
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/runs', this.buildProps)
|
|
.reply(500, {})
|
|
|
|
return api.createRun({
|
|
...this.buildProps,
|
|
protocolManager: this.protocolManager,
|
|
})
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err).to.have.property('isApiError', true)
|
|
expect(this.protocolManager.prepareAndSetupProtocol).not.to.be.called
|
|
})
|
|
})
|
|
|
|
it('tags errors on /preflight', function () {
|
|
preflightNock(API_BASEURL)
|
|
.times(2)
|
|
.reply(500, {})
|
|
|
|
return api.createRun({})
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
expect(err).to.have.property('isApiError', true)
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.postInstanceTests', () => {
|
|
beforeEach(function () {
|
|
this.props = {
|
|
runId: 'run-id-123',
|
|
instanceId: 'instance-id-123',
|
|
config: {},
|
|
tests: [],
|
|
hooks: [],
|
|
}
|
|
|
|
this.bodyProps = _.omit(this.props, 'instanceId', 'runId')
|
|
})
|
|
|
|
it('POSTs /instances/:id/results', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '1')
|
|
.matchHeader('x-cypress-run-id', this.props.runId)
|
|
.matchHeader('x-cypress-request-attempt', '0')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/instances/instance-id-123/tests', this.bodyProps)
|
|
.reply(200)
|
|
|
|
return api.postInstanceTests(this.props)
|
|
})
|
|
|
|
it('PUT /instances/:id failure formatting', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '1')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/instances/instance-id-123/tests')
|
|
.reply(422, {
|
|
errors: {
|
|
tests: ['is required'],
|
|
},
|
|
})
|
|
|
|
return api.postInstanceTests({ instanceId: 'instance-id-123' })
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq(`\
|
|
422
|
|
|
|
{
|
|
"errors": {
|
|
"tests": [
|
|
"is required"
|
|
]
|
|
}
|
|
}\
|
|
`)
|
|
})
|
|
})
|
|
|
|
it('handles timeouts', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '1')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/instances/instance-id-123/tests')
|
|
.delayConnection(5000)
|
|
.reply(200, {})
|
|
|
|
return api.postInstanceTests({
|
|
instanceId: 'instance-id-123',
|
|
timeout: 100,
|
|
})
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq('Error: ESOCKETTIMEDOUT')
|
|
})
|
|
})
|
|
|
|
it('sets timeout to 60 seconds', () => {
|
|
sinon.stub(api.rp, 'post').resolves()
|
|
|
|
return api.postInstanceTests({})
|
|
.then(() => {
|
|
expect(api.rp.post).to.be.calledWithMatch({ timeout: 60000 })
|
|
})
|
|
})
|
|
|
|
it('tags errors', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '1')
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/instances/instance-id-123/tests', this.bodyProps)
|
|
.reply(500, {})
|
|
|
|
return api.postInstanceTests(this.props)
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
expect(err).to.have.property('isApiError', true)
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.postInstanceResults', () => {
|
|
beforeEach(function () {
|
|
this.updateProps = {
|
|
runId: 'run-id-123',
|
|
instanceId: 'instance-id-123',
|
|
stats: {},
|
|
error: 'err msg',
|
|
video: true,
|
|
screenshots: [],
|
|
reporterStats: {},
|
|
}
|
|
|
|
this.postProps = _.pick(this.updateProps, 'stats', 'video', 'screenshots', 'reporterStats')
|
|
})
|
|
|
|
it('POSTs /instances/:id/results', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '1')
|
|
.matchHeader('x-cypress-run-id', this.updateProps.runId)
|
|
.matchHeader('x-cypress-request-attempt', '0')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/instances/instance-id-123/results', this.postProps)
|
|
.reply(200)
|
|
|
|
return api.postInstanceResults(this.updateProps)
|
|
})
|
|
|
|
it('PUT /instances/:id failure formatting', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '1')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/instances/instance-id-123/results')
|
|
.reply(422, {
|
|
errors: {
|
|
tests: ['is required'],
|
|
},
|
|
})
|
|
|
|
return api.postInstanceResults({ instanceId: 'instance-id-123' })
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq(`\
|
|
422
|
|
|
|
{
|
|
"errors": {
|
|
"tests": [
|
|
"is required"
|
|
]
|
|
}
|
|
}\
|
|
`)
|
|
})
|
|
})
|
|
|
|
it('handles timeouts', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '1')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/instances/instance-id-123/results')
|
|
.delayConnection(5000)
|
|
.reply(200, {})
|
|
|
|
return api.postInstanceResults({
|
|
instanceId: 'instance-id-123',
|
|
timeout: 100,
|
|
})
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq('Error: ESOCKETTIMEDOUT')
|
|
})
|
|
})
|
|
|
|
it('sets timeout to 60 seconds', () => {
|
|
sinon.stub(api.rp, 'post').resolves()
|
|
|
|
return api.postInstanceResults({})
|
|
.then(() => {
|
|
expect(api.rp.post).to.be.calledWithMatch({ timeout: 60000 })
|
|
})
|
|
})
|
|
|
|
it('tags errors', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '1')
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/instances/instance-id-123/results', this.postProps)
|
|
.reply(500, {})
|
|
|
|
return api.postInstanceResults(this.updateProps)
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
expect(err).to.have.property('isApiError', true)
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.updateInstanceStdout', () => {
|
|
it('PUTs /instances/:id/stdout', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-run-id', 'run-id-123')
|
|
.matchHeader('x-cypress-request-attempt', '0')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.put('/instances/instance-id-123/stdout', {
|
|
stdout: 'foobarbaz\n',
|
|
})
|
|
.reply(200)
|
|
|
|
return api.updateInstanceStdout({
|
|
runId: 'run-id-123',
|
|
instanceId: 'instance-id-123',
|
|
stdout: 'foobarbaz\n',
|
|
})
|
|
})
|
|
|
|
it('PUT /instances/:id/stdout failure formatting', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.put('/instances/instance-id-123/stdout')
|
|
.reply(422, {
|
|
errors: {
|
|
tests: ['is required'],
|
|
},
|
|
})
|
|
|
|
return api.updateInstanceStdout({ instanceId: 'instance-id-123' })
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq(`\
|
|
422
|
|
|
|
{
|
|
"errors": {
|
|
"tests": [
|
|
"is required"
|
|
]
|
|
}
|
|
}\
|
|
`)
|
|
})
|
|
})
|
|
|
|
it('handles timeouts', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.put('/instances/instance-id-123/stdout')
|
|
.delayConnection(5000)
|
|
.reply(200, {})
|
|
|
|
return api.updateInstanceStdout({
|
|
instanceId: 'instance-id-123',
|
|
timeout: 100,
|
|
})
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq('Error: ESOCKETTIMEDOUT')
|
|
})
|
|
})
|
|
|
|
it('sets timeout to 60 seconds', () => {
|
|
sinon.stub(api.rp, 'put').resolves()
|
|
|
|
return api.updateInstanceStdout({})
|
|
.then(() => {
|
|
expect(api.rp.put).to.be.calledWithMatch({ timeout: 60000 })
|
|
})
|
|
})
|
|
|
|
it('tags errors', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.put('/instances/instance-id-123/stdout', {
|
|
stdout: 'foobarbaz\n',
|
|
})
|
|
.reply(500, {})
|
|
|
|
return api.updateInstanceStdout({
|
|
instanceId: 'instance-id-123',
|
|
stdout: 'foobarbaz\n',
|
|
})
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
expect(err).to.have.property('isApiError', true)
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.getAuthUrls', () => {
|
|
it('GET /auth + returns the urls', () => {
|
|
return api.getAuthUrls().then((urls) => {
|
|
expect(urls).to.deep.eq(AUTH_URLS)
|
|
})
|
|
})
|
|
|
|
it('tags errors', () => {
|
|
nock.cleanAll()
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.matchHeader('x-route-version', '2')
|
|
.get('/auth')
|
|
.reply(500, {})
|
|
|
|
return api.getAuthUrls()
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
expect(err).to.have.property('isApiError', true)
|
|
})
|
|
})
|
|
|
|
it('caches the response from the first request', () => {
|
|
return api.getAuthUrls()
|
|
.then(() => {
|
|
// nock will throw if this makes a second HTTP call
|
|
return api.getAuthUrls()
|
|
}).then((urls) => {
|
|
expect(urls).to.deep.eq(AUTH_URLS)
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.postLogout', () => {
|
|
beforeEach(() => {
|
|
return sinon.stub(machineId, 'machineId').resolves('foo')
|
|
})
|
|
|
|
it('POSTs /logout', () => {
|
|
nock(CLOUD_BASEURL)
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.matchHeader('x-machine-id', 'foo')
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/logout')
|
|
.reply(200)
|
|
|
|
return api.postLogout('auth-token-123')
|
|
})
|
|
|
|
it('tags errors', () => {
|
|
nock(CLOUD_BASEURL)
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.matchHeader('x-machine-id', 'foo')
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/logout')
|
|
.reply(500, {})
|
|
|
|
return api.postLogout('auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
expect(err).to.have.property('isApiError', true)
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.createCrashReport', () => {
|
|
beforeEach(function () {
|
|
this.setup = (body, authToken, delay = 0) => {
|
|
return nock(API_BASEURL)
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.matchHeader('authorization', `Bearer ${authToken}`)
|
|
.post('/exceptions', body)
|
|
.delayConnection(delay)
|
|
.reply(200)
|
|
}
|
|
})
|
|
|
|
it('POSTs /exceptions', function () {
|
|
this.setup({ foo: 'bar' }, 'auth-token-123')
|
|
|
|
return api.createCrashReport({ foo: 'bar' }, 'auth-token-123')
|
|
})
|
|
|
|
it('by default times outs after 3 seconds', function () {
|
|
// return our own specific promise
|
|
// so we can spy on the timeout function
|
|
const p = Promise.resolve({})
|
|
|
|
sinon.spy(p, 'timeout')
|
|
sinon.stub(api.rp, 'post').returns(p)
|
|
|
|
this.setup({ foo: 'bar' }, 'auth-token-123')
|
|
|
|
return api.createCrashReport({ foo: 'bar' }, 'auth-token-123').then(() => {
|
|
expect(p.timeout).to.be.calledWith(3000)
|
|
})
|
|
})
|
|
|
|
it('times out after exceeding timeout', function () {
|
|
// force our connection to be delayed 5 seconds
|
|
this.setup({ foo: 'bar' }, 'auth-token-123', 5000)
|
|
|
|
// and set the timeout to only be 50ms
|
|
return api.createCrashReport({ foo: 'bar' }, 'auth-token-123', 50)
|
|
.then(() => {
|
|
throw new Error('errored: it did not catch the timeout error!')
|
|
}).catch(Promise.TimeoutError, () => {})
|
|
})
|
|
|
|
it('tags errors', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/exceptions', { foo: 'bar' })
|
|
.reply(500, {})
|
|
|
|
return api.createCrashReport({ foo: 'bar' }, 'auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
})
|
|
.catch((err) => {
|
|
expect(err).to.have.property('isApiError', true)
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.retryWithBackoff', () => {
|
|
beforeEach(() => {
|
|
process.env.DISABLE_API_RETRIES = ''
|
|
|
|
return sinon.stub(Promise, 'delay').resolves()
|
|
})
|
|
|
|
it('attempts passed-in function', () => {
|
|
const fn = sinon.stub()
|
|
|
|
return api.retryWithBackoff(fn).then(() => {
|
|
expect(fn).to.be.called
|
|
})
|
|
})
|
|
|
|
it('retries if function times out', () => {
|
|
const fn = sinon.stub()
|
|
.rejects(new Promise.TimeoutError())
|
|
|
|
fn.onCall(1).resolves()
|
|
|
|
return api.retryWithBackoff(fn)
|
|
.then(() => {
|
|
expect(fn).to.be.calledTwice
|
|
expect(fn.firstCall.args[0]).eq(0)
|
|
expect(fn.secondCall.args[0]).eq(1)
|
|
})
|
|
})
|
|
|
|
it('retries on 5xx errors', () => {
|
|
const fn1 = sinon.stub().rejects(makeError({ statusCode: 500 }))
|
|
|
|
fn1.onCall(1).resolves()
|
|
|
|
const fn2 = sinon.stub().rejects(makeError({ statusCode: 599 }))
|
|
|
|
fn2.onCall(1).resolves()
|
|
|
|
return api.retryWithBackoff(fn1)
|
|
.then(() => {
|
|
expect(fn1).to.be.calledTwice
|
|
|
|
return api.retryWithBackoff(fn2)
|
|
}).then(() => {
|
|
expect(fn2).to.be.calledTwice
|
|
})
|
|
})
|
|
|
|
it('retries on error without status code', () => {
|
|
const fn = sinon.stub().rejects(makeError())
|
|
|
|
fn.onCall(1).resolves()
|
|
|
|
return api.retryWithBackoff(fn)
|
|
.then(() => {
|
|
expect(fn).to.be.calledTwice
|
|
})
|
|
})
|
|
|
|
it('does not retry on non-5xx errors', () => {
|
|
const fn1 = sinon.stub().rejects(makeError({ message: '499 error', statusCode: 499 }))
|
|
|
|
const fn2 = sinon.stub().rejects(makeError({ message: '600 error', statusCode: 600 }))
|
|
|
|
return api.retryWithBackoff(fn1)
|
|
.then(() => {
|
|
throw new Error('Should not resolve 499 error')
|
|
})
|
|
.catch((err) => {
|
|
expect(err.message).to.equal('499 error')
|
|
|
|
return api.retryWithBackoff(fn2)
|
|
})
|
|
.then(() => {
|
|
throw new Error('Should not resolve 600 error')
|
|
})
|
|
.catch((err) => {
|
|
expect(err.message).to.equal('600 error')
|
|
})
|
|
})
|
|
|
|
it('backs off with strategy: 30s, 60s, 2m', () => {
|
|
const fn = sinon.stub().rejects(new Promise.TimeoutError())
|
|
|
|
fn.onCall(3).resolves()
|
|
|
|
return api.retryWithBackoff(fn).then(() => {
|
|
expect(Promise.delay).to.be.calledThrice
|
|
expect(Promise.delay.firstCall).to.be.calledWith(30 * 1000)
|
|
expect(Promise.delay.secondCall).to.be.calledWith(60 * 1000)
|
|
|
|
expect(Promise.delay.thirdCall).to.be.calledWith(2 * 60 * 1000)
|
|
})
|
|
})
|
|
|
|
it('fails after third retry fails', () => {
|
|
const fn = sinon.stub().rejects(makeError({ message: '500 error', statusCode: 500 }))
|
|
|
|
return api.retryWithBackoff(fn)
|
|
.then(() => {
|
|
throw new Error('Should not resolve')
|
|
}).catch((err) => {
|
|
expect(err.message).to.equal('500 error')
|
|
})
|
|
})
|
|
|
|
it('calls errors.warning before each retry', () => {
|
|
const err = makeError({ message: '500 error', statusCode: 500 })
|
|
|
|
sinon.spy(errors, 'warning')
|
|
const fn = sinon.stub().rejects(err)
|
|
|
|
fn.onCall(3).resolves()
|
|
|
|
return api.retryWithBackoff(fn).then(() => {
|
|
expect(errors.warning).to.be.calledThrice
|
|
expect(errors.warning.firstCall.args[0]).to.eql('CLOUD_API_RESPONSE_FAILED_RETRYING')
|
|
expect(errors.warning.firstCall.args[1]).to.eql({
|
|
delayMs: 30000,
|
|
tries: 3,
|
|
response: err,
|
|
})
|
|
|
|
expect(errors.warning.secondCall.args[1]).to.eql({
|
|
delayMs: 60000,
|
|
tries: 2,
|
|
response: err,
|
|
})
|
|
|
|
expect(errors.warning.thirdCall.args[1]).to.eql({
|
|
delayMs: 120000,
|
|
tries: 1,
|
|
response: err,
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.updateInstanceArtifacts', () => {
|
|
beforeEach(function () {
|
|
this.artifactOptions = {
|
|
runId: 'run-id-123',
|
|
instanceId: 'instance-id-123',
|
|
}
|
|
|
|
this.artifactProps = {
|
|
screenshots: [{
|
|
url: `http://localhost:1234/screenshots/upload/instance-id-123/a877e957-f90e-4ba4-9fa8-569812f148c4.png`,
|
|
uploadSize: 100,
|
|
uploadDuration: 100,
|
|
}],
|
|
video: {
|
|
url: `http://localhost:1234/video/upload/instance-id-123/f17754c4-581d-4e08-a922-1fa402f9c6de.mp4`,
|
|
uploadSize: 122,
|
|
uploadDuration: 100,
|
|
},
|
|
protocol: {
|
|
url: `http://localhost:1234/protocol/upload/instance-id-123/2ed89c81-e7eb-4b97-8a6e-185c410471df.db`,
|
|
uploadSize: 123,
|
|
uploadDuration: 100,
|
|
},
|
|
}
|
|
// TODO: add schema validation
|
|
})
|
|
|
|
it('PUTs/instances/:id/artifacts', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '1')
|
|
.matchHeader('x-cypress-run-id', this.artifactOptions.runId)
|
|
.matchHeader('x-cypress-request-attempt', '0')
|
|
.matchHeader('x-os-name', OS_PLATFORM)
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.put('/instances/instance-id-123/artifacts', {
|
|
protocol: this.artifactProps.protocol,
|
|
screenshots: this.artifactProps.screenshots,
|
|
video: this.artifactProps.video,
|
|
})
|
|
.reply(200)
|
|
|
|
return api.updateInstanceArtifacts(this.artifactOptions, this.artifactProps)
|
|
})
|
|
})
|
|
})
|