@assertrx/dino 1.0.10 → 1.0.11
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/package.json +1 -1
- package/prompts/generate-k6-test-from-endpoint.md +68 -0
- package/prompts/generate-k6-test-from-openapi.md +85 -0
- package/prompts/setup-k6.md +130 -0
- package/scripts/api-server.mjs +23 -0
- package/skills/analyzing-test-results/SKILL.md +150 -0
- package/skills/analyzing-test-results/reference/bottleneck-patterns.md +130 -0
- package/skills/analyzing-test-results/reference/metrics.md +134 -0
- package/skills/analyzing-test-results/reference/visualization.md +152 -0
- package/skills/designing-test-scenarios/SKILL.md +247 -0
- package/skills/designing-test-scenarios/reference/executors.md +215 -0
- package/skills/designing-test-scenarios/reference/test-types.md +215 -0
- package/skills/designing-test-scenarios/reference/thresholds.md +208 -0
- package/skills/generating-api-load-tests/SKILL.md +199 -0
- package/skills/generating-api-load-tests/reference/data-parameterization.md +343 -0
- package/skills/generating-api-load-tests/reference/grpc.md +315 -0
- package/skills/generating-api-load-tests/reference/http.md +380 -0
- package/skills/generating-api-load-tests/reference/websocket.md +369 -0
- package/skills/generating-browser-tests/SKILL.md +269 -0
- package/skills/generating-browser-tests/reference/browser-api.md +251 -0
- package/skills/generating-browser-tests/reference/web-vitals.md +148 -0
- package/skills/generating-tests-from-code/SKILL.md +142 -0
- package/skills/generating-tests-from-code/reference/auth-analysis.md +220 -0
- package/skills/generating-tests-from-code/reference/framework-patterns.md +242 -0
- package/skills/generating-tests-from-openapi/SKILL.md +234 -0
- package/skills/generating-tests-from-openapi/reference/openapi-auth-mapping.md +247 -0
- package/skills/generating-tests-from-openapi/reference/openapi-parsing.md +194 -0
- package/skills/operating-k6-in-ci-cd/SKILL.md +98 -0
- package/skills/operating-k6-in-ci-cd/reference/ci-cd-pipelines.md +265 -0
- package/skills/operating-k6-in-ci-cd/reference/extensions.md +144 -0
- package/skills/operating-k6-in-ci-cd/reference/k6-operator.md +214 -0
- package/skills/testing-resilience/SKILL.md +230 -0
- package/skills/testing-resilience/reference/fault-injection.md +153 -0
- package/skills/testing-resilience/reference/resilience-patterns.md +500 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# k6 gRPC API Reference
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
k6 supports gRPC load testing via the `k6/net/grpc` module. Supports unary RPCs, client/server/bidirectional streaming, TLS, metadata, and server reflection.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import grpc from 'k6/net/grpc';
|
|
11
|
+
import { check, sleep } from 'k6';
|
|
12
|
+
|
|
13
|
+
const client = new grpc.Client();
|
|
14
|
+
client.load(['definitions'], 'hello.proto'); // Load in init context
|
|
15
|
+
|
|
16
|
+
export const options = {
|
|
17
|
+
vus: 5,
|
|
18
|
+
duration: '30s',
|
|
19
|
+
thresholds: {
|
|
20
|
+
grpc_req_duration: ['p(95)<500'],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default function () {
|
|
25
|
+
client.connect('localhost:50051', { plaintext: true });
|
|
26
|
+
|
|
27
|
+
const response = client.invoke('hello.HelloService/SayHello', {
|
|
28
|
+
greeting: 'k6',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
check(response, {
|
|
32
|
+
'status is OK': (r) => r && r.status === grpc.StatusOK,
|
|
33
|
+
'message contains greeting': (r) => r && r.message.reply.includes('k6'),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
client.close();
|
|
37
|
+
sleep(1);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Client Setup
|
|
42
|
+
|
|
43
|
+
### Creating and Loading Protos
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
const client = new grpc.Client();
|
|
47
|
+
|
|
48
|
+
// Load from proto files (must be in init context)
|
|
49
|
+
client.load(['path/to/import/dir'], 'service.proto');
|
|
50
|
+
client.load([], 'another.proto'); // No additional import paths
|
|
51
|
+
|
|
52
|
+
// Load from compiled protoset file
|
|
53
|
+
client.loadProtoset('path/to/service.protoset');
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Connecting
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
// Plaintext (no TLS)
|
|
60
|
+
client.connect('localhost:50051', { plaintext: true });
|
|
61
|
+
|
|
62
|
+
// TLS with default settings
|
|
63
|
+
client.connect('api.example.com:443');
|
|
64
|
+
|
|
65
|
+
// TLS with custom certificates
|
|
66
|
+
client.connect('api.example.com:443', {
|
|
67
|
+
tls: {
|
|
68
|
+
cacerts: open('ca.pem'), // CA certificate
|
|
69
|
+
cert: open('client.pem'), // Client certificate
|
|
70
|
+
key: open('client-key.pem'), // Client key
|
|
71
|
+
password: 'keyfile-password', // Key password (optional)
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// With server reflection (no proto files needed)
|
|
76
|
+
client.connect('localhost:50051', {
|
|
77
|
+
plaintext: true,
|
|
78
|
+
reflect: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// With timeout
|
|
82
|
+
client.connect('localhost:50051', {
|
|
83
|
+
plaintext: true,
|
|
84
|
+
timeout: '10s',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Max message sizes
|
|
88
|
+
client.connect('localhost:50051', {
|
|
89
|
+
plaintext: true,
|
|
90
|
+
maxReceiveSize: 16 * 1024 * 1024, // 16MB (default 4MB)
|
|
91
|
+
maxSendSize: 16 * 1024 * 1024,
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Connection Parameters
|
|
96
|
+
|
|
97
|
+
| Parameter | Type | Default | Description |
|
|
98
|
+
|-----------|------|---------|-------------|
|
|
99
|
+
| `plaintext` | boolean | `false` | Use insecure connection (no TLS) |
|
|
100
|
+
| `reflect` | boolean | `false` | Use gRPC server reflection |
|
|
101
|
+
| `reflectMetadata` | object | `null` | Metadata for reflection request |
|
|
102
|
+
| `timeout` | string/number | `'60s'` | Connection timeout |
|
|
103
|
+
| `maxReceiveSize` | number | 4MB | Max message size to receive |
|
|
104
|
+
| `maxSendSize` | number | ~2GB | Max message size to send |
|
|
105
|
+
| `tls` | object | `null` | TLS settings (cert, key, password, cacerts) |
|
|
106
|
+
|
|
107
|
+
## Unary RPC Calls
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// Basic invoke
|
|
111
|
+
const response = client.invoke('package.Service/Method', {
|
|
112
|
+
field1: 'value1',
|
|
113
|
+
field2: 42,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// With metadata (headers)
|
|
117
|
+
const response = client.invoke('package.Service/Method', requestMessage, {
|
|
118
|
+
metadata: {
|
|
119
|
+
'authorization': 'Bearer token123',
|
|
120
|
+
'x-request-id': 'abc-123',
|
|
121
|
+
'bin-data-bin': encoding.b64encode('binary data'), // Binary metadata (key ends with -bin)
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// With custom tags and timeout
|
|
126
|
+
const response = client.invoke('package.Service/Method', requestMessage, {
|
|
127
|
+
tags: { name: 'MyRPC' },
|
|
128
|
+
timeout: '5s',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Async invoke
|
|
132
|
+
const promise = client.asyncInvoke('package.Service/Method', requestMessage);
|
|
133
|
+
// ... do other work ...
|
|
134
|
+
const response = await promise;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Invoke Parameters
|
|
138
|
+
|
|
139
|
+
| Parameter | Type | Description |
|
|
140
|
+
|-----------|------|-------------|
|
|
141
|
+
| `metadata` | object | Custom metadata headers |
|
|
142
|
+
| `tags` | object | Custom metric tags |
|
|
143
|
+
| `timeout` | string/number | Request timeout |
|
|
144
|
+
| `authority` | string | `:authority` pseudo-header override |
|
|
145
|
+
| `discardResponseMessage` | boolean | Discard response to reduce memory |
|
|
146
|
+
|
|
147
|
+
## Response Object
|
|
148
|
+
|
|
149
|
+
| Property | Type | Description |
|
|
150
|
+
|----------|------|-------------|
|
|
151
|
+
| `status` | number | gRPC status code |
|
|
152
|
+
| `message` | object | Response message as JSON (null if error) |
|
|
153
|
+
| `headers` | object | Response metadata headers |
|
|
154
|
+
| `trailers` | object | Response metadata trailers |
|
|
155
|
+
| `error` | object | Error details (if status != OK) |
|
|
156
|
+
|
|
157
|
+
## gRPC Status Codes
|
|
158
|
+
|
|
159
|
+
| Constant | Code | Description |
|
|
160
|
+
|----------|------|-------------|
|
|
161
|
+
| `grpc.StatusOK` | 0 | Success |
|
|
162
|
+
| `grpc.StatusCanceled` | 1 | Operation cancelled |
|
|
163
|
+
| `grpc.StatusUnknown` | 2 | Unknown error |
|
|
164
|
+
| `grpc.StatusInvalidArgument` | 3 | Invalid argument |
|
|
165
|
+
| `grpc.StatusDeadlineExceeded` | 4 | Deadline exceeded |
|
|
166
|
+
| `grpc.StatusNotFound` | 5 | Resource not found |
|
|
167
|
+
| `grpc.StatusAlreadyExists` | 6 | Resource already exists |
|
|
168
|
+
| `grpc.StatusPermissionDenied` | 7 | Permission denied |
|
|
169
|
+
| `grpc.StatusResourceExhausted` | 8 | Resource exhausted |
|
|
170
|
+
| `grpc.StatusFailedPrecondition` | 9 | Failed precondition |
|
|
171
|
+
| `grpc.StatusAborted` | 10 | Operation aborted |
|
|
172
|
+
| `grpc.StatusOutOfRange` | 11 | Out of range |
|
|
173
|
+
| `grpc.StatusUnimplemented` | 12 | Not implemented |
|
|
174
|
+
| `grpc.StatusInternal` | 13 | Internal error |
|
|
175
|
+
| `grpc.StatusUnavailable` | 14 | Service unavailable |
|
|
176
|
+
| `grpc.StatusDataLoss` | 15 | Data loss |
|
|
177
|
+
| `grpc.StatusUnauthenticated` | 16 | Unauthenticated |
|
|
178
|
+
|
|
179
|
+
## Streaming
|
|
180
|
+
|
|
181
|
+
### Client Streaming
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
const stream = new grpc.Stream(client, 'package.Service/ClientStreamMethod');
|
|
185
|
+
|
|
186
|
+
stream.on('data', (response) => {
|
|
187
|
+
check(response, {
|
|
188
|
+
'stream response OK': (r) => r.status === grpc.StatusOK,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
stream.on('error', (err) => {
|
|
193
|
+
console.error('Stream error:', JSON.stringify(err));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
stream.on('end', () => {
|
|
197
|
+
console.log('Stream ended');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Send multiple messages
|
|
201
|
+
stream.write({ data: 'message 1' });
|
|
202
|
+
stream.write({ data: 'message 2' });
|
|
203
|
+
stream.write({ data: 'message 3' });
|
|
204
|
+
stream.end(); // Signal end of client stream
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Server Streaming
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
const stream = new grpc.Stream(client, 'package.Service/ServerStreamMethod');
|
|
211
|
+
|
|
212
|
+
stream.on('data', (message) => {
|
|
213
|
+
console.log('Received:', JSON.stringify(message));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
stream.on('error', (err) => {
|
|
217
|
+
console.error('Error:', JSON.stringify(err));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
stream.on('end', () => {
|
|
221
|
+
console.log('All messages received');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Send single request to start server stream
|
|
225
|
+
stream.write({ query: 'search term' });
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Bidirectional Streaming
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
const stream = new grpc.Stream(client, 'package.Service/BidiStreamMethod', {
|
|
232
|
+
metadata: { 'authorization': 'Bearer token123' },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
stream.on('data', (message) => {
|
|
236
|
+
console.log('Server:', JSON.stringify(message));
|
|
237
|
+
// Respond to server messages
|
|
238
|
+
if (message.needsReply) {
|
|
239
|
+
stream.write({ reply: 'acknowledged' });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
stream.on('error', (err) => {
|
|
244
|
+
console.error('Error:', JSON.stringify(err));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
stream.on('end', () => {
|
|
248
|
+
console.log('Stream ended');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
stream.write({ action: 'subscribe', topic: 'updates' });
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Stream API
|
|
255
|
+
|
|
256
|
+
| Method/Event | Description |
|
|
257
|
+
|-------------|-------------|
|
|
258
|
+
| `new grpc.Stream(client, url, [params])` | Create stream |
|
|
259
|
+
| `stream.write(message)` | Send message to server |
|
|
260
|
+
| `stream.end()` | Signal end of client messages |
|
|
261
|
+
| `stream.on('data', handler)` | Handle incoming messages |
|
|
262
|
+
| `stream.on('error', handler)` | Handle errors |
|
|
263
|
+
| `stream.on('end', handler)` | Handle stream completion |
|
|
264
|
+
|
|
265
|
+
## Complete Example: Authenticated gRPC Test
|
|
266
|
+
|
|
267
|
+
```javascript
|
|
268
|
+
import grpc from 'k6/net/grpc';
|
|
269
|
+
import { check, sleep } from 'k6';
|
|
270
|
+
|
|
271
|
+
const client = new grpc.Client();
|
|
272
|
+
client.load(['proto'], 'user_service.proto');
|
|
273
|
+
|
|
274
|
+
export const options = {
|
|
275
|
+
scenarios: {
|
|
276
|
+
grpc_test: {
|
|
277
|
+
executor: 'constant-arrival-rate',
|
|
278
|
+
rate: 50,
|
|
279
|
+
timeUnit: '1s',
|
|
280
|
+
duration: '1m',
|
|
281
|
+
preAllocatedVUs: 20,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
thresholds: {
|
|
285
|
+
grpc_req_duration: ['p(95)<300', 'p(99)<500'],
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export function setup() {
|
|
290
|
+
client.connect('auth.example.com:443');
|
|
291
|
+
const authRes = client.invoke('auth.AuthService/Login', {
|
|
292
|
+
username: 'loadtest',
|
|
293
|
+
password: 'secret',
|
|
294
|
+
});
|
|
295
|
+
client.close();
|
|
296
|
+
return { token: authRes.message.token };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export default function (data) {
|
|
300
|
+
client.connect('api.example.com:443');
|
|
301
|
+
|
|
302
|
+
const res = client.invoke('user.UserService/GetUser', { id: 1 }, {
|
|
303
|
+
metadata: { authorization: `Bearer ${data.token}` },
|
|
304
|
+
tags: { name: 'GetUser' },
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
check(res, {
|
|
308
|
+
'status OK': (r) => r && r.status === grpc.StatusOK,
|
|
309
|
+
'user found': (r) => r && r.message !== null,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
client.close();
|
|
313
|
+
sleep(0.5);
|
|
314
|
+
}
|
|
315
|
+
```
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# k6 HTTP API Reference
|
|
2
|
+
|
|
3
|
+
## HTTP Methods
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
import http from 'k6/http';
|
|
7
|
+
|
|
8
|
+
http.get(url, [params]) // GET request
|
|
9
|
+
http.post(url, [body], [params]) // POST request
|
|
10
|
+
http.put(url, [body], [params]) // PUT request
|
|
11
|
+
http.patch(url, [body], [params]) // PATCH request
|
|
12
|
+
http.del(url, [body], [params]) // DELETE request
|
|
13
|
+
http.head(url, [params]) // HEAD request
|
|
14
|
+
http.options(url, [body], [params]) // OPTIONS request
|
|
15
|
+
http.request(method, url, [body], [params]) // Any HTTP method
|
|
16
|
+
http.asyncRequest(method, url, [body], [params]) // Async (returns Promise)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Request Parameters (Params Object)
|
|
20
|
+
|
|
21
|
+
| Parameter | Type | Description |
|
|
22
|
+
|-----------|------|-------------|
|
|
23
|
+
| `auth` | string | `'basic'`, `'digest'`, or `'ntlm'` |
|
|
24
|
+
| `cookies` | object | Request-scoped cookies `{ name: 'value' }` or `{ name: { value: 'v', replace: true } }` |
|
|
25
|
+
| `headers` | object | HTTP headers `{ 'Content-Type': 'application/json' }` |
|
|
26
|
+
| `jar` | CookieJar | Override default VU cookie jar |
|
|
27
|
+
| `redirects` | number | Max redirects to follow (default: follow all) |
|
|
28
|
+
| `tags` | object | Custom metric tags `{ name: 'MyRequest' }` |
|
|
29
|
+
| `timeout` | string/number | Max wait time (default: `'60s'`) |
|
|
30
|
+
| `compression` | string | `'gzip'`, `'deflate'`, `'br'`, `'zstd'` or combinations |
|
|
31
|
+
| `responseType` | string | `'text'` (default), `'binary'`, or `'none'` |
|
|
32
|
+
| `responseCallback` | function | Custom response callback (e.g., `http.expectedStatuses(200, 201)`) |
|
|
33
|
+
|
|
34
|
+
## Response Object
|
|
35
|
+
|
|
36
|
+
### Properties
|
|
37
|
+
|
|
38
|
+
| Property | Type | Description |
|
|
39
|
+
|----------|------|-------------|
|
|
40
|
+
| `status` | number | HTTP status code |
|
|
41
|
+
| `statusText` | string | HTTP status text |
|
|
42
|
+
| `body` | string | Response body |
|
|
43
|
+
| `headers` | object | Response headers (canonical key format) |
|
|
44
|
+
| `cookies` | object | Response cookies with metadata |
|
|
45
|
+
| `error` | string | Error message (non-HTTP errors) |
|
|
46
|
+
| `error_code` | number | Error code |
|
|
47
|
+
| `proto` | string | HTTP protocol (`'HTTP/1.0'`, `'HTTP/1.1'`, `'HTTP/2.0'`) |
|
|
48
|
+
| `remote_ip` | string | Server IP address |
|
|
49
|
+
| `remote_port` | number | Server port number |
|
|
50
|
+
| `url` | string | Final URL (after redirects) |
|
|
51
|
+
| `request` | object | Request details (body, cookies, headers, method, url) |
|
|
52
|
+
| `timings` | object | See Timings below |
|
|
53
|
+
| `tls_version` | string | TLS version (e.g., `'tls1.2'`) |
|
|
54
|
+
| `tls_cipher_suite` | string | Cipher suite used |
|
|
55
|
+
|
|
56
|
+
### Timings
|
|
57
|
+
|
|
58
|
+
| Property | Description |
|
|
59
|
+
|----------|-------------|
|
|
60
|
+
| `timings.blocked` | Time spent blocked before starting request (DNS lookup, connection queue) |
|
|
61
|
+
| `timings.connecting` | Time spent establishing TCP connection |
|
|
62
|
+
| `timings.tls_handshaking` | Time spent on TLS handshake |
|
|
63
|
+
| `timings.sending` | Time spent sending request body |
|
|
64
|
+
| `timings.waiting` | Time spent waiting for server response (TTFB) |
|
|
65
|
+
| `timings.receiving` | Time spent receiving response body |
|
|
66
|
+
| `timings.duration` | Total request time (sending + waiting + receiving) |
|
|
67
|
+
|
|
68
|
+
### Methods
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
res.json() // Parse body as JSON
|
|
72
|
+
res.json('path.to.field') // Parse JSON with gjson selector
|
|
73
|
+
res.html() // Parse body as HTML (returns Selection)
|
|
74
|
+
res.submitForm({ // Submit an HTML form
|
|
75
|
+
formSelector: 'form',
|
|
76
|
+
fields: { username: 'test' },
|
|
77
|
+
submitSelector: '[type="submit"]',
|
|
78
|
+
params: {},
|
|
79
|
+
})
|
|
80
|
+
res.clickLink({ selector: 'a' }) // Click a link in HTML response
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Authentication Patterns
|
|
84
|
+
|
|
85
|
+
### Basic Auth
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
// Method 1: Using auth param
|
|
89
|
+
const res = http.get('https://api.example.com/protected', {
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: 'Basic ' + encoding.b64encode('user:pass'),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Method 2: Using built-in auth
|
|
96
|
+
const res = http.get('https://user:pass@api.example.com/protected', {
|
|
97
|
+
auth: 'basic',
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Bearer Token
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
export function setup() {
|
|
105
|
+
const loginRes = http.post('https://api.example.com/auth/login',
|
|
106
|
+
JSON.stringify({ username: 'user', password: 'pass' }),
|
|
107
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
108
|
+
);
|
|
109
|
+
return { token: loginRes.json('access_token') };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default function (data) {
|
|
113
|
+
const params = {
|
|
114
|
+
headers: { Authorization: `Bearer ${data.token}` },
|
|
115
|
+
};
|
|
116
|
+
http.get('https://api.example.com/protected', params);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### OAuth 2.0 (Client Credentials)
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
import encoding from 'k6/encoding';
|
|
124
|
+
|
|
125
|
+
export function setup() {
|
|
126
|
+
const res = http.post('https://auth.example.com/oauth/token',
|
|
127
|
+
'grant_type=client_credentials&scope=read',
|
|
128
|
+
{
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
131
|
+
Authorization: `Basic ${encoding.b64encode('client_id:client_secret')}`,
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
return { token: res.json('access_token') };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default function (data) {
|
|
139
|
+
http.get('https://api.example.com/resource', {
|
|
140
|
+
headers: { Authorization: `Bearer ${data.token}` },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### API Key
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
// In header
|
|
149
|
+
http.get('https://api.example.com/data', {
|
|
150
|
+
headers: { 'X-API-Key': 'your-api-key-here' },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// In query parameter
|
|
154
|
+
http.get('https://api.example.com/data?api_key=your-api-key-here');
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Cookie-Based Session
|
|
158
|
+
|
|
159
|
+
Note: `setup()` runs in a separate VU, so cookies set during `setup()` do **not** transfer to VUs running `default()`. For cookie-based auth, login inside `default()`:
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
export default function () {
|
|
163
|
+
// Login per VU — cookies are stored in this VU's cookie jar
|
|
164
|
+
const loginRes = http.post('https://app.example.com/login', {
|
|
165
|
+
username: 'user',
|
|
166
|
+
password: 'pass',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Subsequent requests in this VU automatically include session cookies
|
|
170
|
+
http.get('https://app.example.com/dashboard');
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## CRUD Patterns
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
import http from 'k6/http';
|
|
178
|
+
import { check } from 'k6';
|
|
179
|
+
|
|
180
|
+
const BASE_URL = 'https://api.example.com';
|
|
181
|
+
const HEADERS = { 'Content-Type': 'application/json' };
|
|
182
|
+
|
|
183
|
+
export default function () {
|
|
184
|
+
// CREATE
|
|
185
|
+
const createRes = http.post(`${BASE_URL}/items`,
|
|
186
|
+
JSON.stringify({ name: 'Test Item', value: 42 }),
|
|
187
|
+
{ headers: HEADERS }
|
|
188
|
+
);
|
|
189
|
+
check(createRes, { 'created': (r) => r.status === 201 });
|
|
190
|
+
const itemId = createRes.json('id');
|
|
191
|
+
|
|
192
|
+
// READ
|
|
193
|
+
const getRes = http.get(`${BASE_URL}/items/${itemId}`);
|
|
194
|
+
check(getRes, { 'fetched': (r) => r.status === 200 });
|
|
195
|
+
|
|
196
|
+
// UPDATE
|
|
197
|
+
const updateRes = http.put(`${BASE_URL}/items/${itemId}`,
|
|
198
|
+
JSON.stringify({ name: 'Updated Item', value: 99 }),
|
|
199
|
+
{ headers: HEADERS }
|
|
200
|
+
);
|
|
201
|
+
check(updateRes, { 'updated': (r) => r.status === 200 });
|
|
202
|
+
|
|
203
|
+
// DELETE
|
|
204
|
+
const deleteRes = http.del(`${BASE_URL}/items/${itemId}`);
|
|
205
|
+
check(deleteRes, { 'deleted': (r) => r.status === 204 });
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Batch Requests
|
|
210
|
+
|
|
211
|
+
Send multiple requests in parallel:
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
// Array format
|
|
215
|
+
const responses = http.batch([
|
|
216
|
+
['GET', 'https://api.example.com/users'],
|
|
217
|
+
['GET', 'https://api.example.com/products'],
|
|
218
|
+
['POST', 'https://api.example.com/logs', JSON.stringify({ event: 'test' }), { headers: HEADERS }],
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
// Named object format (easier to reference results)
|
|
222
|
+
const responses = http.batch({
|
|
223
|
+
users: { method: 'GET', url: 'https://api.example.com/users' },
|
|
224
|
+
products: { method: 'GET', url: 'https://api.example.com/products' },
|
|
225
|
+
});
|
|
226
|
+
check(responses.users, { 'users ok': (r) => r.status === 200 });
|
|
227
|
+
check(responses.products, { 'products ok': (r) => r.status === 200 });
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## File Uploads
|
|
231
|
+
|
|
232
|
+
### Simple File Upload
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
import http from 'k6/http';
|
|
236
|
+
|
|
237
|
+
const file = open('test-file.png', 'b'); // Binary mode, in init context
|
|
238
|
+
|
|
239
|
+
export default function () {
|
|
240
|
+
const res = http.post('https://api.example.com/upload', {
|
|
241
|
+
file: http.file(file, 'test-file.png', 'image/png'),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Multipart Form Data
|
|
247
|
+
|
|
248
|
+
```javascript
|
|
249
|
+
const imgFile = open('photo.jpg', 'b');
|
|
250
|
+
const docFile = open('report.pdf', 'b');
|
|
251
|
+
|
|
252
|
+
export default function () {
|
|
253
|
+
const res = http.post('https://api.example.com/upload', {
|
|
254
|
+
profile_photo: http.file(imgFile, 'photo.jpg', 'image/jpeg'),
|
|
255
|
+
document: http.file(docFile, 'report.pdf', 'application/pdf'),
|
|
256
|
+
description: 'Test upload',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Correlation (Extracting Dynamic Values)
|
|
262
|
+
|
|
263
|
+
### From JSON Response
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
const res = http.get('https://api.example.com/session');
|
|
267
|
+
const sessionId = res.json('data.session_id');
|
|
268
|
+
const items = res.json('data.items.#.id'); // Array of IDs (gjson syntax)
|
|
269
|
+
|
|
270
|
+
http.get(`https://api.example.com/data?session=${sessionId}`);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### From HTML Response
|
|
274
|
+
|
|
275
|
+
```javascript
|
|
276
|
+
const res = http.get('https://app.example.com/form');
|
|
277
|
+
const doc = res.html();
|
|
278
|
+
const csrfToken = doc.find('input[name="csrf_token"]').attr('value');
|
|
279
|
+
|
|
280
|
+
http.post('https://app.example.com/submit', {
|
|
281
|
+
csrf_token: csrfToken,
|
|
282
|
+
data: 'test',
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### From Headers
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
const res = http.post('https://api.example.com/resource', payload);
|
|
290
|
+
const location = res.headers['Location'];
|
|
291
|
+
http.get(location); // Follow redirect manually
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Cookie Handling
|
|
295
|
+
|
|
296
|
+
### Manual Cookie Jar
|
|
297
|
+
|
|
298
|
+
```javascript
|
|
299
|
+
const jar = http.cookieJar();
|
|
300
|
+
|
|
301
|
+
// Set cookies
|
|
302
|
+
jar.set('https://api.example.com', 'session', 'abc123', {
|
|
303
|
+
domain: 'api.example.com',
|
|
304
|
+
path: '/',
|
|
305
|
+
secure: true,
|
|
306
|
+
max_age: 3600,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Get cookies
|
|
310
|
+
const cookies = jar.cookiesForURL('https://api.example.com');
|
|
311
|
+
console.log(cookies.session); // ['abc123']
|
|
312
|
+
|
|
313
|
+
// Clear cookies
|
|
314
|
+
jar.clear('https://api.example.com');
|
|
315
|
+
|
|
316
|
+
// Delete specific cookie
|
|
317
|
+
jar.delete('https://api.example.com', 'session');
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Per-Request Cookies
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
http.get('https://api.example.com/data', {
|
|
324
|
+
cookies: {
|
|
325
|
+
session: 'abc123',
|
|
326
|
+
preference: { value: 'dark', replace: true }, // replace VU jar cookie
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## HTML Form Submission
|
|
332
|
+
|
|
333
|
+
```javascript
|
|
334
|
+
// Load the page with the form
|
|
335
|
+
const res = http.get('https://app.example.com/login');
|
|
336
|
+
|
|
337
|
+
// Submit the form
|
|
338
|
+
const submitRes = res.submitForm({
|
|
339
|
+
formSelector: 'form#login',
|
|
340
|
+
fields: {
|
|
341
|
+
username: 'testuser',
|
|
342
|
+
password: 'testpass',
|
|
343
|
+
},
|
|
344
|
+
submitSelector: 'button[type="submit"]',
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Expected Statuses
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
// Mark specific statuses as expected (affects http_req_failed metric)
|
|
352
|
+
const res = http.get('https://api.example.com/maybe-404', {
|
|
353
|
+
responseCallback: http.expectedStatuses(200, 404),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Range of statuses
|
|
357
|
+
http.get(url, {
|
|
358
|
+
responseCallback: http.expectedStatuses({ min: 200, max: 299 }),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Set globally
|
|
362
|
+
http.setResponseCallback(http.expectedStatuses({ min: 200, max: 399 }));
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## URL Grouping with Name Tag
|
|
366
|
+
|
|
367
|
+
Prevent dynamic URLs from creating separate metric entries:
|
|
368
|
+
|
|
369
|
+
```javascript
|
|
370
|
+
// Without grouping: each user ID creates a separate metric
|
|
371
|
+
http.get(`https://api.example.com/users/${userId}`);
|
|
372
|
+
|
|
373
|
+
// With grouping: all requests grouped under one metric
|
|
374
|
+
http.get(`https://api.example.com/users/${userId}`, {
|
|
375
|
+
tags: { name: 'GetUser' },
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Using http.url template literal (auto-groups)
|
|
379
|
+
http.get(http.url`https://api.example.com/users/${userId}`);
|
|
380
|
+
```
|