Add logging and tests for the homepage and settings page. Make both support dependency injection. Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/42 Co-authored-by: Greg Sarjeant <greg@subcultureofone.org> Co-committed-by: Greg Sarjeant <greg@subcultureofone.org>
		
			
				
	
	
		
			312 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			312 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| use PHPUnit\Framework\TestCase;
 | |
| 
 | |
| class HomeControllerTest extends TestCase
 | |
| {
 | |
|     private PDO $mockPdo;
 | |
|     private PDOStatement $mockStatement;
 | |
|     private ConfigModel $mockConfig;
 | |
|     private UserModel $mockUser;
 | |
|     private string $tempLogDir;
 | |
| 
 | |
|     protected function setUp(): void
 | |
|     {
 | |
|         // Set up temporary logging
 | |
|         $this->tempLogDir = sys_get_temp_dir() . '/tkr_test_logs_' . uniqid();
 | |
|         mkdir($this->tempLogDir . '/logs', 0777, true);
 | |
|         Log::init($this->tempLogDir . '/logs/tkr.log');
 | |
|         
 | |
|         // Set up global config for logging level (DEBUG = 1)
 | |
|         global $config;
 | |
|         $config = new stdClass();
 | |
|         $config->logLevel = 1; // Allow DEBUG level logs
 | |
| 
 | |
|         // Create mock PDO and PDOStatement
 | |
|         $this->mockStatement = $this->createMock(PDOStatement::class);
 | |
|         $this->mockPdo = $this->createMock(PDO::class);
 | |
|         
 | |
|         // Mock config
 | |
|         $this->mockConfig = new ConfigModel($this->mockPdo);
 | |
|         $this->mockConfig->itemsPerPage = 10;
 | |
|         $this->mockConfig->basePath = '/tkr';
 | |
|         
 | |
|         // Mock user
 | |
|         $this->mockUser = new UserModel($this->mockPdo);
 | |
|         $this->mockUser->displayName = 'Test User';
 | |
|         $this->mockUser->mood = '😊';
 | |
|     }
 | |
| 
 | |
|     protected function tearDown(): void
 | |
|     {
 | |
|         // Clean up temp directory
 | |
|         if (is_dir($this->tempLogDir)) {
 | |
|             $this->deleteDirectory($this->tempLogDir);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private function deleteDirectory(string $dir): void
 | |
|     {
 | |
|         if (!is_dir($dir)) return;
 | |
|         
 | |
|         $files = array_diff(scandir($dir), ['.', '..']);
 | |
|         foreach ($files as $file) {
 | |
|             $path = $dir . '/' . $file;
 | |
|             is_dir($path) ? $this->deleteDirectory($path) : unlink($path);
 | |
|         }
 | |
|         rmdir($dir);
 | |
|     }
 | |
| 
 | |
|     private function setupMockDatabase(array $tickData): void
 | |
|     {
 | |
|         // Mock PDO prepare method to return our mock statement
 | |
|         $this->mockPdo->method('prepare')
 | |
|                       ->willReturn($this->mockStatement);
 | |
|         
 | |
|         // Mock statement execute method
 | |
|         $this->mockStatement->method('execute')
 | |
|                            ->willReturn(true);
 | |
|         
 | |
|         // Mock statement fetchAll to return our test data
 | |
|         $this->mockStatement->method('fetchAll')
 | |
|                            ->willReturn($tickData);
 | |
|     }
 | |
| 
 | |
|     private function setupMockDatabaseForInsert(bool $shouldSucceed = true): void
 | |
|     {
 | |
|         if ($shouldSucceed) {
 | |
|             // Mock successful insert
 | |
|             $this->mockPdo->method('prepare')
 | |
|                           ->willReturn($this->mockStatement);
 | |
|             
 | |
|             $this->mockStatement->method('execute')
 | |
|                                ->willReturn(true);
 | |
|         } else {
 | |
|             // Mock database error
 | |
|             $this->mockPdo->method('prepare')
 | |
|                           ->willThrowException(new PDOException("Database error"));
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public function testGetHomeDataWithNoTicks(): void
 | |
|     {
 | |
|         $this->setupMockDatabase([]); // Empty array = no ticks
 | |
|         
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $data = $controller->getHomeData(1);
 | |
|         
 | |
|         // Should return proper structure
 | |
|         $this->assertArrayHasKey('config', $data);
 | |
|         $this->assertArrayHasKey('user', $data);
 | |
|         $this->assertArrayHasKey('tickList', $data);
 | |
|         
 | |
|         // Config and user should be the injected instances
 | |
|         $this->assertSame($this->mockConfig, $data['config']);
 | |
|         $this->assertSame($this->mockUser, $data['user']);
 | |
|         
 | |
|         // Should have tick list HTML (even if empty)
 | |
|         $this->assertIsString($data['tickList']);
 | |
|     }
 | |
| 
 | |
|     public function testGetHomeDataWithTicks(): void
 | |
|     {
 | |
|         // Set up test tick data that the database would return
 | |
|         $testTicks = [
 | |
|             ['id' => 1, 'timestamp' => '2025-01-31 12:00:00', 'tick' => 'First tick'],
 | |
|             ['id' => 2, 'timestamp' => '2025-01-31 13:00:00', 'tick' => 'Second tick'],
 | |
|             ['id' => 3, 'timestamp' => '2025-01-31 14:00:00', 'tick' => 'Third tick'],
 | |
|         ];
 | |
|         
 | |
|         $this->setupMockDatabase($testTicks);
 | |
|         
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $data = $controller->getHomeData(1);
 | |
|         
 | |
|         // Should return proper structure
 | |
|         $this->assertArrayHasKey('config', $data);
 | |
|         $this->assertArrayHasKey('user', $data);
 | |
|         $this->assertArrayHasKey('tickList', $data);
 | |
|         
 | |
|         // Should contain tick content in HTML
 | |
|         $this->assertStringContainsString('First tick', $data['tickList']);
 | |
|         $this->assertStringContainsString('Second tick', $data['tickList']);
 | |
|         $this->assertStringContainsString('Third tick', $data['tickList']);
 | |
|     }
 | |
| 
 | |
|     public function testGetHomeDataCallsDatabaseCorrectly(): void
 | |
|     {
 | |
|         $this->setupMockDatabase([]);
 | |
|         
 | |
|         // Verify that PDO prepare is called with the correct SQL
 | |
|         $this->mockPdo->expects($this->once())
 | |
|                      ->method('prepare')
 | |
|                      ->with('SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?')
 | |
|                      ->willReturn($this->mockStatement);
 | |
|         
 | |
|         // Verify that execute is called with correct parameters for page 2
 | |
|         $this->mockStatement->expects($this->once())
 | |
|                            ->method('execute')
 | |
|                            ->with([10, 10]); // itemsPerPage=10, page 2 = offset 10
 | |
|         
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $controller->getHomeData(2); // Page 2
 | |
|     }
 | |
| 
 | |
|     public function testProcessTickSuccess(): void
 | |
|     {
 | |
|         $this->setupMockDatabaseForInsert(true);
 | |
|         
 | |
|         // Verify the INSERT SQL is called correctly
 | |
|         $this->mockPdo->expects($this->once())
 | |
|                      ->method('prepare')
 | |
|                      ->with('INSERT INTO tick(timestamp, tick) values (?, ?)')
 | |
|                      ->willReturn($this->mockStatement);
 | |
|         
 | |
|         // Verify execute is called with timestamp and content
 | |
|         $this->mockStatement->expects($this->once())
 | |
|                            ->method('execute')
 | |
|                            ->with($this->callback(function($params) {
 | |
|                                // First param should be a timestamp, second should be the tick content
 | |
|                                return count($params) === 2 
 | |
|                                    && is_string($params[0]) 
 | |
|                                    && $params[1] === 'This is a test tick';
 | |
|                            }));
 | |
|         
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $postData = ['new_tick' => 'This is a test tick'];
 | |
|         
 | |
|         $result = $controller->processTick($postData);
 | |
|         
 | |
|         $this->assertTrue($result['success']);
 | |
|         $this->assertEquals('Tick saved successfully', $result['message']);
 | |
|     }
 | |
| 
 | |
|     public function testProcessTickEmptyContent(): void
 | |
|     {
 | |
|         // PDO shouldn't be called at all for empty content
 | |
|         $this->mockPdo->expects($this->never())->method('prepare');
 | |
|         
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $postData = ['new_tick' => '   '];  // Just whitespace
 | |
|         
 | |
|         $result = $controller->processTick($postData);
 | |
|         
 | |
|         $this->assertFalse($result['success']);
 | |
|         $this->assertEquals('Empty tick ignored', $result['message']);
 | |
|     }
 | |
| 
 | |
|     public function testProcessTickMissingField(): void
 | |
|     {
 | |
|         // PDO shouldn't be called at all for missing field
 | |
|         $this->mockPdo->expects($this->never())->method('prepare');
 | |
|         
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $postData = [];  // No new_tick field
 | |
|         
 | |
|         $result = $controller->processTick($postData);
 | |
|         
 | |
|         $this->assertFalse($result['success']);
 | |
|         $this->assertEquals('No tick content provided', $result['message']);
 | |
|     }
 | |
| 
 | |
|     public function testProcessTickTrimsWhitespace(): void
 | |
|     {
 | |
|         $this->setupMockDatabaseForInsert(true);
 | |
|         
 | |
|         // Verify execute is called with trimmed content
 | |
|         $this->mockStatement->expects($this->once())
 | |
|                            ->method('execute')
 | |
|                            ->with($this->callback(function($params) {
 | |
|                                return $params[1] === 'This has whitespace'; // Should be trimmed
 | |
|                            }));
 | |
|         
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $postData = ['new_tick' => '  This has whitespace  '];
 | |
|         
 | |
|         $result = $controller->processTick($postData);
 | |
|         
 | |
|         $this->assertTrue($result['success']);
 | |
|     }
 | |
| 
 | |
|     public function testProcessTickHandlesDatabaseError(): void
 | |
|     {
 | |
|         $this->setupMockDatabaseForInsert(false); // Will throw exception
 | |
|         
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $postData = ['new_tick' => 'This will fail'];
 | |
|         
 | |
|         $result = $controller->processTick($postData);
 | |
|         
 | |
|         $this->assertFalse($result['success']);
 | |
|         $this->assertEquals('Failed to save tick', $result['message']);
 | |
|     }
 | |
| 
 | |
|     public function testLoggingOnHomePageLoad(): void
 | |
|     {
 | |
|         $testTicks = [
 | |
|             ['id' => 1, 'timestamp' => '2025-01-31 12:00:00', 'tick' => 'Test tick']
 | |
|         ];
 | |
|         $this->setupMockDatabase($testTicks);
 | |
|         
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $controller->getHomeData(1);
 | |
|         
 | |
|         // Check that logs were written
 | |
|         $logFile = $this->tempLogDir . '/logs/tkr.log';
 | |
|         $this->assertFileExists($logFile);
 | |
|         
 | |
|         $logContent = file_get_contents($logFile);
 | |
|         $this->assertStringContainsString('Loading home page 1', $logContent);
 | |
|         $this->assertStringContainsString('Home page loaded with 1 ticks', $logContent);
 | |
|     }
 | |
| 
 | |
|     public function testLoggingOnTickCreation(): void
 | |
|     {
 | |
|         $this->setupMockDatabaseForInsert(true);
 | |
|         
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $postData = ['new_tick' => 'Test tick for logging'];
 | |
|         
 | |
|         $controller->processTick($postData);
 | |
|         
 | |
|         // Check that logs were written
 | |
|         $logFile = $this->tempLogDir . '/logs/tkr.log';
 | |
|         $this->assertFileExists($logFile);
 | |
|         
 | |
|         $logContent = file_get_contents($logFile);
 | |
|         $this->assertStringContainsString('New tick created: Test tick for logging', $logContent);
 | |
|     }
 | |
| 
 | |
|     public function testLoggingOnEmptyTick(): void
 | |
|     {
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $postData = ['new_tick' => ''];
 | |
|         
 | |
|         $controller->processTick($postData);
 | |
|         
 | |
|         // Check that logs were written
 | |
|         $logFile = $this->tempLogDir . '/logs/tkr.log';
 | |
|         
 | |
|         // The log file should exist (Log::init creates it) and contain the debug message
 | |
|         $this->assertFileExists($logFile);
 | |
|         
 | |
|         $logContent = file_get_contents($logFile);
 | |
|         $this->assertStringContainsString('Empty tick submission ignored', $logContent);
 | |
|     }
 | |
| 
 | |
|     public function testLoggingOnDatabaseError(): void
 | |
|     {
 | |
|         $this->setupMockDatabaseForInsert(false);
 | |
|         
 | |
|         $controller = new HomeController($this->mockPdo, $this->mockConfig, $this->mockUser);
 | |
|         $postData = ['new_tick' => 'This will fail'];
 | |
|         
 | |
|         $controller->processTick($postData);
 | |
|         
 | |
|         // Check that logs were written
 | |
|         $logFile = $this->tempLogDir . '/logs/tkr.log';
 | |
|         $this->assertFileExists($logFile);
 | |
|         
 | |
|         $logContent = file_get_contents($logFile);
 | |
|         $this->assertStringContainsString('Failed to save tick: Database error', $logContent);
 | |
|     }
 | |
| } |