Completed
Pull Request — master (#58)
by John
03:16
created

ApiTestCase::createClient()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 1
eloc 5
nc 1
nop 2
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 30 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
 * @property bool   validateErrorResponse
27
 * @property string env
28
 * @property array  defaultServerVars
29
 */
30
trait ApiTestCase
31
{
32
    use AssertsTrait;
33
34
    /**
35
     * @var SchemaManager
36
     */
37
    protected static $schemaManager;
38
39
    /**
40
     * @var SwaggerDocument
41
     */
42
    protected static $document;
43
44
    /**
45
     * @var ApiTestClient
46
     */
47
    protected $client;
48
49
    /**
50
     * PHPUnit cannot add this to code coverage
51
     *
52
     * @codeCoverageIgnore
53
     *
54
     * @param string $swaggerPath
55
     *
56
     * @throws \InvalidArgumentException
57
     * @throws \org\bovigo\vfs\vfsStreamException
58
     */
59
    public static function initSchemaManager(string $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->getEnv(), 'debug' => true]);
0 ignored issues
show
Documentation Bug introduced by
It seems like static::createClient(arr...nv(), '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...
92
93
        parent::setUp();
94
    }
95
96
    /**
97
     * Creates a Client.
98
     *
99
     * @param array $options An array of options to pass to the createKernel class
100
     * @param array $server  An array of server parameters
101
     *
102
     * @return Client A Client instance
103
     */
104
    protected static function createClient(array $options = [], array $server = [])
105
    {
106
        static::bootKernel($options);
107
108
        $client = static::$kernel->getContainer()->get('swagger.test.client');
109
        $client->setServerParameters($server);
110
111
        return $client;
112
    }
113
114
    /**
115
     * @return array
116
     */
117
    protected function getDefaultServerVars()
118
    {
119
        return isset($this->defaultServerVars) ? $this->defaultServerVars : [];
120
    }
121
122
    /**
123
     * @return array
124
     */
125
    protected function getEnv()
126
    {
127
        return isset($this->env) ? $this->env : 'test';
128
    }
129
130
    /**
131
     * @return bool
132
     */
133
    protected function getValidateErrorResponse()
134
    {
135
        return isset($this->validateErrorResponse) ? $this->validateErrorResponse : false;
136
    }
137
138
    /**
139
     * @param string $path
140
     * @param array  $params
141
     *
142
     * @return \stdClass
143
     * @throws ApiResponseErrorException
144
     */
145
    protected function get(string $path, array $params = [])
146
    {
147
        return $this->sendRequest($path, 'GET', $params);
148
    }
149
150
    /**
151
     * @param string $path
152
     * @param array  $params
153
     *
154
     * @return \stdClass
155
     * @throws ApiResponseErrorException
156
     */
157
    protected function delete(string $path, array $params = [])
158
    {
159
        return $this->sendRequest($path, 'DELETE', $params);
160
    }
161
162
    /**
163
     * @param string $path
164
     * @param array  $content
165
     * @param array  $params
166
     *
167
     * @return \stdClass
168
     * @throws ApiResponseErrorException
169
     */
170
    protected function patch(string $path, array $content, array $params = [])
171
    {
172
        return $this->sendRequest($path, 'PATCH', $params, $content);
173
    }
174
175
    /**
176
     * @param string $path
177
     * @param array  $content
178
     * @param array  $params
179
     *
180
     * @return \stdClass
181
     * @throws ApiResponseErrorException
182
     */
183
    protected function post(string $path, array $content, array $params = [])
184
    {
185
        return $this->sendRequest($path, 'POST', $params, $content);
186
    }
187
188
    /**
189
     * @param string $path
190
     * @param array  $content
191
     * @param array  $params
192
     *
193
     * @return \stdClass
194
     * @throws ApiResponseErrorException
195
     */
196
    protected function put(string $path, array $content, array $params = [])
197
    {
198
        return $this->sendRequest($path, 'PUT', $params, $content);
199
    }
200
201
    /**
202
     * @param string     $path
203
     * @param string     $method
204
     * @param array      $params
205
     * @param array|null $content
206
     *
207
     * @return \stdClass
208
     * @throws ApiResponseErrorException
209
     */
210
    protected function sendRequest(string $path, string $method, array $params = [], array $content = null)
211
    {
212
        $request = new ApiRequest($this->assembleUri($path, $params), $method);
213
        $request->setServer(array_merge(['CONTENT_TYPE' => 'application/json'], $this->getDefaultServerVars()));
214
        if ($content !== null) {
215
            $request->setContent(json_encode($content));
216
        }
217
        $this->client->requestFromRequest($request);
218
219
        return $this->getJsonForLastRequest($path, $method);
220
    }
221
222
    /**
223
     * @param string $path
224
     * @param array  $params
225
     *
226
     * @return string
227
     */
228
    private function assembleUri(string $path, array $params = [])
229
    {
230
        $uri = $path;
231
        if (count($params)) {
232
            $uri = $path . '?' . http_build_query($params);
233
        }
234
235
        return $uri;
236
    }
237
238
    /**
239
     * @param string $fullPath
240
     * @param string $method
241
     *
242
     * @return \stdClass|null
243
     * @throws ApiResponseErrorException
244
     */
245
    private function getJsonForLastRequest(string $fullPath, string $method)
246
    {
247
        $method = strtolower($method);
248
        $response = $this->client->getResponse();
249
        $json = $response->getContent();
250
        $data = json_decode($json);
251
252
        if ($response->getStatusCode() !== 204) {
253
            static $errors = [
254
                JSON_ERROR_NONE           => 'No error',
255
                JSON_ERROR_DEPTH          => 'Maximum stack depth exceeded',
256
                JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)',
257
                JSON_ERROR_CTRL_CHAR      => 'Control character error, possibly incorrectly encoded',
258
                JSON_ERROR_SYNTAX         => 'Syntax error',
259
                JSON_ERROR_UTF8           => 'Malformed UTF-8 characters, possibly incorrectly encoded'
260
            ];
261
            $error = json_last_error();
262
            $jsonErrorMessage = isset($errors[$error]) ? $errors[$error] : 'Unknown error';
263
            $this->assertSame(
264
                JSON_ERROR_NONE,
265
                json_last_error(),
266
                "Not valid JSON: " . $jsonErrorMessage . "(" . var_export($json, true) . ")"
267
            );
268
        }
269
270
        if (substr((string)$response->getStatusCode(), 0, 1) != '2') {
271
            if ($this->getValidateErrorResponse()) {
272
                $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
273
            }
274
            // This throws an exception so that tests can catch it when it is expected
275
            throw new ApiResponseErrorException($json, $data, $response->getStatusCode());
276
        }
277
278
        $this->validateResponse($response->getStatusCode(), $response, $method, $fullPath, $data);
279
280
        return $data;
281
    }
282
283
    /**
284
     * @param int      $code
285
     * @param Response $response
286
     * @param string   $method
287
     * @param string   $fullPath
288
     * @param mixed    $data
289
     */
290
    private function validateResponse(int $code, Response $response, string $method, string $fullPath, $data)
291
    {
292
        $request = $this->client->getRequest();
293
        if (!self::$schemaManager->hasPath(['paths', $request->get('_swagger_path'), $method, 'responses', $code])) {
294
            $statusClass = (int)substr((string)$code, 0, 1);
295
            if (in_array($statusClass, [4, 5])) {
296
                return;
297
            }
298
            throw new \UnexpectedValueException(
299
                "There is no $code response definition for {$request->get('_swagger_path')}:$method. "
300
            );
301
        }
302
        $headers = [];
303
304
        foreach ($response->headers->all() as $key => $values) {
305
            $headers[str_replace(' ', '-', ucwords(str_replace('-', ' ', $key)))] = $values[0];
306
        }
307
        try {
308
            try {
309
                $this->assertResponseMediaTypeMatch(
310
                    $response->headers->get('Content-Type'),
311
                    self::$schemaManager,
312
                    $fullPath,
313
                    $method
314
                );
315
            } catch (\InvalidArgumentException $e) {
316
                // Not required, so skip if not found
317
            }
318
319
            $this->assertResponseHeadersMatch($headers, self::$schemaManager, $fullPath, $method, $code);
320
            $this->assertResponseBodyMatch($data, self::$schemaManager, $fullPath, $method, $code);
321
        } catch (\UnexpectedValueException $e) {
322
            $statusClass = (int)(string)$code[0];
323
            if (in_array($statusClass, [4, 5])) {
324
                return;
325
            }
326
        }
327
    }
328
329
    /**
330
     * @param mixed  $expected
331
     * @param mixed  $actual
332
     * @param string $message
333
     *
334
     * @return mixed
335
     */
336
    public abstract function assertSame($expected, $actual, $message = '');
337
}
338