Completed
Pull Request — master (#52)
by John
07:37
created

ApiTestCase   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 25
c 3
b 0
f 0
lcom 1
cbo 10
dl 0
loc 241
rs 10

11 Methods

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