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