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