@elench/testkit 0.1.82 → 0.1.84
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 +37 -7
- 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/prompt-builder.mjs +25 -0
- package/lib/cli/agents/providers/claude.mjs +74 -0
- package/lib/cli/agents/providers/codex.mjs +83 -0
- package/lib/cli/agents/providers/shared.mjs +134 -0
- package/lib/cli/command-helpers.mjs +53 -25
- package/lib/cli/commands/investigate.mjs +87 -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/summary-box.mjs +11 -11
- package/lib/cli/presentation/tree-reporter.mjs +159 -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-state.mjs +481 -0
- package/lib/cli/tui/run-tree-state.mjs +1 -0
- package/lib/config-api/auth-fixtures.mjs +15 -10
- package/lib/discovery/index.mjs +1 -1
- package/lib/index.d.ts +5 -1
- package/lib/runner/orchestrator.mjs +1 -0
- package/lib/runtime/index.d.ts +138 -5
- package/lib/runtime/index.mjs +68 -2
- 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-suite-runtime.js +5 -1
- package/lib/runtime-src/k6/http.js +213 -23
- package/lib/runtime-src/shared/error-body.mjs +42 -0
- package/lib/runtime-src/shared/http-parsing.mjs +68 -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 +7 -6
- package/lib/app/configs.test.mjs +0 -34
- package/lib/app/typecheck.test.mjs +0 -24
- package/lib/bundler/index.test.mjs +0 -164
- package/lib/cli/args.test.mjs +0 -110
- package/lib/cli/presentation/code-frames.test.mjs +0 -71
- package/lib/cli/presentation/run-reporter.test.mjs +0 -192
- package/lib/cli/presentation/summary-box.test.mjs +0 -43
- package/lib/cli/presentation/terminal-layout.test.mjs +0 -23
- package/lib/config/database.test.mjs +0 -29
- package/lib/config/discovery.test.mjs +0 -276
- package/lib/config/env.test.mjs +0 -40
- package/lib/config/index.test.mjs +0 -44
- package/lib/config/paths.test.mjs +0 -27
- package/lib/config/runtime.test.mjs +0 -82
- package/lib/config/skip-config.test.mjs +0 -63
- package/lib/config-api/index.test.mjs +0 -344
- package/lib/config-api/next-runtime-tsconfig.test.mjs +0 -58
- package/lib/coverage/backend-discovery.test.mjs +0 -61
- package/lib/coverage/evidence.test.mjs +0 -87
- package/lib/coverage/index.test.mjs +0 -715
- package/lib/coverage/routing.test.mjs +0 -36
- package/lib/coverage/shared.test.mjs +0 -72
- package/lib/database/fingerprint.test.mjs +0 -99
- package/lib/database/index.test.mjs +0 -95
- package/lib/database/naming.test.mjs +0 -39
- package/lib/database/state.test.mjs +0 -66
- package/lib/database/template-steps.test.mjs +0 -43
- package/lib/discovery/file-metadata.test.mjs +0 -51
- package/lib/discovery/index.test.mjs +0 -182
- package/lib/discovery/path-policy.test.mjs +0 -65
- package/lib/drizzle/index.test.mjs +0 -33
- package/lib/env/index.test.mjs +0 -82
- package/lib/history/index.test.mjs +0 -115
- package/lib/package.test.mjs +0 -59
- package/lib/playwright/index.test.mjs +0 -43
- package/lib/regressions/github.test.mjs +0 -324
- package/lib/regressions/index.test.mjs +0 -187
- package/lib/reporters/playwright.test.mjs +0 -167
- package/lib/runner/default-runtime-errors.test.mjs +0 -49
- package/lib/runner/execution-config.test.mjs +0 -67
- package/lib/runner/failure-details.test.mjs +0 -114
- package/lib/runner/formatting.test.mjs +0 -205
- package/lib/runner/metadata.test.mjs +0 -52
- package/lib/runner/planning.test.mjs +0 -371
- package/lib/runner/playwright-config.test.mjs +0 -78
- package/lib/runner/processes.test.mjs +0 -21
- package/lib/runner/regressions.test.mjs +0 -168
- package/lib/runner/reporting.test.mjs +0 -310
- package/lib/runner/results.test.mjs +0 -376
- package/lib/runner/runtime-manager.test.mjs +0 -252
- package/lib/runner/runtime-preparation.test.mjs +0 -141
- package/lib/runner/selection.test.mjs +0 -24
- package/lib/runner/setup-operations.test.mjs +0 -94
- package/lib/runner/state.test.mjs +0 -62
- package/lib/runner/suite-selection.test.mjs +0 -49
- package/lib/runner/template.test.mjs +0 -272
- package/lib/shared/build-config.test.mjs +0 -132
- package/lib/shared/configured-steps.test.mjs +0 -102
- package/lib/shared/execution-schema.test.mjs +0 -26
- package/lib/shared/file-timeout.test.mjs +0 -64
- package/lib/shared/test-context.test.mjs +0 -43
- package/lib/timing/index.test.mjs +0 -64
- package/lib/toolchains/index.test.mjs +0 -168
- package/lib/vitest/index.test.mjs +0 -20
package/lib/runtime/index.d.ts
CHANGED
|
@@ -141,9 +141,36 @@ export interface HttpClientConfig<TSetup = unknown> {
|
|
|
141
141
|
sessionBundle?: AuthSessionBundle<TSetup> | null;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
export interface
|
|
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,
|
|
165
|
+
path: string,
|
|
166
|
+
body?: unknown,
|
|
167
|
+
extraHeaders?: RuntimeHeaders
|
|
168
|
+
): RuntimeResponse;
|
|
169
|
+
as(actorName: string): RawRequestClient;
|
|
145
170
|
delete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
146
171
|
get(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
172
|
+
headers(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
|
|
173
|
+
multipart: MultipartRequestClient;
|
|
147
174
|
patch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
148
175
|
post(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
149
176
|
put(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
@@ -155,24 +182,53 @@ export interface ActorRequestClient {
|
|
|
155
182
|
): RuntimeResponse;
|
|
156
183
|
}
|
|
157
184
|
|
|
158
|
-
export interface
|
|
159
|
-
|
|
185
|
+
export interface ActorRequestClient {
|
|
186
|
+
headers(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
|
|
187
|
+
multipart: MultipartRequestClient;
|
|
188
|
+
raw(
|
|
189
|
+
method: RuntimeMethod,
|
|
190
|
+
path: string,
|
|
191
|
+
body?: unknown,
|
|
192
|
+
extraHeaders?: RuntimeHeaders
|
|
193
|
+
): RuntimeResponse;
|
|
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;
|
|
160
202
|
delete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
161
203
|
get(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
162
204
|
patch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
163
205
|
post(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
164
206
|
put(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
165
|
-
|
|
207
|
+
request(
|
|
166
208
|
method: RuntimeMethod,
|
|
167
209
|
path: string,
|
|
168
210
|
body?: unknown,
|
|
169
211
|
extraHeaders?: RuntimeHeaders
|
|
170
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;
|
|
171
224
|
rawDelete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
172
225
|
rawGet(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
226
|
+
rawHeaders(actorName?: string | null, extraHeaders?: RuntimeHeaders): RuntimeHeaders;
|
|
227
|
+
rawMultipart: MultipartRequestClient;
|
|
173
228
|
rawPatch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
174
229
|
rawPost(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
175
230
|
rawPut(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
231
|
+
raw: RawRequestClient;
|
|
176
232
|
request(
|
|
177
233
|
method: RuntimeMethod,
|
|
178
234
|
path: string,
|
|
@@ -255,7 +311,7 @@ export declare function makeRawReq(
|
|
|
255
311
|
baseUrl: string,
|
|
256
312
|
routeHeaders?: RuntimeHeaders,
|
|
257
313
|
getRawHeaders?: (context: HttpHeaderBuildContext<never>) => RuntimeHeaders | void
|
|
258
|
-
):
|
|
314
|
+
): RawRequestClient;
|
|
259
315
|
|
|
260
316
|
export declare function expectStatus(
|
|
261
317
|
response: RuntimeResponse,
|
|
@@ -283,6 +339,83 @@ export declare function expectJsonPath(
|
|
|
283
339
|
predicate: (value: unknown) => boolean,
|
|
284
340
|
label?: string | null
|
|
285
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;
|
|
286
419
|
|
|
287
420
|
declare global {
|
|
288
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,13 +64,63 @@ export {
|
|
|
48
64
|
defaultOptions as httpDefaultOptions,
|
|
49
65
|
getEnv,
|
|
50
66
|
getHttpTrace,
|
|
51
|
-
makeRawReq,
|
|
52
67
|
makeReq,
|
|
68
|
+
makeRawReq,
|
|
53
69
|
safeJson,
|
|
54
70
|
summarizeHttpTrace,
|
|
55
71
|
toBodyPreview,
|
|
56
72
|
} from "../runtime-src/k6/http.js";
|
|
57
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
|
+
|
|
58
124
|
export function getTestkitContext() {
|
|
59
125
|
return readTestkitContext(__ENV);
|
|
60
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
|
+
}
|
|
@@ -64,14 +64,18 @@ export function createSuiteActors(sessionBundle, client, profileMeta) {
|
|
|
64
64
|
const actorNames = profileMeta?.actorNames || Object.keys(sessionBundle?.actors || {});
|
|
65
65
|
const entries = actorNames.map((actorName) => {
|
|
66
66
|
const actorRecord = sessionBundle?.actors?.[actorName] || null;
|
|
67
|
+
const actorClient = client.as(actorName);
|
|
67
68
|
return {
|
|
68
69
|
email: actorRecord?.email || null,
|
|
70
|
+
headers: actorClient.headers(),
|
|
69
71
|
index: Number(actorRecord?.actorIndex ?? 0),
|
|
70
72
|
key: actorName,
|
|
71
73
|
name: actorRecord?.name || null,
|
|
72
74
|
organizationKey: actorRecord?.organizationKey || null,
|
|
73
75
|
organizationName: actorRecord?.organizationName || null,
|
|
74
|
-
|
|
76
|
+
rawHeaders: actorClient.rawHeaders(),
|
|
77
|
+
rawReq: actorClient.rawReq,
|
|
78
|
+
req: actorClient,
|
|
75
79
|
session: actorRecord?.session || null,
|
|
76
80
|
};
|
|
77
81
|
});
|