Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions docs/gui/mock-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Access values from the incoming request directly in the body:
|---|---|---|
| `{{request.params.id}}` | URL path parameter `:id` | `"42"` |
| `{{request.query.search}}` | Query string parameter `search` | `"laptop"` |
| `{{request.body.email}}` | JSON request body field | `"user@example.com"` |
| `{{request.body.email}}` | Request body field (JSON or XML) | `"user@example.com"` |
| `{{request.method}}` | HTTP method | `"POST"` |
| `{{request.path}}` | Request URL path | `"/products/42"` |
| `{{request.headers.authorization}}` | Request header value | `"Bearer abc…"` |
Expand All @@ -75,6 +75,43 @@ A `GET /products/7?user=alice` call returns:
}
```

### Reusing the request body (JSON & XML)

`request.body` is the parsed request body, so you can echo values from the
incoming request straight back into the response. JSON and XML are both parsed
automatically — JSON first, then XML when the `Content-Type` contains `xml` or
the body starts with `<`. Anything else leaves `request.body` as `{}` (the raw
text is always available as `{{request.bodyRaw}}`).

**JSON** — a `POST /signup` with body `{"email":"ada@example.com"}`:

```json
{ "createdId": "{{faker.string.uuid()}}", "email": "{{request.body.email}}" }
```

**XML** — XML is converted to a plain object that mirrors the document, so
access works the same way. A `POST /order` with body:

```xml
<order><orderId>42</orderId><customer>Roy</customer></order>
```

and response body:

```xml
<ack><id>{{request.body.order.orderId}}</id><for>{{request.body.order.customer}}</for></ack>
```

returns `<ack><id>42</id><for>Roy</for></ack>`. Conversion rules:

- A leaf element becomes its trimmed text — `request.body.order.orderId` → `"42"`.
- Repeated tags become an array — `<item>a</item><item>b</item>` → `request.body.cart.item[1]` is `"b"`.
- Attributes are exposed as `@name` keys — `<order id="7">` → `request.body.order['@id']`.
- Namespaced tags keep their prefix, so use bracket access:
`request.body['soap:Envelope']['soap:Body']`.
- The root element is preserved, so an `<order>` document is reached via
`request.body.order`.

### Faker.js

Generate realistic random data using [Faker.js](https://fakerjs.dev/):
Expand Down Expand Up @@ -128,7 +165,7 @@ The script has access to:
| `request.path` | `string` | URL path |
| `request.params` | `object` | Path parameters extracted from the route pattern |
| `request.query` | `object` | Query string parameters |
| `request.body` | `object` | Parsed JSON body (or `{}` if not JSON) |
| `request.body` | `object` | Parsed JSON or XML body (or `{}` if neither) |
| `request.bodyRaw` | `string` | Raw request body string |
| `request.headers` | `object` | Request headers |

Expand Down
76 changes: 76 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# API Spector — example workspace

A self-contained workspace that demonstrates the core features of API Spector
against the **built-in mock server**. No internet, no third-party services.

## Layout

```
example.spector ← open this in the GUI
collections/basics.json ← GET, POST, PUT, PATCH, DELETE, query params, custom headers
collections/data-extraction.json ← capture a value and reuse it in later requests
collections/auth-mechanisms.json ← every supported auth type
mocks/example-mock.mock.json ← one mock server (port 19200) backing all of the above
```

## Run it

**GUI**

1. Open `example.spector`.
2. In **Mocks**, start *Example API mock* (port **19200**).
3. In **Collections**, run any collection (or a single request). Everything is
wired to `http://127.0.0.1:19200`.

**CLI**

```bash
# terminal 1 — start the mock
npx -y @testsmith/api-spector mock --workspace ./example.spector

# terminal 2 — run the collections
npx -y @testsmith/api-spector run --workspace ./example.spector
```

## What's inside

### `basics` — everyday requests
| Request | Shows |
|---|---|
| GET a resource | path parameter + asserting on JSON fields |
| GET with query parameters | params defined in the Params tab |
| POST — create a resource | sending a JSON body, asserting `201` + echoed fields |
| PUT — replace a resource | full update |
| PATCH — update fields | partial update |
| DELETE a resource | `204 No Content` |
| Set a custom request header | adds `X-Request-Id: {{$uuid}}`; the mock echoes it back |

### `data-extraction` — chaining requests
| Request | Shows |
|---|---|
| Login (extract token) | post-request script stores the token via `sp.collectionVariables.set('authToken', …)` |
| Use the extracted token | sends `Authorization: Bearer {{authToken}}` captured by the previous request |
| Pre-request script sets a value | a pre-request script computes a value the request then sends as a header |

Run the whole `data-extraction` folder top-to-bottom — values captured by one
request flow into the next.

### `auth-mechanisms` — authentication
One request per supported auth type: none, bearer, basic, API key (header &
query), digest, NTLM, and OAuth2 (client credentials). Each asserts a `200` and
that the server reported the request as authenticated.

> Notes: the mock validates digest/basic/NTLM by shape (the script sandbox has
> no `crypto`/`Buffer`), so those exercise the *flow* rather than re-verifying
> the cryptographic proof. NTLM is not supported through a proxy.

## Templating cheat-sheet

The mock response bodies use `{{…}}` tokens, e.g.:

- `{{request.params.id}}`, `{{request.query.q}}`, `{{request.body.field}}`
- `{{request.headers.authorization}}`, `{{request.bodyRaw}}`
- `{{faker.string.uuid()}}`, `{{dayjs().toISOString()}}`

Requests use the same tokens plus built-ins like `{{$uuid}}` and `{{$timestamp}}`,
and any variable you set in a script (`{{authToken}}`, `{{traceId}}`, …).
184 changes: 184 additions & 0 deletions example/collections/auth-mechanisms.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
{
"version": "1.0",
"id": "auth-mechanisms",
"name": "Auth mechanisms",
"description": "Tests every supported auth type (none, bearer, basic, apikey, digest, ntlm, oauth2) against the local mock server.",
"rootFolder": {
"id": "root",
"name": "root",
"description": "",
"folders": [
{
"id": "folder-auth",
"name": "Auth Mechanisms",
"description": "",
"folders": [],
"requestIds": [
"req-none",
"req-bearer",
"req-basic",
"req-apikey-header",
"req-apikey-query",
"req-digest",
"req-ntlm",
"req-oauth2"
]
}
],
"requestIds": []
},
"requests": {
"req-none": {
"id": "req-none",
"name": "None (no auth)",
"method": "GET",
"url": "{{BASE_URL}}/none",
"headers": [],
"params": [],
"auth": {
"type": "none"
},
"body": {
"mode": "none"
},
"postRequestScript": "sp.test('status is 200', function () { sp.expect(sp.response.code).to.equal(200); });\nsp.test('server reports authenticated', function () { sp.expect(sp.response.json().authenticated).to.equal(true); });",
"protocol": "http"
},
"req-bearer": {
"id": "req-bearer",
"name": "Bearer token",
"method": "GET",
"url": "{{BASE_URL}}/bearer",
"headers": [],
"params": [],
"auth": {
"type": "bearer",
"token": "mock-bearer-token-123"
},
"body": {
"mode": "none"
},
"postRequestScript": "sp.test('status is 200', function () { sp.expect(sp.response.code).to.equal(200); });\nsp.test('server reports authenticated', function () { sp.expect(sp.response.json().authenticated).to.equal(true); });",
"protocol": "http"
},
"req-basic": {
"id": "req-basic",
"name": "Basic auth",
"method": "GET",
"url": "{{BASE_URL}}/basic",
"headers": [],
"params": [],
"auth": {
"type": "basic",
"username": "demo",
"password": "p@ssw0rd"
},
"body": {
"mode": "none"
},
"postRequestScript": "sp.test('status is 200', function () { sp.expect(sp.response.code).to.equal(200); });\nsp.test('server reports authenticated', function () { sp.expect(sp.response.json().authenticated).to.equal(true); });",
"protocol": "http"
},
"req-apikey-header": {
"id": "req-apikey-header",
"name": "API key (header)",
"method": "GET",
"url": "{{BASE_URL}}/apikey",
"headers": [],
"params": [],
"auth": {
"type": "apikey",
"apiKeyIn": "header",
"apiKeyName": "X-API-Key",
"apiKeyValue": "mock-api-key-123"
},
"body": {
"mode": "none"
},
"postRequestScript": "sp.test('status is 200', function () { sp.expect(sp.response.code).to.equal(200); });\nsp.test('server reports authenticated', function () { sp.expect(sp.response.json().authenticated).to.equal(true); });",
"protocol": "http"
},
"req-apikey-query": {
"id": "req-apikey-query",
"name": "API key (query)",
"method": "GET",
"url": "{{BASE_URL}}/apikey-query",
"headers": [],
"params": [],
"auth": {
"type": "apikey",
"apiKeyIn": "query",
"apiKeyName": "apikey",
"apiKeyValue": "mock-api-key-123"
},
"body": {
"mode": "none"
},
"postRequestScript": "sp.test('status is 200', function () { sp.expect(sp.response.code).to.equal(200); });\nsp.test('server reports authenticated', function () { sp.expect(sp.response.json().authenticated).to.equal(true); });",
"protocol": "http"
},
"req-digest": {
"id": "req-digest",
"name": "Digest auth",
"method": "GET",
"url": "{{BASE_URL}}/digest",
"headers": [],
"params": [],
"auth": {
"type": "digest",
"username": "demo",
"password": "p@ssw0rd"
},
"body": {
"mode": "none"
},
"postRequestScript": "sp.test('status is 200', function () { sp.expect(sp.response.code).to.equal(200); });\nsp.test('server reports authenticated', function () { sp.expect(sp.response.json().authenticated).to.equal(true); });",
"protocol": "http"
},
"req-ntlm": {
"id": "req-ntlm",
"name": "NTLM auth",
"method": "GET",
"url": "{{BASE_URL}}/ntlm",
"headers": [],
"params": [],
"auth": {
"type": "ntlm",
"username": "demo",
"password": "p@ssw0rd",
"ntlmDomain": "WORKGROUP",
"ntlmWorkstation": "CLIENT"
},
"body": {
"mode": "none"
},
"postRequestScript": "sp.test('status is 200', function () { sp.expect(sp.response.code).to.equal(200); });\nsp.test('server reports authenticated', function () { sp.expect(sp.response.json().authenticated).to.equal(true); });",
"protocol": "http",
"description": "NTLMv2 handshake against the mock's /ntlm route. The mock can't verify the proof cryptographically, so this exercises the client-side negotiate → challenge → authenticate flow."
},
"req-oauth2": {
"id": "req-oauth2",
"name": "OAuth2 (client credentials)",
"method": "GET",
"url": "{{BASE_URL}}/oauth/protected",
"headers": [],
"params": [],
"auth": {
"type": "oauth2",
"oauth2Flow": "client_credentials",
"oauth2TokenUrl": "{{BASE_URL}}/oauth/token",
"oauth2ClientId": "demo-client",
"oauth2ClientSecret": "demo-secret",
"oauth2Scopes": "read"
},
"body": {
"mode": "none"
},
"postRequestScript": "sp.test('status is 200', function () { sp.expect(sp.response.code).to.equal(200); });\nsp.test('server reports authenticated', function () { sp.expect(sp.response.json().authenticated).to.equal(true); });",
"protocol": "http"
}
},
"collectionVariables": {
"BASE_URL": "http://127.0.0.1:19200"
}
}
Loading
Loading