apollo-server/packages/apollo-server-integration-testsuite/src/index.ts

1109 lines
31 KiB
TypeScript

import { expect } from 'chai';
import { stub } from 'sinon';
import 'mocha';
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLError,
GraphQLNonNull,
introspectionQuery,
BREAK,
} from 'graphql';
// tslint:disable-next-line
const request = require('supertest');
import { GraphQLOptions } from 'apollo-server-core';
import * as GraphiQL from 'apollo-server-module-graphiql';
import { OperationStore } from 'apollo-server-module-operation-store';
import gql from 'graphql-tag';
const personType = new GraphQLObjectType({
name: 'PersonType',
fields: {
firstName: {
type: GraphQLString,
},
lastName: {
type: GraphQLString,
},
},
});
const queryType = new GraphQLObjectType({
name: 'QueryType',
fields: {
testString: {
type: GraphQLString,
resolve() {
return 'it works';
},
},
testPerson: {
type: personType,
resolve() {
return { firstName: 'Jane', lastName: 'Doe' };
},
},
testStringWithDelay: {
type: GraphQLString,
args: {
delay: { type: new GraphQLNonNull(GraphQLInt) },
},
resolve(_, args) {
return new Promise(resolve => {
setTimeout(() => resolve('it works'), args['delay']);
});
},
},
testContext: {
type: GraphQLString,
resolve(_root, _args, context) {
if (context.otherField) {
return 'unexpected';
}
context.otherField = true;
return context.testField;
},
},
testRootValue: {
type: GraphQLString,
resolve(rootValue) {
return rootValue;
},
},
testArgument: {
type: GraphQLString,
args: { echo: { type: GraphQLString } },
resolve(_, { echo }) {
return `hello ${echo}`;
},
},
testError: {
type: GraphQLString,
resolve() {
throw new Error('Secret error message');
},
},
},
});
const mutationType = new GraphQLObjectType({
name: 'MutationType',
fields: {
testMutation: {
type: GraphQLString,
args: { echo: { type: GraphQLString } },
resolve(_, { echo }) {
return `not really a mutation, but who cares: ${echo}`;
},
},
testPerson: {
type: personType,
args: {
firstName: {
type: new GraphQLNonNull(GraphQLString),
},
lastName: {
type: new GraphQLNonNull(GraphQLString),
},
},
resolve(_, args) {
return args;
},
},
},
});
export const schema = new GraphQLSchema({
query: queryType,
mutation: mutationType,
});
export interface CreateAppOptions {
excludeParser?: boolean;
graphqlOptions?:
| GraphQLOptions
| { (): GraphQLOptions | Promise<GraphQLOptions> };
graphiqlOptions?:
| GraphiQL.GraphiQLData
| { (): GraphiQL.GraphiQLData | Promise<GraphiQL.GraphiQLData> };
}
export interface CreateAppFunc {
(options?: CreateAppOptions): any | Promise<any>;
}
export interface DestroyAppFunc {
(app: any): void | Promise<void>;
}
export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
describe('apolloServer', () => {
let app;
afterEach(async () => {
if (app) {
if (destroyApp) {
await destroyApp(app);
app = null;
} else {
app = null;
}
}
});
describe('graphqlHTTP', () => {
it('can be called with an options function', async () => {
app = await createApp({
graphqlOptions: (): GraphQLOptions => ({ schema }),
});
const expected = {
testString: 'it works',
};
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testString }',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
});
});
it('can be called with an options function that returns a promise', async () => {
app = await createApp({
graphqlOptions: () => {
return new Promise(resolve => {
resolve({ schema });
});
},
});
const expected = {
testString: 'it works',
};
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testString }',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
});
});
it('throws an error if options promise is rejected', async () => {
app = await createApp({
graphqlOptions: () => {
return (Promise.reject({}) as any) as GraphQLOptions;
},
});
const expected = 'Invalid options';
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testString }',
});
return req.then(res => {
expect(res.status).to.equal(500);
expect(res.error.text).to.contain(expected);
});
});
it('rejects the request if the method is not POST or GET', async () => {
app = await createApp({ excludeParser: true });
const req = request(app)
.head('/graphql')
.send();
return req.then(res => {
expect(res.status).to.equal(405);
expect(res.headers['allow']).to.equal('GET, POST');
});
});
it('throws an error if POST body is missing', async () => {
app = await createApp({ excludeParser: true });
const req = request(app)
.post('/graphql')
.send();
return req.then(res => {
expect(res.status).to.equal(500);
expect(res.error.text).to.contain('POST body missing.');
});
});
it('throws an error if GET query is missing', async () => {
app = await createApp();
const req = request(app).get(`/graphql`);
return req.then(res => {
expect(res.status).to.equal(400);
expect(res.error.text).to.contain('GET query missing.');
});
});
it('can handle a basic GET request', async () => {
app = await createApp();
const expected = {
testString: 'it works',
};
const query = {
query: 'query test{ testString }',
};
const req = request(app)
.get('/graphql')
.query(query);
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
});
});
it('can handle a basic implicit GET request', async () => {
app = await createApp();
const expected = {
testString: 'it works',
};
const query = {
query: '{ testString }',
};
const req = request(app)
.get('/graphql')
.query(query);
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
});
});
it('throws error if trying to use mutation using GET request', async () => {
app = await createApp();
const query = {
query: 'mutation test{ testMutation(echo: "ping") }',
};
const req = request(app)
.get('/graphql')
.query(query);
return req.then(res => {
expect(res.status).to.equal(405);
expect(res.headers['allow']).to.equal('POST');
expect(res.error.text).to.contain(
'GET supports only query operation',
);
});
});
it('throws error if trying to use mutation with fragment using GET request', async () => {
app = await createApp();
const query = {
query: `fragment PersonDetails on PersonType {
firstName
}
mutation test {
testPerson(firstName: "Test", lastName: "Me") {
...PersonDetails
}
}`,
};
const req = request(app)
.get('/graphql')
.query(query);
return req.then(res => {
expect(res.status).to.equal(405);
expect(res.headers['allow']).to.equal('POST');
expect(res.error.text).to.contain(
'GET supports only query operation',
);
});
});
it('can handle a GET request with variables', async () => {
app = await createApp();
const query = {
query: 'query test($echo: String){ testArgument(echo: $echo) }',
variables: JSON.stringify({ echo: 'world' }),
};
const expected = {
testArgument: 'hello world',
};
const req = request(app)
.get('/graphql')
.query(query);
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
});
});
it('can handle a basic request', async () => {
app = await createApp();
const expected = {
testString: 'it works',
};
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testString }',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
});
});
it('can handle a basic request with cacheControl', async () => {
app = await createApp({
graphqlOptions: { schema, cacheControl: true },
});
const expected = {
testPerson: { firstName: 'Jane' },
};
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testPerson { firstName } }',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
expect(res.body.extensions).to.deep.equal({
cacheControl: {
version: 1,
hints: [{ maxAge: 0, path: ['testPerson'] }],
},
});
});
});
it('can handle a basic request with cacheControl and defaultMaxAge', async () => {
app = await createApp({
graphqlOptions: { schema, cacheControl: { defaultMaxAge: 5 } },
});
const expected = {
testPerson: { firstName: 'Jane' },
};
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testPerson { firstName } }',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
expect(res.body.extensions).to.deep.equal({
cacheControl: {
version: 1,
hints: [{ maxAge: 5, path: ['testPerson'] }],
},
});
});
});
it('returns PersistedQueryNotSupported to a GET request', async () => {
app = await createApp();
const req = request(app)
.get('/graphql')
.query({
extensions: JSON.stringify({
persistedQuery: {
version: 1,
sha256Hash:
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
},
}),
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body).to.deep.equal({
errors: [{ message: 'PersistedQueryNotSupported' }],
});
});
});
it('returns PersistedQueryNotSupported to a POST request', async () => {
app = await createApp();
const req = request(app)
.post('/graphql')
.send({
extensions: {
persistedQuery: {
version: 1,
sha256Hash:
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
},
},
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body).to.deep.equal({
errors: [{ message: 'PersistedQueryNotSupported' }],
});
});
});
it('can handle a request with variables', async () => {
app = await createApp();
const expected = {
testArgument: 'hello world',
};
const req = request(app)
.post('/graphql')
.send({
query: 'query test($echo: String){ testArgument(echo: $echo) }',
variables: { echo: 'world' },
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
});
});
it('can handle a request with variables as string', async () => {
app = await createApp();
const expected = {
testArgument: 'hello world',
};
const req = request(app)
.post('/graphql')
.send({
query: 'query test($echo: String!){ testArgument(echo: $echo) }',
variables: '{ "echo": "world" }',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
});
});
it('can handle a request with variables as an invalid string', async () => {
app = await createApp();
const req = request(app)
.post('/graphql')
.send({
query: 'query test($echo: String!){ testArgument(echo: $echo) }',
variables: '{ echo: "world" }',
});
return req.then(res => {
expect(res.status).to.equal(400);
expect(res.error.text).to.contain('Variables are invalid JSON.');
});
});
it('can handle a request with operationName', async () => {
app = await createApp();
const expected = {
testString: 'it works',
};
const req = request(app)
.post('/graphql')
.send({
query: `
query test($echo: String){ testArgument(echo: $echo) }
query test2{ testString }`,
variables: { echo: 'world' },
operationName: 'test2',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
});
});
it('can handle introspection request', async () => {
app = await createApp();
const req = request(app)
.post('/graphql')
.send({ query: introspectionQuery });
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data.__schema.types[0].fields[0].name).to.equal(
'testString',
);
});
});
it('does not accept a query AST', async () => {
app = await createApp();
const req = request(app)
.post('/graphql')
.send({
query: gql`
query test {
testString
}
`,
});
return req.then(res => {
expect(res.status).to.equal(400);
expect(res.text).to.contain('GraphQL queries must be strings');
});
});
it('can handle batch requests', async () => {
app = await createApp();
const expected = [
{
data: {
testString: 'it works',
},
},
{
data: {
testArgument: 'hello yellow',
},
},
];
const req = request(app)
.post('/graphql')
.send([
{
query: `
query test($echo: String){ testArgument(echo: $echo) }
query test2{ testString }`,
variables: { echo: 'world' },
operationName: 'test2',
},
{
query: `
query testX($echo: String){ testArgument(echo: $echo) }`,
variables: { echo: 'yellow' },
operationName: 'testX',
},
]);
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body).to.deep.equal(expected);
});
});
it('can handle batch requests', async () => {
app = await createApp();
const expected = [
{
data: {
testString: 'it works',
},
},
];
const req = request(app)
.post('/graphql')
.send([
{
query: `
query test($echo: String){ testArgument(echo: $echo) }
query test2{ testString }`,
variables: { echo: 'world' },
operationName: 'test2',
},
]);
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body).to.deep.equal(expected);
});
});
it('can handle batch requests in parallel', async function() {
// this test will fail due to timeout if running serially.
const parallels = 100;
const delayPerReq = 40;
this.timeout(3000);
app = await createApp();
const expected = Array(parallels).fill({
data: { testStringWithDelay: 'it works' },
});
const req = request(app)
.post('/graphql')
.send(
Array(parallels).fill({
query: `query test($delay: Int!) { testStringWithDelay(delay: $delay) }`,
operationName: 'test',
variables: { delay: delayPerReq },
}),
);
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body).to.deep.equal(expected);
});
});
it('clones batch context', async () => {
app = await createApp({
graphqlOptions: {
schema,
context: { testField: 'expected' },
},
});
const expected = [
{
data: {
testContext: 'expected',
},
},
{
data: {
testContext: 'expected',
},
},
];
const req = request(app)
.post('/graphql')
.send([
{
query: 'query test{ testContext }',
},
{
query: 'query test{ testContext }',
},
]);
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body).to.deep.equal(expected);
});
});
it('executes batch context if it is a function', async () => {
let callCount = 0;
app = await createApp({
graphqlOptions: {
schema,
context: () => {
callCount++;
return { testField: 'expected' };
},
},
});
const expected = [
{
data: {
testContext: 'expected',
},
},
{
data: {
testContext: 'expected',
},
},
];
const req = request(app)
.post('/graphql')
.send([
{
query: 'query test{ testContext }',
},
{
query: 'query test{ testContext }',
},
]);
return req.then(res => {
expect(callCount).to.equal(2);
expect(res.status).to.equal(200);
expect(res.body).to.deep.equal(expected);
});
});
it('can handle a request with a mutation', async () => {
app = await createApp();
const expected = {
testMutation: 'not really a mutation, but who cares: world',
};
const req = request(app)
.post('/graphql')
.send({
query: 'mutation test($echo: String){ testMutation(echo: $echo) }',
variables: { echo: 'world' },
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
});
});
it('applies the formatResponse function', async () => {
app = await createApp({
graphqlOptions: {
schema,
formatResponse(response) {
response['extensions'] = { it: 'works' };
return response;
},
},
});
const expected = { it: 'works' };
const req = request(app)
.post('/graphql')
.send({
query: 'mutation test($echo: String){ testMutation(echo: $echo) }',
variables: { echo: 'world' },
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.extensions).to.deep.equal(expected);
});
});
it('passes the context to the resolver', async () => {
const expected = 'context works';
app = await createApp({
graphqlOptions: {
schema,
context: { testField: expected },
},
});
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testContext }',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data.testContext).to.equal(expected);
});
});
it('passes the rootValue to the resolver', async () => {
const expected = 'it passes rootValue';
app = await createApp({
graphqlOptions: {
schema,
rootValue: expected,
},
});
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testRootValue }',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data.testRootValue).to.equal(expected);
});
});
it('returns errors', async () => {
const expected = 'Secret error message';
app = await createApp({
graphqlOptions: {
schema,
},
});
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testError }',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.errors[0].message).to.equal(expected);
});
});
it('applies formatError if provided', async () => {
const expected = '--blank--';
app = await createApp({
graphqlOptions: {
schema,
formatError: () => ({ message: expected }),
},
});
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testError }',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.errors[0].message).to.equal(expected);
});
});
it('sends internal server error when formatError fails', async () => {
app = await createApp({
graphqlOptions: {
schema,
formatError: () => {
throw new Error('I should be caught');
},
},
});
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testError }',
});
return req.then(res => {
expect(res.body.errors[0].message).to.equal('Internal server error');
});
});
it('sends stack trace to error if debug mode is set', async () => {
const expected = /at resolveFieldValueOrError/;
const origError = console.error;
const err = stub();
console.error = err;
app = await createApp({
graphqlOptions: {
schema,
debug: true,
},
});
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testError }',
});
return req.then(() => {
console.error = origError;
if (err.called) {
expect(err.calledOnce);
expect(err.getCall(0).args[0]).to.match(expected);
}
});
});
it('sends stack trace to error log if debug mode is set', async () => {
const logStub = stub(console, 'error');
const expected = /at resolveFieldValueOrError/;
app = await createApp({
graphqlOptions: {
schema,
debug: true,
},
});
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testError }',
});
return req.then(() => {
logStub.restore();
if (logStub.called) {
expect(logStub.callCount).to.equal(1);
expect(logStub.getCall(0).args[0]).to.match(expected);
}
});
});
it('applies additional validationRules', async () => {
const expected = 'alwaysInvalidRule was really invalid!';
const alwaysInvalidRule = function(context) {
return {
enter() {
context.reportError(new GraphQLError(expected));
return BREAK;
},
};
};
app = await createApp({
graphqlOptions: {
schema,
validationRules: [alwaysInvalidRule],
},
});
const req = request(app)
.post('/graphql')
.send({
query: 'query test{ testString }',
});
return req.then(res => {
expect(res.status).to.equal(400);
expect(res.body.errors[0].message).to.equal(expected);
});
});
});
describe('renderGraphiQL', () => {
it('presents GraphiQL when accepting HTML', async () => {
app = await createApp({
graphiqlOptions: {
endpointURL: '/graphql',
},
});
const req = request(app)
.get('/graphiql')
.query('query={test}')
.set('Accept', 'text/html');
return req.then(response => {
expect(response.status).to.equal(200);
expect(response.type).to.equal('text/html');
expect(response.text).to.include('{test}');
expect(response.text).to.include('/graphql');
expect(response.text).to.include('graphiql.min.js');
});
});
it('allows options to be a function', async () => {
app = await createApp({
graphiqlOptions: () => ({
endpointURL: '/graphql',
}),
});
const req = request(app)
.get('/graphiql')
.set('Accept', 'text/html');
return req.then(response => {
expect(response.status).to.equal(200);
});
});
it('handles options function errors', async () => {
app = await createApp({
graphiqlOptions: () => {
throw new Error('I should be caught');
},
});
const req = request(app)
.get('/graphiql')
.set('Accept', 'text/html');
return req.then(response => {
expect(response.status).to.equal(500);
});
});
it('presents options variables', async () => {
app = await createApp({
graphiqlOptions: {
endpointURL: '/graphql',
variables: { key: 'optionsValue' },
},
});
const req = request(app)
.get('/graphiql')
.set('Accept', 'text/html');
return req.then(response => {
expect(response.status).to.equal(200);
expect(response.text.replace(/\s/g, '')).to.include(
'variables:"{\\n\\"key\\":\\"optionsValue\\"\\n}"',
);
});
});
it('presents query variables over options variables', async () => {
app = await createApp({
graphiqlOptions: {
endpointURL: '/graphql',
variables: { key: 'optionsValue' },
},
});
const req = request(app)
.get('/graphiql?variables={"key":"queryValue"}')
.set('Accept', 'text/html');
return req.then(response => {
expect(response.status).to.equal(200);
expect(response.text.replace(/\s/g, '')).to.include(
'variables:"{\\n\\"key\\":\\"queryValue\\"\\n}"',
);
});
});
});
describe('stored queries', () => {
it('works with formatParams', async () => {
const store = new OperationStore(schema);
store.put('query testquery{ testString }');
app = await createApp({
graphqlOptions: {
schema,
formatParams(params) {
params['parsedQuery'] = store.get(params.operationName);
delete params['queryString'];
return params;
},
},
});
const expected = { testString: 'it works' };
const req = request(app)
.post('/graphql')
.send({
operationName: 'testquery',
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.data).to.deep.equal(expected);
});
});
it('can reject non-whitelisted queries', async () => {
const store = new OperationStore(schema);
store.put('query testquery{ testString }');
app = await createApp({
graphqlOptions: {
schema,
formatParams(params) {
if (params.queryString) {
throw new Error('Must not provide query, only operationName');
}
params['parsedQuery'] = store.get(params.operationName);
return params;
},
},
});
const expected = [
{
data: {
testString: 'it works',
},
},
{
errors: [
{
message: 'Must not provide query, only operationName',
},
],
},
];
const req = request(app)
.post('/graphql')
.send([
{
operationName: 'testquery',
},
{
query: '{ testString }',
},
]);
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body.length).to.equal(expected.length);
expect(res.body[0]).to.deep.equal(expected[0]);
if (res.body[1].errors[0].extensions) {
if (res.body[1].errors[0].extensions.code) {
expect(res.body[1].errors[0].extensions.code).to.equal(
'INTERNAL_SERVER_ERROR',
);
}
delete res.body[1].errors[0].extensions;
}
expect(res.body[1]).to.deep.equal(expected[1]);
});
});
});
describe('server setup', () => {
it('throws error on 404 routes', async () => {
app = await createApp();
const query = {
query: '{ testString }',
};
const req = request(app)
.get('/bogus-route')
.query(query);
return req.then(res => {
expect(res.status).to.equal(404);
});
});
});
});
};