xref: /plugin/statistics/_test/LoggerTest.php (revision 41d1fffc4a3b58bed7f96d983ba8317fe9e225a5)
1<?php
2
3namespace dokuwiki\plugin\statistics\test;
4
5use dokuwiki\plugin\statistics\Logger;
6use DokuWikiTest;
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    const SESSION_ID = 'test-session-12345';
23    const USER_ID = 'test-uid-12345';
24    const 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';
25
26    public function setUp(): void
27    {
28        parent::setUp();
29
30        // Load the helper plugin
31        $this->helper = plugin_load('helper', 'statistics');
32
33        // set default user agent
34        $_SERVER['HTTP_USER_AGENT'] = self::USER_AGENT;
35
36        // Set up session data that Logger expects
37        $_SESSION[DOKU_COOKIE]['statistics']['uid'] = self::USER_ID;
38        $_SESSION[DOKU_COOKIE]['statistics']['id'] = self::SESSION_ID;
39    }
40
41    public function tearDown(): void
42    {
43        unset($_SERVER['HTTP_USER_AGENT']);
44        unset($_SESSION[DOKU_COOKIE]['statistics']);
45        parent::tearDown();
46    }
47
48    /**
49     * Test constructor initializes properties correctly
50     */
51    public function testConstructor()
52    {
53        $this->assertInstanceOf(Logger::class, $this->helper->getLogger());
54
55        // Test that bot user agents throw exception
56        $_SERVER['HTTP_USER_AGENT'] = 'Googlebot/2.1 (+http://www.google.com/bot.html)';
57
58        $this->expectException(\dokuwiki\plugin\statistics\IgnoreException::class);
59        $this->expectExceptionMessage('Bot detected, not logging');
60        new Logger($this->helper);
61    }
62
63    /**
64     * Test begin and end transaction methods
65     */
66    public function testBeginEnd()
67    {
68        $this->helper->getLogger()->begin();
69
70        // Verify transaction is active by checking PDO
71        $pdo = $this->helper->getDB()->getPdo();
72        $this->assertTrue($pdo->inTransaction());
73
74        $this->helper->getLogger()->end();
75
76        // Verify transaction is committed
77        $this->assertFalse($pdo->inTransaction());
78    }
79
80    /**
81     * Test user logging
82     */
83    public function testLogUser()
84    {
85        // Test with no user (should not log)
86        $_SERVER['REMOTE_USER'] = '';
87        $this->helper->getLogger()->begin();
88        $this->helper->getLogger()->end();
89
90        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM users');
91        $this->assertEquals(0, $count);
92
93        // Test with user
94        $_SERVER['REMOTE_USER'] = 'testuser';
95        $this->helper->getLogger()->begin();
96        $this->helper->getLogger()->end();
97
98        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM users');
99        $this->assertEquals(1, $count);
100
101        $user = $this->helper->getDB()->queryValue('SELECT user FROM users WHERE user = ?', ['testuser']);
102        $this->assertEquals('testuser', $user);
103    }
104
105    /**
106     * Data provider for logGroups test
107     */
108    public function logGroupsProvider()
109    {
110        return [
111            'empty groups' => [[], 0],
112            'single group' => [['admin'], 1],
113            'multiple groups' => [['admin', 'user'], 2],
114            'filtered groups' => [['admin', 'nonexistent'], 2], // all groups are logged
115        ];
116    }
117
118    /**
119     * Test logGroups method
120     * @dataProvider logGroupsProvider
121     */
122    public function testLogGroups($groups, $expectedCount)
123    {
124        global $USERINFO;
125
126        // Set up a test user and groups
127        $_SERVER['REMOTE_USER'] = 'testuser';
128        $USERINFO = ['grps' => $groups];
129
130
131        $this->helper->getLogger()->begin();
132        $this->helper->getLogger()->end();
133
134        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM groups WHERE user = ?', ['testuser']);
135        $this->assertEquals($expectedCount, $count);
136
137        if ($expectedCount > 0) {
138            $loggedGroups = $this->helper->getDB()->queryAll('SELECT `group` FROM groups WHERE user = ?', ['testuser']);
139            $this->assertCount($expectedCount, $loggedGroups);
140        }
141    }
142
143    /**
144     * Data provider fortestLogReferer test
145     */
146    public function logRefererProvider()
147    {
148        return [
149            'google search' => [
150                'https://www.google.com/search?q=dokuwiki+test',
151                'google'
152            ],
153            'non-search referer' => [
154                'https://example.com/page',
155                null,
156            ],
157        ];
158    }
159
160    /**
161     * Test logReferer method
162     * @dataProvider logRefererProvider
163     */
164    public function testLogReferer($referer, $expectedEngine)
165    {
166        $refId = $this->helper->getLogger()->logReferer($referer);
167        $this->assertNotNull($refId);
168        $refererRecord = $this->helper->getDB()->queryRecord('SELECT * FROM referers WHERE id = ?', [$refId]);
169        $this->assertEquals($expectedEngine, $refererRecord['engine']);
170    }
171
172    /**
173     * Test logSearch method
174     */
175    public function testLogSearch()
176    {
177        $query = 'test search query';
178        $words = ['test', 'search', 'query'];
179
180        $this->helper->getLogger()->logSearch($query, $words);
181
182        // Check search table
183        $search = $this->helper->getDB()->queryRecord('SELECT * FROM search ORDER BY dt DESC LIMIT 1');
184        $this->assertEquals($query, $search['query']);
185
186        // Check searchwords table
187        $wordCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM searchwords WHERE sid = ?', [$search['id']]);
188        $this->assertEquals(3, $wordCount);
189
190        $loggedWords = $this->helper->getDB()->queryAll('SELECT word FROM searchwords WHERE sid = ? ORDER BY word', [$search['id']]);
191        $this->assertEquals(['query', 'search', 'test'], array_column($loggedWords, 'word'));
192    }
193
194    /**
195     * Test logSession method
196     */
197    public function testLogSession()
198    {
199        $_SERVER['REMOTE_USER'] = 'testuser';
200
201        // Test session creation
202        $logger = $this->helper->getLogger();
203
204        $logger->begin();
205        $logger->end();
206
207        $sessionCount = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM sessions');
208        $this->assertEquals(1, $sessionCount);
209
210        $session = $this->helper->getDB()->queryRecord('SELECT * FROM sessions LIMIT 1');
211        $this->assertIsArray($session);
212        $this->assertEquals('testuser', $session['user']);
213        $this->assertEquals(self::SESSION_ID, $session['session']);
214        $this->assertEquals(self::USER_ID, $session['uid']);
215        $this->assertEquals(self::USER_AGENT, $session['ua']);
216        $this->assertEquals('Chrome', $session['ua_info']);
217        $this->assertEquals('browser', $session['ua_type']);
218        $this->assertEquals('91', $session['ua_ver']);
219        $this->assertEquals('Windows', $session['os']);
220
221    }
222
223    /**
224     * Test logIp method
225     */
226    public function testLogIp()
227    {
228        $ip = '8.8.8.8';
229        $_SERVER['REMOTE_ADDR'] = $ip;
230
231        // Create a mock HTTP client
232        $mockHttpClient = $this->createMock(\dokuwiki\HTTP\DokuHTTPClient::class);
233
234        // Mock the API response
235        $mockResponse = json_encode([
236            'status' => 'success',
237            'country' => 'United States',
238            'countryCode' => 'US',
239            'city' => 'Ashburn',
240            'query' => $ip
241        ]);
242
243        $mockHttpClient->expects($this->once())
244            ->method('get')
245            ->with('http://ip-api.com/json/' . $ip)
246            ->willReturn($mockResponse);
247
248        // Set timeout property
249        $mockHttpClient->timeout = 10;
250
251        // Create logger with mock HTTP client
252        $logger = new Logger($this->helper, $mockHttpClient);
253
254        // Test with IP that doesn't exist in database
255        $logger->logIp();
256
257        // Verify the IP was logged
258        $ipRecord = $this->helper->getDB()->queryRecord('SELECT * FROM iplocation WHERE ip = ?', [$ip]);
259        $this->assertNotNull($ipRecord);
260        $this->assertEquals($ip, $ipRecord['ip']);
261        $this->assertEquals('United States', $ipRecord['country']);
262        $this->assertEquals('US', $ipRecord['code']);
263        $this->assertEquals('Ashburn', $ipRecord['city']);
264        $this->assertNotEmpty($ipRecord['host']); // gethostbyaddr result
265
266        // Test with IP that already exists and is recent (should not make HTTP call)
267        $mockHttpClient2 = $this->createMock(\dokuwiki\HTTP\DokuHTTPClient::class);
268        $mockHttpClient2->expects($this->never())->method('get');
269
270        $logger2 = new Logger($this->helper, $mockHttpClient2);
271        $logger2->logIp(); // Should not trigger HTTP call
272    }
273
274    /**
275     * Test logOutgoing method
276     */
277    public function testLogOutgoing()
278    {
279        global $INPUT;
280
281        // Test without outgoing link
282        $INPUT->set('ol', '');
283        $this->helper->getLogger()->logOutgoing();
284
285        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM outlinks');
286        $this->assertEquals(0, $count);
287
288        // Test with outgoing link
289        $link = 'https://example.com';
290        $page = 'test:page';
291        $INPUT->set('ol', $link);
292        $INPUT->set('p', $page);
293
294        $this->helper->getLogger()->logOutgoing();
295
296        $count = $this->helper->getDB()->queryValue('SELECT COUNT(*) FROM outlinks');
297        $this->assertEquals(1, $count);
298
299        $outlink = $this->helper->getDB()->queryRecord('SELECT * FROM outlinks ORDER BY dt DESC LIMIT 1');
300        $this->assertEquals($link, $outlink['link']);
301        $this->assertEquals($page, $outlink['page']);
302    }
303
304    /**
305     * Test logPageView method
306     */
307    public function testLogPageView()
308    {
309        global $INPUT, $USERINFO, $conf;
310
311        $conf['plugin']['statistics']['loggroups'] = ['admin', 'user'];
312
313        $page = 'test:page';
314        $referer = 'https://example.com';
315        $user = 'testuser';
316
317        $INPUT->set('p', $page);
318        $INPUT->set('r', $referer);
319        $INPUT->set('sx', 1920);
320        $INPUT->set('sy', 1080);
321        $INPUT->set('vx', 1200);
322        $INPUT->set('vy', 800);
323        $INPUT->server->set('REMOTE_USER', $user);
324
325        $USERINFO = ['grps' => ['admin', 'user']];
326
327        $logger = $this->helper->getLogger();
328        $logger->begin();
329        $logger->logPageView();
330        $logger->end();
331
332        // Check pageviews table
333        $pageview = $this->helper->getDB()->queryRecord('SELECT * FROM pageviews ORDER BY dt DESC LIMIT 1');
334        $this->assertEquals($page, $pageview['page']);
335        $this->assertEquals(1920, $pageview['screen_x']);
336        $this->assertEquals(1080, $pageview['screen_y']);
337        $this->assertEquals(1200, $pageview['view_x']);
338        $this->assertEquals(800, $pageview['view_y']);
339        $this->assertEquals(self::SESSION_ID, $pageview['session']);
340    }
341
342    /**
343     * Data provider for logMedia test
344     */
345    public function logMediaProvider()
346    {
347        return [
348            'image inline' => ['test.jpg', 'image/jpeg', true, 1024],
349            'video not inline' => ['test.mp4', 'video/mp4', false, 2048],
350            'document' => ['test.pdf', 'application/pdf', false, 512],
351        ];
352    }
353
354    /**
355     * Test logMedia method
356     * @dataProvider logMediaProvider
357     */
358    public function testLogMedia($media, $mime, $inline, $size)
359    {
360        global $INPUT;
361
362        $user = 'testuser';
363        $INPUT->server->set('REMOTE_USER', $user);
364
365        $this->helper->getLogger()->logMedia($media, $mime, $inline, $size);
366
367        $mediaLog = $this->helper->getDB()->queryRecord('SELECT * FROM media ORDER BY dt DESC LIMIT 1');
368        $this->assertEquals($media, $mediaLog['media']);
369        $this->assertEquals($size, $mediaLog['size']);
370        $this->assertEquals($inline ? 1 : 0, $mediaLog['inline']);
371
372        [$mime1, $mime2] = explode('/', strtolower($mime));
373        $this->assertEquals($mime1, $mediaLog['mime1']);
374        $this->assertEquals($mime2, $mediaLog['mime2']);
375    }
376
377    /**
378     * Data provider for logEdit test
379     */
380    public function logEditProvider()
381    {
382        return [
383            'create page' => ['new:page', 'create'],
384            'edit page' => ['existing:page', 'edit'],
385            'delete page' => ['old:page', 'delete'],
386        ];
387    }
388
389    /**
390     * Test logEdit method
391     * @dataProvider logEditProvider
392     */
393    public function testLogEdit($page, $type)
394    {
395        global $INPUT, $USERINFO;
396
397
398        $user = 'testuser';
399        $INPUT->server->set('REMOTE_USER', $user);
400        $USERINFO = ['grps' => ['admin']];
401
402        $this->helper->getLogger()->logEdit($page, $type);
403
404        // Check edits table
405        $edit = $this->helper->getDB()->queryRecord('SELECT * FROM edits ORDER BY dt DESC LIMIT 1');
406        $this->assertEquals($page, $edit['page']);
407        $this->assertEquals($type, $edit['type']);
408    }
409
410    /**
411     * Data provider for logLogin test
412     */
413    public function logLoginProvider()
414    {
415        return [
416            'login' => ['login', 'testuser'],
417            'logout' => ['logout', 'testuser'],
418            'create' => ['create', 'newuser'],
419        ];
420    }
421
422    /**
423     * Test logLogin method
424     * @dataProvider logLoginProvider
425     */
426    public function testLogLogin($type, $user)
427    {
428        $this->helper->getLogger()->logLogin($type, $user);
429        $login = $this->helper->getDB()->queryRecord('SELECT * FROM logins ORDER BY dt DESC LIMIT 1');
430        $this->assertEquals($type, $login['type']);
431        $this->assertEquals($user, $login['user']);
432    }
433
434    /**
435     * Test logHistoryPages method
436     */
437    public function testLogHistoryPages()
438    {
439        $this->helper->getLogger()->logHistoryPages();
440
441        // Check that both page_count and page_size entries were created
442        $pageCount = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['page_count']);
443        $pageSize = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['page_size']);
444
445        $this->assertIsNumeric($pageCount);
446        $this->assertIsNumeric($pageSize);
447        $this->assertGreaterThanOrEqual(0, $pageCount);
448        $this->assertGreaterThanOrEqual(0, $pageSize);
449    }
450
451    /**
452     * Test logHistoryMedia method
453     */
454    public function testLogHistoryMedia()
455    {
456        $this->helper->getLogger()->logHistoryMedia();
457
458        // Check that both media_count and media_size entries were created
459        $mediaCount = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['media_count']);
460        $mediaSize = $this->helper->getDB()->queryValue('SELECT value FROM history WHERE info = ?', ['media_size']);
461
462        $this->assertIsNumeric($mediaCount);
463        $this->assertIsNumeric($mediaSize);
464        $this->assertGreaterThanOrEqual(0, $mediaCount);
465        $this->assertGreaterThanOrEqual(0, $mediaSize);
466    }
467
468    /**
469     * Test that feedreader user agents are handled correctly
470     */
471    public function testFeedReaderUserAgent()
472    {
473        // Use a user agent that DeviceDetector recognizes as a feedreader
474        $_SERVER['HTTP_USER_AGENT'] = 'BashPodder/1.0 (http://bashpodder.sourceforge.net/)';
475
476        $logger = new Logger($this->helper);
477
478        // Use reflection to access protected property
479        $reflection = new \ReflectionClass($logger);
480        $uaTypeProperty = $reflection->getProperty('uaType');
481        $uaTypeProperty->setAccessible(true);
482
483        $this->assertEquals('feedreader', $uaTypeProperty->getValue($logger));
484    }
485}
486