---
title: Интеграционное тестирование веб-приложения на инъекции
date: 2014-08-13T10:00
tags: [tests]
---

Если у вас есть веб-приложение и вы задались тем что-бы идеально его покрыть тестами, то вот что у вас должно быть:

- unit-тесты бэкэнда — в основном покрываются модели, генерируется покрытие — получаете необходимость изолировать модели (заодно single responsibility principle выполняется)
- [unit-тесты frontend](http://kurapov.name/rus/lab/quality_control/karma_jasmine_testing_angularjs/) — карма + phantomjs прокрутят все ваши angular-сервисы и backbone-модели — тоже приходится изолировать код
- [e2e (сценарные, системные) тесты](http://kurapov.name/rus/lab/quality_control/protractor/) — наверняка основанный на [selenium](http://kurapov.name/rus/lab/quality_control/selenium_server_phpstorm/) (protractor, selenide). Медленно тестируется функционал работающей системы из UI — приходится задумываться о том что пользователь вообще делает (use cases)
- db/entity тесты миграций — "с нуля" запускают изменения в БД и когда всё готово - сравнивают с [entity/record классами](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html) для синхронизации кода с БД (так находятся лишние свойства и недостающие )

<!-- truncate -->

- тестирования db-процедур я не рассматриваю, потому что PL/SQL не увлекаюсь

- интеграционные тесты внешних систем/api - любого типа (rest, ftp, soap) и источника (соц.сети, бухгалтерия, склад, SMS-gateway), тестируют на

- доступность (а-ля pingdom)
- предсказуемый формат (банальный get и проверка json)
- полное взаимодействие с записью (обычно партнёрская компания с разработчиком ставит тестовую машинку)

- нагрузочные тесты (load, stress) тестируют всю систему что-бы определить максимальное число подключённых клиентов
- тесты производительности (performance) тестируют эффективность использования памяти, CPU, сети, HDD IO в среднем при разных запросах что-бы выявить какие конкретно области приложения медленные и в что можно улучшить
- интеграционные тесты контроллеров/api — запускающиеся без браузера, через CURL запросы, эмулирующие вызов из javascript или мобильных приложений

- простые get - запросы, проверяющие на наличие ошибок/stacktrace
- post/put запросы, меняющие данные
- в запущенных случаях (мобильные приложения), когда с мобильника e2e тесты не запустить, а функционал надо тестировать, то получаются последовательные сценарные (а не одинарные get-post) запросы, сохраняющие состояние сущностей и пользователя (в БД и сессии)

Вот на предпоследних я немножко и остановлюсь

### Unit-тестирование контроллеров неудобно

Тестировать контроллеры с помощью юнит-тестов, хоть и быстро исполняется в phpunit, пишется очень с большим трудом. Да, я слышал Боба Мартина что код надо полностью покрывать, но контроллер это место сосредоточения нескольких сущностей:

- получение конфигурации (из php-include, yaml, БД, констант и проч) — значит надо либо исполнять всю загрузку системы, либо мокать, либо заменять константы/значения
- создание instance новых моделей – значит надо мокать
- вызовы глобальных IO-переменных/методов - значит надо их рефакторить, убирать в нетестируемые модели и мокать в контроллерах
- глобальные переменные, Factory-вызовы, статика - как и с конфигурацией, всё сложно
- аспекты - аннотации, доступ, логгинг + логика системы основанная на reflection — совсем какой-то магический мокинг должен быть. Например определение шаблона в аннотациях к методу контроллера..

Но хуже всего конечно не в самом мокании, а в количестве. Когда у вас метод контроллера использует 5 моделей, то вам надо столько же моков определить. А потом на каждую строчку поведения модели написать порой и не одну строчку эмулирования её поведения — какой и сколько раз метод вызвался, с какими аргументами, что вернулось. А порой ведь мок может вернуть обьект (как в PDO - PDOStatement например) и вызвать у него метод. Получается что моки надо связывать между собой. Я часто ещё и путаюсь в каком порядке их надо регистрировать, ведь вызов тестируемой функции в коде теста должно быть внизу, а регистрация моков в phpunit - до неё, фактически получается что код теста пишется **задом наперёд**. Короче — писать юнит-тесты для контроллеров опасно. (Впрочем некоторые советуют глянуть на [phpspec](http://www.phpspec.net/))

### Как стоит тестировать контроллеры

Интеграционные же тесты в чём-то схожи с e2e тестами, но они не включают в себя UI.

1. Пишем класс с CURL- запросами (get,post.. при необходимости delete и put, если ваш api их использует)  
    
2. Решаем вопрос с авторизацией, если она есть (я использую сохранение сессии в cookie-файл) - CURL это поддерживает
3. Пишем зависимость всех тестов от login-теста, что-бы не насиловать сервер если авторизация провалилась
4. Простые get-запросы с существующими в БД id-шками должны вам будут сказать если где-то закралась ошибка, которую пропустили unit-тесты

Теперь POST/PUT - они чаще содержат ошибки на безопасность, потому что параметров и логики при изменении больше. Добавление и изменения сущностей должно возвращать какой-то результат. Скажем `{result:1, id:3}` в JSON скажет что обьект создался, id такой-то. Кроме обычных тестов на сохранение, надо попробовать во все возможные поля запихнуть sql-инъекцию и XSS. 

SQL-инъекции должны либо выдать сразу ошибку, либо при чтении entity, переданное значение (скажем 1' OR 1=1) будет отличаться от сохранённого в БД (в данном случае, возможно "1"). Иногда, из-за приведения типов, строка станет int-значением и это тоже надо считать как ok.

С XSS чуть сложней - должен быть браузер. Я решаю это так, что e2e тесты запускаются после интеграционных и reset БД не происходит, а значит навигация по системе, где недавно внедрены alert-ы, должно сломать e2e тесты. Список [атакующих XSS токенов](https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet) есть на OWASP.

Для автоматизации я пишу trait для PHPUnit-тестов, потому что так проще всего использовать один и тот же код в разных тестах. 

```php
trait SQLinjection {
    private $AttackTokens = [ 
        '1" OR 1=1'
    ];

    public function checkInfectedUpdate($saveURL,$readURL,$fields,callable $comparisonFn){
        foreach($this->attackTokens as $injection){
            $data = $fields;
            foreach($fields as $k=>$v){
                $data[$k]=($v=='*' ? $injection : $v);            
            }
            
            $saveResult = $this->post($saveURL, $data);
            $getResult = $this->get($getURL);
        
            $comparisonFn($injection, $getResult, $saveResult);
        }
    }
    
    public function checkInfectedInsert(..){..}

}
```

   

Каждый интеграционный тест наследует IntegrationBaseTest - в котором определены CURL-обёртки и путь к серверу. Метод теста сам должен решать как сравнивать результат с инъекцией, потому что иногда результат от вызова из API get() отличается в зависимости от entity - где-то это простой массив, а где-то иерархия со списками, по которым ещё надо пройтись (и скажем вычленить последнюю _версию_)  

```php
InvoiceControllerTest extends IntegrationBaseTest {
    private $phpErrorDetection = 'error';
    
    use SQLinjection;
    
    /**
     * @test
     */
    function login() {
        $result = parent::login();
    }

    /**
     * @test
     * @depends login
     * @group security
     */
    function postSave_AddingInjection() {
        $self = $this;

        $this->checkInfectedUpdate(
            $this->baseURL . 'invoice/save',
            $this->baseURL . 'invoice/get?id=3',
            [
                'company_id'  => '1',
                'title'        => '*',
                'description'  => '*'
            ],
            function ($injection, $getResponse, $insertResponse) use ($self) {
                $this->assertEquals($injection, $getResponse['result']['title']);
                $this->assertEquals($injection, json_decode($getResponse['result']['description']));
            }
        );
    }
}
```

   

По мере того, как вы пишете тесты для контроллеров, получается что заодно вам приходится

1. тестировать привилегии (кто может получить ответ?)
2. рефакторить код контроллера, вынося логику в модели (потому что сложно понять толстые контроллеры)
3. избавляться от stacktrace - потому что это выдаёт лишнюю информацию
4. решать случаи с integrity violation, когда вы пытаетесь скажем добавить entity без проверки его связи с другими entity в БД

Описанный мною вариант не решает вопросов с CSRF, редиректами, несолёными паролями, SSL, сессиями и ошибками конфигурации сервера, но зато улучшает функции приложения по безопасности/логичности, даже если у вас всюду используется безопасный PDO с bindParam().

См. также [ZAP](https://code.google.com/p/zaproxy/)

## Related

- [Kohana](/ru/blog/тестирование/unit-testirovanie-v-kohana-32/)
- [Protractor](/ru/blog/тестирование/protractor/)
- [Изолированное UI тестирование с Protractor](/ru/blog/тестирование/izolirovannoe-ui-testirovanie-s-protractor/)
