xref: /plugin/statistics/_test/LoggerTest.php (revision c7cad24d9d4cb44ac67e74b93fb39ee37efbcdfd)
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        // Create a mock HTTP client
244        $mockHttpClient = $this->createMock(\dokuwiki\HTTP\DokuHTTPClient::class);
245
246        // Mock the API response
247        $mockResponse = json_encode([
248            'status' => 'success',
249            'country' => 'United States',
250            'countryCode' => 'US',
251            'city' => 'Ashburn',
252            'query' => $ip
253        ]);
254
255        $mockHttpClient->expects($this->once())
256            ->method('get')
257            ->with('http://ip-api.com/json/' . $ip)
258            ->willReturn($mockResponse);
259
260        // Set timeout property
261        $mockHttpClient->timeout = 10;
262
263        // Create logger with mock HTTP client
264        $logger = new Logger($this->helper, $mockHttpClient);
265
266        // Test with IP that doesn't exist in database
267        $logger->logIp($ip);
268
269        // Verify the IP was logged
270        $ipRecord = $this->helper->getDB()->queryRecord('SELECT * FROM iplocation WHERE ip = ?', [$ip]);
271        $this->assertNotNull($ipRecord);
272        $this->assertEquals($ip, $ipRecord['ip']);
273        $this->assertEquals('United States', $ipRecord['country']);
274        $this->assertEquals('US', $ipRecord['code']);
275        $this->assertEquals('Ashburn', $ipRecord['city']);
276        $this->assertNotEmpty($ipRecord['host']); // gethostbyaddr result
277
278        // Test with IP that already exists and is recent (should not make HTTP call)
279        $mockHttpClient2 = $this->createMock(\dokuwiki\HTTP\DokuHTTPClient::class);
280        $mockHttpClient2->expects($this->never())->method('get');
281
282        $logger2 = new Logger($this->helper, $mockHttpClient2);
283        $logger2->logIp($ip); // Should not trigger HTTP call
284    }
285
286    /**
287     * Test logOutgoing method
288     */
289    public function testLogOutgoing()
290    {
291        global $INPUT;
292
293        // Test without outgoing link
294        $INPUT->set('ol', '');
295        $this->logger->logOutgoing();
296
297        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM outlinks');
298        $this->assertEquals(0, $count);
299
300        // Test with outgoing link
301        $link = 'https://example.com';
302        $page = 'test:page';
303        $INPUT->set('ol', $link);
304        $INPUT->set('p', $page);
305
306        $this->logger->logOutgoing();
307
308        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM outlinks');
309        $this->assertEquals(1, $count);
310
311        $outlink = $this->helper->getDB()->queryRecord('SELECT * FROM outlinks ORDER BY dt DESC LIMIT 1');
312        $this->assertEquals($link, $outlink['link']);
313        $this->assertEquals(md5($link), $outlink['link_md5']);
314        $this->assertEquals($page, $outlink['page']);
315    }
316
317    /**
318     * Test logAccess method
319     */
320    public function testLogAccess()
321    {
322        global $INPUT, $USERINFO, $conf;
323
324        $conf['plugin']['statistics']['loggroups'] = ['admin', 'user'];
325
326        // Clear any existing data for this test
327        $this->helper->getDB()->exec('DELETE FROM groups WHERE type = ?', ['view']);
328
329        $page = 'test:page';
330        $referer = 'https://example.com';
331        $user = 'testuser';
332
333        $INPUT->set('p', $page);
334        $INPUT->set('r', $referer);
335        $INPUT->set('sx', 1920);
336        $INPUT->set('sy', 1080);
337        $INPUT->set('vx', 1200);
338        $INPUT->set('vy', 800);
339        $INPUT->set('js', 1);
340        $INPUT->server->set('REMOTE_USER', $user);
341
342        $USERINFO = ['grps' => ['admin', 'user']];
343
344        $this->logger->logAccess();
345
346        // Check access table
347        $access = $this->helper->getDB()->queryRecord('SELECT * FROM access ORDER BY dt DESC LIMIT 1');
348        $this->assertEquals($page, $access['page']);
349        $this->assertEquals($user, $access['user']);
350        $this->assertEquals(1920, $access['screen_x']);
351        $this->assertEquals(1080, $access['screen_y']);
352        $this->assertEquals(1200, $access['view_x']);
353        $this->assertEquals(800, $access['view_y']);
354        $this->assertEquals(1, $access['js']);
355        $this->assertEquals('external', $access['ref_type']);
356
357        // Check refseen table
358        $refCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM refseen WHERE ref_md5 = ?', [md5($referer)]);
359        $this->assertEquals(1, $refCount);
360
361        // Check groups table
362        $groupCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM groups WHERE type = ?', ['view']);
363        $this->assertEquals(2, $groupCount);
364    }
365
366    /**
367     * Data provider for logMedia test
368     */
369    public function logMediaProvider()
370    {
371        return [
372            'image inline' => ['test.jpg', 'image/jpeg', true, 1024],
373            'video not inline' => ['test.mp4', 'video/mp4', false, 2048],
374            'document' => ['test.pdf', 'application/pdf', false, 512],
375        ];
376    }
377
378    /**
379     * Test logMedia method
380     * @dataProvider logMediaProvider
381     */
382    public function testLogMedia($media, $mime, $inline, $size)
383    {
384        global $INPUT;
385
386        $user = 'testuser';
387        $INPUT->server->set('REMOTE_USER', $user);
388
389        $this->logger->logMedia($media, $mime, $inline, $size);
390
391        $mediaLog = $this->helper->getDB()->queryRecord('SELECT * FROM media ORDER BY dt DESC LIMIT 1');
392        $this->assertEquals($media, $mediaLog['media']);
393        $this->assertEquals($user, $mediaLog['user']);
394        $this->assertEquals($size, $mediaLog['size']);
395        $this->assertEquals($inline ? 1 : 0, $mediaLog['inline']);
396
397        [$mime1, $mime2] = explode('/', strtolower($mime));
398        $this->assertEquals($mime1, $mediaLog['mime1']);
399        $this->assertEquals($mime2, $mediaLog['mime2']);
400    }
401
402    /**
403     * Data provider for logEdit test
404     */
405    public function logEditProvider()
406    {
407        return [
408            'create page' => ['new:page', 'create'],
409            'edit page' => ['existing:page', 'edit'],
410            'delete page' => ['old:page', 'delete'],
411        ];
412    }
413
414    /**
415     * Test logEdit method
416     * @dataProvider logEditProvider
417     */
418    public function testLogEdit($page, $type)
419    {
420        global $INPUT, $USERINFO, $conf;
421
422        $conf['plugin']['statistics']['loggroups'] = ['admin'];
423
424        // Clear any existing data for this test
425        $this->helper->getDB()->exec('DELETE FROM groups WHERE type = ?', ['edit']);
426
427        $user = 'testuser';
428        $INPUT->server->set('REMOTE_USER', $user);
429        $USERINFO = ['grps' => ['admin']];
430
431        $this->logger->logEdit($page, $type);
432
433        // Check edits table
434        $edit = $this->helper->getDB()->queryRecord('SELECT * FROM edits ORDER BY dt DESC LIMIT 1');
435        $this->assertEquals($page, $edit['page']);
436        $this->assertEquals($type, $edit['type']);
437        $this->assertEquals($user, $edit['user']);
438
439        // Check groups table
440        $groupCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM groups WHERE type = ?', ['edit']);
441        $this->assertEquals(1, $groupCount);
442    }
443
444    /**
445     * Data provider for logLogin test
446     */
447    public function logLoginProvider()
448    {
449        return [
450            'login' => ['login', 'testuser'],
451            'logout' => ['logout', 'testuser'],
452            'create' => ['create', 'newuser'],
453        ];
454    }
455
456    /**
457     * Test logLogin method
458     * @dataProvider logLoginProvider
459     */
460    public function testLogLogin($type, $user)
461    {
462        global $INPUT;
463
464        if ($user === 'testuser') {
465            $INPUT->server->set('REMOTE_USER', $user);
466            $this->logger->logLogin($type);
467        } else {
468            $this->logger->logLogin($type, $user);
469        }
470
471        $login = $this->helper->getDB()->queryRecord('SELECT * FROM logins ORDER BY dt DESC LIMIT 1');
472        $this->assertEquals($type, $login['type']);
473        $this->assertEquals($user, $login['user']);
474    }
475
476    /**
477     * Test logHistoryPages method
478     */
479    public function testLogHistoryPages()
480    {
481        $this->logger->logHistoryPages();
482
483        // Check that both page_count and page_size entries were created
484        $pageCount = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['page_count']);
485        $pageSize = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['page_size']);
486
487        $this->assertIsNumeric($pageCount);
488        $this->assertIsNumeric($pageSize);
489        $this->assertGreaterThanOrEqual(0, $pageCount);
490        $this->assertGreaterThanOrEqual(0, $pageSize);
491    }
492
493    /**
494     * Test logHistoryMedia method
495     */
496    public function testLogHistoryMedia()
497    {
498        $this->logger->logHistoryMedia();
499
500        // Check that both media_count and media_size entries were created
501        $mediaCount = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['media_count']);
502        $mediaSize = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['media_size']);
503
504        $this->assertIsNumeric($mediaCount);
505        $this->assertIsNumeric($mediaSize);
506        $this->assertGreaterThanOrEqual(0, $mediaCount);
507        $this->assertGreaterThanOrEqual(0, $mediaSize);
508    }
509
510    /**
511     * Test that feedreader user agents are handled correctly
512     */
513    public function testFeedReaderUserAgent()
514    {
515        // Use a user agent that DeviceDetector recognizes as a feedreader, not a bot
516        $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (compatible; FeedReader)';
517
518        $logger = new Logger($this->helper);
519
520        // Use reflection to access protected property
521        $reflection = new \ReflectionClass($logger);
522        $uaTypeProperty = $reflection->getProperty('uaType');
523        $uaTypeProperty->setAccessible(true);
524
525        $this->assertEquals('feedreader', $uaTypeProperty->getValue($logger));
526    }
527
528    /**
529     * Test session logging only works for browser type
530     */
531    public function testLogSessionOnlyForBrowser()
532    {
533        // Clear any existing session data
534        $this->helper->getDB()->exec('DELETE FROM session');
535
536        // Change user agent type to feedreader using reflection
537        $reflection = new \ReflectionClass($this->logger);
538        $uaTypeProperty = $reflection->getProperty('uaType');
539        $uaTypeProperty->setAccessible(true);
540        $uaTypeProperty->setValue($this->logger, 'feedreader');
541
542        $this->logger->logSession(1);
543
544        $sessionCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM session');
545        $this->assertEquals(0, $sessionCount);
546    }
547}
548