Completed
Pull Request — master (#58)
by John
09:52
created

ApiTestCase::assembleUri()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 2
eloc 5
nc 2
nop 2
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 $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->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...
87
88
        parent::setUp();
89
    }
90
91
    /**
92
     * Creates a Client.
93
     *
94
     * @param array $options An array of options to pass to the createKernel class
95
     * @param array $server  An array of server parameters
96
     *
97
     * @return Client A Client instance
98
     */
99
    protected static function createClient(array $options = array(), array $server = array())
100
    {
101
        static::bootKernel($options);
102
103
        $client = static::$kernel->getContainer()->get('swagger.test.client');
104
        $client->setServerParameters($server);
105
106
        return $client;
107
    }
108
109
110
    /**
111
     * @param string $path
112
     * @param array  $params
113
     *
114
     * @return object
115
     * @throws ApiResponseErrorException
116
     */
117
    protected function get($path, array $params = [])
118
    {
119
        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...
120
    }
121
122
    /**
123
     * @param string $path
124
     * @param array  $params
125
     *
126
     * @return object
127
     * @throws ApiResponseErrorException
128
     */
129
    protected function delete($path, array $params = [])
130
    {
131
        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...
132
    }
133
134
    /**
135
     * @param string $path
136
     * @param array  $content
137
     * @param array  $params
138
     *
139
     * @return object
140
     * @throws ApiResponseErrorException
141
     */
142
    protected function patch($path, array $content, array $params = [])
143
    {
144
        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...
145
    }
146
147
    /**
148
     * @param string $path
149
     * @param array  $content
150
     * @param array  $params
151
     *
152
     * @return object
153
     * @throws ApiResponseErrorException
154
     */
155
    protected function post($path, array $content, array $params = [])
156
    {
157
        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...
158
    }
159
160
    /**
161
     * @param string $path
162
     * @param array  $content
163
     * @param array  $params
164
     *
165
     * @return object
166
     * @throws ApiResponseErrorException
167
     */
168
    protected function put($path, array $content, array $params = [])
169
    {
170
        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...
171
    }
172
173
    /**
174
     * @param string     $path
175
     * @param array      $method
176
     * @param array      $params
177
     * @param array|null $content
178
     *
179
     * @return object
180
     * @throws ApiResponseErrorException
181
     */
182
    protected function sendRequest($path, $method, array $params = [], array $content = null)
183
    {
184
        $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...
185
        $defaults = 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...
186
        $request->setServer(array_merge($defaults ?: [], ['CONTENT_TYPE' => 'application/json']));
187
        if ($content !== null) {
188
            $request->setContent(json_encode($content));
189
        }
190
        $this->client->requestFromRequest($request);
191
192
        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...
193
    }
194
195
196
    /**
197
     * @param string $path
198
     * @param array  $params
199
     *
200
     * @return string
201
     */
202
    private function assembleUri($path, array $params = [])
203
    {
204
        $uri = $path;
205
        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...
206
            $uri = $path . '?' . http_build_query($params);
207
        }
208
209
        return $uri;
210
    }
211
212
    /**
213
     * @param string $fullPath
214
     * @param string $method
215
     *
216
     * @return object|null
217
     * @throws ApiResponseErrorException
218
     */
219
    private function getJsonForLastRequest($fullPath, $method)
220
    {
221
        $method = strtolower($method);
222
        $response = $this->client->getResponse();
223
        $json = $response->getContent();
224
        $data = json_decode($json);
225
226
        if ($response->getStatusCode() !== 204) {
227
            static $errors = [
228
                JSON_ERROR_NONE           => 'No error',
229
                JSON_ERROR_DEPTH          => 'Maximum stack depth exceeded',
230
                JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
231
                JSON_ERROR_CTRL_CHAR      => 'Control character error, possibly incorrectly encoded',
232
                JSON_ERROR_SYNTAX         => 'Syntax error',
233
                JSON_ERROR_UTF8           => 'Malformed UTF-8 characters, possibly incorrectly encoded'
234
            ];
235
            $error = json_last_error();
236
            $jsonErrorMessage = isset($errors[$error]) ? $errors[$error] : 'Unknown error';
237
            $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...
238
                JSON_ERROR_NONE,
239
                json_last_error(),
240
                "Not valid JSON: " . $jsonErrorMessage . "(" . var_export($json, true) . ")"
241
            );
242
        }
243
244
        if (substr($response->getStatusCode(), 0, 1) != '2') {
245
            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...
246
                $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
247
            }
248
            // This throws an exception so that tests can catch it when it is expected
249
            throw new ApiResponseErrorException($json, $data, $response->getStatusCode());
250
        }
251
252
        $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
253
254
        return $data;
255
    }
256
257
    /**
258
     * @param          $code
259
     * @param Response $response
260
     * @param string   $method
261
     * @param string   $fullPath
262
     * @param mixed    $data
263
     */
264
    private function validateResponse($code, $response, $method, $fullPath, $data)
265
    {
266
        $request = $this->client->getRequest();
267
        if (!self::$schemaManager->hasPath(['paths', $request->get('_swagger_path'), $method, 'responses', $code])) {
268
            $statusClass = (int)substr((string)$code, 0, 1);
269
            if (in_array($statusClass, [4, 5])) {
270
                return;
271
            }
272
            throw new \UnexpectedValueException(
273
                "There is no $code response definition for {$request->get('_swagger_path')}:$method. "
274
            );
275
        }
276
        $headers = [];
277
278
        foreach ($response->headers->all() as $key => $values) {
279
            $headers[str_replace(' ', '-', ucwords(str_replace('-', ' ', $key)))] = $values[0];
280
        }
281
        try {
282
            $this->assertResponseHeadersMatch($headers, self::$schemaManager, $fullPath, $method, $code);
283
            $this->assertResponseBodyMatch($data, self::$schemaManager, $fullPath, $method, $code);
284
        } catch (\UnexpectedValueException $e) {
285
            $statusClass = (int)(string)$code[0];
286
            if (in_array($statusClass, [4, 5])) {
287
                return;
288
            }
289
        }
290
    }
291
}
292