1*da56206cStracker-user<?php 2*da56206cStracker-user 3*da56206cStracker-usernamespace dokuwiki\plugin\annotations\test; 4*da56206cStracker-user 5*da56206cStracker-useruse DokuWikiTest; 6*da56206cStracker-user 7*da56206cStracker-user/** 8*da56206cStracker-user * Storage, CRUD, permission and orphan-detection tests for the annotations 9*da56206cStracker-user * helper. The helper is pure logic, so most of this needs no HTTP request. 10*da56206cStracker-user * 11*da56206cStracker-user * @group plugin_annotations 12*da56206cStracker-user * @group plugins 13*da56206cStracker-user */ 14*da56206cStracker-userclass HelperTest extends DokuWikiTest 15*da56206cStracker-user{ 16*da56206cStracker-user protected $pluginsEnabled = ['annotations']; 17*da56206cStracker-user 18*da56206cStracker-user /** 19*da56206cStracker-user * @return \helper_plugin_annotations 20*da56206cStracker-user */ 21*da56206cStracker-user protected function helper() 22*da56206cStracker-user { 23*da56206cStracker-user return new \helper_plugin_annotations(); 24*da56206cStracker-user } 25*da56206cStracker-user 26*da56206cStracker-user // ----------------------------------------------------------------- 27*da56206cStracker-user // Permission rules (pure functions) 28*da56206cStracker-user // ----------------------------------------------------------------- 29*da56206cStracker-user 30*da56206cStracker-user public function testCanAnnotateRequiresLoginAndRead(): void 31*da56206cStracker-user { 32*da56206cStracker-user $h = $this->helper(); 33*da56206cStracker-user $this->assertFalse($h->canAnnotate('', AUTH_READ), 'anonymous may not annotate'); 34*da56206cStracker-user $this->assertFalse($h->canAnnotate('alice', AUTH_NONE), 'no read access → no annotate'); 35*da56206cStracker-user $this->assertTrue($h->canAnnotate('alice', AUTH_READ), 'logged in + read → annotate'); 36*da56206cStracker-user $this->assertTrue($h->canAnnotate('alice', AUTH_EDIT), 'edit access implies read'); 37*da56206cStracker-user } 38*da56206cStracker-user 39*da56206cStracker-user public function testCanEditAnnotationAuthorOrAdmin(): void 40*da56206cStracker-user { 41*da56206cStracker-user $h = $this->helper(); 42*da56206cStracker-user $ann = ['author' => 'alice']; 43*da56206cStracker-user $this->assertTrue($h->canEditAnnotation($ann, 'alice', false), 'author may edit'); 44*da56206cStracker-user $this->assertFalse($h->canEditAnnotation($ann, 'bob', false), 'non-author may not edit'); 45*da56206cStracker-user $this->assertTrue($h->canEditAnnotation($ann, 'bob', true), 'admin may edit anyone'); 46*da56206cStracker-user $this->assertFalse($h->canEditAnnotation($ann, '', true), 'anonymous never edits'); 47*da56206cStracker-user } 48*da56206cStracker-user 49*da56206cStracker-user public function testCanEditReplyAuthorOrAdmin(): void 50*da56206cStracker-user { 51*da56206cStracker-user $h = $this->helper(); 52*da56206cStracker-user $reply = ['author' => 'alice']; 53*da56206cStracker-user $this->assertTrue($h->canEditReply($reply, 'alice', false)); 54*da56206cStracker-user $this->assertFalse($h->canEditReply($reply, 'bob', false)); 55*da56206cStracker-user $this->assertTrue($h->canEditReply($reply, 'bob', true)); 56*da56206cStracker-user } 57*da56206cStracker-user 58*da56206cStracker-user public function testCanClearAdminOnly(): void 59*da56206cStracker-user { 60*da56206cStracker-user $h = $this->helper(); 61*da56206cStracker-user $this->assertTrue($h->canClear(true)); 62*da56206cStracker-user $this->assertFalse($h->canClear(false)); 63*da56206cStracker-user } 64*da56206cStracker-user 65*da56206cStracker-user // ----------------------------------------------------------------- 66*da56206cStracker-user // Annotation CRUD 67*da56206cStracker-user // ----------------------------------------------------------------- 68*da56206cStracker-user 69*da56206cStracker-user public function testCreateGetAndStats(): void 70*da56206cStracker-user { 71*da56206cStracker-user $h = $this->helper(); 72*da56206cStracker-user $id = 'anntest:crud'; 73*da56206cStracker-user 74*da56206cStracker-user $ann = $h->createAnnotation($id, ['exact' => 'hello world'], 'alice', 'A comment'); 75*da56206cStracker-user $this->assertIsArray($ann); 76*da56206cStracker-user $this->assertNotEmpty($ann['id']); 77*da56206cStracker-user $this->assertEquals('open', $ann['status']); 78*da56206cStracker-user $this->assertEquals('alice', $ann['author']); 79*da56206cStracker-user 80*da56206cStracker-user $this->assertCount(1, $h->getAnnotations($id)); 81*da56206cStracker-user $this->assertEquals($ann['id'], $h->getAnnotation($id, $ann['id'])['id']); 82*da56206cStracker-user $this->assertEquals(['total' => 1, 'open' => 1, 'resolved' => 0], $h->getStats($id)); 83*da56206cStracker-user } 84*da56206cStracker-user 85*da56206cStracker-user public function testCreateRejectsEmptyBodyAnchorOrAuthor(): void 86*da56206cStracker-user { 87*da56206cStracker-user $h = $this->helper(); 88*da56206cStracker-user $id = 'anntest:reject'; 89*da56206cStracker-user 90*da56206cStracker-user $this->assertFalse($h->createAnnotation($id, ['exact' => 'x'], 'alice', ' '), 'empty body'); 91*da56206cStracker-user $this->assertFalse($h->createAnnotation($id, ['exact' => ''], 'alice', 'body'), 'empty exact'); 92*da56206cStracker-user $this->assertFalse($h->createAnnotation($id, ['exact' => 'x'], '', 'body'), 'empty author'); 93*da56206cStracker-user $this->assertSame([], $h->getAnnotations($id), 'nothing was stored'); 94*da56206cStracker-user } 95*da56206cStracker-user 96*da56206cStracker-user public function testBodyAndQuoteAreLengthCapped(): void 97*da56206cStracker-user { 98*da56206cStracker-user $h = $this->helper(); 99*da56206cStracker-user $id = 'anntest:caps'; 100*da56206cStracker-user 101*da56206cStracker-user $ann = $h->createAnnotation($id, ['exact' => str_repeat('q', 2000)], 'alice', str_repeat('x', 20000)); 102*da56206cStracker-user $this->assertIsArray($ann); 103*da56206cStracker-user $this->assertEquals(10000, mb_strlen($ann['body']), 'body capped at MAX_BODY'); 104*da56206cStracker-user $this->assertEquals(1000, mb_strlen($ann['anchor']['exact']), 'quote capped at MAX_QUOTE'); 105*da56206cStracker-user } 106*da56206cStracker-user 107*da56206cStracker-user public function testWhitespaceNormalisedInAnchor(): void 108*da56206cStracker-user { 109*da56206cStracker-user $h = $this->helper(); 110*da56206cStracker-user $id = 'anntest:ws'; 111*da56206cStracker-user $ann = $h->createAnnotation($id, ['exact' => " foo\n\t bar "], 'alice', 'b'); 112*da56206cStracker-user $this->assertEquals('foo bar', $ann['anchor']['exact']); 113*da56206cStracker-user } 114*da56206cStracker-user 115*da56206cStracker-user public function testUpdateAndDeleteAnnotation(): void 116*da56206cStracker-user { 117*da56206cStracker-user $h = $this->helper(); 118*da56206cStracker-user $id = 'anntest:upd'; 119*da56206cStracker-user $ann = $h->createAnnotation($id, ['exact' => 'foo'], 'alice', 'first'); 120*da56206cStracker-user 121*da56206cStracker-user $this->assertTrue($h->updateAnnotationBody($id, $ann['id'], 'second')); 122*da56206cStracker-user $this->assertEquals('second', $h->getAnnotation($id, $ann['id'])['body']); 123*da56206cStracker-user $this->assertFalse($h->updateAnnotationBody($id, 'nope', 'x'), 'missing id → false'); 124*da56206cStracker-user $this->assertFalse($h->updateAnnotationBody($id, $ann['id'], ' '), 'empty body → false'); 125*da56206cStracker-user 126*da56206cStracker-user $this->assertTrue($h->deleteAnnotation($id, $ann['id'])); 127*da56206cStracker-user $this->assertNull($h->getAnnotation($id, $ann['id'])); 128*da56206cStracker-user $this->assertFalse($h->deleteAnnotation($id, $ann['id']), 'already gone → false'); 129*da56206cStracker-user } 130*da56206cStracker-user 131*da56206cStracker-user public function testStatusFlow(): void 132*da56206cStracker-user { 133*da56206cStracker-user $h = $this->helper(); 134*da56206cStracker-user $id = 'anntest:status'; 135*da56206cStracker-user $ann = $h->createAnnotation($id, ['exact' => 'foo'], 'alice', 'b'); 136*da56206cStracker-user 137*da56206cStracker-user $this->assertTrue($h->setStatus($id, $ann['id'], 'resolved', 'bob')); 138*da56206cStracker-user $resolved = $h->getAnnotation($id, $ann['id']); 139*da56206cStracker-user $this->assertEquals('resolved', $resolved['status']); 140*da56206cStracker-user $this->assertEquals('bob', $resolved['resolved_by']); 141*da56206cStracker-user $this->assertGreaterThan(0, $resolved['resolved_at']); 142*da56206cStracker-user 143*da56206cStracker-user $this->assertTrue($h->setStatus($id, $ann['id'], 'open', 'bob')); 144*da56206cStracker-user $reopened = $h->getAnnotation($id, $ann['id']); 145*da56206cStracker-user $this->assertEquals('open', $reopened['status']); 146*da56206cStracker-user $this->assertEquals('', $reopened['resolved_by']); 147*da56206cStracker-user 148*da56206cStracker-user $this->assertFalse($h->setStatus($id, $ann['id'], 'bogus', 'bob'), 'invalid status → false'); 149*da56206cStracker-user } 150*da56206cStracker-user 151*da56206cStracker-user // ----------------------------------------------------------------- 152*da56206cStracker-user // Reply CRUD 153*da56206cStracker-user // ----------------------------------------------------------------- 154*da56206cStracker-user 155*da56206cStracker-user public function testReplyCrud(): void 156*da56206cStracker-user { 157*da56206cStracker-user $h = $this->helper(); 158*da56206cStracker-user $id = 'anntest:reply'; 159*da56206cStracker-user $ann = $h->createAnnotation($id, ['exact' => 'foo'], 'alice', 'b'); 160*da56206cStracker-user 161*da56206cStracker-user $reply = $h->addReply($id, $ann['id'], 'bob', 'a reply'); 162*da56206cStracker-user $this->assertIsArray($reply); 163*da56206cStracker-user $this->assertNotEmpty($reply['id']); 164*da56206cStracker-user $this->assertCount(1, $h->getAnnotation($id, $ann['id'])['replies']); 165*da56206cStracker-user 166*da56206cStracker-user $this->assertTrue($h->updateReply($id, $ann['id'], $reply['id'], 'edited reply')); 167*da56206cStracker-user $this->assertEquals('edited reply', $h->getAnnotation($id, $ann['id'])['replies'][0]['body']); 168*da56206cStracker-user 169*da56206cStracker-user $this->assertTrue($h->deleteReply($id, $ann['id'], $reply['id'])); 170*da56206cStracker-user $this->assertCount(0, $h->getAnnotation($id, $ann['id'])['replies']); 171*da56206cStracker-user 172*da56206cStracker-user $this->assertFalse($h->addReply($id, 'missing-ann', 'bob', 'x'), 'reply to missing annotation → false'); 173*da56206cStracker-user } 174*da56206cStracker-user 175*da56206cStracker-user // ----------------------------------------------------------------- 176*da56206cStracker-user // Bulk maintenance 177*da56206cStracker-user // ----------------------------------------------------------------- 178*da56206cStracker-user 179*da56206cStracker-user public function testClearResolved(): void 180*da56206cStracker-user { 181*da56206cStracker-user $h = $this->helper(); 182*da56206cStracker-user $id = 'anntest:clearres'; 183*da56206cStracker-user $keep = $h->createAnnotation($id, ['exact' => 'one'], 'alice', 'b1'); 184*da56206cStracker-user $drop = $h->createAnnotation($id, ['exact' => 'two'], 'alice', 'b2'); 185*da56206cStracker-user $h->setStatus($id, $drop['id'], 'resolved', 'alice'); 186*da56206cStracker-user 187*da56206cStracker-user $this->assertEquals(1, $h->clearResolved($id)); 188*da56206cStracker-user $remaining = $h->getAnnotations($id); 189*da56206cStracker-user $this->assertCount(1, $remaining); 190*da56206cStracker-user $this->assertEquals($keep['id'], $remaining[0]['id']); 191*da56206cStracker-user } 192*da56206cStracker-user 193*da56206cStracker-user // ----------------------------------------------------------------- 194*da56206cStracker-user // Orphan detection against a rendered page 195*da56206cStracker-user // ----------------------------------------------------------------- 196*da56206cStracker-user 197*da56206cStracker-user public function testFindAndClearOrphanedAgainstRenderedPage(): void 198*da56206cStracker-user { 199*da56206cStracker-user $id = 'anntest:orphan'; 200*da56206cStracker-user saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup'); 201*da56206cStracker-user 202*da56206cStracker-user $h = $this->helper(); 203*da56206cStracker-user $present = $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present'); 204*da56206cStracker-user $gone = $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone'); 205*da56206cStracker-user 206*da56206cStracker-user $orphanIds = array_map(static function ($a) { 207*da56206cStracker-user return $a['id']; 208*da56206cStracker-user }, $h->findOrphaned($id)); 209*da56206cStracker-user 210*da56206cStracker-user $this->assertContains($gone['id'], $orphanIds, 'a missing quote is orphaned'); 211*da56206cStracker-user $this->assertNotContains($present['id'], $orphanIds, 'a present quote is not orphaned'); 212*da56206cStracker-user 213*da56206cStracker-user $this->assertEquals(1, $h->clearOrphaned($id), 'only the orphan is cleared'); 214*da56206cStracker-user $remaining = $h->getAnnotations($id); 215*da56206cStracker-user $this->assertCount(1, $remaining); 216*da56206cStracker-user $this->assertEquals($present['id'], $remaining[0]['id']); 217*da56206cStracker-user } 218*da56206cStracker-user} 219