norn-cli 1.1.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/CHANGELOG.md +556 -0
- package/LICENSE +94 -0
- package/README.md +938 -0
- package/dist/cli.js +23453 -0
- package/package.json +194 -0
package/README.md
ADDED
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
# Norn - REST Client
|
|
2
|
+
|
|
3
|
+
A powerful REST client extension for VS Code with sequences, assertions, environments, script execution, and CLI support for CI/CD pipelines.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **HTTP Requests**: Send GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS requests from `.norn` files
|
|
11
|
+
- **API Definitions**: Define reusable headers and endpoints in `.nornapi` files
|
|
12
|
+
- **Variables**: Define and reference variables with `var name = value` and `{{name}}`
|
|
13
|
+
- **Environments**: Manage dev/staging/prod configurations with `.nornenv` files
|
|
14
|
+
- **Sequences**: Chain multiple requests with response capture using `$N.path`
|
|
15
|
+
- **Test Sequences**: Mark sequences as tests with `test sequence` for CLI execution
|
|
16
|
+
- **Sequence Tags**: Tag sequences with `@smoke`, `@team(CustomerExp)` for filtering in CI/CD
|
|
17
|
+
- **Secret Variables**: Mark sensitive environment variables with `secret` for automatic redaction
|
|
18
|
+
- **Assertions**: Validate responses with `assert` statements supporting comparison, type checking, and existence
|
|
19
|
+
- **Named Requests**: Define reusable requests with `[RequestName]` and call them with `run RequestName`
|
|
20
|
+
- **Conditionals**: Control flow with `if/end if` blocks based on response data
|
|
21
|
+
- **Wait Commands**: Add delays between requests with `wait 1s` or `wait 500ms`
|
|
22
|
+
- **JSON File Loading**: Load test data from JSON files with `run readJson ./file.json`
|
|
23
|
+
- **Property Updates**: Modify loaded JSON data inline with `config.property = value`
|
|
24
|
+
- **Script Execution**: Run bash, PowerShell, or JavaScript scripts within sequences
|
|
25
|
+
- **Print Statements**: Debug and log messages during sequence execution
|
|
26
|
+
- **Cookie Support**: Automatic cookie jar with persistence across requests
|
|
27
|
+
- **Syntax Highlighting**: Full syntax highlighting for requests, headers, JSON bodies
|
|
28
|
+
- **IntelliSense**: Autocomplete for HTTP methods, headers, variables, and keywords
|
|
29
|
+
- **Diagnostics**: Error highlighting for undefined variables
|
|
30
|
+
- **CLI**: Run tests from terminal with JUnit/HTML reports for CI/CD automation
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
1. Create a file with `.norn` extension
|
|
35
|
+
2. Write your HTTP request
|
|
36
|
+
3. Click "Send Request" above the request line
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### Basic Request
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
GET https://api.example.com/users
|
|
44
|
+
Authorization: Bearer my-token
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Variables
|
|
48
|
+
|
|
49
|
+
Variables can hold literal strings or be assigned from expressions:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Literal string (quotes optional for simple values)
|
|
53
|
+
var baseUrl = https://api.example.com
|
|
54
|
+
var name = "John Doe"
|
|
55
|
+
|
|
56
|
+
# Use variables with {{}}
|
|
57
|
+
GET {{baseUrl}}/users
|
|
58
|
+
Authorization: Bearer {{token}}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Variable Assignment in Sequences
|
|
62
|
+
|
|
63
|
+
Inside sequences, you can assign variables from expressions:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
sequence Example
|
|
67
|
+
var data = run readJson ./config.json
|
|
68
|
+
|
|
69
|
+
# Extract a value from JSON variable (evaluated)
|
|
70
|
+
var userId = data.users[0].id
|
|
71
|
+
|
|
72
|
+
# Literal string with interpolation
|
|
73
|
+
var message = "User ID is {{userId}}"
|
|
74
|
+
|
|
75
|
+
# Response capture
|
|
76
|
+
GET https://api.example.com/users/{{userId}}
|
|
77
|
+
var userName = $1.body.name
|
|
78
|
+
|
|
79
|
+
print Result | {{message}}, Name: {{userName}}
|
|
80
|
+
end sequence
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Syntax | Type | Example |
|
|
84
|
+
|--------|------|---------|
|
|
85
|
+
| `var x = "text"` | Literal string | `var name = "John"` |
|
|
86
|
+
| `var x = "Hi {{y}}"` | String with interpolation | `var msg = "Hello {{name}}"` |
|
|
87
|
+
| `var x = someVar.path` | Expression (evaluated) | `var id = data.users[0].id` |
|
|
88
|
+
| `var x = $1.body.id` | Response capture | `var token = $1.body.token` |
|
|
89
|
+
| `var x = run ...` | Script/JSON result | `var data = run readJson ./file.json` |
|
|
90
|
+
|
|
91
|
+
### Request with Body
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
POST https://api.example.com/users
|
|
95
|
+
Content-Type: application/json
|
|
96
|
+
{
|
|
97
|
+
"name": "John Doe",
|
|
98
|
+
"email": "john@example.com"
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Sequences (Chained Requests)
|
|
103
|
+
|
|
104
|
+
Chain multiple requests and capture response data:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
sequence AuthFlow
|
|
108
|
+
POST https://api.example.com/login
|
|
109
|
+
Content-Type: application/json
|
|
110
|
+
|
|
111
|
+
{"username": "admin", "password": "secret"}
|
|
112
|
+
|
|
113
|
+
var token = $1.accessToken
|
|
114
|
+
|
|
115
|
+
GET https://api.example.com/profile
|
|
116
|
+
Authorization: Bearer {{token}}
|
|
117
|
+
end sequence
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Click "▶ Run Sequence" above the `sequence` line to execute all requests in order.
|
|
121
|
+
|
|
122
|
+
### Sequence Composition
|
|
123
|
+
|
|
124
|
+
Sequences can call other sequences, enabling modular test design and natural setup/teardown patterns:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Define reusable setup sequence
|
|
128
|
+
sequence Login
|
|
129
|
+
POST {{baseUrl}}/auth/login
|
|
130
|
+
Content-Type: application/json
|
|
131
|
+
{"username": "admin", "password": "secret"}
|
|
132
|
+
var token = $1.body.accessToken
|
|
133
|
+
print Logged in | Token: {{token}}
|
|
134
|
+
end sequence
|
|
135
|
+
|
|
136
|
+
# Define reusable teardown sequence
|
|
137
|
+
sequence Logout
|
|
138
|
+
POST {{baseUrl}}/auth/logout
|
|
139
|
+
Authorization: Bearer {{token}}
|
|
140
|
+
print Logged out
|
|
141
|
+
end sequence
|
|
142
|
+
|
|
143
|
+
# Main test sequence uses setup/teardown
|
|
144
|
+
sequence UserTests
|
|
145
|
+
# Run setup - token variable becomes available
|
|
146
|
+
run Login
|
|
147
|
+
|
|
148
|
+
# Run actual tests
|
|
149
|
+
GET {{baseUrl}}/users/me
|
|
150
|
+
Authorization: Bearer {{token}}
|
|
151
|
+
assert $1.status == 200
|
|
152
|
+
|
|
153
|
+
# Run teardown
|
|
154
|
+
run Logout
|
|
155
|
+
end sequence
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Key behaviors:**
|
|
159
|
+
- Variables set in called sequences are available to the caller (`token` from `Login`)
|
|
160
|
+
- Sequences can be nested to any depth
|
|
161
|
+
- Circular references are detected and reported as errors
|
|
162
|
+
- If a called sequence fails, the parent sequence stops
|
|
163
|
+
|
|
164
|
+
### Sequence Parameters
|
|
165
|
+
|
|
166
|
+
Sequences can accept parameters with optional default values:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Required parameter
|
|
170
|
+
sequence Greet(name)
|
|
171
|
+
print Hello, {{name}}!
|
|
172
|
+
end sequence
|
|
173
|
+
|
|
174
|
+
# Optional parameter with default
|
|
175
|
+
sequence Login(username, password = "secret123")
|
|
176
|
+
POST {{baseUrl}}/auth/login
|
|
177
|
+
Content-Type: application/json
|
|
178
|
+
{"username": "{{username}}", "password": "{{password}}"}
|
|
179
|
+
|
|
180
|
+
var token = $1.body.accessToken
|
|
181
|
+
return token
|
|
182
|
+
end sequence
|
|
183
|
+
|
|
184
|
+
# Call with positional arguments
|
|
185
|
+
run Greet("World")
|
|
186
|
+
run Login("admin", "mypassword")
|
|
187
|
+
|
|
188
|
+
# Call with named arguments (any order)
|
|
189
|
+
run Login(password: "pass123", username: "admin")
|
|
190
|
+
|
|
191
|
+
# Use defaults - only provide required params
|
|
192
|
+
run Login("admin") # uses default password
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Rules:**
|
|
196
|
+
- Required parameters (no default) must come before optional parameters
|
|
197
|
+
- Positional arguments are bound in order
|
|
198
|
+
- Named arguments can be in any order
|
|
199
|
+
- Cannot mix positional arguments after named arguments
|
|
200
|
+
|
|
201
|
+
### Sequence Return Values
|
|
202
|
+
|
|
203
|
+
Sequences can return values that the caller can capture and use:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
sequence FetchUser(userId)
|
|
207
|
+
GET {{baseUrl}}/users/{{userId}}
|
|
208
|
+
var name = $1.body.name
|
|
209
|
+
var email = $1.body.email
|
|
210
|
+
return name, email
|
|
211
|
+
end sequence
|
|
212
|
+
|
|
213
|
+
sequence MyTests
|
|
214
|
+
# Capture return values
|
|
215
|
+
var user = run FetchUser("123")
|
|
216
|
+
|
|
217
|
+
# Access individual fields
|
|
218
|
+
print User name: {{user.name}}
|
|
219
|
+
print User email: {{user.email}}
|
|
220
|
+
|
|
221
|
+
# Use in requests
|
|
222
|
+
POST {{baseUrl}}/messages
|
|
223
|
+
Content-Type: application/json
|
|
224
|
+
{"to": "{{user.email}}", "subject": "Hello {{user.name}}"}
|
|
225
|
+
end sequence
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**IntelliSense:** When you type `{{user.`, you'll see `name` and `email` as completions based on the sequence's return statement.
|
|
229
|
+
|
|
230
|
+
### Sequence Tags
|
|
231
|
+
|
|
232
|
+
Tag sequences for filtering during test execution. Tags support simple names and key-value pairs:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# Simple tags
|
|
236
|
+
@smoke @regression
|
|
237
|
+
sequence AuthFlow
|
|
238
|
+
POST {{baseUrl}}/auth/login
|
|
239
|
+
Content-Type: application/json
|
|
240
|
+
{"username": "admin", "password": "secret"}
|
|
241
|
+
assert $1.status == 200
|
|
242
|
+
end sequence
|
|
243
|
+
|
|
244
|
+
# Key-value tags for categorization
|
|
245
|
+
@team(CustomerExp)
|
|
246
|
+
@priority(high)
|
|
247
|
+
@jira(NORN-123)
|
|
248
|
+
sequence CheckoutFlow
|
|
249
|
+
GET {{baseUrl}}/checkout
|
|
250
|
+
assert $1.status == 200
|
|
251
|
+
end sequence
|
|
252
|
+
|
|
253
|
+
# Mixed tags on multiple lines
|
|
254
|
+
@smoke
|
|
255
|
+
@team(Platform)
|
|
256
|
+
@feature(payments)
|
|
257
|
+
sequence PaymentValidation
|
|
258
|
+
POST {{baseUrl}}/payments
|
|
259
|
+
assert $1.status == 201
|
|
260
|
+
end sequence
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Tag Syntax:**
|
|
264
|
+
- Simple tags: `@smoke`, `@regression`, `@wip`, `@slow`
|
|
265
|
+
- Key-value tags: `@team(CustomerExp)`, `@priority(high)`, `@jira(NORN-123)`
|
|
266
|
+
- Multiple tags can be on the same line or separate lines
|
|
267
|
+
- Tag names follow the pattern: `[a-zA-Z_][a-zA-Z0-9_-]*`
|
|
268
|
+
- Tag matching is case-insensitive
|
|
269
|
+
|
|
270
|
+
**CLI Filtering:**
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# Run sequences tagged @smoke
|
|
274
|
+
npx norn tests/ --tag smoke
|
|
275
|
+
|
|
276
|
+
# AND logic: must have BOTH tags
|
|
277
|
+
npx norn tests/ --tag smoke --tag auth
|
|
278
|
+
|
|
279
|
+
# OR logic: match ANY tag
|
|
280
|
+
npx norn tests/ --tags smoke,regression
|
|
281
|
+
|
|
282
|
+
# Key-value exact match
|
|
283
|
+
npx norn tests/ --tag team(CustomerExp)
|
|
284
|
+
|
|
285
|
+
# Combine with environment
|
|
286
|
+
npx norn tests/ --env staging --tag smoke
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Behavior:**
|
|
290
|
+
- When a sequence calls `run OtherSequence` and tag filtering is active, non-matching sequences are silently skipped
|
|
291
|
+
- Untagged sequences run when no tag filter is specified
|
|
292
|
+
- Tags can only be applied to sequences (diagnostics warn on misplaced tags)
|
|
293
|
+
|
|
294
|
+
### Imports
|
|
295
|
+
|
|
296
|
+
Organize your tests by importing named requests and sequences from other files:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# common.norn - shared utilities
|
|
300
|
+
var baseUrl = https://api.example.com
|
|
301
|
+
|
|
302
|
+
[AuthRequest]
|
|
303
|
+
POST {{baseUrl}}/auth/login
|
|
304
|
+
Content-Type: application/json
|
|
305
|
+
{"username": "admin", "password": "secret"}
|
|
306
|
+
|
|
307
|
+
sequence SharedSetup
|
|
308
|
+
run AuthRequest
|
|
309
|
+
var token = $1.body.accessToken
|
|
310
|
+
end sequence
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# main-test.norn - imports shared utilities
|
|
315
|
+
import "./common.norn"
|
|
316
|
+
|
|
317
|
+
sequence MyTests
|
|
318
|
+
# Use imported sequence
|
|
319
|
+
run SharedSetup
|
|
320
|
+
|
|
321
|
+
# Use imported request
|
|
322
|
+
run AuthRequest
|
|
323
|
+
|
|
324
|
+
# Token from SharedSetup is available
|
|
325
|
+
GET {{baseUrl}}/users/me
|
|
326
|
+
Authorization: Bearer {{token}}
|
|
327
|
+
assert $1.status == 200
|
|
328
|
+
end sequence
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Key behaviors:**
|
|
332
|
+
- Import paths are relative to the current file
|
|
333
|
+
- Nested imports are supported (imported files can import other files)
|
|
334
|
+
- Circular imports are detected and reported as errors
|
|
335
|
+
- Only named requests `[Name]` and sequences are imported
|
|
336
|
+
- Variables are resolved at import time (baked in), not exported
|
|
337
|
+
- `.nornenv` environment variables are shared across all files
|
|
338
|
+
|
|
339
|
+
### API Definition Files (.nornapi)
|
|
340
|
+
|
|
341
|
+
Define reusable API configurations with header groups and named endpoints in `.nornapi` files.
|
|
342
|
+
|
|
343
|
+
#### Header Groups
|
|
344
|
+
|
|
345
|
+
Header groups define reusable sets of HTTP headers:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
# api.nornapi
|
|
349
|
+
|
|
350
|
+
headers Json
|
|
351
|
+
Content-Type: application/json
|
|
352
|
+
Accept: application/json
|
|
353
|
+
end headers
|
|
354
|
+
|
|
355
|
+
headers Auth
|
|
356
|
+
Authorization: Bearer {{token}}
|
|
357
|
+
X-API-Key: {{apiKey}}
|
|
358
|
+
end headers
|
|
359
|
+
|
|
360
|
+
headers Form
|
|
361
|
+
Content-Type: application/x-www-form-urlencoded
|
|
362
|
+
end headers
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
- Header values can include `{{variables}}` that are resolved at runtime
|
|
366
|
+
- Multiple header groups can be applied to a single request
|
|
367
|
+
|
|
368
|
+
#### Named Endpoints
|
|
369
|
+
|
|
370
|
+
Define named endpoints with path parameters using `{param}` syntax:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
endpoints
|
|
374
|
+
# Basic CRUD operations
|
|
375
|
+
GetUser: GET {{baseUrl}}/users/{id}
|
|
376
|
+
GetAllUsers: GET {{baseUrl}}/users
|
|
377
|
+
CreateUser: POST {{baseUrl}}/users
|
|
378
|
+
UpdateUser: PUT {{baseUrl}}/users/{id}
|
|
379
|
+
DeleteUser: DELETE {{baseUrl}}/users/{id}
|
|
380
|
+
|
|
381
|
+
# Multiple path parameters
|
|
382
|
+
GetUserPosts: GET {{baseUrl}}/users/{userId}/posts
|
|
383
|
+
GetUserPost: GET {{baseUrl}}/users/{userId}/posts/{postId}
|
|
384
|
+
|
|
385
|
+
# All HTTP methods supported
|
|
386
|
+
CheckHealth: HEAD {{baseUrl}}/health
|
|
387
|
+
GetOptions: OPTIONS {{baseUrl}}/api
|
|
388
|
+
end endpoints
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
- Endpoint names are case-sensitive (e.g., `GetUser`, `CreateUser`)
|
|
392
|
+
- Path parameters use single braces: `{id}`, `{userId}`
|
|
393
|
+
- Environment variables use double braces: `{{baseUrl}}`
|
|
394
|
+
|
|
395
|
+
#### Swagger/OpenAPI Import
|
|
396
|
+
|
|
397
|
+
Import endpoints directly from OpenAPI/Swagger specifications:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
# api.nornapi
|
|
401
|
+
|
|
402
|
+
swagger https://petstore.swagger.io/v2/swagger.json
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Click "Import Swagger" CodeLens above the statement to fetch and generate endpoint definitions from the spec.
|
|
406
|
+
|
|
407
|
+
#### Using .nornapi in .norn Files
|
|
408
|
+
|
|
409
|
+
Import your API definitions and use them in requests:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
import "./api.nornapi"
|
|
413
|
+
|
|
414
|
+
# Standalone request with endpoint and header group
|
|
415
|
+
GET GetUser(1) Json
|
|
416
|
+
|
|
417
|
+
# Inside a sequence
|
|
418
|
+
sequence UserTests
|
|
419
|
+
# Basic endpoint call with parameter
|
|
420
|
+
GET GetTodo(1) Json
|
|
421
|
+
assert $1.status == 200
|
|
422
|
+
|
|
423
|
+
# Multiple header groups (space-separated)
|
|
424
|
+
GET GetUser(1) Json Auth
|
|
425
|
+
|
|
426
|
+
# Header groups on separate lines
|
|
427
|
+
POST CreateUser
|
|
428
|
+
Json
|
|
429
|
+
Auth
|
|
430
|
+
{"name": "John", "email": "john@example.com"}
|
|
431
|
+
|
|
432
|
+
# Capture response to variable
|
|
433
|
+
var user = GET GetUser(1) Json
|
|
434
|
+
print User | {{user.body.name}}
|
|
435
|
+
|
|
436
|
+
# Use variables in endpoint parameters
|
|
437
|
+
var userId = 5
|
|
438
|
+
GET GetUser({{userId}}) Json
|
|
439
|
+
|
|
440
|
+
# Quoted string parameters
|
|
441
|
+
GET GetUser("123") Json
|
|
442
|
+
end sequence
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
#### Endpoint Syntax Reference
|
|
446
|
+
|
|
447
|
+
| Syntax | Description |
|
|
448
|
+
|--------|-------------|
|
|
449
|
+
| `GET EndpointName(param)` | Call endpoint with positional parameter |
|
|
450
|
+
| `GET EndpointName(param1, param2)` | Multiple positional parameters |
|
|
451
|
+
| `GET EndpointName(id: "123")` | Named parameter |
|
|
452
|
+
| `GET EndpointName(1) Json` | With header group |
|
|
453
|
+
| `GET EndpointName(1) Json Auth` | Multiple header groups |
|
|
454
|
+
| `var x = GET EndpointName(1) Json` | Capture response to variable |
|
|
455
|
+
|
|
456
|
+
### Assertions
|
|
457
|
+
|
|
458
|
+
Validate response data with powerful assertion operators:
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
sequence ValidateAPI
|
|
462
|
+
GET https://api.example.com/users/1
|
|
463
|
+
|
|
464
|
+
# Status and comparison assertions
|
|
465
|
+
assert $1.status == 200
|
|
466
|
+
assert $1.status < 300
|
|
467
|
+
|
|
468
|
+
# Body value assertions
|
|
469
|
+
assert $1.body.id == 1
|
|
470
|
+
assert $1.body.name == "John"
|
|
471
|
+
assert $1.body.age >= 18
|
|
472
|
+
|
|
473
|
+
# String assertions
|
|
474
|
+
assert $1.body.email contains "@"
|
|
475
|
+
assert $1.body.name startsWith "J"
|
|
476
|
+
assert $1.body.name endsWith "ohn"
|
|
477
|
+
assert $1.body.email matches "[a-z]+@[a-z]+\.[a-z]+"
|
|
478
|
+
|
|
479
|
+
# Timing assertions
|
|
480
|
+
assert $1.duration < 5000
|
|
481
|
+
|
|
482
|
+
# Existence checks
|
|
483
|
+
assert $1.body.id exists
|
|
484
|
+
assert $1.body.deletedAt !exists
|
|
485
|
+
|
|
486
|
+
# Type assertions
|
|
487
|
+
assert $1.body.id isType number
|
|
488
|
+
assert $1.body.name isType string
|
|
489
|
+
assert $1.body.active isType boolean
|
|
490
|
+
assert $1.body.tags isType array
|
|
491
|
+
assert $1.body.address isType object
|
|
492
|
+
|
|
493
|
+
# Header assertions
|
|
494
|
+
assert $1.headers.Content-Type contains "application/json"
|
|
495
|
+
|
|
496
|
+
# Custom failure messages
|
|
497
|
+
assert $1.status == 200 | "API should return success status"
|
|
498
|
+
end sequence
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Environments
|
|
502
|
+
|
|
503
|
+
Create a `.nornenv` file in your workspace root to manage environment-specific variables:
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
# .nornenv file
|
|
507
|
+
# Common variables (always available)
|
|
508
|
+
var timeout = 30000
|
|
509
|
+
var version = v1
|
|
510
|
+
|
|
511
|
+
[env:dev]
|
|
512
|
+
var baseUrl = https://dev-api.example.com
|
|
513
|
+
var apiKey = dev-key-123
|
|
514
|
+
|
|
515
|
+
[env:staging]
|
|
516
|
+
var baseUrl = https://staging-api.example.com
|
|
517
|
+
var apiKey = staging-key-456
|
|
518
|
+
|
|
519
|
+
[env:prod]
|
|
520
|
+
var baseUrl = https://api.example.com
|
|
521
|
+
var apiKey = prod-key-789
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Select the active environment from the VS Code status bar. Environment variables override common variables.
|
|
525
|
+
|
|
526
|
+
### Named Requests
|
|
527
|
+
|
|
528
|
+
Define reusable requests and call them from sequences:
|
|
529
|
+
|
|
530
|
+
```bash
|
|
531
|
+
# Define a reusable login request
|
|
532
|
+
[Login]
|
|
533
|
+
POST {{baseUrl}}/auth/login
|
|
534
|
+
Content-Type: application/json
|
|
535
|
+
{"username": "admin", "password": "secret"}
|
|
536
|
+
|
|
537
|
+
# Define a profile request
|
|
538
|
+
[GetProfile]
|
|
539
|
+
GET {{baseUrl}}/users/me
|
|
540
|
+
Authorization: Bearer {{token}}
|
|
541
|
+
|
|
542
|
+
###
|
|
543
|
+
|
|
544
|
+
sequence AuthFlow
|
|
545
|
+
# Run the named request
|
|
546
|
+
run Login
|
|
547
|
+
var token = $1.body.accessToken
|
|
548
|
+
|
|
549
|
+
# Run another named request
|
|
550
|
+
run GetProfile
|
|
551
|
+
print Profile | Welcome, {{$2.body.name}}!
|
|
552
|
+
end sequence
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Conditionals (if/end if)
|
|
556
|
+
|
|
557
|
+
Control execution flow based on response data:
|
|
558
|
+
|
|
559
|
+
```bash
|
|
560
|
+
sequence ConditionalFlow
|
|
561
|
+
GET https://api.example.com/users/1
|
|
562
|
+
|
|
563
|
+
# Execute block only if condition is true
|
|
564
|
+
if $1.status == 200
|
|
565
|
+
print Success | User found!
|
|
566
|
+
|
|
567
|
+
GET https://api.example.com/users/1/orders
|
|
568
|
+
assert $2.status == 200
|
|
569
|
+
end if
|
|
570
|
+
|
|
571
|
+
# Check for errors
|
|
572
|
+
if $1.status == 404
|
|
573
|
+
print Error | User not found
|
|
574
|
+
end if
|
|
575
|
+
|
|
576
|
+
# Conditions support all assertion operators
|
|
577
|
+
if $1.body.role == "admin"
|
|
578
|
+
print Admin | User has admin privileges
|
|
579
|
+
end if
|
|
580
|
+
end sequence
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Wait Commands
|
|
584
|
+
|
|
585
|
+
Add delays between requests (useful for rate limiting or async operations):
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
sequence RateLimitedFlow
|
|
589
|
+
POST https://api.example.com/jobs
|
|
590
|
+
var jobId = $1.body.id
|
|
591
|
+
|
|
592
|
+
print Waiting | Job submitted, waiting for completion...
|
|
593
|
+
|
|
594
|
+
# Wait 2 seconds
|
|
595
|
+
wait 2s
|
|
596
|
+
|
|
597
|
+
GET https://api.example.com/jobs/{{jobId}}
|
|
598
|
+
|
|
599
|
+
# Wait 500 milliseconds
|
|
600
|
+
wait 500ms
|
|
601
|
+
|
|
602
|
+
GET https://api.example.com/jobs/{{jobId}}/result
|
|
603
|
+
end sequence
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### JSON File Loading
|
|
607
|
+
|
|
608
|
+
Load test data from external JSON files:
|
|
609
|
+
|
|
610
|
+
```bash
|
|
611
|
+
sequence DataDrivenTest
|
|
612
|
+
# Load JSON configuration
|
|
613
|
+
var config = run readJson ./test-config.json
|
|
614
|
+
|
|
615
|
+
# Access properties
|
|
616
|
+
print Config | Using API: {{config.baseUrl}}
|
|
617
|
+
|
|
618
|
+
# Use in requests
|
|
619
|
+
GET {{config.baseUrl}}/users/{{config.testUser.id}}
|
|
620
|
+
|
|
621
|
+
# Access nested values and arrays
|
|
622
|
+
print First Role | {{config.testUser.roles[0]}}
|
|
623
|
+
|
|
624
|
+
# Modify loaded data inline
|
|
625
|
+
config.baseUrl = https://api.updated.com
|
|
626
|
+
config.testUser.name = Updated Name
|
|
627
|
+
|
|
628
|
+
print Updated | New URL: {{config.baseUrl}}
|
|
629
|
+
end sequence
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Example `test-config.json`:
|
|
633
|
+
```json
|
|
634
|
+
{
|
|
635
|
+
"baseUrl": "https://api.example.com",
|
|
636
|
+
"testUser": {
|
|
637
|
+
"id": 1,
|
|
638
|
+
"name": "Test User",
|
|
639
|
+
"roles": ["admin", "user"]
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Script Execution
|
|
645
|
+
|
|
646
|
+
Run scripts within sequences for setup, data generation, or validation:
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
sequence TestWithScripts
|
|
650
|
+
# Run a setup script
|
|
651
|
+
run bash ./scripts/seed-db.sh
|
|
652
|
+
|
|
653
|
+
# Generate a signature and capture output
|
|
654
|
+
var signature = run js ./scripts/sign.js {{payload}}
|
|
655
|
+
|
|
656
|
+
# Use the signature in a request
|
|
657
|
+
POST https://api.example.com/verify
|
|
658
|
+
X-Signature: {{signature}}
|
|
659
|
+
end sequence
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
Scripts receive variables as environment variables with `NORN_` prefix (e.g., `NORN_TOKEN`).
|
|
663
|
+
|
|
664
|
+
#### Capturing Structured Data from Scripts
|
|
665
|
+
|
|
666
|
+
When scripts output JSON, you can access properties directly:
|
|
667
|
+
|
|
668
|
+
```bash
|
|
669
|
+
sequence DatabaseQuery
|
|
670
|
+
# PowerShell script that outputs JSON
|
|
671
|
+
var dbResult = run powershell ./scripts/query-db.ps1
|
|
672
|
+
|
|
673
|
+
# Access properties from the JSON output
|
|
674
|
+
print User Found | ID: {{dbResult.id}}, Name: {{dbResult.name}}
|
|
675
|
+
|
|
676
|
+
# Use in requests
|
|
677
|
+
GET https://api.example.com/users/{{dbResult.id}}
|
|
678
|
+
end sequence
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
**Automatic Format Detection**: Norn automatically parses multiple output formats:
|
|
682
|
+
|
|
683
|
+
| Format | Example | Access |
|
|
684
|
+
|--------|---------|--------|
|
|
685
|
+
| JSON | `{"Id": 123, "Name": "John"}` | `{{result.Id}}` |
|
|
686
|
+
| PowerShell Table | `Id Name`<br>`-- ----`<br>`123 John` | `{{result.Id}}` |
|
|
687
|
+
| PowerShell List | `Id : 123`<br>`Name : John` | `{{result.Id}}` |
|
|
688
|
+
|
|
689
|
+
Norn also strips ANSI color codes automatically, so formatted terminal output won't corrupt your data.
|
|
690
|
+
|
|
691
|
+
### Print Statements
|
|
692
|
+
|
|
693
|
+
Add debug output to your sequences:
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
sequence DebugFlow
|
|
697
|
+
print Starting authentication...
|
|
698
|
+
|
|
699
|
+
POST https://api.example.com/login
|
|
700
|
+
Content-Type: application/json
|
|
701
|
+
{"user": "admin"}
|
|
702
|
+
|
|
703
|
+
var token = $1.token
|
|
704
|
+
print Token received | Value: {{token}}
|
|
705
|
+
end sequence
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
Use `print Title | Body content` for expandable messages in the result view.
|
|
709
|
+
|
|
710
|
+
## CLI Usage
|
|
711
|
+
|
|
712
|
+
Run tests from the command line for CI/CD pipelines. Only sequences marked with `test` are executed.
|
|
713
|
+
|
|
714
|
+
```bash
|
|
715
|
+
# Run all test sequences in a file
|
|
716
|
+
npx norn api-tests.norn
|
|
717
|
+
|
|
718
|
+
# Run all test sequences in a directory (recursive)
|
|
719
|
+
npx norn tests/
|
|
720
|
+
|
|
721
|
+
# Run a specific sequence
|
|
722
|
+
npx norn api-tests.norn --sequence AuthFlow
|
|
723
|
+
|
|
724
|
+
# Run with a specific environment
|
|
725
|
+
npx norn api-tests.norn --env staging
|
|
726
|
+
|
|
727
|
+
# Generate JUnit XML report for CI/CD
|
|
728
|
+
npx norn tests/ --junit --output-dir ./reports
|
|
729
|
+
|
|
730
|
+
# Generate HTML report
|
|
731
|
+
npx norn tests/ --html --output-dir ./reports
|
|
732
|
+
|
|
733
|
+
# Verbose output with colors
|
|
734
|
+
npx norn api-tests.norn -v
|
|
735
|
+
|
|
736
|
+
# Show help
|
|
737
|
+
npx norn --help
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### CLI Options
|
|
741
|
+
|
|
742
|
+
| Option | Description |
|
|
743
|
+
|--------|-------------|
|
|
744
|
+
| `-s, --sequence <name>` | Run a specific sequence by name |
|
|
745
|
+
| `-e, --env <name>` | Use a specific environment from .nornenv |
|
|
746
|
+
| `--tag <name>` | Filter by tag (AND logic, can repeat) |
|
|
747
|
+
| `--tags <list>` | Filter by comma-separated tags (OR logic) |
|
|
748
|
+
| `-j, --json` | Output results as JSON |
|
|
749
|
+
| `--junit` | Generate JUnit XML report |
|
|
750
|
+
| `--html` | Generate HTML report |
|
|
751
|
+
| `--output-dir <path>` | Save reports to directory (auto-timestamps filenames) |
|
|
752
|
+
| `-v, --verbose` | Show detailed output with colors |
|
|
753
|
+
| `--no-fail` | Don't exit with error code on failed tests |
|
|
754
|
+
| `-h, --help` | Show help message |
|
|
755
|
+
|
|
756
|
+
### CI/CD Example (GitHub Actions)
|
|
757
|
+
|
|
758
|
+
```yaml
|
|
759
|
+
jobs:
|
|
760
|
+
api-tests:
|
|
761
|
+
runs-on: ubuntu-latest
|
|
762
|
+
steps:
|
|
763
|
+
- uses: actions/checkout@v4
|
|
764
|
+
|
|
765
|
+
- name: Setup Node.js
|
|
766
|
+
uses: actions/setup-node@v4
|
|
767
|
+
with:
|
|
768
|
+
node-version: '20'
|
|
769
|
+
|
|
770
|
+
- name: Run API Tests
|
|
771
|
+
run: npx norn ./tests/ --junit --output-dir ./reports
|
|
772
|
+
|
|
773
|
+
- name: Upload Test Results
|
|
774
|
+
uses: actions/upload-artifact@v4
|
|
775
|
+
if: always()
|
|
776
|
+
with:
|
|
777
|
+
name: test-results
|
|
778
|
+
path: ./reports/*.xml
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
## Syntax Reference
|
|
782
|
+
|
|
783
|
+
| Syntax | Description |
|
|
784
|
+
|--------|-------------|
|
|
785
|
+
| `var name = value` | Declare a variable (literal) |
|
|
786
|
+
| `var name = "text"` | Declare a string variable |
|
|
787
|
+
| `var x = other.path` | Assign from expression (in sequences) |
|
|
788
|
+
| `{{name}}` | Reference a variable |
|
|
789
|
+
| `{{name.path}}` | Access nested property of JSON variable |
|
|
790
|
+
| `{{name[0].prop}}` | Access array element in JSON variable |
|
|
791
|
+
| `###` | Optional request separator |
|
|
792
|
+
| `[RequestName]` | Define a named reusable request |
|
|
793
|
+
| `run RequestName` | Execute a named request |
|
|
794
|
+
| `sequence Name` | Start a helper sequence block |
|
|
795
|
+
| `test sequence Name` | Start a test sequence (runs from CLI) |
|
|
796
|
+
| `@tagname` | Simple tag on a sequence |
|
|
797
|
+
| `@key(value)` | Key-value tag on a sequence |
|
|
798
|
+
| `end sequence` | End a sequence block |
|
|
799
|
+
| `var x = $1.path` | Capture value from response 1 |
|
|
800
|
+
| `$N.status` | Access status code of response N |
|
|
801
|
+
| `$N.body.path` | Access body property of response N |
|
|
802
|
+
| `$N.headers.Name` | Access header of response N |
|
|
803
|
+
| `$N.duration` | Access request duration in ms |
|
|
804
|
+
| `$N.cookies` | Access cookies from response N |
|
|
805
|
+
|
|
806
|
+
### Assertions
|
|
807
|
+
|
|
808
|
+
| Syntax | Description |
|
|
809
|
+
|--------|-------------|
|
|
810
|
+
| `assert $1.status == 200` | Equality check |
|
|
811
|
+
| `assert $1.body.count != 0` | Inequality check |
|
|
812
|
+
| `assert $1.body.age > 18` | Greater than |
|
|
813
|
+
| `assert $1.body.age >= 18` | Greater than or equal |
|
|
814
|
+
| `assert $1.body.age < 100` | Less than |
|
|
815
|
+
| `assert $1.body.age <= 100` | Less than or equal |
|
|
816
|
+
| `assert $1.body.email contains "@"` | String contains |
|
|
817
|
+
| `assert $1.body.name startsWith "J"` | String starts with |
|
|
818
|
+
| `assert $1.body.name endsWith "n"` | String ends with |
|
|
819
|
+
| `assert $1.body.email matches "regex"` | Regex match |
|
|
820
|
+
| `assert $1.body.id exists` | Property exists |
|
|
821
|
+
| `assert $1.body.deleted !exists` | Property does not exist |
|
|
822
|
+
| `assert $1.body.id isType number` | Type check (number, string, boolean, array, object, null) |
|
|
823
|
+
| `assert $1.duration < 5000` | Duration/timing check |
|
|
824
|
+
| `assert ... \| "message"` | Custom failure message |
|
|
825
|
+
|
|
826
|
+
### Control Flow
|
|
827
|
+
|
|
828
|
+
| Syntax | Description |
|
|
829
|
+
|--------|-------------|
|
|
830
|
+
| `if <condition>` | Start conditional block (uses assertion operators) |
|
|
831
|
+
| `end if` | End conditional block |
|
|
832
|
+
| `wait 2s` | Wait 2 seconds |
|
|
833
|
+
| `wait 500ms` | Wait 500 milliseconds |
|
|
834
|
+
|
|
835
|
+
### Scripts & Data
|
|
836
|
+
|
|
837
|
+
| Syntax | Description |
|
|
838
|
+
|--------|-------------|
|
|
839
|
+
| `run bash ./script.sh` | Run a bash script |
|
|
840
|
+
| `run powershell ./script.ps1` | Run a PowerShell script |
|
|
841
|
+
| `run js ./script.js` | Run a Node.js script |
|
|
842
|
+
| `var x = run js ./script.js` | Run script and capture output |
|
|
843
|
+
| `var data = run readJson ./file.json` | Load JSON file into variable |
|
|
844
|
+
| `data.property = value` | Update loaded JSON property |
|
|
845
|
+
| `print Message` | Print a message |
|
|
846
|
+
| `print Title \| Body` | Print with expandable body |
|
|
847
|
+
|
|
848
|
+
### Environments (.nornenv)
|
|
849
|
+
|
|
850
|
+
| Syntax | Description |
|
|
851
|
+
|--------|-------------|
|
|
852
|
+
| `var name = value` | Common variable (all environments) |
|
|
853
|
+
| `[env:name]` | Start environment section |
|
|
854
|
+
| `# comment` | Comment line |
|
|
855
|
+
|
|
856
|
+
### API Definitions (.nornapi)
|
|
857
|
+
|
|
858
|
+
| Syntax | Description |
|
|
859
|
+
|--------|-------------|
|
|
860
|
+
| `headers Name` | Start header group definition |
|
|
861
|
+
| `end headers` | End header group definition |
|
|
862
|
+
| `HeaderName: value` | Define a header (inside headers block) |
|
|
863
|
+
| `endpoints` | Start endpoints block |
|
|
864
|
+
| `end endpoints` | End endpoints block |
|
|
865
|
+
| `Name: METHOD path` | Define named endpoint (e.g., `GetUser: GET /users/{id}`) |
|
|
866
|
+
| `{param}` | Path parameter placeholder |
|
|
867
|
+
| `swagger https://...` | Import from OpenAPI/Swagger URL |
|
|
868
|
+
| `import "./file.nornapi"` | Import .nornapi file |
|
|
869
|
+
|
|
870
|
+
## Keyboard Shortcuts
|
|
871
|
+
|
|
872
|
+
| Shortcut | Command |
|
|
873
|
+
|----------|---------|
|
|
874
|
+
| `Ctrl+Alt+R` / `Cmd+Alt+R` | Send Request |
|
|
875
|
+
|
|
876
|
+
## Extension Commands
|
|
877
|
+
|
|
878
|
+
- `Norn: Send Request` - Send the HTTP request at cursor
|
|
879
|
+
- `Norn: Run Sequence` - Run the sequence at cursor
|
|
880
|
+
- `Norn: Select Environment` - Choose the active environment from .nornenv
|
|
881
|
+
- `Norn: Clear Cookies` - Clear all stored cookies
|
|
882
|
+
- `Norn: Show Stored Cookies` - Display cookies in output
|
|
883
|
+
|
|
884
|
+
## Requirements
|
|
885
|
+
|
|
886
|
+
- VS Code 1.108.1 or higher
|
|
887
|
+
- Node.js 22+ (for CLI and script execution)
|
|
888
|
+
|
|
889
|
+
## Release Notes
|
|
890
|
+
|
|
891
|
+
### 1.0.14
|
|
892
|
+
|
|
893
|
+
- **Variable Expression Assignment**: `var id = data.users[0].id` - extract values directly
|
|
894
|
+
- **String Literals**: `var name = "John"` - use quotes for string values
|
|
895
|
+
- **PowerShell Auto-Parsing**: Table and list output automatically converted to JSON
|
|
896
|
+
- **ANSI Code Stripping**: Clean PowerShell output without color code corruption
|
|
897
|
+
- **Improved Syntax Highlighting**: Different colors for strings, numbers, variables, booleans
|
|
898
|
+
- **IntelliSense in Assignments**: Shows defined variables after `var x = `
|
|
899
|
+
- **Invalid Assignment Diagnostics**: Red squiggly for unquoted strings with spaces
|
|
900
|
+
|
|
901
|
+
### 1.0.13
|
|
902
|
+
|
|
903
|
+
- **Assertions**: Full assertion system with comparison, type checking, existence, and regex operators
|
|
904
|
+
- **Environments**: `.nornenv` file support with dev/staging/prod configurations
|
|
905
|
+
- **Named Requests**: Define reusable requests with `[RequestName]` syntax
|
|
906
|
+
- **Conditionals**: `if/end if` blocks for conditional execution
|
|
907
|
+
- **Wait Commands**: `wait 2s` / `wait 500ms` for delays
|
|
908
|
+
- **JSON File Loading**: `run readJson` to load test data from JSON files
|
|
909
|
+
- **Property Updates**: Modify loaded JSON data inline
|
|
910
|
+
- **CLI Environments**: `--env` flag to select environment in CLI
|
|
911
|
+
|
|
912
|
+
### 1.0.0
|
|
913
|
+
|
|
914
|
+
- HTTP request support (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
|
|
915
|
+
- Variables with `var` and `{{}}`
|
|
916
|
+
- Sequences with response capture (`$N.path`)
|
|
917
|
+
- Script execution (`run bash/powershell/js`)
|
|
918
|
+
- Print statements with title and body
|
|
919
|
+
- Cookie jar with automatic persistence
|
|
920
|
+
- Full syntax highlighting
|
|
921
|
+
- IntelliSense for methods, headers, variables, keywords
|
|
922
|
+
- Diagnostic errors for undefined variables
|
|
923
|
+
- CLI for CI/CD pipelines
|
|
924
|
+
- JSON output mode for automation
|
|
925
|
+
|
|
926
|
+
## License
|
|
927
|
+
|
|
928
|
+
**Free for Personal Use** - You may use Norn for personal projects, learning, education, and non-commercial open-source projects at no cost.
|
|
929
|
+
|
|
930
|
+
**30-Day Commercial Evaluation** - Businesses may evaluate Norn free for 30 days before purchasing a license.
|
|
931
|
+
|
|
932
|
+
**Commercial Use Requires a License** - After the evaluation period, use within a business, by employees during work, or in CI/CD pipelines for commercial projects requires a license. Contact us for commercial licensing options.
|
|
933
|
+
|
|
934
|
+
See the [LICENSE](LICENSE) file for full terms.
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
**Enjoy using Norn!**
|