CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/mercurius-tests

Wraps Mercurius (Fastify GraphQL plugin) testing patterns: `app.inject()` for HTTP-layer simulation without spinning up a network listener, plugin-registration setup (await app.register(mercurius, { schema, resolvers, graphiql: false })), production-config gates (graphiql: false; jit threshold; query depth limits via fastify-rate-limit + complexity), and the per-test app lifecycle (app.close() in afterEach). Use when writing tests for a Fastify + Mercurius GraphQL server.

80

Quality

100%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

SKILL.md

name:
mercurius-tests
description:
Wraps Mercurius (Fastify GraphQL plugin) testing patterns: `app.inject()` for HTTP-layer simulation without spinning up a network listener, plugin-registration setup (await app.register(mercurius, { schema, resolvers, graphiql: false })), production-config gates (graphiql: false; jit threshold; query depth limits via fastify-rate-limit + complexity), and the per-test app lifecycle (app.close() in afterEach). Use when writing tests for a Fastify + Mercurius GraphQL server.

mercurius-tests

Overview

Per github.com/mercurius-js/mercurius, the plugin is registered as await app.register(mercurius, { schema, resolvers }). Tests then submit POSTs via Fastify's app.inject() - the HTTP-layer simulator that runs requests through the full middleware stack without binding a port.

When to use

  • Unit / integration tests for a Mercurius-based GraphQL server.
  • Production-config gates: graphiql disabled, depth limits.
  • Fastify-specific middleware behaviour (auth, rate-limit, CORS).

Authoring

Install

npm install --save-dev fastify mercurius

Basic test

import Fastify from 'fastify';
import mercurius from 'mercurius';

function buildApp() {
  const app = Fastify();
  app.register(mercurius, {
    schema: `type Query { add(x: Int, y: Int): Int }`,
    resolvers: {
      Query: {
        add: async (_, { x, y }) => x + y,
      },
    },
    graphiql: false,  // disable GraphiQL UI in tests
  });
  return app;
}

test('add', async () => {
  const app = buildApp();
  const response = await app.inject({
    method: 'POST',
    url: '/graphql',
    payload: { query: '{ add(x: 2, y: 3) }' },
  });
  expect(response.statusCode).toBe(200);
  const body = JSON.parse(response.body);
  expect(body.data.add).toBe(5);
  await app.close();
});

Per the README's quickstart pattern, plugin registration takes schema + resolvers. The test pattern is inject then close.

Auth header tests

const response = await app.inject({
  method: 'POST',
  url: '/graphql',
  headers: { authorization: `Bearer ${testToken}` },
  payload: { query: '{ me { id } }' },
});

Headers go through Fastify's middleware (e.g., fastify-jwt) exactly as in production.

Per-test app lifecycle

let app: ReturnType<typeof buildApp>;

beforeEach(() => { app = buildApp(); });
afterEach(async () => { await app.close(); });

Per Fastify convention: rebuild + close per test to avoid plugin state contamination.

Running

npm test

Production-config tests

Per introspection-attack-surface-reference:

test('graphiql disabled in production config', async () => {
  const app = Fastify();
  app.register(mercurius, {
    schema, resolvers,
    graphiql: false,
  });
  const resp = await app.inject({ method: 'GET', url: '/graphiql' });
  expect(resp.statusCode).toBe(404);
  await app.close();
});

test('introspection disabled', async () => {
  // Mercurius doesn't have a direct introspection flag; use
  // mercurius-validation or a custom validator that rejects
  // introspection AST nodes
  const app = Fastify();
  app.register(mercurius, {
    schema, resolvers,
    graphiql: false,
    validationRules: [rejectIntrospectionRule],
  });
  const resp = await app.inject({
    method: 'POST',
    url: '/graphql',
    payload: { query: '{ __schema { types { name } } }' },
  });
  const body = JSON.parse(resp.body);
  expect(body.errors).toBeDefined();
  await app.close();
});

Query-complexity / depth limiting

Use graphql-validation-complexity or graphql-depth-limit as a validation rule passed via Mercurius config:

import depthLimit from 'graphql-depth-limit';

app.register(mercurius, {
  schema, resolvers,
  graphiql: false,
  validationRules: [depthLimit(5)],
});

Test:

test('depth limit', async () => {
  const deep = '{ user { friends { friends { friends { friends { friends { id }}}}}}}';
  const resp = await app.inject({ method: 'POST', url: '/graphql', payload: { query: deep } });
  const body = JSON.parse(resp.body);
  expect(body.errors[0].message).toMatch(/maximum operation depth/i);
});

Parsing results

app.inject() returns a Light My Request response:

{
  statusCode: 200,
  body: '{"data":...}',   // string, not parsed
  headers: { ... },
  payload: '{"data":...}', // alias for body
}

Always JSON.parse(response.body) for GraphQL responses.

The GraphQL response shape is standard:

{
  "data": { ... },
  "errors": [{ "message": "...", "path": [...], "extensions": {...} }]
}

CI integration

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm test

For multi-process / cluster-mode tests, Fastify's inject won't exercise cluster behaviour - use a real app.listen({ port: 0 }) for those cases.

Anti-patterns

Anti-patternWhy it failsFix
graphiql: true in production code pathSchema disclosure via GraphiQLAlways graphiql: false in prod; gate per env
Sharing one app across testsPlugin state leaks; mutations persistPer-test buildApp + close
Skipping await app.close()Fastify keep-alive timers leak; CI hangsAlways close
app.inject with body fieldShould be payload; Fastify ignores body hereUse payload
Asserting on response.payload for non-JSON contentBuffer / stream responses need different handlingInspect headers['content-type'] first
No depth / complexity validationMercurius doesn't add these by defaultAdd validationRules per introspection-attack-surface-reference
Testing on real listening portSlower; flaky parallel CIapp.inject is purpose-built
Mocking the schema instead of using itTests pass against a fake schema, not the real oneAlways test the real schema

Limitations

  • No subscription support in inject. Mercurius subscriptions use WebSockets; tests need a real listen + ws client.
  • JIT compilation off in test. Mercurius supports JIT via jit: 1 (compile after 1 invocation); the prod JIT path may optimise differently than tests.
  • No introspection-disable flag. Have to roll custom validation rules; not as ergonomic as Apollo's introspection: false.
  • Fastify plugin order matters. auth plugin must register before mercurius; test setup must match prod order or auth doesn't apply.

References

SKILL.md

tile.json