xref: /plugin/statistics/_test/LoggerTest.php (revision 23e0cc03acfe31a473eb80d755383b85acb61d2b)
1<?php
2
3namespace dokuwiki\plugin\statistics\test;
4
5use DokuWikiTest;
6use dokuwiki\plugin\statistics\Logger;
7use helper_plugin_statistics;
8
9/**
10 * Tests for the statistics plugin Logger class
11 *
12 * @group plugin_statistics
13 * @group plugins
14 */
15class LoggerTest extends DokuWikiTest
16{
17    protected $pluginsEnabled = ['statistics', 'sqlite'];
18
19    /** @var helper_plugin_statistics */
20    protected $helper;
21
22    /** @var Logger */
23    protected $logger;
24
25    public function setUp(): void
26    {
27        parent::setUp();
28
29        // Load the helper plugin
30        $this->helper = plugin_load('helper', 'statistics');
31
32        // Mock user agent to avoid bot detection
33        $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
34
35        // Initialize logger
36        $this->logger = new Logger($this->helper);
37    }
38
39    public function tearDown(): void
40    {
41        unset($_SERVER['HTTP_USER_AGENT']);
42        parent::tearDown();
43    }
44
45    /**
46     * Test constructor initializes properties correctly
47     */
48    public function testConstructor()
49    {
50        $this->assertInstanceOf(Logger::class, $this->logger);
51
52        // Test that bot user agents throw exception
53        $_SERVER['HTTP_USER_AGENT'] = 'Googlebot/2.1 (+http://www.google.com/bot.html)';
54
55        $this->expectException(\RuntimeException::class);
56        $this->expectExceptionMessage('Bot detected, not logging');
57        new Logger($this->helper);
58    }
59
60    /**
61     * Test begin and end transaction methods
62     */
63    public function testBeginEnd()
64    {
65        $this->logger->begin();
66
67        // Verify transaction is active by checking PDO
68        $pdo = $this->helper->getDB()->getPdo();
69        $this->assertTrue($pdo->inTransaction());
70
71        $this->logger->end();
72
73        // Verify transaction is committed
74        $this->assertFalse($pdo->inTransaction());
75    }
76
77    /**
78     * Test logLastseen method
79     */
80    public function testLogLastseen()
81    {
82        global $INPUT;
83
84        // Test with no user (should not log)
85        $INPUT->server->set('REMOTE_USER', '');
86        $this->logger->logLastseen();
87
88        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM lastseen');
89        $this->assertEquals(0, $count);
90
91        // Test with user
92        $INPUT->server->set('REMOTE_USER', 'testuser');
93        $this->logger->logLastseen();
94
95        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM lastseen');
96        $this->assertEquals(1, $count);
97
98        $user = $this->helper->getDB()->queryValue('SELECT user FROM lastseen WHERE user = ?', ['testuser']);
99        $this->assertEquals('testuser', $user);
100    }
101
102    /**
103     * Data provider for logGroups test
104     */
105    public function logGroupsProvider()
106    {
107        return [
108            'empty groups' => [[], 'view', 0],
109            'single group' => [['admin'], 'view', 1],
110            'multiple groups' => [['admin', 'user'], 'edit', 2],
111            'filtered groups' => [['admin', 'nonexistent'], 'view', 1], // assuming only 'admin' is configured
112        ];
113    }
114
115    /**
116     * Test logGroups method
117     * @dataProvider logGroupsProvider
118     */
119    public function testLogGroups($groups, $type, $expectedCount)
120    {
121        global $conf;
122        $conf['plugin']['statistics']['loggroups'] = ['admin', 'user'];
123
124        // Clear any existing data for this test
125        $this->helper->getDB()->exec('DELETE FROM groups WHERE type = ?', [$type]);
126
127        $this->logger->logGroups($type, $groups);
128
129        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM groups WHERE type = ?', [$type]);
130        $this->assertEquals($expectedCount, $count);
131
132        if ($expectedCount > 0) {
133            $loggedGroups = $this->helper->getDB()->queryAll('SELECT `group` FROM groups WHERE type = ?', [$type]);
134            $this->assertCount($expectedCount, $loggedGroups);
135        }
136    }
137
138    /**
139     * Data provider for logExternalSearch test
140     */
141    public function logExternalSearchProvider()
142    {
143        return [
144            'google search' => [
145                'https://www.google.com/search?q=dokuwiki+test',
146                'search',
147                'dokuwiki test',
148                'google'
149            ],
150            'non-search referer' => [
151                'https://example.com/page',
152                '',
153                null,
154                null
155            ],
156        ];
157    }
158
159    /**
160     * Test logExternalSearch method
161     * @dataProvider logExternalSearchProvider
162     */
163    public function testLogExternalSearch($referer, $expectedType, $expectedQuery, $expectedEngine)
164    {
165        global $INPUT;
166        $INPUT->set('p', 'test:page');
167
168        $type = '';
169        $this->logger->logExternalSearch($referer, $type);
170
171        $this->assertEquals($expectedType, $type);
172
173        if ($expectedType === 'search') {
174            $searchCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM search');
175            $this->assertEquals(1, $searchCount);
176
177            $search = $this->helper->getDB()->queryRecord('SELECT * FROM search ORDER BY dt DESC LIMIT 1');
178            $this->assertEquals($expectedQuery, $search['query']);
179            $this->assertEquals($expectedEngine, $search['engine']);
180        }
181    }
182
183    /**
184     * Test logSearch method
185     */
186    public function testLogSearch()
187    {
188        $page = 'test:page';
189        $query = 'test search query';
190        $words = ['test', 'search', 'query'];
191        $engine = 'Google';
192
193        $this->logger->logSearch($page, $query, $words, $engine);
194
195        // Check search table
196        $search = $this->helper->getDB()->queryRecord('SELECT * FROM search ORDER BY dt DESC LIMIT 1');
197        $this->assertEquals($page, $search['page']);
198        $this->assertEquals($query, $search['query']);
199        $this->assertEquals($engine, $search['engine']);
200
201        // Check searchwords table
202        $wordCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM searchwords WHERE sid = ?', [$search['id']]);
203        $this->assertEquals(3, $wordCount);
204
205        $loggedWords = $this->helper->getDB()->queryAll('SELECT word FROM searchwords WHERE sid = ? ORDER BY word', [$search['id']]);
206        $this->assertEquals(['query', 'search', 'test'], array_column($loggedWords, 'word'));
207    }
208
209    /**
210     * Test logSession method
211     */
212    public function testLogSession()
213    {
214        // Test without adding view
215        $this->logger->logSession(0);
216
217        $sessionCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM session');
218        $this->assertEquals(1, $sessionCount);
219
220        $session = $this->helper->getDB()->queryRecord('SELECT * FROM session ORDER BY dt DESC LIMIT 1');
221        $this->assertEquals(0, $session['views']);
222
223        // Test adding view
224        $this->logger->logSession(1);
225
226        $session = $this->helper->getDB()->queryRecord('SELECT * FROM session ORDER BY dt DESC LIMIT 1');
227        $this->assertEquals(1, $session['views']);
228
229        // Test incrementing views
230        $this->logger->logSession(1);
231
232        $session = $this->helper->getDB()->queryRecord('SELECT * FROM session ORDER BY dt DESC LIMIT 1');
233        $this->assertEquals(2, $session['views']);
234    }
235
236    /**
237     * Test logIp method
238     */
239    public function testLogIp()
240    {
241        $ip = '8.8.8.8';
242
243        // Mock HTTP client response
244        $this->markTestSkipped('Requires mocking HTTP client for external API call');
245
246        // This test would need to mock the DokuHTTPClient to avoid actual API calls
247        // For now, we'll skip it as the requirement was not to mock anything
248    }
249
250    /**
251     * Test logOutgoing method
252     */
253    public function testLogOutgoing()
254    {
255        global $INPUT;
256
257        // Test without outgoing link
258        $INPUT->set('ol', '');
259        $this->logger->logOutgoing();
260
261        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM outlinks');
262        $this->assertEquals(0, $count);
263
264        // Test with outgoing link
265        $link = 'https://example.com';
266        $page = 'test:page';
267        $INPUT->set('ol', $link);
268        $INPUT->set('p', $page);
269
270        $this->logger->logOutgoing();
271
272        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM outlinks');
273        $this->assertEquals(1, $count);
274
275        $outlink = $this->helper->getDB()->queryRecord('SELECT * FROM outlinks ORDER BY dt DESC LIMIT 1');
276        $this->assertEquals($link, $outlink['link']);
277        $this->assertEquals(md5($link), $outlink['link_md5']);
278        $this->assertEquals($page, $outlink['page']);
279    }
280
281    /**
282     * Test logAccess method
283     */
284    public function testLogAccess()
285    {
286        global $INPUT, $USERINFO, $conf;
287
288        $conf['plugin']['statistics']['loggroups'] = ['admin', 'user'];
289
290        // Clear any existing data for this test
291        $this->helper->getDB()->exec('DELETE FROM groups WHERE type = ?', ['view']);
292
293        $page = 'test:page';
294        $referer = 'https://example.com';
295        $user = 'testuser';
296
297        $INPUT->set('p', $page);
298        $INPUT->set('r', $referer);
299        $INPUT->set('sx', 1920);
300        $INPUT->set('sy', 1080);
301        $INPUT->set('vx', 1200);
302        $INPUT->set('vy', 800);
303        $INPUT->set('js', 1);
304        $INPUT->server->set('REMOTE_USER', $user);
305
306        $USERINFO = ['grps' => ['admin', 'user']];
307
308        $this->logger->logAccess();
309
310        // Check access table
311        $access = $this->helper->getDB()->queryRecord('SELECT * FROM access ORDER BY dt DESC LIMIT 1');
312        $this->assertEquals($page, $access['page']);
313        $this->assertEquals($user, $access['user']);
314        $this->assertEquals(1920, $access['screen_x']);
315        $this->assertEquals(1080, $access['screen_y']);
316        $this->assertEquals(1200, $access['view_x']);
317        $this->assertEquals(800, $access['view_y']);
318        $this->assertEquals(1, $access['js']);
319        $this->assertEquals('external', $access['ref_type']);
320
321        // Check refseen table
322        $refCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM refseen WHERE ref_md5 = ?', [md5($referer)]);
323        $this->assertEquals(1, $refCount);
324
325        // Check groups table
326        $groupCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM groups WHERE type = ?', ['view']);
327        $this->assertEquals(2, $groupCount);
328    }
329
330    /**
331     * Data provider for logMedia test
332     */
333    public function logMediaProvider()
334    {
335        return [
336            'image inline' => ['test.jpg', 'image/jpeg', true, 1024],
337            'video not inline' => ['test.mp4', 'video/mp4', false, 2048],
338            'document' => ['test.pdf', 'application/pdf', false, 512],
339        ];
340    }
341
342    /**
343     * Test logMedia method
344     * @dataProvider logMediaProvider
345     */
346    public function testLogMedia($media, $mime, $inline, $size)
347    {
348        global $INPUT;
349
350        $user = 'testuser';
351        $INPUT->server->set('REMOTE_USER', $user);
352
353        $this->logger->logMedia($media, $mime, $inline, $size);
354
355        $mediaLog = $this->helper->getDB()->queryRecord('SELECT * FROM media ORDER BY dt DESC LIMIT 1');
356        $this->assertEquals($media, $mediaLog['media']);
357        $this->assertEquals($user, $mediaLog['user']);
358        $this->assertEquals($size, $mediaLog['size']);
359        $this->assertEquals($inline ? 1 : 0, $mediaLog['inline']);
360
361        [$mime1, $mime2] = explode('/', strtolower($mime));
362        $this->assertEquals($mime1, $mediaLog['mime1']);
363        $this->assertEquals($mime2, $mediaLog['mime2']);
364    }
365
366    /**
367     * Data provider for logEdit test
368     */
369    public function logEditProvider()
370    {
371        return [
372            'create page' => ['new:page', 'create'],
373            'edit page' => ['existing:page', 'edit'],
374            'delete page' => ['old:page', 'delete'],
375        ];
376    }
377
378    /**
379     * Test logEdit method
380     * @dataProvider logEditProvider
381     */
382    public function testLogEdit($page, $type)
383    {
384        global $INPUT, $USERINFO, $conf;
385
386        $conf['plugin']['statistics']['loggroups'] = ['admin'];
387
388        // Clear any existing data for this test
389        $this->helper->getDB()->exec('DELETE FROM groups WHERE type = ?', ['edit']);
390
391        $user = 'testuser';
392        $INPUT->server->set('REMOTE_USER', $user);
393        $USERINFO = ['grps' => ['admin']];
394
395        $this->logger->logEdit($page, $type);
396
397        // Check edits table
398        $edit = $this->helper->getDB()->queryRecord('SELECT * FROM edits ORDER BY dt DESC LIMIT 1');
399        $this->assertEquals($page, $edit['page']);
400        $this->assertEquals($type, $edit['type']);
401        $this->assertEquals($user, $edit['user']);
402
403        // Check groups table
404        $groupCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM groups WHERE type = ?', ['edit']);
405        $this->assertEquals(1, $groupCount);
406    }
407
408    /**
409     * Data provider for logLogin test
410     */
411    public function logLoginProvider()
412    {
413        return [
414            'login' => ['login', 'testuser'],
415            'logout' => ['logout', 'testuser'],
416            'create' => ['create', 'newuser'],
417        ];
418    }
419
420    /**
421     * Test logLogin method
422     * @dataProvider logLoginProvider
423     */
424    public function testLogLogin($type, $user)
425    {
426        global $INPUT;
427
428        if ($user === 'testuser') {
429            $INPUT->server->set('REMOTE_USER', $user);
430            $this->logger->logLogin($type);
431        } else {
432            $this->logger->logLogin($type, $user);
433        }
434
435        $login = $this->helper->getDB()->queryRecord('SELECT * FROM logins ORDER BY dt DESC LIMIT 1');
436        $this->assertEquals($type, $login['type']);
437        $this->assertEquals($user, $login['user']);
438    }
439
440    /**
441     * Test logHistoryPages method
442     */
443    public function testLogHistoryPages()
444    {
445        $this->logger->logHistoryPages();
446
447        // Check that both page_count and page_size entries were created
448        $pageCount = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['page_count']);
449        $pageSize = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['page_size']);
450
451        $this->assertIsNumeric($pageCount);
452        $this->assertIsNumeric($pageSize);
453        $this->assertGreaterThanOrEqual(0, $pageCount);
454        $this->assertGreaterThanOrEqual(0, $pageSize);
455    }
456
457    /**
458     * Test logHistoryMedia method
459     */
460    public function testLogHistoryMedia()
461    {
462        $this->logger->logHistoryMedia();
463
464        // Check that both media_count and media_size entries were created
465        $mediaCount = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['media_count']);
466        $mediaSize = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['media_size']);
467
468        $this->assertIsNumeric($mediaCount);
469        $this->assertIsNumeric($mediaSize);
470        $this->assertGreaterThanOrEqual(0, $mediaCount);
471        $this->assertGreaterThanOrEqual(0, $mediaSize);
472    }
473
474    /**
475     * Test that feedreader user agents are handled correctly
476     */
477    public function testFeedReaderUserAgent()
478    {
479        // Use a user agent that DeviceDetector recognizes as a feedreader, not a bot
480        $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (compatible; FeedReader)';
481
482        $logger = new Logger($this->helper);
483
484        // Use reflection to access protected property
485        $reflection = new \ReflectionClass($logger);
486        $uaTypeProperty = $reflection->getProperty('uaType');
487        $uaTypeProperty->setAccessible(true);
488
489        $this->assertEquals('feedreader', $uaTypeProperty->getValue($logger));
490    }
491
492    /**
493     * Test session logging only works for browser type
494     */
495    public function testLogSessionOnlyForBrowser()
496    {
497        // Clear any existing session data
498        $this->helper->getDB()->exec('DELETE FROM session');
499
500        // Change user agent type to feedreader using reflection
501        $reflection = new \ReflectionClass($this->logger);
502        $uaTypeProperty = $reflection->getProperty('uaType');
503        $uaTypeProperty->setAccessible(true);
504        $uaTypeProperty->setValue($this->logger, 'feedreader');
505
506        $this->logger->logSession(1);
507
508        $sessionCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM session');
509        $this->assertEquals(0, $sessionCount);
510    }
511}
512