Completed
Pull Request — master (#73)
by
unknown
12:57
created

WebApiContext::getClient()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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