1<?php 2 3namespace dokuwiki\plugin\annotations\test; 4 5use DokuWikiTest; 6 7/** 8 * Storage, CRUD, permission and orphan-detection tests for the annotations 9 * helper. The helper is pure logic, so most of this needs no HTTP request. 10 * 11 * @group plugin_annotations 12 * @group plugins 13 */ 14class HelperTest extends DokuWikiTest 15{ 16 protected $pluginsEnabled = ['annotations']; 17 18 /** 19 * @return \helper_plugin_annotations 20 */ 21 protected function helper() 22 { 23 return new \helper_plugin_annotations(); 24 } 25 26 // ----------------------------------------------------------------- 27 // Permission rules (pure functions) 28 // ----------------------------------------------------------------- 29 30 public function testCanAnnotateRequiresLoginAndRead(): void 31 { 32 $h = $this->helper(); 33 $this->assertFalse($h->canAnnotate('', AUTH_READ), 'anonymous may not annotate'); 34 $this->assertFalse($h->canAnnotate('alice', AUTH_NONE), 'no read access → no annotate'); 35 $this->assertTrue($h->canAnnotate('alice', AUTH_READ), 'logged in + read → annotate'); 36 $this->assertTrue($h->canAnnotate('alice', AUTH_EDIT), 'edit access implies read'); 37 } 38 39 public function testCanEditAnnotationAuthorOrAdmin(): void 40 { 41 $h = $this->helper(); 42 $ann = ['author' => 'alice']; 43 $this->assertTrue($h->canEditAnnotation($ann, 'alice', false), 'author may edit'); 44 $this->assertFalse($h->canEditAnnotation($ann, 'bob', false), 'non-author may not edit'); 45 $this->assertTrue($h->canEditAnnotation($ann, 'bob', true), 'admin may edit anyone'); 46 $this->assertFalse($h->canEditAnnotation($ann, '', true), 'anonymous never edits'); 47 } 48 49 public function testCanEditReplyAuthorOrAdmin(): void 50 { 51 $h = $this->helper(); 52 $reply = ['author' => 'alice']; 53 $this->assertTrue($h->canEditReply($reply, 'alice', false)); 54 $this->assertFalse($h->canEditReply($reply, 'bob', false)); 55 $this->assertTrue($h->canEditReply($reply, 'bob', true)); 56 } 57 58 public function testCanClearAdminOnly(): void 59 { 60 $h = $this->helper(); 61 $this->assertTrue($h->canClear(true)); 62 $this->assertFalse($h->canClear(false)); 63 } 64 65 // ----------------------------------------------------------------- 66 // Annotation CRUD 67 // ----------------------------------------------------------------- 68 69 public function testCreateGetAndStats(): void 70 { 71 $h = $this->helper(); 72 $id = 'anntest:crud'; 73 74 $ann = $h->createAnnotation($id, ['exact' => 'hello world'], 'alice', 'A comment'); 75 $this->assertIsArray($ann); 76 $this->assertNotEmpty($ann['id']); 77 $this->assertEquals('open', $ann['status']); 78 $this->assertEquals('alice', $ann['author']); 79 80 $this->assertCount(1, $h->getAnnotations($id)); 81 $this->assertEquals($ann['id'], $h->getAnnotation($id, $ann['id'])['id']); 82 $this->assertEquals(['total' => 1, 'open' => 1, 'resolved' => 0], $h->getStats($id)); 83 } 84 85 public function testCreateRejectsEmptyBodyAnchorOrAuthor(): void 86 { 87 $h = $this->helper(); 88 $id = 'anntest:reject'; 89 90 $this->assertFalse($h->createAnnotation($id, ['exact' => 'x'], 'alice', ' '), 'empty body'); 91 $this->assertFalse($h->createAnnotation($id, ['exact' => ''], 'alice', 'body'), 'empty exact'); 92 $this->assertFalse($h->createAnnotation($id, ['exact' => 'x'], '', 'body'), 'empty author'); 93 $this->assertSame([], $h->getAnnotations($id), 'nothing was stored'); 94 } 95 96 public function testBodyAndQuoteAreLengthCapped(): void 97 { 98 $h = $this->helper(); 99 $id = 'anntest:caps'; 100 101 $ann = $h->createAnnotation($id, ['exact' => str_repeat('q', 2000)], 'alice', str_repeat('x', 20000)); 102 $this->assertIsArray($ann); 103 $this->assertEquals(10000, mb_strlen($ann['body']), 'body capped at MAX_BODY'); 104 $this->assertEquals(1000, mb_strlen($ann['anchor']['exact']), 'quote capped at MAX_QUOTE'); 105 } 106 107 public function testWhitespaceNormalisedInAnchor(): void 108 { 109 $h = $this->helper(); 110 $id = 'anntest:ws'; 111 $ann = $h->createAnnotation($id, ['exact' => " foo\n\t bar "], 'alice', 'b'); 112 $this->assertEquals('foo bar', $ann['anchor']['exact']); 113 } 114 115 public function testUpdateAndDeleteAnnotation(): void 116 { 117 $h = $this->helper(); 118 $id = 'anntest:upd'; 119 $ann = $h->createAnnotation($id, ['exact' => 'foo'], 'alice', 'first'); 120 121 $this->assertTrue($h->updateAnnotationBody($id, $ann['id'], 'second')); 122 $this->assertEquals('second', $h->getAnnotation($id, $ann['id'])['body']); 123 $this->assertFalse($h->updateAnnotationBody($id, 'nope', 'x'), 'missing id → false'); 124 $this->assertFalse($h->updateAnnotationBody($id, $ann['id'], ' '), 'empty body → false'); 125 126 $this->assertTrue($h->deleteAnnotation($id, $ann['id'])); 127 $this->assertNull($h->getAnnotation($id, $ann['id'])); 128 $this->assertFalse($h->deleteAnnotation($id, $ann['id']), 'already gone → false'); 129 } 130 131 public function testStatusFlow(): void 132 { 133 $h = $this->helper(); 134 $id = 'anntest:status'; 135 $ann = $h->createAnnotation($id, ['exact' => 'foo'], 'alice', 'b'); 136 137 $this->assertTrue($h->setStatus($id, $ann['id'], 'resolved', 'bob')); 138 $resolved = $h->getAnnotation($id, $ann['id']); 139 $this->assertEquals('resolved', $resolved['status']); 140 $this->assertEquals('bob', $resolved['resolved_by']); 141 $this->assertGreaterThan(0, $resolved['resolved_at']); 142 143 $this->assertTrue($h->setStatus($id, $ann['id'], 'open', 'bob')); 144 $reopened = $h->getAnnotation($id, $ann['id']); 145 $this->assertEquals('open', $reopened['status']); 146 $this->assertEquals('', $reopened['resolved_by']); 147 148 $this->assertFalse($h->setStatus($id, $ann['id'], 'bogus', 'bob'), 'invalid status → false'); 149 } 150 151 // ----------------------------------------------------------------- 152 // Reply CRUD 153 // ----------------------------------------------------------------- 154 155 public function testReplyCrud(): void 156 { 157 $h = $this->helper(); 158 $id = 'anntest:reply'; 159 $ann = $h->createAnnotation($id, ['exact' => 'foo'], 'alice', 'b'); 160 161 $reply = $h->addReply($id, $ann['id'], 'bob', 'a reply'); 162 $this->assertIsArray($reply); 163 $this->assertNotEmpty($reply['id']); 164 $this->assertCount(1, $h->getAnnotation($id, $ann['id'])['replies']); 165 166 $this->assertTrue($h->updateReply($id, $ann['id'], $reply['id'], 'edited reply')); 167 $this->assertEquals('edited reply', $h->getAnnotation($id, $ann['id'])['replies'][0]['body']); 168 169 $this->assertTrue($h->deleteReply($id, $ann['id'], $reply['id'])); 170 $this->assertCount(0, $h->getAnnotation($id, $ann['id'])['replies']); 171 172 $this->assertFalse($h->addReply($id, 'missing-ann', 'bob', 'x'), 'reply to missing annotation → false'); 173 } 174 175 // ----------------------------------------------------------------- 176 // Bulk maintenance 177 // ----------------------------------------------------------------- 178 179 public function testClearResolved(): void 180 { 181 $h = $this->helper(); 182 $id = 'anntest:clearres'; 183 $keep = $h->createAnnotation($id, ['exact' => 'one'], 'alice', 'b1'); 184 $drop = $h->createAnnotation($id, ['exact' => 'two'], 'alice', 'b2'); 185 $h->setStatus($id, $drop['id'], 'resolved', 'alice'); 186 187 $this->assertEquals(1, $h->clearResolved($id)); 188 $remaining = $h->getAnnotations($id); 189 $this->assertCount(1, $remaining); 190 $this->assertEquals($keep['id'], $remaining[0]['id']); 191 } 192 193 // ----------------------------------------------------------------- 194 // Orphan detection against a rendered page 195 // ----------------------------------------------------------------- 196 197 public function testFindAndClearOrphanedAgainstRenderedPage(): void 198 { 199 $id = 'anntest:orphan'; 200 saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup'); 201 202 $h = $this->helper(); 203 $present = $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present'); 204 $gone = $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone'); 205 206 $orphanIds = array_map(static function ($a) { 207 return $a['id']; 208 }, $h->findOrphaned($id)); 209 210 $this->assertContains($gone['id'], $orphanIds, 'a missing quote is orphaned'); 211 $this->assertNotContains($present['id'], $orphanIds, 'a present quote is not orphaned'); 212 213 $this->assertEquals(1, $h->clearOrphaned($id), 'only the orphan is cleared'); 214 $remaining = $h->getAnnotations($id); 215 $this->assertCount(1, $remaining); 216 $this->assertEquals($present['id'], $remaining[0]['id']); 217 } 218 219 // ----------------------------------------------------------------- 220 // Admin overview (enumeration & counts) 221 // ----------------------------------------------------------------- 222 223 public function testGetAnnotatedPages(): void 224 { 225 $h = $this->helper(); 226 $h->createAnnotation('anntest:listone', ['exact' => 'foo'], 'alice', 'b'); 227 $h->createAnnotation('anntest:nested:listtwo', ['exact' => 'bar'], 'alice', 'b'); 228 229 // a page whose only annotation is then deleted leaves an empty file and 230 // must NOT be listed 231 $gone = $h->createAnnotation('anntest:emptied', ['exact' => 'baz'], 'alice', 'b'); 232 $h->deleteAnnotation('anntest:emptied', $gone['id']); 233 234 $pages = $h->getAnnotatedPages(); 235 $this->assertContains('anntest:listone', $pages); 236 $this->assertContains('anntest:nested:listtwo', $pages); 237 $this->assertNotContains('anntest:emptied', $pages, 'emptied page is excluded'); 238 } 239 240 public function testPageCountsSplitsNormalAndOrphaned(): void 241 { 242 $id = 'anntest:counts'; 243 saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup'); 244 245 $h = $this->helper(); 246 $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present'); 247 $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone'); 248 249 $this->assertEquals( 250 ['total' => 2, 'normal' => 1, 'orphaned' => 1], 251 $h->pageCounts($id) 252 ); 253 } 254 255 public function testPageCountsEmptyForUnannotatedPage(): void 256 { 257 $h = $this->helper(); 258 $this->assertEquals( 259 ['total' => 0, 'normal' => 0, 'orphaned' => 0], 260 $h->pageCounts('anntest:never') 261 ); 262 } 263 264 public function testClearOrphanedAllAcrossPages(): void 265 { 266 $h = $this->helper(); 267 $ids = ['anntest:sweepa', 'anntest:sweepb']; 268 foreach ($ids as $id) { 269 saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup'); 270 $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present'); 271 $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone'); 272 } 273 274 // clearOrphanedAll() is wiki-wide and the shared test data dir may carry 275 // annotated pages from earlier tests, so assert the seeded orphans are 276 // included (>= 2) rather than an exact global total, and verify each of 277 // our pages keeps exactly its one present annotation. 278 $removed = $h->clearOrphanedAll(); 279 $this->assertGreaterThanOrEqual(2, $removed, 'at least the two seeded orphans are cleared'); 280 foreach ($ids as $id) { 281 $remaining = $h->getAnnotations($id); 282 $this->assertCount(1, $remaining, $id . ' keeps only the present annotation'); 283 $this->assertEquals('present', $remaining[0]['body'], $id . ' kept the right annotation'); 284 $this->assertSame(0, $h->pageCounts($id)['orphaned'], $id . ' has no orphans left'); 285 } 286 } 287} 288