@elench/testkit 0.1.81 → 0.1.83
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/README.md +64 -27
- package/lib/cli/agents/index.mjs +64 -0
- package/lib/cli/agents/investigate.mjs +75 -0
- package/lib/cli/agents/investigation-context.mjs +102 -0
- package/lib/cli/agents/investigation-context.test.mjs +144 -0
- package/lib/cli/agents/prompt-builder.mjs +25 -0
- package/lib/cli/agents/providers/claude.mjs +74 -0
- package/lib/cli/agents/providers/claude.test.mjs +95 -0
- package/lib/cli/agents/providers/codex.mjs +83 -0
- package/lib/cli/agents/providers/codex.test.mjs +93 -0
- package/lib/cli/agents/providers/shared.mjs +134 -0
- package/lib/cli/command-helpers.mjs +53 -25
- package/lib/cli/command-helpers.test.mjs +122 -0
- package/lib/cli/commands/investigate.mjs +87 -0
- package/lib/cli/commands/investigate.test.mjs +83 -0
- package/lib/cli/entrypoint.mjs +3 -0
- package/lib/cli/presentation/colors.mjs +12 -0
- package/lib/cli/presentation/events-reporter.mjs +135 -0
- package/lib/cli/presentation/events-reporter.test.mjs +73 -0
- package/lib/cli/presentation/summary-box.mjs +11 -11
- package/lib/cli/presentation/summary-box.test.mjs +17 -0
- package/lib/cli/presentation/tree-reporter.mjs +159 -0
- package/lib/cli/presentation/tree-reporter.test.mjs +166 -0
- package/lib/cli/tui/run-app.mjs +1 -0
- package/lib/cli/tui/run-session-app.mjs +370 -0
- package/lib/cli/tui/run-session-app.test.mjs +50 -0
- package/lib/cli/tui/run-session-state.mjs +481 -0
- package/lib/cli/tui/run-tree-state.mjs +1 -0
- package/lib/cli/tui/run-tree-state.test.mjs +324 -0
- package/lib/config-api/auth-fixtures.mjs +767 -0
- package/lib/config-api/index.d.ts +92 -108
- package/lib/config-api/index.mjs +22 -12
- package/lib/config-api/index.test.mjs +103 -210
- package/lib/discovery/index.mjs +1 -1
- package/lib/index.d.ts +34 -10
- package/lib/runner/orchestrator.mjs +1 -0
- package/lib/runtime/index.d.ts +177 -27
- package/lib/runtime/index.mjs +68 -3
- package/lib/runtime-src/k6/http-assertions.js +31 -1
- package/lib/runtime-src/k6/http-checks.js +120 -0
- package/lib/runtime-src/k6/http-checks.test.mjs +120 -0
- package/lib/runtime-src/k6/http-suite-runtime.js +151 -0
- package/lib/runtime-src/k6/http.js +285 -56
- package/lib/runtime-src/k6/http.test.mjs +205 -0
- package/lib/runtime-src/k6/scenario-suite.js +13 -110
- package/lib/runtime-src/k6/suite.js +13 -107
- package/lib/runtime-src/shared/error-body.mjs +42 -0
- package/lib/runtime-src/shared/http-parsing.mjs +68 -0
- package/lib/runtime-src/shared/http-parsing.test.mjs +69 -0
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +5 -5
- package/lib/config-api/profiles.mjs +0 -640
package/lib/runtime/index.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface RuntimeResponse {
|
|
|
23
23
|
|
|
24
24
|
export interface RuntimeHttpTrace {
|
|
25
25
|
id: string;
|
|
26
|
+
actorName: string | null;
|
|
27
|
+
organizationKey?: string | null;
|
|
28
|
+
organizationName?: string | null;
|
|
26
29
|
requestId: string;
|
|
27
30
|
startedAt: string;
|
|
28
31
|
finishedAt?: string;
|
|
@@ -93,6 +96,27 @@ export interface RuntimeHttpClient {
|
|
|
93
96
|
put(url: string, body?: unknown, params?: HttpRequestParams): RuntimeResponse;
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
export interface RuntimeActorRecord<TSession = Record<string, unknown>> {
|
|
100
|
+
actorIndex: number;
|
|
101
|
+
actorName: string;
|
|
102
|
+
email?: string;
|
|
103
|
+
name?: string;
|
|
104
|
+
organizationKey?: string;
|
|
105
|
+
organizationName?: string;
|
|
106
|
+
session: TSession | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface AuthSessionBundle<TSession = Record<string, unknown>> {
|
|
110
|
+
actors: Record<string, RuntimeActorRecord<TSession> & TSession>;
|
|
111
|
+
primaryActor: string | null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface HttpHeaderBuildContext<TSession = Record<string, unknown>> {
|
|
115
|
+
actor: (RuntimeActorRecord<TSession> & TSession) | null;
|
|
116
|
+
actorName: string | null;
|
|
117
|
+
sessionBundle: AuthSessionBundle<TSession> | null;
|
|
118
|
+
}
|
|
119
|
+
|
|
96
120
|
export interface Metric {
|
|
97
121
|
add(value: number): void;
|
|
98
122
|
}
|
|
@@ -109,53 +133,105 @@ export declare class Trend implements Metric {
|
|
|
109
133
|
|
|
110
134
|
export interface HttpClientConfig<TSetup = unknown> {
|
|
111
135
|
baseUrl: string;
|
|
136
|
+
defaultActor?: string | null;
|
|
112
137
|
defaultHeaders?: RuntimeHeaders;
|
|
113
|
-
getHeaders?: (
|
|
114
|
-
getRawHeaders?: (
|
|
138
|
+
getHeaders?: (context: HttpHeaderBuildContext<TSetup>) => RuntimeHeaders | void;
|
|
139
|
+
getRawHeaders?: (context: HttpHeaderBuildContext<TSetup>) => RuntimeHeaders | void;
|
|
115
140
|
routeHeaders?: RuntimeHeaders;
|
|
141
|
+
sessionBundle?: AuthSessionBundle<TSetup> | null;
|
|
116
142
|
}
|
|
117
143
|
|
|
118
|
-
export interface
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
export interface MultipartFileInput {
|
|
145
|
+
contentType?: string;
|
|
146
|
+
data: unknown;
|
|
147
|
+
field: string;
|
|
148
|
+
filename?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface MultipartPayload {
|
|
152
|
+
fields?: Record<string, unknown>;
|
|
153
|
+
files?: MultipartFileInput[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface MultipartRequestClient {
|
|
157
|
+
patch(path: string, payload: MultipartPayload, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
158
|
+
post(path: string, payload: MultipartPayload, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
159
|
+
put(path: string, payload: MultipartPayload, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface RawRequestClient {
|
|
163
|
+
(
|
|
164
|
+
method: RuntimeMethod,
|
|
127
165
|
path: string,
|
|
128
|
-
setupData?: TSetup | null,
|
|
129
166
|
body?: unknown,
|
|
130
167
|
extraHeaders?: RuntimeHeaders
|
|
131
168
|
): RuntimeResponse;
|
|
132
|
-
|
|
169
|
+
as(actorName: string): RawRequestClient;
|
|
170
|
+
delete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
171
|
+
get(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
172
|
+
headers(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
|
|
173
|
+
multipart: MultipartRequestClient;
|
|
174
|
+
patch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
175
|
+
post(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
176
|
+
put(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
177
|
+
request(
|
|
178
|
+
method: RuntimeMethod,
|
|
133
179
|
path: string,
|
|
134
|
-
setupData?: TSetup | null,
|
|
135
180
|
body?: unknown,
|
|
136
181
|
extraHeaders?: RuntimeHeaders
|
|
137
182
|
): RuntimeResponse;
|
|
138
|
-
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface ActorRequestClient {
|
|
186
|
+
headers(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
|
|
187
|
+
multipart: MultipartRequestClient;
|
|
188
|
+
raw(
|
|
189
|
+
method: RuntimeMethod,
|
|
139
190
|
path: string,
|
|
140
|
-
setupData?: TSetup | null,
|
|
141
191
|
body?: unknown,
|
|
142
192
|
extraHeaders?: RuntimeHeaders
|
|
143
193
|
): RuntimeResponse;
|
|
144
|
-
|
|
194
|
+
rawDelete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
195
|
+
rawGet(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
196
|
+
rawHeaders(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
|
|
197
|
+
rawMultipart: MultipartRequestClient;
|
|
198
|
+
rawPatch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
199
|
+
rawPost(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
200
|
+
rawPut(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
201
|
+
rawReq: RawRequestClient;
|
|
202
|
+
delete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
203
|
+
get(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
204
|
+
patch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
205
|
+
post(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
206
|
+
put(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
207
|
+
request(
|
|
145
208
|
method: RuntimeMethod,
|
|
146
209
|
path: string,
|
|
147
210
|
body?: unknown,
|
|
148
211
|
extraHeaders?: RuntimeHeaders
|
|
149
212
|
): RuntimeResponse;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface HttpClient<TSetup = unknown> {
|
|
216
|
+
as(actorName: string): ActorRequestClient;
|
|
217
|
+
headers(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
|
|
218
|
+
multipart: MultipartRequestClient;
|
|
219
|
+
delete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
220
|
+
get(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
221
|
+
patch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
222
|
+
post(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
223
|
+
put(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
150
224
|
rawDelete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
151
225
|
rawGet(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
226
|
+
rawHeaders(actorName?: string | null, extraHeaders?: RuntimeHeaders): RuntimeHeaders;
|
|
227
|
+
rawMultipart: MultipartRequestClient;
|
|
152
228
|
rawPatch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
153
229
|
rawPost(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
154
230
|
rawPut(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
231
|
+
raw: RawRequestClient;
|
|
155
232
|
request(
|
|
156
233
|
method: RuntimeMethod,
|
|
157
234
|
path: string,
|
|
158
|
-
setupData?: TSetup | null,
|
|
159
235
|
body?: unknown,
|
|
160
236
|
extraHeaders?: RuntimeHeaders
|
|
161
237
|
): RuntimeResponse;
|
|
@@ -226,19 +302,16 @@ export declare function createHttpClient<TSetup = unknown>(
|
|
|
226
302
|
): HttpClient<TSetup>;
|
|
227
303
|
export declare function makeReq<TSetup = unknown>(
|
|
228
304
|
baseUrl: string,
|
|
305
|
+
sessionBundle?: AuthSessionBundle<TSetup> | null,
|
|
229
306
|
routeHeaders?: RuntimeHeaders,
|
|
230
|
-
getHeaders?: (
|
|
231
|
-
|
|
307
|
+
getHeaders?: (context: HttpHeaderBuildContext<TSetup>) => RuntimeHeaders | void,
|
|
308
|
+
defaultActor?: string | null
|
|
309
|
+
): HttpClient<TSetup>;
|
|
232
310
|
export declare function makeRawReq(
|
|
233
311
|
baseUrl: string,
|
|
234
312
|
routeHeaders?: RuntimeHeaders,
|
|
235
|
-
getRawHeaders?: (
|
|
236
|
-
):
|
|
237
|
-
export declare function makeGetWithHeaders<TSetup = unknown>(
|
|
238
|
-
baseUrl: string,
|
|
239
|
-
routeHeaders?: RuntimeHeaders,
|
|
240
|
-
getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void
|
|
241
|
-
): HttpClient<TSetup>["getWithHeaders"];
|
|
313
|
+
getRawHeaders?: (context: HttpHeaderBuildContext<never>) => RuntimeHeaders | void
|
|
314
|
+
): RawRequestClient;
|
|
242
315
|
|
|
243
316
|
export declare function expectStatus(
|
|
244
317
|
response: RuntimeResponse,
|
|
@@ -266,6 +339,83 @@ export declare function expectJsonPath(
|
|
|
266
339
|
predicate: (value: unknown) => boolean,
|
|
267
340
|
label?: string | null
|
|
268
341
|
): boolean;
|
|
342
|
+
export declare function expectErrorShape(
|
|
343
|
+
response: RuntimeResponse,
|
|
344
|
+
label?: string | null
|
|
345
|
+
): boolean;
|
|
346
|
+
export declare function expectErrorMessage(
|
|
347
|
+
response: RuntimeResponse,
|
|
348
|
+
label?: string | null
|
|
349
|
+
): boolean;
|
|
350
|
+
export declare function expectResponse(
|
|
351
|
+
response: RuntimeResponse,
|
|
352
|
+
predicate: (response: RuntimeResponse) => boolean,
|
|
353
|
+
label: string
|
|
354
|
+
): boolean;
|
|
355
|
+
export declare function expectValue<T>(
|
|
356
|
+
value: T,
|
|
357
|
+
predicate: (value: T) => boolean,
|
|
358
|
+
label: string
|
|
359
|
+
): boolean;
|
|
360
|
+
export declare function expectCondition(
|
|
361
|
+
predicate: () => boolean,
|
|
362
|
+
label: string
|
|
363
|
+
): boolean;
|
|
364
|
+
|
|
365
|
+
export declare function runAuthGateChecks(
|
|
366
|
+
rawReq: RawRequestClient,
|
|
367
|
+
scope: string,
|
|
368
|
+
descriptors: {
|
|
369
|
+
delete?: Array<string>;
|
|
370
|
+
get?: Array<string>;
|
|
371
|
+
patch?: Array<string | [string, unknown]>;
|
|
372
|
+
post?: Array<string | [string, unknown]>;
|
|
373
|
+
put?: Array<string | [string, unknown]>;
|
|
374
|
+
validateGetErrorShape?: boolean;
|
|
375
|
+
}
|
|
376
|
+
): void;
|
|
377
|
+
export declare function runPaginationChecks(
|
|
378
|
+
req: Pick<HttpClient, "get">,
|
|
379
|
+
endpoint: string,
|
|
380
|
+
options?: { auditLogsExtra?: boolean }
|
|
381
|
+
): void;
|
|
382
|
+
|
|
383
|
+
export interface RuntimeExpectNamespace {
|
|
384
|
+
condition: typeof expectCondition;
|
|
385
|
+
error: {
|
|
386
|
+
message: typeof expectErrorMessage;
|
|
387
|
+
shape: typeof expectErrorShape;
|
|
388
|
+
};
|
|
389
|
+
json: typeof expectJson;
|
|
390
|
+
jsonPath: typeof expectJsonPath;
|
|
391
|
+
notStatus: typeof expectNotStatus;
|
|
392
|
+
response: typeof expectResponse;
|
|
393
|
+
status: typeof expectStatus;
|
|
394
|
+
statusOneOf: typeof expectStatusOneOf;
|
|
395
|
+
value: typeof expectValue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export interface RuntimeParseNamespace {
|
|
399
|
+
cookie(response: Pick<RuntimeResponse, "headers">, cookieName: string): string | null;
|
|
400
|
+
json<T = unknown>(response: Pick<RuntimeResponse, "body">): T;
|
|
401
|
+
safeJson: typeof safeJson;
|
|
402
|
+
sse(body: string): Array<{ event: string | null; data: unknown }>;
|
|
403
|
+
sseEvent<T = unknown>(body: string, eventName: string): T | null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export interface RuntimeChecksNamespace {
|
|
407
|
+
authGate: typeof runAuthGateChecks;
|
|
408
|
+
pagination: typeof runPaginationChecks;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface RuntimeNetworkNamespace {
|
|
412
|
+
deterministicIp(seed: string | number, offset?: number): string;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export declare const expect: RuntimeExpectNamespace;
|
|
416
|
+
export declare const parse: RuntimeParseNamespace;
|
|
417
|
+
export declare const checks: RuntimeChecksNamespace;
|
|
418
|
+
export declare const network: RuntimeNetworkNamespace;
|
|
269
419
|
|
|
270
420
|
declare global {
|
|
271
421
|
const __ENV: Record<string, string | undefined>;
|
package/lib/runtime/index.mjs
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import rawHttp from "k6/http";
|
|
2
2
|
import { Rate, Trend } from "k6/metrics";
|
|
3
3
|
import { fail, sleep } from "k6";
|
|
4
|
+
import { deriveDeterministicIp } from "../runtime-src/shared/error-body.mjs";
|
|
5
|
+
import {
|
|
6
|
+
extractCookie,
|
|
7
|
+
getSseEventData,
|
|
8
|
+
parseJsonBody,
|
|
9
|
+
parseSseEvents,
|
|
10
|
+
} from "../runtime-src/shared/http-parsing.mjs";
|
|
4
11
|
import {
|
|
5
12
|
formatWaitForTimeoutError,
|
|
6
13
|
normalizeWaitIntervalSeconds,
|
|
@@ -27,17 +34,26 @@ export {
|
|
|
27
34
|
contains,
|
|
28
35
|
defaultOptions,
|
|
29
36
|
evaluateCheck,
|
|
30
|
-
isSorted,
|
|
31
37
|
json,
|
|
38
|
+
isSorted,
|
|
32
39
|
singleIterationOptions,
|
|
33
40
|
} from "../runtime-src/k6/checks.js";
|
|
34
41
|
export {
|
|
42
|
+
expectCondition,
|
|
43
|
+
expectErrorMessage,
|
|
44
|
+
expectErrorShape,
|
|
35
45
|
expectJson,
|
|
36
46
|
expectJsonPath,
|
|
37
47
|
expectNotStatus,
|
|
48
|
+
expectResponse,
|
|
38
49
|
expectStatus,
|
|
39
50
|
expectStatusOneOf,
|
|
51
|
+
expectValue,
|
|
40
52
|
} from "../runtime-src/k6/http-assertions.js";
|
|
53
|
+
export {
|
|
54
|
+
runAuthGateChecks,
|
|
55
|
+
runPaginationChecks,
|
|
56
|
+
} from "../runtime-src/k6/http-checks.js";
|
|
41
57
|
export {
|
|
42
58
|
createDalContext,
|
|
43
59
|
openDb,
|
|
@@ -48,14 +64,63 @@ export {
|
|
|
48
64
|
defaultOptions as httpDefaultOptions,
|
|
49
65
|
getEnv,
|
|
50
66
|
getHttpTrace,
|
|
51
|
-
makeGetWithHeaders,
|
|
52
|
-
makeRawReq,
|
|
53
67
|
makeReq,
|
|
68
|
+
makeRawReq,
|
|
54
69
|
safeJson,
|
|
55
70
|
summarizeHttpTrace,
|
|
56
71
|
toBodyPreview,
|
|
57
72
|
} from "../runtime-src/k6/http.js";
|
|
58
73
|
|
|
74
|
+
import {
|
|
75
|
+
expectCondition,
|
|
76
|
+
expectErrorMessage,
|
|
77
|
+
expectErrorShape,
|
|
78
|
+
expectJson,
|
|
79
|
+
expectJsonPath,
|
|
80
|
+
expectNotStatus,
|
|
81
|
+
expectResponse,
|
|
82
|
+
expectStatus,
|
|
83
|
+
expectStatusOneOf,
|
|
84
|
+
expectValue,
|
|
85
|
+
} from "../runtime-src/k6/http-assertions.js";
|
|
86
|
+
import {
|
|
87
|
+
runAuthGateChecks,
|
|
88
|
+
runPaginationChecks,
|
|
89
|
+
} from "../runtime-src/k6/http-checks.js";
|
|
90
|
+
import { safeJson } from "../runtime-src/k6/http.js";
|
|
91
|
+
|
|
92
|
+
export const expect = {
|
|
93
|
+
condition: expectCondition,
|
|
94
|
+
error: {
|
|
95
|
+
message: expectErrorMessage,
|
|
96
|
+
shape: expectErrorShape,
|
|
97
|
+
},
|
|
98
|
+
json: expectJson,
|
|
99
|
+
jsonPath: expectJsonPath,
|
|
100
|
+
notStatus: expectNotStatus,
|
|
101
|
+
response: expectResponse,
|
|
102
|
+
status: expectStatus,
|
|
103
|
+
statusOneOf: expectStatusOneOf,
|
|
104
|
+
value: expectValue,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const parse = {
|
|
108
|
+
cookie: extractCookie,
|
|
109
|
+
json: parseJsonBody,
|
|
110
|
+
safeJson,
|
|
111
|
+
sse: parseSseEvents,
|
|
112
|
+
sseEvent: getSseEventData,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const checks = {
|
|
116
|
+
authGate: runAuthGateChecks,
|
|
117
|
+
pagination: runPaginationChecks,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const network = {
|
|
121
|
+
deterministicIp: deriveDeterministicIp,
|
|
122
|
+
};
|
|
123
|
+
|
|
59
124
|
export function getTestkitContext() {
|
|
60
125
|
return readTestkitContext(__ENV);
|
|
61
126
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { evaluateCheck, recordFailureDetail } from "./checks.js";
|
|
1
|
+
import { check, evaluateCheck, recordFailureDetail } from "./checks.js";
|
|
2
|
+
import {
|
|
3
|
+
extractErrorMessageFromBody,
|
|
4
|
+
hasStandardErrorShape,
|
|
5
|
+
} from "../shared/error-body.mjs";
|
|
2
6
|
import { safeJson, summarizeHttpTrace, toBodyPreview } from "./http.js";
|
|
3
7
|
|
|
4
8
|
export function expectStatus(response, expected, label = null) {
|
|
@@ -122,6 +126,32 @@ export function expectJsonPath(response, path, predicate, label = null) {
|
|
|
122
126
|
);
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
export function expectErrorShape(response, label = "response has standard error shape") {
|
|
130
|
+
return expectJson(response, hasStandardErrorShape, label);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function expectErrorMessage(response, label = "response includes extractable error message") {
|
|
134
|
+
return expectJson(response, (body) => extractErrorMessageFromBody(body) !== null, label);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function expectResponse(response, predicate, label) {
|
|
138
|
+
return check(response, {
|
|
139
|
+
[normalizeLabel(label, "response matches expectation")]: predicate,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function expectValue(value, predicate, label) {
|
|
144
|
+
return check(value, {
|
|
145
|
+
[normalizeLabel(label, "value matches expectation")]: predicate,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function expectCondition(predicate, label) {
|
|
150
|
+
return check(null, {
|
|
151
|
+
[normalizeLabel(label, "condition holds")]: () => Boolean(predicate()),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
125
155
|
function buildHttpAssertionDetail({ kind, title, trace, expected, actual, response, message }) {
|
|
126
156
|
return {
|
|
127
157
|
kind,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { group } from "./checks.js";
|
|
2
|
+
import {
|
|
3
|
+
expectErrorShape,
|
|
4
|
+
expectNotStatus,
|
|
5
|
+
expectResponse,
|
|
6
|
+
expectStatus,
|
|
7
|
+
expectStatusOneOf,
|
|
8
|
+
} from "./http-assertions.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PAGINATION_CASES = [
|
|
11
|
+
{ qs: "limit=0", label: "limit=0", expect400: false },
|
|
12
|
+
{ qs: "limit=-1", label: "limit=-1", expect400: true },
|
|
13
|
+
{ qs: "limit=999999", label: "limit=999999", expect400: false },
|
|
14
|
+
{ qs: "limit=abc", label: "limit=abc", expect400: true },
|
|
15
|
+
{ qs: "offset=-1", label: "offset=-1", expect400: true },
|
|
16
|
+
{ qs: "offset=1.5", label: "offset=1.5", expect400: true },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const AUDIT_LOGS_PAGINATION_CASES = [
|
|
20
|
+
{ qs: "limit=1e3", label: "limit=1e3 (scientific notation)" },
|
|
21
|
+
{ qs: "limit=Infinity", label: "limit=Infinity" },
|
|
22
|
+
{ qs: "limit=NaN", label: "limit=NaN" },
|
|
23
|
+
{ qs: "offset=NaN", label: "offset=NaN" },
|
|
24
|
+
{ qs: "limit=", label: "limit= (empty)" },
|
|
25
|
+
{ qs: "offset=", label: "offset= (empty)" },
|
|
26
|
+
{ qs: "limit=0x10", label: "limit=0x10 (hex)" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function runAuthGateChecks(rawReq, scope, descriptors = {}) {
|
|
30
|
+
const {
|
|
31
|
+
get = [],
|
|
32
|
+
post = [],
|
|
33
|
+
patch = [],
|
|
34
|
+
delete: deleteCases = [],
|
|
35
|
+
put = [],
|
|
36
|
+
validateGetErrorShape = false,
|
|
37
|
+
} = descriptors;
|
|
38
|
+
|
|
39
|
+
runMethodAuthGateChecks(rawReq, scope, "GET", get, validateGetErrorShape);
|
|
40
|
+
runMethodAuthGateChecks(rawReq, scope, "POST", post);
|
|
41
|
+
runMethodAuthGateChecks(rawReq, scope, "PATCH", patch);
|
|
42
|
+
runMethodAuthGateChecks(rawReq, scope, "DELETE", deleteCases);
|
|
43
|
+
runMethodAuthGateChecks(rawReq, scope, "PUT", put);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function runPaginationChecks(req, endpoint, options = {}) {
|
|
47
|
+
group(`${endpoint} — pagination abuse`, () => {
|
|
48
|
+
for (const { qs, label, expect400 } of DEFAULT_PAGINATION_CASES) {
|
|
49
|
+
const url = `${endpoint}?${qs}`;
|
|
50
|
+
const response = req.get(url);
|
|
51
|
+
|
|
52
|
+
expectNotStatus(response, 500, `${label} → not 500`);
|
|
53
|
+
if (response.status === 500) {
|
|
54
|
+
expectResponse(response, () => true, `BUG: ${endpoint} crashes on ${label}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (expect400) {
|
|
58
|
+
expectStatus(response, 400, `${label} → 400`);
|
|
59
|
+
if (response.status === 200) {
|
|
60
|
+
expectResponse(response, () => true, `BUG: ${endpoint} accepts ${label}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (label === "limit=abc" && response.body) {
|
|
65
|
+
expectResponse(response, (value) => !value.body.includes("NaN"), `${label} → no NaN in response`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!options.auditLogsExtra) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const { qs, label } of AUDIT_LOGS_PAGINATION_CASES) {
|
|
74
|
+
const url = `${endpoint}?${qs}`;
|
|
75
|
+
const response = req.get(url);
|
|
76
|
+
|
|
77
|
+
expectNotStatus(response, 500, `audit-logs ${label} → not 500`);
|
|
78
|
+
if (response.status === 500) {
|
|
79
|
+
expectResponse(response, () => true, `BUG: audit-logs crashes on ${label}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
expectStatusOneOf(response, [400, 200], `audit-logs ${label} → 400 (not silently accepted)`);
|
|
83
|
+
if (response.status === 200 && response.body) {
|
|
84
|
+
expectResponse(response, (value) => !value.body.includes("NaN"), `audit-logs ${label} → no NaN in response`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function runMethodAuthGateChecks(rawReq, scope, method, cases, validateErrorShape = false) {
|
|
91
|
+
if (!Array.isArray(cases) || cases.length === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
group(`${scope} ${method} endpoints return 401 without auth`, () => {
|
|
96
|
+
for (const entry of cases) {
|
|
97
|
+
const { path, body } = normalizeRequestCase(entry);
|
|
98
|
+
const response = rawReq(method, path, body);
|
|
99
|
+
|
|
100
|
+
expectStatus(response, 401, `${method} ${path} → 401`);
|
|
101
|
+
if (validateErrorShape && method === "GET" && response.status === 401) {
|
|
102
|
+
expectErrorShape(response, `${method} ${path} error shape`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeRequestCase(entry) {
|
|
109
|
+
if (Array.isArray(entry)) {
|
|
110
|
+
return {
|
|
111
|
+
path: entry[0],
|
|
112
|
+
body: entry[1],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
path: entry,
|
|
118
|
+
body: undefined,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mockCheck = vi.fn((value, checks) =>
|
|
4
|
+
Object.values(checks).every((predicate) => predicate(value))
|
|
5
|
+
);
|
|
6
|
+
const mockGroup = vi.fn((name, fn) => fn());
|
|
7
|
+
|
|
8
|
+
vi.mock("k6", () => ({
|
|
9
|
+
check: mockCheck,
|
|
10
|
+
group: mockGroup,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("k6/http", () => ({
|
|
14
|
+
default: {
|
|
15
|
+
del: vi.fn(),
|
|
16
|
+
file: vi.fn(),
|
|
17
|
+
get: vi.fn(),
|
|
18
|
+
patch: vi.fn(),
|
|
19
|
+
post: vi.fn(),
|
|
20
|
+
put: vi.fn(),
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock("k6/metrics", () => ({
|
|
25
|
+
Rate: class {
|
|
26
|
+
add() {}
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const { runAuthGateChecks, runPaginationChecks } = await import("./http-checks.js");
|
|
31
|
+
|
|
32
|
+
function makeJsonResponse(status, body) {
|
|
33
|
+
return {
|
|
34
|
+
status,
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("runtime http checks", () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
mockCheck.mockClear();
|
|
43
|
+
mockGroup.mockClear();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("executes auth-gate checks across configured methods", () => {
|
|
47
|
+
const rawReq = vi.fn((method, path, body) => {
|
|
48
|
+
return makeJsonResponse(401, {
|
|
49
|
+
error: {
|
|
50
|
+
code: "UNAUTHORIZED",
|
|
51
|
+
message: `${method} ${path} requires auth`,
|
|
52
|
+
},
|
|
53
|
+
body,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
runAuthGateChecks(rawReq, "sessions", {
|
|
58
|
+
get: ["/api/v1/sessions"],
|
|
59
|
+
post: [["/api/v1/sessions", { name: "draft" }]],
|
|
60
|
+
validateGetErrorShape: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(rawReq.mock.calls).toEqual([
|
|
64
|
+
["GET", "/api/v1/sessions", undefined],
|
|
65
|
+
["POST", "/api/v1/sessions", { name: "draft" }],
|
|
66
|
+
]);
|
|
67
|
+
expect(mockGroup).toHaveBeenCalledWith(
|
|
68
|
+
"sessions GET endpoints return 401 without auth",
|
|
69
|
+
expect.any(Function)
|
|
70
|
+
);
|
|
71
|
+
expect(mockGroup).toHaveBeenCalledWith(
|
|
72
|
+
"sessions POST endpoints return 401 without auth",
|
|
73
|
+
expect.any(Function)
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("executes default and audit-log pagination abuse cases", () => {
|
|
78
|
+
const requested = [];
|
|
79
|
+
const req = {
|
|
80
|
+
get: vi.fn((url) => {
|
|
81
|
+
requested.push(url);
|
|
82
|
+
const expects400 =
|
|
83
|
+
url.includes("limit=-1") ||
|
|
84
|
+
url.includes("limit=abc") ||
|
|
85
|
+
url.includes("offset=-1") ||
|
|
86
|
+
url.includes("offset=1.5") ||
|
|
87
|
+
url.includes("limit=Infinity") ||
|
|
88
|
+
url.includes("limit=NaN") ||
|
|
89
|
+
url.includes("offset=NaN") ||
|
|
90
|
+
url.endsWith("limit=") ||
|
|
91
|
+
url.endsWith("offset=");
|
|
92
|
+
|
|
93
|
+
if (expects400) {
|
|
94
|
+
return makeJsonResponse(400, {
|
|
95
|
+
error: {
|
|
96
|
+
code: "VALIDATION_ERROR",
|
|
97
|
+
message: "invalid pagination",
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return makeJsonResponse(200, {
|
|
103
|
+
data: [],
|
|
104
|
+
pagination: {
|
|
105
|
+
limit: 25,
|
|
106
|
+
offset: 0,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
runPaginationChecks(req, "/api/v1/audit-logs", { auditLogsExtra: true });
|
|
113
|
+
|
|
114
|
+
expect(requested).toHaveLength(13);
|
|
115
|
+
expect(requested).toContain("/api/v1/audit-logs?limit=-1");
|
|
116
|
+
expect(requested).toContain("/api/v1/audit-logs?offset=1.5");
|
|
117
|
+
expect(requested).toContain("/api/v1/audit-logs?limit=Infinity");
|
|
118
|
+
expect(requested).toContain("/api/v1/audit-logs?limit=0x10");
|
|
119
|
+
});
|
|
120
|
+
});
|