diff --git a/.secrets.baseline b/.secrets.baseline
index 6b603402..97e9df08 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -3,7 +3,7 @@
"files": "package-lock.json|^.secrets.baseline$",
"lines": null
},
- "generated_at": "2024-12-16T19:27:38Z",
+ "generated_at": "2024-12-19T16:14:03Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
diff --git a/docs/ibm-cloud-rules.md b/docs/ibm-cloud-rules.md
index 7193f980..09c5d0bf 100644
--- a/docs/ibm-cloud-rules.md
+++ b/docs/ibm-cloud-rules.md
@@ -114,6 +114,7 @@ which is delivered in the `@ibm-cloud/openapi-ruleset` NPM package.
* [ibm-summary-sentence-style](#ibm-summary-sentence-style)
* [ibm-unevaluated-properties](#ibm-unevaluated-properties)
* [ibm-unique-parameter-request-property-names](#ibm-unique-parameter-request-property-names)
+ * [ibm-use-date-based-format](#ibm-use-date-based-format)
* [ibm-valid-path-segments](#ibm-valid-path-segments)
* [ibm-well-defined-dictionaries](#ibm-well-defined-dictionaries)
@@ -675,6 +676,12 @@ specific "allow-listed" keywords.
oas3 |
+ibm-use-date-based-format |
+warning |
+Checks each schema and heuristically determines if it should be a string schema that uses a format of "date" or "date-time". |
+oas3 |
+
+
ibm-valid-path-segments |
error |
Checks each path string in the API to make sure path parameter references are valid within path segments. |
@@ -7197,6 +7204,66 @@ paths:
+### ibm-use-date-based-format
+
+
+Rule id: |
+ibm-use-date-based-format |
+
+
+Description: |
+ Schemas or properties that are date-based (i.e. the values they model
+are dates or times) must be strings with a format of "date" or "date-time".
+This rule validates that is the case for relevant schemas, which are determined
+heuristically using the property name, in the case of schema properties, or
+the example value provided for a schema or property.
+ |
+
+
+Severity: |
+warning |
+
+
+OAS Versions: |
+oas3 |
+
+
+Non-compliant example: |
+
+
+Resource
+ type: object
+ properties:
+ created_at: # Name indicates it should be a date or date-time
+ type: integer
+ stamp: # Example value indicates it should be a date-time
+ type: string
+ example: '1990-12-31T23:59:60Z'
+ ...
+
+ |
+
+
+Compliant example: |
+
+
+Resource
+ type: object
+ properties:
+ created_at:
+ type: string
+ format: date-time
+ stamp:
+ type: string
+ format: date-time
+ example: '1990-12-31T23:59:60Z'
+ ...
+
+ |
+
+
+
+
### ibm-valid-path-segments
diff --git a/packages/ruleset/src/functions/index.js b/packages/ruleset/src/functions/index.js
index 2c6df832..3c4c8cbf 100644
--- a/packages/ruleset/src/functions/index.js
+++ b/packages/ruleset/src/functions/index.js
@@ -78,6 +78,7 @@ module.exports = {
unevaluatedProperties: require('./unevaluated-properties'),
uniqueParameterRequestPropertyNames: require('./unique-parameter-request-property-names'),
unusedTags: require('./unused-tags'),
+ useDateBasedFormat: require('./use-date-based-format'),
validatePathSegments: require('./valid-path-segments'),
wellDefinedDictionaries: require('./well-defined-dictionaries'),
};
diff --git a/packages/ruleset/src/functions/no-ambiguous-paths.js b/packages/ruleset/src/functions/no-ambiguous-paths.js
index 68177d00..1380c1d2 100644
--- a/packages/ruleset/src/functions/no-ambiguous-paths.js
+++ b/packages/ruleset/src/functions/no-ambiguous-paths.js
@@ -27,7 +27,7 @@ module.exports = function (paths, _options, context) {
* 1. "/v1/clouds/{id}", "/v1/clouds/{cloud_id}"
* 2. "/v1/clouds/foo", "/v1/clouds/{cloud_id}"
* 3. "/v1/{resource_type}/foo", "/v1/users/{user_id}"
- * @param {*} apidef the entire API definition
+ * @param {*} paths map containing all path objects
* @returns an array containing zero or more error objects
*/
function checkAmbiguousPaths(paths) {
diff --git a/packages/ruleset/src/functions/use-date-based-format.js b/packages/ruleset/src/functions/use-date-based-format.js
new file mode 100644
index 00000000..d1a8729c
--- /dev/null
+++ b/packages/ruleset/src/functions/use-date-based-format.js
@@ -0,0 +1,312 @@
+/**
+ * Copyright 2024 IBM Corporation.
+ * SPDX-License-Identifier: Apache2.0
+ */
+
+const {
+ getExamplesForSchema,
+ isDateSchema,
+ isDateTimeSchema,
+ isIntegerSchema,
+ isObject,
+ isStringSchema,
+ validateNestedSchemas,
+} = require('@ibm-cloud/openapi-ruleset-utilities');
+
+const {
+ LoggerFactory,
+ isDateBasedName,
+ isDateBasedValue,
+ isParamContentSchema,
+ isParamSchema,
+ isRequestBodySchema,
+ isResponseSchema,
+ isSchemaProperty,
+} = require('../utils');
+
+let ruleId;
+let logger;
+
+/**
+ * The implementation for this rule makes assumptions that are dependent on the
+ * presence of the following other rules:
+ *
+ * - oas3-valid-media-example
+ * - oas3-valid-schema-example
+ *
+ * These rules verify that the correct, specific format (date vs date-time) is
+ * used for schemas based on their example value. So, we aren't as specific
+ * with that check in this rule - we recommend either "date" or "date-time".
+ */
+
+module.exports = function (schema, _opts, context) {
+ if (!logger) {
+ ruleId = context.rule.name;
+ logger = LoggerFactory.getInstance().getLogger(ruleId);
+ }
+
+ return checkForDateBasedFormat(
+ schema,
+ context.path,
+ context.documentInventory.resolved
+ );
+};
+
+/**
+ * This function implements a rule that enforces date-based schemas use either
+ * the "date" or "date-time" format, so that they're accurately documented as
+ * date-based logical types. We use a heuristic based on either the name of a
+ * schema (derived from the property name, if the schema is a property schema)
+ * or the example value provided for a given schema or schema property.
+ *
+ * The logic here recursively checks all schemas for the presence of unmarked
+ * date-based schemas. As it traverses the schemas, it compiles a list of
+ * potentially-relevant example values. This way, if an object schema defines
+ * its own example, which includes a value for a nested property that should
+ * be identified by the rule, we can track down the value once we reach the
+ * schema for said property. The logic will also gather any relevant parameter
+ * or media type examples that may be defined outside of the schema path.
+ *
+ * @param {object} s the schema to check
+ * @param {array} p the array of path segments indicating the "location" of the schema within the API definition
+ * @param {object} apidef the resolved API definition
+ * @returns an array containing the violations found or [] if no violations
+ */
+function checkForDateBasedFormat(s, p, apidef) {
+ // Map connecting a list of examples for a schema to its logical path.
+ const examples = {};
+
+ // Check for any examples outside of the schema path - they may be in
+ // request bodies, response bodies, or parameters. Store these separately.
+ const indirectExamples = checkForIndirectExamples(p, apidef);
+
+ return validateNestedSchemas(s, p, (schema, path, logicalPath) => {
+ logger.debug(`${ruleId}: checking schema at location: ${path.join('.')}`);
+ logger.debug(
+ `${ruleId}: logical schema path is : ${logicalPathForLogger(logicalPath)}`
+ );
+
+ // Use a composition-aware utility to gather any examples relevant to this
+ // schema, including those defined on applicator schemas in oneOf, etc.
+ const schemaExamples = getExamplesForSchema(schema);
+ logger.debug(`${ruleId}: ${schemaExamples.length} examples found`);
+
+ // Examples have already been stored on the parent - as we go through the
+ // schemas, check for the presence of an example value for the current
+ // property within the parent's example.
+ const parentalExamples =
+ // If the logical path is empty, there are no parents, but there may be
+ // indirect examples to add in the ":" branch.
+ logicalPath.length > 0
+ ? // Look at the example values for the logical parent schema, if any.
+ // For successive, nested properties, this will end up propagating
+ // examples through the recursive descent so that example values
+ // separated from a property by multiple degrees of nesting will
+ // still be preserved.
+ examples[logicalPath.slice(0, -1).join('.')]
+ .map(e => {
+ // Check the parental example values for
+ // the presence of the current property.
+ const prop = logicalPath.at(-1);
+
+ // Check for sentinel indicating an array.
+ if (prop === '[]' && Array.isArray(e)) {
+ return e;
+ }
+
+ // Check for sentinel indicating a dictionary.
+ if (prop === '*' && isObject(e)) {
+ return Object.values(e);
+ }
+
+ // Standard model path. Wrap value in an array to match list and
+ // dictionary behavior - it will be flattened out later.
+ return [e[prop]];
+ })
+
+ // Each example may map to multiple examples - flatten the result
+ // of the mapping to include all relevant examples in the list.
+ .flat()
+
+ // We are not guaranteed to find a value - filter
+ // out any values that are not defined.
+ .filter(e => e !== undefined)
+ : // Add indirect examples to the map - note that they will necessarily
+ // be indexed with the empty string key, like primary schemas.
+ indirectExamples;
+
+ logger.debug(
+ `${ruleId}: ${parentalExamples.length} examples found in logical parent`
+ );
+
+ // Index the examples with the stringified logical path.
+ // Note that the unconditional assignment is intentional - there may be
+ // existing entries for this same logical path (e.g. for the same
+ // nested property within a different oneOf sibling) but we want to
+ // override them, always. Otherwise, the behavior would depend on the
+ // order the schemas are checked in (the logic may look at more examples
+ // for one instance of a property than another, arbitrarily) and we don't
+ // make a guarantee that order will be stable in `validateNestedSchemas`.
+ examples[logicalPath.join('.')] = [...schemaExamples, ...parentalExamples];
+
+ // Perform the validation using the first value example value found for the
+ // schema at this logical path.
+ const exampleValue = examples[logicalPath.join('.')].find(
+ e => e !== undefined
+ );
+
+ return performValidation(schema, path, exampleValue);
+ });
+}
+
+// This function performs the actual checks against a schema to determine if
+// it should be a "date" or "date-time" schema, but isn't defined as one.
+// It is wrapped in the outer function for the gathering of examples, etc. but
+// this function implements the checks: 1) see if the name of a property
+// indicates that it is a date-based schema and 2) see if the example value for
+// a schema indicates that it is a date-based schema.
+function performValidation(schema, path, exampleValue) {
+ // If this is already a date or date-time schema, no need to check if it should be.
+ if (isDateSchema(schema) || isDateTimeSchema(schema)) {
+ logger.debug(
+ `${ruleId}: skipping date-based schema at location: ${path.join('.')}`
+ );
+
+ return [];
+ }
+
+ // Check if this is a schema property
+ if (isSchemaProperty(path)) {
+ logger.debug(`${ruleId}: detected named property at "${path.join('.')}"`);
+
+ // Check for a name that would indicate the property should be date-based
+ if (isDateBasedName(path.at(-1))) {
+ logger.debug(
+ `${ruleId}: property name at "${path.join('.')}" is date-based`
+ );
+
+ // We only assume a property could be a date-time value if it's a string or integer
+ if (isStringSchema(schema) || isIntegerSchema(schema)) {
+ logger.debug(
+ `${ruleId}: date-based property name at "${path.join(
+ '.'
+ )}" is a string or integer`
+ );
+
+ // If the schema is determined to be a date-time schema by the name alone,
+ // we can return - no need to look for an example value.
+ return [
+ {
+ message:
+ 'According to its name, this property should use type "string" and format "date" or "date-time"',
+ path,
+ },
+ ];
+ }
+ }
+ }
+
+ // Check example values for string schemas.
+ if (isStringSchema(schema)) {
+ if (exampleValue !== undefined) {
+ logger.debug(`${ruleId}: example value found: ${exampleValue}`);
+
+ if (isDateBasedValue(exampleValue)) {
+ return [
+ {
+ message:
+ 'According to its example value, this schema should use type "string" and format "date" or "date-time"',
+ path,
+ },
+ ];
+ }
+ } else {
+ logger.debug(`${ruleId}: no example value found`);
+ }
+ }
+
+ return [];
+}
+
+// This function takes an object, as well as a path to a specific value, and
+// parses the object, looking for the value at that path. If it finds one,
+// the value will be returned. If not, the function will return `undefined`.
+function getObjectValueAtPath(obj, pathToValue) {
+ return pathToValue.reduce((value, field) => value?.[field], obj);
+}
+
+// "Indirect" examples are those coming from request bodies, response bodies, and parameters.
+function checkForIndirectExamples(path, apidef) {
+ logger.debug(
+ `${ruleId}: checking indirect examples for schema at location: ${path.join(
+ '.'
+ )}`
+ );
+
+ // Parameter and Media Type objects have the same format when it comes
+ // to examples, so we can treat all of these scenarios the same way.
+ if (
+ isRequestBodySchema(path) ||
+ isResponseSchema(path) ||
+ isParamSchema(path) ||
+ isParamContentSchema(path)
+ ) {
+ // Example fields would be siblings of the schema we're looking at, so we need to look in the API
+ // for the path, minus the last value (which is "schema").
+ const examples = getOpenApiExamples(
+ getObjectValueAtPath(apidef, path.slice(0, -1))
+ );
+
+ // Check for the special case of looking at a content schema for a parameter that
+ // itself defines an example (pull the last three values off the path to check).
+ if (isParamContentSchema(path)) {
+ examples.push(
+ ...getOpenApiExamples(getObjectValueAtPath(apidef, path.slice(0, -3)))
+ );
+ }
+
+ logger.debug(
+ `${ruleId}: ${
+ examples.length
+ } indirect examples found for schema at location: ${path.join('.')}`
+ );
+
+ // Put the examples in the format the downstream algorithm for this rule needs.
+ return examples;
+ }
+
+ return [];
+}
+
+// OpenAPI defines its own example structure, separate from schema examples,
+// on Parameter and Media Type objects. Use this function to parse those
+// structures and return any relevant examples. The argument may be either a
+// Parameter or Media Type object and will return a list.
+function getOpenApiExamples(artifact) {
+ if (!isObject(artifact)) {
+ return [];
+ }
+
+ // The `example` and `examples` fields are mutually exclusive.
+ if (artifact.example) {
+ return [artifact.example];
+ }
+
+ // This will be a map, potentially containing multiple examples. Return all of them.
+ if (artifact.examples) {
+ return Object.values(artifact.examples).map(
+ exampleObject => exampleObject.value
+ );
+ }
+
+ return [];
+}
+
+// Format the logical path in a way that makes sense when the array is empty.
+function logicalPathForLogger(logicalPath) {
+ if (!logicalPath.length) {
+ return `'' (primary schema)`;
+ }
+
+ return `'${logicalPath.join('.')}'`;
+}
diff --git a/packages/ruleset/src/ibm-oas.js b/packages/ruleset/src/ibm-oas.js
index 2ec859ca..d2055330 100644
--- a/packages/ruleset/src/ibm-oas.js
+++ b/packages/ruleset/src/ibm-oas.js
@@ -197,6 +197,7 @@ module.exports = {
'ibm-unevaluated-properties': ibmRules.unevaluatedProperties,
'ibm-unique-parameter-request-property-names':
ibmRules.uniqueParameterRequestPropertyNames,
+ 'ibm-use-date-based-format': ibmRules.useDateBasedFormat,
'ibm-valid-path-segments': ibmRules.validPathSegments,
'ibm-well-defined-dictionaries': ibmRules.wellDefinedDictionaries,
},
diff --git a/packages/ruleset/src/rules/index.js b/packages/ruleset/src/rules/index.js
index 129db06a..8e0fd15c 100644
--- a/packages/ruleset/src/rules/index.js
+++ b/packages/ruleset/src/rules/index.js
@@ -90,6 +90,7 @@ module.exports = {
unevaluatedProperties: require('./unevaluated-properties'),
unusedTags: require('./unused-tags'),
uniqueParameterRequestPropertyNames: require('./unique-parameter-request-property-names'),
+ useDateBasedFormat: require('./use-date-based-format'),
validPathSegments: require('./valid-path-segments'),
wellDefinedDictionaries: require('./well-defined-dictionaries'),
};
diff --git a/packages/ruleset/src/rules/use-date-based-format.js b/packages/ruleset/src/rules/use-date-based-format.js
new file mode 100644
index 00000000..cf4276e2
--- /dev/null
+++ b/packages/ruleset/src/rules/use-date-based-format.js
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2024 IBM Corporation.
+ * SPDX-License-Identifier: Apache2.0
+ */
+
+const {
+ schemas,
+} = require('@ibm-cloud/openapi-ruleset-utilities/src/collections');
+const { oas3 } = require('@stoplight/spectral-formats');
+const { useDateBasedFormat } = require('../functions');
+
+module.exports = {
+ description:
+ 'Heuristically determine when a schema should have a format of "date" or "date-time"',
+ message: '{{error}}',
+ severity: 'warn',
+ formats: [oas3],
+ resolved: true,
+ given: schemas,
+ then: {
+ function: useDateBasedFormat,
+ },
+};
diff --git a/packages/ruleset/src/utils/date-based-utils.js b/packages/ruleset/src/utils/date-based-utils.js
new file mode 100644
index 00000000..3ac62615
--- /dev/null
+++ b/packages/ruleset/src/utils/date-based-utils.js
@@ -0,0 +1,106 @@
+/**
+ * Copyright 2024 IBM Corporation.
+ * SPDX-License-Identifier: Apache2.0
+ */
+
+/**
+ * This function takes a schema property name and uses a collection of regular
+ * expressions to heuristically determine if the property is a date or a date
+ * time property. It returns `true` if the name matches one of the expressions.
+ *
+ * @param {string} name the name of a schema property
+ * @returns a boolean value indicating that the property seems to be date-based
+ */
+function isDateBasedName(name) {
+ // Check for matching against certain patterns.
+ const dateBasedNamePatterns = [
+ // The name `created`.
+ /^created$/,
+
+ // The name `updated`.
+ /^updated$/,
+
+ // The name `modified`.
+ /^modified$/,
+
+ // The name `expired`.
+ /^expired$/,
+
+ // The name `expires`.
+ /^expires$/,
+
+ // Any name ending in `_at`.
+ /.*_at$/,
+
+ // Any name ending in `_on`.
+ /.*_on$/,
+
+ // Any name starting with `date_`.
+ /^date_.*/,
+
+ // Any name containing `_date_`.
+ /.*_date_.*/,
+
+ // Any name ending in `_date`.
+ /.*_date$/,
+
+ // Any name starting with `time_`.
+ /^time_.*/,
+
+ // Any name containing `_time_`.
+ /.*_time_.*/,
+
+ // Not including any name ending in `_time` because there are
+ // counterexamples, but we still want to catch common date-based
+ // names that end in `_time`.
+ /^start_time$/,
+ /^end_time$/,
+ /^create_time$/,
+ /^created_time$/,
+ /^modify_time$/,
+ /^modified_time$/,
+ /^update_time$/,
+
+ // Any name containing `timestamp`.
+ /.*timestamp.*/,
+ ];
+
+ return dateBasedNamePatterns.some(regex => regex.test(name));
+}
+
+/**
+ * This function takes an example string value and uses a collection of regular
+ * expressions to heuristically determine if the value is a date or a date
+ * time. It returns `true` if the value matches one of the expressions.
+ *
+ * @param {string} value an example value for a schema or schema property
+ * @returns a boolean value indicating that the value seems to be date-based
+ */
+function isDateBasedValue(value) {
+ const regularExpressions = [
+ // Includes abbreviated month name.
+ /^\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\b/,
+
+ // Includes full month name.
+ /^\b(January|February|March|April|May|June|July|August|September|October|November|December)\b/,
+
+ // Includes date in the format YYYY(./-)MM(./-)DD(T).
+ /\b\d{4}[./-](0?[1-9]|1[012])[./-]([012]?[1-9]|3[01])(\b|T)/,
+
+ // Includes date in the format DD(./-)MM(./-)YYYY.
+ /\b([012]?[1-9]|3[01])[./-](0?[1-9]|1[012])[./-]\d{4}\b/,
+
+ // Includes date in the format MM(./-)DD(./-)YYYY.
+ /\b(0?[1-9]|1[012])[./-]([012]?[1-9]|3[01])[./-]\d{4}\b/,
+
+ // Includes time in the format (T)tt:tt:tt (where t can be s/m/h/etc.)
+ /(\b|T)\d\d:\d\d:\d\d\b/,
+ ];
+
+ return regularExpressions.some(r => r.test(value));
+}
+
+module.exports = {
+ isDateBasedName,
+ isDateBasedValue,
+};
diff --git a/packages/ruleset/src/utils/index.js b/packages/ruleset/src/utils/index.js
index 82e2f705..5ea3ea17 100644
--- a/packages/ruleset/src/utils/index.js
+++ b/packages/ruleset/src/utils/index.js
@@ -21,6 +21,7 @@ module.exports = {
operationMethods: require('./constants'),
pathHasMinimallyRepresentedResource: require('./path-has-minimally-represented-resource'),
pathMatchesRegexp: require('./path-matches-regexp'),
+ ...require('./date-based-utils'),
...require('./mimetype-utils'),
...require('./pagination-utils'),
...require('./path-location-utils'),
diff --git a/packages/ruleset/test/rules/required-enum-properties-in-response.test.js b/packages/ruleset/test/rules/required-enum-properties-in-response.test.js
index 2ca1a232..034d092d 100644
--- a/packages/ruleset/test/rules/required-enum-properties-in-response.test.js
+++ b/packages/ruleset/test/rules/required-enum-properties-in-response.test.js
@@ -3,13 +3,13 @@
* SPDX-License-Identifier: Apache2.0
*/
-const { requiredEnumPropertiesInResponse } = require('../src/rules');
+const { requiredEnumPropertiesInResponse } = require('../../src/rules');
const {
makeCopy,
rootDocument,
testRule,
severityCodes,
-} = require('./test-utils');
+} = require('../test-utils');
const rule = requiredEnumPropertiesInResponse;
const ruleId = 'ibm-required-enum-properties-in-response';
diff --git a/packages/ruleset/test/rules/use-date-based-format.test.js b/packages/ruleset/test/rules/use-date-based-format.test.js
new file mode 100644
index 00000000..bf1a33ef
--- /dev/null
+++ b/packages/ruleset/test/rules/use-date-based-format.test.js
@@ -0,0 +1,4185 @@
+/**
+ * Copyright 2024 IBM Corporation.
+ * SPDX-License-Identifier: Apache2.0
+ */
+
+const { useDateBasedFormat } = require('../../src/rules');
+const {
+ makeCopy,
+ rootDocument,
+ testRule,
+ severityCodes,
+} = require('../test-utils');
+
+const rule = useDateBasedFormat;
+const ruleId = 'ibm-use-date-based-format';
+const expectedSeverity = severityCodes.warning;
+const expectedNameMsg =
+ 'According to its name, this property should use type "string" and format "date" or "date-time"';
+const expectedExampleMsg =
+ 'According to its example value, this schema should use type "string" and format "date" or "date-time"';
+
+// To enable debug logging in the rule function, copy this statement to an it() block:
+// LoggerFactory.getInstance().addLoggerSetting(ruleId, 'debug');
+// and uncomment this import statement:
+// const LoggerFactory = require('../../src/utils/logger-factory');
+
+describe(`Spectral rule: ${ruleId}`, () => {
+ describe('Should not yield errors', () => {
+ it('Clean spec', async () => {
+ const results = await testRule(ruleId, rule, rootDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('date/time property ending in _at', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['date', 'date-time'].forEach(format => {
+ ['created_at', 'modified_at', 'updated_at'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ format,
+ };
+ });
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('date/time property ending in _on', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['date', 'date-time'].forEach(format => {
+ ['created_on', 'modified_on', 'expires_on'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ format,
+ };
+ });
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('date/time property containing the word "date"', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['date', 'date-time'].forEach(format => {
+ ['first_date', 'new_date_when', 'date_next'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ format,
+ };
+ });
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('date/time property containing the word "time"', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['date', 'date-time'].forEach(format => {
+ ['a_time_for_updating', 'time_is'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ format,
+ };
+ });
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('date/time property containing the word "timestamp"', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['date', 'date-time'].forEach(format => {
+ ['photo_timestamp', 'photo_timestamp_value', 'timestamp_value'].forEach(
+ propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ format,
+ };
+ }
+ );
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('date/time property with a time-based name', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['date', 'date-time'].forEach(format => {
+ ['created', 'updated', 'modified', 'expired', 'expires'].forEach(
+ propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ format,
+ };
+ }
+ );
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('top level date/time property with example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.some_datetimeprop = {
+ type: 'string',
+ format: 'date-time',
+ example: '1990-12-31T23:59:60Z',
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('nested date/time property with example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ format: 'date-time',
+ example: 'July 3, 2023, 4:15 PM',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('doubly nested date/time property with example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ modification_info: {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ format: 'date-time',
+ example: '2023-07-03T16:15:00+00:00',
+ },
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('date/time items schema with example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'array',
+ items: {
+ type: 'string',
+ format: 'date-time',
+ example: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('date/time dictionary schema with example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ format: 'date-time',
+ example: 'Monday, 03-Jul-23 16:15:00 GMT',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('primitive date/time oneOf schema with example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.first_completed = {
+ oneOf: [
+ {
+ type: 'string',
+ format: 'date-time',
+ example: '01/01/2023',
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+
+ it('date/time property nested in oneOf schema with example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ oneOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ format: 'date-time',
+ example: 'Oct. 31',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(0);
+ });
+ });
+
+ describe('Should yield errors', () => {
+ describe('Should detect date times based on names', () => {
+ it('string property ending in _at', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['created_at', 'modified_at', 'updated_at'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ };
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(12);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created_at',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified_at',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.updated_at',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created_at',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified_at',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.updated_at',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created_at',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified_at',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.updated_at',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created_at',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified_at',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.updated_at',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('integer property ending in _at', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['created_at', 'modified_at', 'updated_at'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'integer',
+ };
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(12);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created_at',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified_at',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.updated_at',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created_at',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified_at',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.updated_at',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created_at',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified_at',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.updated_at',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created_at',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified_at',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.updated_at',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('string property ending in _on', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['created_on', 'modified_on', 'expires_on'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ };
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(12);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created_on',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expires_on',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified_on',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created_on',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expires_on',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified_on',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created_on',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expires_on',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified_on',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created_on',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expires_on',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified_on',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('integer property ending in _on', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['created_on', 'modified_on', 'expires_on'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'integer',
+ };
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(12);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created_on',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expires_on',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified_on',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created_on',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expires_on',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified_on',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created_on',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expires_on',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified_on',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created_on',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expires_on',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified_on',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('string property containing the word "date"', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['first_date', 'new_date_when', 'date_next'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ };
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(12);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.date_next',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.first_date',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.new_date_when',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.date_next',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.first_date',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.new_date_when',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.date_next',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_date',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.new_date_when',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.date_next',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_date',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.new_date_when',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('integer property containing the word "date"', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['first_date', 'new_date_when', 'date_next'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'integer',
+ };
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(12);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.date_next',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.first_date',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.new_date_when',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.date_next',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.first_date',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.new_date_when',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.date_next',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_date',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.new_date_when',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.date_next',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_date',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.new_date_when',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('string property containing the word "time"', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['a_time_for_updating', 'time_is'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ };
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(8);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.a_time_for_updating',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.time_is',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.a_time_for_updating',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.time_is',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.a_time_for_updating',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.time_is',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.a_time_for_updating',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.time_is',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('integer property containing the word "time"', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['a_time_for_updating', 'time_is'].forEach(propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'integer',
+ };
+ });
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(8);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.a_time_for_updating',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.time_is',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.a_time_for_updating',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.time_is',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.a_time_for_updating',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.time_is',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.a_time_for_updating',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.time_is',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('string property containing the word "timestamp"', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['photo_timestamp', 'photo_timestamp_value', 'timestamp_value'].forEach(
+ propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ };
+ }
+ );
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(12);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.photo_timestamp',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.photo_timestamp_value',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.timestamp_value',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.photo_timestamp',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.photo_timestamp_value',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.timestamp_value',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.photo_timestamp',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.photo_timestamp_value',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.timestamp_value',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.photo_timestamp',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.photo_timestamp_value',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.timestamp_value',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('integer property containing the word "timestamp"', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['photo_timestamp', 'photo_timestamp_value', 'timestamp_value'].forEach(
+ propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'integer',
+ };
+ }
+ );
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(12);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.photo_timestamp',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.photo_timestamp_value',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.timestamp_value',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.photo_timestamp',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.photo_timestamp_value',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.timestamp_value',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.photo_timestamp',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.photo_timestamp_value',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.timestamp_value',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.photo_timestamp',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.photo_timestamp_value',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.timestamp_value',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('string property with a time-based name', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['created', 'updated', 'modified', 'expired', 'expires'].forEach(
+ propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'string',
+ };
+ }
+ );
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(20);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expired',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expires',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.updated',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expired',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expires',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.updated',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expired',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expires',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.updated',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expired',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expires',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.updated',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('integer property with a time-based name', async () => {
+ const testDocument = makeCopy(rootDocument);
+ ['created', 'updated', 'modified', 'expired', 'expires'].forEach(
+ propName => {
+ testDocument.components.schemas.Movie.properties[propName] = {
+ type: 'integer',
+ };
+ }
+ );
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(20);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expired',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expires',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified',
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.updated',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expired',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expires',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.updated',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expired',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expires',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.updated',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expired',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expires',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.updated',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedNameMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('Should detect date times based on example values', () => {
+ describe('primitive parameters', () => {
+ it('parameter defined with schema, example in schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.paths['/v1/drinks'].parameters = [
+ {
+ name: 'some_datetime_param',
+ in: 'query',
+ required: false,
+ schema: {
+ type: 'string',
+ example: '1990-12-31T23:59:60Z',
+ },
+ },
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = ['paths./v1/drinks.parameters.0.schema'];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('parameter defined with schema, example in example field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.paths['/v1/drinks'].parameters = [
+ {
+ name: 'some_datetime_param',
+ in: 'query',
+ required: false,
+ schema: {
+ type: 'string',
+ },
+ example: '1990-12-31T23:59:60Z',
+ },
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = ['paths./v1/drinks.parameters.0.schema'];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('parameter defined with schema, example in examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.paths['/v1/drinks'].parameters = [
+ {
+ name: 'some_datetime_param',
+ in: 'query',
+ required: false,
+ schema: {
+ type: 'string',
+ },
+ examples: {
+ firstExample: {
+ value: '1990-12-31T23:59:60Z',
+ },
+ },
+ },
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = ['paths./v1/drinks.parameters.0.schema'];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('parameter defined with content, example in schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.paths['/v1/drinks'].parameters = [
+ {
+ name: 'some_datetime_param',
+ in: 'query',
+ required: false,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'string',
+ example: '1990-12-31T23:59:60Z',
+ },
+ },
+ },
+ },
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/drinks.parameters.0.content.application/json.schema',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('parameter defined with content, example in example field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.paths['/v1/drinks'].parameters = [
+ {
+ name: 'some_datetime_param',
+ in: 'query',
+ required: false,
+ example: '1990-12-31T23:59:60Z',
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/drinks.parameters.0.content.application/json.schema',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('parameter defined with content, example in examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.paths['/v1/drinks'].parameters = [
+ {
+ name: 'some_datetime_param',
+ in: 'query',
+ required: false,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'string',
+ },
+ },
+ },
+ examples: {
+ firstExample: {
+ value: '1990-12-31T23:59:60Z',
+ },
+ },
+ },
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/drinks.parameters.0.content.application/json.schema',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('parameter defined with content, example in content example field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.paths['/v1/drinks'].parameters = [
+ {
+ name: 'some_datetime_param',
+ in: 'query',
+ required: false,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'string',
+ },
+ example: '1990-12-31T23:59:60Z',
+ },
+ },
+ },
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/drinks.parameters.0.content.application/json.schema',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('parameter defined with content, example in content examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.paths['/v1/drinks'].parameters = [
+ {
+ name: 'some_datetime_param',
+ in: 'query',
+ required: false,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'string',
+ },
+ examples: {
+ firstExample: {
+ value: '1990-12-31T23:59:60Z',
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/drinks.parameters.0.content.application/json.schema',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('top level properties', () => {
+ it('top level property with its own example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.some_datetimeprop = {
+ type: 'string',
+ example: '1990-12-31T23:59:60Z',
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.some_datetimeprop',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('top level property with example in primary schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.some_datetimeprop = {
+ type: 'string',
+ };
+
+ testDocument.components.schemas.Movie.example = {
+ some_datetimeprop: '1990-12-31T23:59:60Z',
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.some_datetimeprop',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('top level property with example in response body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.some_datetimeprop = {
+ type: 'string',
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].example = {
+ some_datetimeprop: '1990-12-31T23:59:60Z',
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('top level property with example in response body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.some_datetimeprop = {
+ type: 'string',
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ some_datetimeprop: '1990-12-31T23:59:60Z',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('top level property with example in request body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.some_datetimeprop =
+ {
+ type: 'string',
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ some_datetimeprop: '1990-12-31T23:59:60Z',
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('top level property with example in request body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.some_datetimeprop =
+ {
+ type: 'string',
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ some_datetimeprop: '1990-12-31T23:59:60Z',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('nested properties', () => {
+ it('nested property with its own example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ example: 'July 3, 2023, 4:15 PM',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.some_datetimeprop',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested property with example in parent', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ example: {
+ some_datetimeprop: 'July 3, 2023, 4:15 PM',
+ },
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.some_datetimeprop',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested property with example in primary schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ };
+
+ testDocument.components.schemas.Movie.example = {
+ metadata: {
+ some_datetimeprop: '1990-12-31T23:59:60Z',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.some_datetimeprop',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested property with example in response body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ some_datetimeprop: 'July 3, 2023, 4:15 PM',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested property with example in response body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ some_datetimeprop: 'July 3, 2023, 4:15 PM',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested property with example in request body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ some_datetimeprop: 'July 3, 2023, 4:15 PM',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested property with example in request body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ some_datetimeprop: 'July 3, 2023, 4:15 PM',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('doubly nested properties', () => {
+ it('doubly nested property with its own example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ modification_info: {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ example: '2023-07-03T16:15:00+00:00',
+ },
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('doubly nested property with example in parent', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ modification_info: {
+ type: 'object',
+ example: {
+ some_datetimeprop: '2023-07-03T16:15:00+00:00',
+ },
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('doubly nested property with example in grandparent', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ example: {
+ modification_info: {
+ some_datetimeprop: '2023-07-03T16:15:00+00:00',
+ },
+ },
+ properties: {
+ modification_info: {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('doubly nested property with example in primary schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ modification_info: {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ };
+
+ testDocument.components.schemas.Movie.example = {
+ metadata: {
+ modification_info: {
+ some_datetimeprop: '2023-07-03T16:15:00+00:00',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('doubly nested property with example in response body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ modification_info: {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ modification_info: {
+ some_datetimeprop: '2023-07-03T16:15:00+00:00',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('doubly nested property with example in response body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ modification_info: {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ modification_info: {
+ some_datetimeprop: '2023-07-03T16:15:00+00:00',
+ },
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('doubly nested property with example in request body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ type: 'object',
+ properties: {
+ modification_info: {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ modification_info: {
+ some_datetimeprop: '2023-07-03T16:15:00+00:00',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('doubly nested property with example in request body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ type: 'object',
+ properties: {
+ modification_info: {
+ type: 'object',
+ properties: {
+ some_datetimeprop: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ modification_info: {
+ some_datetimeprop: '2023-07-03T16:15:00+00:00',
+ },
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('items schemas', () => {
+ it('items schema with its own example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'array',
+ items: {
+ type: 'string',
+ example: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.items',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('items schema with example in array parent', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'array',
+ example: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ items: {
+ type: 'string',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.items',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('items schema with example in primary schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ };
+
+ testDocument.components.schemas.Movie.example = {
+ changes: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.items',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('items schema with example in response body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].example = {
+ changes: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('items schema with example in response body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ changes: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('items schema with example in request body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.changes = {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ changes: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('items schema with example in request body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.changes = {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ changes: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('nested items schemas', () => {
+ it('nested items schema with its own example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'array',
+ items: {
+ type: 'string',
+ example: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.items',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested items schema with example in array parent', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'array',
+ example: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.items',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested items schema with example in parent object', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ example: {
+ changes: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ },
+ properties: {
+ changes: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.items',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested items schema with example in primary schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ testDocument.components.schemas.Movie.example = {
+ metadata: {
+ changes: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.items',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested items schema with example in response body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ changes: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested items schema with example in response body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ changes: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested items schema with example in request body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ changes: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested items schema with example in request body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ changes: ['Mon, 03 Jul 23 16:15:00 +0000'],
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.changes.items',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('dictionary schemas - additionalProperties', () => {
+ it('dictionary schema with its own example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'object',
+ example: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ additionalProperties: {
+ type: 'string',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.additionalProperties',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('dictionary schema with example in the value schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ example: 'Monday, 03-Jul-23 16:15:00 GMT',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.additionalProperties',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('dictionary schema with example in primary schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ };
+
+ testDocument.components.schemas.Movie.example = {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.additionalProperties',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('dictionary schema with example in response body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].example = {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('dictionary schema with example in response body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('dictionary schema with example in request body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.changes = {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('dictionary schema with example in request body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.changes = {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('dictionary schemas - patternProperties', () => {
+ it('patterned dictionary schema with its own example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'object',
+ example: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ patternProperties: {
+ '^what.*$': {
+ type: 'string',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.patternProperties.^what.*$',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('patterned dictionary schema with example in the value schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'object',
+ patternProperties: {
+ '^what.*$': {
+ type: 'string',
+ example: 'Monday, 03-Jul-23 16:15:00 GMT',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.patternProperties.^what.*$',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('patterned dictionary schema with example in primary schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'object',
+ patternProperties: {
+ '^what.*$': {
+ type: 'string',
+ },
+ },
+ };
+
+ testDocument.components.schemas.Movie.example = {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.patternProperties.^what.*$',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('patterned dictionary schema with example in response body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'object',
+ patternProperties: {
+ '^what.*$': {
+ type: 'string',
+ },
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].example = {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('patterned dictionary schema with example in response body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.changes = {
+ type: 'object',
+ patternProperties: {
+ '^what.*$': {
+ type: 'string',
+ },
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('patterned dictionary schema with example in request body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.changes = {
+ type: 'object',
+ patternProperties: {
+ '^what.*$': {
+ type: 'string',
+ },
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('patterned dictionary schema with example in request body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.changes = {
+ type: 'object',
+ patternProperties: {
+ '^what.*$': {
+ type: 'string',
+ },
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.patternProperties.^what.*$',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('nested dictionary schemas', () => {
+ it('nested dictionary schema with its own example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'object',
+ example: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ additionalProperties: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested dictionary schema with example in the value schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'object',
+ additionalProperties: {
+ example: 'Mon, 03 Jul 23 16:15:00 +0000',
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested dictionary schema with example in parent object', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ example: {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ },
+ properties: {
+ changes: {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested dictionary schema with example in primary schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ testDocument.components.schemas.Movie.example = {
+ metadata: {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested dictionary schema with example in response body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested dictionary schema with example in response body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested dictionary schema with example in request body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('nested dictionary schema with example in request body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ type: 'object',
+ properties: {
+ changes: {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ },
+ },
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ changes: {
+ whatever: 'Mon, 03 Jul 23 16:15:00 +0000',
+ },
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.changes.additionalProperties',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('primitive oneOf schemas', () => {
+ it('primitive oneOf schema with its own example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.first_completed = {
+ oneOf: [
+ {
+ type: 'string',
+ example: '01/01/2023',
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.first_completed',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('primitive oneOf schema with example in parent', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.first_completed = {
+ example: '01/01/2023',
+ oneOf: [
+ {
+ type: 'string',
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.first_completed',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('primitive oneOf schema with example in primary schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.first_completed = {
+ oneOf: [
+ {
+ type: 'string',
+ },
+ ],
+ };
+
+ testDocument.components.schemas.Movie.example = {
+ first_completed: '01/01/2023',
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.first_completed',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('primitive oneOf schema with example in response body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.first_completed = {
+ oneOf: [
+ {
+ type: 'string',
+ },
+ ],
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].example = {
+ first_completed: '01/01/2023',
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('primitive oneOf schema with example in response body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.first_completed = {
+ oneOf: [
+ {
+ type: 'string',
+ },
+ ],
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ first_completed: '01/01/2023',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('primitive oneOf schema with example in request body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.first_completed =
+ {
+ oneOf: [
+ {
+ type: 'string',
+ },
+ ],
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ first_completed: '01/01/2023',
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('primitive oneOf schema with example in request body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.first_completed =
+ {
+ oneOf: [
+ {
+ type: 'string',
+ },
+ ],
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ first_completed: '01/01/2023',
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('properties nested in oneOf schema', () => {
+ it('property nested in oneOf schema with its own example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ oneOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ example: 'Oct. 31',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.properties.first_completed',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in oneOf schema with example in parent', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ example: {
+ first_completed: 'Oct. 31',
+ },
+ oneOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.properties.first_completed',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in oneOf schema with example in primary schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ oneOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ testDocument.components.schemas.Movie.example = {
+ metadata: {
+ first_completed: 'Oct. 31',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.properties.first_completed',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in oneOf schema with example in response body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ oneOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ first_completed: 'Oct. 31',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in oneOf schema with example in response body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ oneOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ first_completed: 'Oct. 31',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in oneOf schema with example in request body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ oneOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ first_completed: 'Oct. 31',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in oneOf schema with example in request body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ oneOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ first_completed: 'Oct. 31',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('properties nested in nested oneOf schema', () => {
+ it('property nested in nested oneOf schema with its own example', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ oneOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ example: 'Oct. 31',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ something_else: {
+ type: 'integer',
+ },
+ },
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in nested oneOf schema with example in parent', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ oneOf: [
+ {
+ anyOf: [
+ {
+ example: {
+ first_completed: 'Oct. 31',
+ },
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ something_else: {
+ type: 'integer',
+ },
+ },
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in nested oneOf schema with example in grandparent', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ oneOf: [
+ {
+ example: {
+ first_completed: 'Oct. 31',
+ },
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ something_else: {
+ type: 'integer',
+ },
+ },
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in nested oneOf schema with example at top level', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ example: {
+ first_completed: 'Oct. 31',
+ },
+ oneOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ something_else: {
+ type: 'integer',
+ },
+ },
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in oneOf schema with example in primary schema', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ oneOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ something_else: {
+ type: 'integer',
+ },
+ },
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ testDocument.components.schemas.Movie.example = {
+ metadata: {
+ first_completed: 'Oct. 31',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in nested oneOf schema with example in response body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ oneOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ something_else: {
+ type: 'integer',
+ },
+ },
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ first_completed: 'Oct. 31',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in nested oneOf schema with example in response body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.metadata = {
+ oneOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ something_else: {
+ type: 'integer',
+ },
+ },
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ testDocument.components.responses.MovieWithETag.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ first_completed: 'Oct. 31',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(2);
+
+ const expectedPaths = [
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in nested oneOf schema with example in request body', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ oneOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ something_else: {
+ type: 'integer',
+ },
+ },
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].example = {
+ metadata: {
+ first_completed: 'Oct. 31',
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('property nested in nested oneOf schema with example in request body examples field', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.MoviePrototype.properties.metadata = {
+ oneOf: [
+ {
+ anyOf: [
+ {
+ type: 'object',
+ properties: {
+ first_completed: {
+ type: 'string',
+ },
+ },
+ },
+ {
+ type: 'object',
+ properties: {
+ something_else: {
+ type: 'integer',
+ },
+ },
+ },
+ ],
+ },
+ {
+ type: 'object',
+ properties: {
+ irrelevant: {
+ type: 'boolean',
+ },
+ },
+ },
+ ],
+ };
+
+ testDocument.paths['/v1/movies'].post.requestBody.content[
+ 'application/json'
+ ].examples = {
+ firstExample: {
+ value: {
+ metadata: {
+ first_completed: 'Oct. 31',
+ },
+ },
+ },
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(1);
+
+ const expectedPaths = [
+ 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+
+ describe('edge cases', () => {
+ it('schema has property with same name as sub-schema property', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.studio = {
+ type: 'object',
+ properties: {
+ made: {
+ type: 'string',
+ },
+ },
+ example: {
+ made: '2024-12-19',
+ },
+ };
+
+ testDocument.components.schemas.Movie.properties.made = {
+ type: 'string',
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.studio.properties.made',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.studio.properties.made',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.studio.properties.made',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.studio.properties.made',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('schema has example defined within allOf', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.made = {
+ type: 'string',
+ allOf: [
+ {
+ example: '2024-12-19',
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.made',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.made',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.made',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.made',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+
+ it('schema property has example defined within anyOf sibling', async () => {
+ const testDocument = makeCopy(rootDocument);
+ testDocument.components.schemas.Movie.properties.studio = {
+ type: 'object',
+ properties: {
+ made: {
+ type: 'string',
+ },
+ },
+ anyOf: [
+ {
+ example: {
+ made: '2024-12-19',
+ },
+ },
+ ],
+ };
+
+ const results = await testRule(ruleId, rule, testDocument);
+ expect(results).toHaveLength(4);
+
+ const expectedPaths = [
+ 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.studio.properties.made',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.studio.properties.made',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.studio.properties.made',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.studio.properties.made',
+ ];
+
+ for (const i in results) {
+ expect(results[i].code).toBe(ruleId);
+ expect(results[i].message).toBe(expectedExampleMsg);
+ expect(results[i].severity).toBe(expectedSeverity);
+ expect(results[i].path.join('.')).toBe(expectedPaths[i]);
+ }
+ });
+ });
+ });
+ });
+});
diff --git a/packages/ruleset/test/utils/date-based-utils.test.js b/packages/ruleset/test/utils/date-based-utils.test.js
new file mode 100644
index 00000000..b3d50275
--- /dev/null
+++ b/packages/ruleset/test/utils/date-based-utils.test.js
@@ -0,0 +1,90 @@
+/**
+ * Copyright 2024 IBM Corporation.
+ * SPDX-License-Identifier: Apache2.0
+ */
+
+const { isDateBasedName, isDateBasedValue } = require('../../src/utils');
+
+describe('Date-based utility functions', () => {
+ describe('isDateBasedValue()', () => {
+ // Positive tests.
+ it('should return `true` for date time values', () => {
+ expect(isDateBasedValue('1990-12-31T23:59:60Z')).toBe(true);
+ expect(isDateBasedValue('1990-12-31T15:59:60-08:00')).toBe(true);
+ expect(isDateBasedValue('2023-01-01')).toBe(true);
+ expect(isDateBasedValue('01/01/2023')).toBe(true);
+ expect(isDateBasedValue('Mon, 03 Jul 2023 16:15:00 +0000')).toBe(true);
+ expect(isDateBasedValue('Monday, 03-Jul-23 16:15:00 GMT')).toBe(true);
+ expect(isDateBasedValue('Mon, 03 Jul 23 16:15:00 +0000')).toBe(true);
+ expect(isDateBasedValue('2023-07-03T16:15:00+00:00')).toBe(true);
+ expect(isDateBasedValue('July 3, 2023, 4:15 PM')).toBe(true);
+ expect(isDateBasedValue('Oct. 31')).toBe(true);
+ });
+
+ // Negative tests.
+ it('should return `false` for non-date time values', () => {
+ expect(isDateBasedValue('This certificate is good until June 2032')).toBe(
+ false
+ );
+ expect(isDateBasedValue('Octopus')).toBe(false);
+ expect(isDateBasedValue('12345678')).toBe(false);
+ expect(isDateBasedValue('0001-01-2000')).toBe(false);
+ expect(isDateBasedValue('10.1.24.1')).toBe(false);
+ expect(isDateBasedValue('10.1.255.1')).toBe(false);
+ expect(isDateBasedValue(undefined)).toBe(false);
+ expect(isDateBasedValue(null)).toBe(false);
+ expect(isDateBasedValue(42)).toBe(false);
+ });
+ });
+
+ describe('isDateBasedName()', () => {
+ // Positive tests.
+ it('should return `true` for date time values', () => {
+ expect(isDateBasedName('created_at')).toBe(true);
+ expect(isDateBasedName('modified_at')).toBe(true);
+ expect(isDateBasedName('updated_at')).toBe(true);
+ expect(isDateBasedName('created_on')).toBe(true);
+ expect(isDateBasedName('modified_on')).toBe(true);
+ expect(isDateBasedName('expires_on')).toBe(true);
+ expect(isDateBasedName('first_date')).toBe(true);
+ expect(isDateBasedName('new_date_when')).toBe(true);
+ expect(isDateBasedName('date_next')).toBe(true);
+ expect(isDateBasedName('a_time_for_updating')).toBe(true);
+ expect(isDateBasedName('time_is')).toBe(true);
+ expect(isDateBasedName('photo_timestamp')).toBe(true);
+ expect(isDateBasedName('photo_timestamp_value')).toBe(true);
+ expect(isDateBasedName('timestamp_value')).toBe(true);
+ expect(isDateBasedName('created')).toBe(true);
+ expect(isDateBasedName('updated')).toBe(true);
+ expect(isDateBasedName('modified')).toBe(true);
+ expect(isDateBasedName('expired')).toBe(true);
+ expect(isDateBasedName('expires')).toBe(true);
+ expect(isDateBasedName('start_time')).toBe(true);
+ expect(isDateBasedName('start_date')).toBe(true);
+ expect(isDateBasedName('end_time')).toBe(true);
+ expect(isDateBasedName('end_date')).toBe(true);
+ expect(isDateBasedName('create_time')).toBe(true);
+ expect(isDateBasedName('create_date')).toBe(true);
+ expect(isDateBasedName('created_time')).toBe(true);
+ expect(isDateBasedName('created_date')).toBe(true);
+ expect(isDateBasedName('modify_time')).toBe(true);
+ expect(isDateBasedName('modify_date')).toBe(true);
+ expect(isDateBasedName('modified_time')).toBe(true);
+ expect(isDateBasedName('modified_date')).toBe(true);
+ expect(isDateBasedName('update_time')).toBe(true);
+ expect(isDateBasedName('update_date')).toBe(true);
+ });
+
+ // Negative tests.
+ it('should return `false` for non-date time values', () => {
+ expect(isDateBasedName('running_time')).toBe(false);
+ expect(isDateBasedName('timeandtimeagain')).toBe(false);
+ expect(isDateBasedName('created_for')).toBe(false);
+ expect(isDateBasedName('ttl')).toBe(false);
+ expect(isDateBasedName('octopus')).toBe(false);
+ expect(isDateBasedName(undefined)).toBe(false);
+ expect(isDateBasedName(null)).toBe(false);
+ expect(isDateBasedName(42)).toBe(false);
+ });
+ });
+});
diff --git a/packages/ruleset/test/utils/is-operation-of-type.test.js b/packages/ruleset/test/utils/is-operation-of-type.test.js
index 5d36d5e2..83909232 100644
--- a/packages/ruleset/test/utils/is-operation-of-type.test.js
+++ b/packages/ruleset/test/utils/is-operation-of-type.test.js
@@ -5,7 +5,7 @@
const { isOperationOfType } = require('../../src/utils');
-describe('Utility function: getResourceSpecificSiblingPath', () => {
+describe('Utility function: isOperationOfType', () => {
it('should return `true` when path matches the given type', () => {
expect(isOperationOfType('get', ['paths', '/v1/things', 'get'])).toBe(true);
});
diff --git a/packages/validator/src/scoring-tool/rubric.js b/packages/validator/src/scoring-tool/rubric.js
index f74c154d..b673b1e6 100644
--- a/packages/validator/src/scoring-tool/rubric.js
+++ b/packages/validator/src/scoring-tool/rubric.js
@@ -425,6 +425,11 @@ module.exports = {
denominator: 'operations',
categories: ['usability', 'robustness'],
},
+ 'ibm-use-date-based-format': {
+ coefficient: 1,
+ denominator: 'schemas',
+ categories: ['usability'],
+ },
'ibm-valid-path-segments': {
coefficient: 2,
denominator: 'operations',