diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8c02471..51ddbe3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,8 +36,14 @@ jobs: env: RELEASE_TYPE: ${{ github.event.inputs.release-type }} run: | - VERSION=$(npm version $RELEASE_TYPE) - git commit --amend -m "chore: release $VERSION" + # Bump root and all workspaces to the same version, no git tag yet + npm version $RELEASE_TYPE --workspaces --no-git-tag-version + # Read the new version from root package.json + VERSION="v$(node -p "require('./package.json').version")" + # Commit all bumps and create a single tag + git add package.json api-guidelines-redocly2-ruleset/package.json + git commit -m "chore: release $VERSION" + git tag $VERSION echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Push changes @@ -56,4 +62,4 @@ jobs: - name: Publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npm publish + run: npm publish --workspaces --include-workspace-root diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 69636411..77e7babe 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,6 +20,6 @@ jobs: cache: npm - run: npm ci - - run: npm run build - - run: npm run tsc - - run: npm run test + - run: npm run build:all + - run: npm run tsc:all + - run: npm run test:all diff --git a/api-guidelines-redocly2-ruleset/package.json b/api-guidelines-redocly2-ruleset/package.json new file mode 100644 index 00000000..a3cf8175 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/package.json @@ -0,0 +1,40 @@ +{ + "name": "@otto-de/api-guidelines-redocly2-ruleset", + "version": "1.0.1", + "description": "Redocly v2 linting plugin for the OTTO API Guidelines ruleset", + "author": "", + "license": "ISC", + "homepage": "https://github.com/otto-de/api-guidelines#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/otto-de/api-guidelines.git" + }, + "bugs": { + "url": "https://github.com/otto-de/api-guidelines/issues" + }, + "engines": { + "node": ">=16" + }, + "type": "module", + "main": "dist/plugin.js", + "files": [ + "dist", + "src", + "!**/*.spec.ts", + "!**/__tests__" + ], + "scripts": { + "build": "npm run clean && npm run build:redocly", + "build:redocly": "esbuild src/plugin.ts --bundle --platform=node --format=esm --outfile=dist/plugin.js --log-level=warning --external:@redocly/openapi-core", + "clean": "rm -rf ./dist", + "test": "vitest run", + "tsc": "tsc --noEmit" + }, + "devDependencies": { + "@redocly/openapi-core": "^2.25.4", + "@types/node": "^20.9.4", + "esbuild": "^0.19.7", + "typescript": "^5.3.2", + "vitest": "^0.34.6" + } +} diff --git a/api-guidelines-redocly2-ruleset/src/plugin.ts b/api-guidelines-redocly2-ruleset/src/plugin.ts new file mode 100644 index 00000000..25b19637 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/plugin.ts @@ -0,0 +1,84 @@ +import { UseTLS } from "./rules/use-tls.js"; +import { AlwaysReturnJsonObject } from "./rules/always-return-json-object.js"; +import { DefinePermissionsWithScope } from "./rules/define-permissions-with-scope.js"; +import { FormatEnumerationUpperSnakeCase } from "./rules/format-enumeration-upper-snake-case.js"; +import { NoRequestBodyInGetMethod } from "./rules/no-request-body-in-get-method.js"; +import { NotUseNullForEmptyArray } from "./rules/not-use-null-for-empty-array.js"; +import { OmitOptionalProperty } from "./rules/omit-optional-property.js"; +import { SecureEndpointsWithOAuth20 } from "./rules/secure-endpoints-with-oauth-2.0.js"; +import { UseAbsoluteCustomLinkRelationUrl } from "./rules/use-absolute-custom-link-relation-url.js"; +import { UseAbsoluteProfileUrl } from "./rules/use-absolute-profile-url.js"; +import { UseAuthorizationGrant } from "./rules/use-authorization-grant.js"; +import { UseCamelCaseForPropertyName } from "./rules/use-camel-case-for-property-name.js"; +import { UseCamelCaseForQueryParameter } from "./rules/use-camel-case-for-query-parameter.js"; +import { UseCommonDateAndTimeFormat } from "./rules/use-common-date-and-time-format.js"; +import { UseCuriedLinkRelationTypes } from "./rules/use-curied-link-relation-types.js"; +import { UseExtensibleEnum } from "./rules/use-extensible-enum.js"; +import { UseKebabCaseForPathParameter } from "./rules/use-kebab-case-for-path-parameter.js"; +import { UseKebabCaseInUri } from "./rules/use-kebab-case-in-uri.js"; +import { UseOas3 } from "./rules/use-oas-3.js"; +import { UseStringEnum } from "./rules/use-string-enum.js"; +import { Operation4xxProblemDetailsRfc9457 } from "./rules/operation-4xx-problem-details-rfc9457.js"; + +export default function ApiGuidelinesPlugin() { + return { + id: "api-guidelines", + rules: { + oas2: { + "use-oas-3": UseOas3, + }, + oas3: { + "always-return-json-object": AlwaysReturnJsonObject, + "define-permissions-with-scope": DefinePermissionsWithScope, + "format-enumeration-upper-snake-case": FormatEnumerationUpperSnakeCase, + "no-request-body-in-get-method": NoRequestBodyInGetMethod, + "not-use-null-for-empty-array": NotUseNullForEmptyArray, + "omit-optional-property": OmitOptionalProperty, + "secure-endpoints-with-oauth-2.0": SecureEndpointsWithOAuth20, + "use-absolute-custom-link-relation-url": UseAbsoluteCustomLinkRelationUrl, + "use-absolute-profile-url": UseAbsoluteProfileUrl, + "use-authorization-grant": UseAuthorizationGrant, + "use-camel-case-for-property-name": UseCamelCaseForPropertyName, + "use-camel-case-for-query-parameter": UseCamelCaseForQueryParameter, + "use-common-date-and-time-format": UseCommonDateAndTimeFormat, + "use-curied-link-relation-types": UseCuriedLinkRelationTypes, + "use-extensible-enum": UseExtensibleEnum, + "use-kebab-case-for-path-parameter": UseKebabCaseForPathParameter, + "use-kebab-case-in-uri": UseKebabCaseInUri, + "use-string-enum": UseStringEnum, + "use-tls": UseTLS, + "operation-4xx-problem-details-rfc9457": Operation4xxProblemDetailsRfc9457, + }, + }, + configs: { + recommended: { + rules: { + "info-contact": "error", // https://api.otto.de/portal/guidelines/r000078 + "operation-2xx-response": "error", // https://api.otto.de/portal/guidelines/r000011 + "api-guidelines/operation-4xx-problem-details-rfc9457": "error", // https://api.otto.de/portal/guidelines/r000034 + "no-path-trailing-slash": "error", // https://api.otto.de/portal/guidelines/r000020 + "api-guidelines/always-return-json-object": "error", // https://api.otto.de/portal/guidelines/r004030 + "api-guidelines/define-permissions-with-scope": "error", // https://api.otto.de/portal/guidelines/r000047 + "api-guidelines/format-enumeration-upper-snake-case": "error", // https://api.otto.de/portal/guidelines/r004090 + "api-guidelines/no-request-body-in-get-method": "error", // https://api.otto.de/portal/guidelines/r000007 + "api-guidelines/not-use-null-for-empty-array": "warn", // https://api.otto.de/portal/guidelines/r004060 + "api-guidelines/omit-optional-property": "warn", // https://api.otto.de/portal/guidelines/r004021 + "api-guidelines/secure-endpoints-with-oauth-2.0": "error", // https://api.otto.de/portal/guidelines/r000051 + "api-guidelines/use-absolute-custom-link-relation-url": "error", // https://api.otto.de/portal/guidelines/r100037 + "api-guidelines/use-absolute-profile-url": "error", // https://api.otto.de/portal/guidelines/r100066 + "api-guidelines/use-authorization-grant": "error", // https://api.otto.de/portal/guidelines/r000052 + "api-guidelines/use-camel-case-for-property-name": "warn", // https://api.otto.de/portal/guidelines/r004010 + "api-guidelines/use-camel-case-for-query-parameter": "error", // https://api.otto.de/portal/guidelines/r000022 + "api-guidelines/use-common-date-and-time-format": "error", // https://api.otto.de/portal/guidelines/r100072 + "api-guidelines/use-curied-link-relation-types": "error", // https://api.otto.de/portal/guidelines/r100038 + "api-guidelines/use-extensible-enum": "warn", // https://api.otto.de/portal/guidelines/r000035 + "api-guidelines/use-kebab-case-for-path-parameter": "off", // TODO guideline rule will follow + "api-guidelines/use-kebab-case-in-uri": "error", // https://api.otto.de/portal/guidelines/r000023 + "api-guidelines/use-oas-3": "error", // https://api.otto.de/portal/guidelines/r000003 + "api-guidelines/use-string-enum": "error", // https://api.otto.de/portal/guidelines/r004080 + "api-guidelines/use-tls": "error", // https://api.otto.de/portal/guidelines/r000046 + }, + }, + }, + }; +} diff --git a/api-guidelines-redocly2-ruleset/src/rules/__tests__/createTestConfig.ts b/api-guidelines-redocly2-ruleset/src/rules/__tests__/createTestConfig.ts new file mode 100644 index 00000000..a6926b04 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/__tests__/createTestConfig.ts @@ -0,0 +1,31 @@ +import { + createConfig, + type Config, + type Plugin, + type RuleConfig, + type RawUniversalConfig, + type Oas3Rule, + type Oas2Rule, + type Async2Rule, + type Async3Rule, +} from "@redocly/openapi-core"; + +type AnyRule = Oas3Rule | Oas2Rule | Async2Rule | Async3Rule; + + +export type CustomRulesConfig = Partial, Record>>; + +export async function createTestConfig(customRulesConfig: CustomRulesConfig): Promise { + const pluginId = "test-plugin"; + + const prefixedRules = Object.fromEntries( + Object.values(customRulesConfig).flatMap((ruleSet) => + Object.keys(ruleSet ?? {}).map((rule) => [`${pluginId}/${rule}`, "error" as RuleConfig]), + ), + ); + + return createConfig({ + plugins: [{ id: pluginId, rules: customRulesConfig as NonNullable }], + rules: prefixedRules, + } as RawUniversalConfig); +} diff --git a/src/linting/rules/__tests__/redocly.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/__tests__/redocly.spec.ts similarity index 95% rename from src/linting/rules/__tests__/redocly.spec.ts rename to api-guidelines-redocly2-ruleset/src/rules/__tests__/redocly.spec.ts index 342f87d2..468b3fc5 100644 --- a/src/linting/rules/__tests__/redocly.spec.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/__tests__/redocly.spec.ts @@ -60,7 +60,7 @@ describe("redocly visitor", () => { it("should enter SchemaProperties only once", async () => { const spy = vi.fn(); - const config = createTestConfig({ + const config = await createTestConfig({ oas3: { // @ts-ignore "test-rule": () => ({ @@ -82,7 +82,7 @@ describe("redocly visitor", () => { it("should enter SchemaProperties multiple times", async () => { const spy = vi.fn(); - const config = createTestConfig({ + const config = await createTestConfig({ oas3: { // @ts-ignore "test-rule": () => ({ @@ -102,7 +102,7 @@ describe("redocly visitor", () => { it("should enter SchemaProperties multiple times", async () => { const spy = vi.fn(); - const config = createTestConfig({ + const config = await createTestConfig({ oas3: { // @ts-ignore "test-rule": () => ({ diff --git a/src/linting/rules/__tests__/removeClutter.ts b/api-guidelines-redocly2-ruleset/src/rules/__tests__/removeClutter.ts similarity index 100% rename from src/linting/rules/__tests__/removeClutter.ts rename to api-guidelines-redocly2-ruleset/src/rules/__tests__/removeClutter.ts diff --git a/src/linting/rules/always-return-json-object.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/always-return-json-object.spec.ts similarity index 93% rename from src/linting/rules/always-return-json-object.spec.ts rename to api-guidelines-redocly2-ruleset/src/rules/always-return-json-object.spec.ts index 4c6c590a..ffa73bec 100644 --- a/src/linting/rules/always-return-json-object.spec.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/always-return-json-object.spec.ts @@ -1,13 +1,16 @@ -import { lintFromString } from "@redocly/openapi-core"; +import { lintFromString, type Config } from "@redocly/openapi-core"; import { AlwaysReturnJsonObject } from "./always-return-json-object.js"; import { createTestConfig } from "./__tests__/createTestConfig.js"; import { removeClutter } from "./__tests__/removeClutter.js"; -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": AlwaysReturnJsonObject, - }, +let config: Config; +beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": AlwaysReturnJsonObject, + }, + }); }); it("should not find any error", async () => { @@ -128,7 +131,7 @@ paths: }, ], "message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, ] @@ -211,7 +214,7 @@ components: }, ], "message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, ] @@ -254,7 +257,7 @@ components: }, ], "message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, ] @@ -300,7 +303,7 @@ components: }, ], "message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -311,7 +314,7 @@ components: }, ], "message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, ] @@ -357,7 +360,7 @@ components: }, ], "message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -368,7 +371,7 @@ components: }, ], "message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, ] @@ -443,7 +446,7 @@ components: }, ], "message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -454,7 +457,7 @@ components: }, ], "message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -465,7 +468,7 @@ components: }, ], "message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, ] diff --git a/src/linting/rules/always-return-json-object.ts b/api-guidelines-redocly2-ruleset/src/rules/always-return-json-object.ts similarity index 84% rename from src/linting/rules/always-return-json-object.ts rename to api-guidelines-redocly2-ruleset/src/rules/always-return-json-object.ts index 625ce7a1..8f32360a 100644 --- a/src/linting/rules/always-return-json-object.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/always-return-json-object.ts @@ -1,11 +1,19 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; -import { isRef, type Oas3Schema } from "@redocly/openapi-core"; -import type { Location } from "@redocly/openapi-core/lib/ref-utils.d.js"; -import type { ResolveFn } from "@redocly/openapi-core/lib/walk.d.js"; +import { + isRef, + type Oas3Rule, + type Oas3Schema, + type Oas3_1Schema, + type Location, + type UserContext, +} from "@redocly/openapi-core"; import { isJsonContentType } from "./utils/isJsonContentType.js"; +type ResolveFn = UserContext["resolve"]; + +type Schema = Oas3Schema | Oas3_1Schema; + const findNonObjectLocations = ( - schema: Oas3Schema, + schema: Schema, location: Location, resolve: ResolveFn, ): Location[] => { diff --git a/api-guidelines-redocly2-ruleset/src/rules/define-permissions-with-scope.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/define-permissions-with-scope.spec.ts new file mode 100644 index 00000000..41351e1b --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/define-permissions-with-scope.spec.ts @@ -0,0 +1,207 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { DefinePermissionsWithScope } from "./define-permissions-with-scope.js"; + +describe("DefinePermissionsWithScope", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": DefinePermissionsWithScope, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 + +paths: + /post: + post: + security: + - clientCredentials: + - foo.read + +components: + securitySchemes: + clientCredentials: + type: oauth2 + flows: + clientCredentials: + tokenUrl: "/oauth2/token" + scopes: + foo.read: Read foo + + authorizationCode: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: "/oauth2/auth" + tokenUrl: "/oauth2/token" + scopes: + foo.read: Read foo +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark missing scopes", async () => { + const spec = ` +openapi: 3.0.3 + +components: + securitySchemes: + clientCredentials: + type: oauth2 + flows: + clientCredentials: + tokenUrl: "/oauth2/token" + + authorizationCode: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: "/oauth2/auth" + tokenUrl: "/oauth2/token" +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/components/securitySchemes", + "reportOnKey": true, + }, + ], + "message": "Must define a scope. See https://api.otto.de/portal/guidelines/r000047", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 + +paths: + /post: + post: + security: + - clientCredentials: + - foo.post + + /put: + put: + security: + - clientCredentials: + - foo.put + +components: + securitySchemes: + clientCredentials: + type: oauth2 + flows: + clientCredentials: + tokenUrl: "/oauth2/token" + scopes: + foo.read: Read foo + + authorizationCode: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: "/oauth2/auth" + tokenUrl: "/oauth2/token" + scopes: + foo.read: Read foo +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1post/post/security/0", + "reportOnKey": false, + }, + ], + "message": "Scope \\"foo.post\\" is not defined. See https://api.otto.de/portal/guidelines/r000047", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/paths/~1put/put/security/0", + "reportOnKey": false, + }, + ], + "message": "Scope \\"foo.put\\" is not defined. See https://api.otto.de/portal/guidelines/r000047", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); + + it("should handle missing flows", async () => { + const spec = ` +openapi: 3.0.3 + +components: + securitySchemes: + test: + type: http +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/components/securitySchemes", + "reportOnKey": true, + }, + ], + "message": "Must define a scope. See https://api.otto.de/portal/guidelines/r000047", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/define-permissions-with-scope.ts b/api-guidelines-redocly2-ruleset/src/rules/define-permissions-with-scope.ts similarity index 84% rename from src/linting/rules/define-permissions-with-scope.ts rename to api-guidelines-redocly2-ruleset/src/rules/define-permissions-with-scope.ts index a3f05060..fdb9bc6d 100644 --- a/src/linting/rules/define-permissions-with-scope.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/define-permissions-with-scope.ts @@ -1,5 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; -import type { Location } from "@redocly/openapi-core/lib/ref-utils.d.js"; +import type { Oas3Rule, Location } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r000047 @@ -9,8 +8,9 @@ export const DefinePermissionsWithScope: Oas3Rule = () => { const usedScopes: [string, Location][] = []; return { - SecurityScheme({ flows }) { - Object.values(flows ?? {}) + SecurityScheme(scheme) { + if (scheme.type !== "oauth2") return; + Object.values(scheme.flows ?? {}) .flatMap((flow) => Object.keys(flow.scopes ?? {})) .forEach((scope) => definedScopes.add(scope)); }, diff --git a/api-guidelines-redocly2-ruleset/src/rules/format-enumeration-upper-snake-case.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/format-enumeration-upper-snake-case.spec.ts new file mode 100644 index 00000000..ef3cea3f --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/format-enumeration-upper-snake-case.spec.ts @@ -0,0 +1,130 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { FormatEnumerationUpperSnakeCase } from "./format-enumeration-upper-snake-case.js"; + +describe("FormatEnumerationUpperSnakeCase", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": FormatEnumerationUpperSnakeCase, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + parameters: + - schema: + enum: + - FOO_BAR + - BAR_FOO + + responses: + 200: + content: + application/hal+json: + schema: + enum: + - 1 + - 2 +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark non upper snake case values", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + parameters: + - schema: + enum: + - FOO_BAR + - BAR_foo + - foo + + responses: + 200: + content: + application/hal+json: + schema: + enum: + - 1a + - 2B + - Bad + - GOOD +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1get/get/parameters/0/schema/enum/1", + "reportOnKey": false, + }, + ], + "message": "enum value \\"BAR_foo\\" is not upper snake case. See https://api.otto.de/portal/guidelines/r004090", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/paths/~1get/get/parameters/0/schema/enum/2", + "reportOnKey": false, + }, + ], + "message": "enum value \\"foo\\" is not upper snake case. See https://api.otto.de/portal/guidelines/r004090", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/enum/0", + "reportOnKey": false, + }, + ], + "message": "enum value \\"1a\\" is not upper snake case. See https://api.otto.de/portal/guidelines/r004090", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/enum/2", + "reportOnKey": false, + }, + ], + "message": "enum value \\"Bad\\" is not upper snake case. See https://api.otto.de/portal/guidelines/r004090", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/format-enumeration-upper-snake-case.ts b/api-guidelines-redocly2-ruleset/src/rules/format-enumeration-upper-snake-case.ts similarity index 91% rename from src/linting/rules/format-enumeration-upper-snake-case.ts rename to api-guidelines-redocly2-ruleset/src/rules/format-enumeration-upper-snake-case.ts index b48d7ad3..5faf0a09 100644 --- a/src/linting/rules/format-enumeration-upper-snake-case.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/format-enumeration-upper-snake-case.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; import { isUpperSnakeCase } from "./utils/isUpperSnakeCase.js"; /** diff --git a/api-guidelines-redocly2-ruleset/src/rules/no-request-body-in-get-method.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/no-request-body-in-get-method.spec.ts new file mode 100644 index 00000000..3e04efc9 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/no-request-body-in-get-method.spec.ts @@ -0,0 +1,126 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { NoRequestBodyInGetMethod } from "./no-request-body-in-get-method.js"; + +describe("NoRequestBodyInGetMethod", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": NoRequestBodyInGetMethod, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + + /post: + post: + requestBody: + content: {} + + /put: + put: + requestBody: + content: {} + + /delete: + delete: + requestBody: + content: {} +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark missing requestBody", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + requestBody: + content: {} + +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1get/get", + "reportOnKey": false, + }, + ], + "message": "Get method must not have a request body. See https://api.otto.de/portal/guidelines/r000007", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "use query parameters", + ], + }, + ] + `); + }); + + it("should mark missing requestBody in get only", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /foo: + get: + requestBody: + content: {} + + put: + requestBody: + content: {} +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1foo/get", + "reportOnKey": false, + }, + ], + "message": "Get method must not have a request body. See https://api.otto.de/portal/guidelines/r000007", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "use query parameters", + ], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/no-request-body-in-get-method.ts b/api-guidelines-redocly2-ruleset/src/rules/no-request-body-in-get-method.ts similarity index 87% rename from src/linting/rules/no-request-body-in-get-method.ts rename to api-guidelines-redocly2-ruleset/src/rules/no-request-body-in-get-method.ts index c10f3f2d..5f07371f 100644 --- a/src/linting/rules/no-request-body-in-get-method.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/no-request-body-in-get-method.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r000007 diff --git a/api-guidelines-redocly2-ruleset/src/rules/not-use-null-for-empty-array.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/not-use-null-for-empty-array.spec.ts new file mode 100644 index 00000000..7066fd54 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/not-use-null-for-empty-array.spec.ts @@ -0,0 +1,249 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { NotUseNullForEmptyArray } from "./not-use-null-for-empty-array.js"; + +describe("NotUseNullForEmptyArray", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": NotUseNullForEmptyArray, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + type: object + properties: + foo: + type: array + example: [] + bar: + type: object + properties: + baz: + type: array + example: [] + example: + baz: [] + example: + foo: [] + bar: + baz: [] + example: + foo: [] + bar: + baz: [] + examples: + default: + $ref: "#/components/examples/Foo" + +components: + examples: + Foo: + foo: [] + bar: + baz: [] +`; + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should not find any error when examples are missing", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + type: object + properties: + foo: + type: array + bar: + type: object + properties: + baz: + type: array +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark null", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + type: object + properties: + foo: + type: array + example: null + bar: + type: object + properties: + baz: + type: array + example: null + example: + baz: null + example: + foo: null + bar: + baz: null + example: + foo: null + bar: + baz: null + examples: + default: + $ref: "#/components/examples/Foo" + +components: + examples: + Foo: + foo: null + bar: + baz: null +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/example/foo", + "reportOnKey": false, + }, + ], + "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "change to []", + ], + }, + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/example/bar/baz", + "reportOnKey": false, + }, + ], + "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "change to []", + ], + }, + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/example/foo", + "reportOnKey": false, + }, + ], + "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "change to []", + ], + }, + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/example/bar/baz", + "reportOnKey": false, + }, + ], + "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "change to []", + ], + }, + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/properties/foo/example", + "reportOnKey": false, + }, + ], + "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "change to []", + ], + }, + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/properties/bar/example/baz", + "reportOnKey": false, + }, + ], + "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "change to []", + ], + }, + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/properties/bar/properties/baz/example", + "reportOnKey": false, + }, + ], + "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "change to []", + ], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/not-use-null-for-empty-array.ts b/api-guidelines-redocly2-ruleset/src/rules/not-use-null-for-empty-array.ts similarity index 83% rename from src/linting/rules/not-use-null-for-empty-array.ts rename to api-guidelines-redocly2-ruleset/src/rules/not-use-null-for-empty-array.ts index c7faca04..43e559c0 100644 --- a/src/linting/rules/not-use-null-for-empty-array.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/not-use-null-for-empty-array.ts @@ -1,16 +1,22 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; -import type { Oas3Schema } from "@redocly/openapi-core"; -import type { Location } from "@redocly/openapi-core/lib/ref-utils.d.js"; -import type { Problem } from "@redocly/openapi-core/lib/walk.d.js"; +import type { + Oas3Rule, + Oas3Schema, + Oas3_1Schema, + Location, + UserContext, +} from "@redocly/openapi-core"; + +type Problem = Parameters[0]; +type Schema = Oas3Schema | Oas3_1Schema; const findArrayNullExamples = ( - { type, properties, items }: Oas3Schema, + { type, properties, items }: Schema, example: unknown, location: Location, ): Location[] => { if (example === null && type === "array") return [location]; - if (Array.isArray(example) && items) { + if (Array.isArray(example) && items && typeof items === "object") { return example .map((value, index) => findArrayNullExamples(items, example[index], location.child(index))) .flat(); diff --git a/src/linting/rules/omit-optional-property.ts b/api-guidelines-redocly2-ruleset/src/rules/omit-optional-property.ts similarity index 76% rename from src/linting/rules/omit-optional-property.ts rename to api-guidelines-redocly2-ruleset/src/rules/omit-optional-property.ts index 67327c6f..0471b0db 100644 --- a/src/linting/rules/omit-optional-property.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/omit-optional-property.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r004021 diff --git a/src/linting/rules/operation-4xx-problem-details-rfc9457.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/operation-4xx-problem-details-rfc9457.spec.ts similarity index 53% rename from src/linting/rules/operation-4xx-problem-details-rfc9457.spec.ts rename to api-guidelines-redocly2-ruleset/src/rules/operation-4xx-problem-details-rfc9457.spec.ts index ef54b252..ebde24fa 100644 --- a/src/linting/rules/operation-4xx-problem-details-rfc9457.spec.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/operation-4xx-problem-details-rfc9457.spec.ts @@ -1,17 +1,21 @@ -import { lintFromString } from "@redocly/openapi-core"; +import { lintFromString, type Config } from "@redocly/openapi-core"; import { Operation4xxProblemDetailsRfc9457 } from "./operation-4xx-problem-details-rfc9457.js"; import { createTestConfig } from "./__tests__/createTestConfig.js"; import { removeClutter } from "./__tests__/removeClutter.js"; -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": Operation4xxProblemDetailsRfc9457, - }, -}); +describe("Operation4xxProblemDetailsRfc9457", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": Operation4xxProblemDetailsRfc9457, + }, + }); + }); -it("should not find any error", async () => { - const spec = ` + it("should not find any error", async () => { + const spec = ` openapi: "3.0.0" paths: /pets: @@ -32,18 +36,18 @@ paths: type: string `; - const result = await lintFromString({ - source: spec, - config, - }); + const result = await lintFromString({ + source: spec, + config, + }); - removeClutter(result); + removeClutter(result); - expect(result).toStrictEqual([]); -}); + expect(result).toStrictEqual([]); + }); -it("should not find any error when using $ref", async () => { - const spec = ` + it("should not find any error when using $ref", async () => { + const spec = ` openapi: "3.0.0" paths: /pets: @@ -68,18 +72,18 @@ components: type: string `; - const result = await lintFromString({ - source: spec, - config, - }); + const result = await lintFromString({ + source: spec, + config, + }); - removeClutter(result); + removeClutter(result); - expect(result).toStrictEqual([]); -}); + expect(result).toStrictEqual([]); + }); -it("should not find any error when using allOf", async () => { - const spec = ` + it("should not find any error when using allOf", async () => { + const spec = ` openapi: "3.0.0" paths: /pets: @@ -115,18 +119,18 @@ components: type: object `; - const result = await lintFromString({ - source: spec, - config, - }); + const result = await lintFromString({ + source: spec, + config, + }); - removeClutter(result); + removeClutter(result); - expect(result).toStrictEqual([]); -}); + expect(result).toStrictEqual([]); + }); -it("should mark wrong content type", async () => { - const spec = ` + it("should mark wrong content type", async () => { + const spec = ` openapi: "3.0.0" paths: /pets: @@ -147,32 +151,32 @@ paths: type: string `; - const result = await lintFromString({ - source: spec, - config, - }); + const result = await lintFromString({ + source: spec, + config, + }); - removeClutter(result); + removeClutter(result); - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1pets/get/responses/400", - "reportOnKey": true, - }, - ], - "message": "Response \`4xx\` must have content-type \`application/problem+json\`.", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1pets/get/responses/400", + "reportOnKey": true, + }, + ], + "message": "Response \`4xx\` must have content-type \`application/problem+json\`.", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); -it("should mark missing property 'title'", async () => { - const spec = ` + it("should mark missing property 'title'", async () => { + const spec = ` openapi: "3.0.0" paths: /pets: @@ -191,32 +195,32 @@ paths: type: string `; - const result = await lintFromString({ - source: spec, - config, - }); + const result = await lintFromString({ + source: spec, + config, + }); - removeClutter(result); + removeClutter(result); - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1pets/get/responses/400/content/application~1problem+json/schema/properties/title", - "reportOnKey": true, - }, - ], - "message": "SchemaProperties object should contain \`title\` field.", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1pets/get/responses/400/content/application~1problem+json/schema/properties/title", + "reportOnKey": true, + }, + ], + "message": "SchemaProperties object should contain \`title\` field.", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); -it("should mark missing property 'schema'", async () => { - const spec = ` + it("should mark missing property 'schema'", async () => { + const spec = ` openapi: "3.0.0" paths: /pets: @@ -231,26 +235,27 @@ paths: example: asd `; - const result = await lintFromString({ - source: spec, - config, - }); + const result = await lintFromString({ + source: spec, + config, + }); - removeClutter(result); + removeClutter(result); - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1pets/get/responses/400/content/application~1problem+json/schema", - "reportOnKey": true, - }, - ], - "message": "MediaType object should contain \`schema\` field.", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1pets/get/responses/400/content/application~1problem+json/schema", + "reportOnKey": true, + }, + ], + "message": "MediaType object should contain \`schema\` field.", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); }); diff --git a/src/linting/rules/operation-4xx-problem-details-rfc9457.ts b/api-guidelines-redocly2-ruleset/src/rules/operation-4xx-problem-details-rfc9457.ts similarity index 63% rename from src/linting/rules/operation-4xx-problem-details-rfc9457.ts rename to api-guidelines-redocly2-ruleset/src/rules/operation-4xx-problem-details-rfc9457.ts index b2586651..9de98bb1 100644 --- a/src/linting/rules/operation-4xx-problem-details-rfc9457.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/operation-4xx-problem-details-rfc9457.ts @@ -8,9 +8,26 @@ Open Issue: https://github.com/Redocly/redocly-cli/issues/932 */ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; -// @ts-ignore -import { validateDefinedAndNonEmpty } from "@redocly/openapi-core/lib/rules/utils"; +import type { Oas3Rule, UserContext } from "@redocly/openapi-core"; + +// Inlined from @redocly/openapi-core/lib/rules/utils.js (not exposed in v2 package exports) +function validateDefinedAndNonEmpty(fieldName: string, value: unknown, ctx: UserContext): void { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return; + } + const obj = value as Record; + if (obj[fieldName] === undefined) { + ctx.report({ + message: `${ctx.type.name} object should contain \`${fieldName}\` field.`, + location: ctx.location.child([fieldName]).key(), + }); + } else if (!obj[fieldName]) { + ctx.report({ + message: `${ctx.type.name} object \`${fieldName}\` must be non-empty string.`, + location: ctx.location.child([fieldName]).key(), + }); + } +} /** * Validation according rfc9457 - https://datatracker.ietf.org/doc/html/rfc9457 diff --git a/api-guidelines-redocly2-ruleset/src/rules/secure-endpoints-with-oauth-2.0.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/secure-endpoints-with-oauth-2.0.spec.ts new file mode 100644 index 00000000..95fb027a --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/secure-endpoints-with-oauth-2.0.spec.ts @@ -0,0 +1,141 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { SecureEndpointsWithOAuth20 } from "./secure-endpoints-with-oauth-2.0.js"; + +describe("SecureEndpointsWithOAuth20", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": SecureEndpointsWithOAuth20, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 +components: + securitySchemes: + foo: + type: oauth2 + + bar: + type: oauth2 + +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark wrong type", async () => { + const spec = ` +openapi: 3.0.3 +components: + securitySchemes: + foo: + type: http + + bar: + type: openIdConnect + + baz: + type: apiKey + +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/components/securitySchemes/foo/type", + "reportOnKey": false, + }, + ], + "message": "Type is not OAuth 2.0. See https://api.otto.de/portal/guidelines/r000051", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "type: oauth2", + ], + }, + { + "location": [ + { + "pointer": "#/components/securitySchemes/bar/type", + "reportOnKey": false, + }, + ], + "message": "Type is not OAuth 2.0. See https://api.otto.de/portal/guidelines/r000051", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "type: oauth2", + ], + }, + { + "location": [ + { + "pointer": "#/components/securitySchemes/baz/type", + "reportOnKey": false, + }, + ], + "message": "Type is not OAuth 2.0. See https://api.otto.de/portal/guidelines/r000051", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "type: oauth2", + ], + }, + ] + `); + }); + + it("should mark missing type", async () => { + const spec = ` +openapi: 3.0.3 +components: + securitySchemes: + foo: {} +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/components/securitySchemes/foo/type", + "reportOnKey": false, + }, + ], + "message": "Type is not OAuth 2.0. See https://api.otto.de/portal/guidelines/r000051", + "ruleId": "test-plugin/test-rule", + "suggest": [ + "type: oauth2", + ], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/secure-endpoints-with-oauth-2.0.ts b/api-guidelines-redocly2-ruleset/src/rules/secure-endpoints-with-oauth-2.0.ts similarity index 85% rename from src/linting/rules/secure-endpoints-with-oauth-2.0.ts rename to api-guidelines-redocly2-ruleset/src/rules/secure-endpoints-with-oauth-2.0.ts index 3e3df7e0..e25f8652 100644 --- a/src/linting/rules/secure-endpoints-with-oauth-2.0.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/secure-endpoints-with-oauth-2.0.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r000051 diff --git a/src/linting/rules/use-absolute-custom-link-relation-url.ts b/api-guidelines-redocly2-ruleset/src/rules/use-absolute-custom-link-relation-url.ts similarity index 69% rename from src/linting/rules/use-absolute-custom-link-relation-url.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-absolute-custom-link-relation-url.ts index e92289d4..cf2a9fd3 100644 --- a/src/linting/rules/use-absolute-custom-link-relation-url.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-absolute-custom-link-relation-url.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r100037 diff --git a/api-guidelines-redocly2-ruleset/src/rules/use-absolute-profile-url.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/use-absolute-profile-url.spec.ts new file mode 100644 index 00000000..11cdc4e1 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/use-absolute-profile-url.spec.ts @@ -0,0 +1,99 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { UseAbsoluteProfileUrl } from "./use-absolute-profile-url.js"; + +describe("UseAbsoluteProfileUrl", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": UseAbsoluteProfileUrl, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /post: + post: + requestBody: + content: + application/hal+json: {} + application/hal+json;profile=https://example.com/profiles/foo+v1: {} + application/hal+json;profile="https://example.com/profiles/foo+v2": {} + + responses: + 200: + content: + application/hal+json: {} + application/hal+json;profile="https://example.com/profiles/foo+v1";foo=bar: {} + application/hal+json;PROFILE=https://example.com/profiles/foo+v2: {} + +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark relative profiles", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /post: + post: + requestBody: + content: + application/hal+json;profile="/profiles/foo+v1": {} + + responses: + 200: + content: + application/hal+json;profile="/profiles/foo+v1": {} + +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1post/post/requestBody/content/0", + "reportOnKey": true, + }, + ], + "message": "Profile url is not absolute. See https://api.otto.de/portal/guidelines/r100066", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/paths/~1post/post/responses/200/content/0", + "reportOnKey": true, + }, + ], + "message": "Profile url is not absolute. See https://api.otto.de/portal/guidelines/r100066", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/use-absolute-profile-url.ts b/api-guidelines-redocly2-ruleset/src/rules/use-absolute-profile-url.ts similarity index 91% rename from src/linting/rules/use-absolute-profile-url.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-absolute-profile-url.ts index 538a6ff6..6f85ebd5 100644 --- a/src/linting/rules/use-absolute-profile-url.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-absolute-profile-url.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; import { isAbsoluteURI } from "./utils/isAbsoluteURI.js"; /** diff --git a/api-guidelines-redocly2-ruleset/src/rules/use-authorization-grant.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/use-authorization-grant.spec.ts new file mode 100644 index 00000000..79b8ac29 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/use-authorization-grant.spec.ts @@ -0,0 +1,133 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { UseAuthorizationGrant } from "./use-authorization-grant.js"; + +describe("UseAuthorizationGrant", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": UseAuthorizationGrant, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 +components: + securitySchemes: + clientCredentials: + type: oauth2 + flows: + clientCredentials: + tokenUrl: "/oauth2/token" + scopes: + foo.read: Read foo + + authorizationCode: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: "/oauth2/auth" + tokenUrl: "/oauth2/token" + scopes: + foo.read: Read foo +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark wrong flow", async () => { + const spec = ` +openapi: 3.0.3 +components: + securitySchemes: + clientCredentials: + type: oauth2 + flows: + implicit: + + authorizationCode: + type: oauth2 + flows: + password: +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/components/securitySchemes/clientCredentials", + "reportOnKey": false, + }, + ], + "message": "Must use Authorization Grant. See https://api.otto.de/portal/guidelines/r000052", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/components/securitySchemes/authorizationCode", + "reportOnKey": false, + }, + ], + "message": "Must use Authorization Grant. See https://api.otto.de/portal/guidelines/r000052", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); + + it("should handle missing flows", async () => { + const spec = ` +openapi: 3.0.3 +components: + securitySchemes: + test: + type: http +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/components/securitySchemes/test", + "reportOnKey": false, + }, + ], + "message": "Must use Authorization Grant. See https://api.otto.de/portal/guidelines/r000052", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/use-authorization-grant.ts b/api-guidelines-redocly2-ruleset/src/rules/use-authorization-grant.ts similarity index 76% rename from src/linting/rules/use-authorization-grant.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-authorization-grant.ts index 4998e053..bb52356a 100644 --- a/src/linting/rules/use-authorization-grant.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-authorization-grant.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r000052 @@ -16,7 +16,8 @@ export const UseAuthorizationGrant: Oas3Rule = () => { } }, - SecurityScheme({ flows }, { report, location }) { + SecurityScheme(scheme, { report, location }) { + const flows = "flows" in scheme ? scheme.flows : undefined; if (!flows?.clientCredentials && !flows?.authorizationCode) { report({ message, diff --git a/api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-property-name.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-property-name.spec.ts new file mode 100644 index 00000000..6e8d1ad4 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-property-name.spec.ts @@ -0,0 +1,283 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { UseCamelCaseForPropertyName } from "./use-camel-case-for-property-name.js"; + +describe("UseCamelCaseForPropertyName", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": UseCamelCaseForPropertyName, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /post: + post: + parameters: + - schema: + type: object + properties: + name: + type: string + jobDescription: + type: string + houses: + type: array + items: + type: object + properties: + fooBar: string + requestBody: + content: + application/hal+json: + schema: + type: object + properties: + name: + type: string + jobDescription: + type: string + houses: + type: array + items: + type: object + properties: + fooBar: string + responses: + 200: + content: + application/hal+json: + schema: + type: object + properties: + name: + type: string + jobDescription: + type: string + houses: + type: array + items: + type: object + properties: + fooBar: string +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should not find any error in valid components", async () => { + const spec = ` +openapi: 3.0.3 + +paths: + /post: + post: + responses: + 200: + content: + application/hal+json: + schema: + allOf: + - $ref: "#/components/schemas/PromoImageHal" + - title: foo + +components: + schemas: + PromoImageHal: + properties: + type: + type: string + _links: + allOf: + - title: foo + - description: bar + - properties: + foo: + type: string +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should not find any error in CURIE", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /post: + post: + responses: + 200: + content: + application/hal+json: + schema: + type: object + properties: + _links: + description: Links section. + type: object + properties: + o:bar: + type: array + _embedded: + type: object + description: Embedded resources + properties: + o:foo: + type: array +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark invalid property names", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /post: + post: + + requestBody: + content: + application/json: + schema: + type: object + properties: + NAME: + type: string + job-description: + type: string + houses: + type: array + items: + type: object + properties: + foo_bar: + type: string + + responses: + 200: + content: + application/hal+json: + schema: + type: object + properties: + NAME: + type: string + job-description: + type: string + houses: + type: array + items: + type: object + properties: + foo_bar: + type: string +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1post/post/requestBody/content/application~1json/schema/properties/NAME", + "reportOnKey": true, + }, + ], + "message": "Property \\"NAME\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/paths/~1post/post/requestBody/content/application~1json/schema/properties/job-description", + "reportOnKey": true, + }, + ], + "message": "Property \\"job-description\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/paths/~1post/post/requestBody/content/application~1json/schema/properties/houses/items/properties/foo_bar", + "reportOnKey": true, + }, + ], + "message": "Property \\"foo_bar\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/paths/~1post/post/responses/200/content/application~1hal+json/schema/properties/NAME", + "reportOnKey": true, + }, + ], + "message": "Property \\"NAME\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/paths/~1post/post/responses/200/content/application~1hal+json/schema/properties/job-description", + "reportOnKey": true, + }, + ], + "message": "Property \\"job-description\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/paths/~1post/post/responses/200/content/application~1hal+json/schema/properties/houses/items/properties/foo_bar", + "reportOnKey": true, + }, + ], + "message": "Property \\"foo_bar\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/use-camel-case-for-property-name.ts b/api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-property-name.ts similarity index 94% rename from src/linting/rules/use-camel-case-for-property-name.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-property-name.ts index 5e0b9e35..37da06e8 100644 --- a/src/linting/rules/use-camel-case-for-property-name.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-property-name.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; import { isCamelCase } from "./utils/isCamelCase.js"; /** diff --git a/api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-query-parameter.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-query-parameter.spec.ts new file mode 100644 index 00000000..cf55513e --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-query-parameter.spec.ts @@ -0,0 +1,87 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { UseCamelCaseForQueryParameter } from "./use-camel-case-for-query-parameter.js"; + +describe("UseCamelCaseForQueryParameter", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": UseCamelCaseForQueryParameter, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + parameters: + - in: query + name: fooBar + - in: query + name: barFoo +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark non upper snake case values", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + parameters: + - in: query + name: foo-bar + - in: query + name: FOO_BAR +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1get/get/parameters/0/name", + "reportOnKey": false, + }, + ], + "message": "Query parameter is not in camel case.", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/paths/~1get/get/parameters/1/name", + "reportOnKey": false, + }, + ], + "message": "Query parameter is not in camel case.", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/use-camel-case-for-query-parameter.ts b/api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-query-parameter.ts similarity index 87% rename from src/linting/rules/use-camel-case-for-query-parameter.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-query-parameter.ts index c6c6d478..e06aee47 100644 --- a/src/linting/rules/use-camel-case-for-query-parameter.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-camel-case-for-query-parameter.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; import { isCamelCase } from "./utils/isCamelCase.js"; /** diff --git a/src/linting/rules/use-common-date-and-time-format.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/use-common-date-and-time-format.spec.ts similarity index 92% rename from src/linting/rules/use-common-date-and-time-format.spec.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-common-date-and-time-format.spec.ts index c2c1b1a0..6f3d6f45 100644 --- a/src/linting/rules/use-common-date-and-time-format.spec.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-common-date-and-time-format.spec.ts @@ -1,13 +1,16 @@ -import { lintFromString } from "@redocly/openapi-core"; +import { lintFromString, type Config } from "@redocly/openapi-core"; import { createTestConfig } from "./__tests__/createTestConfig.js"; import { removeClutter } from "./__tests__/removeClutter.js"; import { UseCommonDateAndTimeFormat } from "./use-common-date-and-time-format.js"; -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": UseCommonDateAndTimeFormat, - }, +let config: Config; +beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": UseCommonDateAndTimeFormat, + }, + }); }); it("should not find any error with format date", async () => { @@ -183,7 +186,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -194,7 +197,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -205,7 +208,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -216,7 +219,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -227,7 +230,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -238,7 +241,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -249,7 +252,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, ] @@ -319,7 +322,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -330,7 +333,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -341,7 +344,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -352,7 +355,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -363,7 +366,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -374,7 +377,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, { @@ -385,7 +388,7 @@ components: }, ], "message": "Invalid date or date-time format. See https://api.otto.de/portal/guidelines/r100072", - "ruleId": "test-rule", + "ruleId": "test-plugin/test-rule", "suggest": [], }, ] diff --git a/src/linting/rules/use-common-date-and-time-format.ts b/api-guidelines-redocly2-ruleset/src/rules/use-common-date-and-time-format.ts similarity index 85% rename from src/linting/rules/use-common-date-and-time-format.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-common-date-and-time-format.ts index c21f431b..15a0a8f1 100644 --- a/src/linting/rules/use-common-date-and-time-format.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-common-date-and-time-format.ts @@ -1,12 +1,18 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; -import type { Oas3Schema } from "@redocly/openapi-core"; -import type { Location } from "@redocly/openapi-core/lib/ref-utils.d.js"; -import type { Problem } from "@redocly/openapi-core/lib/walk.d.js"; +import type { + Oas3Rule, + Oas3Schema, + Oas3_1Schema, + Location, + UserContext, +} from "@redocly/openapi-core"; import { isValidDateFormat } from "./utils/isValidDateFormat.js"; import { isValidDateTimeFormat } from "./utils/isValidDateTimeFormat.js"; +type Problem = Parameters[0]; +type Schema = Oas3Schema | Oas3_1Schema; + const findInvalidDateExamples = ( - { format, properties, items }: Oas3Schema, + { format, properties, items }: Schema, example: unknown, location: Location, ): Location[] => { @@ -19,7 +25,7 @@ const findInvalidDateExamples = ( } } - if (Array.isArray(example) && items) { + if (Array.isArray(example) && items && typeof items === "object") { return example .map((value, index) => findInvalidDateExamples(items, example[index], location.child(index))) .flat(); @@ -35,7 +41,7 @@ const findInvalidDateExamples = ( }; function findAndReport( - schema: Oas3Schema, + schema: Schema, example: unknown, location: Location, report: (problem: Problem) => void, diff --git a/src/linting/rules/use-curied-link-relation-types.ts b/api-guidelines-redocly2-ruleset/src/rules/use-curied-link-relation-types.ts similarity index 78% rename from src/linting/rules/use-curied-link-relation-types.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-curied-link-relation-types.ts index c158c8ec..f21f1b42 100644 --- a/src/linting/rules/use-curied-link-relation-types.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-curied-link-relation-types.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r100038 diff --git a/api-guidelines-redocly2-ruleset/src/rules/use-extensible-enum.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/use-extensible-enum.spec.ts new file mode 100644 index 00000000..5e4c7501 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/use-extensible-enum.spec.ts @@ -0,0 +1,269 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { UseExtensibleEnum } from "./use-extensible-enum.js"; + +describe("UseExtensibleEnum", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": UseExtensibleEnum, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + parameters: + - name: aaa + schema: &schema + type: string + x-extensible-enum: + - value: Foo + description: Credit card payment + - value: Bar + description: Payment by the customer by bank transfer. + deprecated: true + - value: DIRECT_DEBIT + description: Direct debit from a bank account. + preview: true + + responses: + 200: + content: + application/hal+json: + schema: *schema +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark enum and x-extensible-enum clash", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + type: string + enum: + - 1 + - 2 + x-extensible-enum: + - value: foo + description: foooo + - value: bar + description: barrrrr +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/enum", + "reportOnKey": false, + }, + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/x-extensible-enum", + "reportOnKey": false, + }, + ], + "message": "Do not use the enum keyword in combination with x-extensible-enum. See https://api.otto.de/portal/guidelines/r000035", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); + + it("should mark type number", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + type: number + x-extensible-enum: + - value: foo + description: foooo +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/type", + "reportOnKey": false, + }, + ], + "message": "Extensible enum is not represented as string. See https://api.otto.de/portal/guidelines/r000035", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); + + it("should mark non boolean deprecated", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + type: string + x-extensible-enum: + - value: foo + description: foooo + deprecated: "non boolean" +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/x-extensible-enum/0/deprecated", + "reportOnKey": false, + }, + ], + "message": "deprecated is not boolean. See https://api.otto.de/portal/guidelines/r000035", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); + + it("should mark non boolean preview", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + type: string + x-extensible-enum: + - value: foo + description: foooo + preview: "non boolean" +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/x-extensible-enum/0/preview", + "reportOnKey": false, + }, + ], + "message": "preview is not boolean. See https://api.otto.de/portal/guidelines/r000035", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); + + it("should mark non array", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + type: string + x-extensible-enum: + value: foo + description: foooo +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/x-extensible-enum", + "reportOnKey": false, + }, + ], + "message": "x-extensible-enum is not an array. See https://api.otto.de/portal/guidelines/r000035", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/use-extensible-enum.ts b/api-guidelines-redocly2-ruleset/src/rules/use-extensible-enum.ts similarity index 96% rename from src/linting/rules/use-extensible-enum.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-extensible-enum.ts index 8a7bcfa8..c52c3ab7 100644 --- a/src/linting/rules/use-extensible-enum.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-extensible-enum.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r000035 diff --git a/src/linting/rules/use-kebab-case-for-path-parameter.ts b/api-guidelines-redocly2-ruleset/src/rules/use-kebab-case-for-path-parameter.ts similarity index 87% rename from src/linting/rules/use-kebab-case-for-path-parameter.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-kebab-case-for-path-parameter.ts index 2a69f1a5..9e3819e0 100644 --- a/src/linting/rules/use-kebab-case-for-path-parameter.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-kebab-case-for-path-parameter.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; import { isKebabCase } from "./utils/isKebabCase.js"; /** diff --git a/src/linting/rules/use-kebab-case-in-uri.ts b/api-guidelines-redocly2-ruleset/src/rules/use-kebab-case-in-uri.ts similarity index 67% rename from src/linting/rules/use-kebab-case-in-uri.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-kebab-case-in-uri.ts index bc421172..a0358e1b 100644 --- a/src/linting/rules/use-kebab-case-in-uri.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-kebab-case-in-uri.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r000023 diff --git a/api-guidelines-redocly2-ruleset/src/rules/use-oas-3.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/use-oas-3.spec.ts new file mode 100644 index 00000000..9c753543 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/use-oas-3.spec.ts @@ -0,0 +1,60 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { UseOas3 } from "./use-oas-3.js"; + +describe("UseOas3", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas2: { + // @ts-ignore + "test-rule": UseOas3, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.0 +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark oas 2", async () => { + const spec = ` +swagger: '2.0' +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/swagger", + "reportOnKey": true, + }, + ], + "message": "openapi >= 3.0.0 should be used.", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/use-oas-3.ts b/api-guidelines-redocly2-ruleset/src/rules/use-oas-3.ts similarity index 80% rename from src/linting/rules/use-oas-3.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-oas-3.ts index ffa6b778..5523d878 100644 --- a/src/linting/rules/use-oas-3.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-oas-3.ts @@ -1,4 +1,4 @@ -import type { Oas2Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas2Rule } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r000003 diff --git a/api-guidelines-redocly2-ruleset/src/rules/use-string-enum.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/use-string-enum.spec.ts new file mode 100644 index 00000000..4736d71a --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/use-string-enum.spec.ts @@ -0,0 +1,164 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { UseStringEnum } from "./use-string-enum.js"; + +describe("UseStringEnum", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": UseStringEnum, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + parameters: + - name: aaa + schema: + type: string + enum: + - foo + - bar + + responses: + 200: + content: + application/hal+json: + schema: + type: string + properties: + foo: + type: array + bar: + type: object + status: + type: number + enum: [404] +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark type number and integer", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + type: number + enum: + - 1 + - 2 + +components: + schemas: + Foo: + type: object + properties: + foo: + type: integer + enum: + - 3 + - 4 + bar: + type: string + enum: + - BAR1 + - BAR2 +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/type", + "reportOnKey": false, + }, + ], + "message": "Enumeration is not represented as string. See https://api.otto.de/portal/guidelines/r004080", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/components/schemas/Foo/properties/foo/type", + "reportOnKey": false, + }, + ], + "message": "Enumeration is not represented as string. See https://api.otto.de/portal/guidelines/r004080", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); + + it("should mark missing type", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + enum: + - FOO + - BAR +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/type", + "reportOnKey": false, + }, + ], + "message": "Enumeration is not represented as string. See https://api.otto.de/portal/guidelines/r004080", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/use-string-enum.ts b/api-guidelines-redocly2-ruleset/src/rules/use-string-enum.ts similarity index 88% rename from src/linting/rules/use-string-enum.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-string-enum.ts index 042982bd..1f2c3c1f 100644 --- a/src/linting/rules/use-string-enum.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-string-enum.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r004080 diff --git a/api-guidelines-redocly2-ruleset/src/rules/use-tls.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/use-tls.spec.ts new file mode 100644 index 00000000..e6ee3197 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/use-tls.spec.ts @@ -0,0 +1,65 @@ +import { lintFromString, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "./__tests__/createTestConfig.js"; +import { removeClutter } from "./__tests__/removeClutter.js"; +import { UseTLS } from "./use-tls.js"; + +describe("UseTLS", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": UseTLS, + }, + }); + }); + + it("should not find any error", async () => { + const spec = ` +openapi: 3.0.3 +servers: + - url: https://example.org +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toStrictEqual([]); + }); + + it("should mark second server url", async () => { + const spec = ` +openapi: 3.0.3 +servers: + - url: https://example.org + - url: http://example.org +`; + + const result = await lintFromString({ + source: spec, + config, + }); + + removeClutter(result); + + expect(result).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/servers/1/url", + "reportOnKey": false, + }, + ], + "message": "Server url is not secured with TLS. See https://api.otto.de/portal/guidelines/r000046", + "ruleId": "test-plugin/test-rule", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/src/linting/rules/use-tls.ts b/api-guidelines-redocly2-ruleset/src/rules/use-tls.ts similarity index 85% rename from src/linting/rules/use-tls.ts rename to api-guidelines-redocly2-ruleset/src/rules/use-tls.ts index e7f10e1b..61daa457 100644 --- a/src/linting/rules/use-tls.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/use-tls.ts @@ -1,4 +1,4 @@ -import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js"; +import type { Oas3Rule } from "@redocly/openapi-core"; /** * @see https://api.otto.de/portal/guidelines/r000046 diff --git a/src/linting/rules/utils/isAbsoluteURI.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isAbsoluteURI.spec.ts similarity index 100% rename from src/linting/rules/utils/isAbsoluteURI.spec.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isAbsoluteURI.spec.ts diff --git a/src/linting/rules/utils/isAbsoluteURI.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isAbsoluteURI.ts similarity index 100% rename from src/linting/rules/utils/isAbsoluteURI.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isAbsoluteURI.ts diff --git a/src/linting/rules/utils/isCamelCase.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isCamelCase.spec.ts similarity index 100% rename from src/linting/rules/utils/isCamelCase.spec.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isCamelCase.spec.ts diff --git a/src/linting/rules/utils/isCamelCase.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isCamelCase.ts similarity index 100% rename from src/linting/rules/utils/isCamelCase.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isCamelCase.ts diff --git a/src/linting/rules/utils/isJsonContentType.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isJsonContentType.spec.ts similarity index 100% rename from src/linting/rules/utils/isJsonContentType.spec.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isJsonContentType.spec.ts diff --git a/src/linting/rules/utils/isJsonContentType.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isJsonContentType.ts similarity index 100% rename from src/linting/rules/utils/isJsonContentType.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isJsonContentType.ts diff --git a/src/linting/rules/utils/isKebabCase.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isKebabCase.spec.ts similarity index 100% rename from src/linting/rules/utils/isKebabCase.spec.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isKebabCase.spec.ts diff --git a/src/linting/rules/utils/isKebabCase.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isKebabCase.ts similarity index 100% rename from src/linting/rules/utils/isKebabCase.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isKebabCase.ts diff --git a/src/linting/rules/utils/isUpperSnakeCase.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isUpperSnakeCase.spec.ts similarity index 100% rename from src/linting/rules/utils/isUpperSnakeCase.spec.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isUpperSnakeCase.spec.ts diff --git a/src/linting/rules/utils/isUpperSnakeCase.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isUpperSnakeCase.ts similarity index 100% rename from src/linting/rules/utils/isUpperSnakeCase.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isUpperSnakeCase.ts diff --git a/src/linting/rules/utils/isValidDateFormat.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isValidDateFormat.spec.ts similarity index 100% rename from src/linting/rules/utils/isValidDateFormat.spec.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isValidDateFormat.spec.ts diff --git a/src/linting/rules/utils/isValidDateFormat.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isValidDateFormat.ts similarity index 100% rename from src/linting/rules/utils/isValidDateFormat.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isValidDateFormat.ts diff --git a/src/linting/rules/utils/isValidDateTimeFormat.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isValidDateTimeFormat.spec.ts similarity index 100% rename from src/linting/rules/utils/isValidDateTimeFormat.spec.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isValidDateTimeFormat.spec.ts diff --git a/src/linting/rules/utils/isValidDateTimeFormat.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/isValidDateTimeFormat.ts similarity index 100% rename from src/linting/rules/utils/isValidDateTimeFormat.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/isValidDateTimeFormat.ts diff --git a/api-guidelines-redocly2-ruleset/src/rules/utils/resolveRecursive.spec.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/resolveRecursive.spec.ts new file mode 100644 index 00000000..b4843ba6 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/src/rules/utils/resolveRecursive.spec.ts @@ -0,0 +1,113 @@ +import { lintFromString, type OasRef, type Config } from "@redocly/openapi-core"; +import { createTestConfig } from "../__tests__/createTestConfig.js"; +import { resolveRecursive } from "./resolveRecursive.js"; + +describe("resolveRecursive", () => { + let config: Config; + beforeAll(async () => { + config = await createTestConfig({ + oas3: { + // @ts-ignore + "test-rule": () => { + return { + Schema(schema, { report, resolve }) { + const result = resolveRecursive(schema as OasRef, resolve); + report({ + message: `resolves to ${result.location.absolutePointer}`, + }); + }, + }; + }, + }, + }); + }); + + it("should resolve nothing", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + type: object + properties: + foo: + type: array + bar: + type: object + baz: + type: string +`; + + const [{ location }] = await lintFromString({ + source: spec, + config, + }); + + expect(location[0].pointer).toBe( + "#/paths/~1get/get/responses/200/content/application~1hal+json/schema", + ); + }); + + it("should resolve one ref", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + $ref: "#/components/schemas/Foo" + +components: + schemas: + Foo: + type: string +`; + + const [{ location }] = await lintFromString({ + source: spec, + config, + }); + + expect(location[0].pointer).toBe("#/components/schemas/Foo"); + }); + + it("should resolve three refs", async () => { + const spec = ` +openapi: 3.0.3 +paths: + /get: + get: + responses: + 200: + content: + application/hal+json: + schema: + $ref: "#/components/schemas/Foo" + +components: + schemas: + Foo: + $ref: "#/components/schemas/Bar" + Bar: + $ref: "#/components/schemas/Baz" + Baz: + type: string +`; + + const [{ location }] = await lintFromString({ + source: spec, + config, + }); + + expect(location[0].pointer).toBe("#/components/schemas/Baz"); + }); +}); diff --git a/src/linting/rules/utils/resolveRecursive.ts b/api-guidelines-redocly2-ruleset/src/rules/utils/resolveRecursive.ts similarity index 67% rename from src/linting/rules/utils/resolveRecursive.ts rename to api-guidelines-redocly2-ruleset/src/rules/utils/resolveRecursive.ts index a96f30a4..4e7f6c24 100644 --- a/src/linting/rules/utils/resolveRecursive.ts +++ b/api-guidelines-redocly2-ruleset/src/rules/utils/resolveRecursive.ts @@ -1,5 +1,7 @@ -import type { OasRef } from "@redocly/openapi-core"; -import type { ResolveFn, ResolveResult } from "@redocly/openapi-core/lib/walk.d.js"; +import type { OasRef, UserContext } from "@redocly/openapi-core"; + +type ResolveFn = UserContext["resolve"]; +type ResolveResult = ReturnType & { node: T }; export const resolveRecursive = ( node: T, diff --git a/api-guidelines-redocly2-ruleset/tsconfig.json b/api-guidelines-redocly2-ruleset/tsconfig.json new file mode 100644 index 00000000..c1314973 --- /dev/null +++ b/api-guidelines-redocly2-ruleset/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "baseUrl": "./src", + "skipLibCheck": true, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "allowJs": true, + "types": ["vitest/globals"] + }, + "include": ["./src"] +} diff --git a/api-guidelines-redocly2-ruleset/vitest.config.ts b/api-guidelines-redocly2-ruleset/vitest.config.ts new file mode 100644 index 00000000..8ac76dfe --- /dev/null +++ b/api-guidelines-redocly2-ruleset/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.spec.ts"], + reporters: ["verbose"], + globals: true, + }, +}); diff --git a/package-lock.json b/package-lock.json index 464e082a..1acb26b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,13 @@ "name": "@otto-de/api-guidelines", "version": "1.0.1", "license": "ISC", + "workspaces": [ + "api-guidelines-redocly2-ruleset" + ], "devDependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.0", - "@redocly/openapi-core": "^1.4.1", "@types/node": "^20.9.4", - "esbuild": "^0.19.7", "eslint": "^8.54.0", "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-prettier": "^9.0.0", @@ -30,6 +31,37 @@ "node": ">=16" } }, + "api-guidelines-linter-ruleset-redocly2": { + "name": "@otto-de/api-guidelines-linter-ruleset-redocly2", + "version": "0.1.19", + "extraneous": true, + "license": "ISC", + "devDependencies": { + "@redocly/openapi-core": "^2.25.4", + "@types/node": "^20.9.4", + "esbuild": "^0.19.7", + "typescript": "^5.3.2", + "vitest": "^0.34.6" + }, + "engines": { + "node": ">=16" + } + }, + "api-guidelines-redocly2-ruleset": { + "name": "@otto-de/api-guidelines-redocly2-ruleset", + "version": "1.0.1", + "license": "ISC", + "devDependencies": { + "@redocly/openapi-core": "^2.25.4", + "@types/node": "^20.9.4", + "esbuild": "^0.19.7", + "typescript": "^5.3.2", + "vitest": "^0.34.6" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -71,6 +103,16 @@ "undici": "^5.25.4" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.19.7", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.7.tgz", @@ -746,6 +788,10 @@ "@octokit/openapi-types": "^19.0.2" } }, + "node_modules/@otto-de/api-guidelines-redocly2-ruleset": { + "resolved": "api-guidelines-redocly2-ruleset", + "link": true + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -777,15 +823,16 @@ } }, "node_modules/@redocly/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-9GWx27t7xWhDIR02PA18nzBdLcKQRgc46xNQvjFkrYk4UOmvKhJ/dawwiX0cCOeetN5LcaaiqQbVOWYK62SGHw==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.3.tgz", + "integrity": "sha512-l42u0of3hY98sN2A+M4qTX1O/KrpgGH32Hu9kP2GtHyD5Dfqq86PKFLe5dwaD8DEnNmlOlll2BAmeEtf0DaySg==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -796,43 +843,58 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.46.1.tgz", + "integrity": "sha512-dSdkB2wRLtvl3f7ayRu9vqVhUMjjRaxZlHgRbgOtPPXxn4uI/ciDO87h4CJb7Iet+OVpevpAU6gU8bo5qVbQxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "2.7.2" + } }, "node_modules/@redocly/openapi-core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.4.1.tgz", - "integrity": "sha512-oAhnG8MKocM9LuP++NGFxdniNKWSLA7hzHPQoOK92LIP/DdvXx8pEeZ68UTNxIXhKonoUcO6s86I3L0zj143zg==", + "version": "2.25.4", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-2.25.4.tgz", + "integrity": "sha512-zYdKQEsowPNtkTixrfbn5DySWBLQpTsISthVBBEPAa3OZC75UI76CbHXEamJ8Kmlead9IkD5RbgeJvxqJ5/H6Q==", "dev": true, + "license": "MIT", "dependencies": { - "@redocly/ajv": "^8.11.0", - "@types/node": "^14.11.8", + "@redocly/ajv": "^8.18.0", + "@redocly/config": "^0.46.0", + "ajv": "npm:@redocly/ajv@8.18.0", + "ajv-formats": "^3.0.1", "colorette": "^1.2.0", "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", - "lodash.isequal": "^4.5.0", - "minimatch": "^5.0.1", - "node-fetch": "^2.6.1", + "picomatch": "^4.0.4", "pluralize": "^8.0.0", "yaml-ast-parser": "0.0.43" }, "engines": { - "node": ">=14.19.0", - "npm": ">=7.0.0" + "node": ">=22.12.0 || >=20.19.0 <21.0.0", + "npm": ">=10" } }, - "node_modules/@redocly/openapi-core/node_modules/@types/node": { - "version": "14.18.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.42.tgz", - "integrity": "sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg==", - "dev": true - }, - "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@redocly/openapi-core/node_modules/ajv": { + "name": "@redocly/ajv", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA==", "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/@redocly/openapi-core/node_modules/colorette": { @@ -841,16 +903,24 @@ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", "dev": true }, - "node_modules/@redocly/openapi-core/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/@redocly/openapi-core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -1034,8 +1104,7 @@ "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@types/json5": { "version": "0.0.29", @@ -1403,6 +1472,48 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/ansi-escapes": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", @@ -2619,6 +2730,23 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -3492,6 +3620,21 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-schema-to-ts": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-2.7.2.tgz", + "integrity": "sha512-R1JfqKqbBR4qE8UyBR56Ms30LL62/nlhoz+1UkfI/VE7p54Awu919FZ6ZUPG8zIa3XB65usPJgr1ONVncUGSaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@types/json-schema": "^7.0.9", + "ts-algebra": "^1.2.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3637,12 +3780,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3970,26 +4107,6 @@ "dev": true, "peer": true }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/npm-run-path": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", @@ -4467,6 +4584,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5208,11 +5326,12 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "node_modules/ts-algebra": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-1.2.2.tgz", + "integrity": "sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==", + "dev": true, + "license": "MIT" }, "node_modules/tsconfig-paths": { "version": "3.14.2", @@ -5966,22 +6085,6 @@ } } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6232,6 +6335,12 @@ "undici": "^5.25.4" } }, + "@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true + }, "@esbuild/android-arm": { "version": "0.19.7", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.7.tgz", @@ -6623,6 +6732,16 @@ "@octokit/openapi-types": "^19.0.2" } }, + "@otto-de/api-guidelines-redocly2-ruleset": { + "version": "file:api-guidelines-redocly2-ruleset", + "requires": { + "@redocly/openapi-core": "^2.25.4", + "@types/node": "^20.9.4", + "esbuild": "^0.19.7", + "typescript": "^5.3.2", + "vitest": "^0.34.6" + } + }, "@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6645,15 +6764,15 @@ } }, "@redocly/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-9GWx27t7xWhDIR02PA18nzBdLcKQRgc46xNQvjFkrYk4UOmvKhJ/dawwiX0cCOeetN5LcaaiqQbVOWYK62SGHw==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.3.tgz", + "integrity": "sha512-l42u0of3hY98sN2A+M4qTX1O/KrpgGH32Hu9kP2GtHyD5Dfqq86PKFLe5dwaD8DEnNmlOlll2BAmeEtf0DaySg==", "dev": true, "requires": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "dependencies": { "json-schema-traverse": { @@ -6664,37 +6783,43 @@ } } }, + "@redocly/config": { + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.46.1.tgz", + "integrity": "sha512-dSdkB2wRLtvl3f7ayRu9vqVhUMjjRaxZlHgRbgOtPPXxn4uI/ciDO87h4CJb7Iet+OVpevpAU6gU8bo5qVbQxg==", + "dev": true, + "requires": { + "json-schema-to-ts": "2.7.2" + } + }, "@redocly/openapi-core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.4.1.tgz", - "integrity": "sha512-oAhnG8MKocM9LuP++NGFxdniNKWSLA7hzHPQoOK92LIP/DdvXx8pEeZ68UTNxIXhKonoUcO6s86I3L0zj143zg==", + "version": "2.25.4", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-2.25.4.tgz", + "integrity": "sha512-zYdKQEsowPNtkTixrfbn5DySWBLQpTsISthVBBEPAa3OZC75UI76CbHXEamJ8Kmlead9IkD5RbgeJvxqJ5/H6Q==", "dev": true, "requires": { - "@redocly/ajv": "^8.11.0", - "@types/node": "^14.11.8", + "@redocly/ajv": "^8.18.0", + "@redocly/config": "^0.46.0", + "ajv": "npm:@redocly/ajv@8.18.0", + "ajv-formats": "^3.0.1", "colorette": "^1.2.0", "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", - "lodash.isequal": "^4.5.0", - "minimatch": "^5.0.1", - "node-fetch": "^2.6.1", + "picomatch": "^4.0.4", "pluralize": "^8.0.0", "yaml-ast-parser": "0.0.43" }, "dependencies": { - "@types/node": { - "version": "14.18.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.42.tgz", - "integrity": "sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg==", - "dev": true - }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "ajv": { + "version": "npm:@redocly/ajv@8.18.0", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA==", "dev": true, "requires": { - "balanced-match": "^1.0.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" } }, "colorette": { @@ -6703,14 +6828,17 @@ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", "dev": true }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true } } }, @@ -6823,8 +6951,7 @@ "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true, - "peer": true + "dev": true }, "@types/json5": { "version": "0.0.29", @@ -7063,6 +7190,35 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "ansi-escapes": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", @@ -7969,6 +8125,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true + }, "fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -8553,6 +8715,17 @@ "argparse": "^2.0.1" } }, + "json-schema-to-ts": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-2.7.2.tgz", + "integrity": "sha512-R1JfqKqbBR4qE8UyBR56Ms30LL62/nlhoz+1UkfI/VE7p54Awu919FZ6ZUPG8zIa3XB65usPJgr1ONVncUGSaQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.18.3", + "@types/json-schema": "^7.0.9", + "ts-algebra": "^1.2.0" + } + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -8661,12 +8834,6 @@ "p-locate": "^5.0.0" } }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "dev": true - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -8908,15 +9075,6 @@ "dev": true, "peer": true }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, "npm-run-path": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", @@ -9742,10 +9900,10 @@ "is-number": "^7.0.0" } }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "ts-algebra": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-1.2.2.tgz", + "integrity": "sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==", "dev": true }, "tsconfig-paths": { @@ -10151,22 +10309,6 @@ "why-is-node-running": "^2.2.2" } }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index a2db312b..3358c36b 100644 --- a/package.json +++ b/package.json @@ -16,31 +16,28 @@ "node": ">=16" }, "type": "module", + "workspaces": [ + "api-guidelines-redocly2-ruleset" + ], "files": [ - "dist", "api-guidelines", "changes", - "src/linting", "src/portal", - "!**/*.spec.ts", - "!**/__tests__" + "!**/*.spec.ts" ], "scripts": { - "build": "npm run clean && npm run build:redocly", - "build:redocly": "esbuild src/linting/plugin.ts --bundle --platform=node --outfile=dist/plugin.cjs --log-level=warning --external:@redocly/openapi-core", + "build:all": "npm run build --workspaces --include-workspace-root --if-present", + "tsc:all": "npm run tsc --workspaces --include-workspace-root --if-present", + "test:all": "npm run test --workspaces --include-workspace-root --if-present", "changelog": "npx tsx src/scripts/changelog/index.ts", - "clean": "rm -rf ./dist", "prepare": "husky install src/.husky || exit 0", - "prepack": "npm run build", "test": "vitest run --root src", "tsc": "tsc --noEmit -p src/tsconfig.json" }, "devDependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.0", - "@redocly/openapi-core": "^1.4.1", "@types/node": "^20.9.4", - "esbuild": "^0.19.7", "eslint": "^8.54.0", "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-prettier": "^9.0.0", diff --git a/src/linting/plugin.ts b/src/linting/plugin.ts deleted file mode 100644 index 8a48a2c1..00000000 --- a/src/linting/plugin.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { Plugin } from "@redocly/openapi-core/lib/config"; -import { UseTLS } from "./rules/use-tls.js"; -import { AlwaysReturnJsonObject } from "./rules/always-return-json-object.js"; -import { DefinePermissionsWithScope } from "./rules/define-permissions-with-scope.js"; -import { FormatEnumerationUpperSnakeCase } from "./rules/format-enumeration-upper-snake-case.js"; -import { NoRequestBodyInGetMethod } from "./rules/no-request-body-in-get-method.js"; -import { NotUseNullForEmptyArray } from "./rules/not-use-null-for-empty-array.js"; -import { OmitOptionalProperty } from "./rules/omit-optional-property.js"; -import { SecureEndpointsWithOAuth20 } from "./rules/secure-endpoints-with-oauth-2.0.js"; -import { UseAbsoluteCustomLinkRelationUrl } from "./rules/use-absolute-custom-link-relation-url.js"; -import { UseAbsoluteProfileUrl } from "./rules/use-absolute-profile-url.js"; -import { UseAuthorizationGrant } from "./rules/use-authorization-grant.js"; -import { UseCamelCaseForPropertyName } from "./rules/use-camel-case-for-property-name.js"; -import { UseCamelCaseForQueryParameter } from "./rules/use-camel-case-for-query-parameter.js"; -import { UseCommonDateAndTimeFormat } from "./rules/use-common-date-and-time-format.js"; -import { UseCuriedLinkRelationTypes } from "./rules/use-curied-link-relation-types.js"; -import { UseExtensibleEnum } from "./rules/use-extensible-enum.js"; -import { UseKebabCaseForPathParameter } from "./rules/use-kebab-case-for-path-parameter.js"; -import { UseKebabCaseInUri } from "./rules/use-kebab-case-in-uri.js"; -import { UseOas3 } from "./rules/use-oas-3.js"; -import { UseStringEnum } from "./rules/use-string-enum.js"; -import { Operation4xxProblemDetailsRfc9457 } from "./rules/operation-4xx-problem-details-rfc9457.js"; - -export const id: Plugin["id"] = "api-guidelines"; - -export const rules = { - oas2: { - "use-oas-3": UseOas3, - }, - oas3: { - "always-return-json-object": AlwaysReturnJsonObject, - "define-permissions-with-scope": DefinePermissionsWithScope, - "format-enumeration-upper-snake-case": FormatEnumerationUpperSnakeCase, - "no-request-body-in-get-method": NoRequestBodyInGetMethod, - "not-use-null-for-empty-array": NotUseNullForEmptyArray, - "omit-optional-property": OmitOptionalProperty, - "secure-endpoints-with-oauth-2.0": SecureEndpointsWithOAuth20, - "use-absolute-custom-link-relation-url": UseAbsoluteCustomLinkRelationUrl, - "use-absolute-profile-url": UseAbsoluteProfileUrl, - "use-authorization-grant": UseAuthorizationGrant, - "use-camel-case-for-property-name": UseCamelCaseForPropertyName, - "use-camel-case-for-query-parameter": UseCamelCaseForQueryParameter, - "use-common-date-and-time-format": UseCommonDateAndTimeFormat, - "use-curied-link-relation-types": UseCuriedLinkRelationTypes, - "use-extensible-enum": UseExtensibleEnum, - "use-kebab-case-for-path-parameter": UseKebabCaseForPathParameter, - "use-kebab-case-in-uri": UseKebabCaseInUri, - "use-string-enum": UseStringEnum, - "use-tls": UseTLS, - "operation-4xx-problem-details-rfc9457": Operation4xxProblemDetailsRfc9457, - }, -}; - -export const configs = { - recommended: { - rules: { - "info-contact": "error", // https://api.otto.de/portal/guidelines/r000078 - "operation-2xx-response": "error", // https://api.otto.de/portal/guidelines/r000011 - // "operation-4xx-problem-details-rfc9457": "error", // https://api.otto.de/portal/guidelines/r000034 - "api-guidelines/operation-4xx-problem-details-rfc9457": "error", // https://api.otto.de/portal/guidelines/r000034 - "no-path-trailing-slash": "error", // https://api.otto.de/portal/guidelines/r000020 - "api-guidelines/always-return-json-object": "error", // https://api.otto.de/portal/guidelines/r004030 - "api-guidelines/define-permissions-with-scope": "error", // https://api.otto.de/portal/guidelines/r000047 - "api-guidelines/format-enumeration-upper-snake-case": "error", // https://api.otto.de/portal/guidelines/r004090 - "api-guidelines/no-request-body-in-get-method": "error", // https://api.otto.de/portal/guidelines/r000007 - "api-guidelines/not-use-null-for-empty-array": "warn", // https://api.otto.de/portal/guidelines/r004060 - "api-guidelines/omit-optional-property": "warn", // https://api.otto.de/portal/guidelines/r004021 - "api-guidelines/secure-endpoints-with-oauth-2.0": "error", // https://api.otto.de/portal/guidelines/r000051 - "api-guidelines/use-absolute-custom-link-relation-url": "error", // https://api.otto.de/portal/guidelines/r100037 - "api-guidelines/use-absolute-profile-url": "error", // https://api.otto.de/portal/guidelines/r100066 - "api-guidelines/use-authorization-grant": "error", // https://api.otto.de/portal/guidelines/r000052 - "api-guidelines/use-camel-case-for-property-name": "warn", // https://api.otto.de/portal/guidelines/r004010 - "api-guidelines/use-camel-case-for-query-parameter": "error", // https://api.otto.de/portal/guidelines/r000022 - "api-guidelines/use-common-date-and-time-format": "error", // https://api.otto.de/portal/guidelines/r100072 - "api-guidelines/use-curied-link-relation-types": "error", // https://api.otto.de/portal/guidelines/r100038 - "api-guidelines/use-extensible-enum": "warn", // https://api.otto.de/portal/guidelines/r000035 - "api-guidelines/use-kebab-case-for-path-parameter": "off", // TODO guideline rule will follow - "api-guidelines/use-kebab-case-in-uri": "error", // https://api.otto.de/portal/guidelines/r000023 - "api-guidelines/use-oas-3": "error", // https://api.otto.de/portal/guidelines/r000003 - "api-guidelines/use-string-enum": "error", // https://api.otto.de/portal/guidelines/r004080 - "api-guidelines/use-tls": "error", // https://api.otto.de/portal/guidelines/r000046 - }, - }, -}; diff --git a/src/linting/rules/__tests__/createTestConfig.ts b/src/linting/rules/__tests__/createTestConfig.ts deleted file mode 100644 index 2352c384..00000000 --- a/src/linting/rules/__tests__/createTestConfig.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { CustomRulesConfig } from "@redocly/openapi-core/lib/config/types.d.js"; -import { Config } from "@redocly/openapi-core"; - -export function createTestConfig(customRulesConfig: CustomRulesConfig) { - return new Config({ - apis: {}, - styleguide: { - plugins: [ - { - id: "test-plugin", - // @ts-ignore - rules: customRulesConfig, - }, - ], - rules: Object.fromEntries( - Object.values(customRulesConfig).map((ruleSet) => - Object.keys(ruleSet) - .map((rule) => [rule, "error"]) - .flat(), - ), - ), - }, - }); -} diff --git a/src/linting/rules/define-permissions-with-scope.spec.ts b/src/linting/rules/define-permissions-with-scope.spec.ts deleted file mode 100644 index 09b9b2b2..00000000 --- a/src/linting/rules/define-permissions-with-scope.spec.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { DefinePermissionsWithScope } from "./define-permissions-with-scope.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": DefinePermissionsWithScope, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 - -paths: - /post: - post: - security: - - clientCredentials: - - foo.read - -components: - securitySchemes: - clientCredentials: - type: oauth2 - flows: - clientCredentials: - tokenUrl: "/oauth2/token" - scopes: - foo.read: Read foo - - authorizationCode: - type: oauth2 - flows: - authorizationCode: - authorizationUrl: "/oauth2/auth" - tokenUrl: "/oauth2/token" - scopes: - foo.read: Read foo -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark missing scopes", async () => { - const spec = ` -openapi: 3.0.3 - -components: - securitySchemes: - clientCredentials: - type: oauth2 - flows: - clientCredentials: - tokenUrl: "/oauth2/token" - - authorizationCode: - type: oauth2 - flows: - authorizationCode: - authorizationUrl: "/oauth2/auth" - tokenUrl: "/oauth2/token" -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/components/securitySchemes", - "reportOnKey": true, - }, - ], - "message": "Must define a scope. See https://api.otto.de/portal/guidelines/r000047", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 - -paths: - /post: - post: - security: - - clientCredentials: - - foo.post - - /put: - put: - security: - - clientCredentials: - - foo.put - -components: - securitySchemes: - clientCredentials: - type: oauth2 - flows: - clientCredentials: - tokenUrl: "/oauth2/token" - scopes: - foo.read: Read foo - - authorizationCode: - type: oauth2 - flows: - authorizationCode: - authorizationUrl: "/oauth2/auth" - tokenUrl: "/oauth2/token" - scopes: - foo.read: Read foo -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1post/post/security/0", - "reportOnKey": false, - }, - ], - "message": "Scope \\"foo.post\\" is not defined. See https://api.otto.de/portal/guidelines/r000047", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/paths/~1put/put/security/0", - "reportOnKey": false, - }, - ], - "message": "Scope \\"foo.put\\" is not defined. See https://api.otto.de/portal/guidelines/r000047", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); - -it("should handle missing flows", async () => { - const spec = ` -openapi: 3.0.3 - -components: - securitySchemes: - test: - type: http -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/components/securitySchemes", - "reportOnKey": true, - }, - ], - "message": "Must define a scope. See https://api.otto.de/portal/guidelines/r000047", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); diff --git a/src/linting/rules/format-enumeration-upper-snake-case.spec.ts b/src/linting/rules/format-enumeration-upper-snake-case.spec.ts deleted file mode 100644 index 32157fcd..00000000 --- a/src/linting/rules/format-enumeration-upper-snake-case.spec.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { FormatEnumerationUpperSnakeCase } from "./format-enumeration-upper-snake-case.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": FormatEnumerationUpperSnakeCase, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - parameters: - - schema: - enum: - - FOO_BAR - - BAR_FOO - - responses: - 200: - content: - application/hal+json: - schema: - enum: - - 1 - - 2 -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark non upper snake case values", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - parameters: - - schema: - enum: - - FOO_BAR - - BAR_foo - - foo - - responses: - 200: - content: - application/hal+json: - schema: - enum: - - 1a - - 2B - - Bad - - GOOD -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1get/get/parameters/0/schema/enum/1", - "reportOnKey": false, - }, - ], - "message": "enum value \\"BAR_foo\\" is not upper snake case. See https://api.otto.de/portal/guidelines/r004090", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/paths/~1get/get/parameters/0/schema/enum/2", - "reportOnKey": false, - }, - ], - "message": "enum value \\"foo\\" is not upper snake case. See https://api.otto.de/portal/guidelines/r004090", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/enum/0", - "reportOnKey": false, - }, - ], - "message": "enum value \\"1a\\" is not upper snake case. See https://api.otto.de/portal/guidelines/r004090", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/enum/2", - "reportOnKey": false, - }, - ], - "message": "enum value \\"Bad\\" is not upper snake case. See https://api.otto.de/portal/guidelines/r004090", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); diff --git a/src/linting/rules/no-request-body-in-get-method.spec.ts b/src/linting/rules/no-request-body-in-get-method.spec.ts deleted file mode 100644 index f76cb597..00000000 --- a/src/linting/rules/no-request-body-in-get-method.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { NoRequestBodyInGetMethod } from "./no-request-body-in-get-method.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": NoRequestBodyInGetMethod, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - - /post: - post: - requestBody: - content: {} - - /put: - put: - requestBody: - content: {} - - /delete: - delete: - requestBody: - content: {} -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark missing requestBody", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - requestBody: - content: {} - -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1get/get", - "reportOnKey": false, - }, - ], - "message": "Get method must not have a request body. See https://api.otto.de/portal/guidelines/r000007", - "ruleId": "test-rule", - "suggest": [ - "use query parameters", - ], - }, - ] - `); -}); - -it("should mark missing requestBody in get only", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /foo: - get: - requestBody: - content: {} - - put: - requestBody: - content: {} -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1foo/get", - "reportOnKey": false, - }, - ], - "message": "Get method must not have a request body. See https://api.otto.de/portal/guidelines/r000007", - "ruleId": "test-rule", - "suggest": [ - "use query parameters", - ], - }, - ] - `); -}); diff --git a/src/linting/rules/not-use-null-for-empty-array.spec.ts b/src/linting/rules/not-use-null-for-empty-array.spec.ts deleted file mode 100644 index a572c8b4..00000000 --- a/src/linting/rules/not-use-null-for-empty-array.spec.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { NotUseNullForEmptyArray } from "./not-use-null-for-empty-array.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": NotUseNullForEmptyArray, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - type: object - properties: - foo: - type: array - example: [] - bar: - type: object - properties: - baz: - type: array - example: [] - example: - baz: [] - example: - foo: [] - bar: - baz: [] - example: - foo: [] - bar: - baz: [] - examples: - default: - $ref: "#/components/examples/Foo" - -components: - examples: - Foo: - foo: [] - bar: - baz: [] -`; - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should not find any error when examples are missing", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - type: object - properties: - foo: - type: array - bar: - type: object - properties: - baz: - type: array -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark null", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - type: object - properties: - foo: - type: array - example: null - bar: - type: object - properties: - baz: - type: array - example: null - example: - baz: null - example: - foo: null - bar: - baz: null - example: - foo: null - bar: - baz: null - examples: - default: - $ref: "#/components/examples/Foo" - -components: - examples: - Foo: - foo: null - bar: - baz: null -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/example/foo", - "reportOnKey": false, - }, - ], - "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", - "ruleId": "test-rule", - "suggest": [ - "change to []", - ], - }, - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/example/bar/baz", - "reportOnKey": false, - }, - ], - "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", - "ruleId": "test-rule", - "suggest": [ - "change to []", - ], - }, - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/example/foo", - "reportOnKey": false, - }, - ], - "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", - "ruleId": "test-rule", - "suggest": [ - "change to []", - ], - }, - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/example/bar/baz", - "reportOnKey": false, - }, - ], - "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", - "ruleId": "test-rule", - "suggest": [ - "change to []", - ], - }, - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/properties/foo/example", - "reportOnKey": false, - }, - ], - "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", - "ruleId": "test-rule", - "suggest": [ - "change to []", - ], - }, - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/properties/bar/example/baz", - "reportOnKey": false, - }, - ], - "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", - "ruleId": "test-rule", - "suggest": [ - "change to []", - ], - }, - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/properties/bar/properties/baz/example", - "reportOnKey": false, - }, - ], - "message": "Example for type array is null. See https://api.otto.de/portal/guidelines/r004060", - "ruleId": "test-rule", - "suggest": [ - "change to []", - ], - }, - ] - `); -}); diff --git a/src/linting/rules/secure-endpoints-with-oauth-2.0.spec.ts b/src/linting/rules/secure-endpoints-with-oauth-2.0.spec.ts deleted file mode 100644 index 698338a5..00000000 --- a/src/linting/rules/secure-endpoints-with-oauth-2.0.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { SecureEndpointsWithOAuth20 } from "./secure-endpoints-with-oauth-2.0.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": SecureEndpointsWithOAuth20, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 -components: - securitySchemes: - foo: - type: oauth2 - - bar: - type: oauth2 - -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark wrong type", async () => { - const spec = ` -openapi: 3.0.3 -components: - securitySchemes: - foo: - type: http - - bar: - type: openIdConnect - - baz: - type: apiKey - -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/components/securitySchemes/foo/type", - "reportOnKey": false, - }, - ], - "message": "Type is not OAuth 2.0. See https://api.otto.de/portal/guidelines/r000051", - "ruleId": "test-rule", - "suggest": [ - "type: oauth2", - ], - }, - { - "location": [ - { - "pointer": "#/components/securitySchemes/bar/type", - "reportOnKey": false, - }, - ], - "message": "Type is not OAuth 2.0. See https://api.otto.de/portal/guidelines/r000051", - "ruleId": "test-rule", - "suggest": [ - "type: oauth2", - ], - }, - { - "location": [ - { - "pointer": "#/components/securitySchemes/baz/type", - "reportOnKey": false, - }, - ], - "message": "Type is not OAuth 2.0. See https://api.otto.de/portal/guidelines/r000051", - "ruleId": "test-rule", - "suggest": [ - "type: oauth2", - ], - }, - ] - `); -}); - -it("should mark missing type", async () => { - const spec = ` -openapi: 3.0.3 -components: - securitySchemes: - foo: {} -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/components/securitySchemes/foo/type", - "reportOnKey": false, - }, - ], - "message": "Type is not OAuth 2.0. See https://api.otto.de/portal/guidelines/r000051", - "ruleId": "test-rule", - "suggest": [ - "type: oauth2", - ], - }, - ] - `); -}); diff --git a/src/linting/rules/use-absolute-profile-url.spec.ts b/src/linting/rules/use-absolute-profile-url.spec.ts deleted file mode 100644 index 0437a40a..00000000 --- a/src/linting/rules/use-absolute-profile-url.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { UseAbsoluteProfileUrl } from "./use-absolute-profile-url.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": UseAbsoluteProfileUrl, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /post: - post: - requestBody: - content: - application/hal+json: {} - application/hal+json;profile=https://example.com/profiles/foo+v1: {} - application/hal+json;profile="https://example.com/profiles/foo+v2": {} - - responses: - 200: - content: - application/hal+json: {} - application/hal+json;profile="https://example.com/profiles/foo+v1";foo=bar: {} - application/hal+json;PROFILE=https://example.com/profiles/foo+v2: {} - -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark relative profiles", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /post: - post: - requestBody: - content: - application/hal+json;profile="/profiles/foo+v1": {} - - responses: - 200: - content: - application/hal+json;profile="/profiles/foo+v1": {} - -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1post/post/requestBody/content/0", - "reportOnKey": true, - }, - ], - "message": "Profile url is not absolute. See https://api.otto.de/portal/guidelines/r100066", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/paths/~1post/post/responses/200/content/0", - "reportOnKey": true, - }, - ], - "message": "Profile url is not absolute. See https://api.otto.de/portal/guidelines/r100066", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); diff --git a/src/linting/rules/use-authorization-grant.spec.ts b/src/linting/rules/use-authorization-grant.spec.ts deleted file mode 100644 index 85738fe7..00000000 --- a/src/linting/rules/use-authorization-grant.spec.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { UseAuthorizationGrant } from "./use-authorization-grant.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": UseAuthorizationGrant, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 -components: - securitySchemes: - clientCredentials: - type: oauth2 - flows: - clientCredentials: - tokenUrl: "/oauth2/token" - scopes: - foo.read: Read foo - - authorizationCode: - type: oauth2 - flows: - authorizationCode: - authorizationUrl: "/oauth2/auth" - tokenUrl: "/oauth2/token" - scopes: - foo.read: Read foo -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark wrong flow", async () => { - const spec = ` -openapi: 3.0.3 -components: - securitySchemes: - clientCredentials: - type: oauth2 - flows: - implicit: - - authorizationCode: - type: oauth2 - flows: - password: -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/components/securitySchemes/clientCredentials", - "reportOnKey": false, - }, - ], - "message": "Must use Authorization Grant. See https://api.otto.de/portal/guidelines/r000052", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/components/securitySchemes/authorizationCode", - "reportOnKey": false, - }, - ], - "message": "Must use Authorization Grant. See https://api.otto.de/portal/guidelines/r000052", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); - -it("should handle missing flows", async () => { - const spec = ` -openapi: 3.0.3 -components: - securitySchemes: - test: - type: http -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/components/securitySchemes/test", - "reportOnKey": false, - }, - ], - "message": "Must use Authorization Grant. See https://api.otto.de/portal/guidelines/r000052", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); diff --git a/src/linting/rules/use-camel-case-for-property-name.spec.ts b/src/linting/rules/use-camel-case-for-property-name.spec.ts deleted file mode 100644 index 0b84ab68..00000000 --- a/src/linting/rules/use-camel-case-for-property-name.spec.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { UseCamelCaseForPropertyName } from "./use-camel-case-for-property-name.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": UseCamelCaseForPropertyName, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /post: - post: - parameters: - - schema: - type: object - properties: - name: - type: string - jobDescription: - type: string - houses: - type: array - items: - type: object - properties: - fooBar: string - requestBody: - content: - application/hal+json: - schema: - type: object - properties: - name: - type: string - jobDescription: - type: string - houses: - type: array - items: - type: object - properties: - fooBar: string - responses: - 200: - content: - application/hal+json: - schema: - type: object - properties: - name: - type: string - jobDescription: - type: string - houses: - type: array - items: - type: object - properties: - fooBar: string -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); -it("should not find any error in valid components", async () => { - const spec = ` -openapi: 3.0.3 - -paths: - /post: - post: - responses: - 200: - content: - application/hal+json: - schema: - allOf: - - $ref: "#/components/schemas/PromoImageHal" - - title: foo - -components: - schemas: - PromoImageHal: - properties: - type: - type: string - _links: - allOf: - - title: foo - - description: bar - - properties: - foo: - type: string -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should not find any error in CURIE", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /post: - post: - responses: - 200: - content: - application/hal+json: - schema: - type: object - properties: - _links: - description: Links section. - type: object - properties: - o:bar: - type: array - _embedded: - type: object - description: Embedded resources - properties: - o:foo: - type: array -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark invalid property names", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /post: - post: - - requestBody: - content: - application/json: - schema: - type: object - properties: - NAME: - type: string - job-description: - type: string - houses: - type: array - items: - type: object - properties: - foo_bar: - type: string - - responses: - 200: - content: - application/hal+json: - schema: - type: object - properties: - NAME: - type: string - job-description: - type: string - houses: - type: array - items: - type: object - properties: - foo_bar: - type: string -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1post/post/requestBody/content/application~1json/schema/properties/NAME", - "reportOnKey": true, - }, - ], - "message": "Property \\"NAME\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/paths/~1post/post/requestBody/content/application~1json/schema/properties/job-description", - "reportOnKey": true, - }, - ], - "message": "Property \\"job-description\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/paths/~1post/post/requestBody/content/application~1json/schema/properties/houses/items/properties/foo_bar", - "reportOnKey": true, - }, - ], - "message": "Property \\"foo_bar\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/paths/~1post/post/responses/200/content/application~1hal+json/schema/properties/NAME", - "reportOnKey": true, - }, - ], - "message": "Property \\"NAME\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/paths/~1post/post/responses/200/content/application~1hal+json/schema/properties/job-description", - "reportOnKey": true, - }, - ], - "message": "Property \\"job-description\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/paths/~1post/post/responses/200/content/application~1hal+json/schema/properties/houses/items/properties/foo_bar", - "reportOnKey": true, - }, - ], - "message": "Property \\"foo_bar\\" is not camelCase. See https://api.otto.de/portal/guidelines/r004010", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); diff --git a/src/linting/rules/use-camel-case-for-query-parameter.spec.ts b/src/linting/rules/use-camel-case-for-query-parameter.spec.ts deleted file mode 100644 index 1459fde2..00000000 --- a/src/linting/rules/use-camel-case-for-query-parameter.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { UseCamelCaseForQueryParameter } from "./use-camel-case-for-query-parameter.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": UseCamelCaseForQueryParameter, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - parameters: - - in: query - name: fooBar - - in: query - name: barFoo -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark non upper snake case values", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - parameters: - - in: query - name: foo-bar - - in: query - name: FOO_BAR -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1get/get/parameters/0/name", - "reportOnKey": false, - }, - ], - "message": "Query parameter is not in camel case.", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/paths/~1get/get/parameters/1/name", - "reportOnKey": false, - }, - ], - "message": "Query parameter is not in camel case.", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); diff --git a/src/linting/rules/use-extensible-enum.spec.ts b/src/linting/rules/use-extensible-enum.spec.ts deleted file mode 100644 index 5fcd24a8..00000000 --- a/src/linting/rules/use-extensible-enum.spec.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { UseExtensibleEnum } from "./use-extensible-enum.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": UseExtensibleEnum, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - parameters: - - name: aaa - schema: &schema - type: string - x-extensible-enum: - - value: Foo - description: Credit card payment - - value: Bar - description: Payment by the customer by bank transfer. - deprecated: true - - value: DIRECT_DEBIT - description: Direct debit from a bank account. - preview: true - - responses: - 200: - content: - application/hal+json: - schema: *schema -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark enum and x-extensible-enum clash", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - type: string - enum: - - 1 - - 2 - x-extensible-enum: - - value: foo - description: foooo - - value: bar - description: barrrrr -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/enum", - "reportOnKey": false, - }, - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/x-extensible-enum", - "reportOnKey": false, - }, - ], - "message": "Do not use the enum keyword in combination with x-extensible-enum. See https://api.otto.de/portal/guidelines/r000035", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); - -it("should mark type number", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - type: number - x-extensible-enum: - - value: foo - description: foooo -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/type", - "reportOnKey": false, - }, - ], - "message": "Extensible enum is not represented as string. See https://api.otto.de/portal/guidelines/r000035", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); - -it("should mark non boolean deprecated", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - type: string - x-extensible-enum: - - value: foo - description: foooo - deprecated: "non boolean" -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/x-extensible-enum/0/deprecated", - "reportOnKey": false, - }, - ], - "message": "deprecated is not boolean. See https://api.otto.de/portal/guidelines/r000035", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); - -it("should mark non boolean preview", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - type: string - x-extensible-enum: - - value: foo - description: foooo - preview: "non boolean" -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/x-extensible-enum/0/preview", - "reportOnKey": false, - }, - ], - "message": "preview is not boolean. See https://api.otto.de/portal/guidelines/r000035", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); - -it("should mark non array", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - type: string - x-extensible-enum: - value: foo - description: foooo -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/x-extensible-enum", - "reportOnKey": false, - }, - ], - "message": "x-extensible-enum is not an array. See https://api.otto.de/portal/guidelines/r000035", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); diff --git a/src/linting/rules/use-oas-3.spec.ts b/src/linting/rules/use-oas-3.spec.ts deleted file mode 100644 index de6edbfb..00000000 --- a/src/linting/rules/use-oas-3.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { UseOas3 } from "./use-oas-3.js"; - -const config = createTestConfig({ - oas2: { - // @ts-ignore - "test-rule": UseOas3, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.0 -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark oas 2", async () => { - const spec = ` -swagger: '2.0' -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/swagger", - "reportOnKey": true, - }, - ], - "message": "openapi >= 3.0.0 should be used.", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); diff --git a/src/linting/rules/use-string-enum.spec.ts b/src/linting/rules/use-string-enum.spec.ts deleted file mode 100644 index 57dc3258..00000000 --- a/src/linting/rules/use-string-enum.spec.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { UseStringEnum } from "./use-string-enum.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": UseStringEnum, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - parameters: - - name: aaa - schema: - type: string - enum: - - foo - - bar - - responses: - 200: - content: - application/hal+json: - schema: - type: string - properties: - foo: - type: array - bar: - type: object - status: - type: number - enum: [404] -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark type number and integer", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - type: number - enum: - - 1 - - 2 - -components: - schemas: - Foo: - type: object - properties: - foo: - type: integer - enum: - - 3 - - 4 - bar: - type: string - enum: - - BAR1 - - BAR2 -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/type", - "reportOnKey": false, - }, - ], - "message": "Enumeration is not represented as string. See https://api.otto.de/portal/guidelines/r004080", - "ruleId": "test-rule", - "suggest": [], - }, - { - "location": [ - { - "pointer": "#/components/schemas/Foo/properties/foo/type", - "reportOnKey": false, - }, - ], - "message": "Enumeration is not represented as string. See https://api.otto.de/portal/guidelines/r004080", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); - -it("should mark missing type", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - enum: - - FOO - - BAR -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/paths/~1get/get/responses/200/content/application~1hal+json/schema/type", - "reportOnKey": false, - }, - ], - "message": "Enumeration is not represented as string. See https://api.otto.de/portal/guidelines/r004080", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); diff --git a/src/linting/rules/use-tls.spec.ts b/src/linting/rules/use-tls.spec.ts deleted file mode 100644 index 4c129466..00000000 --- a/src/linting/rules/use-tls.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { lintFromString } from "@redocly/openapi-core"; -import { createTestConfig } from "./__tests__/createTestConfig.js"; -import { removeClutter } from "./__tests__/removeClutter.js"; -import { UseTLS } from "./use-tls.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": UseTLS, - }, -}); - -it("should not find any error", async () => { - const spec = ` -openapi: 3.0.3 -servers: - - url: https://example.org -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toStrictEqual([]); -}); - -it("should mark second server url", async () => { - const spec = ` -openapi: 3.0.3 -servers: - - url: https://example.org - - url: http://example.org -`; - - const result = await lintFromString({ - source: spec, - config, - }); - - removeClutter(result); - - expect(result).toMatchInlineSnapshot(` - [ - { - "location": [ - { - "pointer": "#/servers/1/url", - "reportOnKey": false, - }, - ], - "message": "Server url is not secured with TLS. See https://api.otto.de/portal/guidelines/r000046", - "ruleId": "test-rule", - "suggest": [], - }, - ] - `); -}); diff --git a/src/linting/rules/utils/resolveRecursive.spec.ts b/src/linting/rules/utils/resolveRecursive.spec.ts deleted file mode 100644 index a76aa050..00000000 --- a/src/linting/rules/utils/resolveRecursive.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { lintFromString, type OasRef } from "@redocly/openapi-core"; -import { createTestConfig } from "../__tests__/createTestConfig.js"; -import { resolveRecursive } from "./resolveRecursive.js"; - -const config = createTestConfig({ - oas3: { - // @ts-ignore - "test-rule": () => { - return { - Schema(schema, { report, resolve }) { - const result = resolveRecursive(schema as OasRef, resolve); - report({ - message: `resolves to ${result.location.absolutePointer}`, - }); - }, - }; - }, - }, -}); - -it("should resolve nothing", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - type: object - properties: - foo: - type: array - bar: - type: object - baz: - type: string -`; - - const [{ location }] = await lintFromString({ - source: spec, - config, - }); - - expect(location[0].pointer).toBe( - "#/paths/~1get/get/responses/200/content/application~1hal+json/schema", - ); -}); - -it("should resolve one ref", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - $ref: "#/components/schemas/Foo" - -components: - schemas: - Foo: - type: string -`; - - const [{ location }] = await lintFromString({ - source: spec, - config, - }); - - expect(location[0].pointer).toBe("#/components/schemas/Foo"); -}); - -it("should resolve three refs", async () => { - const spec = ` -openapi: 3.0.3 -paths: - /get: - get: - responses: - 200: - content: - application/hal+json: - schema: - $ref: "#/components/schemas/Foo" - -components: - schemas: - Foo: - $ref: "#/components/schemas/Bar" - Bar: - $ref: "#/components/schemas/Baz" - Baz: - type: string -`; - - const [{ location }] = await lintFromString({ - source: spec, - config, - }); - - expect(location[0].pointer).toBe("#/components/schemas/Baz"); -}); diff --git a/src/tsconfig.json b/src/tsconfig.json index fb51233a..42a4d61c 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -10,5 +10,5 @@ "allowJs": true, "types": ["vitest/globals"] }, - "include": ["./scripts", "./linting", "./portal", "./vitest.config.ts"] + "include": ["./scripts", "./portal", "./vitest.config.ts"] } diff --git a/src/vitest.config.ts b/src/vitest.config.ts index b30c9415..4ae3768e 100644 --- a/src/vitest.config.ts +++ b/src/vitest.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - include: ["linting/**/*.spec.ts", "scripts/**/*.spec.ts"], + include: ["scripts/**/*.spec.ts"], reporters: ["verbose"], globals: true, },