xref: /plugin/annotations/_test/HelperTest.php (revision 72d60f2d94b24cb66fabf596a2ec440f459ba88f)
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    public function testClearResolvedAllAcrossPages(): void
194    {
195        $h = $this->helper();
196        $ids = ['anntest:resweepa', 'anntest:resweepb'];
197        foreach ($ids as $id) {
198            $h->createAnnotation($id, ['exact' => 'keep open'], 'alice', 'open');
199            $done = $h->createAnnotation($id, ['exact' => 'mark done'], 'alice', 'done');
200            $h->setStatus($id, $done['id'], 'resolved', 'alice');
201        }
202
203        // clearResolvedAll() is wiki-wide and the shared test data dir may carry
204        // resolved annotations from earlier tests, so assert the seeded ones are
205        // included (>= 2) rather than an exact global total, and verify each of
206        // our pages keeps exactly its one open annotation.
207        $removed = $h->clearResolvedAll();
208        $this->assertGreaterThanOrEqual(2, $removed, 'at least the two seeded resolved are cleared');
209        foreach ($ids as $id) {
210            $remaining = $h->getAnnotations($id);
211            $this->assertCount(1, $remaining, $id . ' keeps only the open annotation');
212            $this->assertEquals('open', $remaining[0]['body'], $id . ' kept the right annotation');
213        }
214    }
215
216    // -----------------------------------------------------------------
217    //  Orphan detection against a rendered page
218    // -----------------------------------------------------------------
219
220    public function testFindAndClearOrphanedAgainstRenderedPage(): void
221    {
222        $id = 'anntest:orphan';
223        saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup');
224
225        $h = $this->helper();
226        $present = $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present');
227        $gone    = $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone');
228
229        $orphanIds = array_map(static function ($a) {
230            return $a['id'];
231        }, $h->findOrphaned($id));
232
233        $this->assertContains($gone['id'], $orphanIds, 'a missing quote is orphaned');
234        $this->assertNotContains($present['id'], $orphanIds, 'a present quote is not orphaned');
235
236        $this->assertEquals(1, $h->clearOrphaned($id), 'only the orphan is cleared');
237        $remaining = $h->getAnnotations($id);
238        $this->assertCount(1, $remaining);
239        $this->assertEquals($present['id'], $remaining[0]['id']);
240    }
241
242    // -----------------------------------------------------------------
243    //  Admin overview (enumeration & counts)
244    // -----------------------------------------------------------------
245
246    public function testGetAnnotatedPages(): void
247    {
248        $h = $this->helper();
249        $h->createAnnotation('anntest:listone', ['exact' => 'foo'], 'alice', 'b');
250        $h->createAnnotation('anntest:nested:listtwo', ['exact' => 'bar'], 'alice', 'b');
251
252        // a page whose only annotation is then deleted leaves an empty file and
253        // must NOT be listed
254        $gone = $h->createAnnotation('anntest:emptied', ['exact' => 'baz'], 'alice', 'b');
255        $h->deleteAnnotation('anntest:emptied', $gone['id']);
256
257        $pages = $h->getAnnotatedPages();
258        $this->assertContains('anntest:listone', $pages);
259        $this->assertContains('anntest:nested:listtwo', $pages);
260        $this->assertNotContains('anntest:emptied', $pages, 'emptied page is excluded');
261    }
262
263    public function testPageCountsSplitsNormalAndOrphaned(): void
264    {
265        $id = 'anntest:counts';
266        saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup');
267
268        $h = $this->helper();
269        $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present');
270        $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone');
271
272        $this->assertEquals(
273            ['total' => 2, 'normal' => 1, 'resolved' => 0, 'orphaned' => 1],
274            $h->pageCounts($id)
275        );
276    }
277
278    public function testPageCountsCountsResolvedAcrossFacets(): void
279    {
280        $id = 'anntest:countsres';
281        saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup');
282
283        $h = $this->helper();
284        // present + resolved → counted in both normal and resolved
285        $present = $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present');
286        $h->setStatus($id, $present['id'], 'resolved', 'alice');
287        // orphaned + resolved → counted in both orphaned and resolved
288        $gone = $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone');
289        $h->setStatus($id, $gone['id'], 'resolved', 'alice');
290
291        $this->assertEquals(
292            ['total' => 2, 'normal' => 1, 'resolved' => 2, 'orphaned' => 1],
293            $h->pageCounts($id)
294        );
295    }
296
297    public function testPageCountsEmptyForUnannotatedPage(): void
298    {
299        $h = $this->helper();
300        $this->assertEquals(
301            ['total' => 0, 'normal' => 0, 'resolved' => 0, 'orphaned' => 0],
302            $h->pageCounts('anntest:never')
303        );
304    }
305
306    public function testClearOrphanedAllAcrossPages(): void
307    {
308        $h = $this->helper();
309        $ids = ['anntest:sweepa', 'anntest:sweepb'];
310        foreach ($ids as $id) {
311            saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup');
312            $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present');
313            $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone');
314        }
315
316        // clearOrphanedAll() is wiki-wide and the shared test data dir may carry
317        // annotated pages from earlier tests, so assert the seeded orphans are
318        // included (>= 2) rather than an exact global total, and verify each of
319        // our pages keeps exactly its one present annotation.
320        $removed = $h->clearOrphanedAll();
321        $this->assertGreaterThanOrEqual(2, $removed, 'at least the two seeded orphans are cleared');
322        foreach ($ids as $id) {
323            $remaining = $h->getAnnotations($id);
324            $this->assertCount(1, $remaining, $id . ' keeps only the present annotation');
325            $this->assertEquals('present', $remaining[0]['body'], $id . ' kept the right annotation');
326            $this->assertSame(0, $h->pageCounts($id)['orphaned'], $id . ' has no orphans left');
327        }
328    }
329}
330