Проверка работы входа в систему: тестирование PHP-логина
Тестирование логина (аутентификации) один из ключевых элементов обеспечения безопасности PHP приложений. Необходимо проверить корректную обработку учетных данных, управление сессиями и устойчивость к атакам. В этой статье рассматриваются различные способы тестирования, от модульных тестов до функциональных сценариев.
Тестирование логина: комплексный подход
Основное решение: модульное тестирование с PHPUnit и моками
Наиболее эффективный способ заключается в изолировании логики аутентификации от внешних зависимостей (базы данных, сессии) с помощью моков. Это даёт быстрые и надёжные тесты.
// Класс Login
class Login {
private PDO $pdo;
private array &$session;
public function __construct(PDO $pdo, array &$session) {
$this->pdo = $pdo;
$this->session = &$session;
}
public function authenticate(string $email, string $password): bool {
$stmt = $this->pdo->prepare('SELECT id, password_hash FROM users WHERE email = :email LIMIT 1');
$stmt->execute([':email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) return false;
if (!password_verify($password, $user['password_hash'])) return false;
$this->session['user_id'] = (int)$user['id'];
return true;
}
}// Тест с моком PDO
use PHPUnit\Framework\TestCase;
class LoginTest extends TestCase {
public function testSuccessfulLogin() {
$pdoMock = $this->createMock(PDO::class);
$stmtMock = $this->createMock(PDOStatement::class);
$pdoMock->method('prepare')->with($this->stringContains('SELECT'))->willReturn($stmtMock);
$stmtMock->method('execute')->willReturn(true);
$stmtMock->method('fetch')->willReturn(['id' => 1, 'password_hash' => password_hash('correct', PASSWORD_DEFAULT)]);
$session = [];
$login = new Login($pdoMock, $session);
$this->assertTrue($login->authenticate('test@example.com', 'correct'));
$this->assertEquals(1, $session['user_id']);
}
}Результат: тест проходит, если мок возвращает корректные данные.
Типичные ошибки:
- Несоответствие сигнатуры мока (например, fetch возвращает false).
- Забывают проверять, что в сессию записан правильный идентификатор.
Как протестировать логин с реальной базой данных, но без постоянного хранилища?
Использование SQLite в памяти (in-memory) позволяет выполнять тесты с настоящим SQL, но не затрагивая рабочую БД.
public function testWithSqliteInMemory() {
$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, password_hash TEXT)');
$pdo->exec("INSERT INTO users (email, password_hash) VALUES ('a@b.com', '".password_hash('pass', PASSWORD_DEFAULT)."')");
$session = [];
$login = new Login($pdo, $session);
$this->assertTrue($login->authenticate('a@b.com', 'pass'));
$this->assertFalse($login->authenticate('a@b.com', 'wrong'));
}Проблемы: необходимо каждый раз создавать схему; тесты выполняются медленнее; возможны конфликты при параллельном запуске.
Как проверить, что сессия корректно устанавливается и сбрасывается при выходе?
Можно передавать массив-ссылку и проверять его содержимое.
// Тест на выход
public function testLogoutClearsSession() {
$session = ['user_id' => 5];
$login = new Login($pdoMock, $session);
$login->logout(); // предположим, что есть метод
$this->assertArrayNotHasKey('user_id', $session);
}Если сессия управляется через $_SESSION, её сложно тестировать из-за глобального состояния. Рекомендуется оборачивать работу с сессией в отдельный класс.
Как автоматизировать тестирование через веб-интерфейс (функциональное тестирование)?
Инструмент Codeception позволяет писать приёмосдаточные тесты, имитирующие действия пользователя.
// acceptance/LoginCest.php
class LoginCest {
public function testSuccessfulLogin(AcceptanceTester $I) {
$I->amOnPage('/login');
$I->fillField('email', 'user@example.com');
$I->fillField('password', 'secret');
$I->click('Login');
$I->see('Welcome, user');
}
}Требуется настроенный Selenium или PhpBrowser; медленные; зависят от реального приложения.
Как проверить устойчивость к SQL-инъекциям и XSS в форме логина?
Написать тесты, передающие вредоносные строки, и убедиться, что приложение не выводит их и не допускает неавторизованный вход.
public function testSqlInjectionAttempt() {
$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, password_hash TEXT)');
$session = [];
$login = new Login($pdo, $session);
$this->assertFalse($login->authenticate("' OR 1=1 --", 'any'));
}Не все виды атак можно проверить в unit-тесте; необходимы интеграционные тесты с реальным веб-сервером.
Расширенные примеры тестов логина
В данном разделе представлен полный набор тестов для класса Login, включая проверку блокировки после 5 неудачных попыток и защиту от CSRF.
// Тест с несколькими сценариями
class LoginAdvancedTest extends TestCase {
private PDO $pdo;
private array $session;
protected function setUp(): void {
$this->pdo = new PDO('sqlite::memory:');
$this->pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, password_hash TEXT, attempts INTEGER DEFAULT 0, locked_until DATETIME NULL)');
// Добавляем тестового пользователя
$hash = password_hash('secret', PASSWORD_DEFAULT);
$this->pdo->exec("INSERT INTO users (id, email, password_hash) VALUES (1, 'test@test.com', '$hash')");
$this->session = [];
}
public function testValidLogin() {
$login = new Login($this->pdo, $this->session);
$this->assertTrue($login->authenticate('test@test.com', 'secret'));
$this->assertEquals(1, $this->session['user_id']);
}
public function testInvalidPassword() {
$login = new Login($this->pdo, $this->session);
$this->assertFalse($login->authenticate('test@test.com', 'wrong'));
$this->assertArrayNotHasKey('user_id', $this->session);
}
public function testTooManyAttemptsBlockUser() {
$login = new Login($this->pdo, $this->session);
// 5 неудачных попыток
for ($i=0; $i<5; $i++) {
$login->authenticate('test@test.com', 'wrong');
}
// Шестая попытка должна быть отклонена
$this->assertFalse($login->authenticate('test@test.com', 'secret'));
// Проверяем, что сессия не установлена
$this->assertArrayNotHasKey('user_id', $this->session);
}
public function testCsrfTokenValidation() {
// Предположим, у Login есть метод с CSRF токеном
$login = new Login($this->pdo, $this->session);
// Передаём неверный токен
$result = $login->authenticateWithCsrf('test@test.com', 'secret', 'invalid_token');
$this->assertFalse($result);
// Проверяем, что сообщение об ошибке содержит 'CSRF'
$this->assertStringContainsString('CSRF', $login->getLastError());
}
}Результат выполнения (phpunit --verbose):
PHPUnit 9.5.29 OK (4 tests, 8 assertions)
Каждый тест проверяет отдельный аспект: успешный вход, неправильный пароль, блокировку аккаунта и защиту от CSRF.