@riddledc/riddle-proof 0.7.125 → 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 +34 -2
- package/dist/{chunk-JVZWSI55.js → chunk-JLINSUKO.js} +460 -7
- package/dist/cli.cjs +478 -8
- package/dist/cli.js +19 -2
- package/dist/index.cjs +460 -7
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/profile.cjs +460 -7
- package/dist/profile.d.cts +29 -1
- package/dist/profile.d.ts +29 -1
- package/dist/profile.js +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -7076,6 +7076,9 @@ function valueFromOwn(input, ...keys) {
|
|
|
7076
7076
|
function numberValue(value) {
|
|
7077
7077
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
7078
7078
|
}
|
|
7079
|
+
function booleanValue(value) {
|
|
7080
|
+
return typeof value === "boolean" ? value : void 0;
|
|
7081
|
+
}
|
|
7079
7082
|
function horizontalBoundsOverflowPx(value) {
|
|
7080
7083
|
if (!isRecord(value)) return 0;
|
|
7081
7084
|
let max = maxPositiveNumber(
|
|
@@ -7170,6 +7173,156 @@ function toJsonValue(value) {
|
|
|
7170
7173
|
if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
|
|
7171
7174
|
return String(value);
|
|
7172
7175
|
}
|
|
7176
|
+
function jsonValueType(value) {
|
|
7177
|
+
if (value === null) return "null";
|
|
7178
|
+
if (Array.isArray(value)) return "array";
|
|
7179
|
+
if (typeof value === "boolean") return "boolean";
|
|
7180
|
+
if (typeof value === "number") return "number";
|
|
7181
|
+
if (typeof value === "string") return "string";
|
|
7182
|
+
return "object";
|
|
7183
|
+
}
|
|
7184
|
+
function deepJsonEqual(left, right) {
|
|
7185
|
+
if (left === right) return true;
|
|
7186
|
+
if (typeof left !== typeof right) return false;
|
|
7187
|
+
if (left === null || right === null) return left === right;
|
|
7188
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
7189
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
7190
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
7191
|
+
return left.every((item, index) => deepJsonEqual(item, right[index]));
|
|
7192
|
+
}
|
|
7193
|
+
if (!isRecord(left) || !isRecord(right)) return false;
|
|
7194
|
+
const leftKeys = Object.keys(left).sort();
|
|
7195
|
+
const rightKeys = Object.keys(right).sort();
|
|
7196
|
+
if (!deepJsonEqual(leftKeys, rightKeys)) return false;
|
|
7197
|
+
return leftKeys.every((key) => deepJsonEqual(left[key], right[key]));
|
|
7198
|
+
}
|
|
7199
|
+
function jsonContains(observed, expected) {
|
|
7200
|
+
if (typeof observed === "string" && typeof expected === "string") {
|
|
7201
|
+
return observed.includes(expected);
|
|
7202
|
+
}
|
|
7203
|
+
if (Array.isArray(observed)) {
|
|
7204
|
+
return observed.some((item) => deepJsonEqual(item, expected));
|
|
7205
|
+
}
|
|
7206
|
+
if (isRecord(observed) && isRecord(expected)) {
|
|
7207
|
+
return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
|
|
7208
|
+
}
|
|
7209
|
+
return false;
|
|
7210
|
+
}
|
|
7211
|
+
function parseJsonPathSegments(path7) {
|
|
7212
|
+
let input = path7.trim();
|
|
7213
|
+
if (!input) throw new Error("path is empty");
|
|
7214
|
+
if (input === "$") return [];
|
|
7215
|
+
if (input.startsWith("$.")) input = input.slice(2);
|
|
7216
|
+
else if (input.startsWith("$[")) input = input.slice(1);
|
|
7217
|
+
const segments = [];
|
|
7218
|
+
let token = "";
|
|
7219
|
+
const pushToken = () => {
|
|
7220
|
+
if (!token) return;
|
|
7221
|
+
segments.push(token);
|
|
7222
|
+
token = "";
|
|
7223
|
+
};
|
|
7224
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
7225
|
+
const char = input[index];
|
|
7226
|
+
if (char === ".") {
|
|
7227
|
+
pushToken();
|
|
7228
|
+
continue;
|
|
7229
|
+
}
|
|
7230
|
+
if (char !== "[") {
|
|
7231
|
+
token += char;
|
|
7232
|
+
continue;
|
|
7233
|
+
}
|
|
7234
|
+
pushToken();
|
|
7235
|
+
const closeIndex = input.indexOf("]", index + 1);
|
|
7236
|
+
if (closeIndex === -1) throw new Error(`unterminated bracket at ${index}`);
|
|
7237
|
+
const bracket = input.slice(index + 1, closeIndex).trim();
|
|
7238
|
+
if (!bracket) throw new Error(`empty bracket at ${index}`);
|
|
7239
|
+
if (/^\d+$/.test(bracket)) {
|
|
7240
|
+
segments.push(Number(bracket));
|
|
7241
|
+
} else if (bracket.startsWith('"') && bracket.endsWith('"') || bracket.startsWith("'") && bracket.endsWith("'")) {
|
|
7242
|
+
const quoted = bracket.startsWith("'") ? `"${bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : bracket;
|
|
7243
|
+
segments.push(String(JSON.parse(quoted)));
|
|
7244
|
+
} else {
|
|
7245
|
+
segments.push(bracket);
|
|
7246
|
+
}
|
|
7247
|
+
index = closeIndex;
|
|
7248
|
+
}
|
|
7249
|
+
pushToken();
|
|
7250
|
+
return segments;
|
|
7251
|
+
}
|
|
7252
|
+
function resolveJsonPath(root, path7) {
|
|
7253
|
+
let segments;
|
|
7254
|
+
try {
|
|
7255
|
+
segments = parseJsonPathSegments(path7);
|
|
7256
|
+
} catch (error) {
|
|
7257
|
+
return { exists: false, error: String(error instanceof Error ? error.message : error) };
|
|
7258
|
+
}
|
|
7259
|
+
let current = root;
|
|
7260
|
+
for (const segment of segments) {
|
|
7261
|
+
if (typeof segment === "number") {
|
|
7262
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
|
|
7263
|
+
current = current[segment];
|
|
7264
|
+
continue;
|
|
7265
|
+
}
|
|
7266
|
+
if (!isRecord(current) || !hasOwn(current, segment)) return { exists: false };
|
|
7267
|
+
current = current[segment];
|
|
7268
|
+
}
|
|
7269
|
+
return { exists: true, value: current };
|
|
7270
|
+
}
|
|
7271
|
+
function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
|
|
7272
|
+
const resolved = resolveJsonPath(root, assertion.path);
|
|
7273
|
+
const errors = [];
|
|
7274
|
+
const result = {
|
|
7275
|
+
label: assertion.label || assertion.path,
|
|
7276
|
+
path: assertion.path,
|
|
7277
|
+
ok: true,
|
|
7278
|
+
exists: resolved.exists,
|
|
7279
|
+
observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
|
|
7280
|
+
};
|
|
7281
|
+
if (resolved.exists) result.observed = toJsonValue(resolved.value);
|
|
7282
|
+
if (resolved.error) errors.push(resolved.error);
|
|
7283
|
+
if (hasOwn(assertion, "exists")) {
|
|
7284
|
+
result.expected_exists = assertion.exists;
|
|
7285
|
+
if (resolved.exists !== assertion.exists) errors.push(`expected exists=${assertion.exists}`);
|
|
7286
|
+
}
|
|
7287
|
+
if (hasOwn(assertion, "type")) {
|
|
7288
|
+
result.type = assertion.type;
|
|
7289
|
+
if (!resolved.exists || jsonValueType(resolved.value) !== assertion.type) errors.push(`expected type ${assertion.type}`);
|
|
7290
|
+
}
|
|
7291
|
+
if (hasOwn(assertion, "equals")) {
|
|
7292
|
+
result.equals = assertion.equals;
|
|
7293
|
+
if (!resolved.exists || !deepJsonEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
|
|
7294
|
+
}
|
|
7295
|
+
if (hasOwn(assertion, "not_equals")) {
|
|
7296
|
+
result.not_equals = assertion.not_equals;
|
|
7297
|
+
if (resolved.exists && deepJsonEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
|
|
7298
|
+
}
|
|
7299
|
+
if (hasOwn(assertion, "contains")) {
|
|
7300
|
+
result.contains = assertion.contains;
|
|
7301
|
+
if (!resolved.exists || !jsonContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
|
|
7302
|
+
}
|
|
7303
|
+
result.ok = errors.length === 0;
|
|
7304
|
+
if (errors.length) result.errors = errors;
|
|
7305
|
+
return result;
|
|
7306
|
+
}
|
|
7307
|
+
function evaluateHttpStatusBodyJsonAssertions(bodyText, assertions) {
|
|
7308
|
+
const expected = assertions?.filter((assertion) => assertion.path) ?? [];
|
|
7309
|
+
if (!expected.length) return [];
|
|
7310
|
+
let parsed;
|
|
7311
|
+
try {
|
|
7312
|
+
parsed = JSON.parse(bodyText);
|
|
7313
|
+
} catch (error) {
|
|
7314
|
+
const message = `response body is not valid JSON: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`;
|
|
7315
|
+
return expected.map((assertion) => ({
|
|
7316
|
+
label: assertion.label || assertion.path,
|
|
7317
|
+
path: assertion.path,
|
|
7318
|
+
ok: false,
|
|
7319
|
+
exists: false,
|
|
7320
|
+
observed_type: "missing",
|
|
7321
|
+
errors: [message]
|
|
7322
|
+
}));
|
|
7323
|
+
}
|
|
7324
|
+
return expected.map((assertion) => evaluateHttpStatusBodyJsonAssertion(parsed, assertion));
|
|
7325
|
+
}
|
|
7173
7326
|
function compactProfileSetupSummaryText(value, limit = 160) {
|
|
7174
7327
|
const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
7175
7328
|
if (!text) return void 0;
|
|
@@ -7533,6 +7686,20 @@ function normalizeNetworkMock(input, index) {
|
|
|
7533
7686
|
if (maxHitCount !== void 0 && effectiveRequiredHitCount > maxHitCount) {
|
|
7534
7687
|
throw new Error(`target.network_mocks[${index}].max_hit_count cannot be less than its required hit count.`);
|
|
7535
7688
|
}
|
|
7689
|
+
const sequenceScopeInput = stringValue2(
|
|
7690
|
+
input.sequence_scope ?? input.sequenceScope ?? input.response_sequence_scope ?? input.responseSequenceScope
|
|
7691
|
+
);
|
|
7692
|
+
let sequenceScope;
|
|
7693
|
+
if (sequenceScopeInput) {
|
|
7694
|
+
const normalizedScope = sequenceScopeInput.toLowerCase().replace(/[-\s]+/g, "_");
|
|
7695
|
+
if (normalizedScope === "global" || normalizedScope === "profile" || normalizedScope === "run") {
|
|
7696
|
+
sequenceScope = "global";
|
|
7697
|
+
} else if (normalizedScope === "viewport" || normalizedScope === "per_viewport" || normalizedScope === "viewport_scoped") {
|
|
7698
|
+
sequenceScope = "viewport";
|
|
7699
|
+
} else {
|
|
7700
|
+
throw new Error(`target.network_mocks[${index}].sequence_scope must be "global" or "viewport".`);
|
|
7701
|
+
}
|
|
7702
|
+
}
|
|
7536
7703
|
return {
|
|
7537
7704
|
...payload,
|
|
7538
7705
|
label: normalizeName(input.label || input.name, `network-mock-${index + 1}`),
|
|
@@ -7540,6 +7707,7 @@ function normalizeNetworkMock(input, index) {
|
|
|
7540
7707
|
method: stringValue2(input.method)?.toUpperCase(),
|
|
7541
7708
|
responses,
|
|
7542
7709
|
repeat_responses: input.repeat_responses === true || input.repeatResponses === true || input.cycle_responses === true || input.cycleResponses === true,
|
|
7710
|
+
sequence_scope: sequenceScope,
|
|
7543
7711
|
required_hit_count: requiredHitCount,
|
|
7544
7712
|
max_hit_count: maxHitCount,
|
|
7545
7713
|
forbidden,
|
|
@@ -7809,6 +7977,46 @@ function validateRegexPatterns(patterns, label) {
|
|
|
7809
7977
|
}
|
|
7810
7978
|
}
|
|
7811
7979
|
}
|
|
7980
|
+
function normalizeHttpStatusBodyJsonAssertions(value, label) {
|
|
7981
|
+
if (value === void 0) return void 0;
|
|
7982
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
7983
|
+
if (!value.length) throw new Error(`${label} must not be empty.`);
|
|
7984
|
+
return value.map((item, index) => {
|
|
7985
|
+
const itemLabel = `${label}[${index}]`;
|
|
7986
|
+
if (typeof item === "string") {
|
|
7987
|
+
const path8 = stringValue2(item);
|
|
7988
|
+
if (!path8) throw new Error(`${itemLabel} path must not be empty.`);
|
|
7989
|
+
return { path: path8, exists: true };
|
|
7990
|
+
}
|
|
7991
|
+
if (!isRecord(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
|
|
7992
|
+
const path7 = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
|
|
7993
|
+
if (!path7) throw new Error(`${itemLabel}.path is required.`);
|
|
7994
|
+
const assertion = {
|
|
7995
|
+
label: stringValue2(item.label),
|
|
7996
|
+
path: path7
|
|
7997
|
+
};
|
|
7998
|
+
const exists = booleanValue(valueFromOwn(item, "exists", "present"));
|
|
7999
|
+
if (exists !== void 0) assertion.exists = exists;
|
|
8000
|
+
const type = stringValue2(valueFromOwn(item, "type", "value_type", "valueType"));
|
|
8001
|
+
if (type !== void 0) {
|
|
8002
|
+
const allowedTypes = ["array", "boolean", "null", "number", "object", "string"];
|
|
8003
|
+
if (!allowedTypes.includes(type)) {
|
|
8004
|
+
throw new Error(`${itemLabel}.type must be one of ${allowedTypes.join(", ")}.`);
|
|
8005
|
+
}
|
|
8006
|
+
assertion.type = type;
|
|
8007
|
+
}
|
|
8008
|
+
const equalsValue = valueFromOwn(item, "equals", "expected", "expected_value", "expectedValue", "value");
|
|
8009
|
+
if (equalsValue !== void 0) assertion.equals = toJsonValue(equalsValue);
|
|
8010
|
+
const notEqualsValue = valueFromOwn(item, "not_equals", "notEquals", "forbidden", "forbidden_value", "forbiddenValue");
|
|
8011
|
+
if (notEqualsValue !== void 0) assertion.not_equals = toJsonValue(notEqualsValue);
|
|
8012
|
+
const containsValue = valueFromOwn(item, "contains", "includes", "contains_value", "containsValue", "include");
|
|
8013
|
+
if (containsValue !== void 0) assertion.contains = toJsonValue(containsValue);
|
|
8014
|
+
if (assertion.exists === void 0 && assertion.type === void 0 && !hasOwn(assertion, "equals") && !hasOwn(assertion, "not_equals") && !hasOwn(assertion, "contains")) {
|
|
8015
|
+
assertion.exists = true;
|
|
8016
|
+
}
|
|
8017
|
+
return assertion;
|
|
8018
|
+
});
|
|
8019
|
+
}
|
|
7812
8020
|
function isDialogCountCheckType(type) {
|
|
7813
8021
|
return type === "dialog_count_equals" || type === "dialog_accept_count_equals" || type === "dialog_dismiss_count_equals";
|
|
7814
8022
|
}
|
|
@@ -7908,6 +8116,10 @@ function normalizeCheck(input, index) {
|
|
|
7908
8116
|
`checks[${index}] body_not_patterns`
|
|
7909
8117
|
) : void 0;
|
|
7910
8118
|
if (bodyNotPatterns?.length) validateRegexPatterns(bodyNotPatterns, `checks[${index}] body_not_patterns`);
|
|
8119
|
+
const bodyJsonAssertions = isHttpStatusCheck ? normalizeHttpStatusBodyJsonAssertions(
|
|
8120
|
+
input.body_json_assertions ?? input.bodyJsonAssertions ?? input.json_body_assertions ?? input.jsonBodyAssertions ?? input.json_assertions ?? input.jsonAssertions ?? input.response_json_assertions ?? input.responseJsonAssertions,
|
|
8121
|
+
`checks[${index}] body_json_assertions`
|
|
8122
|
+
) : void 0;
|
|
7911
8123
|
if (isLinkStatusCheck) {
|
|
7912
8124
|
if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
|
|
7913
8125
|
throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
|
|
@@ -7933,6 +8145,7 @@ function normalizeCheck(input, index) {
|
|
|
7933
8145
|
body_contains: bodyContains,
|
|
7934
8146
|
body_not_contains: bodyNotContains,
|
|
7935
8147
|
body_not_patterns: bodyNotPatterns,
|
|
8148
|
+
body_json_assertions: bodyJsonAssertions,
|
|
7936
8149
|
expected_texts: expectedTexts,
|
|
7937
8150
|
link_selector: stringValue2(input.link_selector) || stringValue2(input.linkSelector),
|
|
7938
8151
|
source_selector: stringValue2(input.source_selector) || stringValue2(input.sourceSelector),
|
|
@@ -8139,6 +8352,34 @@ function httpStatusBodyNotPatternFailures(result, check) {
|
|
|
8139
8352
|
const observed = isRecord(result.body_not_patterns) ? result.body_not_patterns : {};
|
|
8140
8353
|
return forbidden.filter((pattern) => observed[pattern] !== false);
|
|
8141
8354
|
}
|
|
8355
|
+
function httpStatusBodyJsonAssertionFailures(result, check) {
|
|
8356
|
+
const expected = check.body_json_assertions?.filter((assertion) => assertion.path) ?? [];
|
|
8357
|
+
if (!expected.length) return [];
|
|
8358
|
+
if (!Array.isArray(result.body_json_assertions)) {
|
|
8359
|
+
return expected.map((assertion) => ({
|
|
8360
|
+
label: assertion.label || assertion.path,
|
|
8361
|
+
path: assertion.path,
|
|
8362
|
+
ok: false,
|
|
8363
|
+
exists: false,
|
|
8364
|
+
observed_type: "missing",
|
|
8365
|
+
errors: ["body_json_assertions evidence missing"]
|
|
8366
|
+
}));
|
|
8367
|
+
}
|
|
8368
|
+
return result.body_json_assertions.filter((assertion) => isRecord(assertion) && assertion.ok !== true).map((assertion) => ({
|
|
8369
|
+
label: stringValue2(assertion.label) || stringValue2(assertion.path) || "json assertion",
|
|
8370
|
+
path: stringValue2(assertion.path) || "",
|
|
8371
|
+
ok: false,
|
|
8372
|
+
exists: assertion.exists === true,
|
|
8373
|
+
observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
|
|
8374
|
+
observed_type: stringValue2(assertion.observed_type) || "missing",
|
|
8375
|
+
expected_exists: booleanValue(assertion.expected_exists),
|
|
8376
|
+
equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
|
|
8377
|
+
not_equals: hasOwn(assertion, "not_equals") ? toJsonValue(assertion.not_equals) : void 0,
|
|
8378
|
+
contains: hasOwn(assertion, "contains") ? toJsonValue(assertion.contains) : void 0,
|
|
8379
|
+
type: stringValue2(assertion.type),
|
|
8380
|
+
errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : void 0
|
|
8381
|
+
}));
|
|
8382
|
+
}
|
|
8142
8383
|
function linkStatusResultOk(result, check) {
|
|
8143
8384
|
const status = numberValue(result.status);
|
|
8144
8385
|
if (!httpStatusIsAllowed(status, check)) return false;
|
|
@@ -8156,6 +8397,7 @@ function linkStatusResultOk(result, check) {
|
|
|
8156
8397
|
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
8157
8398
|
if (httpStatusBodyNotContainsFailures(result, check).length) return false;
|
|
8158
8399
|
if (httpStatusBodyNotPatternFailures(result, check).length) return false;
|
|
8400
|
+
if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
|
|
8159
8401
|
return true;
|
|
8160
8402
|
}
|
|
8161
8403
|
function responseHeader(response, name) {
|
|
@@ -8224,7 +8466,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
8224
8466
|
statusText = typeof response.statusText === "string" ? response.statusText : "";
|
|
8225
8467
|
result.content_type = responseHeader(response, "content-type");
|
|
8226
8468
|
result.content_length = responseContentLength(response);
|
|
8227
|
-
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);
|
|
8469
|
+
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);
|
|
8228
8470
|
if (shouldReadBody && method !== "HEAD") {
|
|
8229
8471
|
const body = await responseBodyText(response);
|
|
8230
8472
|
result.bytes = body.bytes;
|
|
@@ -8237,6 +8479,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
8237
8479
|
if (check.body_not_patterns?.length) {
|
|
8238
8480
|
result.body_not_patterns = Object.fromEntries(check.body_not_patterns.filter(Boolean).map((pattern) => [pattern, new RegExp(pattern).test(body.text)]));
|
|
8239
8481
|
}
|
|
8482
|
+
if (check.body_json_assertions?.length) {
|
|
8483
|
+
result.body_json_assertions = evaluateHttpStatusBodyJsonAssertions(body.text, check.body_json_assertions);
|
|
8484
|
+
}
|
|
8240
8485
|
}
|
|
8241
8486
|
} catch (caught) {
|
|
8242
8487
|
error = String(caught instanceof Error ? caught.message : caught).slice(0, 500);
|
|
@@ -8245,6 +8490,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
8245
8490
|
const bodyContainsMissing = httpStatusBodyContainsFailures(result, check);
|
|
8246
8491
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(result, check);
|
|
8247
8492
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(result, check);
|
|
8493
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(result, check);
|
|
8248
8494
|
const ok = !error && linkStatusResultOk(result, check);
|
|
8249
8495
|
return {
|
|
8250
8496
|
index,
|
|
@@ -8263,7 +8509,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
8263
8509
|
body_not_contains: isRecord(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
|
|
8264
8510
|
body_not_contains_found: bodyNotContainsFound,
|
|
8265
8511
|
body_not_patterns: isRecord(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
|
|
8266
|
-
body_not_patterns_found: bodyNotPatternsFound
|
|
8512
|
+
body_not_patterns_found: bodyNotPatternsFound,
|
|
8513
|
+
body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
|
|
8514
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed
|
|
8267
8515
|
};
|
|
8268
8516
|
}
|
|
8269
8517
|
async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}) {
|
|
@@ -8308,6 +8556,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
8308
8556
|
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
8309
8557
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
|
|
8310
8558
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
|
|
8559
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
|
|
8311
8560
|
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
8312
8561
|
failures.push({
|
|
8313
8562
|
code: "http_status_failed",
|
|
@@ -8326,6 +8575,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
8326
8575
|
body_not_contains_found: bodyNotContainsFound,
|
|
8327
8576
|
body_not_patterns: check.body_not_patterns ?? null,
|
|
8328
8577
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
8578
|
+
body_json_assertions: check.body_json_assertions ?? null,
|
|
8579
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
|
|
8329
8580
|
body_sample: stringValue2(statusEvidence.body_sample) ?? null
|
|
8330
8581
|
});
|
|
8331
8582
|
}
|
|
@@ -8347,6 +8598,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
8347
8598
|
body_not_contains_found: bodyNotContainsFound,
|
|
8348
8599
|
body_not_patterns: isRecord(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
|
|
8349
8600
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
8601
|
+
body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
|
|
8602
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
|
|
8350
8603
|
body_sample: stringValue2(statusEvidence.body_sample) ?? null,
|
|
8351
8604
|
failures
|
|
8352
8605
|
};
|
|
@@ -8983,6 +9236,7 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
8983
9236
|
body_contains: check.body_contains ?? [],
|
|
8984
9237
|
body_not_contains: check.body_not_contains ?? [],
|
|
8985
9238
|
body_not_patterns: check.body_not_patterns ?? [],
|
|
9239
|
+
body_json_assertions: toJsonValue(check.body_json_assertions ?? []),
|
|
8986
9240
|
viewports: summaries.map((summary) => toJsonValue(summary)),
|
|
8987
9241
|
failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue2(summary.viewport) ?? null, failure })) : [])
|
|
8988
9242
|
},
|
|
@@ -9710,6 +9964,36 @@ function httpStatusBodyNotPatternFailures(result, check) {
|
|
|
9710
9964
|
: {};
|
|
9711
9965
|
return forbidden.filter((pattern) => observed[pattern] !== false);
|
|
9712
9966
|
}
|
|
9967
|
+
function httpStatusBodyJsonAssertionFailures(result, check) {
|
|
9968
|
+
const expected = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
9969
|
+
if (!expected.length) return [];
|
|
9970
|
+
if (!Array.isArray(result.body_json_assertions)) {
|
|
9971
|
+
return expected.map((assertion) => ({
|
|
9972
|
+
label: assertion.label || assertion.path,
|
|
9973
|
+
path: assertion.path,
|
|
9974
|
+
ok: false,
|
|
9975
|
+
exists: false,
|
|
9976
|
+
observed_type: "missing",
|
|
9977
|
+
errors: ["body_json_assertions evidence missing"],
|
|
9978
|
+
}));
|
|
9979
|
+
}
|
|
9980
|
+
return result.body_json_assertions
|
|
9981
|
+
.filter((assertion) => assertion && typeof assertion === "object" && assertion.ok !== true)
|
|
9982
|
+
.map((assertion) => ({
|
|
9983
|
+
label: typeof assertion.label === "string" && assertion.label ? assertion.label : typeof assertion.path === "string" && assertion.path ? assertion.path : "json assertion",
|
|
9984
|
+
path: typeof assertion.path === "string" ? assertion.path : "",
|
|
9985
|
+
ok: false,
|
|
9986
|
+
exists: assertion.exists === true,
|
|
9987
|
+
observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
|
|
9988
|
+
observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
|
|
9989
|
+
expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
|
|
9990
|
+
equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
|
|
9991
|
+
not_equals: Object.hasOwn(assertion, "not_equals") ? assertion.not_equals : undefined,
|
|
9992
|
+
contains: Object.hasOwn(assertion, "contains") ? assertion.contains : undefined,
|
|
9993
|
+
type: typeof assertion.type === "string" ? assertion.type : undefined,
|
|
9994
|
+
errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : undefined,
|
|
9995
|
+
}));
|
|
9996
|
+
}
|
|
9713
9997
|
function linkStatusResultOk(result, check) {
|
|
9714
9998
|
if (!result || typeof result !== "object" || Array.isArray(result)) return false;
|
|
9715
9999
|
if (!httpStatusIsAllowed(result.status, check)) return false;
|
|
@@ -9727,6 +10011,7 @@ function linkStatusResultOk(result, check) {
|
|
|
9727
10011
|
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
9728
10012
|
if (httpStatusBodyNotContainsFailures(result, check).length) return false;
|
|
9729
10013
|
if (httpStatusBodyNotPatternFailures(result, check).length) return false;
|
|
10014
|
+
if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
|
|
9730
10015
|
return true;
|
|
9731
10016
|
}
|
|
9732
10017
|
function summarizeHttpStatusEvidence(viewport, check) {
|
|
@@ -9747,6 +10032,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
9747
10032
|
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
9748
10033
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
|
|
9749
10034
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
|
|
10035
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
|
|
9750
10036
|
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
9751
10037
|
failures.push({
|
|
9752
10038
|
code: "http_status_failed",
|
|
@@ -9765,6 +10051,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
9765
10051
|
body_not_contains_found: bodyNotContainsFound,
|
|
9766
10052
|
body_not_patterns: Array.isArray(check.body_not_patterns) ? check.body_not_patterns : null,
|
|
9767
10053
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
10054
|
+
body_json_assertions: Array.isArray(check.body_json_assertions) ? check.body_json_assertions : null,
|
|
10055
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed,
|
|
9768
10056
|
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
9769
10057
|
});
|
|
9770
10058
|
}
|
|
@@ -9792,6 +10080,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
9792
10080
|
? statusEvidence.body_not_patterns
|
|
9793
10081
|
: null,
|
|
9794
10082
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
10083
|
+
body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? statusEvidence.body_json_assertions : null,
|
|
10084
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed,
|
|
9795
10085
|
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
9796
10086
|
failures,
|
|
9797
10087
|
};
|
|
@@ -11175,6 +11465,7 @@ async function setupLocatorVisible(locator, index) {
|
|
|
11175
11465
|
async function registerNetworkMocks(mocks) {
|
|
11176
11466
|
for (const mock of mocks || []) {
|
|
11177
11467
|
let hitCount = 0;
|
|
11468
|
+
const scopedHitCounts = {};
|
|
11178
11469
|
await page.route(mock.url, async (route) => {
|
|
11179
11470
|
const request = route.request();
|
|
11180
11471
|
const method = request.method ? request.method() : "";
|
|
@@ -11190,8 +11481,13 @@ async function registerNetworkMocks(mocks) {
|
|
|
11190
11481
|
const responses = Array.isArray(mock.responses) ? mock.responses : [];
|
|
11191
11482
|
const hitIndex = hitCount;
|
|
11192
11483
|
hitCount += 1;
|
|
11484
|
+
const sequenceScope = mock.sequence_scope === "viewport" ? "viewport" : "global";
|
|
11485
|
+
const viewportName = activeViewportName || null;
|
|
11486
|
+
const sequenceScopeKey = sequenceScope === "viewport" ? (viewportName || "__unknown_viewport__") : "__global__";
|
|
11487
|
+
const sequenceHitIndex = sequenceScope === "viewport" ? (scopedHitCounts[sequenceScopeKey] || 0) : hitIndex;
|
|
11488
|
+
if (sequenceScope === "viewport") scopedHitCounts[sequenceScopeKey] = sequenceHitIndex + 1;
|
|
11193
11489
|
const sequenceResponseIndex = responses.length
|
|
11194
|
-
? (mock.repeat_responses ?
|
|
11490
|
+
? (mock.repeat_responses ? sequenceHitIndex % responses.length : Math.min(sequenceHitIndex, responses.length - 1))
|
|
11195
11491
|
: null;
|
|
11196
11492
|
let responseIndex = sequenceResponseIndex;
|
|
11197
11493
|
let responseSelection = responseIndex === null ? "mock" : "sequence";
|
|
@@ -11226,11 +11522,14 @@ async function registerNetworkMocks(mocks) {
|
|
|
11226
11522
|
label: mock.label,
|
|
11227
11523
|
response_label: response.label || null,
|
|
11228
11524
|
hit_index: hitIndex,
|
|
11525
|
+
sequence_hit_index: responseIndex === null ? undefined : sequenceHitIndex,
|
|
11526
|
+
sequence_scope: responseIndex === null ? undefined : sequenceScope,
|
|
11527
|
+
viewport: viewportName,
|
|
11229
11528
|
response_index: responseIndex,
|
|
11230
11529
|
sequence_response_index: responseSelection === "request_body" ? sequenceResponseIndex : undefined,
|
|
11231
11530
|
response_selection: responseIndex === null ? null : responseSelection,
|
|
11232
|
-
sequence_reused: responseSelection === "sequence" && responseIndex !== null && !mock.repeat_responses &&
|
|
11233
|
-
sequence_cycle: responseSelection === "sequence" && responseIndex !== null && mock.repeat_responses === true &&
|
|
11531
|
+
sequence_reused: responseSelection === "sequence" && responseIndex !== null && !mock.repeat_responses && sequenceHitIndex >= responses.length,
|
|
11532
|
+
sequence_cycle: responseSelection === "sequence" && responseIndex !== null && mock.repeat_responses === true && sequenceHitIndex >= responses.length,
|
|
11234
11533
|
url: request.url(),
|
|
11235
11534
|
method,
|
|
11236
11535
|
};
|
|
@@ -11272,6 +11571,7 @@ async function registerNetworkMocks(mocks) {
|
|
|
11272
11571
|
});
|
|
11273
11572
|
}
|
|
11274
11573
|
}
|
|
11574
|
+
let activeViewportName = null;
|
|
11275
11575
|
async function executeSetupAction(action, ordinal, viewport) {
|
|
11276
11576
|
const type = setupActionType(action);
|
|
11277
11577
|
const frameSelector = setupFrameSelector(action);
|
|
@@ -11884,6 +12184,155 @@ function linkProbeResponseFields(response, method) {
|
|
|
11884
12184
|
content_length: contentLength,
|
|
11885
12185
|
};
|
|
11886
12186
|
}
|
|
12187
|
+
function jsonProbeValueType(value) {
|
|
12188
|
+
if (value === null) return "null";
|
|
12189
|
+
if (Array.isArray(value)) return "array";
|
|
12190
|
+
if (typeof value === "boolean") return "boolean";
|
|
12191
|
+
if (typeof value === "number") return "number";
|
|
12192
|
+
if (typeof value === "string") return "string";
|
|
12193
|
+
return "object";
|
|
12194
|
+
}
|
|
12195
|
+
function jsonProbeDeepEqual(left, right) {
|
|
12196
|
+
if (left === right) return true;
|
|
12197
|
+
if (typeof left !== typeof right) return false;
|
|
12198
|
+
if (left === null || right === null) return left === right;
|
|
12199
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
12200
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
12201
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
12202
|
+
return left.every((item, index) => jsonProbeDeepEqual(item, right[index]));
|
|
12203
|
+
}
|
|
12204
|
+
const leftKeys = Object.keys(left).sort();
|
|
12205
|
+
const rightKeys = Object.keys(right).sort();
|
|
12206
|
+
if (!jsonProbeDeepEqual(leftKeys, rightKeys)) return false;
|
|
12207
|
+
return leftKeys.every((key) => jsonProbeDeepEqual(left[key], right[key]));
|
|
12208
|
+
}
|
|
12209
|
+
function jsonProbeContains(observed, expected) {
|
|
12210
|
+
if (typeof observed === "string" && typeof expected === "string") return observed.includes(expected);
|
|
12211
|
+
if (Array.isArray(observed)) return observed.some((item) => jsonProbeDeepEqual(item, expected));
|
|
12212
|
+
if (observed && expected && typeof observed === "object" && typeof expected === "object" && !Array.isArray(observed) && !Array.isArray(expected)) {
|
|
12213
|
+
return Object.entries(expected).every(([key, value]) => Object.hasOwn(observed, key) && jsonProbeDeepEqual(observed[key], value));
|
|
12214
|
+
}
|
|
12215
|
+
return false;
|
|
12216
|
+
}
|
|
12217
|
+
function parseJsonProbePathSegments(path) {
|
|
12218
|
+
let input = String(path || "").trim();
|
|
12219
|
+
if (!input) throw new Error("path is empty");
|
|
12220
|
+
if (input === "$") return [];
|
|
12221
|
+
if (input.startsWith("$.")) input = input.slice(2);
|
|
12222
|
+
else if (input.startsWith("$[")) input = input.slice(1);
|
|
12223
|
+
const segments = [];
|
|
12224
|
+
let token = "";
|
|
12225
|
+
const pushToken = () => {
|
|
12226
|
+
if (!token) return;
|
|
12227
|
+
segments.push(token);
|
|
12228
|
+
token = "";
|
|
12229
|
+
};
|
|
12230
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
12231
|
+
const char = input[index];
|
|
12232
|
+
if (char === ".") {
|
|
12233
|
+
pushToken();
|
|
12234
|
+
continue;
|
|
12235
|
+
}
|
|
12236
|
+
if (char !== "[") {
|
|
12237
|
+
token += char;
|
|
12238
|
+
continue;
|
|
12239
|
+
}
|
|
12240
|
+
pushToken();
|
|
12241
|
+
const closeIndex = input.indexOf("]", index + 1);
|
|
12242
|
+
if (closeIndex === -1) throw new Error("unterminated bracket at " + index);
|
|
12243
|
+
const bracket = input.slice(index + 1, closeIndex).trim();
|
|
12244
|
+
if (!bracket) throw new Error("empty bracket at " + index);
|
|
12245
|
+
if (/^\d+$/.test(bracket)) {
|
|
12246
|
+
segments.push(Number(bracket));
|
|
12247
|
+
} else if ((bracket.startsWith('"') && bracket.endsWith('"')) || (bracket.startsWith("'") && bracket.endsWith("'"))) {
|
|
12248
|
+
const quoted = bracket.startsWith("'")
|
|
12249
|
+
? '"' + bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'
|
|
12250
|
+
: bracket;
|
|
12251
|
+
segments.push(String(JSON.parse(quoted)));
|
|
12252
|
+
} else {
|
|
12253
|
+
segments.push(bracket);
|
|
12254
|
+
}
|
|
12255
|
+
index = closeIndex;
|
|
12256
|
+
}
|
|
12257
|
+
pushToken();
|
|
12258
|
+
return segments;
|
|
12259
|
+
}
|
|
12260
|
+
function resolveJsonProbePath(root, path) {
|
|
12261
|
+
let segments;
|
|
12262
|
+
try {
|
|
12263
|
+
segments = parseJsonProbePathSegments(path);
|
|
12264
|
+
} catch (error) {
|
|
12265
|
+
return { exists: false, error: String(error && error.message ? error.message : error) };
|
|
12266
|
+
}
|
|
12267
|
+
let current = root;
|
|
12268
|
+
for (const segment of segments) {
|
|
12269
|
+
if (typeof segment === "number") {
|
|
12270
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
|
|
12271
|
+
current = current[segment];
|
|
12272
|
+
continue;
|
|
12273
|
+
}
|
|
12274
|
+
if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, segment)) {
|
|
12275
|
+
return { exists: false };
|
|
12276
|
+
}
|
|
12277
|
+
current = current[segment];
|
|
12278
|
+
}
|
|
12279
|
+
return { exists: true, value: current };
|
|
12280
|
+
}
|
|
12281
|
+
function evaluateJsonProbeAssertion(root, assertion) {
|
|
12282
|
+
const resolved = resolveJsonProbePath(root, assertion.path);
|
|
12283
|
+
const errors = [];
|
|
12284
|
+
const result = {
|
|
12285
|
+
label: assertion.label || assertion.path,
|
|
12286
|
+
path: assertion.path,
|
|
12287
|
+
ok: true,
|
|
12288
|
+
exists: resolved.exists,
|
|
12289
|
+
observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
|
|
12290
|
+
};
|
|
12291
|
+
if (resolved.exists) result.observed = resolved.value;
|
|
12292
|
+
if (resolved.error) errors.push(resolved.error);
|
|
12293
|
+
if (Object.hasOwn(assertion, "exists")) {
|
|
12294
|
+
result.expected_exists = assertion.exists;
|
|
12295
|
+
if (resolved.exists !== assertion.exists) errors.push("expected exists=" + assertion.exists);
|
|
12296
|
+
}
|
|
12297
|
+
if (Object.hasOwn(assertion, "type")) {
|
|
12298
|
+
result.type = assertion.type;
|
|
12299
|
+
if (!resolved.exists || jsonProbeValueType(resolved.value) !== assertion.type) errors.push("expected type " + assertion.type);
|
|
12300
|
+
}
|
|
12301
|
+
if (Object.hasOwn(assertion, "equals")) {
|
|
12302
|
+
result.equals = assertion.equals;
|
|
12303
|
+
if (!resolved.exists || !jsonProbeDeepEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
|
|
12304
|
+
}
|
|
12305
|
+
if (Object.hasOwn(assertion, "not_equals")) {
|
|
12306
|
+
result.not_equals = assertion.not_equals;
|
|
12307
|
+
if (resolved.exists && jsonProbeDeepEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
|
|
12308
|
+
}
|
|
12309
|
+
if (Object.hasOwn(assertion, "contains")) {
|
|
12310
|
+
result.contains = assertion.contains;
|
|
12311
|
+
if (!resolved.exists || !jsonProbeContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
|
|
12312
|
+
}
|
|
12313
|
+
result.ok = errors.length === 0;
|
|
12314
|
+
if (errors.length) result.errors = errors;
|
|
12315
|
+
return result;
|
|
12316
|
+
}
|
|
12317
|
+
function evaluateJsonProbeAssertions(text, assertions) {
|
|
12318
|
+
const expected = Array.isArray(assertions) ? assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
12319
|
+
if (!expected.length) return [];
|
|
12320
|
+
let parsed;
|
|
12321
|
+
try {
|
|
12322
|
+
parsed = JSON.parse(text);
|
|
12323
|
+
} catch (error) {
|
|
12324
|
+
const message = "response body is not valid JSON: " + String(error && error.message ? error.message : error).slice(0, 200);
|
|
12325
|
+
return expected.map((assertion) => ({
|
|
12326
|
+
label: assertion.label || assertion.path,
|
|
12327
|
+
path: assertion.path,
|
|
12328
|
+
ok: false,
|
|
12329
|
+
exists: false,
|
|
12330
|
+
observed_type: "missing",
|
|
12331
|
+
errors: [message],
|
|
12332
|
+
}));
|
|
12333
|
+
}
|
|
12334
|
+
return expected.map((assertion) => evaluateJsonProbeAssertion(parsed, assertion));
|
|
12335
|
+
}
|
|
11887
12336
|
async function collectHttpStatus(check) {
|
|
11888
12337
|
const url = httpStatusRequestUrl(check, page.url() || targetUrl);
|
|
11889
12338
|
const method = httpStatusMethod(check);
|
|
@@ -11900,6 +12349,7 @@ async function collectHttpStatus(check) {
|
|
|
11900
12349
|
const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
|
|
11901
12350
|
const bodyNotContains = Array.isArray(check.body_not_contains) ? check.body_not_contains.filter(Boolean) : [];
|
|
11902
12351
|
const bodyNotPatterns = Array.isArray(check.body_not_patterns) ? check.body_not_patterns.filter(Boolean) : [];
|
|
12352
|
+
const bodyJsonAssertions = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
11903
12353
|
const options = {
|
|
11904
12354
|
method,
|
|
11905
12355
|
redirect: "follow",
|
|
@@ -11925,17 +12375,18 @@ async function collectHttpStatus(check) {
|
|
|
11925
12375
|
Object.assign(result, linkProbeResponseFields(response, method));
|
|
11926
12376
|
result.url = url;
|
|
11927
12377
|
result.status_text = response.statusText || "";
|
|
11928
|
-
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;
|
|
12378
|
+
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;
|
|
11929
12379
|
if (shouldReadBody) {
|
|
11930
12380
|
try {
|
|
11931
12381
|
const buffer = await response.arrayBuffer();
|
|
11932
12382
|
result.bytes = buffer.byteLength;
|
|
11933
|
-
if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length) {
|
|
12383
|
+
if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length || bodyJsonAssertions.length) {
|
|
11934
12384
|
const text = new TextDecoder().decode(buffer);
|
|
11935
12385
|
result.body_sample = text.slice(0, 1000);
|
|
11936
12386
|
if (bodyContains.length) result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
|
|
11937
12387
|
if (bodyNotContains.length) result.body_not_contains = Object.fromEntries(bodyNotContains.map((forbidden) => [forbidden, text.includes(forbidden)]));
|
|
11938
12388
|
if (bodyNotPatterns.length) result.body_not_patterns = Object.fromEntries(bodyNotPatterns.map((pattern) => [pattern, new RegExp(pattern).test(text)]));
|
|
12389
|
+
if (bodyJsonAssertions.length) result.body_json_assertions = evaluateJsonProbeAssertions(text, bodyJsonAssertions);
|
|
11939
12390
|
}
|
|
11940
12391
|
} catch (error) {
|
|
11941
12392
|
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
@@ -11948,6 +12399,7 @@ async function collectHttpStatus(check) {
|
|
|
11948
12399
|
&& (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
|
|
11949
12400
|
&& (!bodyNotContains.length || bodyNotContains.every((forbidden) => result.body_not_contains && result.body_not_contains[forbidden] === false))
|
|
11950
12401
|
&& (!bodyNotPatterns.length || bodyNotPatterns.every((pattern) => result.body_not_patterns && result.body_not_patterns[pattern] === false))
|
|
12402
|
+
&& (!bodyJsonAssertions.length || (Array.isArray(result.body_json_assertions) && result.body_json_assertions.every((assertion) => assertion.ok === true)))
|
|
11951
12403
|
&& !result.error;
|
|
11952
12404
|
return result;
|
|
11953
12405
|
} catch (error) {
|
|
@@ -12534,6 +12986,7 @@ async function collectRouteInventory(check, viewport) {
|
|
|
12534
12986
|
};
|
|
12535
12987
|
}
|
|
12536
12988
|
async function captureViewport(viewport) {
|
|
12989
|
+
activeViewportName = viewport && viewport.name ? viewport.name : null;
|
|
12537
12990
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
12538
12991
|
let httpStatus = null;
|
|
12539
12992
|
let navigationError;
|
|
@@ -13659,6 +14112,21 @@ function profileHttpStatusAssertionKeys(evidence, viewports, field) {
|
|
|
13659
14112
|
}
|
|
13660
14113
|
return [...keys];
|
|
13661
14114
|
}
|
|
14115
|
+
function profileHttpStatusJsonAssertionCount(viewports) {
|
|
14116
|
+
if (!viewports.length) return void 0;
|
|
14117
|
+
let passed = 0;
|
|
14118
|
+
let total = 0;
|
|
14119
|
+
for (const viewport of viewports) {
|
|
14120
|
+
if (!Array.isArray(viewport.body_json_assertions)) continue;
|
|
14121
|
+
for (const assertion of viewport.body_json_assertions) {
|
|
14122
|
+
const record = cliRecord(assertion);
|
|
14123
|
+
if (!record) continue;
|
|
14124
|
+
total += 1;
|
|
14125
|
+
if (record.ok === true) passed += 1;
|
|
14126
|
+
}
|
|
14127
|
+
}
|
|
14128
|
+
return total ? { passed, total } : void 0;
|
|
14129
|
+
}
|
|
13662
14130
|
function profileHttpStatusSummaryMarkdown(result) {
|
|
13663
14131
|
const httpStatusChecks = result.checks.filter((check) => check.type === "http_status");
|
|
13664
14132
|
const lines = [];
|
|
@@ -13689,10 +14157,12 @@ function profileHttpStatusSummaryMarkdown(result) {
|
|
|
13689
14157
|
profileHttpStatusAssertionKeys(evidence, viewports, "body_not_patterns"),
|
|
13690
14158
|
false
|
|
13691
14159
|
);
|
|
14160
|
+
const bodyJsonAssertions = profileHttpStatusJsonAssertionCount(viewports);
|
|
13692
14161
|
const bodyParts = [
|
|
13693
14162
|
bodyContains ? `body_contains ${bodyContains.passed}/${bodyContains.total}` : "",
|
|
13694
14163
|
bodyNotContains ? `body_not_contains clean ${bodyNotContains.passed}/${bodyNotContains.total}` : "",
|
|
13695
|
-
bodyNotPatterns ? `body_not_patterns clean ${bodyNotPatterns.passed}/${bodyNotPatterns.total}` : ""
|
|
14164
|
+
bodyNotPatterns ? `body_not_patterns clean ${bodyNotPatterns.passed}/${bodyNotPatterns.total}` : "",
|
|
14165
|
+
bodyJsonAssertions ? `body_json_assertions ${bodyJsonAssertions.passed}/${bodyJsonAssertions.total}` : ""
|
|
13696
14166
|
].filter(Boolean);
|
|
13697
14167
|
lines.push(
|
|
13698
14168
|
`- ${label}: ${method}${url ? ` ${markdownInlineCode(url)}` : ""}, statuses ${statuses.length ? statuses.join("/") : "unknown"}${bodyParts.length ? `, ${bodyParts.join(", ")}` : ""}, failures ${failedTotal}`
|