Наверное каждый разработчик и QA-инженер, в рабочем процессе сталкивался с необходимостью подмены отправляемых/принимаемых данных. Когда эта задача касается данных, гуляющих между клиентом и сервером, особых проблем она не приносит. Запрос можно модифицировать и отправить ручками, к примеру через Postman, а для модификации ответа можно использовать инструменты вроде Burp Suite, Charles и т.д., но что делать если целевой запрос отправляется с сервера?
Рассмотрим простую схему процесса оплаты:

Пользователь заполняет платежные данные
Фронт отправляет их на endpoint API
Бэкэнд общается с другими системами по HTTP, выполняет различные действия и отправляет ответ на фронт
Фронт отображает сообщение для клиента
Предположим, перед QA стоит задача протестировать фикс плавающего, крайне редко встречающегося бага, возникающего из-за получения бэкэндом некорректных данных от сторонней системы.
На моей практике такие ситуации возникали не раз и всегда вовлекали в процесс тестирования бэкэнд специалиста, который далее выкручивался как мог. Чаще всего решением проблемы служил какой-то временный костыль в коде, который добавлялся для проведения теста и удалялся сразу после этого.
Встречались ситуации и сложнее, когда данные нужно было подменить не в одном, а сразу в серии запросов, каждый последующий из которой формировался на основе ответов предыдущих.
Рассмотрим простую схему процесса бронирования:

Пользователь заполняет данные о пассажирах
Фронт отправляет их на endpoint API
Бэкэнд запрашивает у поставщика актуальную информацию о рейсе
Далее формирует запрос на бронирование с учетом полученной ранее актуальной информации о рейсе и отправляет ответ на фронт
Фронт отображает сообщение для клиента
В таком случае уже не получится обойтись простым моком ответа конкретного запроса, т.к. часть запросов из серии могут быть зависимы друг от друга. К тому же, для полноценного тестирования, скорей всего нужно будет сделать несколько итераций, с разными значениями. Возможно будет недостаточно одного деплоя и код придётся выкладывать в тестовое окружение несколько раз. Как итог - количество костылей и их сложность растет, а время разработчика расходуется не оптимально.
Если вы пишете на 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”

Думаю, на этом статью можно закончить. Спасибо за внимание.
Если после прочтения у вас возникли вопросы или предложения, я с радостью готов обсудить их в комментариях.
