From 9593a43cc05108184257062a3fd7e758645edc23 Mon Sep 17 00:00:00 2001 From: Greg Sarjeant Date: Sat, 2 Aug 2025 01:43:48 +0000 Subject: [PATCH] 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 Co-committed-by: Greg Sarjeant --- public/index.php | 9 +- .../AdminController/AdminController.php | 268 +++++++------ src/Controller/Controller.php | 2 + .../FeedController/FeedController.php | 10 +- .../HomeController/HomeController.php | 69 ++-- .../LogController/LogController.php | 3 +- src/Framework/Router/Router.php | 6 +- src/Model/ConfigModel/ConfigModel.php | 25 +- src/Model/TickModel/TickModel.php | 15 +- src/Model/UserModel/UserModel.php | 31 +- .../AdminController/AdminControllerTest.php | 367 ++++++++++++++++++ .../FeedController/FeedControllerTest.php | 175 +++++++++ .../HomeController/HomeControllerTest.php | 312 +++++++++++++++ .../LogController/LogControllerTest.php | 43 +- tests/Feed/AtomGeneratorTest.php | 3 +- tests/Feed/FeedGeneratorTest.php | 12 +- tests/Feed/RssGeneratorTest.php | 3 +- 17 files changed, 1149 insertions(+), 204 deletions(-) create mode 100644 tests/Controller/AdminController/AdminControllerTest.php create mode 100644 tests/Controller/FeedController/FeedControllerTest.php create mode 100644 tests/Controller/HomeController/HomeControllerTest.php diff --git a/public/index.php b/public/index.php index 327c91a..2fa0a57 100644 --- a/public/index.php +++ b/public/index.php @@ -52,8 +52,10 @@ $db = Database::get(); global $config; global $user; -$config = ConfigModel::load(); -$user = UserModel::load(); +$config = new ConfigModel($db); +$config = $config->loadFromDatabase(); +$user = new UserModel($db); +$user = $user->loadFromDatabase(); // Start a session and generate a CSRF Token // if there isn't already an active session @@ -97,7 +99,8 @@ if ($method === 'POST' && $path != 'setup') { header('Content-Type: text/html; charset=utf-8'); // Render the requested route or throw a 404 -if (!Router::route($path, $method)){ +$router = new Router($db, $config, $user); +if (!$router->route($path, $method)){ http_response_code(404); echo "404 - Page Not Found"; exit; diff --git a/src/Controller/AdminController/AdminController.php b/src/Controller/AdminController/AdminController.php index d0f17ef..f2ff3a6 100644 --- a/src/Controller/AdminController/AdminController.php +++ b/src/Controller/AdminController/AdminController.php @@ -3,147 +3,167 @@ class AdminController extends Controller { // GET handler // render the admin page public function index(){ - global $config; - global $user; - - $vars = [ - 'user' => $user, - 'config' => $config, - 'isSetup' => false, - ]; - - $this->render("admin.php", $vars); + $data = $this->getAdminData(false); + $this->render("admin.php", $data); } public function showSetup(){ - global $config; - global $user; - - $vars = [ - 'user' => $user, - 'config' => $config, - 'isSetup' => true, + $data = $this->getAdminData(true); + $this->render("admin.php", $data); + } + + public function getAdminData(bool $isSetup): array { + Log::debug("Loading admin page" . ($isSetup ? " (setup mode)" : "")); + + return [ + 'user' => $this->user, + 'config' => $this->config, + 'isSetup' => $isSetup, ]; - - $this->render("admin.php", $vars); } public function handleSave(){ if (!Session::isLoggedIn()){ - header('Location: ' . Util::buildRelativeUrl($config->basePath, 'login')); + header('Location: ' . Util::buildRelativeUrl($this->config->basePath, 'login')); exit; } - $this->save(); + $result = $this->processSettingsSave($_POST, false); + header('Location: ' . $_SERVER['PHP_SELF']); + exit; } public function handleSetup(){ // for setup, we don't care if they're logged in // (because they can't be until setup is complete) - $this->save(); - } - - // save updated settings - private function save(){ - global $config; - global $user; - - // handle form submission - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $errors = []; - - // User profile - $username = trim($_POST['username'] ?? ''); - $displayName = trim($_POST['display_name'] ?? ''); - $website = trim($_POST['website'] ?? ''); - - // Site settings - $siteTitle = trim($_POST['site_title']) ?? ''; - $siteDescription = trim($_POST['site_description']) ?? ''; - $baseUrl = trim($_POST['base_url'] ?? ''); - $basePath = trim($_POST['base_path'] ?? '/'); - $itemsPerPage = (int) ($_POST['items_per_page'] ?? 25); - $strictAccessibility = isset($_POST['strict_accessibility']); - $logLevel = (int) ($_POST['log_level'] ?? ''); - - // Password - $password = $_POST['password'] ?? ''; - $confirmPassword = $_POST['confirm_password'] ?? ''; - - // Validate user profile - if (!$username) { - $errors[] = "Username is required."; - } - if (!$displayName) { - $errors[] = "Display name is required."; - } - if (!$baseUrl) { - $errors[] = "Base URL is required."; - } - // Make sure the website looks like a URL and starts with a protocol - if ($website) { - if (!filter_var($website, FILTER_VALIDATE_URL)) { - $errors[] = "Please enter a valid URL (including http:// or https://)."; - } elseif (!preg_match('/^https?:\/\//i', $website)) { - $errors[] = "URL must start with http:// or https://."; - } - } - - // Validate site settings - if (!$siteTitle) { - $errors[] = "Site title is required."; - } - if (!preg_match('#^/[^?<>:"|\\*]*$#', $basePath)) { - $errors[] = "Base path must look like a valid URL path (e.g. / or /tkr/)."; - } - if ($itemsPerPage < 1 || $itemsPerPage > 50) { - $errors[] = "Items per page must be a number between 1 and 50."; - } - - // If a password was sent, make sure it matches the confirmation - if ($password && !($password === $confirmPassword)){ - $errors[] = "Passwords do not match"; - } - - // Validation complete - if (empty($errors)) { - // Update site settings - $config->siteTitle = $siteTitle; - $config->siteDescription = $siteDescription; - $config->baseUrl = $baseUrl; - $config->basePath = $basePath; - $config->itemsPerPage = $itemsPerPage; - $config->strictAccessibility = $strictAccessibility; - $config->logLevel = $logLevel; - - // Save site settings and reload config from database - // TODO - raise and handle exception on failure - $config = $config->save(); - - // Update user profile - $user->username = $username; - $user->displayName = $displayName; - $user->website = $website; - - // Save user profile and reload user from database - // TODO - raise and handle exception on failure - $user = $user->save(); - - // Update the password if one was sent - // TODO - raise and handle exception on failure - if($password){ - $user->setPassword($password); - } - - Session::setFlashMessage('success', 'Settings updated'); - } else { - foreach($errors as $error){ - Session::setFlashMessage('error', $error); - } - } - } - + $result = $this->processSettingsSave($_POST, true); header('Location: ' . $_SERVER['PHP_SELF']); exit; } + + public function processSettingsSave(array $postData, bool $isSetup): array { + $result = ['success' => false, 'errors' => []]; + + Log::debug("Processing settings save" . ($isSetup ? " (setup mode)" : "")); + + // handle form submission + if (empty($postData)) { + Log::warning("Settings save called with no POST data"); + $result['errors'][] = 'No data provided'; + return $result; + } + + $errors = []; + + // User profile + $username = trim($postData['username'] ?? ''); + $displayName = trim($postData['display_name'] ?? ''); + $website = trim($postData['website'] ?? ''); + + // Site settings + $siteTitle = trim($postData['site_title'] ?? ''); + $siteDescription = trim($postData['site_description'] ?? ''); + $baseUrl = trim($postData['base_url'] ?? ''); + $basePath = trim($postData['base_path'] ?? '/'); + $itemsPerPage = (int) ($postData['items_per_page'] ?? 25); + $strictAccessibility = isset($postData['strict_accessibility']); + $logLevel = (int) ($postData['log_level'] ?? 0); + + // Password + $password = $postData['password'] ?? ''; + $confirmPassword = $postData['confirm_password'] ?? ''; + + Log::info("Processing settings for user: $username"); + + // Validate user profile + if (!$username) { + $errors[] = "Username is required."; + } + if (!$displayName) { + $errors[] = "Display name is required."; + } + if (!$baseUrl) { + $errors[] = "Base URL is required."; + } + // Make sure the website looks like a URL and starts with a protocol + if ($website) { + if (!filter_var($website, FILTER_VALIDATE_URL)) { + $errors[] = "Please enter a valid URL (including http:// or https://)."; + } elseif (!preg_match('/^https?:\/\//i', $website)) { + $errors[] = "URL must start with http:// or https://."; + } + } + + // Validate site settings + if (!$siteTitle) { + $errors[] = "Site title is required."; + } + if (!preg_match('#^/[^?<>:"|\\*]*$#', $basePath)) { + $errors[] = "Base path must look like a valid URL path (e.g. / or /tkr/)."; + } + if ($itemsPerPage < 1 || $itemsPerPage > 50) { + $errors[] = "Items per page must be a number between 1 and 50."; + } + + // If a password was sent, make sure it matches the confirmation + if ($password && !($password === $confirmPassword)){ + $errors[] = "Passwords do not match"; + } + + // Log validation results + if (!empty($errors)) { + Log::warning("Settings validation failed with " . count($errors) . " errors"); + foreach ($errors as $error) { + Log::debug("Validation error: $error"); + } + } + + // Validation complete + if (empty($errors)) { + try { + // Update site settings + $this->config->siteTitle = $siteTitle; + $this->config->siteDescription = $siteDescription; + $this->config->baseUrl = $baseUrl; + $this->config->basePath = $basePath; + $this->config->itemsPerPage = $itemsPerPage; + $this->config->strictAccessibility = $strictAccessibility; + $this->config->logLevel = $logLevel; + + // Save site settings and reload config from database + $this->config = $this->config->save(); + Log::info("Site settings updated"); + + // Update user profile + $this->user->username = $username; + $this->user->displayName = $displayName; + $this->user->website = $website; + + // Save user profile and reload user from database + $this->user = $this->user->save(); + Log::info("User profile updated"); + + // Update the password if one was sent + if($password){ + $this->user->setPassword($password); + Log::info("User password updated"); + } + + Session::setFlashMessage('success', 'Settings updated'); + $result['success'] = true; + + } catch (Exception $e) { + Log::error("Failed to save settings: " . $e->getMessage()); + Session::setFlashMessage('error', 'Failed to save settings'); + $result['errors'][] = 'Failed to save settings'; + } + } else { + foreach($errors as $error){ + Session::setFlashMessage('error', $error); + } + $result['errors'] = $errors; + } + + return $result; + } } diff --git a/src/Controller/Controller.php b/src/Controller/Controller.php index 043acf2..d72bd58 100644 --- a/src/Controller/Controller.php +++ b/src/Controller/Controller.php @@ -1,5 +1,7 @@ config = ConfigModel::load(); - $tickModel = new TickModel(); - $this->ticks = $tickModel->getPage($this->config->itemsPerPage); + public function __construct(PDO $db, ConfigModel $config, UserModel $user){ + parent::__construct($db, $config, $user); + + $tickModel = new TickModel($db, $config); + $this->ticks = $tickModel->getPage($config->itemsPerPage); Log::debug("Loaded " . count($this->ticks) . " ticks for feeds"); } diff --git a/src/Controller/HomeController/HomeController.php b/src/Controller/HomeController/HomeController.php index f417540..06e5097 100644 --- a/src/Controller/HomeController/HomeController.php +++ b/src/Controller/HomeController/HomeController.php @@ -4,43 +4,68 @@ class HomeController extends Controller { // renders the homepage view. public function index(){ $page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; - global $config; - global $user; + $data = $this->getHomeData($page); + $this->render("home.php", $data); + } + + public function getHomeData(int $page): array { + Log::debug("Loading home page $page"); - $tickModel = new TickModel(); - $limit = $config->itemsPerPage; + $tickModel = new TickModel($this->db, $this->config); + $limit = $this->config->itemsPerPage; $offset = ($page - 1) * $limit; $ticks = $tickModel->getPage($limit, $offset); - $view = new TicksView($config, $ticks, $page); + $view = new TicksView($this->config, $ticks, $page); $tickList = $view->getHtml(); - $vars = [ - 'config' => $config, - 'user' => $user, + Log::info("Home page loaded with " . count($ticks) . " ticks"); + + return [ + 'config' => $this->config, + 'user' => $this->user, 'tickList' => $tickList, ]; - - $this->render("home.php", $vars); } // POST handler // Saves the tick and reloads the homepage public function handleTick(){ - if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['new_tick'])) { - // save the tick - if (trim($_POST['new_tick'])){ - $tickModel = new TickModel(); - $tickModel->insert($_POST['new_tick']); - } - } - - // get the config - global $config; - + $result = $this->processTick($_POST); + // redirect to the index (will show the latest tick if one was sent) - header('Location: ' . Util::buildRelativeUrl($config->basePath)); + header('Location: ' . Util::buildRelativeUrl($this->config->basePath)); exit; } + + public function processTick(array $postData): array { + $result = ['success' => false, 'message' => '']; + + if (!isset($postData['new_tick'])) { + Log::warning("Tick submission without new_tick field"); + $result['message'] = 'No tick content provided'; + return $result; + } + + $tickContent = trim($postData['new_tick']); + if (empty($tickContent)) { + Log::debug("Empty tick submission ignored"); + $result['message'] = 'Empty tick ignored'; + return $result; + } + + try { + $tickModel = new TickModel($this->db, $this->config); + $tickModel->insert($tickContent); + Log::info("New tick created: " . substr($tickContent, 0, 50) . (strlen($tickContent) > 50 ? '...' : '')); + $result['success'] = true; + $result['message'] = 'Tick saved successfully'; + } catch (Exception $e) { + Log::error("Failed to save tick: " . $e->getMessage()); + $result['message'] = 'Failed to save tick'; + } + + return $result; + } } \ No newline at end of file diff --git a/src/Controller/LogController/LogController.php b/src/Controller/LogController/LogController.php index ff807ca..f9fe2fb 100644 --- a/src/Controller/LogController/LogController.php +++ b/src/Controller/LogController/LogController.php @@ -2,7 +2,8 @@ class LogController extends Controller { private string $storageDir; - public function __construct(?string $storageDir = null) { + public function __construct(PDO $db, ConfigModel $config, UserModel $user, ?string $storageDir = null) { + parent::__construct($db, $config, $user); $this->storageDir = $storageDir ?? STORAGE_DIR; } diff --git a/src/Framework/Router/Router.php b/src/Framework/Router/Router.php index 5d3e7b4..3cea851 100644 --- a/src/Framework/Router/Router.php +++ b/src/Framework/Router/Router.php @@ -1,6 +1,8 @@ db, $this->config, $this->user); call_user_func_array([$instance, $functionName], $matches); return true; } diff --git a/src/Model/ConfigModel/ConfigModel.php b/src/Model/ConfigModel/ConfigModel.php index 98414c6..e6f0d92 100644 --- a/src/Model/ConfigModel/ConfigModel.php +++ b/src/Model/ConfigModel/ConfigModel.php @@ -11,15 +11,23 @@ class ConfigModel { public bool $strictAccessibility = true; public ?int $logLevel = null; - // load config from sqlite database + public function __construct(private PDO $db) {} + + // load config from sqlite database (backward compatibility) public static function load(): self { + global $db; + $instance = new self($db); + return $instance->loadFromDatabase(); + } + + // Instance method that uses injected database + public function loadFromDatabase(): self { $init = require APP_ROOT . '/config/init.php'; - $c = new self(); + $c = new self($this->db); $c->baseUrl = ($c->baseUrl === '') ? $init['base_url'] : $c->baseUrl; $c->basePath = ($c->basePath === '') ? $init['base_path'] : $c->basePath; - global $db; - $stmt = $db->query("SELECT site_title, + $stmt = $this->db->query("SELECT site_title, site_description, base_url, base_path, @@ -58,11 +66,10 @@ class ConfigModel { } public function save(): self { - global $db; - $settingsCount = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); + $settingsCount = (int) $this->db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); if ($settingsCount === 0){ - $stmt = $db->prepare("INSERT INTO settings ( + $stmt = $this->db->prepare("INSERT INTO settings ( id, site_title, site_description, @@ -75,7 +82,7 @@ class ConfigModel { ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)"); } else { - $stmt = $db->prepare("UPDATE settings SET + $stmt = $this->db->prepare("UPDATE settings SET site_title=?, site_description=?, base_url=?, @@ -97,6 +104,6 @@ class ConfigModel { $this->logLevel ]); - return self::load(); + return $this->loadFromDatabase(); } } diff --git a/src/Model/TickModel/TickModel.php b/src/Model/TickModel/TickModel.php index be947a3..d75be74 100644 --- a/src/Model/TickModel/TickModel.php +++ b/src/Model/TickModel/TickModel.php @@ -1,27 +1,24 @@ prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?"); + $stmt = $this->db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?"); $stmt->execute([$limit, $offset]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } public function insert(string $tick, ?DateTimeImmutable $datetime = null): void { - global $db; $datetime ??= new DateTimeImmutable('now', new DateTimeZone('UTC')); $timestamp = $datetime->format('Y-m-d H:i:s'); - $stmt = $db->prepare("INSERT INTO tick(timestamp, tick) values (?, ?)"); + $stmt = $this->db->prepare("INSERT INTO tick(timestamp, tick) values (?, ?)"); $stmt->execute([$timestamp, $tick]); } public function get(int $id): array { - global $db; - - $stmt = $db->prepare("SELECT timestamp, tick FROM tick WHERE id=?"); + $stmt = $this->db->prepare("SELECT timestamp, tick FROM tick WHERE id=?"); $stmt->execute([$id]); $row = $stmt->fetch(PDO::FETCH_ASSOC); @@ -29,7 +26,7 @@ class TickModel { return [ 'tickTime' => $row['timestamp'], 'tick' => $row['tick'], - 'config' => ConfigModel::load(), + 'config' => $this->config, ]; } } diff --git a/src/Model/UserModel/UserModel.php b/src/Model/UserModel/UserModel.php index 7556016..48b032f 100644 --- a/src/Model/UserModel/UserModel.php +++ b/src/Model/UserModel/UserModel.php @@ -6,14 +6,21 @@ class UserModel { public string $website = ''; public string $mood = ''; - // load user settings from sqlite database + public function __construct(private PDO $db) {} + + // load user settings from sqlite database (backward compatibility) public static function load(): self { global $db; - + $instance = new self($db); + return $instance->loadFromDatabase(); + } + + // Instance method that uses injected database + public function loadFromDatabase(): self { // There's only ever one user. I'm just leaning into that. - $stmt = $db->query("SELECT username, display_name, website, mood FROM user WHERE id=1"); + $stmt = $this->db->query("SELECT username, display_name, website, mood FROM user WHERE id=1"); $row = $stmt->fetch(PDO::FETCH_ASSOC); - $u = new self(); + $u = new self($this->db); if ($row) { $u->username = $row['username']; @@ -26,33 +33,29 @@ class UserModel { } public function save(): self { - global $db; - $userCount = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn(); + $userCount = (int) $this->db->query("SELECT COUNT(*) FROM user")->fetchColumn(); if ($userCount === 0){ - $stmt = $db->prepare("INSERT INTO user (id, username, display_name, website, mood) VALUES (1, ?, ?, ?, ?)"); + $stmt = $this->db->prepare("INSERT INTO user (id, username, display_name, website, mood) VALUES (1, ?, ?, ?, ?)"); } else { - $stmt = $db->prepare("UPDATE user SET username=?, display_name=?, website=?, mood=? WHERE id=1"); + $stmt = $this->db->prepare("UPDATE user SET username=?, display_name=?, website=?, mood=? WHERE id=1"); } $stmt->execute([$this->username, $this->displayName, $this->website, $this->mood]); - return self::load(); + return $this->loadFromDatabase(); } // Making this a separate function to avoid // loading the password into memory public function setPassword(string $password): void { - global $db; - $hash = password_hash($password, PASSWORD_DEFAULT); - $stmt = $db->prepare("UPDATE user SET password_hash=? WHERE id=1"); + $stmt = $this->db->prepare("UPDATE user SET password_hash=? WHERE id=1"); $stmt->execute([$hash]); } public function getByUsername($username){ - global $db; - $stmt = $db->prepare("SELECT id, username, password_hash FROM user WHERE username = ?"); + $stmt = $this->db->prepare("SELECT id, username, password_hash FROM user WHERE username = ?"); $stmt->execute([$username]); $record = $stmt->fetch(); diff --git a/tests/Controller/AdminController/AdminControllerTest.php b/tests/Controller/AdminController/AdminControllerTest.php new file mode 100644 index 0000000..1725369 --- /dev/null +++ b/tests/Controller/AdminController/AdminControllerTest.php @@ -0,0 +1,367 @@ +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 (needed for base constructor) + $this->mockPdo = $this->createMock(PDO::class); + + // Create real config and user objects with mocked PDO + $this->config = new ConfigModel($this->mockPdo); + $this->config->siteTitle = 'Test Site'; + $this->config->siteDescription = 'Test Description'; + $this->config->baseUrl = 'https://example.com'; + $this->config->basePath = '/tkr'; + $this->config->itemsPerPage = 10; + + $this->user = new UserModel($this->mockPdo); + $this->user->username = 'testuser'; + $this->user->displayName = 'Test User'; + $this->user->website = 'https://example.com'; + } + + 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); + } + + public function testGetAdminDataRegularMode(): void + { + $controller = new AdminController($this->mockPdo, $this->config, $this->user); + $data = $controller->getAdminData(false); + + // Should return proper structure + $this->assertArrayHasKey('config', $data); + $this->assertArrayHasKey('user', $data); + $this->assertArrayHasKey('isSetup', $data); + + // Should be the injected instances + $this->assertSame($this->config, $data['config']); + $this->assertSame($this->user, $data['user']); + $this->assertFalse($data['isSetup']); + } + + public function testGetAdminDataSetupMode(): void + { + $controller = new AdminController($this->mockPdo, $this->config, $this->user); + $data = $controller->getAdminData(true); + + // Should return proper structure + $this->assertArrayHasKey('config', $data); + $this->assertArrayHasKey('user', $data); + $this->assertArrayHasKey('isSetup', $data); + + // Should be the injected instances + $this->assertSame($this->config, $data['config']); + $this->assertSame($this->user, $data['user']); + $this->assertTrue($data['isSetup']); + } + + public function testProcessSettingsSaveWithEmptyData(): void + { + $controller = new AdminController($this->mockPdo, $this->config, $this->user); + $result = $controller->processSettingsSave([], false); + + $this->assertFalse($result['success']); + $this->assertContains('No data provided', $result['errors']); + } + + public function testProcessSettingsSaveValidationErrors(): void + { + $controller = new AdminController($this->mockPdo, $this->config, $this->user); + + // Test data with multiple validation errors + $postData = [ + 'username' => '', // Missing username + 'display_name' => '', // Missing display name + 'website' => 'invalid-url', // Invalid URL + 'site_title' => '', // Missing site title + 'base_url' => '', // Missing base URL + 'base_path' => 'invalid', // Invalid base path + 'items_per_page' => 100, // Too high + 'password' => 'test123', + 'confirm_password' => 'different' // Passwords don't match + ]; + + $result = $controller->processSettingsSave($postData, false); + + $this->assertFalse($result['success']); + $this->assertNotEmpty($result['errors']); + + // Should have multiple validation errors + $this->assertGreaterThan(5, count($result['errors'])); + } + + public function testProcessSettingsSaveValidData(): void + { + // Mock PDO to simulate successful database operations + $mockStatement = $this->createMock(PDOStatement::class); + $mockStatement->method('execute')->willReturn(true); + $mockStatement->method('fetchColumn')->willReturn(1); // Existing record count + $mockStatement->method('fetch')->willReturnOnConsecutiveCalls( + [ + 'site_title' => 'Updated Site', + 'site_description' => 'Updated Description', + 'base_url' => 'https://updated.com', + 'base_path' => '/updated', + 'items_per_page' => 15, + 'css_id' => null, + 'strict_accessibility' => true, + 'log_level' => 2 + ], + [ + 'username' => 'newuser', + 'display_name' => 'New User', + 'website' => 'https://example.com', + 'mood' => '' + ] + ); + + $this->mockPdo->method('prepare')->willReturn($mockStatement); + $this->mockPdo->method('query')->willReturn($mockStatement); + + // Create models with mocked PDO + $config = new ConfigModel($this->mockPdo); + $user = new UserModel($this->mockPdo); + + $controller = new AdminController($this->mockPdo, $config, $user); + + $postData = [ + 'username' => 'newuser', + 'display_name' => 'New User', + 'website' => 'https://example.com', + 'site_title' => 'Updated Site', + 'site_description' => 'Updated Description', + 'base_url' => 'https://updated.com', + 'base_path' => '/updated', + 'items_per_page' => 15, + 'strict_accessibility' => 'on', + 'log_level' => 2 + ]; + + $result = $controller->processSettingsSave($postData, false); + + $this->assertTrue($result['success']); + $this->assertEmpty($result['errors']); + } + + public function testProcessSettingsSaveWithPassword(): void + { + // Mock PDO for successful save operations + $mockStatement = $this->createMock(PDOStatement::class); + $mockStatement->method('execute')->willReturn(true); + $mockStatement->method('fetchColumn')->willReturn(1); + $mockStatement->method('fetch')->willReturnOnConsecutiveCalls( + [ + 'site_title' => 'Test Site', + 'site_description' => 'Test Description', + 'base_url' => 'https://example.com', + 'base_path' => '/tkr', + 'items_per_page' => 10, + 'css_id' => null, + 'strict_accessibility' => true, + 'log_level' => 2 + ], + [ + 'username' => 'testuser', + 'display_name' => 'Test User', + 'website' => '', + 'mood' => '' + ] + ); + + // Verify password hash is called + $this->mockPdo->expects($this->atLeastOnce()) + ->method('prepare') + ->willReturn($mockStatement); + + $this->mockPdo->method('query')->willReturn($mockStatement); + + // Create models with mocked PDO + $config = new ConfigModel($this->mockPdo); + $user = new UserModel($this->mockPdo); + + $controller = new AdminController($this->mockPdo, $config, $user); + + $postData = [ + 'username' => 'testuser', + 'display_name' => 'Test User', + 'site_title' => 'Test Site', + 'site_description' => 'Test Description', + 'base_url' => 'https://example.com', + 'base_path' => '/tkr', + 'items_per_page' => 10, + 'password' => 'newpassword', + 'confirm_password' => 'newpassword' + ]; + + $result = $controller->processSettingsSave($postData, false); + + $this->assertTrue($result['success']); + } + + public function testProcessSettingsSaveDatabaseError(): void + { + // Mock PDO to throw exception on save + $this->mockPdo->method('query') + ->willThrowException(new PDOException("Database error")); + + $config = new ConfigModel($this->mockPdo); + $user = new UserModel($this->mockPdo); + + $controller = new AdminController($this->mockPdo, $config, $user); + + $postData = [ + 'username' => 'testuser', + 'display_name' => 'Test User', + 'site_title' => 'Test Site', + 'site_description' => 'Test Description', + 'base_url' => 'https://example.com', + 'base_path' => '/tkr', + 'items_per_page' => 10 + ]; + + $result = $controller->processSettingsSave($postData, false); + + $this->assertFalse($result['success']); + $this->assertContains('Failed to save settings', $result['errors']); + } + + public function testLoggingOnAdminPageLoad(): void + { + $controller = new AdminController($this->mockPdo, $this->config, $this->user); + $controller->getAdminData(false); + + // Check that logs were written + $logFile = $this->tempLogDir . '/logs/tkr.log'; + $this->assertFileExists($logFile); + + $logContent = file_get_contents($logFile); + $this->assertStringContainsString('Loading admin page', $logContent); + } + + public function testLoggingOnSetupPageLoad(): void + { + $controller = new AdminController($this->mockPdo, $this->config, $this->user); + $controller->getAdminData(true); + + // Check that logs were written + $logFile = $this->tempLogDir . '/logs/tkr.log'; + $this->assertFileExists($logFile); + + $logContent = file_get_contents($logFile); + $this->assertStringContainsString('Loading admin page (setup mode)', $logContent); + } + + public function testLoggingOnValidationErrors(): void + { + $controller = new AdminController($this->mockPdo, $this->config, $this->user); + + $postData = [ + 'username' => '', // Will cause validation error + 'display_name' => 'Test User', + 'site_title' => 'Test Site', + 'base_url' => 'https://example.com', + 'base_path' => '/tkr', + 'items_per_page' => 10 + ]; + + $controller->processSettingsSave($postData, false); + + // Check that logs were written + $logFile = $this->tempLogDir . '/logs/tkr.log'; + $this->assertFileExists($logFile); + + $logContent = file_get_contents($logFile); + $this->assertStringContainsString('Settings validation failed', $logContent); + $this->assertStringContainsString('Validation error: Username is required', $logContent); + } + + public function testLoggingOnSuccessfulSave(): void + { + // Mock successful database operations + $mockStatement = $this->createMock(PDOStatement::class); + $mockStatement->method('execute')->willReturn(true); + $mockStatement->method('fetchColumn')->willReturn(1); + $mockStatement->method('fetch')->willReturnOnConsecutiveCalls( + [ + 'site_title' => 'Test Site', + 'site_description' => 'Test Description', + 'base_url' => 'https://example.com', + 'base_path' => '/tkr', + 'items_per_page' => 10, + 'css_id' => null, + 'strict_accessibility' => true, + 'log_level' => 2 + ], + [ + 'username' => 'testuser', + 'display_name' => 'Test User', + 'website' => '', + 'mood' => '' + ] + ); + + $this->mockPdo->method('prepare')->willReturn($mockStatement); + $this->mockPdo->method('query')->willReturn($mockStatement); + + $config = new ConfigModel($this->mockPdo); + $user = new UserModel($this->mockPdo); + + $controller = new AdminController($this->mockPdo, $config, $user); + + $postData = [ + 'username' => 'testuser', + 'display_name' => 'Test User', + 'site_title' => 'Test Site', + 'site_description' => 'Test Description', + 'base_url' => 'https://example.com', + 'base_path' => '/tkr', + 'items_per_page' => 10 + ]; + + $controller->processSettingsSave($postData, false); + + // Check that logs were written + $logFile = $this->tempLogDir . '/logs/tkr.log'; + $this->assertFileExists($logFile); + + $logContent = file_get_contents($logFile); + $this->assertStringContainsString('Processing settings for user: testuser', $logContent); + $this->assertStringContainsString('Site settings updated', $logContent); + $this->assertStringContainsString('User profile updated', $logContent); + } +} \ No newline at end of file diff --git a/tests/Controller/FeedController/FeedControllerTest.php b/tests/Controller/FeedController/FeedControllerTest.php new file mode 100644 index 0000000..4f8ddd1 --- /dev/null +++ b/tests/Controller/FeedController/FeedControllerTest.php @@ -0,0 +1,175 @@ +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 with feed-relevant properties + $this->mockConfig = new ConfigModel($this->mockPdo); + $this->mockConfig->itemsPerPage = 10; + $this->mockConfig->basePath = '/tkr'; + $this->mockConfig->siteTitle = 'Test Site'; + $this->mockConfig->siteDescription = 'Test Description'; + $this->mockConfig->baseUrl = 'https://test.example.com'; + + // Mock user + $this->mockUser = new UserModel($this->mockPdo); + $this->mockUser->displayName = 'Test User'; + } + + 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); + } + + public function testControllerInstantiationWithNoTicks(): void + { + $this->setupMockDatabase([]); + + $controller = new FeedController($this->mockPdo, $this->mockConfig, $this->mockUser); + + // Verify it was created successfully + $this->assertInstanceOf(FeedController::class, $controller); + + // Check logs + $logFile = $this->tempLogDir . '/logs/tkr.log'; + $this->assertFileExists($logFile); + + $logContent = file_get_contents($logFile); + $this->assertStringContainsString('Loaded 0 ticks for feeds', $logContent); + } + + public function testControllerInstantiationWithTicks(): void + { + $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'], + ]; + + $this->setupMockDatabase($testTicks); + + $controller = new FeedController($this->mockPdo, $this->mockConfig, $this->mockUser); + + // Verify it was created successfully + $this->assertInstanceOf(FeedController::class, $controller); + + // Check logs + $logFile = $this->tempLogDir . '/logs/tkr.log'; + $this->assertFileExists($logFile); + + $logContent = file_get_contents($logFile); + $this->assertStringContainsString('Loaded 2 ticks for feeds', $logContent); + } + + public function testControllerCallsDatabaseCorrectly(): void + { + $this->setupMockDatabase([]); + + // Verify that PDO prepare is called with the correct SQL for tick loading + $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 (page 1, offset 0) + $this->mockStatement->expects($this->once()) + ->method('execute') + ->with([10, 0]); // itemsPerPage=10, page 1 = offset 0 + + new FeedController($this->mockPdo, $this->mockConfig, $this->mockUser); + } + + public function testRssMethodLogsCorrectly(): void + { + $testTicks = [ + ['id' => 1, 'timestamp' => '2025-01-31 12:00:00', 'tick' => 'Test tick'] + ]; + + $this->setupMockDatabase($testTicks); + + $controller = new FeedController($this->mockPdo, $this->mockConfig, $this->mockUser); + + // Capture output to prevent headers/content from affecting test + ob_start(); + $controller->rss(); + ob_end_clean(); + + // Check logs for RSS generation + $logFile = $this->tempLogDir . '/logs/tkr.log'; + $logContent = file_get_contents($logFile); + $this->assertStringContainsString('Generating RSS feed with 1 ticks', $logContent); + } + + public function testAtomMethodLogsCorrectly(): void + { + $testTicks = [ + ['id' => 1, 'timestamp' => '2025-01-31 12:00:00', 'tick' => 'Test tick'], + ['id' => 2, 'timestamp' => '2025-01-31 13:00:00', 'tick' => 'Another tick'] + ]; + + $this->setupMockDatabase($testTicks); + + $controller = new FeedController($this->mockPdo, $this->mockConfig, $this->mockUser); + + // Capture output to prevent headers/content from affecting test + ob_start(); + $controller->atom(); + ob_end_clean(); + + // Check logs for Atom generation + $logFile = $this->tempLogDir . '/logs/tkr.log'; + $logContent = file_get_contents($logFile); + $this->assertStringContainsString('Generating Atom feed with 2 ticks', $logContent); + } +} \ No newline at end of file diff --git a/tests/Controller/HomeController/HomeControllerTest.php b/tests/Controller/HomeController/HomeControllerTest.php new file mode 100644 index 0000000..647e94f --- /dev/null +++ b/tests/Controller/HomeController/HomeControllerTest.php @@ -0,0 +1,312 @@ +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); + } +} \ No newline at end of file diff --git a/tests/Controller/LogController/LogControllerTest.php b/tests/Controller/LogController/LogControllerTest.php index 0858c5f..6c90c22 100644 --- a/tests/Controller/LogController/LogControllerTest.php +++ b/tests/Controller/LogController/LogControllerTest.php @@ -21,7 +21,8 @@ class LogControllerTest extends TestCase // Mock global config global $config; - $config = new ConfigModel(); + $mockPdo = $this->createMock(PDO::class); + $config = new ConfigModel($mockPdo); $config->baseUrl = 'https://example.com'; $config->basePath = '/tkr/'; } @@ -50,7 +51,10 @@ class LogControllerTest extends TestCase public function testGetLogDataWithNoLogFiles(): void { - $controller = new LogController($this->tempLogDir); + $mockPdo = $this->createMock(PDO::class); + $mockConfig = new ConfigModel($mockPdo); + $mockUser = new UserModel($mockPdo); + $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir); $data = $controller->getLogData(); // Should return empty log entries but valid structure @@ -81,7 +85,10 @@ class LogControllerTest extends TestCase file_put_contents($this->testLogFile, $logContent); - $controller = new LogController($this->tempLogDir); + $mockPdo = $this->createMock(PDO::class); + $mockConfig = new ConfigModel($mockPdo); + $mockUser = new UserModel($mockPdo); + $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir); $data = $controller->getLogData(); // Should parse all valid entries and ignore invalid ones @@ -122,7 +129,10 @@ class LogControllerTest extends TestCase file_put_contents($this->testLogFile, $logContent); - $controller = new LogController($this->tempLogDir); + $mockPdo = $this->createMock(PDO::class); + $mockConfig = new ConfigModel($mockPdo); + $mockUser = new UserModel($mockPdo); + $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir); $data = $controller->getLogData('ERROR'); // Should only include ERROR entries @@ -142,7 +152,10 @@ class LogControllerTest extends TestCase file_put_contents($this->testLogFile, $logContent); - $controller = new LogController($this->tempLogDir); + $mockPdo = $this->createMock(PDO::class); + $mockConfig = new ConfigModel($mockPdo); + $mockUser = new UserModel($mockPdo); + $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir); $data = $controller->getLogData('', 'GET /admin'); // Should only include GET /admin entries @@ -162,7 +175,10 @@ class LogControllerTest extends TestCase file_put_contents($this->testLogFile, $logContent); - $controller = new LogController($this->tempLogDir); + $mockPdo = $this->createMock(PDO::class); + $mockConfig = new ConfigModel($mockPdo); + $mockUser = new UserModel($mockPdo); + $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir); $data = $controller->getLogData('ERROR', 'GET /admin'); // Should only include entries matching both filters @@ -185,7 +201,10 @@ class LogControllerTest extends TestCase $rotatedLog2 = '[2025-01-31 12:00:00] WARNING: 127.0.0.1 - Rotated log entry 2'; file_put_contents($this->testLogFile . '.2', $rotatedLog2); - $controller = new LogController($this->tempLogDir); + $mockPdo = $this->createMock(PDO::class); + $mockConfig = new ConfigModel($mockPdo); + $mockUser = new UserModel($mockPdo); + $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir); $data = $controller->getLogData(); // Should read from all log files, newest first @@ -207,7 +226,10 @@ class LogControllerTest extends TestCase file_put_contents($this->testLogFile, $logContent); - $controller = new LogController($this->tempLogDir); + $mockPdo = $this->createMock(PDO::class); + $mockConfig = new ConfigModel($mockPdo); + $mockUser = new UserModel($mockPdo); + $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir); $data = $controller->getLogData(); // Should extract unique routes, sorted @@ -226,7 +248,10 @@ class LogControllerTest extends TestCase file_put_contents($this->testLogFile, $logContent); - $controller = new LogController($this->tempLogDir); + $mockPdo = $this->createMock(PDO::class); + $mockConfig = new ConfigModel($mockPdo); + $mockUser = new UserModel($mockPdo); + $controller = new LogController($mockPdo, $mockConfig, $mockUser, $this->tempLogDir); $data = $controller->getLogData(); // Should only include valid entries, ignore invalid ones diff --git a/tests/Feed/AtomGeneratorTest.php b/tests/Feed/AtomGeneratorTest.php index fe74773..c64c7bb 100644 --- a/tests/Feed/AtomGeneratorTest.php +++ b/tests/Feed/AtomGeneratorTest.php @@ -4,7 +4,8 @@ use PHPUnit\Framework\TestCase; class AtomGeneratorTest extends TestCase { private function createMockConfig() { - $config = new ConfigModel(); + $mockPdo = $this->createMock(PDO::class); + $config = new ConfigModel($mockPdo); $config->siteTitle = 'Test Site'; $config->siteDescription = 'Test Description'; $config->baseUrl = 'https://example.com'; diff --git a/tests/Feed/FeedGeneratorTest.php b/tests/Feed/FeedGeneratorTest.php index 677dfa0..406032a 100644 --- a/tests/Feed/FeedGeneratorTest.php +++ b/tests/Feed/FeedGeneratorTest.php @@ -4,7 +4,8 @@ use PHPUnit\Framework\TestCase; class FeedGeneratorTest extends TestCase { private function createMockConfig() { - $config = new ConfigModel(); + $mockPdo = $this->createMock(PDO::class); + $config = new ConfigModel($mockPdo); $config->siteTitle = 'Test Site'; $config->siteDescription = 'Test Description'; $config->baseUrl = 'https://example.com'; @@ -65,7 +66,8 @@ class FeedGeneratorTest extends TestCase } public function testUrlMethodsHandleSubdomainConfiguration() { - $config = new ConfigModel(); + $mockPdo = $this->createMock(PDO::class); + $config = new ConfigModel($mockPdo); $config->siteTitle = 'Test Site'; $config->baseUrl = 'https://tkr.example.com'; $config->basePath = '/'; @@ -77,7 +79,8 @@ class FeedGeneratorTest extends TestCase } public function testUrlMethodsHandleEmptyBasePath() { - $config = new ConfigModel(); + $mockPdo = $this->createMock(PDO::class); + $config = new ConfigModel($mockPdo); $config->siteTitle = 'Test Site'; $config->baseUrl = 'https://example.com'; $config->basePath = ''; @@ -100,7 +103,8 @@ class FeedGeneratorTest extends TestCase ]; foreach ($testCases as [$basePath, $expectedSiteUrl, $expectedTickUrl]) { - $config = new ConfigModel(); + $mockPdo = $this->createMock(PDO::class); + $config = new ConfigModel($mockPdo); $config->siteTitle = 'Test Site'; $config->baseUrl = 'https://example.com'; $config->basePath = $basePath; diff --git a/tests/Feed/RssGeneratorTest.php b/tests/Feed/RssGeneratorTest.php index 7405d45..d2b2b22 100644 --- a/tests/Feed/RssGeneratorTest.php +++ b/tests/Feed/RssGeneratorTest.php @@ -4,7 +4,8 @@ use PHPUnit\Framework\TestCase; class RssGeneratorTest extends TestCase { private function createMockConfig() { - $config = new ConfigModel(); + $mockPdo = $this->createMock(PDO::class); + $config = new ConfigModel($mockPdo); $config->siteTitle = 'Test Site'; $config->siteDescription = 'Test Description'; $config->baseUrl = 'https://example.com';