This is an extension to my article on the codecentric blog about how to secure a GraphQL service using persisted queries. It will handle testing the resulting service with jest. You will find the link to the complete code in the summary at the end of this article.
We will spin up a plain express server and only include the
GraphqlRequestHandler
.
let url;
let server;
beforeAll(() => {
const app = express();
app.use(express.json());
app.post("/graphql", new GraphqlRequestHandler([getMockQuery()]));
server = app.listen(0);
url = `http://localhost:${server.address().port}/graphql`;
})
While this seems straight forward,
there is one pitfall we need to be aware of. In the code example
above you can see the we pass the mocked query directly to the
constructor of the GraphqlRequestHandler
. This is needed as
the application loads graphql queries using
require.context(...)
. This webpack functionality is not
available when executing tests with jest. Therefore I moved the call for
this method to a separate file which I neither import in the test nor in
the handler under test.
This also gives us the opportunity to provide a specific query, the MOCK_QUERY to our tests.
const MOCK_QUERY = require("./mockQuery.graphql");
function getMockQuery() {
return {
...MOCK_QUERY,
documentId: "my-dummy-hash",
}
}
# mockQuery.graphql
query getMockContinents {
continents {
name
code
}
}
Do not forget to close the server after running the tests.
afterAll(() => {
server.close();
})
Now that the setup is complete, we will write our first test case. We will send the hash of the query to the service and expect the real result.
test("should send correct graphql query by its hash and return the result", async () => {
const mock = mockExternalQueryForContinents();
const response = await sendGraphqlRequest(getMockQuery(), {
zone: "universe",
});
expect(response.data).toEqual(MOCKED_CONTINENTS.data);
expect(mock.isDone()).toEqual(true);
})
To get stable test results, we need to avoid calling external services.
Therefore we create a mock for the real graphql service using
nock. This mock only
responds if the correct parameters are set and thus verifies that the
external service is called correctly.
function mockExternalQueryForContinents() {
return nock("https://countries.trevorblades.com")
.post("/", body => {
return (
body.operationName === "getMockContinents" &&
body.variables.zone === "universe"
);
})
.reply(200, MOCKED_CONTINENTS);
}
The mocked return data is stored in a local variable called MOCKED_CONTINENTS.
const MOCKED_CONTINENTS = {
data: {
continents: [{ name: "Universe", code: "U", __typename: "Continent" }]
}
};
Then we can send the graphql request, which contains basically just the hash of a query. The hash is generated by the apollo client library using the same mock query as the handler under test. You might recognize this code as it is similar to the client side of the original blog article.
async function sendGraphqlRequest(query, variables) {
const httpLink = createHttpLink({
uri: url,
fetch,
});
const automaticPersistedQueryLink = createPersistedQueryLink({
generateHash: ({ documentId }) => documentId,
});
const apolloClient = new ApolloClient({
link: ApolloLink.from([automaticPersistedQueryLink, httpLink]),
cache: new InMemoryCache(),
});
return await apolloClient.query({
query,
variables,
});
}
Back to the last two lines of our test case. We verify that the response contains the requested data and that the mock for the external service is actually called.
We already verified that our handler works, great! Now it is time to ensure that direct queries without known and thereby allowed hashes are blocked.
test("should not allow to send graphql queries without allowed hash", async () => {
const mock = mockAllExternalQueries();
const response = await sendPlainGrahqlQuery(MOCK_QUERY);
expect(response.status).toEqual(400);
expect(mock.isDone()).toEqual(false);
})
In this test case we do not want to watch for a specific call to the mocked external service. We want to ensure that the external service is not called at all. Therefore we use a slightly different mock than before.
function mockAllExternalQueries() {
return nock("https://countries.trevorblades.com")
.post("/")
.reply(200, MOCKED_CONTINENTS);
}
The request is also slightly different. This time we do not send the hash, we send the whole query.
async function sendPlainGrahqlQuery(query) {
return await fetch(url, {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: print(query) })
});
}
Finally we can expect that the response for this call contains the status code 400 and not the result of the query. We also expect that the mock to the external service did not get called.
That's it. The basic functionality is now covered with tests. Of course you can add further test cases if you want to. The test setup and principles which are used here will most likely help you with that. You will find a repository with the whole example and test implementation here on GitHub.