Assertions & Dot-Paths

Assertions are the core validation engine of Gherkio. Placed inside the expect block of any step, assertions inspect, type-check, and validate the returned HTTP response status, headers, body structure, and even embedded JWT tokens.


🏗️ The Anatomy of expect

The expect block defines a set of strict contracts that the HTTP response must satisfy. Gherkio evaluates these assertions sequentially upon request completion.

⚡ Assertion Failure Behavior

Gherkio evaluates the assertions for a response and records every result in the report. A failed assertion marks the step and scenario as failed.

The CLI continues with remaining steps by default so one run can expose multiple failures. Teardown always runs. MCP callers can request fail-fast execution through the run_test tool when stopping after the first failed step is preferable.


🧭 Dot-Path Prefixes & Resolvers

Gherkio uses a declarative dot-notation system to target specific parts of the response. The prefix of the path determines which resolver is invoked:

Path PrefixTarget ComponentKey Case-SensitivityPurposeExample
statusHTTP Status CodeN/AValidates the exact status codestatus: 200
body.<path>JSON Response BodyCase-SensitiveTraverses response JSON structuresbody.user.email: email
headers.<key>HTTP Response HeadersCase-InsensitiveInspects response headersheaders.content-type: contains json
jwt.<claim>Decoded JWT payloadCase-SensitiveAutomatically decodes tokens and inspects claimsjwt.role: admin
schemaJSON Schema ContractN/AEnforces a full JSON schema contractschema: users/create
timing.durationRound-trip request timeN/AAsserts latency SLA/performancetiming.duration: lte 500ms

1. Response Body Traversal (body.<path>)

The body. prefix targets the response body payload (expected to be JSON). Gherkio traverses nested properties, list indices, and dictionary keys.

Key Syntax Features:

  • Nested Navigation: Use dots to descend into maps (e.g., body.user.profile.firstName).
  • Array Indexing: Use brackets to access specific list items (e.g., body.items[0].id).
  • Case Sensitivity: JSON keys are strictly case-sensitive. body.userId is different from body.userid.

Supported Matchers

Assertion ValueTypeExampleBehavior
Literal valueEqualitybody.role: adminExact string match
existsExistencebody.email: existsField must be present (any value)
not existsAbsencebody.error: not existsField must be absent
uuidFormatbody.id: uuidValid UUID v4 format
emailFormatbody.email: emailValid email format
datetimeFormatbody.createdAt: datetimeValid RFC3339 / ISO8601 datetime
uriFormatbody.avatar: uriValid URI format (e.g. https://...)
stringTypebody.name: stringValue is a string
numberTypebody.price: numberValue is numeric (int, float)
booleanTypebody.active: booleanValue is boolean
arrayTypebody.tags: arrayValue is an array
objectTypebody.meta: objectValue is an object/map
nullTypebody.deletedAt: nullValue is null
true / falseLiteralbody.active: trueValue is boolean true/false
contains <s>Substringbody.message: contains errorString contains substring
startsWith <s>Prefixbody.id: startsWith usr_String starts with prefix
endsWith <s>Suffixbody.email: endsWith @example.comString ends with suffix
regex <p>Patternbody.code: regex ^[A-Z]{3}\d{4}$String matches regex
oneOf <opts>Membershipbody.role: oneOf admin, editorValue matches any option
in <opts>Membershipbody.status: in active, pendingAlias for oneOf
schema: <name>Schemaschema: users/profileFull body validated against schema
schema: not <name>Negated schemaschema: not errors/validationBody must NOT match error schema

Negative Assertions with not exists

You can assert that a field is absent from the response body using not exists. This is useful for verifying that optional fields are omitted on purpose, or that error/cleanup fields are not present:

expect:
  body.deletedAt: not exists    # soft-delete field should not appear
  body.error: not exists        # no error on success path
  body.archived: not exists     # field completely absent from payload
```text

#### 💡 Intelligent Spelling Suggestion Engine
If you write an assertion for a path that does not exist in the response payload, Gherkio's smart diff engine doesn't just fail; it analyzes the keys that *were* actually present in the response and prints helpful spelling suggestions in your terminal:

```text
✗ timing.duration = lte 500ms (actual: 312ms)
✗ body.accessToken = exists
  Reason: field 'accessToken' not found in response body
  Suggestions:
    - message
    - statusCode
```text

---

### 1.1 JSONPath Assertions (`$.<path>`)

For complex payloads where dot-notation or array indexing are insufficient (e.g. querying elements dynamically based on filter criteria, wildcards, or deep scans), Gherkio supports standard JSONPath expressions.

Any assertion path starting with `$.` will automatically be evaluated as a JSONPath expression against the response body.

#### Syntax & Examples:

```yaml
expect:
  # Verify that the ID of the first book in the list is a number
  $.store.book[0].price: 8.95

  # Verify that all books under store have a category
  $.store.book[*].category: exists

  # Extract values matching filter conditions and assert
  $.store.book[?(@.price < 10)].title: contains "Sayings"
```text

---

### 2. HTTP Headers (`headers.<key>`)

The `headers.` prefix allows you to inspect HTTP response headers.

#### Key Syntax Features:
*   **Case-Insensitivity**: In accordance with the HTTP RFC, header lookups are fully case-insensitive. Gherkio normalizes these keys internally, meaning `headers.content-type`, `headers.Content-Type`, and `headers.CONTENT-TYPE` are completely equivalent.
*   **Multi-Value Headers**: If a header has multiple values, Gherkio normalizes them to a single comma-separated string for easy assertion matching (e.g. `headers.cache-control: contains no-cache`).

---

### 3. Automatic JWT Claim Decoding (`jwt.<claim>`)

One of Gherkio's most powerful native features is the automatic discovery and decoding of JSON Web Tokens.

#### How It Works:
1.  When a request completes, Gherkio scans the response body for standard JWT field names:
    *   `token`
    *   `accessToken`
    *   `access_token`
    *   `idToken`
    *   `id_token`
2.  If any of these fields are present and contain a valid base64-encoded, three-part JWT structure (`header.payload.signature`), Gherkio automatically decodes the payload section into memory.
3.  You can assert directly against claims in the payload using the `jwt.` prefix.

#### Example:
If the server returns:
```json
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MTY4ODQwMDB9.sig"
}
```text
You can immediately assert:
```yaml
expect:
  jwt.sub: "123"
  jwt.role: "admin"
  jwt.exp: datetime  # Asserts that the expiration claim is present
```text

---

### 4. JSON Schema Contracts (`schema`)

For absolute validation of deep objects and API payloads, individual key assertions can become unwieldy. The `schema` directive enforces full, deep-object JSON Schema validation.

#### Syntax:
```yaml
expect:
  schema: path/to/schema
```text

#### How It Works:
*   **Path Resolution**: Gherkio looks for the schema file under your project's configured schemas directory (usually `.gherkio/schemas/`). It automatically appends `.yaml`, `.yml`, or `.json` to locate the file (e.g., `schema: users/profile` resolves to `.gherkio/schemas/users/profile.yaml`).
*   **Negation (`not`)**: You can assert that a response **does not** match a schema by prefixing the value with `not ` (e.g. `schema: not errors/validation`). This is ideal for testing error states where a validation error structure should *not* be returned.

---

### 5. Response Timing Assertions (`timing`)

To enforce service-level agreements (SLAs) or target performance constraints, Gherkio includes a native latency-checking block. This is configured at the **step level** (outside the `expect` block):

*   **Syntax**: `timing: { max: <duration> }`
*   **Duration Syntax**: A numeric value followed by `ms` (milliseconds) or `s` (seconds).
*   **Result**: It generates a `timing.duration = lte <duration>` assertion result in the test output.

```yaml
steps:
  - request:
      method: GET
      url: /v1/users
    timing:
      max: 500ms   # Step fails if request takes longer than 500ms
    expect:
      status: 200
```text

---

## 🔄 Dynamic Variables in Assertions

To avoid hardcoding values and to support end-to-end testing, Gherkio supports dynamic variable lookup directly in your assertion values.

### 1. Interpolating Saved & Global Variables (`$<variable>`)
Expected values inside the `expect` block are fully evaluated through the variable interpolation engine. This includes:
*   **Saved Variables**: Values extracted from previous steps using the `save` block (e.g. `$userId`).
*   **Environment & Credentials Variables**: Values loaded from the current environment YAML or credentials accounts (e.g. `$accounts.admin.email`).

```yaml
steps:
  - request:
      method: POST
      url: /v1/users
      body:
        email: "bob@example.com"
    save:
      newUserId: body.id

  - request:
      method: GET
      url: /v1/users/$newUserId
    expect:
      status: 200
      # Validates that the returned ID matches the ID saved from the previous step
      body.id: $newUserId
      # Validates against a global test account email from your environment config
      body.invitedBy: $accounts.admin.email
```text

### 2. Current Request References (`request.body.<field>`)
When verifying creation or update steps, you often want to assert that the response body contains the exact same data sent in the request. You can refer to the *current request's body* fields dynamically using the `request.body.` prefix.

```yaml
steps:
  - request:
      method: POST
      url: /v1/products
      body:
        title: "Mechanical Keyboard"
        price: 99.99
    expect:
      status: 201
      # Asserts that the response payload echoes back the sent request fields dynamically
      body.data.title: request.body.title
      body.data.price: request.body.price
```text

---

## 🗄️ Collection & Array Assertions

Gherkio provides advanced built-in collection resolvers to perform validations across lists without writing loops.

### 1. Array Size Validation (`count(<array-path>)`)

Asserts the exact length of a list returned in the response.

*   **Exact Count**: `count(body.items): 5`
*   **Comparators**: Append `.gt`, `.gte`, `.lt`, or `.lte` suffixes to the path to perform numeric bounds checks:
    *   `count(body.items).gte: 1` — Asserts that the array is not empty (has at least 1 item).
    *   `count(body.items).lt: 10` — Asserts that the array contains fewer than 10 items.

> [!NOTE]
> Unlike standard body paths, `count()` paths resolve directly against the parsed response body root. The `body.` prefix inside the parenthesis is optional, but supported.

---

### 2. Universal Collection Asserts (`all(<array-path>)`)

Asserts that **every single element** in a list satisfies a specific condition.

*   **Equality Check**: `all(body.items.status): active` — Asserts that every item in the `items` list has a `status` equal to `"active"`.
*   **Matcher Assertion**: `all(body.items.price): number` — Asserts that the `price` field in every element is a number.
*   **Negative Assertion**: `all(body.items.errors): not exists` — Asserts that the `errors` field is absent from every element.

| Condition | Example | Description |
|-----------|---------|-------------|
| Literal value | `all(body.items.role): admin` | Every element's field equals the value |
| Type matcher | `all(body.items.price): number` | Every element's field matches a type matcher |
| Format matcher | `all(body.items.email): email` | Every element's field matches a format matcher |
| Existence | `all(body.items.id): exists` | Every element has the field |
| **Absence** | `all(body.items.errors): not exists` | **No element has the field** |
| Collection matcher | `all(body.items.count): gte 1` | Every array element satisfies a count condition |

---

## 🌟 Comprehensive Real-World Example

Below is a complete, production-grade Gherkio test step demonstrating status codes, headers, nested body lookups, collection checks, automatic JWT assertions, and schema contract verification running side-by-side:

```yaml
scenario: Complete API contract and session validation
steps:
  - request:
      method: POST
      url: /v1/auth/session
      body:
        username: "admin"
        device: "laptop"
    expect:
      # 1. Enforce exact HTTP status code
      status: 201

      # 2. Case-insensitive header checks
      headers.Content-Type: contains json
      headers.x-transaction-id: exists

      # 3. Deep-path JSON body validations using matchers & dynamic variables
      body.session.id: uuid
      body.session.metadata.device: request.body.device     # Dynamic reference to current request payload
      body.session.metadata.tenant: $tenantId               # Interpolates variable saved from previous steps
      body.session.active: true

      # 4. Collection checks with comparison bounds
      count(body.session.permissions).gte: 3
      all(body.session.permissions): string

      # 5. Automatic JWT discovery and claims validation
      jwt.role: "administrator"
      jwt.sub: uuid

      # 6. Full API schema validation (located at .gherkio/schemas/auth/session-response.yaml)
      schema: auth/session-response
```text