-
Notifications
You must be signed in to change notification settings - Fork 2k
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
apollo-server-testing context is not receiving the req
object
#2277
Comments
Experiencing this and it's making it impossible to write proper integration tests for logged in users etc., Edit: well, from the examples it seems like you are supposed to basically return a fixture from the server instance context, but I don't think this is ideal. I want to be able to test how my app integrates with apollo server, how can this be done properly if the behaviour of the apollo server when writing tests is different to what it will be like in production? |
…ation Fixes apollographql#2277. `apollo-server-testing` relies on `apollo-server-core` to execute queries for integration tests. However, `apollo-server-core` was not passing the `req` object to the `context` callback function used to create the server. This is fine if you're using a mock `context` object directly in your tests, but for tests that want to run a `context` callback that contains logic that depends on that `req` object, it would fail. Note that long-term the best fix for this is that `apollo-server-testing` should use the `ApolloServer` class from `apollo-server-express` internally, since it seems like that is the class that is used by default in `apollo-server`.
…ation Fixes apollographql#2277. `apollo-server-testing` relies on `apollo-server-core` to execute queries for integration tests, via the `executeOperation` method. However, when executing a query via `executeOperation`, `apollo-server-core` was not passing the `req` object to the `context` callback function used to create the server. This is fine if you're using a mock `context` object directly in your tests, but for tests that want to run a `context` callback that contains logic that depends on that `req` object, it would fail. Note that long-term the best fix for this is that `apollo-server-testing` should use the `ApolloServer` class from `apollo-server-express` internally, instead of `ApolloServerBase` from `apollo-server-core`, since it seems like that is the class that is used by default in `apollo-server`.
…ation Fixes apollographql#2277. `apollo-server-testing` relies on `apollo-server-core` to execute queries for integration tests, via the `executeOperation` method. However, when executing a query via `executeOperation`, `apollo-server-core` was not passing the `req` object to the `context` callback function used to create the server. This is fine if you're using a mock `context` object directly in your tests, but for tests that want to run a `context` callback that contains logic that depends on that `req` object, it would fail. Note that long-term the best fix for this is that `apollo-server-testing` should use the `ApolloServer` class from `apollo-server-express` internally, instead of `ApolloServerBase` from `apollo-server-core`, since it seems like that is the class that is used by default in `apollo-server`.
I just ran into this. I don't consider it a true integration test unless you can actually test how your server's true context is constructed. To hide behind a mock context doesn't actually test anything. |
I've been facing a similar issue. I've implemented a token-based authentication.
|
i am using workaround based on
|
here are two solutions i came up with: if you are comfortable "polluting" the prototype const { ApolloServer } = require("apollo-server-X"); // import from your flavor
const serverConfig = require("../api/config");
// server config is the object you pass into the ApolloServer constructor
// { resolvers, typeDefs, schemaDirectives, context, ... }
// execute the context function to get the base context object
// optionally you can add a default req or res in this step
const baseContext = serverConfig.context({});
// use 1 or more of the following functions as needed
ApolloServer.prototype.setContext = function setContext(newContext) {
this.context = newContext;
}
ApolloServer.prototype.mergeContext = function mergeContext(partialContext) {
this.context = Object.assign({}, this.context, partialContext);
}
ApolloServer.prototype.resetContext = function resetContext() {
this.context = baseContext;
}
module.exports = {
testServer: new ApolloServer({
...serverConfig,
context: baseContext,
}),
baseContext, // easy access in tests
} "cleaner" solution with a subclass const { ApolloServer } = require("apollo-server-X"); // import from your flavor
const serverConfig = require("../api/config");
// server config is the object you pass into the ApolloServer constructor
// { resolvers, typeDefs, schemaDirectives, context, ... }
// execute the context function to get the base context object
// optionally you can add a default req or res in this step
const baseContext = serverConfig.context({});
// create a test server subclass with the methods built in
class ApolloTestServer extends ApolloServer {
constructor(config) {
super(config);
this.context = baseContext;
}
setContext(newContext) {
this.context = newContext;
}
mergeContext(partialContext) {
this.context = Object.assign({}, this.context, partialContext);
}
resetContext() {
this.context = baseContext;
}
}
module.exports = {
baseContext,
testServer: new ApolloTestServer(serverConfig),
}; usage for either approach const { createTestClient } = require("apollo-server-testing");
const { testServer, baseContext } = require("./test-utils/test-server");
const { query, mutate } = createTestClient(testServer);
test("something", async () => {
// set / reset / merge the context as needed before calling query or mutate
testServer.mergeContext({
req: { headers: { Authorization: `Bearer ${token}` } },
});
const res = await query({ query, variables });
expect(res)...
}); |
in typescript, context is private 🤕 |
I tried some of the solutions listed here and none of them worked for me. I ended up creating a new package that mimics the https://github.com/zapier/apollo-server-integration-testing We've been using it successfully at my company for the last 6 months to write real integration tests. Posting it here in case anyone else is interested in giving it a try :) |
I think you could put a spy around the context factory and inject a request object that way. I've been tinkering a bit with it and I'll post a gist if I get it to work. |
Here is the gist of the approach I mentioned yesterday. This is a module that creates and exports a singleton test client. This assumes you're using Jest, and that your context factory is in its own module. I think the paths are self explanatory but feel free to ask if anything isn't clear. You could probably modify this to be a test client builder rather than a singleton which would be a little safer so you don't have to worry about maintaining token state between tests.
In case the context factory isn't clear, the idea is that your ApolloServer is instantiated like so:
... and the context.js file is like:
Hope this helps someone. |
Apologies for recommending other package, but it is from the same ecosystem, not the competing one. I hope it is not displeasing the authors. Since I could not find any pleasant solution, I have ended up writing tests using import ApolloClient, { gql } from "apollo-boost"
import fetch from "node-fetch"
test("graphql response with auth header", async () => {
const uri = "http://localhost:4000/graphql"
const client = new ApolloClient({
uri,
fetch,
request: operation => {
operation.setContext({
headers: {
authorization: "Bearer <token>",
},
})
},
})
const queryResponse = await client.query({ query: gql`query{ ... } ` })
const mutationResponse = await client.mutate({ mutation: gql`mutation{ ... }` })
expect(queryResponse.data).toBe("expected-qeury-data")
expect(mutationResponse.data).toBe("expected-mutation-data")
}) I am not really sure if this is still called "integration testing" and not "e2e" but it works really for me. Thought this might come handy to someone still struggling. |
I found a much simpler solution to set the initial context arguments. What it does is that it wraps the context function with another function that "injects" the context argument: const { ApolloServer } = require('apollo-server') // Or `apollo-server-express`
const { createTestClient } = require('apollo-server-testing')
/**
* Simple test client with custom context argument
* @param config Apollo Server config object
* @param ctxArg Argument object to be passed
*/
const testClient = (config, ctxArg) => {
return createTestClient(new ApolloServer({
...config,
context: () => config.context(ctxArg)
}))
} Usage: const { query, mutate } = testClient(config, { req: { headers: { authorization: '<token>' } } })
// Use as usual
query({
query: GET_USER,
variables: { id: 1 }
}) If you need to set custom context arguments per query or mutate, it can be further extended to be something like this: const { ApolloServer } = require('apollo-server') // Or `apollo-server-express`
const { createTestClient } = require('apollo-server-testing')
/**
* Test client with custom context argument that can be set per query or mutate call
* @param config Apollo Server config object
* @param ctxArg Default argument object to be passed
*/
const testClient = (config, ctxArg) => {
const baseCtxArg = ctxArg
let currentCtxArg = baseCtxArg
const { query, mutate, ...others } = createTestClient(new ApolloServer({
...config,
context: () => config.context(currentCtxArg)
}))
// Wraps query and mutate function to set context arguments
const wrap = fn => ({ ctxArg, ...args }) => {
currentCtxArg = ctxArg != null ? ctxArg : baseCtxArg
return fn(args)
}
return { query: wrap(query), mutate: wrap(mutate), ...others }
} Usage: const { query, mutate } = testClient(config, { req: { headers: { authorization: '<token>' } } })
// Set context argument per query or mutate
query({
query: GET_USER,
variables: { id: 1 },
ctxArg: { req: { headers: { authorization: '<new-token>' } } }
}) Hope this helps :) |
I know there are many solutions here already, but for some reason they weren't sitting well with me. I made a very thin wrapper around import { createTestClient } from 'apollo-server-testing'
import { ApolloServer } from 'apollo-server'
// This is a simple wrapper around apollo-server-testing's createTestClient.
// A massive shortcoming of that code is that the ctx object gets passed in
// to the context function as an empty object. So, critically, we can't test
// authorization headers, which is gonna be like all the requests we do in our
// tests. This wrapper allows some headers to be passed in. See:
// https://github.com/apollographql/apollo-server/issues/2277
export default function (server: ApolloServer, headers = {} as any) {
// @ts-ignore B/c context is marked as private.
const oldContext = server.context
const context = ({ req, res }) => {
return oldContext({ res, req: { ...req, headers }})
}
const serverWithHeaderContext = Object.assign({}, server, { context })
// @ts-ignore -- Typescript doesn't know about __proto__, huh...
serverWithHeaderContext.__proto__ = server.__proto__
return createTestClient(serverWithHeaderContext)
} And instead of |
I found a solution through supertest // create-app.ts
import { ApolloServer } from 'apollo-server-express'
import { config as configEnv } from 'dotenv'
import express from 'express'
import 'reflect-metadata'
import { createSchema } from './create-shema'
import { getContext } from './get-context'
configEnv()
export async function createApp() {
const server = new ApolloServer({
schema: await createSchema(),
context: getContext,
})
const app = express()
server.applyMiddleware({ app })
return { server, app }
} // query test
test('get auth user', async () => {
const { app } = await createApp()
const [userData] = fakeUsers
const user = await usersService.findUser({ email: userData.email })
const token = authService.createToken(user!.id)
const meQuery = `
{
me {
id
name
email
passwordHash
}
}
`
const result = await makeQuery({ app, query: meQuery, token })
expect(result.errors).toBeUndefined()
expect(result.data).toBeDefined()
expect(result.data).toHaveProperty('me', {
id: user!.id.toString(),
name: user!.name,
email: user!.email,
passwordHash: expect.any(String),
})
}) // make-query
import { Express } from 'express'
import supertest from 'supertest'
type MakeQuery = {
app: Express
query: string
token?: string
variables?: object
}
export async function makeQuery({ token, query, app, variables }: MakeQuery) {
const headers: { Authorization?: string } = {}
if (token) {
headers.Authorization = `Bearer ${token}`
}
const { body } = await supertest(app)
.post('/graphql')
.send({ query, variables })
.set(headers)
return body
} |
I've tried to implement your example but I'm getting the error: |
@KristianWEB You're instantiating a different server than the one in Also, you shouldn't be passing an ApolloServer instance to |
@BjornLuG What should the Edit: I see what you meant: adding the resolvers and the schema. Now the apollo server doesnt recognize the context ( const { mutate, query } = testClient({
resolvers,
typeDefs,
}); When I try to add the context to the testClient config in this way: const { mutate, query } = testClient({
resolvers,
typeDefs,
context: () => ({}),
}); It throws me the headers of undefined error again. I've tried this as well: const { mutate, query } = testClient({
resolvers,
typeDefs,
context: async ({ req }) => ({ req }),
}); But with no success I'm still receiving How do I instantiate the |
have a look at the solution i posted originally. it works #2277 (comment) |
Unfortunately, I had to resort to using a graphql client created from |
Found a solution to still test production code, but mock the req // createApolloServer.js
// authorizationStore is anything that could add fields to req
export const createContext = (authorizationStore) => ({ req }) => {
authorizationStore.setProfileId(req); // of course in tests this won't work because req is null
return {
userId: req.profile.id
};
};
export const createApolloServer (authorizationStore, context = createContext) => {
const apolloServer = new ApolloServer({
schema,
context: context(authorizationStore)
});
return apolloServer;
}; in your main production code: import { createApolloServer } from './createApolloServer';
// const authenticationStore = up to you
const apolloServer = createApolloServer(authenticationStore) and in your test: import { createApolloServer, createContext } from './createApolloServer';
it('should work ', async () => {
const authenticationStore = {
set: jest.fn()
};
const mockCreateContext = () => {
const req = {
profile: {
id: 123
}
};
return createContext(authenticationStore)({ req });
};
const apolloServer = createApolloServer(authenticationStore, mockCreateContext);
const { query } = createTestClient(apolloServer);
// etc.
}); |
Hi, is there a proper solution atm? It is crazy that there is no supported way to pass custom headers to the req. |
@FrancescoSaverioZuppichini I've implemented a custom testClient. Check it out |
Nice, thank you @KristianWEB |
@KristianWEB this has also helped us with our ApolloServer testing. Thank you! |
The following code worked for me. import { ApolloServer } from "apollo-server"
import { createTestClient } from "apollo-server-testing"
const createTestServer = () => {
const server = new ApolloServer({
// ...
});
return server;
};
test('auth', async () => {
const server = createTestServer();
const { mutate } = createTestClient(server);
server.context = () => {
const req = {
headers: {
"authorization": `accessToken ${ accessToken }`
}
}
return { req }
};
response = await mutate({
mutation: MUTATION_1,
variables: { var1, var2 }
});
} |
So, for those like me who came here looking for how to do this who use TS and don't use express, I ended up writing my own stub for the context function. It's really not ideal and I'm still beyond shocked that something so basic isn't handled, but you gotta work with what you've got, so here we are: // wherever_you_are_creating_your_apollo_server.ts
export interface Context {
currentUser?: IUser;
}
export async function createApolloServer(
findUserContext?: (req: any) => Promise<Context>
) {
const schema = await buildSchema({ ... });
const contextToUse = findUserContext || validateUser;
const apolloServer = new ApolloServer({
schema,
...
context: async ({ req }) => {
return await contextToUse(req);
},
});
return apolloServer;
}
export const validateUser = async (req: any) => {
return findUserWithRequest(req);
};
export const findUserWithRequest = async (req: any): Promise<Context> => {
// Find your user here
const headers = req?.headers["cookie/auth/token/whatever_youre_using"]
const currentUser = User.findFromHeaders(headers)
return { currentUser }
} // my_handy_spec_helper.ts
import { createApolloServer, findUserWithRequest } from '~/server
export async function setupAuthTestApolloClient(token: string) {
const apolloServer = await createApolloServer(findUserForToken(token));
const { query, mutate } = createTestClient(apolloServer);
return {
query,
mutate,
};
}
const findUserForToken = (token: String) => async (req: any) => {
const headers = req?.headers || [];
// construct your stubbed headers here
headers['token'] = token
const stubbedReq = {
...req,
headers: headers,
};
return findUserWithRequest(stubbedReq);
}; // my_important_spec.spec.ts
import {
setupTestDBConnection,
setupTestApolloClient,
} from "../../utils/helpers";
describe("User queries", () => {
it("can fetch the currently logged in user", async () => {
const connection = await setupTestDBConnection();
const email = "a_random_email@gmail.com.au.net";
const password = await hashPassword("battery_horse_staple");
await createUser({ email, password });
const token = generateJwtToken({ email });
const { query } = await setupAuthTestApolloClient(token);
try {
const res = await query({
query: GET_ME,
});
const loggedInUser: IUser = res?.data?.me;
expect(loggedInUser.email).toEqual(email);
} finally {
connection.close();
}
});
}); With this setup you can |
Are there any plans to solve this as part of the official testing package? As others have already pointed out, not being able to mock out the |
Similar to @aaronik's solution (#2277 (comment)), this works fine for my use-case. I'm using type-graphql and have a custom auth guard that handles jwt verification. createApolloTestServer.ts import { ApolloServer } from "apollo-server-micro";
import { createSchema } from "pages/api/graphql";
export const createApolloTestServer = async (headers = {} as any) => {
const schema = await createSchema();
const server = new ApolloServer({
schema,
debug: true,
context: ({ req, res }) => {
return {
res,
req: {
...req,
headers
}
};
}
});
return server;
}; testSomthing.spec.ts import { createTestClient } from "apollo-server-testing";
import { createApolloTestServer } from "test-utils/createApolloTestServer";
beforeAll(async () => {
apolloServer = await createApolloTestServer({
authorization: `Bearer ${access_token}`
});
});
it("does something", async () => {
const { mutate } = createTestClient(apolloServer);
// req: { headers: { authorization: "..." } } will be accessible in the resolver
const response = await mutate({
mutation: GQL_MUTATION,
variables: {
...mutationVariables
}
});
}); |
We are still experiencing this issue. |
1 similar comment
We are still experiencing this issue. |
Amalgamated the work by @vitorbal, the Zapier Team Apollo-Server-Integration-Testing, and the Apollo Server Testing Library Here's some of the changes that have been made
https://gist.github.com/preetjdp/178643c5854ae775b005834be6687edc |
Hello, @preetjdp! Last few days I try to update dependencies for apollo-server. I have some problems with integration tests. And I found your solution and try. I think that line 142 is incorrect ...args.variables, If I start test with query: const res = await query({
query: SOME_QUERY,
variables: { ids: [-3, -2] },
headers: {
authorization: undefined,
},
}) I have the incorrect error: HttpQueryError: {"errors":[{"message":"Variable \"$ids\" of required type \"[Int]!\" was not provided.","locations":[{"line":1,"column":18}],"extensions":{"code":"INTERNAL_SERVER_ERROR"}}]} But if I change line 142 on (I'm forked your gist https://gist.github.com/dipiash/bf69150518baf8ddb9b0e136fdf3c9d0#file-createtestclient-ts-L142): variables: args.variables, I have correct result. |
Hey friends, maybe I don't see the problem, but what stops you from creating a import { JWT_SECRET } from './config.js'
import jwt from 'jsonwebtoken'
// export your context function from a file and import it in your tests
export default function ({ req }) {
let token = req.headers.authorization || ''
token = token.replace('Bearer ', '')
try {
return jwt.verify(token, JWT_SECRET)
} catch (e) {
return {}
}
} You can construct a fake Of course it would be nice to import a constructor from import { createTestClient } from 'apollo-server-testing'
import { GraphQLError } from 'graphql'
import schema from '../schema'
import { ApolloServer, gql } from 'apollo-server'
import context from '../context' // this is your context
let reqMock
let resMock
const contextPipeline = () => context({ req: reqMock, res: resMock })
const server = new ApolloServer({ schema, context: contextPipeline })
beforeEach(() => {
// For me a mock `express.Request` containing some headers is enough.
// I assume typescript will complain here:
reqMock = { headers: {} }
resMock = {}
})
describe('mutations', () => {
describe('write', () => {
const opts = {
mutation: gql`
mutation($postInput: PostInput!) {
write(post: $postInput) {
id
title
author {
id
}
}
}
`,
variables: { postInput: { title: 'New post' } }
}
describe('unauthenticated', () => {
beforeEach(() => {
reqMock = { headers: {} }
})
it('throws authorization error', async () => {
await expect(mutate(opts)).resolves.toMatchObject({
data: { write: null },
errors: [
new GraphQLError('Not Authorised!')
]
})
})
})
describe('authenticated', () => {
beforeEach(() => {
reqMock = {
headers: {
authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImFsaWNlIiwiaWF0IjoxNjA2OTQyMzc4fQ.RSXUAq7IVrxb3WeIMJ3pBmszjJzmFBP97h-pINbi3sc'
}
}
})
it('returns created post', async () => {
await expect(mutate(opts)).resolves.toMatchObject({
errors: undefined,
data: {
write: {
id: expect.any(String),
title: 'New post',
author: { id: 'alice' }
}
}
})
})
})
})
}) |
Any plans to implement this? I'm also experiencing testing limitations due to this issue |
We're currently figuring out what parts of Apollo Server are really essential and which could be pared down in the upcoming AS3. My instincts are that I don't think it's likely that we'll change the existing |
You can't really write real integration tests with apollo-server-testing, because it doesn't support servers which rely on the context option being a function that uses the req object despite this functionality being supported by the real apollo server. The official integration example code from Apollo solves this by instantiating an ApolloServer inside the test and mocking the context value by hand. But I don't consider this a real integration test, since you're not using the same instantiation code that your production code uses. * description of issue with apollo-server-testing: apollographql/apollo-server#2277 * workaround: https://github.com/apollographql/fullstack-tutorial/blob/6988f6948668ccc2dea3f7a216dd44bdf25a0b9f/final/server/src/__tests__/integration.js#L68-L74
Previously, the apollo-server-testing was used for the tests. This had key limitations, such as non-access to context without a low-confidence workaround (see apollographql/apollo-server#2277 fof more info). This limitation also made creating some tests, such as ?should return current user if logged in" impossible. To resolve, migrate to https://github.com/zapier/apollo-server-integration-testing and create the tests that were previously impossible. * Update queries-mutations constant with queries/mutations for new tests * refactor the user.test file to remove old approach * add missing test coverage
…pendency https://github.com/zapier/apollo-server-integration-testing since it allows for high confidence testing. The problems with the apollo-testing can be found here: apollographql/apollo-server#2277 and are commented in other commit messages as well.
I am trying to write tests for header handling. I'm setting up the tests as follows:
The test relies on the
req
object being passed to thecontext
function in the server setup, but I'm only getting an empty object. Is this the expected behavior? If so, how do you go about writing tests for authentication headers etc?Thanks!
The text was updated successfully, but these errors were encountered: