1<?php 2 3namespace dokuwiki\plugin\structtasks\test; 4 5use DateInterval; 6use DokuWikiTest; 7 8use dokuwiki\plugin\structtasks\meta\AssignedNotifier; 9use dokuwiki\plugin\structtasks\meta\ClosedStatusNotifier; 10use dokuwiki\plugin\structtasks\meta\DateNotifier; 11use dokuwiki\plugin\structtasks\meta\DeletedNotifier; 12use dokuwiki\plugin\structtasks\meta\OpenStatusNotifier; 13use dokuwiki\plugin\structtasks\meta\RemovedNotifier; 14use dokuwiki\plugin\structtasks\meta\ReminderNotifier; 15use dokuwiki\plugin\structtasks\meta\SelfRemovalNotifier; 16use dokuwiki\plugin\structtasks\meta\TodayNotifier; 17use dokuwiki\plugin\structtasks\meta\OverdueNotifier; 18 19 20/** 21 * Tests of the "notifier" classes for the structtasks plugin. These 22 * examin the changes that have been made and will send out emails 23 * accordingly. Here, the mailer class is mocked so we can check it is 24 * being called correctly. 25 * 26 * @group plugin_structtasks 27 * @group plugins 28 */ 29 30class notifiers_plugin_structtasks_test extends DokuWikiTest { 31 32 const subject = 'Check substitutions: 33@TITLE@ 34@TITLELINK@ 35@EDITURL@ 36@EDITOR@ 37@STATUS@ 38@PREVSTATUS@ 39@DUEDATE@ 40@PREVDUEDATE@ 41@DUEIN@ 42@WIKINAME@'; 43 private $expected_subject; 44 private $text_replacements; 45 private $html_replacements; 46 const email_text = 'Text string to be sent as message body.'; 47 const email_html = 'Formatted HTML to be sent as message body.'; 48 const page_id = 'some:page_id'; 49 const page_title = 'Task Title'; 50 const editor = 'Some User'; 51 const editor_email = 'Some User <some.user@example.com>'; 52 private $new_data; 53 54 const defaultSettings = [ 55 'schema' => '', 56 'reminder' => array('1', '0'), 57 'overdue_reminder' => 1, 58 'completed' => '/^(completed?|closed|cancelled|finished)$/i' 59 ]; 60 61 static function fakeGetConf($key) { 62 return self::defaultSettings[$key]; 63 } 64 65 private function getLangCallback($expected_key) { 66 return function ($key) use ($expected_key){ 67 if ($key == $expected_key . '_subject') { 68 return $this::subject; 69 } 70 else if ($key == $expected_key . '_text') { 71 return $this::email_text; 72 } 73 else if ($key == $expected_key . '_html') { 74 return $this::email_html; 75 } 76 else { 77 throw new \Exception("Unexpected argument: ${key}"); 78 } 79 }; 80 } 81 82 public function provideDateIntervals() { 83 return [ 84 [date_create()->add(new DateInterval('P2D')), '2 days'], 85 [date_create()->sub(new DateInterval('P2D')), '2 days'], 86 [date_create()->add(new DateInterval('P1D')), '1 day'], 87 [date_create()->add(new DateInterval('P1Y20D')), '1 year and 20 days'], 88 [date_create()->add(new DateInterval('P2M0D')), '2 months'], 89 [date_create()->add(new DateInterval('P2Y2M2D')), '2 years, 2 months, and 2 days'], 90 [date_create()->add(new DateInterval('P10Y1M')), '10 years and 1 month'], 91 ]; 92 } 93 94 /** 95 * @dataProvider provideDateIntervals 96 */ 97 function testDueIn($date, $expected) { 98 $notifier = new AssignedNotifier( 99 [$this, 'fakeGetConf'], $this->getLangCallback('assigned')); 100 $this->assertEquals($expected, $notifier->dueIn($date)); 101 } 102 103 public function provideNotifiers() { 104 $date1 = date_create('2023-03-19'); 105 $date1->setTime(0, 0); 106 $date2 = date_create('2023-03-12 12:00'); 107 $new_data = [ 108 'content' => '====== Task Title ====== 109Brief updated description of the task.', 110 'duedate' => $date1, 111 'duedate_formatted' => '19 Mar 2023', 112 'assignees' => ['User One <user1@thing.com>', 'User 2 <u2@abc.com>', 'Third Guy <guy3@abc.com>', 'Some User <some.user@example.com>'], 113 'status' => 'Complete', 114 ]; 115 $old_data = [ 116 'content' => '====== Old Title ====== 117Brief description of the task.', 118 'duedate' => $date2, 119 'duedate_formatted' => '12 March 2023 12:00', 120 'assignees' => [ 'User 2 <u2@abc.com>', 'Third Guy <guy3@abc.com>', 'User Four <u4@thingy.co.uk', 'Some User <some.user@example.com>'], 121 'status' => 'Ongoing', 122 ]; 123 $new_data2 = array_replace($new_data, ['duedate' => clone $new_data['duedate']]); 124 $old_data2 = array_replace($old_data, ['duedate' => clone $old_data['duedate']]); 125 $all_but_editor = array_slice($new_data['assignees'], 0, -1); 126 $empty_data = ['content' => '', 'duedate' => date_create(''), 127 'duedate_formatted' => '', 'assignees' => [], 'status' => '']; 128 $today = date_create(); 129 $yesterday = date_create()->sub(new DateInterval('P1D')); 130 $tomorrow = date_create()->add(new DateInterval('P1D')); 131 132 return [ 133 'AssignedNotifier' => [ 134 AssignedNotifier::class, [$new_data['assignees'][0]], 135 $new_data, $old_data, 'assigned' 136 ], 137 'RemovedNotifier' => [ 138 RemovedNotifier::class, [$old_data['assignees'][2]], 139 $new_data, $old_data, 'removed' 140 ], 141 'DateNotifier' => [ 142 DateNotifier::class, $all_but_editor, $new_data, 143 $old_data, 'date' 144 ], 145 'OpenStatusNotifier' => [ 146 OpenStatusNotifier::class, $all_but_editor, 147 array_replace($new_data, ['status' => 'Ongoing']), 148 array_replace($old_data, ['status' => 'Completed']), 149 'openstatus' 150 ], 151 'ClosedStatusNotifier' => [ 152 ClosedStatusNotifier::class, $all_but_editor, $new_data, 153 $old_data, 'closedstatus' 154 ], 155 'SelfRemovalNotifier' => [ 156 SelfRemovalNotifier::class, $all_but_editor, 157 array_replace($new_data, ['assignees' => $all_but_editor]), 158 $old_data, 'self_removal' 159 ], 160 'DeletedNotifier' => [ 161 DeletedNotifier::class, 162 array_slice($old_data['assignees'], 0, -1), $empty_data, 163 $old_data, 'deleted' 164 ], 165 'Not AssignedNotifier' => [ 166 AssignedNotifier::class, [], $new_data, 167 $new_data2, 'assigned' 168 ], 169 'Not RemovedNotifier' => [ 170 RemovedNotifier::class, [], $new_data, 171 $new_data2, 'removed' 172 ], 173 'Not DateNotifier' => [ 174 DateNotifier::class, [], $new_data, 175 $new_data2, 'date' 176 ], 177 'Not ClosedStatusNotifier' => [ 178 ClosedStatusNotifier::class, [], $new_data, 179 $new_data2, 'closedstatus' 180 ], 181 'Not OpenStatusNotifier' => [ 182 OpenStatusNotifier::class, [], $old_data, 183 $old_data2, 'openstatus' 184 ], 185 'Not SelfRemovalNotifier' => [ 186 SelfRemovalNotifier::class, [], $old_data, 187 $new_data, 'self_removal' 188 ], 189 'Not SelfRemovalNotifier 2' => [ 190 SelfRemovalNotifier::class, [], 191 array_replace($old_data, ['assignees' => $all_but_editor]), 192 array_replace($new_data, ['assignees' => $all_but_editor]), 193 'self_removal' 194 ], 195 'Not DeletedNotifier' => [ 196 DeletedNotifier::class, [], $new_data, 197 $old_data, 'deleted' 198 ], 199 'Not DeletedNotifier 2' => [ 200 DeletedNotifier::class, [], $empty_data, $new_data, 'deleted' 201 ], 202 'New page AssignedNotifier' => [ 203 AssignedNotifier::class, $all_but_editor, $new_data, 204 array_replace($new_data, ['content' => '']), 'assigned' 205 ], 206 'New page DateNotifier' => [ 207 DateNotifier::class, [], $new_data, 208 array_replace($new_data, ['content' => '']), 'date' 209 ], 210 'New page ClosedStatusNotifier' => [ 211 ClosedStatusNotifier::class, [], $new_data, 212 array_replace($new_data, ['content' => '']), 'closedstatus' 213 ], 214 'New page OpenStatusNotifier' => [ 215 OpenStatusNotifier::class, [], $old_data, 216 array_replace($new_data, ['content' => '']), 'openstatus' 217 ], 218 'Delete page RemovedNotifier' => [ 219 RemovedNotifier::class, [], 220 $empty_data, $new_data, 'removed' 221 ], 222 'Delete page SelfRemovalNotifier' => [ 223 SelfRemovalNotifier::class, [], 224 $empty_data, $old_data, 'selfremoval' 225 ], 226 'Delete page DateNotifier' => [ 227 DateNotifier::class, [], $empty_data, $new_data, 228 'date' 229 ], 230 'Delete page ClosedStatusNotifier' => [ 231 ClosedStatusNotifier::class, [], $empty_data, $old_data, 232 'closedstatus' 233 ], 234 'Delete page OpenStatusNotifier' => [ 235 OpenStatusNotifier::class, [], $empty_data, $new_data, 236 'openstatus' 237 ], 238 'ReminderNotifier' => [ 239 ReminderNotifier::class, $old_data['assignees'], 240 array_replace($old_data, ['duedate' => $tomorrow]), 241 array_replace($old_data, ['duedate' => $tomorrow]), 242 'reminder' 243 ], 244 'Closed ReminderNotifier' => [ 245 ReminderNotifier::class, [], 246 array_replace($new_data, ['duedate' => $tomorrow]), 247 array_replace($new_data, ['duedate' => $tomorrow]), 248 'reminder' 249 ], 250 'Not ReminderNotifier' => [ 251 ReminderNotifier::class, [], $old_data, $old_data, 252 'reminder' 253 ], 254 'No Date ReminderNotifier' => [ 255 ReminderNotifier::class, [], 256 array_replace($old_data, ['duedate' => null, 'duedate_formatted' => '']), 257 array_replace($old_data, ['duedate' => null, 'duedate_formatted' => '']), 258 'reminder' 259 ], 260 'TodayNotifier' => [ 261 TodayNotifier::class, $old_data['assignees'], 262 array_replace($old_data, ['duedate' => $today]), 263 array_replace($old_data, ['duedate' => clone $today]), 264 'today' 265 ], 266 'Closed TodayNotifier' => [ 267 TodayNotifier::class, [], 268 array_replace($new_data, ['duedate' => $today]), 269 array_replace($new_data, ['duedate' => $today]), 270 'today' 271 ], 272 'Not TodayNotifier' => [ 273 TodayNotifier::class, [], $old_data, $old_data, 'today' 274 ], 275 'No Date TodayNotifier' => [ 276 TodayNotifier::class, [], 277 array_replace($old_data, ['duedate' => null, 'duedate_formatted' => '']), 278 array_replace($old_data, ['duedate' => null, 'duedate_formatted' => '']), 279 'today' 280 ], 281 'OverdueNotifier' => [ 282 OverdueNotifier::class, $old_data['assignees'], $old_data, 283 $old_data2, 'overdue' 284 ], 285 'Closed OverdueNotifier' => [ 286 OverdueNotifier::class, [], $new_data, $new_data, 'overdue' 287 ], 288 'Not OverdueNotifier' => [ 289 OverdueNotifier::class, [], 290 array_replace($old_data, ['duedate' => $tomorrow]), 291 array_replace($old_data, ['duedate' => $tomorrow]), 292 'overdue' 293 ], 294 'No Date OverdueNotifier' => [ 295 OverdueNotifier::class, [], 296 array_replace($old_data, ['duedate' => null, 'duedate_formatted' => '']), 297 array_replace($old_data, ['duedate' => null, 'duedate_formatted' => '']), 298 'overdue' 299 ], 300 ]; 301 } 302 303 /** 304 * @dataProvider provideNotifiers 305 */ 306 public function testNotifiers($notifier, $recipients, $new_data, $old_data, $key) { 307 $calls = count($recipients); 308 $mailer = $this->createMock(\Mailer::class); 309 $mailer->expects($this->exactly($calls)) 310 ->method('to') 311 ->withConsecutive(...array_chunk(array_map([$this, 'equalTo'], $recipients), 1)); 312 if ($calls > 0) { 313 $url = DOKU_URL . DOKU_SCRIPT . '?id=' . $this::page_id; 314 $text_replacements = [ 315 'TITLE' => $this::page_title, 316 'TITLELINK' => '"' . $this::page_title . "\" <${url}>", 317 'EDITURL' => "${url}&do=edit", 318 'EDITOR' => $this::editor, 319 'STATUS' => $new_data['status'], 320 'PREVSTATUS' => $old_data['status'], 321 'DUEDATE' => $new_data['duedate_formatted'], 322 'PREVDUEDATE' => $old_data['duedate_formatted'], 323 'DUEIN' => $notifier::dueIn($new_data['duedate']), 324 'WIKINAME' => 'My Test Wiki', 325 ]; 326 $html_replacements = [ 327 'TITLELINK' => "“<a href=\"${url}\">" . $this::page_title . '</a>”', 328 'EDITURL' => "<a href=\"${url}&do=edit\">edit the page</a>", 329 ]; 330 $expected_subject = "Check substitutions:"; 331 foreach($text_replacements as $t) { 332 $expected_subject .= "\n$t"; 333 } 334 $mailer->expects($this->once()) 335 ->method('setBody') 336 ->with($this->equalTo($this::email_text), 337 $this->equalTo($text_replacements), 338 $this->equalTo($html_replacements), 339 $this->equalTo($this::email_html)); 340 $mailer->expects($this->exactly($calls)) 341 ->method('subject') 342 ->with($this->equalTo($expected_subject)); 343 } else { 344 $mailer->expects($this->never()) 345 ->method('setBody'); 346 $mailer->expects($this->never()) 347 ->method('subject'); 348 } 349 $mailer->expects($this->exactly($calls))->method('send')->with(); 350 $n = new $notifier([$this, 'fakeGetConf'], $this->getLangCallback($key)); 351 $n->sendMessage( 352 $this::page_id, 353 $this::page_title, 354 $this::editor, 355 $this::editor_email, 356 $new_data, 357 $old_data, 358 $mailer 359 ); 360 } 361 362 public function test_empty_title() { 363 $old_data = [ 364 'content' => '====== Old Title ====== 365Brief description of the task.', 366 'duedate' => date_create('2023-03-12 12:00'), 367 'duedate_formatted' => '12 March 2023 12:00', 368 'assignees' => ['User 2 <u2@abc.com>', 'Some User <some.user@example.com>'], 369 'status' => 'Ongoing', 370 ]; 371 $new_data = ['content' => '', 'duedate' => date_create(''), 372 'duedate_formatted' => '', 'assignees' => [], 'status' => '']; 373 $mailer = $this->createMock(\Mailer::class); 374 $mailer->expects($this->once()) 375 ->method('to') 376 ->with($this->equalTo($old_data['assignees'][0])); 377 $url = DOKU_URL . DOKU_SCRIPT . '?id=' . $this::page_id; 378 $text_replacements = [ 379 'TITLE' => $this::page_id, 380 'TITLELINK' => '"' . $this::page_id . "\" <${url}>", 381 'EDITURL' => "${url}&do=edit", 382 'EDITOR' => $this::editor, 383 'STATUS' => $new_data['status'], 384 'PREVSTATUS' => $old_data['status'], 385 'DUEDATE' => $new_data['duedate_formatted'], 386 'PREVDUEDATE' => $old_data['duedate_formatted'], 387 'DUEIN' => DeletedNotifier::dueIn($new_data['duedate']), 388 'WIKINAME' => 'My Test Wiki', 389 ]; 390 $html_replacements = [ 391 'TITLELINK' => "“<a href=\"${url}\">" . $this::page_id . '</a>”', 392 'EDITURL' => "<a href=\"${url}&do=edit\">edit the page</a>", 393 ]; 394 $expected_subject = "Check substitutions:"; 395 foreach($text_replacements as $t) { 396 $expected_subject .= "\n$t"; 397 } 398 $mailer->expects($this->once()) 399 ->method('setBody') 400 ->with($this->equalTo($this::email_text), 401 $this->equalTo($text_replacements), 402 $this->equalTo($html_replacements), 403 $this->equalTo($this::email_html)); 404 $mailer->expects($this->once()) 405 ->method('subject') 406 ->with($this->equalTo($expected_subject)); 407 $mailer->expects($this->once())->method('send')->with(); 408 $n = new DeletedNotifier([$this, 'fakeGetConf'], $this->getLangCallback('deleted')); 409 $n->sendMessage( 410 $this::page_id, 411 '', 412 $this::editor, 413 $this::editor_email, 414 $new_data, 415 $old_data, 416 $mailer 417 ); 418 } 419} 420