xref: /plugin/annotations/_test/HelperTest.php (revision 72d60f2d94b24cb66fabf596a2ec440f459ba88f)
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
193*72d60f2dStracker-user    public function testClearResolvedAllAcrossPages(): void
194*72d60f2dStracker-user    {
195*72d60f2dStracker-user        $h = $this->helper();
196*72d60f2dStracker-user        $ids = ['anntest:resweepa', 'anntest:resweepb'];
197*72d60f2dStracker-user        foreach ($ids as $id) {
198*72d60f2dStracker-user            $h->createAnnotation($id, ['exact' => 'keep open'], 'alice', 'open');
199*72d60f2dStracker-user            $done = $h->createAnnotation($id, ['exact' => 'mark done'], 'alice', 'done');
200*72d60f2dStracker-user            $h->setStatus($id, $done['id'], 'resolved', 'alice');
201*72d60f2dStracker-user        }
202*72d60f2dStracker-user
203*72d60f2dStracker-user        // clearResolvedAll() is wiki-wide and the shared test data dir may carry
204*72d60f2dStracker-user        // resolved annotations from earlier tests, so assert the seeded ones are
205*72d60f2dStracker-user        // included (>= 2) rather than an exact global total, and verify each of
206*72d60f2dStracker-user        // our pages keeps exactly its one open annotation.
207*72d60f2dStracker-user        $removed = $h->clearResolvedAll();
208*72d60f2dStracker-user        $this->assertGreaterThanOrEqual(2, $removed, 'at least the two seeded resolved are cleared');
209*72d60f2dStracker-user        foreach ($ids as $id) {
210*72d60f2dStracker-user            $remaining = $h->getAnnotations($id);
211*72d60f2dStracker-user            $this->assertCount(1, $remaining, $id . ' keeps only the open annotation');
212*72d60f2dStracker-user            $this->assertEquals('open', $remaining[0]['body'], $id . ' kept the right annotation');
213*72d60f2dStracker-user        }
214*72d60f2dStracker-user    }
215*72d60f2dStracker-user
216da56206cStracker-user    // -----------------------------------------------------------------
217da56206cStracker-user    //  Orphan detection against a rendered page
218da56206cStracker-user    // -----------------------------------------------------------------
219da56206cStracker-user
220da56206cStracker-user    public function testFindAndClearOrphanedAgainstRenderedPage(): void
221da56206cStracker-user    {
222da56206cStracker-user        $id = 'anntest:orphan';
223da56206cStracker-user        saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup');
224da56206cStracker-user
225da56206cStracker-user        $h = $this->helper();
226da56206cStracker-user        $present = $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present');
227da56206cStracker-user        $gone    = $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone');
228da56206cStracker-user
229da56206cStracker-user        $orphanIds = array_map(static function ($a) {
230da56206cStracker-user            return $a['id'];
231da56206cStracker-user        }, $h->findOrphaned($id));
232da56206cStracker-user
233da56206cStracker-user        $this->assertContains($gone['id'], $orphanIds, 'a missing quote is orphaned');
234da56206cStracker-user        $this->assertNotContains($present['id'], $orphanIds, 'a present quote is not orphaned');
235da56206cStracker-user
236da56206cStracker-user        $this->assertEquals(1, $h->clearOrphaned($id), 'only the orphan is cleared');
237da56206cStracker-user        $remaining = $h->getAnnotations($id);
238da56206cStracker-user        $this->assertCount(1, $remaining);
239da56206cStracker-user        $this->assertEquals($present['id'], $remaining[0]['id']);
240da56206cStracker-user    }
2419fd890c3Stracker-user
2429fd890c3Stracker-user    // -----------------------------------------------------------------
2439fd890c3Stracker-user    //  Admin overview (enumeration & counts)
2449fd890c3Stracker-user    // -----------------------------------------------------------------
2459fd890c3Stracker-user
2469fd890c3Stracker-user    public function testGetAnnotatedPages(): void
2479fd890c3Stracker-user    {
2489fd890c3Stracker-user        $h = $this->helper();
2499fd890c3Stracker-user        $h->createAnnotation('anntest:listone', ['exact' => 'foo'], 'alice', 'b');
2509fd890c3Stracker-user        $h->createAnnotation('anntest:nested:listtwo', ['exact' => 'bar'], 'alice', 'b');
2519fd890c3Stracker-user
2529fd890c3Stracker-user        // a page whose only annotation is then deleted leaves an empty file and
2539fd890c3Stracker-user        // must NOT be listed
2549fd890c3Stracker-user        $gone = $h->createAnnotation('anntest:emptied', ['exact' => 'baz'], 'alice', 'b');
2559fd890c3Stracker-user        $h->deleteAnnotation('anntest:emptied', $gone['id']);
2569fd890c3Stracker-user
2579fd890c3Stracker-user        $pages = $h->getAnnotatedPages();
2589fd890c3Stracker-user        $this->assertContains('anntest:listone', $pages);
2599fd890c3Stracker-user        $this->assertContains('anntest:nested:listtwo', $pages);
2609fd890c3Stracker-user        $this->assertNotContains('anntest:emptied', $pages, 'emptied page is excluded');
2619fd890c3Stracker-user    }
2629fd890c3Stracker-user
2639fd890c3Stracker-user    public function testPageCountsSplitsNormalAndOrphaned(): void
2649fd890c3Stracker-user    {
2659fd890c3Stracker-user        $id = 'anntest:counts';
2669fd890c3Stracker-user        saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup');
2679fd890c3Stracker-user
2689fd890c3Stracker-user        $h = $this->helper();
2699fd890c3Stracker-user        $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present');
2709fd890c3Stracker-user        $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone');
2719fd890c3Stracker-user
2729fd890c3Stracker-user        $this->assertEquals(
273*72d60f2dStracker-user            ['total' => 2, 'normal' => 1, 'resolved' => 0, 'orphaned' => 1],
274*72d60f2dStracker-user            $h->pageCounts($id)
275*72d60f2dStracker-user        );
276*72d60f2dStracker-user    }
277*72d60f2dStracker-user
278*72d60f2dStracker-user    public function testPageCountsCountsResolvedAcrossFacets(): void
279*72d60f2dStracker-user    {
280*72d60f2dStracker-user        $id = 'anntest:countsres';
281*72d60f2dStracker-user        saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup');
282*72d60f2dStracker-user
283*72d60f2dStracker-user        $h = $this->helper();
284*72d60f2dStracker-user        // present + resolved → counted in both normal and resolved
285*72d60f2dStracker-user        $present = $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present');
286*72d60f2dStracker-user        $h->setStatus($id, $present['id'], 'resolved', 'alice');
287*72d60f2dStracker-user        // orphaned + resolved → counted in both orphaned and resolved
288*72d60f2dStracker-user        $gone = $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone');
289*72d60f2dStracker-user        $h->setStatus($id, $gone['id'], 'resolved', 'alice');
290*72d60f2dStracker-user
291*72d60f2dStracker-user        $this->assertEquals(
292*72d60f2dStracker-user            ['total' => 2, 'normal' => 1, 'resolved' => 2, 'orphaned' => 1],
2939fd890c3Stracker-user            $h->pageCounts($id)
2949fd890c3Stracker-user        );
2959fd890c3Stracker-user    }
2969fd890c3Stracker-user
2979fd890c3Stracker-user    public function testPageCountsEmptyForUnannotatedPage(): void
2989fd890c3Stracker-user    {
2999fd890c3Stracker-user        $h = $this->helper();
3009fd890c3Stracker-user        $this->assertEquals(
301*72d60f2dStracker-user            ['total' => 0, 'normal' => 0, 'resolved' => 0, 'orphaned' => 0],
3029fd890c3Stracker-user            $h->pageCounts('anntest:never')
3039fd890c3Stracker-user        );
3049fd890c3Stracker-user    }
3059fd890c3Stracker-user
3069fd890c3Stracker-user    public function testClearOrphanedAllAcrossPages(): void
3079fd890c3Stracker-user    {
3089fd890c3Stracker-user        $h = $this->helper();
3099fd890c3Stracker-user        $ids = ['anntest:sweepa', 'anntest:sweepb'];
3109fd890c3Stracker-user        foreach ($ids as $id) {
3119fd890c3Stracker-user            saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup');
3129fd890c3Stracker-user            $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present');
3139fd890c3Stracker-user            $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone');
3149fd890c3Stracker-user        }
3159fd890c3Stracker-user
3169fd890c3Stracker-user        // clearOrphanedAll() is wiki-wide and the shared test data dir may carry
3179fd890c3Stracker-user        // annotated pages from earlier tests, so assert the seeded orphans are
3189fd890c3Stracker-user        // included (>= 2) rather than an exact global total, and verify each of
3199fd890c3Stracker-user        // our pages keeps exactly its one present annotation.
3209fd890c3Stracker-user        $removed = $h->clearOrphanedAll();
3219fd890c3Stracker-user        $this->assertGreaterThanOrEqual(2, $removed, 'at least the two seeded orphans are cleared');
3229fd890c3Stracker-user        foreach ($ids as $id) {
3239fd890c3Stracker-user            $remaining = $h->getAnnotations($id);
3249fd890c3Stracker-user            $this->assertCount(1, $remaining, $id . ' keeps only the present annotation');
3259fd890c3Stracker-user            $this->assertEquals('present', $remaining[0]['body'], $id . ' kept the right annotation');
3269fd890c3Stracker-user            $this->assertSame(0, $h->pageCounts($id)['orphaned'], $id . ' has no orphans left');
3279fd890c3Stracker-user        }
3289fd890c3Stracker-user    }
329da56206cStracker-user}
330