tkr/tests/Controller/HomeController/HomeControllerTest.php
Greg Sarjeant 9593a43cc0
Some checks are pending
Run unit tests / run-unit-tests (push) Waiting to run
make-homepage-testable (#42)
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>
2025-08-02 01:43:48 +00:00

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);
}
}