Completed
Pull Request — master (#58)
by John
02:57
created

ApiTestCase::setUp()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 2
eloc 3
nc 1
nop 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 21 and the first side effect is on line 2.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
declare(strict_types=1);
3
/*
4
 * This file is part of the KleijnWeb\SwaggerBundle package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace KleijnWeb\SwaggerBundle\Test;
11
12
use FR3D\SwaggerAssertions\{PhpUnit\AssertsTrait, SchemaManager};
13
use org\bovigo\vfs\{vfsStream, vfsStreamDirectory, vfsStreamWrapper};
14
use Symfony\Component\{Yaml\Yaml, HttpFoundation\Response};
15
use KleijnWeb\SwaggerBundle\Document\{DocumentRepository, SwaggerDocument};
16
use JsonSchema\Validator;
17
18
/**
19
 * @author John Kleijn <[email protected]>
20
 */
21
trait ApiTestCase
22
{
23
    use AssertsTrait;
24
25
    /**
26
     * @var SchemaManager
27
     */
28
    protected static $schemaManager;
29
30
    /**
31
     * @var SwaggerDocument
32
     */
33
    protected static $document;
34
35
    /**
36
     * @var ApiTestClient
37
     */
38
    protected $client;
39
40
    /**
41
     * PHPUnit cannot add this to code coverage
42
     *
43
     * @codeCoverageIgnore
44
     *
45
     * @param $swaggerPath
46
     *
47
     * @throws \InvalidArgumentException
48
     * @throws \org\bovigo\vfs\vfsStreamException
49
     */
50
    public static function initSchemaManager($swaggerPath)
51
    {
52
        $validator = new Validator();
53
        $validator->check(
54
            json_decode(json_encode(Yaml::parse(file_get_contents($swaggerPath)))),
55
            json_decode(file_get_contents(__DIR__ . '/../../assets/swagger-schema.json'))
56
        );
57
58
        if (!$validator->isValid()) {
59
            throw new \InvalidArgumentException(
60
                "Swagger '$swaggerPath' not valid"
61
            );
62
        }
63
64
        vfsStreamWrapper::register();
65
        vfsStreamWrapper::setRoot(new vfsStreamDirectory('root'));
66
67
        file_put_contents(
68
            vfsStream::url('root') . '/swagger.json',
69
            json_encode(Yaml::parse(file_get_contents($swaggerPath)))
70
        );
71
72
        self::$schemaManager = new SchemaManager(vfsStream::url('root') . '/swagger.json');
0 ignored issues
show
Documentation Bug introduced by
It seems like new \FR3D\SwaggerAsserti...ot') . '/swagger.json') of type object<FR3D\SwaggerAssertions\SchemaManager> is incompatible with the declared type object<SchemaManager> of property $schemaManager.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
73
        $repository = new DocumentRepository(dirname($swaggerPath));
74
        self::$document = $repository->get(basename($swaggerPath));
0 ignored issues
show
Documentation Bug introduced by
It seems like $repository->get(basename($swaggerPath)) of type object<KleijnWeb\Swagger...cument\SwaggerDocument> is incompatible with the declared type object<SwaggerDocument> of property $document.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
75
    }
76
77
    /**
78
     * Create a client, booting the kernel using SYMFONY_ENV = $this->env
79
     */
80
    protected function setUp()
81
    {
82
        $this->client = static::createClient(['environment' => $this->env ?: 'test', 'debug' => true]);
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...
Documentation Bug introduced by
It seems like static::createClient(arr...est', 'debug' => true)) of type object<KleijnWeb\SwaggerBundle\Test\Client> is incompatible with the declared type object<KleijnWeb\Swagger...dle\Test\ApiTestClient> of property $client.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
83
84
        parent::setUp();
85
    }
86
87
    /**
88
     * Creates a Client.
89
     *
90
     * @param array $options An array of options to pass to the createKernel class
91
     * @param array $server  An array of server parameters
92
     *
93
     * @return Client A Client instance
94
     */
95
    protected static function createClient(array $options = array(), array $server = array())
96
    {
97
        static::bootKernel($options);
98
99
        $client = static::$kernel->getContainer()->get('swagger.test.client');
100
        $client->setServerParameters($server);
101
102
        return $client;
103
    }
104
105
106
    /**
107
     * @param string $path
108
     * @param array  $params
109
     *
110
     * @return \stdClass
111
     * @throws ApiResponseErrorException
112
     */
113
    protected function get($path, array $params = [])
114
    {
115
        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...
116
    }
117
118
    /**
119
     * @param string $path
120
     * @param array  $params
121
     *
122
     * @return \stdClass
123
     * @throws ApiResponseErrorException
124
     */
125
    protected function delete($path, array $params = [])
126
    {
127
        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...
128
    }
129
130
    /**
131
     * @param string $path
132
     * @param array  $content
133
     * @param array  $params
134
     *
135
     * @return \stdClass
136
     * @throws ApiResponseErrorException
137
     */
138
    protected function patch($path, array $content, array $params = [])
139
    {
140
        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...
141
    }
142
143
    /**
144
     * @param string $path
145
     * @param array  $content
146
     * @param array  $params
147
     *
148
     * @return \stdClass
149
     * @throws ApiResponseErrorException
150
     */
151
    protected function post($path, array $content, array $params = [])
152
    {
153
        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...
154
    }
155
156
    /**
157
     * @param string $path
158
     * @param array  $content
159
     * @param array  $params
160
     *
161
     * @return \stdClass
162
     * @throws ApiResponseErrorException
163
     */
164
    protected function put($path, array $content, array $params = [])
165
    {
166
        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...
167
    }
168
169
    /**
170
     * @param string     $path
171
     * @param array      $method
172
     * @param array      $params
173
     * @param array|null $content
174
     *
175
     * @return \stdClass
176
     * @throws ApiResponseErrorException
177
     */
178
    protected function sendRequest($path, $method, array $params = [], array $content = null)
179
    {
180
        $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...
181
        $defaults = $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...
182
        $request->setServer(array_merge($defaults ?: [], ['CONTENT_TYPE' => 'application/json']));
183
        if ($content !== null) {
184
            $request->setContent(json_encode($content));
185
        }
186
        $this->client->requestFromRequest($request);
187
188
        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...
189
    }
190
191
192
    /**
193
     * @param string $path
194
     * @param array  $params
195
     *
196
     * @return string
197
     */
198
    private function assembleUri($path, array $params = [])
199
    {
200
        $uri = $path;
201
        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...
202
            $uri = $path . '?' . http_build_query($params);
203
        }
204
205
        return $uri;
206
    }
207
208
    /**
209
     * @param string $fullPath
210
     * @param string $method
211
     *
212
     * @return \stdClass|null
213
     * @throws ApiResponseErrorException
214
     */
215
    private function getJsonForLastRequest($fullPath, $method)
216
    {
217
        $method = strtolower($method);
218
        $response = $this->client->getResponse();
219
        $json = $response->getContent();
220
        $data = json_decode($json);
221
222
        if ($response->getStatusCode() !== 204) {
223
            static $errors = [
224
                JSON_ERROR_NONE           => 'No error',
225
                JSON_ERROR_DEPTH          => 'Maximum stack depth exceeded',
226
                JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
227
                JSON_ERROR_CTRL_CHAR      => 'Control character error, possibly incorrectly encoded',
228
                JSON_ERROR_SYNTAX         => 'Syntax error',
229
                JSON_ERROR_UTF8           => 'Malformed UTF-8 characters, possibly incorrectly encoded'
230
            ];
231
            $error = json_last_error();
232
            $jsonErrorMessage = isset($errors[$error]) ? $errors[$error] : 'Unknown error';
233
            $this->assertSame(
0 ignored issues
show
Bug introduced by
It seems like assertSame() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
234
                JSON_ERROR_NONE,
235
                json_last_error(),
236
                "Not valid JSON: " . $jsonErrorMessage . "(" . var_export($json, true) . ")"
237
            );
238
        }
239
240
        if (substr((string)$response->getStatusCode(), 0, 1) != '2') {
241
            if (!isset($this->validateErrorResponse) || $this->validateErrorResponse) {
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...
242
                $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
243
            }
244
            // This throws an exception so that tests can catch it when it is expected
245
            throw new ApiResponseErrorException($json, $data, $response->getStatusCode());
246
        }
247
248
        $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
249
250
        return $data;
251
    }
252
253
    /**
254
     * @param          $code
255
     * @param Response $response
256
     * @param string   $method
257
     * @param string   $fullPath
258
     * @param mixed    $data
259
     */
260
    private function validateResponse($code, $response, $method, $fullPath, $data)
261
    {
262
        $request = $this->client->getRequest();
263
        if (!self::$schemaManager->hasPath(['paths', $request->get('_swagger_path'), $method, 'responses', $code])) {
264
            $statusClass = (int)substr((string)$code, 0, 1);
265
            if (in_array($statusClass, [4, 5])) {
266
                return;
267
            }
268
            throw new \UnexpectedValueException(
269
                "There is no $code response definition for {$request->get('_swagger_path')}:$method. "
270
            );
271
        }
272
        $headers = [];
273
274
        foreach ($response->headers->all() as $key => $values) {
275
            $headers[str_replace(' ', '-', ucwords(str_replace('-', ' ', $key)))] = $values[0];
276
        }
277
        try {
278
            $this->assertResponseHeadersMatch($headers, self::$schemaManager, $fullPath, $method, $code);
279
            $this->assertResponseBodyMatch($data, self::$schemaManager, $fullPath, $method, $code);
280
        } catch (\UnexpectedValueException $e) {
281
            $statusClass = (int)(string)$code[0];
282
            if (in_array($statusClass, [4, 5])) {
283
                return;
284
            }
285
        }
286
    }
287
}
288