Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom Spectral rule to ensure content objects contain schema #258

Merged
merged 1 commit into from
Mar 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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