# GraphQL

> Send queries, mutations and subscriptions to GraphQL services, and check their data and their errors, against the services' schemas. A federated graph's gateway and a subgraph alone are GraphQL services alike: register each at its URL.

Source: https://axx.nimbusxr.us/references/packs/graphql/

Send queries, mutations and subscriptions to GraphQL services, and check their data and their errors, against the services' schemas. A federated graph's gateway and a subgraph alone are GraphQL services alike: register each at its URL.

```gherkin
Given the parcels graphql service with the following properties:
  | url    | http://localhost:8400/graphql |
  | schema | introspection                 |
```

- **Operations** come from a file of the project (`.graphql`) or a doc string, with variables from a table: a variable takes the type the operation declares for it, so `10115` is text for a `String!`.
- **The checks read the service's last answer** in the scenario. An answer with errors does not fail the step that sends the operation: check its errors.
- **Subscriptions** go over graphql-ws (the `graphql-transport-ws` protocol, on the url with `ws://` or `wss://`) or over server-sent events, and belong to the scenario, which ends them when it ends.
- **The schema is the contract:** with a `schema` row (an SDL file of the project, a URL, or `introspection`), every operation and its variables are checked against it before they are sent, and every answer's data against the operation's types. A subgraph's SDL may use the federation directives (`@key`, `@link`...).

Findings have keys and levels, like the REST pack's OpenAPI findings:

| Key | Found when |
|---|---|
| `validation.operation.<rule>` | the operation breaks a validation rule of the GraphQL spec (`FieldsOnCorrectType`, `KnownArgumentNames`, `ProvidedRequiredArguments`...) |
| `validation.variables.missing` | a required variable is missing |
| `validation.variables.type` | a variable is not of its type |
| `validation.response.nonNull` | a non-null field is null, without an error that explains it |
| `validation.response.type` | a field's value is not of its type |
| `validation.response.enum` | a value is not one of its enum's |
| `validation.response.missingField` | a field the operation selects is missing |
| `validation.response.unknownField` | the data has a field the operation does not select |

- **Levels:** `ERROR` (or `FAIL`) fails the step, `WARN` and `INFO` log the finding, `IGNORE` drops it. Every finding is an `ERROR` unless `packs.graphql.levels` in axx.yaml, or the scenario's `the GraphQL validation levels are:`, says otherwise.
- **Keys cover the keys below them:** `validation.operation` sets every operation finding; the most specific key set wins.
- **Secrets stay secret:** `${env:..}` values in the properties are masked in logs and failures, and `${token:..}` names a token of the scenario.

Add it to a project with `axx pack add graphql`.

## Settings

In `axx.yaml`, under `packs.graphql`:

| Setting | Takes | Values |
|---|---|---|
| `levels` | Validation key -> ERROR (or FAIL), WARN, INFO or IGNORE, for every scenario. | names and values |

## Steps

### `graphql.service`

```gherkin
Given the {word} graphql service with the following properties:
  | property | value |
```

The table may start with the row naming its columns, as above, or go straight to its rows.

Register a GraphQL service under a name: a federated graph's gateway, a subgraph, or any GraphQL API.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

| Property | Takes | Values | Default |
|---|---|---|---|
| `url` | the URL operations are posted to |  | _required_ |
| `schema` | its schema, which operations and answers are checked against: an SDL file of the project, a URL, or `introspection` to ask the service |  | |
| `header.<name>` | a header sent with every operation, such as `header.Authorization` |  | |
| `timeout` | how long an operation may take, like `5s` |  | `10s` |
| `subscriptions` | how subscriptions go | `graphql-ws`, `sse` | `graphql-ws` |
| `subscriptions url` | where subscriptions go |  | `the url, with `ws://` or `wss://` for graphql-ws` |

Values are expanded (`${env:..}`, `${sys:..}`, `${token:..}`); what `${env:..}` references expand to is masked everywhere.

**Example:**

```gherkin
Given the parcels graphql service with the following properties:
  | url    | http://localhost:8400/graphql |
  | schema | introspection                 |
Given the graph graphql service with the following properties:
  | url                  | https://graph.parcels.example/graphql |
  | schema               | graphql/supergraph.graphql            |
  | header.Authorization | Bearer ${token:shop}                  |
```

_Since 0.1.5._

### `graphql.send.file`

```gherkin
When the {filepath} query/mutation is sent to the {word} graphql service[[ with the following variables:]]
```

Send the query or the mutation of a file of the project, with the table's variables when the step has one.

| Parameter | Takes | For example |
|---|---|---|
| `{filepath}` | a file of the project: a path relative to the feature file's directory, the `resources` directories or axx.yaml's directory (looked for in that order), or an absolute path, quoted when it has a space | `seeds/parcels.yaml`, `kafka/scan-delivered.json` |
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

Each row is a variable, or a path into one (`address.postcode`), and its value: a value takes the type the operation declares for its variable, double quotes make it text, `null` is null and `undefined` leaves it out.

**Variants**, the parts in `[[...]]` said or left out:

- `the {filepath} query/mutation is sent to the {word} graphql service`
- `the {filepath} query/mutation is sent to the {word} graphql service with the following variables:`

**Example:**

```gherkin
When the graphql/parcel.graphql query is sent to the parcels graphql service with the following variables:
  | reference | PX-GQL-7201 |
When the graphql/hold-parcel.graphql mutation is sent to the parcels graphql service with the following variables:
  | reference | PX-GQL-7202 |
  | until     | 2026-10-05  |
```

_Since 0.1.5._

### `graphql.send.doc`

```gherkin
When a query/mutation is sent to the {word} graphql service:
  """
  ...
  """
```

Send the query or the mutation of the doc string.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

**Example:**

```gherkin
When a query is sent to the parcels graphql service:
  """
  { parcel(reference: "PX-GQL-7201") { status } }
  """
```

_Since 0.1.5._

### `graphql.subscribe`

```gherkin
When the {filepath} subscription is started on the {word} graphql service[[ with the following variables:]]
```

Start the subscription of a file of the project, with the table's variables when the step has one. It belongs to the scenario, which ends it when it ends.

| Parameter | Takes | For example |
|---|---|---|
| `{filepath}` | a file of the project: a path relative to the feature file's directory, the `resources` directories or axx.yaml's directory (looked for in that order), or an absolute path, quoted when it has a space | `seeds/parcels.yaml`, `kafka/scan-delivered.json` |
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

Each row is a variable, or a path into one (`address.postcode`), and its value: a value takes the type the operation declares for its variable, double quotes make it text, `null` is null and `undefined` leaves it out.

**Variants**, the parts in `[[...]]` said or left out:

- `the {filepath} subscription is started on the {word} graphql service`
- `the {filepath} subscription is started on the {word} graphql service with the following variables:`

**Example:**

```gherkin
When the graphql/parcel-scanned.graphql subscription is started on the parcels graphql service with the following variables:
  | reference | PX-GQL-7203 |
```

_Since 0.1.5._

### `graphql.no.errors`

```gherkin
Then the {word} graphql service answered without errors
```

Check that the service's last answer has no errors.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

**Example:**

```gherkin
Then the parcels graphql service answered without errors
```

_Since 0.1.5._

### `graphql.data`

```gherkin
Then the {word} graphql service's data has the following properties:
  | path | value |
```

The table may start with the row naming its columns, as above, or go straight to its rows.

Check the data of the service's last answer.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

Each row is a path into the data (a field, a dotted path like `parcel.shop.name`, or a JSONPath) and its value, compared as text: `null` for null and `undefined` for absent.

**Example:**

```gherkin
Then the parcels graphql service's data has the following properties:
  | parcel.status    | IN_TRANSIT |
  | parcel.shop.name | Maple Home |
```

_Since 0.1.5._

### `graphql.error`

```gherkin
Then the {word} graphql service answered an error where:
  | path | value |
```

The table may start with the row naming its columns, as above, or go straight to its rows.

Check that the service's last answer has an error with those values.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

Each row is a path into the error (`message`, `path`, `extensions.code`) and its value, compared as text; the error's `path` is its fields and indexes joined with dots, like `parcel.shop` or `parcels.0.reference`.

**Example:**

```gherkin
Then the parcels graphql service answered an error where:
  | path            | holdParcel   |
  | extensions.code | NOT_HOLDABLE |
```

_Since 0.1.5._

### `graphql.received`

```gherkin
Then [[within {duration} ]]the {word} graphql service's subscription received a message where:
  | path | value |
```

The table may start with the row naming its columns, as above, or go straight to its rows.

Check that the service's subscription received a message whose data has those values.

- The check waits for it: 10 seconds, or `within {duration}`.
- When the subscription has ended, it fails at once, saying why.

| Parameter | Takes | For example |
|---|---|---|
| `{duration}` | a duration in seconds (`s`) or minutes (`m`) | `5s`, `2m` |
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

Each row is a path into the message's data and its value, compared as text: `null` for null and `undefined` for absent.

**Variants**, the parts in `[[...]]` said or left out:

- `the {word} graphql service's subscription received a message where:`
- `within {duration} the {word} graphql service's subscription received a message where:`

**Example:**

```gherkin
Then within 20s the parcels graphql service's subscription received a message where:
  | parcelScanned.reference | PX-GQL-7203      |
  | parcelScanned.status    | OUT_FOR_DELIVERY |
```

_Since 0.1.5._

### `graphql.levels`

```gherkin
Given the GraphQL validation levels are:
  | validation key | level |
```

The table may start with the row naming its columns, as above, or go straight to its rows.

Set the level of GraphQL validation findings for this scenario, on every GraphQL service it sends operations to.

- A key also sets the keys below it: `validation.operation` relaxes `validation.operation.FieldsOnCorrectType` too. The most specific key set wins.
- The rows are merged over `packs.graphql.levels` of axx.yaml.
- The pack's documentation lists the keys, and what each level does.

A row is a validation key and its level: `ERROR` (or `FAIL`), `WARN`, `INFO` or `IGNORE`. The key must be one the pack reports, or a prefix of such keys, like `validation.operation`: any other fails the step, which names the closest keys.

**Example:**

```gherkin
Given the GraphQL validation levels are:
  | validation.operation.FieldsOnCorrectType | IGNORE |
```

_Since 0.1.5._
