c87cd6783cf412a4c1c98b222b64f606de15f740
[civicrm-core.git] / tests / phpunit / CRM / Core / CommunityMessagesTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * Class CRM_Core_CommunityMessagesTest
14 * @group headless
15 */
16 class CRM_Core_CommunityMessagesTest extends CiviUnitTestCase {
17
18 /**
19 * The max difference between two times such that they should be
20 * treated as equals (expressed in seconds).
21 */
22 const APPROX_TIME_EQUALITY = 2;
23
24 /**
25 * @var CRM_Utils_Cache_Interface
26 */
27 protected $cache;
28
29 /**
30 * @var array list of possible web responses
31 */
32 protected static $webResponses = NULL;
33
34 /**
35 * @return array
36 */
37 public static function initWebResponses() {
38 if (self::$webResponses === NULL) {
39 self::$webResponses = [
40 'http-error' => [
41 CRM_Utils_HttpClient::STATUS_DL_ERROR,
42 NULL,
43 ],
44 'bad-json' => [
45 CRM_Utils_HttpClient::STATUS_OK,
46 '<html>this is not json!</html>',
47 ],
48 'invalid-ttl-document' => [
49 CRM_Utils_HttpClient::STATUS_OK,
50 json_encode([
51 // not an integer!
52 'ttl' => 'z',
53 // not an integer!
54 'retry' => 'z',
55 'messages' => [
56 [
57 'markup' => '<h1>Invalid document</h1>',
58 ],
59 ],
60 ]),
61 ],
62 'first-valid-response' => [
63 CRM_Utils_HttpClient::STATUS_OK,
64 json_encode([
65 'ttl' => 600,
66 'retry' => 600,
67 'messages' => [
68 [
69 'markup' => '<h1>First valid response</h1>',
70 ],
71 ],
72 ]),
73 ],
74 'second-valid-response' => [
75 CRM_Utils_HttpClient::STATUS_OK,
76 json_encode([
77 'ttl' => 600,
78 'retry' => 600,
79 'messages' => [
80 [
81 'markup' => '<h1>Second valid response</h1>',
82 ],
83 ],
84 ]),
85 ],
86 'two-messages' => [
87 CRM_Utils_HttpClient::STATUS_OK,
88 json_encode([
89 'ttl' => 600,
90 'retry' => 600,
91 'messages' => [
92 [
93 'markup' => '<h1>One</h1>',
94 'components' => ['CiviMail'],
95 ],
96 [
97 'markup' => '<h1>Two</h1>',
98 'components' => ['CiviMail'],
99 ],
100 ],
101 ]),
102 ],
103 'two-messages-halfbadcomp' => [
104 CRM_Utils_HttpClient::STATUS_OK,
105 json_encode([
106 'ttl' => 600,
107 'retry' => 600,
108 'messages' => [
109 [
110 'markup' => '<h1>One</h1>',
111 'components' => ['NotARealComponent'],
112 ],
113 [
114 'markup' => '<h1>Two</h1>',
115 'components' => ['CiviMail'],
116 ],
117 ],
118 ]),
119 ],
120 ];
121 }
122 return self::$webResponses;
123 }
124
125 public function setUp() {
126 parent::setUp();
127 $this->cache = new CRM_Utils_Cache_Arraycache([]);
128 self::initWebResponses();
129 }
130
131 public function tearDown() {
132 parent::tearDown();
133 CRM_Utils_Time::resetTime();
134 }
135
136 /**
137 * A list of bad web-responses; in general, whenever the downloader
138 * encounters one of these bad responses, it should ignore the
139 * document, retain the old data, and retry again later.
140 *
141 * @return array
142 */
143 public function badWebResponses() {
144 self::initWebResponses();
145 $result = [
146 [self::$webResponses['http-error']],
147 [self::$webResponses['bad-json']],
148 [self::$webResponses['invalid-ttl-document']],
149 ];
150 return $result;
151 }
152
153 public function testIsEnabled() {
154 $communityMessages = new CRM_Core_CommunityMessages(
155 $this->cache,
156 $this->expectNoHttpRequest()
157 );
158 $this->assertTrue($communityMessages->isEnabled());
159 }
160
161 public function testIsEnabled_false() {
162 $communityMessages = new CRM_Core_CommunityMessages(
163 $this->cache,
164 $this->expectNoHttpRequest(),
165 FALSE
166 );
167 $this->assertFalse($communityMessages->isEnabled());
168 }
169
170 /**
171 * Download a document; after the set expiration period, download again.
172 */
173 public function testGetDocument_NewOK_CacheOK_UpdateOK() {
174 // first try, good response
175 CRM_Utils_Time::setTime('2013-03-01 10:00:00');
176 $communityMessages = new CRM_Core_CommunityMessages(
177 $this->cache,
178 $this->expectOneHttpRequest(self::$webResponses['first-valid-response'])
179 );
180 $doc1 = $communityMessages->getDocument();
181 $this->assertEquals('<h1>First valid response</h1>', $doc1['messages'][0]['markup']);
182 $this->assertApproxEquals(strtotime('2013-03-01 10:10:00'), $doc1['expires'], self::APPROX_TIME_EQUALITY);
183
184 // second try, $doc1 hasn't expired yet, so still use it
185 CRM_Utils_Time::setTime('2013-03-01 10:09:00');
186 $communityMessages = new CRM_Core_CommunityMessages(
187 $this->cache,
188 $this->expectNoHttpRequest()
189 );
190 $doc2 = $communityMessages->getDocument();
191 $this->assertEquals('<h1>First valid response</h1>', $doc2['messages'][0]['markup']);
192 $this->assertApproxEquals(strtotime('2013-03-01 10:10:00'), $doc2['expires'], self::APPROX_TIME_EQUALITY);
193
194 // third try, $doc1 expired, update it
195 // more than 2 hours later (DEFAULT_RETRY)
196 CRM_Utils_Time::setTime('2013-03-01 12:00:02');
197 $communityMessages = new CRM_Core_CommunityMessages(
198 $this->cache,
199 $this->expectOneHttpRequest(self::$webResponses['second-valid-response'])
200 );
201 $doc3 = $communityMessages->getDocument();
202 $this->assertEquals('<h1>Second valid response</h1>', $doc3['messages'][0]['markup']);
203 $this->assertApproxEquals(strtotime('2013-03-01 12:10:02'), $doc3['expires'], self::APPROX_TIME_EQUALITY);
204 }
205
206 /**
207 * First download attempt fails (due to some bad web request).
208 * Store the NACK and retry after the default time period (DEFAULT_RETRY).
209 *
210 * @dataProvider badWebResponses
211 * @param array $badWebResponse
212 * Description of a web request that returns some kind of failure.
213 */
214 public function testGetDocument_NewFailure_CacheOK_UpdateOK($badWebResponse) {
215 $this->assertNotEmpty($badWebResponse);
216
217 // first try, bad response
218 CRM_Utils_Time::setTime('2013-03-01 10:00:00');
219 $communityMessages = new CRM_Core_CommunityMessages(
220 $this->cache,
221 $this->expectOneHttpRequest($badWebResponse)
222 );
223 $doc1 = $communityMessages->getDocument();
224 $this->assertEquals([], $doc1['messages']);
225 $this->assertTrue($doc1['expires'] > CRM_Utils_Time::getTimeRaw());
226
227 // second try, $doc1 hasn't expired yet, so still use it
228 CRM_Utils_Time::setTime('2013-03-01 10:09:00');
229 $communityMessages = new CRM_Core_CommunityMessages(
230 $this->cache,
231 $this->expectNoHttpRequest()
232 );
233 $doc2 = $communityMessages->getDocument();
234 $this->assertEquals([], $doc2['messages']);
235 $this->assertEquals($doc1['expires'], $doc2['expires']);
236
237 // third try, $doc1 expired, try again, get a good response
238 // more than 2 hours later (DEFAULT_RETRY)
239 CRM_Utils_Time::setTime('2013-03-01 12:00:02');
240 $communityMessages = new CRM_Core_CommunityMessages(
241 $this->cache,
242 $this->expectOneHttpRequest(self::$webResponses['first-valid-response'])
243 );
244 $doc3 = $communityMessages->getDocument();
245 $this->assertEquals('<h1>First valid response</h1>', $doc3['messages'][0]['markup']);
246 $this->assertTrue($doc3['expires'] > CRM_Utils_Time::getTimeRaw());
247 }
248
249 /**
250 * First download of new doc is OK.
251 * The update fails (due to some bad web response).
252 * The old data is retained in the cache.
253 * The failure eventually expires.
254 * A new update succeeds.
255 *
256 * @dataProvider badWebResponses
257 * @param array $badWebResponse
258 * Description of a web request that returns some kind of failure.
259 */
260 public function testGetDocument_NewOK_UpdateFailure_CacheOK_UpdateOK($badWebResponse) {
261 $this->assertNotEmpty($badWebResponse);
262
263 // first try, good response
264 CRM_Utils_Time::setTime('2013-03-01 10:00:00');
265 $communityMessages = new CRM_Core_CommunityMessages(
266 $this->cache,
267 $this->expectOneHttpRequest(self::$webResponses['first-valid-response'])
268 );
269 $doc1 = $communityMessages->getDocument();
270 $this->assertEquals('<h1>First valid response</h1>', $doc1['messages'][0]['markup']);
271 $this->assertApproxEquals(strtotime('2013-03-01 10:10:00'), $doc1['expires'], self::APPROX_TIME_EQUALITY);
272
273 // second try, $doc1 has expired; bad response; keep old data
274 // more than 2 hours later (DEFAULT_RETRY)
275 CRM_Utils_Time::setTime('2013-03-01 12:00:02');
276 $communityMessages = new CRM_Core_CommunityMessages(
277 $this->cache,
278 $this->expectOneHttpRequest($badWebResponse)
279 );
280 $doc2 = $communityMessages->getDocument();
281 $this->assertEquals('<h1>First valid response</h1>', $doc2['messages'][0]['markup']);
282 $this->assertTrue($doc2['expires'] > CRM_Utils_Time::getTimeRaw());
283
284 // third try, $doc2 hasn't expired yet; no request; keep old data
285 CRM_Utils_Time::setTime('2013-03-01 12:09:00');
286 $communityMessages = new CRM_Core_CommunityMessages(
287 $this->cache,
288 $this->expectNoHttpRequest()
289 );
290 $doc3 = $communityMessages->getDocument();
291 $this->assertEquals('<h1>First valid response</h1>', $doc3['messages'][0]['markup']);
292 $this->assertEquals($doc2['expires'], $doc3['expires']);
293
294 // fourth try, $doc2 has expired yet; new request; replace data
295 CRM_Utils_Time::setTime('2013-03-01 12:10:02');
296 $communityMessages = new CRM_Core_CommunityMessages(
297 $this->cache,
298 $this->expectOneHttpRequest(self::$webResponses['second-valid-response'])
299 );
300 $doc4 = $communityMessages->getDocument();
301 $this->assertEquals('<h1>Second valid response</h1>', $doc4['messages'][0]['markup']);
302 $this->assertApproxEquals(strtotime('2013-03-01 12:20:02'), $doc4['expires'], self::APPROX_TIME_EQUALITY);
303 }
304
305 /**
306 * Randomly pick among two options.
307 */
308 public function testPick_rand() {
309 $communityMessages = new CRM_Core_CommunityMessages(
310 $this->cache,
311 $this->expectOneHttpRequest(self::$webResponses['two-messages'])
312 );
313 $doc1 = $communityMessages->getDocument();
314 $this->assertEquals('<h1>One</h1>', $doc1['messages'][0]['markup']);
315 $this->assertEquals('<h1>Two</h1>', $doc1['messages'][1]['markup']);
316
317 // randomly pick many times
318 $trials = 80;
319 // array($message => $count)
320 $freq = [];
321 for ($i = 0; $i < $trials; $i++) {
322 $message = $communityMessages->pick();
323 $freq[$message['markup']] = CRM_Utils_Array::value($message['markup'], $freq, 0) + 1;
324 }
325
326 // assert the probabilities
327 $this->assertApproxEquals(0.5, $freq['<h1>One</h1>'] / $trials, 0.3);
328 $this->assertApproxEquals(0.5, $freq['<h1>Two</h1>'] / $trials, 0.3);
329 $this->assertEquals($trials, $freq['<h1>One</h1>'] + $freq['<h1>Two</h1>']);
330 }
331
332 /**
333 * When presented with two options using component filters, always
334 * choose the one which references an active component.
335 */
336 public function testPick_componentFilter() {
337 $communityMessages = new CRM_Core_CommunityMessages(
338 $this->cache,
339 $this->expectOneHttpRequest(self::$webResponses['two-messages-halfbadcomp'])
340 );
341 $doc1 = $communityMessages->getDocument();
342 $this->assertEquals('<h1>One</h1>', $doc1['messages'][0]['markup']);
343 $this->assertEquals('<h1>Two</h1>', $doc1['messages'][1]['markup']);
344
345 // randomly pick many times
346 $trials = 10;
347 // array($message => $count)
348 $freq = [];
349 for ($i = 0; $i < $trials; $i++) {
350 $message = $communityMessages->pick();
351 $freq[$message['markup']] = CRM_Utils_Array::value($message['markup'], $freq, 0) + 1;
352 }
353
354 $this->assertEquals($trials, $freq['<h1>Two</h1>']);
355 }
356
357 public function testEvalMarkup() {
358 $communityMessages = new CRM_Core_CommunityMessages(
359 $this->cache,
360 $this->expectNoHttpRequest()
361 );
362 $this->assertEquals('cms=UnitTests cms=UnitTests', $communityMessages->evalMarkup('cms=%%uf%% cms={{uf}}'));
363 }
364
365 /**
366 * Generate a mock HTTP client with the expectation that it is never called.
367 *
368 * @return CRM_Utils_HttpClient|PHPUnit\Framework\MockObject\MockObject
369 */
370 protected function expectNoHttpRequest() {
371 $mockFunction = $this->mockMethod;
372 $client = $this->$mockFunction('CRM_Utils_HttpClient');
373 $client->expects($this->never())
374 ->method('get');
375 return $client;
376 }
377
378 /**
379 * Generate a mock HTTP client with the expectation that it is called once.
380 *
381 * @param $response
382 *
383 * @return CRM_Utils_HttpClient|PHPUnit\Framework\MockObject\MockObject
384 */
385 protected function expectOneHttpRequest($response) {
386 $mockFunction = $this->mockMethod;
387 $client = $this->$mockFunction('CRM_Utils_HttpClient');
388 $client->expects($this->once())
389 ->method('get')
390 ->will($this->returnValue($response));
391 return $client;
392 }
393
394 }