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