Skip to content

Commit

Permalink
Add the JavaScript stoppable test suite
Browse files Browse the repository at this point in the history
  • Loading branch information
glasser committed Jul 23, 2021
1 parent 26ac42c commit aadbeb6
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 0 deletions.
278 changes: 278 additions & 0 deletions packages/apollo-server/src/__tests__/stoppable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// This file is adapted from the stoppable npm package:
// https://github.com/hunterloftis/stoppable
//
// We've ported it to TypeScript and made some further changes.
// Here's the license of the original code:
//
// The MIT License (MIT)
//
// Copyright (c) 2017 Hunter Loftis <hunter@hunterloftis.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

const http = require('http')
const https = require('https')
const a = require('awaiting')
const request = require('requisition')
const assert = require('chai').assert
const fs = require('fs')
const stoppable = require('..')
const child = require('child_process')
const path = require('path')

const PORT = 8000

const schemes = {
http: {
agent: (opts = {}) => new http.Agent(opts),
server: handler => http.createServer(handler || ((req, res) => res.end('hello')))
},
https: {
agent: (opts = {}) => https.Agent(Object.assign({rejectUnauthorized: false}, opts)),
server: handler => https.createServer({
key: fs.readFileSync('test/fixture.key'),
cert: fs.readFileSync('test/fixture.cert')
}, handler || ((req, res) => res.end('hello')))
}
}

Object.keys(schemes).forEach(schemeName => {
const scheme = schemes[schemeName]

describe(`${schemeName}.Server`, function () {
describe('.close()', () => {
let server

beforeEach(function () {
server = scheme.server()
})

describe('without keep-alive connections', () => {
let closed = 0
it('stops accepting new connections', async () => {
server.on('close', () => closed++)
server.listen(PORT)
await a.event(server, 'listening')
const res1 =
await request(`${schemeName}://localhost:${PORT}`).agent(scheme.agent())
const text1 = await res1.text()
assert.equal(text1, 'hello')
server.close()
const err = await a.failure(
request(`${schemeName}://localhost:${PORT}`).agent(scheme.agent()))
assert.match(err.message, /ECONNREFUSED/)
})

it('closes', () => {
assert.equal(closed, 1)
})
})

describe('with keep-alive connections', () => {
let closed = 0

it('stops accepting new connections', async () => {
server.on('close', () => closed++)
server.listen(PORT)
await a.event(server, 'listening')
const res1 = await request(`${schemeName}://localhost:${PORT}`)
.agent(scheme.agent({keepAlive: true}))
const text1 = await res1.text()
assert.equal(text1, 'hello')
server.close()
const err =
await a.failure(request(`${schemeName}://localhost:${PORT}`)
.agent(scheme.agent({keepAlive: true})))
assert.match(err.message, /ECONNREFUSED/)
})

it("doesn't close", () => {
assert.equal(closed, 0)
})
})
})

describe('.stop()', function () {
describe('without keep-alive connections', function () {
let closed = 0
let gracefully = false
let server

beforeEach(function () {
server = stoppable(scheme.server())
})

it('stops accepting new connections', async () => {
server.on('close', () => closed++)
server.listen(PORT)
await a.event(server, 'listening')
const res1 =
await request(`${schemeName}://localhost:${PORT}`).agent(scheme.agent())
const text1 = await res1.text()
assert.equal(text1, 'hello')
server.stop((e, g) => {
gracefully = g
})
const err = await a.failure(
request(`${schemeName}://localhost:${PORT}`).agent(scheme.agent()))
assert.match(err.message, /ECONNREFUSED/)
})

it('closes', () => {
assert.equal(closed, 1)
})

it('gracefully', () => {
assert.isOk(gracefully)
})
})

describe('with keep-alive connections', () => {
let closed = 0
let gracefully = false
let server

beforeEach(function () {
server = stoppable(scheme.server())
})

it('stops accepting new connections', async () => {
server.on('close', () => closed++)
server.listen(PORT)
await a.event(server, 'listening')
const res1 = await request(`${schemeName}://localhost:${PORT}`)
.agent(scheme.agent({keepAlive: true}))
const text1 = await res1.text()
assert.equal(text1, 'hello')
server.stop((e, g) => {
gracefully = g
})
const err = await a.failure(request(`${schemeName}://localhost:${PORT}`)
.agent(scheme.agent({ keepAlive: true })))
assert.match(err.message, /ECONNREFUSED/)
})

it('closes', () => { assert.equal(closed, 1) })

it('gracefully', () => {
assert.isOk(gracefully)
})

it('empties all sockets once closed',
() => { assert.equal(server._pendingSockets.size, 0) })

it('registers the "close" callback', (done) => {
server.listen(PORT)
server.stop(done)
})
})
})

describe('with a 0.5s grace period', () => {
let gracefully = true
let server

beforeEach(function () {
server = stoppable(scheme.server((req, res) => {
res.writeHead(200)
res.write('hi')
}), 500)
})

it('kills connections after 0.5s', async () => {
server.listen(PORT)
await a.event(server, 'listening')
await Promise.all([
request(`${schemeName}://localhost:${PORT}`)
.agent(scheme.agent({keepAlive: true})),
request(`${schemeName}://localhost:${PORT}`)
.agent(scheme.agent({keepAlive: true}))
])
const start = Date.now()
server.stop((e, g) => {
gracefully = g
})
await a.event(server, 'close')
assert.closeTo(Date.now() - start, 500, 50)
})

it('gracefully', () => {
assert.isNotOk(gracefully)
})

it('empties all sockets', () => {
assert.equal(server._pendingSockets.size, 0)
})
})

describe('with requests in-flight', () => {
let server
let gracefully = false

beforeEach(function () {
server = stoppable(scheme.server((req, res) => {
const delay = parseInt(req.url.slice(1), 10)
res.writeHead(200)
res.write('hello')
setTimeout(() => res.end('world'), delay)
}))
})

it('closes their sockets once they finish', async () => {
server.listen(PORT)
await a.event(server, 'listening')
const start = Date.now()
const res = await Promise.all([
request(`${schemeName}://localhost:${PORT}/250`)
.agent(scheme.agent({keepAlive: true})),
request(`${schemeName}://localhost:${PORT}/500`)
.agent(scheme.agent({keepAlive: true}))
])
server.stop((e, g) => {
gracefully = g
})
const bodies = await Promise.all(res.map(r => r.text()))
await a.event(server, 'close')
assert.equal(bodies[0], 'helloworld')
assert.closeTo(Date.now() - start, 500, 100)
})
it('gracefully', () => {
assert.isOk(gracefully)
})

describe('with in-flights finishing before grace period ends', function () {
if (schemeName !== 'http') {
return
}

it('exits immediately', async () => {
const file = path.join(__dirname, 'server.js')
const server = child.spawn('node', [file, '500'])
await a.event(server.stdout, 'data')
const start = Date.now()
const res = await request(`${schemeName}://localhost:${PORT}/250`)
.agent(scheme.agent({keepAlive: true}))
const body = await res.text()
assert.equal(body, 'helloworld')
assert.closeTo(Date.now() - start, 250, 100)
})
})
})
})
})
19 changes: 19 additions & 0 deletions packages/apollo-server/src/__tests__/stoppable/fixture.cert
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDBzCCAe+gAwIBAgIJALpsaWTYcddKMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV
BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNzA1MTkxOTAwMzZaFw0yNzA1MTcxOTAw
MzZaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAOix4xb3jnIdWwqfT+vtoXadLuQJE21HN19Y2falkJuB
k/ME0TKMeMjmQQZCO7G0K+5fMnQsEtQjcjY3XUXViliGfkAQplUD3Z9/xwsTSGN2
ahBXE2W/GV+xJ6uX9KbJx2pMOXCuux6cKylYhmr8cTs2f9E6QpPji4LqtHv/9cAE
QKRmv2rSAP1Q+1Ne2WYNbgHBuI35vuQsvZTN5QsozsferP9Qqtx8kpnBaLTgFZYD
ZaEreYwFFYAQNfq2jOGEAAxStiXUpn3rT9T8KeOvLfWOifqYzDOTzL0t2py9bnvl
x2fl8aJHc3NiU+4qlq3DuDEitiUoOkirGhFL7JFH4K0CAwEAAaNQME4wHQYDVR0O
BBYEFAI/PRTwA3VKpSQAwXg2JDmDGVXxMB8GA1UdIwQYMBaAFAI/PRTwA3VKpSQA
wXg2JDmDGVXxMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHfYOl+n
qB0Oyq/2jXE33fR5+QsaDrciXH0BWCjIVM8ADLPjYPAD3StTIceg3wfCw2n9xuku
pObukGbApSqvGBSAIsvbbNAwcFp31DCqGQdfVMJMG1TCp3J7y0WslYMqU7DM1jYt
ybqF9ICuGdPZ0KVsv0/ZmbSs98/fSyjUYoHfD+GngVrguuU/v0XUi4hjVqFyMZQZ
AxGNq4QIlKxdo55L45vCMzGiajT7BE0EnChvFpOGXF5/pk072RESI7uxJBiAssWP
uCk0xHxLtacOQK3seFFw0d7t3769gVDNi732eTMhoFQj+loSgmnRwDKL7QPhZ8tj
pRRUGV4sPR+ucpo=
-----END CERTIFICATE-----
27 changes: 27 additions & 0 deletions packages/apollo-server/src/__tests__/stoppable/fixture.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA6LHjFveOch1bCp9P6+2hdp0u5AkTbUc3X1jZ9qWQm4GT8wTR
Mox4yOZBBkI7sbQr7l8ydCwS1CNyNjddRdWKWIZ+QBCmVQPdn3/HCxNIY3ZqEFcT
Zb8ZX7Enq5f0psnHakw5cK67HpwrKViGavxxOzZ/0TpCk+OLguq0e//1wARApGa/
atIA/VD7U17ZZg1uAcG4jfm+5Cy9lM3lCyjOx96s/1Cq3HySmcFotOAVlgNloSt5
jAUVgBA1+raM4YQADFK2JdSmfetP1Pwp468t9Y6J+pjMM5PMvS3anL1ue+XHZ+Xx
okdzc2JT7iqWrcO4MSK2JSg6SKsaEUvskUfgrQIDAQABAoIBAE8UJRipCL+/OjFh
8sc6+qRUxpq4euGoUikVCP3JRluSrbTo7i8/jcy4c2CtIZxCnqtjrsHMOJnfcfD6
37fb2ig7jKw4/E3oAmkyA3LAGtmyZFkpPm5Vg0oB6nlmKr6D1EFLpjmlJ/I/IGvs
qcGyCMkWvFlec0HPEpprKOr7EYkvQscy99ny7JswG1P9PELwo7tIeb0BioPYnFmF
I0BPgI1lxDHKQTUPAao9rStiHsuPMCkw51qUgp/Z814ld4KDXCaWFQPy5riHDykH
wm9n2hkM6pq4d6eHuMVj7CuBdp141k2BAnZdysMHpE9y1315+didoEcox8+zOLeO
OC4XZAECgYEA/U98ld2YnVcSL9/Pr17SVG/q3iZaUueUkf+CEdj7LpvStedpky5W
dOM7ad0kBcPqIafgn/O3teYjVl8FM0oOtOheMHHMkYxbXuECA5hkk7emu3oIJcAO
+9Pb/uGdufWmAVyRueRam4tubiLxv46xeGUmscCnwG78bj+rq74ATjcCgYEA6ypd
qt/b43y4SHY4LDuuJk5jfC5MNXztIi3sOtuGoJNUlzC/hI/NNhEDhP3Pzo9c/i0k
aCzyjhRyiaFK2SHQ5SQdCFi44PM+MptwFjY1KPGv20m5omfBgJOoF+Ad13qrUQF/
b7/C5j3PZkOZfwaYO+erLeaayWKRJi2AEoXb9jsCgYEAnxAHuo/A4qQnXnqbDpNr
Xew9Pqw0sbSLvbYFNjHbYKQmh2U+DVbeoV2DFHHxydEBN4sUaTyAUq+l5vmZ6WAK
phz38FG1VHwfcA+41QsftQZwo274qMPWZNnfXkjMY1ZWnKpFM8aqAtxmRrCYv2Ha
HTDfQGUqsZK/3ncK1LhltrcCgYBiJKk4wfpL42YpX6Ur2LBibj6Yud22SO/SXuYC
3lE+PJ6GBqM3GKilEs6sNxz98Nj3fzF9hJyp7SCsDbNmEPXUW5D+RcDKqNlhV3uc
2XywHMWuuAMQI0sfdQAnDrKFlj1fLkfYBGi7nDotTLMHz2HDRnkrS913hHpdO4oC
sPjOtwKBgQDDiG7Vagk4SgPXt7zE0aSiFIIjJLpM28mES+k6zOtKAyOcTMHcDrI7
YmSN1kq3w2g7RS5eMUpszqbUGoR6VDAjbgGAakDOno/uZWfEMjiQiKvRDSY1nmlc
xSKubMZDf/OKUYTGasL1rqJJN7mxW2irptygc26NxMeAWZfgkmiPLg==
-----END RSA PRIVATE KEY-----
13 changes: 13 additions & 0 deletions packages/apollo-server/src/__tests__/stoppable/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const http = require('http')
const stoppable = require('..')

const grace = Number(process.argv[2] || Infinity)
const server = http.createServer((req, res) => {
const delay = parseInt(req.url.slice(1), 10)
res.writeHead(200)
res.write('hello')
setTimeout(() => res.end('world'), delay)
server.stop()
})
stoppable(server, grace)
server.listen(8000, () => console.log('listening'))

0 comments on commit aadbeb6

Please sign in to comment.