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

ApiTestCase   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 270
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 27
c 6
b 0
f 0
lcom 1
cbo 10
dl 0
loc 270
rs 10

12 Methods

Rating   Name   Duplication   Size   Complexity  
B initSchemaManager() 0 26 2
A setUp() 0 6 2
A getDefaultServerVars() 0 4 1
A get() 0 4 1
A delete() 0 4 1
A patch() 0 4 1
A post() 0 4 1
A put() 0 4 1
A sendRequest() 0 11 2
A assembleUri() 0 9 2
B getJsonForLastRequest() 0 37 6
C validateResponse() 0 38 7
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
     * @var array
46
     */
47
    protected $defaultServerVars = [];
48
49
    /**
50
     * PHPUnit cannot add this to code coverage
51
     *
52
     * @codeCoverageIgnore
53
     *
54
     * @param $swaggerPath
55
     *
56
     * @throws \InvalidArgumentException
57
     * @throws \org\bovigo\vfs\vfsStreamException
58
     */
59
    public static function initSchemaManager($swaggerPath)
60
    {
61
        $validator = new Validator();
62
        $validator->check(
63
            json_decode(json_encode(Yaml::parse(file_get_contents($swaggerPath)))),
64
            json_decode(file_get_contents(__DIR__ . '/../../assets/swagger-schema.json'))
65
        );
66
67
        if (!$validator->isValid()) {
68
            throw new \InvalidArgumentException(
69
                "Swagger '$swaggerPath' not valid"
70
            );
71
        }
72
73
        vfsStreamWrapper::register();
74
        vfsStreamWrapper::setRoot(new vfsStreamDirectory('root'));
75
76
        file_put_contents(
77
            vfsStream::url('root') . '/swagger.json',
78
            json_encode(Yaml::parse(file_get_contents($swaggerPath)))
79
        );
80
81
        self::$schemaManager = new SchemaManager(vfsStream::url('root') . '/swagger.json');
82
        $repository = new DocumentRepository(dirname($swaggerPath));
83
        self::$document = $repository->get(basename($swaggerPath));
84
    }
85
86
    /**
87
     * Create a client, booting the kernel using SYMFONY_ENV = $this->env
88
     */
89
    protected function setUp()
90
    {
91
        $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...
92
93
        parent::setUp();
94
    }
95
96
    /**
97
     * @return array
98
     */
99
    protected function getDefaultServerVars()
100
    {
101
        return $this->defaultServerVars;
102
    }
103
104
    /**
105
     * @param string $path
106
     * @param array  $params
107
     *
108
     * @return object
109
     * @throws ApiResponseErrorException
110
     */
111
    protected function get($path, array $params = [])
112
    {
113
        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...
114
    }
115
116
    /**
117
     * @param string $path
118
     * @param array  $params
119
     *
120
     * @return object
121
     * @throws ApiResponseErrorException
122
     */
123
    protected function delete($path, array $params = [])
124
    {
125
        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...
126
    }
127
128
    /**
129
     * @param string $path
130
     * @param array  $content
131
     * @param array  $params
132
     *
133
     * @return object
134
     * @throws ApiResponseErrorException
135
     */
136
    protected function patch($path, array $content, array $params = [])
137
    {
138
        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...
139
    }
140
141
    /**
142
     * @param string $path
143
     * @param array  $content
144
     * @param array  $params
145
     *
146
     * @return object
147
     * @throws ApiResponseErrorException
148
     */
149
    protected function post($path, array $content, array $params = [])
150
    {
151
        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...
152
    }
153
154
    /**
155
     * @param string $path
156
     * @param array  $content
157
     * @param array  $params
158
     *
159
     * @return object
160
     * @throws ApiResponseErrorException
161
     */
162
    protected function put($path, array $content, array $params = [])
163
    {
164
        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...
165
    }
166
167
    /**
168
     * @param string     $path
169
     * @param array      $method
170
     * @param array      $params
171
     * @param array|null $content
172
     *
173
     * @return object
174
     * @throws ApiResponseErrorException
175
     */
176
    protected function sendRequest($path, $method, array $params = [], array $content = null)
177
    {
178
        $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...
179
        $request->setServer(array_merge(['CONTENT_TYPE' => 'application/json'], $this->getDefaultServerVars()));
180
        if ($content !== null) {
181
            $request->setContent(json_encode($content));
182
        }
183
        $this->client->requestFromRequest($request);
184
185
        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...
186
    }
187
188
    /**
189
     * @param string $path
190
     * @param array  $params
191
     *
192
     * @return string
193
     */
194
    private function assembleUri($path, array $params = [])
195
    {
196
        $uri = $path;
197
        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...
198
            $uri = $path . '?' . http_build_query($params);
199
        }
200
201
        return $uri;
202
    }
203
204
    /**
205
     * @param string $fullPath
206
     * @param string $method
207
     *
208
     * @return object|null
209
     * @throws ApiResponseErrorException
210
     */
211
    private function getJsonForLastRequest($fullPath, $method)
212
    {
213
        $method = strtolower($method);
214
        $response = $this->client->getResponse();
215
        $json = $response->getContent();
216
        $data = json_decode($json);
217
218
        if ($response->getStatusCode() !== 204) {
219
            static $errors = [
220
                JSON_ERROR_NONE           => 'No error',
221
                JSON_ERROR_DEPTH          => 'Maximum stack depth exceeded',
222
                JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
223
                JSON_ERROR_CTRL_CHAR      => 'Control character error, possibly incorrectly encoded',
224
                JSON_ERROR_SYNTAX         => 'Syntax error',
225
                JSON_ERROR_UTF8           => 'Malformed UTF-8 characters, possibly incorrectly encoded'
226
            ];
227
            $error = json_last_error();
228
            $jsonErrorMessage = isset($errors[$error]) ? $errors[$error] : 'Unknown error';
229
            $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...
230
                JSON_ERROR_NONE,
231
                json_last_error(),
232
                "Not valid JSON: " . $jsonErrorMessage . "(" . var_export($json, true) . ")"
233
            );
234
        }
235
236
        if (substr($response->getStatusCode(), 0, 1) != '2') {
237
            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...
238
                $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
239
            }
240
            // This throws an exception so that tests can catch it when it is expected
241
            throw new ApiResponseErrorException($json, $data, $response->getStatusCode());
242
        }
243
244
        $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
245
246
        return $data;
247
    }
248
249
    /**
250
     * @param          $code
251
     * @param Response $response
252
     * @param string   $method
253
     * @param string   $fullPath
254
     * @param mixed    $data
255
     */
256
    private function validateResponse($code, $response, $method, $fullPath, $data)
257
    {
258
        $request = $this->client->getRequest();
259
        if (!self::$schemaManager->hasPath(['paths', $request->get('_swagger_path'), $method, 'responses', $code])) {
260
            $statusClass = (int)substr((string)$code, 0, 1);
261
            if (in_array($statusClass, [4, 5])) {
262
                return;
263
            }
264
            throw new \UnexpectedValueException(
265
                "There is no $code response definition for {$request->get('_swagger_path')}:$method. "
266
            );
267
        }
268
        $headers = [];
269
270
        foreach ($response->headers->all() as $key => $values) {
271
            $headers[str_replace(' ', '-', ucwords(str_replace('-', ' ', $key)))] = $values[0];
272
        }
273
        try {
274
            try {
275
                $this->assertResponseMediaTypeMatch(
276
                    $response->headers->get('Content-Type'),
277
                    self::$schemaManager,
278
                    $fullPath,
279
                    $method
280
                );
281
            } catch (\InvalidArgumentException $e) {
282
                // Not required, so skip if not found
283
            }
284
285
            $this->assertResponseHeadersMatch($headers, self::$schemaManager, $fullPath, $method, $code);
286
            $this->assertResponseBodyMatch($data, self::$schemaManager, $fullPath, $method, $code);
287
        } catch (\UnexpectedValueException $e) {
288
            $statusClass = (int)(string)$code[0];
289
            if (in_array($statusClass, [4, 5])) {
290
                return;
291
            }
292
        }
293
    }
294
}
295