├── .gitignore ├── .scrutinizer.yml ├── .travis.analyze.sh ├── .travis.coverage.sh ├── .travis.yml ├── README.md ├── composer.json ├── phpcs.xml.dist ├── phpmd.xml.dist ├── phpstan.neon ├── phpunit.xml.dist ├── src ├── Decoder │ ├── Adapter │ │ └── SymfonyDecoderAdapter.php │ ├── DecoderInterface.php │ └── DecoderUtils.php ├── Definition │ ├── MessageDefinition.php │ ├── Parameter.php │ ├── Parameters.php │ ├── RequestDefinition.php │ ├── RequestDefinitions.php │ └── ResponseDefinition.php ├── Factory │ ├── CachedSchemaFactoryDecorator.php │ ├── SchemaFactory.php │ └── SwaggerSchemaFactory.php ├── JsonSchema │ └── Uri │ │ └── YamlUriRetriever.php ├── Normalizer │ └── QueryParamsNormalizer.php ├── Schema.php └── Validator │ ├── ConstraintViolation.php │ ├── Exception │ └── ConstraintViolations.php │ └── MessageValidator.php └── tests ├── Decoder ├── Adapter │ └── SymfonyDecoderAdapterTest.php └── DecoderUtilsTest.php ├── Definition ├── RequestDefinitionTest.php ├── RequestDefinitionsTest.php ├── RequestParameterTest.php ├── RequestParametersTest.php └── ResponseDefinitionTest.php ├── Factory ├── CachedSchemaFactoryDecoratorTest.php └── SwaggerSchemaFactoryTest.php ├── JsonSchema └── Uri │ └── YamlUriRetrieverTest.php ├── Normalizer └── QueryParamsNormalizerTest.php ├── SchemaTest.php ├── Validator ├── ConstraintViolationTest.php └── MessageValidatorTest.php └── fixtures ├── operation-without-an-id.json ├── operation-without-parameters.json ├── operation-without-responses.json ├── petstore.json ├── petstore.txt ├── petstore.yaml ├── request-with-conflicting-locations.json ├── request-without-content-types.json └── schema-with-default-consumes-and-produces-properties.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /vendor/ 3 | /composer.lock 4 | /build 5 | /phpstan.phar 6 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | tools: 2 | external_code_coverage: 3 | timeout: 600 4 | 5 | build: 6 | environment: 7 | php: 8 | version: 7.2 9 | nodes: 10 | analysis: 11 | tests: 12 | override: 13 | - command: php-scrutinizer-run 14 | - command: phpcs-run --standard=phpcs.xml.dist 15 | use_website_config: false 16 | -------------------------------------------------------------------------------- /.travis.analyze.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | version() { 6 | printf "%03d%03d" $(echo "$1" | tr '.' ' '); 7 | } 8 | 9 | phpstan_download() { 10 | if [ ! -f "phpstan.phar" ];then 11 | curl -sOL https://github.com/phpstan/phpstan/releases/download/0.10.5/phpstan.phar 12 | fi 13 | } 14 | 15 | phpstan_analyze() { 16 | php ./phpstan.phar analyze --ansi 17 | } 18 | 19 | main() { 20 | PHP_VERSION=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;") 21 | if [ $(version $PHP_VERSION) -ge $(version "7.1") ]; then 22 | phpstan_download 23 | phpstan_analyze 24 | else 25 | echo "[warn] you need at least PHP 7.1 to use analyze the project with phpstan" 26 | fi 27 | } 28 | 29 | main 30 | -------------------------------------------------------------------------------- /.travis.coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | main() { 6 | wget -q https://scrutinizer-ci.com/ocular.phar 7 | php ocular.phar code-coverage:upload --format=php-clover ./build/phpunit.coverage.xml 8 | } 9 | 10 | main 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | cache: 6 | directories: 7 | - $HOME/.composer/cache/files 8 | 9 | matrix: 10 | include: 11 | - php: 5.6 12 | env: SYMFONY_VERSION='^2' 13 | - php: 5.6 14 | env: SYMFONY_VERSION='^3' 15 | - php: 7.0 16 | env: SYMFONY_VERSION='^3' 17 | - php: 7.1 18 | env: SYMFONY_VERSION='^3' PHPUNIT_ARGS='--testdox' 19 | - php: 7.2 20 | env: SYMFONY_VERSION='^3' PHPUNIT_ARGS='--testdox' 21 | - php: 7.2 22 | env: SYMFONY_VERSION='^4' COVERAGE='' PHPCS='' PHPSTAN='' PHPUNIT_ARGS='--testdox --coverage-text --coverage-clover=build/phpunit.coverage.xml' 23 | 24 | before_install: 25 | - if [ -z "${COVERAGE+x}" ];then echo 'disabling xdebug'; phpenv config-rm xdebug.ini || true;fi 26 | 27 | before_script: 28 | - composer require --dev --no-update dunglas/symfony-lock=$SYMFONY_VERSION 29 | - composer update $COMPOSER_FLAGS --prefer-dist 30 | 31 | script: 32 | - vendor/bin/phpunit $PHPUNIT_ARGS --disallow-test-output --colors=always 33 | - if [ -n "${PHPCS+x}" ];then php vendor/bin/phpcs;fi 34 | - if [ -n "${PHPSTAN+x}" ];then sh .travis.analyze.sh ;fi 35 | 36 | after_script: 37 | - if [ -n "${COVERAGE+x}" ];then sh .travis.coverage.sh;fi 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API Validator 2 | 3 | [![Build Status](https://travis-ci.org/eleven-labs/api-validator.svg?branch=master)](https://travis-ci.org/eleven-labs/api-validator) 4 | [![Code Coverage](https://scrutinizer-ci.com/g/eleven-labs/api-validator/badges/coverage.png)](https://scrutinizer-ci.com/g/eleven-labs/api-validator/) 5 | [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/eleven-labs/api-validator/badges/quality-score.png)](https://scrutinizer-ci.com/g/eleven-labs/api-validator/) 6 | 7 | This library provides a set of classes suited to describe a WebService based on the HTTP protocol. 8 | 9 | It can validate [PSR-7 Requests](http://www.php-fig.org/psr/psr-7/) against a schema. 10 | 11 | It's design is heavily inspired by the OpenAPI/Swagger2.0 specifications. 12 | 13 | As of now, it only support the OpenAPi/Swagger2.0 specifications but we plan to 14 | support [RAML 1.0](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/) 15 | and [API Elements (API Blueprint)](https://github.com/apiaryio/api-elements) in the future. 16 | 17 | ## Dependencies 18 | 19 | We rely on the [justinrainbow/json-schema](https://github.com/justinrainbow/json-schema) library 20 | to parse specification files and to validate requests and responses: 21 | 22 | - Request's `headers`, `query`, `uri` and `body` parts. 23 | - Response `headers` and `body` parts. 24 | 25 | ## Usage 26 | 27 | ### Before you start 28 | 29 | You will need to write a **valid** Swagger 2.0 file in order to use this library. Ensure that this file is valid using 30 | the [Swagger Editor](http://editor.swagger.io/). 31 | 32 | You can also validate your specifications 33 | using the [Swagger 2.0 JSONSchema](https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v2.0/schema.json). 34 | 35 | ### Validate a request 36 | 37 | You can validate any PSR-7: 38 | 39 | - Request implementing the `Psr\Http\Message\RequestInterface` interface. 40 | - Response implementing the `Psr\Http\Message\ResponseInterface` interface. 41 | 42 | ```php 43 | createSchema('file://path/to/your/swagger.json'); 79 | 80 | // Find the Request Definition in the Schema API 81 | $requestDefinition = $schema->getRequestDefinition( 82 | $schema->findOperationId($request->getMethod(), $request->getUri()->getPath()) 83 | ); 84 | 85 | // Validate the Request 86 | $messageValidator = new MessageValidator($validator, $decoder); 87 | $messageValidator->validateRequest($request, $requestDefinition); 88 | 89 | // Check if the request has violations 90 | if ($messageValidator->hasViolations()) { 91 | // Get violations and do something with them 92 | $violations = $messageValidator->getViolations(); 93 | } 94 | 95 | // Using the message Validator, you can also validate a Response 96 | // It will find the proper ResponseDefinition from a RequestDefinition 97 | $response = new Response( 98 | 200, 99 | ['Content-Type' => 'application/json'], 100 | '{"id": 1}' 101 | ); 102 | 103 | $messageValidator->validateResponse($response, $requestDefinition); 104 | 105 | // ... 106 | ``` 107 | 108 | ### Working with Symfony HTTPFoundation Requests 109 | 110 | You will need an adapter in order to validate symfony requests. 111 | 112 | We recommend you to use the [symfony/psr-http-message-bridge](https://github.com/symfony/psr-http-message-bridge) 113 | 114 | ### Using the schema 115 | 116 | You can navigate the `ElevenLabs\Api\Schema` to meet other use cases. 117 | 118 | Example: 119 | 120 | ```php 121 | createSchema('file://path/to/your/swagger.json'); 125 | 126 | // Find a request definition from an HTTP method and a path. 127 | $requestDefinition = $schema->getRequestDefinition( 128 | $schema->findOperationId('GET', '/pets/1234') 129 | ); 130 | 131 | // Get the response definition for the status code 200 (HTTP OK) 132 | $responseDefinition = $requestDefinition->getResponseDefinition(200); 133 | 134 | // From here, you can access the JSON Schema describing the expected response 135 | $responseSchema = $responseDefinition->getBodySchema(); 136 | ``` 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleven-labs/api-validator", 3 | "description": "Validate PSR-7 Requests against an OpenAPI/Swagger2 Schema", 4 | "authors": [ 5 | { 6 | "name": "Guillem CANAL", 7 | "email": "gcanal@eleven-labs.com" 8 | } 9 | ], 10 | "autoload": { 11 | "psr-4": { 12 | "ElevenLabs\\Api\\": "src/" 13 | } 14 | }, 15 | "autoload-dev": { 16 | "psr-4": { 17 | "ElevenLabs\\Api\\": "tests/" 18 | }, 19 | "files": [ 20 | "vendor/phpunit/phpunit/src/Framework/Assert/Functions.php" 21 | ] 22 | }, 23 | "require": { 24 | "php": "^5.6 || ^7.0", 25 | "ext-json": "*", 26 | "psr/http-message": "^1.0", 27 | "psr/cache": "^1.0", 28 | "justinrainbow/json-schema": "^2.0", 29 | "rize/uri-template": "^0.3.0" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^5.4 || ^7.4", 33 | "guzzlehttp/psr7": "^1.3", 34 | "squizlabs/php_codesniffer": "^3.3", 35 | "symfony/yaml": "^2.7 || ^3.4 || ^4.0", 36 | "symfony/serializer": "^2.7 || ^3.4 || ^4.0" 37 | }, 38 | "scripts": { 39 | "test": "vendor/bin/phpunit --disallow-test-output --coverage-text --colors=always", 40 | "cs": "vendor/bin/phpcs", 41 | "phpstan": "sh .travis.analyze.sh" 42 | }, 43 | "suggest": { 44 | "symfony/yaml": "Allow the SwaggerSchemaFactory to handle YAML files", 45 | "symfony/serializer": "Implementation that can decode JSON or XML request bodies" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./src 18 | ./tests 19 | 20 | 21 | -------------------------------------------------------------------------------- /phpmd.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 0 3 | paths: 4 | - src 5 | - tests 6 | 7 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | tests 23 | 24 | 25 | 26 | 27 | 28 | ./src/ 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Decoder/Adapter/SymfonyDecoderAdapter.php: -------------------------------------------------------------------------------- 1 | decoder = $decoder; 17 | } 18 | 19 | public function decode($data, $format) 20 | { 21 | $context = []; 22 | 23 | if ($format === 'json') { 24 | // the JSON schema validator need an object hierarchy 25 | $context['json_decode_associative'] = false; 26 | } 27 | 28 | $decoded = $this->decoder->decode($data, $format, $context); 29 | 30 | if ($format === 'xml') { 31 | // the JSON schema validator need an object hierarchy 32 | $decoded = json_decode(json_encode($decoded)); 33 | } 34 | 35 | return $decoded; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Decoder/DecoderInterface.php: -------------------------------------------------------------------------------- 1 | 'application/x-www-form-urlencoded', 'body' => 'application/json']; 9 | 10 | /** 11 | * Location of the parameter in the request 12 | * 13 | * @var string 14 | */ 15 | private $location; 16 | 17 | /** 18 | * @var string 19 | */ 20 | private $name; 21 | 22 | /** 23 | * Indicate if the parameter should be present 24 | * 25 | * @var bool 26 | */ 27 | private $required; 28 | 29 | /** 30 | * A JSON Schema object 31 | * 32 | * @var \stdClass 33 | */ 34 | private $schema; 35 | 36 | public function __construct($location, $name, $required, \stdClass $schema = null) 37 | { 38 | $this->location = $location; 39 | $this->name = $name; 40 | $this->required = $required; 41 | $this->schema = $schema; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getLocation() 48 | { 49 | return $this->location; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getName() 56 | { 57 | return $this->name; 58 | } 59 | 60 | /** 61 | * @return boolean 62 | */ 63 | public function isRequired() 64 | { 65 | return $this->required; 66 | } 67 | 68 | /** 69 | * @return \stdClass 70 | */ 71 | public function getSchema() 72 | { 73 | return $this->schema; 74 | } 75 | 76 | /** 77 | * @return bool 78 | */ 79 | public function hasSchema() 80 | { 81 | return $this->schema !== null; 82 | } 83 | 84 | public function serialize() 85 | { 86 | return serialize([ 87 | 'location' => $this->location, 88 | 'name' => $this->name, 89 | 'required' => $this->required, 90 | 'schema' => $this->schema 91 | ]); 92 | } 93 | 94 | public function unserialize($serialized) 95 | { 96 | $data = unserialize($serialized); 97 | $this->location = $data['location']; 98 | $this->name = $data['name']; 99 | $this->required = $data['required']; 100 | $this->schema = $data['schema']; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Definition/Parameters.php: -------------------------------------------------------------------------------- 1 | parameters = []; 16 | foreach ($parameters as $parameter) { 17 | $this->addParameter($parameter); 18 | } 19 | } 20 | 21 | public function getIterator() 22 | { 23 | return new \ArrayIterator($this->parameters); 24 | } 25 | 26 | public function hasBodySchema() 27 | { 28 | $body = $this->getBody(); 29 | 30 | return ($body !== null && $body->hasSchema()); 31 | } 32 | 33 | /** 34 | * JSON Schema for a the body 35 | * 36 | * @return \stdClass|null 37 | */ 38 | public function getBodySchema() 39 | { 40 | return $this->getBody()->getSchema(); 41 | } 42 | 43 | /** 44 | * @return bool 45 | */ 46 | public function hasQueryParametersSchema() 47 | { 48 | return ($this->getSchema($this->getQuery()) !== null); 49 | } 50 | 51 | /** 52 | * JSON Schema for a the query parameters 53 | * 54 | * @return \stdClass 55 | */ 56 | public function getQueryParametersSchema() 57 | { 58 | return $this->getSchema($this->getQuery()); 59 | } 60 | 61 | /** 62 | * @return bool 63 | */ 64 | public function hasHeadersSchema() 65 | { 66 | return ($this->getHeadersSchema() !== null); 67 | } 68 | 69 | /** 70 | * JSON Schema for the headers 71 | * 72 | * @return \stdClass 73 | */ 74 | public function getHeadersSchema() 75 | { 76 | return $this->getSchema($this->getHeaders()); 77 | } 78 | 79 | /** 80 | * @return Parameter[] 81 | */ 82 | public function getPath() 83 | { 84 | return $this->findByLocation('path'); 85 | } 86 | 87 | /** 88 | * @return Parameter[] 89 | */ 90 | public function getQuery() 91 | { 92 | return $this->findByLocation('query'); 93 | } 94 | 95 | /** 96 | * @return Parameter[] 97 | */ 98 | public function getHeaders() 99 | { 100 | return $this->findByLocation('header'); 101 | } 102 | 103 | /** 104 | * @return Parameter|null 105 | */ 106 | public function getBody() 107 | { 108 | $match = $this->findByLocation('body'); 109 | if (empty($match)) { 110 | return null; 111 | } 112 | 113 | return current($match); 114 | } 115 | 116 | /** 117 | * Get one request parameter by name 118 | * 119 | * @param string $name 120 | * @return Parameter|null 121 | */ 122 | public function getByName($name) 123 | { 124 | if (! isset($this->parameters[$name])) { 125 | return null; 126 | } 127 | 128 | return $this->parameters[$name]; 129 | } 130 | 131 | /** 132 | * @param Parameter[] $parameters 133 | * 134 | * @return \stdClass|null 135 | */ 136 | private function getSchema(array $parameters) 137 | { 138 | if (empty($parameters)) { 139 | return null; 140 | } 141 | 142 | $schema = new \stdClass(); 143 | $schema->type = 'object'; 144 | $schema->required = []; 145 | $schema->properties = new \stdClass(); 146 | foreach ($parameters as $name => $parameter) { 147 | if ($parameter->isRequired()) { 148 | $schema->required[] = $parameter->getName(); 149 | } 150 | $schema->properties->{$name} = $parameter->getSchema(); 151 | } 152 | 153 | return $schema; 154 | } 155 | 156 | public function serialize() 157 | { 158 | return serialize(['parameters' => $this->parameters]); 159 | } 160 | 161 | public function unserialize($serialized) 162 | { 163 | $data = unserialize($serialized); 164 | $this->parameters = $data['parameters']; 165 | } 166 | 167 | private function findByLocation($location) 168 | { 169 | return array_filter( 170 | $this->parameters, 171 | function (Parameter $parameter) use ($location) { 172 | return $parameter->getLocation() === $location; 173 | } 174 | ); 175 | } 176 | 177 | private function addParameter(Parameter $parameter) 178 | { 179 | if (!in_array($parameter->getLocation(), Parameter::LOCATIONS, true)) { 180 | throw new \InvalidArgumentException( 181 | sprintf( 182 | '%s is not a supported parameter location, supported: %s', 183 | $parameter->getLocation(), 184 | implode(', ', Parameter::LOCATIONS) 185 | ) 186 | ); 187 | } 188 | 189 | $this->parameters[$parameter->getName()] = $parameter; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Definition/RequestDefinition.php: -------------------------------------------------------------------------------- 1 | method = $method; 35 | $this->operationId = $operationId; 36 | $this->pathTemplate = $pathTemplate; 37 | $this->parameters = $parameters; 38 | $this->contentTypes = $contentTypes; 39 | foreach ($responses as $response) { 40 | $this->addResponseDefinition($response); 41 | } 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getMethod() 48 | { 49 | return $this->method; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getOperationId() 56 | { 57 | return $this->operationId; 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getPathTemplate() 64 | { 65 | return $this->pathTemplate; 66 | } 67 | 68 | /** 69 | * @return Parameters 70 | */ 71 | public function getRequestParameters() 72 | { 73 | return $this->parameters; 74 | } 75 | 76 | /** 77 | * Supported content types 78 | * 79 | * @return array 80 | */ 81 | public function getContentTypes() 82 | { 83 | return $this->contentTypes; 84 | } 85 | 86 | /** 87 | * @param $statusCode 88 | * @return ResponseDefinition 89 | */ 90 | public function getResponseDefinition($statusCode) 91 | { 92 | if (isset($this->responses[$statusCode])) { 93 | return $this->responses[$statusCode]; 94 | } else if (isset($this->responses['default'])) { 95 | return $this->responses['default']; 96 | } else { 97 | throw new \InvalidArgumentException( 98 | sprintf( 99 | 'No response definition for %s %s is available for status code %s', 100 | $this->method, 101 | $this->pathTemplate, 102 | $statusCode 103 | ) 104 | ); 105 | } 106 | } 107 | 108 | public function hasBodySchema() 109 | { 110 | return $this->parameters->hasBodySchema(); 111 | } 112 | 113 | public function getBodySchema() 114 | { 115 | return $this->parameters->getBodySchema(); 116 | } 117 | 118 | public function hasHeadersSchema() 119 | { 120 | return $this->parameters->hasHeadersSchema(); 121 | } 122 | 123 | public function getHeadersSchema() 124 | { 125 | return $this->parameters->getHeadersSchema(); 126 | } 127 | 128 | public function hasQueryParametersSchema() 129 | { 130 | return $this->parameters->hasQueryParametersSchema(); 131 | } 132 | 133 | public function getQueryParametersSchema() 134 | { 135 | return $this->parameters->getQueryParametersSchema(); 136 | } 137 | 138 | private function addResponseDefinition(ResponseDefinition $response) 139 | { 140 | $this->responses[$response->getStatusCode()] = $response; 141 | } 142 | 143 | public function serialize() 144 | { 145 | return serialize([ 146 | 'method' => $this->method, 147 | 'operationId' => $this->operationId, 148 | 'pathTemplate' => $this->pathTemplate, 149 | 'parameters' => $this->parameters, 150 | 'contentTypes' => $this->contentTypes, 151 | 'responses' => $this->responses 152 | ]); 153 | } 154 | 155 | public function unserialize($serialized) 156 | { 157 | $data = unserialize($serialized); 158 | $this->method = $data['method']; 159 | $this->operationId = $data['operationId']; 160 | $this->pathTemplate = $data['pathTemplate']; 161 | $this->parameters = $data['parameters']; 162 | $this->contentTypes = $data['contentTypes']; 163 | $this->responses = $data['responses']; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Definition/RequestDefinitions.php: -------------------------------------------------------------------------------- 1 | addRequestDefinition($requestDefinition); 15 | } 16 | } 17 | 18 | /** 19 | * @param string $operationId 20 | * 21 | * @return RequestDefinition 22 | */ 23 | public function getRequestDefinition($operationId) 24 | { 25 | if (!isset($this->definitions[$operationId])) { 26 | throw new \InvalidArgumentException('Unable to find request definition for operationId '.$operationId); 27 | } 28 | 29 | return $this->definitions[$operationId]; 30 | } 31 | 32 | private function addRequestDefinition(RequestDefinition $requestDefinition) 33 | { 34 | $this->definitions[$requestDefinition->getOperationId()] = $requestDefinition; 35 | } 36 | 37 | /** 38 | * @return \ArrayIterator|RequestDefinition[] 39 | */ 40 | public function getIterator() 41 | { 42 | return new \ArrayIterator($this->definitions); 43 | } 44 | 45 | public function serialize() 46 | { 47 | return serialize([ 48 | 'definitions' => $this->definitions 49 | ]); 50 | } 51 | 52 | public function unserialize($serialized) 53 | { 54 | $data = unserialize($serialized); 55 | $this->definitions = $data['definitions']; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Definition/ResponseDefinition.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 23 | $this->contentTypes = $allowedContentTypes; 24 | $this->parameters = $parameters; 25 | } 26 | 27 | /** 28 | * @return int 29 | */ 30 | public function getStatusCode() 31 | { 32 | return $this->statusCode; 33 | } 34 | 35 | /** 36 | * @return bool 37 | */ 38 | public function hasBodySchema() 39 | { 40 | return $this->parameters->hasBodySchema(); 41 | } 42 | 43 | /** 44 | * @return \stdClass 45 | */ 46 | public function getBodySchema() 47 | { 48 | return $this->parameters->getBodySchema(); 49 | } 50 | 51 | public function hasHeadersSchema() 52 | { 53 | return $this->parameters->hasHeadersSchema(); 54 | } 55 | 56 | public function getHeadersSchema() 57 | { 58 | return $this->parameters->getHeadersSchema(); 59 | } 60 | 61 | /** 62 | * Supported response types 63 | * @return array 64 | */ 65 | public function getContentTypes() 66 | { 67 | return $this->contentTypes; 68 | } 69 | 70 | public function serialize() 71 | { 72 | return serialize([ 73 | 'statusCode' => $this->statusCode, 74 | 'contentTypes' => $this->contentTypes, 75 | 'parameters' => $this->parameters 76 | ]); 77 | } 78 | 79 | public function unserialize($serialized) 80 | { 81 | $data = unserialize($serialized); 82 | $this->statusCode = $data['statusCode']; 83 | $this->contentTypes = $data['contentTypes']; 84 | $this->parameters = $data['parameters']; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Factory/CachedSchemaFactoryDecorator.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 20 | $this->schemaFactory = $schemaFactory; 21 | } 22 | 23 | public function createSchema($schemaFile) 24 | { 25 | $cacheKey = hash('sha1', $schemaFile); 26 | $item = $this->cache->getItem($cacheKey); 27 | if ($item->isHit()) { 28 | $schema = $item->get(); 29 | } else { 30 | $schema = $this->schemaFactory->createSchema($schemaFile); 31 | $this->cache->save($item->set($schema)); 32 | } 33 | 34 | return $schema; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Factory/SchemaFactory.php: -------------------------------------------------------------------------------- 1 | resolveSchemaFile($schemaFile); 29 | 30 | $host = isset($schema->host) ? $schema->host : null; 31 | $basePath = isset($schema->basePath) ? $schema->basePath : ''; 32 | $schemes = isset($schema->schemes) ? $schema->schemes : ['http']; 33 | 34 | return new Schema( 35 | $this->createRequestDefinitions($schema), 36 | $basePath, 37 | $host, 38 | $schemes 39 | ); 40 | } 41 | 42 | /** 43 | * 44 | * @param string $schemaFile 45 | * 46 | * @return object 47 | */ 48 | protected function resolveSchemaFile($schemaFile) 49 | { 50 | $extension = pathinfo($schemaFile, PATHINFO_EXTENSION); 51 | switch ($extension) { 52 | case 'yml': 53 | case 'yaml': 54 | if (!class_exists(Yaml::class)) { 55 | // @codeCoverageIgnoreStart 56 | throw new \InvalidArgumentException( 57 | 'You need to require the "symfony/yaml" component in order to parse yml files' 58 | ); 59 | // @codeCoverageIgnoreEnd 60 | } 61 | 62 | $uriRetriever = new YamlUriRetriever(); 63 | break; 64 | case 'json': 65 | $uriRetriever = new UriRetriever(); 66 | break; 67 | default: 68 | throw new \InvalidArgumentException( 69 | sprintf( 70 | 'file "%s" does not provide a supported extension choose either json, yml or yaml', 71 | $schemaFile 72 | ) 73 | ); 74 | } 75 | 76 | $refResolver = new RefResolver( 77 | $uriRetriever, 78 | new UriResolver() 79 | ); 80 | 81 | return $refResolver->resolve($schemaFile); 82 | } 83 | 84 | /** 85 | * @param \stdClass $schema 86 | * @return RequestDefinitions 87 | */ 88 | protected function createRequestDefinitions(\stdClass $schema) 89 | { 90 | $definitions = []; 91 | $defaultConsumedContentTypes = []; 92 | $defaultProducedContentTypes = []; 93 | 94 | if (isset($schema->consumes)) { 95 | $defaultConsumedContentTypes = $schema->consumes; 96 | } 97 | if (isset($schema->produces)) { 98 | $defaultProducedContentTypes = $schema->produces; 99 | } 100 | 101 | $basePath = isset($schema->basePath) ? $schema->basePath : ''; 102 | 103 | foreach ($schema->paths as $pathTemplate => $methods) { 104 | foreach ($methods as $method => $definition) { 105 | $method = strtoupper($method); 106 | $contentTypes = $defaultConsumedContentTypes; 107 | if (isset($definition->consumes)) { 108 | $contentTypes = $definition->consumes; 109 | } 110 | 111 | if (!isset($definition->operationId)) { 112 | throw new \LogicException( 113 | sprintf( 114 | 'You need to provide an operationId for %s %s', 115 | $method, 116 | $pathTemplate 117 | ) 118 | ); 119 | } 120 | 121 | if (empty($contentTypes) && $this->containsBodyParametersLocations($definition)) { 122 | $contentTypes = $this->guessSupportedContentTypes($definition, $pathTemplate); 123 | } 124 | 125 | if (!isset($definition->responses)) { 126 | throw new \LogicException( 127 | sprintf( 128 | 'You need to specify at least one response for %s %s', 129 | $method, 130 | $pathTemplate 131 | ) 132 | ); 133 | } 134 | 135 | if (!isset($definition->parameters)) { 136 | $definition->parameters = []; 137 | } 138 | 139 | $requestParameters = []; 140 | foreach ($definition->parameters as $parameter) { 141 | $requestParameters[] = $this->createParameter($parameter); 142 | } 143 | 144 | $responseContentTypes = $defaultProducedContentTypes; 145 | if (isset($definition->produces)) { 146 | $responseContentTypes = $definition->produces; 147 | } 148 | 149 | $responseDefinitions = []; 150 | foreach ($definition->responses as $statusCode => $response) { 151 | $responseDefinitions[] = $this->createResponseDefinition( 152 | $statusCode, 153 | $responseContentTypes, 154 | $response 155 | ); 156 | } 157 | 158 | $definitions[] = new RequestDefinition( 159 | $method, 160 | $definition->operationId, 161 | $basePath.$pathTemplate, 162 | new Parameters($requestParameters), 163 | $contentTypes, 164 | $responseDefinitions 165 | ); 166 | } 167 | } 168 | 169 | return new RequestDefinitions($definitions); 170 | } 171 | 172 | /** 173 | * @return bool 174 | */ 175 | private function containsBodyParametersLocations(\stdClass $definition) 176 | { 177 | if (!isset($definition->parameters)) { 178 | return false; 179 | } 180 | 181 | foreach ($definition->parameters as $parameter) { 182 | if (isset($parameter->in) && \in_array($parameter->in, Parameter::BODY_LOCATIONS, true)) { 183 | return true; 184 | } 185 | } 186 | 187 | return false; 188 | } 189 | 190 | /** 191 | * @param \stdClass $definition 192 | * @param string $pathTemplate 193 | * 194 | * @return array 195 | */ 196 | private function guessSupportedContentTypes(\stdClass $definition, $pathTemplate) 197 | { 198 | if (!isset($definition->parameters)) { 199 | return []; 200 | } 201 | 202 | $bodyLocations = []; 203 | foreach ($definition->parameters as $parameter) { 204 | if (isset($parameter->in) && \in_array($parameter->in, Parameter::BODY_LOCATIONS, true)) { 205 | $bodyLocations[] = $parameter->in; 206 | } 207 | } 208 | 209 | if (count($bodyLocations) > 1) { 210 | throw new \LogicException( 211 | sprintf( 212 | 'Parameters cannot have %s locations at the same time in %s', 213 | implode(' and ', $bodyLocations), 214 | $pathTemplate 215 | ) 216 | ); 217 | } 218 | 219 | if (count($bodyLocations) === 1) { 220 | return [Parameter::BODY_LOCATIONS_TYPES[current($bodyLocations)]]; 221 | } 222 | 223 | return []; 224 | } 225 | 226 | protected function createResponseDefinition($statusCode, array $defaultProducedContentTypes, \stdClass $response) 227 | { 228 | $allowedContentTypes = $defaultProducedContentTypes; 229 | $parameters = []; 230 | if (isset($response->schema)) { 231 | $parameters[] = $this->createParameter((object) [ 232 | 'in' => 'body', 233 | 'name' => 'body', 234 | 'required' => true, 235 | 'schema' => $response->schema 236 | ]); 237 | } 238 | 239 | if (isset($response->headers)) { 240 | foreach ($response->headers as $headerName => $schema) { 241 | $schema->in = 'header'; 242 | $schema->name = $headerName; 243 | $schema->required = true; 244 | $parameters[] = $this->createParameter($schema); 245 | } 246 | } 247 | 248 | return new ResponseDefinition($statusCode, $allowedContentTypes, new Parameters($parameters)); 249 | } 250 | 251 | /** 252 | * Create a Parameter from a swagger parameter 253 | * 254 | * @param \stdClass $parameter 255 | * 256 | * @return Parameter 257 | */ 258 | protected function createParameter(\stdClass $parameter) 259 | { 260 | $parameter = get_object_vars($parameter); 261 | $location = $parameter['in']; 262 | $name = $parameter['name']; 263 | $schema = isset($parameter['schema']) ? $parameter['schema'] : new \stdClass(); 264 | $required = isset($parameter['required']) ? $parameter['required'] : false; 265 | 266 | unset($parameter['in']); 267 | unset($parameter['name']); 268 | unset($parameter['required']); 269 | unset($parameter['schema']); 270 | 271 | // Every remaining parameter may be json schema properties 272 | foreach ($parameter as $key => $value) { 273 | $schema->{$key} = $value; 274 | } 275 | 276 | // It's not relevant to validate file type 277 | if (isset($schema->format) && $schema->format === 'file') { 278 | $schema = null; 279 | } 280 | 281 | return new Parameter($location, $name, $required, $schema); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/JsonSchema/Uri/YamlUriRetriever.php: -------------------------------------------------------------------------------- 1 | schemaCache[$fetchUri])) { 21 | return $this->schemaCache[$fetchUri]; 22 | } 23 | 24 | $contents = $this->getUriRetriever()->retrieve($fetchUri); 25 | 26 | $contents = Yaml::parse($contents); 27 | $jsonSchema = json_decode(json_encode($contents)); 28 | 29 | $this->schemaCache[$fetchUri] = $jsonSchema; 30 | 31 | return $jsonSchema; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Normalizer/QueryParamsNormalizer.php: -------------------------------------------------------------------------------- 1 | properties as $name => $queryParamSchema) { 17 | $type = isset($queryParamSchema->type) 18 | ? $queryParamSchema->type 19 | : 'string'; 20 | 21 | if (array_key_exists($name, $queryParams)) { 22 | switch ($type) { 23 | case 'boolean': 24 | if ($queryParams[$name] === 'false') { 25 | $queryParams[$name] = false; 26 | } 27 | if ($queryParams[$name] === 'true') { 28 | $queryParams[$name] = true; 29 | } 30 | if (in_array($queryParams[$name], ['0', '1'])) { 31 | $queryParams[$name] = (bool) $queryParams[$name]; 32 | } 33 | break; 34 | case 'integer': 35 | if (is_numeric($queryParams[$name])) { 36 | $queryParams[$name] = (int) $queryParams[$name]; 37 | } 38 | break; 39 | case 'number': 40 | if (is_numeric($queryParams[$name])) { 41 | $queryParams[$name] = (float) $queryParams[$name]; 42 | } 43 | break; 44 | } 45 | 46 | if (isset($queryParamSchema->collectionFormat)) { 47 | switch ($queryParamSchema->collectionFormat) { 48 | case 'csv': 49 | $separator = ','; 50 | break; 51 | case 'ssv': 52 | $separator = ' '; 53 | break; 54 | case 'pipes': 55 | $separator = '|'; 56 | break; 57 | case 'tsv': 58 | $separator = "\t"; 59 | break; 60 | default: 61 | throw new \InvalidArgumentException( 62 | sprintf( 63 | '%s is not a supported query collection format', 64 | $queryParamSchema->collectionFormat 65 | ) 66 | ); 67 | } 68 | 69 | $queryParams[$name] = explode($separator, $queryParams[$name]); 70 | } 71 | } 72 | } 73 | 74 | return $queryParams; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Schema.php: -------------------------------------------------------------------------------- 1 | addRequestDefinition($request); 32 | } 33 | $this->host = $host; 34 | $this->basePath = $basePath; 35 | $this->schemes = $schemes; 36 | } 37 | 38 | /** 39 | * Find the operationId associated to a given path and method 40 | * 41 | * @todo Implement a less expensive finder 42 | * @param string $method An HTTP method 43 | * @param string $path A path (ex: /foo/1) 44 | * 45 | * @return string The operationId 46 | */ 47 | public function findOperationId($method, $path) 48 | { 49 | $uriTemplateManager = new UriTemplate(); 50 | foreach ($this->requestDefinitions as $requestDefinition) { 51 | if ($requestDefinition->getMethod() !== $method) { 52 | continue; 53 | } 54 | $params = $uriTemplateManager->extract($requestDefinition->getPathTemplate(), $path, true); 55 | if ($params !== null) { 56 | return $requestDefinition->getOperationId(); 57 | } 58 | } 59 | 60 | throw new \InvalidArgumentException('Unable to resolve the operationId for path ' . $path); 61 | } 62 | 63 | public function getRequestDefinition($operationId) 64 | { 65 | if (!isset($this->requestDefinitions[$operationId])) { 66 | throw new \InvalidArgumentException('Unable to get the request definition for '.$operationId); 67 | } 68 | 69 | return $this->requestDefinitions[$operationId]; 70 | } 71 | 72 | /** 73 | * @return string 74 | */ 75 | public function getHost() 76 | { 77 | return $this->host; 78 | } 79 | 80 | /** 81 | * @return string 82 | */ 83 | public function getBasePath() 84 | { 85 | return $this->basePath; 86 | } 87 | 88 | /** 89 | * @return array 90 | */ 91 | public function getSchemes() 92 | { 93 | return $this->schemes; 94 | } 95 | 96 | public function serialize() 97 | { 98 | return serialize([ 99 | 'host' => $this->host, 100 | 'basePath' => $this->basePath, 101 | 'schemes' => $this->schemes, 102 | 'requests' => $this->requestDefinitions 103 | ]); 104 | } 105 | 106 | public function unserialize($serialized) 107 | { 108 | $data = unserialize($serialized); 109 | $this->host = $data['host']; 110 | $this->basePath = $data['basePath']; 111 | $this->schemes = $data['schemes']; 112 | $this->requestDefinitions = $data['requests']; 113 | } 114 | 115 | private function addRequestDefinition(RequestDefinition $request) 116 | { 117 | $this->requestDefinitions[$request->getOperationId()] = $request; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Validator/ConstraintViolation.php: -------------------------------------------------------------------------------- 1 | property = $property; 30 | $this->message = $message; 31 | $this->constraint = $constraint; 32 | $this->location = $location; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getProperty() 39 | { 40 | return $this->property; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getMessage() 47 | { 48 | return $this->message; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getConstraint() 55 | { 56 | return $this->constraint; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getLocation() 63 | { 64 | return $this->location; 65 | } 66 | 67 | public function toArray() 68 | { 69 | return [ 70 | 'property' => $this->getProperty(), 71 | 'message' => $this->getMessage(), 72 | 'constraint' => $this->getConstraint(), 73 | 'location' => $this->getLocation() 74 | ]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Validator/Exception/ConstraintViolations.php: -------------------------------------------------------------------------------- 1 | violations = $violations; 16 | parent::__construct((string) $this); 17 | } 18 | 19 | public function getViolations() 20 | { 21 | return $this->violations; 22 | } 23 | 24 | /** 25 | * @return string 26 | */ 27 | public function __toString() 28 | { 29 | $message = "Request constraint violations:\n"; 30 | foreach ($this->violations as $violation) { 31 | $message .= sprintf( 32 | "[property]: %s\n[message]: %s\n[constraint]: %s\n[location]: %s\n\n", 33 | $violation->getProperty(), 34 | $violation->getMessage(), 35 | $violation->getConstraint(), 36 | $violation->getLocation() 37 | ); 38 | } 39 | 40 | return $message; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Validator/MessageValidator.php: -------------------------------------------------------------------------------- 1 | validator = $validator; 30 | $this->decoder = $decoder; 31 | } 32 | 33 | public function validateRequest(RequestInterface $request, RequestDefinition $definition) 34 | { 35 | if ($definition->hasBodySchema()) { 36 | $contentTypeValid = $this->validateContentType($request, $definition); 37 | if ($contentTypeValid && in_array($request->getMethod(), ['PUT', 'PATCH', 'POST'])) { 38 | $this->validateMessageBody($request, $definition); 39 | } 40 | } 41 | 42 | $this->validateHeaders($request, $definition); 43 | $this->validateQueryParameters($request, $definition); 44 | } 45 | 46 | public function validateResponse(ResponseInterface $response, RequestDefinition $definition) 47 | { 48 | $responseDefinition = $definition->getResponseDefinition($response->getStatusCode()); 49 | if ($responseDefinition->hasBodySchema()) { 50 | $contentTypeValid = $this->validateContentType($response, $responseDefinition); 51 | if ($contentTypeValid) { 52 | $this->validateMessageBody($response, $responseDefinition); 53 | } 54 | } 55 | 56 | $this->validateHeaders($response, $responseDefinition); 57 | } 58 | 59 | public function validateMessageBody(MessageInterface $message, MessageDefinition $definition) 60 | { 61 | if ($message instanceof ServerRequestInterface) { 62 | $bodyString = json_encode((array) $message->getParsedBody()); 63 | } else { 64 | $bodyString = (string) $message->getBody(); 65 | } 66 | if ($bodyString !== '' && $definition->hasBodySchema()) { 67 | $contentType = $message->getHeaderLine('Content-Type'); 68 | $decodedBody = $this->decoder->decode( 69 | $bodyString, 70 | DecoderUtils::extractFormatFromContentType($contentType) 71 | ); 72 | 73 | $this->validate($decodedBody, $definition->getBodySchema(), 'body'); 74 | } 75 | } 76 | 77 | public function validateHeaders(MessageInterface $message, MessageDefinition $definition) 78 | { 79 | if ($definition->hasHeadersSchema()) { 80 | // Transform each header values into a string 81 | $headers = array_map( 82 | function (array $values) { 83 | return implode(', ', $values); 84 | }, 85 | $message->getHeaders() 86 | ); 87 | 88 | $this->validate( 89 | (object) array_change_key_case($headers, CASE_LOWER), 90 | $definition->getHeadersSchema(), 91 | 'header' 92 | ); 93 | } 94 | } 95 | 96 | /** 97 | * Validate an HTTP message content-type against a message definition 98 | * 99 | * @param MessageInterface $message 100 | * @param MessageDefinition $definition 101 | * 102 | * @return bool When the content-type is valid 103 | */ 104 | public function validateContentType(MessageInterface $message, MessageDefinition $definition) 105 | { 106 | $contentType = $message->getHeaderLine('Content-Type'); 107 | $contentTypes = $definition->getContentTypes(); 108 | 109 | if (!in_array($contentType, $contentTypes, true)) { 110 | if ($contentType === '') { 111 | $violationMessage = 'Content-Type should not be empty'; 112 | $constraint = 'required'; 113 | } else { 114 | $violationMessage = sprintf( 115 | '%s is not a supported content type, supported: %s', 116 | $message->getHeaderLine('Content-Type'), 117 | implode(', ', $contentTypes) 118 | ); 119 | $constraint = 'enum'; 120 | } 121 | 122 | $this->addViolation( 123 | new ConstraintViolation( 124 | 'Content-Type', 125 | $violationMessage, 126 | $constraint, 127 | 'header' 128 | ) 129 | ); 130 | 131 | return false; 132 | } 133 | 134 | return true; 135 | } 136 | 137 | public function validateQueryParameters(RequestInterface $request, RequestDefinition $definition) 138 | { 139 | if ($definition->hasQueryParametersSchema()) { 140 | parse_str($request->getUri()->getQuery(), $queryParams); 141 | $schema = $definition->getQueryParametersSchema(); 142 | $queryParams = QueryParamsNormalizer::normalize($queryParams, $schema); 143 | 144 | $this->validate( 145 | (object) $queryParams, 146 | $schema, 147 | 'query' 148 | ); 149 | } 150 | } 151 | 152 | /** 153 | * @return bool 154 | */ 155 | public function hasViolations() 156 | { 157 | return !empty($this->violations); 158 | } 159 | 160 | /** 161 | * @return ConstraintViolation[] 162 | */ 163 | public function getViolations() 164 | { 165 | return $this->violations; 166 | } 167 | 168 | /** 169 | * @param mixed $data 170 | * @param \stdClass $schema 171 | * @param string $location (possible values: query, path, body, headers) 172 | */ 173 | protected function validate($data, $schema, $location) 174 | { 175 | $this->validator->check($data, $schema); 176 | if (! $this->validator->isValid()) { 177 | $violations = array_map( 178 | function ($error) use ($location) { 179 | return new ConstraintViolation( 180 | $error['property'], 181 | $error['message'], 182 | $error['constraint'], 183 | $location 184 | ); 185 | }, 186 | $this->validator->getErrors() 187 | ); 188 | 189 | foreach ($violations as $violation) { 190 | $this->addViolation($violation); 191 | } 192 | } 193 | 194 | $this->validator->reset(); 195 | } 196 | 197 | protected function addViolation(ConstraintViolation $violation) 198 | { 199 | $this->violations[] = $violation; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /tests/Decoder/Adapter/SymfonyDecoderAdapterTest.php: -------------------------------------------------------------------------------- 1 | 0, 'foo' => 'foo1'], 15 | (object) ['@key' => 1, 'foo' => 'foo2'], 16 | ]; 17 | 18 | $data = 'foo1foo2'; 19 | $decoder = new SymfonyDecoderAdapter(new XmlEncoder()); 20 | $actual = $decoder->decode($data, 'xml'); 21 | 22 | assertThat($actual, equalTo($expected)); 23 | } 24 | 25 | /** @test */ 26 | public function itShouldDecodeAJsonStringIntoAnArrayOfObject() 27 | { 28 | $expected = [ 29 | (object) ['foo' => 'foo1'], 30 | (object) ['foo' => 'foo2'], 31 | ]; 32 | 33 | $data = '[{"foo": "foo1"}, {"foo": "foo2"}]'; 34 | 35 | $decoder = new SymfonyDecoderAdapter(new JsonDecode(true)); 36 | $actual = $decoder->decode($data, 'json'); 37 | 38 | assertThat($actual, equalTo($expected)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Decoder/DecoderUtilsTest.php: -------------------------------------------------------------------------------- 1 | prophesize(ResponseDefinition::class); 28 | $responseDefinition->getStatusCode()->willReturn(200); 29 | 30 | $requestDefinition = new RequestDefinition( 31 | 'GET', 32 | 'getFoo', 33 | '/foo/{id}', 34 | new Parameters([]), 35 | ['application/json'], 36 | [$responseDefinition->reveal()] 37 | ); 38 | 39 | assertThat($requestDefinition->getResponseDefinition(200), self::isInstanceOf(ResponseDefinition::class)); 40 | } 41 | 42 | /** @test */ 43 | public function itProvideAResponseDefinitionUsingDefaultValue() 44 | { 45 | $statusCodes = [200, 'default']; 46 | $responseDefinitions = []; 47 | foreach ($statusCodes as $statusCode) { 48 | $responseDefinition = $this->prophesize(ResponseDefinition::class); 49 | $responseDefinition->getStatusCode()->willReturn($statusCode); 50 | $responseDefinitions[] = $responseDefinition->reveal(); 51 | } 52 | 53 | $requestDefinition = new RequestDefinition( 54 | 'GET', 55 | 'getFoo', 56 | '/foo/{id}', 57 | new Parameters([]), 58 | ['application/json'], 59 | $responseDefinitions 60 | ); 61 | 62 | assertThat($requestDefinition->getResponseDefinition(500), self::isInstanceOf(ResponseDefinition::class)); 63 | } 64 | 65 | /** @test */ 66 | public function itThrowAnExceptionWhenNoResponseDefinitionIsFound() 67 | { 68 | $this->expectException(\InvalidArgumentException::class); 69 | $this->expectExceptionMessage('No response definition for GET /foo/{id} is available for status code 200'); 70 | 71 | $requestDefinition = new RequestDefinition( 72 | 'GET', 73 | 'getFoo', 74 | '/foo/{id}', 75 | new Parameters([]), 76 | ['application/json'], 77 | [] 78 | ); 79 | 80 | $requestDefinition->getResponseDefinition(200); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/Definition/RequestDefinitionsTest.php: -------------------------------------------------------------------------------- 1 | prophesize(RequestDefinition::class); 21 | $requestDefinition->getOperationId()->willReturn('getFoo'); 22 | 23 | $requestDefinitions = new RequestDefinitions([$requestDefinition->reveal()]); 24 | 25 | assertThat($requestDefinitions->getRequestDefinition('getFoo'), self::isInstanceOf(RequestDefinition::class)); 26 | } 27 | 28 | /** @test */ 29 | public function itThrowAnExceptionWhenNoRequestDefinitionIsFound() 30 | { 31 | $this->expectException(\InvalidArgumentException::class); 32 | $this->expectExceptionMessage('Unable to find request definition for operationId getFoo'); 33 | 34 | $requestDefinitions = new RequestDefinitions([]); 35 | $requestDefinitions->getRequestDefinition('getFoo'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Definition/RequestParameterTest.php: -------------------------------------------------------------------------------- 1 | hasSchema()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Definition/RequestParametersTest.php: -------------------------------------------------------------------------------- 1 | prophesize(Parameter::class); 12 | $requestParameter->getLocation()->willReturn('query'); 13 | $requestParameter->getName()->willReturn('foo'); 14 | 15 | $requestParameters = new Parameters([$requestParameter->reveal()]); 16 | 17 | assertThat($requestParameters, isInstanceOf(\Traversable::class)); 18 | assertThat($requestParameters, containsOnlyInstancesOf(Parameter::class)); 19 | } 20 | 21 | /** @test */ 22 | public function itCanBeSerialized() 23 | { 24 | $requestParameters = new Parameters([]); 25 | $serialized = serialize($requestParameters); 26 | 27 | assertThat(unserialize($serialized), self::equalTo($requestParameters)); 28 | } 29 | 30 | /** @test */ 31 | public function itThrowAnExceptionOnUnsupportedParameterLocation() 32 | { 33 | $this->expectException(\InvalidArgumentException::class); 34 | $this->expectExceptionMessage( 35 | 'nowhere is not a supported parameter location, ' . 36 | 'supported: path, header, query, body, formData' 37 | ); 38 | 39 | $param = $this->prophesize(Parameter::class); 40 | $param->getName()->willreturn('foo'); 41 | $param->getLocation()->willreturn('nowhere'); 42 | 43 | new Parameters([$param->reveal()]); 44 | } 45 | 46 | /** @test */ 47 | public function itCanResolveARequestParameterByName() 48 | { 49 | $requestParameter = $this->prophesize(Parameter::class); 50 | $requestParameter->getLocation()->willReturn('query'); 51 | $requestParameter->getName()->willReturn('foo'); 52 | 53 | $requestParameters = new Parameters([$requestParameter->reveal()]); 54 | 55 | assertThat($requestParameters->getByName('foo'), equalTo($requestParameter->reveal())); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Definition/ResponseDefinitionTest.php: -------------------------------------------------------------------------------- 1 | prophesize(Schema::class); 17 | 18 | $item = $this->prophesize(CacheItemInterface::class); 19 | $item->isHit()->shouldBeCalled()->willReturn(false); 20 | $item->set($schema)->shouldBeCalled()->willReturn($item); 21 | 22 | $cache = $this->prophesize(CacheItemPoolInterface::class); 23 | $cache->getItem('3f470a326a5926a2e323aaadd767c0e64302a080')->willReturn($item); 24 | $cache->save($item)->willReturn(true); 25 | 26 | $schemaFactory = $this->prophesize(SchemaFactory::class); 27 | $schemaFactory->createSchema($schemaFile)->willReturn($schema); 28 | 29 | $cachedSchema = new CachedSchemaFactoryDecorator( 30 | $cache->reveal(), 31 | $schemaFactory->reveal() 32 | ); 33 | 34 | $expectedSchema = $schema->reveal(); 35 | $actualSchema = $cachedSchema->createSchema($schemaFile); 36 | 37 | assertThat($actualSchema, isInstanceOf(Schema::class)); 38 | assertThat($actualSchema, equalTo($expectedSchema)); 39 | } 40 | 41 | /** @test */ 42 | public function itShouldLoadASchemaFromACacheStore() 43 | { 44 | $schemaFile = 'file://fake-schema.yml'; 45 | $schema = $this->prophesize(Schema::class); 46 | 47 | $item = $this->prophesize(CacheItemInterface::class); 48 | $item->isHit()->shouldBeCalled()->willReturn(true); 49 | $item->get()->shouldBeCalled()->willReturn($schema); 50 | 51 | $cache = $this->prophesize(CacheItemPoolInterface::class); 52 | $cache->getItem('3f470a326a5926a2e323aaadd767c0e64302a080')->willReturn($item); 53 | 54 | $schemaFactory = $this->prophesize(SchemaFactory::class); 55 | $schemaFactory->createSchema(Argument::any())->shouldNotBeCalled(); 56 | 57 | $cachedSchema = new CachedSchemaFactoryDecorator( 58 | $cache->reveal(), 59 | $schemaFactory->reveal() 60 | ); 61 | 62 | $expectedSchema = $schema->reveal(); 63 | $actualSchema = $cachedSchema->createSchema($schemaFile); 64 | 65 | assertThat($actualSchema, isInstanceOf(Schema::class)); 66 | assertThat($actualSchema, equalTo($expectedSchema)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Factory/SwaggerSchemaFactoryTest.php: -------------------------------------------------------------------------------- 1 | getPetStoreSchemaJson(); 17 | 18 | assertThat($schema, isInstanceOf(Schema::class)); 19 | } 20 | 21 | /** @test */ 22 | public function itCanCreateASchemaFromAYamlFile() 23 | { 24 | $schema = $this->getPetStoreSchemaYaml(); 25 | 26 | assertThat($schema, isInstanceOf(Schema::class)); 27 | } 28 | 29 | /** @test */ 30 | public function itThrowAnExceptionWhenTheSchemaFileIsNotSupported() 31 | { 32 | $unsupportedFile = 'file://'.dirname(__DIR__).'/fixtures/petstore.txt'; 33 | 34 | $this->expectException(\InvalidArgumentException::class); 35 | $this->expectExceptionMessageRegExp('/does not provide a supported extension/'); 36 | 37 | (new SwaggerSchemaFactory())->createSchema($unsupportedFile); 38 | } 39 | 40 | /** @test */ 41 | public function itShouldHaveSchemaProperties() 42 | { 43 | $schema = $this->getPetStoreSchemaJson(); 44 | 45 | assertThat($schema->getHost(), equalTo('petstore.swagger.io')); 46 | assertThat($schema->getBasePath(), equalTo('/v2')); 47 | assertThat($schema->getSchemes(), equalTo(['https', 'http'])); 48 | } 49 | 50 | /** @test */ 51 | public function itThrowAnExceptionWhenAnOperationDoesNotProvideAnId() 52 | { 53 | $this->expectException(\LogicException::class); 54 | $this->expectExceptionMessage('You need to provide an operationId for GET /something'); 55 | 56 | $this->getSchemaFromFile('operation-without-an-id.json'); 57 | } 58 | 59 | /** @test */ 60 | public function itThrowAnExceptionWhenAnOperationDoesNotProvideResponses() 61 | { 62 | $this->expectException(\LogicException::class); 63 | $this->expectExceptionMessage('You need to specify at least one response for GET /something'); 64 | 65 | $this->getSchemaFromFile('operation-without-responses.json'); 66 | } 67 | 68 | /** @test */ 69 | public function itSupportAnOperationWithoutParameters() 70 | { 71 | $schema = $this->getSchemaFromFile('operation-without-parameters.json'); 72 | $definition = $schema->getRequestDefinition('getSomething'); 73 | 74 | assertThat($definition->hasHeadersSchema(), isFalse()); 75 | assertThat($definition->hasBodySchema(), isFalse()); 76 | assertThat($definition->hasQueryParametersSchema(), isFalse()); 77 | } 78 | 79 | /** @test */ 80 | public function itCanCreateARequestDefinition() 81 | { 82 | $schema = $this->getPetStoreSchemaJson(); 83 | 84 | $requestDefinition = $schema->getRequestDefinition('findPetsByStatus'); 85 | 86 | assertThat($requestDefinition, isInstanceOf(RequestDefinition::class)); 87 | assertThat($requestDefinition->getMethod(), equalTo('GET')); 88 | assertThat($requestDefinition->getOperationId(), equalTo('findPetsByStatus')); 89 | assertThat($requestDefinition->getPathTemplate(), equalTo('/v2/pet/findByStatus')); 90 | assertThat($requestDefinition->getContentTypes(), equalTo([])); 91 | assertThat($requestDefinition->getRequestParameters(), isInstanceOf(Parameters::class)); 92 | assertThat($requestDefinition->getResponseDefinition(200), isInstanceOf(ResponseDefinition::class)); 93 | assertThat($requestDefinition->getResponseDefinition(400), isInstanceOf(ResponseDefinition::class)); 94 | } 95 | 96 | /** @test */ 97 | public function itCanCreateARequestBodyParameter() 98 | { 99 | $schema = $this->getPetStoreSchemaJson(); 100 | 101 | $requestParameters = $schema->getRequestDefinition('addPet')->getRequestParameters(); 102 | 103 | assertThat($requestParameters, isInstanceOf(Parameters::class)); 104 | assertThat($requestParameters->getBody(), isInstanceOf(Parameter::class)); 105 | assertThat($requestParameters->hasBodySchema(), isTrue()); 106 | assertThat($requestParameters->getBodySchema(), isType('object')); 107 | } 108 | 109 | /** @test */ 110 | public function itCanCreateRequestPathParameters() 111 | { 112 | $schema = $this->getPetStoreSchemaJson(); 113 | 114 | $requestParameters = $schema->getRequestDefinition('getPetById')->getRequestParameters(); 115 | 116 | assertThat($requestParameters->getPath(), containsOnlyInstancesOf(Parameter::class)); 117 | } 118 | 119 | /** @test */ 120 | public function itCanCreateRequestQueryParameters() 121 | { 122 | $schema = $this->getPetStoreSchemaJson(); 123 | 124 | $requestParameters = $schema->getRequestDefinition('findPetsByStatus')->getRequestParameters(); 125 | 126 | assertThat($requestParameters->getQuery(), containsOnlyInstancesOf(Parameter::class)); 127 | assertThat($requestParameters->getQueryParametersSchema(), isType('object')); 128 | } 129 | 130 | /** @test */ 131 | public function itCanCreateRequestHeadersParameter() 132 | { 133 | $schema = $this->getPetStoreSchemaJson(); 134 | 135 | $requestParameters = $schema->getRequestDefinition('deletePet')->getRequestParameters(); 136 | 137 | assertThat($requestParameters->getHeaders(), containsOnlyInstancesOf(Parameter::class)); 138 | assertThat($requestParameters->hasHeadersSchema(), isTrue()); 139 | assertThat($requestParameters->getHeadersSchema(), isType('object')); 140 | } 141 | 142 | /** @test */ 143 | public function itCanCreateAResponseDefinition() 144 | { 145 | $schema = $this->getPetStoreSchemaJson(); 146 | 147 | $responseDefinition = $schema->getRequestDefinition('getPetById')->getResponseDefinition(200); 148 | 149 | assertThat($responseDefinition, isInstanceOf(ResponseDefinition::class)); 150 | assertThat($responseDefinition->getBodySchema(), isType('object')); 151 | assertThat($responseDefinition->getStatusCode(), equalTo(200)); 152 | assertThat($responseDefinition->getContentTypes(), contains('application/json')); 153 | } 154 | 155 | public function itUseTheSchemaDefaultConsumesPropertyWhenNotProvidedByAnOperation() 156 | { 157 | $schema = $this->getSchemaFromFile('schema-with-default-consumes-and-produces-properties.json'); 158 | $definition = $schema->getRequestDefinition('postSomething'); 159 | 160 | assertThat($definition->getContentTypes(), contains('application/json')); 161 | } 162 | 163 | /** @test */ 164 | public function itUseTheSchemaDefaultProducesPropertyWhenNotProvidedByAnOperationResponse() 165 | { 166 | $schema = $this->getSchemaFromFile('schema-with-default-consumes-and-produces-properties.json'); 167 | $responseDefinition = $schema 168 | ->getRequestDefinition('postSomething') 169 | ->getResponseDefinition(201); 170 | 171 | assertThat($responseDefinition->getContentTypes(), contains('application/json')); 172 | } 173 | 174 | /** 175 | * @test 176 | * @dataProvider getGuessableContentTypes 177 | */ 178 | public function itGuessTheContentTypeFromRequestParameters($operationId, $expectedContentType) 179 | { 180 | $schema = $this->getSchemaFromFile('request-without-content-types.json'); 181 | 182 | $definition = $schema->getRequestDefinition($operationId); 183 | 184 | assertThat($definition->getContentTypes(), contains($expectedContentType)); 185 | } 186 | 187 | public function getGuessableContentTypes() 188 | { 189 | return [ 190 | 'body' => [ 191 | 'operationId' => 'postBodyWithoutAContentType', 192 | 'contentType' => 'application/json' 193 | ], 194 | 'formData' => [ 195 | 'operationId' => 'postFromDataWithoutAContentType', 196 | 'contentType' => 'application/x-www-form-urlencoded' 197 | ], 198 | ]; 199 | } 200 | 201 | /** @test */ 202 | public function itFailWhenTryingToGuessTheContentTypeFromARequestWithMultipleBodyLocations() 203 | { 204 | $this->expectException(\LogicException::class); 205 | $this->expectExceptionMessage( 206 | 'Parameters cannot have body and formData locations ' . 207 | 'at the same time in /post/with-conflicting-locations' 208 | ); 209 | 210 | $schemaFile = 'file://'.dirname(__DIR__).'/fixtures/request-with-conflicting-locations.json'; 211 | (new SwaggerSchemaFactory())->createSchema($schemaFile); 212 | } 213 | 214 | /** 215 | * @return Schema 216 | */ 217 | private function getPetStoreSchemaJson() 218 | { 219 | return $this->getSchemaFromFile('petstore.json'); 220 | } 221 | 222 | /** 223 | * @return Schema 224 | */ 225 | private function getPetStoreSchemaYaml() 226 | { 227 | return $this->getSchemaFromFile('petstore.yaml'); 228 | } 229 | 230 | /** 231 | * @param $name 232 | * 233 | * @return Schema 234 | */ 235 | private function getSchemaFromFile($name) 236 | { 237 | $schemaFile = 'file://' . dirname(__DIR__) . '/fixtures/'.$name; 238 | $factory = new SwaggerSchemaFactory(); 239 | 240 | return $factory->createSchema($schemaFile); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /tests/JsonSchema/Uri/YamlUriRetrieverTest.php: -------------------------------------------------------------------------------- 1 | retrieve('file://'.__DIR__.'/../../fixtures/petstore.yaml'); 12 | 13 | assertThat($object, isType('object')); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Normalizer/QueryParamsNormalizerTest.php: -------------------------------------------------------------------------------- 1 | toObject([ 15 | 'type' => 'object', 16 | 'properties' => [ 17 | 'param' => [ 18 | 'type' => $schemaType 19 | ] 20 | ] 21 | ]); 22 | 23 | $normalizedValue = QueryParamsNormalizer::normalize(['param' => $actualValue], $jsonSchema); 24 | 25 | assertThat($normalizedValue['param'], equalTo($expectedValue)); 26 | } 27 | 28 | 29 | public function getValidQueryParameters() 30 | { 31 | return [ 32 | // description => [schemaType, actual, expected] 33 | 'with an integer' => ['integer', '123', 123 ], 34 | 'with a number' => ['number', '12.15', 12.15 ], 35 | 'with true given as a string' => ['boolean', 'true', true ], 36 | 'with true given as a numeric' => ['boolean', '1', true ], 37 | 'with false given as a string' => ['boolean', 'false', false ], 38 | 'with false given as a numeric string' => ['boolean', '0', false ] 39 | ]; 40 | } 41 | 42 | /** 43 | * @test 44 | * @dataProvider getValidCollectionFormat 45 | */ 46 | public function itTransformCollectionFormatIntoArray($collectionFormat, $rawValue, array $expectedValue) 47 | { 48 | $jsonSchema = $this->toObject([ 49 | 'type' => 'object', 50 | 'properties' => [ 51 | 'param' => [ 52 | 'type' => 'array', 53 | 'items' => [ 54 | 'string' 55 | ], 56 | 'collectionFormat' => $collectionFormat 57 | ] 58 | ] 59 | ]); 60 | 61 | $normalizedValue = QueryParamsNormalizer::normalize(['param' => $rawValue], $jsonSchema); 62 | 63 | assertThat($normalizedValue['param'], equalTo($expectedValue)); 64 | } 65 | 66 | public function getValidCollectionFormat() 67 | { 68 | return [ 69 | 'with csv' => ['csv', 'foo,bar,baz', ['foo','bar','baz']], 70 | 'with ssv' => ['ssv', 'foo bar baz', ['foo','bar','baz']], 71 | 'with pipes' => ['pipes', 'foo|bar|baz', ['foo','bar','baz']], 72 | 'with tabs' => ['tsv', "foo\tbar\tbaz", ['foo','bar','baz']] 73 | ]; 74 | } 75 | 76 | /** @test */ 77 | public function itThrowAnExceptionOnUnsupportedCollectionFormat() 78 | { 79 | $this->expectException(\InvalidArgumentException::class); 80 | $this->expectExceptionMessage('unknown is not a supported query collection format'); 81 | 82 | $jsonSchema = $this->toObject([ 83 | 'type' => 'object', 84 | 'properties' => [ 85 | 'param' => [ 86 | 'type' => 'array', 87 | 'items' => ['string'], 88 | 'collectionFormat' => 'unknown' 89 | ] 90 | ] 91 | ]); 92 | 93 | QueryParamsNormalizer::normalize(['param' => 'foo%bar'], $jsonSchema); 94 | } 95 | 96 | private function toObject(array $array) 97 | { 98 | return json_decode(json_encode($array)); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/SchemaTest.php: -------------------------------------------------------------------------------- 1 | prophesize(RequestDefinition::class); 14 | $request->getMethod()->willReturn('GET'); 15 | $request->getPathTemplate()->willReturn('/api/pets/{id}'); 16 | $request->getOperationId()->willReturn('getPet'); 17 | 18 | $requests = $this->prophesize(RequestDefinitions::class); 19 | $requests->getIterator()->willReturn(new \ArrayIterator([$request->reveal()])); 20 | 21 | $schema = new Schema($requests->reveal()); 22 | 23 | $operationId = $schema->findOperationId('GET', '/api/pets/1234'); 24 | 25 | assertThat($operationId, equalTo('getPet')); 26 | } 27 | 28 | /** @test */ 29 | public function itThrowAnExceptionWhenNoOperationIdCanBeResolved() 30 | { 31 | $this->expectException(\InvalidArgumentException::class); 32 | $this->expectExceptionMessage('Unable to resolve the operationId for path /api/pets/1234'); 33 | 34 | $requests = $this->prophesize(RequestDefinitions::class); 35 | $requests->getIterator()->willReturn(new \ArrayIterator()); 36 | 37 | $schema = new Schema($requests->reveal(), '/api'); 38 | $schema->findOperationId('GET', '/api/pets/1234'); 39 | } 40 | 41 | /** @test */ 42 | public function itProvideARequestDefinition() 43 | { 44 | $request = $this->prophesize(RequestDefinition::class); 45 | $request->getMethod()->willReturn('GET'); 46 | $request->getPathTemplate()->willReturn('/pets/{id}'); 47 | $request->getOperationId()->willReturn('getPet'); 48 | 49 | $requests = $this->prophesize(RequestDefinitions::class); 50 | $requests->getIterator()->willReturn(new \ArrayIterator([$request->reveal()])); 51 | 52 | $schema = new Schema($requests->reveal(), '/api'); 53 | $actual = $schema->getRequestDefinition('getPet'); 54 | 55 | assertThat($actual, equalTo($request->reveal())); 56 | } 57 | 58 | /** @test */ 59 | public function itThrowAnExceptionWhenNoRequestDefinitionIsFound() 60 | { 61 | $this->expectException(\InvalidArgumentException::class); 62 | $this->expectExceptionMessage('Unable to get the request definition for getPet'); 63 | 64 | $requests = $this->prophesize(RequestDefinitions::class); 65 | $requests->getIterator()->willReturn(new \ArrayIterator()); 66 | 67 | $schema = new Schema($requests->reveal(), '/api'); 68 | $schema->getRequestDefinition('getPet'); 69 | } 70 | 71 | /** @test */ 72 | public function itCanBeSerialized() 73 | { 74 | $requests = $this->prophesize(RequestDefinitions::class); 75 | $requests->getIterator()->willReturn(new \ArrayIterator()); 76 | 77 | $schema = new Schema($requests->reveal()); 78 | $serialized = serialize($schema); 79 | 80 | assertThat(unserialize($serialized), equalTo($schema)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/Validator/ConstraintViolationTest.php: -------------------------------------------------------------------------------- 1 | 'property_one', 12 | 'message' => 'a violation message', 13 | 'constraint' => 'required', 14 | 'location' => 'query' 15 | ]; 16 | 17 | $violation = new ConstraintViolation('property_one', 'a violation message', 'required', 'query'); 18 | 19 | assertEquals($expectedArray, $violation->toArray()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Validator/MessageValidatorTest.php: -------------------------------------------------------------------------------- 1 | messageValidator = new MessageValidator( 30 | $validator, 31 | $decoder 32 | ); 33 | } 34 | 35 | /** @test */ 36 | public function itValidateAMessageContentType() 37 | { 38 | $expectedViolations = [ 39 | new ConstraintViolation( 40 | 'Content-Type', 41 | 'Content-Type should not be empty', 42 | 'required', 43 | 'header' 44 | ) 45 | ]; 46 | 47 | $message = $this->prophesize(MessageInterface::class); 48 | $message->getHeaderLine('Content-Type')->willReturn(''); 49 | 50 | $definition = $this->prophesize(MessageDefinition::class); 51 | $definition->getContentTypes()->willReturn(['application/json']); 52 | 53 | $this->messageValidator->validateContentType( 54 | $message->reveal(), 55 | $definition->reveal() 56 | ); 57 | 58 | assertThat($this->messageValidator->hasViolations(), isTrue()); 59 | assertThat($this->messageValidator->getViolations(), containsOnlyInstancesOf(ConstraintViolation::class)); 60 | assertThat($this->messageValidator->getViolations(), equalTo($expectedViolations)); 61 | } 62 | 63 | /** @test */ 64 | public function itValidateAMessageUnsupportedContentType() 65 | { 66 | $expectedViolations = [ 67 | new ConstraintViolation( 68 | 'Content-Type', 69 | 'text/plain is not a supported content type, supported: application/json', 70 | 'enum', 71 | 'header' 72 | ) 73 | ]; 74 | 75 | $message = $this->prophesize(MessageInterface::class); 76 | $message->getHeaderLine('Content-Type')->willReturn('text/plain'); 77 | 78 | $definition = $this->prophesize(MessageDefinition::class); 79 | $definition->getContentTypes()->willReturn(['application/json']); 80 | 81 | $this->messageValidator->validateContentType( 82 | $message->reveal(), 83 | $definition->reveal() 84 | ); 85 | 86 | assertThat($this->messageValidator->hasViolations(), isTrue()); 87 | assertThat($this->messageValidator->getViolations(), containsOnlyInstancesOf(ConstraintViolation::class)); 88 | assertThat($this->messageValidator->getViolations(), equalTo($expectedViolations)); 89 | } 90 | 91 | /** @test */ 92 | public function itValidateAMessageHeaders() 93 | { 94 | $expectedViolation = [ 95 | new ConstraintViolation( 96 | 'X-Required-Header', 97 | 'The property X-Required-Header is required', 98 | 'required', 99 | 'header' 100 | ) 101 | ]; 102 | 103 | $headersSchema = $this->toObject([ 104 | 'type' => 'object', 105 | 'required' => ['X-Required-Header'], 106 | 'properties' => [ 107 | 'X-Required-Header' => [ 108 | 'type' => 'string' 109 | ] 110 | ] 111 | ]); 112 | 113 | $message = $this->prophesize(MessageInterface::class); 114 | $message->getHeaders()->willReturn(['X-Foo' => ['bar', 'baz']]); 115 | 116 | $definition = $this->prophesize(MessageDefinition::class); 117 | $definition->hasHeadersSchema()->willReturn(true); 118 | $definition->getHeadersSchema()->willReturn($headersSchema); 119 | 120 | $this->messageValidator->validateHeaders( 121 | $message->reveal(), 122 | $definition->reveal() 123 | ); 124 | 125 | assertThat($this->messageValidator->hasViolations(), isTrue()); 126 | assertThat($this->messageValidator->getViolations(), containsOnlyInstancesOf(ConstraintViolation::class)); 127 | assertThat($this->messageValidator->getViolations(), equalTo($expectedViolation)); 128 | } 129 | 130 | /** @test */ 131 | public function itValidateTheRequestBody() 132 | { 133 | $expectedViolation = [ 134 | new ConstraintViolation( 135 | 'id', 136 | 'String value found, but an integer is required', 137 | 'type', 138 | 'body' 139 | ), 140 | ]; 141 | 142 | $bodySchema = $this->toObject([ 143 | 'type' => 'object', 144 | 'properties' => [ 145 | 'id' => [ 146 | 'type' => 'integer', 147 | 'format' => 'int32' 148 | ] 149 | ] 150 | ]); 151 | 152 | $message = $this->prophesize(MessageInterface::class); 153 | $message->getHeaderLine('Content-Type')->willReturn('application/json'); 154 | $message->getBody()->willReturn('{"id": "invalid"}'); 155 | 156 | $definition = $this->prophesize(MessageDefinition::class); 157 | $definition->getContentTypes()->willReturn(['application/json']); 158 | $definition->hasBodySchema()->willReturn(true); 159 | $definition->getBodySchema()->willReturn($bodySchema); 160 | 161 | $this->messageValidator->validateMessageBody( 162 | $message->reveal(), 163 | $definition->reveal() 164 | ); 165 | 166 | assertThat($this->messageValidator->hasViolations(), isTrue()); 167 | assertThat($this->messageValidator->getViolations(), containsOnlyInstancesOf(ConstraintViolation::class)); 168 | assertThat($this->messageValidator->getViolations(), equalTo($expectedViolation)); 169 | } 170 | 171 | /** @test */ 172 | public function itValidateARequestQueryParameters() 173 | { 174 | $expectedViolation = [ 175 | new ConstraintViolation( 176 | 'limit', 177 | 'String value found, but an integer is required', 178 | 'type', 179 | 'query' 180 | ) 181 | ]; 182 | 183 | $queryParametersSchema = $this->toObject([ 184 | 'type' => 'object', 185 | 'properties' => [ 186 | 'limit' => [ 187 | 'type' => 'integer' 188 | ] 189 | ] 190 | ]); 191 | 192 | $requestUri = $this->prophesize(UriInterface::class); 193 | $requestUri->getQuery()->willreturn('limit=invalid'); 194 | 195 | $request = $this->prophesize(RequestInterface::class); 196 | $request->getUri()->willReturn($requestUri); 197 | 198 | $definition = $this->prophesize(RequestDefinition::class); 199 | $definition->hasQueryParametersSchema()->willReturn(true); 200 | $definition->getQueryParametersSchema()->willReturn($queryParametersSchema); 201 | 202 | $this->messageValidator->validateQueryParameters( 203 | $request->reveal(), 204 | $definition->reveal() 205 | ); 206 | 207 | assertThat($this->messageValidator->hasViolations(), isTrue()); 208 | assertThat($this->messageValidator->getViolations(), containsOnlyInstancesOf(ConstraintViolation::class)); 209 | assertThat($this->messageValidator->getViolations(), equalTo($expectedViolation)); 210 | } 211 | 212 | /** @test */ 213 | public function itValidateARequest() 214 | { 215 | $expectedViolations = [ 216 | new ConstraintViolation('id', 'String value found, but an integer is required', 'type', 'body'), 217 | new ConstraintViolation('X-Required-Header', 'The property X-Required-Header is required', 'required', 'header'), 218 | new ConstraintViolation('limit', 'String value found, but an integer is required', 'type', 'query'), 219 | ]; 220 | 221 | $headersSchema = $this->toObject([ 222 | 'type' => 'object', 223 | 'required' => ['X-Required-Header'], 224 | 'properties' => [ 225 | 'X-Required-Header' => [ 226 | 'type' => 'string' 227 | ] 228 | ] 229 | ]); 230 | 231 | $bodySchema = $this->toObject([ 232 | 'type' => 'object', 233 | 'properties' => [ 234 | 'id' => [ 235 | 'type' => 'integer', 236 | 'format' => 'int32' 237 | ] 238 | ] 239 | ]); 240 | 241 | $queryParametersSchema = $this->toObject([ 242 | 'type' => 'object', 243 | 'properties' => [ 244 | 'limit' => [ 245 | 'type' => 'integer' 246 | ] 247 | ] 248 | ]); 249 | 250 | $uri = $this->prophesize(UriInterface::class); 251 | $uri->getQuery()->willreturn('limit=invalid'); 252 | 253 | $request = $this->prophesize(RequestInterface::class); 254 | $request->getMethod()->willReturn('POST'); 255 | $request->getUri()->willReturn($uri); 256 | $request->getBody()->willReturn('{"id": "invalid"}'); 257 | $request->getHeaderLine('Content-Type')->willReturn('application/json'); 258 | $request->getHeaders()->willReturn([]); 259 | 260 | $definition = $this->prophesize(RequestDefinition::class); 261 | $definition->getContentTypes()->willReturn(['application/json']); 262 | $definition->hasBodySchema()->willReturn(true); 263 | $definition->getBodySchema()->willReturn($bodySchema); 264 | $definition->hasHeadersSchema()->willReturn(true); 265 | $definition->getHeadersSchema()->willReturn($headersSchema); 266 | $definition->hasQueryParametersSchema()->willReturn(true); 267 | $definition->getQueryParametersSchema()->willReturn($queryParametersSchema); 268 | 269 | $this->messageValidator->validateRequest( 270 | $request->reveal(), 271 | $definition->reveal() 272 | ); 273 | 274 | assertThat($this->messageValidator->hasViolations(), isTrue()); 275 | assertThat($this->messageValidator->getViolations(), containsOnlyInstancesOf(ConstraintViolation::class)); 276 | assertThat($this->messageValidator->getViolations(), equalTo($expectedViolations)); 277 | } 278 | 279 | /** @test */ 280 | public function itValidateAResponse() 281 | { 282 | $expectedViolations = [ 283 | new ConstraintViolation('id', 'String value found, but an integer is required', 'type', 'body'), 284 | new ConstraintViolation('X-Required-Header', 'The property X-Required-Header is required', 'required', 'header'), 285 | ]; 286 | 287 | $headersSchema = $this->toObject([ 288 | 'type' => 'object', 289 | 'required' => ['X-Required-Header'], 290 | 'properties' => [ 291 | 'X-Required-Header' => [ 292 | 'type' => 'string' 293 | ] 294 | ] 295 | ]); 296 | 297 | $bodySchema = $this->toObject([ 298 | 'type' => 'object', 299 | 'properties' => [ 300 | 'id' => [ 301 | 'type' => 'integer', 302 | 'format' => 'int32' 303 | ] 304 | ] 305 | ]); 306 | 307 | $response = $this->prophesize(ResponseInterface::class); 308 | $response->getStatusCode()->willReturn('200'); 309 | $response->getBody()->willReturn('{"id": "invalid"}'); 310 | $response->getHeaderLine('Content-Type')->willReturn('application/json'); 311 | $response->getHeaders()->willReturn([]); 312 | 313 | $responseDefinition = $this->prophesize(RequestDefinition::class); 314 | $responseDefinition->getContentTypes()->willReturn(['application/json']); 315 | $responseDefinition->hasBodySchema()->willReturn(true); 316 | $responseDefinition->getBodySchema()->willReturn($bodySchema); 317 | $responseDefinition->hasHeadersSchema()->willReturn(true); 318 | $responseDefinition->getHeadersSchema()->willReturn($headersSchema); 319 | 320 | $definition = $this->prophesize(RequestDefinition::class); 321 | $definition->getResponseDefinition('200')->willReturn($responseDefinition); 322 | 323 | $this->messageValidator->validateResponse( 324 | $response->reveal(), 325 | $definition->reveal() 326 | ); 327 | 328 | assertThat($this->messageValidator->hasViolations(), isTrue()); 329 | assertThat($this->messageValidator->getViolations(), containsOnlyInstancesOf(ConstraintViolation::class)); 330 | assertThat($this->messageValidator->getViolations(), equalTo($expectedViolations)); 331 | } 332 | 333 | private function toObject(array $array) 334 | { 335 | return json_decode(json_encode($array)); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /tests/fixtures/operation-without-an-id.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "host": "domain.tld", 4 | "paths": { 5 | "/something": { 6 | "get": { 7 | "responses": { 8 | "200": { 9 | "description": "successful operation" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/operation-without-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "host": "domain.tld", 4 | "paths": { 5 | "/something": { 6 | "get": { 7 | "operationId": "getSomething", 8 | "responses": { 9 | "200": { 10 | "description": "successful operation" 11 | } 12 | } 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/fixtures/operation-without-responses.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "host": "domain.tld", 4 | "paths": { 5 | "/something": { 6 | "get": { 7 | "operationId": "getSomething" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/petstore.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", 5 | "version": "1.0.0", 6 | "title": "Swagger Petstore", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "email": "apiteam@swagger.io" 10 | }, 11 | "license": { 12 | "name": "Apache 2.0", 13 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 14 | } 15 | }, 16 | "host": "petstore.swagger.io", 17 | "basePath": "/v2", 18 | "tags": [ 19 | { 20 | "name": "pet", 21 | "description": "Everything about your Pets", 22 | "externalDocs": { 23 | "description": "Find out more", 24 | "url": "http://swagger.io" 25 | } 26 | }, 27 | { 28 | "name": "store", 29 | "description": "Access to Petstore orders" 30 | }, 31 | { 32 | "name": "user", 33 | "description": "Operations about user", 34 | "externalDocs": { 35 | "description": "Find out more about our store", 36 | "url": "http://swagger.io" 37 | } 38 | } 39 | ], 40 | "schemes": [ 41 | "https", 42 | "http" 43 | ], 44 | "paths": { 45 | "/pet": { 46 | "post": { 47 | "tags": [ 48 | "pet" 49 | ], 50 | "summary": "Add a new pet to the store", 51 | "description": "", 52 | "operationId": "addPet", 53 | "consumes": [ 54 | "application/json", 55 | "application/xml" 56 | ], 57 | "produces": [ 58 | "application/xml", 59 | "application/json" 60 | ], 61 | "parameters": [ 62 | { 63 | "in": "body", 64 | "name": "body", 65 | "description": "Pet object that needs to be added to the store", 66 | "required": true, 67 | "schema": { 68 | "$ref": "#/definitions/Pet" 69 | } 70 | } 71 | ], 72 | "responses": { 73 | "405": { 74 | "description": "Invalid input" 75 | } 76 | }, 77 | "security": [ 78 | { 79 | "petstore_auth": [ 80 | "write:pets", 81 | "read:pets" 82 | ] 83 | } 84 | ] 85 | }, 86 | "put": { 87 | "tags": [ 88 | "pet" 89 | ], 90 | "summary": "Update an existing pet", 91 | "description": "", 92 | "operationId": "updatePet", 93 | "consumes": [ 94 | "application/json", 95 | "application/xml" 96 | ], 97 | "produces": [ 98 | "application/xml", 99 | "application/json" 100 | ], 101 | "parameters": [ 102 | { 103 | "in": "body", 104 | "name": "body", 105 | "description": "Pet object that needs to be added to the store", 106 | "required": true, 107 | "schema": { 108 | "$ref": "#/definitions/Pet" 109 | } 110 | } 111 | ], 112 | "responses": { 113 | "400": { 114 | "description": "Invalid ID supplied" 115 | }, 116 | "404": { 117 | "description": "Pet not found" 118 | }, 119 | "405": { 120 | "description": "Validation exception" 121 | } 122 | }, 123 | "security": [ 124 | { 125 | "petstore_auth": [ 126 | "write:pets", 127 | "read:pets" 128 | ] 129 | } 130 | ] 131 | } 132 | }, 133 | "/pet/findByStatus": { 134 | "get": { 135 | "tags": [ 136 | "pet" 137 | ], 138 | "summary": "Finds Pets by status", 139 | "description": "Multiple status values can be provided with comma separated strings", 140 | "operationId": "findPetsByStatus", 141 | "produces": [ 142 | "application/xml", 143 | "application/json" 144 | ], 145 | "parameters": [ 146 | { 147 | "name": "status", 148 | "in": "query", 149 | "description": "Status values that need to be considered for filter", 150 | "required": true, 151 | "type": "array", 152 | "items": { 153 | "type": "string", 154 | "enum": [ 155 | "available", 156 | "pending", 157 | "sold" 158 | ], 159 | "default": "available" 160 | }, 161 | "collectionFormat": "multi" 162 | } 163 | ], 164 | "responses": { 165 | "200": { 166 | "description": "successful operation", 167 | "schema": { 168 | "type": "array", 169 | "items": { 170 | "$ref": "#/definitions/Pet" 171 | } 172 | } 173 | }, 174 | "400": { 175 | "description": "Invalid status value" 176 | } 177 | }, 178 | "security": [ 179 | { 180 | "petstore_auth": [ 181 | "write:pets", 182 | "read:pets" 183 | ] 184 | } 185 | ] 186 | } 187 | }, 188 | "/pet/findByTags": { 189 | "get": { 190 | "tags": [ 191 | "pet" 192 | ], 193 | "summary": "Finds Pets by tags", 194 | "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", 195 | "operationId": "findPetsByTags", 196 | "produces": [ 197 | "application/xml", 198 | "application/json" 199 | ], 200 | "parameters": [ 201 | { 202 | "name": "tags", 203 | "in": "query", 204 | "description": "Tags to filter by", 205 | "required": true, 206 | "type": "array", 207 | "items": { 208 | "type": "string" 209 | }, 210 | "collectionFormat": "multi" 211 | } 212 | ], 213 | "responses": { 214 | "200": { 215 | "description": "successful operation", 216 | "schema": { 217 | "type": "array", 218 | "items": { 219 | "$ref": "#/definitions/Pet" 220 | } 221 | } 222 | }, 223 | "400": { 224 | "description": "Invalid tag value" 225 | } 226 | }, 227 | "security": [ 228 | { 229 | "petstore_auth": [ 230 | "write:pets", 231 | "read:pets" 232 | ] 233 | } 234 | ], 235 | "deprecated": true 236 | } 237 | }, 238 | "/pet/{petId}": { 239 | "get": { 240 | "tags": [ 241 | "pet" 242 | ], 243 | "summary": "Find pet by ID", 244 | "description": "Returns a single pet", 245 | "operationId": "getPetById", 246 | "produces": [ 247 | "application/xml", 248 | "application/json" 249 | ], 250 | "parameters": [ 251 | { 252 | "name": "petId", 253 | "in": "path", 254 | "description": "ID of pet to return", 255 | "required": true, 256 | "type": "integer", 257 | "format": "int64" 258 | } 259 | ], 260 | "responses": { 261 | "200": { 262 | "description": "successful operation", 263 | "schema": { 264 | "$ref": "#/definitions/Pet" 265 | } 266 | }, 267 | "400": { 268 | "description": "Invalid ID supplied" 269 | }, 270 | "404": { 271 | "description": "Pet not found" 272 | } 273 | }, 274 | "security": [ 275 | { 276 | "api_key": [] 277 | } 278 | ] 279 | }, 280 | "post": { 281 | "tags": [ 282 | "pet" 283 | ], 284 | "summary": "Updates a pet in the store with form data", 285 | "description": "", 286 | "operationId": "updatePetWithForm", 287 | "consumes": [ 288 | "application/x-www-form-urlencoded" 289 | ], 290 | "produces": [ 291 | "application/xml", 292 | "application/json" 293 | ], 294 | "parameters": [ 295 | { 296 | "name": "petId", 297 | "in": "path", 298 | "description": "ID of pet that needs to be updated", 299 | "required": true, 300 | "type": "integer", 301 | "format": "int64" 302 | }, 303 | { 304 | "name": "name", 305 | "in": "formData", 306 | "description": "Updated name of the pet", 307 | "required": false, 308 | "type": "string" 309 | }, 310 | { 311 | "name": "status", 312 | "in": "formData", 313 | "description": "Updated status of the pet", 314 | "required": false, 315 | "type": "string" 316 | } 317 | ], 318 | "responses": { 319 | "405": { 320 | "description": "Invalid input" 321 | } 322 | }, 323 | "security": [ 324 | { 325 | "petstore_auth": [ 326 | "write:pets", 327 | "read:pets" 328 | ] 329 | } 330 | ] 331 | }, 332 | "delete": { 333 | "tags": [ 334 | "pet" 335 | ], 336 | "summary": "Deletes a pet", 337 | "description": "", 338 | "operationId": "deletePet", 339 | "produces": [ 340 | "application/xml", 341 | "application/json" 342 | ], 343 | "parameters": [ 344 | { 345 | "name": "api_key", 346 | "in": "header", 347 | "required": false, 348 | "type": "string" 349 | }, 350 | { 351 | "name": "petId", 352 | "in": "path", 353 | "description": "Pet id to delete", 354 | "required": true, 355 | "type": "integer", 356 | "format": "int64" 357 | } 358 | ], 359 | "responses": { 360 | "400": { 361 | "description": "Invalid ID supplied" 362 | }, 363 | "404": { 364 | "description": "Pet not found" 365 | } 366 | }, 367 | "security": [ 368 | { 369 | "petstore_auth": [ 370 | "write:pets", 371 | "read:pets" 372 | ] 373 | } 374 | ] 375 | } 376 | }, 377 | "/pet/{petId}/uploadImage": { 378 | "post": { 379 | "tags": [ 380 | "pet" 381 | ], 382 | "summary": "uploads an image", 383 | "description": "", 384 | "operationId": "uploadFile", 385 | "consumes": [ 386 | "multipart/form-data" 387 | ], 388 | "produces": [ 389 | "application/json" 390 | ], 391 | "parameters": [ 392 | { 393 | "name": "petId", 394 | "in": "path", 395 | "description": "ID of pet to update", 396 | "required": true, 397 | "type": "integer", 398 | "format": "int64" 399 | }, 400 | { 401 | "name": "additionalMetadata", 402 | "in": "formData", 403 | "description": "Additional data to pass to server", 404 | "required": false, 405 | "type": "string" 406 | }, 407 | { 408 | "name": "file", 409 | "in": "formData", 410 | "description": "file to upload", 411 | "required": false, 412 | "type": "file" 413 | } 414 | ], 415 | "responses": { 416 | "200": { 417 | "description": "successful operation", 418 | "schema": { 419 | "$ref": "#/definitions/ApiResponse" 420 | } 421 | } 422 | }, 423 | "security": [ 424 | { 425 | "petstore_auth": [ 426 | "write:pets", 427 | "read:pets" 428 | ] 429 | } 430 | ] 431 | } 432 | }, 433 | "/store/inventory": { 434 | "get": { 435 | "tags": [ 436 | "store" 437 | ], 438 | "summary": "Returns pet inventories by status", 439 | "description": "Returns a map of status codes to quantities", 440 | "operationId": "getInventory", 441 | "produces": [ 442 | "application/json" 443 | ], 444 | "parameters": [], 445 | "responses": { 446 | "200": { 447 | "description": "successful operation", 448 | "schema": { 449 | "type": "object", 450 | "additionalProperties": { 451 | "type": "integer", 452 | "format": "int32" 453 | } 454 | } 455 | } 456 | }, 457 | "security": [ 458 | { 459 | "api_key": [] 460 | } 461 | ] 462 | } 463 | }, 464 | "/store/order": { 465 | "post": { 466 | "tags": [ 467 | "store" 468 | ], 469 | "summary": "Place an order for a pet", 470 | "description": "", 471 | "operationId": "placeOrder", 472 | "produces": [ 473 | "application/xml", 474 | "application/json" 475 | ], 476 | "parameters": [ 477 | { 478 | "in": "body", 479 | "name": "body", 480 | "description": "order placed for purchasing the pet", 481 | "required": true, 482 | "schema": { 483 | "$ref": "#/definitions/Order" 484 | } 485 | } 486 | ], 487 | "responses": { 488 | "200": { 489 | "description": "successful operation", 490 | "schema": { 491 | "$ref": "#/definitions/Order" 492 | } 493 | }, 494 | "400": { 495 | "description": "Invalid Order" 496 | } 497 | } 498 | } 499 | }, 500 | "/store/order/{orderId}": { 501 | "get": { 502 | "tags": [ 503 | "store" 504 | ], 505 | "summary": "Find purchase order by ID", 506 | "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", 507 | "operationId": "getOrderById", 508 | "produces": [ 509 | "application/xml", 510 | "application/json" 511 | ], 512 | "parameters": [ 513 | { 514 | "name": "orderId", 515 | "in": "path", 516 | "description": "ID of pet that needs to be fetched", 517 | "required": true, 518 | "type": "integer", 519 | "maximum": 10.0, 520 | "minimum": 1.0, 521 | "format": "int64" 522 | } 523 | ], 524 | "responses": { 525 | "200": { 526 | "description": "successful operation", 527 | "schema": { 528 | "$ref": "#/definitions/Order" 529 | } 530 | }, 531 | "400": { 532 | "description": "Invalid ID supplied" 533 | }, 534 | "404": { 535 | "description": "Order not found" 536 | } 537 | } 538 | }, 539 | "delete": { 540 | "tags": [ 541 | "store" 542 | ], 543 | "summary": "Delete purchase order by ID", 544 | "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", 545 | "operationId": "deleteOrder", 546 | "produces": [ 547 | "application/xml", 548 | "application/json" 549 | ], 550 | "parameters": [ 551 | { 552 | "name": "orderId", 553 | "in": "path", 554 | "description": "ID of the order that needs to be deleted", 555 | "required": true, 556 | "type": "integer", 557 | "minimum": 1.0, 558 | "format": "int64" 559 | } 560 | ], 561 | "responses": { 562 | "400": { 563 | "description": "Invalid ID supplied" 564 | }, 565 | "404": { 566 | "description": "Order not found" 567 | } 568 | } 569 | } 570 | }, 571 | "/user": { 572 | "post": { 573 | "tags": [ 574 | "user" 575 | ], 576 | "summary": "Create user", 577 | "description": "This can only be done by the logged in user.", 578 | "operationId": "createUser", 579 | "produces": [ 580 | "application/xml", 581 | "application/json" 582 | ], 583 | "parameters": [ 584 | { 585 | "in": "body", 586 | "name": "body", 587 | "description": "Created user object", 588 | "required": true, 589 | "schema": { 590 | "$ref": "#/definitions/User" 591 | } 592 | } 593 | ], 594 | "responses": { 595 | "default": { 596 | "description": "successful operation" 597 | } 598 | } 599 | } 600 | }, 601 | "/user/createWithArray": { 602 | "post": { 603 | "tags": [ 604 | "user" 605 | ], 606 | "summary": "Creates list of users with given input array", 607 | "description": "", 608 | "operationId": "createUsersWithArrayInput", 609 | "produces": [ 610 | "application/xml", 611 | "application/json" 612 | ], 613 | "parameters": [ 614 | { 615 | "in": "body", 616 | "name": "body", 617 | "description": "List of user object", 618 | "required": true, 619 | "schema": { 620 | "type": "array", 621 | "items": { 622 | "$ref": "#/definitions/User" 623 | } 624 | } 625 | } 626 | ], 627 | "responses": { 628 | "default": { 629 | "description": "successful operation" 630 | } 631 | } 632 | } 633 | }, 634 | "/user/createWithList": { 635 | "post": { 636 | "tags": [ 637 | "user" 638 | ], 639 | "summary": "Creates list of users with given input array", 640 | "description": "", 641 | "operationId": "createUsersWithListInput", 642 | "produces": [ 643 | "application/xml", 644 | "application/json" 645 | ], 646 | "parameters": [ 647 | { 648 | "in": "body", 649 | "name": "body", 650 | "description": "List of user object", 651 | "required": true, 652 | "schema": { 653 | "type": "array", 654 | "items": { 655 | "$ref": "#/definitions/User" 656 | } 657 | } 658 | } 659 | ], 660 | "responses": { 661 | "default": { 662 | "description": "successful operation" 663 | } 664 | } 665 | } 666 | }, 667 | "/user/login": { 668 | "get": { 669 | "tags": [ 670 | "user" 671 | ], 672 | "summary": "Logs user into the system", 673 | "description": "", 674 | "operationId": "loginUser", 675 | "produces": [ 676 | "application/xml", 677 | "application/json" 678 | ], 679 | "parameters": [ 680 | { 681 | "name": "username", 682 | "in": "query", 683 | "description": "The user name for login", 684 | "required": true, 685 | "type": "string" 686 | }, 687 | { 688 | "name": "password", 689 | "in": "query", 690 | "description": "The password for login in clear text", 691 | "required": true, 692 | "type": "string" 693 | } 694 | ], 695 | "responses": { 696 | "200": { 697 | "description": "successful operation", 698 | "schema": { 699 | "type": "string" 700 | }, 701 | "headers": { 702 | "X-Rate-Limit": { 703 | "type": "integer", 704 | "format": "int32", 705 | "description": "calls per hour allowed by the user" 706 | }, 707 | "X-Expires-After": { 708 | "type": "string", 709 | "format": "date-time", 710 | "description": "date in UTC when token expires" 711 | } 712 | } 713 | }, 714 | "400": { 715 | "description": "Invalid username/password supplied" 716 | } 717 | } 718 | } 719 | }, 720 | "/user/logout": { 721 | "get": { 722 | "tags": [ 723 | "user" 724 | ], 725 | "summary": "Logs out current logged in user session", 726 | "description": "", 727 | "operationId": "logoutUser", 728 | "produces": [ 729 | "application/xml", 730 | "application/json" 731 | ], 732 | "parameters": [], 733 | "responses": { 734 | "default": { 735 | "description": "successful operation" 736 | } 737 | } 738 | } 739 | }, 740 | "/user/{username}": { 741 | "get": { 742 | "tags": [ 743 | "user" 744 | ], 745 | "summary": "Get user by user name", 746 | "description": "", 747 | "operationId": "getUserByName", 748 | "produces": [ 749 | "application/xml", 750 | "application/json" 751 | ], 752 | "parameters": [ 753 | { 754 | "name": "username", 755 | "in": "path", 756 | "description": "The name that needs to be fetched. Use user1 for testing. ", 757 | "required": true, 758 | "type": "string" 759 | } 760 | ], 761 | "responses": { 762 | "200": { 763 | "description": "successful operation", 764 | "schema": { 765 | "$ref": "#/definitions/User" 766 | } 767 | }, 768 | "400": { 769 | "description": "Invalid username supplied" 770 | }, 771 | "404": { 772 | "description": "User not found" 773 | } 774 | } 775 | }, 776 | "put": { 777 | "tags": [ 778 | "user" 779 | ], 780 | "summary": "Updated user", 781 | "description": "This can only be done by the logged in user.", 782 | "operationId": "updateUser", 783 | "produces": [ 784 | "application/xml", 785 | "application/json" 786 | ], 787 | "parameters": [ 788 | { 789 | "name": "username", 790 | "in": "path", 791 | "description": "name that need to be updated", 792 | "required": true, 793 | "type": "string" 794 | }, 795 | { 796 | "in": "body", 797 | "name": "body", 798 | "description": "Updated user object", 799 | "required": true, 800 | "schema": { 801 | "$ref": "#/definitions/User" 802 | } 803 | } 804 | ], 805 | "responses": { 806 | "400": { 807 | "description": "Invalid user supplied" 808 | }, 809 | "404": { 810 | "description": "User not found" 811 | } 812 | } 813 | }, 814 | "delete": { 815 | "tags": [ 816 | "user" 817 | ], 818 | "summary": "Delete user", 819 | "description": "This can only be done by the logged in user.", 820 | "operationId": "deleteUser", 821 | "produces": [ 822 | "application/xml", 823 | "application/json" 824 | ], 825 | "parameters": [ 826 | { 827 | "name": "username", 828 | "in": "path", 829 | "description": "The name that needs to be deleted", 830 | "required": true, 831 | "type": "string" 832 | } 833 | ], 834 | "responses": { 835 | "400": { 836 | "description": "Invalid username supplied" 837 | }, 838 | "404": { 839 | "description": "User not found" 840 | } 841 | } 842 | } 843 | } 844 | }, 845 | "securityDefinitions": { 846 | "petstore_auth": { 847 | "type": "oauth2", 848 | "authorizationUrl": "https://petstore.swagger.io/oauth/dialog", 849 | "flow": "implicit", 850 | "scopes": { 851 | "write:pets": "modify pets in your account", 852 | "read:pets": "read your pets" 853 | } 854 | }, 855 | "api_key": { 856 | "type": "apiKey", 857 | "name": "api_key", 858 | "in": "header" 859 | } 860 | }, 861 | "definitions": { 862 | "Order": { 863 | "type": "object", 864 | "properties": { 865 | "id": { 866 | "type": "integer", 867 | "format": "int64" 868 | }, 869 | "petId": { 870 | "type": "integer", 871 | "format": "int64" 872 | }, 873 | "quantity": { 874 | "type": "integer", 875 | "format": "int32" 876 | }, 877 | "shipDate": { 878 | "type": "string", 879 | "format": "date-time" 880 | }, 881 | "status": { 882 | "type": "string", 883 | "description": "Order Status", 884 | "enum": [ 885 | "placed", 886 | "approved", 887 | "delivered" 888 | ] 889 | }, 890 | "complete": { 891 | "type": "boolean", 892 | "default": false 893 | } 894 | }, 895 | "xml": { 896 | "name": "Order" 897 | } 898 | }, 899 | "User": { 900 | "type": "object", 901 | "properties": { 902 | "id": { 903 | "type": "integer", 904 | "format": "int64" 905 | }, 906 | "username": { 907 | "type": "string" 908 | }, 909 | "firstName": { 910 | "type": "string" 911 | }, 912 | "lastName": { 913 | "type": "string" 914 | }, 915 | "email": { 916 | "type": "string" 917 | }, 918 | "password": { 919 | "type": "string" 920 | }, 921 | "phone": { 922 | "type": "string" 923 | }, 924 | "userStatus": { 925 | "type": "integer", 926 | "format": "int32", 927 | "description": "User Status" 928 | } 929 | }, 930 | "xml": { 931 | "name": "User" 932 | } 933 | }, 934 | "Category": { 935 | "type": "object", 936 | "properties": { 937 | "id": { 938 | "type": "integer", 939 | "format": "int64" 940 | }, 941 | "name": { 942 | "type": "string" 943 | } 944 | }, 945 | "xml": { 946 | "name": "Category" 947 | } 948 | }, 949 | "Tag": { 950 | "type": "object", 951 | "properties": { 952 | "id": { 953 | "type": "integer", 954 | "format": "int64" 955 | }, 956 | "name": { 957 | "type": "string" 958 | } 959 | }, 960 | "xml": { 961 | "name": "Tag" 962 | } 963 | }, 964 | "ApiResponse": { 965 | "type": "object", 966 | "properties": { 967 | "code": { 968 | "type": "integer", 969 | "format": "int32" 970 | }, 971 | "type": { 972 | "type": "string" 973 | }, 974 | "message": { 975 | "type": "string" 976 | } 977 | } 978 | }, 979 | "Pet": { 980 | "type": "object", 981 | "required": [ 982 | "name", 983 | "photoUrls" 984 | ], 985 | "properties": { 986 | "id": { 987 | "type": "integer", 988 | "format": "int64" 989 | }, 990 | "category": { 991 | "$ref": "#/definitions/Category" 992 | }, 993 | "name": { 994 | "type": "string", 995 | "example": "doggie" 996 | }, 997 | "photoUrls": { 998 | "type": "array", 999 | "xml": { 1000 | "name": "photoUrl", 1001 | "wrapped": true 1002 | }, 1003 | "items": { 1004 | "type": "string" 1005 | } 1006 | }, 1007 | "tags": { 1008 | "type": "array", 1009 | "xml": { 1010 | "name": "tag", 1011 | "wrapped": true 1012 | }, 1013 | "items": { 1014 | "$ref": "#/definitions/Tag" 1015 | } 1016 | }, 1017 | "status": { 1018 | "type": "string", 1019 | "description": "pet status in the store", 1020 | "enum": [ 1021 | "available", 1022 | "pending", 1023 | "sold" 1024 | ] 1025 | } 1026 | }, 1027 | "xml": { 1028 | "name": "Pet" 1029 | } 1030 | } 1031 | }, 1032 | "externalDocs": { 1033 | "description": "Find out more about Swagger", 1034 | "url": "http://swagger.io" 1035 | } 1036 | } 1037 | -------------------------------------------------------------------------------- /tests/fixtures/petstore.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleven-labs/api-validator/e9f016eb86dc8e2afd3baafbea28c08158b7eec3/tests/fixtures/petstore.txt -------------------------------------------------------------------------------- /tests/fixtures/petstore.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | swagger: "2.0" 3 | info: 4 | description: | 5 | This is a sample server Petstore server. You can find out more about 6 | Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). 7 | For this sample, you can use the api key `special-key` to test the authorization 8 | filters. 9 | version: "1.0.0" 10 | title: "Swagger Petstore" 11 | termsOfService: "http://swagger.io/terms/" 12 | contact: 13 | email: "apiteam@swagger.io" 14 | license: 15 | name: "Apache 2.0" 16 | url: "http://www.apache.org/licenses/LICENSE-2.0.html" 17 | host: "petstore.swagger.io" 18 | basePath: "/v2" 19 | tags: 20 | - name: "pet" 21 | description: "Everything about your Pets" 22 | externalDocs: 23 | description: "Find out more" 24 | url: "http://swagger.io" 25 | - name: "store" 26 | description: "Access to Petstore orders" 27 | - name: "user" 28 | description: "Operations about user" 29 | externalDocs: 30 | description: "Find out more about our store" 31 | url: "http://swagger.io" 32 | schemes: 33 | - "https" 34 | - "http" 35 | paths: 36 | /pet: 37 | post: 38 | tags: 39 | - "pet" 40 | summary: "Add a new pet to the store" 41 | description: "" 42 | operationId: "addPet" 43 | consumes: 44 | - "application/json" 45 | - "application/xml" 46 | produces: 47 | - "application/xml" 48 | - "application/json" 49 | parameters: 50 | - in: "body" 51 | name: "body" 52 | description: "Pet object that needs to be added to the store" 53 | required: true 54 | schema: 55 | $ref: "#/definitions/Pet" 56 | responses: 57 | 405: 58 | description: "Invalid input" 59 | security: 60 | - petstore_auth: 61 | - "write:pets" 62 | - "read:pets" 63 | put: 64 | tags: 65 | - "pet" 66 | summary: "Update an existing pet" 67 | description: "" 68 | operationId: "updatePet" 69 | consumes: 70 | - "application/json" 71 | - "application/xml" 72 | produces: 73 | - "application/xml" 74 | - "application/json" 75 | parameters: 76 | - in: "body" 77 | name: "body" 78 | description: "Pet object that needs to be added to the store" 79 | required: true 80 | schema: 81 | $ref: "#/definitions/Pet" 82 | responses: 83 | 400: 84 | description: "Invalid ID supplied" 85 | 404: 86 | description: "Pet not found" 87 | 405: 88 | description: "Validation exception" 89 | security: 90 | - petstore_auth: 91 | - "write:pets" 92 | - "read:pets" 93 | /pet/findByStatus: 94 | get: 95 | tags: 96 | - "pet" 97 | summary: "Finds Pets by status" 98 | description: "Multiple status values can be provided with comma separated strings" 99 | operationId: "findPetsByStatus" 100 | produces: 101 | - "application/xml" 102 | - "application/json" 103 | parameters: 104 | - name: "status" 105 | in: "query" 106 | description: "Status values that need to be considered for filter" 107 | required: true 108 | type: "array" 109 | items: 110 | type: "string" 111 | enum: 112 | - "available" 113 | - "pending" 114 | - "sold" 115 | default: "available" 116 | collectionFormat: "multi" 117 | responses: 118 | 200: 119 | description: "successful operation" 120 | schema: 121 | type: "array" 122 | items: 123 | $ref: "#/definitions/Pet" 124 | 400: 125 | description: "Invalid status value" 126 | security: 127 | - petstore_auth: 128 | - "write:pets" 129 | - "read:pets" 130 | /pet/findByTags: 131 | get: 132 | tags: 133 | - "pet" 134 | summary: "Finds Pets by tags" 135 | description: | 136 | Muliple tags can be provided with comma separated strings. Use 137 | tag1, tag2, tag3 for testing." 138 | operationId: "findPetsByTags" 139 | produces: 140 | - "application/xml" 141 | - "application/json" 142 | parameters: 143 | - name: "tags" 144 | in: "query" 145 | description: "Tags to filter by" 146 | required: true 147 | type: "array" 148 | items: 149 | type: "string" 150 | collectionFormat: "multi" 151 | responses: 152 | 200: 153 | description: "successful operation" 154 | schema: 155 | type: "array" 156 | items: 157 | $ref: "#/definitions/Pet" 158 | 400: 159 | description: "Invalid tag value" 160 | security: 161 | - petstore_auth: 162 | - "write:pets" 163 | - "read:pets" 164 | deprecated: true 165 | /pet/{petId}: 166 | get: 167 | tags: 168 | - "pet" 169 | summary: "Find pet by ID" 170 | description: "Returns a single pet" 171 | operationId: "getPetById" 172 | produces: 173 | - "application/xml" 174 | - "application/json" 175 | parameters: 176 | - name: "petId" 177 | in: "path" 178 | description: "ID of pet to return" 179 | required: true 180 | type: "integer" 181 | format: "int64" 182 | responses: 183 | 200: 184 | description: "successful operation" 185 | schema: 186 | $ref: "#/definitions/Pet" 187 | 400: 188 | description: "Invalid ID supplied" 189 | 404: 190 | description: "Pet not found" 191 | security: 192 | - api_key: [] 193 | post: 194 | tags: 195 | - "pet" 196 | summary: "Updates a pet in the store with form data" 197 | description: "" 198 | operationId: "updatePetWithForm" 199 | consumes: 200 | - "application/x-www-form-urlencoded" 201 | produces: 202 | - "application/xml" 203 | - "application/json" 204 | parameters: 205 | - name: "petId" 206 | in: "path" 207 | description: "ID of pet that needs to be updated" 208 | required: true 209 | type: "integer" 210 | format: "int64" 211 | - name: "name" 212 | in: "formData" 213 | description: "Updated name of the pet" 214 | required: false 215 | type: "string" 216 | - name: "status" 217 | in: "formData" 218 | description: "Updated status of the pet" 219 | required: false 220 | type: "string" 221 | responses: 222 | 405: 223 | description: "Invalid input" 224 | security: 225 | - petstore_auth: 226 | - "write:pets" 227 | - "read:pets" 228 | delete: 229 | tags: 230 | - "pet" 231 | summary: "Deletes a pet" 232 | description: "" 233 | operationId: "deletePet" 234 | produces: 235 | - "application/xml" 236 | - "application/json" 237 | parameters: 238 | - name: "api_key" 239 | in: "header" 240 | required: false 241 | type: "string" 242 | - name: "petId" 243 | in: "path" 244 | description: "Pet id to delete" 245 | required: true 246 | type: "integer" 247 | format: "int64" 248 | responses: 249 | 400: 250 | description: "Invalid ID supplied" 251 | 404: 252 | description: "Pet not found" 253 | security: 254 | - petstore_auth: 255 | - "write:pets" 256 | - "read:pets" 257 | /pet/{petId}/uploadImage: 258 | post: 259 | tags: 260 | - "pet" 261 | summary: "uploads an image" 262 | description: "" 263 | operationId: "uploadFile" 264 | consumes: 265 | - "multipart/form-data" 266 | produces: 267 | - "application/json" 268 | parameters: 269 | - name: "petId" 270 | in: "path" 271 | description: "ID of pet to update" 272 | required: true 273 | type: "integer" 274 | format: "int64" 275 | - name: "additionalMetadata" 276 | in: "formData" 277 | description: "Additional data to pass to server" 278 | required: false 279 | type: "string" 280 | - name: "file" 281 | in: "formData" 282 | description: "file to upload" 283 | required: false 284 | type: "file" 285 | responses: 286 | 200: 287 | description: "successful operation" 288 | schema: 289 | $ref: "#/definitions/ApiResponse" 290 | security: 291 | - petstore_auth: 292 | - "write:pets" 293 | - "read:pets" 294 | /store/inventory: 295 | get: 296 | tags: 297 | - "store" 298 | summary: "Returns pet inventories by status" 299 | description: "Returns a map of status codes to quantities" 300 | operationId: "getInventory" 301 | produces: 302 | - "application/json" 303 | parameters: [] 304 | responses: 305 | 200: 306 | description: "successful operation" 307 | schema: 308 | type: "object" 309 | additionalProperties: 310 | type: "integer" 311 | format: "int32" 312 | security: 313 | - api_key: [] 314 | /store/order: 315 | post: 316 | tags: 317 | - "store" 318 | summary: "Place an order for a pet" 319 | description: "" 320 | operationId: "placeOrder" 321 | produces: 322 | - "application/xml" 323 | - "application/json" 324 | parameters: 325 | - in: "body" 326 | name: "body" 327 | description: "order placed for purchasing the pet" 328 | required: true 329 | schema: 330 | $ref: "#/definitions/Order" 331 | responses: 332 | 200: 333 | description: "successful operation" 334 | schema: 335 | $ref: "#/definitions/Order" 336 | 400: 337 | description: "Invalid Order" 338 | /store/order/{orderId}: 339 | get: 340 | tags: 341 | - "store" 342 | summary: "Find purchase order by ID" 343 | description: | 344 | For valid response try integer IDs with value >= 1 and <= 10. 345 | Other values will generated exceptions" 346 | operationId: "getOrderById" 347 | produces: 348 | - "application/xml" 349 | - "application/json" 350 | parameters: 351 | - name: "orderId" 352 | in: "path" 353 | description: "ID of pet that needs to be fetched" 354 | required: true 355 | type: "integer" 356 | maximum: 10.0 357 | minimum: 1.0 358 | format: "int64" 359 | responses: 360 | 200: 361 | description: "successful operation" 362 | schema: 363 | $ref: "#/definitions/Order" 364 | 400: 365 | description: "Invalid ID supplied" 366 | 404: 367 | description: "Order not found" 368 | delete: 369 | tags: 370 | - "store" 371 | summary: "Delete purchase order by ID" 372 | description: | 373 | For valid response try integer IDs with positive integer value. 374 | Negative or non-integer values will generate API errors 375 | operationId: "deleteOrder" 376 | produces: 377 | - "application/xml" 378 | - "application/json" 379 | parameters: 380 | - name: "orderId" 381 | in: "path" 382 | description: "ID of the order that needs to be deleted" 383 | required: true 384 | type: "integer" 385 | minimum: 1.0 386 | format: "int64" 387 | responses: 388 | 400: 389 | description: "Invalid ID supplied" 390 | 404: 391 | description: "Order not found" 392 | /user: 393 | post: 394 | tags: 395 | - "user" 396 | summary: "Create user" 397 | description: "This can only be done by the logged in user." 398 | operationId: "createUser" 399 | produces: 400 | - "application/xml" 401 | - "application/json" 402 | parameters: 403 | - in: "body" 404 | name: "body" 405 | description: "Created user object" 406 | required: true 407 | schema: 408 | $ref: "#/definitions/User" 409 | responses: 410 | default: 411 | description: "successful operation" 412 | /user/createWithArray: 413 | post: 414 | tags: 415 | - "user" 416 | summary: "Creates list of users with given input array" 417 | description: "" 418 | operationId: "createUsersWithArrayInput" 419 | produces: 420 | - "application/xml" 421 | - "application/json" 422 | parameters: 423 | - in: "body" 424 | name: "body" 425 | description: "List of user object" 426 | required: true 427 | schema: 428 | type: "array" 429 | items: 430 | $ref: "#/definitions/User" 431 | responses: 432 | default: 433 | description: "successful operation" 434 | /user/createWithList: 435 | post: 436 | tags: 437 | - "user" 438 | summary: "Creates list of users with given input array" 439 | description: "" 440 | operationId: "createUsersWithListInput" 441 | produces: 442 | - "application/xml" 443 | - "application/json" 444 | parameters: 445 | - in: "body" 446 | name: "body" 447 | description: "List of user object" 448 | required: true 449 | schema: 450 | type: "array" 451 | items: 452 | $ref: "#/definitions/User" 453 | responses: 454 | default: 455 | description: "successful operation" 456 | /user/login: 457 | get: 458 | tags: 459 | - "user" 460 | summary: "Logs user into the system" 461 | description: "" 462 | operationId: "loginUser" 463 | produces: 464 | - "application/xml" 465 | - "application/json" 466 | parameters: 467 | - name: "username" 468 | in: "query" 469 | description: "The user name for login" 470 | required: true 471 | type: "string" 472 | - name: "password" 473 | in: "query" 474 | description: "The password for login in clear text" 475 | required: true 476 | type: "string" 477 | responses: 478 | 200: 479 | description: "successful operation" 480 | schema: 481 | type: "string" 482 | headers: 483 | X-Rate-Limit: 484 | type: "integer" 485 | format: "int32" 486 | description: "calls per hour allowed by the user" 487 | X-Expires-After: 488 | type: "string" 489 | format: "date-time" 490 | description: "date in UTC when token expires" 491 | 400: 492 | description: "Invalid username/password supplied" 493 | /user/logout: 494 | get: 495 | tags: 496 | - "user" 497 | summary: "Logs out current logged in user session" 498 | description: "" 499 | operationId: "logoutUser" 500 | produces: 501 | - "application/xml" 502 | - "application/json" 503 | parameters: [] 504 | responses: 505 | default: 506 | description: "successful operation" 507 | /user/{username}: 508 | get: 509 | tags: 510 | - "user" 511 | summary: "Get user by user name" 512 | description: "" 513 | operationId: "getUserByName" 514 | produces: 515 | - "application/xml" 516 | - "application/json" 517 | parameters: 518 | - name: "username" 519 | in: "path" 520 | description: "The name that needs to be fetched. Use user1 for testing. " 521 | required: true 522 | type: "string" 523 | responses: 524 | 200: 525 | description: "successful operation" 526 | schema: 527 | $ref: "#/definitions/User" 528 | 400: 529 | description: "Invalid username supplied" 530 | 404: 531 | description: "User not found" 532 | put: 533 | tags: 534 | - "user" 535 | summary: "Updated user" 536 | description: "This can only be done by the logged in user." 537 | operationId: "updateUser" 538 | produces: 539 | - "application/xml" 540 | - "application/json" 541 | parameters: 542 | - name: "username" 543 | in: "path" 544 | description: "name that need to be updated" 545 | required: true 546 | type: "string" 547 | - in: "body" 548 | name: "body" 549 | description: "Updated user object" 550 | required: true 551 | schema: 552 | $ref: "#/definitions/User" 553 | responses: 554 | 400: 555 | description: "Invalid user supplied" 556 | 404: 557 | description: "User not found" 558 | delete: 559 | tags: 560 | - "user" 561 | summary: "Delete user" 562 | description: "This can only be done by the logged in user." 563 | operationId: "deleteUser" 564 | produces: 565 | - "application/xml" 566 | - "application/json" 567 | parameters: 568 | - name: "username" 569 | in: "path" 570 | description: "The name that needs to be deleted" 571 | required: true 572 | type: "string" 573 | responses: 574 | 400: 575 | description: "Invalid username supplied" 576 | 404: 577 | description: "User not found" 578 | securityDefinitions: 579 | petstore_auth: 580 | type: "oauth2" 581 | authorizationUrl: "https://petstore.swagger.io/oauth/dialog" 582 | flow: "implicit" 583 | scopes: 584 | write:pets: "modify pets in your account" 585 | read:pets: "read your pets" 586 | api_key: 587 | type: "apiKey" 588 | name: "api_key" 589 | in: "header" 590 | definitions: 591 | Order: 592 | type: "object" 593 | properties: 594 | id: 595 | type: "integer" 596 | format: "int64" 597 | petId: 598 | type: "integer" 599 | format: "int64" 600 | quantity: 601 | type: "integer" 602 | format: "int32" 603 | shipDate: 604 | type: "string" 605 | format: "date-time" 606 | status: 607 | type: "string" 608 | description: "Order Status" 609 | enum: 610 | - "placed" 611 | - "approved" 612 | - "delivered" 613 | complete: 614 | type: "boolean" 615 | default: false 616 | xml: 617 | name: "Order" 618 | User: 619 | type: "object" 620 | properties: 621 | id: 622 | type: "integer" 623 | format: "int64" 624 | username: 625 | type: "string" 626 | firstName: 627 | type: "string" 628 | lastName: 629 | type: "string" 630 | email: 631 | type: "string" 632 | password: 633 | type: "string" 634 | phone: 635 | type: "string" 636 | userStatus: 637 | type: "integer" 638 | format: "int32" 639 | description: "User Status" 640 | xml: 641 | name: "User" 642 | Category: 643 | type: "object" 644 | properties: 645 | id: 646 | type: "integer" 647 | format: "int64" 648 | name: 649 | type: "string" 650 | xml: 651 | name: "Category" 652 | Tag: 653 | type: "object" 654 | properties: 655 | id: 656 | type: "integer" 657 | format: "int64" 658 | name: 659 | type: "string" 660 | xml: 661 | name: "Tag" 662 | ApiResponse: 663 | type: "object" 664 | properties: 665 | code: 666 | type: "integer" 667 | format: "int32" 668 | type: 669 | type: "string" 670 | message: 671 | type: "string" 672 | Pet: 673 | type: "object" 674 | required: 675 | - "name" 676 | - "photoUrls" 677 | properties: 678 | id: 679 | type: "integer" 680 | format: "int64" 681 | category: 682 | $ref: "#/definitions/Category" 683 | name: 684 | type: "string" 685 | example: "doggie" 686 | photoUrls: 687 | type: "array" 688 | xml: 689 | name: "photoUrl" 690 | wrapped: true 691 | items: 692 | type: "string" 693 | tags: 694 | type: "array" 695 | xml: 696 | name: "tag" 697 | wrapped: true 698 | items: 699 | $ref: "#/definitions/Tag" 700 | status: 701 | type: "string" 702 | description: "pet status in the store" 703 | enum: 704 | - "available" 705 | - "pending" 706 | - "sold" 707 | xml: 708 | name: "Pet" 709 | externalDocs: 710 | description: "Find out more about Swagger" 711 | url: "http://swagger.io" 712 | -------------------------------------------------------------------------------- /tests/fixtures/request-with-conflicting-locations.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "host": "petstore.swagger.io", 4 | "schemes": [ 5 | "https", 6 | "http" 7 | ], 8 | "paths": { 9 | "/post/with-conflicting-locations": { 10 | "post": { 11 | "operationId": "requestWithConflictingLocations", 12 | "produces": ["application/json"], 13 | "parameters": [ 14 | { 15 | "in": "body", 16 | "name": "body", 17 | "required": true, 18 | "schema": { 19 | "type": "object" 20 | } 21 | }, 22 | { 23 | "in": "formData", 24 | "name": "foo", 25 | "type": "string" 26 | } 27 | ], 28 | "responses": { 29 | "200": { 30 | "description": "successful operation" 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/fixtures/request-without-content-types.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "host": "petstore.swagger.io", 4 | "schemes": [ 5 | "https", 6 | "http" 7 | ], 8 | "paths": { 9 | "/post/body-without-a-content-type": { 10 | "post": { 11 | "operationId": "postBodyWithoutAContentType", 12 | "produces": ["application/json"], 13 | "parameters": [ 14 | { 15 | "in": "body", 16 | "name": "body", 17 | "required": true, 18 | "schema": { 19 | "type": "object" 20 | } 21 | } 22 | ], 23 | "responses": { 24 | "200": { 25 | "description": "successful operation" 26 | } 27 | } 28 | } 29 | }, 30 | "/post/form-data-without-a-content-type": { 31 | "post": { 32 | "operationId": "postFromDataWithoutAContentType", 33 | "produces": ["application/json"], 34 | "parameters": [ 35 | { 36 | "in": "formData", 37 | "name": "foo", 38 | "required": true, 39 | "type": "string" 40 | } 41 | ], 42 | "responses": { 43 | "200": { 44 | "description": "successful operation" 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/fixtures/schema-with-default-consumes-and-produces-properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "host": "domain.tld", 4 | "consumes": ["application/json"], 5 | "produces": ["application/json"], 6 | "paths": { 7 | "/something": { 8 | "post": { 9 | "operationId": "postSomething", 10 | "responses": { 11 | "201": { 12 | "description": "created" 13 | } 14 | } 15 | } 16 | } 17 | } 18 | } 19 | --------------------------------------------------------------------------------