# test

Source: https://apigenie.pl/techdocs/reference/cli-test

Runs the requests of `.http` files and requests generated from an OpenAPI or AsyncAPI document, a
GraphQL schema or a `.proto` file against a live server, checks their assertions and scripts, and
validates every request, response and message against the document or the schema with the checks
of the [validating proxy](/techdocs/reference/cli-server). HTTP requests are checked against
an OpenAPI document, [GraphQL requests](#graphql-requests) against a GraphQL schema,
[`GRPC` requests](#grpc-requests) against a `.proto` file, and
[WebSocket and MQTT requests](#websocket-and-mqtt-requests) and the requests of
[HTTP channels](#requests-of-http-channels) against an AsyncAPI document.

## Usage

```sh
apigenie test [OPTIONS] FILES...
apigenie test --spec SPEC --generate [--base-url URL] [OPTIONS] [FILES...]
apigenie test --spec schema.graphql [--generate --base-url URL] [OPTIONS] [FILES...]
apigenie test --spec service.proto [--proto-path DIR ...] FILES...
apigenie test --spec ASYNCAPI --generate [--base-url ws://HOST:PORT] [--mqtt-url mqtt://HOST:PORT] [OPTIONS]
apigenie test import COLLECTION -o DIR [--environment FILE ...] [--spec SPEC] [--force]
```

## Description

The request files are written in the [httpYac](https://httpyac.github.io/) flavour of the `.http`
format, which the JetBrains HTTP client and VS Code's REST Client read too. Directories are searched
for `.http` and `.rest` files.

```http
@host = http://localhost:4010/v1

### List pets
# @name list
GET {{host}}/pets?limit=2
Accept: application/json

?? status == 200
?? body $ isArray

### Read the first one
GET {{host}}/pets/{{list.response.body.$[0].id}}

{{
  exports.petName = response.parsedBody.name;
  test("has a name", () => assert.ok(response.parsedBody.name));
}}

### Create a pet
POST {{host}}/pets
Content-Type: application/json

{"id": 7, "name": "{{petName}}"}

?? status == 201
```

```sh
apigenie test --spec openapi.yaml pets.http --report report.json --report-junit report.xml
```

```text
PASS GET http://localhost:4010/v1/pets?limit=2 -> 200 (9 ms) listPets
PASS GET http://localhost:4010/v1/pets/746 -> 200 (4 ms) showPetById
PASS POST http://localhost:4010/v1/pets -> 201 (2 ms) createPets
Requests: 3 passed, 0 failed, 0 skipped
Contract: 3 requests (0 undocumented), 0 errors, 0 warnings
Coverage: 70.0% of 10 branches, 3 of 3 operations
```

### Requests

A file is split into regions by lines starting with `###` (the rest of the line is the region's
title). A region has comments and directives (`#` or `//`), variable definitions, the request line
(`METHOD URL [HTTP/1.1]`; a URL alone is a `GET`), headers up to the first blank line, and a body up
to the next region, script or assertion. Lines starting with `?` or `&` right after the request line
continue the URL. A body of `< ./file.json` is read from that file, `<@ ./file.json` with its
variables substituted. Requests run in the order of the file, files in the order given.

`http://` and `https://` URLs both work. For `https://` (and `grpcs://`, `wss://` and `mqtts://`
alike), the server's certificate is checked against the system's root certificates and the URL's
host name. `--ca-cert FILE` adds authorities,
such as a test CA or a company's root, and `--insecure` skips the check. `SSL_CERT_FILE` replaces
the system's roots. A certificate that is rejected fails the request with the reason, for example
`no response: TLS with 10.0.0.5:443: certificate verification failed: The certificate is not
correctly signed by the trusted CA`.

A server behind mutual TLS asks for a client certificate: `--client-cert client.pem --client-key
client.key` presents one to every `https://`, `grpcs://`, `wss://` and `mqtts://` server that asks
(with `--insecure` too). The key must be PEM and not encrypted; a file that can't be read, or a key
of another certificate, stops the run with exit status 2. Without one, such a server fails the
request, for example `no response: TLS with api.example.com:443: the peer refused the connection:
the server requires a client certificate`. TLS sessions aren't resumed (every connection does a
full handshake), and certificates aren't checked for revocation (no CRL or OCSP).

| Directive | Meaning |
| --- | --- |
| `# @name list` | Names the request, so that others can refer to its response |
| `# @ref list` | Runs `list` first, unless it already ran |
| `# @forceRef list` | Runs `list` first, every time |
| `# @operation getPet` | Checks the request as the operation `getPet` of `--spec` (an AsyncAPI document's operation of an HTTP, WS or MQTT channel); for a GraphQL request, the operation to run (`operationName`) |
| `# @operation off` | Doesn't check the request against `--spec`, nor count it in the coverage |
| `# @channel pickupDesk` | WS and MQTT: checks the messages against that channel of the AsyncAPI document |
| `# @timeout 8000` | WS and MQTT, GraphQL subscriptions, gRPC client and bidi streams: waits up to 8000 ms for messages (`--message-timeout`, 5000 by default) |
| `# @expect 2` | WS and MQTT, GraphQL subscriptions: stops waiting after 2 messages; `0` doesn't wait |
| `# @property content-type application/json` | MQTT 5: a property of the messages the region publishes (see [MQTT 5 properties](#mqtt-5-properties)) |
| `# @disabled` | Skips the request |
| `# @postman` | Enables the collection-import compatibility API for this file |
| `# @title`, `# @description`, `# @tag`, `# @no-log`, `# @no-redirect`, `# @verbose`, ... | Accepted and ignored |

Any other directive is a warning: the request still runs.

### GraphQL requests

A `GRAPHQL` request has the GraphQL document as its body and, after a blank line, its variables as
a JSON object, as in httpYac and the JetBrains HTTP client. It is sent as a `POST` of
`{"query", "variables", "operationName"}` with `Content-Type: application/json`; `# @operation`
gives the `operationName`. The query can also come from a file (`< ./book.graphql`), without inline
variables then.

```http
# @operation GetBook
GRAPHQL {{host}}/graphql

query GetBook($id: ID!) {
  book(id: $id) {
    title
    author { name }
  }
}

{ "id": "1" }
```

A `POST` whose body starts with `query`, `mutation`, `subscription` or `fragment` is sent the same
way, when it has no `Content-Type` or a JSON or `application/graphql` one; with another
`Content-Type` (`text/plain`) it is sent as written. A `POST` of a JSON body that has a `query` (or
a batch of them) and a `GET` with a `query` parameter are GraphQL requests as they are. Without
`--spec`, GraphQL requests are only sent and asserted on.

#### Subscriptions

A `GRAPHQL` request (or a `POST` of a query) whose operation is a `subscription` goes over
WebSocket with the graphql-ws protocol (the `graphql-transport-ws` subprotocol of Apollo Client,
urql and GraphiQL). The URL's `http://` becomes `ws://` and `https://` becomes `wss://`; `ws://`
and `wss://` URLs are taken as written. The headers go with the handshake and, as an object, as the
payload of `connection_init`, where servers look for credentials.

The events are the response, as for a [`WS` region](#websocket-and-mqtt-requests):

- The body is the JSON array of the events (`{"data": ...}` each).
- The status is 101.
- Scripts get `response.messages`.

The subscription ends when the server completes it, after `# @expect` events, or when `# @timeout`
(`--message-timeout`, 5000 ms) has passed; then the client completes it. An `error` message from the
server fails the request with its errors.

```http
# @expect 3
GRAPHQL {{host}}/graphql
Authorization: Bearer {{token}}

subscription OnStory($topic: String!) {
  storyAdded(topic: $topic) {
    id
    title
  }
}

{ "topic": "space" }

?? body $.length == 3
?? body $[0].data.storyAdded.id == 1
```

```text
PASS GRAPHQL http://localhost:4000/graphql -> 1 sent, 3 received (162 ms) OnStory
```

A server that doesn't offer `graphql-transport-ws` fails the request (`no connection: the server
doesn't speak graphql-transport-ws (graphql-ws)`). The legacy `graphql-ws` subprotocol of
`subscriptions-transport-ws` and server-sent events aren't spoken.

### Validation

With `--spec`, each request is bound to an operation by its method and path, as the validating
proxy does, then checked: parameters, body, security (credentials must be present), and the
response's status, media type, headers and body. The checks, their severities and `--severity` are
those of [contract testing](/techdocs/reference/cli-server#contract-testing-validating-proxy).
XML request and response bodies are mapped by the schema's `xml` objects and checked like JSON ones
(see [XML bodies](/techdocs/reference/cli-server#xml-bodies)).
The coverage counts each operation, its request body and each declared response.

A request sent with an [OAuth 2](#oauth-2) token is also checked against the operation's security
requirements (or the document's), as warnings by default:

| Check | Default | Meaning |
| --- | --- | --- |
| `request.authorization.oauth2-scheme` | `warn` | The operation has no `oauth2` or `openIdConnect` scheme (or no security at all) |
| `request.authorization.oauth2-scope` | `warn` | A scope of `oauth2_scope` isn't declared for the operation, or no requirement has all its scopes in `oauth2_scope` |

When several OAuth 2 schemes apply, the one whose flow's `tokenUrl` is the token endpoint is
used. Without `oauth2_scope` the scopes aren't checked.

A Swagger 2.0 specification is converted to OpenAPI 3.2 in memory, as `apigenie-server` does (see
[Swagger 2.0 documents](/techdocs/reference/cli-server#swagger-20-documents)); no file is
written. What the conversion can only approximate is told once before the requests are sent, as
`SPEC:LINE: info: POINTER: MESSAGE` lines on the standard error (`{"event":"info",...}` with
`--formatter json`), and the report's operation pointers point into the Swagger file.

With a GraphQL schema as `--spec` (`.graphql`, `.graphqls` or `.gql`), the GraphQL requests are
checked by the [GraphQL validating proxy](/techdocs/reference/cli-server#contract-testing-of-a-graphql-api)'s checks. The
operation is validated against the schema before it is sent: an invalid one (a syntax error, an
unknown field or argument, an unknown `operationName`, variables of the wrong type) fails the
request, which isn't sent, with the errors the server would give (`invalid request: Cannot query
field "nope" on type "Book".`), and the run goes on. The response is checked against the request's
selection set: the shape of `data` and `errors`, the type of each value, `null` in non-null fields
that no error's `path` explains (`null-in-non-null`), `__typename` of abstract types, and errors
of operations the schema allows (`response.schema-drift`). Each event of a subscription is checked
the same way, at `next[0]`, `next[1]`, ... (`next[1]/data/storyAdded/title: Story.title is an
Int`). Deprecated fields selected are notes (`request.field.deprecated`). Requests that aren't
GraphQL requests aren't checked.

An answer with `errors` next to `data` is valid GraphQL: the operation ran, but fields failed. Each
of those errors is a warning (`response.field-error`), so a server that answers every field with
an error doesn't pass unnoticed. `--fail-on warnings` fails such requests, and
`--severity response.field-error=error` makes them errors. The coverage counts the fields of the
schema's types that requests selected, whether or not the answers had them, and each root field;
`--min-coverage` is the share of fields.

```text
PASS GRAPHQL http://localhost:4000/graphql -> 200 (7 ms) GetBook
FAIL GRAPHQL http://localhost:4000/graphql Broken
    invalid request: Cannot query field "nope" on type "Book".
Requests: 1 passed, 1 failed, 0 skipped
Contract: 2 requests (0 to other paths), 1 errors, 0 warnings
Coverage: 40.0% of 15 fields, 1 of 5 root fields
  not tested: Query.books, Query.node, Mutation.addBook, Mutation.deleteBook
```

With an AsyncAPI 3.x `--spec`, WS and MQTT requests are checked against the document's channels.
The channel of a `WS` request is the one served at its URL's path; that of an `MQTT` request is the
one of the topic it publishes to (or of its first subscription without wildcards), and each message
received is matched by its own topic. `# @channel` names the channel when its address can't be
matched (`address: null`, wildcard filters), `# @operation` checks the messages sent as that
operation, and `# @operation off` checks nothing. A path or topic of no channel is a note, and its
messages aren't checked.

The messages sent are the client's: they are checked against the channel's `receive` operations.
The messages received are the application's: they are checked against its `send` operations or,
when they answer a request, against the reply's messages. When a message sent matched an operation
with a `reply`, the first later message on the reply's channel that is one of the reply's messages
is its reply:

| Check | Default | Meaning |
| --- | --- | --- |
| `message.reply.missing` | `warn` | No reply came while the request waited |
| `message.reply.uncorrelated` | `error` | The reply's `correlationId` (in the payload, or in a header over MQTT 5) isn't the request's, or is missing; over MQTT 5, the reply doesn't repeat the request's Correlation Data |
| `message.properties.invalid` | `error` | MQTT 5: a message's properties don't match its MQTT bindings (see [MQTT 5 properties](#mqtt-5-properties)) |

Violations are located in the message: `sent[0]/message/carrier`, `received[1]/message/requestId`,
`sent[0]/message/property.contentType`. Over MQTT 5 a message's user properties are its headers:
a `correlationId` or a reply address in `$message.header#/...` is read from them, and they are
checked against the message's `headers` (`message.headers.invalid`). MQTT 3.1.1 has no message
headers: such a `correlationId` or reply address can't be checked, and the request gets a note
saying so. WS and MQTT
requests aren't checked against an OpenAPI document. The coverage counts each message of each
operation, those of HTTP channels included, as the
[AsyncAPI validating proxy](/techdocs/reference/cli-server#contract-testing-of-a-websocket-api) does.

#### Requests of HTTP channels

HTTP requests are checked against the AsyncAPI document's HTTP channels (the channels of `http` and
`https` servers) with the checks of the
[validating proxy](/techdocs/reference/cli-server#contract-testing-of-http-channels), so both
report the same violations:

- The channel and operation are found by the request's path (under a server's `pathname` or not)
  and method: a `receive` operation's method is its HTTP binding's (`POST` by default); a `send`
  operation is asked for with `GET`, as the mock serves it. `# @operation` names the operation
  instead, whatever the method; a path that isn't its channel's is an error.
- A `receive` operation's request: the body against the operation's messages (`body/...`), the
  query against the HTTP binding's `query` schema, and the header fields against the message's
  `headers` and its HTTP binding's `headers` (its traits' too). Its 2XX response: the body and
  header fields against the reply's messages (`response/...`).
- A `send` operation's 2XX response with a body: against the operation's messages. Its header
  fields aren't checked: they are those of the webhook the application sends, which doesn't pass
  `apigenie test`.
- Responses of other statuses, and the reply's `statusCode` binding, aren't checked.

| Check | Default | Meaning |
| --- | --- | --- |
| `operation.query.invalid` | `error` | The query doesn't match the HTTP binding's `query` (`query.dryRun`) |
| `message.headers.invalid` | `error` | The header fields the message's header schemas name don't match them (`header.X-Batch-Size`) |

Query parameters and header fields are read as the type their schema says (`integer`, `number`,
`boolean`, an `array` from repeated parameters); header fields the schemas don't name are left
out. A path of no HTTP channel, or a method its channel doesn't take, is a note, and the request
isn't checked.

```http
### A carrier's status batch
POST {{http}}/http/carriers/dhl/events?dryRun=true
Content-Type: application/json

{"events":[{"trackingNumber":"CE1234567890","statusCode":"DLV","occurredAt":"2026-10-08T10:00:00Z"}]}

### The webhooks, asked for as the mock serves send operations
GET {{http}}/http/
```

### WebSocket and MQTT requests

A `WS` (or `WEBSOCKET`) region connects to a `ws://` or `wss://` URL, its headers going with the
handshake; an `MQTT` region connects to an `mqtt://host:port` broker, or `mqtts://host:port` (also
`ssl://`) over TLS (MQTT 5, or 3.1.1 when the broker refuses it). Without a port, `wss://` uses 443, `mqtt://` 1883 and `mqtts://`
8883. Over TLS the certificate is checked as for `https://`: `--ca-cert` and `--insecure` apply. The body is the message to
send; lines `===` separate several messages, and `=== wait-for-server` waits for a message of the
server before sending the next one, as in httpYac. Then the region waits for the server's messages
until it has `# @expect` of them or `# @timeout` (`--message-timeout`, 5000 ms) has passed. The
messages received are the response: its body is their JSON array, each message parsed when it is
JSON.

```http
### Request a pickup: the reply carries the request's id
# @expect 1
WS ws://localhost:4010/pickups

{"type":"pickup.request","requestId":"6f1c2b9e-7f8a-4c35-9a51-2d1e8b7f0c11","carrier":"dhl", ...}

?? body $[0].type == pickup.confirmed
?? body $[0].requestId == 6f1c2b9e-7f8a-4c35-9a51-2d1e8b7f0c11

### Report a courier's position
# @expect 0
# @property content-type application/json
# @property payload-format-indicator utf-8
MQTT mqtt://localhost:1883
publish: couriers/cou_4821/position
qos: 1

{"courierId":"cou_4821","at":"2026-10-06T07:41:02Z","position":{"type":"Point","coordinates":[21.0067,52.2319]}}

### Receive a job offer
# @channel courierJobOffers
# @expect 1
MQTT mqtt://localhost:1883
subscribe: couriers/+/offers

?? body $.length == 1
```

```text
PASS WS ws://localhost:4010/pickups -> 1 sent, 1 received (12 ms) requestPickup
PASS MQTT mqtt://localhost:1883 -> 1 sent, 0 received (3 ms) receiveCourierPositions
PASS MQTT mqtt://localhost:1883 -> 0 sent, 1 received (6 ms) offerPickupJob
```

The headers of an `MQTT` region are its settings:

| Header | Meaning |
| --- | --- |
| `topic` | The topic messages are published to, also subscribed to (unless `subscribe` is given) |
| `publish` | The topic messages are published to |
| `subscribe` | The topic filters subscribed to before publishing, comma-separated (`couriers/+/offers, hubs/#`) |
| `qos` | `0` (the default), `1` or `2`, for publishing and subscribing |
| `retain` | `true` to publish retained messages |
| `clientId`, `keepAlive`, `clean` | The connection's client id (a random one by default), keep-alive in seconds (60) and clean session (`true`) |
| `username`, `password` | The CONNECT's user name and password; without them, `APIGENIE_MQTT_USERNAME` and `APIGENIE_MQTT_PASSWORD` |
| `version` | `auto` (the default: MQTT 5, else 3.1.1 when the broker refuses it), `5` or `3.1.1` |

The client's own messages coming back on a topic it subscribed to are left out. Retained messages
of a subscription count as received.

Settings are substituted like any header, so a password can stay out of the file:
`password: {{$processEnv BROKER_PASSWORD}}`, or a variable of an environment file. A setting the
region doesn't give comes from `APIGENIE_MQTT_USERNAME` or `APIGENIE_MQTT_PASSWORD`. MQTT 5 sends
a password without a user name; MQTT 3.1.1 sends one only with a user name, so with `version:
3.1.1` a password alone is a warning and isn't sent. A broker that refuses them fails the request
with `the broker refused the connection (reason code 0x86) (bad user name or password)` (MQTT 5),
or `(return code 4) (bad user name or password)` (MQTT 3.1.1). Credentials never show in the output or the reports: wherever they
would, they read `[redacted]`, and a URL's `user:password@` is shown as `***@`. The user name and password of the URL itself aren't sent.

| In checks and scripts | Value |
| --- | --- |
| `?? body $.length == 2` | How many messages came (`.length` also gives the size of any array or string) |
| `?? body $[0].type == pickup.confirmed` | A value of the first message |
| `?? status == 101` | The handshake's status (WS); the CONNACK's return code, `0` (MQTT) |
| `response.messages[i]` | In scripts: `topic` (the topic, or the WebSocket's path), `body` (parsed when JSON), `rawBody`, `binary` |

#### MQTT 5 properties

An `MQTT` region connects with MQTT 5. A broker that refuses it (reason code 0x84, a 3.1.1 broker's
return code 1, or a connection closed before the CONNACK) gets a second connection with MQTT 3.1.1,
and the request notes `the broker refused MQTT 5`. `version: 5` doesn't fall back; `version: 3.1.1`
doesn't try MQTT 5.

`# @property` gives the properties of every message the region publishes:

| Directive | Property |
| --- | --- |
| `# @property content-type application/json` | Content Type |
| `# @property response-topic clients/{{client}}/quotes` | Response Topic (no wildcards); the region also subscribes to it |
| `# @property correlation-data {{$uuid}}` | Correlation Data |
| `# @property payload-format-indicator utf-8` | Payload Format Indicator: `utf-8` or `1`, `bytes` or `0` |
| `# @property message-expiry-interval 60` | Message Expiry Interval, in seconds |
| `# @property user replyTo dispatch/d-1/replies` | A user property (repeat the line for more; the value may be empty) |

Values are substituted like headers. An unknown property, a missing value or a bad value is a
warning, and that property isn't sent. Over MQTT 3.1.1 none is sent, and the request notes it.

Over MQTT 5 the messages sent and received are checked with their properties, against the
message's and the operation's MQTT bindings (0.2.0), as `message.properties.invalid` (`error`):

| Binding | Checked |
| --- | --- |
| message `payloadFormatIndicator` | The Payload Format Indicator is the same (absent counts as 0) |
| message `contentType` | The Content Type is the same, ignoring case |
| message `correlationData` | Correlation Data is present and matches the schema |
| message `responseTopic` | A Response Topic is present, the string given or matching the schema |
| operation `messageExpiryInterval` | A Message Expiry Interval is present and no longer |

A request whose reply address is in a header (`$message.header#/replyTo`) is answered on the topic
in that user property; one whose reply channel has `address: null` is answered on its Response
Topic. Either way the region waits for the reply on that topic, checks it as the reply, and
compares its correlation id (header or payload) and, when a binding declares `correlationData`, its
Correlation Data with the request's.

```http
### Ask for a quote: the reply comes to the Response Topic
# @expect 1
# @property content-type application/json
# @property response-topic clients/7/quotes
# @property correlation-data {{$uuid}}
# @property user correlationId c-42
MQTT mqtt://localhost:1883
publish: quotes
qos: 1

{"parcel":"P1"}

?? body $[0].price exists
```

A message whose bindings state properties counts in the coverage as checked (MQTT 5) or unchecked
(MQTT 3.1.1): `summary.properties` and `operations[].properties` in the report, `MQTT 5 properties:
checked on 2 messages, not on 0 (MQTT 3.1.1)` in the summary.

### Kafka requests

A `KAFKA` region produces records to a Kafka cluster and reads what comes on its topics (ADR-170).
The URL names a broker, `kafka://host:port` (port 9092 by default) or `kafkas://host:port` (9093,
over TLS, verified as `https://` with `--ca-cert`, `--insecure` and `--client-cert`); several
bootstrap servers are separated by commas. The cluster needs Kafka 2.3 or later. A cluster that
creates topics on first use creates the region's topic, as Kafka's producers ask it to.

```http
### Place an order: the confirmation carries its correlation id
# @expect 1
# @timeout 10000
# @property correlationId {{$uuid}}
KAFKA kafka://localhost:9092
topic: shop.orders
subscribe: shop.confirmations
key: cust-1

{"sku": "A-1"}

?? body $[0].accepted == true

### Await a shipment the application produces
# @channel shipments
# @expect 1
KAFKA kafka://localhost:9092
subscribe: shop.shipments
```

| Header | Meaning |
| --- | --- |
| `topic` | The topic the records are produced to; also read, unless `subscribe` is given |
| `subscribe` | The topics read, separated by commas |
| `key` | The records' key |
| `partition` | The records' partition; without it, the key's (murmur2, as Kafka's producers choose), else the next in turn |
| `username`, `password` | SASL PLAIN credentials; without them `APIGENIE_KAFKA_USERNAME` and `APIGENIE_KAFKA_PASSWORD`. They are `[redacted]` in the output and reports |

`# @property NAME VALUE` lines are the records' headers, the name as written (`# @property user
NAME VALUE` too). The body's parts (`===`) are the records; `=== wait-for-server` waits for a record
before the next part. Before the first record is produced, the region takes the latest offsets of
the topics it reads, so a reply can't come too early; then it reads until it has `# @expect` records
or `# @timeout` has passed. Its own records are left out. The records read are the response, as an
MQTT region's messages are.

With an AsyncAPI `--spec`, what is produced is checked against the receive operations of the topic's
channel and what is read against its send operations, or as the reply to a request: the value, the
headers against the message's `headers`, the key against the message's Kafka binding `key`
(`message.key.invalid`) and the partition against the channel's Kafka binding `partitions`
(`message.partition.invalid`). A channel's topic is its Kafka binding's `topic`, else its address.
A reply is paired with its request by the correlation id in a header (`$message.header#/...`) or the
payload, and a reply address in a header is followed, as for MQTT 5. Compressed records aren't read;
they are reported as errors.

### AMQP requests

An `AMQP` region publishes to an AMQP 0-9-1 broker and reads what comes to its queues (ADR-171). The
URL is `amqp://[user:password@]host[:port][/vhost]` (port 5672, the virtual host percent-encoded:
`/%2Fcarpet-express`), or `amqps://` (5671, over TLS, verified as `https://`).

```http
### File a declaration: the decision comes to the decisions queue
# @expect 1
# @property delivery-mode 2
# @property correlationId {{$uuid}}
AMQP amqp://localhost:5672/%2Fcarpet-express
exchange: customs
routing-key: customs.GB.filing
subscribe: customs.decisions

{"destinationCountry": "GB"}

### Await what the application publishes to the customs exchange
# @expect 1
AMQP amqp://localhost:5672/%2Fcarpet-express
bind: customs customs.*.filing
```

| Header | Meaning |
| --- | --- |
| `exchange` | The exchange the messages go to; without it, the default exchange (routing keys are queue names) |
| `routing-key` | The messages' routing key |
| `subscribe` | Queues consumed, separated by commas; a queue's messages go to the region, not to the application |
| `bind` | `EXCHANGE KEY`: a queue of the region's own, bound to the exchange with that binding key (none for fanout); can be repeated |
| `mandatory` | `false`: a message no queue takes isn't an error (`true` by default) |
| `username`, `password` | PLAIN credentials; without them the URL's, then `APIGENIE_AMQP_USERNAME` and `APIGENIE_AMQP_PASSWORD`, then `guest`. They are `[redacted]` in the output and reports |

`# @property NAME VALUE` lines with the name of a basic property set it: `content-type`,
`content-encoding`, `delivery-mode`, `priority`, `correlation-id`, `reply-to`, `expiration`
(milliseconds), `message-id`, `timestamp` (seconds since the epoch, or `now`), `type`, `user-id` and
`app-id`. Other names, and `# @property user NAME VALUE`, are application headers. The region
consumes its queues before it publishes, then reads until it has `# @expect` messages or `# @timeout`
has passed; its own messages coming back are left out. RabbitMQ's direct reply-to works:
`subscribe: amq.rabbitmq.reply-to` with `# @property reply-to amq.rabbitmq.reply-to`.

With an AsyncAPI `--spec`, the channel is the one of the exchange and routing key (a queue channel's
for the default exchange and its queue's name), and a message read from a queue is that queue's
channel's. What is published is checked against the channel's receive operations and what is read
against its send operations or as a reply: the body, the headers against the message's `headers`,
and the properties against the AMQP bindings (`message.properties.invalid`). A reply is paired with
its request by its correlation id in a header or a property: properties are seen by their AMQP and
AsyncAPI names (`correlation_id` and `correlationId`, `reply_to` and `replyTo`), so
`$message.header#/correlationId` finds either.

### gRPC requests

With a `.proto` file as `--spec`, `GRPC` requests call the methods of its services, as httpYac
writes them:

```http
@host = localhost:50051

### say hello
GRPC {{host}}/acme.greeter.v1.Greeter/SayHello
x-tenant: acme

{"name": "Ann"}

?? status == 0
?? body message == Hello Ann
?? header grpc-status == 0
```

```sh
apigenie test --spec greeter.proto greeter.http
```

```text
PASS GRPC localhost:50051/acme.greeter.v1.Greeter/SayHello -> OK (6 ms) acme.greeter.v1.Greeter/SayHello
Requests: 1 passed, 0 failed, 0 skipped
Contract: 1 calls (0 undocumented), 0 errors, 0 warnings
Coverage: 33.3% of 3 methods
  not tested: acme.greeter.v1.Greeter/SayHelloToAll, acme.greeter.v1.Greeter/Ping
```

- **The request line** is `GRPC HOST:PORT/package.Service/Method`, or a URL with `grpc://`, or with
  `grpcs://` for TLS. The method must be one of the file's: another is a failed request.
- **Headers** are the call's metadata (names in lower case); `-bin` values are sent as written,
  base64-encoded.
- **The body** is the request message in its proto3 JSON form (field names or their JSON names,
  64-bit integers as numbers or strings, enum names, base64 bytes, the well-known types in their
  own forms); no body is `{}`. A body that isn't a valid message fails the request before anything
  is sent, naming the field: `the body isn't a valid acme.greeter.v1.HelloRequest: name: expected a string`.
- **The `.proto` file is `--spec`**, with its imports looked up in `--proto-path` directories, then
  next to the file, then among the standard files (`google/protobuf/*`, `google/api/*`,
  `google/rpc/*`), as `apigenie-server --proto-path` does. httpYac's `proto < ./file.proto` line is
  read and ignored with a warning.
- **Assertions and scripts** see the call as a response: `status` is the gRPC status code (`0` for
  OK, `5` for NOT_FOUND) and the status text its name, the headers are the response metadata and
  the trailers (`grpc-status`, `grpc-message` among them), and the body is the response message as
  JSON. A server-streaming or bidi-streaming call's body is a JSON array of its messages.
- **Every kind of method.** Client- and bidi-streaming methods take a stream of messages, written
  as for [WS regions](#websocket-and-mqtt-requests): lines `===` separate them, and
  `=== wait-for-server` waits for a message of the server before the next one is sent (bidi
  methods only). After the last message the call is half-closed, and its end (the trailers) is
  awaited for `# @timeout` (`--message-timeout`, 5000 ms); a call that doesn't end in time is
  cancelled and fails. Unary and server-streaming methods take one message: more fail the request.
- **HTTP/2**, cleartext with prior knowledge (h2c), or over TLS with ALPN `h2` for `grpcs://` (and
  `https://`) URLs, verified as `https://` requests are (`--ca-cert`, `--insecure`). `--timeout`
  bounds the connection and each wait for the server, and is sent as `grpc-timeout`.

```http
### a client stream: the messages, then one answer
GRPC {{host}}/acme.chat.v1.Chat/Upload

{"author": "Ann", "text": "hi"}
===
{"author": "Bob", "text": "hello"}

?? body lines == 2

### a bidi stream that waits for each answer before it goes on
GRPC {{host}}/acme.chat.v1.Chat/Talk

{"author": "Ann", "text": "hi"}
=== wait-for-server
{"author": "Ann", "text": "bye"}

?? body $.length == 2
?? body $[1].text == re: bye
```

Each call is checked with the checks of
[gRPC contract testing](/techdocs/reference/cli-server#contract-testing-of-a-grpc-api): the
request and response messages against their types (fields marked
`[(google.api.field_behavior) = REQUIRED]` included), each message of a stream at `message[0]`,
`message[1]`, ..., the status and its `google.rpc` details, the number of messages of a unary or
client-streaming call. The coverage counts the file's methods. Without a `.proto` file
as `--spec`, `GRPC` requests are skipped; HTTP requests in the same files are sent but not checked.

### Environments and variables

`{{name}}` is replaced by the variable `name`. Variables come from, the later winning:

1. the environment files of the request file's directory or, when it has none, the working
   directory: `http-client.env.json` (its `$shared` object, then the object of `--env`),
   `http-client.private.env.json` (the same), `.env` and `.env.NAME`;
2. `--env-file` files (JSON like `http-client.env.json`, or `KEY=VALUE` lines);
3. `@name = value` lines outside of requests;
4. what scripts set (`exports`, `client.global.set`);
5. `@name = value` lines of the request's region;
6. `--var NAME=VALUE`.

Responses of named requests can be used in the requests after them:

| Reference | Value |
| --- | --- |
| `{{list.response.status}}` | The status |
| `{{list.response.headers.Location}}` | A header |
| `{{list.response.body.$[0].id}}`, `{{list.response.body./0/id}}` | A value of the JSON body (a path or a JSON pointer) |
| `{{list.response.body}}` | The body |
| `{{list.id}}` | httpYac's form: a value of the JSON body |

Built-in variables: `{{$uuid}}` (and `$guid`, `$random.uuid`), `{{$timestamp}}`, `{{$isoTimestamp}}`,
`{{$datetime iso8601}}`, `{{$datetime rfc1123}}`, `{{$randomInt MIN MAX}}`, `{{$processEnv NAME}}`
and `{{$dotenv NAME}}`. A variable that isn't defined is a warning and stays as it is. `\{{` is a
literal `{{`.

`Authorization: Basic user:password` (or `Basic user password`) is sent base64-encoded, as httpYac
and the JetBrains client do.

### OAuth 2

`Authorization: oauth2 [GRANT] [PREFIX]` gets a token from the token endpoint and sends
`Authorization: Bearer TOKEN` instead, with httpYac's syntax and variables:

```http
GET {{host}}/pets
Authorization: oauth2 client_credentials
```

| Grant | Sent to the token endpoint |
| --- | --- |
| `client_credentials` (when no grant is written) | The client's credentials |
| `password` | `PREFIX_username` and `PREFIX_password` too |
| `refresh_token` | `PREFIX_refreshToken` |

The token request is configured by variables named after the prefix (`oauth2` when no prefix is
written; `Authorization: oauth2 password keycloak` reads `keycloak_tokenEndpoint` and so on). They
come from environment files, `@` variables and `--var`, like any other variable:

| Variable | Meaning |
| --- | --- |
| `oauth2_tokenEndpoint` | The token endpoint's URL (required) |
| `oauth2_clientId` | The client's id (required) |
| `oauth2_clientSecret` | The client's secret; none for a public client |
| `oauth2_scope` | Space-separated scopes |
| `oauth2_audience`, `oauth2_resource` | Sent as `audience` and `resource` when set |
| `oauth2_username`, `oauth2_password` | The `password` grant's user |
| `oauth2_refreshToken` | The `refresh_token` grant's refresh token |
| `oauth2_useAuthorizationHeader` | `true` (the default): the client's id and secret go in a `Basic` `Authorization` header; `false`: in the form |
| `oauth2_issuer` | `openid` only: the token endpoint is read from `ISSUER/.well-known/openid-configuration` when `oauth2_tokenEndpoint` isn't set |

`Authorization: openid ...` is the same, with the OpenID Connect discovery of `oauth2_issuer`.
The token is requested once per run for each configuration (the endpoint, the client, the scopes,
the user) and requested again, with its refresh token when it has one, shortly before its
`expires_in` runs out. The token endpoint is reached like any request: `https://` is verified as
`--ca-cert` and `--insecure` say. A token endpoint that refuses fails the request, which isn't
sent:

```text
FAIL GET http://localhost:8080/pets
    no OAuth 2 token: the OAuth 2 token endpoint http://localhost:8080/token answered 401 (invalid_client: unknown client)
```

The `authorization_code` (or `code`), `implicit` and `device_code` grants need a browser or a
person, and token exchange isn't supported: those requests are skipped with a warning. Tokens,
refresh tokens, client secrets, passwords and the credentials of `Authorization` headers are shown
as `[redacted]` wherever they would appear: in URLs, errors, assertion messages, script logs,
warnings and the reports.

### Assertions

A line `?? SUBJECT [ARGUMENT] PREDICATE [VALUE]` checks the response:

| Subject | Value |
| --- | --- |
| `status` | The status |
| `header NAME` | The header's value |
| `body` | The body; `body PATH` a value of the JSON body (`$.items[0].id`, `items[0].id` or `/items/0/id`) |
| `duration` | The time to the response, in milliseconds |
| `js EXPRESSION` | The value of a JavaScript expression with the scripts' API |

Predicates: `==`, `!=`, `>`, `>=`, `<`, `<=` (numbers when both sides are numbers), `startsWith`,
`endsWith`, `includes` (or `contains`), `matches` (a regular expression, `/.../flags` too),
`exists`, `isArray`, `isBoolean`, `isNumber`, `isString`, `isTrue` and `isFalse`. A `js` subject
without a predicate must be true. Variables are substituted in the argument and the value.

### Scripts

`{{` and `}}` on lines of their own enclose an httpYac script: before the request line it runs
before the request is sent, after it once the response came. JetBrains handlers work too:
`< {% ... %}` before the request line, `> {% ... %}` after it. Scripts run in a sandboxed
JavaScript engine (QuickJS) with no file system or network access, five seconds each. Both
flavours get the same API:

| Name | What it is |
| --- | --- |
| `response.status`, `response.statusCode` | The status |
| `response.headers` | Headers by lower-case name; `headers.valueOf(name)`, `headers.valuesOf(name)` |
| `response.body` | The body, parsed when it is JSON; `response.parsedBody`, `response.rawBody` |
| `response.contentType.mimeType`, `.charset` | The media type |
| `response.duration` | Milliseconds |
| `request.method`, `request.url`, `request.headers`, `request.body` | The request as written |
| `request.variables.set(name, value)`, `.get(name)` | Variables of this request (pre-request scripts) |
| `exports.name = value`, `client.global.set(name, value)` | Variables of the following requests |
| `client.global.get(name)`, `request.environment.get(name)` | A variable |
| `test(name, fn)`, `client.test(name, fn)` | A named check: it fails when `fn` throws (or its promise rejects) |
| `test.status(code)`, `test.header(name, value)`, `test.headerContains(name, text)`, `test.hasResponseBody()`, `test.totalTime(ms)` | httpYac's shortcuts |
| `assert(value, message)`, `client.assert(...)`, `assert.equal`, `assert.strictEqual`, `assert.deepStrictEqual`, `assert.notEqual`, `assert.fail` | Throws when the check fails |
| `console.log`, `console.warn`, ..., `client.log` | Lines of the request's log (`--verbose`, the reports) |

Variables are also local names of scripts when they are identifiers. A script that throws fails its
request. Changes an ordinary pre-request script makes to `request` are not sent: set variables
instead. Imported files opt into the header-addition API described below.

### Importing collections

`apigenie test import` reads a Postman Collection Format v2.0 or v2.1 JSON export and writes
request files that `apigenie test` can run. It uses an independently implemented reader, not
Postman's JavaScript or the `postman-collection` SDK. Collection v1 is not supported: export as
v2.1 from Postman.

```sh
apigenie test import carpet-express.collection.json -o requests \
  --environment mock.environment.json --spec carpet-express.openapi.yaml
apigenie test requests --env mock --spec carpet-express.openapi.yaml \
  --var baseUrl=http://localhost:4010/v2
```

| Import option | Meaning |
| --- | --- |
| `COLLECTION` | Collection JSON file to import |
| `-o DIR`, `--output DIR` | Required output directory |
| `--environment FILE` | An exported environment JSON file; can be repeated |
| `--spec SPEC` | Bind matching requests to operation IDs and write saved response examples as an OpenAPI overlay |
| `--force` | Replace existing generated files; never delete unrelated files |

The collection's root requests go into `<collection-slug>.http`. A folder `Shipping` becomes
`shipping.http`; its nested folder `Tracking` becomes `shipping/tracking.http`. Every request
is a `###` region with a stable `# @name` slug. Collection and inherited folder variables are
written as `@name = value` defaults at the top of each file. Slug collisions get deterministic
suffixes. Collection and folder auth and pre-request/test scripts are inherited by descendant
requests; explicit `noauth` disables inherited authentication.

URLs and variables retain `{{name}}` references. Active headers and query parameters, raw,
urlencoded, multipart form-data, GraphQL and file bodies are mapped. GraphQL becomes an HTTP
POST with a JSON envelope, not the unsupported `GRAPHQL` pseudo-method and not GraphQL-specific
contract validation. Available upload files are copied into the output with relative `< ./path`
references; missing files are kept as references and reported as warnings. Relative source
upload paths are resolved against the collection file's directory. Multipart file references
are read as binary data when the request runs.

Exported environments become named entries in `http-client.env.json`. Bearer, basic and API-key
authentication becomes headers or query parameters with environment-variable references.
Literal credentials and known credential defaults are stored only in the generated
`http-client.private.env.json` (with the same `$shared` and environment entries), not in
`http-client.env.json`, request files, overlays or the import summary; `apigenie test` reads both
files. **The private environment file contains secrets: protect it and do not commit it to source
control.** The importer cannot discover arbitrary secrets hidden in unrelated application data.

OAuth 2 auth with the client credentials or password grant becomes
`Authorization: oauth2 client_credentials` (or `password`) and the
[OAuth 2 variables](#oauth-2): the access token URL, client id, scope, audience and client
authentication (`header` or `body`) in `http-client.env.json`, the client secret, user name and
password in `http-client.private.env.json`. Each distinct OAuth 2 configuration gets its own
prefix (`oauth2`, `oauth2_2`, ...). A token stored in the collection isn't used: a new one is
requested. The authorization code, PKCE and implicit grants need a browser: those requests are
written disabled, their auth kept as a comment without its secrets, with a warning. Adding the
token to the query and header prefixes other than `Bearer` are warnings; the token is sent in the
`Authorization` header with the type the token endpoint gives.

Imported files follow the ordinary `.http` variable precedence: file defaults override
environment values, and `--var` overrides both. This differs from Postman's precedence.
For marked files, environment discovery searches parent directories too, so nested request files
can find the generated root environment. Script changes are in memory only, not saved back to
the environment file. Files are run in the existing explicit-file or sorted-directory order;
splitting folders into separate files does not preserve arbitrary cross-folder collection order.

With `--spec`, requests that bind by method and path to an operation with an ID get
`# @operation <operationId>`. Saved responses become additive examples in
`<collection-slug>.overlay.json`; the specification is never edited. Apply the overlay explicitly:

```sh
apigenie overlay carpet-express.openapi.yaml \
  --overlay requests/carpet-express.overlay.json -o carpet-express.with-examples.yaml
```

Without a specification or a usable binding, examples are preserved as comments. Unresolved
bindings or overlay targets produce warnings rather than guessed operation IDs.

#### Imported script API

Files carrying `# @postman` get a small compatibility shim in QuickJS. Unmarked files do not
get a `pm` global. Pre-request blocks go before the request; test blocks go after it.
Scripts containing their own standalone `}}` line are JSON-escaped inside the block so that
multiline strings and JavaScript tokens remain intact.
Simple `pm.test` callbacks that only assert a literal response status become `?? status == N`
instead, so they do not require scripting support.

| API | Supported subset |
| --- | --- |
| `pm.test(name, fn)` | Records a named check; a thrown assertion fails it |
| `pm.expect(value)` | `to.equal`, `to.eql`, `to.be.true`, `to.be.false`, `to.have.property`, `to.have.status`, `to.include`, `to.be.oneOf`, `to.be.a` |
| `pm.response` | `code`, `status`, `json()`, `text()`, case-insensitive `headers.get(name)`, `to.have.status(code)` |
| `pm.environment` | `get`, `set`, `unset` |
| `pm.collectionVariables` | `get`, `set` |
| `pm.variables` | `get`, `set`; request-local changes |
| `pm.globals` | `get`, `set` |
| `pm.request.headers.add({key, value})` | Adds a header before sending a marked request |

This is not the full SDK or Chai. `pm.sendRequest`, `postman.setNextRequest`, `pm.cookies` and
`pm.visualizer` cause the **whole affected script block** to be kept as comments, with warnings
that name the call and its source line. OAuth 2 auth with a grant that needs a browser is
preserved as comments and the request is disabled. The summary lists unsupported features; they
are never silently dropped.
Other unknown script APIs fail explicitly at runtime. Scripts remain subject to the existing
QuickJS time limit and lack of filesystem/network access. Imported `https://` URLs are sent over
TLS like any other request (see `--ca-cert` and `--insecure`).

Import returns 0 after a successful conversion, including one with reported warnings, 2 for
bad command usage, and 3 for input or output failures. Existing generated filenames are refused
unless `--force` is supplied. A directory literally named `import` can still be run with
`apigenie test -- import`.

### Generated requests

`--generate` sends one request per operation of `--spec`, checked as that operation: path
parameters, required query, header and cookie parameters, and a body of the first media type (JSON
first), each from the document's examples or generated from its schemas (`readOnly` properties left
out). An XML media type gets an XML body, written as the schema's `xml` objects describe it. The
URL is `--base-url` (or the document's first server) followed by the operation's path.
Request files and `--generate` can be combined.

With an AsyncAPI document, `--generate` sends one region per WebSocket or MQTT channel, bound with
`# @channel` (and `# @operation` when the channel has one receive operation): WebSocket channels
at `--base-url` (`ws://host:port`) followed by the channel's address, MQTT channels at
`--mqtt-url`, publishing to the address and subscribing to it when the application sends there.
Parameters of the address take their default, first enum value or example. The messages are those
of the channel's receive operations, from their examples or generated from their schemas; MQTT
regions get the `# @property` lines of the first one's bindings and headers (correlation data and
response topics generated from their schemas, user properties from the first example's headers or
generated); `# @expect` is the number of replies, plus one when the application sends on the channel. HTTP
channels get one request per operation, bound with `# @operation`, at `--base-url` (a `ws://` URL
is taken as `http://`) followed by the channel's address: a `receive` operation's with its HTTP
binding's method and its first message (an example, else generated) as the body, a `send`
operation's a `GET`. Channels without an address are left out with a warning, as are the channels
of a protocol whose URL wasn't given.

Kafka channels get one `KAFKA` region per operation at `--kafka-url`, bound with `# @operation`. A
`receive` operation's region produces its first message (an example, else generated) to the
channel's topic, with a key made from the message's Kafka binding and `# @property` headers from
its headers; it reads the reply's topic for one record (`# @expect 1`) when the operation has a
reply, and ends once the record is produced otherwise. A `send` operation's region only reads the
topic, for one record the application produces.

AMQP channels get one `AMQP` region per operation at `--amqp-url`, bound with `# @operation`. A
`receive` operation's region publishes its first message to the channel's exchange and routing key
(a queue channel's queue through the default exchange) with the properties its bindings say and its
headers; it reads the reply's channel, its queue or a queue bound to its exchange, when the
operation has a reply. A `send` operation's region reads the channel for one message the application
publishes.

With a `.proto` file, `--generate` sends one `GRPC` request per method of its services (the
built-in reflection and health services aside) to `--base-url`, which is then required and is
`HOST:PORT`, `grpc://HOST:PORT` or `grpcs://HOST:PORT` (TLS). The message is generated from the input type as the mock
generates answers (`{}` for `google.protobuf.Empty`): REQUIRED fields set, `OUTPUT_ONLY` fields left out.
Client- and bidi-streaming methods get a stream of two generated messages.

With a GraphQL schema, `--generate` sends one `GRAPHQL` request per field of `Query`, `Mutation`
and `Subscription` (deprecated fields left out), named after the field (`Query.book`) and bound to
its operation (`QueryBook`). A subscription goes over graphql-ws with `# @expect 1`: the first event
is enough. Required arguments get values made up from their types as
`apigenie-server` makes up values (the scalar's or the argument's name: a UUID for `ID`, an email for
`email`, a date for `DateTime`, an enum value that isn't deprecated, an input object with its
required fields); optional ones are left out. The selection set has the leaf fields without
required arguments, objects two levels down, and `__typename` with a fragment per type of an
interface or a union. `--base-url` is required: it is the endpoint, with `/graphql` added when it
has no path.

```graphql
mutation MutationAddBook {
  addBook(input: {title: "Twenty Years After", genre: POETRY}) {
    id
    title
    genre
    pages
    author {
      id
      name
      email
    }
  }
}
```

### Reports

`--report` writes the [contract report](/techdocs/reference/cli-server#contract-testing-validating-proxy)
of `apigenie-server` with `"report": "apigenie-test"`, plus `tests` (the counts and the run's gate)
and `requests` (each request's file, line, URL, status, duration, operation, outcome, failures,
assertions, violations and log; `grpc` and `grpcStatus` for `GRPC` requests). With a `.proto` file
the contract members are those of the gRPC report (`"specification": "grpc"`, `methods`,
`services`), with a GraphQL schema those of the GraphQL report (`"specification": "graphql"`,
`rootFields`, `fields`, `uncoveredFields`, `operations`). Without `--spec`, only `report`, `formatVersion`, `version`, `startedAt`, `endedAt`, `tests` and `requests` are written.
`formatVersion` is the version of the format, which changes only when a member is removed or
changes meaning (see [Compatibility](/techdocs/reference/cli-compatibility)); `version` is its
former name, kept for one release. The JSON Schema of the report is
[apigenie-test-report.json](https://apigenie.pl/schemas/apigenie-test-report.json). `--report-junit` writes a `requests` suite (a test case per request: failed, or
skipped for disabled requests) and, with `--spec`, the contract suite of `apigenie-server`.
With an AsyncAPI document, the report is the AsyncAPI proxy's (`"specification": "asyncapi"`,
channels, operations with their messages, violations); WS and MQTT requests also have `channel`,
`address`, `messagesSent`, `messagesReceived` and `notes` (what wasn't checked, and why), and the
coverage `--min-coverage` checks is the share of the operations' messages seen.

## Options

| Option | Description |
| --- | --- |
| `FILES` | `.http` and `.rest` files, or directories to search for them |
| `--spec SPEC` | The OpenAPI 3.0, 3.1 or 3.2 (or Swagger 2.0) document HTTP requests are validated against, the GraphQL schema (`.graphql`, `.graphqls`, `.gql`) of GraphQL requests, the `.proto` file of `GRPC` requests, or the AsyncAPI 3.x document WS, MQTT, KAFKA and AMQP requests and the requests of its HTTP channels are validated against |
| `--proto-path DIR` | A directory in which the imports of a `.proto` `--spec` are looked up; can be repeated |
| `--generate` | Also send one request per operation (OpenAPI), root field (GraphQL), method (`.proto`) or channel (AsyncAPI; one per operation of an HTTP, Kafka or AMQP channel) of `--spec` |
| `--base-url URL` | The server's URL for `--generate` (the endpoint, required, for a GraphQL schema; `HOST:PORT` for a `.proto` file; `ws://` or `http://` for an AsyncAPI document's WebSocket and HTTP channels), also put before request URLs that start with `/` |
| `--mqtt-url URL` | The MQTT broker (`mqtt://host:port`) of an AsyncAPI document's channels for `--generate` |
| `--amqp-url URL` | The AMQP broker (`amqp://host:port/vhost`, `amqps://` over TLS) of an AsyncAPI document's AMQP channels for `--generate` |
| `--kafka-url URL` | A broker of the Kafka cluster (`kafka://host:port`, `kafkas://` over TLS) of an AsyncAPI document's Kafka channels for `--generate` |
| `--env NAME` | The environment of the environment files |
| `--env-file FILE` | An environment file to read after the others; can be repeated |
| `--var NAME=VALUE` | A variable above every other one; can be repeated |
| `--header NAME:VALUE` | A header for every request that doesn't have it; can be repeated |
| `--timeout MS` | How long to wait for a connection and for a response (30000) |
| `--message-timeout MS` | How long WS, MQTT, KAFKA and AMQP requests, GraphQL subscriptions and gRPC client and bidi streams wait for messages (5000); `# @timeout` overrides it |
| `--bail` | Stop at the first failed request |
| `--ca-cert FILE` | A PEM file of certificate authorities trusted for `https://`, `grpcs://`, `wss://`, `mqtts://`, `kafkas://` and `amqps://` URLs, besides the system's; can be repeated |
| `--insecure` | Don't verify the certificates of `https://`, `grpcs://`, `wss://`, `mqtts://`, `kafkas://` and `amqps://` servers; the traffic stays encrypted |
| `--client-cert FILE` | A PEM client certificate (chain) presented to `https://`, `grpcs://`, `wss://`, `mqtts://`, `kafkas://` and `amqps://` servers that ask for one (mTLS); needs `--client-key` |
| `--client-key FILE` | The unencrypted PEM private key of `--client-cert` |
| `--report FILE` | Write the JSON report |
| `--report-junit FILE` | Write the JUnit XML report |
| `--fail-on none\|errors\|warnings` | The contract violations that fail the run (`errors`) |
| `--min-coverage PERCENT` | Fail the run below this coverage of `--spec` (a GraphQL schema: the share of its fields; a `.proto` file: the share of its methods; an AsyncAPI document: the share of its operations' messages) |
| `--severity CHECK=LEVEL` | Change a contract check's severity: `error`, `warn`, `info` or `off` (`ignore`); can be repeated. The checks are those of the OpenAPI, gRPC and AsyncAPI proxies or, with a GraphQL schema, of the GraphQL proxy; an unknown check is a bad option |
| `--seed N` | Seed of the values `--generate` makes up |
| `--formatter stylish\|json` | A line per request and a summary (`stylish`), or a JSON object per line |
| `--quiet` | Show only failed requests and the summary |
| `--verbose` | Also show the warnings and notes of the contract checks, and the scripts' log |

### --fail-on errors|warnings|none

A failed request (an assertion, a script's test or error, no response, an unknown `@ref`) always
fails the run. `--fail-on` says which contract violations do too: `errors` (the default),
`warnings` (errors and warnings) or `none`. A request with such violations is shown as `FAIL`.

### --formatter json

Each request is a line `{"event":"request",...}` with the members of the JSON report's
`requests`; warnings are `{"event":"warning","file":...,"line":...,"message":...}`, notes about a
Swagger 2.0 specification `{"event":"info",...}` with the same members; the last line is
`{"event":"summary","formatVersion":1,"total":...,"passed":...,"failed":...,"skipped":...,"gate":{...}}`
with the contract's `errors`, `warnings` and `coverage` when there is a `--spec`. Its
`formatVersion` versions the whole stream, as `formatVersion` versions the report.

## httpYac compatibility

**Supported**

| Feature | Notes |
| --- | --- |
| `###` regions with titles, `#` and `//` comments | |
| `GRPC` requests | Every method of the `.proto` file of `--spec` (streams of messages separated by `===`), over HTTP/2 (cleartext, or TLS for `grpcs://`); `proto <` lines are ignored with a warning |
| Request line with or without method and HTTP version, multi-line query (`?`, `&`) | |
| Headers, inline bodies, `< file` and `<@ file` bodies | `<@utf8 file`: the encoding is ignored |
| `@name = value` variables | Outside of requests (file) or in a request's region |
| `{{name}}`, `{{$uuid}}`, `{{$timestamp}}`, `{{$datetime}}`, `{{$randomInt}}`, `{{$processEnv}}`, `{{$dotenv}}` | See [Environments and variables](#environments-and-variables) |
| `# @name`, `# @ref`, `# @forceRef`, `# @disabled` | Named responses are kept per file |
| `{{ }}` scripts before and after the request | `exports`, `test`, `assert`, `console`, `request`, `response` |
| JetBrains `< {% %}` and `> {% %}` handlers | `client.global`, `client.test`, `client.assert`, `client.log`, `request.variables` |
| `??` assertions | `status`, `header`, `body`, `duration` and `js` subjects |
| `WS` and `WEBSOCKET` requests | `ws://` and `wss://`; headers go with the handshake; text messages |
| `MQTT` requests | `mqtt://` and `mqtts://`, MQTT 3.1.1; `topic`, `publish`, `subscribe`, `qos`, `retain`, `clientId`, `keepAlive`, `clean`, `username` and `password` headers |
| `===` and `=== wait-for-server` between messages | |
| `.env`, `.env.NAME`, `http-client.env.json`, `http-client.private.env.json` | `$shared` and the `--env` environment |
| `Authorization: Basic user:password` | Encoded before sending |
| `Authorization: oauth2` and `openid` with the `client_credentials`, `password` and `refresh_token` grants | See [OAuth 2](#oauth-2); `refresh_token` and OpenID Connect discovery are apigenie's additions |
| `GRAPHQL` requests, `POST` requests of a query | Variables after a blank line; `# @operation` is the `operationName`; subscriptions over graphql-ws |

**Not supported yet**

| Feature | What happens |
| --- | --- |
| Server-sent events (`SSE`), AMQP | Not request lines |
| `gql` fragment imports (`# @import ./fragments.gql`) | A warning; the request runs without them |
| GraphQL subscriptions over the legacy `graphql-ws` subprotocol or SSE | The request fails: only `graphql-transport-ws` is spoken |
| MQTT 5, client certificates | MQTT is 3.1.1; no certificate of the client is sent |
| `# @keepStreaming`, binary WebSocket messages sent | A warning; messages are sent as text |
| Redirects | Not followed: a 3xx response is checked as it is |
| OAuth 2 and OpenID `authorization_code`, `implicit` and `device_code` grants, token exchange, `oauth2_keepAlive`, `oauth2_interceptRequest` | The request is skipped with a warning (the variables are ignored) |
| AWS and digest authorization | The request is skipped with a warning |
| `# @import`, `# @loop`, `# @sleep`, `# @jwt`, `# @keepStreaming`, other directives | A warning; the request runs |
| `xpath` assertions, `sha256`/`md5` predicates | A warning; the assertion is ignored |
| Offsets of `$datetime` and `$timestamp`, `$localDatetime` time zones | A warning; the current time in UTC is used |
| `{{ expression }}` JavaScript inside a line, global `{{ }}` scripts outside of requests | Not evaluated (the latter with a warning) |
| `require()`, npm packages, `client.exit()` in scripts | The script fails |
| Response handler files (`> ./handler.js`) | A warning |
| Changes a pre-request script makes to `request` | Not sent for ordinary files; marked imports support `pm.request.headers.add` |
| `.httpyac.js`, `.httpyac.json`, `httpyac.config.js` | Not read |
| Cookie jars | No cookies are kept between requests |

## Exit status

| Code | Meaning |
| --- | --- |
| 0 | Every request passed and the contract gate held |
| 2 | Bad options: no request files without `--generate`, `--generate` without `--spec`, or with a GraphQL schema or a `.proto` file but no `--base-url`, `--min-coverage` without `--spec`, an unknown `--severity` check; a request file, an environment file or the specification (an OpenAPI or AsyncAPI document, a GraphQL schema, or a `.proto` file with its imports) that can't be read or parsed; `--generate` without a URL it can use (no `--base-url` and no absolute server URL in the document, or a URL of the wrong scheme); a report that can't be written, or an unexpected error |
| 5 | A request failed, or contract violations (`--fail-on`) or the coverage (`--min-coverage`) failed the run |

## See also

- [apigenie-server](/techdocs/reference/cli-server), whose validating proxy checks traffic
  something else drives, with the same checks and reports
- [lint](/techdocs/reference/cli-lint), which checks the document itself
