Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests to demonstrate client abort #84

Merged
merged 2 commits into from
Jun 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Belvedere_Apollo_Pio-Clementino_Inv1015.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
"watch": "watch 'npm test --silent' src --interval 1",
"test": "FORCE_COLOR=1 TAP_COLORS=1 npm-run-all build:clean -p build:mjs build:js -s build:prettier -p lint:* tap:* -c --aggregate-output --silent",
"prepublishOnly": "npm test",
"test:js": "FORCE_COLOR=1 TAP_COLORS=1 npm-run-all build:clean build:js tap:js",
"test:mjs": "FORCE_COLOR=1 TAP_COLORS=1 npm-run-all build:clean build:mjs tap:mjs",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following the reasons for this change, but I have a new approach to scripts that I'll copy over soon from graphql-api-koa and graphql-react: https://github.com/jaydenseric/graphql-api-koa/blob/master/package.json#L58. It leverages prepare so people can npm install a git url to use forks and even specific commits.

"precommit": "lint-staged"
},
"lint-staged": {
Expand Down
281 changes: 267 additions & 14 deletions src/test.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import fs from 'fs'
import { Transform } from 'stream'
import http from 'http'
import t from 'tap'
import Koa from 'koa'
import express from 'express'
Expand All @@ -11,13 +13,21 @@ import {
MaxFilesUploadError,
MapBeforeOperationsUploadError,
FilesBeforeMapUploadError,
FileMissingUploadError
FileMissingUploadError,
UploadPromiseDisconnectUploadError,
FileStreamDisconnectUploadError
} from '.'

// GraphQL multipart request spec:
// https://github.com/jaydenseric/graphql-multipart-request-spec

const TEST_FILE_PATH = 'package.json'
const TEST_FILE_PATH_JSON = 'package.json'
const TEST_FILE_PATH_SVG = 'apollo-upload-logo.svg'

// The max TCP packet size is 64KB, so this file is guaranteed to arrive
// in multiple chunks. It is a public domain image retreived from:
// https://commons.wikimedia.org/wiki/File:Belvedere_Apollo_Pio-Clementino_Inv1015.jpg
const TEST_FILE_PATH_JPG = 'Belvedere_Apollo_Pio-Clementino_Inv1015.jpg'

const startServer = (t, app) =>
new Promise((resolve, reject) => {
Expand Down Expand Up @@ -65,7 +75,7 @@ t.test('Single file.', async t => {
)

body.append('map', JSON.stringify({ 1: ['variables.file'] }))
body.append(1, fs.createReadStream(TEST_FILE_PATH))
body.append(1, fs.createReadStream(TEST_FILE_PATH_JSON))

await fetch(`http://localhost:${port}`, { method: 'POST', body })
}
Expand Down Expand Up @@ -105,6 +115,249 @@ t.test('Single file.', async t => {
})
})

t.test('Aborted request.', async t => {
t.jobs = 1

const abortedStreamTest = upload => async () => {
const resolved = await upload

await new Promise((resolve, reject) => {
resolved.stream.on('error', err => {
t.type(err, FileStreamDisconnectUploadError)
resolve()
})
resolved.stream.on('end', reject)
})

return resolved
}

const abortedPromiseTest = upload => t => {
t.rejects(upload, UploadPromiseDisconnectUploadError)
return Promise.resolve()
}

const testRequest = port =>
new Promise((resolve, reject) => {
const body = new FormData()

body.append(
'operations',
JSON.stringify({
variables: {
file1: null,
file2: null,
file3: null
}
})
)

body.append(
'map',
JSON.stringify({
1: ['variables.file1'],
2: ['variables.file2'],
3: ['variables.file3']
})
)
body.append(1, fs.createReadStream(TEST_FILE_PATH_JSON))
body.append(2, fs.createReadStream(TEST_FILE_PATH_SVG))
body.append(3, fs.createReadStream(TEST_FILE_PATH_JPG))

const request = http.request({
method: 'POST',
host: 'localhost',
port: port,
headers: body.getHeaders()
})

// This is expected, since we're aborting the connection
request.on('error', err => {
if (err.code !== 'ECONNRESET') reject(err)
})

// Note that this may emit before the downstream middleware has
// been processed.
request.on('close', resolve)

let data = ''
const transform = new Transform({
transform(chunk, encoding, callback) {
if (this._aborted) return

const chunkString = chunk.toString('utf8')

// Concatenate the data
data += chunkString

// When we encounter the final contents of the SVG, we will
// abort the request. This ensures that we are testing:
// 1. successful upload
// 2. FileStreamDisconnectUploadError
// 3. UploadPromiseDisconnectUploadError
if (data.includes('</svg>')) {
// How much of this chunk do we want to pipe to the request
// before aborting?
const length =
chunkString.length - (data.length - data.indexOf('</svg>'))

// Abort now.
if (length < 1) {
request.abort()
return
}

// Send partial chunk and then abort
this._aborted = true
callback(null, chunkString.substr(0, length))
process.nextTick(() => request.abort())
return
}

callback(null, chunk)
}
})

body.pipe(transform).pipe(request)
})

await t.test('Koa middleware.', async t => {
t.plan(3)

let resume
const delay = new Promise(resolve => (resume = resolve))
const app = new Koa().use(apolloUploadKoa()).use(async (ctx, next) => {
try {
await Promise.all([
t.test(
'Upload resolves.',
uploadTest(ctx.request.body.variables.file1)
),

t.test(
'In-progress upload streams are destroyed.',
abortedStreamTest(ctx.request.body.variables.file2)
),

t.test(
'Unresolved upload promises are rejected.',
abortedPromiseTest(ctx.request.body.variables.file3)
)
])
} finally {
resume()
}

ctx.status = 204
await next()
})
const port = await startServer(t, app)
await testRequest(port)
await delay
})

await t.test('Koa middleware without stream error handler.', async t => {
t.plan(2)

let resume
const delay = new Promise(resolve => (resume = resolve))
const app = new Koa().use(apolloUploadKoa()).use(async (ctx, next) => {
try {
await Promise.all([
t.test(
'Upload resolves.',
uploadTest(ctx.request.body.variables.file1)
),

t.test(
'Unresolved upload promises are rejected.',
abortedPromiseTest(ctx.request.body.variables.file3)
)
])
} finally {
resume()
}

ctx.status = 204
await next()
})

const port = await startServer(t, app)

await testRequest(port)
await delay
})

await t.test('Express middleware.', async t => {
t.plan(3)

let resume
const delay = new Promise(resolve => (resume = resolve))
const app = express()
.use(apolloUploadExpress())
.use((request, response, next) => {
Promise.all([
t.test('Upload resolves.', uploadTest(request.body.variables.file1)),

t.test(
'In-progress upload streams are destroyed.',
abortedStreamTest(request.body.variables.file2)
),

t.test(
'Unresolved upload promises are rejected.',
abortedPromiseTest(request.body.variables.file3)
)
])
.then(() => {
resume()
next()
})
.catch(err => {
resume()
next(err)
})
})

const port = await startServer(t, app)

await testRequest(port)
await delay
})

await t.test('Express middleware without stream error handler.', async t => {
t.plan(2)

let resume
const delay = new Promise(resolve => (resume = resolve))
const app = express()
.use(apolloUploadExpress())
.use((request, response, next) => {
Promise.all([
t.test('Upload resolves.', uploadTest(request.body.variables.file1)),

t.test(
'Unresolved upload promises are rejected.',
abortedPromiseTest(request.body.variables.file3)
)
])
.then(() => {
resume()
next()
})
.catch(err => {
resume()
next(err)
})
})

const port = await startServer(t, app)

await testRequest(port)
await delay
})
})

t.todo('Deduped files.', async t => {
t.jobs = 2

Expand All @@ -127,7 +380,7 @@ t.todo('Deduped files.', async t => {
})
)

body.append(1, fs.createReadStream(TEST_FILE_PATH))
body.append(1, fs.createReadStream(TEST_FILE_PATH_JSON))

await fetch(`http://localhost:${port}`, { method: 'POST', body })
}
Expand Down Expand Up @@ -264,8 +517,8 @@ t.test('Extraneous file.', async t => {
})
)

body.append(1, fs.createReadStream(TEST_FILE_PATH))
body.append(2, fs.createReadStream(TEST_FILE_PATH))
body.append(1, fs.createReadStream(TEST_FILE_PATH_JSON))
body.append(2, fs.createReadStream(TEST_FILE_PATH_JSON))

await fetch(`http://localhost:${port}`, { method: 'POST', body })
}
Expand Down Expand Up @@ -327,8 +580,8 @@ t.test('Exceed max files.', async t => {
})
)

body.append(1, fs.createReadStream(TEST_FILE_PATH))
body.append(2, fs.createReadStream(TEST_FILE_PATH))
body.append(1, fs.createReadStream(TEST_FILE_PATH_JSON))
body.append(2, fs.createReadStream(TEST_FILE_PATH_JSON))

const { status } = await fetch(`http://localhost:${port}`, {
method: 'POST',
Expand Down Expand Up @@ -394,9 +647,9 @@ t.test('Exceed max files with extraneous files interspersed.', async t => {
})
)

body.append('1', fs.createReadStream(TEST_FILE_PATH))
body.append('extraneous', fs.createReadStream(TEST_FILE_PATH))
body.append('2', fs.createReadStream(TEST_FILE_PATH))
body.append('1', fs.createReadStream(TEST_FILE_PATH_JSON))
body.append('extraneous', fs.createReadStream(TEST_FILE_PATH_JSON))
body.append('2', fs.createReadStream(TEST_FILE_PATH_JSON))

await fetch(`http://localhost:${port}`, { method: 'POST', body })
}
Expand Down Expand Up @@ -468,7 +721,7 @@ t.todo('Exceed max file size.', async t => {
)

body.append('map', JSON.stringify({ '1': ['variables.file'] }))
body.append('1', fs.createReadStream(TEST_FILE_PATH))
body.append('1', fs.createReadStream(TEST_FILE_PATH_JSON))

await fetch(`http://localhost:${port}`, { method: 'POST', body })
}
Expand Down Expand Up @@ -550,7 +803,7 @@ t.test('Misorder “map” before “operations”.', async t => {
})
)

body.append('1', fs.createReadStream(TEST_FILE_PATH))
body.append('1', fs.createReadStream(TEST_FILE_PATH_JSON))

const { status } = await fetch(`http://localhost:${port}`, {
method: 'POST',
Expand Down Expand Up @@ -608,7 +861,7 @@ t.test('Misorder files before “map”.', async t => {
})
)

body.append('1', fs.createReadStream(TEST_FILE_PATH))
body.append('1', fs.createReadStream(TEST_FILE_PATH_JSON))

body.append(
'map',
Expand Down