Проверка работы входа в систему: тестирование PHP-логина

Раздел: Разработка на PHP -> Тестирование 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.

Тестирование логина PHP - comments

En
Test login php (php)