Merge pull request #18982 from mlutfy/wp82
[civicrm-core.git] / ext / authx / tests / phpunit / Civi / Authx / AllFlowsTest.php
1 <?php
2
3 namespace Civi\Authx;
4
5 use Civi\Test\HttpTestTrait;
6 use CRM_Authx_ExtensionUtil as E;
7 use Civi\Test\EndToEndInterface;
8 use GuzzleHttp\Cookie\CookieJar;
9 use GuzzleHttp\Psr7\AppendStream;
10 use GuzzleHttp\Psr7\Request;
11 use GuzzleHttp\Psr7\Uri;
12 use Psr\Http\Message\ResponseInterface;
13 use function GuzzleHttp\Psr7\stream_for;
14
15 /**
16 * This is a matrix-style test which assesses all supported permutations of
17 *
18 * @group e2e
19 */
20 class AllFlowsTest extends \PHPUnit\Framework\TestCase implements EndToEndInterface {
21
22 use HttpTestTrait;
23
24 /**
25 * Backup copy of the original settings.
26 *
27 * @var array
28 */
29 protected $settingsBackup;
30
31 /**
32 * List of CMS-dependent quirks that should be ignored during testing.
33 * @var array
34 */
35 protected $quirks = [];
36
37 public static function setUpBeforeClass(): void {
38 \Civi\Test::e2e()
39 ->installMe(__DIR__)
40 ->callback(
41 function() {
42 \CRM_Utils_System::synchronizeUsers();
43 },
44 'synchronizeUsers'
45 )
46 ->apply();
47 }
48
49 public function setUp(): void {
50 $quirks = [
51 'Joomla' => ['sendsExcessCookies', 'authErrorShowsForm'],
52 'WordPress' => ['sendsExcessCookies'],
53 ];
54 $this->quirks = $quirks[CIVICRM_UF] ?? [];
55
56 parent::setUp();
57 $this->settingsBackup = [];
58 foreach (\Civi\Authx\Meta::getFlowTypes() as $flowType) {
59 foreach (["authx_{$flowType}_cred", "authx_{$flowType}_user", "authx_guards"] as $setting) {
60 $this->settingsBackup[$setting] = \Civi::settings()->get($setting);
61 }
62 }
63
64 \Civi::settings()->set('authx_guards', []);
65 }
66
67 public function tearDown(): void {
68 foreach ($this->settingsBackup as $setting => $value) {
69 \Civi::settings()->set($setting, $value);
70 }
71 parent::tearDown();
72 }
73
74 public function getStatelessExamples() {
75 $exs = [];
76 $exs[] = ['pass', 'param'];
77 $exs[] = ['pass', 'header'];
78 $exs[] = ['pass', 'xheader'];
79 $exs[] = ['api_key', 'param'];
80 $exs[] = ['api_key', 'header'];
81 $exs[] = ['api_key', 'xheader'];
82 $exs[] = ['jwt', 'param'];
83 $exs[] = ['jwt', 'header'];
84 $exs[] = ['jwt', 'xheader'];
85 return $exs;
86 }
87
88 public function getCredTypes() {
89 $exs = [];
90 $exs[] = ['pass'];
91 $exs[] = ['api_key'];
92 $exs[] = ['jwt'];
93 return $exs;
94 }
95
96 public function testAnonymous(): void {
97 $http = $this->createGuzzle(['http_errors' => FALSE]);
98
99 /** @var \Psr\Http\Message\RequestInterface $request */
100 $request = $this->requestMyContact();
101 $response = $http->send($request);
102 $this->assertAnonymousContact($response);
103 }
104
105 /**
106 * Send a request using a stateless protocol. Assert that identities are setup correctly.
107 *
108 * @param string $credType
109 * The type of credential to put in the `Authorization:` header.
110 * @param string $flowType
111 * The "flow" determines how the credential is added on top of the base-request (e.g. adding a parameter or header).
112 * @throws \CiviCRM_API3_Exception
113 * @throws \GuzzleHttp\Exception\GuzzleException
114 * @dataProvider getStatelessExamples
115 */
116 public function testStatelessContactOnly($credType, $flowType): void {
117 if ($credType === 'pass') {
118 $this->assertTrue(TRUE, 'No need to test password credentials with non-user contacts');
119 return;
120 }
121 $http = $this->createGuzzle(['http_errors' => FALSE]);
122
123 /** @var \Psr\Http\Message\RequestInterface $request */
124 $request = $this->applyAuth($this->requestMyContact(), $credType, $flowType, $this->getLebowskiCID());
125
126 // Phase 1: Request fails if this credential type is not enabled
127 \Civi::settings()->set("authx_{$flowType}_cred", []);
128 $response = $http->send($request);
129 $this->assertFailedDueToProhibition($response);
130
131 // Phase 2: Request succeeds if this credential type is enabled
132 \Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
133 $response = $http->send($request);
134 $this->assertMyContact($this->getLebowskiCID(), NULL, $response);
135 if (!in_array('sendsExcessCookies', $this->quirks)) {
136 $this->assertNoCookies($response);
137 }
138 }
139
140 /**
141 * Send a request using a stateless protocol. Assert that identities are setup correctly.
142 *
143 * @param string $credType
144 * The type of credential to put in the `Authorization:` header.
145 * @param string $flowType
146 * The "flow" determines how the credential is added on top of the base-request (e.g. adding a parameter or header).
147 * @throws \CiviCRM_API3_Exception
148 * @throws \GuzzleHttp\Exception\GuzzleException
149 * @dataProvider getStatelessExamples
150 */
151 public function testStatelessUserContact($credType, $flowType): void {
152 $http = $this->createGuzzle(['http_errors' => FALSE]);
153
154 /** @var \Psr\Http\Message\RequestInterface $request */
155 $request = $this->applyAuth($this->requestMyContact(), $credType, $flowType, $this->getDemoCID());
156
157 // Phase 1: Request fails if this credential type is not enabled
158 \Civi::settings()->set("authx_{$flowType}_cred", []);
159 $response = $http->send($request);
160 $this->assertFailedDueToProhibition($response);
161
162 // Phase 2: Request succeeds if this credential type is enabled
163 \Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
164 $response = $http->send($request);
165 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
166 if (!in_array('sendsExcessCookies', $this->quirks)) {
167 $this->assertNoCookies($response);
168 }
169 }
170
171 /**
172 * The setting "authx_guard" may be used to require (or not require) the site_key.
173 *
174 * @throws \CiviCRM_API3_Exception
175 * @throws \GuzzleHttp\Exception\GuzzleException
176 */
177 public function testStatelessGuardSiteKey() {
178 if (!defined('CIVICRM_SITE_KEY')) {
179 $this->markTestIncomplete("Cannot run test without CIVICRM_SITE_KEY");
180 }
181
182 $addParam = function($request, $key, $value) {
183 $query = $request->getUri()->getQuery();
184 return $request->withUri(
185 $request->getUri()->withQuery($query . '&' . urlencode($key) . '=' . urlencode($value))
186 );
187 };
188
189 [$credType, $flowType] = ['pass', 'header'];
190 $http = $this->createGuzzle(['http_errors' => FALSE]);
191 \Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
192
193 /** @var \Psr\Http\Message\RequestInterface $request */
194 $request = $this->applyAuth($this->requestMyContact(), $credType, $flowType, $this->getDemoCID());
195
196 // Request OK. Policy requires site_key, and we have one.
197 \Civi::settings()->set("authx_guards", ['site_key']);
198 $response = $http->send($request->withHeader('X-Civi-Key', CIVICRM_SITE_KEY));
199 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
200
201 // Request OK. Policy does not require site_key, and we do not have one
202 \Civi::settings()->set("authx_guards", []);
203 $response = $http->send($request);
204 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
205
206 // Request fails. Policy requires site_key, but we don't have the wrong value.
207 \Civi::settings()->set("authx_guards", ['site_key']);
208 $response = $http->send($request->withHeader('X-Civi-Key', 'not-the-site-key'));
209 $this->assertFailedDueToProhibition($response);
210
211 // Request fails. Policy requires site_key, but we don't have one.
212 \Civi::settings()->set("authx_guards", ['site_key']);
213 $response = $http->send($request);
214 $this->assertFailedDueToProhibition($response);
215 }
216
217 /**
218 * The login flow allows you use 'civicrm/authx/login' and 'civicrm/authx/logout'
219 * to setup/teardown a session.
220 *
221 * @param string $credType
222 * The type of credential to put in the login request.
223 * @throws \CiviCRM_API3_Exception
224 * @throws \GuzzleHttp\Exception\GuzzleException
225 * @dataProvider getCredTypes
226 */
227 public function testStatefulLoginAllowed($credType): void {
228 $flowType = 'login';
229 $credFunc = 'cred' . ucfirst(preg_replace(';[^a-zA-Z0-9];', '', $credType));
230
231 // Phase 1: Some pages are not accessible.
232 $http = $this->createGuzzle(['http_errors' => FALSE]);
233 $http->get('civicrm/user');
234 $this->assertDashboardUnauthorized();
235
236 // Phase 2: Request succeeds if this credential type is enabled
237 $cookieJar = new CookieJar();
238 $http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
239 \Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
240 $response = $http->post('civicrm/authx/login', [
241 'form_params' => ['_authx' => $this->$credFunc($this->getDemoCID())],
242 ]);
243 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
244 $this->assertHasCookies($response);
245
246 // Phase 3: We can use cookies to request other pages
247 $response = $http->get('civicrm/authx/id');
248 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
249 $response = $http->get('civicrm/user');
250 $this->assertDashboardOk();
251
252 // Phase 4: After logout, requests should fail.
253 $oldCookies = clone $cookieJar;
254 $http->get('civicrm/authx/logout');
255 $this->assertStatusCode(200);
256 $http->get('civicrm/user');
257 $this->assertDashboardUnauthorized();
258
259 $httpHaxor = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $oldCookies]);
260 $httpHaxor->get('civicrm/user');
261 $this->assertDashboardUnauthorized();
262 }
263
264 /**
265 * The login flow 'civicrm/authx/login' may be prohibited by policy.
266 *
267 * @param string $credType
268 * The type of credential to put in the login request.
269 * @throws \CiviCRM_API3_Exception
270 * @throws \GuzzleHttp\Exception\GuzzleException
271 * @dataProvider getCredTypes
272 */
273 public function testStatefulLoginProhibited($credType): void {
274 $flowType = 'login';
275 $http = $this->createGuzzle(['http_errors' => FALSE]);
276 $credFunc = 'cred' . ucfirst(preg_replace(';[^a-zA-Z0-9];', '', $credType));
277
278 \Civi::settings()->set("authx_{$flowType}_cred", []);
279 $response = $http->post('civicrm/authx/login', [
280 'form_params' => ['_authx' => $this->$credFunc($this->getDemoCID())],
281 ]);
282 $this->assertFailedDueToProhibition($response);
283 }
284
285 /**
286 * The auto-login flow allows you to request a specific page with specific
287 * credentials. The new session is setup, and the page is displayed.
288 *
289 * @param string $credType
290 * The type of credential to put in the login request.
291 * @throws \CiviCRM_API3_Exception
292 * @throws \GuzzleHttp\Exception\GuzzleException
293 * @dataProvider getCredTypes
294 */
295 public function testStatefulAutoAllowed($credType): void {
296 $flowType = 'auto';
297 $cookieJar = new CookieJar();
298 $http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
299
300 /** @var \Psr\Http\Message\RequestInterface $request */
301 $request = $this->applyAuth($this->requestMyContact(), $credType, $flowType, $this->getDemoCID());
302
303 \Civi::settings()->set("authx_{$flowType}_cred", [$credType]);
304 $this->assertEquals(0, $cookieJar->count());
305 $response = $http->send($request);
306 $this->assertTrue($cookieJar->count() >= 1);
307 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
308
309 // FIXME: Assert that re-using cookies yields correct result.
310 }
311
312 /**
313 * The auto-login flow allows you to request a specific page with specific
314 * credentials. The new session is setup, and the page is displayed.
315 *
316 * @param string $credType
317 * The type of credential to put in the login request.
318 * @throws \CiviCRM_API3_Exception
319 * @throws \GuzzleHttp\Exception\GuzzleException
320 * @dataProvider getCredTypes
321 */
322 public function testStatefulAutoProhibited($credType): void {
323 $flowType = 'auto';
324 $cookieJar = new CookieJar();
325 $http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
326
327 /** @var \Psr\Http\Message\RequestInterface $request */
328 $request = $this->applyAuth($this->requestMyContact(), $credType, $flowType, $this->getDemoCID());
329
330 \Civi::settings()->set("authx_{$flowType}_cred", []);
331 $response = $http->send($request);
332 $this->assertFailedDueToProhibition($response);
333 }
334
335 /**
336 * Create a session for $demoCID. Within the session, make a single
337 * stateless request as $lebowskiCID.
338 *
339 * @throws \CiviCRM_API3_Exception
340 * @throws \GuzzleHttp\Exception\GuzzleException
341 */
342 public function testStatefulStatelessOverlap(): void {
343 \Civi::settings()->set("authx_login_cred", ['api_key']);
344 \Civi::settings()->set("authx_header_cred", ['api_key']);
345
346 $cookieJar = new CookieJar();
347 $http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
348
349 // Phase 1: Login, create a session.
350 $response = $http->post('civicrm/authx/login', [
351 'form_params' => ['_authx' => $this->credApikey($this->getDemoCID())],
352 ]);
353 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
354 $this->assertHasCookies($response);
355 $response = $http->get('civicrm/authx/id');
356 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
357
358 // Phase 2: Make a single, stateless request with different creds
359 /** @var \Psr\Http\Message\RequestInterface $request */
360 $request = $this->applyAuth($this->requestMyContact(), 'api_key', 'header', $this->getLebowskiCID());
361 $response = $http->send($request);
362 $this->assertFailedDueToProhibition($response);
363 // The following assertion merely identifies current behavior. If you can get it working generally, then huzza.
364 $this->assertBodyRegexp(';Session already active;', $response);
365 // $this->assertMyContact($this->getLebowskiCID(), NULL, $response);
366 // $this->assertNoCookies($response);
367
368 // Phase 3: Original session is still valid
369 $response = $http->get('civicrm/authx/id');
370 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response);
371 }
372
373 /**
374 * This consumer intends to make stateless requests with a handful of different identities,
375 * but their browser happens to be cookie-enabled. Ensure that identities do not leak between requests.
376 *
377 * @throws \CiviCRM_API3_Exception
378 * @throws \GuzzleHttp\Exception\GuzzleException
379 */
380 public function testMultipleStateless(): void {
381 \Civi::settings()->set("authx_header_cred", ['api_key']);
382 $cookieJar = new CookieJar();
383 $http = $this->createGuzzle(['http_errors' => FALSE, 'cookies' => $cookieJar]);
384
385 /** @var \Psr\Http\Message\RequestInterface $request */
386
387 // Alternate calls among (A)nonymous, (D)emo, and (L)ebowski
388 $planSteps = 'LADA LDLD DDLLAA';
389 $actualSteps = '';
390
391 for ($i = 0; $i < strlen($planSteps); $i++) {
392 switch ($planSteps[$i]) {
393 case 'L':
394 $request = $this->applyAuth($this->requestMyContact(), 'api_key', 'header', $this->getLebowskiCID());
395 $response = $http->send($request);
396 $this->assertMyContact($this->getLebowskiCID(), NULL, $response, 'Expected Lebowski in step #' . $i);
397 $actualSteps .= 'L';
398 break;
399
400 case 'A':
401 $request = $this->requestMyContact();
402 $response = $http->send($request);
403 $this->assertAnonymousContact($response);
404 $actualSteps .= 'A';
405 break;
406
407 case 'D':
408 $request = $this->applyAuth($this->requestMyContact(), 'api_key', 'header', $this->getDemoCID());
409 $response = $http->send($request);
410 $this->assertMyContact($this->getDemoCID(), $this->getDemoUID(), $response, 'Expected demo in step #' . $i);
411 $actualSteps .= 'D';
412 break;
413
414 case ' ':
415 $actualSteps .= ' ';
416 break;
417
418 default:
419 $this->fail('Unrecognized step #' . $i);
420 }
421 }
422
423 $this->assertEquals($actualSteps, $planSteps);
424 }
425
426 /**
427 * Filter a request, applying the given authentication options
428 *
429 * @param \Psr\Http\Message\RequestInterface $request
430 * @param string $credType
431 * Ex: 'pass', 'jwt', 'api_key'
432 * @param string $flowType
433 * Ex: 'param', 'header', 'xheader'
434 * @param int $cid
435 * @return \Psr\Http\Message\RequestInterface
436 */
437 protected function applyAuth($request, $credType, $flowType, $cid) {
438 $credFunc = 'cred' . ucfirst(preg_replace(';[^a-zA-Z0-9];', '', $credType));
439 $flowFunc = 'auth' . ucfirst(preg_replace(';[^a-zA-Z0-9];', '', $flowType));
440 return $this->$flowFunc($request, $this->$credFunc($cid));
441 }
442
443 // ------------------------------------------------
444 // Library: Base requests
445
446 /**
447 * Make an AJAX request with info about the current contact.
448 *
449 * @return \GuzzleHttp\Psr7\Request
450 */
451 public function requestMyContact() {
452 $p = (['where' => [['id', '=', 'user_contact_id']]]);
453 $uri = (new Uri('civicrm/authx/id'))
454 ->withQuery('params=' . urlencode(json_encode($p)));
455 $req = new Request('GET', $uri);
456 return $req;
457 }
458
459 /**
460 * Assert the AJAX request provided the expected contact.
461 *
462 * @param int $cid
463 * The expected contact ID
464 * @param int|null $uid
465 * The expected user ID
466 * @param \Psr\Http\Message\ResponseInterface $response
467 */
468 public function assertMyContact($cid, $uid, ResponseInterface $response): void {
469 $this->assertContentType('application/json', $response);
470 $this->assertStatusCode(200, $response);
471 $j = json_decode((string) $response->getBody(), 1);
472 $formattedFailure = $this->formatFailure($response);
473 $this->assertEquals($cid, $j['contact_id'], "Response did not give expected contact ID\n" . $formattedFailure);
474 $this->assertEquals($uid, $j['user_id'], "Response did not give expected user ID\n" . $formattedFailure);
475 }
476
477 /**
478 * Assert the AJAX request provided empty contact information
479 *
480 * @param \Psr\Http\Message\ResponseInterface $response
481 */
482 public function assertAnonymousContact(ResponseInterface $response): void {
483 $formattedFailure = $this->formatFailure($response);
484 $this->assertContentType('application/json', $response);
485 $this->assertStatusCode(200, $response);
486 $j = json_decode((string) $response->getBody(), 1);
487 if (json_last_error() !== JSON_ERROR_NONE || empty($j)) {
488 $this->fail('Malformed JSON' . $formattedFailure);
489 }
490 $this->assertTrue(array_key_exists('contact_id', $j) && $j['contact_id'] === NULL, 'contact_id should be null' . $formattedFailure);
491 $this->assertTrue(array_key_exists('user_id', $j) && $j['user_id'] === NULL, 'user_id should be null' . $formattedFailure);
492 }
493
494 /**
495 * Assert that the $response indicates the user cannot view the dashboard.
496 *
497 * @param \Psr\Http\Message\ResponseInterface $response
498 */
499 public function assertDashboardUnauthorized($response = NULL): void {
500 $response = $this->resolveResponse($response);
501 if (!in_array('authErrorShowsForm', $this->quirks)) {
502 $this->assertStatusCode(403, $response);
503 }
504 $this->assertFalse(
505 (bool) preg_match(';crm-dashboard-groups;', (string) $response->getBody()),
506 'Response should not contain a dashboard' . $this->formatFailure($response)
507 );
508 }
509
510 public function assertDashboardOk($response = NULL): void {
511 $response = $this->resolveResponse($response);
512 $this->assertStatusCode(200, $response);
513 $this->assertContentType('text/html', $response);
514 // If the first two assertions pass but the next fails, then... perhaps the
515 // local site permissions are wrong?
516 $this->assertTrue(
517 (bool) preg_match(';crm-dashboard-groups;', (string) $response->getBody()),
518 'Response should contain a dashboard' . $this->formatFailure($response)
519 );
520 }
521
522 // ------------------------------------------------
523 // Library: Flow functions
524
525 /**
526 * Add query parameter ("&_authx=<CRED>").
527 *
528 * @param \GuzzleHttp\Psr7\Request $request
529 * @param string $cred
530 * The credential add to the request (e.g. "Basic ASDF==" or "Bearer FDSA").
531 * @return \GuzzleHttp\Psr7\Request
532 */
533 public function authParam(Request $request, $cred) {
534 $query = $request->getUri()->getQuery();
535 return $request->withUri(
536 $request->getUri()->withQuery($query . '&_authx=' . urlencode($cred))
537 );
538 }
539
540 /**
541 * Add query parameter ("&_authx=<CRED>&_authxSes=1").
542 *
543 * @param \GuzzleHttp\Psr7\Request $request
544 * @param string $cred
545 * The credential add to the request (e.g. "Basic ASDF==" or "Bearer FDSA").
546 * @return \GuzzleHttp\Psr7\Request
547 */
548 public function authAuto(Request $request, $cred) {
549 $query = $request->getUri()->getQuery();
550 return $request->withUri(
551 $request->getUri()->withQuery($query . '&_authx=' . urlencode($cred) . '&_authxSes=1')
552 );
553 }
554
555 public function authLogin(Request $request, $cred) {
556 return $request->withMethod('POST')
557 ->withBody(new AppendStream([
558 stream_for('_authx=' . urlencode($cred) . '&'),
559 $request->getBody(),
560 ]));
561 }
562
563 public function authHeader(Request $request, $cred) {
564 return $request->withHeader('Authorization', $cred);
565 }
566
567 public function authXHeader(Request $request, $cred) {
568 return $request->withHeader('X-Civi-Auth', $cred);
569 }
570
571 public function authNone(Request $request, $cred) {
572 return $request;
573 }
574
575 // ------------------------------------------------
576 // Library: Credential functions
577
578 /**
579 * @param int $cid
580 * @return string
581 * The credential add to the request (e.g. "Basic ASDF==" or "Bearer FDSA").
582 */
583 public function credPass($cid) {
584 if ($cid === $this->getDemoCID()) {
585 return 'Basic ' . base64_encode($GLOBALS['_CV']['DEMO_USER'] . ':' . $GLOBALS['_CV']['DEMO_PASS']);
586 }
587 else {
588 $this->fail("This test does have the password the requested contact.");
589 }
590 }
591
592 public function credApikey($cid) {
593 $api_key = md5(\random_bytes(16));
594 \civicrm_api3('Contact', 'create', [
595 'id' => $cid,
596 'api_key' => $api_key,
597 ]);
598 return 'Bearer ' . $api_key;
599 }
600
601 public function credJwt($cid) {
602 if (empty(\Civi::service('crypto.registry')->findKeysByTag('SIGN'))) {
603 $this->markTestIncomplete('Cannot test JWT. No CIVICRM_SIGN_KEYS are defined.');
604 }
605 $token = \Civi::service('crypto.jwt')->encode([
606 'exp' => time() + 60 * 60,
607 'sub' => "cid:$cid",
608 'scope' => 'authx',
609 ]);
610 return 'Bearer ' . $token;
611 }
612
613 public function credNone($cid) {
614 return NULL;
615 }
616
617 /**
618 * @param \Psr\Http\Message\ResponseInterface $response
619 */
620 private function assertFailedDueToProhibition($response): void {
621 $this->assertBodyRegexp(';HTTP 401;', $response);
622 $this->assertContentType('text/plain', $response);
623 if (!in_array('sendsExcessCookies', $this->quirks)) {
624 $this->assertNoCookies($response);
625 }
626 $this->assertStatusCode(401, $response);
627
628 }
629
630 /**
631 * @param \Psr\Http\Message\ResponseInterface $response
632 */
633 private function assertNoCookies($response = NULL) {
634 $response = $this->resolveResponse($response);
635 $this->assertEmpty(
636 preg_grep('/Set-Cookie/i', array_keys($response->getHeaders())),
637 'Response should not have cookies' . $this->formatFailure($response)
638 );
639 return $this;
640 }
641
642 /**
643 * @param \Psr\Http\Message\ResponseInterface $response
644 */
645 private function assertHasCookies($response = NULL) {
646 $response = $this->resolveResponse($response);
647 $this->assertNotEmpty(
648 preg_grep('/Set-Cookie/i', array_keys($response->getHeaders())),
649 'Response should have cookies' . $this->formatFailure($response)
650 );
651 return $this;
652 }
653
654 /**
655 * @param $regexp
656 * @param \Psr\Http\Message\ResponseInterface $response
657 */
658 private function assertBodyRegexp($regexp, $response = NULL) {
659 $response = $this->resolveResponse($response);
660 $this->assertRegexp($regexp, (string) $response->getBody(),
661 'Response body does not match pattern' . $this->formatFailure($response));
662 return $this;
663 }
664
665 /**
666 * @return int
667 * @throws \CiviCRM_API3_Exception
668 */
669 private function getDemoCID(): int {
670 if (!isset(\Civi::$statics[__CLASS__]['demoId'])) {
671 \Civi::$statics[__CLASS__]['demoId'] = (int) \civicrm_api3('Contact', 'getvalue', [
672 'id' => '@user:' . $GLOBALS['_CV']['DEMO_USER'],
673 'return' => 'id',
674 ]);
675 }
676 return \Civi::$statics[__CLASS__]['demoId'];
677 }
678
679 private function getDemoUID(): int {
680 return \CRM_Core_Config::singleton()->userSystem->getUfId($GLOBALS['_CV']['DEMO_USER']);
681 }
682
683 public function getLebowskiCID() {
684 if (!isset(\Civi::$statics[__CLASS__]['lebowskiCID'])) {
685 $contact = \civicrm_api3('Contact', 'create', [
686 'contact_type' => 'Individual',
687 'first_name' => 'Jeffrey',
688 'last_name' => 'Lebowski',
689 'external_identifier' => __CLASS__,
690 'options' => [
691 'match' => 'external_identifier',
692 ],
693 ]);
694 \Civi::$statics[__CLASS__]['lebowskiCID'] = $contact['id'];
695 }
696 return \Civi::$statics[__CLASS__]['lebowskiCID'];
697 }
698
699 }