diff --git a/.secrets.baseline b/.secrets.baseline index 6b603402..97e9df08 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "package-lock.json|^.secrets.baseline$", "lines": null }, - "generated_at": "2024-12-16T19:27:38Z", + "generated_at": "2024-12-19T16:14:03Z", "plugins_used": [ { "name": "AWSKeyDetector" diff --git a/docs/ibm-cloud-rules.md b/docs/ibm-cloud-rules.md index 7193f980..09c5d0bf 100644 --- a/docs/ibm-cloud-rules.md +++ b/docs/ibm-cloud-rules.md @@ -114,6 +114,7 @@ which is delivered in the `@ibm-cloud/openapi-ruleset` NPM package. * [ibm-summary-sentence-style](#ibm-summary-sentence-style) * [ibm-unevaluated-properties](#ibm-unevaluated-properties) * [ibm-unique-parameter-request-property-names](#ibm-unique-parameter-request-property-names) + * [ibm-use-date-based-format](#ibm-use-date-based-format) * [ibm-valid-path-segments](#ibm-valid-path-segments) * [ibm-well-defined-dictionaries](#ibm-well-defined-dictionaries) @@ -675,6 +676,12 @@ specific "allow-listed" keywords. oas3 +ibm-use-date-based-format +warning +Checks each schema and heuristically determines if it should be a string schema that uses a format of "date" or "date-time". +oas3 + + ibm-valid-path-segments error Checks each path string in the API to make sure path parameter references are valid within path segments. @@ -7197,6 +7204,66 @@ paths: +### ibm-use-date-based-format + + + + + + + + + + + + + + + + + + + + + + + + + +
Rule id:ibm-use-date-based-format
Description: Schemas or properties that are date-based (i.e. the values they model +are dates or times) must be strings with a format of "date" or "date-time". +This rule validates that is the case for relevant schemas, which are determined +heuristically using the property name, in the case of schema properties, or +the example value provided for a schema or property. +
Severity:warning
OAS Versions:oas3
Non-compliant example: +
+Resource
+  type: object
+  properties:
+    created_at: # Name indicates it should be a date or date-time
+      type: integer
+    stamp: # Example value indicates it should be a date-time
+      type: string
+      example: '1990-12-31T23:59:60Z'
+    ...
+
+
Compliant example: +
+Resource
+  type: object
+  properties:
+    created_at:
+      type: string
+      format: date-time
+    stamp:
+      type: string
+      format: date-time
+      example: '1990-12-31T23:59:60Z'
+    ...
+
+
+ + ### ibm-valid-path-segments diff --git a/packages/ruleset/src/functions/index.js b/packages/ruleset/src/functions/index.js index 2c6df832..3c4c8cbf 100644 --- a/packages/ruleset/src/functions/index.js +++ b/packages/ruleset/src/functions/index.js @@ -78,6 +78,7 @@ module.exports = { unevaluatedProperties: require('./unevaluated-properties'), uniqueParameterRequestPropertyNames: require('./unique-parameter-request-property-names'), unusedTags: require('./unused-tags'), + useDateBasedFormat: require('./use-date-based-format'), validatePathSegments: require('./valid-path-segments'), wellDefinedDictionaries: require('./well-defined-dictionaries'), }; diff --git a/packages/ruleset/src/functions/no-ambiguous-paths.js b/packages/ruleset/src/functions/no-ambiguous-paths.js index 68177d00..1380c1d2 100644 --- a/packages/ruleset/src/functions/no-ambiguous-paths.js +++ b/packages/ruleset/src/functions/no-ambiguous-paths.js @@ -27,7 +27,7 @@ module.exports = function (paths, _options, context) { * 1. "/v1/clouds/{id}", "/v1/clouds/{cloud_id}" * 2. "/v1/clouds/foo", "/v1/clouds/{cloud_id}" * 3. "/v1/{resource_type}/foo", "/v1/users/{user_id}" - * @param {*} apidef the entire API definition + * @param {*} paths map containing all path objects * @returns an array containing zero or more error objects */ function checkAmbiguousPaths(paths) { diff --git a/packages/ruleset/src/functions/use-date-based-format.js b/packages/ruleset/src/functions/use-date-based-format.js new file mode 100644 index 00000000..f4d98e6f --- /dev/null +++ b/packages/ruleset/src/functions/use-date-based-format.js @@ -0,0 +1,359 @@ +/** + * Copyright 2024 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const { + isArraySchema, + isDateSchema, + isDateTimeSchema, + isIntegerSchema, + isObject, + isObjectSchema, + isStringSchema, + schemaHasConstraint, + validateSubschemas, +} = require('@ibm-cloud/openapi-ruleset-utilities'); + +const { + LoggerFactory, + isDateBasedName, + isDateBasedValue, + isParamContentSchema, + isParamSchema, + isRequestBodySchema, + isResponseSchema, + isSchemaProperty, +} = require('../utils'); + +let ruleId; +let logger; + +/** + * The implementation for this rule makes assumptions that are dependent on the + * presence of the following other rules: + * + * - oas3-valid-media-example + * - oas3-valid-schema-example + * + * These rules verify that the correct, specific format (date vs date-time) is + * used for schemas based on their example value. So, we aren't as specific + * with that check in this rule - we recommend either "date" or "date-time". + */ + +module.exports = function (schema, _opts, context) { + if (!logger) { + ruleId = context.rule.name; + logger = LoggerFactory.getInstance().getLogger(ruleId); + } + + return checkForDateBasedFormat( + schema, + context.path, + context.documentInventory.resolved + ); +}; + +/** + * This function implements a rule that enforces date-based schemas use either + * the "date" or "date-time" format, so that they're accurately documented as + * date-based logical types. We use a heuristic based on either the name of a + * schema (derived from the property name, if the schema is a property schema) + * or the example value provided for a given schema or schema property. + * + * The logic here recursively checks all schemas for the presence of unmarked + * date-based schemas. As it traverses the schemas, it compiles a list of + * potentially-relevant example values. This way, if an object schema defines + * its own example, which includes a value for a nested property that should + * be identified by the rule, we can track down the value once we reach the + * schema for said property. The logic will also gather any relevant parameter + * or media type examples that may be defined outside of the schema path. + * + * @param {object} s the schema to check + * @param {array} p the array of path segments indicating the "location" of the schema within the API definition + * @param {object} apidef the resolved API definition + * @returns an array containing the violations found or [] if no violations + */ +function checkForDateBasedFormat(s, p, apidef) { + const examples = []; + const propertyPath = []; + + return validateSubschemas(s, p, (schema, path) => { + logger.debug(`${ruleId}: checking schema at location: ${path.join('.')}`); + + // Check for any examples outside of the schema path - they may be in + // request bodies, response bodies, or parameters. + examples.push(...checkForIndirectExamples(path, apidef)); + + // We can look at primitive schemas directly but for objects and arrays, + // we need more specialized handling in case we need to find a particular + // property within their examples. + if (isObjectSchema(schema) || isArraySchema(schema)) { + // Maintain a running path to each schema that we look at. This will be + // used to determine where to look for a property value within an example + // object, relative to that example's location. + if (isSchemaProperty(path)) { + propertyPath.push(path.at(-1)); + } + + // Keep a running hierarchy of all examples we find as we look through + // the schemas in the API. Nested properties may only have an example + // value defined within a parent schema example. + if (schema.example) { + logger.debug( + `${ruleId}: adding example for schema at location: ${path.join('.')}` + ); + + examples.push({ + example: schema.example, + examplePath: propertyPath.slice(), // Use a copy to prevent modification. + }); + } + + // Add sentinels for arrays/dictionaries to the running path, + // to assist the example-parsing logic later on. This must come + // after we push the example to the list. + if (isSchemaProperty(path)) { + if (isArraySchema(schema)) { + propertyPath.push('[]'); + } + + if (isDictionarySchema(schema)) { + propertyPath.push('{}'); + } + } + } + + // Use a slice (a copy) of the `propertyPath` array so that the + // invoked function can modify it without modifying the original. + return performValidation( + schema, + path, + apidef, + propertyPath.slice(), + examples + ); + }); +} + +// This function performs the actual checks against a schema to determine if +// it should be a "date" or "date-time" schema, but isn't defined as one. +// It is wrapped in the outer function for the gathering of examples, etc. but +// this function implements the checks: 1) see if the name of a property +// indicates that it is a date-based schema and 2) see if the example value for +// a schema indicates that it is a date-based schema. +function performValidation(schema, path, apidef, propertyPath, examples) { + // If this is already a date or date-time schema, no need to check if it should be. + if (isDateSchema(schema) || isDateTimeSchema(schema)) { + logger.debug( + `${ruleId}: skipping date-based schema at location: ${path.join('.')}` + ); + + return []; + } + + // Check for a name that would indicate the property should be date-based. + const hasDateTimeName = + isSchemaProperty(path) && isDateBasedName(path.at(-1)); + + logger.debug( + `${ruleId}: property at location: ${path.join('.')} has a date-based name` + ); + + if (hasDateTimeName && (isStringSchema(schema) || isIntegerSchema(schema))) { + // If the schema is determined to be a date-time schema by the name alone, + // we can return - no need to look for an example value. + return [ + { + message: + 'According to its name, this property should use type "string" and format "date" or "date-time"', + path, + }, + ]; + } + + // Check example values for string schemas. + if (isStringSchema(schema)) { + // If this is a property, we need to include its name in the path. + if (isSchemaProperty(path)) { + propertyPath.push(path.at(-1)); + } + + // Either use the schema example directly or search the list of examples + // for an example object that contains a value for this property. + const exampleValue = schema.example || findExample(propertyPath, examples); + if (exampleValue) { + logger.debug( + `${ruleId}: example value found for string schema at location ${path.join( + '.' + )}: ${exampleValue}` + ); + + if (isDateBasedValue(exampleValue)) { + return [ + { + message: + 'According to its example value, this schema should use type "string" and format "date" or "date-time"', + path, + }, + ]; + } + } + } + + return []; +} + +// This function checks all of the examples we've gathered while processing +// schemas to check if once of them defines a value for the specific property +// or string schema that we are looking at. It returns the first value found. +function findExample(propertyPath, examples) { + let exampleValue; + + // According to the OpenAPI specification, Media Type/Parameter examples + // override any examples defined on the schemas themselves. Going "in order" + // through this loop ensures we prioritize those examples, followed by + // higher-level schema examples. If it turns out that we should prioritize + // nested examples, we can simply reverse this loop. + for (const { example, examplePath } of examples) { + // First thing is to find the relevant segment of the property path relative + // to the example path, which should be the first element where they differ. + const index = propertyPath.findIndex((prop, i) => prop !== examplePath[i]); + const value = getObjectValueAtPath(example, propertyPath.slice(index)); + + // If we find a value, go ahead and break from the loop. + if (value) { + logger.debug( + `${ruleId}: value found in example at location: ${examplePath.join( + '.' + )}` + ); + + exampleValue = value; + break; + } + } + + logger.debug( + `${ruleId}: no example value found for schema at location: ${propertyPath.join( + '.' + )}` + ); + + // This will return `undefined` if we never find a value; + return exampleValue; +} + +// This function takes an object, as well as a path to a specific value, and +// recursively parses the object looking for the value at that path. If it +// finds one, the value will be returned. If not, the function will return +// `undefined`. One important note is that the array given as the `pathToValue` +// argument *will* be modified by the logic, so if that is not desired, a copy +// should be passed by the caller (using .slice(), for example). +function getObjectValueAtPath(obj, pathToValue) { + // If obj is undefined, there is nothing to process. + if (obj === undefined) { + return; + } + + // If we've exhausted the whole path, we've found the desired value! + if (!pathToValue.length) { + return obj; + } + + const p = pathToValue.shift(); + + // Check for sentinel indicating an array. + if (p === '[]' && Array.isArray(obj) && obj.length) { + return getObjectValueAtPath(obj[0], pathToValue); + } + + // Check for sentinel indicating a dictionary. + if (p === '{}' && isObject(obj) && Object.values(obj).length) { + return getObjectValueAtPath(Object.values(obj)[0], pathToValue); + } + + // Standard model path. + if (obj[p]) { + return getObjectValueAtPath(obj[p], pathToValue); + } + + // Return undefined if we don't find anything. + return; +} + +// "Indirect" examples are those coming from request bodies, response bodies, and parameters. +function checkForIndirectExamples(path, apidef) { + // Parameter and Media Type objects have the same format when it comes + // to examples, so we can treat all of these scenarios the same way. + if ( + isRequestBodySchema(path) || + isResponseSchema(path) || + isParamSchema(path) || + isParamContentSchema(path) + ) { + // Example fields would be siblings of the schema we're looking at, so we need to look in the API + // for the path, minus the last value (which is "schema"). + const examples = getOpenApiExamples( + getObjectValueAtPath(apidef, path.slice(0, -1)) + ); + + // Check for the special case of looking at a content schema for a parameter that + // itself defines an example (pull the last three values off the path to check). + if (isParamContentSchema(path)) { + examples.push( + ...getOpenApiExamples(getObjectValueAtPath(apidef, path.slice(0, -3))) + ); + } + + logger.debug( + `${ruleId}: ${ + examples.length + } indirect examples found for schema at location: ${path.join('.')}` + ); + + // Put the examples in the format the downstream algorithm for this rule needs. + return examples.map(example => { + return { + example, + examplePath: [], // All top-level examples get an empty array for the path. + }; + }); + } + + return []; +} + +// OpenAPI defines its own example structure, separate from schema examples, +// on Parameter and Media Type objects. Use this function to parse those +// structures and return any relevant examples. The argument may be either a +// Parameter or Media Type object and will return a list. +function getOpenApiExamples(artifact) { + if (!isObject(artifact)) { + return []; + } + + // The `example` and `examples` fields are mutually exclusive. + if (artifact.example) { + return [artifact.example]; + } + + // This will be a map, potentially containing multiple examples. Return all of them. + if (artifact.examples) { + return Object.values(artifact.examples).map( + exampleObject => exampleObject.value + ); + } + + return []; +} + +// This function determines if a schema is a "dictionary" (as opposed to a +// standard model with static properties) based on the presence of either +// `additionalProperties` or `patternProperties` (OpenAPI 3.1 only). +function isDictionarySchema(schema) { + return schemaHasConstraint( + schema, + s => isObjectSchema(s) && (s.additionalProperties || s.patternProperties) + ); +} diff --git a/packages/ruleset/src/ibm-oas.js b/packages/ruleset/src/ibm-oas.js index 2ec859ca..d2055330 100644 --- a/packages/ruleset/src/ibm-oas.js +++ b/packages/ruleset/src/ibm-oas.js @@ -197,6 +197,7 @@ module.exports = { 'ibm-unevaluated-properties': ibmRules.unevaluatedProperties, 'ibm-unique-parameter-request-property-names': ibmRules.uniqueParameterRequestPropertyNames, + 'ibm-use-date-based-format': ibmRules.useDateBasedFormat, 'ibm-valid-path-segments': ibmRules.validPathSegments, 'ibm-well-defined-dictionaries': ibmRules.wellDefinedDictionaries, }, diff --git a/packages/ruleset/src/rules/index.js b/packages/ruleset/src/rules/index.js index 129db06a..8e0fd15c 100644 --- a/packages/ruleset/src/rules/index.js +++ b/packages/ruleset/src/rules/index.js @@ -90,6 +90,7 @@ module.exports = { unevaluatedProperties: require('./unevaluated-properties'), unusedTags: require('./unused-tags'), uniqueParameterRequestPropertyNames: require('./unique-parameter-request-property-names'), + useDateBasedFormat: require('./use-date-based-format'), validPathSegments: require('./valid-path-segments'), wellDefinedDictionaries: require('./well-defined-dictionaries'), }; diff --git a/packages/ruleset/src/rules/use-date-based-format.js b/packages/ruleset/src/rules/use-date-based-format.js new file mode 100644 index 00000000..cf4276e2 --- /dev/null +++ b/packages/ruleset/src/rules/use-date-based-format.js @@ -0,0 +1,23 @@ +/** + * Copyright 2024 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const { + schemas, +} = require('@ibm-cloud/openapi-ruleset-utilities/src/collections'); +const { oas3 } = require('@stoplight/spectral-formats'); +const { useDateBasedFormat } = require('../functions'); + +module.exports = { + description: + 'Heuristically determine when a schema should have a format of "date" or "date-time"', + message: '{{error}}', + severity: 'warn', + formats: [oas3], + resolved: true, + given: schemas, + then: { + function: useDateBasedFormat, + }, +}; diff --git a/packages/ruleset/src/utils/date-based-utils.js b/packages/ruleset/src/utils/date-based-utils.js new file mode 100644 index 00000000..4327d305 --- /dev/null +++ b/packages/ruleset/src/utils/date-based-utils.js @@ -0,0 +1,101 @@ +/** + * Copyright 2024 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +/** + * This function takes a schema property name and uses a collection of regular + * expressions to heuristically determine if the property is a date or a date + * time property. It returns `true` if the name matches one of the expressions. + * + * @param {string} name the name of a schema property + * @returns a boolean value indicating that the property seems to be date-based + */ +function isDateBasedName(name) { + // Check for matching against certain patterns. + const dateBasedNamePatterns = [ + // The name `created`. + new RegExp('^created$'), + + // The name `updated`. + new RegExp('^updated$'), + + // The name `modified`. + new RegExp('^modified$'), + + // The name `expired`. + new RegExp('^expired$'), + + // The name `expires`. + new RegExp('^expires$'), + + // Any name ending in `_at`. + new RegExp('.*_at$'), + + // Any name ending in `_on`. + new RegExp('.*_on$'), + + // Any name starting with `date_`. + new RegExp('^date_.*'), + + // Any name containing `_date_`. + new RegExp('.*_date_.*'), + + // Any name ending in `_date`. + new RegExp('.*_date$'), + + // Any name starting with `time_`. + new RegExp('^time_.*'), + + // Any name containing `_time_`. + new RegExp('.*_time_.*'), + + // Note: not including any name ending in `_time` because it was too easy + // to think of counterexamples. `running_time` in the "movies" API of our + // test document in this project is one of them. + + // Any name containing `timestamp`. + new RegExp('.*timestamp.*'), + ]; + + return dateBasedNamePatterns.some(regex => regex.test(name)); +} + +/** + * This function takes an example string value and uses a collection of regular + * expressions to heuristically determine if the value is a date or a date + * time. It returns `true` if the value matches one of the expressions. + * + * @param {string} value an example value for a schema or schema property + * @returns a boolean value indicating that the value seems to be date-based + */ +function isDateBasedValue(value) { + const regularExpressions = [ + // Includes abbreviated month name. + new RegExp('\\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\b'), + + // Includes full month name. + new RegExp( + '\\b(January|February|March|April|May|June|July|August|September|October|November|December)\\b' + ), + + // Includes date in the format YYYY(./-)MM(./-)DD(T). + new RegExp('\\b\\d{4}[./-](0?[1-9]|1[012])[./-]([012]?[1-9]|3[01])(\\b|T)'), + + // Includes date in the format DD(./-)MM(./-)YYYY. + new RegExp('\\b([012]?[1-9]|3[01])[./-](0?[1-9]|1[012])[./-]\\d{4}\\b'), + + // Includes date in the format MM(./-)DD(./-)YYYY. + new RegExp('\\b(0?[1-9]|1[012])[./-]([012]?[1-9]|3[01])[./-]\\d{4}\\b'), + + // Includes time in the format (T)tt:tt:tt (where t can be s/m/h/etc.) + new RegExp('(\\b|T)\\d\\d:\\d\\d:\\d\\d\\b'), + ]; + + return regularExpressions.some(r => r.test(value)); +} + +module.exports = { + isDateBasedName, + isDateBasedValue, +}; diff --git a/packages/ruleset/src/utils/index.js b/packages/ruleset/src/utils/index.js index 82e2f705..5ea3ea17 100644 --- a/packages/ruleset/src/utils/index.js +++ b/packages/ruleset/src/utils/index.js @@ -21,6 +21,7 @@ module.exports = { operationMethods: require('./constants'), pathHasMinimallyRepresentedResource: require('./path-has-minimally-represented-resource'), pathMatchesRegexp: require('./path-matches-regexp'), + ...require('./date-based-utils'), ...require('./mimetype-utils'), ...require('./pagination-utils'), ...require('./path-location-utils'), diff --git a/packages/ruleset/test/rules/use-date-based-format.test.js b/packages/ruleset/test/rules/use-date-based-format.test.js new file mode 100644 index 00000000..d4f65dd2 --- /dev/null +++ b/packages/ruleset/test/rules/use-date-based-format.test.js @@ -0,0 +1,4082 @@ +/** + * Copyright 2024 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const { useDateBasedFormat } = require('../../src/rules'); +const { + makeCopy, + rootDocument, + testRule, + severityCodes, +} = require('../test-utils'); + +const rule = useDateBasedFormat; +const ruleId = 'ibm-use-date-based-format'; +const expectedSeverity = severityCodes.warning; +const expectedNameMsg = + 'According to its name, this property should use type "string" and format "date" or "date-time"'; +const expectedExampleMsg = + 'According to its example value, this schema should use type "string" and format "date" or "date-time"'; + +// To enable debug logging in the rule function, copy this statement to an it() block: +// LoggerFactory.getInstance().addLoggerSetting(ruleId, 'debug'); +// and uncomment this import statement: +// const LoggerFactory = require('../../src/utils/logger-factory'); + +describe(`Spectral rule: ${ruleId}`, () => { + describe('Should not yield errors', () => { + it('Clean spec', async () => { + const results = await testRule(ruleId, rule, rootDocument); + expect(results).toHaveLength(0); + }); + + it('date/time property ending in _at', async () => { + const testDocument = makeCopy(rootDocument); + ['date', 'date-time'].forEach(format => { + ['created_at', 'modified_at', 'updated_at'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + format, + }; + }); + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('date/time property ending in _on', async () => { + const testDocument = makeCopy(rootDocument); + ['date', 'date-time'].forEach(format => { + ['created_on', 'modified_on', 'expires_on'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + format, + }; + }); + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('date/time property containing the word "date"', async () => { + const testDocument = makeCopy(rootDocument); + ['date', 'date-time'].forEach(format => { + ['first_date', 'new_date_when', 'date_next'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + format, + }; + }); + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('date/time property containing the word "time"', async () => { + const testDocument = makeCopy(rootDocument); + ['date', 'date-time'].forEach(format => { + ['a_time_for_updating', 'time_is'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + format, + }; + }); + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('date/time property containing the word "timestamp"', async () => { + const testDocument = makeCopy(rootDocument); + ['date', 'date-time'].forEach(format => { + ['photo_timestamp', 'photo_timestamp_value', 'timestamp_value'].forEach( + propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + format, + }; + } + ); + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('date/time property with a time-based name', async () => { + const testDocument = makeCopy(rootDocument); + ['date', 'date-time'].forEach(format => { + ['created', 'updated', 'modified', 'expired', 'expires'].forEach( + propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + format, + }; + } + ); + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('top level date/time property with example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.some_datetimeprop = { + type: 'string', + format: 'date-time', + example: '1990-12-31T23:59:60Z', + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('nested date/time property with example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + format: 'date-time', + example: 'July 3, 2023, 4:15 PM', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('doubly nested date/time property with example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + modification_info: { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + format: 'date-time', + example: '2023-07-03T16:15:00+00:00', + }, + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('date/time items schema with example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'array', + items: { + type: 'string', + format: 'date-time', + example: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('date/time dictionary schema with example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'object', + additionalProperties: { + type: 'string', + format: 'date-time', + example: 'Monday, 03-Jul-23 16:15:00 GMT', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('primitive date/time oneOf schema with example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.first_completed = { + oneOf: [ + { + type: 'string', + format: 'date-time', + example: '01/01/2023', + }, + ], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + + it('date/time property nested in oneOf schema with example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + oneOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + format: 'date-time', + example: 'Oct. 31', + }, + }, + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); + }); + + describe('Should yield errors', () => { + describe('Should detect date times based on names', () => { + it('string property ending in _at', async () => { + const testDocument = makeCopy(rootDocument); + ['created_at', 'modified_at', 'updated_at'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + }; + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(12); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created_at', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified_at', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.updated_at', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created_at', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified_at', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.updated_at', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created_at', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified_at', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.updated_at', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created_at', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified_at', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.updated_at', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('integer property ending in _at', async () => { + const testDocument = makeCopy(rootDocument); + ['created_at', 'modified_at', 'updated_at'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'integer', + }; + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(12); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created_at', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified_at', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.updated_at', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created_at', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified_at', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.updated_at', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created_at', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified_at', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.updated_at', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created_at', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified_at', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.updated_at', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('string property ending in _on', async () => { + const testDocument = makeCopy(rootDocument); + ['created_on', 'modified_on', 'expires_on'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + }; + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(12); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created_on', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expires_on', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified_on', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created_on', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expires_on', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified_on', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created_on', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expires_on', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified_on', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created_on', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expires_on', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified_on', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('integer property ending in _on', async () => { + const testDocument = makeCopy(rootDocument); + ['created_on', 'modified_on', 'expires_on'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'integer', + }; + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(12); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created_on', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expires_on', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified_on', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created_on', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expires_on', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified_on', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created_on', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expires_on', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified_on', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created_on', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expires_on', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified_on', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('string property containing the word "date"', async () => { + const testDocument = makeCopy(rootDocument); + ['first_date', 'new_date_when', 'date_next'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + }; + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(12); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.date_next', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.first_date', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.new_date_when', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.date_next', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.first_date', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.new_date_when', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.date_next', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_date', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.new_date_when', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.date_next', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_date', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.new_date_when', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('integer property containing the word "date"', async () => { + const testDocument = makeCopy(rootDocument); + ['first_date', 'new_date_when', 'date_next'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'integer', + }; + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(12); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.date_next', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.first_date', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.new_date_when', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.date_next', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.first_date', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.new_date_when', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.date_next', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_date', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.new_date_when', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.date_next', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_date', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.new_date_when', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('string property containing the word "time"', async () => { + const testDocument = makeCopy(rootDocument); + ['a_time_for_updating', 'time_is'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + }; + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(8); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.a_time_for_updating', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.time_is', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.a_time_for_updating', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.time_is', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.a_time_for_updating', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.time_is', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.a_time_for_updating', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.time_is', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('integer property containing the word "time"', async () => { + const testDocument = makeCopy(rootDocument); + ['a_time_for_updating', 'time_is'].forEach(propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'integer', + }; + }); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(8); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.a_time_for_updating', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.time_is', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.a_time_for_updating', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.time_is', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.a_time_for_updating', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.time_is', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.a_time_for_updating', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.time_is', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('string property containing the word "timestamp"', async () => { + const testDocument = makeCopy(rootDocument); + ['photo_timestamp', 'photo_timestamp_value', 'timestamp_value'].forEach( + propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + }; + } + ); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(12); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.photo_timestamp', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.photo_timestamp_value', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.timestamp_value', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.photo_timestamp', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.photo_timestamp_value', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.timestamp_value', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.photo_timestamp', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.photo_timestamp_value', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.timestamp_value', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.photo_timestamp', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.photo_timestamp_value', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.timestamp_value', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('integer property containing the word "timestamp"', async () => { + const testDocument = makeCopy(rootDocument); + ['photo_timestamp', 'photo_timestamp_value', 'timestamp_value'].forEach( + propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'integer', + }; + } + ); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(12); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.photo_timestamp', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.photo_timestamp_value', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.timestamp_value', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.photo_timestamp', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.photo_timestamp_value', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.timestamp_value', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.photo_timestamp', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.photo_timestamp_value', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.timestamp_value', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.photo_timestamp', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.photo_timestamp_value', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.timestamp_value', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('string property with a time-based name', async () => { + const testDocument = makeCopy(rootDocument); + ['created', 'updated', 'modified', 'expired', 'expires'].forEach( + propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'string', + }; + } + ); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(20); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expired', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expires', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.updated', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expired', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expires', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.updated', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expired', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expires', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.updated', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expired', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expires', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.updated', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('integer property with a time-based name', async () => { + const testDocument = makeCopy(rootDocument); + ['created', 'updated', 'modified', 'expired', 'expires'].forEach( + propName => { + testDocument.components.schemas.Movie.properties[propName] = { + type: 'integer', + }; + } + ); + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(20); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.created', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expired', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.expires', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.modified', + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.updated', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.created', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expired', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.expires', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.modified', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.updated', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.created', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expired', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.expires', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.modified', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.updated', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.created', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expired', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.expires', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.modified', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.updated', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedNameMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('Should detect date times based on example values', () => { + describe('primitive parameters', () => { + it('parameter defined with schema, example in schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/drinks'].parameters = [ + { + name: 'some_datetime_param', + in: 'query', + required: false, + schema: { + type: 'string', + example: '1990-12-31T23:59:60Z', + }, + }, + ]; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = ['paths./v1/drinks.parameters.0.schema']; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('parameter defined with schema, example in example field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/drinks'].parameters = [ + { + name: 'some_datetime_param', + in: 'query', + required: false, + schema: { + type: 'string', + }, + example: '1990-12-31T23:59:60Z', + }, + ]; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = ['paths./v1/drinks.parameters.0.schema']; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('parameter defined with schema, example in examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/drinks'].parameters = [ + { + name: 'some_datetime_param', + in: 'query', + required: false, + schema: { + type: 'string', + }, + examples: { + firstExample: { + value: '1990-12-31T23:59:60Z', + }, + }, + }, + ]; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = ['paths./v1/drinks.parameters.0.schema']; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('parameter defined with content, example in schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/drinks'].parameters = [ + { + name: 'some_datetime_param', + in: 'query', + required: false, + content: { + 'application/json': { + schema: { + type: 'string', + example: '1990-12-31T23:59:60Z', + }, + }, + }, + }, + ]; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/drinks.parameters.0.content.application/json.schema', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('parameter defined with content, example in example field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/drinks'].parameters = [ + { + name: 'some_datetime_param', + in: 'query', + required: false, + example: '1990-12-31T23:59:60Z', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + ]; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/drinks.parameters.0.content.application/json.schema', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('parameter defined with content, example in examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/drinks'].parameters = [ + { + name: 'some_datetime_param', + in: 'query', + required: false, + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + examples: { + firstExample: { + value: '1990-12-31T23:59:60Z', + }, + }, + }, + ]; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/drinks.parameters.0.content.application/json.schema', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('parameter defined with content, example in content example field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/drinks'].parameters = [ + { + name: 'some_datetime_param', + in: 'query', + required: false, + content: { + 'application/json': { + schema: { + type: 'string', + }, + example: '1990-12-31T23:59:60Z', + }, + }, + }, + ]; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/drinks.parameters.0.content.application/json.schema', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('parameter defined with content, example in content examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/drinks'].parameters = [ + { + name: 'some_datetime_param', + in: 'query', + required: false, + content: { + 'application/json': { + schema: { + type: 'string', + }, + examples: { + firstExample: { + value: '1990-12-31T23:59:60Z', + }, + }, + }, + }, + }, + ]; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/drinks.parameters.0.content.application/json.schema', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('top level properties', () => { + it('top level property with its own example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.some_datetimeprop = { + type: 'string', + example: '1990-12-31T23:59:60Z', + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.some_datetimeprop', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('top level property with example in primary schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.some_datetimeprop = { + type: 'string', + }; + + testDocument.components.schemas.Movie.example = { + some_datetimeprop: '1990-12-31T23:59:60Z', + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.some_datetimeprop', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('top level property with example in response body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.some_datetimeprop = { + type: 'string', + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].example = { + some_datetimeprop: '1990-12-31T23:59:60Z', + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('top level property with example in response body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.some_datetimeprop = { + type: 'string', + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + some_datetimeprop: '1990-12-31T23:59:60Z', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('top level property with example in request body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.some_datetimeprop = + { + type: 'string', + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].example = { + some_datetimeprop: '1990-12-31T23:59:60Z', + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('top level property with example in request body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.some_datetimeprop = + { + type: 'string', + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + some_datetimeprop: '1990-12-31T23:59:60Z', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('nested properties', () => { + it('nested property with its own example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + example: 'July 3, 2023, 4:15 PM', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.some_datetimeprop', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested property with example in parent', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + example: { + some_datetimeprop: 'July 3, 2023, 4:15 PM', + }, + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.some_datetimeprop', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested property with example in primary schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }; + + testDocument.components.schemas.Movie.example = { + metadata: { + some_datetimeprop: '1990-12-31T23:59:60Z', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.some_datetimeprop', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested property with example in response body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].example = { + metadata: { + some_datetimeprop: 'July 3, 2023, 4:15 PM', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested property with example in response body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + some_datetimeprop: 'July 3, 2023, 4:15 PM', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested property with example in request body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].example = { + metadata: { + some_datetimeprop: 'July 3, 2023, 4:15 PM', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested property with example in request body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + some_datetimeprop: 'July 3, 2023, 4:15 PM', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('doubly nested properties', () => { + it('doubly nested property with its own example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + modification_info: { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + example: '2023-07-03T16:15:00+00:00', + }, + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('doubly nested property with example in parent', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + modification_info: { + type: 'object', + example: { + some_datetimeprop: '2023-07-03T16:15:00+00:00', + }, + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('doubly nested property with example in grandparent', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + example: { + modification_info: { + some_datetimeprop: '2023-07-03T16:15:00+00:00', + }, + }, + properties: { + modification_info: { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('doubly nested property with example in primary schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + modification_info: { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }, + }, + }; + + testDocument.components.schemas.Movie.example = { + metadata: { + modification_info: { + some_datetimeprop: '2023-07-03T16:15:00+00:00', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('doubly nested property with example in response body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + modification_info: { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }, + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].example = { + metadata: { + modification_info: { + some_datetimeprop: '2023-07-03T16:15:00+00:00', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('doubly nested property with example in response body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + modification_info: { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }, + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + modification_info: { + some_datetimeprop: '2023-07-03T16:15:00+00:00', + }, + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('doubly nested property with example in request body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + type: 'object', + properties: { + modification_info: { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }, + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].example = { + metadata: { + modification_info: { + some_datetimeprop: '2023-07-03T16:15:00+00:00', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('doubly nested property with example in request body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + type: 'object', + properties: { + modification_info: { + type: 'object', + properties: { + some_datetimeprop: { + type: 'string', + }, + }, + }, + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + modification_info: { + some_datetimeprop: '2023-07-03T16:15:00+00:00', + }, + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.modification_info.properties.some_datetimeprop', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('items schemas', () => { + it('items schema with its own example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'array', + items: { + type: 'string', + example: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.items', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.items', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.items', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('items schema with example in array parent', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'array', + example: ['Mon, 03 Jul 23 16:15:00 +0000'], + items: { + type: 'string', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.items', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.items', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.items', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('items schema with example in primary schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'array', + items: { + type: 'string', + }, + }; + + testDocument.components.schemas.Movie.example = { + changes: ['Mon, 03 Jul 23 16:15:00 +0000'], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.items', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.items', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.items', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('items schema with example in response body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'array', + items: { + type: 'string', + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].example = { + changes: ['Mon, 03 Jul 23 16:15:00 +0000'], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.items', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('items schema with example in response body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'array', + items: { + type: 'string', + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + changes: ['Mon, 03 Jul 23 16:15:00 +0000'], + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.items', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('items schema with example in request body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.changes = { + type: 'array', + items: { + type: 'string', + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].example = { + changes: ['Mon, 03 Jul 23 16:15:00 +0000'], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('items schema with example in request body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.changes = { + type: 'array', + items: { + type: 'string', + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + changes: ['Mon, 03 Jul 23 16:15:00 +0000'], + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('nested items schemas', () => { + it('nested items schema with its own example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'array', + items: { + type: 'string', + example: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.items', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.items', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested items schema with example in array parent', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'array', + example: ['Mon, 03 Jul 23 16:15:00 +0000'], + items: { + type: 'string', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.items', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.items', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested items schema with example in parent object', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + example: { + changes: ['Mon, 03 Jul 23 16:15:00 +0000'], + }, + properties: { + changes: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.items', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.items', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested items schema with example in primary schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }; + + testDocument.components.schemas.Movie.example = { + metadata: { + changes: ['Mon, 03 Jul 23 16:15:00 +0000'], + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.items', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.items', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested items schema with example in response body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].example = { + metadata: { + changes: ['Mon, 03 Jul 23 16:15:00 +0000'], + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested items schema with example in response body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + changes: ['Mon, 03 Jul 23 16:15:00 +0000'], + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested items schema with example in request body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].example = { + metadata: { + changes: ['Mon, 03 Jul 23 16:15:00 +0000'], + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested items schema with example in request body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + changes: ['Mon, 03 Jul 23 16:15:00 +0000'], + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.changes.items', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('dictionary schemas - additionalProperties', () => { + it('dictionary schema with its own example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'object', + example: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + additionalProperties: { + type: 'string', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.additionalProperties', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('dictionary schema with example in the value schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'object', + additionalProperties: { + type: 'string', + example: 'Monday, 03-Jul-23 16:15:00 GMT', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.additionalProperties', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('dictionary schema with example in primary schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'object', + additionalProperties: { + type: 'string', + }, + }; + + testDocument.components.schemas.Movie.example = { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.additionalProperties', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('dictionary schema with example in response body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'object', + additionalProperties: { + type: 'string', + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].example = { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('dictionary schema with example in response body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'object', + additionalProperties: { + type: 'string', + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('dictionary schema with example in request body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.changes = { + type: 'object', + additionalProperties: { + type: 'string', + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].example = { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('dictionary schema with example in request body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.changes = { + type: 'object', + additionalProperties: { + type: 'string', + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('dictionary schemas - patternProperties', () => { + it('patterned dictionary schema with its own example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'object', + example: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + patternProperties: { + '^what.*$': { + type: 'string', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.patternProperties.^what.*$', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.patternProperties.^what.*$', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('patterned dictionary schema with example in the value schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'object', + patternProperties: { + '^what.*$': { + type: 'string', + example: 'Monday, 03-Jul-23 16:15:00 GMT', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.patternProperties.^what.*$', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.patternProperties.^what.*$', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('patterned dictionary schema with example in primary schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'object', + patternProperties: { + '^what.*$': { + type: 'string', + }, + }, + }; + + testDocument.components.schemas.Movie.example = { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.changes.patternProperties.^what.*$', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.changes.patternProperties.^what.*$', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('patterned dictionary schema with example in response body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'object', + patternProperties: { + '^what.*$': { + type: 'string', + }, + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].example = { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('patterned dictionary schema with example in response body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.changes = { + type: 'object', + patternProperties: { + '^what.*$': { + type: 'string', + }, + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.changes.patternProperties.^what.*$', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('patterned dictionary schema with example in request body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.changes = { + type: 'object', + patternProperties: { + '^what.*$': { + type: 'string', + }, + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].example = { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.patternProperties.^what.*$', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('patterned dictionary schema with example in request body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.changes = { + type: 'object', + patternProperties: { + '^what.*$': { + type: 'string', + }, + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.changes.patternProperties.^what.*$', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('nested dictionary schemas', () => { + it('nested dictionary schema with its own example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'object', + example: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + additionalProperties: { + type: 'string', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested dictionary schema with example in the value schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'object', + additionalProperties: { + example: 'Mon, 03 Jul 23 16:15:00 +0000', + type: 'string', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested dictionary schema with example in parent object', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + example: { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }, + properties: { + changes: { + type: 'object', + additionalProperties: { + type: 'string', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested dictionary schema with example in primary schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'object', + additionalProperties: { + type: 'string', + }, + }, + }, + }; + + testDocument.components.schemas.Movie.example = { + metadata: { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested dictionary schema with example in response body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'object', + additionalProperties: { + type: 'string', + }, + }, + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].example = { + metadata: { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested dictionary schema with example in response body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'object', + additionalProperties: { + type: 'string', + }, + }, + }, + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested dictionary schema with example in request body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'object', + additionalProperties: { + type: 'string', + }, + }, + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].example = { + metadata: { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('nested dictionary schema with example in request body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + type: 'object', + properties: { + changes: { + type: 'object', + additionalProperties: { + type: 'string', + }, + }, + }, + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + changes: { + whatever: 'Mon, 03 Jul 23 16:15:00 +0000', + }, + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.properties.changes.additionalProperties', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('primitive oneOf schemas', () => { + it('primitive oneOf schema with its own example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.first_completed = { + oneOf: [ + { + type: 'string', + example: '01/01/2023', + }, + ], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.first_completed.oneOf.0', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.first_completed.oneOf.0', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_completed.oneOf.0', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_completed.oneOf.0', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('primitive oneOf schema with example in parent', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.first_completed = { + example: '01/01/2023', + oneOf: [ + { + type: 'string', + }, + ], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.first_completed', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.first_completed', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('primitive oneOf schema with example in primary schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.first_completed = { + oneOf: [ + { + type: 'string', + }, + ], + }; + + testDocument.components.schemas.Movie.example = { + first_completed: '01/01/2023', + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.first_completed', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.first_completed', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('primitive oneOf schema with example in response body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.first_completed = { + oneOf: [ + { + type: 'string', + }, + ], + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].example = { + first_completed: '01/01/2023', + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('primitive oneOf schema with example in response body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.first_completed = { + oneOf: [ + { + type: 'string', + }, + ], + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + first_completed: '01/01/2023', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('primitive oneOf schema with example in request body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.first_completed = + { + oneOf: [ + { + type: 'string', + }, + ], + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].example = { + first_completed: '01/01/2023', + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('primitive oneOf schema with example in request body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.first_completed = + { + oneOf: [ + { + type: 'string', + }, + ], + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + first_completed: '01/01/2023', + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('properties nested in oneOf schema', () => { + it('property nested in oneOf schema with its own example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + oneOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + example: 'Oct. 31', + }, + }, + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.properties.first_completed', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in oneOf schema with example in parent', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + example: { + first_completed: 'Oct. 31', + }, + oneOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.properties.first_completed', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in oneOf schema with example in primary schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + oneOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + testDocument.components.schemas.Movie.example = { + metadata: { + first_completed: 'Oct. 31', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.properties.first_completed', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in oneOf schema with example in response body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + oneOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].example = { + metadata: { + first_completed: 'Oct. 31', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in oneOf schema with example in response body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + oneOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + first_completed: 'Oct. 31', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in oneOf schema with example in request body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + oneOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].example = { + metadata: { + first_completed: 'Oct. 31', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in oneOf schema with example in request body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + oneOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + first_completed: 'Oct. 31', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.oneOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + + describe('properties nested in nested oneOf schema', () => { + it('property nested in nested oneOf schema with its own example', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + oneOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + example: 'Oct. 31', + }, + }, + }, + { + type: 'object', + properties: { + something_else: { + type: 'integer', + }, + }, + }, + ], + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in nested oneOf schema with example in parent', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + oneOf: [ + { + anyOf: [ + { + example: { + first_completed: 'Oct. 31', + }, + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + something_else: { + type: 'integer', + }, + }, + }, + ], + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in nested oneOf schema with example in grandparent', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + oneOf: [ + { + example: { + first_completed: 'Oct. 31', + }, + anyOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + something_else: { + type: 'integer', + }, + }, + }, + ], + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in nested oneOf schema with example at top level', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + example: { + first_completed: 'Oct. 31', + }, + oneOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + something_else: { + type: 'integer', + }, + }, + }, + ], + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in oneOf schema with example in primary schema', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + oneOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + something_else: { + type: 'integer', + }, + }, + }, + ], + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + testDocument.components.schemas.Movie.example = { + metadata: { + first_completed: 'Oct. 31', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(4); + + const expectedPaths = [ + 'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies.post.responses.201.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in nested oneOf schema with example in response body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + oneOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + something_else: { + type: 'integer', + }, + }, + }, + ], + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].example = { + metadata: { + first_completed: 'Oct. 31', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in nested oneOf schema with example in response body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.Movie.properties.metadata = { + oneOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + something_else: { + type: 'integer', + }, + }, + }, + ], + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + testDocument.components.responses.MovieWithETag.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + first_completed: 'Oct. 31', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPaths = [ + 'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + 'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in nested oneOf schema with example in request body', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + oneOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + something_else: { + type: 'integer', + }, + }, + }, + ], + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].example = { + metadata: { + first_completed: 'Oct. 31', + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + + it('property nested in nested oneOf schema with example in request body examples field', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.components.schemas.MoviePrototype.properties.metadata = { + oneOf: [ + { + anyOf: [ + { + type: 'object', + properties: { + first_completed: { + type: 'string', + }, + }, + }, + { + type: 'object', + properties: { + something_else: { + type: 'integer', + }, + }, + }, + ], + }, + { + type: 'object', + properties: { + irrelevant: { + type: 'boolean', + }, + }, + }, + ], + }; + + testDocument.paths['/v1/movies'].post.requestBody.content[ + 'application/json' + ].examples = { + firstExample: { + value: { + metadata: { + first_completed: 'Oct. 31', + }, + }, + }, + }; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPaths = [ + 'paths./v1/movies.post.requestBody.content.application/json.schema.properties.metadata.oneOf.0.anyOf.0.properties.first_completed', + ]; + + for (const i in results) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedExampleMsg); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPaths[i]); + } + }); + }); + }); + }); +}); diff --git a/packages/ruleset/test/utils/date-based-utils.test.js b/packages/ruleset/test/utils/date-based-utils.test.js new file mode 100644 index 00000000..7c0cb802 --- /dev/null +++ b/packages/ruleset/test/utils/date-based-utils.test.js @@ -0,0 +1,73 @@ +/** + * Copyright 2024 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const { isDateBasedName, isDateBasedValue } = require('../../src/utils'); + +describe('Date-based utility functions', () => { + describe('isDateBasedValue()', () => { + // Positive tests. + it('should return `true` for date time values', () => { + expect(isDateBasedValue('1990-12-31T23:59:60Z')).toBe(true); + expect(isDateBasedValue('1990-12-31T15:59:60-08:00')).toBe(true); + expect(isDateBasedValue('2023-01-01')).toBe(true); + expect(isDateBasedValue('01/01/2023')).toBe(true); + expect(isDateBasedValue('Mon, 03 Jul 2023 16:15:00 +0000')).toBe(true); + expect(isDateBasedValue('Monday, 03-Jul-23 16:15:00 GMT')).toBe(true); + expect(isDateBasedValue('Mon, 03 Jul 23 16:15:00 +0000')).toBe(true); + expect(isDateBasedValue('2023-07-03T16:15:00+00:00')).toBe(true); + expect(isDateBasedValue('July 3, 2023, 4:15 PM')).toBe(true); + expect(isDateBasedValue('Oct. 31')).toBe(true); + }); + + // Negative tests. + it('should return `false` for non-date time values', () => { + expect(isDateBasedValue('Octopus')).toBe(false); + expect(isDateBasedValue('12345678')).toBe(false); + expect(isDateBasedValue('0001-01-2000')).toBe(false); + expect(isDateBasedValue('10.1.24.1')).toBe(false); + expect(isDateBasedValue('10.1.255.1')).toBe(false); + expect(isDateBasedValue(undefined)).toBe(false); + expect(isDateBasedValue(null)).toBe(false); + expect(isDateBasedValue(42)).toBe(false); + }); + }); + + describe('isDateBasedName()', () => { + // Positive tests. + it('should return `true` for date time values', () => { + expect(isDateBasedName('created_at')).toBe(true); + expect(isDateBasedName('modified_at')).toBe(true); + expect(isDateBasedName('updated_at')).toBe(true); + expect(isDateBasedName('created_on')).toBe(true); + expect(isDateBasedName('modified_on')).toBe(true); + expect(isDateBasedName('expires_on')).toBe(true); + expect(isDateBasedName('first_date')).toBe(true); + expect(isDateBasedName('new_date_when')).toBe(true); + expect(isDateBasedName('date_next')).toBe(true); + expect(isDateBasedName('a_time_for_updating')).toBe(true); + expect(isDateBasedName('time_is')).toBe(true); + expect(isDateBasedName('photo_timestamp')).toBe(true); + expect(isDateBasedName('photo_timestamp_value')).toBe(true); + expect(isDateBasedName('timestamp_value')).toBe(true); + expect(isDateBasedName('created')).toBe(true); + expect(isDateBasedName('updated')).toBe(true); + expect(isDateBasedName('modified')).toBe(true); + expect(isDateBasedName('expired')).toBe(true); + expect(isDateBasedName('expires')).toBe(true); + }); + + // Negative tests. + it('should return `false` for non-date time values', () => { + expect(isDateBasedName('running_time')).toBe(false); + expect(isDateBasedName('timeandtimeagain')).toBe(false); + expect(isDateBasedName('created_for')).toBe(false); + expect(isDateBasedName('ttl')).toBe(false); + expect(isDateBasedName('octopus')).toBe(false); + expect(isDateBasedName(undefined)).toBe(false); + expect(isDateBasedName(null)).toBe(false); + expect(isDateBasedName(42)).toBe(false); + }); + }); +}); diff --git a/packages/ruleset/test/utils/is-operation-of-type.test.js b/packages/ruleset/test/utils/is-operation-of-type.test.js index 5d36d5e2..83909232 100644 --- a/packages/ruleset/test/utils/is-operation-of-type.test.js +++ b/packages/ruleset/test/utils/is-operation-of-type.test.js @@ -5,7 +5,7 @@ const { isOperationOfType } = require('../../src/utils'); -describe('Utility function: getResourceSpecificSiblingPath', () => { +describe('Utility function: isOperationOfType', () => { it('should return `true` when path matches the given type', () => { expect(isOperationOfType('get', ['paths', '/v1/things', 'get'])).toBe(true); }); diff --git a/packages/validator/src/scoring-tool/rubric.js b/packages/validator/src/scoring-tool/rubric.js index f74c154d..b673b1e6 100644 --- a/packages/validator/src/scoring-tool/rubric.js +++ b/packages/validator/src/scoring-tool/rubric.js @@ -425,6 +425,11 @@ module.exports = { denominator: 'operations', categories: ['usability', 'robustness'], }, + 'ibm-use-date-based-format': { + coefficient: 1, + denominator: 'schemas', + categories: ['usability'], + }, 'ibm-valid-path-segments': { coefficient: 2, denominator: 'operations',