Files
cypress/packages/server/test/unit/server_spec.coffee
Chris Breiding 474b80a50f Fix race condition when there's an early asynchronous error in… (#6610)
* fix race condition when there's an async error in root of plugins file

* return the promise

* fix routes creation

* fix tests

* fix error throwing and add tests

* update snapshots

* revert changes to server.open signatures in tests

* fix test

* properly wrap error so it doesn't log twice

* slow down test to ensure plugins error occurs before run is over

* wait to log early exit error until after run start
2020-03-04 11:59:01 -05:00

386 lines
10 KiB
CoffeeScript

require("../spec_helper")
_ = require("lodash")
os = require("os")
http = require("http")
express = require("express")
Promise = require("bluebird")
connect = require("@packages/network").connect
routes = require("#{root}lib/routes")
config = require("#{root}lib/config")
logger = require("#{root}lib/logger")
Server = require("#{root}lib/server")
Socket = require("#{root}lib/socket")
fileServer = require("#{root}lib/file_server")
ensureUrl = require("#{root}lib/util/ensure-url")
morganFn = ->
mockery.registerMock("morgan", -> morganFn)
describe "lib/server", ->
beforeEach ->
@fileServer = {
close: ->
port: -> 1111
}
sinon.stub(fileServer, "create").returns(@fileServer)
config.set({projectRoot: "/foo/bar/"})
.then (cfg) =>
@config = cfg
@server = Server()
@oldFileServer = @server._fileServer
@server._fileServer = @fileServer
afterEach ->
@server and @server.close()
context "#createExpressApp", ->
beforeEach ->
@use = sinon.spy(express.application, "use")
it "instantiates express instance without morgan", ->
app = @server.createExpressApp({ morgan: false })
expect(app.get("view engine")).to.eq("html")
expect(@use).not.to.be.calledWith(morganFn)
it "requires morgan if true", ->
@server.createExpressApp({ morgan: true })
expect(@use).to.be.calledWith(morganFn)
context "#open", ->
beforeEach ->
sinon.stub(@server, "createServer").resolves()
it "calls #createExpressApp with morgan", ->
sinon.spy(@server, "createExpressApp")
_.extend @config, {port: 54321, morgan: false}
@server.open(@config)
.then =>
expect(@server.createExpressApp).to.be.calledWithMatch({ morgan: false })
it "calls #createServer with port", ->
_.extend @config, {port: 54321}
obj = {}
sinon.stub(@server, "createRoutes")
sinon.stub(@server, "createExpressApp").returns(obj)
@server.open(@config)
.then =>
expect(@server.createServer).to.be.calledWith(obj, @config)
it "calls #createRoutes with app + config", ->
app = {}
project = {}
onError = sinon.spy()
sinon.stub(@server, "createRoutes")
sinon.stub(@server, "createExpressApp").returns(app)
@server.open(@config, project, onError)
.then =>
expect(@server.createRoutes).to.be.called
expect(@server.createRoutes.lastCall.args[0].app).to.equal(app)
expect(@server.createRoutes.lastCall.args[0].config).to.equal(@config)
expect(@server.createRoutes.lastCall.args[0].project).to.equal(project)
expect(@server.createRoutes.lastCall.args[0].onError).to.equal(onError)
it "calls #createServer with port + fileServerFolder + socketIoRoute + app", ->
obj = {}
sinon.stub(@server, "createRoutes")
sinon.stub(@server, "createExpressApp").returns(obj)
@server.open(@config)
.then =>
expect(@server.createServer).to.be.calledWith(obj, @config)
it "calls logger.setSettings with config", ->
sinon.spy(logger, "setSettings")
@server.open(@config)
.then (ret) =>
expect(logger.setSettings).to.be.calledWith(@config)
context "#createServer", ->
beforeEach ->
@port = 54321
@app = @server.createExpressApp({ morgan: true })
it "isListening=true", ->
@server.createServer(@app, {port: @port})
.then =>
expect(@server.isListening).to.be.true
it "resolves with http server port", ->
@server.createServer(@app, {port: @port})
.spread (port) =>
expect(port).to.eq(@port)
it "all servers listen only on localhost and no other interface", ->
fileServer.create.restore()
@server._fileServer = @oldFileServer
interfaces = _.flatten(_.values(os.networkInterfaces()))
nonLoopback = interfaces.find (iface) =>
iface.family == "IPv4" && iface.address != "127.0.0.1"
## verify that we can connect to `port` over loopback
## and not over another configured IPv4 address
tryOnlyLoopbackConnect = (port) =>
Promise.all([
connect.byPortAndAddress(port, "127.0.0.1")
connect.byPortAndAddress(port, nonLoopback)
.then ->
throw new Error("Shouldn't be able to connect on #{nonLoopback.address}:#{port}")
.catch { errno: "ECONNREFUSED" }, ->
])
@server.createServer(@app, {})
.spread (port) =>
Promise.map(
[
port
@server._fileServer.port()
@server._httpsProxy._sniPort
],
tryOnlyLoopbackConnect
)
it "resolves with warning if cannot connect to baseUrl", ->
sinon.stub(ensureUrl, "isListening").rejects()
@server.createServer(@app, {port: @port, baseUrl: "http://localhost:#{@port}"})
.spread (port, warning) =>
expect(warning.type).to.eq("CANNOT_CONNECT_BASE_URL_WARNING")
expect(warning.message).to.include(@port)
context "errors", ->
it "rejects with portInUse", ->
@server.createServer(@app, {port: @port})
.then =>
@server.createServer(@app, {port: @port})
.then ->
throw new Error("should have failed but didn't")
.catch (err) =>
expect(err.type).to.eq("PORT_IN_USE_SHORT")
expect(err.message).to.include(@port)
context "#end", ->
it "calls this._socket.end", ->
socket = sinon.stub({
end: ->
close: ->
})
@server._socket = socket
@server.end()
expect(socket.end).to.be.called
it "is noop without this._socket", ->
@server.end()
context "#startWebsockets", ->
beforeEach ->
@startListening = sinon.stub(Socket.prototype, "startListening")
it "sets _socket and calls _socket#startListening", ->
@server.open(@config)
.then =>
@server.startWebsockets(1, 2, 3)
expect(@startListening).to.be.calledWith(@server.getHttpServer(), 1, 2, 3)
context "#reset", ->
beforeEach ->
@server.open(@config)
.then =>
@buffers = @server._networkProxy.http
sinon.stub(@buffers, "reset")
it "resets the buffers", ->
@server.reset()
expect(@buffers.reset).to.be.called
it "sets the domain to the previous base url if set", ->
@server._baseUrl = "http://localhost:3000"
@server.reset()
expect(@server._remoteStrategy).to.equal("http")
it "sets the domain to <root> if not set", ->
@server.reset()
expect(@server._remoteStrategy).to.equal("file")
context "#close", ->
it "returns a promise", ->
expect(@server.close()).to.be.instanceof Promise
it "calls close on this.server", ->
@server.open(@config)
.then =>
@server.close()
it "isListening=false", ->
@server.open(@config)
.then =>
@server.close()
.then =>
expect(@server.isListening).to.be.false
it "clears settings from Log", ->
logger.setSettings({})
@server.close()
.then ->
expect(logger.getSettings()).to.be.undefined
it "calls close on this._socket", ->
@server._socket = {close: sinon.spy()}
@server.close()
.then =>
expect(@server._socket.close).to.be.calledOnce
context "#proxyWebsockets", ->
beforeEach ->
@proxy = sinon.stub({
ws: ->
on: ->
})
@socket = sinon.stub({end: ->})
@head = {}
it "is noop if req.url startsWith socketIoRoute", ->
socket = {
remotePort: 12345
remoteAddress: '127.0.0.1'
}
@server._socketWhitelist.add({
localPort: socket.remotePort,
once: _.noop
})
noop = @server.proxyWebsockets(@proxy, "/foo", {
url: "/foobarbaz",
socket
})
expect(noop).to.be.undefined
it "calls proxy.ws with hostname + port", ->
@server._onDomainSet("https://www.google.com")
req = {
url: "/"
headers: {
host: "www.google.com"
}
}
@server.proxyWebsockets(@proxy, "/foo", req, @socket, @head)
expect(@proxy.ws).to.be.calledWithMatch(req, @socket, @head, {
secure: false
target: {
host: "www.google.com"
port: "443"
protocol: "https:"
}
})
it "ends the socket if its writable and there is no __cypress.remoteHost", ->
req = {
url: "/"
headers: {
cookie: "foo=bar"
}
}
@server.proxyWebsockets(@proxy, "/foo", req, @socket, @head)
expect(@socket.end).not.to.be.called
@socket.writable = true
@server.proxyWebsockets(@proxy, "/foo", req, @socket, @head)
expect(@socket.end).to.be.called
context "#_onDomainSet", ->
beforeEach ->
@server = Server()
it "sets port to 443 when omitted and https:", ->
ret = @server._onDomainSet("https://staging.google.com/foo/bar")
expect(ret).to.deep.eq({
auth: undefined
origin: "https://staging.google.com"
strategy: "http"
domainName: "google.com"
visiting: undefined
fileServer: null
props: {
port: "443"
domain: "google"
tld: "com"
}
})
it "sets port to 80 when omitted and http:", ->
ret = @server._onDomainSet("http://staging.google.com/foo/bar")
expect(ret).to.deep.eq({
auth: undefined
origin: "http://staging.google.com"
strategy: "http"
domainName: "google.com"
visiting: undefined
fileServer: null
props: {
port: "80"
domain: "google"
tld: "com"
}
})
it "sets host + port to localhost", ->
ret = @server._onDomainSet("http://localhost:4200/a/b?q=1#asdf")
expect(ret).to.deep.eq({
auth: undefined
origin: "http://localhost:4200"
strategy: "http"
domainName: "localhost"
visiting: undefined
fileServer: null
props: {
port: "4200"
domain: ""
tld: "localhost"
}
})
it "sets <root> when not http url", ->
@server._server = {
address: -> {port: 9999}
}
@server._fileServer = {
port: -> 9998
}
ret = @server._onDomainSet("/index.html")
expect(ret).to.deep.eq({
auth: undefined
origin: "http://localhost:9999"
strategy: "file"
domainName: "localhost"
fileServer: "http://localhost:9998"
props: null
visiting: undefined
})