UNPKG

@ibm-cloud/openapi-ruleset

Version:

Spectral ruleset for validating IBM Cloud services

678 lines (630 loc) 19.1 kB
/** * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ const { requiredPropertyMissing } = require('../../src/rules'); const { makeCopy, rootDocument, testRule, severityCodes, } = require('../test-utils'); const ruleId = 'ibm-define-required-properties'; const rule = requiredPropertyMissing; describe(`Spectral rule: ${ruleId}`, () => { it('should not error with a clean spec', async () => { const results = await testRule(ruleId, rule, rootDocument); expect(results).toHaveLength(0); }); it('should not error with properly defined allOf, anyOf, and oneOf schemas', async () => { const testDocument = makeCopy(rootDocument); testDocument.paths['v1/books'] = { post: { parameters: [ { name: 'metadata', in: 'header', content: { 'application/json': { schema: { type: 'object', required: ['foo'], allOf: [ { type: 'object', properties: { foo: { type: 'string', }, }, }, { type: 'object', properties: { bar: { type: 'string', }, }, }, ], }, }, }, }, ], requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['foo'], anyOf: [ { type: 'object', properties: { foo: { type: 'string', }, bar: { type: 'string', }, }, }, { type: 'object', properties: { foo: { type: 'string', }, baz: { type: 'string', }, }, }, ], }, }, }, }, responses: { 201: { content: { 'application/json': { schema: { type: 'object', required: ['foo'], oneOf: [ { type: 'object', properties: { foo: { type: 'string', }, bar: { type: 'string', }, }, }, { type: 'object', properties: { foo: { type: 'string', }, baz: { type: 'string', }, }, }, ], }, }, }, }, }, }, }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(0); }); it('should not error if not schema is missing a required property', async () => { const testDocument = makeCopy(rootDocument); testDocument.paths['v1/books'] = { post: { parameters: [ { name: 'metadata', in: 'header', content: { 'application/json': { schema: { type: 'object', not: { type: 'object', required: ['foo'], properties: { baz: { type: 'string', }, }, }, }, }, }, }, ], }, }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(0); }); it('should not error if "required" in separate allOf element', async () => { const testDocument = makeCopy(rootDocument); testDocument.components.schemas['DrinkCollection'] = { type: 'object', description: 'A single page of results containing Drink instances.', allOf: [ { $ref: '#/components/schemas/TokenPaginationBase', }, { type: 'object', required: ['drinks'], properties: { drinks: { description: 'The set of Drink instances in this page of results.', type: 'array', minItems: 0, maxItems: 50, items: { $ref: '#/components/schemas/Drink', }, }, }, }, { // The "first" property is defined in TokenPaginationBase. required: ['first'], }, ], }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(0); }); it('should error if allOf schema is missing a required property ("required" in main schema)', async () => { const testDocument = makeCopy(rootDocument); testDocument.paths['v1/books'] = { post: { parameters: [ { name: 'metadata', in: 'header', content: { 'application/json': { schema: { type: 'object', required: ['foo'], allOf: [ { type: 'object', properties: { baz: { type: 'string', }, }, }, { type: 'object', properties: { bar: { type: 'string', }, }, }, ], }, }, }, }, ], }, }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); const validation = results[0]; expect(validation.code).toBe(ruleId); expect(validation.message).toBe( 'Required property must be defined in the schema: foo' ); expect(validation.path.join('.')).toBe( 'paths.v1/books.post.parameters.0.content.application/json.schema.required' ); expect(validation.severity).toBe(severityCodes.error); }); it('should error if allOf schema is missing a required property ("required" in allOf element)', async () => { const testDocument = makeCopy(rootDocument); testDocument.paths['v1/books'] = { post: { parameters: [ { name: 'metadata', in: 'header', content: { 'application/json': { schema: { type: 'object', allOf: [ { type: 'object', required: ['baz'], properties: { baz: { type: 'string', }, }, }, { type: 'object', required: ['bar'], properties: { bar: { type: 'string', }, }, }, { required: ['foo'], }, ], }, }, }, }, ], }, }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); const validation = results[0]; expect(validation.code).toBe(ruleId); expect(validation.message).toBe( 'Required property must be defined in the schema: foo' ); expect(validation.path.join('.')).toBe( 'paths.v1/books.post.parameters.0.content.application/json.schema.allOf.2.required' ); expect(validation.severity).toBe(severityCodes.error); }); it('should error if anyOf schema is missing a required property', async () => { const testDocument = makeCopy(rootDocument); testDocument.paths['v1/books'] = { post: { requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['foo'], anyOf: [ { type: 'object', properties: { foo: { type: 'string', }, bar: { type: 'string', }, }, }, { type: 'object', properties: { baz: { type: 'string', }, }, }, ], }, }, }, }, }, }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); const validation = results[0]; expect(validation.code).toBe(ruleId); expect(validation.message).toBe( 'Required property must be defined in the schema: foo' ); expect(validation.path.join('.')).toBe( 'paths.v1/books.post.requestBody.content.application/json.schema.required' ); expect(validation.severity).toBe(severityCodes.error); }); it('should error if oneOf schema is missing a required property', async () => { const testDocument = makeCopy(rootDocument); testDocument.paths['v1/books'] = { post: { responses: { 201: { content: { 'application/json': { schema: { type: 'object', required: ['foo'], oneOf: [ { type: 'object', properties: { foo: { type: 'string', }, bar: { type: 'string', }, }, }, { type: 'object', properties: { baz: { type: 'string', }, }, }, ], }, }, }, }, }, }, }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); const validation = results[0]; expect(validation.code).toBe(ruleId); expect(validation.message).toBe( 'Required property must be defined in the schema: foo' ); expect(validation.path.join('.')).toBe( 'paths.v1/books.post.responses.201.content.application/json.schema.required' ); expect(validation.severity).toBe(severityCodes.error); }); it('should error if nested oneOf schema is missing a required property', async () => { const testDocument = makeCopy(rootDocument); testDocument.paths['v1/books'] = { post: { responses: { 201: { content: { 'application/json': { schema: { type: 'object', required: ['foo'], oneOf: [ { type: 'object', properties: { foo: { type: 'string', }, bar: { type: 'string', }, }, }, { type: 'object', oneOf: [ { type: 'object', properties: { foo: { type: 'string', }, baz: { type: 'string', }, }, }, { type: 'object', properties: { bat: { type: 'string', }, }, }, ], }, ], }, }, }, }, }, }, }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); const validation = results[0]; expect(validation.code).toBe(ruleId); expect(validation.message).toBe( 'Required property must be defined in the schema: foo' ); expect(validation.path.join('.')).toBe( 'paths.v1/books.post.responses.201.content.application/json.schema.required' ); expect(validation.severity).toBe(severityCodes.error); }); it('should error if anyOf list element schema is missing a required property', async () => { const testDocument = makeCopy(rootDocument); testDocument.paths['v1/books'] = { post: { requestBody: { content: { 'application/json': { schema: { type: 'object', anyOf: [ { type: 'object', properties: { foo: { type: 'string', }, bar: { type: 'string', }, }, }, { type: 'object', properties: { baz: { type: 'string', }, }, required: ['foo'], }, ], }, }, }, }, }, }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); const validation = results[0]; expect(validation.code).toBe(ruleId); expect(validation.message).toBe( 'Required property must be defined in the schema: foo' ); expect(validation.path.join('.')).toBe( 'paths.v1/books.post.requestBody.content.application/json.schema.anyOf.1.required' ); expect(validation.severity).toBe(severityCodes.error); }); it('should error if oneOf list element schema is missing a required property', async () => { const testDocument = makeCopy(rootDocument); testDocument.paths['v1/books'] = { post: { responses: { 201: { content: { 'application/json': { schema: { type: 'object', oneOf: [ { type: 'object', properties: { foo: { type: 'string', }, bar: { type: 'string', }, }, }, { type: 'object', properties: { baz: { type: 'string', }, }, required: ['foo'], }, ], }, }, }, }, }, }, }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(1); const validation = results[0]; expect(validation.code).toBe(ruleId); expect(validation.message).toBe( 'Required property must be defined in the schema: foo' ); expect(validation.path.join('.')).toBe( 'paths.v1/books.post.responses.201.content.application/json.schema.oneOf.1.required' ); expect(validation.severity).toBe(severityCodes.error); }); it('should error for schemas that are missing definitions for required properties', async () => { const testDocument = makeCopy(rootDocument); testDocument.paths['v1/books'] = { post: { parameters: [ { name: 'metadata', in: 'header', content: { 'application/json': { schema: { type: 'object', required: ['foo'], properties: {}, }, }, }, }, ], requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['foo'], properties: {}, }, }, }, }, responses: { 201: { content: { 'application/json': { schema: { type: 'object', required: ['foo'], properties: {}, }, }, }, }, }, }, }; const results = await testRule(ruleId, rule, testDocument); expect(results).toHaveLength(3); results.forEach(r => { expect(r.code).toBe(ruleId); expect(r.message).toBe( 'Required property must be defined in the schema: foo' ); expect(r.severity).toBe(severityCodes.error); }); expect(results[0].path.join('.')).toBe( 'paths.v1/books.post.parameters.0.content.application/json.schema.required' ); expect(results[1].path.join('.')).toBe( 'paths.v1/books.post.requestBody.content.application/json.schema.required' ); expect(results[2].path.join('.')).toBe( 'paths.v1/books.post.responses.201.content.application/json.schema.required' ); }); });