make-homepage-testable (#42)
Some checks are pending
Run unit tests / run-unit-tests (push) Waiting to run

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:
Greg Sarjeant 2025-08-02 01:43:48 +00:00 committed by greg
parent 879bd9ff9f
commit 9593a43cc0
17 changed files with 1149 additions and 204 deletions

View File

@ -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;

View File

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

View File

@ -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";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
];
}
}

View File

@ -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();

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

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

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

View File

@ -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

View File

@ -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';

View File

@ -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;

View File

@ -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';