mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-26 00:50:41 -05:00
f89a8236a8
* desktop-gui: use authBegin api * server: add auth lib for in-browser auth * server: unneeded * server: send proper cors header, actually receive authinfo * desktop-gui: DASHBOARD_LOGIN * server: send both auth flows through dashboard login * server, desktop-gui: exchange code for token, move logic out of desktop gui, cache auth urls * server: cleanup * server: refresh token [wip] * server: cleanup * server: focus main window on login * server: focus cypress after login * server: fix * server: use id_token so Google Login works, fixes to fallback electron auth flow * desktop-gui: "Log In with GitHub" -> "Log In to Dashboard" * server: work without electron * server: what a bizarre function signature, but ok * server: unit test for new auth.js * driver, server: cleanup * server: use redirects instead of XHR * server: update api spec * add some tests for token refreshin * desktop-gui: update tests, cleanup * unit tests * add user unit tests * server: rely on dashboard to set us up with access_token, user profile, etc * server: cleanup * redirect errors too * server: don't worry about refresh tokens in TR * guard against null server * don't regenerate state * fix auth unit test * fall back to electron auth if native auth never opens * break out MarkdownRenderer component * warn if browser not opened, offer copyable url * remove electron fallback and all login window handling * update tests * send more feedback when authing * add success message when logging in * update tests to expect continue button, warnings * use :contains * send machineId, version, platform, arch with login * createSignout -> createLogout, get logoutUrl from /auth v2 endpoint * Change version queryParam to cypressVersion, keep platform as platform, don't bother sending arch * Change " Opening browser..." button to display " Browser failed to open" * "You are now logged in to the Cypress Dashboard as Zach Bloomquist." -> "You are now logged in as Zach Bloomquist." * POST /signout -> GET /logout * make fallback URL click-to-select * add tests for edge cases in browser launching * cleanup * logoutUrl -> dashboardLogoutUrl * getLogout -> postLogout * getLogout -> postLogout * send machineId with postLogout
1084 lines
30 KiB
CoffeeScript
1084 lines
30 KiB
CoffeeScript
require("../spec_helper")
|
|
|
|
_ = require("lodash")
|
|
os = require("os")
|
|
agent = require("@packages/network").agent
|
|
pkg = require("@packages/root")
|
|
api = require("#{root}lib/api")
|
|
browsers = require("#{root}lib/browsers")
|
|
cache = require("#{root}lib/cache")
|
|
machineId = require("#{root}lib/util/machine_id")
|
|
Promise = require("bluebird")
|
|
|
|
API_BASEURL = "http://localhost:1234"
|
|
DASHBOARD_BASEURL = "http://localhost:3000"
|
|
AUTH_URLS = {
|
|
"dashboardAuthUrl": "http://localhost:3000/test-runner.html"
|
|
"dashboardLogoutUrl": "http://localhost:3000/logout"
|
|
}
|
|
|
|
makeError = (details = {}) ->
|
|
_.extend(new Error(details.message or "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")
|
|
|
|
sinon.stub(cache, 'getUser').resolves({
|
|
name: 'foo bar'
|
|
email: 'foo@bar'
|
|
#authToken: 'auth-token-123'
|
|
})
|
|
|
|
context ".rp", ->
|
|
beforeEach ->
|
|
sinon.spy(agent, 'addRequest')
|
|
nock.enableNetConnect() ## nock will prevent requests from reaching the agent
|
|
|
|
it "makes calls using the correct agent", ->
|
|
nock.cleanAll()
|
|
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 ->
|
|
nock.cleanAll()
|
|
@oldEnv = Object.assign({}, process.env)
|
|
|
|
it "makes calls using the correct agent", ->
|
|
process.env.HTTP_PROXY = process.env.HTTPS_PROXY = 'http://foo.invalid:1234'
|
|
process.env.NO_PROXY = ''
|
|
|
|
api.ping()
|
|
.thenThrow()
|
|
.catch =>
|
|
expect(agent.addRequest).to.be.calledOnce
|
|
expect(agent.addRequest).to.be.calledWithMatch(sinon.match.any, {
|
|
href: 'http://localhost:1234/ping'
|
|
})
|
|
|
|
afterEach ->
|
|
process.env = @oldEnv
|
|
|
|
context ".getOrgs", ->
|
|
it "GET /orgs + returns orgs", ->
|
|
orgs = []
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader("authorization", "Bearer auth-token-123")
|
|
.matchHeader("accept-encoding", /gzip/)
|
|
.get("/organizations")
|
|
.reply(200, orgs)
|
|
|
|
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, {})
|
|
|
|
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", ->
|
|
projects = []
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader("authorization", "Bearer auth-token-123")
|
|
.matchHeader("accept-encoding", /gzip/)
|
|
.get("/projects")
|
|
.reply(200, projects)
|
|
|
|
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, {})
|
|
|
|
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", ->
|
|
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)
|
|
|
|
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, {})
|
|
|
|
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", ->
|
|
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)
|
|
|
|
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, [])
|
|
|
|
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: -> {
|
|
catch: -> {
|
|
then: (fn) -> fn()
|
|
}
|
|
then: (fn) -> fn()
|
|
}
|
|
then: (fn) -> fn()
|
|
})
|
|
|
|
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"]
|
|
}
|
|
})
|
|
|
|
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, {})
|
|
|
|
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")
|
|
|
|
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, {})
|
|
|
|
api.ping()
|
|
.then ->
|
|
throw new Error("should have thrown here")
|
|
.catch (err) ->
|
|
expect(err.isApiError).to.be.true
|
|
|
|
context ".createRun", ->
|
|
beforeEach ->
|
|
@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"]
|
|
}
|
|
|
|
it "POST /runs + returns runId", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("x-route-version", "4")
|
|
.matchHeader("x-os-name", "linux")
|
|
.matchHeader("x-cypress-version", pkg.version)
|
|
.post("/runs", @buildProps)
|
|
.reply(200, {
|
|
runId: "new-run-id-123"
|
|
})
|
|
|
|
api.createRun(@buildProps)
|
|
.then (ret) ->
|
|
expect(ret).to.deep.eq({ runId: "new-run-id-123" })
|
|
|
|
it "POST /runs failure formatting", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("x-route-version", "4")
|
|
.matchHeader("x-os-name", "linux")
|
|
.matchHeader("x-cypress-version", pkg.version)
|
|
.post("/runs", @buildProps)
|
|
.reply(422, {
|
|
errors: {
|
|
runId: ["is required"]
|
|
}
|
|
})
|
|
|
|
api.createRun(@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, {})
|
|
|
|
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'})
|
|
|
|
api.createRun({})
|
|
.then ->
|
|
expect(api.rp.post).to.be.calledWithMatch({timeout: 60000})
|
|
|
|
it "tags errors", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("x-route-version", "4")
|
|
.matchHeader("authorization", "Bearer auth-token-123")
|
|
.matchHeader("accept-encoding", /gzip/)
|
|
.post("/runs", @buildProps)
|
|
.reply(500, {})
|
|
|
|
api.createRun(@buildProps)
|
|
.then ->
|
|
throw new Error("should have thrown here")
|
|
.catch (err) ->
|
|
expect(err.isApiError).to.be.true
|
|
|
|
context ".createInstance", ->
|
|
beforeEach ->
|
|
Object.defineProperty(process.versions, "chrome", {
|
|
value: "53"
|
|
})
|
|
|
|
@createProps = {
|
|
runId: "run-id-123"
|
|
spec: "cypress/integration/app_spec.js"
|
|
groupId: "groupId123"
|
|
machineId: "machineId123"
|
|
platform: {}
|
|
}
|
|
|
|
@postProps = _.omit(@createProps, "runId")
|
|
|
|
it "POSTs /runs/:id/instances", ->
|
|
os.platform.returns("darwin")
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader("x-route-version", "5")
|
|
.matchHeader("x-os-name", "darwin")
|
|
.matchHeader("x-cypress-version", pkg.version)
|
|
.post("/runs/run-id-123/instances", @postProps)
|
|
.reply(200, {
|
|
instanceId: "instance-id-123"
|
|
})
|
|
|
|
api.createInstance(@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"]
|
|
}
|
|
})
|
|
|
|
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, {})
|
|
|
|
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"
|
|
})
|
|
|
|
api.createInstance({})
|
|
.then ->
|
|
expect(api.rp.post).to.be.calledWithMatch({timeout: 60000})
|
|
|
|
it "tags errors", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("authorization", "Bearer auth-token-123")
|
|
.matchHeader("accept-encoding", /gzip/)
|
|
.post("/runs/run-id-123/instances", @postProps)
|
|
.reply(500, {})
|
|
|
|
api.createInstance(@createProps)
|
|
.then ->
|
|
throw new Error("should have thrown here")
|
|
.catch (err) ->
|
|
expect(err.isApiError).to.be.true
|
|
|
|
context ".updateInstance", ->
|
|
beforeEach ->
|
|
@updateProps = {
|
|
instanceId: "instance-id-123"
|
|
stats: {}
|
|
error: "err msg"
|
|
video: true
|
|
screenshots: []
|
|
cypressConfig: {}
|
|
reporterStats: {}
|
|
stdout: "foo\nbar\nbaz"
|
|
}
|
|
|
|
@putProps = _.omit(@updateProps, "instanceId")
|
|
|
|
it "PUTs /instances/:id", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("x-route-version", "2")
|
|
.matchHeader("x-os-name", "linux")
|
|
.matchHeader("x-cypress-version", pkg.version)
|
|
.put("/instances/instance-id-123", @putProps)
|
|
.reply(200)
|
|
|
|
api.updateInstance(@updateProps)
|
|
|
|
it "PUT /instances/:id failure formatting", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("x-route-version", "2")
|
|
.matchHeader("x-os-name", "linux")
|
|
.matchHeader("x-cypress-version", pkg.version)
|
|
.put("/instances/instance-id-123")
|
|
.reply(422, {
|
|
errors: {
|
|
tests: ["is required"]
|
|
}
|
|
})
|
|
|
|
api.updateInstance({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", "2")
|
|
.matchHeader("x-os-name", "linux")
|
|
.matchHeader("x-cypress-version", pkg.version)
|
|
.put("/instances/instance-id-123")
|
|
.socketDelay(5000)
|
|
.reply(200, {})
|
|
|
|
api.updateInstance({
|
|
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()
|
|
|
|
api.updateInstance({})
|
|
.then ->
|
|
expect(api.rp.put).to.be.calledWithMatch({timeout: 60000})
|
|
|
|
it "tags errors", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("x-route-version", "2")
|
|
.matchHeader("authorization", "Bearer auth-token-123")
|
|
.matchHeader("accept-encoding", /gzip/)
|
|
.put("/instances/instance-id-123", @putProps)
|
|
.reply(500, {})
|
|
|
|
api.updateInstance(@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-version", pkg.version)
|
|
.put("/instances/instance-id-123/stdout", {
|
|
stdout: "foobarbaz\n"
|
|
})
|
|
.reply(200)
|
|
|
|
api.updateInstanceStdout({
|
|
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"]
|
|
}
|
|
})
|
|
|
|
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, {})
|
|
|
|
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()
|
|
|
|
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, {})
|
|
|
|
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", ->
|
|
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, {})
|
|
|
|
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", ->
|
|
api.getAuthUrls()
|
|
.then ->
|
|
# nock will throw if this makes a second HTTP call
|
|
api.getAuthUrls()
|
|
.then (urls) ->
|
|
expect(urls).to.deep.eq(AUTH_URLS)
|
|
|
|
context ".postLogout", ->
|
|
beforeEach ->
|
|
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)
|
|
|
|
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, {})
|
|
|
|
api.postLogout("auth-token-123")
|
|
.then ->
|
|
throw new Error("should have thrown here")
|
|
.catch (err) ->
|
|
expect(err.isApiError).to.be.true
|
|
|
|
context ".createProject", ->
|
|
beforeEach ->
|
|
@postProps = {
|
|
name: "foobar"
|
|
orgId: "org-id-123"
|
|
public: true
|
|
remoteOrigin: "remoteOrigin"
|
|
}
|
|
|
|
@createProps = {
|
|
projectName: "foobar"
|
|
orgId: "org-id-123"
|
|
public: true
|
|
}
|
|
|
|
it "POST /projects", ->
|
|
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", @postProps)
|
|
.reply(200, {
|
|
id: "id-123"
|
|
name: "foobar"
|
|
orgId: "org-id-123"
|
|
public: true
|
|
})
|
|
|
|
api.createProject(@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"]
|
|
}
|
|
})
|
|
|
|
projectDetails = {
|
|
projectName: "foobar"
|
|
orgId: "org-id-123"
|
|
public: true
|
|
}
|
|
|
|
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", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("authorization", "Bearer auth-token-123")
|
|
.matchHeader("accept-encoding", /gzip/)
|
|
.post("/projects", @postProps)
|
|
.reply(500, {})
|
|
|
|
api.createProject(@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", ->
|
|
recordKeys = []
|
|
|
|
nock(API_BASEURL)
|
|
.matchHeader("authorization", "Bearer auth-token-123")
|
|
.matchHeader("accept-encoding", /gzip/)
|
|
.get("/projects/id-123/keys")
|
|
.reply(200, recordKeys)
|
|
|
|
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, {})
|
|
|
|
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)
|
|
|
|
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"]
|
|
}
|
|
})
|
|
|
|
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, {})
|
|
|
|
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 ".getProjectToken", ->
|
|
it "GETs /projects/:id/token", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("x-os-name", "linux")
|
|
.matchHeader("x-cypress-version", pkg.version)
|
|
.matchHeader("authorization", "Bearer auth-token-123")
|
|
.matchHeader("accept-encoding", /gzip/)
|
|
.get("/projects/project-123/token")
|
|
.reply(200, {
|
|
apiToken: "token-123"
|
|
})
|
|
|
|
api.getProjectToken("project-123", "auth-token-123")
|
|
.then (resp) ->
|
|
expect(resp).to.eq("token-123")
|
|
|
|
it "tags errors", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("authorization", "Bearer auth-token-123")
|
|
.matchHeader("accept-encoding", /gzip/)
|
|
.get("/projects/project-123/token")
|
|
.reply(500, {})
|
|
|
|
api.getProjectToken("project-123", "auth-token-123")
|
|
.then ->
|
|
throw new Error("should have thrown here")
|
|
.catch (err) ->
|
|
expect(err.isApiError).to.be.true
|
|
|
|
context ".updateProjectToken", ->
|
|
it "PUTs /projects/:id/token", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("x-os-name", "linux")
|
|
.matchHeader("x-cypress-version", pkg.version)
|
|
.matchHeader("authorization", "Bearer auth-token-123")
|
|
.matchHeader("accept-encoding", /gzip/)
|
|
.put("/projects/project-123/token")
|
|
.reply(200, {
|
|
apiToken: "token-123"
|
|
})
|
|
|
|
api.updateProjectToken("project-123", "auth-token-123")
|
|
.then (resp) ->
|
|
expect(resp).to.eq("token-123")
|
|
|
|
it "tags errors", ->
|
|
nock(API_BASEURL)
|
|
.matchHeader("authorization", "Bearer auth-token-123")
|
|
.matchHeader("accept-encoding", /gzip/)
|
|
.put("/projects/project-id-123/token")
|
|
.reply(500, {})
|
|
|
|
api.updateProjectToken("project-123", "auth-token-123")
|
|
.then ->
|
|
throw new Error("should have thrown here")
|
|
.catch (err) ->
|
|
expect(err.isApiError).to.be.true
|
|
|
|
context ".createCrashReport", ->
|
|
beforeEach ->
|
|
@setup = (body, authToken, delay = 0) ->
|
|
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", ->
|
|
@setup({foo: "bar"}, "auth-token-123")
|
|
api.createCrashReport({foo: "bar"}, "auth-token-123")
|
|
|
|
it "by default times outs after 3 seconds", ->
|
|
## return our own specific promise
|
|
## so we can spy on the timeout function
|
|
p = Promise.resolve({})
|
|
sinon.spy(p, "timeout")
|
|
sinon.stub(api.rp, "post").returns(p)
|
|
|
|
@setup({foo: "bar"}, "auth-token-123")
|
|
api.createCrashReport({foo: "bar"}, "auth-token-123").then ->
|
|
expect(p.timeout).to.be.calledWith(3000)
|
|
|
|
it "times out after exceeding timeout", ->
|
|
## force our connection to be delayed 5 seconds
|
|
@setup({foo: "bar"}, "auth-token-123", 5000)
|
|
|
|
## and set the timeout to only be 50ms
|
|
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, {})
|
|
|
|
api.createCrashReport({foo: "bar"}, "auth-token-123")
|
|
.then ->
|
|
throw new Error("should have thrown here")
|
|
.catch (err) ->
|
|
expect(err.isApiError).to.be.true
|
|
|
|
context ".retryWithBackoff", ->
|
|
beforeEach ->
|
|
sinon.stub(Promise, "delay").resolves()
|
|
|
|
it "attempts passed-in function", ->
|
|
fn = sinon.stub()
|
|
api.retryWithBackoff(fn).then =>
|
|
expect(fn).to.be.called
|
|
|
|
it "retries if function times out", ->
|
|
fn = sinon.stub().rejects(new Promise.TimeoutError())
|
|
fn.onCall(1).resolves()
|
|
api.retryWithBackoff(fn).then =>
|
|
expect(fn).to.be.calledTwice
|
|
|
|
it "retries on 5xx errors", ->
|
|
fn1 = sinon.stub().rejects(makeError({ statusCode: 500 }))
|
|
fn1.onCall(1).resolves()
|
|
|
|
fn2 = sinon.stub().rejects(makeError({ statusCode: 599 }))
|
|
fn2.onCall(1).resolves()
|
|
|
|
api.retryWithBackoff(fn1)
|
|
.then ->
|
|
expect(fn1).to.be.calledTwice
|
|
api.retryWithBackoff(fn2)
|
|
.then ->
|
|
expect(fn2).to.be.calledTwice
|
|
|
|
it "retries on error without status code", ->
|
|
fn = sinon.stub().rejects(makeError())
|
|
fn.onCall(1).resolves()
|
|
|
|
api.retryWithBackoff(fn)
|
|
.then ->
|
|
expect(fn).to.be.calledTwice
|
|
|
|
it "does not retry on non-5xx errors", ->
|
|
fn1 = sinon.stub().rejects(makeError({ message: "499 error", statusCode: 499 }))
|
|
|
|
fn2 = sinon.stub().rejects(makeError({ message: "600 error", statusCode: 600 }))
|
|
|
|
api.retryWithBackoff(fn1)
|
|
.then ->
|
|
throw new Error("Should not resolve 499 error")
|
|
.catch (err) ->
|
|
expect(err.message).to.equal("499 error")
|
|
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", ->
|
|
fn = sinon.stub().rejects(new Promise.TimeoutError())
|
|
fn.onCall(3).resolves()
|
|
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", ->
|
|
fn = sinon.stub().rejects(makeError({ message: "500 error", statusCode: 500 }))
|
|
api.retryWithBackoff(fn)
|
|
.then ->
|
|
throw new Error("Should not resolve")
|
|
.catch (err) =>
|
|
expect(err.message).to.equal("500 error")
|
|
|
|
it "calls onBeforeRetry before each retry", ->
|
|
err = makeError({ message: "500 error", statusCode: 500 })
|
|
onBeforeRetry = sinon.stub()
|
|
fn = sinon.stub().rejects(err)
|
|
fn.onCall(3).resolves()
|
|
api.retryWithBackoff(fn, { onBeforeRetry }).then =>
|
|
expect(onBeforeRetry).to.be.calledThrice
|
|
expect(onBeforeRetry.firstCall.args[0]).to.eql({
|
|
retryIndex: 0
|
|
delay: 30 * 1000
|
|
total: 3
|
|
err
|
|
})
|
|
expect(onBeforeRetry.secondCall.args[0]).to.eql({
|
|
retryIndex: 1
|
|
delay: 60 * 1000
|
|
total: 3
|
|
err
|
|
})
|
|
expect(onBeforeRetry.thirdCall.args[0]).to.eql({
|
|
retryIndex: 2
|
|
delay: 2 * 60 * 1000
|
|
total: 3
|
|
err
|
|
})
|