mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-11 01:29:59 -06:00
1356 lines
35 KiB
JavaScript
1356 lines
35 KiB
JavaScript
require('../spec_helper')
|
|
|
|
const _ = require('lodash')
|
|
const os = require('os')
|
|
const {
|
|
agent,
|
|
} = require('@packages/network')
|
|
const pkg = require('@packages/root')
|
|
const api = require(`${root}lib/api`)
|
|
const cache = require(`${root}lib/cache`)
|
|
const errors = require(`${root}lib/errors`)
|
|
const machineId = require(`${root}lib/util/machine_id`)
|
|
const Promise = require('bluebird')
|
|
|
|
const API_BASEURL = 'http://localhost:1234'
|
|
const ON_BASEURL = 'http://localhost:8080'
|
|
const DASHBOARD_BASEURL = 'http://localhost:3000'
|
|
const AUTH_URLS = {
|
|
'dashboardAuthUrl': 'http://localhost:3000/test-runner.html',
|
|
'dashboardLogoutUrl': 'http://localhost:3000/logout',
|
|
}
|
|
|
|
const makeError = (details = {}) => {
|
|
return _.extend(new Error(details.message || 'Some error'), details)
|
|
}
|
|
|
|
describe('lib/api', () => {
|
|
beforeEach(() => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '2')
|
|
.get('/auth')
|
|
.reply(200, AUTH_URLS)
|
|
|
|
api.clearCache()
|
|
sinon.stub(os, 'platform').returns('linux')
|
|
|
|
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'
|
|
})
|
|
})
|
|
|
|
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',
|
|
})
|
|
})
|
|
})
|
|
|
|
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('.getOrgs', () => {
|
|
it('GET /orgs + returns orgs', () => {
|
|
const orgs = []
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/organizations')
|
|
.reply(200, orgs)
|
|
|
|
return api.getOrgs('auth-token-123')
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq(orgs)
|
|
})
|
|
})
|
|
|
|
it('tags errors', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/organizations')
|
|
.reply(500, {})
|
|
|
|
return api.getOrgs('auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.getProjects', () => {
|
|
it('GET /projects + returns projects', () => {
|
|
const projects = []
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/projects')
|
|
.reply(200, projects)
|
|
|
|
return api.getProjects('auth-token-123')
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq(projects)
|
|
})
|
|
})
|
|
|
|
it('tags errors', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/projects')
|
|
.reply(500, {})
|
|
|
|
return api.getProjects('auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.getProject', () => {
|
|
it('GET /projects/:id + returns project', () => {
|
|
const project = { id: 'id-123' }
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.matchHeader('x-route-version', '2')
|
|
.get('/projects/id-123')
|
|
.reply(200, project)
|
|
|
|
return api.getProject('id-123', 'auth-token-123')
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq(project)
|
|
})
|
|
})
|
|
|
|
it('tags errors', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/projects/id-123')
|
|
.reply(500, {})
|
|
|
|
return api.getProject('id-123', 'auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.getProjectRuns', () => {
|
|
it('GET /projects/:id/runs + returns runs', () => {
|
|
const runs = []
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '3')
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/projects/id-123/runs')
|
|
.reply(200, runs)
|
|
|
|
return api.getProjectRuns('id-123', 'auth-token-123')
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq(runs)
|
|
})
|
|
})
|
|
|
|
it('handles timeouts', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/projects/id-123/runs')
|
|
.socketDelay(5000)
|
|
.reply(200, [])
|
|
|
|
return api.getProjectRuns('id-123', 'auth-token-123', { timeout: 100 })
|
|
.then((ret) => {
|
|
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, 'get').returns({
|
|
catch () {
|
|
return {
|
|
catch () {
|
|
return {
|
|
then (fn) {
|
|
return fn()
|
|
},
|
|
}
|
|
},
|
|
then (fn) {
|
|
return fn()
|
|
},
|
|
}
|
|
},
|
|
then (fn) {
|
|
return fn()
|
|
},
|
|
})
|
|
|
|
return api.getProjectRuns('id-123', 'auth-token-123')
|
|
.then((ret) => {
|
|
expect(api.rp.get).to.be.calledWithMatch({ timeout: 10000 })
|
|
})
|
|
})
|
|
|
|
it('GET /projects/:id/runs failure formatting', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/projects/id-123/runs')
|
|
.twice()
|
|
.reply(401, {
|
|
errors: {
|
|
permission: ['denied'],
|
|
},
|
|
})
|
|
|
|
return api.getProjectRuns('id-123', 'auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq(`\
|
|
401
|
|
|
|
{
|
|
"errors": {
|
|
"permission": [
|
|
"denied"
|
|
]
|
|
}
|
|
}\
|
|
`)
|
|
})
|
|
})
|
|
|
|
it('tags errors', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/projects/id-123/runs')
|
|
.reply(500, {})
|
|
|
|
return api.getProjectRuns('id-123', 'auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.ping', () => {
|
|
it('GET /ping', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-os-name', 'linux')
|
|
.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.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.createRun', () => {
|
|
beforeEach(function () {
|
|
this.buildProps = {
|
|
group: null,
|
|
parallel: null,
|
|
ciBuildId: null,
|
|
projectId: 'id-123',
|
|
recordKey: 'token-123',
|
|
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: {
|
|
'dynamicSpecsInSerialMode': true,
|
|
'skipSpecAction': true,
|
|
},
|
|
}
|
|
})
|
|
|
|
it('POST /runs + returns runId', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '4')
|
|
.matchHeader('x-os-name', 'linux')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/runs', this.buildProps)
|
|
.reply(200, {
|
|
runId: 'new-run-id-123',
|
|
})
|
|
|
|
return api.createRun(this.buildProps)
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq({ runId: 'new-run-id-123' })
|
|
})
|
|
})
|
|
|
|
it('POST /runs failure formatting', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '4')
|
|
.matchHeader('x-os-name', 'linux')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/runs', this.buildProps)
|
|
.reply(422, {
|
|
errors: {
|
|
runId: ['is required'],
|
|
},
|
|
})
|
|
|
|
return api.createRun(this.buildProps)
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq(`\
|
|
422
|
|
|
|
{
|
|
"errors": {
|
|
"runId": [
|
|
"is required"
|
|
]
|
|
}
|
|
}\
|
|
`)
|
|
})
|
|
})
|
|
|
|
it('handles timeouts', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '4')
|
|
.matchHeader('x-os-name', 'linux')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/runs')
|
|
.socketDelay(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' })
|
|
|
|
return api.createRun({})
|
|
.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)
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.createInstance', () => {
|
|
beforeEach(function () {
|
|
Object.defineProperty(process.versions, 'chrome', {
|
|
value: '53',
|
|
})
|
|
|
|
this.createProps = {
|
|
runId: 'run-id-123',
|
|
spec: 'cypress/integration/app_spec.js',
|
|
groupId: 'groupId123',
|
|
machineId: 'machineId123',
|
|
platform: {},
|
|
}
|
|
|
|
this.postProps = _.omit(this.createProps, 'runId')
|
|
})
|
|
|
|
it('POSTs /runs/:id/instances', function () {
|
|
os.platform.returns('darwin')
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '5')
|
|
.matchHeader('x-cypress-run-id', this.createProps.runId)
|
|
.matchHeader('x-cypress-request-attempt', '0')
|
|
.matchHeader('x-os-name', 'darwin')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/runs/run-id-123/instances', this.postProps)
|
|
.reply(200, {
|
|
instanceId: 'instance-id-123',
|
|
})
|
|
|
|
return api.createInstance(this.createProps)
|
|
.get('instanceId')
|
|
.then((instanceId) => {
|
|
expect(instanceId).to.eq('instance-id-123')
|
|
})
|
|
})
|
|
|
|
it('POST /runs/:id/instances failure formatting', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-route-version', '5')
|
|
.matchHeader('x-os-name', 'linux')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/runs/run-id-123/instances')
|
|
.reply(422, {
|
|
errors: {
|
|
tests: ['is required'],
|
|
},
|
|
})
|
|
|
|
return api.createInstance({ runId: 'run-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', '5')
|
|
.matchHeader('x-os-name', 'linux')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/runs/run-id-123/instances')
|
|
.socketDelay(5000)
|
|
.reply(200, {})
|
|
|
|
return api.createInstance({
|
|
runId: 'run-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({
|
|
instanceId: 'instanceId123',
|
|
})
|
|
|
|
return api.createInstance({})
|
|
.then(() => {
|
|
expect(api.rp.post).to.be.calledWithMatch({ timeout: 60000 })
|
|
})
|
|
})
|
|
|
|
it('tags errors', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/runs/run-id-123/instances', this.postProps)
|
|
.reply(500, {})
|
|
|
|
return api.createInstance(this.createProps)
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.isApiError).to.be.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', 'linux')
|
|
.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', 'linux')
|
|
.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', 'linux')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/instances/instance-id-123/tests')
|
|
.socketDelay(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.isApiError).to.be.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', 'linux')
|
|
.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', 'linux')
|
|
.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', 'linux')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.post('/instances/instance-id-123/results')
|
|
.socketDelay(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.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.updateInstanceStdout', () => {
|
|
it('PUTs /instances/:id/stdout', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-os-name', 'linux')
|
|
.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', 'linux')
|
|
.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', 'linux')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.put('/instances/instance-id-123/stdout')
|
|
.socketDelay(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.isApiError).to.be.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.isApiError).to.be.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(DASHBOARD_BASEURL)
|
|
.matchHeader('x-os-name', 'linux')
|
|
.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(DASHBOARD_BASEURL)
|
|
.matchHeader('x-os-name', 'linux')
|
|
.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.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.createProject', () => {
|
|
beforeEach(function () {
|
|
this.postProps = {
|
|
name: 'foobar',
|
|
orgId: 'org-id-123',
|
|
public: true,
|
|
remoteOrigin: 'remoteOrigin',
|
|
}
|
|
|
|
this.createProps = {
|
|
projectName: 'foobar',
|
|
orgId: 'org-id-123',
|
|
public: true,
|
|
}
|
|
})
|
|
|
|
it('POST /projects', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-os-name', 'linux')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.matchHeader('x-route-version', '2')
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/projects', this.postProps)
|
|
.reply(200, {
|
|
id: 'id-123',
|
|
name: 'foobar',
|
|
orgId: 'org-id-123',
|
|
public: true,
|
|
})
|
|
|
|
return api.createProject(this.createProps, 'remoteOrigin', 'auth-token-123')
|
|
.then((projectDetails) => {
|
|
expect(projectDetails).to.deep.eq({
|
|
id: 'id-123',
|
|
name: 'foobar',
|
|
orgId: 'org-id-123',
|
|
public: true,
|
|
})
|
|
})
|
|
})
|
|
|
|
it('POST /projects failure formatting', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('x-os-name', 'linux')
|
|
.matchHeader('x-cypress-version', pkg.version)
|
|
.matchHeader('x-route-version', '2')
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/projects', {
|
|
name: 'foobar',
|
|
orgId: 'org-id-123',
|
|
public: true,
|
|
remoteOrigin: 'remoteOrigin',
|
|
})
|
|
.reply(422, {
|
|
errors: {
|
|
orgId: ['is required'],
|
|
},
|
|
})
|
|
|
|
const projectDetails = {
|
|
projectName: 'foobar',
|
|
orgId: 'org-id-123',
|
|
public: true,
|
|
}
|
|
|
|
return api.createProject(projectDetails, 'remoteOrigin', 'auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq(`\
|
|
422
|
|
|
|
{
|
|
"errors": {
|
|
"orgId": [
|
|
"is required"
|
|
]
|
|
}
|
|
}\
|
|
`)
|
|
})
|
|
})
|
|
|
|
it('tags errors', function () {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/projects', this.postProps)
|
|
.reply(500, {})
|
|
|
|
return api.createProject(this.createProps, 'remoteOrigin', 'auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.getProjectRecordKeys', () => {
|
|
it('GET /projects/:id/keys + returns keys', () => {
|
|
const recordKeys = []
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/projects/id-123/keys')
|
|
.reply(200, recordKeys)
|
|
|
|
return api.getProjectRecordKeys('id-123', 'auth-token-123')
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq(recordKeys)
|
|
})
|
|
})
|
|
|
|
it('tags errors', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.get('/projects/id-123/keys')
|
|
.reply(500, {})
|
|
|
|
return api.getProjectRecordKeys('id-123', 'auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.requestAccess', () => {
|
|
it('POST /projects/:id/membership_requests + returns response', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/projects/project-id-123/membership_requests')
|
|
.reply(200)
|
|
|
|
return api.requestAccess('project-id-123', 'auth-token-123')
|
|
.then((ret) => {
|
|
expect(ret).to.be.undefined
|
|
})
|
|
})
|
|
|
|
it('POST /projects/:id/membership_requests failure formatting', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/projects/project-id-123/membership_requests')
|
|
.reply(422, {
|
|
errors: {
|
|
access: ['already requested'],
|
|
},
|
|
})
|
|
|
|
return api.requestAccess('project-id-123', 'auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.message).to.eq(`\
|
|
422
|
|
|
|
{
|
|
"errors": {
|
|
"access": [
|
|
"already requested"
|
|
]
|
|
}
|
|
}\
|
|
`)
|
|
})
|
|
})
|
|
|
|
it('tags errors', () => {
|
|
nock(API_BASEURL)
|
|
.matchHeader('authorization', 'Bearer auth-token-123')
|
|
.matchHeader('accept-encoding', /gzip/)
|
|
.post('/projects/project-id-123/membership_requests')
|
|
.reply(500, {})
|
|
|
|
return api.requestAccess('project-id-123', 'auth-token-123')
|
|
.then(() => {
|
|
throw new Error('should have thrown here')
|
|
}).catch((err) => {
|
|
expect(err.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.createCrashReport', () => {
|
|
beforeEach(function () {
|
|
this.setup = (body, authToken, delay = 0) => {
|
|
return nock(API_BASEURL)
|
|
.matchHeader('x-os-name', 'linux')
|
|
.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', 'linux')
|
|
.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.isApiError).to.be.true
|
|
})
|
|
})
|
|
})
|
|
|
|
context('.getReleaseNotes', () => {
|
|
it('GET /release-notes/:version + returns result', () => {
|
|
const releaseNotes = { title: 'New in 1.2.3!' }
|
|
|
|
nock(ON_BASEURL)
|
|
.get('/release-notes/1.2.3')
|
|
.reply(200, releaseNotes)
|
|
|
|
return api.getReleaseNotes('1.2.3')
|
|
.then((ret) => {
|
|
expect(ret).to.deep.eq(releaseNotes)
|
|
})
|
|
})
|
|
|
|
it('ignores errors + returns empty object', () => {
|
|
nock(ON_BASEURL)
|
|
.get('/release-notes/1.2.3')
|
|
.reply(500, {})
|
|
|
|
return api.getReleaseNotes('1.2.3')
|
|
.then((result) => {
|
|
expect(result).to.deep.eq({})
|
|
})
|
|
})
|
|
})
|
|
|
|
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(() => {
|
|
console.log('gsd')
|
|
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('DASHBOARD_API_RESPONSE_FAILED_RETRYING')
|
|
expect(errors.warning.firstCall.args[1]).to.eql({
|
|
delay: '30 seconds',
|
|
tries: 3,
|
|
response: err,
|
|
})
|
|
|
|
expect(errors.warning.secondCall.args[1]).to.eql({
|
|
delay: '1 minute',
|
|
tries: 2,
|
|
response: err,
|
|
})
|
|
|
|
expect(errors.warning.thirdCall.args[1]).to.eql({
|
|
delay: '2 minutes',
|
|
tries: 1,
|
|
response: err,
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|