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..f4d98e6f
--- /dev/null
+++ b/packages/ruleset/src/functions/use-date-based-format.js
@@ -0,0 +1,359 @@
+/**
+ * Copyright 2024 IBM Corporation.
+ * SPDX-License-Identifier: Apache2.0
+ */
+
+const {
+ isArraySchema,
+ isDateSchema,
+ isDateTimeSchema,
+ isIntegerSchema,
+ isObject,
+ isObjectSchema,
+ isStringSchema,
+ schemaHasConstraint,
+ validateSubschemas,
+} = 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) {
+ const examples = [];
+ const propertyPath = [];
+
+ return validateSubschemas(s, p, (schema, path) => {
+ logger.debug(`${ruleId}: checking schema at location: ${path.join('.')}`);
+
+ // Check for any examples outside of the schema path - they may be in
+ // request bodies, response bodies, or parameters.
+ examples.push(...checkForIndirectExamples(path, apidef));
+
+ // We can look at primitive schemas directly but for objects and arrays,
+ // we need more specialized handling in case we need to find a particular
+ // property within their examples.
+ if (isObjectSchema(schema) || isArraySchema(schema)) {
+ // Maintain a running path to each schema that we look at. This will be
+ // used to determine where to look for a property value within an example
+ // object, relative to that example's location.
+ if (isSchemaProperty(path)) {
+ propertyPath.push(path.at(-1));
+ }
+
+ // Keep a running hierarchy of all examples we find as we look through
+ // the schemas in the API. Nested properties may only have an example
+ // value defined within a parent schema example.
+ if (schema.example) {
+ logger.debug(
+ `${ruleId}: adding example for schema at location: ${path.join('.')}`
+ );
+
+ examples.push({
+ example: schema.example,
+ examplePath: propertyPath.slice(), // Use a copy to prevent modification.
+ });
+ }
+
+ // Add sentinels for arrays/dictionaries to the running path,
+ // to assist the example-parsing logic later on. This must come
+ // after we push the example to the list.
+ if (isSchemaProperty(path)) {
+ if (isArraySchema(schema)) {
+ propertyPath.push('[]');
+ }
+
+ if (isDictionarySchema(schema)) {
+ propertyPath.push('{}');
+ }
+ }
+ }
+
+ // Use a slice (a copy) of the `propertyPath` array so that the
+ // invoked function can modify it without modifying the original.
+ return performValidation(
+ schema,
+ path,
+ apidef,
+ propertyPath.slice(),
+ examples
+ );
+ });
+}
+
+// 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, apidef, propertyPath, examples) {
+ // 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 for a name that would indicate the property should be date-based.
+ const hasDateTimeName =
+ isSchemaProperty(path) && isDateBasedName(path.at(-1));
+
+ logger.debug(
+ `${ruleId}: property at location: ${path.join('.')} has a date-based name`
+ );
+
+ if (hasDateTimeName && (isStringSchema(schema) || isIntegerSchema(schema))) {
+ // 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 this is a property, we need to include its name in the path.
+ if (isSchemaProperty(path)) {
+ propertyPath.push(path.at(-1));
+ }
+
+ // Either use the schema example directly or search the list of examples
+ // for an example object that contains a value for this property.
+ const exampleValue = schema.example || findExample(propertyPath, examples);
+ if (exampleValue) {
+ logger.debug(
+ `${ruleId}: example value found for string schema at location ${path.join(
+ '.'
+ )}: ${exampleValue}`
+ );
+
+ if (isDateBasedValue(exampleValue)) {
+ return [
+ {
+ message:
+ 'According to its example value, this schema should use type "string" and format "date" or "date-time"',
+ path,
+ },
+ ];
+ }
+ }
+ }
+
+ return [];
+}
+
+// This function checks all of the examples we've gathered while processing
+// schemas to check if once of them defines a value for the specific property
+// or string schema that we are looking at. It returns the first value found.
+function findExample(propertyPath, examples) {
+ let exampleValue;
+
+ // According to the OpenAPI specification, Media Type/Parameter examples
+ // override any examples defined on the schemas themselves. Going "in order"
+ // through this loop ensures we prioritize those examples, followed by
+ // higher-level schema examples. If it turns out that we should prioritize
+ // nested examples, we can simply reverse this loop.
+ for (const { example, examplePath } of examples) {
+ // First thing is to find the relevant segment of the property path relative
+ // to the example path, which should be the first element where they differ.
+ const index = propertyPath.findIndex((prop, i) => prop !== examplePath[i]);
+ const value = getObjectValueAtPath(example, propertyPath.slice(index));
+
+ // If we find a value, go ahead and break from the loop.
+ if (value) {
+ logger.debug(
+ `${ruleId}: value found in example at location: ${examplePath.join(
+ '.'
+ )}`
+ );
+
+ exampleValue = value;
+ break;
+ }
+ }
+
+ logger.debug(
+ `${ruleId}: no example value found for schema at location: ${propertyPath.join(
+ '.'
+ )}`
+ );
+
+ // This will return `undefined` if we never find a value;
+ return exampleValue;
+}
+
+// This function takes an object, as well as a path to a specific value, and
+// recursively 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`. One important note is that the array given as the `pathToValue`
+// argument *will* be modified by the logic, so if that is not desired, a copy
+// should be passed by the caller (using .slice(), for example).
+function getObjectValueAtPath(obj, pathToValue) {
+ // If obj is undefined, there is nothing to process.
+ if (obj === undefined) {
+ return;
+ }
+
+ // If we've exhausted the whole path, we've found the desired value!
+ if (!pathToValue.length) {
+ return obj;
+ }
+
+ const p = pathToValue.shift();
+
+ // Check for sentinel indicating an array.
+ if (p === '[]' && Array.isArray(obj) && obj.length) {
+ return getObjectValueAtPath(obj[0], pathToValue);
+ }
+
+ // Check for sentinel indicating a dictionary.
+ if (p === '{}' && isObject(obj) && Object.values(obj).length) {
+ return getObjectValueAtPath(Object.values(obj)[0], pathToValue);
+ }
+
+ // Standard model path.
+ if (obj[p]) {
+ return getObjectValueAtPath(obj[p], pathToValue);
+ }
+
+ // Return undefined if we don't find anything.
+ return;
+}
+
+// "Indirect" examples are those coming from request bodies, response bodies, and parameters.
+function checkForIndirectExamples(path, apidef) {
+ // 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.map(example => {
+ return {
+ example,
+ examplePath: [], // All top-level examples get an empty array for the path.
+ };
+ });
+ }
+
+ 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 [];
+}
+
+// This function determines if a schema is a "dictionary" (as opposed to a
+// standard model with static properties) based on the presence of either
+// `additionalProperties` or `patternProperties` (OpenAPI 3.1 only).
+function isDictionarySchema(schema) {
+ return schemaHasConstraint(
+ schema,
+ s => isObjectSchema(s) && (s.additionalProperties || s.patternProperties)
+ );
+}
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..4327d305
--- /dev/null
+++ b/packages/ruleset/src/utils/date-based-utils.js
@@ -0,0 +1,101 @@
+/**
+ * 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`.
+ new RegExp('^created$'),
+
+ // The name `updated`.
+ new RegExp('^updated$'),
+
+ // The name `modified`.
+ new RegExp('^modified$'),
+
+ // The name `expired`.
+ new RegExp('^expired$'),
+
+ // The name `expires`.
+ new RegExp('^expires$'),
+
+ // Any name ending in `_at`.
+ new RegExp('.*_at$'),
+
+ // Any name ending in `_on`.
+ new RegExp('.*_on$'),
+
+ // Any name starting with `date_`.
+ new RegExp('^date_.*'),
+
+ // Any name containing `_date_`.
+ new RegExp('.*_date_.*'),
+
+ // Any name ending in `_date`.
+ new RegExp('.*_date$'),
+
+ // Any name starting with `time_`.
+ new RegExp('^time_.*'),
+
+ // Any name containing `_time_`.
+ new RegExp('.*_time_.*'),
+
+ // Note: not including any name ending in `_time` because it was too easy
+ // to think of counterexamples. `running_time` in the "movies" API of our
+ // test document in this project is one of them.
+
+ // Any name containing `timestamp`.
+ new RegExp('.*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.
+ new RegExp('\\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\b'),
+
+ // Includes full month name.
+ new RegExp(
+ '\\b(January|February|March|April|May|June|July|August|September|October|November|December)\\b'
+ ),
+
+ // Includes date in the format YYYY(./-)MM(./-)DD(T).
+ new RegExp('\\b\\d{4}[./-](0?[1-9]|1[012])[./-]([012]?[1-9]|3[01])(\\b|T)'),
+
+ // Includes date in the format DD(./-)MM(./-)YYYY.
+ new RegExp('\\b([012]?[1-9]|3[01])[./-](0?[1-9]|1[012])[./-]\\d{4}\\b'),
+
+ // Includes date in the format MM(./-)DD(./-)YYYY.
+ new RegExp('\\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.)
+ new RegExp('(\\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/use-date-based-format.test.js b/packages/ruleset/test/rules/use-date-based-format.test.js
new file mode 100644
index 00000000..d4f65dd2
--- /dev/null
+++ b/packages/ruleset/test/rules/use-date-based-format.test.js
@@ -0,0 +1,4082 @@
+/**
+ * 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.oneOf.0',
+ 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.first_completed.oneOf.0',
+ 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_completed.oneOf.0',
+ 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_completed.oneOf.0',
+ ];
+
+ 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]);
+ }
+ });
+ });
+ });
+ });
+});
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..7c0cb802
--- /dev/null
+++ b/packages/ruleset/test/utils/date-based-utils.test.js
@@ -0,0 +1,73 @@
+/**
+ * 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('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);
+ });
+
+ // 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',