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' => "&ldquo;<a href=\"${url}\">" . $this::page_title . '</a>&rdquo;',
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' => "&ldquo;<a href=\"${url}\">" . $this::page_id . '</a>&rdquo;',
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