@postman-cse/onboarding-bootstrap 2.2.0 → 2.3.0
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 +11 -1
- package/dist/action.cjs +598 -12
- package/dist/cli.cjs +598 -12
- package/dist/index.cjs +598 -12
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -298341,7 +298341,11 @@ function securityCheckFor(schemeName, scheme) {
|
|
|
298341
298341
|
if (scheme?.type === "http") {
|
|
298342
298342
|
const httpScheme = String(scheme.scheme || "").toLowerCase();
|
|
298343
298343
|
if (httpScheme === "basic") return { scheme: schemeName, kind, checkable: true, prefix: "Basic " };
|
|
298344
|
-
if (httpScheme === "bearer")
|
|
298344
|
+
if (httpScheme === "bearer") {
|
|
298345
|
+
const check = { scheme: schemeName, kind, checkable: true, prefix: "Bearer " };
|
|
298346
|
+
if (typeof scheme.bearerFormat === "string" && scheme.bearerFormat) check.bearerFormat = scheme.bearerFormat;
|
|
298347
|
+
return check;
|
|
298348
|
+
}
|
|
298345
298349
|
if (httpScheme) return { scheme: schemeName, kind, checkable: true, prefix: `${httpScheme.charAt(0).toUpperCase()}${httpScheme.slice(1)} ` };
|
|
298346
298350
|
return { scheme: schemeName, kind, checkable: true, in: "header", name: "Authorization" };
|
|
298347
298351
|
}
|
|
@@ -298480,6 +298484,7 @@ function collectParameterChecks(root, pathItem, operation, version2, operationId
|
|
|
298480
298484
|
if (defaultSerialization) warnings.push(`CONTRACT_SCHEMA_NOT_COMPILED: parameter ${location2}:${name} schema on ${operationId} skipped (${packed.unsupported})`);
|
|
298481
298485
|
continue;
|
|
298482
298486
|
}
|
|
298487
|
+
validateParameterExamples(root, param, packed, `${location2}:${name} of ${operationId}`, warnings);
|
|
298483
298488
|
if (location2 === "path") {
|
|
298484
298489
|
const containingSegment = pathTemplate.split("/").find((segment) => segment.includes(`{${name}}`));
|
|
298485
298490
|
if (containingSegment !== void 0 && containingSegment !== `{${name}}`) {
|
|
@@ -298496,12 +298501,32 @@ function collectParameterChecks(root, pathItem, operation, version2, operationId
|
|
|
298496
298501
|
}
|
|
298497
298502
|
const scalarSchema = packedScalarSchema(packed);
|
|
298498
298503
|
if (scalarSchema !== void 0) {
|
|
298499
|
-
|
|
298504
|
+
const decodablePathStyle = location2 === "path" && (style === "label" || style === "matrix") && !explode ? style : void 0;
|
|
298505
|
+
if (!defaultSerialization && !decodablePathStyle) continue;
|
|
298506
|
+
if (!defaultSerialization) warnings.push(...noteWarnings);
|
|
298500
298507
|
const check2 = { in: location2, name, required: param.required === true, schema: scalarSchema };
|
|
298508
|
+
if (decodablePathStyle) check2.pathStyle = decodablePathStyle;
|
|
298501
298509
|
if (location2 === "query" && param.allowEmptyValue === true) check2.allowEmptyValue = true;
|
|
298502
298510
|
checks.push(check2);
|
|
298503
298511
|
continue;
|
|
298504
298512
|
}
|
|
298513
|
+
if (location2 === "query" && style === "deepObject" && explode) {
|
|
298514
|
+
const objectSchema = asRecord5(packed.schema);
|
|
298515
|
+
const properties = objectSchema ? asRecord5(objectSchema.properties) : null;
|
|
298516
|
+
const allScalar = properties !== null && Object.keys(properties).length > 0 && Object.values(properties).every((prop) => {
|
|
298517
|
+
const record = asRecord5(prop);
|
|
298518
|
+
if (!record) return false;
|
|
298519
|
+
const types2 = Array.isArray(record.type) ? record.type : [record.type];
|
|
298520
|
+
return types2.every((entry) => typeof entry === "string" && SCALAR_SCHEMA_TYPES.has(entry));
|
|
298521
|
+
});
|
|
298522
|
+
if (allScalar) {
|
|
298523
|
+
warnings.push(...noteWarnings);
|
|
298524
|
+
const check2 = { in: "query", name, required: param.required === true, schema: packed.schema, decode: "deepObject" };
|
|
298525
|
+
if (param.allowEmptyValue === true) check2.allowEmptyValue = true;
|
|
298526
|
+
checks.push(check2);
|
|
298527
|
+
continue;
|
|
298528
|
+
}
|
|
298529
|
+
}
|
|
298505
298530
|
if (location2 !== "query" && location2 !== "header") continue;
|
|
298506
298531
|
const items = packedArrayItemsSchema(packed);
|
|
298507
298532
|
if (items === void 0) continue;
|
|
@@ -298812,6 +298837,185 @@ function responseHeaders(root, version2, response, context, warnings) {
|
|
|
298812
298837
|
}
|
|
298813
298838
|
return entries;
|
|
298814
298839
|
}
|
|
298840
|
+
var IANA_HTTP_AUTH_SCHEMES = /* @__PURE__ */ new Set([
|
|
298841
|
+
"basic",
|
|
298842
|
+
"bearer",
|
|
298843
|
+
"concealed",
|
|
298844
|
+
"digest",
|
|
298845
|
+
"dpop",
|
|
298846
|
+
"gnap",
|
|
298847
|
+
"hoba",
|
|
298848
|
+
"mutual",
|
|
298849
|
+
"negotiate",
|
|
298850
|
+
"oauth",
|
|
298851
|
+
"privatetoken",
|
|
298852
|
+
"scram-sha-1",
|
|
298853
|
+
"scram-sha-256",
|
|
298854
|
+
"vapid"
|
|
298855
|
+
]);
|
|
298856
|
+
function httpsUrlLint(value, label, schemeName) {
|
|
298857
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
298858
|
+
try {
|
|
298859
|
+
const parsed = new URL(value);
|
|
298860
|
+
if (parsed.protocol !== "https:") return `CONTRACT_SECURITY_SCHEME_URL: security scheme ${schemeName} ${label} ${value} is not an HTTPS URL`;
|
|
298861
|
+
} catch {
|
|
298862
|
+
return `CONTRACT_SECURITY_SCHEME_URL: security scheme ${schemeName} ${label} ${value} is not a valid URL`;
|
|
298863
|
+
}
|
|
298864
|
+
return void 0;
|
|
298865
|
+
}
|
|
298866
|
+
function collectSecurityStaticLints(root, operation) {
|
|
298867
|
+
const securitySchemes = asRecord5(asRecord5(root.components)?.securitySchemes);
|
|
298868
|
+
const requirements = operation.security === void 0 ? asArray3(root.security) : asArray3(operation.security);
|
|
298869
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
298870
|
+
for (const requirement of requirements.map((entry) => asRecord5(entry)).filter((entry) => Boolean(entry))) {
|
|
298871
|
+
for (const [schemeName, requiredScopes] of Object.entries(requirement)) {
|
|
298872
|
+
let scheme;
|
|
298873
|
+
try {
|
|
298874
|
+
scheme = resolveInternalRef(root, securitySchemes?.[schemeName]);
|
|
298875
|
+
} catch {
|
|
298876
|
+
scheme = null;
|
|
298877
|
+
}
|
|
298878
|
+
if (!scheme) continue;
|
|
298879
|
+
if (scheme.type === "http") {
|
|
298880
|
+
const httpScheme = String(scheme.scheme || "").toLowerCase();
|
|
298881
|
+
if (httpScheme && !IANA_HTTP_AUTH_SCHEMES.has(httpScheme)) {
|
|
298882
|
+
warnings.add(`CONTRACT_UNKNOWN_HTTP_AUTH_SCHEME: security scheme ${schemeName} uses "${httpScheme}", which is not in the IANA HTTP Authentication Scheme registry`);
|
|
298883
|
+
}
|
|
298884
|
+
}
|
|
298885
|
+
if (scheme.type === "apiKey" && String(scheme.in) === "query") {
|
|
298886
|
+
warnings.add(`CONTRACT_CREDENTIALS_IN_QUERY: security scheme ${schemeName} sends credentials in the query string, which leaks into logs and referrers`);
|
|
298887
|
+
}
|
|
298888
|
+
if (scheme.type === "openIdConnect") {
|
|
298889
|
+
const urlWarning = httpsUrlLint(scheme.openIdConnectUrl, "openIdConnectUrl", schemeName);
|
|
298890
|
+
if (urlWarning) warnings.add(urlWarning);
|
|
298891
|
+
else if (typeof scheme.openIdConnectUrl === "string" && !scheme.openIdConnectUrl.endsWith("/.well-known/openid-configuration")) {
|
|
298892
|
+
warnings.add(`CONTRACT_SECURITY_SCHEME_URL: security scheme ${schemeName} openIdConnectUrl does not end in /.well-known/openid-configuration`);
|
|
298893
|
+
}
|
|
298894
|
+
}
|
|
298895
|
+
if (scheme.type === "oauth2") {
|
|
298896
|
+
const flows = asRecord5(scheme.flows) ?? {};
|
|
298897
|
+
const declaredScopes = /* @__PURE__ */ new Set();
|
|
298898
|
+
for (const [flowName, rawFlow] of Object.entries(flows)) {
|
|
298899
|
+
const flow = asRecord5(rawFlow);
|
|
298900
|
+
if (!flow) continue;
|
|
298901
|
+
for (const scope of Object.keys(asRecord5(flow.scopes) ?? {})) declaredScopes.add(scope);
|
|
298902
|
+
const urlFields = [["refreshUrl", flow.refreshUrl]];
|
|
298903
|
+
if (flowName === "implicit" || flowName === "authorizationCode") urlFields.push(["authorizationUrl", flow.authorizationUrl]);
|
|
298904
|
+
if (flowName === "password" || flowName === "clientCredentials" || flowName === "authorizationCode") urlFields.push(["tokenUrl", flow.tokenUrl]);
|
|
298905
|
+
for (const [label, value] of urlFields) {
|
|
298906
|
+
const urlWarning = httpsUrlLint(value, `${flowName} ${label}`, schemeName);
|
|
298907
|
+
if (urlWarning) warnings.add(urlWarning);
|
|
298908
|
+
}
|
|
298909
|
+
}
|
|
298910
|
+
for (const scope of asArray3(requiredScopes).filter((entry) => typeof entry === "string")) {
|
|
298911
|
+
if (!declaredScopes.has(scope)) {
|
|
298912
|
+
warnings.add(`CONTRACT_OAUTH2_UNDECLARED_SCOPE: operation requires scope "${scope}" of ${schemeName}, which no flow of the scheme declares`);
|
|
298913
|
+
}
|
|
298914
|
+
}
|
|
298915
|
+
}
|
|
298916
|
+
}
|
|
298917
|
+
}
|
|
298918
|
+
return [...warnings];
|
|
298919
|
+
}
|
|
298920
|
+
function collectSecurityResponseLints(root, operation, responses, operationId) {
|
|
298921
|
+
const warnings = [];
|
|
298922
|
+
const requirements = operation.security === void 0 ? asArray3(root.security) : asArray3(operation.security);
|
|
298923
|
+
const requirementRecords = requirements.map((entry) => asRecord5(entry)).filter((entry) => Boolean(entry));
|
|
298924
|
+
const secured = requirementRecords.length > 0 && requirementRecords.every((entry) => Object.keys(entry).length > 0);
|
|
298925
|
+
const statusKeys = new Set(Object.keys(responses));
|
|
298926
|
+
const hasCatchAll = statusKeys.has("default") || statusKeys.has("4XX");
|
|
298927
|
+
if (secured && !statusKeys.has("401") && !hasCatchAll) {
|
|
298928
|
+
warnings.push(`CONTRACT_SECURITY_RESPONSES_INCOMPLETE: ${operationId} requires authentication but documents no 401 (or 4XX/default) response`);
|
|
298929
|
+
}
|
|
298930
|
+
const usesScopes = requirementRecords.some((entry) => Object.values(entry).some((scopes) => Array.isArray(scopes) && scopes.length > 0));
|
|
298931
|
+
if (secured && usesScopes && !statusKeys.has("403") && !hasCatchAll) {
|
|
298932
|
+
warnings.push(`CONTRACT_SECURITY_RESPONSES_INCOMPLETE: ${operationId} requires scopes but documents no 403 (or 4XX/default) response`);
|
|
298933
|
+
}
|
|
298934
|
+
if (requirementRecords.length === 0) {
|
|
298935
|
+
for (const status of ["401", "403"]) {
|
|
298936
|
+
if (statusKeys.has(status)) warnings.push(`CONTRACT_UNSECURED_AUTH_RESPONSES: ${operationId} documents a ${status} response but declares no security requirement`);
|
|
298937
|
+
}
|
|
298938
|
+
}
|
|
298939
|
+
return warnings;
|
|
298940
|
+
}
|
|
298941
|
+
function collectLinkExpressions(root, response, operationId, warnings) {
|
|
298942
|
+
const links = asRecord5(response.links);
|
|
298943
|
+
if (!links) return [];
|
|
298944
|
+
const expressions = [];
|
|
298945
|
+
let unevaluated = false;
|
|
298946
|
+
for (const [linkName, rawLink] of Object.entries(links)) {
|
|
298947
|
+
let link;
|
|
298948
|
+
try {
|
|
298949
|
+
link = resolveInternalRef(root, rawLink);
|
|
298950
|
+
} catch {
|
|
298951
|
+
link = null;
|
|
298952
|
+
}
|
|
298953
|
+
if (!link) {
|
|
298954
|
+
unevaluated = true;
|
|
298955
|
+
continue;
|
|
298956
|
+
}
|
|
298957
|
+
const values = Object.values(asRecord5(link.parameters) ?? {});
|
|
298958
|
+
if (link.requestBody !== void 0) values.push(link.requestBody);
|
|
298959
|
+
if (values.length === 0) unevaluated = true;
|
|
298960
|
+
for (const value of values) {
|
|
298961
|
+
if (typeof value !== "string" || !value.startsWith("$")) continue;
|
|
298962
|
+
const bodyMatch = value.match(/^\$response\.body#(\/.*)$/);
|
|
298963
|
+
if (bodyMatch) {
|
|
298964
|
+
expressions.push({ link: linkName, kind: "body", pointer: bodyMatch[1] });
|
|
298965
|
+
continue;
|
|
298966
|
+
}
|
|
298967
|
+
const headerMatch = value.match(/^\$response\.header\.([!#$%&'*+.^_`|~0-9A-Za-z-]+)$/);
|
|
298968
|
+
if (headerMatch) {
|
|
298969
|
+
expressions.push({ link: linkName, kind: "header", header: headerMatch[1] });
|
|
298970
|
+
continue;
|
|
298971
|
+
}
|
|
298972
|
+
unevaluated = true;
|
|
298973
|
+
}
|
|
298974
|
+
}
|
|
298975
|
+
if (expressions.length === 0) {
|
|
298976
|
+
if (unevaluated) warnings.add(`CONTRACT_LINKS_NOT_VALIDATED: response links are not validated for ${operationId}`);
|
|
298977
|
+
} else if (unevaluated) {
|
|
298978
|
+
warnings.add(`CONTRACT_LINKS_PARTIALLY_VALIDATED: some link expressions for ${operationId} are not runtime-evaluable and are skipped`);
|
|
298979
|
+
}
|
|
298980
|
+
return expressions;
|
|
298981
|
+
}
|
|
298982
|
+
function escapeRegExpLiteral(value) {
|
|
298983
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
298984
|
+
}
|
|
298985
|
+
function serverAdvisoryPatterns(root, pathItem, operation) {
|
|
298986
|
+
const serverLists = [asArray3(operation.servers), asArray3(pathItem.servers), asArray3(root.servers)];
|
|
298987
|
+
const servers = serverLists.find((list) => list.length > 0) ?? [];
|
|
298988
|
+
const patterns = [];
|
|
298989
|
+
for (const rawServer of servers) {
|
|
298990
|
+
const server = asRecord5(rawServer);
|
|
298991
|
+
if (!server) continue;
|
|
298992
|
+
const url = typeof server.url === "string" ? server.url.trim() : "";
|
|
298993
|
+
if (!url || url === "/") continue;
|
|
298994
|
+
const variables = asRecord5(server.variables);
|
|
298995
|
+
const pattern = url.split(/(\{[^}]+\})/).map((part) => {
|
|
298996
|
+
const varMatch = part.match(/^\{([^}]+)\}$/);
|
|
298997
|
+
if (!varMatch) return escapeRegExpLiteral(part);
|
|
298998
|
+
const variable = asRecord5(variables?.[varMatch[1]]);
|
|
298999
|
+
const enumValues = asArray3(variable?.enum).filter((entry) => typeof entry === "string");
|
|
299000
|
+
if (enumValues.length > 0) return `(${enumValues.map(escapeRegExpLiteral).join("|")})`;
|
|
299001
|
+
return "[^/]*";
|
|
299002
|
+
}).join("");
|
|
299003
|
+
patterns.push(`^${pattern}`);
|
|
299004
|
+
}
|
|
299005
|
+
return patterns.length > 0 ? patterns : void 0;
|
|
299006
|
+
}
|
|
299007
|
+
function validateParameterExamples(root, param, packed, context, warnings) {
|
|
299008
|
+
if (packed.schema === void 0 || packed.unsupported) return;
|
|
299009
|
+
const candidates = exampleCandidates(root, param);
|
|
299010
|
+
if (candidates.length === 0) return;
|
|
299011
|
+
const validate3 = compileSchemaValidator(packed.schema);
|
|
299012
|
+
if (!validate3) return;
|
|
299013
|
+
for (const candidate of candidates) {
|
|
299014
|
+
if (!validate3(candidate.value)) {
|
|
299015
|
+
warnings.push(`CONTRACT_EXAMPLE_SCHEMA_MISMATCH: ${candidate.label} for parameter ${context} does not match its schema`);
|
|
299016
|
+
}
|
|
299017
|
+
}
|
|
299018
|
+
}
|
|
298815
299019
|
function buildContractIndex(root) {
|
|
298816
299020
|
if (root.swagger === "2.0") throw new Error("CONTRACT_UNSUPPORTED_OPENAPI_VERSION: Dynamic contract tests require OpenAPI 3.0 or 3.1 (found swagger 2.0)");
|
|
298817
299021
|
if (!("openapi" in root)) throw new Error("CONTRACT_UNSUPPORTED_OPENAPI_VERSION: Dynamic contract tests require OpenAPI 3.0 or 3.1 (missing openapi)");
|
|
@@ -298830,6 +299034,9 @@ function buildContractIndex(root) {
|
|
|
298830
299034
|
const operation = resolveInternalRef(root, rawOperation);
|
|
298831
299035
|
if (!operation) continue;
|
|
298832
299036
|
if (operation.callbacks) warnings.push(`CONTRACT_CALLBACKS_NOT_VALIDATED: callbacks are not validated for ${lowerMethod.toUpperCase()} ${path10}`);
|
|
299037
|
+
if (operation.requestBody !== void 0 && ["get", "head", "delete"].includes(lowerMethod)) {
|
|
299038
|
+
warnings.push(`CONTRACT_METHOD_BODY_SEMANTICS: ${lowerMethod.toUpperCase()} ${path10} declares a request body; RFC 9110 defines no request-body semantics for ${lowerMethod.toUpperCase()}`);
|
|
299039
|
+
}
|
|
298833
299040
|
const responses = asRecord5(operation.responses);
|
|
298834
299041
|
if (!responses || Object.keys(responses).length === 0) {
|
|
298835
299042
|
throw new Error(`CONTRACT_OPERATION_NO_RESPONSES: ${lowerMethod.toUpperCase()} ${path10} must define at least one response`);
|
|
@@ -298839,11 +299046,15 @@ function buildContractIndex(root) {
|
|
|
298839
299046
|
for (const [status, rawResponse] of Object.entries(responses)) {
|
|
298840
299047
|
const response = resolveInternalRef(root, rawResponse);
|
|
298841
299048
|
if (!response) continue;
|
|
298842
|
-
if (
|
|
298843
|
-
responseWarnings.add(`
|
|
299049
|
+
if (status !== "default" && !/^[1-5]XX$/.test(status) && !/^[1-5][0-9][0-9]$/.test(status)) {
|
|
299050
|
+
responseWarnings.add(`CONTRACT_INVALID_STATUS_CODE: ${lowerMethod.toUpperCase()} ${path10} declares response status "${status}" outside RFC 9110's 100-599, 1XX-5XX, or default forms`);
|
|
298844
299051
|
}
|
|
299052
|
+
const linkExpressions = collectLinkExpressions(root, response, `${lowerMethod.toUpperCase()} ${path10}`, responseWarnings);
|
|
298845
299053
|
const responseContext = `${lowerMethod.toUpperCase()} ${path10} status ${status}`;
|
|
298846
299054
|
const content = responseContent(root, version2, response, responseContext, responseWarnings);
|
|
299055
|
+
if ((status === "204" || status === "205" || status === "304") && Object.keys(content).length > 0) {
|
|
299056
|
+
responseWarnings.add(`CONTRACT_BODYLESS_STATUS_WITH_CONTENT: ${lowerMethod.toUpperCase()} ${path10} declares content for status ${status}, which RFC 9110 forbids on the wire`);
|
|
299057
|
+
}
|
|
298847
299058
|
for (const [contentType2, media] of Object.entries(content)) {
|
|
298848
299059
|
const base = contentType2.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
298849
299060
|
const schemaType = asRecord5(media.schema)?.type;
|
|
@@ -298855,7 +299066,8 @@ function buildContractIndex(root) {
|
|
|
298855
299066
|
contractResponses[normalizeResponseKey(status)] = {
|
|
298856
299067
|
content,
|
|
298857
299068
|
hasBody: Object.keys(content).length > 0,
|
|
298858
|
-
headers
|
|
299069
|
+
headers,
|
|
299070
|
+
...linkExpressions.length > 0 ? { links: linkExpressions } : {}
|
|
298859
299071
|
};
|
|
298860
299072
|
}
|
|
298861
299073
|
const candidates = [...new Set([
|
|
@@ -298868,11 +299080,13 @@ function buildContractIndex(root) {
|
|
|
298868
299080
|
opWarnings.push(...collectSecuritySchemeWarnings(root, operation));
|
|
298869
299081
|
const parameterChecks = collectParameterChecks(root, pathItem, operation, version2, operationId, path10, opWarnings);
|
|
298870
299082
|
const checkedKeys = new Set((parameterChecks ?? []).map((check) => `${check.in}:${check.name.toLowerCase()}`));
|
|
298871
|
-
const decodedKeys = new Set((parameterChecks ?? []).filter((check) => check.decode).map((check) => `${check.in}:${check.name.toLowerCase()}`));
|
|
299083
|
+
const decodedKeys = new Set((parameterChecks ?? []).filter((check) => check.decode || check.pathStyle).map((check) => `${check.in}:${check.name.toLowerCase()}`));
|
|
298872
299084
|
opWarnings.push(...collectSerializationWarnings(root, pathItem, operation, decodedKeys));
|
|
298873
299085
|
if (operation.deprecated === true) {
|
|
298874
299086
|
opWarnings.push(`CONTRACT_OPERATION_DEPRECATED: ${lowerMethod.toUpperCase()} ${path10} is marked deprecated in the OpenAPI document`);
|
|
298875
299087
|
}
|
|
299088
|
+
opWarnings.push(...collectSecurityStaticLints(root, operation));
|
|
299089
|
+
opWarnings.push(...collectSecurityResponseLints(root, operation, responses, operationId));
|
|
298876
299090
|
const requiredParameters = collectParameters(root, pathItem, operation);
|
|
298877
299091
|
for (const parameter of requiredParameters.filter((entry) => entry.securityDerived)) {
|
|
298878
299092
|
opWarnings.push(`CONTRACT_SECURITY_NOT_VALIDATED: security parameter ${parameter.in}:${parameter.name} is not statically required in generated requests`);
|
|
@@ -298902,6 +299116,8 @@ function buildContractIndex(root) {
|
|
|
298902
299116
|
requestBody: collectRequestBody(root, operation, version2, operationId, opWarnings),
|
|
298903
299117
|
security: collectSecurityRuntimeChecks(root, operation),
|
|
298904
299118
|
pathMethods: Object.keys(pathItem).filter((key) => HTTP_METHODS.has(key)).map((key) => key.toUpperCase()),
|
|
299119
|
+
deprecated: operation.deprecated === true || void 0,
|
|
299120
|
+
servers: serverAdvisoryPatterns(root, pathItem, operation),
|
|
298905
299121
|
warnings: opWarnings
|
|
298906
299122
|
});
|
|
298907
299123
|
}
|
|
@@ -299078,7 +299294,7 @@ function buildValidatorAssignments(operation, warnings, skipped) {
|
|
|
299078
299294
|
return lines;
|
|
299079
299295
|
}
|
|
299080
299296
|
function createContractScript(operation, warnings = []) {
|
|
299081
|
-
const contract = { method: operation.method, path: operation.path, responses: operation.responses, security: operation.security, parameters: operation.parameterChecks, pathMethods: operation.pathMethods };
|
|
299297
|
+
const contract = { method: operation.method, path: operation.path, responses: operation.responses, security: operation.security, parameters: operation.parameterChecks, pathMethods: operation.pathMethods, deprecated: operation.deprecated, servers: operation.servers };
|
|
299082
299298
|
const skipped = [];
|
|
299083
299299
|
const validatorLines = buildValidatorAssignments(operation, warnings, skipped);
|
|
299084
299300
|
return [
|
|
@@ -299197,6 +299413,13 @@ function createContractScript(operation, warnings = []) {
|
|
|
299197
299413
|
' if (!challenge) pm.expect.fail("RFC 9110 requires WWW-Authenticate on 401 responses");',
|
|
299198
299414
|
' var wantsBearer = (contract.security || []).some(function (alternative) { return alternative.some(function (check) { return check.prefix && check.prefix.toLowerCase().indexOf("bearer") === 0; }); });',
|
|
299199
299415
|
' if (wantsBearer && challenge && challenge.toLowerCase().indexOf("bearer") === -1) pm.expect.fail("RFC 6750 expects a Bearer challenge on 401 for bearer-secured operations; got: " + challenge);',
|
|
299416
|
+
' if (challenge && /\\bbasic\\b/i.test(challenge) && !/realm\\s*=/i.test(challenge)) pm.expect.fail("RFC 7617 requires a realm parameter on Basic challenges: " + challenge);',
|
|
299417
|
+
' if (challenge && /\\bdigest\\b/i.test(challenge) && (!/realm\\s*=/i.test(challenge) || !/nonce\\s*=/i.test(challenge))) pm.expect.fail("RFC 7616 requires realm and nonce on Digest challenges: " + challenge);',
|
|
299418
|
+
" }",
|
|
299419
|
+
" if (code === 401 || code === 403) {",
|
|
299420
|
+
' var authChallenge = respHeader("WWW-Authenticate");',
|
|
299421
|
+
' var bearerError = authChallenge && /\\bbearer\\b/i.test(authChallenge) ? authChallenge.match(/\\berror\\s*=\\s*"?([A-Za-z0-9_]+)"?/i) : null;',
|
|
299422
|
+
' if (bearerError && ["invalid_request", "invalid_token", "insufficient_scope"].indexOf(bearerError[1]) === -1) pm.expect.fail("RFC 6750 Bearer error code must be invalid_request, invalid_token, or insufficient_scope; got " + bearerError[1]);',
|
|
299200
299423
|
" }",
|
|
299201
299424
|
" if (code === 405) {",
|
|
299202
299425
|
' var allow = respHeader("Allow");',
|
|
@@ -299205,11 +299428,6 @@ function createContractScript(operation, warnings = []) {
|
|
|
299205
299428
|
' (contract.pathMethods || []).forEach(function (method) { if (allowed.indexOf(method) === -1) pm.expect.fail("Allow on 405 must list every method the OpenAPI path declares (RFC 9110); missing " + method + " in: " + allow); });',
|
|
299206
299429
|
" }",
|
|
299207
299430
|
' if (code === 304 && responseText().trim().length > 0) pm.expect.fail("RFC 9110 forbids content in a 304 response");',
|
|
299208
|
-
" if (code === 206) {",
|
|
299209
|
-
' var range = respHeader("Content-Range");',
|
|
299210
|
-
' if (!range) pm.expect.fail("RFC 9110 requires Content-Range on a single-part 206 response");',
|
|
299211
|
-
' if (range && !/^\\S+ (\\d+-\\d+|\\*)\\/(\\d+|\\*)$/.test(range)) pm.expect.fail("Content-Range is not a valid RFC 9110 range: " + range);',
|
|
299212
|
-
" }",
|
|
299213
299431
|
' var retryAfter = respHeader("Retry-After");',
|
|
299214
299432
|
" if (retryAfter && (code === 429 || code === 503 || (code >= 300 && code < 400))) {",
|
|
299215
299433
|
' if (!/^\\d+$/.test(retryAfter.trim()) && isNaN(Date.parse(retryAfter))) pm.expect.fail("Retry-After must be delay-seconds or an HTTP-date (RFC 9110): " + retryAfter);',
|
|
@@ -299227,6 +299445,7 @@ function createContractScript(operation, warnings = []) {
|
|
|
299227
299445
|
' try { problem = pm.response.json(); } catch (error) { pm.expect.fail("application/problem+json body is not valid JSON (RFC 9457): " + error); }',
|
|
299228
299446
|
' if (!problem || typeof problem !== "object" || Array.isArray(problem)) pm.expect.fail("problem details must be a JSON object (RFC 9457)");',
|
|
299229
299447
|
' ["type", "title", "detail", "instance"].forEach(function (member) { if (problem[member] !== undefined && typeof problem[member] !== "string") pm.expect.fail("RFC 9457 " + member + " member must be a string; got " + typeof problem[member]); });',
|
|
299448
|
+
' ["type", "instance"].forEach(function (member) { if (typeof problem[member] === "string" && /\\s/.test(problem[member].trim())) pm.expect.fail("RFC 9457 " + member + " member must be a URI-reference (RFC 3986): " + problem[member]); });',
|
|
299230
299449
|
" if (problem.status !== undefined) {",
|
|
299231
299450
|
' if (typeof problem.status !== "number") pm.expect.fail("RFC 9457 status member must be a number; got " + typeof problem.status);',
|
|
299232
299451
|
' else if (problem.status !== pm.response.code) pm.expect.fail("RFC 9457 status member (" + problem.status + ") must match the HTTP status code (" + pm.response.code + ")");',
|
|
@@ -299244,6 +299463,359 @@ function createContractScript(operation, warnings = []) {
|
|
|
299244
299463
|
" });",
|
|
299245
299464
|
" }",
|
|
299246
299465
|
"});",
|
|
299466
|
+
"var rfcAdvisories = [];",
|
|
299467
|
+
"function rfcAdvise(message) { if (rfcAdvisories.indexOf(message) === -1) rfcAdvisories.push(message); }",
|
|
299468
|
+
'function rfcRespHeader(name) { return pm.response.headers.get(name) || ""; }',
|
|
299469
|
+
"function rfcHeaderAll(name) { var out = []; pm.response.headers.each(function (header) { if (header && String(header.key).toLowerCase() === String(name).toLowerCase()) out.push(String(header.value)); }); return out; }",
|
|
299470
|
+
"function rfcIsHttpDate(value) { return /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), [0-3][0-9] (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4} [0-2][0-9]:[0-5][0-9]:[0-5][0-9] GMT$/.test(String(value).trim()) && !isNaN(Date.parse(value)); }",
|
|
299471
|
+
"function rfcIsToken(value) { return /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(String(value)); }",
|
|
299472
|
+
'function rfcIsEntityTag(value) { return /^(W\\/)?"[\\x21\\x23-\\x7e\\x80-\\xff]*"$/.test(String(value).trim()); }',
|
|
299473
|
+
"function rfcIsFieldContent(value) { return /^[\\t \\x21-\\x7e\\x80-\\xff]*$/.test(String(value)); }",
|
|
299474
|
+
'function rfcTokenList(value) { var parts = String(value).split(","); for (var i = 0; i < parts.length; i += 1) { if (!rfcIsToken(parts[i].trim())) return false; } return true; }',
|
|
299475
|
+
`function rfcSplitList(value) { var out = []; var current = ""; var inQuote = false; for (var i = 0; i < value.length; i += 1) { var ch = value.charAt(i); if (ch === "\\\\" && inQuote) { current += ch + (value.charAt(i + 1) || ""); i += 1; continue; } if (ch === '"') inQuote = !inQuote; if (ch === "," && !inQuote) { out.push(current); current = ""; continue; } current += ch; } out.push(current); return out; }`,
|
|
299476
|
+
'function rfcBase64Decode(value) { var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var clean = String(value).replace(/=+$/, ""); if (clean.length === 0 || /[^A-Za-z0-9+\\/]/.test(clean)) return null; var bits = 0, buffer = 0, out = ""; for (var i = 0; i < clean.length; i += 1) { buffer = (buffer << 6) | alphabet.indexOf(clean.charAt(i)); bits += 6; if (bits >= 8) { bits -= 8; out += String.fromCharCode((buffer >> bits) & 255); } } return out; }',
|
|
299477
|
+
"function rfcSfParse(input, kind) {",
|
|
299478
|
+
" var s = String(input), i = 0;",
|
|
299479
|
+
' function ws() { while (i < s.length && (s.charAt(i) === " " || s.charAt(i) === "\\t")) i += 1; }',
|
|
299480
|
+
" function key() { if (!/[a-z*]/.test(s.charAt(i))) return null; var start = i; i += 1; while (i < s.length && /[a-z0-9_.*-]/.test(s.charAt(i))) i += 1; return s.slice(start, i); }",
|
|
299481
|
+
" function bareItem() {",
|
|
299482
|
+
" var ch = s.charAt(i);",
|
|
299483
|
+
` if (ch === '"') { i += 1; while (i < s.length) { var c = s.charAt(i); if (c === "\\\\") { if (!/["\\\\]/.test(s.charAt(i + 1))) return null; i += 2; continue; } if (c === '"') { i += 1; return true; } if (c < " " || c > "~") return null; i += 1; } return null; }`,
|
|
299484
|
+
' if (ch === ":") { i += 1; var start = i; while (i < s.length && s.charAt(i) !== ":") i += 1; if (s.charAt(i) !== ":") return null; var body = s.slice(start, i); i += 1; return /^[A-Za-z0-9+\\/=]*$/.test(body) ? true : null; }',
|
|
299485
|
+
' if (ch === "?") { i += 1; if (s.charAt(i) !== "0" && s.charAt(i) !== "1") return null; i += 1; return true; }',
|
|
299486
|
+
' if (ch === "@") { i += 1; if (s.charAt(i) === "-") i += 1; if (!/[0-9]/.test(s.charAt(i))) return null; while (i < s.length && /[0-9]/.test(s.charAt(i))) i += 1; return true; }',
|
|
299487
|
+
' if (/[-0-9]/.test(ch)) { if (ch === "-") i += 1; if (!/[0-9]/.test(s.charAt(i))) return null; while (i < s.length && /[0-9]/.test(s.charAt(i))) i += 1; if (s.charAt(i) === ".") { i += 1; if (!/[0-9]/.test(s.charAt(i))) return null; while (i < s.length && /[0-9]/.test(s.charAt(i))) i += 1; } return true; }',
|
|
299488
|
+
" if (/[A-Za-z*]/.test(ch)) { i += 1; while (i < s.length && /[!#$%&'*+.^_`|~:\\/0-9A-Za-z-]/.test(s.charAt(i))) i += 1; return true; }",
|
|
299489
|
+
" return null;",
|
|
299490
|
+
" }",
|
|
299491
|
+
' function params() { while (s.charAt(i) === ";") { i += 1; ws(); if (key() === null) return null; if (s.charAt(i) === "=") { i += 1; if (bareItem() === null) return null; } } return true; }',
|
|
299492
|
+
' function item() { if (s.charAt(i) === "(") { i += 1; ws(); while (s.charAt(i) !== ")") { if (i >= s.length) return null; if (bareItem() === null || params() === null) return null; ws(); } i += 1; return params(); } if (bareItem() === null) return null; return params(); }',
|
|
299493
|
+
" ws();",
|
|
299494
|
+
' if (kind === "item") { if (item() === null) return false; ws(); return i === s.length; }',
|
|
299495
|
+
" if (i === s.length) return true;",
|
|
299496
|
+
" while (i < s.length) {",
|
|
299497
|
+
' if (kind === "dict") { if (key() === null) return false; if (s.charAt(i) === "=") { i += 1; if (item() === null) return false; } else if (params() === null) return false; }',
|
|
299498
|
+
" else if (item() === null) return false;",
|
|
299499
|
+
" ws();",
|
|
299500
|
+
" if (i === s.length) return true;",
|
|
299501
|
+
' if (s.charAt(i) !== ",") return false;',
|
|
299502
|
+
" i += 1; ws();",
|
|
299503
|
+
" if (i === s.length) return false;",
|
|
299504
|
+
" }",
|
|
299505
|
+
" return true;",
|
|
299506
|
+
"}",
|
|
299507
|
+
"pm.test('Response header fields satisfy RFC 9110 field syntax', function () {",
|
|
299508
|
+
" pm.response.headers.each(function (header) {",
|
|
299509
|
+
" if (!header) return;",
|
|
299510
|
+
' if (!rfcIsToken(String(header.key))) pm.expect.fail("Response header name is not a valid RFC 9110 token: " + header.key);',
|
|
299511
|
+
' if (!rfcIsFieldContent(String(header.value))) pm.expect.fail("Response header value contains characters forbidden by RFC 9110 field-content: " + header.key);',
|
|
299512
|
+
" });",
|
|
299513
|
+
' ["content-type", "content-length", "etag", "location", "date", "age", "expires", "last-modified", "retry-after"].forEach(function (name) {',
|
|
299514
|
+
" var values = rfcHeaderAll(name);",
|
|
299515
|
+
' for (var i = 1; i < values.length; i += 1) { if (values[i] !== values[0]) pm.expect.fail("Singleton response header " + name + " appears " + values.length + " times with differing values (RFC 9110)"); }',
|
|
299516
|
+
" });",
|
|
299517
|
+
"});",
|
|
299518
|
+
"pm.test('Response header values satisfy their RFC grammars', function () {",
|
|
299519
|
+
' var date = rfcRespHeader("Date");',
|
|
299520
|
+
' if (date && !rfcIsHttpDate(date)) pm.expect.fail("Date must be an IMF-fixdate (RFC 9110): " + date);',
|
|
299521
|
+
' if (!date) rfcAdvise("RFC 9110: origin servers SHOULD send a Date header");',
|
|
299522
|
+
' var etag = rfcRespHeader("ETag");',
|
|
299523
|
+
' if (etag && !rfcIsEntityTag(etag)) pm.expect.fail("ETag is not a valid entity-tag (RFC 9110): " + etag);',
|
|
299524
|
+
' var lastModified = rfcRespHeader("Last-Modified");',
|
|
299525
|
+
" if (lastModified) {",
|
|
299526
|
+
' if (!rfcIsHttpDate(lastModified)) pm.expect.fail("Last-Modified must be a valid HTTP-date (RFC 9110): " + lastModified);',
|
|
299527
|
+
' else if (date && rfcIsHttpDate(date) && Date.parse(lastModified) > Date.parse(date)) pm.expect.fail("Last-Modified must not be later than Date (RFC 9110): " + lastModified + " > " + date);',
|
|
299528
|
+
" }",
|
|
299529
|
+
' var vary = rfcRespHeader("Vary");',
|
|
299530
|
+
" if (vary) {",
|
|
299531
|
+
' var varyMembers = vary.split(",").map(function (entry) { return entry.trim(); });',
|
|
299532
|
+
' if (varyMembers.indexOf("*") !== -1 && varyMembers.length > 1) pm.expect.fail("Vary: * must not be combined with other members (RFC 9110): " + vary);',
|
|
299533
|
+
' varyMembers.forEach(function (member) { if (member !== "*" && !rfcIsToken(member)) pm.expect.fail("Vary member is not a field-name token (RFC 9110): " + member); });',
|
|
299534
|
+
" }",
|
|
299535
|
+
' var contentLocation = rfcRespHeader("Content-Location");',
|
|
299536
|
+
' if (contentLocation && (/\\s/.test(contentLocation.trim()) || contentLocation.trim().length === 0)) pm.expect.fail("Content-Location must be a valid URI-reference (RFC 9110): " + contentLocation);',
|
|
299537
|
+
' var acceptRanges = rfcRespHeader("Accept-Ranges");',
|
|
299538
|
+
' if (acceptRanges && !rfcTokenList(acceptRanges)) pm.expect.fail("Accept-Ranges must be a list of range-unit tokens (RFC 9110): " + acceptRanges);',
|
|
299539
|
+
' var contentLanguage = rfcRespHeader("Content-Language");',
|
|
299540
|
+
' if (contentLanguage) contentLanguage.split(",").forEach(function (tag) { if (!/^[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*$/.test(tag.trim())) pm.expect.fail("Content-Language carries a malformed BCP 47 language-tag (RFC 5646): " + tag.trim()); });',
|
|
299541
|
+
' var allow = rfcRespHeader("Allow");',
|
|
299542
|
+
' if (allow && allow.trim() && !rfcTokenList(allow)) pm.expect.fail("Allow must be a comma-separated list of method tokens (RFC 9110): " + allow);',
|
|
299543
|
+
' if (allow && contract.method === "OPTIONS" && pm.response.code >= 200 && pm.response.code < 300) {',
|
|
299544
|
+
' var optionsAllowed = allow.split(",").map(function (entry) { return entry.trim().toUpperCase(); });',
|
|
299545
|
+
' (contract.pathMethods || []).forEach(function (method) { if (optionsAllowed.indexOf(method) === -1) pm.expect.fail("Allow on an OPTIONS response must list every method the OpenAPI path declares (RFC 9110); missing " + method + " in: " + allow); });',
|
|
299546
|
+
" }",
|
|
299547
|
+
' var age = rfcRespHeader("Age");',
|
|
299548
|
+
' if (age && !/^[0-9]+$/.test(age.trim())) pm.expect.fail("Age must be a non-negative integer of delta-seconds (RFC 9111): " + age);',
|
|
299549
|
+
' var expires = rfcRespHeader("Expires");',
|
|
299550
|
+
' if (expires && !rfcIsHttpDate(expires)) rfcAdvise("RFC 9111: Expires is not a valid HTTP-date and will be treated as already expired: " + expires);',
|
|
299551
|
+
' if (rfcHeaderAll("warning").length > 0) rfcAdvise("RFC 9111 obsoleted the Warning header; the server still emits it");',
|
|
299552
|
+
' var cacheControl = rfcRespHeader("Cache-Control");',
|
|
299553
|
+
" if (cacheControl) {",
|
|
299554
|
+
" var seenDirectives = {};",
|
|
299555
|
+
" rfcSplitList(cacheControl).forEach(function (entry) {",
|
|
299556
|
+
" var directive = entry.trim();",
|
|
299557
|
+
' if (!directive) { pm.expect.fail("Cache-Control contains an empty directive (RFC 9111): " + cacheControl); return; }',
|
|
299558
|
+
' var eq = directive.indexOf("=");',
|
|
299559
|
+
" var name = (eq === -1 ? directive : directive.slice(0, eq)).trim().toLowerCase();",
|
|
299560
|
+
" var argument = eq === -1 ? undefined : directive.slice(eq + 1).trim();",
|
|
299561
|
+
' if (!rfcIsToken(name)) pm.expect.fail("Cache-Control directive name is not a token (RFC 9111): " + directive);',
|
|
299562
|
+
" seenDirectives[name] = argument === undefined ? true : argument;",
|
|
299563
|
+
' if (["max-age", "s-maxage", "stale-while-revalidate", "stale-if-error"].indexOf(name) !== -1 && (argument === undefined || !/^"?[0-9]+"?$/.test(argument))) pm.expect.fail("Cache-Control " + name + " requires a delta-seconds argument (RFC 9111): " + directive);',
|
|
299564
|
+
" });",
|
|
299565
|
+
' if (seenDirectives["no-store"] && seenDirectives["max-age"] !== undefined) pm.expect.fail("Cache-Control combines no-store with max-age; the directives contradict (RFC 9111): " + cacheControl);',
|
|
299566
|
+
" }",
|
|
299567
|
+
' var acceptPatch = rfcRespHeader("Accept-Patch");',
|
|
299568
|
+
' if (acceptPatch) acceptPatch.split(",").forEach(function (entry) { var parts = mediaParts(entry); if (!parts.type || !parts.subtype) pm.expect.fail("Accept-Patch must be a list of media types (RFC 5789): " + entry.trim()); });',
|
|
299569
|
+
' var deprecation = rfcRespHeader("Deprecation");',
|
|
299570
|
+
' if (deprecation && !/^@-?[0-9]+$/.test(deprecation.trim())) pm.expect.fail("Deprecation must be an RFC 9745 Date structured field (@unix-timestamp): " + deprecation);',
|
|
299571
|
+
' var sunset = rfcRespHeader("Sunset");',
|
|
299572
|
+
" if (sunset) {",
|
|
299573
|
+
' if (!rfcIsHttpDate(sunset)) pm.expect.fail("Sunset must be a valid HTTP-date (RFC 8594): " + sunset);',
|
|
299574
|
+
' else if (Date.parse(sunset) < Date.now()) rfcAdvise("RFC 8594: Sunset date is already in the past: " + sunset);',
|
|
299575
|
+
" }",
|
|
299576
|
+
' var preferenceApplied = rfcRespHeader("Preference-Applied");',
|
|
299577
|
+
" if (preferenceApplied) {",
|
|
299578
|
+
' var requestPrefer = requestHeader("Prefer").toLowerCase();',
|
|
299579
|
+
" rfcSplitList(preferenceApplied).forEach(function (entry) {",
|
|
299580
|
+
' var token = entry.split("=")[0].trim().toLowerCase();',
|
|
299581
|
+
' if (token && requestPrefer.indexOf(token) === -1) pm.expect.fail("Preference-Applied echoes a preference the request never sent (RFC 7240): " + entry.trim());',
|
|
299582
|
+
" });",
|
|
299583
|
+
" }",
|
|
299584
|
+
"});",
|
|
299585
|
+
"pm.test('Response satisfies RFC 9110 message framing requirements', function () {",
|
|
299586
|
+
" var code = pm.response.code;",
|
|
299587
|
+
' if ((code === 204 || code < 200) && rfcRespHeader("Content-Length")) pm.expect.fail("RFC 9110 forbids Content-Length on 1xx and 204 responses");',
|
|
299588
|
+
' if ([301, 302, 303, 307, 308].indexOf(code) !== -1 && !rfcRespHeader("Location")) pm.expect.fail("RFC 9110 expects Location on a " + code + " redirect response");',
|
|
299589
|
+
" if (code === 416) {",
|
|
299590
|
+
' var unsatisfiedRange = rfcRespHeader("Content-Range");',
|
|
299591
|
+
' if (!unsatisfiedRange) pm.expect.fail("RFC 9110 requires Content-Range (unsatisfied-range form) on 416 responses");',
|
|
299592
|
+
' else if (!/^\\S+ \\*\\/[0-9]+$/.test(unsatisfiedRange.trim())) pm.expect.fail("Content-Range on 416 must use the unsatisfied-range form <unit> */<complete-length> (RFC 9110): " + unsatisfiedRange);',
|
|
299593
|
+
" }",
|
|
299594
|
+
" if (code === 206) {",
|
|
299595
|
+
' var contentRange = rfcRespHeader("Content-Range");',
|
|
299596
|
+
' var responseMedia = mediaParts(rfcRespHeader("Content-Type"));',
|
|
299597
|
+
' var isByteranges = responseMedia.type === "multipart" && responseMedia.subtype === "byteranges";',
|
|
299598
|
+
' if (!contentRange && !isByteranges) pm.expect.fail("RFC 9110 requires Content-Range on a single-part 206 response (or multipart/byteranges for multi-range)");',
|
|
299599
|
+
' if (contentRange && isByteranges) pm.expect.fail("RFC 9110 forbids Content-Range on a multipart/byteranges 206 response");',
|
|
299600
|
+
" if (contentRange) {",
|
|
299601
|
+
" var rangeParts = contentRange.trim().match(/^(\\S+) (?:([0-9]+)-([0-9]+)|\\*)\\/([0-9]+|\\*)$/);",
|
|
299602
|
+
' if (!rangeParts) pm.expect.fail("Content-Range is not a valid RFC 9110 range: " + contentRange);',
|
|
299603
|
+
" else {",
|
|
299604
|
+
' if (rangeParts[1] !== "bytes") rfcAdvise("RFC 9110: 206 Content-Range uses a non-bytes range unit: " + rangeParts[1]);',
|
|
299605
|
+
" if (rangeParts[2] !== undefined) {",
|
|
299606
|
+
' if (Number(rangeParts[2]) > Number(rangeParts[3])) pm.expect.fail("Content-Range first-byte-pos must be <= last-byte-pos (RFC 9110): " + contentRange);',
|
|
299607
|
+
' if (rangeParts[4] !== "*" && Number(rangeParts[3]) >= Number(rangeParts[4])) pm.expect.fail("Content-Range last-byte-pos must be < complete-length (RFC 9110): " + contentRange);',
|
|
299608
|
+
" }",
|
|
299609
|
+
" }",
|
|
299610
|
+
" }",
|
|
299611
|
+
" }",
|
|
299612
|
+
' if (code === 407 && !rfcRespHeader("Proxy-Authenticate")) pm.expect.fail("RFC 9110 requires Proxy-Authenticate on 407 responses");',
|
|
299613
|
+
' if (code === 415 && contract.method === "PATCH" && !rfcRespHeader("Accept-Patch")) rfcAdvise("RFC 5789: a 415 response to PATCH SHOULD carry Accept-Patch");',
|
|
299614
|
+
"});",
|
|
299615
|
+
"pm.test('Response media type is acceptable under the request Accept header', function () {",
|
|
299616
|
+
" if (pm.response.code < 200 || pm.response.code >= 300 || isBodyless()) return;",
|
|
299617
|
+
' var accept = requestHeader("Accept");',
|
|
299618
|
+
' if (!accept || accept.indexOf("{{") !== -1) return;',
|
|
299619
|
+
' var actual = mediaParts(rfcRespHeader("Content-Type"));',
|
|
299620
|
+
" if (!actual.type) return;",
|
|
299621
|
+
" var acceptable = false;",
|
|
299622
|
+
" var jsonSoftened = false;",
|
|
299623
|
+
" rfcSplitList(accept).forEach(function (entry) {",
|
|
299624
|
+
" var range = mediaParts(entry);",
|
|
299625
|
+
' var qMatch = entry.match(/;\\s*q\\s*=\\s*"?([0-9.]+)"?/i);',
|
|
299626
|
+
" if (qMatch && Number(qMatch[1]) <= 0) return;",
|
|
299627
|
+
' if ((range.type === "*" && range.subtype === "*") || (range.type === actual.type && (range.subtype === "*" || range.subtype === actual.subtype))) acceptable = true;',
|
|
299628
|
+
' if (range.type === actual.type && range.subtype === "json" && isJsonSubtype(actual.subtype)) jsonSoftened = true;',
|
|
299629
|
+
" });",
|
|
299630
|
+
' if (!acceptable && jsonSoftened) { rfcAdvise("Content negotiation: response " + actual.raw + " is a +json type while the request only accepted application/json"); return; }',
|
|
299631
|
+
' if (!acceptable) pm.expect.fail("Response Content-Type " + actual.raw + " is not acceptable under the request Accept header (RFC 9110): " + accept);',
|
|
299632
|
+
"});",
|
|
299633
|
+
"pm.test('Response body satisfies its media type RFC conventions', function () {",
|
|
299634
|
+
' var contentTypeValue = rfcRespHeader("Content-Type");',
|
|
299635
|
+
" var media = mediaParts(contentTypeValue);",
|
|
299636
|
+
" var text = responseText();",
|
|
299637
|
+
' if (pm.response.code === 406 && !text.trim()) rfcAdvise("RFC 9110: a 406 response SHOULD include a list of available representations");',
|
|
299638
|
+
" if (!text) return;",
|
|
299639
|
+
" if (isJsonSubtype(media.subtype)) {",
|
|
299640
|
+
' if (text.charCodeAt(0) === 65279) pm.expect.fail("RFC 8259 forbids a byte order mark at the start of JSON text");',
|
|
299641
|
+
' var charsetParam = contentTypeValue.match(/charset\\s*=\\s*"?([^";\\s]+)"?/i);',
|
|
299642
|
+
' if (charsetParam) rfcAdvise("RFC 8259 defines no charset parameter for JSON media types; got charset=" + charsetParam[1]);',
|
|
299643
|
+
" }",
|
|
299644
|
+
' if (media.raw === "application/x-ndjson" || media.raw === "application/jsonl" || media.raw === "application/x-jsonlines") {',
|
|
299645
|
+
' text.split(/\\r?\\n/).forEach(function (line, lineNumber) { if (!line.trim()) return; try { JSON.parse(line); } catch (error) { pm.expect.fail("NDJSON line " + (lineNumber + 1) + " is not valid JSON: " + error); } });',
|
|
299646
|
+
" }",
|
|
299647
|
+
' if (media.raw === "text/event-stream") {',
|
|
299648
|
+
' text.split(/\\r?\\n/).forEach(function (line) { if (!line || line.charAt(0) === ":") return; var field = line.split(":")[0]; if (["data", "event", "id", "retry"].indexOf(field) === -1) pm.expect.fail("SSE line does not start with a known field or comment: " + line); if (field === "retry" && !/^retry:\\s*[0-9]+\\s*$/.test(line)) pm.expect.fail("SSE retry field must be an integer: " + line); });',
|
|
299649
|
+
" }",
|
|
299650
|
+
' if (media.type === "multipart") {',
|
|
299651
|
+
' var boundary = contentTypeValue.match(/;\\s*boundary=(?:"([^"]*)"|([^;]*))/i);',
|
|
299652
|
+
' var boundaryValue = boundary ? (boundary[1] !== undefined ? boundary[1] : boundary[2].trim()) : "";',
|
|
299653
|
+
' if (!boundaryValue) pm.expect.fail("multipart responses must carry a boundary parameter (RFC 2046): " + contentTypeValue);',
|
|
299654
|
+
" else {",
|
|
299655
|
+
' if (boundaryValue.length > 70) pm.expect.fail("multipart boundary must be 1-70 characters (RFC 2046): " + boundaryValue);',
|
|
299656
|
+
` if (!/^[0-9A-Za-z'()+_,\\-.\\/:=? ]*[0-9A-Za-z'()+_,\\-.\\/:=?]$/.test(boundaryValue)) pm.expect.fail("multipart boundary contains characters outside RFC 2046 bchars or ends with a space: " + boundaryValue);`,
|
|
299657
|
+
" }",
|
|
299658
|
+
" }",
|
|
299659
|
+
' if (media.raw === "application/hal+json") {',
|
|
299660
|
+
" var hal; try { hal = JSON.parse(text); } catch (error) { hal = null; }",
|
|
299661
|
+
' if (hal && typeof hal === "object" && !Array.isArray(hal)) {',
|
|
299662
|
+
" var halLinks = hal._links;",
|
|
299663
|
+
' if (halLinks !== undefined && (typeof halLinks !== "object" || Array.isArray(halLinks) || halLinks === null)) pm.expect.fail("HAL _links must be an object of link relations");',
|
|
299664
|
+
' if (halLinks) Object.keys(halLinks).forEach(function (rel) { var linkValue = halLinks[rel]; (Array.isArray(linkValue) ? linkValue : [linkValue]).forEach(function (linkObject) { if (!linkObject || typeof linkObject !== "object" || typeof linkObject.href !== "string") pm.expect.fail("HAL link relation " + rel + " must be a Link Object (or array of them) with a string href"); }); });',
|
|
299665
|
+
" var halEmbedded = hal._embedded;",
|
|
299666
|
+
' if (halEmbedded !== undefined && (typeof halEmbedded !== "object" || Array.isArray(halEmbedded) || halEmbedded === null)) pm.expect.fail("HAL _embedded must be an object of resource names");',
|
|
299667
|
+
" }",
|
|
299668
|
+
" }",
|
|
299669
|
+
' if (media.raw === "application/vnd.api+json") {',
|
|
299670
|
+
" var jsonApi; try { jsonApi = JSON.parse(text); } catch (error) { jsonApi = null; }",
|
|
299671
|
+
' if (jsonApi && typeof jsonApi === "object" && !Array.isArray(jsonApi)) {',
|
|
299672
|
+
' if (jsonApi.data === undefined && jsonApi.errors === undefined && jsonApi.meta === undefined) pm.expect.fail("JSON:API documents must contain at least one of data, errors, meta");',
|
|
299673
|
+
' if (jsonApi.data !== undefined && jsonApi.errors !== undefined) pm.expect.fail("JSON:API forbids data and errors in the same document");',
|
|
299674
|
+
" }",
|
|
299675
|
+
" }",
|
|
299676
|
+
' if (media.subtype === "problem+xml") {',
|
|
299677
|
+
' if (text.indexOf("urn:ietf:rfc:7807") === -1) rfcAdvise("application/problem+xml body does not reference the urn:ietf:rfc:7807 namespace");',
|
|
299678
|
+
" var xmlStatus = text.match(/<status[^>]*>\\s*([0-9]+)\\s*<\\/status>/);",
|
|
299679
|
+
' if (xmlStatus && Number(xmlStatus[1]) !== pm.response.code) pm.expect.fail("RFC 9457 status member (" + xmlStatus[1] + ") must match the HTTP status code (" + pm.response.code + ")");',
|
|
299680
|
+
" }",
|
|
299681
|
+
"});",
|
|
299682
|
+
"pm.test('Structured field response headers parse per RFC 8941', function () {",
|
|
299683
|
+
' [["Cache-Status", "list"], ["Proxy-Status", "list"], ["Priority", "dict"], ["RateLimit", "dict"], ["RateLimit-Policy", "dict"], ["Signature", "dict"], ["Signature-Input", "dict"]].forEach(function (pair) {',
|
|
299684
|
+
' var value = rfcHeaderAll(pair[0]).join(", ");',
|
|
299685
|
+
" if (!value) return;",
|
|
299686
|
+
' if (!rfcSfParse(value, pair[1])) pm.expect.fail(pair[0] + " is not a valid RFC 8941 structured field (" + pair[1] + "): " + value);',
|
|
299687
|
+
" });",
|
|
299688
|
+
"});",
|
|
299689
|
+
"pm.test('Content-Digest and Repr-Digest match the response body (RFC 9530)', function () {",
|
|
299690
|
+
" var cryptoLib = null;",
|
|
299691
|
+
' try { cryptoLib = require("crypto-js"); } catch (error) { cryptoLib = null; }',
|
|
299692
|
+
' ["Content-Digest", "Repr-Digest"].forEach(function (name) {',
|
|
299693
|
+
" var value = rfcRespHeader(name);",
|
|
299694
|
+
" if (!value) return;",
|
|
299695
|
+
' if (!rfcSfParse(value, "dict")) { pm.expect.fail(name + " is not a valid RFC 8941 dictionary (RFC 9530): " + value); return; }',
|
|
299696
|
+
' if (!cryptoLib || rfcRespHeader("Content-Encoding")) return;',
|
|
299697
|
+
' var media = mediaParts(rfcRespHeader("Content-Type"));',
|
|
299698
|
+
' if (media.type !== "text" && !isJsonSubtype(media.subtype) && !/xml$/.test(media.subtype)) return;',
|
|
299699
|
+
" rfcSplitList(value).forEach(function (entry) {",
|
|
299700
|
+
" var match = entry.trim().match(/^(sha-256|sha-512)=:([A-Za-z0-9+\\/=]+):$/);",
|
|
299701
|
+
" if (!match) return;",
|
|
299702
|
+
' var computed = match[1] === "sha-256" ? cryptoLib.SHA256(responseText()) : cryptoLib.SHA512(responseText());',
|
|
299703
|
+
" var encoded = cryptoLib.enc.Base64.stringify(computed);",
|
|
299704
|
+
' if (encoded !== match[2]) pm.expect.fail(name + " " + match[1] + " does not match the response body (RFC 9530): computed " + encoded + " but header carries " + match[2]);',
|
|
299705
|
+
" });",
|
|
299706
|
+
" });",
|
|
299707
|
+
"});",
|
|
299708
|
+
"pm.test('Request credentials are well-formed per their authentication scheme RFCs', function () {",
|
|
299709
|
+
' var authorization = requestHeader("Authorization");',
|
|
299710
|
+
' if (authorization && authorization.indexOf("{{") === -1) {',
|
|
299711
|
+
" var schemeMatch = authorization.match(/^(\\S+)(?:\\s+([\\s\\S]*))?$/);",
|
|
299712
|
+
' var authScheme = schemeMatch ? schemeMatch[1].toLowerCase() : "";',
|
|
299713
|
+
' var authParams = schemeMatch && schemeMatch[2] !== undefined ? schemeMatch[2].trim() : "";',
|
|
299714
|
+
' if (authScheme === "basic") {',
|
|
299715
|
+
" var decoded = rfcBase64Decode(authParams);",
|
|
299716
|
+
' if (decoded === null) pm.expect.fail("Basic credentials must be base64 (RFC 7617)");',
|
|
299717
|
+
' else if (decoded.indexOf(":") === -1) pm.expect.fail("Basic credentials must decode to user-id:password (RFC 7617)");',
|
|
299718
|
+
" }",
|
|
299719
|
+
' if (authScheme === "bearer" && !/^[A-Za-z0-9\\-._~+\\/]+=*$/.test(authParams)) pm.expect.fail("Bearer token does not match the b64token grammar (RFC 6750)");',
|
|
299720
|
+
' if (authScheme === "digest") {',
|
|
299721
|
+
' rfcSplitList(authParams).forEach(function (entry) { var param = entry.trim(); if (param && !/^[!#$%&\'*+.^_`|~0-9A-Za-z-]+\\s*=\\s*("([^"\\\\]|\\\\.)*"|[!#$%&\'*+.^_`|~0-9A-Za-z-]+)$/.test(param)) pm.expect.fail("Digest auth-param is malformed (RFC 7616): " + param); });',
|
|
299722
|
+
' var digestResponse = authParams.match(/\\bresponse\\s*=\\s*"?([^",\\s]+)"?/i);',
|
|
299723
|
+
' if (digestResponse && !/^[0-9a-fA-F]+$/.test(digestResponse[1])) pm.expect.fail("Digest response parameter must be hex (RFC 7616): " + digestResponse[1]);',
|
|
299724
|
+
" }",
|
|
299725
|
+
" }",
|
|
299726
|
+
' var wantsJwt = (contract.security || []).some(function (alternative) { return alternative.some(function (check) { return check.prefix === "Bearer " && String(check.bearerFormat || "").toUpperCase() === "JWT"; }); });',
|
|
299727
|
+
' if (wantsJwt && authorization && authorization.indexOf("{{") === -1 && authorization.toLowerCase().indexOf("bearer ") === 0) {',
|
|
299728
|
+
" var jwtToken = authorization.slice(7).trim();",
|
|
299729
|
+
' var jwtSegments = jwtToken.split(".");',
|
|
299730
|
+
' if (jwtSegments.length !== 3) pm.expect.fail("bearerFormat JWT tokens must have three base64url segments (RFC 7519); got " + jwtSegments.length);',
|
|
299731
|
+
' else if (!jwtSegments.every(function (segment) { return /^[A-Za-z0-9_-]+$/.test(segment); })) pm.expect.fail("JWT segments must be base64url (RFC 7515)");',
|
|
299732
|
+
" else {",
|
|
299733
|
+
' var jwtDecode = function (segment) { var padded = segment.replace(/-/g, "+").replace(/_/g, "/"); while (padded.length % 4 !== 0) padded += "="; return rfcBase64Decode(padded); };',
|
|
299734
|
+
" var jwtHeader = null; var jwtPayload = null;",
|
|
299735
|
+
' try { jwtHeader = JSON.parse(jwtDecode(jwtSegments[0])); } catch (error) { pm.expect.fail("JWT header segment does not decode to JSON (RFC 7515)"); }',
|
|
299736
|
+
' try { jwtPayload = JSON.parse(jwtDecode(jwtSegments[1])); } catch (error) { pm.expect.fail("JWT payload segment does not decode to JSON (RFC 7519)"); }',
|
|
299737
|
+
' if (jwtHeader && typeof jwtHeader.alg !== "string") pm.expect.fail("JWT header must carry a string alg member (RFC 7515)");',
|
|
299738
|
+
" if (jwtPayload) {",
|
|
299739
|
+
' ["exp", "nbf", "iat"].forEach(function (claim) { if (jwtPayload[claim] !== undefined && typeof jwtPayload[claim] !== "number") pm.expect.fail("JWT " + claim + " claim must be numeric (RFC 7519)"); });',
|
|
299740
|
+
' if (typeof jwtPayload.exp === "number" && jwtPayload.exp * 1000 < Date.now()) rfcAdvise("RFC 7519: the outgoing JWT exp claim is already in the past");',
|
|
299741
|
+
" }",
|
|
299742
|
+
" }",
|
|
299743
|
+
" }",
|
|
299744
|
+
' if (hasQueryParam("access_token")) rfcAdvise("RFC 6750: bearer tokens SHOULD NOT travel in the query string");',
|
|
299745
|
+
" (contract.security || []).forEach(function (alternative) {",
|
|
299746
|
+
" alternative.forEach(function (check) {",
|
|
299747
|
+
" if (!check.checkable || !check.name) return;",
|
|
299748
|
+
' if (check.in === "query") { if (hasQueryParam(check.name)) rfcAdvise("Security scheme " + check.scheme + " sends credentials in the query string"); return; }',
|
|
299749
|
+
' if (check.in !== "header" || String(check.name).toLowerCase() === "authorization") return;',
|
|
299750
|
+
" var apiKeyValue = requestHeader(check.name);",
|
|
299751
|
+
' if (!apiKeyValue || apiKeyValue.indexOf("{{") !== -1) return;',
|
|
299752
|
+
' if (apiKeyValue !== apiKeyValue.trim()) pm.expect.fail("API key header " + check.name + " carries leading or trailing whitespace");',
|
|
299753
|
+
' if (!rfcIsFieldContent(apiKeyValue)) pm.expect.fail("API key header " + check.name + " contains characters forbidden by RFC 9110 field-content");',
|
|
299754
|
+
" });",
|
|
299755
|
+
" });",
|
|
299756
|
+
"});",
|
|
299757
|
+
"pm.test('Request preconditions, preferences, and patch bodies follow their RFCs', function () {",
|
|
299758
|
+
' ["If-Match", "If-None-Match"].forEach(function (name) {',
|
|
299759
|
+
" var value = requestHeader(name);",
|
|
299760
|
+
' if (!value || value.indexOf("{{") !== -1 || value.trim() === "*") return;',
|
|
299761
|
+
' rfcSplitList(value).forEach(function (entry) { if (entry.trim() && !rfcIsEntityTag(entry.trim())) pm.expect.fail(name + " must be * or a list of entity-tags (RFC 9110): " + entry.trim()); });',
|
|
299762
|
+
" });",
|
|
299763
|
+
' var prefer = requestHeader("Prefer");',
|
|
299764
|
+
' if (prefer && prefer.indexOf("{{") === -1) {',
|
|
299765
|
+
' rfcSplitList(prefer).forEach(function (entry) { var token = entry.split("=")[0].split(";")[0].trim(); if (token && !rfcIsToken(token)) pm.expect.fail("Prefer preference name must be a token (RFC 7240): " + entry.trim()); });',
|
|
299766
|
+
" }",
|
|
299767
|
+
' var requestContentType = mediaBase(requestHeader("Content-Type"));',
|
|
299768
|
+
" var body = pm.request.body;",
|
|
299769
|
+
' var raw = body && body.mode === "raw" && typeof body.raw === "string" ? body.raw : "";',
|
|
299770
|
+
' if (!raw.trim() || raw.indexOf("{{") !== -1 || /"<[^"<>]*>"/.test(raw)) return;',
|
|
299771
|
+
' if (requestContentType === "application/json-patch+json") {',
|
|
299772
|
+
' var patch; try { patch = JSON.parse(raw); } catch (error) { if (/<[A-Za-z][A-Za-z0-9_ -]*>/.test(raw)) return; pm.expect.fail("application/json-patch+json request body is not valid JSON (RFC 6902): " + error); return; }',
|
|
299773
|
+
' if (!Array.isArray(patch)) { pm.expect.fail("A JSON Patch document must be an array of operations (RFC 6902)"); return; }',
|
|
299774
|
+
" patch.forEach(function (operation, operationIndex) {",
|
|
299775
|
+
' if (!operation || typeof operation !== "object" || Array.isArray(operation)) { pm.expect.fail("JSON Patch operation " + operationIndex + " must be an object (RFC 6902)"); return; }',
|
|
299776
|
+
' if (["add", "remove", "replace", "move", "copy", "test"].indexOf(operation.op) === -1) pm.expect.fail("JSON Patch operation " + operationIndex + " has an invalid op (RFC 6902): " + operation.op);',
|
|
299777
|
+
" var pointerPattern = /^(\\/([^\\/~]|~[01])*)*$/;",
|
|
299778
|
+
' if (typeof operation.path !== "string" || !pointerPattern.test(operation.path)) pm.expect.fail("JSON Patch operation " + operationIndex + " path must be an RFC 6901 JSON Pointer");',
|
|
299779
|
+
' if (["add", "replace", "test"].indexOf(operation.op) !== -1 && operation.value === undefined) pm.expect.fail("JSON Patch " + operation.op + " operation " + operationIndex + " requires a value member (RFC 6902)");',
|
|
299780
|
+
' if (["move", "copy"].indexOf(operation.op) !== -1 && (typeof operation.from !== "string" || !pointerPattern.test(operation.from))) pm.expect.fail("JSON Patch " + operation.op + " operation " + operationIndex + " requires an RFC 6901 from pointer (RFC 6902)");',
|
|
299781
|
+
" });",
|
|
299782
|
+
" }",
|
|
299783
|
+
' if (requestContentType === "application/merge-patch+json") {',
|
|
299784
|
+
' try { JSON.parse(raw); } catch (error) { if (/<[A-Za-z][A-Za-z0-9_ -]*>/.test(raw)) return; pm.expect.fail("application/merge-patch+json request body must be valid JSON (RFC 7386): " + error); }',
|
|
299785
|
+
" }",
|
|
299786
|
+
"});",
|
|
299787
|
+
"pm.test('Deprecated operation signals deprecation in the response', function () {",
|
|
299788
|
+
" if (!contract.deprecated) return;",
|
|
299789
|
+
' if (!rfcRespHeader("Deprecation") && !rfcRespHeader("Sunset")) rfcAdvise("RFC 9745: the OpenAPI document deprecates this operation but the response carries neither Deprecation nor Sunset");',
|
|
299790
|
+
"});",
|
|
299791
|
+
"pm.test('OpenAPI link expressions resolve against the response', function () {",
|
|
299792
|
+
" if (!selected || !selected.value.links || selected.value.links.length === 0) return;",
|
|
299793
|
+
" var linkBody = null; var linkBodyParsed = false;",
|
|
299794
|
+
" selected.value.links.forEach(function (expression) {",
|
|
299795
|
+
' if (expression.kind === "header") {',
|
|
299796
|
+
' if (!pm.response.headers.get(expression.header)) pm.expect.fail("OpenAPI link " + expression.link + " references response header " + expression.header + " which is absent");',
|
|
299797
|
+
" return;",
|
|
299798
|
+
" }",
|
|
299799
|
+
" if (!linkBodyParsed) { linkBodyParsed = true; try { linkBody = JSON.parse(responseText()); } catch (error) { linkBody = null; } }",
|
|
299800
|
+
' if (linkBody === null) { pm.expect.fail("OpenAPI link " + expression.link + " references the response body but the body is not JSON"); return; }',
|
|
299801
|
+
" var target = linkBody;",
|
|
299802
|
+
' var tokens = String(expression.pointer).split("/").slice(1).map(function (token) { return token.replace(/~1/g, "/").replace(/~0/g, "~"); });',
|
|
299803
|
+
" for (var t = 0; t < tokens.length; t += 1) {",
|
|
299804
|
+
' if (target !== null && typeof target === "object") target = Array.isArray(target) ? target[Number(tokens[t])] : target[tokens[t]];',
|
|
299805
|
+
" else { target = undefined; break; }",
|
|
299806
|
+
" }",
|
|
299807
|
+
' if (target === undefined) pm.expect.fail("OpenAPI link " + expression.link + " expression $response.body#" + expression.pointer + " does not resolve in the response body");',
|
|
299808
|
+
" });",
|
|
299809
|
+
"});",
|
|
299810
|
+
"pm.test('Request URL conforms to an OpenAPI servers entry', function () {",
|
|
299811
|
+
" if (!contract.servers || contract.servers.length === 0) return;",
|
|
299812
|
+
' var requestUrl = "";',
|
|
299813
|
+
' try { requestUrl = String(pm.request.url.toString()); } catch (ignored) { requestUrl = ""; }',
|
|
299814
|
+
' if (!requestUrl || requestUrl.indexOf("{{") !== -1) return;',
|
|
299815
|
+
' var pathOnly = requestUrl.replace(/^[a-z][a-z0-9+.-]*:\\/\\/[^\\/]+/i, "");',
|
|
299816
|
+
' var matched = contract.servers.some(function (pattern) { try { var serverPattern = new RegExp(pattern, "i"); return serverPattern.test(requestUrl) || serverPattern.test(pathOnly); } catch (ignored) { return true; } });',
|
|
299817
|
+
' if (!matched) rfcAdvise("Request URL does not match any OpenAPI servers entry: " + requestUrl);',
|
|
299818
|
+
"});",
|
|
299247
299819
|
...operation.security ? [
|
|
299248
299820
|
"pm.test('Request carries credentials required by OpenAPI security', function () {",
|
|
299249
299821
|
" function satisfied(check) {",
|
|
@@ -299283,6 +299855,12 @@ function createContractScript(operation, warnings = []) {
|
|
|
299283
299855
|
' if (param.allowEmptyValue && entries.length === 1 && entries[0] === "") return;',
|
|
299284
299856
|
" if (entries.some(isPlaceholder)) return;",
|
|
299285
299857
|
" value = entries.map(function (entry) { return coerceBySchema(decodeComponent(entry), param.items); });",
|
|
299858
|
+
' } else if (param.decode === "deepObject") {',
|
|
299859
|
+
" var deepValue = {}; var deepFound = false; var deepPlaceholder = false;",
|
|
299860
|
+
' pm.request.url.query.each(function (queryParam) { if (!queryParam || queryParam.disabled === true) return; var deepMatch = String(queryParam.key).match(/^([^\\[]+)\\[([^\\]]+)\\]$/); if (!deepMatch || deepMatch[1].toLowerCase() !== String(param.name).toLowerCase()) return; deepFound = true; var deepRaw = queryParam.value === null || queryParam.value === undefined ? "" : String(queryParam.value); if (isPlaceholder(deepRaw)) { deepPlaceholder = true; return; } deepValue[deepMatch[2]] = coerceBySchema(decodeComponent(deepRaw), (param.schema && param.schema.properties && param.schema.properties[deepMatch[2]]) || {}); });',
|
|
299861
|
+
' if (!deepFound) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
|
|
299862
|
+
" if (deepPlaceholder) return;",
|
|
299863
|
+
" value = deepValue;",
|
|
299286
299864
|
" } else if (param.decode) {",
|
|
299287
299865
|
' var joined = param.in === "query" ? queryValue(String(param.name).toLowerCase()) : headerValue(param.name);',
|
|
299288
299866
|
' if (joined === undefined) { if (param.required) pm.expect.fail("Required parameter " + param.in + ":" + param.name + " was not sent for " + contract.method + " " + contract.path); return; }',
|
|
@@ -299299,6 +299877,8 @@ function createContractScript(operation, warnings = []) {
|
|
|
299299
299877
|
' } else if (param.in === "path") {',
|
|
299300
299878
|
" value = pathParamValue(param.name);",
|
|
299301
299879
|
" if (value === undefined) return;",
|
|
299880
|
+
' if (param.pathStyle === "label") { if (String(value).charAt(0) !== ".") return; value = String(value).slice(1); }',
|
|
299881
|
+
' if (param.pathStyle === "matrix") { var matrixPrefix = ";" + param.name + "="; if (String(value).indexOf(matrixPrefix) !== 0) return; value = String(value).slice(matrixPrefix.length); }',
|
|
299302
299882
|
' if (isPlaceholder(value) || value.charAt(0) === ":" || value.charAt(0) === "{") return;',
|
|
299303
299883
|
" value = coerceBySchema(value, param.schema);",
|
|
299304
299884
|
' } else if (param.in === "cookie") {',
|
|
@@ -299350,6 +299930,9 @@ function createContractScript(operation, warnings = []) {
|
|
|
299350
299930
|
' if (pm.response.headers.get("Content-Encoding")) return;',
|
|
299351
299931
|
" var mustBeEmpty = isBodyless() || (selected && Object.keys(selected.value.content || {}).length === 0);",
|
|
299352
299932
|
' if (mustBeEmpty && Number(String(raw).trim()) !== 0) pm.expect.fail("OpenAPI defines no response body for " + contract.method + " " + contract.path + " status " + pm.response.code + " but Content-Length was " + raw);',
|
|
299933
|
+
"});",
|
|
299934
|
+
"pm.test('RFC SHOULD-level advisories are documented', function () {",
|
|
299935
|
+
' pm.expect(rfcAdvisories, "SHOULD-level findings (advisory, non-failing): " + rfcAdvisories.join("; ")).to.be.an("array");',
|
|
299353
299936
|
"});"
|
|
299354
299937
|
];
|
|
299355
299938
|
}
|
|
@@ -299419,6 +300002,9 @@ function assertStaticRequestShape(operation, request) {
|
|
|
299419
300002
|
}
|
|
299420
300003
|
}
|
|
299421
300004
|
const warnings = collectStaticBodyWarnings(operation, request, contentType2);
|
|
300005
|
+
if (["GET", "HEAD", "DELETE"].includes(operation.method) && hasRequestBody(request)) {
|
|
300006
|
+
warnings.push(`CONTRACT_METHOD_BODY_SEMANTICS: ${operation.id} sends a request body with ${operation.method}; RFC 9110 defines no request-body semantics for this method`);
|
|
300007
|
+
}
|
|
299422
300008
|
if (operation.requestBody && !operation.requestBody.required && operation.requestBody.contentTypes.length > 0 && hasRequestBody(request) && contentType2) {
|
|
299423
300009
|
const actual = contentType2.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
299424
300010
|
const matches = operation.requestBody.contentTypes.some((expected) => mediaTypeMatchesPattern(expected.toLowerCase(), actual));
|