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