From f30a1d55e310c17b464c137bccf1fccb2c9e14e3 Mon Sep 17 00:00:00 2001 From: Muhammad Nawaz Date: Sat, 18 Nov 2023 21:20:17 +0500 Subject: [PATCH] Initial implementation --- .busted | 7 + .editorconfig | 22 + .gitattributes | 1 + .github/workflows/license-checker.yml | 52 + .github/workflows/lint.yml | 22 + .github/workflows/test.yml | 64 + .gitignore | 16 + .license-scripts/COPYRIGHT-HEADER | 2 + .license-scripts/EE-LICENSE | 21 + .license-scripts/license-checker.sh | 49 + .luacheckrc | 45 + .luacov | 7 + .pongo/pongorc | 3 + .travis.yml.example | 36 + LICENSE | 2 +- README.md | 104 + data/openAPI_example.json | 2872 +++++++++++++++++ kong-plugin-oasvalidator-1.0.0-1.rockspec | 38 + kong/plugins/oasvalidator/handler.lua | 69 + kong/plugins/oasvalidator/schema.lua | 122 + spec/oasvalidator/01-schema_spec.lua | 29 + .../02-path_param_validator_spec.lua | 106 + .../02-query_param_validator_spec.lua | 91 + .../02-request_validator_spec.lua | 104 + 24 files changed, 3883 insertions(+), 1 deletion(-) create mode 100644 .busted create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/license-checker.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .license-scripts/COPYRIGHT-HEADER create mode 100644 .license-scripts/EE-LICENSE create mode 100755 .license-scripts/license-checker.sh create mode 100644 .luacheckrc create mode 100644 .luacov create mode 100644 .pongo/pongorc create mode 100644 .travis.yml.example create mode 100644 README.md create mode 100644 data/openAPI_example.json create mode 100644 kong-plugin-oasvalidator-1.0.0-1.rockspec create mode 100644 kong/plugins/oasvalidator/handler.lua create mode 100644 kong/plugins/oasvalidator/schema.lua create mode 100644 spec/oasvalidator/01-schema_spec.lua create mode 100644 spec/oasvalidator/02-path_param_validator_spec.lua create mode 100644 spec/oasvalidator/02-query_param_validator_spec.lua create mode 100644 spec/oasvalidator/02-request_validator_spec.lua diff --git a/.busted b/.busted new file mode 100644 index 0000000..313c185 --- /dev/null +++ b/.busted @@ -0,0 +1,7 @@ +return { + default = { + verbose = true, + coverage = true, + output = "gtest", + }, +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3434e8a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.lua] +indent_style = space +indent_size = 2 + +[kong/templates/nginx*] +indent_style = space +indent_size = 4 + +[*.template] +indent_style = space +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml new file mode 100644 index 0000000..a11614f --- /dev/null +++ b/.github/workflows/license-checker.yml @@ -0,0 +1,52 @@ +name: "Apply EE License and Copyright" +on: [create] + +env: + SENDER: ${{ github.event.sender.login }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MESSAGE: "chore(license): EE License and Copyright" + +jobs: + license_checker: + if: contains( + fromJson('[ + "kong", + "konghq-cx" + ]'), + github.event.repository.owner.login + ) && + github.event.ref_type == 'branch' && + github.event.repository.full_name != 'kong/kong-plugin' && + github.event.repository.private == true + runs-on: ubuntu-latest + name: Apply EE License and Copyright + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: License and Copyright Checker + run: | + PR_BRANCH="chore/$GITHUB_REF_NAME-license-updated" + git config --global user.name "${{ env.SENDER }}" + git config --global user.email "${{ env.SENDER }}@users.noreply.github.com" + git fetch origin + git checkout -b "$PR_BRANCH" + .license-scripts/license-checker.sh + git commit -am "${{ env.MESSAGE }}" + git push origin "$PR_BRANCH" + gh pr create --title "${{ env.MESSAGE }}" --body "" --base "$GITHUB_REF_NAME" + cleanup: + if: ${{ always() && github.event.repository.full_name != 'kong/kong-plugin'}} + runs-on: ubuntu-latest + needs: license_checker + name: Cleanup + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Cleanup + run: | + git config --global user.name "${{ env.SENDER }}" + git config --global user.email "${{ env.SENDER }}@users.noreply.github.com" + git switch "$GITHUB_REF_NAME" + rm -r .github/workflows/license-checker.yml .license-scripts + git commit -am "${{ env.MESSAGE }} Action Cleanup" + git push diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..1e9cebc --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: Lint + +concurrency: + group: ${{ github.workflow }} ${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +on: + pull_request: {} + push: + branches: + - master + +jobs: + luacheck: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: LuaCheck linter + uses: lunarmodules/luacheck@master diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..bc0c4d3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,64 @@ +name: "Test" + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + kongVersion: + - "2.3.x" + - "2.8.x" + - "3.0.x" + - "dev" + - "3.0.0.x" + - "dev-ee" + + steps: + - uses: actions/checkout@v4 + + - name: Clone kong-pongo + run: git clone https://github.com/nawaz1991/kong-pongo.git + + - name: Setup env + run: | + PATH=$PATH:~/.local/bin + mkdir -p ~/.local/bin + ln -s $(realpath kong-pongo/pongo.sh) ~/.local/bin/pongo + + - run: pongo run -- --coverage + + # Optional upload of coverage data, + # just ugly, something to fix... + - uses: leafo/gh-actions-lua@v10 + if: success() + - uses: leafo/gh-actions-luarocks@v4 + if: success() + - name: Report test coverage + if: success() + continue-on-error: true + run: | + luarocks install luacov-coveralls + # hack: luacov config file has a path for inside the pongo container + # rewrite those to the local location in GHA + if [ -f .luacov ]; then + cp .luacov .luacov_backup + cat .luacov_backup | sed 's/\/kong-plugin\/luacov./luacov./' > .luacov + fi + + rm *.report.out + luacov-coveralls + #luacov-coveralls --output coveralls.out + # undo the hack + if [ -f .luacov_backup ]; then + mv .luacov_backup .luacov + fi + env: + COVERALLS_REPO_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c964cd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# servroot is typically the nginx/Kong workingdirectory when testing +servroot + +# packed distribution format for LuaRocks +*.rock +# exclude Pongo shell history +.pongo/.ash_history +.pongo/.bash_history +# exclude LuaCov statistics file +luacov.stats.out +# exclude LuaCov report +luacov.report.out +# exclude Pongo containerid file +.containerid +.idea +/kong-test-helper-docs diff --git a/.license-scripts/COPYRIGHT-HEADER b/.license-scripts/COPYRIGHT-HEADER new file mode 100644 index 0000000..7025ba6 --- /dev/null +++ b/.license-scripts/COPYRIGHT-HEADER @@ -0,0 +1,2 @@ +-- Copyright (c) 2023 Muhammad Nawaz +-- Licensed under the MIT License. See LICENSE file for more information. diff --git a/.license-scripts/EE-LICENSE b/.license-scripts/EE-LICENSE new file mode 100644 index 0000000..6a6e301 --- /dev/null +++ b/.license-scripts/EE-LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Muhammad Nawaz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/.license-scripts/license-checker.sh b/.license-scripts/license-checker.sh new file mode 100755 index 0000000..830d07b --- /dev/null +++ b/.license-scripts/license-checker.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# +# * check that all lua files have the COPYRIGHT's sha in the header +# * if not, add the content of ./COPYRIGHT-HEADER to each file +# +# * replace the LICENSE file from the root directory (if it exists) +# with the content of ./EE-LICENSE (otherwise create it). +# + +LOCAL_PATH=$(dirname $(realpath $0)) + +function add_copyright { + echo "Adding copyright headers" + + local ret=0 + local count=0 + local header="$LOCAL_PATH/COPYRIGHT-HEADER" + local sha=$(shasum $header | cut -f1 -d' ') + local eol="-- [ END OF LICENSE $sha ]" + for f in $(find "." -type f -name "*.lua"); do + grep -Fq "$sha" $f || { + ret=1 + cat "$header" <(echo "$eol") <(echo) $f >${f}.new + mv $f.new $f + ((count++)) + } + done + + [[ $ret -ne 0 ]] && + echo "Added headers to $count files." +} + +function replace_license { + echo "Replacing license" + + cat "$LOCAL_PATH/EE-LICENSE" > "$LOCAL_PATH/../LICENSE" || return 1 +} + +main() { + + add_copyright + replace_license + RET=$? + + return $RET + +} + +main diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..fd543c9 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,45 @@ +-- Configuration file for LuaCheck +-- see: https://luacheck.readthedocs.io/en/stable/ +-- +-- To run do: `luacheck .` from the repo + +std = "ngx_lua" +unused_args = false +redefined = false +max_line_length = false + + +globals = { + "_KONG", + "kong", + "ngx.IS_CLI", +} + + +not_globals = { + "string.len", + "table.getn", +} + + +ignore = { + "6.", -- ignore whitespace warnings +} + + +include_files = { + "**/*.lua", + "*.rockspec", + ".busted", + ".luacheckrc", +} + +exclude_files = { + --"spec/fixtures/invalid-module.lua", + --"spec-old-api/fixtures/invalid-module.lua", +} + + +files["spec/**/*.lua"] = { + std = "ngx_lua+busted", +} diff --git a/.luacov b/.luacov new file mode 100644 index 0000000..0563ed7 --- /dev/null +++ b/.luacov @@ -0,0 +1,7 @@ +include = { + "%/kong%-plugin%/kong%/.+$", +} + +statsfile = "/kong-plugin/luacov.stats.out" +reportfile = "/kong-plugin/luacov.report.out" +runreport = true \ No newline at end of file diff --git a/.pongo/pongorc b/.pongo/pongorc new file mode 100644 index 0000000..e8c1390 --- /dev/null +++ b/.pongo/pongorc @@ -0,0 +1,3 @@ +--postgres +--no-cassandra + diff --git a/.travis.yml.example b/.travis.yml.example new file mode 100644 index 0000000..4a5aa3c --- /dev/null +++ b/.travis.yml.example @@ -0,0 +1,36 @@ +# rename to ".travis.yml" to enable this file for Travis CI + +dist: focal + +jobs: + include: + # to add Kong Enterprise releases, check the Pongo docs + - name: Kong CE 2.3.x + env: KONG_VERSION=2.3.x + - name: Kong CE 2.4.x + env: KONG_VERSION=2.4.x + - name: Kong CE master + env: KONG_VERSION=nightly POSTGRES=latest CASSANDRA=latest + - name: Enterprise 2.3.3.x + env: KONG_VERSION=2.3.3.x + - name: Enterprise 2.4.1.x + env: KONG_VERSION=2.4.1.x + - name: Nightly EE-master + env: KONG_VERSION=nightly-ee POSTGRES=latest CASSANDRA=latest + +install: +- git clone --single-branch https://github.com/Kong/kong-pongo ../kong-pongo +- "../kong-pongo/pongo.sh up" +- "../kong-pongo/pongo.sh build" + +script: +- "../kong-pongo/pongo.sh lint" +- "../kong-pongo/pongo.sh run" + +notifications: + slack: + if: branch = master AND type != pull_request + on_success: change + on_failure: always + rooms: + secure: HdpUy77gMJEapyYOnKBqYeWFALG771KnLzUzbM5Et8br7RNrUTvA4fZ+paoLnIWU+0Sb+8A8/tRxBvPpxnVJFYZReQT4oBhX+wVTYbVr4i+UKfIDrU4DJ2nj3lHTa5t/dX7WZttBwFB8fuVugceWVF95DZCC9ll38d646+27wYydamu2hgCBs+PI4+J6msmFMC0T2vMr3A+B9reyiWQ+KG+E5U7mcmgMZ/xh10pBinPA7nXe0N4z50hR/ooHukFzMHDCyfVIKox9z9WQzS3SUI+Wxj6dts+dDVmVfTlM4XUM9e2MMDkBZgvQIedrjaR5pgdTl2xTWrMuMeeKbuimGa7FGD5rirgBg5gkP9LG1aSzJLP0lp4ldogTX+9VVaDE5N+ACKcR/10U3CskJYuOXx4cp0ub+TDIfe34NxgBe9PmTmJbTtRBgZ0sNVfZqfCmDYeCMTTyA+zug+XkPI+lQ56QqgFg4Hxohr+EsCQcQzA2YI9QUw0fIZPKyWQ91neE/ytF4xvNM8YI0yuLLgRYbbcvu6Tn4q0rwJkIdjh9eExD9ddsgTmENVN0KEYMj/Rk3WUJi6k7MDcSmxaBLC9REObgddyGM4hgXtbLGBAnJaDBHTaDBetTmrae2PAiZs478P4l5X1TzlZZMYVLlpVukpDJPzH6NBXZgiYjkyFipVc= diff --git a/LICENSE b/LICENSE index e3898cc..6a6e301 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac6d3e0 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# 🛡️ OASValidator Kong Plugin + +`kong-plugin-oasvalidator` is a Kong plugin that validates incoming HTTP requests against OpenAPI specifications. It offers a granular level of validation including request, body, path parameters, query parameters, header parameters, and routes. + +## 📚 Table of Contents +1. [Features](#-features) +2. [Prerequisites](#-prerequisites) +3. [Installation](#-installation) +4. [Configuration](#-configuration) +5. [Usage](#-usage) +6. [Validation Strategies](#-validation-strategies) +7. [Troubleshooting](#-troubleshooting) +8. [License](#-license) + +## 🌟 Features + +- Validates the entire request or individual parts like body, path, query, and header. +- Low latency and high efficiency. +- Highly configurable through Kong's admin API. + +## 🔧 Prerequisites + +- Kong >= 2.x.x +- LUA >= 5.1 + +## 📦 Installation + +Install it as a LuaRocks package: + +```bash +luarocks install oasvalidator +``` + +## ⚙️ Configuration + +You can add the plugin with the following request: + +```bash +curl -X POST http://localhost:8001/services/{serviceName|Id}/plugins \ +--data "name=oasvalidator" \ +--data "config.oas_spec_path=/path/to/oas/spec" \ +--data "config.validate_request=true" \ +--data "config.validate_body=false" \ +--data "config.validate_path_params=false" \ +--data "config.validate_query_params=false" \ +--data "config.validate_header_params=false" \ +--data "config.validate_route=false" +``` + +Or, you can use *Declarative (YAML)* to configure: +```yaml +_services: +- name: my-service + url: http://example.com + plugins: + - name: oasvalidator + config: + oas_spec_path: "/path/to/oas/spec" + validate_request: true + validate_body: false + validate_path_params: false + validate_query_params: false + validate_header_params: false + validate_route: false +``` + +### Schema + +```lua +-- Refer to the schema.lua file for the full configuration schema +``` + +### Parameters + +- `oas_spec_path`: Path to the OpenAPI specification file (required). +- `validate_request`: Validate the entire request (super set of all validations). Default is true. +- `validate_body`: Validate request body against the OpenAPI spec. Default is false. +- `validate_path_params`: Validate path parameters against the OpenAPI spec. Default is false. +- `validate_query_params`: Validate query parameters against the OpenAPI spec. Default is false. +- `validate_header_params`: Validate header parameters against the OpenAPI spec. Default is false. +- `validate_route`: Validate route against the OpenAPI spec. Default is false. + +## 🔍 Usage + +After installation and configuration, the plugin will validate incoming requests based on the rules you've set. + +## 📜 Validation Strategies + +- **Validate Request**: This is a super set of all other validators. If this is enabled, all other validators should be set to false. +- **Individual Validations**: You can also use individual validators for the body, path parameters, query parameters, header parameters, and routes. + +## 🛠️ Troubleshooting + +Check the Kong error logs for any issues. Error logs provide detailed information about what went wrong, aiding in rapid debugging. + +```bash +tail -f /usr/local/kong/logs/error.log +``` + +## 📄 License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for the full license text. + +© 2023 [Muhammad Nawaz](mailto:m.nawaz2003@gmail.com). All Rights Reserved. \ No newline at end of file diff --git a/data/openAPI_example.json b/data/openAPI_example.json new file mode 100644 index 0000000..d6a3730 --- /dev/null +++ b/data/openAPI_example.json @@ -0,0 +1,2872 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "REST Server Testing API", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://0.0.0.0:9000/test/api" + } + ], + "paths": { + "/test/dummy": + { + "get": + { + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/integer_simple_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "maximum": 1000 + }, + "style": "simple", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/integer_simple_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/integer_label_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "style": "label", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/integer_label_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "style": "label", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/integer_matrix_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "style": "matrix", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/integer_matrix_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "style": "matrix", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/string_simple_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "style": "simple", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/string_simple_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/string_label_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "string", + "maxLength": 2 + }, + "style": "label", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/string_label_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "style": "label", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/string_matrix_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "style": "matrix", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/string_matrix_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "style": "matrix", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/array_integer_simple_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "simple", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/array_integer_simple_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/array_integer_label_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "label", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/array_integer_label_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "label", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/array_string_simple_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "simple", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/array_string_simple_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/array_string_label_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "label", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/array_string_label_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "label", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/array_string_matrix_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "matrix", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/array_string_matrix_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "matrix", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/object_simple_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "simple", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/object_simple_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/object_label_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "label", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/object_label_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "label", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/object_matrix_true/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "matrix", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/object_matrix_false/{param}": { + "get": { + "parameters": [ + { + "name": "param", + "in": "path", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "matrix", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_integer_form_true": { + "get": { + "parameters": [ + { + "name": "param", + "in": "query", + "required": true, + "schema": { + "type": "integer" + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_integer_form_false": { + "get": { + "parameters": [ + { + "name": "param", + "in": "query", + "required": true, + "schema": { + "type": "integer" + }, + "style": "form", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_string_form_true": { + "get": { + "parameters": [ + { + "name": "param", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_string_form_false": { + "get": { + "parameters": [ + { + "name": "param", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "style": "form", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_array_integer_form_true": { + "get": { + "parameters": [ + { + "name": "param", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_array_integer_form_false": { + "get": { + "parameters": [ + { + "name": "param", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "form", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_array_string_form_true": { + "get": { + "parameters": [ + { + "name": "param", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_array_string_form_false": { + "get": { + "parameters": [ + { + "name": "param", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "form", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_object_form_true": { + "get": { + "parameters": [ + { + "name": "param", + "in": "query", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_object_form_false": { + "get": { + "parameters": [ + { + "name": "param", + "in": "query", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "form", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_two_integer_form_true": { + "get": { + "parameters": [ + { + "name": "param1", + "in": "query", + "required": true, + "schema": { + "type": "integer" + }, + "style": "form", + "explode": true + }, + { + "name": "param2", + "in": "query", + "required": true, + "schema": { + "type": "integer" + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_two_integer_form_mixed": { + "get": { + "parameters": [ + { + "name": "param1", + "in": "query", + "required": true, + "schema": { + "type": "integer" + }, + "style": "form", + "explode": true + }, + { + "name": "param2", + "in": "query", + "required": true, + "schema": { + "type": "integer" + }, + "style": "form", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_integer_string_form_true": { + "get": { + "parameters": [ + { + "name": "param1", + "in": "query", + "required": true, + "schema": { + "type": "integer" + }, + "style": "form", + "explode": true + }, + { + "name": "param2", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/query_integer_string_form_mixed": { + "get": { + "parameters": [ + { + "name": "param1", + "in": "query", + "required": true, + "schema": { + "type": "integer" + }, + "style": "form", + "explode": true + }, + { + "name": "param2", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "style": "form", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/complex_scenario1": { + "get": { + "parameters": [ + { + "name": "integer_param", + "in": "query", + "schema": { + "type": "integer" + }, + "style": "form", + "explode": true + }, + { + "name": "string_param", + "in": "query", + "schema": { + "type": "string" + }, + "style": "form", + "explode": true + }, + { + "name": "array_int_param", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/complex_scenario2": { + "get": { + "parameters": [ + { + "name": "integer_param", + "in": "query", + "schema": { + "type": "integer" + }, + "style": "form", + "explode": false + }, + { + "name": "array_str_param", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "form", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/complex_scenario3": { + "get": { + "parameters": [ + { + "name": "object_param", + "in": "query", + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "form", + "explode": true + }, + { + "name": "integer_param", + "in": "query", + "schema": { + "type": "integer" + }, + "style": "form", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/complex_scenario4": { + "get": { + "parameters": [ + { + "name": "integer_param", + "in": "query", + "schema": { + "type": "integer" + }, + "style": "form", + "explode": true + }, + { + "name": "string_param", + "in": "query", + "schema": { + "type": "string" + }, + "style": "form", + "explode": false + }, + { + "name": "array_int_param", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/complex_scenario5": { + "get": { + "parameters": [ + { + "name": "object1_param", + "in": "query", + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + } + } + }, + "style": "form", + "explode": false + }, + { + "name": "object2_param", + "in": "query", + "schema": { + "type": "object", + "properties": { + "fieldA": { + "type": "string" + } + } + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario1": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario2": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string", + "pattern": "^[a-z]+$" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario3": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario4": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario5": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + }, + "required": [ + "field1" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario6": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario7": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "array", + "items": { + "type": "integer" + } + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario8": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1, + "maxItems": 5 + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario9": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario11": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string", + "enum": [ + "option1", + "option2" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario12": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "field": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario13": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario14": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "properties": { + "basicField": { + "type": "integer" + } + } + }, + { + "type": "object", + "properties": { + "extraField": { + "type": "integer" + } + } + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario15": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "complexField": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "object", + "properties": { + "subfield1": { + "type": "string" + } + } + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario16": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario17": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario18": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "multiLogic": { + "oneOf": [ + { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario19": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "readField": { + "type": "string", + "readOnly": true + }, + "writeField": { + "type": "string", + "writeOnly": true + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/body_scenario20": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "level1": { + "type": "object", + "properties": { + "level2": { + "type": "object", + "properties": { + "level3": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_single1": { + "get": { + "parameters": [ + { + "name": "intHeader", + "in": "header", + "required": true, + "schema": { + "type": "integer" + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_single2": { + "get": { + "parameters": [ + { + "name": "stringHeader", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "style": "simple", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_single3": { + "get": { + "parameters": [ + { + "name": "intArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_single4": { + "get": { + "parameters": [ + { + "name": "stringArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "simple", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_single5": { + "get": { + "parameters": [ + { + "name": "objectHeader", + "in": "header", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_double1": { + "get": { + "parameters": [ + { + "name": "intHeader", + "in": "header", + "required": true, + "schema": { + "type": "integer" + }, + "style": "simple", + "explode": true + }, + { + "name": "stringHeader", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_double2": { + "get": { + "parameters": [ + { + "name": "intHeader", + "in": "header", + "required": true, + "schema": { + "type": "integer" + }, + "style": "simple", + "explode": false + }, + { + "name": "intArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "simple", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_double3": { + "get": { + "parameters": [ + { + "name": "stringHeader", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "style": "simple", + "explode": true + }, + { + "name": "stringArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_double4": { + "get": { + "parameters": [ + { + "name": "intArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "simple", + "explode": true + }, + { + "name": "stringArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_double5": { + "get": { + "parameters": [ + { + "name": "objectHeader", + "in": "header", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "simple", + "explode": true + }, + { + "name": "stringHeader", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_triple1": { + "get": { + "parameters": [ + { + "name": "intHeader", + "in": "header", + "required": true, + "schema": { + "type": "integer" + }, + "style": "simple", + "explode": true + }, + { + "name": "stringHeader", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "style": "simple", + "explode": true + }, + { + "name": "intArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_triple2": { + "get": { + "parameters": [ + { + "name": "intHeader", + "in": "header", + "required": true, + "schema": { + "type": "integer" + }, + "style": "simple", + "explode": false + }, + { + "name": "stringArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "simple", + "explode": true + }, + { + "name": "objectHeader", + "in": "header", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_triple3": { + "get": { + "parameters": [ + { + "name": "stringHeader", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "style": "simple", + "explode": true + }, + { + "name": "intArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "simple", + "explode": true + }, + { + "name": "stringArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "simple", + "explode": false + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_triple4": { + "get": { + "parameters": [ + { + "name": "intArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + }, + "style": "simple", + "explode": false + }, + { + "name": "stringArrayHeader", + "in": "header", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "simple", + "explode": true + }, + { + "name": "objectHeader", + "in": "header", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "simple", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/header_triple5": { + "get": { + "parameters": [ + { + "name": "objectHeader", + "in": "header", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "simple", + "explode": true + }, + { + "name": "stringHeader", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "style": "simple", + "explode": false + }, + { + "name": "intHeader", + "in": "header", + "required": true, + "schema": { + "type": "integer" + }, + "style": "simple", + "explode": true + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/api_key_header": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/test/api_key_query": { + "get": { + "security": [ + { + "ApiKeyQueryParamAuth": [] + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/test/content/{param1}/abc/{param2}/{param3}": { + "get": { + "parameters": [ + { + "name": "param1", + "in": "path", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + }, + { + "name": "param2", + "in": "path", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + { + "name": "param3", + "in": "path", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + } + } + } + }, + { + "name": "param4", + "in": "query", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + { + "name": "param5", + "in": "query", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + } + } + } + }, + { + "name": "param6", + "in": "query", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + } + } + } + }, + { + "name": "param7", + "in": "query", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + } + } + } + } + ], + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/all/{param1}/abc/{param2}/{param3}": { + "post": { + "parameters": [ + { + "name": "param1", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "style": "simple", + "explode": false + }, + { + "name": "param2", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "param3", + "in": "path", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + } + }, + { + "name": "param4", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "param5", + "in": "query", + "required": true, + "explode": false, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + } + }, + { + "name": "param6", + "in": "query", + "required": true, + "explode": false, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "form" + }, + { + "name": "param7", + "in": "query", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "form", + "explode": false + }, + { + "name": "param8", + "in": "query", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "spaceDelimited", + "explode": false + }, + { + "name": "param9", + "in": "query", + "required": true, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "pipeDelimited", + "explode": false + }, + { + "name": "param10", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "param11", + "in": "header", + "required": true, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "param12", + "in": "header", + "required": false, + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + } + } + }, + "style": "form", + "explode": true + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + }, + "field3": { + "type": "array", + "items": { + "type": "string" + } + }, + "field4": { + "type": "object", + "properties": { + "subfield1": { + "type": "integer" + }, + "subfield2": { + "type": "string" + } + } + }, + "field5": { + "type": "object", + "properties": { + "subfield1": { + "type": "integer" + }, + "subfield2": { + "type": "string" + } + }, + "required": [ + "subfield1" + ] + }, + "field6": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "field7": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "array", + "items": { + "type": "integer" + } + } + ] + }, + "field8": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1, + "maxItems": 5 + }, + "field9": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "field10": { + "type": "string", + "enum": [ + "option1", + "option2" + ] + }, + "field11": { + "type": "object", + "properties": { + "field": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + } + } + }, + "field12": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + }, + "/test/all/{param1}": { + "post": { + "parameters": [ + { + "name": "param1", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "style": "simple", + "explode": false + }, + { + "name": "param4", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + { + "name": "param11", + "in": "header", + "required": true, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "field1": { + "type": "integer" + }, + "field2": { + "type": "string" + }, + "field3": { + "type": "array", + "items": { + "type": "string" + } + }, + "field4": { + "type": "object", + "properties": { + "subfield1": { + "type": "integer" + }, + "subfield2": { + "type": "string" + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully received data." + } + } + } + } + }, + "components": { + "schemas": { + "Schema1": { + "type": "object", + "properties": { + "basicField": { + "type": "integer" + } + } + } + }, + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "Api-Key" + }, + "ApiKeyQueryParamAuth": { + "type": "apiKey", + "in": "query", + "name": "api_key" + } + } + } +} \ No newline at end of file diff --git a/kong-plugin-oasvalidator-1.0.0-1.rockspec b/kong-plugin-oasvalidator-1.0.0-1.rockspec new file mode 100644 index 0000000..97f8fce --- /dev/null +++ b/kong-plugin-oasvalidator-1.0.0-1.rockspec @@ -0,0 +1,38 @@ +local plugin_name = "oasvalidator" +local package_name = "kong-plugin-" .. plugin_name +local package_version = "1.0.0" +local rockspec_revision = "1" + +local github_account_name = "nawaz1991" +local github_repo_name = "kong-plugin-oasvalidator" +local git_checkout = package_version == "dev" and "master" or package_version + + +package = package_name +version = package_version .. "-" .. rockspec_revision +supported_platforms = { "linux", "macosx" } +source = { + url = "git+https://github.com/"..github_account_name.."/"..github_repo_name..".git", + branch = git_checkout, +} + + +description = { + summary = "A Kong plugin for validating HTTP requests against OpenAPI specifications.", + homepage = "https://"..github_account_name..".github.io/"..github_repo_name, + license = "MIT", +} + + +dependencies = { + "oasvalidator >= 1.0" +} + + +build = { + type = "builtin", + modules = { + ["kong.plugins."..plugin_name..".handler"] = "kong/plugins/"..plugin_name.."/handler.lua", + ["kong.plugins."..plugin_name..".schema"] = "kong/plugins/"..plugin_name.."/schema.lua", + } +} diff --git a/kong/plugins/oasvalidator/handler.lua b/kong/plugins/oasvalidator/handler.lua new file mode 100644 index 0000000..e3fcd97 --- /dev/null +++ b/kong/plugins/oasvalidator/handler.lua @@ -0,0 +1,69 @@ +-- Copyright (c) 2023 Muhammad Nawaz +-- Licensed under the MIT License. See LICENSE file for more information. +-- [ END OF LICENSE e2f4afc94de80d2c104519cd5b5e65ca0f4d5b62 ] + +local oasvalidator = require("oasvalidator") + +local kong_oasvalidator = { + PRIORITY = 1000, + VERSION = "1.0.0", +} + +local validators +local prefix_len = 1 + +function kong_oasvalidator:init_worker() +end + +local function handleError(err_code, err_msg) + kong.log.err("error_code: ", err_code, " error_message: ", err_msg) + return kong.response.exit(400, err_msg, {["Content-Type"] = "application/json"}) +end + +function kong_oasvalidator:access(conf) + if not validators then + validators = oasvalidator.GetValidators(conf.oas_spec_path) + local prefix = kong.request.get_forwarded_prefix() + prefix_len = prefix and (#prefix + 1) or 1 + end + + local method = kong.request.get_method() + local path = kong.request.get_path_with_query():sub(prefix_len) + local err_code, err_msg + + if conf.validate_request then + local body = kong.request.get_raw_body() + local headers = kong.request.get_headers() + err_code, err_msg = validators:ValidateRequest(method, path, body, headers) + if err_code ~= 0 then return handleError(err_code, err_msg) end + else + if conf.validate_body then + local body = kong.request.get_raw_body() + err_code, err_msg = validators:ValidateBody(method, path, body) + if err_code ~= 0 then return handleError(err_code, err_msg) end + end + + if conf.validate_path_params then + err_code, err_msg = validators:ValidatePathParam(method, path) + if err_code ~= 0 then return handleError(err_code, err_msg) end + end + + if conf.validate_query_params then + err_code, err_msg = validators:ValidateQueryParam(method, path) + if err_code ~= 0 then return handleError(err_code, err_msg) end + end + + if conf.validate_header_params then + local headers = kong.request.get_headers() + err_code, err_msg = validators:ValidateHeaders(method, path, headers) + if err_code ~= 0 then return handleError(err_code, err_msg) end + end + + if conf.validate_route then + err_code, err_msg = validators:ValidateRoute(method, path) + if err_code ~= 0 then return handleError(err_code, err_msg) end + end + end +end + +return kong_oasvalidator diff --git a/kong/plugins/oasvalidator/schema.lua b/kong/plugins/oasvalidator/schema.lua new file mode 100644 index 0000000..751449b --- /dev/null +++ b/kong/plugins/oasvalidator/schema.lua @@ -0,0 +1,122 @@ +-- Copyright (c) 2023 Muhammad Nawaz +-- Licensed under the MIT License. See LICENSE file for more information. +-- [ END OF LICENSE e2f4afc94de80d2c104519cd5b5e65ca0f4d5b62 ] + +local typedefs = require "kong.db.schema.typedefs" + +local PLUGIN_NAME = "oasvalidator" + +local oas_validator_config_schema = { + name = PLUGIN_NAME, + fields = { + { consumer = typedefs.no_consumer }, + { protocols = typedefs.protocols_http }, + { config = { + type = "record", + fields = { + { oas_spec_path = { + type = "string", + required = true, + }, + }, + { validate_request = { + type = "boolean", + default = true, + required = false, + }, + }, + { validate_body = { + type = "boolean", + default = false, + required = false, + }, + }, + { validate_path_params = { + type = "boolean", + default = false, + required = false, + }, + }, + { validate_query_params = { + type = "boolean", + default = false, + required = false, + }, + }, + { validate_header_params = { + type = "boolean", + default = false, + required = false, + }, + }, + { validate_route = { + type = "boolean", + default = false, + required = false, + }, + }, + }, + entity_checks = { + { + conditional = { + if_field = "validate_request", + if_match = { eq = true }, + then_field = "validate_route", + then_match = { eq = false } + } + }, + { + conditional = { + if_field = "validate_request", + if_match = { eq = true }, + then_field = "validate_path_params", + then_match = { eq = false } + } + }, + { + conditional = { + if_field = "validate_request", + if_match = { eq = true }, + then_field = "validate_query_params", + then_match = { eq = false } + } + }, + { + conditional = { + if_field = "validate_request", + if_match = { eq = true }, + then_field = "validate_header_params", + then_match = { eq = false } + } + }, + { + conditional = { + if_field = "validate_path_params", + if_match = { eq = true }, + then_field = "validate_route", + then_match = { eq = false } + } + }, + { + conditional = { + if_field = "validate_query_params", + if_match = { eq = true }, + then_field = "validate_route", + then_match = { eq = false } + } + }, + { + conditional = { + if_field = "validate_header_params", + if_match = { eq = true }, + then_field = "validate_route", + then_match = { eq = false } + } + }, + }, + }, + }, + }, +} + +return oas_validator_config_schema diff --git a/spec/oasvalidator/01-schema_spec.lua b/spec/oasvalidator/01-schema_spec.lua new file mode 100644 index 0000000..0bcfa9c --- /dev/null +++ b/spec/oasvalidator/01-schema_spec.lua @@ -0,0 +1,29 @@ +-- Copyright (c) 2023 Muhammad Nawaz +-- Licensed under the MIT License. See LICENSE file for more information. +-- [ END OF LICENSE e2f4afc94de80d2c104519cd5b5e65ca0f4d5b62 ] + +local PLUGIN_NAME = "oasvalidator" + + +-- helper function to validate data against a schema +local validate do + local validate_entity = require("spec.helpers").validate_plugin_config_schema + local plugin_schema = require("kong.plugins."..PLUGIN_NAME..".schema") + + function validate(data) + return validate_entity(data, plugin_schema) + end +end + + +describe(PLUGIN_NAME .. ": (schema)", function() + + it("OAS spec file provided", function() + local ok, err = validate({ + oas_spec_path = "/data/openAPI_example.json", + }) + assert.is_nil(err) + assert.is_truthy(ok) + end) + +end) diff --git a/spec/oasvalidator/02-path_param_validator_spec.lua b/spec/oasvalidator/02-path_param_validator_spec.lua new file mode 100644 index 0000000..aaf9fbf --- /dev/null +++ b/spec/oasvalidator/02-path_param_validator_spec.lua @@ -0,0 +1,106 @@ +-- Copyright (c) 2023 Muhammad Nawaz +-- Licensed under the MIT License. See LICENSE file for more information. +-- [ END OF LICENSE e2f4afc94de80d2c104519cd5b5e65ca0f4d5b62 ] + +local helpers = require "spec.helpers" +local cjson = require "cjson" + + +local PLUGIN_NAME = "oasvalidator" + + +for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then + describe(PLUGIN_NAME .. ": (access) [#" .. strategy .. "]", function() + local client + + lazy_setup(function() + + local bp = helpers.get_db_utils(strategy == "off" and "postgres" or strategy, nil, { PLUGIN_NAME }) + + -- Inject a test route. No need to create a service, there is a default + -- service which will echo the request. + local route1 = bp.routes:insert({ + hosts = { "test1.com" }, + }) + -- add the plugin to test to the route we created + bp.plugins:insert { + name = PLUGIN_NAME, + route = { id = route1.id }, + config = { oas_spec_path = "/data/openAPI_example.json", + validate_request = false, + validate_path_params = true,}, + } + + -- start kong + assert(helpers.start_kong({ + -- set the strategy + database = strategy, + -- use the custom test template to create a local mock server + nginx_conf = "spec/fixtures/custom_nginx.template", + -- make sure our plugin gets loaded + plugins = "bundled," .. PLUGIN_NAME, + -- write & load declarative config, only if 'strategy=off' + declarative_config = strategy == "off" and helpers.make_yaml_file() or nil, + })) + end) + + lazy_teardown(function() + helpers.stop_kong(nil, true) + end) + + before_each(function() + client = helpers.proxy_client() + end) + + after_each(function() + if client then client:close() end + end) + + describe("Invalid:", function() + it("Path param", function() + local r = client:get("/test/integer_simple_true/not_an_integer", { + headers = { + host = "test1.com" + } + }) + -- validate that the request is unsuccessful, response status 400 + assert.response(r).has.status(400) + local body = assert.res_status(400, r) + local json = cjson.decode(body) + assert.equal("INVALID_PATH_PARAM", json["errorCode"]) + end) + end) + + describe("Invalid:", function() + it("Path param", function() + local r = client:get("/test/integer_simple_false/not_an_integer", { + headers = { + host = "test1.com" + } + }) + -- validate that the request is unsuccessful, response status 400 + assert.response(r).has.status(400) + local body = assert.res_status(400, r) + local json = cjson.decode(body) + assert.equal("INVALID_PATH_PARAM", json["errorCode"]) + end) + end) + + describe("Invalid:", function() + it("Path param", function() + local r = client:get("/test/integer_label_true/123", { + headers = { + host = "test1.com" + } + }) + -- validate that the request is unsuccessful, response status 400 + assert.response(r).has.status(400) + local body = assert.res_status(400, r) + local json = cjson.decode(body) + assert.equal("INVALID_PATH_PARAM", json["errorCode"]) + end) + end) + + end) + +end end diff --git a/spec/oasvalidator/02-query_param_validator_spec.lua b/spec/oasvalidator/02-query_param_validator_spec.lua new file mode 100644 index 0000000..db9c82a --- /dev/null +++ b/spec/oasvalidator/02-query_param_validator_spec.lua @@ -0,0 +1,91 @@ +-- Copyright (c) 2023 Muhammad Nawaz +-- Licensed under the MIT License. See LICENSE file for more information. +-- [ END OF LICENSE e2f4afc94de80d2c104519cd5b5e65ca0f4d5b62 ] + +local helpers = require "spec.helpers" +local cjson = require "cjson" + + +local PLUGIN_NAME = "oasvalidator" + + +for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then + describe(PLUGIN_NAME .. ": (access) [#" .. strategy .. "]", function() + local client + + lazy_setup(function() + + local bp = helpers.get_db_utils(strategy == "off" and "postgres" or strategy, nil, { PLUGIN_NAME }) + + -- Inject a test route. No need to create a service, there is a default + -- service which will echo the request. + local route1 = bp.routes:insert({ + hosts = { "test1.com" }, + }) + -- add the plugin to test to the route we created + bp.plugins:insert { + name = PLUGIN_NAME, + route = { id = route1.id }, + config = { oas_spec_path = "/data/openAPI_example.json", + validate_request = false, + validate_query_params = true,}, + } + + -- start kong + assert(helpers.start_kong({ + -- set the strategy + database = strategy, + -- use the custom test template to create a local mock server + nginx_conf = "spec/fixtures/custom_nginx.template", + -- make sure our plugin gets loaded + plugins = "bundled," .. PLUGIN_NAME, + -- write & load declarative config, only if 'strategy=off' + declarative_config = strategy == "off" and helpers.make_yaml_file() or nil, + })) + end) + + lazy_teardown(function() + helpers.stop_kong(nil, true) + end) + + before_each(function() + client = helpers.proxy_client() + end) + + after_each(function() + if client then client:close() end + end) + + describe("Invalid:", function() + it("Query param", function() + local r = client:get("/test/query_array_integer_form_true?124.4565", { + headers = { + host = "test1.com" + } + }) + -- validate that the request is unsuccessful, response status 400 + assert.response(r).has.status(400) + local body = assert.res_status(400, r) + local json = cjson.decode(body) + assert.equal("INVALID_QUERY_PARAM", json["errorCode"]) + end) + end) + + describe("Invalid:", function() + it("Query param", function() + local r = client:get("/test/query_array_integer_form_true?not_a_number", { + headers = { + host = "test1.com" + } + }) + -- validate that the request is unsuccessful, response status 400 + assert.response(r).has.status(400) + local body = assert.res_status(400, r) + local json = cjson.decode(body) + assert.equal("INVALID_QUERY_PARAM", json["errorCode"]) + end) + end) + + end) + +end end diff --git a/spec/oasvalidator/02-request_validator_spec.lua b/spec/oasvalidator/02-request_validator_spec.lua new file mode 100644 index 0000000..6f4934c --- /dev/null +++ b/spec/oasvalidator/02-request_validator_spec.lua @@ -0,0 +1,104 @@ +-- Copyright (c) 2023 Muhammad Nawaz +-- Licensed under the MIT License. See LICENSE file for more information. +-- [ END OF LICENSE e2f4afc94de80d2c104519cd5b5e65ca0f4d5b62 ] + +local helpers = require "spec.helpers" +local cjson = require "cjson" + + +local PLUGIN_NAME = "oasvalidator" + + +for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then + describe(PLUGIN_NAME .. ": (access) [#" .. strategy .. "]", function() + local client + + lazy_setup(function() + + local bp = helpers.get_db_utils(strategy == "off" and "postgres" or strategy, nil, { PLUGIN_NAME }) + + -- Inject a test route. No need to create a service, there is a default + -- service which will echo the request. + local route1 = bp.routes:insert({ + hosts = { "test1.com" }, + }) + -- add the plugin to test to the route we created + bp.plugins:insert { + name = PLUGIN_NAME, + route = { id = route1.id }, + config = { oas_spec_path = "/data/openAPI_example.json", + validate_request = true }, + } + + -- start kong + assert(helpers.start_kong({ + -- set the strategy + database = strategy, + -- use the custom test template to create a local mock server + nginx_conf = "spec/fixtures/custom_nginx.template", + -- make sure our plugin gets loaded + plugins = "bundled," .. PLUGIN_NAME, + -- write & load declarative config, only if 'strategy=off' + declarative_config = strategy == "off" and helpers.make_yaml_file() or nil, + })) + end) + + lazy_teardown(function() + helpers.stop_kong(nil, true) + end) + + before_each(function() + client = helpers.proxy_client() + end) + + after_each(function() + if client then client:close() end + end) + + -- Valid request + describe("valid request", function() + it("A test valid request", function() + local r = client:get("/test/query_two_integer_form_mixed?param1=123¶m2=6", { + headers = { + host = "test1.com" + } + }) + -- validate that the request succeeded, response status 404 as no valid upstream server present + assert.response(r).has.status(404) + end) + end) + + describe("invalid path", function() + it("An invalid test request", function() + local r = client:get("/invalid/path", { + headers = { + host = "test1.com" + } + }) + -- validate that the request is unsuccessful, response status 400 + assert.response(r).has.status(400) + local body = assert.res_status(400, r) + local json = cjson.decode(body) + assert.equal("INVALID_ROUTE", json["errorCode"]) + end) + end) + + -- Invalid query param + describe("invalid query param", function() + it("An invalid test request", function() + local r = client:get("/test/query_two_integer_form_mixed?param1=123¶m2=not_a_num", { + headers = { + host = "test1.com" + } + }) + -- validate that the request is unsuccessful, response status 400 + assert.response(r).has.status(400) + local body = assert.res_status(400, r) + local json = cjson.decode(body) + assert.equal("INVALID_QUERY_PARAM", json["errorCode"]) + end) + end) + + end) + +end end