1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* This file is part of the KleijnWeb\SwaggerBundle package. |
4
|
|
|
* |
5
|
|
|
* For the full copyright and license information, please view the LICENSE |
6
|
|
|
* file that was distributed with this source code. |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace KleijnWeb\SwaggerBundle\Test; |
10
|
|
|
|
11
|
|
|
use FR3D\SwaggerAssertions\PhpUnit\AssertsTrait; |
12
|
|
|
use FR3D\SwaggerAssertions\SchemaManager; |
13
|
|
|
use JsonSchema\Validator; |
14
|
|
|
use KleijnWeb\SwaggerBundle\Document\DocumentRepository; |
15
|
|
|
use KleijnWeb\SwaggerBundle\Document\SwaggerDocument; |
16
|
|
|
use org\bovigo\vfs\vfsStream; |
17
|
|
|
use org\bovigo\vfs\vfsStreamDirectory; |
18
|
|
|
use org\bovigo\vfs\vfsStreamWrapper; |
19
|
|
|
use Symfony\Component\HttpFoundation\Response; |
20
|
|
|
use Symfony\Component\Yaml\Yaml; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @author John Kleijn <[email protected]> |
24
|
|
|
* |
25
|
|
|
* @property bool validateErrorResponse |
26
|
|
|
* @property string env |
27
|
|
|
* @property array defaultServerVars |
28
|
|
|
*/ |
29
|
|
|
trait ApiTestCase |
30
|
|
|
{ |
31
|
|
|
use AssertsTrait; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var SchemaManager |
35
|
|
|
*/ |
36
|
|
|
protected static $schemaManager; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @var SwaggerDocument |
40
|
|
|
*/ |
41
|
|
|
protected static $document; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var ApiTestClient |
45
|
|
|
*/ |
46
|
|
|
protected $client; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* PHPUnit cannot add this to code coverage |
50
|
|
|
* |
51
|
|
|
* @codeCoverageIgnore |
52
|
|
|
* |
53
|
|
|
* @param string $swaggerPath |
54
|
|
|
* |
55
|
|
|
* @throws \InvalidArgumentException |
56
|
|
|
* @throws \org\bovigo\vfs\vfsStreamException |
57
|
|
|
*/ |
58
|
|
|
public static function initSchemaManager($swaggerPath) |
59
|
|
|
{ |
60
|
|
|
$validator = new Validator(); |
61
|
|
|
$validator->check( |
62
|
|
|
json_decode(json_encode(Yaml::parse(file_get_contents($swaggerPath)))), |
63
|
|
|
json_decode(file_get_contents(__DIR__ . '/../../assets/swagger-schema.json')) |
64
|
|
|
); |
65
|
|
|
|
66
|
|
|
if (!$validator->isValid()) { |
67
|
|
|
throw new \InvalidArgumentException( |
68
|
|
|
"Swagger '$swaggerPath' not valid" |
69
|
|
|
); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
vfsStreamWrapper::register(); |
73
|
|
|
vfsStreamWrapper::setRoot(new vfsStreamDirectory('root')); |
74
|
|
|
|
75
|
|
|
file_put_contents( |
76
|
|
|
vfsStream::url('root') . '/swagger.json', |
77
|
|
|
json_encode(Yaml::parse(file_get_contents($swaggerPath))) |
78
|
|
|
); |
79
|
|
|
|
80
|
|
|
self::$schemaManager = new SchemaManager(vfsStream::url('root') . '/swagger.json'); |
81
|
|
|
$repository = new DocumentRepository(dirname($swaggerPath)); |
82
|
|
|
self::$document = $repository->get(basename($swaggerPath)); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Create a client, booting the kernel using SYMFONY_ENV = $this->env |
87
|
|
|
*/ |
88
|
|
|
protected function setUp() |
89
|
|
|
{ |
90
|
|
|
$this->client = static::createClient(['environment' => $this->getEnv(), 'debug' => true]); |
91
|
|
|
|
92
|
|
|
parent::setUp(); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* @return array |
97
|
|
|
*/ |
98
|
|
|
protected function getDefaultServerVars() |
99
|
|
|
{ |
100
|
|
|
return isset($this->defaultServerVars) ? $this->defaultServerVars : []; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* @return array |
105
|
|
|
*/ |
106
|
|
|
protected function getEnv() |
107
|
|
|
{ |
108
|
|
|
return isset($this->env) ? $this->env : 'test'; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* @return bool |
113
|
|
|
*/ |
114
|
|
|
protected function getValidateErrorResponse() |
115
|
|
|
{ |
116
|
|
|
return isset($this->validateErrorResponse) ? $this->validateErrorResponse : false; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @param string $path |
121
|
|
|
* @param array $params |
122
|
|
|
* |
123
|
|
|
* @return object |
124
|
|
|
* @throws ApiResponseErrorException |
125
|
|
|
*/ |
126
|
|
|
protected function get($path, array $params = []) |
127
|
|
|
{ |
128
|
|
|
return $this->sendRequest($path, 'GET', $params); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* @param string $path |
133
|
|
|
* @param array $params |
134
|
|
|
* |
135
|
|
|
* @return object |
136
|
|
|
* @throws ApiResponseErrorException |
137
|
|
|
*/ |
138
|
|
|
protected function delete($path, array $params = []) |
139
|
|
|
{ |
140
|
|
|
return $this->sendRequest($path, 'DELETE', $params); |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* @param string $path |
145
|
|
|
* @param array $content |
146
|
|
|
* @param array $params |
147
|
|
|
* |
148
|
|
|
* @return object |
149
|
|
|
* @throws ApiResponseErrorException |
150
|
|
|
*/ |
151
|
|
|
protected function patch($path, array $content, array $params = []) |
152
|
|
|
{ |
153
|
|
|
return $this->sendRequest($path, 'PATCH', $params, $content); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* @param string $path |
158
|
|
|
* @param array $content |
159
|
|
|
* @param array $params |
160
|
|
|
* |
161
|
|
|
* @return object |
162
|
|
|
* @throws ApiResponseErrorException |
163
|
|
|
*/ |
164
|
|
|
protected function post($path, array $content, array $params = []) |
165
|
|
|
{ |
166
|
|
|
return $this->sendRequest($path, 'POST', $params, $content); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* @param string $path |
171
|
|
|
* @param array $content |
172
|
|
|
* @param array $params |
173
|
|
|
* |
174
|
|
|
* @return object |
175
|
|
|
* @throws ApiResponseErrorException |
176
|
|
|
*/ |
177
|
|
|
protected function put($path, array $content, array $params = []) |
178
|
|
|
{ |
179
|
|
|
return $this->sendRequest($path, 'PUT', $params, $content); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* @param string $path |
184
|
|
|
* @param string $method |
185
|
|
|
* @param array $params |
186
|
|
|
* @param array|null $content |
187
|
|
|
* |
188
|
|
|
* @return object |
189
|
|
|
* @throws ApiResponseErrorException |
190
|
|
|
*/ |
191
|
|
|
protected function sendRequest($path, $method, array $params = [], array $content = null) |
192
|
|
|
{ |
193
|
|
|
$request = new ApiRequest($this->assembleUri($path, $params), $method); |
194
|
|
|
$request->setServer(array_merge(['CONTENT_TYPE' => 'application/json'], $this->getDefaultServerVars())); |
195
|
|
|
if ($content !== null) { |
196
|
|
|
$request->setContent(json_encode($content)); |
197
|
|
|
} |
198
|
|
|
$this->client->requestFromRequest($request); |
199
|
|
|
|
200
|
|
|
return $this->getJsonForLastRequest($path, $method); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* @param string $path |
205
|
|
|
* @param array $params |
206
|
|
|
* |
207
|
|
|
* @return string |
208
|
|
|
*/ |
209
|
|
|
private function assembleUri($path, array $params = []) |
210
|
|
|
{ |
211
|
|
|
$uri = $path; |
212
|
|
|
if ($params) { |
|
|
|
|
213
|
|
|
$uri = $path . '?' . http_build_query($params); |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
return $uri; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* @param string $fullPath |
221
|
|
|
* @param string $method |
222
|
|
|
* |
223
|
|
|
* @return object|null |
224
|
|
|
* @throws ApiResponseErrorException |
225
|
|
|
*/ |
226
|
|
|
private function getJsonForLastRequest($fullPath, $method) |
227
|
|
|
{ |
228
|
|
|
$method = strtolower($method); |
229
|
|
|
$response = $this->client->getResponse(); |
230
|
|
|
$json = $response->getContent(); |
231
|
|
|
$data = json_decode($json); |
232
|
|
|
|
233
|
|
|
if ($response->getStatusCode() !== 204) { |
234
|
|
|
static $errors = [ |
235
|
|
|
JSON_ERROR_NONE => 'No error', |
236
|
|
|
JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', |
237
|
|
|
JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)', |
238
|
|
|
JSON_ERROR_CTRL_CHAR => 'Control character error, possibly incorrectly encoded', |
239
|
|
|
JSON_ERROR_SYNTAX => 'Syntax error', |
240
|
|
|
JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded' |
241
|
|
|
]; |
242
|
|
|
$error = json_last_error(); |
243
|
|
|
$jsonErrorMessage = isset($errors[$error]) ? $errors[$error] : 'Unknown error'; |
244
|
|
|
$this->assertSame( |
245
|
|
|
JSON_ERROR_NONE, |
246
|
|
|
json_last_error(), |
247
|
|
|
"Not valid JSON: " . $jsonErrorMessage . "(" . var_export($json, true) . ")" |
248
|
|
|
); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
if (substr($response->getStatusCode(), 0, 1) != '2') { |
252
|
|
|
if ($this->getValidateErrorResponse()) { |
253
|
|
|
$this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data); |
254
|
|
|
} |
255
|
|
|
// This throws an exception so that tests can catch it when it is expected |
256
|
|
|
throw new ApiResponseErrorException($json, $data, $response->getStatusCode()); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
$this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data); |
260
|
|
|
|
261
|
|
|
return $data; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* @param $code |
266
|
|
|
* @param Response $response |
267
|
|
|
* @param string $method |
268
|
|
|
* @param string $fullPath |
269
|
|
|
* @param mixed $data |
270
|
|
|
*/ |
271
|
|
|
private function validateResponse($code, $response, $method, $fullPath, $data) |
272
|
|
|
{ |
273
|
|
|
$request = $this->client->getRequest(); |
274
|
|
|
if (!self::$schemaManager->hasPath(['paths', $request->get('_swagger_path'), $method, 'responses', $code])) { |
275
|
|
|
$statusClass = (int)substr((string)$code, 0, 1); |
276
|
|
|
if (in_array($statusClass, [4, 5])) { |
277
|
|
|
return; |
278
|
|
|
} |
279
|
|
|
throw new \UnexpectedValueException( |
280
|
|
|
"There is no $code response definition for {$request->get('_swagger_path')}:$method. " |
281
|
|
|
); |
282
|
|
|
} |
283
|
|
|
$headers = []; |
284
|
|
|
|
285
|
|
|
foreach ($response->headers->all() as $key => $values) { |
286
|
|
|
$headers[str_replace(' ', '-', ucwords(str_replace('-', ' ', $key)))] = $values[0]; |
287
|
|
|
} |
288
|
|
|
try { |
289
|
|
|
try { |
290
|
|
|
$this->assertResponseMediaTypeMatch( |
291
|
|
|
$response->headers->get('Content-Type'), |
292
|
|
|
self::$schemaManager, |
293
|
|
|
$fullPath, |
294
|
|
|
$method |
295
|
|
|
); |
296
|
|
|
} catch (\InvalidArgumentException $e) { |
297
|
|
|
// Not required, so skip if not found |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
$this->assertResponseHeadersMatch($headers, self::$schemaManager, $fullPath, $method, $code); |
301
|
|
|
$this->assertResponseBodyMatch($data, self::$schemaManager, $fullPath, $method, $code); |
302
|
|
|
} catch (\UnexpectedValueException $e) { |
303
|
|
|
$statusClass = (int)(string)$code[0]; |
304
|
|
|
if (in_array($statusClass, [4, 5])) { |
305
|
|
|
return; |
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* @param mixed $expected |
312
|
|
|
* @param mixed $actual |
313
|
|
|
* @param string $message |
314
|
|
|
* |
315
|
|
|
* @return mixed |
316
|
|
|
*/ |
317
|
|
|
public abstract function assertSame($expected, $actual, $message = ''); |
318
|
|
|
} |
319
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.