xref: /plugin/annotations/_test/HelperTest.php (revision 9fd890c3d28899ac6132f5f0d76a031cc5c27f1a)
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