Closes https://gitea.subcultureofone.org/greg/tkr/issues/43 Use a global $app dictionary to manage global state rather than having complex class constructors that expect three input arguments. Update and fix tests. Add tests for Util class functions that broke in the refactor. Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/44 Co-authored-by: Greg Sarjeant <greg@subcultureofone.org> Co-committed-by: Greg Sarjeant <greg@subcultureofone.org>
318 lines
11 KiB
PHP
318 lines
11 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');
|
|
|
|
// 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 = '😊';
|
|
|
|
// Set up global $app for simplified dependency access
|
|
global $app;
|
|
$app = [
|
|
'db' => $this->mockPdo,
|
|
'config' => $this->mockConfig,
|
|
'user' => $this->mockUser,
|
|
];
|
|
|
|
// Set log level on config for Log class
|
|
$this->mockConfig->logLevel = 1; // Allow DEBUG level logs
|
|
}
|
|
|
|
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();
|
|
$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();
|
|
$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();
|
|
$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();
|
|
$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();
|
|
$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();
|
|
$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();
|
|
$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();
|
|
$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();
|
|
$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();
|
|
$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();
|
|
$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();
|
|
$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);
|
|
}
|
|
} |