@schwabyio/gta 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/SPEC.md ADDED
@@ -0,0 +1,1354 @@
1
+ # The Gravity file format
2
+
3
+ Version 0.9
4
+
5
+ This document specifies the YAML files that **Gravity**, the desktop app, and **`gta`**,
6
+ the command-line runner, read and write. Together they make up Gravity Test
7
+ Automation, a tool for testing HTTP APIs. A folder that follows this document works in
8
+ both of them.
9
+
10
+ It is written for people and for coding agents alike. Every key has a table saying its
11
+ type, whether it is required and its default. Every rule that makes a file invalid is
12
+ stated where it applies, and all of them are collected in
13
+ [Appendix A](#appendix-a-validation-rules). [Appendix B](#appendix-b-a-complete-project)
14
+ is a complete, valid project to start from.
15
+
16
+ **Conventions**
17
+
18
+ - **must**, **must not**, **should** and **may** are used as in RFC 2119.
19
+ - Every file is UTF-8 YAML 1.2, one document holding a mapping, with the extension
20
+ **`.yml`**. A `.yaml` file is not read.
21
+ - Paths written in files are relative, and use `/`.
22
+ - Section numbers are stable. Gravity's error messages cite them, as in
23
+ "(SPEC.md §2.5)".
24
+
25
+ ---
26
+
27
+ ## At a glance
28
+
29
+ | File | Where | What it is | § |
30
+ | ----------------------------- | ------------------------------------------ | ------------------------------------------- | ---- |
31
+ | `collections/<id>.yml` | `collections/`, or one directory inside it | A collection: requests run in order | §2 |
32
+ | `collections/<id>.csv\|.json` | Beside its collection | A data file: the collection runs once a row | §2.8 |
33
+ | `environments/<name>.yml` | `environments/` | Variables, secrets and flags for one target | §6 |
34
+ | `project.yml` | The project folder | Name, global project, variables, trust | §1.1 |
35
+ | `settings.yml` | The project folder | How `gta` runs the project | §1.3 |
36
+ | `requests/<id>.yml` | `requests/`, or one directory inside it | A request set, run by `use:` | §2.5 |
37
+ | `endpoints/<id>.yml` | `endpoints/`, or one directory inside it | Defaults and checks per method and path | §2.6 |
38
+ | `bases/<id>.yml` | `bases/`, or one directory inside it | A base collection, for `extends:` | §2.7 |
39
+ | `checks/<name>.js` | `checks/` | Shared check functions | §5 |
40
+ | `.env` | The project folder, never committed | Values for secrets | §6 |
41
+
42
+ The smallest project `gta` runs is three files:
43
+
44
+ ```yaml
45
+ # collections/health.yml
46
+ id: health
47
+ steps:
48
+ - name: service is up
49
+ GET: '{{baseUrl}}/health'
50
+ tests: |
51
+ gta.expectResponseStatusCodeToBe(200)
52
+ ```
53
+
54
+ ```yaml
55
+ # environments/local.yml
56
+ vars:
57
+ baseUrl: http://localhost:8080
58
+ ```
59
+
60
+ ```yaml
61
+ # settings.yml: how gta runs the project (§1.3)
62
+ environmentType: local
63
+ ```
64
+
65
+ The desktop app needs only the first two, and the environment is picked in the app.
66
+
67
+ **The mistakes that come up most:**
68
+
69
+ 1. **A value that starts with `{{` must be quoted.** YAML reads an unquoted `{` as the
70
+ start of a map, so `GET: {{baseUrl}}/x` is invalid. Write `GET: '{{baseUrl}}/x'`.
71
+ 2. **A collection's `id` must equal its file name** without `.yml` (§2).
72
+ 3. **Each step has exactly one method key**, in capitals, whose value is the URL: `GET:`,
73
+ not `get:` or `method: GET` (§2.1).
74
+ 4. **A JSON body is a string**, not a YAML map: `json: |` followed by the JSON (§2.2).
75
+ 5. **Code goes in `tests` and `before.script`** as a block (`|`). Checks are calls on `gta`
76
+ (§3, §5).
77
+ 6. **`vars` hold plain values only**. Anything computed is set in `before.script` with
78
+ `gta.set` (§4).
79
+ 7. **An unknown `{{variable}}` fails the step.** It is never sent as literal text (§4).
80
+ 8. **Step tags need `stepTags: true`** on the collection (§2.4).
81
+
82
+ ---
83
+
84
+ ## 1. Layout
85
+
86
+ A **project** is a folder holding `collections/` and, usually, `environments/`:
87
+
88
+ ```
89
+ payments/ a project
90
+ ├── project.yml optional (§1.1)
91
+ ├── settings.yml how gta runs it (§1.3)
92
+ ├── collections/
93
+ │ ├── smoke.yml a collection
94
+ │ └── checkout/ a directory grouping collections: one level only
95
+ │ ├── sessions.yml
96
+ │ ├── sessions.csv its data file (§2.8)
97
+ │ └── refunds.yml
98
+ ├── environments/
99
+ │ ├── local.yml
100
+ │ └── staging.yml
101
+ ├── requests/ request sets (§2.5)
102
+ ├── endpoints/ endpoint bases (§2.6)
103
+ ├── bases/ base collections (§2.7)
104
+ ├── checks/ check files (§5)
105
+ ├── files/ anything a body uploads (§2.2); any name will do
106
+ └── .env secret values; not committed (§6)
107
+ ```
108
+
109
+ **Every `.yml` file directly in `collections/`, or in a directory one level down, is a
110
+ collection.** Nothing else is. Discovery is exact: no other `.yml` in a repository is
111
+ mistaken for a collection. A file that does not parse is reported as a broken
112
+ collection, never skipped in silence.
113
+
114
+ - A directory inside a directory of `collections/` is reported as a problem and not
115
+ read. `requests/`, `endpoints/` and `bases/` are read to the same depth, and
116
+ `checks/` only at its top level.
117
+ - Names starting with `.` are ignored, as are the directories `node_modules`, `.git`,
118
+ `reports`, `test-results`, `out` and `dist`.
119
+ - Directories carry no configuration and need no file of their own. They group
120
+ collections for display and for running a group.
121
+
122
+ A project is any folder: the root of a repository, or one service of a monorepo. A
123
+ monorepo is several projects, one per service, and they can share a **global project**
124
+ (§1.1):
125
+
126
+ ```
127
+ platform/ a repository, not itself a project
128
+ ├── services/auth/ a project
129
+ │ ├── project.yml uses: ../../shared
130
+ │ ├── collections/login.yml
131
+ │ └── environments/local.yml
132
+ ├── services/users/ a project
133
+ │ └── collections/users.yml
134
+ └── shared/ a global project
135
+ ├── project.yml
136
+ └── environments/local.yml
137
+ ```
138
+
139
+ A project reads nothing above its own folder except the global project it names.
140
+
141
+ ### 1.1 `project.yml` and global projects
142
+
143
+ ```yaml
144
+ name: Payments # shown instead of the folder name
145
+ uses: ../../shared # a global project, relative to this one
146
+ vars: # for every collection in the project
147
+ region: eu
148
+ tls:
149
+ ca: # certificate files to trust, besides the system's
150
+ - certs/company-root.pem
151
+ ```
152
+
153
+ The file is optional, and so is every key in it. Any other key is an error.
154
+
155
+ | Key | Type | Default | Meaning |
156
+ | -------- | ---------------------- | ----------- | ------------------------------------------------------------------ |
157
+ | `name` | string | folder name | Display name. |
158
+ | `uses` | string (relative path) | none | A global project whose variables and environments this one shares. |
159
+ | `vars` | map of plain values | none | Variables for every collection in the project (§4). |
160
+ | `tls.ca` | list of relative paths | none | Certificate files that requests trust (below). |
161
+
162
+ **`uses`** names a **global project**: an ordinary project whose `project.yml`
163
+ variables, `environments/`, `requests/`, `endpoints/`, `bases/` and `checks/` every
164
+ project using it shares.
165
+
166
+ - It must be a relative path. An absolute path is refused, since it would only work on
167
+ one machine. Write it with `/`; `\` reads the same.
168
+ - The folder it names must hold a `project.yml`.
169
+ - A global project must not `uses` another: one level, no chains, no loops.
170
+ - An environment in the global project merges under the project's environment of the
171
+ same name, key by key, and the project's values win. An environment only the global
172
+ project has is available too.
173
+
174
+ **`tls.ca`** lists certificate files that requests trust, for a server whose certificate
175
+ a company or local CA signed, or a server's own self-signed certificate. A request
176
+ always trusts:
177
+
178
+ 1. Node's bundled Mozilla roots, and any in `NODE_EXTRA_CA_CERTS`.
179
+ 2. The operating system's trust store: the macOS Keychain, the Windows certificate
180
+ store, or the Linux CA bundle. A CA that IT installed, or that `mkcert -install`
181
+ added, is trusted with nothing written here.
182
+ 3. `tls.ca`: the project's own files, then its global project's.
183
+
184
+ - Each entry is a relative path from the `project.yml` that lists it. An absolute path
185
+ is refused.
186
+ - A file is PEM (one certificate or a bundle) or a single DER certificate, such as a
187
+ `.cer` exported on Windows. A CA's certificate is public and safe to commit. A private
188
+ key never belongs here.
189
+ - A file that is missing or holds no certificate is a problem on the project. Gravity
190
+ sends nothing from the project until it is fixed, and `gta` will not start.
191
+ - `tls.ca` only adds trust. Host names and expiry are still checked, and verification
192
+ is never turned off.
193
+
194
+ ### 1.2 Portability
195
+
196
+ Projects are shared between macOS, Windows and Linux, and read the same on all three:
197
+
198
+ - Every path written in a file uses `/`. A `\` reads the same, but is never written.
199
+ - **Names match exactly, case included.** macOS and Windows find `requests/Auth/login.yml`
200
+ when the file is `requests/auth/login.yml`; Linux does not, so a project that works on a
201
+ laptop would fail in CI. Every name a file refers to must be spelled as it is on disk: a
202
+ `use:` or `extends:` name, `uses`, a `tls.ca` file, a file a body sends, and an
203
+ environment's file name. One that differs only in case is an error on every platform,
204
+ naming the spelling on disk. The folders and files of §1 are lower case.
205
+ - **No two names in one directory may differ only in case**, such as `Checkout/` and
206
+ `checkout/`: Linux can hold both, but a macOS or Windows checkout only one. This
207
+ applies in `collections/`, `requests/`, `endpoints/`, `bases/`, `environments/` and
208
+ `checks/`. Collection ids are unique ignoring case too (§2).
209
+ - File, directory and environment names must avoid what Windows refuses: the characters
210
+ `< > : " / \ | ? *`, control characters, a trailing dot or space, and the names `CON`,
211
+ `PRN`, `AUX`, `NUL`, `CONIN$`, `CONOUT$`, `COM1`–`COM9` and `LPT1`–`LPT9`, with or
212
+ without an extension. A file with such a name, made on macOS or Linux, is reported.
213
+ - Files should use LF line endings. Gravity writes LF, and can add a `.gitattributes`
214
+ scoped to the project's own files so that git keeps them LF on every platform. The same
215
+ block keeps `files/` exactly as committed, so a body sends the same bytes everywhere. For
216
+ files a body sends from another folder, add a `-text` line of your own.
217
+ - The process environment is read by exact name on every platform, although Windows
218
+ itself ignores case (§4, §6).
219
+
220
+ ### 1.3 `settings.yml`
221
+
222
+ How `gta` runs the project. It sits beside `collections/`, is committed, and `gta`
223
+ refuses to run without it. The desktop app does not read it.
224
+
225
+ ```yaml
226
+ environmentType: staging # environments/<name>.yml
227
+ limitConcurrency: 4
228
+ timeoutCollection: 3600000
229
+ bail: false
230
+ tags: [smoke]
231
+ notTags: [slow]
232
+ generateJUnitResults: true
233
+ generateJsonResults: true
234
+ generateHtmlResults: true
235
+ autoOpenTestResultHtml: false
236
+ testResultsBasePath: test-results
237
+ ```
238
+
239
+ Every key is optional. **A key not listed here is an error**, so a typo cannot quietly
240
+ run with a default.
241
+
242
+ | Key | Type | Default | Meaning |
243
+ | ------------------------ | ------------ | -------------- | --------------------------------------------------------------------------- |
244
+ | `environmentType` | string | none | The environment to run against, by name. It must exist (§6). |
245
+ | `limitConcurrency` | integer ≥ 1 | `1` | Collections run at once. Steps within a collection always run in order. |
246
+ | `timeoutCollection` | integer ≥ 1 | `3600000` | Milliseconds before a collection is stopped and reported failed. |
247
+ | `bail` | boolean | `false` | Stop a collection at its first failing step; the rest are reported skipped. |
248
+ | `tags` | list of tags | `[]` | Run only what these select (§2.4). Empty runs everything. |
249
+ | `notTags` | list of tags | `[]` | Leave out what these name (§2.4). |
250
+ | `generateJUnitResults` | boolean | `false` | Write `<testResultsBasePath>/junit/junit.xml`. |
251
+ | `generateJsonResults` | boolean | `false` | Write `<testResultsBasePath>/json/results.json`. |
252
+ | `generateHtmlResults` | boolean | `false` | Write `<testResultsBasePath>/html/summary.html` and a page per collection. |
253
+ | `autoOpenTestResultHtml` | boolean | `false` | Write the HTML report and open it when the run ends. |
254
+ | `testResultsBasePath` | string | `test-results` | Where reports go: relative to the project folder, or absolute. |
255
+
256
+ The results folder is emptied before every run. `gta` refuses to empty one that holds
257
+ files it did not write, or one that holds the project itself. Every report replaces
258
+ each secret's value with `[secret: NAME]` (§6).
259
+
260
+ **Overrides.** Each setting can be overridden by an environment variable, then by a
261
+ command-line flag: `GTA_LIMIT_CONCURRENCY=8`, then `--limitConcurrency 8`. A list is
262
+ comma-separated: `--tags smoke,api`.
263
+
264
+ **Running.** `gta` runs from the project folder:
265
+
266
+ | Command | Runs |
267
+ | --------------------- | ----------------------------------------------------------- |
268
+ | `gta get` | Nothing. It lists what `gta all` would run. |
269
+ | `gta all` | Every collection, except those with `exclude: true` (§2.4). |
270
+ | `gta smoke,checkout/` | The collections and directories named, in that order. |
271
+
272
+ A collection is named by its `id`, or by its place (`checkout/sessions`). A directory is
273
+ named by its name, and `checkout/` names only the directory. `--flag name=value` sets a
274
+ feature flag (§2.9), and `--json` prints the results as JSON. The exit code is `0` when
275
+ everything passed, `1` when something failed, and `2` when `gta` could not run.
276
+
277
+ ---
278
+
279
+ ## 2. Collection file
280
+
281
+ A **collection** is one file holding an ordered list of requests, called **steps**. Its
282
+ steps run in list order and share one variable scope, so what one step captures, the
283
+ next can use.
284
+
285
+ ```yaml
286
+ # collections/checkout.yml
287
+ id: checkout
288
+ docs: |
289
+ Create a session, capture it, then read it back.
290
+ tags: [smoke]
291
+ headers: # sent with every step
292
+ Accept: application/json
293
+ settings:
294
+ timeout: 10000
295
+ vars:
296
+ apiVersion: '2'
297
+
298
+ steps:
299
+ - name: create session
300
+ POST: '{{baseUrl}}/v{{apiVersion}}/sessions'
301
+ body:
302
+ json: |
303
+ { "amount": 1200 }
304
+ tests: |
305
+ gta.expectResponseStatusCodeToBe(201)
306
+ gta.expectResponseBodyToHaveProperty('id', 'sessionId', 'setAsCollectionVariable')
307
+
308
+ - name: read it back
309
+ GET: '{{baseUrl}}/v{{apiVersion}}/sessions/{{sessionId}}'
310
+ tests: |
311
+ gta.expectResponseStatusCodeToBe(200)
312
+ gta.expectResponseBodyToHaveProperty('amount', 1200)
313
+ ```
314
+
315
+ ### Collection keys
316
+
317
+ Any key not listed here is an error.
318
+
319
+ | Key | Type | Required | Default | Meaning |
320
+ | ---------- | ------------------- | -------- | ------- | ------------------------------------------------------------------------------ |
321
+ | `id` | string | **yes** | | The file name without `.yml` (below). |
322
+ | `steps` | list of steps | no | `[]` | The requests, in run order (§2.1). |
323
+ | `docs` | string | no | | Markdown. |
324
+ | `tags` | list of tags | no | | Tags that select the whole collection (§2.4). |
325
+ | `stepTags` | boolean | no | `false` | `true` lets steps carry their own tags (§2.4). |
326
+ | `exclude` | boolean | no | `false` | `true` leaves it out of group runs (§2.4). |
327
+ | `flags` | map | no | | Feature flags the whole collection needs (§2.9). |
328
+ | `headers` | map | no | | Sent with every step; a step's own header of the same name wins (§2.3). |
329
+ | `settings` | map | no | | Defaults for every step (§2.3). |
330
+ | `vars` | map of plain values | no | | Collection variables (§4). |
331
+ | `before` | map | no | | `script:` run before every step (§5). |
332
+ | `tests` | string | no | | JavaScript run after every step, before the step's own (§5). |
333
+ | `extends` | string | no | | A base collection to build on (§2.7). |
334
+ | `params` | map | no | | The inputs it takes as a request set (§2.5). Only in `requests/`, in practice. |
335
+
336
+ A collection has no `name` key. Its `id` is its name, and a file with `name:` is
337
+ rejected with a message saying so.
338
+
339
+ ### `id`
340
+
341
+ Every collection file must say its **id**, which is its file name without `.yml`,
342
+ exactly. `collections/checkout/sessions.yml` starts `id: sessions`.
343
+
344
+ - **It must match the file name.** A file whose `id` is missing or different is a
345
+ broken collection. Renaming a file means changing its `id` too.
346
+ - **It must be unique in its home, ignoring case.** No two files in a project's
347
+ `collections/` may share an id, including files in different directories of it; the
348
+ same holds for `requests/`, `bases/` and `endpoints/`. Case is ignored because macOS
349
+ and Windows file systems ignore it. Every file sharing an id is broken.
350
+ - **It is letters, digits and `- _ .`, starting with a letter or digit**:
351
+ `^[A-Za-z0-9][A-Za-z0-9._-]*$`. It is also typed on a command line.
352
+
353
+ Because an id is unique, it names the collection everywhere on its own: in the app, on
354
+ `gta`'s command line and in reports.
355
+
356
+ ### 2.1 Steps
357
+
358
+ **List order is run order.** A step is a request: it carries **exactly one HTTP method
359
+ key**, in capitals, whose value is the URL as a string.
360
+
361
+ ```yaml
362
+ - name: create session
363
+ POST: '{{baseUrl}}/sessions?source=api'
364
+ ```
365
+
366
+ Methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`.
367
+
368
+ | Key | Type | Required | Meaning |
369
+ | ---------- | ------------ | -------- | ------------------------------------------------------------------------- |
370
+ | `<METHOD>` | string | **yes** | The URL, query string included. |
371
+ | `name` | string | no | Display name. Defaults to the method and URL. |
372
+ | `headers` | map | no | Over the collection's headers (§2.3). |
373
+ | `body` | map | no | Exactly one kind of body (§2.2). |
374
+ | `settings` | map | no | Over the collection's settings (§2.3). |
375
+ | `before` | map | no | `script:` run before the request (§5). |
376
+ | `tests` | string | no | The checks, as JavaScript: calls on `gta` (§3) and any other code (§5). |
377
+ | `tags` | list of tags | no | The step's own tags. Only with `stepTags: true` on the collection (§2.4). |
378
+ | `flags` | map | no | Feature flags the step needs (§2.9). |
379
+ | `base` | `false` | no | `false` leaves the step's endpoint base out (§2.6). |
380
+ | `docs` | string | no | Markdown. |
381
+
382
+ - A step with no method key, or with two, is an error.
383
+ - **The URL is authoritative, query string included.** There is no separate block of
384
+ query parameters. Editors show a parameter table as a view over the URL.
385
+ - A step may instead run a request set with `use:` (§2.5). A use step has no method
386
+ key.
387
+ - **Any other key is an error**, so a misspelled key such as `heders:` fails at once
388
+ rather than being ignored. A method key in lower case (`get:`) is reported as such.
389
+
390
+ ### 2.2 `body`
391
+
392
+ A body declares **exactly one** of these keys. Omit `body` for no body.
393
+
394
+ ```text
395
+ body: { json: '{ "a": 1 }' } # Content-Type: application/json
396
+ body: { xml: '<a/>' } # application/xml
397
+ body: { text: hello } # text/plain
398
+ body: { form: { a: '1', b: two } } # application/x-www-form-urlencoded
399
+ body: { multipart: { … } } # multipart/form-data (below)
400
+ body: { graphql: { query: '…', variables: { … } } } # application/json
401
+ body: { file: files/order.json } # the file as it is, typed by its extension
402
+ ```
403
+
404
+ | Key | Type | Sent as |
405
+ | ----------- | ------------------------------------ | ---------------------------------------------------------- |
406
+ | `json` | string | The text, as written. It must be a string, not a YAML map. |
407
+ | `xml` | string | The text. |
408
+ | `text` | string | The text. |
409
+ | `form` | map of strings | URL-encoded, names and values resolved first. |
410
+ | `multipart` | map of fields (below) | `multipart/form-data`. |
411
+ | `graphql` | `{ query: string, variables?: map }` | `{"query": …, "variables": …}` as JSON. |
412
+ | `file` | string (relative path) | The file's bytes. |
413
+
414
+ - The implied `Content-Type` is added only when `headers` does not declare one.
415
+ - `{{variables}}` resolve in every kind of body. A form's values are resolved and then
416
+ encoded, so a value may hold `&` or `=`.
417
+ - A number in `form` or `multipart` must be quoted: `count: '3'`.
418
+
419
+ Write JSON as a block, so it needs no escaping:
420
+
421
+ ```yaml
422
+ body:
423
+ json: |
424
+ {
425
+ "user": "{{username}}",
426
+ "amount": 1200
427
+ }
428
+ ```
429
+
430
+ **`multipart`** is keyed by field name, in order:
431
+
432
+ ```yaml
433
+ body:
434
+ multipart:
435
+ description: A photo of {{name}} # text
436
+ metadata: # text with a Content-Type of its own
437
+ value: '{"album": "{{album}}"}'
438
+ contentType: application/json
439
+ avatar: # a file from the project folder
440
+ file: files/avatar.png
441
+ contentType: image/png # optional: else from the extension, else application/octet-stream
442
+ filename: me.png # optional: else the file's own name
443
+ tags: [red, blue] # a list sends the name once for each
444
+ ```
445
+
446
+ | A field is | Sent as |
447
+ | ----------------------------------- | ------------------------------------------------ |
448
+ | a string | A text part. |
449
+ | `{ value, contentType? }` | A text part with its own `Content-Type`. |
450
+ | `{ file, contentType?, filename? }` | A file part. |
451
+ | a list of the above | One part for each item, all with the field name. |
452
+
453
+ The `Content-Type` is `multipart/form-data` with a boundary chosen for the request. A
454
+ declared multipart type without a boundary, such as `multipart/mixed`, gets one added.
455
+ A declared boundary is used as written.
456
+
457
+ **Files**, in `body.file` and a multipart part's `file`, are read from the folder of
458
+ the project the step belongs to. For a step of a request set, that is the set's own
459
+ project, which may be a global one. The path is the same wherever the collection sits
460
+ inside `collections/`.
461
+
462
+ - It must be a relative path, written with `/`. An absolute path is refused.
463
+ - `{{variables}}` resolve in the path, so a data file row (§2.8) can choose the file.
464
+ - A file is sent byte for byte. `{{…}}` inside it is not a variable.
465
+ - A file that cannot be read stops the step before anything is sent, in its own `body`
466
+ phase, naming the file.
467
+ - Where a request is shown (results, reports, `req.body`), a file's bytes appear as
468
+ `‹file files/avatar.png, 1234 bytes›`.
469
+
470
+ ### 2.3 `settings` and `headers`
471
+
472
+ **`settings`** on a collection apply to every step; a step's own settings merge over
473
+ them. Any other key is an error.
474
+
475
+ | Key | Type | Default | Meaning |
476
+ | ----------------- | ----------- | ------- | -------------------------------------------------------------- |
477
+ | `timeout` | number ≥ 0 | `0` | Milliseconds for the whole request; `0` means no limit. |
478
+ | `followRedirects` | boolean | `true` | Follow 3xx responses. |
479
+ | `maxRedirects` | integer ≥ 0 | `5` | The most redirects followed. |
480
+ | `encodeUrl` | boolean | `true` | Percent-encode what a hand-typed URL left raw, before sending. |
481
+
482
+ **`headers`** is a map from header name to one of:
483
+
484
+ ```yaml
485
+ headers:
486
+ Accept: application/json # a value
487
+ X-Forwarded-For: [10.0.0.1, 10.0.0.2] # a list: sent once per value
488
+ X-Debug: { value: '1', enabled: false, description: turn on to trace } # the long form
489
+ ```
490
+
491
+ - A collection's headers are sent with every step. A step header of the same name,
492
+ compared case-insensitively as HTTP does, replaces the collection's for that step.
493
+ - A header with `enabled: false` is not sent. On a step it replaces nothing, so
494
+ switching it off brings back the collection's header.
495
+
496
+ ### 2.4 `tags`, `stepTags` and `exclude`
497
+
498
+ Tags pick what to run. **A collection's `tags` select the whole collection**: every
499
+ step, in order, sharing one variable scope.
500
+
501
+ ```yaml
502
+ id: checkout
503
+ tags: [api, payments] # a run for api or payments runs every step here
504
+ ```
505
+
506
+ Some collections have steps that stand alone. There, `stepTags: true` lets each step
507
+ carry its own tags, so a run can pick single steps:
508
+
509
+ ```yaml
510
+ id: status-codes
511
+ stepTags: true
512
+ steps:
513
+ - name: not found
514
+ GET: '{{baseUrl}}/status/404'
515
+ tags: [smoke, errors]
516
+ ```
517
+
518
+ - **A step must not have `tags` unless its collection has `stepTags: true`.** Most
519
+ collections are a flow, where later steps use what earlier ones set, and running part
520
+ of one fails for reasons that have nothing to do with the API.
521
+ - A tag is letters, digits and `- _ . :` with no spaces (`^[A-Za-z0-9._:-]+$`). Tags are
522
+ case-sensitive and have no prefix. A tag is for grouping only: a feature flag is not
523
+ a tag (§2.9).
524
+
525
+ **Selecting** with `tags`: a collection whose own tags match runs whole. Otherwise, with
526
+ `stepTags: true`, its steps whose tags match run, in order. Otherwise nothing in it runs.
527
+
528
+ **Leaving out** with `notTags`: a collection whose own tags match runs nothing, whatever
529
+ else selects it. With `stepTags: true`, its steps whose tags match are dropped from what
530
+ was selected.
531
+
532
+ **`exclude: true`** leaves a collection out of group runs: `gta all`, with or without
533
+ tags, and a directory named to `gta`. Named on its own, it still runs. Use it for work in
534
+ progress, a manual-only collection, or one waiting on a fix. `gta` lists what it left
535
+ out, so a suite never shrinks without saying so.
536
+
537
+ ### 2.5 Request sets and `use:`
538
+
539
+ A **request set** is a collection in `requests/`, directly or one directory down, with a
540
+ `params:` key: the inputs it takes. A step elsewhere runs it with **`use:`**, and passes
541
+ values with **`with:`**.
542
+
543
+ ```yaml
544
+ # requests/login.yml
545
+ id: login
546
+ params:
547
+ username: { required: true, description: Account to log in as }
548
+ password: { required: true }
549
+ expectStatus: 200 # a default
550
+ steps:
551
+ - name: log in
552
+ POST: '{{baseUrl}}/login'
553
+ body:
554
+ json: '{ "user": "{{params.username}}", "password": "{{params.password}}" }'
555
+ tests: |
556
+ gta.expectResponseStatusCodeToBe(params.expectStatus)
557
+ if (params.expectStatus === 200) {
558
+ gta.expectResponseBodyToHaveProperty('token', 'authToken', 'setAsCollectionVariable')
559
+ }
560
+ ```
561
+
562
+ ```yaml
563
+ # collections/checkout.yml
564
+ id: checkout
565
+ steps:
566
+ - use: login
567
+ with:
568
+ username: '{{adminUser}}'
569
+ password: '{{adminPassword}}'
570
+ - use: login
571
+ name: rejects a bad password
572
+ with: { username: alice, password: wrong, expectStatus: 401 }
573
+ tests: |
574
+ gta.expectResponseBodyToHaveProperty('error', 'invalid credentials')
575
+ - name: get profile
576
+ GET: '{{baseUrl}}/me'
577
+ ```
578
+
579
+ **Params.** Each param is a plain default (`expectStatus: 200`), or
580
+ `{ required: true, default, description }`. In a request a param reads as
581
+ `{{params.name}}`, and in code as `params.name`, a read-only object holding the real
582
+ values. No variable can take the place of a `params.` name.
583
+
584
+ **A use step** holds only `use`, `with`, `name`, `tags`, `flags`, `docs` and `tests`. A
585
+ method key, `headers`, `body`, `settings` or `before` on it is an error, and `with`
586
+ without `use` is an error too.
587
+
588
+ - **Finding the set.** `use: login` is `requests/login.yml` in the project, else in its
589
+ global project. `use: auth/login` is one directory down. `use: global:login` looks
590
+ only in the global project.
591
+ - **`with:`** gives plain values; a param left out takes its default. A string may hold
592
+ `{{variables}}`, resolved when the set starts. A missing required value, or a name the
593
+ set does not take, stops the step before anything is sent.
594
+ - **Running.** A use step runs each of the set's steps in turn, in the collection's
595
+ variable scope, so what one sets the next can read, and so can the steps after the use
596
+ step. Each request is reported as its own result.
597
+ - **Layers.** Headers and settings: the collection's, under the set's, under each
598
+ step's own. Scripts run collection, then set, then step: `before.script` before the
599
+ request and `tests` after. The use step's own `tests` run last, after the set's last
600
+ step.
601
+ - **One level.** A request set must not `use:` another, and has `params`, not `vars`. A
602
+ file in `requests/` without `params` is not a request set.
603
+
604
+ ### 2.6 Endpoint bases
605
+
606
+ What every request to one endpoint gets, wherever the request is written. A file in
607
+ `endpoints/` (the project's own, or its global project's) is a collection whose steps
608
+ are **endpoints**: a method and a **path pattern**, with the headers, settings, scripts
609
+ and checks for every request to it.
610
+
611
+ ```yaml
612
+ # endpoints/users.yml
613
+ id: users
614
+ headers:
615
+ X-Api: users # every endpoint in this file
616
+ steps:
617
+ - GET: /users/{id}
618
+ headers:
619
+ Accept: application/json
620
+ tests: |
621
+ gta.expectResponseStatusCodeToBe(200)
622
+ gta.expectResponseBodyToHaveProperty('id', endpoint.id)
623
+ - POST: /users
624
+ tests: |
625
+ gta.expectResponseStatusCodeToBe(201)
626
+ ```
627
+
628
+ - An endpoint's URL must be a path starting with `/`. An endpoints file must not hold a
629
+ use step.
630
+ - **Matching.** A step's request is under the endpoint with its method and a matching
631
+ path. The path is read from the URL as written. The host, or a leading
632
+ `{{variable}}` standing for it, is ignored, and so is the query string, so one base
633
+ serves every environment.
634
+ - `{name}` in a pattern stands for one path segment, whether a literal value or a
635
+ `{{variable}}`. A variable never matches a literal segment of a pattern.
636
+ - When several endpoints match, the one with the most literal segments wins
637
+ (`/users/me` over `/users/{id}`), then the one listed first. A project's endpoint
638
+ replaces its global project's with the same method and path.
639
+ - **`endpoint`** in scripts holds each `{name}`'s value from the step's URL, resolved:
640
+ `endpoint.id` is `42` for `{{baseUrl}}/users/{{userId}}` with `userId: 42`.
641
+ - **What applies**, outermost first: the endpoints file's own `headers`, `settings`,
642
+ `before` and `tests`, then the endpoint's, then the base collection's (§2.7), the
643
+ collection's, the request set's (§2.5) and the step's. Nearer headers and settings
644
+ win.
645
+ - **A step's own check replaces the base's check of the same thing**: the status, a
646
+ header by name, or a body property by path. A negative test only says what it expects,
647
+ so checking for a 404 replaces the base's 200. Checks of other things stay, and named
648
+ tests (`gta.test`) are never replaced.
649
+ - **`base: false`** on a step leaves its endpoint base out altogether.
650
+
651
+ ### 2.7 `extends`: base collections
652
+
653
+ ```yaml
654
+ # bases/authenticated.yml
655
+ id: authenticated
656
+ headers:
657
+ Authorization: Bearer {{token}}
658
+ before:
659
+ script: |
660
+ gta.set('requestId', gta.uuidv7())
661
+ ```
662
+
663
+ ```yaml
664
+ # collections/checkout.yml
665
+ id: checkout
666
+ extends: authenticated
667
+ steps:
668
+ - GET: '{{baseUrl}}/cart'
669
+ ```
670
+
671
+ A collection that `extends:` a base collection builds on its `headers`, `settings`,
672
+ `vars`, `before` and `tests`, with its own on top: nearer headers, settings and
673
+ variables win, and scripts run the base's first.
674
+
675
+ - `extends: name` looks in the project's `bases/`, then its global project's.
676
+ `extends: global:name` looks only in the global project's.
677
+ - A base collection must have no `steps` and no `params`, and must not `extends`
678
+ another.
679
+ - A base that cannot be used (missing, broken, or breaking these rules) stops every
680
+ step of the collection before anything is sent, saying why.
681
+
682
+ ### 2.8 Data files
683
+
684
+ A collection can be driven by a **data file**: `<id>.csv` or `<id>.json` beside
685
+ `<id>.yml`, with exactly the same name. The whole collection runs once per row, and each
686
+ column of the row is a variable for that run: `{{userId}}`, or `gta.get('userId')`.
687
+
688
+ ```
689
+ collections/
690
+ ├── users.yml
691
+ └── users.csv
692
+ ```
693
+
694
+ ```csv
695
+ userId,expectedStatus,iterationLabel
696
+ 1001,200,Happy path
697
+ 9999,404,Unknown user
698
+ ```
699
+
700
+ - **One run per row.** Every step runs for row 1, then every step again for row 2, and
701
+ so on. Each run starts from a fresh variable scope, so nothing one row sets leaks into
702
+ the next. Three steps and two rows report six results.
703
+ - **Precedence.** A row's values sit over the environment and under what code sets
704
+ (§4).
705
+ - **`iterationLabel`**, an optional column, names the row in reports, as in
706
+ `Iteration 2 (Unknown user) - get user`. It is a variable like the others.
707
+ - **CSV** follows RFC 4180: a header row, then one row per line. A value in double
708
+ quotes may hold commas and line breaks, and `""` is a quote. **Every CSV value is a
709
+ string**: a zip code `01234` stays `01234`. A short row leaves its last columns empty;
710
+ a row with more values than the header is an error.
711
+ - **JSON** is an array of objects, one per row. Values keep their type, which must be
712
+ string, number, boolean or null.
713
+ - **`.csv` wins** when both exist. The name must match exactly, case included.
714
+ - A data file must be at most 10 MB and have at least one row. The header must not have
715
+ an empty or repeated column name. A data file that will not read makes the collection
716
+ broken: `gta` reports it and runs nothing from it.
717
+ - With `bail`, a failing step also stops the rows still to come.
718
+
719
+ In the desktop app, a single step runs with one chosen row. **Run all** runs every row,
720
+ as `gta` does.
721
+
722
+ ### 2.9 Feature flags
723
+
724
+ A collection or a step says which feature flags it needs, and runs only when they hold:
725
+
726
+ ```yaml
727
+ id: checkout
728
+ flags: { newCheckout: true } # the whole collection runs only when newCheckout is on
729
+ steps:
730
+ - name: total (new)
731
+ GET: '{{baseUrl}}/checkout/total'
732
+ flags: { newCheckout: true }
733
+ - name: total (old)
734
+ GET: '{{baseUrl}}/checkout/total'
735
+ flags: { newCheckout: false } # only one of the pair ever runs
736
+ - name: v2 pricing
737
+ GET: '{{baseUrl}}/prices'
738
+ flags: { pricingVersion: v2 } # any value, not just on/off
739
+ ```
740
+
741
+ - **Every flag named must have the value given.** A collection's `flags` apply to each
742
+ of its steps, and a use step's to each request of its set. Values compare as text, so
743
+ `true` matches `true` or `"true"`, and `2` matches `"2"`.
744
+ - **A step whose flags do not hold is skipped**, not failed. It sends nothing and is
745
+ reported as skipped with the reason, such as `feature flag newCheckout is off`. A
746
+ skipped step never fails a run.
747
+ - **A flag the run does not know is an error.** Otherwise a typo, or a flag deleted from
748
+ the flag service, would quietly run or skip the wrong tests.
749
+ - **In code, `gta.flag(name)`** returns a flag's value, for checking something
750
+ different rather than skipping a step.
751
+ - A flag name is letters, digits and `- _ .` (`^[A-Za-z0-9_][A-Za-z0-9_.-]*$`). A value
752
+ is a string, number or boolean.
753
+ - Skipping an early step can make later steps fail, such as a login that sets a token.
754
+ Flags on the whole collection are usually the safer choice.
755
+
756
+ **Where the values come from.** Each environment file gives its flags (§6):
757
+
758
+ ```yaml
759
+ # environments/staging.yml
760
+ vars: { baseUrl: https://staging.example.com }
761
+ flags:
762
+ command: node scripts/flags.mjs staging # prints the flags as JSON
763
+ values: # used without a command, and for any flag its output leaves out
764
+ newCheckout: false
765
+ ```
766
+
767
+ Lowest precedence first: the global project's environment's `values`, the project's
768
+ own, what the `command` prints, then overrides.
769
+
770
+ - **`command`** runs a program once before any test of a run, in the folder of the
771
+ project whose environment file names it. It must print, on standard output, a JSON
772
+ object of flag names to string, number or boolean values, such as
773
+ `{ "newCheckout": true, "pricingVersion": "v2" }`.
774
+ - **It runs the same way on every platform, without a shell.** The command is split into
775
+ words at spaces. The first word is the program, found on the `PATH` or given as a path
776
+ from the project folder, and the rest are its arguments. `'…'` or `"…"` keeps spaces
777
+ in a word. Inside `"…"`, `\"` stands for `"` and `\\` for `\`; anywhere else `\` is an
778
+ ordinary character, so a Windows path works as written. Pipes, `&&`, redirection,
779
+ `$VAR` and `%VAR%` mean nothing. Put logic in a script and run it with its interpreter,
780
+ as in `node scripts/flags.mjs staging`. On Windows, a `.cmd` or `.bat` script such as
781
+ `npx` cannot be started this way, so name the program it runs instead.
782
+ - It inherits the environment `gta` or the app runs in, so a flag service's API key
783
+ comes from CI or the shell, never from the repository. The desktop app adds the `PATH`
784
+ a terminal would have, so `node` is found however the app was opened.
785
+ - If it exits with anything but `0`, prints anything else, or takes longer than 60
786
+ seconds, **nothing runs**, and what it wrote to standard error is shown. A command that
787
+ takes too long is stopped, with anything it started.
788
+ - A project's command wins over its global project's for the same environment.
789
+ - **Overrides** win over everything: `gta … --flag newCheckout=false` (any number of
790
+ them), or the environment variable `GTA_FLAG_newCheckout=false`. The desktop app has an
791
+ override per flag. `true` and `false` are booleans, a number is a number, and anything
792
+ else is text.
793
+
794
+ Every report records the flag values a run used and where each came from.
795
+
796
+ ---
797
+
798
+ ## 3. Checking a response
799
+
800
+ A step's checks are calls to the [xtest](https://github.com/schwabyio/xtest) functions
801
+ on `gta`, in its `tests` (§5 lists them). This section is what the calls mean.
802
+
803
+ ```yaml
804
+ tests: |
805
+ gta.useStrictValidation()
806
+ gta.expectResponseStatusCodeToBe(200)
807
+ gta.expectResponseToHaveHeader('Content-Type', /^application\/json/)
808
+ gta.expectResponseBodyToHaveProperty('user.name', 'Ada')
809
+ gta.expectResponseBodyToHaveProperty('user.score', 100, 'integerWithin2')
810
+ gta.expectResponseBodyToHaveProperty('user.token', 'sessionToken', 'setAsCollectionVariable')
811
+ gta.expectResponseBodyToHaveUnorderedArray('user.roles', ['admin', 'editor'])
812
+ gta.ignoreResponseBodyProperty('user.lastSeen')
813
+ ```
814
+
815
+ ### Paths
816
+
817
+ A path addresses the body in dot or bracket notation: `user.name`; `groups.0.name` or
818
+ `groups[0].name` for an index; and `sessions[].id` for a property of every item.
819
+
820
+ ### Body conversion
821
+
822
+ A JSON body is used as it is. An XML body is converted: the root element is the single
823
+ top-level key, namespace prefixes and attributes are dropped, an element holding only
824
+ text becomes that string (an empty one `""`), and a repeated element becomes an array.
825
+ A `text/plain` or HTML body is the single property `plaintext`.
826
+
827
+ ### Values and patterns
828
+
829
+ - A value compares with its type: `'12345'` does not equal `12345`.
830
+ - A `RegExp` is a pattern, flags included, tested against the value as text.
831
+ - Header names match case-insensitively. The status and header values compare as text.
832
+
833
+ ### `specialHandling`
834
+
835
+ The last argument of a check may be one of these strings:
836
+
837
+ | String | Means |
838
+ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
839
+ | _(none, and no value)_ | The property or header exists. |
840
+ | `notThisExpectedKey` | It must not exist. Pass `null` as the value. |
841
+ | `notThisExpectedValue` | It must exist and not equal the value (or not match the RegExp). |
842
+ | `setAsCollectionVariable` | Capture it into the variable the value names, for the rest of the run. |
843
+ | `setAsEnvironmentVariable` | The same. Nothing is written back to the environment file. |
844
+ | `dateAsEpoch` | Compare by calendar day: a number is a seconds offset from when the step started, a string a date whose first ten characters must match. |
845
+ | `dateWithin<X>Sec` | A date within X seconds of the value, as in `dateWithin5Sec`. |
846
+ | `integerWithin<X>` | A number within X of the value, as in `integerWithin2`. |
847
+ | `isArray` | An array; its contents are not checked. |
848
+ | `isArrayAndEmpty` / `isArrayAndNotEmpty` | An empty or non-empty array. |
849
+ | `isArrayAndHasLength` | An array of exactly the value's length. |
850
+
851
+ A mistake in a call, such as an unknown `specialHandling`, is reported as a failed check
852
+ saying so, and the checks after it still run.
853
+
854
+ ### Unordered arrays
855
+
856
+ `gta.expectResponseBodyToHaveUnorderedArray(path, list)` passes when the array holds
857
+ every item of a simple `list`, in any order.
858
+
859
+ A list of `{ pathToProperty, expectedValue, specialHandling? }` objects describes **one**
860
+ item, property by property; call it once per item. A property may appear twice, once to
861
+ check it and once to capture it:
862
+
863
+ ```js
864
+ gta.expectResponseBodyToHaveUnorderedArray('users', [
865
+ { pathToProperty: 'name', expectedValue: 'Ada' },
866
+ { pathToProperty: 'id', expectedValue: 'adaId', specialHandling: 'setAsCollectionVariable' }
867
+ ])
868
+ ```
869
+
870
+ `gta.expectResponseBodyToHaveUnorderedArrayNotThisItem(path, list)` passes when no item
871
+ matches.
872
+
873
+ ### Strict validation
874
+
875
+ `gta.useStrictValidation()` fails the step unless **every** property of the body is
876
+ checked, ignored or captured.
877
+
878
+ - `null`, `""` and empty arrays or objects never need a check of their own.
879
+ - A check on a value's content accounts for that value and everything beneath it. A
880
+ shape-only check (`isArray`, `isArrayAndHasLength`, or an object's existence) does not
881
+ vouch for what is inside.
882
+ - It is judged after the collection's and the step's `tests` have both run, over what
883
+ either checked.
884
+
885
+ ### Sorting
886
+
887
+ `gta.sortResponseBodyArrays(property)` sorts every array of objects holding the
888
+ property, before the checks after it.
889
+
890
+ - The property may be a path (`id.value`), and nested arrays are sorted too.
891
+ - Items without the property go last.
892
+ - Values compare alphanumerically, so `Group 2` comes before `Group 10`.
893
+ - Indexed paths refer to the sorted order.
894
+
895
+ ---
896
+
897
+ ## 4. Variables
898
+
899
+ Variables come from `vars` in `project.yml` and in collections, the chosen environment,
900
+ a data file's row, and what code sets while a step runs.
901
+
902
+ ```yaml
903
+ vars:
904
+ apiVersion: '2' # plain values only: string, number, boolean or null
905
+
906
+ before:
907
+ script: |
908
+ gta.set('traceId', gta.uuidv7())
909
+ gta.set('today', gta.date('%Y-%m-%d', 0, 'utc'))
910
+ ```
911
+
912
+ **`vars` hold plain data.** A value must be a string, number, boolean or null. Anything
913
+ computed, such as an id, a date, a random number or a value built from other
914
+ variables, is set in `before.script` with `gta.set` (§5). A collection's
915
+ `before.script` runs before every step, so a value set there is fresh for each.
916
+
917
+ ### Interpolation
918
+
919
+ `{{name}}` resolves in the URL, headers and body, and recursively in what it resolves
920
+ to. Whitespace inside the braces is ignored. In code, read a variable with
921
+ `gta.get(name)` instead.
922
+
923
+ - **Values keep their type.** A string that is exactly one reference returns the
924
+ variable's own value, so a variable written as `true` arrives as a boolean. Anything
925
+ else is text, and `null` becomes the empty string inside it.
926
+ - **An unknown variable is an error**, never literal text. The step fails in its own
927
+ `interpolate` phase, naming the variable, and nothing is sent. A reference loop, or a
928
+ chain more than 16 deep, fails the same way.
929
+ - **In YAML, quote a value that starts with `{{`.** Unquoted, YAML reads `{` as a map.
930
+
931
+ ### Built-in variables
932
+
933
+ These are usable anywhere a variable is, with no declaration:
934
+
935
+ | Reference | Value |
936
+ | ------------------- | -------------------------------------- |
937
+ | `{{$uuid}}` | A random UUID, new for each reference. |
938
+ | `{{$timestamp}}` | Epoch milliseconds. |
939
+ | `{{$isoTimestamp}}` | An ISO-8601 instant. |
940
+ | `{{$randomInt}}` | A whole number from 0 to 999. |
941
+
942
+ ### Resolution order
943
+
944
+ Lowest precedence first:
945
+
946
+ ```
947
+ global project vars → project vars → base collection vars → collection vars
948
+ → environment → data file row → gta.set and captures → process environment
949
+ ```
950
+
951
+ The environment is the global project's file of that name, if there is one, with the
952
+ project's own over it.
953
+
954
+ **The process environment only overrides a name that another layer already declares.**
955
+ Otherwise `PATH`, `HOME` and every credential on the machine would be reachable from a
956
+ request URL. To let CI override a value, declare the variable, usually in an environment
957
+ file and often as a secret (§6).
958
+
959
+ A name is matched exactly, case included, on every platform. Windows ignores case in its
960
+ environment, but a variable called `path` or `username` is still not replaced by the
961
+ system's `Path` or `USERNAME`.
962
+
963
+ ---
964
+
965
+ ## 5. Code: `tests` and `before.script`
966
+
967
+ A step, or a collection for every step, carries JavaScript. `before.script` prepares the
968
+ request, and `tests` checks the response:
969
+
970
+ ```yaml
971
+ - name: get user
972
+ GET: '{{baseUrl}}/users/7'
973
+ before:
974
+ script: |
975
+ gta.set('traceId', gta.uuidv7())
976
+ tests: |
977
+ gta.expectResponseStatusCodeToBe(200)
978
+ gta.expectResponseBodyToHaveProperty('user.name', 'Ada')
979
+ gta.expectResponseBodyToHaveProperty('user.nickname', null, 'notThisExpectedKey')
980
+
981
+ const ids = res.body.user.accounts.map((a) => a.id)
982
+ gta.test('account ids are unique', () => assert.equal(new Set(ids).size, ids.length))
983
+ ```
984
+
985
+ - `before` must hold only `script`.
986
+ - Write code as a block scalar (`|`) so it needs no quoting.
987
+ - **Order:** the collection's `before.script`, the step's `before.script`, the request,
988
+ then the collection's `tests` and the step's `tests`. Strict validation is judged last.
989
+
990
+ ### The `gta` object
991
+
992
+ The xtest functions keep their original names, arguments and `specialHandling` strings.
993
+ There is nothing to load, and no `startXTest` or `endXTest`.
994
+
995
+ | Function | In `before.script` |
996
+ | ------------------------------------------------------------------------------------------------ | :----------------: |
997
+ | `gta.expectResponseStatusCodeToBe(expected, specialHandling?)` | |
998
+ | `gta.expectResponseToHaveHeader(name, expected?, specialHandling?)` | |
999
+ | `gta.expectResponseBodyToHaveProperty(path, expected?, specialHandling?)` | |
1000
+ | `gta.expectResponseBodyToHaveUnorderedArray(path, list)` | |
1001
+ | `gta.expectResponseBodyToHaveUnorderedArrayNotThisItem(path, list)` | |
1002
+ | `gta.ignoreResponseBodyProperty(path)` | |
1003
+ | `gta.ignoreResponseBodyArrayObjectProperty(arrayPath, propertyPath)` | |
1004
+ | `gta.sortResponseBodyArrays(property)` | |
1005
+ | `gta.useStrictValidation(enabled = true)` | |
1006
+ | `gta.test(name, fn)`: a named check that passes unless `fn` throws or rejects; `fn` may be async | |
1007
+ | `gta.get(name)`: a variable's current value | ✓ |
1008
+ | `gta.set(name, value)`: set a variable for the rest of the run | ✓ |
1009
+ | `gta.flag(name)`: a feature flag's value; an unknown flag is an error | ✓ |
1010
+ | `gta.uuid()`: a random (version 4) UUID | ✓ |
1011
+ | `gta.uuidv7()`: a time-ordered (version 7) UUID | ✓ |
1012
+ | `gta.randomInt(min, max)`: a whole number, both ends included | ✓ |
1013
+ | `gta.date(format, secondsOffset = 0, timeZone = 'local')` | ✓ |
1014
+
1015
+ Calling a response check in `before.script` is an error.
1016
+
1017
+ `gta.date` formats with strftime specifiers: `%Y %y %m %d %e %H %I %M %S %L %p %b %B %a
1018
+ %A %j %Z %z %s %F %T %%`. An unrecognized specifier is left in the output, so a typo is
1019
+ visible. `timeZone` is `local`, `utc`, an IANA name such as `America/New_York`, or one of
1020
+ xtest's military zone letters (`U` is -08:00, not UTC).
1021
+
1022
+ ### Other globals
1023
+
1024
+ | Global | What it is |
1025
+ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1026
+ | `res` | `tests` only: `status`, `statusText`, `headers` (lower-cased names), `header(name)`, `body` (parsed JSON, converted XML, or text), `text`, `time` (ms), `size` (bytes). |
1027
+ | `req` | `method`, `url`, `headers`, `body`: as sent in `tests`, as written in `before.script`. |
1028
+ | `assert` | Node's strict `assert`, for use inside `gta.test`. |
1029
+ | `console` | Captured into the step's result. |
1030
+ | `params` | A request set's params (§2.5), in its own scripts and in the tests of the use step running it. |
1031
+ | `endpoint` | An endpoint base's `{name}` values (§2.6), in its scripts and in every script of a step under it. |
1032
+ | `checks` | The project's check files (below). |
1033
+
1034
+ Also available are the language itself and the web-standard globals: timers, `URL`,
1035
+ `URLSearchParams`, `TextEncoder`, `TextDecoder`, `atob`, `btoa`, `structuredClone` and
1036
+ `crypto`.
1037
+
1038
+ **There is no `require`, `import`, `process`, filesystem or `fetch`.** A collection must
1039
+ run the same on any machine, and a request belongs in a step, where it is recorded.
1040
+
1041
+ - A script that throws stops that script. It is reported with its line, and the step is
1042
+ marked as errored. Checks made before it are kept.
1043
+ - A script that runs for more than 10 seconds is stopped.
1044
+
1045
+ ### Check files
1046
+
1047
+ `checks/*.js` in a project, and in its global project, hold functions every script can
1048
+ call as `checks.<file>.<function>`:
1049
+
1050
+ ```js
1051
+ // checks/pagination.js
1052
+ export function expectPage({ size }) {
1053
+ gta.expectResponseStatusCodeToBe(200)
1054
+ gta.expectResponseBodyToHaveProperty('page.size', size)
1055
+ }
1056
+ ```
1057
+
1058
+ ```js
1059
+ // in a step's tests
1060
+ checks.pagination.expectPage({ size: 20 })
1061
+ ```
1062
+
1063
+ - A function uses the calling script's `gta`, `res`, `req`, `assert` and `params`, so
1064
+ what it checks is reported on the step that called it.
1065
+ - A file exports with `export function name`, `export async function name` or
1066
+ `export const name =`.
1067
+ - The file name, without `.js`, must be a JavaScript identifier
1068
+ (`^[A-Za-z_$][\w$]*$`). A file with any other name is not loaded.
1069
+ - A project's file replaces its global project's file of the same name.
1070
+
1071
+ ---
1072
+
1073
+ ## 6. `environments/<name>.yml`
1074
+
1075
+ ```yaml
1076
+ name: staging # optional: else the file name
1077
+ vars:
1078
+ baseUrl: https://staging.example.com
1079
+ strictValidation: true # a real boolean
1080
+ apiKey: { secret: true } # the value comes from the process environment or .env
1081
+ region: { value: eu, description: Where the test accounts live }
1082
+ flags: # feature flags for this environment (§2.9)
1083
+ command: node scripts/flags.mjs staging
1084
+ values: { newCheckout: true }
1085
+ ```
1086
+
1087
+ Any other top-level key is an error.
1088
+
1089
+ | Key | Type | Meaning |
1090
+ | ------- | ------ | ------------------------------------------------------------------------------- |
1091
+ | `name` | string | The environment's name. Defaults to the file name without `.yml`. |
1092
+ | `vars` | map | Each value is a plain value, or `{ value?, secret?: true, description? }`. |
1093
+ | `flags` | map | `command` (string) and `values` (flag name → string, number or boolean) (§2.9). |
1094
+
1095
+ **Secrets.** A variable written `{ secret: true }` never has its value in a file or a
1096
+ report. Its value is read from the process environment variable of **exactly the same
1097
+ name**, case included, else from `.env` at the project's root, else from `.env` at its
1098
+ global project's root.
1099
+
1100
+ - A secret with no value anywhere fails the run, naming it. An empty credential is never
1101
+ sent.
1102
+ - Reports replace a secret's value with `[secret: NAME]`, wherever it appears.
1103
+ - `.env` holds `NAME=value` lines. `#` starts a comment line, `export ` before a name is
1104
+ allowed, and a value may be quoted. It must not be committed.
1105
+
1106
+ An environment is selected by its `name`, else its file name. A project can use its own
1107
+ `environments/` and its global project's (§1.1). Two files of the same name are one
1108
+ environment, with the project's values over the global project's.
1109
+
1110
+ ---
1111
+
1112
+ ## 7. Editing files
1113
+
1114
+ Files are meant to be written by hand, by tools and by agents, and kept in git.
1115
+
1116
+ - **Gravity edits in place.** Saving a file Gravity did not change leaves it as it was,
1117
+ byte for byte. Changing one field changes that field. Comments, key order, quoting and
1118
+ formatting survive, and so does every step not touched.
1119
+ - **An edited file is written with LF line endings.**
1120
+ - **Gravity writes only against what it read.** If a file changed on disk while it was
1121
+ being edited, nothing is written, and the change is shown instead.
1122
+ - Code (`tests`, `before.script`) that Gravity writes is a `|` block, even for one line.
1123
+
1124
+ When writing files yourself, especially from a program or an agent:
1125
+
1126
+ - Use two-space indentation and block style. Flow style (`[a, b]`, `{ secret: true }`)
1127
+ is fine for short values.
1128
+ - Quote any string that starts with `{`, `[`, `*`, `&`, `!`, `%`, `@` or `` ` ``, or
1129
+ that holds `: ` or ` #`.
1130
+ - Write JSON bodies and code as `|` blocks.
1131
+ - Keep `id` equal to the file name, and change both together.
1132
+ - Check the result against [Appendix A](#appendix-a-validation-rules).
1133
+
1134
+ ---
1135
+
1136
+ ## Appendix A. Validation rules
1137
+
1138
+ A file that breaks one of these rules is reported with a message naming the rule. A
1139
+ broken collection is shown as broken, and `gta` reports it as failed without running
1140
+ it. Rules checked at run time fail the step, or the run, before anything is sent.
1141
+
1142
+ **Every document**
1143
+
1144
+ - It must be YAML that parses, holding a mapping, in a file ending in `.yml`.
1145
+ - Unknown keys are errors in: a collection's top level, a step, `settings`, `before`,
1146
+ `body`, `project.yml`, `tls`, an environment file, an environment's `flags`, a param
1147
+ spec and `settings.yml`.
1148
+
1149
+ **Collections**
1150
+
1151
+ - `id` is present, equals the file name without `.yml`, and matches
1152
+ `^[A-Za-z0-9][A-Za-z0-9._-]*$`.
1153
+ - `id` is unique in its home (`collections/`, `requests/`, `bases/` or `endpoints/`),
1154
+ ignoring case.
1155
+ - There is no `name` key (use `id`).
1156
+ - A step has `tags` only if the collection has `stepTags: true`.
1157
+ - With `params`: no step is a use step, and there is no `vars`.
1158
+ - A data file, when present, reads (§2.8).
1159
+
1160
+ **Steps**
1161
+
1162
+ - A step has exactly one method key, out of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`,
1163
+ `HEAD` and `OPTIONS`, in capitals, and its value is a string.
1164
+ - A step has no key outside the table in §2.1.
1165
+ - A use step has no method key, `headers`, `body`, `settings` or `before`. `with` is used
1166
+ only with `use`.
1167
+ - `before` holds only `script`. `before.set` is not supported.
1168
+ - There is no `expect:` key; checks go in `tests`.
1169
+ - `base` is only ever `false`.
1170
+
1171
+ **Values**
1172
+
1173
+ - A variable value, in `vars`, `with` or a param, is a string, number, boolean or null.
1174
+ - A header is a string, a list of strings, or `{ value, enabled?, description? }`.
1175
+ - A tag matches `^[A-Za-z0-9._:-]+$`.
1176
+ - A flag name matches `^[A-Za-z0-9_][A-Za-z0-9_.-]*$`, and a flag value is a string,
1177
+ number or boolean.
1178
+ - A `settings` value has its type in §2.3.
1179
+
1180
+ **Bodies**
1181
+
1182
+ - A body declares exactly one of `json`, `xml`, `text`, `form`, `multipart`, `graphql`
1183
+ and `file`.
1184
+ - `json`, `xml` and `text` are strings, and `form` is a map of strings.
1185
+ - A multipart field is a string, `{ value, contentType? }`,
1186
+ `{ file, contentType?, filename? }`, or a non-empty list of them.
1187
+ - `file` paths are relative.
1188
+
1189
+ **Library files**
1190
+
1191
+ - A request set (`requests/`) has `params`, uses no other set, and has no `vars`.
1192
+ - A base collection (`bases/`) has no `steps` or `params`, and no `extends`.
1193
+ - An endpoint (`endpoints/`) has a URL that is a path starting with `/`, and no use
1194
+ steps.
1195
+ - A check file's name is a JavaScript identifier; if it isn't, the file is not loaded.
1196
+ - `collections/`, `requests/`, `endpoints/` and `bases/` hold files at most one directory
1197
+ down.
1198
+
1199
+ **Portability (§1.2)**
1200
+
1201
+ - A name a file refers to is spelled as it is on disk, case included: a `use:` or
1202
+ `extends:` name, `uses`, a `tls.ca` file and a file a body sends.
1203
+ - The folders and files gta looks for in a project are lower case: `collections/`, not
1204
+ `Collections/`.
1205
+ - No two names in `collections/`, `requests/`, `endpoints/`, `bases/`, `environments/`
1206
+ or `checks/` differ only in case.
1207
+ - No file or directory name is one Windows refuses.
1208
+
1209
+ **Projects**
1210
+
1211
+ - `uses` and `tls.ca` entries are relative paths.
1212
+ - The folder `uses` names has a `project.yml`, and does not itself `uses` another.
1213
+ - Each `tls.ca` file exists and holds a PEM or DER certificate.
1214
+ - `settings.yml` exists for `gta`, and its `environmentType`, if set, names an
1215
+ environment that exists.
1216
+
1217
+ **At run time**
1218
+
1219
+ - Every `{{variable}}` resolves, with no loop and no chain deeper than 16.
1220
+ - Every secret has a value.
1221
+ - Every feature flag named is known to the run.
1222
+ - The flag `command`, if any, has every quote closed, starts, and exits `0` within 60
1223
+ seconds, printing a JSON object.
1224
+ - Every file a body names can be read from the project folder.
1225
+ - Every `use:` and `extends:` names a usable file.
1226
+
1227
+ ---
1228
+
1229
+ ## Appendix B. A complete project
1230
+
1231
+ ```
1232
+ shop/
1233
+ ├── project.yml
1234
+ ├── settings.yml
1235
+ ├── collections/
1236
+ │ ├── health.yml
1237
+ │ └── users/
1238
+ │ ├── profile.yml
1239
+ │ ├── profile.csv
1240
+ │ └── avatar.yml
1241
+ ├── requests/
1242
+ │ └── login.yml
1243
+ ├── checks/
1244
+ │ └── common.js
1245
+ ├── files/
1246
+ │ └── avatar.png
1247
+ ├── environments/
1248
+ │ └── staging.yml
1249
+ └── .env apiKey=… (not committed)
1250
+ ```
1251
+
1252
+ ```yaml
1253
+ # project.yml
1254
+ name: Shop
1255
+ vars:
1256
+ apiVersion: '2'
1257
+ ```
1258
+
1259
+ ```yaml
1260
+ # settings.yml
1261
+ environmentType: staging
1262
+ limitConcurrency: 2
1263
+ generateJUnitResults: true
1264
+ ```
1265
+
1266
+ ```yaml
1267
+ # environments/staging.yml
1268
+ vars:
1269
+ baseUrl: https://staging.example.com
1270
+ adminUser: admin@example.com
1271
+ apiKey: { secret: true }
1272
+ flags:
1273
+ values: { avatars: true }
1274
+ ```
1275
+
1276
+ ```yaml
1277
+ # requests/login.yml
1278
+ id: login
1279
+ params:
1280
+ username: { required: true }
1281
+ steps:
1282
+ - name: log in
1283
+ POST: '{{baseUrl}}/v{{apiVersion}}/login'
1284
+ headers:
1285
+ X-Api-Key: '{{apiKey}}'
1286
+ body:
1287
+ json: '{ "user": "{{params.username}}" }'
1288
+ tests: |
1289
+ gta.expectResponseStatusCodeToBe(200)
1290
+ gta.expectResponseBodyToHaveProperty('token', 'token', 'setAsCollectionVariable')
1291
+ ```
1292
+
1293
+ ```js
1294
+ // checks/common.js
1295
+ export function expectJson() {
1296
+ gta.expectResponseToHaveHeader('Content-Type', /^application\/json/)
1297
+ }
1298
+ ```
1299
+
1300
+ ```yaml
1301
+ # collections/health.yml
1302
+ id: health
1303
+ tags: [smoke]
1304
+ steps:
1305
+ - name: service is up
1306
+ GET: '{{baseUrl}}/health'
1307
+ tests: |
1308
+ gta.expectResponseStatusCodeToBe(200)
1309
+ checks.common.expectJson()
1310
+ ```
1311
+
1312
+ ```yaml
1313
+ # collections/users/profile.yml
1314
+ id: profile
1315
+ steps:
1316
+ - use: login # sets token for the steps after it
1317
+ with: { username: '{{adminUser}}' }
1318
+
1319
+ - name: get user
1320
+ GET: '{{baseUrl}}/v{{apiVersion}}/users/{{userId}}'
1321
+ headers:
1322
+ Authorization: Bearer {{token}}
1323
+ tests: |
1324
+ gta.expectResponseStatusCodeToBe(gta.get('expectedStatus'))
1325
+ ```
1326
+
1327
+ ```csv
1328
+ userId,expectedStatus,iterationLabel
1329
+ 1001,200,Known user
1330
+ 9999,404,Unknown user
1331
+ ```
1332
+
1333
+ ```yaml
1334
+ # collections/users/avatar.yml
1335
+ id: avatar
1336
+ flags: { avatars: true } # runs only where the environment turns avatars on
1337
+ steps:
1338
+ - use: login
1339
+ with: { username: '{{adminUser}}' }
1340
+
1341
+ - name: upload avatar
1342
+ POST: '{{baseUrl}}/v{{apiVersion}}/users/1001/avatar'
1343
+ headers:
1344
+ Authorization: Bearer {{token}}
1345
+ body:
1346
+ multipart:
1347
+ caption: Profile photo
1348
+ image: { file: files/avatar.png }
1349
+ tests: |
1350
+ gta.expectResponseStatusCodeToBe(201)
1351
+ ```
1352
+
1353
+ `gta all` runs `health` and `avatar` once each, and `profile` twice: once for each row
1354
+ of `profile.csv`.