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