Skip to content

Commit

Permalink
feat: custom Spectral rule to ensure content objects contain schema
Browse files Browse the repository at this point in the history
Purpose:
- Establish a pattern in the validator for adding custom spectral rules and custom spectral functions
- Ensure content objects contain a schema

Changes:
- Add custom spectral rule to ensure that content objects contain a schema (defaults to warning)

Tests:
- Add tests for each use case outlined in the spectral rule

Docs:
- Add a central location for documenting custom Spectral rules
- Document the `content-entry-contains-schema` rule
  • Loading branch information
Barrett Schonefeld committed Mar 16, 2021
1 parent d672b91 commit 8167eef
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 4 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,8 @@ This ruleset has the alias `ibm:oas`, and you can "extend" this ruleset or speci
with a [Spectral ruleset file](https://meta.stoplight.io/docs/spectral/docs/getting-started/3-rulesets.md).
Note that all of the rules in the `spectral:oas` ruleset are defined in `ibm:oas` but only the rules listed above are enabled by default.

The `ibm:oas` ruleset also includes custom Spectral rules, [documented here](docs/spectral-rules.md). These are configurable rules in the `ibm:oas` ruleset that are not part of the `spectral:oas` ruleset.

You can provide a Spectral ruleset file to the IBM OpenAPI validator in a file named `.spectral.yaml`
in the current directory or with the `--ruleset` command line option of the validator.

Expand Down
30 changes: 30 additions & 0 deletions docs/spectral-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Custom Spectral Rules

This document outlines the custom Spectral rules implemented in `ibm:oas` ruleset.

## content-entry-contains-schema

Any request or response body that has `content` should contain a schema.

**Bad Example**

```yaml
responses:
200:
content:
application/json:
# schema not provided
```

**Good Example**

```yaml
responses:
200:
content:
application/json:
schema:
type: string
```

**Default Severity**: warn
12 changes: 12 additions & 0 deletions src/spectral/rulesets/.defaultsForSpectral.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,15 @@ rules:
oas3-schema: off
# Turn off - duplicates non-configurable validation in base validator
oas3-unused-components-schema: off

# custom Spectral rule to ensure content object contains schema
content-entry-contains-schema:
description: Content entries in request and response bodies must specify a schema
given:
- $.paths[*].[post,put,patch].requestBody.content[*]
- $.paths[*].[get,post,put,patch,delete].responses[*].content[*]
severity: warn
resolved: true
then:
field: schema
function: truthy
177 changes: 177 additions & 0 deletions test/spectral/tests/custom-rules/content-entry-contains-schema.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
const inCodeValidator = require('../../../../src/lib');

describe('spectral - test validation that schema provided in content object', function() {
it('should not error when the content object contains a schema', async () => {
const spec = {
Openapi: '3.0.0',
paths: {
path1: {
get: {
responses: {
'200': {
$ref: '#/components/responses/GenericResponse'
}
}
}
}
},
components: {
responses: {
GenericResponse: {
content: {
'application/json': {
// schema provided
schema: {
type: 'string'
}
}
}
}
}
}
};

const res = await inCodeValidator(spec, true);
const expectedWarnings = res.warnings.filter(
warn =>
warn.message ===
'Content entries in request and response bodies must specify a schema'
);
expect(expectedWarnings.length).toBe(0);
});

it('should error when a content object in a requestBody reference does not contain a schema', async () => {
const spec = {
Openapi: '3.0.0',
paths: {
path1: {
post: {
requestBody: {
$ref: '#/components/requestBodies/GenericRequestBody'
}
}
}
},
components: {
requestBodies: {
GenericRequestBody: {
content: {
'application/json': {
// schema not provided
}
}
}
}
}
};

const res = await inCodeValidator(spec, true);
const expectedWarnings = res.warnings.filter(
warn =>
warn.message ===
'Content entries in request and response bodies must specify a schema'
);
expect(expectedWarnings.length).toBe(1);
});

it('should error when a content object in a response reference does not contain a schema', async () => {
const spec = {
Openapi: '3.0.0',
paths: {
path1: {
post: {
responses: {
'200': {
$ref: '#/components/responses/GenericResponse'
}
}
}
}
},
components: {
responses: {
GenericResponse: {
content: {
'application/json': {
// schema not provided
}
}
}
}
}
};

const res = await inCodeValidator(spec, true);
const expectedWarnings = res.warnings.filter(
warn =>
warn.message ===
'Content entries in request and response bodies must specify a schema'
);
expect(expectedWarnings.length).toBe(1);
});

it('should error when the content object does not contain a schema in a response', async () => {
const spec = {
Openapi: '3.0.0',
paths: {
'pets/{petId}': {
get: {
operationId: 'getPetsById',
responses: {
200: {
content: {
'*/*': {
// schema not provided
}
}
},
default: {
content: {
'text/html': {
// schema not provided
}
}
}
}
}
}
}
};

const res = await inCodeValidator(spec, true);
const expectedWarnings = res.warnings.filter(
warn =>
warn.message ===
'Content entries in request and response bodies must specify a schema'
);
expect(expectedWarnings.length).toBe(2);
});

it('should error when the content object does not contain a schema in a request body', async () => {
const spec = {
Openapi: '3.0.0',
paths: {
createPet: {
post: {
operationId: 'addPet',
requestBody: {
content: {
'application/json': {
// no schema provided
}
}
}
}
}
}
};

const res = await inCodeValidator(spec, true);
const expectedWarnings = res.warnings.filter(
warn =>
warn.message ===
'Content entries in request and response bodies must specify a schema'
);
expect(expectedWarnings.length).toBe(1);
});
});
6 changes: 3 additions & 3 deletions test/spectral/tests/spectral-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('Spectral - test custom configuration', function() {
);
const mockConfig = jest
.spyOn(config, 'getSpectralRuleset')
.mockReturnValue(mockPath);
.mockResolvedValue(mockPath);

const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
// set up mock user input
Expand Down Expand Up @@ -66,7 +66,7 @@ describe('Spectral - test custom configuration', function() {
);
const mockConfig = jest
.spyOn(config, 'getSpectralRuleset')
.mockReturnValue(mockPath);
.mockResolvedValue(mockPath);

const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
// set up mock user input
Expand Down Expand Up @@ -114,7 +114,7 @@ describe('Spectral - test custom configuration', function() {
);
const mockConfig = jest
.spyOn(config, 'getSpectralRuleset')
.mockReturnValue(mockPath);
.mockResolvedValue(mockPath);

const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
// set up mock user input
Expand Down
2 changes: 1 addition & 1 deletion test/spectral/tests/spectral-validator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe('spectral - test config file changes with .spectral.yml', function() {
);
const mockConfig = jest
.spyOn(config, 'getSpectralRuleset')
.mockReturnValue(mockPath);
.mockResolvedValue(mockPath);

// Below is used from enabled-rules.test.js
// set up mock user input
Expand Down

0 comments on commit 8167eef

Please sign in to comment.