@riddledc/riddle-proof 0.7.126 → 0.7.128
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 +26 -2
- package/dist/{chunk-55KDZEB3.js → chunk-TO3FVF5S.js} +502 -4
- package/dist/cli.cjs +520 -5
- package/dist/cli.js +19 -2
- package/dist/index.cjs +502 -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 +502 -4
- package/dist/profile.d.cts +32 -1
- package/dist/profile.d.ts +32 -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,187 @@ 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 compactJsonAssertionSample(value, depth = 0) {
|
|
7185
|
+
if (typeof value === "string") return value.length > 240 ? `${value.slice(0, 237)}...` : value;
|
|
7186
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") return toJsonValue(value);
|
|
7187
|
+
if (Array.isArray(value)) {
|
|
7188
|
+
if (depth >= 2) return `[array:${value.length}]`;
|
|
7189
|
+
return value.slice(0, 3).map((item) => compactJsonAssertionSample(item, depth + 1));
|
|
7190
|
+
}
|
|
7191
|
+
if (isRecord(value)) {
|
|
7192
|
+
const entries = Object.entries(value).slice(0, 8);
|
|
7193
|
+
if (depth >= 2) return `[object:${Object.keys(value).length} keys]`;
|
|
7194
|
+
return Object.fromEntries(entries.map(([key, child]) => [key, compactJsonAssertionSample(child, depth + 1)]));
|
|
7195
|
+
}
|
|
7196
|
+
return String(value);
|
|
7197
|
+
}
|
|
7198
|
+
function attachJsonAssertionObservedValue(result, value) {
|
|
7199
|
+
const type = jsonValueType(value);
|
|
7200
|
+
if (type === "array" && Array.isArray(value)) {
|
|
7201
|
+
result.observed_length = value.length;
|
|
7202
|
+
result.observed_omitted_count = Math.max(0, value.length - 3);
|
|
7203
|
+
result.observed_sample = compactJsonAssertionSample(value);
|
|
7204
|
+
return;
|
|
7205
|
+
}
|
|
7206
|
+
if (type === "object" && isRecord(value)) {
|
|
7207
|
+
const keyCount = Object.keys(value).length;
|
|
7208
|
+
result.observed_key_count = keyCount;
|
|
7209
|
+
result.observed_omitted_count = Math.max(0, keyCount - 8);
|
|
7210
|
+
result.observed_sample = compactJsonAssertionSample(value);
|
|
7211
|
+
return;
|
|
7212
|
+
}
|
|
7213
|
+
result.observed = toJsonValue(value);
|
|
7214
|
+
}
|
|
7215
|
+
function deepJsonEqual(left, right) {
|
|
7216
|
+
if (left === right) return true;
|
|
7217
|
+
if (typeof left !== typeof right) return false;
|
|
7218
|
+
if (left === null || right === null) return left === right;
|
|
7219
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
7220
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
7221
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
7222
|
+
return left.every((item, index) => deepJsonEqual(item, right[index]));
|
|
7223
|
+
}
|
|
7224
|
+
if (!isRecord(left) || !isRecord(right)) return false;
|
|
7225
|
+
const leftKeys = Object.keys(left).sort();
|
|
7226
|
+
const rightKeys = Object.keys(right).sort();
|
|
7227
|
+
if (!deepJsonEqual(leftKeys, rightKeys)) return false;
|
|
7228
|
+
return leftKeys.every((key) => deepJsonEqual(left[key], right[key]));
|
|
7229
|
+
}
|
|
7230
|
+
function jsonContains(observed, expected) {
|
|
7231
|
+
if (typeof observed === "string" && typeof expected === "string") {
|
|
7232
|
+
return observed.includes(expected);
|
|
7233
|
+
}
|
|
7234
|
+
if (Array.isArray(observed)) {
|
|
7235
|
+
return observed.some((item) => deepJsonEqual(item, expected));
|
|
7236
|
+
}
|
|
7237
|
+
if (isRecord(observed) && isRecord(expected)) {
|
|
7238
|
+
return Object.entries(expected).every(([key, value]) => hasOwn(observed, key) && deepJsonEqual(observed[key], value));
|
|
7239
|
+
}
|
|
7240
|
+
return false;
|
|
7241
|
+
}
|
|
7242
|
+
function parseJsonPathSegments(path7) {
|
|
7243
|
+
let input = path7.trim();
|
|
7244
|
+
if (!input) throw new Error("path is empty");
|
|
7245
|
+
if (input === "$") return [];
|
|
7246
|
+
if (input.startsWith("$.")) input = input.slice(2);
|
|
7247
|
+
else if (input.startsWith("$[")) input = input.slice(1);
|
|
7248
|
+
const segments = [];
|
|
7249
|
+
let token = "";
|
|
7250
|
+
const pushToken = () => {
|
|
7251
|
+
if (!token) return;
|
|
7252
|
+
segments.push(token);
|
|
7253
|
+
token = "";
|
|
7254
|
+
};
|
|
7255
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
7256
|
+
const char = input[index];
|
|
7257
|
+
if (char === ".") {
|
|
7258
|
+
pushToken();
|
|
7259
|
+
continue;
|
|
7260
|
+
}
|
|
7261
|
+
if (char !== "[") {
|
|
7262
|
+
token += char;
|
|
7263
|
+
continue;
|
|
7264
|
+
}
|
|
7265
|
+
pushToken();
|
|
7266
|
+
const closeIndex = input.indexOf("]", index + 1);
|
|
7267
|
+
if (closeIndex === -1) throw new Error(`unterminated bracket at ${index}`);
|
|
7268
|
+
const bracket = input.slice(index + 1, closeIndex).trim();
|
|
7269
|
+
if (!bracket) throw new Error(`empty bracket at ${index}`);
|
|
7270
|
+
if (/^\d+$/.test(bracket)) {
|
|
7271
|
+
segments.push(Number(bracket));
|
|
7272
|
+
} else if (bracket.startsWith('"') && bracket.endsWith('"') || bracket.startsWith("'") && bracket.endsWith("'")) {
|
|
7273
|
+
const quoted = bracket.startsWith("'") ? `"${bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : bracket;
|
|
7274
|
+
segments.push(String(JSON.parse(quoted)));
|
|
7275
|
+
} else {
|
|
7276
|
+
segments.push(bracket);
|
|
7277
|
+
}
|
|
7278
|
+
index = closeIndex;
|
|
7279
|
+
}
|
|
7280
|
+
pushToken();
|
|
7281
|
+
return segments;
|
|
7282
|
+
}
|
|
7283
|
+
function resolveJsonPath(root, path7) {
|
|
7284
|
+
let segments;
|
|
7285
|
+
try {
|
|
7286
|
+
segments = parseJsonPathSegments(path7);
|
|
7287
|
+
} catch (error) {
|
|
7288
|
+
return { exists: false, error: String(error instanceof Error ? error.message : error) };
|
|
7289
|
+
}
|
|
7290
|
+
let current = root;
|
|
7291
|
+
for (const segment of segments) {
|
|
7292
|
+
if (typeof segment === "number") {
|
|
7293
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
|
|
7294
|
+
current = current[segment];
|
|
7295
|
+
continue;
|
|
7296
|
+
}
|
|
7297
|
+
if (!isRecord(current) || !hasOwn(current, segment)) return { exists: false };
|
|
7298
|
+
current = current[segment];
|
|
7299
|
+
}
|
|
7300
|
+
return { exists: true, value: current };
|
|
7301
|
+
}
|
|
7302
|
+
function evaluateHttpStatusBodyJsonAssertion(root, assertion) {
|
|
7303
|
+
const resolved = resolveJsonPath(root, assertion.path);
|
|
7304
|
+
const errors = [];
|
|
7305
|
+
const result = {
|
|
7306
|
+
label: assertion.label || assertion.path,
|
|
7307
|
+
path: assertion.path,
|
|
7308
|
+
ok: true,
|
|
7309
|
+
exists: resolved.exists,
|
|
7310
|
+
observed_type: resolved.exists ? jsonValueType(resolved.value) : "missing"
|
|
7311
|
+
};
|
|
7312
|
+
if (resolved.exists) attachJsonAssertionObservedValue(result, resolved.value);
|
|
7313
|
+
if (resolved.error) errors.push(resolved.error);
|
|
7314
|
+
if (hasOwn(assertion, "exists")) {
|
|
7315
|
+
result.expected_exists = assertion.exists;
|
|
7316
|
+
if (resolved.exists !== assertion.exists) errors.push(`expected exists=${assertion.exists}`);
|
|
7317
|
+
}
|
|
7318
|
+
if (hasOwn(assertion, "type")) {
|
|
7319
|
+
result.type = assertion.type;
|
|
7320
|
+
if (!resolved.exists || jsonValueType(resolved.value) !== assertion.type) errors.push(`expected type ${assertion.type}`);
|
|
7321
|
+
}
|
|
7322
|
+
if (hasOwn(assertion, "equals")) {
|
|
7323
|
+
result.equals = assertion.equals;
|
|
7324
|
+
if (!resolved.exists || !deepJsonEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
|
|
7325
|
+
}
|
|
7326
|
+
if (hasOwn(assertion, "not_equals")) {
|
|
7327
|
+
result.not_equals = assertion.not_equals;
|
|
7328
|
+
if (resolved.exists && deepJsonEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
|
|
7329
|
+
}
|
|
7330
|
+
if (hasOwn(assertion, "contains")) {
|
|
7331
|
+
result.contains = assertion.contains;
|
|
7332
|
+
if (!resolved.exists || !jsonContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
|
|
7333
|
+
}
|
|
7334
|
+
result.ok = errors.length === 0;
|
|
7335
|
+
if (errors.length) result.errors = errors;
|
|
7336
|
+
return result;
|
|
7337
|
+
}
|
|
7338
|
+
function evaluateHttpStatusBodyJsonAssertions(bodyText, assertions) {
|
|
7339
|
+
const expected = assertions?.filter((assertion) => assertion.path) ?? [];
|
|
7340
|
+
if (!expected.length) return [];
|
|
7341
|
+
let parsed;
|
|
7342
|
+
try {
|
|
7343
|
+
parsed = JSON.parse(bodyText);
|
|
7344
|
+
} catch (error) {
|
|
7345
|
+
const message = `response body is not valid JSON: ${String(error instanceof Error ? error.message : error).slice(0, 200)}`;
|
|
7346
|
+
return expected.map((assertion) => ({
|
|
7347
|
+
label: assertion.label || assertion.path,
|
|
7348
|
+
path: assertion.path,
|
|
7349
|
+
ok: false,
|
|
7350
|
+
exists: false,
|
|
7351
|
+
observed_type: "missing",
|
|
7352
|
+
errors: [message]
|
|
7353
|
+
}));
|
|
7354
|
+
}
|
|
7355
|
+
return expected.map((assertion) => evaluateHttpStatusBodyJsonAssertion(parsed, assertion));
|
|
7356
|
+
}
|
|
7173
7357
|
function compactProfileSetupSummaryText(value, limit = 160) {
|
|
7174
7358
|
const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
7175
7359
|
if (!text) return void 0;
|
|
@@ -7824,6 +8008,46 @@ function validateRegexPatterns(patterns, label) {
|
|
|
7824
8008
|
}
|
|
7825
8009
|
}
|
|
7826
8010
|
}
|
|
8011
|
+
function normalizeHttpStatusBodyJsonAssertions(value, label) {
|
|
8012
|
+
if (value === void 0) return void 0;
|
|
8013
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
8014
|
+
if (!value.length) throw new Error(`${label} must not be empty.`);
|
|
8015
|
+
return value.map((item, index) => {
|
|
8016
|
+
const itemLabel = `${label}[${index}]`;
|
|
8017
|
+
if (typeof item === "string") {
|
|
8018
|
+
const path8 = stringValue2(item);
|
|
8019
|
+
if (!path8) throw new Error(`${itemLabel} path must not be empty.`);
|
|
8020
|
+
return { path: path8, exists: true };
|
|
8021
|
+
}
|
|
8022
|
+
if (!isRecord(item)) throw new Error(`${itemLabel} must be an object or JSON path string.`);
|
|
8023
|
+
const path7 = stringFromOwn(item, "path", "json_path", "jsonPath", "key");
|
|
8024
|
+
if (!path7) throw new Error(`${itemLabel}.path is required.`);
|
|
8025
|
+
const assertion = {
|
|
8026
|
+
label: stringValue2(item.label),
|
|
8027
|
+
path: path7
|
|
8028
|
+
};
|
|
8029
|
+
const exists = booleanValue(valueFromOwn(item, "exists", "present"));
|
|
8030
|
+
if (exists !== void 0) assertion.exists = exists;
|
|
8031
|
+
const type = stringValue2(valueFromOwn(item, "type", "value_type", "valueType"));
|
|
8032
|
+
if (type !== void 0) {
|
|
8033
|
+
const allowedTypes = ["array", "boolean", "null", "number", "object", "string"];
|
|
8034
|
+
if (!allowedTypes.includes(type)) {
|
|
8035
|
+
throw new Error(`${itemLabel}.type must be one of ${allowedTypes.join(", ")}.`);
|
|
8036
|
+
}
|
|
8037
|
+
assertion.type = type;
|
|
8038
|
+
}
|
|
8039
|
+
const equalsValue = valueFromOwn(item, "equals", "expected", "expected_value", "expectedValue", "value");
|
|
8040
|
+
if (equalsValue !== void 0) assertion.equals = toJsonValue(equalsValue);
|
|
8041
|
+
const notEqualsValue = valueFromOwn(item, "not_equals", "notEquals", "forbidden", "forbidden_value", "forbiddenValue");
|
|
8042
|
+
if (notEqualsValue !== void 0) assertion.not_equals = toJsonValue(notEqualsValue);
|
|
8043
|
+
const containsValue = valueFromOwn(item, "contains", "includes", "contains_value", "containsValue", "include");
|
|
8044
|
+
if (containsValue !== void 0) assertion.contains = toJsonValue(containsValue);
|
|
8045
|
+
if (assertion.exists === void 0 && assertion.type === void 0 && !hasOwn(assertion, "equals") && !hasOwn(assertion, "not_equals") && !hasOwn(assertion, "contains")) {
|
|
8046
|
+
assertion.exists = true;
|
|
8047
|
+
}
|
|
8048
|
+
return assertion;
|
|
8049
|
+
});
|
|
8050
|
+
}
|
|
7827
8051
|
function isDialogCountCheckType(type) {
|
|
7828
8052
|
return type === "dialog_count_equals" || type === "dialog_accept_count_equals" || type === "dialog_dismiss_count_equals";
|
|
7829
8053
|
}
|
|
@@ -7923,6 +8147,10 @@ function normalizeCheck(input, index) {
|
|
|
7923
8147
|
`checks[${index}] body_not_patterns`
|
|
7924
8148
|
) : void 0;
|
|
7925
8149
|
if (bodyNotPatterns?.length) validateRegexPatterns(bodyNotPatterns, `checks[${index}] body_not_patterns`);
|
|
8150
|
+
const bodyJsonAssertions = isHttpStatusCheck ? normalizeHttpStatusBodyJsonAssertions(
|
|
8151
|
+
input.body_json_assertions ?? input.bodyJsonAssertions ?? input.json_body_assertions ?? input.jsonBodyAssertions ?? input.json_assertions ?? input.jsonAssertions ?? input.response_json_assertions ?? input.responseJsonAssertions,
|
|
8152
|
+
`checks[${index}] body_json_assertions`
|
|
8153
|
+
) : void 0;
|
|
7926
8154
|
if (isLinkStatusCheck) {
|
|
7927
8155
|
if (minCount !== void 0 && (!Number.isInteger(minCount) || minCount < 0)) {
|
|
7928
8156
|
throw new Error(`checks[${index}] ${type} min_count must be a non-negative integer.`);
|
|
@@ -7948,6 +8176,7 @@ function normalizeCheck(input, index) {
|
|
|
7948
8176
|
body_contains: bodyContains,
|
|
7949
8177
|
body_not_contains: bodyNotContains,
|
|
7950
8178
|
body_not_patterns: bodyNotPatterns,
|
|
8179
|
+
body_json_assertions: bodyJsonAssertions,
|
|
7951
8180
|
expected_texts: expectedTexts,
|
|
7952
8181
|
link_selector: stringValue2(input.link_selector) || stringValue2(input.linkSelector),
|
|
7953
8182
|
source_selector: stringValue2(input.source_selector) || stringValue2(input.sourceSelector),
|
|
@@ -8154,6 +8383,38 @@ function httpStatusBodyNotPatternFailures(result, check) {
|
|
|
8154
8383
|
const observed = isRecord(result.body_not_patterns) ? result.body_not_patterns : {};
|
|
8155
8384
|
return forbidden.filter((pattern) => observed[pattern] !== false);
|
|
8156
8385
|
}
|
|
8386
|
+
function httpStatusBodyJsonAssertionFailures(result, check) {
|
|
8387
|
+
const expected = check.body_json_assertions?.filter((assertion) => assertion.path) ?? [];
|
|
8388
|
+
if (!expected.length) return [];
|
|
8389
|
+
if (!Array.isArray(result.body_json_assertions)) {
|
|
8390
|
+
return expected.map((assertion) => ({
|
|
8391
|
+
label: assertion.label || assertion.path,
|
|
8392
|
+
path: assertion.path,
|
|
8393
|
+
ok: false,
|
|
8394
|
+
exists: false,
|
|
8395
|
+
observed_type: "missing",
|
|
8396
|
+
errors: ["body_json_assertions evidence missing"]
|
|
8397
|
+
}));
|
|
8398
|
+
}
|
|
8399
|
+
return result.body_json_assertions.filter((assertion) => isRecord(assertion) && assertion.ok !== true).map((assertion) => ({
|
|
8400
|
+
label: stringValue2(assertion.label) || stringValue2(assertion.path) || "json assertion",
|
|
8401
|
+
path: stringValue2(assertion.path) || "",
|
|
8402
|
+
ok: false,
|
|
8403
|
+
exists: assertion.exists === true,
|
|
8404
|
+
observed: hasOwn(assertion, "observed") ? toJsonValue(assertion.observed) : void 0,
|
|
8405
|
+
observed_sample: hasOwn(assertion, "observed_sample") ? toJsonValue(assertion.observed_sample) : void 0,
|
|
8406
|
+
observed_length: numberValue(assertion.observed_length),
|
|
8407
|
+
observed_key_count: numberValue(assertion.observed_key_count),
|
|
8408
|
+
observed_omitted_count: numberValue(assertion.observed_omitted_count),
|
|
8409
|
+
observed_type: stringValue2(assertion.observed_type) || "missing",
|
|
8410
|
+
expected_exists: booleanValue(assertion.expected_exists),
|
|
8411
|
+
equals: hasOwn(assertion, "equals") ? toJsonValue(assertion.equals) : void 0,
|
|
8412
|
+
not_equals: hasOwn(assertion, "not_equals") ? toJsonValue(assertion.not_equals) : void 0,
|
|
8413
|
+
contains: hasOwn(assertion, "contains") ? toJsonValue(assertion.contains) : void 0,
|
|
8414
|
+
type: stringValue2(assertion.type),
|
|
8415
|
+
errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : void 0
|
|
8416
|
+
}));
|
|
8417
|
+
}
|
|
8157
8418
|
function linkStatusResultOk(result, check) {
|
|
8158
8419
|
const status = numberValue(result.status);
|
|
8159
8420
|
if (!httpStatusIsAllowed(status, check)) return false;
|
|
@@ -8171,6 +8432,7 @@ function linkStatusResultOk(result, check) {
|
|
|
8171
8432
|
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
8172
8433
|
if (httpStatusBodyNotContainsFailures(result, check).length) return false;
|
|
8173
8434
|
if (httpStatusBodyNotPatternFailures(result, check).length) return false;
|
|
8435
|
+
if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
|
|
8174
8436
|
return true;
|
|
8175
8437
|
}
|
|
8176
8438
|
function responseHeader(response, name) {
|
|
@@ -8239,7 +8501,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
8239
8501
|
statusText = typeof response.statusText === "string" ? response.statusText : "";
|
|
8240
8502
|
result.content_type = responseHeader(response, "content-type");
|
|
8241
8503
|
result.content_length = responseContentLength(response);
|
|
8242
|
-
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);
|
|
8504
|
+
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);
|
|
8243
8505
|
if (shouldReadBody && method !== "HEAD") {
|
|
8244
8506
|
const body = await responseBodyText(response);
|
|
8245
8507
|
result.bytes = body.bytes;
|
|
@@ -8252,6 +8514,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
8252
8514
|
if (check.body_not_patterns?.length) {
|
|
8253
8515
|
result.body_not_patterns = Object.fromEntries(check.body_not_patterns.filter(Boolean).map((pattern) => [pattern, new RegExp(pattern).test(body.text)]));
|
|
8254
8516
|
}
|
|
8517
|
+
if (check.body_json_assertions?.length) {
|
|
8518
|
+
result.body_json_assertions = evaluateHttpStatusBodyJsonAssertions(body.text, check.body_json_assertions);
|
|
8519
|
+
}
|
|
8255
8520
|
}
|
|
8256
8521
|
} catch (caught) {
|
|
8257
8522
|
error = String(caught instanceof Error ? caught.message : caught).slice(0, 500);
|
|
@@ -8260,6 +8525,7 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
8260
8525
|
const bodyContainsMissing = httpStatusBodyContainsFailures(result, check);
|
|
8261
8526
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(result, check);
|
|
8262
8527
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(result, check);
|
|
8528
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(result, check);
|
|
8263
8529
|
const ok = !error && linkStatusResultOk(result, check);
|
|
8264
8530
|
return {
|
|
8265
8531
|
index,
|
|
@@ -8278,7 +8544,9 @@ async function preflightHttpStatusCheck(check, index, targetUrl, fetchImpl) {
|
|
|
8278
8544
|
body_not_contains: isRecord(result.body_not_contains) ? Object.fromEntries(Object.entries(result.body_not_contains).map(([key, value]) => [key, value === true])) : null,
|
|
8279
8545
|
body_not_contains_found: bodyNotContainsFound,
|
|
8280
8546
|
body_not_patterns: isRecord(result.body_not_patterns) ? Object.fromEntries(Object.entries(result.body_not_patterns).map(([key, value]) => [key, value === true])) : null,
|
|
8281
|
-
body_not_patterns_found: bodyNotPatternsFound
|
|
8547
|
+
body_not_patterns_found: bodyNotPatternsFound,
|
|
8548
|
+
body_json_assertions: Array.isArray(result.body_json_assertions) ? result.body_json_assertions : null,
|
|
8549
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed
|
|
8282
8550
|
};
|
|
8283
8551
|
}
|
|
8284
8552
|
async function preflightRiddleProofProfileHttpStatusChecks(profile, options = {}) {
|
|
@@ -8323,6 +8591,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
8323
8591
|
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
8324
8592
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
|
|
8325
8593
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
|
|
8594
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
|
|
8326
8595
|
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
8327
8596
|
failures.push({
|
|
8328
8597
|
code: "http_status_failed",
|
|
@@ -8341,6 +8610,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
8341
8610
|
body_not_contains_found: bodyNotContainsFound,
|
|
8342
8611
|
body_not_patterns: check.body_not_patterns ?? null,
|
|
8343
8612
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
8613
|
+
body_json_assertions: check.body_json_assertions ?? null,
|
|
8614
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
|
|
8344
8615
|
body_sample: stringValue2(statusEvidence.body_sample) ?? null
|
|
8345
8616
|
});
|
|
8346
8617
|
}
|
|
@@ -8362,6 +8633,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
8362
8633
|
body_not_contains_found: bodyNotContainsFound,
|
|
8363
8634
|
body_not_patterns: isRecord(statusEvidence.body_not_patterns) ? toJsonValue(statusEvidence.body_not_patterns) : null,
|
|
8364
8635
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
8636
|
+
body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? toJsonValue(statusEvidence.body_json_assertions) : null,
|
|
8637
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed.map((assertion) => toJsonValue(assertion)),
|
|
8365
8638
|
body_sample: stringValue2(statusEvidence.body_sample) ?? null,
|
|
8366
8639
|
failures
|
|
8367
8640
|
};
|
|
@@ -8998,6 +9271,7 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
8998
9271
|
body_contains: check.body_contains ?? [],
|
|
8999
9272
|
body_not_contains: check.body_not_contains ?? [],
|
|
9000
9273
|
body_not_patterns: check.body_not_patterns ?? [],
|
|
9274
|
+
body_json_assertions: toJsonValue(check.body_json_assertions ?? []),
|
|
9001
9275
|
viewports: summaries.map((summary) => toJsonValue(summary)),
|
|
9002
9276
|
failures: failed.flatMap((summary) => Array.isArray(summary.failures) ? summary.failures.map((failure) => toJsonValue({ viewport: stringValue2(summary.viewport) ?? null, failure })) : [])
|
|
9003
9277
|
},
|
|
@@ -9725,6 +9999,40 @@ function httpStatusBodyNotPatternFailures(result, check) {
|
|
|
9725
9999
|
: {};
|
|
9726
10000
|
return forbidden.filter((pattern) => observed[pattern] !== false);
|
|
9727
10001
|
}
|
|
10002
|
+
function httpStatusBodyJsonAssertionFailures(result, check) {
|
|
10003
|
+
const expected = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
10004
|
+
if (!expected.length) return [];
|
|
10005
|
+
if (!Array.isArray(result.body_json_assertions)) {
|
|
10006
|
+
return expected.map((assertion) => ({
|
|
10007
|
+
label: assertion.label || assertion.path,
|
|
10008
|
+
path: assertion.path,
|
|
10009
|
+
ok: false,
|
|
10010
|
+
exists: false,
|
|
10011
|
+
observed_type: "missing",
|
|
10012
|
+
errors: ["body_json_assertions evidence missing"],
|
|
10013
|
+
}));
|
|
10014
|
+
}
|
|
10015
|
+
return result.body_json_assertions
|
|
10016
|
+
.filter((assertion) => assertion && typeof assertion === "object" && assertion.ok !== true)
|
|
10017
|
+
.map((assertion) => ({
|
|
10018
|
+
label: typeof assertion.label === "string" && assertion.label ? assertion.label : typeof assertion.path === "string" && assertion.path ? assertion.path : "json assertion",
|
|
10019
|
+
path: typeof assertion.path === "string" ? assertion.path : "",
|
|
10020
|
+
ok: false,
|
|
10021
|
+
exists: assertion.exists === true,
|
|
10022
|
+
observed: Object.hasOwn(assertion, "observed") ? assertion.observed : undefined,
|
|
10023
|
+
observed_sample: Object.hasOwn(assertion, "observed_sample") ? assertion.observed_sample : undefined,
|
|
10024
|
+
observed_length: typeof assertion.observed_length === "number" && Number.isFinite(assertion.observed_length) ? assertion.observed_length : undefined,
|
|
10025
|
+
observed_key_count: typeof assertion.observed_key_count === "number" && Number.isFinite(assertion.observed_key_count) ? assertion.observed_key_count : undefined,
|
|
10026
|
+
observed_omitted_count: typeof assertion.observed_omitted_count === "number" && Number.isFinite(assertion.observed_omitted_count) ? assertion.observed_omitted_count : undefined,
|
|
10027
|
+
observed_type: typeof assertion.observed_type === "string" && assertion.observed_type ? assertion.observed_type : "missing",
|
|
10028
|
+
expected_exists: typeof assertion.expected_exists === "boolean" ? assertion.expected_exists : undefined,
|
|
10029
|
+
equals: Object.hasOwn(assertion, "equals") ? assertion.equals : undefined,
|
|
10030
|
+
not_equals: Object.hasOwn(assertion, "not_equals") ? assertion.not_equals : undefined,
|
|
10031
|
+
contains: Object.hasOwn(assertion, "contains") ? assertion.contains : undefined,
|
|
10032
|
+
type: typeof assertion.type === "string" ? assertion.type : undefined,
|
|
10033
|
+
errors: Array.isArray(assertion.errors) ? assertion.errors.map(String) : undefined,
|
|
10034
|
+
}));
|
|
10035
|
+
}
|
|
9728
10036
|
function linkStatusResultOk(result, check) {
|
|
9729
10037
|
if (!result || typeof result !== "object" || Array.isArray(result)) return false;
|
|
9730
10038
|
if (!httpStatusIsAllowed(result.status, check)) return false;
|
|
@@ -9742,6 +10050,7 @@ function linkStatusResultOk(result, check) {
|
|
|
9742
10050
|
if (httpStatusBodyContainsFailures(result, check).length) return false;
|
|
9743
10051
|
if (httpStatusBodyNotContainsFailures(result, check).length) return false;
|
|
9744
10052
|
if (httpStatusBodyNotPatternFailures(result, check).length) return false;
|
|
10053
|
+
if (httpStatusBodyJsonAssertionFailures(result, check).length) return false;
|
|
9745
10054
|
return true;
|
|
9746
10055
|
}
|
|
9747
10056
|
function summarizeHttpStatusEvidence(viewport, check) {
|
|
@@ -9762,6 +10071,7 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
9762
10071
|
const bodyContainsMissing = httpStatusBodyContainsFailures(statusEvidence, check);
|
|
9763
10072
|
const bodyNotContainsFound = httpStatusBodyNotContainsFailures(statusEvidence, check);
|
|
9764
10073
|
const bodyNotPatternsFound = httpStatusBodyNotPatternFailures(statusEvidence, check);
|
|
10074
|
+
const bodyJsonAssertionsFailed = httpStatusBodyJsonAssertionFailures(statusEvidence, check);
|
|
9765
10075
|
if (!linkStatusResultOk(statusEvidence, check)) {
|
|
9766
10076
|
failures.push({
|
|
9767
10077
|
code: "http_status_failed",
|
|
@@ -9780,6 +10090,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
9780
10090
|
body_not_contains_found: bodyNotContainsFound,
|
|
9781
10091
|
body_not_patterns: Array.isArray(check.body_not_patterns) ? check.body_not_patterns : null,
|
|
9782
10092
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
10093
|
+
body_json_assertions: Array.isArray(check.body_json_assertions) ? check.body_json_assertions : null,
|
|
10094
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed,
|
|
9783
10095
|
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
9784
10096
|
});
|
|
9785
10097
|
}
|
|
@@ -9807,6 +10119,8 @@ function summarizeHttpStatusEvidence(viewport, check) {
|
|
|
9807
10119
|
? statusEvidence.body_not_patterns
|
|
9808
10120
|
: null,
|
|
9809
10121
|
body_not_patterns_found: bodyNotPatternsFound,
|
|
10122
|
+
body_json_assertions: Array.isArray(statusEvidence.body_json_assertions) ? statusEvidence.body_json_assertions : null,
|
|
10123
|
+
body_json_assertions_failed: bodyJsonAssertionsFailed,
|
|
9810
10124
|
body_sample: typeof statusEvidence.body_sample === "string" ? statusEvidence.body_sample : null,
|
|
9811
10125
|
failures,
|
|
9812
10126
|
};
|
|
@@ -11909,6 +12223,187 @@ function linkProbeResponseFields(response, method) {
|
|
|
11909
12223
|
content_length: contentLength,
|
|
11910
12224
|
};
|
|
11911
12225
|
}
|
|
12226
|
+
function jsonProbeValueType(value) {
|
|
12227
|
+
if (value === null) return "null";
|
|
12228
|
+
if (Array.isArray(value)) return "array";
|
|
12229
|
+
if (typeof value === "boolean") return "boolean";
|
|
12230
|
+
if (typeof value === "number") return "number";
|
|
12231
|
+
if (typeof value === "string") return "string";
|
|
12232
|
+
return "object";
|
|
12233
|
+
}
|
|
12234
|
+
function compactJsonProbeSample(value, depth) {
|
|
12235
|
+
const level = typeof depth === "number" ? depth : 0;
|
|
12236
|
+
if (typeof value === "string") return value.length > 240 ? value.slice(0, 237) + "..." : value;
|
|
12237
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") return value;
|
|
12238
|
+
if (Array.isArray(value)) {
|
|
12239
|
+
if (level >= 2) return "[array:" + value.length + "]";
|
|
12240
|
+
return value.slice(0, 3).map((item) => compactJsonProbeSample(item, level + 1));
|
|
12241
|
+
}
|
|
12242
|
+
if (value && typeof value === "object") {
|
|
12243
|
+
const entries = Object.entries(value);
|
|
12244
|
+
if (level >= 2) return "[object:" + entries.length + " keys]";
|
|
12245
|
+
return Object.fromEntries(entries.slice(0, 8).map(([key, child]) => [key, compactJsonProbeSample(child, level + 1)]));
|
|
12246
|
+
}
|
|
12247
|
+
return String(value);
|
|
12248
|
+
}
|
|
12249
|
+
function attachJsonProbeObservedValue(result, value) {
|
|
12250
|
+
const type = jsonProbeValueType(value);
|
|
12251
|
+
if (type === "array" && Array.isArray(value)) {
|
|
12252
|
+
result.observed_length = value.length;
|
|
12253
|
+
result.observed_omitted_count = Math.max(0, value.length - 3);
|
|
12254
|
+
result.observed_sample = compactJsonProbeSample(value, 0);
|
|
12255
|
+
return;
|
|
12256
|
+
}
|
|
12257
|
+
if (type === "object" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
12258
|
+
const keyCount = Object.keys(value).length;
|
|
12259
|
+
result.observed_key_count = keyCount;
|
|
12260
|
+
result.observed_omitted_count = Math.max(0, keyCount - 8);
|
|
12261
|
+
result.observed_sample = compactJsonProbeSample(value, 0);
|
|
12262
|
+
return;
|
|
12263
|
+
}
|
|
12264
|
+
result.observed = value;
|
|
12265
|
+
}
|
|
12266
|
+
function jsonProbeDeepEqual(left, right) {
|
|
12267
|
+
if (left === right) return true;
|
|
12268
|
+
if (typeof left !== typeof right) return false;
|
|
12269
|
+
if (left === null || right === null) return left === right;
|
|
12270
|
+
if (typeof left !== "object" || typeof right !== "object") return false;
|
|
12271
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
12272
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
12273
|
+
return left.every((item, index) => jsonProbeDeepEqual(item, right[index]));
|
|
12274
|
+
}
|
|
12275
|
+
const leftKeys = Object.keys(left).sort();
|
|
12276
|
+
const rightKeys = Object.keys(right).sort();
|
|
12277
|
+
if (!jsonProbeDeepEqual(leftKeys, rightKeys)) return false;
|
|
12278
|
+
return leftKeys.every((key) => jsonProbeDeepEqual(left[key], right[key]));
|
|
12279
|
+
}
|
|
12280
|
+
function jsonProbeContains(observed, expected) {
|
|
12281
|
+
if (typeof observed === "string" && typeof expected === "string") return observed.includes(expected);
|
|
12282
|
+
if (Array.isArray(observed)) return observed.some((item) => jsonProbeDeepEqual(item, expected));
|
|
12283
|
+
if (observed && expected && typeof observed === "object" && typeof expected === "object" && !Array.isArray(observed) && !Array.isArray(expected)) {
|
|
12284
|
+
return Object.entries(expected).every(([key, value]) => Object.hasOwn(observed, key) && jsonProbeDeepEqual(observed[key], value));
|
|
12285
|
+
}
|
|
12286
|
+
return false;
|
|
12287
|
+
}
|
|
12288
|
+
function parseJsonProbePathSegments(path) {
|
|
12289
|
+
let input = String(path || "").trim();
|
|
12290
|
+
if (!input) throw new Error("path is empty");
|
|
12291
|
+
if (input === "$") return [];
|
|
12292
|
+
if (input.startsWith("$.")) input = input.slice(2);
|
|
12293
|
+
else if (input.startsWith("$[")) input = input.slice(1);
|
|
12294
|
+
const segments = [];
|
|
12295
|
+
let token = "";
|
|
12296
|
+
const pushToken = () => {
|
|
12297
|
+
if (!token) return;
|
|
12298
|
+
segments.push(token);
|
|
12299
|
+
token = "";
|
|
12300
|
+
};
|
|
12301
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
12302
|
+
const char = input[index];
|
|
12303
|
+
if (char === ".") {
|
|
12304
|
+
pushToken();
|
|
12305
|
+
continue;
|
|
12306
|
+
}
|
|
12307
|
+
if (char !== "[") {
|
|
12308
|
+
token += char;
|
|
12309
|
+
continue;
|
|
12310
|
+
}
|
|
12311
|
+
pushToken();
|
|
12312
|
+
const closeIndex = input.indexOf("]", index + 1);
|
|
12313
|
+
if (closeIndex === -1) throw new Error("unterminated bracket at " + index);
|
|
12314
|
+
const bracket = input.slice(index + 1, closeIndex).trim();
|
|
12315
|
+
if (!bracket) throw new Error("empty bracket at " + index);
|
|
12316
|
+
if (/^\d+$/.test(bracket)) {
|
|
12317
|
+
segments.push(Number(bracket));
|
|
12318
|
+
} else if ((bracket.startsWith('"') && bracket.endsWith('"')) || (bracket.startsWith("'") && bracket.endsWith("'"))) {
|
|
12319
|
+
const quoted = bracket.startsWith("'")
|
|
12320
|
+
? '"' + bracket.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"'
|
|
12321
|
+
: bracket;
|
|
12322
|
+
segments.push(String(JSON.parse(quoted)));
|
|
12323
|
+
} else {
|
|
12324
|
+
segments.push(bracket);
|
|
12325
|
+
}
|
|
12326
|
+
index = closeIndex;
|
|
12327
|
+
}
|
|
12328
|
+
pushToken();
|
|
12329
|
+
return segments;
|
|
12330
|
+
}
|
|
12331
|
+
function resolveJsonProbePath(root, path) {
|
|
12332
|
+
let segments;
|
|
12333
|
+
try {
|
|
12334
|
+
segments = parseJsonProbePathSegments(path);
|
|
12335
|
+
} catch (error) {
|
|
12336
|
+
return { exists: false, error: String(error && error.message ? error.message : error) };
|
|
12337
|
+
}
|
|
12338
|
+
let current = root;
|
|
12339
|
+
for (const segment of segments) {
|
|
12340
|
+
if (typeof segment === "number") {
|
|
12341
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) return { exists: false };
|
|
12342
|
+
current = current[segment];
|
|
12343
|
+
continue;
|
|
12344
|
+
}
|
|
12345
|
+
if (!current || typeof current !== "object" || Array.isArray(current) || !Object.hasOwn(current, segment)) {
|
|
12346
|
+
return { exists: false };
|
|
12347
|
+
}
|
|
12348
|
+
current = current[segment];
|
|
12349
|
+
}
|
|
12350
|
+
return { exists: true, value: current };
|
|
12351
|
+
}
|
|
12352
|
+
function evaluateJsonProbeAssertion(root, assertion) {
|
|
12353
|
+
const resolved = resolveJsonProbePath(root, assertion.path);
|
|
12354
|
+
const errors = [];
|
|
12355
|
+
const result = {
|
|
12356
|
+
label: assertion.label || assertion.path,
|
|
12357
|
+
path: assertion.path,
|
|
12358
|
+
ok: true,
|
|
12359
|
+
exists: resolved.exists,
|
|
12360
|
+
observed_type: resolved.exists ? jsonProbeValueType(resolved.value) : "missing",
|
|
12361
|
+
};
|
|
12362
|
+
if (resolved.exists) attachJsonProbeObservedValue(result, resolved.value);
|
|
12363
|
+
if (resolved.error) errors.push(resolved.error);
|
|
12364
|
+
if (Object.hasOwn(assertion, "exists")) {
|
|
12365
|
+
result.expected_exists = assertion.exists;
|
|
12366
|
+
if (resolved.exists !== assertion.exists) errors.push("expected exists=" + assertion.exists);
|
|
12367
|
+
}
|
|
12368
|
+
if (Object.hasOwn(assertion, "type")) {
|
|
12369
|
+
result.type = assertion.type;
|
|
12370
|
+
if (!resolved.exists || jsonProbeValueType(resolved.value) !== assertion.type) errors.push("expected type " + assertion.type);
|
|
12371
|
+
}
|
|
12372
|
+
if (Object.hasOwn(assertion, "equals")) {
|
|
12373
|
+
result.equals = assertion.equals;
|
|
12374
|
+
if (!resolved.exists || !jsonProbeDeepEqual(resolved.value, assertion.equals)) errors.push("expected JSON value equality");
|
|
12375
|
+
}
|
|
12376
|
+
if (Object.hasOwn(assertion, "not_equals")) {
|
|
12377
|
+
result.not_equals = assertion.not_equals;
|
|
12378
|
+
if (resolved.exists && jsonProbeDeepEqual(resolved.value, assertion.not_equals)) errors.push("expected JSON value inequality");
|
|
12379
|
+
}
|
|
12380
|
+
if (Object.hasOwn(assertion, "contains")) {
|
|
12381
|
+
result.contains = assertion.contains;
|
|
12382
|
+
if (!resolved.exists || !jsonProbeContains(resolved.value, assertion.contains)) errors.push("expected JSON value containment");
|
|
12383
|
+
}
|
|
12384
|
+
result.ok = errors.length === 0;
|
|
12385
|
+
if (errors.length) result.errors = errors;
|
|
12386
|
+
return result;
|
|
12387
|
+
}
|
|
12388
|
+
function evaluateJsonProbeAssertions(text, assertions) {
|
|
12389
|
+
const expected = Array.isArray(assertions) ? assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
12390
|
+
if (!expected.length) return [];
|
|
12391
|
+
let parsed;
|
|
12392
|
+
try {
|
|
12393
|
+
parsed = JSON.parse(text);
|
|
12394
|
+
} catch (error) {
|
|
12395
|
+
const message = "response body is not valid JSON: " + String(error && error.message ? error.message : error).slice(0, 200);
|
|
12396
|
+
return expected.map((assertion) => ({
|
|
12397
|
+
label: assertion.label || assertion.path,
|
|
12398
|
+
path: assertion.path,
|
|
12399
|
+
ok: false,
|
|
12400
|
+
exists: false,
|
|
12401
|
+
observed_type: "missing",
|
|
12402
|
+
errors: [message],
|
|
12403
|
+
}));
|
|
12404
|
+
}
|
|
12405
|
+
return expected.map((assertion) => evaluateJsonProbeAssertion(parsed, assertion));
|
|
12406
|
+
}
|
|
11912
12407
|
async function collectHttpStatus(check) {
|
|
11913
12408
|
const url = httpStatusRequestUrl(check, page.url() || targetUrl);
|
|
11914
12409
|
const method = httpStatusMethod(check);
|
|
@@ -11925,6 +12420,7 @@ async function collectHttpStatus(check) {
|
|
|
11925
12420
|
const bodyContains = Array.isArray(check.body_contains) ? check.body_contains.filter(Boolean) : [];
|
|
11926
12421
|
const bodyNotContains = Array.isArray(check.body_not_contains) ? check.body_not_contains.filter(Boolean) : [];
|
|
11927
12422
|
const bodyNotPatterns = Array.isArray(check.body_not_patterns) ? check.body_not_patterns.filter(Boolean) : [];
|
|
12423
|
+
const bodyJsonAssertions = Array.isArray(check.body_json_assertions) ? check.body_json_assertions.filter((assertion) => assertion && assertion.path) : [];
|
|
11928
12424
|
const options = {
|
|
11929
12425
|
method,
|
|
11930
12426
|
redirect: "follow",
|
|
@@ -11950,17 +12446,18 @@ async function collectHttpStatus(check) {
|
|
|
11950
12446
|
Object.assign(result, linkProbeResponseFields(response, method));
|
|
11951
12447
|
result.url = url;
|
|
11952
12448
|
result.status_text = response.statusText || "";
|
|
11953
|
-
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;
|
|
12449
|
+
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;
|
|
11954
12450
|
if (shouldReadBody) {
|
|
11955
12451
|
try {
|
|
11956
12452
|
const buffer = await response.arrayBuffer();
|
|
11957
12453
|
result.bytes = buffer.byteLength;
|
|
11958
|
-
if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length) {
|
|
12454
|
+
if (bodyContains.length || bodyNotContains.length || bodyNotPatterns.length || bodyJsonAssertions.length) {
|
|
11959
12455
|
const text = new TextDecoder().decode(buffer);
|
|
11960
12456
|
result.body_sample = text.slice(0, 1000);
|
|
11961
12457
|
if (bodyContains.length) result.body_contains = Object.fromEntries(bodyContains.map((expected) => [expected, text.includes(expected)]));
|
|
11962
12458
|
if (bodyNotContains.length) result.body_not_contains = Object.fromEntries(bodyNotContains.map((forbidden) => [forbidden, text.includes(forbidden)]));
|
|
11963
12459
|
if (bodyNotPatterns.length) result.body_not_patterns = Object.fromEntries(bodyNotPatterns.map((pattern) => [pattern, new RegExp(pattern).test(text)]));
|
|
12460
|
+
if (bodyJsonAssertions.length) result.body_json_assertions = evaluateJsonProbeAssertions(text, bodyJsonAssertions);
|
|
11964
12461
|
}
|
|
11965
12462
|
} catch (error) {
|
|
11966
12463
|
result.error = String(error && error.message ? error.message : error).slice(0, 500);
|
|
@@ -11973,6 +12470,7 @@ async function collectHttpStatus(check) {
|
|
|
11973
12470
|
&& (!bodyContains.length || bodyContains.every((expected) => result.body_contains && result.body_contains[expected] === true))
|
|
11974
12471
|
&& (!bodyNotContains.length || bodyNotContains.every((forbidden) => result.body_not_contains && result.body_not_contains[forbidden] === false))
|
|
11975
12472
|
&& (!bodyNotPatterns.length || bodyNotPatterns.every((pattern) => result.body_not_patterns && result.body_not_patterns[pattern] === false))
|
|
12473
|
+
&& (!bodyJsonAssertions.length || (Array.isArray(result.body_json_assertions) && result.body_json_assertions.every((assertion) => assertion.ok === true)))
|
|
11976
12474
|
&& !result.error;
|
|
11977
12475
|
return result;
|
|
11978
12476
|
} catch (error) {
|
|
@@ -13685,6 +14183,21 @@ function profileHttpStatusAssertionKeys(evidence, viewports, field) {
|
|
|
13685
14183
|
}
|
|
13686
14184
|
return [...keys];
|
|
13687
14185
|
}
|
|
14186
|
+
function profileHttpStatusJsonAssertionCount(viewports) {
|
|
14187
|
+
if (!viewports.length) return void 0;
|
|
14188
|
+
let passed = 0;
|
|
14189
|
+
let total = 0;
|
|
14190
|
+
for (const viewport of viewports) {
|
|
14191
|
+
if (!Array.isArray(viewport.body_json_assertions)) continue;
|
|
14192
|
+
for (const assertion of viewport.body_json_assertions) {
|
|
14193
|
+
const record = cliRecord(assertion);
|
|
14194
|
+
if (!record) continue;
|
|
14195
|
+
total += 1;
|
|
14196
|
+
if (record.ok === true) passed += 1;
|
|
14197
|
+
}
|
|
14198
|
+
}
|
|
14199
|
+
return total ? { passed, total } : void 0;
|
|
14200
|
+
}
|
|
13688
14201
|
function profileHttpStatusSummaryMarkdown(result) {
|
|
13689
14202
|
const httpStatusChecks = result.checks.filter((check) => check.type === "http_status");
|
|
13690
14203
|
const lines = [];
|
|
@@ -13715,10 +14228,12 @@ function profileHttpStatusSummaryMarkdown(result) {
|
|
|
13715
14228
|
profileHttpStatusAssertionKeys(evidence, viewports, "body_not_patterns"),
|
|
13716
14229
|
false
|
|
13717
14230
|
);
|
|
14231
|
+
const bodyJsonAssertions = profileHttpStatusJsonAssertionCount(viewports);
|
|
13718
14232
|
const bodyParts = [
|
|
13719
14233
|
bodyContains ? `body_contains ${bodyContains.passed}/${bodyContains.total}` : "",
|
|
13720
14234
|
bodyNotContains ? `body_not_contains clean ${bodyNotContains.passed}/${bodyNotContains.total}` : "",
|
|
13721
|
-
bodyNotPatterns ? `body_not_patterns clean ${bodyNotPatterns.passed}/${bodyNotPatterns.total}` : ""
|
|
14235
|
+
bodyNotPatterns ? `body_not_patterns clean ${bodyNotPatterns.passed}/${bodyNotPatterns.total}` : "",
|
|
14236
|
+
bodyJsonAssertions ? `body_json_assertions ${bodyJsonAssertions.passed}/${bodyJsonAssertions.total}` : ""
|
|
13722
14237
|
].filter(Boolean);
|
|
13723
14238
|
lines.push(
|
|
13724
14239
|
`- ${label}: ${method}${url ? ` ${markdownInlineCode(url)}` : ""}, statuses ${statuses.length ? statuses.join("/") : "unknown"}${bodyParts.length ? `, ${bodyParts.join(", ")}` : ""}, failures ${failedTotal}`
|