@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/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/SPEC.md +1354 -0
- package/dist/THIRD_PARTY_NOTICES.txt +71 -0
- package/dist/chunks/chunk-VRAFYQNE.js +58985 -0
- package/dist/gta.js +1703 -0
- package/dist/worker.js +10 -0
- package/package.json +51 -0
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`.
|