xref: /plugin/annotations/_test/HelperTest.php (revision da56206cc13612db0df36be97c0f01d8f3c5e9f4)
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    // -----------------------------------------------------------------
194    //  Orphan detection against a rendered page
195    // -----------------------------------------------------------------
196
197    public function testFindAndClearOrphanedAgainstRenderedPage(): void
198    {
199        $id = 'anntest:orphan';
200        saveWikiText($id, 'Hello world, this is a wiki page about cats.', 'setup');
201
202        $h = $this->helper();
203        $present = $h->createAnnotation($id, ['exact' => 'wiki page about cats'], 'alice', 'present');
204        $gone    = $h->createAnnotation($id, ['exact' => 'text that is not here'], 'alice', 'gone');
205
206        $orphanIds = array_map(static function ($a) {
207            return $a['id'];
208        }, $h->findOrphaned($id));
209
210        $this->assertContains($gone['id'], $orphanIds, 'a missing quote is orphaned');
211        $this->assertNotContains($present['id'], $orphanIds, 'a present quote is not orphaned');
212
213        $this->assertEquals(1, $h->clearOrphaned($id), 'only the orphan is cleared');
214        $remaining = $h->getAnnotations($id);
215        $this->assertCount(1, $remaining);
216        $this->assertEquals($present['id'], $remaining[0]['id']);
217    }
218}
219