diff --git a/__tests__/__helpers__/helper.ts b/__tests__/__helpers__/helper.ts index 3752fca..128519d 100644 --- a/__tests__/__helpers__/helper.ts +++ b/__tests__/__helpers__/helper.ts @@ -1,8 +1,14 @@ -import { IRuleResult, Spectral, Document, Ruleset, RulesetDefinition } from '@stoplight/spectral-core'; -import { httpAndFileResolver } from '@stoplight/spectral-ref-resolver'; -import apisYouWontHateRuleset from '../../src/ruleset'; +import { + IRuleResult, + Spectral, + Document, + Ruleset, + RulesetDefinition, +} from "@stoplight/spectral-core"; +import { httpAndFileResolver } from "@stoplight/spectral-ref-resolver"; +import apisYouWontHateRuleset from "../../src/ruleset"; -export type RuleName = keyof Ruleset['rules']; +export type RuleName = keyof Ruleset["rules"]; type Scenario = ReadonlyArray< Readonly<{ @@ -15,27 +21,32 @@ type Scenario = ReadonlyArray< export default (ruleName: RuleName, tests: Scenario): void => { describe(`Rule ${ruleName}`, () => { - const concurrent = tests.every(test => test.mocks === void 0 || Object.keys(test.mocks).length === 0); + const concurrent = tests.every( + (test) => test.mocks === void 0 || Object.keys(test.mocks).length === 0 + ); for (const testCase of tests) { (concurrent ? it.concurrent : it)(testCase.name, async () => { const s = createWithRules([ruleName]); - const doc = testCase.document instanceof Document ? testCase.document : JSON.stringify(testCase.document); + const doc = + testCase.document instanceof Document + ? testCase.document + : JSON.stringify(testCase.document); const errors = await s.run(doc); expect(errors.filter(({ code }) => code === ruleName)).toEqual( - testCase.errors.map(error => expect.objectContaining(error) as unknown), + testCase.errors.map( + (error) => expect.objectContaining(error) as unknown + ) ); }); } }); }; -export function createWithRules(rules: (keyof Ruleset['rules'])[]): Spectral { +export function createWithRules(rules: (keyof Ruleset["rules"])[]): Spectral { const s = new Spectral({ resolver: httpAndFileResolver }); s.setRuleset({ - extends: [ - [apisYouWontHateRuleset as RulesetDefinition, 'off'], - ], + extends: [[apisYouWontHateRuleset as RulesetDefinition, "off"]], rules: rules.reduce((obj: Record, name) => { obj[name] = true; return obj; diff --git a/__tests__/adv-security-schemes-defined.test.ts b/__tests__/adv-security-schemes-defined.test.ts deleted file mode 100644 index 2584a61..0000000 --- a/__tests__/adv-security-schemes-defined.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { DiagnosticSeverity } from "@stoplight/types"; -import testRule from "./__helpers__/helper"; - -testRule("adv-security-schemes-defined", [ - { - name: "valid case", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - components: { - securitySchemes: { - "oAuth2": { - type: "oauth2", - flow: {}, - }, - }, - }, - }, - errors: [], - }, - - { - name: "invalid case", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - components: {}, - }, - errors: [ - { - message: "This API definition does not have any security scheme defined.", - path: ["components"], - severity: DiagnosticSeverity.Error, - }, - ], - }, -]); diff --git a/__tests__/api-health-format.test.ts b/__tests__/api-health-format.test.ts index e3e75e1..893c21b 100644 --- a/__tests__/api-health-format.test.ts +++ b/__tests__/api-health-format.test.ts @@ -1,54 +1,72 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import testRule from './__helpers__/helper'; +import { DiagnosticSeverity } from "@stoplight/types"; +import testRule from "./__helpers__/helper"; const template = (contentType: string) => { return { - openapi: '3.1.0', - info: { version: '1.0', contact: {} }, - paths: { + openapi: "3.1.0", + info: { version: "1.0", contact: {} }, + paths: { "/health": { - "get": { - "summary": "Your health endpoint", - "responses": { + get: { + summary: "Your health endpoint", + responses: { "200": { - "description": "Error", - "content": { - [contentType]: {} - } - } - } - } - } - } - } + description: "Error", + content: { + [contentType]: {}, + }, + }, + }, + }, + }, + }, + }; }; -testRule('api-health-format', [ +testRule("api-health-format", [ { - name: 'valid case', - document: template('application/vnd.health+json'), + name: "valid case", + document: template("application/vnd.health+json"), errors: [], }, { - name: 'invalid case if plain json', - document: template('application/json'), + name: "invalid case if plain json", + document: template("application/json"), errors: [ { - message: 'Use existing standards (and draft standards) wherever possible, like the draft standard for health checks: https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check.', - path: ['paths', "/health", "get", "responses", "200", "content", "application/json"], + message: + "Health path (`/heath`) SHOULD support Health Check Response Format", + path: [ + "paths", + "/health", + "get", + "responses", + "200", + "content", + "application/json", + ], severity: DiagnosticSeverity.Warning, }, ], }, { - name: 'invalid case if any other mime type', - document: template('text/png'), + name: "invalid case if any other mime type", + document: template("text/png"), errors: [ { - message: 'Use existing standards (and draft standards) wherever possible, like the draft standard for health checks: https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check.', - path: ['paths', "/health", "get", "responses", "200", "content", "text/png"], + message: + "Health path (`/heath`) SHOULD support Health Check Response Format", + path: [ + "paths", + "/health", + "get", + "responses", + "200", + "content", + "text/png", + ], severity: DiagnosticSeverity.Warning, }, ], diff --git a/__tests__/api-health.test.ts b/__tests__/api-health.test.ts index 83dac84..ebf6d04 100644 --- a/__tests__/api-health.test.ts +++ b/__tests__/api-health.test.ts @@ -1,28 +1,28 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import testRule from './__helpers__/helper'; +import { DiagnosticSeverity } from "@stoplight/types"; +import testRule from "./__helpers__/helper"; -testRule('api-health', [ +testRule("api-health", [ { - name: 'valid case', + name: "valid case", document: { - openapi: '3.1.0', - info: { version: '1.0', contact: {} }, - paths: { '/health': {} }, + openapi: "3.1.0", + info: { version: "1.0", contact: {} }, + paths: { "/health": {} }, }, errors: [], }, { - name: 'invalid case', + name: "invalid case", document: { - openapi: '3.1.0', - info: { version: '1.0', contact: {} }, + openapi: "3.1.0", + info: { version: "1.0", contact: {} }, paths: {}, }, errors: [ { - message: 'Creating a `/health` endpoint is a simple solution for pull-based monitoring and manually checking the status of an API.', - path: ['paths'], + message: "APIs MUST have a health path (`/health`) defined.", + path: ["paths"], severity: DiagnosticSeverity.Warning, }, ], diff --git a/__tests__/api-home-get.test.ts b/__tests__/api-home-get.test.ts index d440849..813cf61 100644 --- a/__tests__/api-home-get.test.ts +++ b/__tests__/api-home-get.test.ts @@ -1,34 +1,34 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import testRule from './__helpers__/helper'; +import { DiagnosticSeverity } from "@stoplight/types"; +import testRule from "./__helpers__/helper"; -testRule('api-home-get', [ +testRule("api-home-get", [ { - name: 'valid case', + name: "valid case", document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { - '/': { - 'get': {} - } + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { + "/": { + get: {}, + }, }, }, errors: [], }, - + { - name: 'invalid case', + name: "invalid case", document: { - openapi: '3.1.0', - info: { version: '1.0' }, + openapi: "3.1.0", + info: { version: "1.0" }, paths: { - '/': {} + "/": {}, }, }, errors: [ { - message: "Otherwise people won't know how to get it.", - path: ['paths', '/'], + message: "APIs root path (`/`) MUST have a GET operation.", + path: ["paths", "/"], severity: DiagnosticSeverity.Warning, }, ], diff --git a/__tests__/api-home.test.ts b/__tests__/api-home.test.ts index 7f1605a..78edff2 100644 --- a/__tests__/api-home.test.ts +++ b/__tests__/api-home.test.ts @@ -1,28 +1,28 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import testRule from './__helpers__/helper'; +import { DiagnosticSeverity } from "@stoplight/types"; +import testRule from "./__helpers__/helper"; -testRule('api-home', [ +testRule("api-home", [ { - name: 'valid case', + name: "valid case", document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { '/': {} }, + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { "/": {} }, }, errors: [], }, { - name: 'invalid case', + name: "invalid case", document: { - openapi: '3.1.0', - info: { version: '1.0' }, + openapi: "3.1.0", + info: { version: "1.0" }, paths: {}, }, errors: [ { - message: 'Stop forcing all API consumers to visit documentation for basic interactions when the API could do that itself.', - path: ['paths'], + message: "APIs MUST have a root path (`/`) defined.", + path: ["paths"], severity: DiagnosticSeverity.Warning, }, ], diff --git a/__tests__/hosts-https-only-oas2.test.ts b/__tests__/hosts-https-only-oas2.test.ts index 7e84851..5968603 100644 --- a/__tests__/hosts-https-only-oas2.test.ts +++ b/__tests__/hosts-https-only-oas2.test.ts @@ -1,68 +1,68 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import testRule from './__helpers__/helper'; +import { DiagnosticSeverity } from "@stoplight/types"; +import testRule from "./__helpers__/helper"; -testRule('hosts-https-only-oas2', [ +testRule("hosts-https-only-oas2", [ { - name: 'valid case', + name: "valid case", document: { - swagger: '2.0', - info: { version: '1.0' }, - paths: { '/': {} }, - host: 'example.com', - schemes: ['https'], + swagger: "2.0", + info: { version: "1.0" }, + paths: { "/": {} }, + host: "example.com", + schemes: ["https"], }, errors: [], }, { - name: 'an invalid server.url using http', + name: "an invalid server.url using http", document: { - swagger: '2.0', - info: { version: '1.0' }, - paths: { '/': {} }, - host: 'example.com', - schemes: ['http'], + swagger: "2.0", + info: { version: "1.0" }, + paths: { "/": {} }, + host: "example.com", + schemes: ["http"], }, errors: [ { - message: 'Schemes MUST be https and no other protocol is allowed.', - path: ['schemes', '0'], + message: "Schemes MUST be https and no other protocol is allowed.", + path: ["schemes", "0"], severity: DiagnosticSeverity.Error, }, ], }, { - name: 'an invalid server.url using http and https', + name: "an invalid server.url using http and https", document: { - swagger: '2.0', - info: { version: '1.0' }, - paths: { '/': {} }, - host: 'example.com', - schemes: ['https', 'http'], + swagger: "2.0", + info: { version: "1.0" }, + paths: { "/": {} }, + host: "example.com", + schemes: ["https", "http"], }, errors: [ { - message: 'Schemes MUST be https and no other protocol is allowed.', - path: ['schemes', '1'], + message: "Schemes MUST be https and no other protocol is allowed.", + path: ["schemes", "1"], severity: DiagnosticSeverity.Error, }, ], }, { - name: 'an invalid server using ftp', + name: "an invalid server using ftp", document: { - swagger: '2.0', - info: { version: '1.0' }, - paths: { '/': {} }, - host: 'example.com', - schemes: ['ftp'], + swagger: "2.0", + info: { version: "1.0" }, + paths: { "/": {} }, + host: "example.com", + schemes: ["ftp"], }, errors: [ { - message: 'Schemes MUST be https and no other protocol is allowed.', - path: ['schemes', '0'], + message: "Schemes MUST be https and no other protocol is allowed.", + path: ["schemes", "0"], severity: DiagnosticSeverity.Error, }, ], diff --git a/__tests__/hosts-https-only-oas3.test.ts b/__tests__/hosts-https-only-oas3.test.ts index be2de0a..8618a8d 100644 --- a/__tests__/hosts-https-only-oas3.test.ts +++ b/__tests__/hosts-https-only-oas3.test.ts @@ -1,47 +1,47 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import testRule from './__helpers__/helper'; +import { DiagnosticSeverity } from "@stoplight/types"; +import testRule from "./__helpers__/helper"; -testRule('hosts-https-only-oas3', [ +testRule("hosts-https-only-oas3", [ { - name: 'valid case', + name: "valid case", document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { '/': {} }, - servers: [{ url: 'https://api.example.com/' }] + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { "/": {} }, + servers: [{ url: "https://api.example.com/" }], }, errors: [], }, { - name: 'an invalid server.url using http', + name: "an invalid server.url using http", document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { '/': {} }, - servers: [{ url: 'http://api.example.com/' }] + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { "/": {} }, + servers: [{ url: "http://api.example.com/" }], }, errors: [ { - message: 'Servers MUST be https and no other protocol is allowed.', - path: ['servers', '0', 'url'], + message: "Servers MUST be https and no other protocol is allowed.", + path: ["servers", "0", "url"], severity: DiagnosticSeverity.Error, }, ], }, { - name: 'an invalid server using ftp', + name: "an invalid server using ftp", document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { '/': {} }, - servers: [{ url: 'ftp://api.example.com/' }] + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { "/": {} }, + servers: [{ url: "ftp://api.example.com/" }], }, errors: [ { - message: 'Servers MUST be https and no other protocol is allowed.', - path: ['servers', '0', 'url'], + message: "Servers MUST be https and no other protocol is allowed.", + path: ["servers", "0", "url"], severity: DiagnosticSeverity.Error, }, ], diff --git a/__tests__/no-file-extensions-in-paths-oas2.ts b/__tests__/no-file-extensions-in-paths-oas2.ts deleted file mode 100644 index b1cdb41..0000000 --- a/__tests__/no-file-extensions-in-paths-oas2.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import testRule from './__helpers__/helper'; - -testRule('no-file-extensions-in-paths', [ - { - name: 'valid case', - document: { - swagger: "2.0", - info: { version: '1.0' }, - paths: { 'resources': {} } - }, - errors: [], - }, - - { - name: 'an API definition that is returning a json file', - document: { - swagger: "2.0", - info: { version: '1.0' }, - paths: { 'resources.json': {} } - }, - errors: [ - { - message: 'Paths must not include file extensions such as .json, .xml, .html and .txt. Use the OpenAPI `content` keyword to tell consumers which Media Types are available.', - path: ["paths", "resources.json"], - severity: DiagnosticSeverity.Error, - }, - ], - }, - { - name: 'an API definition that is returning a xml file', - document: { - swagger: "2.0", - info: { version: '1.0' }, - paths: { 'resources.xml': {} } - }, - errors: [ - { - message: 'Paths must not include file extensions such as .json, .xml, .html and .txt. Use the OpenAPI `content` keyword to tell consumers which Media Types are available.', - path: ["paths", "resources.xml"], - severity: DiagnosticSeverity.Error, - }, - ], - }, - { - name: 'an API definition that is returning a html file', - document: { - swagger: "2.0", - info: { version: '1.0' }, - paths: { 'resources.html': {} } - }, - errors: [ - { - message: 'Paths must not include file extensions such as .json, .xml, .html and .txt. Use the OpenAPI `content` keyword to tell consumers which Media Types are available.', - path: ["paths", "resources.html"], - severity: DiagnosticSeverity.Error, - }, - ], - }, - { - name: 'an API definition that is returning a txt file', - document: { - swagger: "2.0", - info: { version: '1.0' }, - paths: { 'resources.txt': {} } - }, - errors: [ - { - message: 'Paths must not include file extensions such as .json, .xml, .html and .txt. Use the OpenAPI `content` keyword to tell consumers which Media Types are available.', - path: ["paths", "resources.txt"], - severity: DiagnosticSeverity.Error, - }, - ], - } -]); diff --git a/__tests__/no-file-extensions-in-paths-oas3.ts b/__tests__/no-file-extensions-in-paths-oas3.ts deleted file mode 100644 index ea32091..0000000 --- a/__tests__/no-file-extensions-in-paths-oas3.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import testRule from './__helpers__/helper'; - -testRule('no-file-extensions-in-paths', [ - { - name: 'valid case', - document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { 'resources': {} } - }, - errors: [], - }, - - { - name: 'an API definition that is returning a json file', - document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { 'resources.json': {} } - }, - errors: [ - { - message: 'Paths must not include file extensions such as .json, .xml, .html and .txt. Use the OpenAPI `content` keyword to tell consumers which Media Types are available.', - path: ["paths", "resources.json"], - severity: DiagnosticSeverity.Error, - }, - ], - }, - { - name: 'an API definition that is returning a xml file', - document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { 'resources.xml': {} } - }, - errors: [ - { - message: 'Paths must not include file extensions such as .json, .xml, .html and .txt. Use the OpenAPI `content` keyword to tell consumers which Media Types are available.', - path: ["paths", "resources.xml"], - severity: DiagnosticSeverity.Error, - }, - ], - }, - { - name: 'an API definition that is returning a html file', - document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { 'resources.html': {} } - }, - errors: [ - { - message: 'Paths must not include file extensions such as .json, .xml, .html and .txt. Use the OpenAPI `content` keyword to tell consumers which Media Types are available.', - path: ["paths", "resources.html"], - severity: DiagnosticSeverity.Error, - }, - ], - }, - { - name: 'an API definition that is returning a txt file', - document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { 'resources.txt': {} } - }, - errors: [ - { - message: 'Paths must not include file extensions such as .json, .xml, .html and .txt. Use the OpenAPI `content` keyword to tell consumers which Media Types are available.', - path: ["paths", "resources.txt"], - severity: DiagnosticSeverity.Error, - }, - ], - } -]); diff --git a/__tests__/no-global-versioning.test.ts b/__tests__/no-global-versioning.test.ts index 7a68eeb..7b7ca44 100644 --- a/__tests__/no-global-versioning.test.ts +++ b/__tests__/no-global-versioning.test.ts @@ -1,47 +1,47 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import testRule from './__helpers__/helper'; +import { DiagnosticSeverity } from "@stoplight/types"; +import testRule from "./__helpers__/helper"; -testRule('no-global-versioning', [ +testRule("no-global-versioning", [ { - name: 'valid case', + name: "valid case", document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { '/': {} }, - servers: [{ url: 'https://api.example.com/' }] + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { "/": {} }, + servers: [{ url: "https://api.example.com/" }], }, errors: [], }, { - name: 'an API that is getting ready to give its consumers a really bad time', + name: "an API that is getting ready to give its consumers a really bad time", document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { '/': {} }, - servers: [{ url: 'https://api.example.com/v1' }] + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { "/": {} }, + servers: [{ url: "https://api.example.com/v1" }], }, errors: [ { - message: 'Using global versions just forces all your clients to do a lot more work for each upgrade. Please consider using API Evolution instead. More: https://apisyouwonthate.com/blog/api-evolution-for-rest-http-apis.', - path: ['servers', '0', 'url'], + message: "Server URL should not contain global versions.", + path: ["servers", "0", "url"], severity: DiagnosticSeverity.Warning, }, ], }, { - name: 'an API that got massively out of control as usual', + name: "an API that got massively out of control as usual", document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { '/': {} }, - servers: [{ url: 'https://api.example.com/v13' }] + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { "/": {} }, + servers: [{ url: "https://api.example.com/v13" }], }, errors: [ { - message: 'Using global versions just forces all your clients to do a lot more work for each upgrade. Please consider using API Evolution instead. More: https://apisyouwonthate.com/blog/api-evolution-for-rest-http-apis.', - path: ['servers', '0', 'url'], + message: "Server URL should not contain global versions.", + path: ["servers", "0", "url"], severity: DiagnosticSeverity.Warning, }, ], diff --git a/__tests__/no-http-basic.test.ts b/__tests__/no-http-basic.test.ts index 7c4813a..72733c6 100644 --- a/__tests__/no-http-basic.test.ts +++ b/__tests__/no-http-basic.test.ts @@ -2,49 +2,43 @@ import { DiagnosticSeverity } from "@stoplight/types"; import testRule from "./__helpers__/helper"; testRule("no-http-basic", [ - { - name: "valid case", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - components: { - securitySchemes: { - "anything-else": { - type: "http", - scheme: "bearer", - }, - }, - }, - }, - errors: [], - }, + { + name: "valid case", + document: { + openapi: "3.1.0", + info: { version: "1.0" }, + components: { + securitySchemes: { + "anything-else": { + type: "http", + scheme: "bearer", + }, + }, + }, + }, + errors: [], + }, - { - name: "invalid case", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - components: { - securitySchemes: { - "please-hack-me": { - type: "http", - scheme: "basic", - }, - }, - }, - }, - errors: [ - { - message: - "HTTP Basic is a pretty insecure way to pass credentials around, please consider an alternative.", - path: [ - "components", - "securitySchemes", - "please-hack-me", - "scheme", - ], - severity: DiagnosticSeverity.Error, - }, - ], - }, + { + name: "invalid case", + document: { + openapi: "3.1.0", + info: { version: "1.0" }, + components: { + securitySchemes: { + "please-hack-me": { + type: "http", + scheme: "basic", + }, + }, + }, + }, + errors: [ + { + message: "Please consider a more secure alternative to HTTP Basic.", + path: ["components", "securitySchemes", "please-hack-me", "scheme"], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/__tests__/no-numeric-ids.test.ts b/__tests__/no-numeric-ids.test.ts index 0ef153b..e5ffec6 100644 --- a/__tests__/no-numeric-ids.test.ts +++ b/__tests__/no-numeric-ids.test.ts @@ -2,64 +2,64 @@ import { DiagnosticSeverity } from "@stoplight/types"; import testRule from "./__helpers__/helper"; testRule("no-numeric-ids", [ - { - name: "valid case", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - paths: { - "/foo/{id}": { - get: { - description: "get", - parameters: [ - { - name: "id", - in: "path", - required: true, - schema: { - type: "string", - format: "uuid", - }, - }, - ], - }, - }, - }, - }, - errors: [], - }, + { + name: "valid case", + document: { + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { + "/foo/{id}": { + get: { + description: "get", + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { + type: "string", + format: "uuid", + }, + }, + ], + }, + }, + }, + }, + errors: [], + }, - { - name: "invalid if its an integer", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - paths: { - "/foo/{id}": { - get: { - description: "get", - parameters: [ - { - name: "id", - in: "path", - required: true, - schema: { - type: "integer", - format: "int32", - }, - }, - ], - }, - }, - }, - }, - errors: [ - { - message: - "Please avoid exposing IDs as an integer, UUIDs are preferred.", - path: ["paths", "/foo/{id}", "get", "parameters", "0", "schema"], - severity: DiagnosticSeverity.Error, - }, - ], - }, + { + name: "invalid if its an integer", + document: { + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { + "/foo/{id}": { + get: { + description: "get", + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { + type: "integer", + format: "int32", + }, + }, + ], + }, + }, + }, + }, + errors: [ + { + message: + "Please avoid exposing IDs as an integer, UUIDs are preferred.", + path: ["paths", "/foo/{id}", "get", "parameters", "0", "schema"], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/__tests__/no-unknown-error-format.test.ts b/__tests__/no-unknown-error-format.test.ts index 9ab41af..b56503e 100644 --- a/__tests__/no-unknown-error-format.test.ts +++ b/__tests__/no-unknown-error-format.test.ts @@ -1,53 +1,53 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import testRule from './__helpers__/helper'; +import { DiagnosticSeverity } from "@stoplight/types"; +import testRule from "./__helpers__/helper"; const template = (contentType: string) => { return { - openapi: '3.1.0', - info: { version: '1.0', contact: {} }, - paths: { + openapi: "3.1.0", + info: { version: "1.0", contact: {} }, + paths: { "/unknown-error": { - "get": { - "summary": "Your GET endpoint", - "responses": { + get: { + summary: "Your GET endpoint", + responses: { "400": { - "description": "Error", - "content": { - [contentType]: {} - } - } - } - } - } - } - } + description: "Error", + content: { + [contentType]: {}, + }, + }, + }, + }, + }, + }, + }; }; -testRule('no-unknown-error-format', [ +testRule("no-unknown-error-format", [ { - name: 'valid error format (JSON:API)', + name: "valid error format (JSON:API)", document: template("application/vnd.api+json"), errors: [], }, { - name: 'valid error format (RFC 7807, XML)', + name: "valid error format (RFC 7807, XML)", document: template("application/problem+xml"), errors: [], }, { - name: 'valid error format (RFC 7807, JSON)', + name: "valid error format (RFC 7807, JSON)", document: template("application/problem+json"), errors: [], }, { - name: 'invalid error format (plain JSON)', + name: "invalid error format (plain JSON)", document: template("application/json"), errors: [ { - message: 'Every error response SHOULD support either RFC 7807 (https://tools.ietf.org/html/rfc7807) or the JSON:API Error format.', + message: "Error response should use a standard error format.", path: [ "paths", "/unknown-error", diff --git a/__tests__/no-x-headers.test.ts b/__tests__/no-x-headers.test.ts index b173927..f2aab44 100644 --- a/__tests__/no-x-headers.test.ts +++ b/__tests__/no-x-headers.test.ts @@ -2,94 +2,93 @@ import { DiagnosticSeverity } from "@stoplight/types"; import testRule from "./__helpers__/helper"; testRule("no-x-headers", [ - { - name: "valid case", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - paths: { - "/foo": { - get: { - parameters: [ - { - name: "RateLimit-Limit", - in: "header", - description: - "standards are cool: https://www.ietf.org/archive/id/draft-polli-ratelimit-headers-02.html#name-ratelimit-limit", - required: true, - schema: { - type: "string", - examples: ["100, 100;w=10"], - }, - }, - ], - responses: { - "200": { - description: "ok", - headers: { - "X-Doesnt-Matter": { - description: - "Because OAS has two totally different ways of doing headers for request or response, this will be picked up by another rule.", - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - errors: [], - }, + { + name: "valid case", + document: { + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { + "/foo": { + get: { + parameters: [ + { + name: "RateLimit-Limit", + in: "header", + description: + "standards are cool: https://www.ietf.org/archive/id/draft-polli-ratelimit-headers-02.html#name-ratelimit-limit", + required: true, + schema: { + type: "string", + examples: ["100, 100;w=10"], + }, + }, + ], + responses: { + "200": { + description: "ok", + headers: { + "X-Doesnt-Matter": { + description: + "Because OAS has two totally different ways of doing headers for request or response, this will be picked up by another rule.", + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, - { - name: "invalid case", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - paths: { - "/foo": { - get: { - description: "get", - parameters: [ - { - name: "X-Rate-Limit", - in: "header", - description: "calls per hour allowed by the user", - required: true, - schema: { - type: "integer", - format: "int32", - }, - }, - ], - responses: { - "200": { - description: "ok", - headers: { - "X-Doesnt-Matter": { - description: - "Because OAS has two totally different ways of doing headers for request or response, this will be picked up by another rule.", - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - errors: [ - { - message: - "Headers cannot start with X-, so please find a new name for name. More: https://tools.ietf.org/html/rfc6648.", - path: ["paths", "/foo", "get", "parameters", "0", "name"], - severity: DiagnosticSeverity.Error, - } - ], - }, + { + name: "invalid case", + document: { + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { + "/foo": { + get: { + description: "get", + parameters: [ + { + name: "X-Rate-Limit", + in: "header", + description: "calls per hour allowed by the user", + required: true, + schema: { + type: "integer", + format: "int32", + }, + }, + ], + responses: { + "200": { + description: "ok", + headers: { + "X-Doesnt-Matter": { + description: + "Because OAS has two totally different ways of doing headers for request or response, this will be picked up by another rule.", + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: 'Header `name` should not start with "X-".', + path: ["paths", "/foo", "get", "parameters", "0", "name"], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/__tests__/no-x-response-headers.test.ts b/__tests__/no-x-response-headers.test.ts index 0adaab1..7598a42 100644 --- a/__tests__/no-x-response-headers.test.ts +++ b/__tests__/no-x-response-headers.test.ts @@ -2,110 +2,109 @@ import { DiagnosticSeverity } from "@stoplight/types"; import testRule from "./__helpers__/helper"; testRule("no-x-response-headers", [ - { - name: "valid case", - document: { - openapi: "3.1.0", - info: { version: "1.0", contact: {} }, - paths: { - "/foo": { - get: { + { + name: "valid case", + document: { + openapi: "3.1.0", + info: { version: "1.0", contact: {} }, + paths: { + "/foo": { + get: { parameters: [ - { - name: "X-Doesnt-Matter", - in: "header", - description: - "Because OAS has two totally different ways of doing headers for request or response, this will be picked up by another rule.", - required: true, - schema: { - type: "string", - }, - }, - ], - responses: { - "200": { - description: "ok", - headers: { - "Retry-After": { - description: - "How long the user agent should wait before making a follow-up request.", - schema: { - oneOf: [ - { - type: "string", - format: "date-time", - examples: ["Wed, 21 Oct 2015 07:28:00 GMT"], - }, - { - type: "integer", - examples: [60], - }, - ], - }, - }, - }, - }, - }, - }, - }, - }, - }, - errors: [], - }, + { + name: "X-Doesnt-Matter", + in: "header", + description: + "Because OAS has two totally different ways of doing headers for request or response, this will be picked up by another rule.", + required: true, + schema: { + type: "string", + }, + }, + ], + responses: { + "200": { + description: "ok", + headers: { + "Retry-After": { + description: + "How long the user agent should wait before making a follow-up request.", + schema: { + oneOf: [ + { + type: "string", + format: "date-time", + examples: ["Wed, 21 Oct 2015 07:28:00 GMT"], + }, + { + type: "integer", + examples: [60], + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, - { - name: "invalid case", - document: { - openapi: "3.1.0", - info: { version: "1.0", contact: {} }, - paths: { - "/foo": { - get: { + { + name: "invalid case", + document: { + openapi: "3.1.0", + info: { version: "1.0", contact: {} }, + paths: { + "/foo": { + get: { parameters: [ - { - name: "X-Doesnt-Matter", - in: "header", - description: - "Because OAS has two totally different ways of doing headers for request or response, this will be picked up by another rule.", - required: true, - schema: { - type: "string", - }, - }, - ], - responses: { - "200": { - description: "ok", - headers: { - "X-Expires-After": { - description: - "Some custom made header that could will confuse everyone and probably has a standard HTTP header already.", - schema: { + { + name: "X-Doesnt-Matter", + in: "header", + description: + "Because OAS has two totally different ways of doing headers for request or response, this will be picked up by another rule.", + required: true, + schema: { + type: "string", + }, + }, + ], + responses: { + "200": { + description: "ok", + headers: { + "X-Expires-After": { + description: + "Some custom made header that could will confuse everyone and probably has a standard HTTP header already.", + schema: { type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - errors: [ - { - message: - "Headers cannot start with X-, so please find a new name for X-Expires-After. More: https://tools.ietf.org/html/rfc6648.", - path: [ - "paths", - "/foo", - "get", - "responses", - "200", - "headers", - "X-Expires-After", - ], - severity: DiagnosticSeverity.Error, - }, - ], - }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: 'Header `X-Expires-After` should not start with "X-".', + path: [ + "paths", + "/foo", + "get", + "responses", + "200", + "headers", + "X-Expires-After", + ], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/__tests__/paths-kebab-case.test.ts b/__tests__/paths-kebab-case.test.ts index 561fc48..d925b75 100644 --- a/__tests__/paths-kebab-case.test.ts +++ b/__tests__/paths-kebab-case.test.ts @@ -1,28 +1,29 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import testRule from './__helpers__/helper'; +import { DiagnosticSeverity } from "@stoplight/types"; +import testRule from "./__helpers__/helper"; -testRule('paths-kebab-case', [ +testRule("paths-kebab-case", [ { - name: 'valid case', + name: "valid case", document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { '/this-is-kebab-case': {} }, + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { "/this-is-kebab-case": {} }, }, errors: [], }, { - name: 'invalid case', + name: "invalid case", document: { - openapi: '3.1.0', - info: { version: '1.0' }, - paths: { '/this_is_snake_case': {} }, + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { "/this_is_snake_case": {} }, }, errors: [ { - message: '/this_is_snake_case should be kebab-case (lower case and separated with hyphens).', - path: ['paths', '/this_is_snake_case'], + message: + "/this_is_snake_case should be kebab-case (lower case and separated with hyphens).", + path: ["paths", "/this_is_snake_case"], severity: DiagnosticSeverity.Warning, }, ], diff --git a/__tests__/request-GET-no-body-oas2.test.ts b/__tests__/request-GET-no-body-oas2.test.ts index 22dd7c0..37e1756 100644 --- a/__tests__/request-GET-no-body-oas2.test.ts +++ b/__tests__/request-GET-no-body-oas2.test.ts @@ -2,49 +2,49 @@ import { DiagnosticSeverity } from "@stoplight/types"; import testRule from "./__helpers__/helper"; testRule("request-GET-no-body-oas2", [ - { - name: "valid case", - document: { - swagger: "2.0", - info: { version: "1.0" }, - paths: { - "/": { - get: {}, - }, - }, - }, - errors: [], - }, + { + name: "valid case", + document: { + swagger: "2.0", + info: { version: "1.0" }, + paths: { + "/": { + get: {}, + }, + }, + }, + errors: [], + }, - { - name: "invalid case", - document: { - swagger: "2.0.0", - info: { version: "1.0" }, - paths: { - "/": { - get: { - summary: "Get is a question but this looks like an answer", - consumes: ["application/json"], - parameters: [ - { - in: "body", - name: "user", - schema: { - type: "object", - }, - }, - ], - }, - }, - }, - }, - errors: [ - { - message: "A `GET` request MUST NOT accept a `body` parameter", - path: ["paths", "/", "get", "parameters", "0", "in"], - severity: DiagnosticSeverity.Error, - }, - ], - }, + { + name: "invalid case", + document: { + swagger: "2.0.0", + info: { version: "1.0" }, + paths: { + "/": { + get: { + summary: "Get is a question but this looks like an answer", + consumes: ["application/json"], + parameters: [ + { + in: "body", + name: "user", + schema: { + type: "object", + }, + }, + ], + }, + }, + }, + }, + errors: [ + { + message: "A `GET` request MUST NOT accept a request body.", + path: ["paths", "/", "get", "parameters", "0", "in"], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/__tests__/request-GET-no-body-oas3.test.ts b/__tests__/request-GET-no-body-oas3.test.ts index 29d9966..eba0483 100644 --- a/__tests__/request-GET-no-body-oas3.test.ts +++ b/__tests__/request-GET-no-body-oas3.test.ts @@ -2,48 +2,48 @@ import { DiagnosticSeverity } from "@stoplight/types"; import testRule from "./__helpers__/helper"; testRule("request-GET-no-body-oas3", [ - { - name: "valid case", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - paths: { - "/": { - get: {}, - }, - }, - }, - errors: [], - }, + { + name: "valid case", + document: { + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { + "/": { + get: {}, + }, + }, + }, + errors: [], + }, - { - name: "invalid case", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - paths: { - "/": { - get: { - requestBody: { - description: "Get is a question but this looks like an answer", - content: { - "application/json": { - schema: { - type: "object", - }, - }, - }, - }, - }, - }, - }, - }, - errors: [ - { - message: "A `GET` request MUST NOT accept a request body", - path: ["paths", "/", "get", "requestBody"], - severity: DiagnosticSeverity.Error, - }, - ], - }, + { + name: "invalid case", + document: { + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { + "/": { + get: { + requestBody: { + description: "Get is a question but this looks like an answer", + content: { + "application/json": { + schema: { + type: "object", + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: "A `GET` request MUST NOT accept a request body.", + path: ["paths", "/", "get", "requestBody"], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/__tests__/request-support-json-oas3.test.ts b/__tests__/request-support-json-oas3.test.ts index 17b6ce5..006c328 100644 --- a/__tests__/request-support-json-oas3.test.ts +++ b/__tests__/request-support-json-oas3.test.ts @@ -2,65 +2,66 @@ import { DiagnosticSeverity } from "@stoplight/types"; import testRule from "./__helpers__/helper"; testRule("request-support-json-oas3", [ - { - name: "valid case", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - paths: { - "/": { - get: { - requestBody: { - description: "JSON and CSV? How courteous of you!", - content: { - "application/json": { - schema: { - type: "object", - }, - }, - "text/csv": { - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - errors: [], - }, + { + name: "valid case", + document: { + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { + "/": { + get: { + requestBody: { + description: "JSON and CSV? How courteous of you!", + content: { + "application/json": { + schema: { + type: "object", + }, + }, + "text/csv": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, - { - name: "invalid case", - document: { - openapi: "3.1.0", - info: { version: "1.0" }, - paths: { - "/": { - get: { - requestBody: { - description: - "only csv is going to annoy folks who want to use JSON so this is invalid", - content: { - "text/csv": { - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - errors: [ - { - message: 'Every request SHOULD support at least one `application/json` content type.', - path: ["paths", "/", "get", "requestBody", "content"], - severity: DiagnosticSeverity.Warning, - }, - ], - }, + { + name: "invalid case", + document: { + openapi: "3.1.0", + info: { version: "1.0" }, + paths: { + "/": { + get: { + requestBody: { + description: + "only csv is going to annoy folks who want to use JSON so this is invalid", + content: { + "text/csv": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: + "Every request SHOULD support at least one `application/json` content type.", + path: ["paths", "/", "get", "requestBody", "content"], + severity: DiagnosticSeverity.Warning, + }, + ], + }, ]); diff --git a/src/ruleset.ts b/src/ruleset.ts index 29313eb..b3550f2 100644 --- a/src/ruleset.ts +++ b/src/ruleset.ts @@ -13,15 +13,13 @@ import { import { oas2, oas3 } from "@stoplight/spectral-formats"; import { DiagnosticSeverity } from "@stoplight/types"; - export default { rules: { - // Author: Phil Sturgeon (https://github.com/philsturgeon) "api-home": { - description: "APIs MUST have a root path (`/`) defined.", - message: - "Stop forcing all API consumers to visit documentation for basic interactions when the API could do that itself.", + message: "APIs MUST have a root path (`/`) defined.", + description: + "Good documentation is always welcome, but API consumers should be able to get a pretty long way through interaction with the API alone. They should at least know they're looking at the right place instead of getting a 404 or random 500 error as is common in some APIs.\n\nThere are various efforts around to standardize the home document, but the best is probably this one: https://webconcepts.info/specs/IETF/I-D/nottingham-json-home", given: "$.paths", then: { field: "/", @@ -32,8 +30,9 @@ export default { // Author: Phil Sturgeon (https://github.com/philsturgeon) "api-home-get": { - description: "APIs root path (`/`) MUST have a GET operation.", - message: "Otherwise people won't know how to get it.", + message: "APIs root path (`/`) MUST have a GET operation.", + description: + "Good documentation is always welcome, but API consumers should be able to get a pretty long way through interaction with the API alone. They should at least know they're looking at the right place instead of getting a 404 or random 500 error as is common in some APIs.\n\nThere are various efforts around to standardize the home document, but the best is probably this one: https://webconcepts.info/specs/IETF/I-D/nottingham-json-home", given: "$.paths[/]", then: { field: "get", @@ -44,9 +43,9 @@ export default { // Author: Phil Sturgeon (https://github.com/philsturgeon) "api-health": { - description: "APIs MUST have a health path (`/health`) defined.", - message: - "Creating a `/health` endpoint is a simple solution for pull-based monitoring and manually checking the status of an API.", + message: "APIs MUST have a health path (`/health`) defined.", + description: + "Creating a `/health` endpoint is a simple solution for pull-based monitoring and manually checking the status of an API. To learn more about health check endpoints see https://apisyouwonthate.com/blog/health-checks-with-kubernetes.", given: "$.paths", then: { field: "/health", @@ -57,10 +56,10 @@ export default { // Author: Phil Sturgeon (https://github.com/philsturgeon) "api-health-format": { - description: - "Health path (`/heath`) SHOULD support Health Check Response Format", message: - "Use existing standards (and draft standards) wherever possible, like the draft standard for health checks: https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check.", + "Health path (`/heath`) SHOULD support Health Check Response Format", + description: + "Use existing standards (and draft standards) wherever possible, like the draft standard for health checks: https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check. To learn more about health check endpoints see https://apisyouwonthate.com/blog/health-checks-with-kubernetes.", formats: [oas3], given: "$.paths[/health]..responses[*].content.*~", then: { @@ -74,9 +73,10 @@ export default { // Author: Phil Sturgeon (https://github.com/philsturgeon) "paths-kebab-case": { - description: "Should paths be kebab-case.", message: "{{property}} should be kebab-case (lower case and separated with hyphens).", + description: + "Naming conventions don't particular matter, and picking something consistent is the most important thing. So let's pick kebab-case for paths, because... well it's nice and why not.", given: "$.paths[*]~", then: { function: pattern, @@ -89,8 +89,9 @@ export default { // Author: Phil Sturgeon (https://github.com/philsturgeon) "no-numeric-ids": { + message: "Please avoid exposing IDs as an integer, UUIDs are preferred.", description: - "Please avoid exposing IDs as an integer, UUIDs are preferred.", + "Using auto-incrementing IDs in your API means people can download your entire database with a for() loop, whether its public or protected. Using UUID, ULID, snowflake, etc. can help to avoid this, or at least slow them down, depending on how you have your API set up.\n\nThis is recommended by the OWASP API Security Project. https://github.com/OWASP/API-Security/blob/master/2019/en/src/0xa1-broken-object-level-authorization.md.\n\nLearn more about this over here. https://phil.tech/2015/auto-incrementing-to-destruction/", given: '$.paths..parameters[*][?(@property === "name" && (@ === "id" || @.match(/(_id|Id|-id)$/)))]^.schema', then: { @@ -118,9 +119,9 @@ export default { // Author: Phil Sturgeon (https://github.com/philsturgeon) "no-http-basic": { - description: "Consider a more secure alternative to HTTP Basic", - message: - "HTTP Basic is a pretty insecure way to pass credentials around, please consider an alternative.", + message: "Please consider a more secure alternative to HTTP Basic.", + description: + "HTTP Basic is an inherently insecure way to pass credentials to the API. They're placed in the URL in base64 which can be decrypted easily. Even if you're using a token, there are far better ways to handle passing tokens to an API which are less likely to leak.\n\nSee OWASP advice. https://github.com/OWASP/API-Security/blob/master/2019/en/src/0xa2-broken-user-authentication.md", given: "$.components.securitySchemes[*]", then: { field: "scheme", @@ -134,9 +135,9 @@ export default { // Author: Phil Sturgeon (https://github.com/philsturgeon) "no-x-headers": { - description: "Please do not use headers with X-", - message: - "Headers cannot start with X-, so please find a new name for {{property}}. More: https://tools.ietf.org/html/rfc6648.", + message: 'Header `{{property}}` should not start with "X-".', + description: + "Headers starting with X- is an awkward convention which is entirely unnecessary. There is probably a standard for what you're trying to do, so it would be better to use that. If there is not a standard already perhaps there's a draft that you could help mature through use and feedback.\n\nSee what you can find on https://standards.rest.\n\nMore about X- headers here: https://tools.ietf.org/html/rfc6648.", given: "$..parameters[?(@.in === 'header')].name", then: { function: pattern, @@ -149,9 +150,9 @@ export default { // Author: Phil Sturgeon (https://github.com/philsturgeon) "no-x-response-headers": { - description: "Please do not use headers with X-", - message: - "Headers cannot start with X-, so please find a new name for {{property}}. More: https://tools.ietf.org/html/rfc6648.", + message: 'Header `{{property}}` should not start with "X-".', + description: + "Headers starting with X- is an awkward convention which is entirely unnecessary. There is probably a standard for what you're trying to do, so it would be better to use that. If there is not a standard already perhaps there's a draft that you could help mature through use and feedback.\n\nSee what you can find on https://standards.rest.\n\nMore about X- headers here: https://tools.ietf.org/html/rfc6648.", given: "$..headers.*~", then: { function: pattern, @@ -164,7 +165,9 @@ export default { // Author: Andrzej (https://github.com/jerzyn) "request-GET-no-body-oas2": { - description: "A `GET` request MUST NOT accept a `body` parameter", + message: "A `GET` request MUST NOT accept a request body.", + description: + "Defining a request body on a HTTP GET is technically possible in some implementations, but is increasingly frowned upon due to the confusion that comes from unspecified behavior in the HTTP specification.", given: "$.paths..get.parameters..in", then: { function: pattern, @@ -178,7 +181,9 @@ export default { // Author: Andrzej (https://github.com/jerzyn) "request-GET-no-body-oas3": { - description: "A `GET` request MUST NOT accept a request body", + message: "A `GET` request MUST NOT accept a request body.", + description: + "Defining a request body on a HTTP GET is in some implementations, but is increasingly frowned upon due to the confusion that comes from unspecified behavior in the HTTP specification.", given: "$.paths..get.requestBody", then: { function: undefinedFunc, @@ -189,9 +194,9 @@ export default { // Author: Andrzej (https://github.com/jerzyn) "hosts-https-only-oas2": { - description: "ALL requests MUST go through `https` protocol only", - type: "style", message: "Schemes MUST be https and no other protocol is allowed.", + description: + "Using http in production is reckless, advised against by OWASP API Security, and generally unnecessary thanks to free SSL on loads of hosts, gateways like Cloudflare, and OSS tools like Lets Encrypt.", given: "$.schemes", then: { function: schema, @@ -211,8 +216,9 @@ export default { // Author: Andrzej (https://github.com/jerzyn) "hosts-https-only-oas3": { - description: "ALL requests MUST go through https:// protocol only", message: "Servers MUST be https and no other protocol is allowed.", + description: + "Using http in production is reckless, advised against by OWASP API Security, and generally unnecessary thanks to free SSL on loads of hosts, gateways like Cloudflare, and OSS tools like Lets Encrypt.", given: "$.servers..url", then: { function: pattern, @@ -226,8 +232,10 @@ export default { // Author: Andrzej (https://github.com/jerzyn) "request-support-json-oas3": { - description: + message: "Every request SHOULD support at least one `application/json` content type.", + description: + "Maybe you've got an XML heavy API or you're using a special binary format like BSON or CSON. That's lovely, but supporting JSON too is going to help a lot of people avoid a lot of confusion, and probably make you more money than you spend on supporting it.", given: "$.paths[*][*].requestBody.content", then: { function: schema, @@ -247,16 +255,17 @@ export default { // Author: Phil Sturgeon (https://github.com/philsturgeon) "no-unknown-error-format": { + message: "Error response should use a standard error format.", description: - "Every error response SHOULD support either RFC 7807 (https://tools.ietf.org/html/rfc7807) or the JSON:API Error format.", + "Error responses can be unique snowflakes, different to every API, but standards exist to make them consistent, which reduces surprises and increase interoperability. Please use either RFC 7807 (https://tools.ietf.org/html/rfc7807) or the JSON:API Error format (https://jsonapi.org/format/#error-objects).", given: "$.paths[*]..responses[?(@property.match(/^(4|5)/))].content.*~", then: { function: enumeration, functionOptions: { values: [ "application/vnd.api+json", - "application/problem+xml", "application/problem+json", + "application/problem+xml", ], }, }, @@ -266,9 +275,9 @@ export default { // Author: Nauman Ali (https://github.com/naumanali-stoplight) "no-global-versioning": { - description: "Server URL should not contain global versions", - message: - "Using global versions just forces all your clients to do a lot more work for each upgrade. Please consider using API Evolution instead. More: https://apisyouwonthate.com/blog/api-evolution-for-rest-http-apis.", + message: "Server URL should not contain global versions.", + description: + "Using global versions just forces all your clients to do a lot more work for each upgrade. Please consider using API Evolution instead.\n\nMore: https://apisyouwonthate.com/blog/api-evolution-for-rest-http-apis.", given: "$.servers[*].url", then: { function: pattern, @@ -282,27 +291,29 @@ export default { // Author: Advanced API & Integrations Team (https://www.oneadvanced.com/) "no-file-extensions-in-paths": { - description: "Paths must not include file extensions such as .json, .xml, .html and .txt", message: - "Paths must not include file extensions such as .json, .xml, .html and .txt. Use the OpenAPI `content` keyword to tell consumers which Media Types are available.", - given: "$.paths[*]~", - then: { + "Paths must not include file extensions such as .json, .xml, .html and .txt.", + description: + "Paths must not include file extensions such as `.json`, `.xml`, `.html` and `.txt`. Use the OpenAPI `content` keyword to tell consumers which Media Types are available.", + given: "$.paths[*]~", + then: { function: pattern, functionOptions: { - notMatch: "\.(json|xml|html|txt)$", + notMatch: ".(json|xml|html|txt)$", }, }, severity: DiagnosticSeverity.Error, }, // Author: Advanced API & Integrations Team (https://www.oneadvanced.com/) - "adv-security-schemes-defined": { - description: "All APIs MUST have a security scheme defined.", - message: "This API definition does not have any security scheme defined.", + "no-security-schemes-defined": { + message: "All APIs MUST have a security scheme defined.", + description: + "This API definition does not have any security scheme defined, which means the entire API is open to the public. That's probably not what you want, even if all the data is read-only. Setting lower rate limits for the public and letting known consumers use more resources is a handy path to monetization, and helps know who your power users are when changes need feedback or migration, even if not just good practice.", given: "$..components", then: { field: "securitySchemes", - function: truthy + function: truthy, }, formats: [oas3], severity: DiagnosticSeverity.Error,