diff --git a/tools/spectral/ipa/__tests__/utils/validations.test.js b/tools/spectral/ipa/__tests__/utils/validations.test.js deleted file mode 100644 index a542e5f2ad..0000000000 --- a/tools/spectral/ipa/__tests__/utils/validations.test.js +++ /dev/null @@ -1,253 +0,0 @@ -// schemaUtils.test.js -import { checkForbiddenPropertyAttributesAndReturnErrors } from '../../rulesets/functions/utils/validations.js'; -import { describe, expect, it } from '@jest/globals'; - -describe('tools/spectral/ipa/rulesets/functions/utils/validations.js', () => { - describe('checkForbiddenPropertyAttributesAndReturnErrors', () => { - const mockPath = ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json']; - const errorMessage = 'Test error message'; - - it('handles primitive values', () => { - expect(checkForbiddenPropertyAttributesAndReturnErrors(null, 'readOnly', mockPath, [], errorMessage)).toEqual([]); - expect( - checkForbiddenPropertyAttributesAndReturnErrors(undefined, 'readOnly', mockPath, [], errorMessage) - ).toEqual([]); - expect(checkForbiddenPropertyAttributesAndReturnErrors('string', 'readOnly', mockPath, [], errorMessage)).toEqual( - [] - ); - expect(checkForbiddenPropertyAttributesAndReturnErrors(123, 'readOnly', mockPath, [], errorMessage)).toEqual([]); - expect(checkForbiddenPropertyAttributesAndReturnErrors(true, 'readOnly', mockPath, [], errorMessage)).toEqual([]); - }); - - it('detects direct attribute match', () => { - const schema = { - type: 'string', - readOnly: true, - }; - - const errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); - - expect(errors).toHaveLength(1); - expect(errors[0]).toEqual({ - path: mockPath, - message: `${errorMessage} Found readOnly property at one of the inline schemas.`, - }); - }); - - it('detects properties with the specified attribute', () => { - const schema = { - type: 'object', - properties: { - id: { - type: 'string', - readOnly: true, - }, - name: { - type: 'string', - }, - password: { - type: 'string', - writeOnly: true, - }, - }, - }; - - // Testing readOnly detection - let errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); - expect(errors).toHaveLength(1); - expect(errors[0].message).toContain('Found readOnly property at: id.'); - - // Testing writeOnly detection - errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'writeOnly', mockPath, [], errorMessage); - expect(errors).toHaveLength(1); - expect(errors[0].message).toContain('Found writeOnly property at: password.'); - }); - - it('detects nested properties with the specified attribute', () => { - const schema = { - type: 'object', - properties: { - user: { - type: 'object', - properties: { - id: { - type: 'string', - readOnly: true, - }, - credentials: { - type: 'object', - properties: { - password: { - type: 'string', - writeOnly: true, - }, - }, - }, - }, - }, - }, - }; - - // Testing deep readOnly detection - let errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); - expect(errors).toHaveLength(1); - expect(errors[0].message).toContain('Found readOnly property at: user.id.'); - - // Testing deep writeOnly detection - errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'writeOnly', mockPath, [], errorMessage); - expect(errors).toHaveLength(1); - expect(errors[0].message).toContain('Found writeOnly property at: user.credentials.password.'); - }); - - it('detects properties in array items', () => { - const schema = { - type: 'object', - properties: { - items: { - type: 'array', - items: { - type: 'object', - properties: { - id: { - type: 'string', - readOnly: true, - }, - secret: { - type: 'string', - writeOnly: true, - }, - }, - }, - }, - }, - }; - - // Testing readOnly in array items - let errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); - expect(errors).toHaveLength(1); - expect(errors[0].message).toContain('Found readOnly property at: items.items.id.'); - - // Testing writeOnly in array items - errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'writeOnly', mockPath, [], errorMessage); - expect(errors).toHaveLength(1); - expect(errors[0].message).toContain('Found writeOnly property at: items.items.secret.'); - }); - - it('detects properties in schema combiners', () => { - const schema = { - allOf: [ - { - type: 'object', - properties: { - id: { - type: 'string', - readOnly: true, - }, - }, - }, - ], - anyOf: [ - { - type: 'object', - properties: { - key: { - type: 'string', - writeOnly: true, - }, - }, - }, - ], - oneOf: [ - { - type: 'object', - }, - { - type: 'object', - properties: { - token: { - type: 'string', - readOnly: true, - }, - }, - }, - ], - }; - - // Testing readOnly in combiners - let errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); - expect(errors).toHaveLength(2); - expect(errors[0].message).toContain('Found readOnly property at: allOf.0.id.'); - expect(errors[1].message).toContain('Found readOnly property at: oneOf.1.token.'); - - // Testing writeOnly in combiners - errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'writeOnly', mockPath, [], errorMessage); - expect(errors).toHaveLength(1); - expect(errors[0].message).toContain('Found writeOnly property at: anyOf.0.key.'); - }); - - it('correctly accumulates multiple errors', () => { - const schema = { - type: 'object', - properties: { - id: { - type: 'string', - readOnly: true, - }, - nested: { - type: 'object', - properties: { - innerId: { - type: 'string', - readOnly: true, - }, - }, - }, - items: { - type: 'array', - items: { - readOnly: true, - }, - }, - }, - }; - - const errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); - - expect(errors).toHaveLength(3); - expect(errors[0].message).toContain('Found readOnly property at: id.'); - expect(errors[1].message).toContain('Found readOnly property at: nested.innerId.'); - expect(errors[2].message).toContain('Found readOnly property at: items.items.'); - }); - - it('handles empty objects', () => { - const schema = {}; - const errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); - expect(errors).toHaveLength(0); - }); - - it('handles schemas with no matching attributes', () => { - const schema = { - type: 'object', - properties: { - id: { type: 'string' }, - name: { type: 'string' }, - nested: { - type: 'object', - properties: { - value: { type: 'number' }, - }, - }, - items: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - }; - - const errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); - expect(errors).toHaveLength(0); - }); - }); -}); diff --git a/tools/spectral/ipa/__tests__/utils/validations/checkForbiddenPropertyAttributesAndReturnErrors.test.js b/tools/spectral/ipa/__tests__/utils/validations/checkForbiddenPropertyAttributesAndReturnErrors.test.js new file mode 100644 index 0000000000..33901563dd --- /dev/null +++ b/tools/spectral/ipa/__tests__/utils/validations/checkForbiddenPropertyAttributesAndReturnErrors.test.js @@ -0,0 +1,250 @@ +import { describe, expect, it } from '@jest/globals'; +import { checkForbiddenPropertyAttributesAndReturnErrors } from '../../../rulesets/functions/utils/validations/checkForbiddenPropertyAttributesAndReturnErrors.js'; + +describe('tools/spectral/ipa/rulesets/functions/utils/validations/checkForbiddenPropertyAttributesAndReturnErrors.js', () => { + const mockPath = ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json']; + const errorMessage = 'Test error message'; + + it('handles primitive values', () => { + expect(checkForbiddenPropertyAttributesAndReturnErrors(null, 'readOnly', mockPath, [], errorMessage)).toEqual([]); + expect(checkForbiddenPropertyAttributesAndReturnErrors(undefined, 'readOnly', mockPath, [], errorMessage)).toEqual( + [] + ); + expect(checkForbiddenPropertyAttributesAndReturnErrors('string', 'readOnly', mockPath, [], errorMessage)).toEqual( + [] + ); + expect(checkForbiddenPropertyAttributesAndReturnErrors(123, 'readOnly', mockPath, [], errorMessage)).toEqual([]); + expect(checkForbiddenPropertyAttributesAndReturnErrors(true, 'readOnly', mockPath, [], errorMessage)).toEqual([]); + }); + + it('detects direct attribute match', () => { + const schema = { + type: 'string', + readOnly: true, + }; + + const errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); + + expect(errors).toHaveLength(1); + expect(errors[0]).toEqual({ + path: mockPath, + message: `${errorMessage} Found readOnly property at one of the inline schemas.`, + }); + }); + + it('detects properties with the specified attribute', () => { + const schema = { + type: 'object', + properties: { + id: { + type: 'string', + readOnly: true, + }, + name: { + type: 'string', + }, + password: { + type: 'string', + writeOnly: true, + }, + }, + }; + + // Testing readOnly detection + let errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('Found readOnly property at: id.'); + + // Testing writeOnly detection + errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'writeOnly', mockPath, [], errorMessage); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('Found writeOnly property at: password.'); + }); + + it('detects nested properties with the specified attribute', () => { + const schema = { + type: 'object', + properties: { + user: { + type: 'object', + properties: { + id: { + type: 'string', + readOnly: true, + }, + credentials: { + type: 'object', + properties: { + password: { + type: 'string', + writeOnly: true, + }, + }, + }, + }, + }, + }, + }; + + // Testing deep readOnly detection + let errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('Found readOnly property at: user.id.'); + + // Testing deep writeOnly detection + errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'writeOnly', mockPath, [], errorMessage); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('Found writeOnly property at: user.credentials.password.'); + }); + + it('detects properties in array items', () => { + const schema = { + type: 'object', + properties: { + items: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + readOnly: true, + }, + secret: { + type: 'string', + writeOnly: true, + }, + }, + }, + }, + }, + }; + + // Testing readOnly in array items + let errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('Found readOnly property at: items.items.id.'); + + // Testing writeOnly in array items + errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'writeOnly', mockPath, [], errorMessage); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('Found writeOnly property at: items.items.secret.'); + }); + + it('detects properties in schema combiners', () => { + const schema = { + allOf: [ + { + type: 'object', + properties: { + id: { + type: 'string', + readOnly: true, + }, + }, + }, + ], + anyOf: [ + { + type: 'object', + properties: { + key: { + type: 'string', + writeOnly: true, + }, + }, + }, + ], + oneOf: [ + { + type: 'object', + }, + { + type: 'object', + properties: { + token: { + type: 'string', + readOnly: true, + }, + }, + }, + ], + }; + + // Testing readOnly in combiners + let errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); + expect(errors).toHaveLength(2); + expect(errors[0].message).toContain('Found readOnly property at: allOf.0.id.'); + expect(errors[1].message).toContain('Found readOnly property at: oneOf.1.token.'); + + // Testing writeOnly in combiners + errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'writeOnly', mockPath, [], errorMessage); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('Found writeOnly property at: anyOf.0.key.'); + }); + + it('correctly accumulates multiple errors', () => { + const schema = { + type: 'object', + properties: { + id: { + type: 'string', + readOnly: true, + }, + nested: { + type: 'object', + properties: { + innerId: { + type: 'string', + readOnly: true, + }, + }, + }, + items: { + type: 'array', + items: { + readOnly: true, + }, + }, + }, + }; + + const errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); + + expect(errors).toHaveLength(3); + expect(errors[0].message).toContain('Found readOnly property at: id.'); + expect(errors[1].message).toContain('Found readOnly property at: nested.innerId.'); + expect(errors[2].message).toContain('Found readOnly property at: items.items.'); + }); + + it('handles empty objects', () => { + const schema = {}; + const errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); + expect(errors).toHaveLength(0); + }); + + it('handles schemas with no matching attributes', () => { + const schema = { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + nested: { + type: 'object', + properties: { + value: { type: 'number' }, + }, + }, + items: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }; + + const errors = checkForbiddenPropertyAttributesAndReturnErrors(schema, 'readOnly', mockPath, [], errorMessage); + expect(errors).toHaveLength(0); + }); +}); diff --git a/tools/spectral/ipa/rulesets/functions/IPA104GetMethodResponseHasNoInputFields.js b/tools/spectral/ipa/rulesets/functions/IPA104GetMethodResponseHasNoInputFields.js index ecae56e9aa..b3351276eb 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA104GetMethodResponseHasNoInputFields.js +++ b/tools/spectral/ipa/rulesets/functions/IPA104GetMethodResponseHasNoInputFields.js @@ -7,7 +7,7 @@ import { isSingletonResource, } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; -import { checkForbiddenPropertyAttributesAndReturnErrors } from './utils/validations.js'; +import { checkForbiddenPropertyAttributesAndReturnErrors } from './utils/validations/checkForbiddenPropertyAttributesAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-104-get-method-response-has-no-input-fields'; const ERROR_MESSAGE = 'The get method response object must not include output fields (writeOnly properties).'; diff --git a/tools/spectral/ipa/rulesets/functions/IPA104GetMethodReturnsResponseSuffixedObject.js b/tools/spectral/ipa/rulesets/functions/IPA104GetMethodReturnsResponseSuffixedObject.js index 2a7dbbb0e4..e2b1781941 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA104GetMethodReturnsResponseSuffixedObject.js +++ b/tools/spectral/ipa/rulesets/functions/IPA104GetMethodReturnsResponseSuffixedObject.js @@ -7,7 +7,7 @@ import { import { resolveObject } from './utils/componentUtils.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; -import { checkSchemaRefSuffixAndReturnErrors } from './utils/validations.js'; +import { checkSchemaRefSuffixAndReturnErrors } from './utils/validations/checkSchemaRefSuffixAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-104-get-method-returns-response-suffixed-object'; diff --git a/tools/spectral/ipa/rulesets/functions/IPA104GetResponseCodeShouldBe200OK.js b/tools/spectral/ipa/rulesets/functions/IPA104GetResponseCodeShouldBe200OK.js index f85c002211..3180d6ea6e 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA104GetResponseCodeShouldBe200OK.js +++ b/tools/spectral/ipa/rulesets/functions/IPA104GetResponseCodeShouldBe200OK.js @@ -6,7 +6,7 @@ import { isSingleResourceIdentifier, isSingletonResource, } from './utils/resourceEvaluation.js'; -import { checkResponseCodeAndReturnErrors } from './utils/validations.js'; +import { checkResponseCodeAndReturnErrors } from './utils/validations/checkResponseCodeAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-104-get-method-response-code-is-200'; const ERROR_MESSAGE = diff --git a/tools/spectral/ipa/rulesets/functions/IPA104ValidOperationID.js b/tools/spectral/ipa/rulesets/functions/IPA104ValidOperationID.js index 60dee4bdd3..9786501d9a 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA104ValidOperationID.js +++ b/tools/spectral/ipa/rulesets/functions/IPA104ValidOperationID.js @@ -1,12 +1,11 @@ -import { generateOperationID } from './utils/operationIdGeneration.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; import { hasException } from './utils/exceptions.js'; import { getResourcePathItems, isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; import { hasCustomMethodOverride, hasMethodVerbOverride } from './utils/extensions.js'; import { isInvalidGetMethod } from './utils/methodLogic.js'; +import { validateOperationIdAndReturnErrors } from './utils/validations/validateOperationIdAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-104-valid-operation-id'; -const ERROR_MESSAGE = 'Invalid OperationID.'; export default (input, { methodName }, { path, documentInventory }) => { const resourcePath = path[1]; @@ -27,14 +26,9 @@ export default (input, { methodName }, { path, documentInventory }) => { return; } - const expectedOperationId = generateOperationID(methodName, resourcePath); - if (expectedOperationId !== input.operationId) { - const errors = [ - { - path, - message: `${ERROR_MESSAGE} Found ${input.operationId}, expected ${expectedOperationId}.`, - }, - ]; + const errors = validateOperationIdAndReturnErrors(methodName, resourcePath, input, path); + + if (errors.length > 0) { return collectAndReturnViolation(path, RULE_NAME, errors); } diff --git a/tools/spectral/ipa/rulesets/functions/IPA105ListResponseCodeShouldBe200OK.js b/tools/spectral/ipa/rulesets/functions/IPA105ListResponseCodeShouldBe200OK.js index 5d04d4e567..333fb36cf6 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA105ListResponseCodeShouldBe200OK.js +++ b/tools/spectral/ipa/rulesets/functions/IPA105ListResponseCodeShouldBe200OK.js @@ -5,7 +5,7 @@ import { isResourceCollectionIdentifier, isSingletonResource, } from './utils/resourceEvaluation.js'; -import { checkResponseCodeAndReturnErrors } from './utils/validations.js'; +import { checkResponseCodeAndReturnErrors } from './utils/validations/checkResponseCodeAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-105-list-method-response-code-is-200'; const ERROR_MESSAGE = diff --git a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js index 7b4f6188de..a28a6324d2 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js +++ b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js @@ -8,7 +8,7 @@ import { resolveObject } from './utils/componentUtils.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; import { getResponseOfGetMethodByMediaType } from './utils/methodUtils.js'; -import { checkRequestResponseResourceEqualityAndReturnErrors } from './utils/validations.js'; +import { checkRequestResponseResourceEqualityAndReturnErrors } from './utils/validations/checkRequestResponseResourceEqualityAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-106-create-method-request-body-is-get-method-response'; const ERROR_MESSAGE = diff --git a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.js b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.js index e6d70de3c0..a428acb448 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.js +++ b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsRequestSuffixedObject.js @@ -7,7 +7,7 @@ import { isSingletonResource, } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; -import { checkSchemaRefSuffixAndReturnErrors } from './utils/validations.js'; +import { checkSchemaRefSuffixAndReturnErrors } from './utils/validations/checkSchemaRefSuffixAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-106-create-method-request-body-is-request-suffixed-object'; diff --git a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestHasNoReadonlyFields.js b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestHasNoReadonlyFields.js index ccd0a2c8ac..9c35c8a1f1 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestHasNoReadonlyFields.js +++ b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestHasNoReadonlyFields.js @@ -7,7 +7,7 @@ import { import { resolveObject } from './utils/componentUtils.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; -import { checkForbiddenPropertyAttributesAndReturnErrors } from './utils/validations.js'; +import { checkForbiddenPropertyAttributesAndReturnErrors } from './utils/validations/checkForbiddenPropertyAttributesAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-106-create-method-request-has-no-readonly-fields'; const ERROR_MESSAGE = 'The Create method request object must not include input fields (readOnly properties).'; diff --git a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodResponseCodeIs201Created.js b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodResponseCodeIs201Created.js index 33328e01e2..1ad7467148 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodResponseCodeIs201Created.js +++ b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodResponseCodeIs201Created.js @@ -6,7 +6,7 @@ import { } from './utils/resourceEvaluation.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; -import { checkResponseCodeAndReturnErrors } from './utils/validations.js'; +import { checkResponseCodeAndReturnErrors } from './utils/validations/checkResponseCodeAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-106-create-method-response-code-is-201'; const ERROR_MESSAGE = diff --git a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsGetResponse.js b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsGetResponse.js index 69a0e73f38..3a43a539ff 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsGetResponse.js +++ b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsGetResponse.js @@ -8,7 +8,7 @@ import { resolveObject } from './utils/componentUtils.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; import { getGETMethodResponseSchemaFromPathItem } from './utils/methodUtils.js'; -import { checkRequestResponseResourceEqualityAndReturnErrors } from './utils/validations.js'; +import { checkRequestResponseResourceEqualityAndReturnErrors } from './utils/validations/checkRequestResponseResourceEqualityAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-107-update-method-request-body-is-get-method-response'; const ERROR_MESSAGE = diff --git a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject.js b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject.js index 8ab9778188..c451baf7a3 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject.js +++ b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsUpdateRequestSuffixedObject.js @@ -7,7 +7,7 @@ import { isSingleResourceIdentifier, isSingletonResource, } from './utils/resourceEvaluation.js'; -import { checkSchemaRefSuffixAndReturnErrors } from './utils/validations.js'; +import { checkSchemaRefSuffixAndReturnErrors } from './utils/validations/checkSchemaRefSuffixAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-107-update-method-request-body-is-update-request-suffixed-object'; diff --git a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestHasNoReadonlyFields.js b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestHasNoReadonlyFields.js index e1b4490ca5..b5072599f5 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestHasNoReadonlyFields.js +++ b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestHasNoReadonlyFields.js @@ -7,7 +7,7 @@ import { import { resolveObject } from './utils/componentUtils.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; -import { checkForbiddenPropertyAttributesAndReturnErrors } from './utils/validations.js'; +import { checkForbiddenPropertyAttributesAndReturnErrors } from './utils/validations/checkForbiddenPropertyAttributesAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-107-update-method-request-has-no-readonly-fields'; const ERROR_MESSAGE = 'The Update method request object must not include input fields (readOnly properties).'; diff --git a/tools/spectral/ipa/rulesets/functions/IPA107UpdateResponseCodeShouldBe200OK.js b/tools/spectral/ipa/rulesets/functions/IPA107UpdateResponseCodeShouldBe200OK.js index cf992845ae..82c8f969ec 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA107UpdateResponseCodeShouldBe200OK.js +++ b/tools/spectral/ipa/rulesets/functions/IPA107UpdateResponseCodeShouldBe200OK.js @@ -6,7 +6,7 @@ import { isSingleResourceIdentifier, isSingletonResource, } from './utils/resourceEvaluation.js'; -import { checkResponseCodeAndReturnErrors } from './utils/validations.js'; +import { checkResponseCodeAndReturnErrors } from './utils/validations/checkResponseCodeAndReturnErrors.js'; const ERROR_MESSAGE = 'The Update method response status code should be 200 OK. This method either lacks a 200 OK response or defines a different 2xx status code.'; diff --git a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasItemsPerPageQueryParam.js b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasItemsPerPageQueryParam.js index e828f8f996..23862488d7 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasItemsPerPageQueryParam.js +++ b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasItemsPerPageQueryParam.js @@ -5,7 +5,7 @@ import { isResourceCollectionIdentifier, isSingletonResource, } from './utils/resourceEvaluation.js'; -import { checkPaginationQueryParameterAndReturnErrors } from './utils/validations.js'; +import { checkPaginationQueryParameterAndReturnErrors } from './utils/validations/checkPaginationQueryParameterAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-110-collections-request-has-itemsPerPage-query-param'; diff --git a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasPageNumQueryParam.js b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasPageNumQueryParam.js index bb0783f735..6808dd1e63 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasPageNumQueryParam.js +++ b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsRequestHasPageNumQueryParam.js @@ -5,7 +5,7 @@ import { isResourceCollectionIdentifier, isSingletonResource, } from './utils/resourceEvaluation.js'; -import { checkPaginationQueryParameterAndReturnErrors } from './utils/validations.js'; +import { checkPaginationQueryParameterAndReturnErrors } from './utils/validations/checkPaginationQueryParameterAndReturnErrors.js'; const RULE_NAME = 'xgen-IPA-110-collections-request-has-pageNum-query-param'; diff --git a/tools/spectral/ipa/rulesets/functions/utils/validations.js b/tools/spectral/ipa/rulesets/functions/utils/validations.js deleted file mode 100644 index 58187c9730..0000000000 --- a/tools/spectral/ipa/rulesets/functions/utils/validations.js +++ /dev/null @@ -1,271 +0,0 @@ -import { handleInternalError } from './collectionUtils.js'; -import { getSchemaRef } from './methodUtils.js'; -import { isDeepEqual, removeRequestProperties, removeResponseProperties } from './compareUtils.js'; - -/** - * Common validation function for checking that responses have the expected status code. - * Returns errors in case of violations, ready to be used in a custom validation function. - * - * @param {Object} operationObject the operation object to evaluate - * @param {string} expectedStatusCode the expected status code to validate for - * @param {string[]} path the path to the operation object being evaluated - * @param {string} ruleName the rule name - * @param errorMessage the error message - * @returns {*[]|[{path, message}]} the errors found, or an empty array in case of no errors - */ -export function checkResponseCodeAndReturnErrors(operationObject, expectedStatusCode, path, ruleName, errorMessage) { - try { - const responses = operationObject.responses; - // If the expected status code is not present, return a violation - if (!responses || !responses[expectedStatusCode]) { - return [{ path, message: errorMessage }]; - } - - // If there are other responses within the same status code group (hundreds), return a violation - if ( - Object.keys(responses).some((key) => key.startsWith(expectedStatusCode.charAt(0)) && key !== expectedStatusCode) - ) { - return [{ path, message: errorMessage }]; - } - return []; - } catch (e) { - handleInternalError(ruleName, path, e); - } -} - -/** - * Recursively searches a schema to find properties with a specific attribute, - * and returns errors if such a property is found, ready to be used in a custom validation function. - * - * @param {Object} schema - The schema to check - * @param {string} attributeName - The attribute to check for (e.g. 'readOnly', 'writeOnly') - * @param {Array} path - The path to the current schema in the document - * @param {Array} errors - Accumulator for errors found - * @param {string} errorMessage - The base error message to use - * @param {Array} propPath - The current property path (for error messages) - * @returns {Array} The accumulated errors - */ -export function checkForbiddenPropertyAttributesAndReturnErrors( - schema, - attributeName, - path, - errors, - errorMessage, - propPath = [] -) { - if (!schema || typeof schema !== 'object') { - return errors; - } - - // Check if this schema has the attribute set to true - if (schema[attributeName] === true) { - errors.push({ - path, - message: - propPath.length > 0 - ? `${errorMessage} Found ${attributeName} property at: ${propPath.join('.')}.` - : `${errorMessage} Found ${attributeName} property at one of the inline schemas.`, - }); - return errors; - } - - // Check properties in object schemas - if (schema.properties) { - for (const [propName, propSchema] of Object.entries(schema.properties)) { - checkForbiddenPropertyAttributesAndReturnErrors(propSchema, attributeName, path, errors, errorMessage, [ - ...propPath, - propName, - ]); - } - } - - // Check items in array schemas - if (schema.items) { - checkForbiddenPropertyAttributesAndReturnErrors(schema.items, attributeName, path, errors, errorMessage, [ - ...propPath, - 'items', - ]); - } - - // Check allOf, anyOf, oneOf schemas - ['allOf', 'anyOf', 'oneOf'].forEach((combiner) => { - if (Array.isArray(schema[combiner])) { - schema[combiner].forEach((subSchema, index) => { - checkForbiddenPropertyAttributesAndReturnErrors(subSchema, attributeName, path, errors, errorMessage, [ - ...propPath, - combiner, - index, - ]); - }); - } - }); - - return errors; -} - -/** - * Checks if a request body schema matches a response schema. - * writeOnly:true properties of the request will be ignored. - * readOnly:true properties of the response will be ignored. - * Returns errors if the schemas are not equal, ready to be used in a custom validation function. - * - * @param {string[]} path the path to the request object being evaluated - * @param {object} requestContent the resolved request content for a media type - * @param {object} responseContent the resolved response content for a media type - * @param {object} requestContentUnresolved the unresolved request content for a media type - * @param {object} responseContentUnresolved the unresolved response content for a media type - * @param {'Create' | 'Update'} requestMethod the method of the request, e.g. 'create', 'update' - * @param {'Get' | 'List'} responseMethod the method of the response, e.g. 'get', 'list' - * @param {string} errorMessage the error message - * @returns {[{path, message: string}]} the errors found, or an empty array in case of no errors - */ -export function checkRequestResponseResourceEqualityAndReturnErrors( - path, - requestContent, - responseContent, - requestContentUnresolved, - responseContentUnresolved, - requestMethod, - responseMethod, - errorMessage -) { - const errors = []; - - if (!responseContent.schema) { - return [ - { - path, - message: `Could not validate that the ${requestMethod} request body schema matches the response schema of the ${responseMethod} method. The ${responseMethod} method does not have a schema.`, - }, - ]; - } - - const requestSchemaRef = getSchemaRef(requestContentUnresolved.schema); - const responseSchemaRef = getSchemaRef(responseContentUnresolved.schema); - - if (requestSchemaRef && responseSchemaRef) { - if (requestSchemaRef === responseSchemaRef) { - return []; - } - } - - const filteredRequestSchema = removeRequestProperties(requestContent.schema); - const filteredResponseSchema = removeResponseProperties(responseContent.schema); - - if (!isDeepEqual(filteredRequestSchema, filteredResponseSchema)) { - errors.push({ - path, - message: errorMessage, - }); - } - - return errors; -} - -/** - * Checks if a request or response content for a media type references a schema with a specific suffix. - * Returns errors if the schema doesn't reference a schema, or if the schema name does not end with the expected suffix. - * Ready to be used in a custom validation function. - * - * @param {string[]} path the path to the object being evaluated - * @param {object} contentPerMediaType the content to evaluate for a specific media type (unresolved) - * @param {string} expectedSuffix the expected suffix to evaluate the schema name for - * @param {string} ruleName the rule name - * @returns {[{path, message: string}]} the errors found, or an empty array in case of no errors - */ -export function checkSchemaRefSuffixAndReturnErrors(path, contentPerMediaType, expectedSuffix, ruleName) { - try { - const schema = contentPerMediaType.schema; - const schemaRef = getSchemaRef(schema); - - if (!schemaRef) { - return [ - { - path, - message: 'The schema is defined inline and must reference a predefined schema.', - }, - ]; - } - if (!schemaRef.endsWith(expectedSuffix)) { - return [ - { - path, - message: `The schema must reference a schema with a ${expectedSuffix} suffix.`, - }, - ]; - } - return []; - } catch (e) { - handleInternalError(ruleName, path, e); - } -} - -/** - * Checks if a list method has the required pagination query parameter with correct configuration - * - * @param {Object} operation - The OpenAPI operation object to check - * @param {string[]} path - The path to the operation - * @param {string} paramName - The name of the parameter to check ('pageNum' or 'itemsPerPage') - * @param {number} defaultValue - The expected default value (1 for pageNum, 100 for itemsPerPage) - * @param {string} ruleName - The rule name for error handling - * @returns {Array} - Array of error objects or empty array if no errors - */ -export function checkPaginationQueryParameterAndReturnErrors(operation, path, paramName, defaultValue, ruleName) { - try { - const parameters = operation.parameters; - - if (!parameters) { - return [ - { - path, - message: `List method is missing query parameters.`, - }, - ]; - } - - const param = parameters.find( - (p) => p.name === paramName && p.in === 'query' && p.schema && p.schema.type === 'integer' - ); - - if (!param) { - return [ - { - path, - message: `List method is missing a ${paramName} query parameter.`, - }, - ]; - } - - if (param.required === true) { - return [ - { - path, - message: `${paramName} query parameter of List method must not be required.`, - }, - ]; - } - - if (param.schema.default === undefined) { - return [ - { - path, - message: `${paramName} query parameter of List method must have a default value defined.`, - }, - ]; - } - - if (param.schema.default !== defaultValue) { - return [ - { - path, - message: `${paramName} query parameter of List method must have a default value of ${defaultValue}.`, - }, - ]; - } - - return []; - } catch (e) { - handleInternalError(ruleName, path, e); - return []; - } -} diff --git a/tools/spectral/ipa/rulesets/functions/utils/validations/checkForbiddenPropertyAttributesAndReturnErrors.js b/tools/spectral/ipa/rulesets/functions/utils/validations/checkForbiddenPropertyAttributesAndReturnErrors.js new file mode 100644 index 0000000000..c63d440eff --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/utils/validations/checkForbiddenPropertyAttributesAndReturnErrors.js @@ -0,0 +1,69 @@ +/** + * Recursively searches a schema to find properties with a specific attribute, + * and returns errors if such a property is found, ready to be used in a custom validation function. + * + * @param {Object} schema - The schema to check + * @param {string} attributeName - The attribute to check for (e.g. 'readOnly', 'writeOnly') + * @param {Array} path - The path to the current schema in the document + * @param {Array} errors - Accumulator for errors found + * @param {string} errorMessage - The base error message to use + * @param {Array} propPath - The current property path (for error messages) + * @returns {Array} The accumulated errors + */ +export function checkForbiddenPropertyAttributesAndReturnErrors( + schema, + attributeName, + path, + errors, + errorMessage, + propPath = [] +) { + if (!schema || typeof schema !== 'object') { + return errors; + } + + // Check if this schema has the attribute set to true + if (schema[attributeName] === true) { + errors.push({ + path, + message: + propPath.length > 0 + ? `${errorMessage} Found ${attributeName} property at: ${propPath.join('.')}.` + : `${errorMessage} Found ${attributeName} property at one of the inline schemas.`, + }); + return errors; + } + + // Check properties in object schemas + if (schema.properties) { + for (const [propName, propSchema] of Object.entries(schema.properties)) { + checkForbiddenPropertyAttributesAndReturnErrors(propSchema, attributeName, path, errors, errorMessage, [ + ...propPath, + propName, + ]); + } + } + + // Check items in array schemas + if (schema.items) { + checkForbiddenPropertyAttributesAndReturnErrors(schema.items, attributeName, path, errors, errorMessage, [ + ...propPath, + 'items', + ]); + } + + // Check allOf, anyOf, oneOf schemas + ['allOf', 'anyOf', 'oneOf'].forEach((combiner) => { + if (Array.isArray(schema[combiner])) { + schema[combiner].forEach((subSchema, index) => { + checkForbiddenPropertyAttributesAndReturnErrors(subSchema, attributeName, path, errors, errorMessage, [ + ...propPath, + combiner, + index, + ]); + }); + } + }); + + return errors; +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/validations/checkPaginationQueryParameterAndReturnErrors.js b/tools/spectral/ipa/rulesets/functions/utils/validations/checkPaginationQueryParameterAndReturnErrors.js new file mode 100644 index 0000000000..67900a9b52 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/utils/validations/checkPaginationQueryParameterAndReturnErrors.js @@ -0,0 +1,71 @@ +import { handleInternalError } from '../collectionUtils.js'; + +/** + * Checks if a list method has the required pagination query parameter with correct configuration + * + * @param {Object} operation - The OpenAPI operation object to check + * @param {string[]} path - The path to the operation + * @param {string} paramName - The name of the parameter to check ('pageNum' or 'itemsPerPage') + * @param {number} defaultValue - The expected default value (1 for pageNum, 100 for itemsPerPage) + * @param {string} ruleName - The rule name for error handling + * @returns {Array} - Array of error objects or empty array if no errors + */ +export function checkPaginationQueryParameterAndReturnErrors(operation, path, paramName, defaultValue, ruleName) { + try { + const parameters = operation.parameters; + + if (!parameters) { + return [ + { + path, + message: `List method is missing query parameters.`, + }, + ]; + } + + const param = parameters.find( + (p) => p.name === paramName && p.in === 'query' && p.schema && p.schema.type === 'integer' + ); + + if (!param) { + return [ + { + path, + message: `List method is missing a ${paramName} query parameter.`, + }, + ]; + } + + if (param.required === true) { + return [ + { + path, + message: `${paramName} query parameter of List method must not be required.`, + }, + ]; + } + + if (param.schema.default === undefined) { + return [ + { + path, + message: `${paramName} query parameter of List method must have a default value defined.`, + }, + ]; + } + + if (param.schema.default !== defaultValue) { + return [ + { + path, + message: `${paramName} query parameter of List method must have a default value of ${defaultValue}.`, + }, + ]; + } + + return []; + } catch (e) { + handleInternalError(ruleName, path, e); + return []; + } +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/validations/checkRequestResponseResourceEqualityAndReturnErrors.js b/tools/spectral/ipa/rulesets/functions/utils/validations/checkRequestResponseResourceEqualityAndReturnErrors.js new file mode 100644 index 0000000000..977d232e66 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/utils/validations/checkRequestResponseResourceEqualityAndReturnErrors.js @@ -0,0 +1,61 @@ +import { getSchemaRef } from '../methodUtils.js'; +import { isDeepEqual, removeRequestProperties, removeResponseProperties } from '../compareUtils.js'; + +/** + * Checks if a request body schema matches a response schema. + * writeOnly:true properties of the request will be ignored. + * readOnly:true properties of the response will be ignored. + * Returns errors if the schemas are not equal, ready to be used in a custom validation function. + * + * @param {string[]} path the path to the request object being evaluated + * @param {object} requestContent the resolved request content for a media type + * @param {object} responseContent the resolved response content for a media type + * @param {object} requestContentUnresolved the unresolved request content for a media type + * @param {object} responseContentUnresolved the unresolved response content for a media type + * @param {'Create' | 'Update'} requestMethod the method of the request, e.g. 'create', 'update' + * @param {'Get' | 'List'} responseMethod the method of the response, e.g. 'get', 'list' + * @param {string} errorMessage the error message + * @returns {[{path, message: string}]} the errors found, or an empty array in case of no errors + */ +export function checkRequestResponseResourceEqualityAndReturnErrors( + path, + requestContent, + responseContent, + requestContentUnresolved, + responseContentUnresolved, + requestMethod, + responseMethod, + errorMessage +) { + const errors = []; + + if (!responseContent.schema) { + return [ + { + path, + message: `Could not validate that the ${requestMethod} request body schema matches the response schema of the ${responseMethod} method. The ${responseMethod} method does not have a schema.`, + }, + ]; + } + + const requestSchemaRef = getSchemaRef(requestContentUnresolved.schema); + const responseSchemaRef = getSchemaRef(responseContentUnresolved.schema); + + if (requestSchemaRef && responseSchemaRef) { + if (requestSchemaRef === responseSchemaRef) { + return []; + } + } + + const filteredRequestSchema = removeRequestProperties(requestContent.schema); + const filteredResponseSchema = removeResponseProperties(responseContent.schema); + + if (!isDeepEqual(filteredRequestSchema, filteredResponseSchema)) { + errors.push({ + path, + message: errorMessage, + }); + } + + return errors; +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/validations/checkResponseCodeAndReturnErrors.js b/tools/spectral/ipa/rulesets/functions/utils/validations/checkResponseCodeAndReturnErrors.js new file mode 100644 index 0000000000..7b9b6a4ad8 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/utils/validations/checkResponseCodeAndReturnErrors.js @@ -0,0 +1,32 @@ +import { handleInternalError } from '../collectionUtils.js'; + +/** + * Common validation function for checking that responses have the expected status code. + * Returns errors in case of violations, ready to be used in a custom validation function. + * + * @param {Object} operationObject the operation object to evaluate + * @param {string} expectedStatusCode the expected status code to validate for + * @param {string[]} path the path to the operation object being evaluated + * @param {string} ruleName the rule name + * @param errorMessage the error message + * @returns {*[]|[{path, message}]} the errors found, or an empty array in case of no errors + */ +export function checkResponseCodeAndReturnErrors(operationObject, expectedStatusCode, path, ruleName, errorMessage) { + try { + const responses = operationObject.responses; + // If the expected status code is not present, return a violation + if (!responses || !responses[expectedStatusCode]) { + return [{ path, message: errorMessage }]; + } + + // If there are other responses within the same status code group (hundreds), return a violation + if ( + Object.keys(responses).some((key) => key.startsWith(expectedStatusCode.charAt(0)) && key !== expectedStatusCode) + ) { + return [{ path, message: errorMessage }]; + } + return []; + } catch (e) { + handleInternalError(ruleName, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/validations/checkSchemaRefSuffixAndReturnErrors.js b/tools/spectral/ipa/rulesets/functions/utils/validations/checkSchemaRefSuffixAndReturnErrors.js new file mode 100644 index 0000000000..9562130297 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/utils/validations/checkSchemaRefSuffixAndReturnErrors.js @@ -0,0 +1,40 @@ +import { getSchemaRef } from '../methodUtils.js'; +import { handleInternalError } from '../collectionUtils.js'; + +/** + * Checks if a request or response content for a media type references a schema with a specific suffix. + * Returns errors if the schema doesn't reference a schema, or if the schema name does not end with the expected suffix. + * Ready to be used in a custom validation function. + * + * @param {string[]} path the path to the object being evaluated + * @param {object} contentPerMediaType the content to evaluate for a specific media type (unresolved) + * @param {string} expectedSuffix the expected suffix to evaluate the schema name for + * @param {string} ruleName the rule name + * @returns {[{path, message: string}]} the errors found, or an empty array in case of no errors + */ +export function checkSchemaRefSuffixAndReturnErrors(path, contentPerMediaType, expectedSuffix, ruleName) { + try { + const schema = contentPerMediaType.schema; + const schemaRef = getSchemaRef(schema); + + if (!schemaRef) { + return [ + { + path, + message: 'The schema is defined inline and must reference a predefined schema.', + }, + ]; + } + if (!schemaRef.endsWith(expectedSuffix)) { + return [ + { + path, + message: `The schema must reference a schema with a ${expectedSuffix} suffix.`, + }, + ]; + } + return []; + } catch (e) { + handleInternalError(ruleName, path, e); + } +}