apollo-server/packages/apollo-server-express/src/apolloServerHttp.test.ts

554 lines
14 KiB
TypeScript
Raw Normal View History

2016-06-24 16:58:52 -04:00
// tslint:disable
// TODO: enable when you figure out how to automatically fix trailing commas
// TODO: maybe we should get rid of these tests entirely, and move them to expressApollo.test.ts
// TODO: wherever possible the tests should be rewritten to make them easily work with Hapi, express, Koa etc.
2016-06-24 16:58:52 -04:00
/*
* Below are the HTTP tests from express-graphql. We're using them here to make
* sure apolloServer still works if used in the place of express-graphql.
*/
import { graphqlExpress } from './expressApollo';
2016-06-24 16:58:52 -04:00
/**
* Copyright (c) 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
import { expect } from 'chai';
2016-06-27 16:15:03 -04:00
import * as zlib from 'zlib';
2016-06-24 16:58:52 -04:00
import * as multer from 'multer';
import * as bodyParser from 'body-parser';
const request = require('supertest');
2016-06-24 16:58:52 -04:00
const express4 = require('express'); // modern
//import express3 from 'express3'; // old but commonly still used
const express3 = express4;
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLNonNull,
GraphQLString,
GraphQLScalarType,
2016-06-24 16:58:52 -04:00
GraphQLError,
BREAK,
2016-06-24 16:58:52 -04:00
} from 'graphql';
const QueryRootType = new GraphQLObjectType({
name: 'QueryRoot',
fields: {
test: {
type: GraphQLString,
args: {
who: {
type: GraphQLString,
},
2016-06-24 16:58:52 -04:00
},
resolve: (root, args) => 'Hello ' + (args['who'] || 'World'),
2016-06-24 16:58:52 -04:00
},
thrower: {
type: new GraphQLNonNull(GraphQLString),
resolve: () => {
throw new Error('Throws!');
},
2016-06-24 16:58:52 -04:00
},
custom: {
type: GraphQLString,
args: {
foo: {
type: new GraphQLScalarType({
name: 'Foo',
serialize: v => v,
parseValue: () => {
throw new Error('Something bad happened');
},
parseLiteral: () => {
throw new Error('Something bad happened');
},
}),
},
},
},
2016-06-24 16:58:52 -04:00
context: {
type: GraphQLString,
resolve: (obj, args, context) => context,
},
},
2016-06-24 16:58:52 -04:00
});
const TestSchema = new GraphQLSchema({
query: QueryRootType,
mutation: new GraphQLObjectType({
name: 'MutationRoot',
fields: {
writeTest: {
type: QueryRootType,
resolve: () => ({}),
},
},
}),
2016-06-24 16:58:52 -04:00
});
function catchError(p) {
return p.then(
res => {
// workaround for unknown issues with testing against npm package of express-graphql.
2016-06-24 16:58:52 -04:00
// the same code works when testing against the source, I'm not sure why.
if (res && res.error) {
return { response: res };
}
throw new Error('Expected to catch error.');
},
error => {
if (!(error instanceof Error)) {
throw new Error('Expected error to be instanceof Error.');
}
return error;
},
2016-06-24 16:58:52 -04:00
);
}
function promiseTo(fn) {
return new Promise((resolve, reject) => {
fn((error, result) => (error ? reject(error) : resolve(result)));
2016-06-24 16:58:52 -04:00
});
}
describe('test harness', () => {
it('expects to catch errors', async () => {
let caught;
try {
await catchError(Promise.resolve());
} catch (error) {
caught = error;
}
expect(caught && caught.message).to.equal('Expected to catch error.');
});
it('expects to catch actual errors', async () => {
let caught;
try {
await catchError(Promise.reject('not a real error'));
} catch (error) {
caught = error;
}
expect(caught && caught.message).to.equal(
'Expected error to be instanceof Error.',
);
2016-06-24 16:58:52 -04:00
});
it('resolves callback promises', async () => {
const resolveValue = {};
const result = await promiseTo(cb => cb(null, resolveValue));
expect(result).to.equal(resolveValue);
});
it('rejects callback promises with errors', async () => {
const rejectError = new Error();
let caught;
try {
await promiseTo(cb => cb(rejectError));
} catch (error) {
caught = error;
}
expect(caught).to.equal(rejectError);
});
});
const express = express4;
const version = 'modern';
describe(`GraphQL-HTTP (apolloServer) tests for ${version} express`, () => {
describe('POST functionality', () => {
it('allows gzipped POST bodies', async () => {
const app = express();
app.use('/graphql', bodyParser.json());
app.use(
'/graphql',
graphqlExpress(() => ({
schema: TestSchema,
})),
);
const data = { query: '{ test(who: "World") }' };
const json = JSON.stringify(data);
// TODO had to write "as any as Buffer" to make tsc accept it. Does it matter?
const gzippedJson = await promiseTo(cb =>
zlib.gzip((json as any) as Buffer, cb),
);
const req = request(app)
.post('/graphql')
.set('Content-Type', 'application/json')
.set('Content-Encoding', 'gzip');
req.write(gzippedJson);
const response = await req;
expect(JSON.parse(response.text)).to.deep.equal({
data: {
test: 'Hello World',
},
2016-06-24 16:58:52 -04:00
});
});
2016-06-24 16:58:52 -04:00
it('allows deflated POST bodies', async () => {
const app = express();
app.use('/graphql', bodyParser.json());
app.use(
'/graphql',
graphqlExpress(() => ({
schema: TestSchema,
})),
);
const data = { query: '{ test(who: "World") }' };
const json = JSON.stringify(data);
// TODO had to write "as any as Buffer" to make tsc accept it. Does it matter?
const deflatedJson = await promiseTo(cb =>
zlib.deflate((json as any) as Buffer, cb),
);
const req = request(app)
.post('/graphql')
.set('Content-Type', 'application/json')
.set('Content-Encoding', 'deflate');
req.write(deflatedJson);
const response = await req;
expect(JSON.parse(response.text)).to.deep.equal({
data: {
test: 'Hello World',
},
2016-06-24 16:58:52 -04:00
});
});
2016-06-24 16:58:52 -04:00
it('allows for pre-parsed POST bodies', () => {
// Note: this is not the only way to handle file uploads with GraphQL,
// but it is terse and illustrative of using express-graphql and multer
// together.
// A simple schema which includes a mutation.
const UploadedFileType = new GraphQLObjectType({
name: 'UploadedFile',
fields: {
originalname: { type: GraphQLString },
mimetype: { type: GraphQLString },
},
});
2016-06-24 16:58:52 -04:00
const TestMutationSchema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'QueryRoot',
2016-06-24 16:58:52 -04:00
fields: {
test: { type: GraphQLString },
},
}),
mutation: new GraphQLObjectType({
name: 'MutationRoot',
fields: {
uploadFile: {
type: UploadedFileType,
resolve(rootValue) {
// For this test demo, we're just returning the uploaded
// file directly, but presumably you might return a Promise
// to go store the file somewhere first.
return rootValue.request.file;
},
},
},
}),
});
2016-06-24 16:58:52 -04:00
const app = express();
// Multer provides multipart form data parsing.
const storage = multer.memoryStorage();
app.use('/graphql', multer({ storage }).single('file'));
// Providing the request as part of `rootValue` allows it to
// be accessible from within Schema resolve functions.
app.use(
'/graphql',
graphqlExpress(req => {
return {
schema: TestMutationSchema,
rootValue: { request: req },
};
}),
);
const req = request(app)
.post('/graphql')
.field(
'query',
`mutation TestMutation {
uploadFile { originalname, mimetype }
}`,
)
.attach('file', __filename);
return req.then(response => {
expect(JSON.parse(response.text)).to.deep.equal({
data: {
uploadFile: {
originalname: 'apolloServerHttp.test.js',
mimetype: 'application/javascript',
},
},
2016-06-24 16:58:52 -04:00
});
});
});
});
2016-06-24 16:58:52 -04:00
describe('Error handling functionality', () => {
2016-11-15 18:47:46 +02:00
it('handles field errors caught by GraphQL', async () => {
const app = express();
2016-06-24 16:58:52 -04:00
app.use('/graphql', bodyParser.json());
app.use(
'/graphql',
graphqlExpress({
schema: TestSchema,
}),
);
2016-06-24 16:58:52 -04:00
const response = await request(app)
.post('/graphql')
.send({
query: '{thrower}',
2016-06-24 16:58:52 -04:00
});
// console.log(response.text);
expect(response.status).to.equal(200);
expect(JSON.parse(response.text)).to.deep.equal({
data: null,
errors: [
{
extensions: {
code: 'INTERNAL_SERVER_ERROR',
},
message: 'Throws!',
locations: [{ line: 1, column: 2 }],
path: ['thrower'],
},
],
});
});
it('handles type validation', async () => {
const app = express();
app.use('/graphql', bodyParser.json());
app.use(
'/graphql',
graphqlExpress({
schema: TestSchema,
}),
);
const response = await request(app)
.post('/graphql')
.send({
query: '{notExists}',
});
expect(response.status).to.equal(400);
expect(JSON.parse(response.text)).to.deep.equal({
errors: [
{
extensions: {
code: 'GRAPHQL_VALIDATION_FAILED',
},
message: 'Cannot query field "notExists" on type "QueryRoot".',
locations: [{ line: 1, column: 2 }],
},
],
});
});
it('handles type validation (GET)', async () => {
const app = express();
app.use(
'/graphql',
require('connect-query')(),
graphqlExpress({
schema: TestSchema,
}),
);
const response = await request(app)
.get('/graphql')
.query({ query: '{notExists}' });
expect(response.status).to.equal(400);
expect(JSON.parse(response.text)).to.deep.equal({
errors: [
{
extensions: {
code: 'GRAPHQL_VALIDATION_FAILED',
},
message: 'Cannot query field "notExists" on type "QueryRoot".',
locations: [{ line: 1, column: 2 }],
},
],
});
});
it('handles errors thrown during custom graphql type handling', async () => {
const app = express();
app.use('/graphql', bodyParser.json());
app.use(
'/graphql',
graphqlExpress({
schema: TestSchema,
}),
);
const response = await request(app)
.post('/graphql')
.send({
query: '{custom(foo: 123)}',
});
expect(response.status).to.equal(400);
});
2016-06-24 16:58:52 -04:00
it('allows for custom error formatting to sanitize', async () => {
const app = express();
2016-06-24 16:58:52 -04:00
app.use('/graphql', bodyParser.json());
app.use(
'/graphql',
graphqlExpress({
schema: TestSchema,
formatError(error) {
return { message: 'Custom error format: ' + error.message };
},
}),
);
2016-06-24 16:58:52 -04:00
const response = await request(app)
.post('/graphql')
.send({
query: '{thrower}',
2016-06-24 16:58:52 -04:00
});
expect(response.status).to.equal(200);
expect(JSON.parse(response.text)).to.deep.equal({
data: null,
errors: [
{
message: 'Custom error format: Throws!',
},
],
2016-06-24 16:58:52 -04:00
});
});
2016-06-24 16:58:52 -04:00
it('allows for custom error formatting to elaborate', async () => {
const app = express();
2016-06-24 16:58:52 -04:00
app.use('/graphql', bodyParser.json());
app.use(
'/graphql',
graphqlExpress({
schema: TestSchema,
formatError(error) {
return {
message: error.message,
locations: error.locations,
stack: 'Stack trace',
};
},
}),
);
2016-06-24 16:58:52 -04:00
const response = await request(app)
.post('/graphql')
.send({
query: '{thrower}',
2016-06-24 16:58:52 -04:00
});
expect(response.status).to.equal(200);
expect(JSON.parse(response.text)).to.deep.equal({
data: null,
errors: [
{
message: 'Throws!',
locations: [{ line: 1, column: 2 }],
stack: 'Stack trace',
},
],
2016-06-24 16:58:52 -04:00
});
});
2016-06-24 16:58:52 -04:00
it('handles unsupported HTTP methods', async () => {
const app = express();
2016-06-24 16:58:52 -04:00
app.use('/graphql', bodyParser.json());
app.use('/graphql', graphqlExpress({ schema: TestSchema }));
2016-06-24 16:58:52 -04:00
const response = await request(app)
.put('/graphql')
.query({ query: '{test}' });
expect(response.status).to.equal(405);
expect(response.headers.allow).to.equal('GET, POST');
expect(response.text).to.contain(
'Apollo Server supports only GET/POST requests.',
);
2016-06-24 16:58:52 -04:00
});
});
2016-07-02 22:51:17 -07:00
describe('Custom validation rules', () => {
const AlwaysInvalidRule = function(context) {
return {
enter() {
context.reportError(
new GraphQLError('AlwaysInvalidRule was really invalid!'),
);
return BREAK;
},
2016-07-02 22:51:17 -07:00
};
};
2016-07-02 22:51:17 -07:00
it('Do not execute a query if it do not pass the custom validation.', async () => {
const app = express();
2016-07-02 22:51:17 -07:00
app.use('/graphql', bodyParser.json());
app.use(
'/graphql',
graphqlExpress({
2016-07-02 22:51:17 -07:00
schema: TestSchema,
validationRules: [AlwaysInvalidRule],
}),
);
2016-07-02 22:51:17 -07:00
const response = await request(app)
.post('/graphql')
.send({
query: '{thrower}',
2016-07-02 22:51:17 -07:00
});
expect(response.status).to.equal(400);
expect(JSON.parse(response.text)).to.deep.equal({
errors: [
{
extensions: {
code: 'GRAPHQL_VALIDATION_FAILED',
},
message: 'AlwaysInvalidRule was really invalid!',
},
],
2016-07-02 22:51:17 -07:00
});
});
});
2016-06-24 16:58:52 -04:00
});