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