xref: /plugin/acknowledge/_test/NotificationIntegrationTest.php (revision b55c1d2d1a0da3f1d0a253b758a162b4c0c2a0a0)
1<?php
2
3namespace dokuwiki\plugin\acknowledge\test;
4
5use DokuWikiTest;
6use dokuwiki\Extension\Event;
7
8/**
9 * Tests for the notification plugin integration (action/notification.php).
10 *
11 * The notification plugin is pull-based: it fires PLUGIN_NOTIFICATION_GATHER per user and
12 * deduplicates the returned notifications by (plugin, id, user) permanently. These tests drive
13 * the gather handler directly and assert on the produced notification ids, which encode the
14 * "$page:$lastmod" semantics that define when a user is (re-)notified.
15 *
16 * @group plugin_acknowledge
17 * @group plugins
18 */
19class NotificationIntegrationTest extends DokuWikiTest
20{
21    /** @var array */
22    protected $pluginsEnabled = ['acknowledge', 'sqlite', 'approve'];
23
24    /** @var \helper_plugin_acknowledge */
25    protected $helper;
26
27    /** @var \dokuwiki\plugin\sqlite\SQLiteDB */
28    protected $db;
29
30    /** @var \helper_plugin_approve_db */
31    protected $approve;
32
33    public static function setUpBeforeClass(): void
34    {
35        parent::setUpBeforeClass();
36        /** @var \auth_plugin_authplain $auth */
37        global $auth;
38        $auth->createUser('alice', 'none', 'alice', 'alice@example.com', ['staff']);
39    }
40
41    public function setUp(): void
42    {
43        parent::setUp();
44
45        // setApprovedStatus() records the approving user from $INFO
46        global $INFO;
47        $INFO['client'] = 'someapprover';
48
49        $this->helper = plugin_load('helper', 'acknowledge');
50        $this->db = $this->helper->getDB();
51        /** @var \helper_plugin_approve_db approve */
52        $this->approve = plugin_load('helper', 'approve_db');
53        $this->approve->addMaintainer('approved:**', 'someapprover');
54
55        // due (never acked), current (acked >= lastmod), outdated (acked < lastmod).
56        // These are virtual pages with no changelog, so storePageDate() simply records the lastmod.
57        $this->helper->storePageDate('wiki:due_new', 1000, '');
58        $this->helper->storePageDate('wiki:done', 1000, '');
59        $this->helper->storePageDate('wiki:outdated', 2000, '');
60
61        $this->helper->setPageAssignees('wiki:due_new', '@staff');
62        $this->helper->setPageAssignees('wiki:done', '@staff');
63        $this->helper->setPageAssignees('wiki:outdated', '@staff');
64
65        $this->helper->importAcknowledgement('wiki:done', 'alice', 2000);
66        $this->helper->importAcknowledgement('wiki:outdated', 'alice', 1000);
67    }
68
69    /**
70     * Invoke the real gather handler for a user and return the produced notifications.
71     *
72     * @param string $user
73     * @return array
74     */
75    protected function gather($user)
76    {
77        $data = [
78            'plugins' => ['acknowledge'],
79            'user' => $user,
80            'notifications' => [],
81        ];
82        $event = new Event('PLUGIN_NOTIFICATION_GATHER', $data);
83
84        /** @var \action_plugin_acknowledge_notification $action */
85        $action = plugin_load('action', 'acknowledge_notification');
86        $action->gatherNotifications($event);
87
88        return $data['notifications'];
89    }
90
91    /**
92     * Due and outdated pages are notified; up-to-date pages are not. The id is "$page:$lastmod".
93     */
94    public function testDueAndOutdatedNotifiedCurrentNot()
95    {
96        $ids = array_column($this->gather('alice'), 'id');
97        sort($ids);
98
99        $this->assertEquals(['wiki:due_new:1000', 'wiki:outdated:2000'], $ids);
100    }
101
102    /**
103     * The notification carries the assigned plugin name and a rendered link.
104     */
105    public function testNotificationShape()
106    {
107        $notifications = $this->gather('alice');
108        $this->assertNotEmpty($notifications);
109
110        $notification = $notifications[0];
111        $this->assertEquals('acknowledge', $notification['plugin']);
112        $this->assertStringContainsString('href', $notification['full']);
113        $this->assertIsInt($notification['timestamp']);
114    }
115
116    /**
117     * A page edit (new lastmod) yields a new id, so the user is re-notified after re-ack falls due.
118     */
119    public function testPageChangeProducesNewId()
120    {
121        $before = array_column($this->gather('alice'), 'id');
122        $this->assertContains('wiki:due_new:1000', $before);
123
124        // simulate a page edit bumping the stored modification date
125        $this->helper->storePageDate('wiki:due_new', 1500, 'edited');
126
127        $after = array_column($this->gather('alice'), 'id');
128        $this->assertContains('wiki:due_new:1500', $after);
129        $this->assertNotContains('wiki:due_new:1000', $after);
130    }
131
132    /**
133     * Changing assignment rules (without a page edit) keeps the same id, so dedup prevents re-notify.
134     */
135    public function testAssignmentChurnKeepsSameId()
136    {
137        $before = array_column($this->gather('alice'), 'id');
138        $this->assertContains('wiki:due_new:1000', $before);
139
140        // rule churn: widen the assignees but leave lastmod untouched
141        $this->helper->setPageAssignees('wiki:due_new', '@staff,@other');
142
143        $after = array_column($this->gather('alice'), 'id');
144        $this->assertContains('wiki:due_new:1000', $after);
145    }
146
147    /**
148     * With the integration disabled, the gather handler produces nothing.
149     */
150    public function testDisabledIntegrationProducesNothing()
151    {
152        global $conf;
153        $conf['plugin']['acknowledge']['notification_integration'] = 0;
154
155        $this->assertEquals([], $this->gather('alice'));
156    }
157
158    /**
159     * A page blocked by approve is not notified until it is approved.
160     */
161    public function testApproveBlockedPageNotNotifiedUntilApproved()
162    {
163        $id = 'approved:doc';
164        saveWikiText($id, 'content', 'test');
165        $this->approve->handlePageEdit($id);
166        $lastmod = (int) @filemtime(wikiFN($id));
167
168        // real saved page: pin the pages row to the exact filemtime directly, since
169        // storePageDate() would compare content and may skip the write
170        $this->db->exec("REPLACE INTO pages(page,lastmod) VALUES (?, ?)", [$id, $lastmod]);
171        $this->helper->setPageAssignees($id, '@staff');
172
173        // still a draft -> blocked -> not notified
174        $ids = array_column($this->gather('alice'), 'id');
175        $this->assertNotContains($id . ':' . $lastmod, $ids);
176
177        // once approved -> notified
178        $this->approve->setApprovedStatus($id);
179        $ids = array_column($this->gather('alice'), 'id');
180        $this->assertContains($id . ':' . $lastmod, $ids);
181    }
182}
183