Completed
Push — 7.2 ( 50b219...900a7f )
by
unknown
31:36
created

TestCase::createBrowser()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the Functional\TestCase class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Bundle\EzPublishRestBundle\Tests\Functional;
10
11
use Buzz\Browser;
12
use Buzz\Client\BuzzClientInterface;
13
use Buzz\Client\Curl;
14
use Nyholm\Psr7\Factory\Psr17Factory as HttpFactory;
15
use Nyholm\Psr7\Request as HttpRequest;
16
use PHPUnit\Framework\TestCase as BaseTestCase;
17
use PHPUnit\Framework\ExpectationFailedException;
18
use Psr\Http\Message\RequestInterface;
19
use Psr\Http\Message\ResponseInterface;
20
use RuntimeException;
21
22
class TestCase extends BaseTestCase
23
{
24
    const X_HTTP_METHOD_OVERRIDE_MAP = [
25
        'PUBLISH' => 'POST',
26
        'MOVE' => 'POST',
27
        'PATCH' => 'PATCH',
28
        'COPY' => 'POST',
29
    ];
30
31
    /**
32
     * @var \Buzz\Client\BuzzClientInterface
33
     */
34
    private $httpClient;
35
36
    /**
37
     * @var string
38
     */
39
    private $httpHost;
40
41
    /**
42
     * @var string
43
     * Basic auth login:password
44
     */
45
    private $httpAuth;
46
47
    protected static $testSuffix;
48
49
    /**
50
     * @var array
51
     */
52
    private $headers = [];
53
54
    /**
55
     * The username to use for login.
56
     * @var string
57
     */
58
    private $loginUsername;
59
60
    /**
61
     * The password to use for login.
62
     * @var string
63
     */
64
    private $loginPassword;
65
66
    /**
67
     * If true, a login request is automatically done during setUp().
68
     * @var bool
69
     */
70
    protected $autoLogin = true;
71
72
    /**
73
     * List of REST contentId (/content/objects/12345) created by tests.
74
     *
75
     * @var array
76
     */
77
    private static $createdContent = array();
78
79
    protected function setUp()
80
    {
81
        parent::setUp();
82
83
        $this->httpHost = getenv('EZP_TEST_REST_HOST') ?: 'localhost';
84
        $this->httpAuth = getenv('EZP_TEST_REST_AUTH') ?: 'admin:publish';
85
        list($this->loginUsername, $this->loginPassword) = explode(':', $this->httpAuth);
86
87
        $this->httpClient = new Curl(
88
            [
89
                'verify' => false,
90
                'timeout' => 90,
91
                'allow_redirects' => false,
92
            ],
93
            new HttpFactory()
94
        );
95
96
        if ($this->autoLogin) {
97
            $session = $this->login();
98
            $this->headers['Cookie'] = sprintf('%s=%s', $session->name, $session->identifier);
99
            $this->headers['X-CSRF-Token'] = $session->csrfToken;
100
        }
101
    }
102
103
    /**
104
     * Instantiate Browser object.
105
     *
106
     * @return \Buzz\Client\BuzzClientInterface
107
     */
108
    public function createBrowser(): BuzzClientInterface
109
    {
110
        if ($this->httpClient === null) {
111
            throw new RuntimeException('Unable to create browser - test is not initialized');
112
        }
113
114
        return new Browser($this->httpClient, new HttpFactory());
115
    }
116
117
    /**
118
     * @param \Psr\Http\Message\RequestInterface $request
119
     *
120
     * @return \Psr\Http\Message\ResponseInterface
121
     *
122
     * @throws \Psr\Http\Client\ClientException
123
     */
124
    public function sendHttpRequest(RequestInterface $request): ResponseInterface
125
    {
126
        return $this->httpClient->sendRequest($request);
127
    }
128
129
    protected function getHttpHost()
130
    {
131
        return $this->httpHost;
132
    }
133
134
    protected function getLoginUsername()
135
    {
136
        return $this->loginUsername;
137
    }
138
139
    protected function getLoginPassword()
140
    {
141
        return $this->loginPassword;
142
    }
143
144
    /**
145
     * Get base URI for \Buzz\Browser based requests.
146
     *
147
     * @return string
148
     */
149
    protected function getBaseURI()
150
    {
151
        return "http://{$this->httpHost}";
152
    }
153
154
    /**
155
     * @param string $method
156
     * @param string $uri
157
     * @param string $contentType
158
     * @param string $acceptType
159
     * @param string $body
160
     *
161
     * @param array $extraHeaders [key => value] array of extra headers
162
     *
163
     * @return \Psr\Http\Message\RequestInterface
164
     */
165
    public function createHttpRequest(
166
        string $method,
167
        string $uri,
168
        string $contentType = '',
169
        string $acceptType = '',
170
        string $body = '',
171
        array $extraHeaders = []
172
    ): RequestInterface {
173
        $headers = array_merge(
174
            $method === 'POST' && $uri === '/api/ezp/v2/user/sessions' ? [] : $this->headers,
175
            [
176
                'Content-Type' => $this->generateMediaTypeString($contentType),
177
                'Accept' => $this->generateMediaTypeString($acceptType),
178
            ]
179
        );
180
181
        if (isset(static::X_HTTP_METHOD_OVERRIDE_MAP[$method])) {
182
            $headers['X-HTTP-Method-Override'] = $method;
183
            $method = static::X_HTTP_METHOD_OVERRIDE_MAP[$method];
184
        }
185
186
        return new HttpRequest(
187
            $method,
188
            $this->getBaseURI() . $uri,
189
            array_merge($headers, $extraHeaders),
190
            $body
191
        );
192
    }
193
194
    protected function assertHttpResponseCodeEquals(ResponseInterface $response, $expected)
195
    {
196
        $responseCode = $response->getStatusCode();
197
        try {
198
            self::assertEquals($expected, $responseCode);
199
        } catch (ExpectationFailedException $e) {
200
            $errorMessageString = '';
201
            $contentTypeHeader = $response->hasHeader('Content-Type')
202
                ? $response->getHeader('Content-Type')[0]
203
                : '';
204
205
            if (strpos($contentTypeHeader, 'application/vnd.ez.api.ErrorMessage+xml') !== false) {
206
                $body = \simplexml_load_string($response->getBody());
207
                $errorMessageString = $this->getHttpResponseCodeErrorMessage($body);
208
            } elseif (strpos($contentTypeHeader, 'application/vnd.ez.api.ErrorMessage+json') !== false) {
209
                $body = json_decode($response->getBody());
210
                $errorMessageString = $this->getHttpResponseCodeErrorMessage($body->ErrorMessage);
211
            }
212
213
            self::assertEquals($expected, $responseCode, $errorMessageString);
214
        }
215
    }
216
217
    private function getHttpResponseCodeErrorMessage($errorMessage)
218
    {
219
        $errorMessageString = <<< EOF
220
Server error message ({$errorMessage->errorCode}): {$errorMessage->errorMessage}
221
222
{$errorMessage->errorDescription}
223
224
EOF;
225
226
        // If server is in debug mode it will return file, line and trace.
227
        if (!empty($errorMessage->file)) {
228
            $errorMessageString .= "\nIn {$errorMessage->file}:{$errorMessage->line}\n\n{$errorMessage->trace}";
229
        } else {
230
            $errorMessageString .= "\nIn \<no trace, debug disabled\>";
231
        }
232
233
        return $errorMessageString;
234
    }
235
236
    protected function assertHttpResponseHasHeader(ResponseInterface $response, $header, $expectedValue = null)
237
    {
238
        $headerValue = $response->hasHeader($header) ? $response->getHeader($header)[0] : null;
239
        self::assertNotNull($headerValue, "Failed asserting that response has a {$header} header");
240
        if ($expectedValue !== null) {
241
            self::assertEquals($expectedValue, $headerValue);
242
        }
243
    }
244
245
    protected function generateMediaTypeString($typeString)
246
    {
247
        return "application/vnd.ez.api.$typeString";
248
    }
249
250
    protected function getMediaFromTypeString($typeString)
251
    {
252
        $prefix = 'application/vnd.ez.api.';
253
        self::assertStringStartsWith(
254
            $prefix,
255
            $typeString,
256
            "Unknown media: {$typeString}"
257
        );
258
259
        return substr($typeString, strlen($prefix));
260
    }
261
262
    protected function addCreatedElement($href)
263
    {
264
        $testCase = $this;
265
        self::$createdContent[$href] = function () use ($href, $testCase) {
266
            $testCase->sendHttpRequest(
267
                $testCase->createHttpRequest('DELETE', $href)
268
            );
269
        };
270
    }
271
272
    public static function tearDownAfterClass()
273
    {
274
        self::clearCreatedElement(self::$createdContent);
275
    }
276
277
    private static function clearCreatedElement(array $contentArray)
278
    {
279
        foreach (array_reverse($contentArray) as $href => $callback) {
280
            $callback();
281
        }
282
    }
283
284
    /**
285
     * @param string $parentLocationId The REST id of the parent location
286
     *
287
     * @return array created Content, as an array
288
     */
289
    protected function createFolder($string, $parentLocationId)
290
    {
291
        $string = $this->addTestSuffix($string);
292
        $remoteId = md5(uniqid($string, true));
293
        $xml = <<< XML
294
<?xml version="1.0" encoding="UTF-8"?>
295
<ContentCreate>
296
  <ContentType href="/api/ezp/v2/content/types/1" />
297
  <mainLanguageCode>eng-GB</mainLanguageCode>
298
  <LocationCreate>
299
    <ParentLocation href="{$parentLocationId}" />
300
    <priority>0</priority>
301
    <hidden>false</hidden>
302
    <sortField>PATH</sortField>
303
    <sortOrder>ASC</sortOrder>
304
  </LocationCreate>
305
  <Section href="/api/ezp/v2/content/sections/1" />
306
  <alwaysAvailable>true</alwaysAvailable>
307
  <remoteId>{$remoteId}</remoteId>
308
  <User href="/api/ezp/v2/user/users/14" />
309
  <modificationDate>2012-09-30T12:30:00</modificationDate>
310
  <fields>
311
    <field>
312
      <fieldDefinitionIdentifier>name</fieldDefinitionIdentifier>
313
      <languageCode>eng-GB</languageCode>
314
      <fieldValue>{$string}</fieldValue>
315
    </field>
316
  </fields>
317
</ContentCreate>
318
XML;
319
320
        return $this->createContent($xml);
321
    }
322
323
    /**
324
     * @param $xml
325
     *
326
     * @return array Content key of the Content struct array
327
     */
328
    protected function createContent($xml)
329
    {
330
        $request = $this->createHttpRequest(
331
            'POST',
332
            '/api/ezp/v2/content/objects',
333
            'ContentCreate+xml',
334
            'Content+json',
335
            $xml
336
        );
337
        $response = $this->sendHttpRequest($request);
338
339
        self::assertHttpResponseCodeEquals($response, 201);
340
341
        $content = json_decode($response->getBody(), true);
342
343
        if (!isset($content['Content']['CurrentVersion']['Version'])) {
344
            self::fail("Incomplete response (no version):\n" . $response->getBody() . "\n");
345
        }
346
347
        $response = $this->sendHttpRequest(
348
            $request = $this->createHttpRequest('PUBLISH', $content['Content']['CurrentVersion']['Version']['_href'])
349
        );
350
351
        self::assertHttpResponseCodeEquals($response, 204);
352
353
        $this->addCreatedElement($content['Content']['_href']);
354
355
        return $content['Content'];
356
    }
357
358
    /**
359
     * @param string $contentHref
360
     *
361
     * @return array
362
     */
363 View Code Duplication
    protected function getContentLocations($contentHref)
364
    {
365
        $response = $this->sendHttpRequest(
366
            $this->createHttpRequest('GET', "$contentHref/locations", '', 'LocationList+json')
367
        );
368
        self::assertHttpResponseCodeEquals($response, 200);
369
        $folderLocations = json_decode($response->getBody(), true);
370
371
        return $folderLocations;
372
    }
373
374
    protected function addTestSuffix($string)
375
    {
376
        if (!isset(self::$testSuffix)) {
377
            self::$testSuffix = uniqid();
378
        }
379
380
        return $string . '_' . self::$testSuffix;
381
    }
382
383
    /**
384
     * Sends a login request to the REST server.
385
     *
386
     * @return \stdClass an object with the name, identifier, csrftoken properties.
387
     */
388
    protected function login()
389
    {
390
        $request = $this->createAuthenticationHttpRequest($this->getLoginUsername(), $this->getLoginPassword());
391
        $response = $this->sendHttpRequest($request);
392
        self::assertHttpResponseCodeEquals($response, 201);
393
394
        return json_decode($response->getBody())->Session;
395
    }
396
397
    /**
398
     * @param string $login
399
     * @param string $password
400
     * @param array $extraHeaders extra [key => value] headers to be passed with the authentication request.
401
     *
402
     * @return \Psr\Http\Message\RequestInterface
403
     */
404
    protected function createAuthenticationHttpRequest(string $login, string $password, array $extraHeaders = [])
405
    {
406
        return $this->createHttpRequest(
407
            'POST',
408
            '/api/ezp/v2/user/sessions',
409
            'SessionInput+json',
410
            'Session+json',
411
            sprintf('{"SessionInput": {"login": "%s", "password": "%s"}}', $login, $password),
412
            $extraHeaders
413
        );
414
    }
415
}
416