Completed
Pull Request — master (#64)
by John
02:51
created

ApiTestCase::getValidateErrorResponse()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 2
eloc 2
nc 2
nop 0
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
trait ApiTestCase
26
{
27
    use AssertsTrait;
28
29
    /**
30
     * @var SchemaManager
31
     */
32
    protected static $schemaManager;
33
34
    /**
35
     * @var SwaggerDocument
36
     */
37
    protected static $document;
38
39
    /**
40
     * @var ApiTestClient
41
     */
42
    protected $client;
43
44
    /**
45
     * PHPUnit cannot add this to code coverage
46
     *
47
     * @codeCoverageIgnore
48
     *
49
     * @param string $swaggerPath
50
     *
51
     * @throws \InvalidArgumentException
52
     * @throws \org\bovigo\vfs\vfsStreamException
53
     */
54
    public static function initSchemaManager($swaggerPath)
55
    {
56
        $validator = new Validator();
57
        $validator->check(
58
            json_decode(json_encode(Yaml::parse(file_get_contents($swaggerPath)))),
59
            json_decode(file_get_contents(__DIR__ . '/../../assets/swagger-schema.json'))
60
        );
61
62
        if (!$validator->isValid()) {
63
            throw new \InvalidArgumentException(
64
                "Swagger '$swaggerPath' not valid"
65
            );
66
        }
67
68
        vfsStreamWrapper::register();
69
        vfsStreamWrapper::setRoot(new vfsStreamDirectory('root'));
70
71
        file_put_contents(
72
            vfsStream::url('root') . '/swagger.json',
73
            json_encode(Yaml::parse(file_get_contents($swaggerPath)))
74
        );
75
76
        self::$schemaManager = new SchemaManager(vfsStream::url('root') . '/swagger.json');
77
        $repository = new DocumentRepository(dirname($swaggerPath));
78
        self::$document = $repository->get(basename($swaggerPath));
79
    }
80
81
    /**
82
     * Create a client, booting the kernel using SYMFONY_ENV = $this->env
83
     */
84
    protected function setUp()
85
    {
86
        $this->client = static::createClient(['environment' => $this->getEnv(), 'debug' => true]);
87
88
        parent::setUp();
89
    }
90
91
    /**
92
     * @return array
93
     */
94
    protected function getDefaultServerVars()
95
    {
96
        return isset($this->defaultServerVars) ? $this->defaultServerVars : [];
0 ignored issues
show
Bug introduced by
The property defaultServerVars does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
97
    }
98
99
    /**
100
     * @return array
101
     */
102
    protected function getEnv()
103
    {
104
        return isset($this->env) ? $this->env : 'test';
0 ignored issues
show
Bug introduced by
The property env does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
105
    }
106
107
    /**
108
     * @return bool
109
     */
110
    protected function getValidateErrorResponse()
111
    {
112
        return isset($this->validateErrorResponse) ? $this->validateErrorResponse : false;
0 ignored issues
show
Bug introduced by
The property validateErrorResponse does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
113
    }
114
115
    /**
116
     * @param string $path
117
     * @param array  $params
118
     *
119
     * @return object
120
     * @throws ApiResponseErrorException
121
     */
122
    protected function get($path, array $params = [])
123
    {
124
        return $this->sendRequest($path, 'GET', $params);
0 ignored issues
show
Documentation introduced by
'GET' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
125
    }
126
127
    /**
128
     * @param string $path
129
     * @param array  $params
130
     *
131
     * @return object
132
     * @throws ApiResponseErrorException
133
     */
134
    protected function delete($path, array $params = [])
135
    {
136
        return $this->sendRequest($path, 'DELETE', $params);
0 ignored issues
show
Documentation introduced by
'DELETE' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
137
    }
138
139
    /**
140
     * @param string $path
141
     * @param array  $content
142
     * @param array  $params
143
     *
144
     * @return object
145
     * @throws ApiResponseErrorException
146
     */
147
    protected function patch($path, array $content, array $params = [])
148
    {
149
        return $this->sendRequest($path, 'PATCH', $params, $content);
0 ignored issues
show
Documentation introduced by
'PATCH' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
150
    }
151
152
    /**
153
     * @param string $path
154
     * @param array  $content
155
     * @param array  $params
156
     *
157
     * @return object
158
     * @throws ApiResponseErrorException
159
     */
160
    protected function post($path, array $content, array $params = [])
161
    {
162
        return $this->sendRequest($path, 'POST', $params, $content);
0 ignored issues
show
Documentation introduced by
'POST' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
163
    }
164
165
    /**
166
     * @param string $path
167
     * @param array  $content
168
     * @param array  $params
169
     *
170
     * @return object
171
     * @throws ApiResponseErrorException
172
     */
173
    protected function put($path, array $content, array $params = [])
174
    {
175
        return $this->sendRequest($path, 'PUT', $params, $content);
0 ignored issues
show
Documentation introduced by
'PUT' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
176
    }
177
178
    /**
179
     * @param string     $path
180
     * @param array      $method
181
     * @param array      $params
182
     * @param array|null $content
183
     *
184
     * @return object
185
     * @throws ApiResponseErrorException
186
     */
187
    protected function sendRequest($path, $method, array $params = [], array $content = null)
188
    {
189
        $request = new ApiRequest($this->assembleUri($path, $params), $method);
0 ignored issues
show
Documentation introduced by
$method is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
190
        $request->setServer(array_merge(['CONTENT_TYPE' => 'application/json'], $this->getDefaultServerVars()));
191
        if ($content !== null) {
192
            $request->setContent(json_encode($content));
193
        }
194
        $this->client->requestFromRequest($request);
195
196
        return $this->getJsonForLastRequest($path, $method);
0 ignored issues
show
Documentation introduced by
$method is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
197
    }
198
199
    /**
200
     * @param string $path
201
     * @param array  $params
202
     *
203
     * @return string
204
     */
205
    private function assembleUri($path, array $params = [])
206
    {
207
        $uri = $path;
208
        if ($params) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $params of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
209
            $uri = $path . '?' . http_build_query($params);
210
        }
211
212
        return $uri;
213
    }
214
215
    /**
216
     * @param string $fullPath
217
     * @param string $method
218
     *
219
     * @return object|null
220
     * @throws ApiResponseErrorException
221
     */
222
    private function getJsonForLastRequest($fullPath, $method)
223
    {
224
        $method = strtolower($method);
225
        $response = $this->client->getResponse();
226
        $json = $response->getContent();
227
        $data = json_decode($json);
228
229
        if ($response->getStatusCode() !== 204) {
230
            static $errors = [
231
                JSON_ERROR_NONE           => 'No error',
232
                JSON_ERROR_DEPTH          => 'Maximum stack depth exceeded',
233
                JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
234
                JSON_ERROR_CTRL_CHAR      => 'Control character error, possibly incorrectly encoded',
235
                JSON_ERROR_SYNTAX         => 'Syntax error',
236
                JSON_ERROR_UTF8           => 'Malformed UTF-8 characters, possibly incorrectly encoded'
237
            ];
238
            $error = json_last_error();
239
            $jsonErrorMessage = isset($errors[$error]) ? $errors[$error] : 'Unknown error';
240
            $this->assertSame(
241
                JSON_ERROR_NONE,
242
                json_last_error(),
243
                "Not valid JSON: " . $jsonErrorMessage . "(" . var_export($json, true) . ")"
244
            );
245
        }
246
247
        if (substr($response->getStatusCode(), 0, 1) != '2') {
248
            if ($this->getValidateErrorResponse()) {
249
                $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
250
            }
251
            // This throws an exception so that tests can catch it when it is expected
252
            throw new ApiResponseErrorException($json, $data, $response->getStatusCode());
253
        }
254
255
        $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
256
257
        return $data;
258
    }
259
260
    /**
261
     * @param          $code
262
     * @param Response $response
263
     * @param string   $method
264
     * @param string   $fullPath
265
     * @param mixed    $data
266
     */
267
    private function validateResponse($code, $response, $method, $fullPath, $data)
268
    {
269
        $request = $this->client->getRequest();
270
        if (!self::$schemaManager->hasPath(['paths', $request->get('_swagger_path'), $method, 'responses', $code])) {
271
            $statusClass = (int)substr((string)$code, 0, 1);
272
            if (in_array($statusClass, [4, 5])) {
273
                return;
274
            }
275
            throw new \UnexpectedValueException(
276
                "There is no $code response definition for {$request->get('_swagger_path')}:$method. "
277
            );
278
        }
279
        $headers = [];
280
281
        foreach ($response->headers->all() as $key => $values) {
282
            $headers[str_replace(' ', '-', ucwords(str_replace('-', ' ', $key)))] = $values[0];
283
        }
284
        try {
285
            try {
286
                $this->assertResponseMediaTypeMatch(
287
                    $response->headers->get('Content-Type'),
288
                    self::$schemaManager,
289
                    $fullPath,
290
                    $method
291
                );
292
            } catch (\InvalidArgumentException $e) {
293
                // Not required, so skip if not found
294
            }
295
296
            $this->assertResponseHeadersMatch($headers, self::$schemaManager, $fullPath, $method, $code);
297
            $this->assertResponseBodyMatch($data, self::$schemaManager, $fullPath, $method, $code);
298
        } catch (\UnexpectedValueException $e) {
299
            $statusClass = (int)(string)$code[0];
300
            if (in_array($statusClass, [4, 5])) {
301
                return;
302
            }
303
        }
304
    }
305
306
    /**
307
     * @param mixed  $expected
308
     * @param mixed  $actual
309
     * @param string $message
310
     *
311
     * @return mixed
312
     */
313
    public abstract function assertSame($expected, $actual, $message = '');
314
}
315