xref: /plugin/acknowledge/_test/NotificationIntegrationTest.php (revision 4bf952c9005c7028e75b1ab7185e9d7f7b566d80)
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        $pages = "REPLACE INTO pages(page,lastmod)
57            VALUES ('wiki:due_new', 1000),
58            ('wiki:done', 1000),
59            ('wiki:outdated', 2000)";
60        $this->db->exec($pages);
61
62        $assignments = "REPLACE INTO assignments(page,pageassignees)
63            VALUES ('wiki:due_new', '@staff'),
64            ('wiki:done', '@staff'),
65            ('wiki:outdated', '@staff')";
66        $this->db->exec($assignments);
67
68        $acks = "REPLACE INTO acks(page,user,ack)
69            VALUES ('wiki:done', 'alice', 2000),
70            ('wiki:outdated', 'alice', 1000)";
71        $this->db->exec($acks);
72    }
73
74    /**
75     * Invoke the real gather handler for a user and return the produced notifications.
76     *
77     * @param string $user
78     * @return array
79     */
80    protected function gather($user)
81    {
82        $data = [
83            'plugins' => ['acknowledge'],
84            'user' => $user,
85            'notifications' => [],
86        ];
87        $event = new Event('PLUGIN_NOTIFICATION_GATHER', $data);
88
89        /** @var \action_plugin_acknowledge_notification $action */
90        $action = plugin_load('action', 'acknowledge_notification');
91        $action->gatherNotifications($event);
92
93        return $data['notifications'];
94    }
95
96    /**
97     * Due and outdated pages are notified; up-to-date pages are not. The id is "$page:$lastmod".
98     */
99    public function testDueAndOutdatedNotifiedCurrentNot()
100    {
101        $ids = array_column($this->gather('alice'), 'id');
102        sort($ids);
103
104        $this->assertEquals(['wiki:due_new:1000', 'wiki:outdated:2000'], $ids);
105    }
106
107    /**
108     * The notification carries the assigned plugin name and a rendered link.
109     */
110    public function testNotificationShape()
111    {
112        $notifications = $this->gather('alice');
113        $this->assertNotEmpty($notifications);
114
115        $notification = $notifications[0];
116        $this->assertEquals('acknowledge', $notification['plugin']);
117        $this->assertStringContainsString('href', $notification['full']);
118        $this->assertIsInt($notification['timestamp']);
119    }
120
121    /**
122     * A page edit (new lastmod) yields a new id, so the user is re-notified after re-ack falls due.
123     */
124    public function testPageChangeProducesNewId()
125    {
126        $before = array_column($this->gather('alice'), 'id');
127        $this->assertContains('wiki:due_new:1000', $before);
128
129        // simulate a page edit bumping the stored modification date
130        $this->db->exec("UPDATE pages SET lastmod = 1500 WHERE page = 'wiki:due_new'");
131
132        $after = array_column($this->gather('alice'), 'id');
133        $this->assertContains('wiki:due_new:1500', $after);
134        $this->assertNotContains('wiki:due_new:1000', $after);
135    }
136
137    /**
138     * Changing assignment rules (without a page edit) keeps the same id, so dedup prevents re-notify.
139     */
140    public function testAssignmentChurnKeepsSameId()
141    {
142        $before = array_column($this->gather('alice'), 'id');
143        $this->assertContains('wiki:due_new:1000', $before);
144
145        // rule churn: widen the assignees but leave lastmod untouched
146        $this->db->exec(
147            "REPLACE INTO assignments(page,pageassignees) VALUES ('wiki:due_new', '@staff,@other')"
148        );
149
150        $after = array_column($this->gather('alice'), 'id');
151        $this->assertContains('wiki:due_new:1000', $after);
152    }
153
154    /**
155     * With the integration disabled, the gather handler produces nothing.
156     */
157    public function testDisabledIntegrationProducesNothing()
158    {
159        global $conf;
160        $conf['plugin']['acknowledge']['notification_integration'] = 0;
161
162        $this->assertEquals([], $this->gather('alice'));
163    }
164
165    /**
166     * A page blocked by approve is not notified until it is approved.
167     */
168    public function testApproveBlockedPageNotNotifiedUntilApproved()
169    {
170        $id = 'approved:doc';
171        saveWikiText($id, 'content', 'test');
172        $this->approve->handlePageEdit($id);
173        $lastmod = (int) @filemtime(wikiFN($id));
174
175        $this->db->exec("REPLACE INTO pages(page,lastmod) VALUES (?, ?)", [$id, $lastmod]);
176        $this->db->exec("REPLACE INTO assignments(page,pageassignees) VALUES (?, '@staff')", [$id]);
177
178        // still a draft -> blocked -> not notified
179        $ids = array_column($this->gather('alice'), 'id');
180        $this->assertNotContains($id . ':' . $lastmod, $ids);
181
182        // once approved -> notified
183        $this->approve->setApprovedStatus($id);
184        $ids = array_column($this->gather('alice'), 'id');
185        $this->assertContains($id . ':' . $lastmod, $ids);
186    }
187}
188