Skip to content

Commit 1a1c68e

Browse files
committed
add error pages and expand unit tests
1 parent 2f00ed6 commit 1a1c68e

11 files changed

Lines changed: 767 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.1] - 2024
9+
10+
### Added
11+
- **Custom Error Pages**: User-friendly error pages for 404 (Not Found), 500 (Server Error), and 403 (Forbidden) with consistent styling and navigation
12+
- **Comprehensive Test Coverage**: Expanded unit test suite with 35 new tests covering core functionality
13+
- **CsrfTest**: 10 tests for CSRF token generation, validation, expiration, and field generation
14+
- **AuthTest**: 13 tests for authentication, login/logout, registration, and user preferences
15+
- **FeedParserTest**: 12 tests for feed type detection (RSS/Atom/JSON) and parsing functionality
16+
17+
### Changed
18+
- **Router Error Handling**: Router now displays custom error pages instead of plain text messages
19+
- **Test Infrastructure**: Test suite expanded from 24 to 59 tests (146% increase) with 113 total assertions
20+
21+
### Improved
22+
- **User Experience**: Professional error pages with helpful messages and navigation options
23+
- **Code Quality**: Significantly improved test coverage for authentication, CSRF protection, and feed parsing
24+
- **Error Handling**: Better error presentation for users encountering 404, 500, or 403 errors
25+
26+
### Technical Details
27+
- Error pages follow the same design system as the rest of the application
28+
- Router's `showErrorPage()` method handles error page rendering
29+
- All new tests follow existing test patterns and use proper database setup/teardown
30+
- Test coverage now includes: Config, Logger, FeedService, RateLimiter, Response, Csrf, Auth, and FeedParser
31+
832
## [1.1.0] - 2024
933

1034
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# VibeReader
22

3-
**Version:** 1.1.0
3+
**Version:** 1.1.1
44

55
A PHP-based RSS reading platform similar to Google Reader. This application allows you to securely manage and read RSS, Atom, and JSON feeds with a clean three-pane interface.
66

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "php-vibe-reader/reader",
33
"description": "A PHP-based RSS reading platform similar to Google Reader",
4-
"version": "1.1.0",
4+
"version": "1.1.1",
55
"type": "project",
66
"require": {
77
"php": ">=8.0",

src/Router.php

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace PhpRss;
44

5+
use PhpRss\View;
6+
57
/**
68
* Simple HTTP router for handling application routes.
79
*
@@ -126,8 +128,7 @@ public function dispatch(): void
126128
}
127129

128130
// 404
129-
http_response_code(404);
130-
echo "Page not found";
131+
$this->showErrorPage(404);
131132
}
132133

133134
/**
@@ -209,18 +210,40 @@ private function handleRoute(string $handler, array $params = []): void
209210
$controllerClass = "PhpRss\\Controllers\\$controller";
210211

211212
if (!class_exists($controllerClass)) {
212-
http_response_code(500);
213-
echo "Controller not found";
213+
$this->showErrorPage(500);
214214
return;
215215
}
216216

217217
$controllerInstance = new $controllerClass();
218218
if (!method_exists($controllerInstance, $method)) {
219-
http_response_code(500);
220-
echo "Method not found";
219+
$this->showErrorPage(500);
221220
return;
222221
}
223222

224223
$controllerInstance->$method($params);
225224
}
225+
226+
/**
227+
* Show an error page based on the HTTP status code.
228+
*
229+
* Renders the appropriate error page template (404, 500, 403) and
230+
* sets the corresponding HTTP response code.
231+
*
232+
* @param int $statusCode The HTTP status code (404, 500, or 403)
233+
* @return void
234+
*/
235+
private function showErrorPage(int $statusCode): void
236+
{
237+
http_response_code($statusCode);
238+
239+
$templateMap = [
240+
404 => 'error_404',
241+
500 => 'error_500',
242+
403 => 'error_403',
243+
];
244+
245+
$template = $templateMap[$statusCode] ?? 'error_500';
246+
247+
View::render($template);
248+
}
226249
}

src/Version.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Version
1515
*
1616
* @var string
1717
*/
18-
public const VERSION = '1.1.0';
18+
public const VERSION = '1.1.1';
1919

2020
/**
2121
* Application name
@@ -27,7 +27,7 @@ class Version
2727
/**
2828
* Get the full application version string.
2929
*
30-
* @return string Version string (e.g., "VibeReader/1.1.0")
30+
* @return string Version string (e.g., "VibeReader/1.1.1")
3131
*/
3232
public static function getVersionString(): string
3333
{
@@ -37,7 +37,7 @@ public static function getVersionString(): string
3737
/**
3838
* Get the version number only.
3939
*
40-
* @return string Version number (e.g., "1.1.0")
40+
* @return string Version number (e.g., "1.1.1")
4141
*/
4242
public static function getVersion(): string
4343
{

tests/Unit/AuthTest.php

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
namespace PhpRss\Tests\Unit;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use PhpRss\Auth;
7+
use PhpRss\Database;
8+
use PDO;
9+
10+
class AuthTest extends TestCase
11+
{
12+
protected function setUp(): void
13+
{
14+
// Initialize database for testing
15+
Database::init();
16+
Database::setup();
17+
18+
// Start session for Auth tests
19+
if (session_status() === PHP_SESSION_NONE) {
20+
session_start();
21+
}
22+
23+
// Clear session
24+
$_SESSION = [];
25+
}
26+
27+
protected function tearDown(): void
28+
{
29+
// Clean up: remove test users
30+
$db = Database::getConnection();
31+
$db->exec("DELETE FROM users WHERE username LIKE 'test_%'");
32+
33+
// Clear session
34+
$_SESSION = [];
35+
}
36+
37+
public function testCheckReturnsFalseWhenNotLoggedIn(): void
38+
{
39+
$this->assertFalse(Auth::check());
40+
}
41+
42+
public function testCheckReturnsTrueWhenLoggedIn(): void
43+
{
44+
// Create a test user and login
45+
$this->createTestUser('test_user', 'test@example.com', 'password123');
46+
$this->assertTrue(Auth::login('test_user', 'password123'));
47+
$this->assertTrue(Auth::check());
48+
}
49+
50+
public function testUserReturnsNullWhenNotAuthenticated(): void
51+
{
52+
$user = Auth::user();
53+
$this->assertNull($user);
54+
}
55+
56+
public function testUserReturnsUserDataWhenAuthenticated(): void
57+
{
58+
$username = 'test_user';
59+
$email = 'test@example.com';
60+
$this->createTestUser($username, $email, 'password123');
61+
62+
Auth::login($username, 'password123');
63+
$user = Auth::user();
64+
65+
$this->assertNotNull($user);
66+
$this->assertEquals($username, $user['username']);
67+
$this->assertEquals($email, $user['email']);
68+
$this->assertArrayHasKey('id', $user);
69+
}
70+
71+
public function testLoginWithValidCredentials(): void
72+
{
73+
$username = 'test_user';
74+
$this->createTestUser($username, 'test@example.com', 'password123');
75+
76+
$result = Auth::login($username, 'password123');
77+
$this->assertTrue($result);
78+
$this->assertTrue(Auth::check());
79+
$this->assertEquals($username, $_SESSION['username'] ?? null);
80+
}
81+
82+
public function testLoginWithInvalidPassword(): void
83+
{
84+
$username = 'test_user';
85+
$this->createTestUser($username, 'test@example.com', 'password123');
86+
87+
$result = Auth::login($username, 'wrong_password');
88+
$this->assertFalse($result);
89+
$this->assertFalse(Auth::check());
90+
}
91+
92+
public function testLoginWithInvalidUsername(): void
93+
{
94+
$result = Auth::login('nonexistent_user', 'password123');
95+
$this->assertFalse($result);
96+
$this->assertFalse(Auth::check());
97+
}
98+
99+
public function testLoginWithEmail(): void
100+
{
101+
$email = 'test@example.com';
102+
$this->createTestUser('test_user', $email, 'password123');
103+
104+
$result = Auth::login($email, 'password123');
105+
$this->assertTrue($result);
106+
$this->assertTrue(Auth::check());
107+
}
108+
109+
public function testRegisterWithNewUser(): void
110+
{
111+
$result = Auth::register('test_new_user', 'newuser@example.com', 'password123');
112+
$this->assertTrue($result);
113+
114+
// Verify user was created
115+
$db = Database::getConnection();
116+
$stmt = $db->prepare("SELECT * FROM users WHERE username = ?");
117+
$stmt->execute(['test_new_user']);
118+
$user = $stmt->fetch();
119+
$this->assertNotFalse($user);
120+
$this->assertEquals('newuser@example.com', $user['email']);
121+
}
122+
123+
public function testRegisterWithDuplicateUsername(): void
124+
{
125+
$username = 'test_duplicate';
126+
$this->createTestUser($username, 'first@example.com', 'password123');
127+
128+
// Try to register with same username
129+
$result = Auth::register($username, 'second@example.com', 'password456');
130+
$this->assertFalse($result);
131+
}
132+
133+
public function testRegisterWithDuplicateEmail(): void
134+
{
135+
$email = 'duplicate@example.com';
136+
$this->createTestUser('test_first', $email, 'password123');
137+
138+
// Try to register with same email
139+
$result = Auth::register('test_second', $email, 'password456');
140+
$this->assertFalse($result);
141+
}
142+
143+
public function testLogout(): void
144+
{
145+
// Login first
146+
$this->createTestUser('test_user', 'test@example.com', 'password123');
147+
Auth::login('test_user', 'password123');
148+
$this->assertTrue(Auth::check());
149+
150+
// Logout
151+
Auth::logout();
152+
$this->assertFalse(Auth::check());
153+
}
154+
155+
public function testUserLoadsPreferencesIntoSession(): void
156+
{
157+
$username = 'test_user';
158+
$this->createTestUser($username, 'test@example.com', 'password123');
159+
160+
Auth::login($username, 'password123');
161+
Auth::user(); // Load preferences
162+
163+
// Check that preferences are in session
164+
$this->assertArrayHasKey('hide_read_items', $_SESSION);
165+
$this->assertArrayHasKey('dark_mode', $_SESSION);
166+
$this->assertArrayHasKey('timezone', $_SESSION);
167+
}
168+
169+
/**
170+
* Helper method to create a test user in the database.
171+
*/
172+
private function createTestUser(string $username, string $email, string $password): void
173+
{
174+
$db = Database::getConnection();
175+
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
176+
$stmt = $db->prepare("INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)");
177+
$stmt->execute([$username, $email, $passwordHash]);
178+
}
179+
}

0 commit comments

Comments
 (0)