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

ApiTestCase::put()   A

Complexity

Conditions 1
Paths 1

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 1
eloc 2
nc 1
nop 3
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 26 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;
13
use FR3D\SwaggerAssertions\SchemaManager;
14
use JsonSchema\Validator;
15
use KleijnWeb\SwaggerBundle\Document\DocumentRepository;
16
use KleijnWeb\SwaggerBundle\Document\SwaggerDocument;
17
use org\bovigo\vfs\vfsStream;
18
use org\bovigo\vfs\vfsStreamDirectory;
19
use org\bovigo\vfs\vfsStreamWrapper;
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(string $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...
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...
88
89
        parent::setUp();
90
    }
91
92
    /**
93
     * Creates a Client.
94
     *
95
     * @param array $options An array of options to pass to the createKernel class
96
     * @param array $server  An array of server parameters
97
     *
98
     * @return Client A Client instance
99
     */
100
    protected static function createClient(array $options = [], array $server = [])
101
    {
102
        static::bootKernel($options);
103
104
        $client = static::$kernel->getContainer()->get('swagger.test.client');
105
        $client->setServerParameters($server);
106
107
        return $client;
108
    }
109
110
111
    /**
112
     * @param string $path
113
     * @param array  $params
114
     *
115
     * @return \stdClass
116
     * @throws ApiResponseErrorException
117
     */
118
    protected function get(string $path, array $params = [])
119
    {
120
        return $this->sendRequest($path, 'GET', $params);
121
    }
122
123
    /**
124
     * @param string $path
125
     * @param array  $params
126
     *
127
     * @return \stdClass
128
     * @throws ApiResponseErrorException
129
     */
130
    protected function delete(string $path, array $params = [])
131
    {
132
        return $this->sendRequest($path, 'DELETE', $params);
133
    }
134
135
    /**
136
     * @param string $path
137
     * @param array  $content
138
     * @param array  $params
139
     *
140
     * @return \stdClass
141
     * @throws ApiResponseErrorException
142
     */
143
    protected function patch(string $path, array $content, array $params = [])
144
    {
145
        return $this->sendRequest($path, 'PATCH', $params, $content);
146
    }
147
148
    /**
149
     * @param string $path
150
     * @param array  $content
151
     * @param array  $params
152
     *
153
     * @return \stdClass
154
     * @throws ApiResponseErrorException
155
     */
156
    protected function post(string $path, array $content, array $params = [])
157
    {
158
        return $this->sendRequest($path, 'POST', $params, $content);
159
    }
160
161
    /**
162
     * @param string $path
163
     * @param array  $content
164
     * @param array  $params
165
     *
166
     * @return \stdClass
167
     * @throws ApiResponseErrorException
168
     */
169
    protected function put(string $path, array $content, array $params = [])
170
    {
171
        return $this->sendRequest($path, 'PUT', $params, $content);
172
    }
173
174
    /**
175
     * @param string     $path
176
     * @param array      $method
177
     * @param array      $params
178
     * @param array|null $content
179
     *
180
     * @return \stdClass
181
     * @throws ApiResponseErrorException
182
     */
183
    protected function sendRequest(string $path, string $method, array $params = [], array $content = null)
184
    {
185
        $request = new ApiRequest($this->assembleUri($path, $params), $method);
0 ignored issues
show
Bug introduced by
It seems like $method defined by parameter $method on line 183 can also be of type array; however, Symfony\Component\Browse...\Request::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
186
        $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...
187
        $request->setServer(array_merge($defaults ?: [], ['CONTENT_TYPE' => 'application/json']));
188
        if ($content !== null) {
189
            $request->setContent(json_encode($content));
190
        }
191
        $this->client->requestFromRequest($request);
192
193
        return $this->getJsonForLastRequest($path, $method);
0 ignored issues
show
Bug introduced by
It seems like $method defined by parameter $method on line 183 can also be of type array; however, KleijnWeb\SwaggerBundle\...getJsonForLastRequest() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
194
    }
195
196
197
    /**
198
     * @param string $path
199
     * @param array  $params
200
     *
201
     * @return string
202
     */
203
    private function assembleUri(string $path, array $params = [])
204
    {
205
        $uri = $path;
206
        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...
207
            $uri = $path . '?' . http_build_query($params);
208
        }
209
210
        return $uri;
211
    }
212
213
    /**
214
     * @param string $fullPath
215
     * @param string $method
216
     *
217
     * @return \stdClass|null
218
     * @throws ApiResponseErrorException
219
     */
220
    private function getJsonForLastRequest(string $fullPath, string $method)
221
    {
222
        $method = strtolower($method);
223
        $response = $this->client->getResponse();
224
        $json = $response->getContent();
225
        $data = json_decode($json);
226
227
        if ($response->getStatusCode() !== 204) {
228
            static $errors = [
229
                JSON_ERROR_NONE           => 'No error',
230
                JSON_ERROR_DEPTH          => 'Maximum stack depth exceeded',
231
                JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
232
                JSON_ERROR_CTRL_CHAR      => 'Control character error, possibly incorrectly encoded',
233
                JSON_ERROR_SYNTAX         => 'Syntax error',
234
                JSON_ERROR_UTF8           => 'Malformed UTF-8 characters, possibly incorrectly encoded'
235
            ];
236
            $error = json_last_error();
237
            $jsonErrorMessage = isset($errors[$error]) ? $errors[$error] : 'Unknown error';
238
            $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...
239
                JSON_ERROR_NONE,
240
                json_last_error(),
241
                "Not valid JSON: " . $jsonErrorMessage . "(" . var_export($json, true) . ")"
242
            );
243
        }
244
245
        if (substr((string)$response->getStatusCode(), 0, 1) != '2') {
246
            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...
247
                $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
248
            }
249
            // This throws an exception so that tests can catch it when it is expected
250
            throw new ApiResponseErrorException($json, $data, $response->getStatusCode());
251
        }
252
253
        $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
254
255
        return $data;
256
    }
257
258
    /**
259
     * @param int      $code
260
     * @param Response $response
261
     * @param string   $method
262
     * @param string   $fullPath
263
     * @param mixed    $data
264
     */
265
    private function validateResponse(int $code, Response $response, string $method, string $fullPath, $data)
266
    {
267
        $request = $this->client->getRequest();
268
        if (!self::$schemaManager->hasPath(['paths', $request->get('_swagger_path'), $method, 'responses', $code])) {
269
            $statusClass = (int)substr((string)$code, 0, 1);
270
            if (in_array($statusClass, [4, 5])) {
271
                return;
272
            }
273
            throw new \UnexpectedValueException(
274
                "There is no $code response definition for {$request->get('_swagger_path')}:$method. "
275
            );
276
        }
277
        $headers = [];
278
279
        foreach ($response->headers->all() as $key => $values) {
280
            $headers[str_replace(' ', '-', ucwords(str_replace('-', ' ', $key)))] = $values[0];
281
        }
282
        try {
283
            $this->assertResponseHeadersMatch($headers, self::$schemaManager, $fullPath, $method, $code);
284
            $this->assertResponseBodyMatch($data, self::$schemaManager, $fullPath, $method, $code);
285
        } catch (\UnexpectedValueException $e) {
286
            $statusClass = (int)(string)$code[0];
287
            if (in_array($statusClass, [4, 5])) {
288
                return;
289
            }
290
        }
291
    }
292
}
293