Наверное каждый разработчик и QA-инженер, в рабочем процессе сталкивался с необходимостью подмены отправляемых/принимаемых данных. Когда эта задача касается данных, гуляющих между клиентом и сервером, особых проблем она не приносит. Запрос можно модифицировать и отправить ручками, к примеру через Postman, а для модификации ответа можно использовать инструменты вроде Burp Suite, Charles и т.д., но что делать если целевой запрос отправляется с сервера?
Рассмотрим простую схему процесса оплаты:
Пользователь заполняет платежные данные
Фронт отправляет их на endpoint API
Бэкэнд общается с другими системами по HTTP, выполняет различные действия и отправляет ответ на фронт
Фронт отображает сообщение для клиента
Предположим, перед QA стоит задача протестировать фикс плавающего, крайне редко встречающегося бага, возникающего из-за получения бэкэндом некорректных данных от сторонней системы.
На моей практике такие ситуации возникали не раз и всегда вовлекали в процесс тестирования бэкэнд специалиста, который далее выкручивался как мог. Чаще всего решением проблемы служил какой-то временный костыль в коде, который добавлялся для проведения теста и удалялся сразу после этого.
Встречались ситуации и сложнее, когда данные нужно было подменить не в одном, а сразу в серии запросов, каждый последующий из которой формировался на основе ответов предыдущих.
Рассмотрим простую схему процесса бронирования:
Пользователь заполняет данные о пассажирах
Фронт отправляет их на endpoint API
Бэкэнд запрашивает у поставщика актуальную информацию о рейсе
Далее формирует запрос на бронирование с учетом полученной ранее актуальной информации о рейсе и отправляет ответ на фронт
Фронт отображает сообщение для клиента
В таком случае уже не получится обойтись простым моком ответа конкретного запроса, т.к. часть запросов из серии могут быть зависимы друг от друга. К тому же, для полноценного тестирования, скорей всего нужно будет сделать несколько итераций, с разными значениями. Возможно будет недостаточно одного деплоя и код придётся выкладывать в тестовое окружение несколько раз. Как итог - количество костылей и их сложность растет, а время разработчика расходуется не оптимально.
UPD: После пары коротких дискуссий в коментариях, мне показалось что не все читатели "правильно" понимают описываемую мной проблему. Поэтому решил добавить максимально конкретный и однозначный пример ситуации, в которой был бы полезен инструмент, о котором дальше и пойдет речь.
Реальный пример из жизни
Допустим есть программная система, реализующая полный цикл процессов продажи каких-либо билетов (поиск, бронирование, оплата, обмен, возврат и т.д.). Естественно, эта система берет билеты не из собственной БД. Она интегрирована с множеством различных поставщиков, которые их предлагают.
За продажу билета система берет какую-то динамически определяемую наценку, которая включется в стоимость билета еще на этапе его поиска.
Цена у поставщика может измениться. Это случай достаточно редкий, но возможный. Поэтому в момент бронирования, мы должны актуализировать данные о билете, чтоб не продать его себе в минус. Если цена в момент актуализации увеличилась, система должна уведомить об этом клиента, а процесс бронирования прерваться, т.к. новая цена его может не устроить. Если же цена уменьшилась, то система может заработать доп. прибыль, ведь клиенту можно и не сообщать об изменении в меньшую сторону, а разницу забрать себе.
Или же другая ситация, в момент актуализации информации о билете, цена остается прежней, но меняются каки-то важные для клиента условия (влияющие на цену). Естественно система должна клиента об этом уведомить. Иначе он вернется с претензией, что получил совсем не то, за что платил деньги.
Допустим программист написал логику, которая реализует все описанные выше требования. Задача уходит на тестирование в QA-отдел. Но т.к. подобная ситуация (изменения цены или условий при актуализации) происходит крайне редко, задача зависает в тестировании надолго. Рано или поздно к разработчику приходит QA-специалист и говорит: "Хоть убей, не могу воспроизвести случай, когда изменятся условия или цена.. Помоги, а?".
И тут разработчик становится перед выбором:
- брать на себя ответственность и давать добро лить правки в прод без апрува от QA отдела, т.к. они этот кейс проверить не могут
- начинает придумывать какие-то костыли на тестовом окружении, для того чтоб имитировать возникновение этой ситуации
Инструмент, о котором дальше пойдет речь, нужен как-раз для того, чтоб разработчик не вставал перед таким выбором. Чтоб он вообще не был вовлечен в процесс тестирования, спокойно продолжал заниматься своими задачами и не отвлекался от работы, а QA могли бы сами, без его помощи, имитировать любые трудно-воспроизводимые ситуации.
Если вы пишете на PHP и вам знакома эта боль, добро пожаловать под кат.
Дальнейшая часть статьи является мануалом к библиотеке https://github.com/Chetkov/http-client-mitmproxy, которую предлагаю в качестве решения описанной выше проблемы.
Библиотека предоставляет набор инструментов для клиентской и серверной стороны:
Консольный клиент для пользователя
Декоратор над \Psr\Http\Client\ClientInterface, который через канал коммуникации общается с консольным клиентом и дает возможность модифицировать запросы и ответы в интерактивном режиме
Для наглядности я подготовил demo-проект https://github.com/Chetkov/http-client-mitmproxy-example. Сильно изощряться не стал, т.к. считаю что это было бы избыточно, но для демонстрации работы http-client-mitmproxy его вполне достаточно.
В нем доступны:
artisan команда
currency-rates:show {code}
(возвращает список курсов валют)endpoint
/currency-rates
(возвращает список курсов валют)endpoint
/currency-rates/{code}
(возвращает курс конкретной валюты)endpoint
/calculate
(делает несколько запросов кcurrency-rates/{code}
, получает курсы и рассчитывает суммы в разных валютах, на основе суммы в рублях)
Установка http-client-mitmproxy
composer require v.chetkov/http-client-mitmproxy
Настройка http-client-mitmproxy
Общее
В данный момент, канал коммуникации \Chetkov\HttpClientMitmproxy\Communication\CommunicationChannelInterface между декоратором и пользовательским клиентом, реализован на базе Redis, поэтому на клиентской и серверной стороне, в конфиге необходимо указать идентичные данные для подключения к нему:
<?php
declare(strict_types=1);
return [
// For redis based communication channels
'redis' => [
'host' => 'redis-host',
'port' => 6379,
'timeout' => 0,
],
];
Настройка бэкэнда
Также, на стороне бэкэнда, для нужного окружения, в случае обнаружения ProxyUID, в качестве реализации \Psr\Http\Client\ClientInterface необходимо использовать декоратор, поставляемый библиотекой:
<?php
// ...
$this->app->bind(ClientInterface::class, function (Container $container) {
$client = new Client(['allow_redirects' => true]);
if ($proxyUid = ProxyUID::detect()) {
$config = require dirname(__DIR__, 2) . '/config/mitmproxy.config.php';
$mitmproxyFactory = new DefaultFactory($config);
$client = $mitmproxyFactory->createHttpClientDecorator($proxyUid, $client);
}
return $client;
});
Использование
Запускаем PHP web-сервер
cd http-client-mitmproxy-example
php -S localhost:8000 -t public/
Запускаем консольный клиент
С помощью опций можно задать:
--config
путь к файлу конфигурации--temp-dir
путь к временной директории (должна быть доступна для записи)--app-mode
режим работы целевого приложения (cli, web)--format
предпочитаемый формат для редактирования данных (yaml, json, php)--editor
предпочитаемый редактор (nano, vim, gedit)
В случае отсутствия в списке опций последних трех (app-mode, format, editor), клиент запросит их в интерактивном режиме
Затем будет выведено сообщение с дальнейшими инструкциями.
Для WEB mode:
Для CLI mode:
Запускаем целевое приложение
Следуя предложенным инструкциям выполним artisan команду
export MITM_PROXY_UID=0ff6b1f2a9ebc702ea9b84b0fe019f6b &&
php artisan currency-rates:show USD
И подменим дату в отправляемом к API центробанка запросе на 2010 год
Затем согласимся с редактированием полученного от центробанка ответа
И изменим название доллара США на "Зелёный"
Затем откажемся от продолжения редактирования других полей и всего ответа целиком и увидим сообщение о завершении текущей сессии
Теперь клиент ожидает новых соединений (т.е. процессов, запущенных с его ProxyUID), а запущенная artisan команда думает, что USD на сайте центробанка называется “Зелёный” и его актуальный курс “30”
Думаю, на этом статью можно закончить. Спасибо за внимание.
Если после прочтения у вас возникли вопросы или предложения, я с радостью готов обсудить их в комментариях.
UPD 2: Огромная просьба к читателям, оставляющим свой голос в опросе - пожалуйста, аргументируйте ваш выбор хотя-бы коротким коментарием. Особенно, если вы считаете инструмент бесполезным, расскажите почему. Еще не сталкивались с подобной проблемой? Сталкивались, но решали её подругому?