Completed
Pull Request — master (#71)
by
unknown
06:32
created

WebApiContext::printResponse()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 1
nop 0
1
<?php
2
3
/*
4
 * This file is part of the Behat WebApiExtension.
5
 * (c) Konstantin Kudryashov <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Behat\WebApiExtension\Context;
12
13
use Behat\Gherkin\Node\PyStringNode;
14
use Behat\Gherkin\Node\TableNode;
15
use GuzzleHttp\ClientInterface;
16
use GuzzleHttp\Exception\RequestException;
17
use GuzzleHttp\Psr7\Request;
18
use PHPUnit_Framework_Assert as Assertions;
19
use Psr\Http\Message\RequestInterface;
20
use Psr\Http\Message\ResponseInterface;
21
22
/**
23
 * Provides web API description definitions.
24
 *
25
 * @author Konstantin Kudryashov <[email protected]>
26
 */
27
class WebApiContext implements ApiClientAwareContext
28
{
29
    /**
30
     * @var string
31
     */
32
    private $authorization;
33
34
    /**
35
     * @var ClientInterface
36
     */
37
    private $client;
38
39
    /**
40
     * @var array
41
     */
42
    private $headers = array();
43
44
    /**
45
     * @var \GuzzleHttp\Message\RequestInterface|RequestInterface
46
     */
47
    private $request;
48
49
    /**
50
     * @var \GuzzleHttp\Message\ResponseInterface|ResponseInterface
51
     */
52
    private $response;
53
54
    private $placeHolders = array();
55
56
    /**
57
     * {@inheritdoc}
58
     */
59
    public function setClient(ClientInterface $client)
60
    {
61
        $this->client = $client;
62
    }
63
64
    /**
65
     * Adds Basic Authentication header to next request.
66
     *
67
     * @param string $username
68
     * @param string $password
69
     *
70
     * @Given /^I am authenticating as "([^"]*)" with "([^"]*)" password$/
71
     */
72
    public function iAmAuthenticatingAs($username, $password)
73
    {
74
        $this->removeHeader('Authorization');
75
        $this->authorization = base64_encode($username . ':' . $password);
76
        $this->addHeader('Authorization', 'Basic ' . $this->authorization);
77
    }
78
79
    /**
80
     * Sets a HTTP Header.
81
     *
82
     * @param string $name  header name
83
     * @param string $value header value
84
     *
85
     * @Given /^I set header "([^"]*)" with value "([^"]*)"$/
86
     */
87
    public function iSetHeaderWithValue($name, $value)
88
    {
89
        $this->addHeader($name, $value);
90
    }
91
92
    /**
93
     * Sends HTTP request to specific relative URL.
94
     *
95
     * @param string $method request method
96
     * @param string $url    relative url
97
     *
98
     * @When /^(?:I )?send a ([A-Z]+) request to "([^"]+)"$/
99
     */
100
    public function iSendARequest($method, $url)
101
    {
102
        $url = $this->prepareUrl($url);
103
104 View Code Duplication
        if (version_compare(ClientInterface::VERSION, '6.0', '>=')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
105
            $this->request = new Request($method, $url, $this->headers);
106
        } else {
107
            $this->request = $this->getClient()->createRequest($method, $url);
0 ignored issues
show
Bug introduced by
The method createRequest() does not exist on GuzzleHttp\ClientInterface. Did you maybe mean request()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
108
            if (!empty($this->headers)) {
109
                $this->request->addHeaders($this->headers);
110
            }
111
        }
112
113
        $this->sendRequest();
114
    }
115
116
    /**
117
     * Sends HTTP request to specific URL with field values from Table.
118
     *
119
     * @param string    $method request method
120
     * @param string    $url    relative url
121
     * @param TableNode $post   table of post values
122
     *
123
     * @When /^(?:I )?send a ([A-Z]+) request to "([^"]+)" with values:$/
124
     */
125
    public function iSendARequestWithValues($method, $url, TableNode $post)
126
    {
127
        $url = $this->prepareUrl($url);
128
        $fields = array();
129
130
        foreach ($post->getRowsHash() as $key => $val) {
131
            $fields[$key] = $this->replacePlaceHolder($val);
132
        }
133
134
        $bodyOption = array(
135
          'body' => json_encode($fields),
136
        );
137
138 View Code Duplication
        if (version_compare(ClientInterface::VERSION, '6.0', '>=')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
139
            $this->request = new Request($method, $url, $this->headers, $bodyOption['body']);
140
        } else {
141
            $this->request = $this->getClient()->createRequest($method, $url, $bodyOption);
0 ignored issues
show
Bug introduced by
The method createRequest() does not exist on GuzzleHttp\ClientInterface. Did you maybe mean request()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
142
            if (!empty($this->headers)) {
143
                $this->request->addHeaders($this->headers);
144
            }
145
        }
146
147
        $this->sendRequest();
148
    }
149
150
    /**
151
     * Sends HTTP request to specific URL with raw body from PyString.
152
     *
153
     * @param string       $method request method
154
     * @param string       $url    relative url
155
     * @param PyStringNode $string request body
156
     *
157
     * @When /^(?:I )?send a ([A-Z]+) request to "([^"]+)" with body:$/
158
     */
159
    public function iSendARequestWithBody($method, $url, PyStringNode $string)
160
    {
161
        $url = $this->prepareUrl($url);
162
        $string = $this->replacePlaceHolder(trim($string));
163
164
        if (version_compare(ClientInterface::VERSION, '6.0', '>=')) {
165
            $this->request = new Request($method, $url, $this->headers, $string);
166
        } else {
167
            $this->request = $this->getClient()->createRequest(
0 ignored issues
show
Bug introduced by
The method createRequest() does not exist on GuzzleHttp\ClientInterface. Did you maybe mean request()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
168
                $method,
169
                $url,
170
                array(
171
                    'headers' => $this->getHeaders(),
172
                    'body' => $string,
173
                )
174
            );
175
        }
176
177
        $this->sendRequest();
178
    }
179
180
    /**
181
     * Sends HTTP request to specific URL with form data from PyString.
182
     *
183
     * @param string       $method request method
184
     * @param string       $url    relative url
185
     * @param PyStringNode $body   request body
186
     *
187
     * @When /^(?:I )?send a ([A-Z]+) request to "([^"]+)" with form data:$/
188
     */
189
    public function iSendARequestWithFormData($method, $url, PyStringNode $body)
190
    {
191
        $url = $this->prepareUrl($url);
192
        $body = $this->replacePlaceHolder(trim($body));
193
194
        $fields = array();
195
        parse_str(implode('&', explode("\n", $body)), $fields);
196
197
        if (version_compare(ClientInterface::VERSION, '6.0', '>=')) {
198
            $this->request = new Request($method, $url, ['Content-Type' => 'application/x-www-form-urlencoded'], http_build_query($fields, null, '&'));
199
        } else {
200
            $this->request = $this->getClient()->createRequest($method, $url);
0 ignored issues
show
Bug introduced by
The method createRequest() does not exist on GuzzleHttp\ClientInterface. Did you maybe mean request()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
201
            /** @var \GuzzleHttp\Post\PostBodyInterface $requestBody */
202
            $requestBody = $this->request->getBody();
203
            foreach ($fields as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $fields of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
204
                $requestBody->setField($key, $value);
205
            }
206
        }
207
208
        $this->sendRequest();
209
    }
210
211
    /**
212
     * Checks that response has specific status code.
213
     *
214
     * @param string $code status code
215
     *
216
     * @Then /^(?:the )?response code should be (\d+)$/
217
     */
218
    public function theResponseCodeShouldBe($code)
219
    {
220
        $expected = intval($code);
221
        $actual = intval($this->response->getStatusCode());
222
        Assertions::assertSame($expected, $actual);
223
    }
224
225
    /**
226
     * Checks that the response has specific header key/value
227
     *
228
     * @param string $key header key
229
     * @param string $value header value
230
     *
231
     * @Then the response header :key should be :value
232
     */
233
    public function theResponseHeaderShouldBe($key, $value)
234
    {
235
        Assertions::assertSame($value, $this->response->getHeader($key)[0]);
236
    }
237
238
    /**
239
     * Checks that response body contains specific text.
240
     *
241
     * @param string $text
242
     *
243
     * @Then /^(?:the )?response should contain "([^"]*)"$/
244
     */
245
    public function theResponseShouldContain($text)
246
    {
247
        $expectedRegexp = '/' . preg_quote($text) . '/i';
248
        $actual = (string) $this->response->getBody();
249
        Assertions::assertRegExp($expectedRegexp, $actual);
250
    }
251
252
    /**
253
     * Checks that response body doesn't contains specific text.
254
     *
255
     * @param string $text
256
     *
257
     * @Then /^(?:the )?response should not contain "([^"]*)"$/
258
     */
259
    public function theResponseShouldNotContain($text)
260
    {
261
        $expectedRegexp = '/' . preg_quote($text) . '/';
262
        $actual = (string) $this->response->getBody();
263
        Assertions::assertNotRegExp($expectedRegexp, $actual);
264
    }
265
266
    /**
267
     * Checks that response body contains JSON from PyString.
268
     *
269
     * Do not check that the response body /only/ contains the JSON from PyString,
270
     *
271
     * @param PyStringNode $jsonString
272
     *
273
     * @throws \RuntimeException
274
     *
275
     * @Then /^(?:the )?response should contain json:$/
276
     */
277
    public function theResponseShouldContainJson(PyStringNode $jsonString)
278
    {
279
        $etalon = json_decode($this->replacePlaceHolder($jsonString->getRaw()), true);
280
        $actual = json_decode($this->response->getBody(), true);
281
282
        if (null === $etalon) {
283
            throw new \RuntimeException(
284
              "Can not convert etalon to json:\n" . $this->replacePlaceHolder($jsonString->getRaw())
285
            );
286
        }
287
288
        if (null === $actual) {
289
            throw new \RuntimeException(
290
              "Can not convert actual to json:\n" . $this->replacePlaceHolder((string) $this->response->getBody())
291
            );
292
        }
293
294
        Assertions::assertGreaterThanOrEqual(count($etalon), count($actual));
295
        foreach ($etalon as $key => $needle) {
296
            Assertions::assertArrayHasKey($key, $actual);
297
            Assertions::assertEquals($etalon[$key], $actual[$key]);
298
        }
299
    }
300
301
    /**
302
     * Prints last response body.
303
     *
304
     * @Then print response
305
     */
306
    public function printResponse()
307
    {
308
        $request = $this->request;
309
        $response = $this->response;
310
311
        echo sprintf(
312
            "%s %s => %d:\n%s",
313
            $request->getMethod(),
314
            (string) ($request instanceof RequestInterface ? $request->getUri() : $request->getUrl()),
315
            $response->getStatusCode(),
316
            (string) $response->getBody()
317
        );
318
    }
319
320
    /**
321
     * Prepare URL by replacing placeholders and trimming slashes.
322
     *
323
     * @param string $url
324
     *
325
     * @return string
326
     */
327
    private function prepareUrl($url)
328
    {
329
        return ltrim($this->replacePlaceHolder($url), '/');
330
    }
331
332
    /**
333
     * Sets place holder for replacement.
334
     *
335
     * you can specify placeholders, which will
336
     * be replaced in URL, request or response body.
337
     *
338
     * @param string $key   token name
339
     * @param string $value replace value
340
     */
341
    public function setPlaceHolder($key, $value)
342
    {
343
        $this->placeHolders[$key] = $value;
344
    }
345
346
    /**
347
     * Replaces placeholders in provided text.
348
     *
349
     * @param string $string
350
     *
351
     * @return string
352
     */
353
    protected function replacePlaceHolder($string)
354
    {
355
        foreach ($this->placeHolders as $key => $val) {
356
            $string = str_replace($key, $val, $string);
357
        }
358
359
        return $string;
360
    }
361
362
    /**
363
     * Returns headers, that will be used to send requests.
364
     *
365
     * @return array
366
     */
367
    protected function getHeaders()
368
    {
369
        return $this->headers;
370
    }
371
372
    /**
373
     * Adds header
374
     *
375
     * @param string $name
376
     * @param string $value
377
     */
378
    protected function addHeader($name, $value)
379
    {
380
        if (isset($this->headers[$name])) {
381
            if (!is_array($this->headers[$name])) {
382
                $this->headers[$name] = array($this->headers[$name]);
383
            }
384
385
            $this->headers[$name][] = $value;
386
        } else {
387
            $this->headers[$name] = $value;
388
        }
389
    }
390
391
    /**
392
     * Removes a header identified by $headerName
393
     *
394
     * @param string $headerName
395
     */
396
    protected function removeHeader($headerName)
397
    {
398
        if (array_key_exists($headerName, $this->headers)) {
399
            unset($this->headers[$headerName]);
400
        }
401
    }
402
403
    private function sendRequest()
404
    {
405
        try {
406
            $this->response = $this->getClient()->send($this->request);
407
        } catch (RequestException $e) {
408
            $this->response = $e->getResponse();
409
410
            if (null === $this->response) {
411
                throw $e;
412
            }
413
        }
414
    }
415
416
    private function getClient()
417
    {
418
        if (null === $this->client) {
419
            throw new \RuntimeException('Client has not been set in WebApiContext');
420
        }
421
422
        return $this->client;
423
    }
424
}
425