mirror of
https://github.com/vale981/apollo-server
synced 2025-03-16 07:46:54 -04:00
553 lines
14 KiB
TypeScript
553 lines
14 KiB
TypeScript
// 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.
|
|
|
|
/*
|
|
* 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';
|
|
|
|
/**
|
|
* 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';
|
|
import * as zlib from 'zlib';
|
|
import * as multer from 'multer';
|
|
import * as bodyParser from 'body-parser';
|
|
const request = require('supertest');
|
|
const express4 = require('express'); // modern
|
|
//import express3 from 'express3'; // old but commonly still used
|
|
const express3 = express4;
|
|
import {
|
|
GraphQLSchema,
|
|
GraphQLObjectType,
|
|
GraphQLNonNull,
|
|
GraphQLString,
|
|
GraphQLScalarType,
|
|
GraphQLError,
|
|
BREAK,
|
|
} from 'graphql';
|
|
|
|
const QueryRootType = new GraphQLObjectType({
|
|
name: 'QueryRoot',
|
|
fields: {
|
|
test: {
|
|
type: GraphQLString,
|
|
args: {
|
|
who: {
|
|
type: GraphQLString,
|
|
},
|
|
},
|
|
resolve: (root, args) => 'Hello ' + (args['who'] || 'World'),
|
|
},
|
|
thrower: {
|
|
type: new GraphQLNonNull(GraphQLString),
|
|
resolve: () => {
|
|
throw new Error('Throws!');
|
|
},
|
|
},
|
|
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');
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
context: {
|
|
type: GraphQLString,
|
|
resolve: (obj, args, context) => context,
|
|
},
|
|
},
|
|
});
|
|
|
|
const TestSchema = new GraphQLSchema({
|
|
query: QueryRootType,
|
|
mutation: new GraphQLObjectType({
|
|
name: 'MutationRoot',
|
|
fields: {
|
|
writeTest: {
|
|
type: QueryRootType,
|
|
resolve: () => ({}),
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
|
|
function catchError(p) {
|
|
return p.then(
|
|
res => {
|
|
// workaround for unknown issues with testing against npm package of express-graphql.
|
|
// 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;
|
|
},
|
|
);
|
|
}
|
|
|
|
function promiseTo(fn) {
|
|
return new Promise((resolve, reject) => {
|
|
fn((error, result) => (error ? reject(error) : resolve(result)));
|
|
});
|
|
}
|
|
|
|
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.',
|
|
);
|
|
});
|
|
|
|
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',
|
|
},
|
|
});
|
|
});
|
|
|
|
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',
|
|
},
|
|
});
|
|
});
|
|
|
|
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 },
|
|
},
|
|
});
|
|
|
|
const TestMutationSchema = new GraphQLSchema({
|
|
query: new GraphQLObjectType({
|
|
name: 'QueryRoot',
|
|
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;
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
|
|
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',
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Error handling functionality', () => {
|
|
it('handles field errors caught by GraphQL', async () => {
|
|
const app = express();
|
|
|
|
app.use('/graphql', bodyParser.json());
|
|
app.use(
|
|
'/graphql',
|
|
graphqlExpress({
|
|
schema: TestSchema,
|
|
}),
|
|
);
|
|
|
|
const response = await request(app)
|
|
.post('/graphql')
|
|
.send({
|
|
query: '{thrower}',
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
|
|
it('allows for custom error formatting to sanitize', async () => {
|
|
const app = express();
|
|
|
|
app.use('/graphql', bodyParser.json());
|
|
app.use(
|
|
'/graphql',
|
|
graphqlExpress({
|
|
schema: TestSchema,
|
|
formatError(error) {
|
|
return { message: 'Custom error format: ' + error.message };
|
|
},
|
|
}),
|
|
);
|
|
|
|
const response = await request(app)
|
|
.post('/graphql')
|
|
.send({
|
|
query: '{thrower}',
|
|
});
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(JSON.parse(response.text)).to.deep.equal({
|
|
data: null,
|
|
errors: [
|
|
{
|
|
message: 'Custom error format: Throws!',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('allows for custom error formatting to elaborate', async () => {
|
|
const app = express();
|
|
|
|
app.use('/graphql', bodyParser.json());
|
|
app.use(
|
|
'/graphql',
|
|
graphqlExpress({
|
|
schema: TestSchema,
|
|
formatError(error) {
|
|
return {
|
|
message: error.message,
|
|
locations: error.locations,
|
|
stack: 'Stack trace',
|
|
};
|
|
},
|
|
}),
|
|
);
|
|
|
|
const response = await request(app)
|
|
.post('/graphql')
|
|
.send({
|
|
query: '{thrower}',
|
|
});
|
|
|
|
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',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('handles unsupported HTTP methods', async () => {
|
|
const app = express();
|
|
|
|
app.use('/graphql', bodyParser.json());
|
|
app.use('/graphql', graphqlExpress({ schema: TestSchema }));
|
|
|
|
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.',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Custom validation rules', () => {
|
|
const AlwaysInvalidRule = function(context) {
|
|
return {
|
|
enter() {
|
|
context.reportError(
|
|
new GraphQLError('AlwaysInvalidRule was really invalid!'),
|
|
);
|
|
return BREAK;
|
|
},
|
|
};
|
|
};
|
|
|
|
it('Do not execute a query if it do not pass the custom validation.', async () => {
|
|
const app = express();
|
|
|
|
app.use('/graphql', bodyParser.json());
|
|
app.use(
|
|
'/graphql',
|
|
graphqlExpress({
|
|
schema: TestSchema,
|
|
validationRules: [AlwaysInvalidRule],
|
|
}),
|
|
);
|
|
|
|
const response = await request(app)
|
|
.post('/graphql')
|
|
.send({
|
|
query: '{thrower}',
|
|
});
|
|
|
|
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!',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
});
|