Mercurial > hg > isophonics-drupal-site
comparison core/modules/rest/tests/src/Functional/ResourceTestBase.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 1fec387a4317 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\Tests\rest\Functional; | |
4 | |
5 use Behat\Mink\Driver\BrowserKitDriver; | |
6 use Drupal\Core\Url; | |
7 use Drupal\rest\RestResourceConfigInterface; | |
8 use Drupal\Tests\BrowserTestBase; | |
9 use Drupal\user\Entity\Role; | |
10 use Drupal\user\RoleInterface; | |
11 use GuzzleHttp\RequestOptions; | |
12 use Psr\Http\Message\ResponseInterface; | |
13 | |
14 /** | |
15 * Subclass this for every REST resource, every format and every auth provider. | |
16 * | |
17 * For more guidance see | |
18 * \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase | |
19 * which has recommendations for testing the | |
20 * \Drupal\rest\Plugin\rest\resource\EntityResource REST resource for every | |
21 * format and every auth provider. It's a special case (because that single REST | |
22 * resource generates supports not just one thing, but many things — multiple | |
23 * entity types), but the same principles apply. | |
24 */ | |
25 abstract class ResourceTestBase extends BrowserTestBase { | |
26 | |
27 /** | |
28 * The format to use in this test. | |
29 * | |
30 * A format is the combination of a certain normalizer and a certain | |
31 * serializer. | |
32 * | |
33 * @see https://www.drupal.org/developing/api/8/serialization | |
34 * | |
35 * (The default is 'json' because that doesn't depend on any module.) | |
36 * | |
37 * @var string | |
38 */ | |
39 protected static $format = 'json'; | |
40 | |
41 /** | |
42 * The MIME type that corresponds to $format. | |
43 * | |
44 * (Sadly this cannot be computed automatically yet.) | |
45 * | |
46 * @var string | |
47 */ | |
48 protected static $mimeType = 'application/json'; | |
49 | |
50 /** | |
51 * The authentication mechanism to use in this test. | |
52 * | |
53 * (The default is 'cookie' because that doesn't depend on any module.) | |
54 * | |
55 * @var string | |
56 */ | |
57 protected static $auth = FALSE; | |
58 | |
59 /** | |
60 * The REST Resource Config entity ID under test (i.e. a resource type). | |
61 * | |
62 * The REST Resource plugin ID can be calculated from this. | |
63 * | |
64 * @var string | |
65 * | |
66 * @see \Drupal\rest\Entity\RestResourceConfig::__construct() | |
67 */ | |
68 protected static $resourceConfigId = NULL; | |
69 | |
70 /** | |
71 * The account to use for authentication, if any. | |
72 * | |
73 * @var null|\Drupal\Core\Session\AccountInterface | |
74 */ | |
75 protected $account = NULL; | |
76 | |
77 /** | |
78 * The REST resource config entity storage. | |
79 * | |
80 * @var \Drupal\Core\Entity\EntityStorageInterface | |
81 */ | |
82 protected $resourceConfigStorage; | |
83 | |
84 /** | |
85 * The serializer service. | |
86 * | |
87 * @var \Symfony\Component\Serializer\Serializer | |
88 */ | |
89 protected $serializer; | |
90 | |
91 /** | |
92 * Modules to install. | |
93 * | |
94 * @var array | |
95 */ | |
96 public static $modules = ['rest']; | |
97 | |
98 /** | |
99 * {@inheritdoc} | |
100 */ | |
101 public function setUp() { | |
102 parent::setUp(); | |
103 | |
104 $this->serializer = $this->container->get('serializer'); | |
105 | |
106 // Ensure the anonymous user role has no permissions at all. | |
107 $user_role = Role::load(RoleInterface::ANONYMOUS_ID); | |
108 foreach ($user_role->getPermissions() as $permission) { | |
109 $user_role->revokePermission($permission); | |
110 } | |
111 $user_role->save(); | |
112 assert('[] === $user_role->getPermissions()', 'The anonymous user role has no permissions at all.'); | |
113 | |
114 if (static::$auth !== FALSE) { | |
115 // Ensure the authenticated user role has no permissions at all. | |
116 $user_role = Role::load(RoleInterface::AUTHENTICATED_ID); | |
117 foreach ($user_role->getPermissions() as $permission) { | |
118 $user_role->revokePermission($permission); | |
119 } | |
120 $user_role->save(); | |
121 assert('[] === $user_role->getPermissions()', 'The authenticated user role has no permissions at all.'); | |
122 | |
123 // Create an account. | |
124 $this->account = $this->createUser(); | |
125 } | |
126 else { | |
127 // Otherwise, also create an account, so that any test involving User | |
128 // entities will have the same user IDs regardless of authentication. | |
129 $this->createUser(); | |
130 } | |
131 | |
132 $this->resourceConfigStorage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config'); | |
133 | |
134 // Ensure there's a clean slate: delete all REST resource config entities. | |
135 $this->resourceConfigStorage->delete($this->resourceConfigStorage->loadMultiple()); | |
136 $this->refreshTestStateAfterRestConfigChange(); | |
137 } | |
138 | |
139 /** | |
140 * Provisions the REST resource under test. | |
141 * | |
142 * @param string[] $formats | |
143 * The allowed formats for this resource. | |
144 * @param string[] $authentication | |
145 * The allowed authentication providers for this resource. | |
146 */ | |
147 protected function provisionResource($formats = [], $authentication = []) { | |
148 $this->resourceConfigStorage->create([ | |
149 'id' => static::$resourceConfigId, | |
150 'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY, | |
151 'configuration' => [ | |
152 'methods' => ['GET', 'POST', 'PATCH', 'DELETE'], | |
153 'formats' => $formats, | |
154 'authentication' => $authentication, | |
155 ], | |
156 'status' => TRUE, | |
157 ])->save(); | |
158 $this->refreshTestStateAfterRestConfigChange(); | |
159 } | |
160 | |
161 /** | |
162 * Refreshes the state of the tester to be in sync with the testee. | |
163 * | |
164 * Should be called after every change made to: | |
165 * - RestResourceConfig entities | |
166 * - the 'rest.settings' simple configuration | |
167 */ | |
168 protected function refreshTestStateAfterRestConfigChange() { | |
169 // Ensure that the cache tags invalidator has its internal values reset. | |
170 // Otherwise the http_response cache tag invalidation won't work. | |
171 $this->refreshVariables(); | |
172 | |
173 // Tests using this base class may trigger route rebuilds due to changes to | |
174 // RestResourceConfig entities or 'rest.settings'. Ensure the test generates | |
175 // routes using an up-to-date router. | |
176 \Drupal::service('router.builder')->rebuildIfNeeded(); | |
177 } | |
178 | |
179 /** | |
180 * Return the expected error message. | |
181 * | |
182 * @param string $method | |
183 * The HTTP method (GET, POST, PATCH, DELETE). | |
184 * | |
185 * @return string | |
186 * The error string. | |
187 */ | |
188 protected function getExpectedUnauthorizedAccessMessage($method) { | |
189 $resource_plugin_id = str_replace('.', ':', static::$resourceConfigId); | |
190 $permission = 'restful ' . strtolower($method) . ' ' . $resource_plugin_id; | |
191 return "The '$permission' permission is required."; | |
192 } | |
193 | |
194 /** | |
195 * Sets up the necessary authorization. | |
196 * | |
197 * In case of a test verifying publicly accessible REST resources: grant | |
198 * permissions to the anonymous user role. | |
199 * | |
200 * In case of a test verifying behavior when using a particular authentication | |
201 * provider: create a user with a particular set of permissions. | |
202 * | |
203 * Because of the $method parameter, it's possible to first set up | |
204 * authentication for only GET, then add POST, et cetera. This then also | |
205 * allows for verifying a 403 in case of missing authorization. | |
206 * | |
207 * @param string $method | |
208 * The HTTP method for which to set up authentication. | |
209 * | |
210 * @see ::grantPermissionsToAnonymousRole() | |
211 * @see ::grantPermissionsToAuthenticatedRole() | |
212 */ | |
213 abstract protected function setUpAuthorization($method); | |
214 | |
215 /** | |
216 * Verifies the error response in case of missing authentication. | |
217 */ | |
218 abstract protected function assertResponseWhenMissingAuthentication(ResponseInterface $response); | |
219 | |
220 /** | |
221 * Asserts normalization-specific edge cases. | |
222 * | |
223 * (Should be called before sending a well-formed request.) | |
224 * | |
225 * @see \GuzzleHttp\ClientInterface::request() | |
226 * | |
227 * @param string $method | |
228 * HTTP method. | |
229 * @param \Drupal\Core\Url $url | |
230 * URL to request. | |
231 * @param array $request_options | |
232 * Request options to apply. | |
233 */ | |
234 abstract protected function assertNormalizationEdgeCases($method, Url $url, array $request_options); | |
235 | |
236 /** | |
237 * Asserts authentication provider-specific edge cases. | |
238 * | |
239 * (Should be called before sending a well-formed request.) | |
240 * | |
241 * @see \GuzzleHttp\ClientInterface::request() | |
242 * | |
243 * @param string $method | |
244 * HTTP method. | |
245 * @param \Drupal\Core\Url $url | |
246 * URL to request. | |
247 * @param array $request_options | |
248 * Request options to apply. | |
249 */ | |
250 abstract protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options); | |
251 | |
252 /** | |
253 * Initializes authentication. | |
254 * | |
255 * E.g. for cookie authentication, we first need to get a cookie. | |
256 */ | |
257 protected function initAuthentication() {} | |
258 | |
259 /** | |
260 * Returns Guzzle request options for authentication. | |
261 * | |
262 * @param string $method | |
263 * The HTTP method for this authenticated request. | |
264 * | |
265 * @return array | |
266 * Guzzle request options to use for authentication. | |
267 * | |
268 * @see \GuzzleHttp\ClientInterface::request() | |
269 */ | |
270 protected function getAuthenticationRequestOptions($method) { | |
271 return []; | |
272 } | |
273 | |
274 /** | |
275 * Grants permissions to the anonymous role. | |
276 * | |
277 * @param string[] $permissions | |
278 * Permissions to grant. | |
279 */ | |
280 protected function grantPermissionsToAnonymousRole(array $permissions) { | |
281 $this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), $permissions); | |
282 } | |
283 | |
284 /** | |
285 * Grants permissions to the authenticated role. | |
286 * | |
287 * @param string[] $permissions | |
288 * Permissions to grant. | |
289 */ | |
290 protected function grantPermissionsToAuthenticatedRole(array $permissions) { | |
291 $this->grantPermissions(Role::load(RoleInterface::AUTHENTICATED_ID), $permissions); | |
292 } | |
293 | |
294 /** | |
295 * Grants permissions to the tested role: anonymous or authenticated. | |
296 * | |
297 * @param string[] $permissions | |
298 * Permissions to grant. | |
299 * | |
300 * @see ::grantPermissionsToAuthenticatedRole() | |
301 * @see ::grantPermissionsToAnonymousRole() | |
302 */ | |
303 protected function grantPermissionsToTestedRole(array $permissions) { | |
304 if (static::$auth) { | |
305 $this->grantPermissionsToAuthenticatedRole($permissions); | |
306 } | |
307 else { | |
308 $this->grantPermissionsToAnonymousRole($permissions); | |
309 } | |
310 } | |
311 | |
312 /** | |
313 * Performs a HTTP request. Wraps the Guzzle HTTP client. | |
314 * | |
315 * Why wrap the Guzzle HTTP client? Because we want to keep the actual test | |
316 * code as simple as possible, and hence not require them to specify the | |
317 * 'http_errors = FALSE' request option, nor do we want them to have to | |
318 * convert Drupal Url objects to strings. | |
319 * | |
320 * We also don't want to follow redirects automatically, to ensure these tests | |
321 * are able to detect when redirects are added or removed. | |
322 * | |
323 * @see \GuzzleHttp\ClientInterface::request() | |
324 * | |
325 * @param string $method | |
326 * HTTP method. | |
327 * @param \Drupal\Core\Url $url | |
328 * URL to request. | |
329 * @param array $request_options | |
330 * Request options to apply. | |
331 * | |
332 * @return \Psr\Http\Message\ResponseInterface | |
333 */ | |
334 protected function request($method, Url $url, array $request_options) { | |
335 $request_options[RequestOptions::HTTP_ERRORS] = FALSE; | |
336 $request_options[RequestOptions::ALLOW_REDIRECTS] = FALSE; | |
337 $request_options = $this->decorateWithXdebugCookie($request_options); | |
338 $client = $this->getSession()->getDriver()->getClient()->getClient(); | |
339 return $client->request($method, $url->setAbsolute(TRUE)->toString(), $request_options); | |
340 } | |
341 | |
342 /** | |
343 * Asserts that a resource response has the given status code and body. | |
344 * | |
345 * @param int $expected_status_code | |
346 * The expected response status. | |
347 * @param string|false $expected_body | |
348 * The expected response body. FALSE in case this should not be asserted. | |
349 * @param \Psr\Http\Message\ResponseInterface $response | |
350 * The response to assert. | |
351 */ | |
352 protected function assertResourceResponse($expected_status_code, $expected_body, ResponseInterface $response) { | |
353 $this->assertSame($expected_status_code, $response->getStatusCode()); | |
354 $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); | |
355 if ($expected_body !== FALSE) { | |
356 $this->assertSame($expected_body, (string) $response->getBody()); | |
357 } | |
358 } | |
359 | |
360 /** | |
361 * Asserts that a resource error response has the given message. | |
362 * | |
363 * @param int $expected_status_code | |
364 * The expected response status. | |
365 * @param string $expected_message | |
366 * The expected error message. | |
367 * @param \Psr\Http\Message\ResponseInterface $response | |
368 * The error response to assert. | |
369 */ | |
370 protected function assertResourceErrorResponse($expected_status_code, $expected_message, ResponseInterface $response) { | |
371 $expected_body = ($expected_message !== FALSE) ? $this->serializer->encode(['message' => $expected_message], static::$format) : FALSE; | |
372 $this->assertResourceResponse($expected_status_code, $expected_body, $response); | |
373 } | |
374 | |
375 /** | |
376 * Adds the Xdebug cookie to the request options. | |
377 * | |
378 * @param array $request_options | |
379 * The request options. | |
380 * | |
381 * @return array | |
382 * Request options updated with the Xdebug cookie if present. | |
383 */ | |
384 protected function decorateWithXdebugCookie(array $request_options) { | |
385 $session = $this->getSession(); | |
386 $driver = $session->getDriver(); | |
387 if ($driver instanceof BrowserKitDriver) { | |
388 $client = $driver->getClient(); | |
389 foreach ($client->getCookieJar()->all() as $cookie) { | |
390 if (isset($request_options[RequestOptions::HEADERS]['Cookie'])) { | |
391 $request_options[RequestOptions::HEADERS]['Cookie'] .= '; ' . $cookie->getName() . '=' . $cookie->getValue(); | |
392 } | |
393 else { | |
394 $request_options[RequestOptions::HEADERS]['Cookie'] = $cookie->getName() . '=' . $cookie->getValue(); | |
395 } | |
396 } | |
397 } | |
398 return $request_options; | |
399 } | |
400 | |
401 } |