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>
This commit is contained in:
		
							parent
							
								
									879bd9ff9f
								
							
						
					
					
						commit
						9593a43cc0
					
				| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| <?php | ||||
| class Controller { | ||||
|     public function __construct(protected PDO $db, protected ConfigModel $config, protected UserModel $user) {} | ||||
|      | ||||
|     // Renders the requested template inside templates/main/php
 | ||||
|     protected function render(string $childTemplateFile, array $vars = []) { | ||||
|         $templatePath = TEMPLATES_DIR . "/main.php"; | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| <?php | ||||
| class FeedController extends Controller { | ||||
|     private $config; | ||||
|     private $ticks; | ||||
| 
 | ||||
|     public function __construct(){ | ||||
|         $this->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"); | ||||
|     } | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -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; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| <?php | ||||
| // Very simple router class
 | ||||
| class Router { | ||||
|     public function __construct(private PDO $db, private ConfigModel $config, private UserModel $user) {} | ||||
|      | ||||
|     // Define the recognized routes.
 | ||||
|     // Anything else will 404.
 | ||||
|     private static $routeHandlers = [ | ||||
| @ -28,7 +30,7 @@ class Router { | ||||
| 
 | ||||
| 
 | ||||
|     // Main router function
 | ||||
|     public static function route(string $requestPath, string $requestMethod): bool { | ||||
|     public function route(string $requestPath, string $requestMethod): bool { | ||||
|         foreach (self::$routeHandlers as $routeHandler) { | ||||
|             $routePattern = $routeHandler[0]; | ||||
|             $controller = $routeHandler[1]; | ||||
| @ -59,7 +61,7 @@ class Router { | ||||
| 
 | ||||
|                     Log::debug("Handling request with Controller {$controllerName} and function {$functionName}"); | ||||
| 
 | ||||
|                     $instance = new $controllerName(); | ||||
|                     $instance = new $controllerName($this->db, $this->config, $this->user); | ||||
|                     call_user_func_array([$instance, $functionName], $matches); | ||||
|                     return true; | ||||
|                 } | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,27 +1,24 @@ | ||||
| <?php | ||||
| class TickModel { | ||||
|     public function __construct(private PDO $db, private ConfigModel $config) {} | ||||
|      | ||||
|     public function getPage(int $limit, int $offset = 0): array { | ||||
|         global $db; | ||||
| 
 | ||||
|         $stmt = $db->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, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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(); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										367
									
								
								tests/Controller/AdminController/AdminControllerTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										367
									
								
								tests/Controller/AdminController/AdminControllerTest.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,367 @@ | ||||
| <?php | ||||
| require_once dirname(dirname(dirname(__DIR__))) . "/config/bootstrap.php"; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| 
 | ||||
| class AdminControllerTest extends TestCase | ||||
| { | ||||
|     private PDO $mockPdo; | ||||
|     private ConfigModel $config; | ||||
|     private UserModel $user; | ||||
|     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 (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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										175
									
								
								tests/Controller/FeedController/FeedControllerTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								tests/Controller/FeedController/FeedControllerTest.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,175 @@ | ||||
| <?php | ||||
| use PHPUnit\Framework\TestCase; | ||||
| 
 | ||||
| class FeedControllerTest 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 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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										312
									
								
								tests/Controller/HomeController/HomeControllerTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								tests/Controller/HomeController/HomeControllerTest.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,312 @@ | ||||
| <?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); | ||||
|     } | ||||
| } | ||||
| @ -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
 | ||||
|  | ||||
| @ -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'; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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'; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user