@riddledc/riddle-proof 0.7.126 → 0.7.127
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 +24 -2
- package/dist/{chunk-55KDZEB3.js → chunk-JLINSUKO.js} +431 -4
- package/dist/cli.cjs +449 -5
- package/dist/cli.js +19 -2
- package/dist/index.cjs +431 -4
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/profile.cjs +431 -4
- package/dist/profile.d.cts +28 -1
- package/dist/profile.d.ts +28 -1
- package/dist/profile.js +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ export { CreateCaptureDiagnosticInput, DEFAULT_DIAGNOSTIC_ARRAY_LIMIT, DEFAULT_D
|
|
|
10
10
|
export { BuildVisualProofSessionInput, RIDDLE_PROOF_VISUAL_SESSION_FINGERPRINT_VERSION, RIDDLE_PROOF_VISUAL_SESSION_VERSION, VisualProofSessionMismatch, buildVisualProofSession, compareVisualProofSessionFingerprint, parseVisualProofSession, visualSessionFingerprint, visualSessionFingerprintBasis } from './proof-session.js';
|
|
11
11
|
export { AssessPlayabilityOptions, RIDDLE_PROOF_PLAYABILITY_ASSESSMENT_VERSION, RIDDLE_PROOF_PLAYABILITY_VERSION, RiddleProofPlayabilityAssessment, RiddleProofPlayabilityEvidence, assessPlayabilityEvidence, extractPlayabilityEvidence, isRiddleProofPlayabilityMode } from './playability.js';
|
|
12
12
|
export { AssessBasicGameplayOptions, AttachBasicGameplayArtifactOptions, BASIC_GAMEPLAY_ACTION_TYPES, BASIC_GAMEPLAY_PROGRESS_CHECK_TYPES, BasicGameplayActionResult, BasicGameplayActionType, BasicGameplayArtifactResolution, BasicGameplayAssessmentSummary, BasicGameplayBoundsOffender, BasicGameplayCanvasState, BasicGameplayCatchRecord, BasicGameplayChangeSummary, BasicGameplayFailureCode, BasicGameplayFixReference, BasicGameplayMetric, BasicGameplayMobileEvidence, BasicGameplayProgressCheckType, BasicGameplayProgressionCheck, BasicGameplayProofArtifact, BasicGameplayResponsiveViewportEvidence, BasicGameplayRouteReference, BasicGameplaySnapshot, BasicGameplaySuiteFailure, BasicGameplayWarningCode, CreateBasicGameplayCatchSummaryInput, RIDDLE_PROOF_BASIC_GAMEPLAY_ASSESSMENT_VERSION, RIDDLE_PROOF_BASIC_GAMEPLAY_CATCH_VERSION, RIDDLE_PROOF_BASIC_GAMEPLAY_VERSION, RiddleProofBasicGameplayAssessment, RiddleProofBasicGameplayCatchSummary, RiddleProofBasicGameplayEvidence, RiddleProofBasicGameplayRouteAssessment, RiddleProofBasicGameplayRouteEvidence, assessBasicGameplayEvidence, assessBasicGameplayProgressionCheck, assessBasicGameplayProgressionChecks, assessBasicGameplayRoute, attachBasicGameplayArtifactScreenshotHashes, augmentBasicGameplayAssessmentWithProgressionChecks, compactBasicGameplayText, createBasicGameplayCatchRecords, createBasicGameplayCatchSummary, extractBasicGameplayEvidence, resolveBasicGameplayProgressionCheckWithArtifactScreenshots, sanitizeBasicGameplayJsonString } from './basic-gameplay.js';
|
|
13
|
-
export { NormalizeRiddleProofProfileOptions, RIDDLE_PROOF_PROFILE_CHECK_TYPES, RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION, RIDDLE_PROOF_PROFILE_NETWORK_ABORT_ERROR_CODES, RIDDLE_PROOF_PROFILE_RESULT_VERSION, RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES, RIDDLE_PROOF_PROFILE_STATUSES, RIDDLE_PROOF_PROFILE_VERSION, RiddleProofArtifactBodyAssertionInput, RiddleProofArtifactBodyAssertionResult, RiddleProofProfile, RiddleProofProfileArtifactRef, RiddleProofProfileBaselinePolicy, RiddleProofProfileBoundsOffender, RiddleProofProfileCheck, RiddleProofProfileCheckResult, RiddleProofProfileCheckType, RiddleProofProfileEvidence, RiddleProofProfileFailureAction, RiddleProofProfileHttpStatusPreflightCheckResult, RiddleProofProfileHttpStatusPreflightFetch, RiddleProofProfileHttpStatusPreflightFetchResponse, RiddleProofProfileHttpStatusPreflightOptions, RiddleProofProfileHttpStatusPreflightResult, RiddleProofProfileNetworkAbortErrorCode, RiddleProofProfileNetworkMock, RiddleProofProfileNetworkMockResponse, RiddleProofProfileResult, RiddleProofProfileRouteEvidence, RiddleProofProfileRouteInventoryRoute, RiddleProofProfileRunner, RiddleProofProfileSetupAction, RiddleProofProfileSetupActionType, RiddleProofProfileStatus, RiddleProofProfileTarget, RiddleProofProfileViewport, RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, collectRiddleProofProfileWarnings, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, deriveRiddleProofArtifactBodyAssertions, extractRiddleProofProfileResult, normalizeRiddleProofProfile, preflightRiddleProofProfileHttpStatusChecks, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult } from './profile.js';
|
|
13
|
+
export { NormalizeRiddleProofProfileOptions, RIDDLE_PROOF_PROFILE_CHECK_TYPES, RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION, RIDDLE_PROOF_PROFILE_NETWORK_ABORT_ERROR_CODES, RIDDLE_PROOF_PROFILE_RESULT_VERSION, RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES, RIDDLE_PROOF_PROFILE_STATUSES, RIDDLE_PROOF_PROFILE_VERSION, RiddleProofArtifactBodyAssertionInput, RiddleProofArtifactBodyAssertionResult, RiddleProofProfile, RiddleProofProfileArtifactRef, RiddleProofProfileBaselinePolicy, RiddleProofProfileBoundsOffender, RiddleProofProfileCheck, RiddleProofProfileCheckResult, RiddleProofProfileCheckType, RiddleProofProfileEvidence, RiddleProofProfileFailureAction, RiddleProofProfileHttpStatusBodyJsonAssertion, RiddleProofProfileHttpStatusBodyJsonAssertionResult, RiddleProofProfileHttpStatusPreflightCheckResult, RiddleProofProfileHttpStatusPreflightFetch, RiddleProofProfileHttpStatusPreflightFetchResponse, RiddleProofProfileHttpStatusPreflightOptions, RiddleProofProfileHttpStatusPreflightResult, RiddleProofProfileJsonValueType, RiddleProofProfileNetworkAbortErrorCode, RiddleProofProfileNetworkMock, RiddleProofProfileNetworkMockResponse, RiddleProofProfileResult, RiddleProofProfileRouteEvidence, RiddleProofProfileRouteInventoryRoute, RiddleProofProfileRunner, RiddleProofProfileSetupAction, RiddleProofProfileSetupActionType, RiddleProofProfileStatus, RiddleProofProfileTarget, RiddleProofProfileViewport, RiddleProofProfileViewportEvidence, assessRiddleProofProfileEvidence, buildRiddleProofProfileScript, collectRiddleProfileArtifactRefs, collectRiddleProofProfileWarnings, createRiddleProofProfileConfigurationError, createRiddleProofProfileEnvironmentBlockedResult, createRiddleProofProfileInsufficientResult, deriveRiddleProofArtifactBodyAssertions, extractRiddleProofProfileResult, normalizeRiddleProofProfile, preflightRiddleProofProfileHttpStatusChecks, profileStatusExitCode, resolveRiddleProofProfileRouteUrl, resolveRiddleProofProfileTargetUrl, resolveRiddleProofProfileTimeoutSec, slugifyRiddleProofProfileName, summarizeRiddleProofProfileResult } from './profile.js';
|
|
14
14
|
export { DEFAULT_RIDDLE_API_BASE_URL, DEFAULT_RIDDLE_API_KEY_FILE, RiddleApiError, RiddleClientConfig, RiddleFetch, RiddlePollJobOptions, RiddlePollJobResult, RiddlePollProgressSnapshot, RiddlePollSummary, RiddlePreviewDeployResult, RiddlePreviewFramework, RiddleRunScriptInput, RiddleServerPreviewInput, RiddleServerPreviewResult, createRiddleApiClient, deployRiddlePreview, deployRiddleStaticPreview, isTerminalRiddleJobStatus, parseRiddleViewport, pollRiddleJob, resolveRiddleApiKey, riddleRequestJson, runRiddleScript, runRiddleServerPreview } from './riddle-client.js';
|
package/dist/index.js
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
resolveRiddleProofProfileTimeoutSec,
|
|
63
63
|
slugifyRiddleProofProfileName,
|
|
64
64
|
summarizeRiddleProofProfileResult
|
|
65
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-JLINSUKO.js";
|
|
66
66
|
import {
|
|
67
67
|
DEFAULT_RIDDLE_API_BASE_URL,
|
|
68
68
|
DEFAULT_RIDDLE_API_KEY_FILE,
|
package/dist/profile.cjs
CHANGED
|
@@ -186,6 +186,9 @@ function valueFromOwn(input, ...keys) {
|
|
|
186
186
|
function numberValue(value) {
|
|
187
187
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
188
188
|
}
|
|
189
|
+
function booleanValue(value) {
|
|
190
|
+
return typeof value === "boolean" ? value : void 0;
|
|
191
|
+
}
|
|
189
192
|
function horizontalBoundsOverflowPx(value) {
|
|
190
193
|
if (!isRecord(value)) return 0;
|
|
191
194
|
let max = maxPositiveNumber(
|
|
@@ -280,6 +283,156 @@ function toJsonValue(value) {
|
|
|
280
283
|
if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
|
|
281
284
|
return String(value);
|
|
282
285
|
}
|
|
286
|
+
function jsonValueType(value) {
|
|
287
|
+
if (value === null) return "null";
|
|
288
|
+
if (Array.isArray(value)) return "array";
|
|
289
|
+
if (typeof value === "boolean") return "boolean";
|
|
290
|
+
if (typeof value === "number") return "number";
|
|
291
|
+
if (typeof value === "string") return "string";
|
|
292
|
+
return "object";
|
|
293
|
+
}
|
|
294
|
+
function deepJsonEqual(left, right) {
|
|
295
|
+
if (left === right) return true;
|
|
296
|
+
if (typeof left !== typeof right) return false;
|
|
297
|
+
if (left === null || right === null) return left === right;
|
|
298
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
299
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
300
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
301
|
+
return left.every((item, index) => deepJsonEqual(item, right[index]));
|
|
302
|
+
}
|
|
303
|
+
if (!isRecord(left) || !isRecord(right)) return false;
|
|
304
|
+
const leftKeys = Object.keys(left).sort();
|
|
305
|
+
const rightKeys = Object.keys(right).sort();
|
|
306
|
+
if (!deepJsonEqual(leftKeys, rightKeys)) return false;
|
|
307
|
+
return leftKeys.every((key) => deepJsonEqual(left[key], right[key]));
|
|
308
|
+
}
|
|
309
|
+
function jsonContains(observed, expected) {
|
|
310
|
+
if (typeof observed === "string" && typeof expected === "string") {
|
|
311
|
+
return observed.includes(expected);
|
|
312
|
+
}
|
|
313
|
+
if (Array.isArray(observed)) {
|
|
314
|
+
return observed.some((item) => deepJsonEqual(item, expected));
|
|
315
|
+
}
|
|
316
|
+
if (isRecord(observed) && isRecord(expected)) {
|
|
317
|
+
return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
function parseJsonPathSegments(path) {
|
|
322
|
+
let input = path.trim();
|
|
323
|
+
if (!input) throw new Error("path is empty");
|
|
324
|
+
if (input === "$") return [];
|
|
325
|
+
if (input.startsWith("$.")) input = input.slice(2);
|
|
326
|
+
else if (input.startsWith("$[")) input = input.slice(1);
|
|
327
|
+
const segments = [];
|
|
328
|
+
let token = "";
|
|
329
|
+
const pushToken = () => {
|
|
330
|
+
if (!token) return;
|
|
331
|
+
segments.push(token);
|
|
332
|
+
token = "";
|
|
333
|
+
};
|
|
334
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
335
|
+
const char = input[index];
|
|
336
|
+
if (char === ".") {
|
|
337
|
+
pushToken();
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (char !== "[") {
|
|
341
|
+
token += char;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
pushToken();
|
|
345
|
+
const closeIndex = input.indexOf("]", index + 1);
|
|
346
|
+
if (closeIndex === -1) throw new Error(`unterminated bracket at ${index}`);
|
|
347
|
+
const bracket = input.slice(index + 1, closeIndex).trim();
|
|
348
|
+
if (!bracket) throw new Error(`empty bracket at ${index}`);
|
|
349
|
+
if (/^\d+$/.test(bracket)) {
|
|
350
|
+
segments.push(Number(bracket));
|
|
351
|
+
} else if (bracket.startsWith('"') && bracket.endsWith('"') || bracket.startsWith("'") && bracket.endsWith("'")) {
|
|
352
|
+
const quoted = bracket.startsWith("'") ? `"${bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : bracket;
|
|
353
|
+
segments.push(String(JSON.parse(quoted)));
|
|
354
|
+
} else {
|
|
355
|
+
segments.push(bracket);
|
|
356
|
+
}
|
|
357
|
+
index = closeIndex;
|
|
358
|
+
}
|
|
359
|
+
pushToken();
|
|
360
|
+
return segments;
|
|
361
|
+
}
|
|
362
|
+
function resolveJsonPath(root, path) {
|
|
363
|
+
let segments;
|
|
364
|
+
try {
|
|
365
|
+
segments = parseJsonPathSegments(path);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return { exists: false, error: String(error instanceof Error ? error.message : error) };
|
|
368
|
+
}
|
|
369
|
+
let current = root;
|
|
370
|
+
for (const segment of segments) {
|
|
371
|
+
if (typeof segment === "number") {
|
|
372
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
|
|
373
|
+
current = current[segment];
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (!isRecord(current) || !hasOwn(current, segment)) return { exists: false };
|
|
377
|
+
current = current[segment];
|
|
378
|
+
}
|
|
379
|
+
return { exists: true, value: current };
|
|
380
|
+
}
|
|
381
|
+
function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
|
|
382
|
+
const resolved = resolveJsonPath(root, assertion.path);
|
|
383
|
+
const errors = [];
|
|
384
|
+
const result = {
|
|
385
|
+
label: assertion.label || assertion.path,
|
|
386
|
+
path: assertion.path,
|
|
387
|
+
ok: true,
|
|
388
|
+
exists: resolved.exists,
|
|
389
|
+
observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
|
|
390
|
+
};
|
|
391
|
+
if (resolved.exists) result.observed = toJsonValue(resolved.value);
|
|
392
|
+
if (resolved.error) errors.push(resolved.error);
|
|
393
|
+
if (hasOwn(assertion, "exists")) {
|
|
394
|
+
result.expected_exists = assertion.exists;
|
|
395
|
+
if (resolved.exists !== assertion.exists) errors.push(`expected exists=${assertion.exists}`);
|
|
396
|
+
}
|
|
397
|
+
if (hasOwn(assertion, "type")) {
|
|
398
|
+
result.type = assertion.type;
|
|
399
|
+
if (!resolved.exists || jsonValueType(resolved.value) !== assertion.type) errors.push(`expected type ${assertion.type}`);
|
|
400
|
+
}
|
|
401
|
+
if (hasOwn(assertion, "equals")) {
|
|
402
|
+
result.equals = assertion.equals;
|
|
403
|
+
if (!resolved.exists || !deepJsonEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
|
|
404
|
+
}
|
|
405
|
+
if (hasOwn(assertion, "not_equals")) {
|
|
406
|
+
result.not_equals = assertion.not_equals;
|
|
407
|
+
if (resolved.exists && deepJsonEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
|
|
408
|
+
}
|
|
409
|
+
if (hasOwn(assertion, "contains")) {
|
|
410
|
+
result.contains = assertion.contains;
|
|
411
|
+
if (!resolved.exists || !jsonContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
|
|
412
|
+
}
|
|
413
|
+
result.ok = errors.length === 0;
|
|
414
|
+
if (errors.length) result.errors = errors;
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
function evaluateHttpStatusBodyJsonAssertions(bodyText, assertions) {
|
|
418
|
+
const expected = assertions?.filter((assertion) => assertion.path) ?? [];
|
|
419
|
+
if (!expected.length) return [];
|
|
420
|
+
let parsed;
|
|
421
|
+
try {
|
|
422
|
+
parsed = JSON.parse(bodyText);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
const message = `response body is not valid JSON: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`;
|
|
425
|
+
return expected.map((assertion) => ({
|
|
426
|
+
label: assertion.label || assertion.path,
|
|
427
|
+
path: assertion.path,
|
|
428
|
+
ok: false,
|
|
429
|
+
exists: false,
|
|
430
|
+
observed_type: "missing",
|
|
431
|
+
errors: [message]
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
return expected.map((assertion) => evaluateHttpStatusBodyJsonAssertion(parsed, assertion));
|
|
435
|
+
}
|
|
283
436
|
function compactProfileSetupSummaryText(value, limit = 160) {
|
|
284
437
|
const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
285
438
|
if (!text) return void 0;
|
|
@@ -934,6 +1087,46 @@ function validateRegexPatterns(patterns, label) {
|
|
|
934
1087
|
}
|
|
935
1088
|
}
|
|
936
1089
|
}
|
|
1090
|
+
function normalizeHttpStatusBodyJsonAssertions(value, label) {
|
|
1091
|
+
if (value === void 0) return void 0;
|
|
1092
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
1093
|
+
if (!value.length) throw new Error(`${label} must not be empty.`);
|
|
1094
|
+
return value.map((item, index) => {
|
|
1095
|
+
const itemLabel = `${label}[${index}]`;
|
|
1096
|
+
if (typeof item === "string") {
|
|
1097
|
+
const path2 = stringValue(item);
|
|
1098
|
+
if (!path2) throw new Error(`${itemLabel} path must not be empty.`);
|
|
1099
|
+
return { path: path2, exists: true };
|
|
1100
|
+
}
|
|
1101
|
+
if (!isRecord(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
|
|
1102
|
+
const path = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
|
|
1103
|
+
if (!path) throw new Error(`${itemLabel}.path is required.`);
|
|
1104
|
+
const assertion = {
|
|
1105
|
+
label: stringValue(item.label),
|
|
1106
|
+
path
|
|
1107
|
+
};
|
|
1108
|
+
const exists = booleanValue(valueFromOwn(item, "exists", "present"));
|
|
1109
|
+
if (exists !== void 0) assertion.exists = exists;
|
|
1110
|
+
const type = stringValue(valueFromOwn(item, "type", "value_type", "valueType"));
|
|
1111
|
+
if (type !== void 0) {
|
|
1112
|
+
const allowedTypes = ["array", "boolean", "null", "number", "object", "string"];
|
|
1113
|
+
if (!allowedTypes.includes(type)) {
|
|
1114
|
+
throw new Error(`${itemLabel}.type must be one of ${allowedTypes.join(", ")}.`);
|
|
1115
|
+
}
|
|
1116
|
+
assertion.type = type;
|
|
1117
|
+
}
|
|
1118
|
+
const equalsValue = valueFromOwn(item, "equals", "expected", "expected_value", "expectedValue", "value");
|
|
1119
|
+
if (equalsValue !== void 0) assertion.equals = toJsonValue(equalsValue);
|
|
1120
|
+
const notEqualsValue = valueFromOwn(item, "not_equals", "notEquals", "forbidden", "forbidden_value", "forbiddenValue");
|
|
1121
|
+
if (notEqualsValue !== void 0) assertion.not_equals = toJsonValue(notEqualsValue);
|
|
1122
|
+
const containsValue = valueFromOwn(item, "contains", "includes", "contains_value", "containsValue", "include");
|
|
1123
|
+
if (containsValue !== void 0) assertion.contains = toJsonValue(containsValue);
|
|
1124
|
+
if (assertion.exists === void 0 && assertion.type === void 0 && !hasOwn(assertion, "equals") && !hasOwn(assertion, "not_equals") && !hasOwn(assertion, "contains")) {
|
|
1125
|
+
assertion.exists = true;
|
|
1126
|
+
}
|
|
1127
|
+
return assertion;
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
937
1130
|
function isDialogCountCheckType(type) {
|
|
938
1131
|
return type === "dialog_count_equals" || type === "dialog_accept_count_equals" || type === "dialog_dismiss_count_equals";
|
|
939
1132
|
}
|
|
@@ -1033,6 +1226,10 @@ function normalizeCheck(input, index) {
|
|
|
1033
1226
|
`checks[${index}] body_not_patterns`
|
|
1034
1227
|
) : void 0;
|
|
1035
1228
|
if (bodyNotPatterns?.length) validateRegexPatterns(bodyNotPatterns, `checks[${index}] body_not_patterns`);
|
|
1229
|
+
const bodyJsonAssertions = isHttpStatusCheck ? normalizeHttpStatusBodyJsonAssertions(
|
|
1230
|
+
input.body_json_assertions ?? input.bodyJsonAssertions ?? input.json_body_assertions ?? input.jsonBodyAssertions ?? input.json_assertions ?? input.jsonAssertions ?? input.response_json_assertions ?? input.responseJsonAssertions,
|
|
1231
|
+
`checks[${index}] body_json_assertions`
|
|
1232
|
+
) : void 0;
|
|
1036
1233
|
if (isLinkStatusCheck) {
|
|
1037
1234
|
if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
|
|
1038
1235
|
throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
|
|
@@ -1058,6 +1255,7 @@ function normalizeCheck(input, index) {
|
|
|
1058
1255
|
body_contains: bodyContains,
|
|
1059
1256
|
body_not_contains: bodyNotContains,
|
|
1060
1257
|
body_not_patterns: bodyNotPatterns,
|
|
1258
|
+
body_json_assertions: bodyJsonAssertions,
|
|
1061
1259
|
expected_texts: expectedTexts,
|
|
1062
1260
|
link_selector: stringValue(input.link_selector) || stringValue(input.linkSelector),
|
|
1063
1261
|
source_selector: stringValue(input.source_selector) || stringValue(input.sourceSelector),
|
|
@@ -1264,6 +1462,34 @@ function httpStatusBodyNotPatternFailures(result, check) {
|
|
|
1264
1462
|
const observed = isRecord(result.body_not_patterns) ? result.body_not_patterns : {};
|
|
1265
1463
|
return forbidden.filter((pattern) => observed[pattern] !== false);
|
|
1266
1464
|
}
|
|
1465
|
+
function httpStatusBodyJsonAssertionFailures(result, check) {
|
|
1466
|
+
const expected = check.body_json_assertions?.filter((assertion) => assertion.path) ?? [];
|
|
1467
|
+
if (!expected.length) return [];
|
|
1468
|
+
if (!Array.isArray(result.body_json_assertions)) {
|
|
1469
|
+
return expected.map((assertion) => ({
|
|
1470
|
+
label: assertion.label || assertion.path,
|
|
1471
|
+
path: assertion.path,
|
|
1472
|
+
ok: false,
|
|
1473
|
+
exists: false,
|
|
1474
|
+
observed_type: "missing",
|
|
1475
|
+
errors: ["body_json_assertions evidence missing"]
|
|
1476
|
+
}));
|
|
1477
|
+
}
|
|
1478
|
+
return result.body_json_assertions.filter((assertion) => isRecord(assertion) && assertion.ok !== true).map((assertion) => ({
|
|
1479
|
+
label: stringValue(assertion.label) || stringValue(assertion.path) || "json assertion",
|
|
1480
|
+
path: stringValue(assertion.path) || "",
|
|
1481
|
+
ok: false,
|
|
1482
|
+
exists: assertion.exists === true,
|
|
1483
|
+
observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
|
|
1484
|
+
observed_type: stringValue(assertion.observed_type) || "missing",
|
|
1485
|
+
expected_exists: booleanValue(assertion.expected_exists),
|
|
1486
|
+
equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
|
|
1487
|
+
not_equals: hasOwn(assertion, "not_equals") ? toJsonValue(assertion.not_equals) : void 0,
|
|
1488
|
+
contains: hasOwn(assertion, "contains") ? toJsonValue(assertion.contains) : void 0,
|
|
1489
|
+
type: stringValue(assertion.type),
|
|
1490
|
+
errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : void 0
|
|
1491
|
+
}));
|
|
1492
|
+
}
|
|
1267
1493
|
function linkStatusResultOk(result, check) {
|
|
1268
1494
|
const status = numberValue(result.status);
|
|
1269
1495
|
if (!httpStatusIsAllowed(status, check)) return false;
|
|
@@ -1281,6 +1507,7 @@ function linkStatusResultOk(result, check) {
|
|
|
1281
1507
|
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
1282
1508
|
if (httpStatusBodyNotContainsFailures(result, check).length) return false;
|
|
1283
1509
|
if (httpStatusBodyNotPatternFailures(result, check).length) return false;
|
|
1510
|
+
if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
|
|
1284
1511
|
return true;
|
|
1285
1512
|
}
|
|
1286
1513
|
function responseHeader(response, name) {
|
|
@@ -1349,7 +1576,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1349
1576
|
statusText = typeof response.statusText === "string" ? response.statusText : "";
|
|
1350
1577
|
result.content_type = responseHeader(response, "content-type");
|
|
1351
1578
|
result.content_length = responseContentLength(response);
|
|
1352
|
-
const shouldReadBody = check.require_nonzero_bytes === true || typeof check.min_bytes === "number" || Boolean(check.body_contains?.length) || Boolean(check.body_not_contains?.length) || Boolean(check.body_not_patterns?.length);
|
|
1579
|
+
const shouldReadBody = check.require_nonzero_bytes === true || typeof check.min_bytes === "number" || Boolean(check.body_contains?.length) || Boolean(check.body_not_contains?.length) || Boolean(check.body_not_patterns?.length) || Boolean(check.body_json_assertions?.length);
|
|
1353
1580
|
if (shouldReadBody && method !== "HEAD") {
|
|
1354
1581
|
const body = await responseBodyText(response);
|
|
1355
1582
|
result.bytes = body.bytes;
|
|
@@ -1362,6 +1589,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1362
1589
|
if (check.body_not_patterns?.length) {
|
|
1363
1590
|
result.body_not_patterns = Object.fromEntries(check.body_not_patterns.filter(Boolean).map((pattern) => [pattern, new RegExp(pattern).test(body.text)]));
|
|
1364
1591
|
}
|
|
1592
|
+
if (check.body_json_assertions?.length) {
|
|
1593
|
+
result.body_json_assertions = evaluateHttpStatusBodyJsonAssertions(body.text, check.body_json_assertions);
|
|
1594
|
+
}
|
|
1365
1595
|
}
|
|
1366
1596
|
} catch (caught) {
|
|
1367
1597
|
error = String(caught instanceof Error ? caught.message : caught).slice(0, 500);
|
|
@@ -1370,6 +1600,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1370
1600
|
const bodyContainsMissing = httpStatusBodyContainsFailures(result, check);
|
|
1371
1601
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(result, check);
|
|
1372
1602
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(result, check);
|
|
1603
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(result, check);
|
|
1373
1604
|
const ok = !error && linkStatusResultOk(result, check);
|
|
1374
1605
|
return {
|
|
1375
1606
|
index,
|
|
@@ -1388,7 +1619,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
1388
1619
|
body_not_contains: isRecord(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
|
|
1389
1620
|
body_not_contains_found: bodyNotContainsFound,
|
|
1390
1621
|
body_not_patterns: isRecord(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
|
|
1391
|
-
body_not_patterns_found: bodyNotPatternsFound
|
|
1622
|
+
body_not_patterns_found: bodyNotPatternsFound,
|
|
1623
|
+
body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
|
|
1624
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed
|
|
1392
1625
|
};
|
|
1393
1626
|
}
|
|
1394
1627
|
async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}) {
|
|
@@ -1433,6 +1666,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
1433
1666
|
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
1434
1667
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
|
|
1435
1668
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
|
|
1669
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
|
|
1436
1670
|
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
1437
1671
|
failures.push({
|
|
1438
1672
|
code: "http_status_failed",
|
|
@@ -1451,6 +1685,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
1451
1685
|
body_not_contains_found: bodyNotContainsFound,
|
|
1452
1686
|
body_not_patterns: check.body_not_patterns ?? null,
|
|
1453
1687
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
1688
|
+
body_json_assertions: check.body_json_assertions ?? null,
|
|
1689
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
|
|
1454
1690
|
body_sample: stringValue(statusEvidence.body_sample) ?? null
|
|
1455
1691
|
});
|
|
1456
1692
|
}
|
|
@@ -1472,6 +1708,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
1472
1708
|
body_not_contains_found: bodyNotContainsFound,
|
|
1473
1709
|
body_not_patterns: isRecord(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
|
|
1474
1710
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
1711
|
+
body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
|
|
1712
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
|
|
1475
1713
|
body_sample: stringValue(statusEvidence.body_sample) ?? null,
|
|
1476
1714
|
failures
|
|
1477
1715
|
};
|
|
@@ -2108,6 +2346,7 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
2108
2346
|
body_contains: check.body_contains ?? [],
|
|
2109
2347
|
body_not_contains: check.body_not_contains ?? [],
|
|
2110
2348
|
body_not_patterns: check.body_not_patterns ?? [],
|
|
2349
|
+
body_json_assertions: toJsonValue(check.body_json_assertions ?? []),
|
|
2111
2350
|
viewports: summaries.map((summary) => toJsonValue(summary)),
|
|
2112
2351
|
failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue(summary.viewport) ?? null, failure })) : [])
|
|
2113
2352
|
},
|
|
@@ -2851,6 +3090,36 @@ function httpStatusBodyNotPatternFailures(result, check) {
|
|
|
2851
3090
|
: {};
|
|
2852
3091
|
return forbidden.filter((pattern) => observed[pattern] !== false);
|
|
2853
3092
|
}
|
|
3093
|
+
function httpStatusBodyJsonAssertionFailures(result, check) {
|
|
3094
|
+
const expected = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
3095
|
+
if (!expected.length) return [];
|
|
3096
|
+
if (!Array.isArray(result.body_json_assertions)) {
|
|
3097
|
+
return expected.map((assertion) => ({
|
|
3098
|
+
label: assertion.label || assertion.path,
|
|
3099
|
+
path: assertion.path,
|
|
3100
|
+
ok: false,
|
|
3101
|
+
exists: false,
|
|
3102
|
+
observed_type: "missing",
|
|
3103
|
+
errors: ["body_json_assertions evidence missing"],
|
|
3104
|
+
}));
|
|
3105
|
+
}
|
|
3106
|
+
return result.body_json_assertions
|
|
3107
|
+
.filter((assertion) => assertion && typeof assertion === "object" && assertion.ok !== true)
|
|
3108
|
+
.map((assertion) => ({
|
|
3109
|
+
label: typeof assertion.label === "string" && assertion.label ? assertion.label : typeof assertion.path === "string" && assertion.path ? assertion.path : "json assertion",
|
|
3110
|
+
path: typeof assertion.path === "string" ? assertion.path : "",
|
|
3111
|
+
ok: false,
|
|
3112
|
+
exists: assertion.exists === true,
|
|
3113
|
+
observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
|
|
3114
|
+
observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
|
|
3115
|
+
expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
|
|
3116
|
+
equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
|
|
3117
|
+
not_equals: Object.hasOwn(assertion, "not_equals") ? assertion.not_equals : undefined,
|
|
3118
|
+
contains: Object.hasOwn(assertion, "contains") ? assertion.contains : undefined,
|
|
3119
|
+
type: typeof assertion.type === "string" ? assertion.type : undefined,
|
|
3120
|
+
errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : undefined,
|
|
3121
|
+
}));
|
|
3122
|
+
}
|
|
2854
3123
|
function linkStatusResultOk(result, check) {
|
|
2855
3124
|
if (!result || typeof result !== "object" || Array.isArray(result)) return false;
|
|
2856
3125
|
if (!httpStatusIsAllowed(result.status, check)) return false;
|
|
@@ -2868,6 +3137,7 @@ function linkStatusResultOk(result, check) {
|
|
|
2868
3137
|
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
2869
3138
|
if (httpStatusBodyNotContainsFailures(result, check).length) return false;
|
|
2870
3139
|
if (httpStatusBodyNotPatternFailures(result, check).length) return false;
|
|
3140
|
+
if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
|
|
2871
3141
|
return true;
|
|
2872
3142
|
}
|
|
2873
3143
|
function summarizeHttpStatusEvidence(viewport, check) {
|
|
@@ -2888,6 +3158,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
2888
3158
|
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
2889
3159
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
|
|
2890
3160
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
|
|
3161
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
|
|
2891
3162
|
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
2892
3163
|
failures.push({
|
|
2893
3164
|
code: "http_status_failed",
|
|
@@ -2906,6 +3177,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
2906
3177
|
body_not_contains_found: bodyNotContainsFound,
|
|
2907
3178
|
body_not_patterns: Array.isArray(check.body_not_patterns) ? check.body_not_patterns : null,
|
|
2908
3179
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
3180
|
+
body_json_assertions: Array.isArray(check.body_json_assertions) ? check.body_json_assertions : null,
|
|
3181
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed,
|
|
2909
3182
|
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
2910
3183
|
});
|
|
2911
3184
|
}
|
|
@@ -2933,6 +3206,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
2933
3206
|
? statusEvidence.body_not_patterns
|
|
2934
3207
|
: null,
|
|
2935
3208
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
3209
|
+
body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? statusEvidence.body_json_assertions : null,
|
|
3210
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed,
|
|
2936
3211
|
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
2937
3212
|
failures,
|
|
2938
3213
|
};
|
|
@@ -5035,6 +5310,155 @@ function linkProbeResponseFields(response, method) {
|
|
|
5035
5310
|
content_length: contentLength,
|
|
5036
5311
|
};
|
|
5037
5312
|
}
|
|
5313
|
+
function jsonProbeValueType(value) {
|
|
5314
|
+
if (value === null) return "null";
|
|
5315
|
+
if (Array.isArray(value)) return "array";
|
|
5316
|
+
if (typeof value === "boolean") return "boolean";
|
|
5317
|
+
if (typeof value === "number") return "number";
|
|
5318
|
+
if (typeof value === "string") return "string";
|
|
5319
|
+
return "object";
|
|
5320
|
+
}
|
|
5321
|
+
function jsonProbeDeepEqual(left, right) {
|
|
5322
|
+
if (left === right) return true;
|
|
5323
|
+
if (typeof left !== typeof right) return false;
|
|
5324
|
+
if (left === null || right === null) return left === right;
|
|
5325
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
5326
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
5327
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
5328
|
+
return left.every((item, index) => jsonProbeDeepEqual(item, right[index]));
|
|
5329
|
+
}
|
|
5330
|
+
const leftKeys = Object.keys(left).sort();
|
|
5331
|
+
const rightKeys = Object.keys(right).sort();
|
|
5332
|
+
if (!jsonProbeDeepEqual(leftKeys, rightKeys)) return false;
|
|
5333
|
+
return leftKeys.every((key) => jsonProbeDeepEqual(left[key], right[key]));
|
|
5334
|
+
}
|
|
5335
|
+
function jsonProbeContains(observed, expected) {
|
|
5336
|
+
if (typeof observed === "string" && typeof expected === "string") return observed.includes(expected);
|
|
5337
|
+
if (Array.isArray(observed)) return observed.some((item) => jsonProbeDeepEqual(item, expected));
|
|
5338
|
+
if (observed && expected && typeof observed === "object" && typeof expected === "object" && !Array.isArray(observed) && !Array.isArray(expected)) {
|
|
5339
|
+
return Object.entries(expected).every(([key, value]) => Object.hasOwn(observed, key) && jsonProbeDeepEqual(observed[key], value));
|
|
5340
|
+
}
|
|
5341
|
+
return false;
|
|
5342
|
+
}
|
|
5343
|
+
function parseJsonProbePathSegments(path) {
|
|
5344
|
+
let input = String(path || "").trim();
|
|
5345
|
+
if (!input) throw new Error("path is empty");
|
|
5346
|
+
if (input === "$") return [];
|
|
5347
|
+
if (input.startsWith("$.")) input = input.slice(2);
|
|
5348
|
+
else if (input.startsWith("$[")) input = input.slice(1);
|
|
5349
|
+
const segments = [];
|
|
5350
|
+
let token = "";
|
|
5351
|
+
const pushToken = () => {
|
|
5352
|
+
if (!token) return;
|
|
5353
|
+
segments.push(token);
|
|
5354
|
+
token = "";
|
|
5355
|
+
};
|
|
5356
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
5357
|
+
const char = input[index];
|
|
5358
|
+
if (char === ".") {
|
|
5359
|
+
pushToken();
|
|
5360
|
+
continue;
|
|
5361
|
+
}
|
|
5362
|
+
if (char !== "[") {
|
|
5363
|
+
token += char;
|
|
5364
|
+
continue;
|
|
5365
|
+
}
|
|
5366
|
+
pushToken();
|
|
5367
|
+
const closeIndex = input.indexOf("]", index + 1);
|
|
5368
|
+
if (closeIndex === -1) throw new Error("unterminated bracket at " + index);
|
|
5369
|
+
const bracket = input.slice(index + 1, closeIndex).trim();
|
|
5370
|
+
if (!bracket) throw new Error("empty bracket at " + index);
|
|
5371
|
+
if (/^\d+$/.test(bracket)) {
|
|
5372
|
+
segments.push(Number(bracket));
|
|
5373
|
+
} else if ((bracket.startsWith('"') && bracket.endsWith('"')) || (bracket.startsWith("'") && bracket.endsWith("'"))) {
|
|
5374
|
+
const quoted = bracket.startsWith("'")
|
|
5375
|
+
? '"' + bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'
|
|
5376
|
+
: bracket;
|
|
5377
|
+
segments.push(String(JSON.parse(quoted)));
|
|
5378
|
+
} else {
|
|
5379
|
+
segments.push(bracket);
|
|
5380
|
+
}
|
|
5381
|
+
index = closeIndex;
|
|
5382
|
+
}
|
|
5383
|
+
pushToken();
|
|
5384
|
+
return segments;
|
|
5385
|
+
}
|
|
5386
|
+
function resolveJsonProbePath(root, path) {
|
|
5387
|
+
let segments;
|
|
5388
|
+
try {
|
|
5389
|
+
segments = parseJsonProbePathSegments(path);
|
|
5390
|
+
} catch (error) {
|
|
5391
|
+
return { exists: false, error: String(error && error.message ? error.message : error) };
|
|
5392
|
+
}
|
|
5393
|
+
let current = root;
|
|
5394
|
+
for (const segment of segments) {
|
|
5395
|
+
if (typeof segment === "number") {
|
|
5396
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
|
|
5397
|
+
current = current[segment];
|
|
5398
|
+
continue;
|
|
5399
|
+
}
|
|
5400
|
+
if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, segment)) {
|
|
5401
|
+
return { exists: false };
|
|
5402
|
+
}
|
|
5403
|
+
current = current[segment];
|
|
5404
|
+
}
|
|
5405
|
+
return { exists: true, value: current };
|
|
5406
|
+
}
|
|
5407
|
+
function evaluateJsonProbeAssertion(root, assertion) {
|
|
5408
|
+
const resolved = resolveJsonProbePath(root, assertion.path);
|
|
5409
|
+
const errors = [];
|
|
5410
|
+
const result = {
|
|
5411
|
+
label: assertion.label || assertion.path,
|
|
5412
|
+
path: assertion.path,
|
|
5413
|
+
ok: true,
|
|
5414
|
+
exists: resolved.exists,
|
|
5415
|
+
observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
|
|
5416
|
+
};
|
|
5417
|
+
if (resolved.exists) result.observed = resolved.value;
|
|
5418
|
+
if (resolved.error) errors.push(resolved.error);
|
|
5419
|
+
if (Object.hasOwn(assertion, "exists")) {
|
|
5420
|
+
result.expected_exists = assertion.exists;
|
|
5421
|
+
if (resolved.exists !== assertion.exists) errors.push("expected exists=" + assertion.exists);
|
|
5422
|
+
}
|
|
5423
|
+
if (Object.hasOwn(assertion, "type")) {
|
|
5424
|
+
result.type = assertion.type;
|
|
5425
|
+
if (!resolved.exists || jsonProbeValueType(resolved.value) !== assertion.type) errors.push("expected type " + assertion.type);
|
|
5426
|
+
}
|
|
5427
|
+
if (Object.hasOwn(assertion, "equals")) {
|
|
5428
|
+
result.equals = assertion.equals;
|
|
5429
|
+
if (!resolved.exists || !jsonProbeDeepEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
|
|
5430
|
+
}
|
|
5431
|
+
if (Object.hasOwn(assertion, "not_equals")) {
|
|
5432
|
+
result.not_equals = assertion.not_equals;
|
|
5433
|
+
if (resolved.exists && jsonProbeDeepEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
|
|
5434
|
+
}
|
|
5435
|
+
if (Object.hasOwn(assertion, "contains")) {
|
|
5436
|
+
result.contains = assertion.contains;
|
|
5437
|
+
if (!resolved.exists || !jsonProbeContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
|
|
5438
|
+
}
|
|
5439
|
+
result.ok = errors.length === 0;
|
|
5440
|
+
if (errors.length) result.errors = errors;
|
|
5441
|
+
return result;
|
|
5442
|
+
}
|
|
5443
|
+
function evaluateJsonProbeAssertions(text, assertions) {
|
|
5444
|
+
const expected = Array.isArray(assertions) ? assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
5445
|
+
if (!expected.length) return [];
|
|
5446
|
+
let parsed;
|
|
5447
|
+
try {
|
|
5448
|
+
parsed = JSON.parse(text);
|
|
5449
|
+
} catch (error) {
|
|
5450
|
+
const message = "response body is not valid JSON: " + String(error && error.message ? error.message : error).slice(0, 200);
|
|
5451
|
+
return expected.map((assertion) => ({
|
|
5452
|
+
label: assertion.label || assertion.path,
|
|
5453
|
+
path: assertion.path,
|
|
5454
|
+
ok: false,
|
|
5455
|
+
exists: false,
|
|
5456
|
+
observed_type: "missing",
|
|
5457
|
+
errors: [message],
|
|
5458
|
+
}));
|
|
5459
|
+
}
|
|
5460
|
+
return expected.map((assertion) => evaluateJsonProbeAssertion(parsed, assertion));
|
|
5461
|
+
}
|
|
5038
5462
|
async function collectHttpStatus(check) {
|
|
5039
5463
|
const url = httpStatusRequestUrl(check, page.url() || targetUrl);
|
|
5040
5464
|
const method = httpStatusMethod(check);
|
|
@@ -5051,6 +5475,7 @@ async function collectHttpStatus(check) {
|
|
|
5051
5475
|
const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
|
|
5052
5476
|
const bodyNotContains = Array.isArray(check.body_not_contains) ? check.body_not_contains.filter(Boolean) : [];
|
|
5053
5477
|
const bodyNotPatterns = Array.isArray(check.body_not_patterns) ? check.body_not_patterns.filter(Boolean) : [];
|
|
5478
|
+
const bodyJsonAssertions = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
5054
5479
|
const options = {
|
|
5055
5480
|
method,
|
|
5056
5481
|
redirect: "follow",
|
|
@@ -5076,17 +5501,18 @@ async function collectHttpStatus(check) {
|
|
|
5076
5501
|
Object.assign(result, linkProbeResponseFields(response, method));
|
|
5077
5502
|
result.url = url;
|
|
5078
5503
|
result.status_text = response.statusText || "";
|
|
5079
|
-
const shouldReadBody = check.require_nonzero_bytes === true || (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) || bodyContains.length > 0 || bodyNotContains.length > 0 || bodyNotPatterns.length > 0;
|
|
5504
|
+
const shouldReadBody = check.require_nonzero_bytes === true || (typeof check.min_bytes === "number" && Number.isFinite(check.min_bytes)) || bodyContains.length > 0 || bodyNotContains.length > 0 || bodyNotPatterns.length > 0 || bodyJsonAssertions.length > 0;
|
|
5080
5505
|
if (shouldReadBody) {
|
|
5081
5506
|
try {
|
|
5082
5507
|
const buffer = await response.arrayBuffer();
|
|
5083
5508
|
result.bytes = buffer.byteLength;
|
|
5084
|
-
if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length) {
|
|
5509
|
+
if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length || bodyJsonAssertions.length) {
|
|
5085
5510
|
const text = new TextDecoder().decode(buffer);
|
|
5086
5511
|
result.body_sample = text.slice(0, 1000);
|
|
5087
5512
|
if (bodyContains.length) result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
|
|
5088
5513
|
if (bodyNotContains.length) result.body_not_contains = Object.fromEntries(bodyNotContains.map((forbidden) => [forbidden, text.includes(forbidden)]));
|
|
5089
5514
|
if (bodyNotPatterns.length) result.body_not_patterns = Object.fromEntries(bodyNotPatterns.map((pattern) => [pattern, new RegExp(pattern).test(text)]));
|
|
5515
|
+
if (bodyJsonAssertions.length) result.body_json_assertions = evaluateJsonProbeAssertions(text, bodyJsonAssertions);
|
|
5090
5516
|
}
|
|
5091
5517
|
} catch (error) {
|
|
5092
5518
|
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
@@ -5099,6 +5525,7 @@ async function collectHttpStatus(check) {
|
|
|
5099
5525
|
&& (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
|
|
5100
5526
|
&& (!bodyNotContains.length || bodyNotContains.every((forbidden) => result.body_not_contains && result.body_not_contains[forbidden] === false))
|
|
5101
5527
|
&& (!bodyNotPatterns.length || bodyNotPatterns.every((pattern) => result.body_not_patterns && result.body_not_patterns[pattern] === false))
|
|
5528
|
+
&& (!bodyJsonAssertions.length || (Array.isArray(result.body_json_assertions) && result.body_json_assertions.every((assertion) => assertion.ok === true)))
|
|
5102
5529
|
&& !result.error;
|
|
5103
5530
|
return result;
|
|
5104
5531
|
} catch (error) {
|