From c1da6bac398f9fefbfce6f1b4ff6f1b22bc853b6 Mon Sep 17 00:00:00 2001 From: Sebastian Mahr Date: Wed, 6 Mar 2024 13:56:59 +0100 Subject: [PATCH] test/checks for query and url parameter definition (#141) * test: test query and url parameter definition * chore: move parser to dev dependencies * chore: typo * chore: revert unwanted change --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- CHANGELOG.md | 6 +-- package-lock.json | 96 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 3 +- test/spec.test.ts | 98 ++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 184 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d33f74cd..59aeeebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,12 @@ -> SDK 4.1.0 -- add routes for querying versions of attrbutes +- add routes for querying versions of attributes - `GET /api/v2/Attributes/Own/Repository` Get all the repository attributes - - `GET /api/v2/Attributes/Own/Shared/Identity` Get all own shared indentity attributes + - `GET /api/v2/Attributes/Own/Shared/Identity` Get all own shared identity attributes - `GET /api/v2/Attributes/Peer/Shared/Identity` Get all peer shared identity attributes - `GET /api/v2/Attributes/{id}/Versions` Get all versions of one repository attribute - - `GET /api/v2/Attributes/{id}/Versions/Shared` Get all shard versions of one repository attribute + - `GET /api/v2/Attributes/{id}/Versions/Shared` Get all shared versions of one repository attribute ## 3.7.3 diff --git a/package-lock.json b/package-lock.json index 6eda84ce..3867f641 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "yamljs": "0.3.0" }, "devDependencies": { + "@apidevtools/swagger-parser": "^10.1.0", "@js-soft/eslint-config-ts": "1.6.6", "@js-soft/license-check": "1.0.9", "@nmshd/connector-sdk": "*", @@ -70,7 +71,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "typescript": "^5.3.3", - "typescript-rest-swagger": "github:nmshd/typescript-rest-swagger#1.1.7" + "typescript-rest-swagger": "github:nmshd/typescript-rest-swagger#1.2.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -108,6 +109,72 @@ "node": ">=6.0.0" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz", + "integrity": "sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==", + "dev": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.0.tgz", + "integrity": "sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw==", + "dev": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "9.0.6", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "ajv": "^8.6.3", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@apitools/openapi-parser": { "version": "0.0.30", "resolved": "https://registry.npmjs.org/@apitools/openapi-parser/-/openapi-parser-0.0.30.tgz", @@ -1685,6 +1752,12 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==" }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true + }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", @@ -3134,6 +3207,20 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ajv-errors": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", @@ -8994,6 +9081,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "peer": true + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", diff --git a/package.json b/package.json index 25952d85..925a23b4 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "yamljs": "0.3.0" }, "devDependencies": { + "@apidevtools/swagger-parser": "^10.1.0", "@js-soft/eslint-config-ts": "1.6.6", "@js-soft/license-check": "1.0.9", "@nmshd/connector-sdk": "*", @@ -117,7 +118,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "typescript": "^5.3.3", - "typescript-rest-swagger": "github:nmshd/typescript-rest-swagger#1.1.7" + "typescript-rest-swagger": "github:nmshd/typescript-rest-swagger#1.2.1" }, "overrides": { "typescript-rest": { diff --git a/test/spec.test.ts b/test/spec.test.ts index c12bd992..3bad4daa 100644 --- a/test/spec.test.ts +++ b/test/spec.test.ts @@ -3,11 +3,15 @@ import yamljs from "yamljs"; import { MetadataGenerator, SpecGenerator, Swagger } from "typescript-rest-swagger"; +import swaggerParser from "@apidevtools/swagger-parser"; import tsConfigBase from "../tsconfig.json"; describe("test openapi spec against routes", () => { - test("all route names should match the generated ones", async () => { - const manualOpenApiSpec: Swagger.Spec = yamljs.load("src/modules/coreHttpApi/openapi.yml"); + let manualOpenApiSpec: Swagger.Spec; + let generatedOpenApiSpec: Swagger.Spec; + + beforeAll(async () => { + manualOpenApiSpec = yamljs.load("src/modules/coreHttpApi/openapi.yml"); const files = "src/modules/**/*.ts"; @@ -26,22 +30,33 @@ describe("test openapi spec against routes", () => { yaml: false }; const generator = new SpecGenerator(metadata, defaultOptions); - const generatedOpenApiSpec: Swagger.Spec = await generator.getOpenApiSpec(); + generatedOpenApiSpec = await generator.getOpenApiSpec(); + generatedOpenApiSpec = (await swaggerParser.dereference(generatedOpenApiSpec as any)) as Swagger.Spec; + manualOpenApiSpec = (await swaggerParser.dereference(manualOpenApiSpec as any)) as Swagger.Spec; harmonizeSpec(manualOpenApiSpec); harmonizeSpec(generatedOpenApiSpec); - - const manualPaths = Object.keys(manualOpenApiSpec.paths); - const generatedPaths = Object.keys(generatedOpenApiSpec.paths); + }); + test("all route names should match the generated ones", () => { + const manualPaths = getPaths(manualOpenApiSpec); + const generatedPaths = getPaths(generatedOpenApiSpec); generatedPaths.forEach((path) => { expect(manualPaths).toContain(path); }); - // Paths not defined in the typescript-rest way - const ignorePaths = ["/health", "/Monitoring/Version", "/Monitoring/Requests", "/Monitoring/Support"]; - // Paths to ignroe in regard to return code consistencie (Post requests that return 200 due to no creation) + manualPaths.forEach((path) => { + if (ignorePaths.includes(path)) { + return; + } + + expect(generatedPaths).toContain(path); + }); + }); + test("all routes should have the same HTTP methods", () => { + const manualPaths = getPaths(manualOpenApiSpec); + // Paths to ignore in regard to return code consistency (Post requests that return 200 due to no creation) /* eslint-disable @typescript-eslint/naming-convention */ - const returnCodeOvererite: Record = { + const returnCodeOverwrite: Record = { "/api/v2/Account/Sync": "200", "/api/v2/Attributes/ExecuteIQLQuery": "200", "/api/v2/Attributes/ValidateIQLQuery": "200", @@ -53,9 +68,6 @@ describe("test openapi spec against routes", () => { if (ignorePaths.includes(path)) { return; } - - expect(generatedPaths).toContain(path); - const generatedMethods = Object.keys(generatedOpenApiSpec.paths[path]) .map((method) => method.toLocaleLowerCase()) .sort(); @@ -69,11 +81,59 @@ describe("test openapi spec against routes", () => { const key = method as "get" | "put" | "post" | "delete" | "options" | "head" | "patch"; const manualResponses = Object.keys(manualOpenApiSpec.paths[path][key]?.responses ?? {}); let expectedResponseCode = key === "post" ? "201" : "200"; - expectedResponseCode = returnCodeOvererite[path] ?? expectedResponseCode; + expectedResponseCode = returnCodeOverwrite[path] ?? expectedResponseCode; expect(manualResponses, `Path ${path} and method ${method} does not contain response code ${expectedResponseCode}`).toContainEqual(expectedResponseCode); }); }); }); + + test("all generated params should be in the manual spec", () => { + const pathsWithDBQueries = [ + { path: "/api/v2/Attributes/Own/Shared/Identity", method: "get" }, + { path: "/api/v2/Attributes/Peer/Shared/Identity", method: "get" }, + { path: "/api/v2/Attributes/Own/Shared/Identity", method: "get" } + ]; + + const generatedPaths = getPaths(generatedOpenApiSpec); + generatedPaths.forEach((path) => { + const generatedMethods = Object.keys(generatedOpenApiSpec.paths[path]) + .map((method) => method.toLowerCase()) + .sort() as (keyof Swagger.Path)[]; + generatedMethods.forEach((method: keyof Swagger.Path) => { + const generatedOperation = generatedOpenApiSpec.paths[path][method]; + if (!isOperation(generatedOperation) || !generatedOperation.parameters) { + return; + } + + const manualOperation = manualOpenApiSpec.paths[path][method]; + if (!isOperation(manualOperation) || !manualOperation.parameters) { + throw new Error(`${path} ${method} does not contain parameters but generated do`); + } + + // DBQuery are used via context.query and not by injection as QueryParameter so they will not be generated and the length will be different + if (!pathsWithDBQueries.some((p) => p.path === path && p.method.toLowerCase() === method.toLowerCase())) { + // eslint-disable-next-line jest/no-conditional-expect + expect(generatedOperation.parameters, `Parameter length for ${method.toUpperCase()} ${path} is wrong`).toHaveLength(manualOperation.parameters.length); + } + + const manualPathParams = manualOperation.parameters.filter((param) => param.in === "path"); + const generatedPathParams = generatedOperation.parameters.filter((param) => param.in === "path"); + expect(generatedPathParams).toHaveLength(manualPathParams.length); + + generatedOperation.parameters + .filter((param) => param.in === "query") + .forEach((param) => { + const manualParameter = manualOperation.parameters!.find((manualParam) => manualParam.name === param.name); + + expect(manualParameter, `${path} ${method} should contain param with name ${param.name}`).toBeDefined(); + + expect(param.name).toBe(manualParameter!.name); + expect(param.in).toBe(manualParameter!.in); + expect(param.required).toBe(manualParameter!.required); + }); + }); + }); + }); }); function harmonizeSpec(spec: any) { @@ -85,3 +145,13 @@ function harmonizeSpec(spec: any) { } } } + +// Paths not defined in the typescript-rest way +const ignorePaths = ["/health", "/Monitoring/Version", "/Monitoring/Requests", "/Monitoring/Support"]; +function getPaths(spec: Swagger.Spec) { + return Object.keys(spec.paths).filter((paths) => !ignorePaths.includes(paths)); +} + +function isOperation(obj: any): obj is Swagger.Operation { + return obj.responses !== undefined; +}