diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 4095e3e4..7d34f28b 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -43,5 +43,18 @@ jobs: DEBUG: openapi-cop:* CI: true + - if: startsWith(matrix.node-version, '12') + name: Test Docker image + run: npm run test:docker + env: + DEBUG: openapi-cop:* + CI: true + + - name: Dump docker logs on failure + if: failure() && startsWith(matrix.node-version, '12') + uses: jwalton/gh-docker-logs@v2 + with: + images: 'lxlu/openapi-cop:test' + - name: Analyze dependencies run: npm run test:deps diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 00000000..9c9607ca --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,11 @@ +{ + "bail": true, + "timeout": 60000, + "exit": true, + "spec": [ + "build/test/*.test.js" + ], + "require": [ + "dotenv/config" + ] +} diff --git a/.nvmrc b/.nvmrc index f599e28b..48082f72 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10 +12 diff --git a/docker/Dockerfile b/docker/Dockerfile index cc18665d..5d66c63d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,22 +1,23 @@ -FROM node:10 AS builder +FROM node:12 AS builder WORKDIR /app COPY . . -RUN npm ci --unsafe-perm +RUN npm ci --no-optional --unsafe-perm RUN npm run compile -FROM node:10 AS main +FROM node:12 AS main ENV TARGET "" ENV FILE "" ENV DEFAULT_FORBID_ADDITIONAL_PROPERTIES "" ENV SILENT "" ENV VERBOSE "" +ENV CLI_ARGUMENTS "" ENV NODE_ENV "production" WORKDIR /openapi-cop-docker COPY package.json . COPY package-lock.json . -RUN npm ci --omit=dev +RUN npm ci --omit=dev --no-optional COPY --from=builder /app/build . COPY docker/entrypoint.bash . diff --git a/docker/README.md b/docker/README.md index c14c174e..ab133eed 100644 --- a/docker/README.md +++ b/docker/README.md @@ -32,6 +32,7 @@ same [openapi-cop CLI flags](https://github.com/EXXETA/openapi-cop#cli-usage): are not allowed. - `SILENT`: When set, the proxy will forward response bodies unchanged and only set validation headers. - `VERBOSE`: When set, activates verbose output. +- `CLI_ARGUMENTS`: Sets all the arguments at once and overrides any other option given. - `NODE_ENV` (default: "production"): When set to "development", stack traces will also be logged. ### Example diff --git a/docker/entrypoint.bash b/docker/entrypoint.bash index 4250954b..97fcb9c4 100644 --- a/docker/entrypoint.bash +++ b/docker/entrypoint.bash @@ -22,4 +22,8 @@ if [ -n "$VERBOSE" ]; then cli_args="${cli_args}--verbose " fi +if [ -n "$CLI_ARGUMENTS" ]; then + cli_args="$CLI_ARGUMENTS" +fi + node src/cli --host 0.0.0.0 $cli_args diff --git a/mock-server/src/app.ts b/mock-server/src/app.ts index a9243417..2a4268ab 100755 --- a/mock-server/src/app.ts +++ b/mock-server/src/app.ts @@ -139,8 +139,6 @@ export async function runApp({ port, apiDocFile }: MockOptions): Promise { resolve(server); }); - }).then(() => { - return server; }); } catch (error) { console.error('Failed to run mock server', error); diff --git a/mock-server/tslint.json b/mock-server/tslint.json deleted file mode 100755 index 305de38c..00000000 --- a/mock-server/tslint.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "gts/tslint.json", - "linterOptions": { - "exclude": ["node_modules/**"] - }, - "rules": { - "no-any": false, - "quotemark": [true, "single", "avoid-escape", "avoid-template"] - } -} diff --git a/package-lock.json b/package-lock.json index f73597a0..6c9301f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,6 @@ "js-yaml": "^4.1.0" }, "dependencies": { - "@types/json-schema": { - "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==" - }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -76,9 +71,9 @@ "integrity": "sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg==" }, "validator": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", - "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", + "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==" }, "z-schema": { "version": "4.2.4", @@ -103,9 +98,9 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", - "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", "dev": true }, "@babel/highlight": { @@ -178,12 +173,12 @@ } }, "@babel/runtime-corejs3": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.0.tgz", - "integrity": "sha512-JyXXoCu1N8GLuKc2ii8y5RGma5FMpFeO2nAQIe0Yzrbq+rQnN+sFj47auLblR5ka6aHNGPDgv8G/iI2Grb0ldQ==", + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.20.13.tgz", + "integrity": "sha512-p39/6rmY9uvlzRiLZBIB3G9/EBr66LBMcYm7fIDeSBNdRjF2AGD3rFZucUyAgGHC2N+7DdLvVi33uTjSE44FIw==", "requires": { - "core-js-pure": "^3.20.2", - "regenerator-runtime": "^0.13.4" + "core-js-pure": "^3.25.1", + "regenerator-runtime": "^0.13.11" } }, "@cloudflare/json-schema-walker": { @@ -218,9 +213,9 @@ } }, "@exxeta/openapi-cop-mock-server": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@exxeta/openapi-cop-mock-server/-/openapi-cop-mock-server-1.0.1.tgz", - "integrity": "sha512-t5e4XvK+9imy66x3tPAYZC/m312Qv7/G7EgbsgsC0knn58C5PzTEbPGRK4VwTKjiJljyfGuig3T5L8qLukUKmQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@exxeta/openapi-cop-mock-server/-/openapi-cop-mock-server-1.1.0.tgz", + "integrity": "sha512-iZbY/ec1kSmBL7wZumJn15cHRpR1GHx0n3BQ6avi393tC9V3FGIUo0PKX1ZwYWq1PCtNg8EsTbRLH2+mKBuzAg==", "dev": true, "requires": { "chalk": "4.1.2", @@ -318,9 +313,9 @@ } }, "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" }, "@sideway/pinpoint": { "version": "2.0.0", @@ -334,6 +329,13 @@ "requires": { "@types/connect": "*", "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + } } }, "@types/caseless": { @@ -354,6 +356,13 @@ "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", "requires": { "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + } } }, "@types/content-type": { @@ -383,13 +392,20 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.30", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz", - "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==", + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", "requires": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*" + }, + "dependencies": { + "@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + } } }, "@types/js-yaml": { @@ -401,13 +417,12 @@ "@types/json-schema": { "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 + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" }, "@types/lodash": { - "version": "4.14.184", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.184.tgz", - "integrity": "sha512-RoZphVtHbxPZizt4IcILciSWiC6dcn+eZ8oX9IWEYfDMcocdd42f7NPI6fQj+6zI8y4E0L7gu2pcZKLGTRaV9Q==", + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", "dev": true }, "@types/lodash.merge": { @@ -445,7 +460,8 @@ "@types/node": { "version": "10.17.60", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true }, "@types/normalize-package-data": { "version": "2.4.1", @@ -498,9 +514,9 @@ } }, "@types/semver": { - "version": "7.3.12", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.12.tgz", - "integrity": "sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==", + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, "@types/serve-static": { @@ -510,6 +526,13 @@ "requires": { "@types/mime": "*", "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + } } }, "@types/swagger-parser": { @@ -897,9 +920,9 @@ } }, "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -946,9 +969,9 @@ "optional": true }, "apib2swagger": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/apib2swagger/-/apib2swagger-1.15.0.tgz", - "integrity": "sha512-zhKPRwXLPwdNrHzCMe+Y8/EmP88urqCrGvpe3Akmt+XoUj4rgeBI1PNxMabVoIxAJIiP9RkYkPD1sD0N68XDng==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/apib2swagger/-/apib2swagger-1.16.1.tgz", + "integrity": "sha512-yNq5Eq/654WJ1K43f6W2dPmTfTFqrKQzFLZdnxSHsI8V0xW84NNH5MeFCmp3Ioocl9E8wztzmqo6FWhV62gqMQ==", "optional": true, "requires": { "apib-include-directive": "^0.1.0", @@ -1035,9 +1058,9 @@ "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" }, "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, "axios": { "version": "0.19.2", @@ -1207,9 +1230,9 @@ } }, "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha512-wCyFsDQkKPwwF8BDwOiWNx/9K45L/hvggQiDbve+viMNMQnWhrlYIuBk09offfwCRtCO9P6XwUttufzU11WCVw==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" }, "callsites": { "version": "3.1.0", @@ -1430,9 +1453,9 @@ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" }, "cookie-signature": { "version": "1.0.6", @@ -1440,9 +1463,9 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==" + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" }, "core-js": { "version": "2.6.12", @@ -1450,9 +1473,9 @@ "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" }, "core-js-pure": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.1.tgz", - "integrity": "sha512-7Fr74bliUDdeJCBMxkkIuQ4xfxn/SwrVg+HkJUAoNEXVqYLv55l6Af0dJ5Lq2YBUW9yKqSkLXaS5SYPK6MGa/A==" + "version": "3.27.2", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.27.2.tgz", + "integrity": "sha512-Cf2jqAbXgWH3VVzjyaaFkY1EBazxugUepGymDoeteyYr9ByX51kD2jdHZlsEF/xnJMyN3Prua7mQuzwMg6Zc9A==" }, "core-util-is": { "version": "1.0.3", @@ -1518,9 +1541,9 @@ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" }, "decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", "dev": true, "requires": { "decamelize": "^1.1.0", @@ -1578,9 +1601,9 @@ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "requires": { "asap": "^2.0.0", "wrappy": "1" @@ -1618,6 +1641,12 @@ "esutils": "^2.0.2" } }, + "dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "dev": true + }, "drafter.js": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/drafter.js/-/drafter.js-2.6.7.tgz", @@ -1902,9 +1931,9 @@ "dev": true }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -2199,11 +2228,6 @@ "vary": "~1.1.2" }, "dependencies": { - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2291,9 +2315,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -2324,9 +2348,9 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -2451,9 +2475,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" }, "forever-agent": { "version": "0.6.1", @@ -2490,21 +2514,14 @@ } }, "formidable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", - "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz", + "integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==", "requires": { - "dezalgo": "1.0.3", - "hexoid": "1.0.0", - "once": "1.4.0", - "qs": "6.9.3" - }, - "dependencies": { - "qs": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", - "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==" - } + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" } }, "forwarded": { @@ -2571,9 +2588,9 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", - "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -2617,9 +2634,9 @@ } }, "globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -3008,9 +3025,9 @@ } }, "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, "import-fresh": { @@ -3151,9 +3168,9 @@ } }, "is-core-module": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", - "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "requires": { "has": "^1.0.3" } @@ -3301,9 +3318,9 @@ "optional": true }, "joi": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz", - "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==", + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.7.0.tgz", + "integrity": "sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==", "requires": { "@hapi/hoek": "^9.0.0", "@hapi/topo": "^5.0.0", @@ -3385,9 +3402,9 @@ "optional": true }, "json-schema-faker": { - "version": "0.5.0-rcv.44", - "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.0-rcv.44.tgz", - "integrity": "sha512-MbDxYFsPXTVMawW1Y6zEU7QhfwsT+ZJ2d+LI8n57Y8+Xw1Cdx1hITgsFTLNOJ1lDMHZqWeXGGgMbc1hW0BGisg==", + "version": "0.5.0-rcv.46", + "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.0-rcv.46.tgz", + "integrity": "sha512-Q+sGrxptZfezwm7M9W9VmHT9E8s5fWPCaRC4J2zUjb3CmDsxokiCBdHdS/psu91Tafc/ITv+GtIztGzUVT2zIg==", "requires": { "json-schema-ref-parser": "^6.1.0", "jsonpath-plus": "^5.1.0" @@ -3439,9 +3456,9 @@ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "jsonpath": { @@ -3572,9 +3589,9 @@ } }, "loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", "dev": true, "requires": { "get-func-name": "^2.0.0" @@ -3897,9 +3914,9 @@ } }, "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, "minimist-options": { "version": "4.1.0", @@ -4349,9 +4366,9 @@ "optional": true }, "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" }, "on-finished": { "version": "2.4.1", @@ -4416,6 +4433,11 @@ "qs": "^6.9.3" }, "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, "openapi-types": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-7.2.3.tgz", @@ -4627,9 +4649,9 @@ "optional": true }, "prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz", + "integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==", "dev": true }, "prettier-linter-helpers": { @@ -4674,9 +4696,9 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" }, "q": { "version": "0.9.7", @@ -4912,9 +4934,9 @@ } }, "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "regexpp": { "version": "3.2.0", @@ -5048,9 +5070,9 @@ } }, "rxjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", - "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", + "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", "requires": { "tslib": "^2.1.0" } @@ -5453,14 +5475,6 @@ "semver": "^7.3.7" }, "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -5479,11 +5493,6 @@ "yallist": "^4.0.0" } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -5495,9 +5504,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "requires": { "lru-cache": "^6.0.0" } @@ -5542,11 +5551,6 @@ "url": "~0.11.0" }, "dependencies": { - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" - }, "is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -5663,9 +5667,9 @@ } }, "table": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", - "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", "dev": true, "requires": { "ajv": "^8.0.1", @@ -5676,9 +5680,9 @@ }, "dependencies": { "ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -5782,9 +5786,9 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha512-kdf4JKs8lbARxWdp7RKdNzoJBhGUcIalSYibuGyHJbmk40pOysQ0+QPvlkCOICOivDWU2IJo2rkrxyTK2AH4fw==" + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", + "integrity": "sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==" }, "trim-newlines": { "version": "3.0.1", @@ -5793,9 +5797,9 @@ "dev": true }, "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, "tsutils": { "version": "3.21.0", @@ -5873,9 +5877,9 @@ } }, "typescript": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz", - "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true }, "underscore": { diff --git a/package.json b/package.json index 8d27767e..d87355c4 100755 --- a/package.json +++ b/package.json @@ -34,8 +34,9 @@ "dev-start": "npm run compile && node build/src/cli --port 8888 --target \"http://localhost:8889\" --file", "dev-start-along-mock": "(export DEBUG=openapi-cop:* || set DEBUG=openapi-cop:*) && npm run compile && node build/test/scripts/spawn", "pretest": "npm run compile", - "test": "mocha --bail --timeout 60000 --exit build/test/*.test.js", - "test:docker": "bash ./test/docker/run-docker-test.bash", + "pretest:docker": "npm run pretest && bash ./test/docker/build-docker-test.bash", + "test": "DOTENV_CONFIG_PATH=./test/config.env mocha", + "test:docker": "DOTENV_CONFIG_PATH=./test/docker/config.env mocha --timeout 120000", "test:deps": "npx depcheck --ignores=\"@types/*\"", "posttest": "npm run check", "package": "npx yarn pack --filename openapi-cop.tgz" @@ -80,11 +81,12 @@ "@typescript-eslint/parser": "5.41.0", "axios": "0.19.2", "chai": "4.3.6", + "dotenv": "^16.0.3", "eslint": "7.32.0", "find-process": "1.4.7", "gts": "3.1.1", "mocha": "9.2.2", - "@exxeta/openapi-cop-mock-server": "1.0.1", - "typescript": "4.0.5" + "@exxeta/openapi-cop-mock-server": "1.1.0", + "typescript": "4.8.4" } } diff --git a/src/app.ts b/src/app.ts index 909d688b..9f19da75 100755 --- a/src/app.ts +++ b/src/app.ts @@ -1,17 +1,18 @@ +import chalk = require('chalk'); + const debug = require('debug')('openapi-cop:proxy'); debug.log = console.log.bind(console); // output to stdout -import chalk = require('chalk'); import * as express from 'express'; -import {Request, Response} from 'express'; +import { Request, Response } from 'express'; import * as http from 'http'; -import {Operation} from 'openapi-backend'; +import { Operation } from 'openapi-backend'; import * as path from 'path'; import * as crypto from 'crypto'; import * as validUrl from 'valid-url'; import * as rp from 'request-promise-native'; import * as errors from 'request-promise-native/errors'; -import {ValidationResults} from '../types/validation'; +import { ValidationResults } from '../types/validation'; import { convertToOpenApiV3, copyHeaders, @@ -23,8 +24,8 @@ import { setValidationHeader, toOasRequest, } from './util'; -import {dereference, hasErrors, resolve, Validator} from './validation'; -import {URL} from "url"; +import { dereference, hasErrors, resolve, Validator } from './validation'; +import { URL } from 'url'; interface BuildOptions { targetUrl: string; @@ -40,10 +41,15 @@ const defaults: BuildOptions = { silent: false, }; -interface ProxyOptions { - port: number; +export type ProxyOptions = BaseProxyOptions & ExtendedProxyOptions; + +export interface BaseProxyOptions { + port: string | number; host: string; targetUrl: string; +} + +export interface ExtendedProxyOptions { apiDocPath: string; defaultForbidAdditionalProperties?: boolean; silent?: boolean; @@ -55,7 +61,7 @@ interface ProxyOptions { export async function buildApp( options: BuildOptions, ): Promise { - const {targetUrl, apiDocPath, defaultForbidAdditionalProperties, silent} = { + const { targetUrl, apiDocPath, defaultForbidAdditionalProperties, silent } = { ...defaults, ...options, }; @@ -69,11 +75,11 @@ export async function buildApp( console.log( chalk.blue( 'Validating against ' + - chalk.bold( - `${path.basename(apiDocPath)} ("${rawApiDoc.info.title}", version: ${ - rawApiDoc.info.version - })`, - ), + chalk.bold( + `${path.basename(apiDocPath)} ("${rawApiDoc.info.title}", version: ${ + rawApiDoc.info.version + })`, + ), ), ); @@ -85,12 +91,16 @@ export async function buildApp( ); } - const apiDoc = await prepareApiDocument(rawApiDoc, apiDocPath, defaultForbidAdditionalProperties); + const apiDoc = await prepareApiDocument( + rawApiDoc, + apiDocPath, + defaultForbidAdditionalProperties, + ); const oasValidator: Validator = new Validator(apiDoc); // Consume raw request body - app.use(express.raw({type: '*/*'})); + app.use(express.raw({ type: '*/*' })); // Global route handler app.all('*', (req: Request, res: Response) => { @@ -146,17 +156,18 @@ export async function buildApp( statusCode, ); - validationResults.responseHeaders = oasValidator.validateResponseHeaders( - serverResponse.headers, - operation as Operation, - statusCode, - ); + validationResults.responseHeaders = + oasValidator.validateResponseHeaders( + serverResponse.headers, + operation as Operation, + statusCode, + ); copyHeaders(serverResponse, res); setValidationHeader(res, validationResults); debug( `Validation results [${oasRequest.method} ${oasRequest.path}] ` + - JSON.stringify(validationResults, null, 2), + JSON.stringify(validationResults, null, 2), ); if (silent || !hasErrors(validationResults)) { @@ -187,7 +198,7 @@ export async function buildApp( setValidationHeader(res, validationResults); debug( `Validation results [${oasRequest.method} ${oasRequest.path}] ` + - JSON.stringify(validationResults, null, 2), + JSON.stringify(validationResults, null, 2), ); if (!reason.response && reason instanceof errors.RequestError) { @@ -232,13 +243,13 @@ export async function buildApp( * the server response untouched */ export async function runProxy({ - port, - host, - targetUrl, - apiDocPath, - defaultForbidAdditionalProperties = false, - silent = false, - }: ProxyOptions): Promise { + port, + host, + targetUrl, + apiDocPath, + defaultForbidAdditionalProperties = false, + silent = false, +}: ProxyOptions): Promise { try { const app = await buildApp({ targetUrl, @@ -247,8 +258,8 @@ export async function runProxy({ silent, }); let server: http.Server; - return new Promise(resolve => { - server = app.listen(port, host, () => { + return new Promise((resolve) => { + server = app.listen(+port, host, () => { resolve(server); }); }); @@ -258,18 +269,22 @@ export async function runProxy({ } } -async function prepareApiDocument(rawApiDoc: any, apiDocPath: string, defaultForbidAdditionalProperties: boolean | undefined): Promise { +async function prepareApiDocument( + rawApiDoc: any, + apiDocPath: string, + defaultForbidAdditionalProperties: boolean | undefined, +): Promise { const apiDocConv = await convertToOpenApiV3(rawApiDoc, apiDocPath).catch( - err => { + (err) => { throw new Error(`Could not convert document to OpenAPI v3: ${err}`); }, ); - const apiDocDeref = await dereference(apiDocConv, apiDocPath).catch(err => { + const apiDocDeref = await dereference(apiDocConv, apiDocPath).catch((err) => { throw new Error(`Reference resolution error: ${err}`); }); - let apiDoc = await resolve(apiDocDeref, apiDocPath).catch(err => { + let apiDoc = await resolve(apiDocDeref, apiDocPath).catch((err) => { throw new Error(`Reference resolution error: ${err}`); }); @@ -284,11 +299,28 @@ async function prepareApiDocument(rawApiDoc: any, apiDocPath: string, defaultFor } // Ensure every operation has a operationId (required by openapi-backend validator, but not by OpenAPI v3 schema). [Issue #4] - if (traversalPath.length === 3 - && traversalPath[0] === 'paths' - && ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(traversalPath[2])) { + if ( + traversalPath.length === 3 && + traversalPath[0] === 'paths' && + [ + 'get', + 'put', + 'post', + 'delete', + 'options', + 'head', + 'patch', + 'trace', + ].includes(traversalPath[2]) + ) { if (obj.operationId === undefined) { - obj.operationId = 'generatedOperationId_' + traversalPath[1].slice(1) + '_' + traversalPath[2] + '_' + crypto.randomBytes(3).toString('hex'); + obj.operationId = + 'generatedOperationId_' + + traversalPath[1].slice(1) + + '_' + + traversalPath[2] + + '_' + + crypto.randomBytes(3).toString('hex'); } } diff --git a/src/util.ts b/src/util.ts index dfaf9650..16780c11 100755 --- a/src/util.ts +++ b/src/util.ts @@ -8,10 +8,10 @@ import * as yaml from 'js-yaml'; import { Request as OasRequest } from 'openapi-backend'; import * as path from 'path'; import * as qs from 'qs'; -import * as waitOn from 'wait-on'; import { ResponseParsingError } from '../types/errors'; import { ValidationResults } from '../types/validation'; import * as rp from 'request-promise-native'; +import { flatMap } from 'lodash'; function isSwaggerV2(apiDoc: any): boolean { return apiDoc.swagger === '2.0'; @@ -65,7 +65,7 @@ export function readFileSync(filePath: string): any { } export async function fetchAndReadFile(uri: string): Promise { - return rp(uri).then(responseBody => parseJsonOrYaml(uri, responseBody)); + return rp(uri).then((responseBody) => parseJsonOrYaml(uri, responseBody)); } /** @@ -209,19 +209,6 @@ export function setSourceRequestHeader( res.setHeader('openapi-cop-source-request', JSON.stringify(oasRequest)); } -/** Closes the server and waits until the port is again free. */ -export async function closeServer(server: http.Server): Promise { - const port = (server.address() as any).port; - await new Promise((resolve, reject) => { - server.close(err => { - if (err) return reject(err); - resolve(); - }); - }); - - await waitOn({ resources: [`http://localhost:${port}`], reverse: true }); -} - /** * Recursively maps a nested object (JSON) given a mapping function. Maps in * depth-first order. If it finds an array it applies the mapping function @@ -231,7 +218,11 @@ export async function closeServer(server: http.Server): Promise { * @param fn Mapping function that returns the new value. * @param traversalPath internal parameter used to track the current traversal path */ -export function mapWalkObject(obj: any, fn: (currentObj: any, traversalPath: Array) => any, traversalPath: Array = []): any { +export function mapWalkObject( + obj: any, + fn: (currentObj: any, traversalPath: Array) => any, + traversalPath: Array = [], +): any { let objCopy = Object.assign({}, obj); for (const key in obj) { if (!Object.prototype.hasOwnProperty.call(obj, key)) continue; @@ -251,3 +242,28 @@ export function mapWalkObject(obj: any, fn: (currentObj: any, traversalPath: Arr objCopy = fn(objCopy, traversalPath); return objCopy; } + +export interface CliFlags { + flag: string; + value?: string | boolean; +} + +/** + * Receives a list of optional CLI flags and maps them to a list of strings. + * Flags are not included if their value is not a string or equal to `true` (boolean flag). + * + * @param flags + */ +export function buildCliArguments(flags: Array): Array { + const effectiveFlags = flags.filter( + ({ value }) => typeof value === 'string' || value === true, + ); + + return flatMap(effectiveFlags, ({ flag, value }) => { + const args = [flag]; + if (typeof value === 'string') { + args.push(value); + } + return args; + }); +} diff --git a/test/01.unit.test.ts b/test/01.unit.test.ts index fe1635b3..f096bbf4 100755 --- a/test/01.unit.test.ts +++ b/test/01.unit.test.ts @@ -4,7 +4,7 @@ import { readJsonOrYamlSync } from '../src/util'; import { validateDocument } from '../src/validation'; import { SCHEMAS_DIR } from './config'; -import { readDirFilesSync } from './util/io'; +import { getFileName, readDirFilesSync } from './util/io'; import { assertThrowsAsync } from './util/testing'; // tslint:disable: only-arrow-functions @@ -13,10 +13,7 @@ describe('Loading and validation of OpenAPI schemas', function() { const schemaDir = path.join(SCHEMAS_DIR, 'v3'); process.chdir(schemaDir); for (const filePath of readDirFilesSync(schemaDir)) { - const fileName = path - .normalize(path.basename(filePath)) - .replace(/\\/g, '/'); - it(`should be able to load valid openapi v3 schemas: ${fileName}`, async function() { + it(`should be able to load valid openapi v3 schemas: ${getFileName(filePath)}`, async function() { const apiDoc = readJsonOrYamlSync(filePath); await validateDocument(apiDoc); }); @@ -27,10 +24,7 @@ describe('Loading and validation of OpenAPI schemas', function() { const schemaDir = path.join(SCHEMAS_DIR, 'v2'); process.chdir(schemaDir); for (const filePath of readDirFilesSync(schemaDir)) { - const fileName = path - .normalize(path.basename(filePath)) - .replace(/\\/g, '/'); - it(`should be able to load valid openapi v2 schemas: ${fileName}`, async function() { + it(`should be able to load valid openapi v2 schemas: ${getFileName(filePath)}`, async function() { const apiDoc = readJsonOrYamlSync(filePath); await validateDocument(apiDoc); }); @@ -41,10 +35,7 @@ describe('Loading and validation of OpenAPI schemas', function() { const schemaDir = path.join(SCHEMAS_DIR, 'invalid'); process.chdir(schemaDir); for (const filePath of readDirFilesSync(schemaDir)) { - const fileName = path - .normalize(path.basename(filePath)) - .replace(/\\/g, '/'); - it(`should fail to load invalid openapi schemas: ${fileName}`, async function() { + it(`should fail to load invalid openapi schemas: ${getFileName(filePath)}`, async function() { const apiDoc = readJsonOrYamlSync(filePath); await assertThrowsAsync( async () => validateDocument(apiDoc), diff --git a/test/02.integration.test.ts b/test/02.integration.test.ts index ecb06b62..1baa4935 100755 --- a/test/02.integration.test.ts +++ b/test/02.integration.test.ts @@ -1,75 +1,107 @@ // tslint:disable: only-arrow-functions -import {assert} from 'chai'; +import { assert } from 'chai'; import * as path from 'path'; -import {INVALID_TEST_REQUESTS, STRICTLY_INVALID_TEST_REQUESTS,} from './test-requests/invalid-requests'; -import {INVALID_RESPONSES, STRICTLY_INVALID_RESPONSES,} from './test-responses/invalid-responses'; -import {killProcesses} from './util/process'; -import {testRequestForEachFile, testRequestForEachFileWithServers,} from './util/testing'; -import {DEFAULT_OPENAPI_FILE, PROXY_PORT, SCHEMAS_DIR, TARGET_SERVER_PORT,} from './config'; -import {ChildProcess} from 'child_process'; -import {Readable} from 'stream'; -import axios, {AxiosRequestConfig} from 'axios'; -import {runProxy} from '../src/app'; -import {closeServer} from '../src/util'; -import {STRICTLY_VALID_TEST_REQUESTS, VALID_TEST_REQUESTS} from './test-requests/valid-requests'; -import {spawnProxyServer} from './util/server'; -import findProcess = require('find-process'); +import { + INVALID_TEST_REQUESTS, + STRICTLY_INVALID_TEST_REQUESTS, +} from './test-requests/invalid-requests'; +import { + INVALID_RESPONSES, + STRICTLY_INVALID_RESPONSES, +} from './test-responses/invalid-responses'; +import { + AssertionFunction, + testRequestsForApiDoc, + testRequestsForEachApiDoc, + testResponsesForEachApiDoc, +} from './util/testing'; +import { + DEFAULT_OPENAPI_FILE, + PROXY_PORT, + SCHEMAS_DIR, + SERVER_RUNTIME, + TARGET_SERVER_PORT, +} from './config'; +import axios, { AxiosRequestConfig } from 'axios'; +import { + STRICTLY_VALID_TEST_REQUESTS, + VALID_TEST_REQUESTS, +} from './test-requests/valid-requests'; +import { + DockerServerOrchestrator, + NodeHttpServerOrchestrator, + ServerOrchestrator, +} from './util/server-orchestrator'; +import { URL } from 'url'; +import { Server } from 'http'; +import { ChildProcess } from 'child_process'; -describe('integration.test.js', function () { +let serverOrchestrator: ServerOrchestrator; +if (SERVER_RUNTIME === 'docker') { + serverOrchestrator = new DockerServerOrchestrator( + new URL(`http://0.0.0.0:${PROXY_PORT}`), + new URL(`http://0.0.0.0:${TARGET_SERVER_PORT}`), + ); +} else { + serverOrchestrator = new NodeHttpServerOrchestrator( + new URL(`http://localhost:${PROXY_PORT}`), + new URL(`http://localhost:${TARGET_SERVER_PORT}`), + ); +} + +describe('integration.test.js', function() { this.slow(1000 * 15); // 15 seconds const contentType = 'application/json'; const clients = { proxy: axios.create({ baseURL: `http://localhost:${PROXY_PORT}`, - headers: {'content-type': contentType}, + headers: { 'content-type': contentType }, validateStatus: () => true, }), target: axios.create({ baseURL: `http://localhost:${TARGET_SERVER_PORT}`, - headers: {'content-type': contentType}, + headers: { 'content-type': contentType }, validateStatus: () => true, }), }; - before(async function () { - // Kill active processes listening on any of the given ports - const pid1 = await findProcess('port', PROXY_PORT); - const pid2 = await findProcess('port', TARGET_SERVER_PORT); - - await killProcesses([ - ...pid1.filter(p => p.cmd.indexOf('node') !== -1).map(p => p.pid), - ...pid2.filter(p => p.cmd.indexOf('node') !== -1).map(p => p.pid), - ]); + before(async function() { + if (process.env.CI?.toLowerCase() !== 'true') { + console.log('Killing existing processes...'); + return serverOrchestrator.kill(); + } }); - describe('OpenAPI v3', function () { + describe('OpenAPI v3', function() { const schemasDirV3 = path.join(SCHEMAS_DIR, 'v3'); - describe('Invariance tests', function () { - testRequestForEachFile({ + describe('Invariance tests', function() { + testRequestsForEachApiDoc({ testTitle: 'should return the same status and response bodies as the target server in silent mode', - dir: schemasDirV3, - testRequests: VALID_TEST_REQUESTS.v3, - client: clients, - silent: true, - callback(proxyRes, targetRes) { + apiDocDirectory: schemasDirV3, + testRequestMap: VALID_TEST_REQUESTS.v3, + clients, + serverOrchestrator, + proxyOptions: { silent: true }, + test: (proxyRes, targetRes) => { assert.deepStrictEqual(proxyRes.data, targetRes.data); assert.equal(proxyRes.status, targetRes.status); }, }); - testRequestForEachFile({ + testRequestsForEachApiDoc({ testTitle: 'should return the same headers as the target server except from the openapi-cop headers in silent mode', - dir: schemasDirV3, - testRequests: VALID_TEST_REQUESTS.v3, - client: clients, - silent: true, - callback(proxyRes, targetRes) { + apiDocDirectory: schemasDirV3, + testRequestMap: VALID_TEST_REQUESTS.v3, + clients, + serverOrchestrator, + proxyOptions: { silent: true }, + test: (proxyRes, targetRes) => { assert.property(proxyRes.headers, 'openapi-cop-validation-result'); assert.property(proxyRes.headers, 'openapi-cop-source-request'); delete proxyRes.headers['openapi-cop-validation-result']; @@ -87,53 +119,53 @@ describe('integration.test.js', function () { }); }); - it('should return the source request object inside the response header', async function () { - console.log('Starting proxy server...'); - const server = await runProxy({ - port: PROXY_PORT, - host: 'localhost', - targetUrl: `http://localhost:${TARGET_SERVER_PORT}`, + { + let headerTestRequest: AxiosRequestConfig; + testRequestsForApiDoc({ + testTitle: + 'should return the source request object inside the response header', apiDocPath: DEFAULT_OPENAPI_FILE, - defaultForbidAdditionalProperties: false, - }); - - const originalRequest: AxiosRequestConfig = { - method: 'GET', - url: '/pets', - data: JSON.stringify({search: 'something'}), - }; - - const proxyResponse = await clients.proxy.request(originalRequest); - - const openapiCopRequest = JSON.parse( - proxyResponse.headers['openapi-cop-source-request'], - ); + testRequests: [ + (headerTestRequest = { + method: 'GET', + url: '/pets', + data: JSON.stringify({ search: 'something' }), + }), + ], + clients, + serverOrchestrator, + proxyOptions: { silent: true }, + test: (proxyResponse) => { + const openapiCopRequest = JSON.parse( + proxyResponse.headers['openapi-cop-source-request'], + ); - assert.deepStrictEqual(openapiCopRequest, { - method: originalRequest.method, - path: originalRequest.url, - body: JSON.parse(originalRequest.data), - query: {}, - headers: { - accept: 'application/json, text/plain, */*', - connection: 'close', - 'content-length': '22', - 'content-type': 'application/json', - host: 'localhost:8888', - 'user-agent': 'axios/0.19.2', + assert.deepStrictEqual(openapiCopRequest, { + method: headerTestRequest.method, + path: headerTestRequest.url, + body: JSON.parse(headerTestRequest.data), + query: {}, + headers: { + accept: 'application/json, text/plain, */*', + connection: 'close', + 'content-length': '22', + 'content-type': 'application/json', + host: 'localhost:8888', + 'user-agent': 'axios/0.19.2', + }, + }); }, }); + } - await closeServer(server); - }); - - testRequestForEachFile({ + testRequestsForEachApiDoc({ testTitle: 'should respond with validation headers that are ValidationResult', - dir: schemasDirV3, - testRequests: VALID_TEST_REQUESTS.v3, - client: clients, - callback(proxyRes) { + apiDocDirectory: schemasDirV3, + testRequestMap: VALID_TEST_REQUESTS.v3, + clients, + serverOrchestrator, + test: (proxyRes) => { const validationResults = JSON.parse( proxyRes.headers['openapi-cop-validation-result'], ); @@ -169,7 +201,7 @@ describe('integration.test.js', function () { ]; validationResults[k]['errors'].forEach((err: any) => { assert( - Object.keys(err).every(k => validKeys.includes(k)), // all keys are valid keys + Object.keys(err).every((k) => validKeys.includes(k)), // all keys are valid keys 'validation error elements should conform with Ajv.ValidationError', ); }); @@ -178,196 +210,146 @@ describe('integration.test.js', function () { }, }); - it('should fail when target server is not available', async function () { + it('should fail when target server is not available', async function() { this.timeout(10000); - console.log('Starting proxy server...'); - const ps: ChildProcess = await spawnProxyServer( - PROXY_PORT, - TARGET_SERVER_PORT, - DEFAULT_OPENAPI_FILE, - ); - - console.log('Reading output'); + await serverOrchestrator.withProxy({ + proxyOptions: { apiDocPath: DEFAULT_OPENAPI_FILE }, + task: async () => { + const proxyResponse = await clients.proxy.request({ + method: 'GET', + url: '/pets', + }); + assert.equal(proxyResponse.status, 500); + assert.isTrue(proxyResponse.data.includes('ECONNREFUSED')); - let output = ''; - (ps.stdout as Readable).on('data', (data: Buffer) => { - output += data; + const validationResults = JSON.parse( + proxyResponse.headers['openapi-cop-validation-result'], + ); - if (data.toString().includes('Proxying client request')) { - setTimeout(() => { - ps.kill(); - }, 1500); - } + assert.hasAllKeys(validationResults, ['request']); + assert.doesNotHaveAnyKeys(validationResults, ['response']); + }, }); + }); - clients.proxy.request({method: 'GET', url: '/pets'}); + const assertDoesNotHaveValidationErrors: AssertionFunction = (proxyRes) => { + const validationResults = JSON.parse( + proxyRes.headers['openapi-cop-validation-result'], + ); + const reqValidationResults = validationResults['request']; + assert.isTrue(reqValidationResults['valid']); + assert.isNull(reqValidationResults['errors']); + }; - return new Promise((resolve) => { - ps.on('exit', (code: number) => { - assert.notEqual(code, -1); - assert.isTrue( - output.includes('Validation results'), - 'should yield validation results', - ); - assert.isTrue( - output.includes('Target server is unreachable'), - 'should show failure to communicate with the target server', - ); - resolve(); - }); - }); - }); + const assertHasRequestValidationError: AssertionFunction = ( + proxyResponse, + targetResponse, + fileName, + expectedError, + ) => { + if (!expectedError) { + throw new Error( + 'Bad test: "expectedError" property should be set for test requests that check for validation errors', + ); + } + const validationResults = JSON.parse( + proxyResponse.headers['openapi-cop-validation-result'], + ); + const reqValidationResults = validationResults['request']; + assert.isNotTrue(reqValidationResults['valid']); + assert.isNotNull(reqValidationResults['errors']); + assert.isArray(reqValidationResults['errors']); + assert.lengthOf(reqValidationResults['errors'], 1); + for (const k of Object.keys(expectedError)) { + assert.deepEqual(reqValidationResults['errors'][0][k], expectedError[k]); + } + }; - testRequestForEachFile({ - testTitle: 'should NOT return any validation errors for valid REQuests', - dir: schemasDirV3, - testRequests: VALID_TEST_REQUESTS.v3, - client: clients, - callback(proxyRes) { - const validationResults = JSON.parse( - proxyRes.headers['openapi-cop-validation-result'], + const assertHasResponseValidationErrors: AssertionFunction = ( + proxyRes, + targetRes, + fileName, + expectedError, + ) => { + if (!expectedError) { + throw new Error( + 'Bad test: "expectedError" property should be set for test requests that check for validation errors', ); - const reqValidationResults = validationResults['request']; - assert.isTrue(reqValidationResults['valid']); - assert.isNull(reqValidationResults['errors']); - }, + } + const validationResults = JSON.parse( + proxyRes.headers['openapi-cop-validation-result'], + ); + const resValidationResults = validationResults['response']; + assert.isNotTrue(resValidationResults['valid'], 'Response should be invalid'); + assert.isNotNull( + resValidationResults['errors'], + 'There should be at least one error present', + ); + assert.isArray(resValidationResults['errors']); + for (const k of Object.keys(expectedError)) { + assert.deepEqual(resValidationResults['errors'][0][k], expectedError[k]); + } + }; + + testRequestsForEachApiDoc({ + testTitle: 'should NOT return any validation errors for valid requests', + apiDocDirectory: schemasDirV3, + testRequestMap: VALID_TEST_REQUESTS.v3, + clients, + serverOrchestrator, + test: assertDoesNotHaveValidationErrors, }); - testRequestForEachFile({ + testRequestsForEachApiDoc({ testTitle: - 'should NOT return any validation errors for strictly valid REQuests', - dir: schemasDirV3, - testRequests: STRICTLY_VALID_TEST_REQUESTS.v3, - client: clients, - callback(proxyRes) { - const validationResults = JSON.parse( - proxyRes.headers['openapi-cop-validation-result'], - ); - const reqValidationResults = validationResults['request']; - assert.isTrue(reqValidationResults['valid']); - assert.isNull(reqValidationResults['errors']); - }, + 'should NOT return any validation errors for strictly valid requests', + apiDocDirectory: schemasDirV3, + testRequestMap: STRICTLY_VALID_TEST_REQUESTS.v3, + clients, + serverOrchestrator, + test: assertDoesNotHaveValidationErrors, }); - testRequestForEachFile({ - testTitle: 'should return correct validation errors for invalid REQuests', - dir: schemasDirV3, - testRequests: INVALID_TEST_REQUESTS.v3, - client: clients, - callback(proxyRes, _targetRes, fileName, requestObject) { - const validationResults = JSON.parse( - proxyRes.headers['openapi-cop-validation-result'], - ); - const reqValidationResults = validationResults['request']; - assert.isNotTrue(reqValidationResults['valid']); - assert.isNotNull(reqValidationResults['errors']); - assert.isArray(reqValidationResults['errors']); - assert.lengthOf(reqValidationResults['errors'], 1); - assert.isDefined( - requestObject.expectedError, - '"expectedError" property should be set for test requests that check for validation errors', - ); - for (const k of Object.keys(requestObject.expectedError)) { - assert.deepEqual( - reqValidationResults['errors'][0][k], - requestObject.expectedError[k], - ); - } - }, + testRequestsForEachApiDoc({ + testTitle: 'should return correct validation errors for invalid requests', + apiDocDirectory: schemasDirV3, + testRequestMap: INVALID_TEST_REQUESTS.v3, + clients, + serverOrchestrator, + test: assertHasRequestValidationError, }); - testRequestForEachFileWithServers({ + testResponsesForEachApiDoc({ testTitle: - 'should return correct validation errors for invalid RESponses', - dir: schemasDirV3, - testServers: INVALID_RESPONSES.v3, + 'should return correct validation errors for invalid responses', + apiDocDirectory: schemasDirV3, + testResponses: INVALID_RESPONSES.v3, client: clients, - callback(proxyRes, targetRes, fileName, expectedError) { - assert.isDefined( - expectedError, - '"expectedError" property should be set for test requests that check for validation errors', - ); - const validationResults = JSON.parse( - proxyRes.headers['openapi-cop-validation-result'], - ); - const resValidationResults = validationResults['response']; - assert.isNotTrue( - resValidationResults['valid'], - 'Response should be invalid', - ); - assert.isNotNull( - resValidationResults['errors'], - 'There should be at least one error present', - ); - assert.isArray(resValidationResults['errors']); - for (const k of Object.keys(expectedError)) { - assert.deepEqual( - resValidationResults['errors'][0][k], - expectedError[k], - ); - } - }, + serverOrchestrator, + test: assertHasResponseValidationErrors, }); - testRequestForEachFile({ + testRequestsForEachApiDoc({ testTitle: - 'should return correct validation errors for strictly invalid REQuests', - dir: schemasDirV3, - testRequests: STRICTLY_INVALID_TEST_REQUESTS.v3, - client: clients, - defaultForbidAdditionalProperties: true, - callback(proxyRes, _targetRes, fileName, requestObject) { - const validationResults = JSON.parse( - proxyRes.headers['openapi-cop-validation-result'], - ); - const reqValidationResults = validationResults['request']; - assert.isNotTrue(reqValidationResults['valid']); - assert.isNotNull(reqValidationResults['errors']); - assert.isArray(reqValidationResults['errors']); - assert.lengthOf(reqValidationResults['errors'], 1); - - for (const k of Object.keys(requestObject.expectedError)) { - assert.deepEqual( - reqValidationResults['errors'][0][k], - requestObject.expectedError[k], - ); - } - }, + 'should return correct validation errors for strictly invalid requests', + apiDocDirectory: schemasDirV3, + testRequestMap: STRICTLY_INVALID_TEST_REQUESTS.v3, + clients, + serverOrchestrator, + proxyOptions: { defaultForbidAdditionalProperties: true }, + test: assertHasRequestValidationError, }); - testRequestForEachFileWithServers({ + testResponsesForEachApiDoc({ testTitle: - 'should return correct validation errors for strictly invalid RESponses', - dir: schemasDirV3, - testServers: STRICTLY_INVALID_RESPONSES.v3, + 'should return correct validation errors for strictly invalid responses', + apiDocDirectory: schemasDirV3, + testResponses: STRICTLY_INVALID_RESPONSES.v3, client: clients, - defaultForbidAdditionalProperties: true, - callback(proxyRes, targetRes, fileName, expectedError) { - assert.isDefined( - expectedError, - '"expectedError" property should be set for test requests that check for validation errors', - ); - const validationResults = JSON.parse( - proxyRes.headers['openapi-cop-validation-result'], - ); - const resValidationResults = validationResults['response']; - assert.isNotTrue( - resValidationResults['valid'], - 'Response should be invalid', - ); - assert.isNotNull( - resValidationResults['errors'], - 'There should be at least one error present', - ); - assert.isArray(resValidationResults['errors']); - for (const k of Object.keys(expectedError)) { - assert.deepEqual( - resValidationResults['errors'][0][k], - expectedError[k], - ); - } - }, + serverOrchestrator, + proxyOptions: { defaultForbidAdditionalProperties: true }, + test: assertHasResponseValidationErrors, }); }); }); diff --git a/test/config.env b/test/config.env new file mode 100644 index 00000000..0e59687f --- /dev/null +++ b/test/config.env @@ -0,0 +1,3 @@ +PROXY_PORT=8888 +TARGET_SERVER_PORT=8889 +SERVER_RUNTIME=node diff --git a/test/config.ts b/test/config.ts index 97ba6ad0..b60c925d 100755 --- a/test/config.ts +++ b/test/config.ts @@ -1,11 +1,12 @@ import * as path from 'path'; +export const PROXY_PORT = Number(process.env.PROXY_PORT); +export const TARGET_SERVER_PORT = Number(process.env.TARGET_SERVER_PORT); +export const SERVER_RUNTIME: 'docker' | 'node' | string | undefined = process.env.SERVER_RUNTIME; + export const MOCK_SERVER_DIR = path.resolve(__dirname, '../../mock-server/'); export const SCHEMAS_DIR = path.resolve(__dirname, '../../test/schemas/'); -export const PROXY_PORT = 8888; -export const TARGET_SERVER_PORT = 8889; - export const DEFAULT_OPENAPI_FILE = path.join( SCHEMAS_DIR, 'v3/7-petstore.yaml', diff --git a/test/docker/build-docker-test.bash b/test/docker/build-docker-test.bash new file mode 100755 index 00000000..a7fac2bd --- /dev/null +++ b/test/docker/build-docker-test.bash @@ -0,0 +1,3 @@ +#!/bin/bash + +docker build . -f docker/Dockerfile -t lxlu/openapi-cop:test --label test diff --git a/test/docker/config.env b/test/docker/config.env new file mode 100644 index 00000000..8c8cd262 --- /dev/null +++ b/test/docker/config.env @@ -0,0 +1,3 @@ +PROXY_PORT=8888 +TARGET_SERVER_PORT=8889 +SERVER_RUNTIME=docker diff --git a/test/docker/entrypoint.bash b/test/docker/entrypoint.bash deleted file mode 100755 index a2288686..00000000 --- a/test/docker/entrypoint.bash +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -cd /data - -echo "Cleaning, then installing..." -(cd mock && npm run clean && npm install) -npm run clean && npm install - -echo "Running tests..." -DEBUG=openapi-cop:* npm test diff --git a/test/docker/kill-docker-test.bash b/test/docker/kill-docker-test.bash new file mode 100755 index 00000000..7a131671 --- /dev/null +++ b/test/docker/kill-docker-test.bash @@ -0,0 +1,3 @@ +#!/bin/bash + +docker kill $(docker ps -q --filter ancestor=lxlu/openapi-cop:test) diff --git a/test/docker/run-docker-test.bash b/test/docker/run-docker-test.bash index 85640ba3..967e3088 100755 --- a/test/docker/run-docker-test.bash +++ b/test/docker/run-docker-test.bash @@ -1,14 +1,13 @@ -#!/usr/bin/env bash +#!/bin/bash -read -r -p "Enter node version [10|12]: " userInput -NODE_VERSION=$(echo $userInput | sed 's/v//g') -echo "Using node version $NODE_VERSION" +cliArguments=$1 +schemasDir=$(readlink -f "$(dirname $0)/../schemas") -MSYS_NO_PATHCONV=1 -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +containerId=$(docker run -d --network="host" \ + -v "$schemasDir:/schemas" \ + --env "CLI_ARGUMENTS=$cliArguments" \ + --env "DEBUG=openapi-cop:*" \ + --env "CI=true" \ + lxlu/openapi-cop:test) -docker run --rm -it \ - -v "$DIR/../..":/data \ - -v "$DIR/entrypoint.bash":/entrypoint.bash \ - --user "$(id -u):$(id -g)" \ - node:$NODE_VERSION bash 'entrypoint.bash' +echo "Proxy container: ${containerId:0:8}" diff --git a/test/scripts/spawn.ts b/test/scripts/spawn.ts deleted file mode 100755 index 9f2677b0..00000000 --- a/test/scripts/spawn.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Utility script for spawning a openapi-cop instance and a mock server in parallel - * that are both based on a given OpenAPI document. - * - * node ./spawn [openapi-path] - * - * openapi-path (optional): path to the OpenAPI file on which both the proxy server - * and the mock server will be based on. If the file path is relative, it is relative to the project base. - * Defaults to a hard-coded file (see below). - * - * This script is called/aliased by `npm run dev-start-along-mock`. - */ - -import * as path from 'path'; - -import {PROXY_PORT, TARGET_SERVER_PORT} from '../config'; -import {spawnProxyWithMockServer} from '../util/server'; - -const apiDocRelativePath = process.argv[2] ? process.argv[2] : 'test/schemas/v3/3-parameters.yaml'; -const apiDocPath = path.resolve(__dirname, '../../..', apiDocRelativePath); - -spawnProxyWithMockServer(PROXY_PORT, TARGET_SERVER_PORT, apiDocPath, {stdio: 'inherit'}); diff --git a/test/test-requests/invalid-requests.ts b/test/test-requests/invalid-requests.ts index 58194074..845ef6bd 100755 --- a/test/test-requests/invalid-requests.ts +++ b/test/test-requests/invalid-requests.ts @@ -2,9 +2,9 @@ * NOTE: To enable tests for a specific OpenAPI file, add the file name * as a key of the object and add at least one TestRequestConfig to the array. */ -import {TestRequests} from '../../types/test-requests'; +import {TestRequestMap} from '../../types/test-request-map'; -export const INVALID_TEST_REQUESTS: { [dir: string]: TestRequests } = { +export const INVALID_TEST_REQUESTS: { [dir: string]: TestRequestMap } = { v3: { '2-path.yaml': [ { @@ -38,7 +38,7 @@ export const INVALID_TEST_REQUESTS: { [dir: string]: TestRequests } = { }, }; -export const STRICTLY_INVALID_TEST_REQUESTS: { [dir: string]: TestRequests } = { +export const STRICTLY_INVALID_TEST_REQUESTS: { [dir: string]: TestRequestMap } = { v3: { '2-path.yaml': [ { diff --git a/test/test-requests/valid-requests.ts b/test/test-requests/valid-requests.ts index 0acd5993..c4de2f62 100755 --- a/test/test-requests/valid-requests.ts +++ b/test/test-requests/valid-requests.ts @@ -2,9 +2,9 @@ * NOTE: To enable tests for a specific OpenAPI file, add the file name * as a key of the object and add at least one `TestRequestConfig` to the array. */ -import {TestRequests} from '../../types/test-requests'; +import {TestRequestMap} from '../../types/test-request-map'; -export const VALID_TEST_REQUESTS: { [dir: string]: TestRequests } = { +export const VALID_TEST_REQUESTS: { [dir: string]: TestRequestMap } = { v3: { '2-path.yaml': [ { @@ -49,7 +49,7 @@ export const VALID_TEST_REQUESTS: { [dir: string]: TestRequests } = { }, }; -export const STRICTLY_VALID_TEST_REQUESTS: { [dir: string]: TestRequests } = { +export const STRICTLY_VALID_TEST_REQUESTS: { [dir: string]: TestRequestMap } = { v3: { '3-parameters.yaml': [ { diff --git a/test/test-responses/invalid-responses.ts b/test/test-responses/invalid-responses.ts index fb9add23..39943355 100755 --- a/test/test-responses/invalid-responses.ts +++ b/test/test-responses/invalid-responses.ts @@ -1,23 +1,6 @@ -import * as express from 'express'; -import {Request, Response} from 'express'; - -import {TARGET_SERVER_PORT} from '../config'; -import {TestResponses} from '../../types/test-requests'; - -/** - * Utility function to create a server that responds to only one given path/method. - */ -function responderTo( - method: string, - path: string, - routeHandler: express.RequestHandler, -) { - return async () => { - const app: express.Application = express(); - ((app as any)[method] as express.IRouterMatcher)(path, routeHandler); - return app.listen(TARGET_SERVER_PORT); - }; -} +import { Request, Response } from 'express'; +import { TestResponses } from '../../types/test-request-map'; +import { responderTo } from '../util/server'; /** * For every OpenAPI file path, an HTTP server is provided along with valid @@ -35,18 +18,18 @@ export const INVALID_RESPONSES: { request: { method: 'POST', url: '/echo', - data: JSON.stringify({input: 'ECHO!'}), + data: JSON.stringify({ input: 'ECHO!' }), }, - runServer: responderTo( + serverFactory: responderTo( 'post', '/echo', (_req: Request, res: Response) => { - res.status(200).json({itseMe: 'Mario!'}); + res.status(200).json({ itseMe: 'Mario!' }); }, ), expectedError: { keyword: 'required', - params: {missingProperty: 'output'}, + params: { missingProperty: 'output' }, }, }, ], @@ -58,16 +41,16 @@ export const INVALID_RESPONSES: { request: { method: 'POST', url: '/echo', - data: JSON.stringify({input: 'ECHO!'}), + data: JSON.stringify({ input: 'ECHO!' }), }, - runServer: responderTo( + serverFactory: responderTo( 'post', '/echo', (_req: Request, res: Response) => { - res.status(400).json({error: {name: 666, message: 42}}); + res.status(400).json({ error: { name: 666, message: 42 } }); }, ), - expectedError: {keyword: 'type'}, + expectedError: { keyword: 'type' }, }, ], '5-external-refs.yaml': [ @@ -75,29 +58,29 @@ export const INVALID_RESPONSES: { request: { method: 'POST', url: '/echo', - data: JSON.stringify({input: 'ECHO!'}), + data: JSON.stringify({ input: 'ECHO!' }), }, - runServer: responderTo( + serverFactory: responderTo( 'post', '/echo', (_req: Request, res: Response) => { - res.status(400).json({error: {name: 666, message: 42}}); + res.status(400).json({ error: { name: 666, message: 42 } }); }, ), - expectedError: {keyword: 'type'}, + expectedError: { keyword: 'type' }, }, ], '6-examples.yaml': [ { - request: {method: 'GET', url: '/pets'}, - runServer: responderTo( + request: { method: 'GET', url: '/pets' }, + serverFactory: responderTo( 'get', '/pets', (_req: Request, res: Response) => { - res.status(200).json([{id: 12, name: 'Figaro'}, 'rofl', 'lol']); + res.status(200).json([{ id: 12, name: 'Figaro' }, 'rofl', 'lol']); }, ), - expectedError: {keyword: 'type', message: 'should be object'}, + expectedError: { keyword: 'type', message: 'should be object' }, }, ], '7-petstore.yaml': [ @@ -118,18 +101,18 @@ export const STRICTLY_INVALID_RESPONSES: { request: { method: 'POST', url: '/echo', - data: JSON.stringify({input: 'ECHO!'}), + data: JSON.stringify({ input: 'ECHO!' }), }, - runServer: responderTo( + serverFactory: responderTo( 'post', '/echo', (_req: Request, res: Response) => { res .status(200) - .json({output: 'The cake is a lie', forrest: 'Gump'}); + .json({ output: 'The cake is a lie', forrest: 'Gump' }); }, ), - expectedError: {keyword: 'additionalProperties'}, + expectedError: { keyword: 'additionalProperties' }, }, ], }, diff --git a/test/util/io.ts b/test/util/io.ts index af076b41..aadc739d 100755 --- a/test/util/io.ts +++ b/test/util/io.ts @@ -1,6 +1,10 @@ import * as fs from 'fs'; import * as path from 'path'; +export function getFileName(filePath: string): string { + return path.normalize(path.basename(filePath)).replace(/\\/g, '/'); +} + /** * Synchronously lists all top-level files in a directory. */ diff --git a/test/util/process.ts b/test/util/process.ts index 514f5fda..e67d9725 100755 --- a/test/util/process.ts +++ b/test/util/process.ts @@ -1,42 +1,39 @@ import { exec as _exec } from 'child_process'; import * as util from 'util'; -const exec = util.promisify(_exec); +import { flatMap } from 'lodash'; +import findProcess = require('find-process'); -/** Kills all processes that are listening on a given port. */ -export async function killOnPort(port: number | string): Promise { - if (!port) { - return Promise.reject(new Error('Invalid argument provided for port')); - } +const exec = util.promisify(_exec); - if (process.platform === 'win32') { - return exec( - `netstat -ano | grep :${port} | grep :LISTEN | awk '{print $5}' | uniq | xargs -n1 -I{} tskill {} /A /V`, - ); - } else { - return exec( - `lsof -i tcp:${port} | grep LISTEN | awk '{print $2}' | uniq | xargs -n1 kill -9`, - ); - } +export async function killNodeProcesses( + ports: Array, +): Promise { + const processResults = await Promise.all( + ports.map((port) => findProcess('port', port)), + ); + return killProcesses( + flatMap(processResults, (results) => + flatMap(results, (process) => + process.cmd.includes('node') ? process.pid : [], + ), + ), + ); } /** Kills many processes by their PIDs. */ export function killProcesses(pids: number[]): Promise> { - return Promise.all(pids.map(pid => killProcess(pid))); + return Promise.all(pids.map((pid) => killProcess(pid))); } /** Kills a process given its PID. */ export async function killProcess(pid: number): Promise { const isWin = /^win/.test(process.platform); if (!pid) return Promise.resolve(); - try { - if (!isWin) { - await exec('kill -9 ' + pid); - } else { - await exec('taskkill /pid ' + pid + ' /f /t'); - } - } catch (ex) { - if (!/not found/.test(ex.stderr)) { - console.log('Failed to kill child process:', ex); - } + + const command = !isWin ? `kill -9 ${pid}` : `taskkill /pid ${pid} /f /t`; + const { stderr } = await exec(command); + + if (stderr && !/not found/.test(stderr)) { + console.log('Failed to kill child process:', stderr); } } diff --git a/test/util/server-orchestrator.ts b/test/util/server-orchestrator.ts new file mode 100644 index 00000000..8e7596ae --- /dev/null +++ b/test/util/server-orchestrator.ts @@ -0,0 +1,209 @@ +import { killNodeProcesses } from './process'; +import { + BaseProxyOptions, + ExtendedProxyOptions, + ProxyOptions, + runProxy as runProxyServer, +} from '../../src/app'; +import { + BaseMockOptions, + MockOptions, + runApp as runMockServer, +} from '@exxeta/openapi-cop-mock-server'; +import { Server } from 'http'; +import { URL } from 'url'; +import { ChildProcess } from 'child_process'; +import { + closeServer, + killDockerProxyServer, + spawnDockerProxyServer, +} from './server'; + +export enum ServerRole { + Proxy = 'proxy', + MockTarget = 'mock', +} + +export abstract class ServerOrchestrator { + protected servers: { + [ServerRole.Proxy]?: S; + [ServerRole.MockTarget]?: S; + } = {}; + + constructor(public readonly proxyUrl: URL, public readonly targetUrl: URL) {} + + get proxyOptions(): BaseProxyOptions { + return { + port: this.proxyUrl.port, + host: this.proxyUrl.hostname, + targetUrl: this.targetUrl.toString(), + }; + } + + get mockOptions(): BaseMockOptions { + return { + port: this.targetUrl.port, + }; + } + + get options(): { + [ServerRole.Proxy]: BaseProxyOptions; + [ServerRole.MockTarget]: BaseMockOptions; + } { + return { + proxy: this.proxyOptions, + mock: this.mockOptions, + }; + } + + public abstract start( + server: ServerRole, + options: ProxyOptions | MockOptions, + ): Promise; + + public abstract stop(server: ServerRole): Promise; + + public abstract kill(): Promise; + + async startAll(proxyOptions: ExtendedProxyOptions): Promise> { + console.log('Starting servers...'); + return Promise.all([ + this.start(ServerRole.Proxy, { + ...this.proxyOptions, + ...proxyOptions, + }), + this.start(ServerRole.MockTarget, { + ...this.mockOptions, + apiDocFile: proxyOptions?.apiDocPath, + }), + ]); + } + + async stopAll(): Promise { + console.log('Shutting down servers...'); + if (!this.servers) { + return; + } + await Promise.all([ + this.stop(ServerRole.Proxy), + this.stop(ServerRole.MockTarget), + ]); + } + + /** + * Starts the servers, performs a task and finally shuts the servers down. + * + * @param task A function to be executed after the servers are up. Afterwards the servers are shut down again. + * @param proxyOptions Temporal proxy options to set only for the execution of this task. + */ + public async withServers({ + task, + proxyOptions, + }: { + task: () => Promise; + proxyOptions: ExtendedProxyOptions; + }): Promise { + await this.startAll(proxyOptions); + console.log('Started both servers!'); + await task(); + await this.stopAll(); + } + + public async withProxy({ + task, + proxyOptions, + }: { + task: () => Promise; + proxyOptions: ExtendedProxyOptions; + }): Promise { + await this.start(ServerRole.Proxy, { + ...this.proxyOptions, + ...proxyOptions, + }); + await task(); + await this.stop(ServerRole.Proxy); + } +} + +/** + * This orchestrator starts Express servers directly on the main process. + */ +export class NodeHttpServerOrchestrator extends ServerOrchestrator { + async start( + serverRole: ServerRole, + options: ProxyOptions | MockOptions, + ): Promise { + switch (serverRole) { + case ServerRole.Proxy: { + this.servers[serverRole] = await runProxyServer( + options as ProxyOptions, + ); + break; + } + case ServerRole.MockTarget: { + this.servers[serverRole] = await runMockServer(options as MockOptions); + break; + } + } + + return this.servers[serverRole] as Server; + } + + async stop(serverRole: ServerRole): Promise { + const server = this.servers[serverRole]; + if (!server) { + return; + } + await closeServer(server, this.options[serverRole].port); + } + + kill(): Promise { + return killNodeProcesses([this.proxyOptions.port, this.mockOptions.port]); + } +} + +export class DockerServerOrchestrator extends ServerOrchestrator< + Server | ChildProcess +> { + private mockServerOrchestrator = new NodeHttpServerOrchestrator( + this.proxyUrl, + this.targetUrl, + ); + + async start( + serverRole: ServerRole, + options: ProxyOptions | MockOptions, + ): Promise { + if (serverRole === ServerRole.Proxy) { + console.log('Starting docker proxy server...'); + this.servers[serverRole] = await spawnDockerProxyServer( + options as ProxyOptions, + { detached: true, stdio: 'inherit' }, + true, + ); + console.log('Spawned docker proxy server!'); + return this.servers[serverRole] as ChildProcess; + } else { + this.servers[serverRole] = await this.mockServerOrchestrator.start( + ServerRole.MockTarget, + options, + ); + return this.servers[serverRole] as Server; + } + } + + async stop(serverRole: ServerRole): Promise { + if (serverRole === ServerRole.Proxy) { + await killDockerProxyServer(this.proxyOptions); + } else { + await this.mockServerOrchestrator.stop(ServerRole.MockTarget); + } + } + + async kill(): Promise { + await Promise.all([ + killNodeProcesses([this.mockOptions.port]), + killDockerProxyServer(this.proxyOptions), + ]); + } +} diff --git a/test/util/server.ts b/test/util/server.ts index 30eb43f0..a2662115 100644 --- a/test/util/server.ts +++ b/test/util/server.ts @@ -1,44 +1,40 @@ -import {runProxy as runProxyApp} from '../../src/app'; -import {MOCK_SERVER_DIR, PROXY_PORT, TARGET_SERVER_PORT} from '../config'; -import {runApp as runMockApp} from '@exxeta/openapi-cop-mock-server'; -import {closeServer} from '../../src/util'; -import {ChildProcess, spawn} from 'child_process'; +import { BaseProxyOptions, ProxyOptions } from '../../src/app'; +import { MOCK_SERVER_DIR, SCHEMAS_DIR } from '../config'; +import { MockOptions } from '@exxeta/openapi-cop-mock-server'; +import { buildCliArguments, CliFlags } from '../../src/util'; +import { ChildProcess, execFile, spawn, SpawnOptions } from 'child_process'; import * as waitOn from 'wait-on'; import debug from 'debug'; +import * as path from 'path'; +import * as http from 'http'; +import { Server } from 'http'; +import * as express from 'express'; -/** - * Executes a function within the context of a proxy and a mock server. - * Resources are created before execution and cleaned up thereafter. - */ -export async function withServers({ - apiDocPath, - callback, - defaultForbidAdditionalProperties, - silent, - }: { - apiDocPath: string; - callback: () => Promise; - defaultForbidAdditionalProperties: boolean; - silent: boolean; -}): Promise { - console.log('Starting servers...'); - const servers = { - proxy: await runProxyApp({ - port: PROXY_PORT, - host: 'localhost', - targetUrl: `http://localhost:${TARGET_SERVER_PORT}`, - apiDocPath, - defaultForbidAdditionalProperties, - silent, - }), - mock: await runMockApp(TARGET_SERVER_PORT, apiDocPath), - }; +function toProxyCliOptions(proxyOptions: ProxyOptions, isVerbose = false) { + const args: Array = [ + { flag: '--host', value: proxyOptions.host }, + { flag: '--port', value: proxyOptions.port.toString() }, + { flag: '--target', value: proxyOptions.targetUrl }, + { flag: '--file', value: proxyOptions.apiDocPath }, + { + flag: '--default-forbid-additional-properties', + value: proxyOptions.defaultForbidAdditionalProperties, + }, + { flag: '--silent', value: proxyOptions.silent }, + { flag: '--verbose', value: isVerbose }, + ]; + + return buildCliArguments(args); +} - console.log('Running test...'); - await callback(); +function toMockCliOptions(mockOptions: MockOptions, isVerbose = false) { + const args: Array = [ + { flag: '--port', value: mockOptions.port.toString() }, + { flag: '--file', value: mockOptions.apiDocFile }, + { flag: '--verbose', value: isVerbose }, + ]; - console.log('Shutting down servers...'); - await Promise.all([closeServer(servers.proxy), closeServer(servers.mock)]); + return buildCliArguments(args); } /** @@ -48,28 +44,25 @@ export async function withServers({ * The `options` can be used to override the `child_process.spawn` options. */ export async function spawnProxyServer( - proxyPort: number, - targetPort: number, - apiDocFile: string, - options: any = {}, -): Promise { + proxyOptions: ProxyOptions, // NOTE: for debugging use the options {detached: true, stdio: 'inherit'} + spawnOptions: SpawnOptions = {}, + isVerbose = false, +): Promise { const cp = spawn( 'node', - [ - '../../src/cli.js', - '--port', - proxyPort.toString(), - '--target', - `http://localhost:${targetPort}`, - '--file', - apiDocFile, - '--verbose', - ], - {cwd: __dirname, stdio: 'pipe', detached: false, ...options}, + ['../../src/cli.js', ...toProxyCliOptions(proxyOptions, isVerbose)], + { + cwd: __dirname, + stdio: 'pipe', + detached: false, + ...spawnOptions, + }, ); - await waitOn({resources: [`tcp:localhost:${proxyPort}`]}); + await waitOn({ + resources: [`tcp:${proxyOptions.host}:${proxyOptions.port}`], + }); return cp; } @@ -81,30 +74,23 @@ export async function spawnProxyServer( * The `options` can be used to override the `child_process.spawn` options. */ export async function spawnMockServer( - port: number, - apiDocFile: string, - options: any = {}, -): Promise { + mockOptions: MockOptions, // NOTE: for debugging use the options {detached: true, stdio: 'inherit'} + spawnOptions: SpawnOptions = {}, + isVerbose = false, +): Promise { const cp = spawn( 'node', - [ - './build/src/cli.js', - '--port', - port.toString(), - '--file', - apiDocFile, - '--verbose', - ], + ['./build/src/cli.js', ...toMockCliOptions(mockOptions, isVerbose)], { cwd: MOCK_SERVER_DIR, stdio: debug.enabled('openapi-cop:mock') ? 'inherit' : 'ignore', detached: false, - ...options, + ...spawnOptions, }, ); - await waitOn({resources: [`tcp:localhost:${port}`]}); + await waitOn({ resources: [`tcp:${mockOptions.port}`] }); return cp; } @@ -113,13 +99,106 @@ export async function spawnMockServer( * Convenience function to spawn a proxy server along a mock server. */ export async function spawnProxyWithMockServer( - proxyPort: number, - targetPort: number, - apiDocFile: string, - options: any = {}, -): Promise<{ proxy: ChildProcess; target: ChildProcess; }> { + proxyOptions: ProxyOptions, + mockOptions: MockOptions, + spawnOptions: SpawnOptions = {}, +): Promise<{ proxy: ChildProcess; target: ChildProcess }> { return { - proxy: await spawnProxyServer(proxyPort, targetPort, apiDocFile, options), - target: await spawnMockServer(targetPort, apiDocFile, options), + proxy: await spawnProxyServer(proxyOptions, spawnOptions), + target: await spawnMockServer(mockOptions, spawnOptions), + }; +} + +export async function spawnDockerProxyServer( + proxyOptions: ProxyOptions, + // NOTE: for debugging use the options {detached: true, stdio: 'inherit'} + spawnOptions: SpawnOptions = {}, + isVerbose = false, +): Promise { + const dockerProxyOptions = { + ...proxyOptions, + apiDocPath: path.join( + '/schemas', + path.relative(SCHEMAS_DIR, proxyOptions.apiDocPath), + ), }; + + const cp = spawn( + 'test/docker/run-docker-test.bash', + [toProxyCliOptions(dockerProxyOptions, isVerbose).join(' ')], + { + cwd: process.env.PWD, + stdio: 'pipe', + detached: false, + ...spawnOptions, + }, + ); + + await waitOn({ + resources: [`tcp:${proxyOptions.host}:${proxyOptions.port}`], + tcpTimeout: 3000, + }); + + return cp; +} + +export async function killDockerProxyServer( + proxyOptions: BaseProxyOptions, +): Promise { + const cp = execFile('test/docker/kill-docker-test.bash', { + cwd: process.env.PWD, + }); + + await waitOn({ + resources: [`tcp:${proxyOptions.host}:${proxyOptions.port}`], + reverse: true, + }); + + return cp; +} + +/** + * Utility function to create a server that responds to only one given path/method. + */ +export function responderTo( + method: string, + path: string, + routeHandler: express.RequestHandler, +): (port: number | string) => http.Server { + return (port: number | string) => { + const app: express.Application = express(); + ((app as any)[method] as express.IRouterMatcher)(path, routeHandler); + return app.listen(port); + }; +} + +export async function withServer({ + serverFactory, + port, + task, +}: { + serverFactory: (port: number | string) => Server; + port: number | string; + task: () => Promise; +}): Promise { + const server = await serverFactory(port); + await task(); + await closeServer(server, port); +} + +/** Closes the server and waits until the port is again free. */ +export async function closeServer( + server: http.Server, + port: number | string, +): Promise { + if (server.address()) { + await new Promise((resolve, reject) => { + server.close((err) => { + if (err) return reject(err); + resolve(); + }); + }); + } + + await waitOn({ resources: [`tcp:${port}`], reverse: true }); } diff --git a/test/util/testing.ts b/test/util/testing.ts index da9133a8..19999604 100755 --- a/test/util/testing.ts +++ b/test/util/testing.ts @@ -1,14 +1,11 @@ -import {TestRequestConfig, TestRequests, TestResponses} from '../../types/test-requests'; +import { TestRequest, TestRequestMap, TestResponses } from '../../types/test-request-map'; import * as assert from 'assert'; -import {AxiosInstance, AxiosRequestConfig, AxiosResponse} from 'axios'; -import * as path from 'path'; - -import {PROXY_PORT, TARGET_SERVER_PORT} from '../config'; -import {runProxy as runProxyApp} from '../../src/app'; -import {closeServer} from '../../src/util'; -import {readDirFilesSync} from './io'; -import {withServers} from './server'; +import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import { ExtendedProxyOptions } from '../../src/app'; +import { getFileName, readDirFilesSync } from './io'; import * as chalk from 'chalk'; +import { ServerOrchestrator } from './server-orchestrator'; +import { withServer } from './server'; /** * Formats a request in a compact way, i.e. METHOD /url {...} @@ -26,8 +23,13 @@ export function formatRequest(req: AxiosRequestConfig): string { } } -export async function assertThrowsAsync(fn: () => Promise, regExp: RegExp): Promise { - let f = () => { return }; +export async function assertThrowsAsync( + fn: () => Promise, + regExp: RegExp, +): Promise { + let f = () => { + return; + }; try { await fn(); } catch (e) { @@ -39,98 +41,101 @@ export async function assertThrowsAsync(fn: () => Promise, regExp: RegExp) } } +export type AssertionFunction = ( + proxyResponse: AxiosResponse, + targetResponse: AxiosResponse, + fileName: string, + expectedError: any, +) => void; + /** * For each OpenAPI file in a given directory, it boots a proxy and a mock * server and runs the provided test requests. It then executes the callback * function that contains the test code. */ -export function testRequestForEachFile({ +export function testRequestsForEachApiDoc(options: { + testTitle: string; + apiDocDirectory: string; + testRequestMap: TestRequestMap; + clients: { proxy: AxiosInstance; target: AxiosInstance }; + serverOrchestrator: ServerOrchestrator; + proxyOptions?: Partial; + test: AssertionFunction; +}): void { + for (const apiDocPath of readDirFilesSync(options.apiDocDirectory)) { + testRequestsForApiDoc({ + ...options, + apiDocPath, + testRequests: options.testRequestMap[getFileName(apiDocPath)] ?? [], + }); + } +} + +export function testRequestsForApiDoc({ testTitle, - dir, + apiDocPath, testRequests, - client, - callback, - defaultForbidAdditionalProperties = false, - silent = false, + clients, + serverOrchestrator, + proxyOptions, + test, }: { testTitle: string; - dir: string; - testRequests: TestRequests; - client: { proxy: AxiosInstance; target: AxiosInstance }; - callback: ( - proxyRes: AxiosResponse, - targetRes: AxiosResponse, - fileName: string, - requestObject: TestRequestConfig, - ) => void; - defaultForbidAdditionalProperties?: boolean; - silent?: boolean; + apiDocPath: string; + testRequests: Array; + clients: { proxy: AxiosInstance; target: AxiosInstance }; + serverOrchestrator: ServerOrchestrator; + proxyOptions?: Partial; + test: AssertionFunction; }): void { - // tslint:disable:only-arrow-functions - for (const p of readDirFilesSync(dir)) { - const fileName = path.normalize(path.basename(p)).replace(/\\/g, '/'); - it(`${testTitle}: ${fileName}`, async function() { - // Skip if no test requests exist for the OpenAPI definition - if (!(fileName in testRequests)) { - console.log( - chalk.keyword('orange')( - `Skipping '${fileName}' due to missing test requests.`, - ), - ); - return; - } + const fileName = getFileName(apiDocPath); + it(`${testTitle}: ${fileName}`, async function () { + // Skip if no test requests exist for the OpenAPI definition + if (testRequests.length === 0) { + console.log( + chalk.keyword('orange')( + `Skipping '${fileName}' due to missing test requests.`, + ), + ); + return; + } - await withServers({ - apiDocPath: p, - defaultForbidAdditionalProperties, - silent, - async callback() { - // Perform all test requests on both servers yield responses - // to compare - for (const req of testRequests[fileName]) { - console.log(`Sending request ${formatRequest(req)}`); - const targetRes = await client.target(req); - const proxyRes = await client.proxy(req); - callback(proxyRes, targetRes, fileName, req); - } - }, - }); + await serverOrchestrator.withServers({ + proxyOptions: { ...proxyOptions, apiDocPath }, + task: async () => { + for (const request of testRequests) { + console.log(`Sending request ${formatRequest(request)}`); + const targetResponse = await clients.target(request); + const proxyResponse = await clients.proxy(request); + test(proxyResponse, targetResponse, fileName, request.expectedError); + } + }, }); - } + }); } -/** - * For each OpenAPI file in a given directory, it boots a proxy server, along - * with a test target server and runs the provided test requests. It then - * executes the callback function that contains the test code. - */ -export function testRequestForEachFileWithServers({ +export function testResponsesForEachApiDoc({ testTitle, - dir, - testServers, + apiDocDirectory, + testResponses, client, - callback, - defaultForbidAdditionalProperties = false, + serverOrchestrator, + proxyOptions, + test, }: { testTitle: string; - dir: string; - testServers: TestResponses; + apiDocDirectory: string; + testResponses: TestResponses; client: { proxy: AxiosInstance; target: AxiosInstance }; - defaultForbidAdditionalProperties?: boolean; - callback: ( - proxyRes: AxiosResponse, - targetRes: AxiosResponse, - fileName: string, - expectedError: any, - ) => void; + serverOrchestrator: ServerOrchestrator; + proxyOptions?: Partial; + test: AssertionFunction; }): void { - // tslint:disable:only-arrow-functions - for (const apiDocFile of readDirFilesSync(dir)) { - const fileName = path - .normalize(path.basename(apiDocFile)) - .replace(/\\/g, '/'); - it(`${testTitle}: ${fileName}`, async function() { - if (!(fileName in testServers)) { + for (const apiDocPath of readDirFilesSync(apiDocDirectory)) { + const fileName = getFileName(apiDocPath); + it(`${testTitle}: ${fileName}`, async function () { + const testData = testResponses[fileName]; + if (!testData?.length) { console.log( chalk.keyword('orange')( `Skipping '${fileName}' due to missing test responses.`, @@ -139,40 +144,23 @@ export function testRequestForEachFileWithServers({ return; } - if (testServers[fileName].length === 0) { - // When no tests are present in the array, this is interpreted as an - // intentional skip - return; - } - - console.log('Starting proxy server...'); - const proxyServer = await runProxyApp({ - port: PROXY_PORT, - host: 'localhost', - targetUrl: `http://localhost:${TARGET_SERVER_PORT}`, - apiDocPath: apiDocFile, - defaultForbidAdditionalProperties, + await serverOrchestrator.withProxy({ + proxyOptions: { ...proxyOptions, apiDocPath }, + task: async () => { + for (const { request, serverFactory, expectedError } of testData) { + await withServer({ + serverFactory, + port: serverOrchestrator.targetUrl.port, + task: async () => { + console.log(`Sending request ${formatRequest(request)}`); + const targetRes = await client.target(request); + const proxyRes = await client.proxy(request); + test(proxyRes, targetRes, apiDocPath, expectedError); + }, + }); + } + }, }); - - console.log('Running test...'); - for (const { request, runServer, expectedError } of testServers[ - fileName - ]) { - console.log('Starting some mock server...'); - const mockServer = await runServer(); - - console.log(`Sending request ${formatRequest(request)}`); - const targetRes = await client.target(request); - const proxyRes = await client.proxy(request); - - callback(proxyRes, targetRes, fileName, expectedError); - - console.log('Shutting down mock server...'); - await closeServer(mockServer); - } - - console.log('Shutting down proxy server...'); - await closeServer(proxyServer); }); } } diff --git a/tsconfig.json b/tsconfig.json index 5fe49ead..45d564b5 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,8 +10,8 @@ }, "strict": true, "typeRoots": ["./node_modules/@types"], - "lib": ["es2018"], - "target": "es2018" + "lib": ["es2019"], + "target": "es2019" }, "include": ["src/**/*.ts", "test/**/*.ts", "types/**/*.ts"] } diff --git a/types/test-request-map.ts b/types/test-request-map.ts new file mode 100644 index 00000000..4e86a754 --- /dev/null +++ b/types/test-request-map.ts @@ -0,0 +1,19 @@ +import {AxiosRequestConfig} from 'axios'; +import * as http from 'http'; + + +export interface TestRequestMap { + [fileName: string]: Array; +} + +export type TestRequest = AxiosRequestConfig & { expectedError?: any }; + +export interface TestResponses { + [fileName: string]: TestResponseConfig; +} + +export type TestResponseConfig = Array<{ + request: TestRequest; + serverFactory: (port: number | string) => http.Server; + expectedError: any; +}>; diff --git a/types/test-requests.ts b/types/test-requests.ts deleted file mode 100644 index dd27f990..00000000 --- a/types/test-requests.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {AxiosRequestConfig} from 'axios'; -import * as http from 'http'; - - -export interface TestRequests { - [fileName: string]: TestRequestConfig[]; -} - -export type TestRequestConfig = AxiosRequestConfig & { expectedError?: any }; - -export interface TestResponses { - [fileName: string]: TestResponseConfig; -} - -export type TestResponseConfig = Array<{ - request: TestRequestConfig; - runServer: () => Promise; - expectedError: any; -}>;