Тестировать API, где сначала надо залогиниться, потом получить CSRF-токен из cookie, потом дергать защищённые эндпоинты - привычная задача, и она регулярно проваливается в длинные цепочки Postman-закладок, потому что curl кажется инструментом одного запроса. Это не так: с файлами cookie-jar curl превращается в полноценного клиента сессии, а пара флажков с jq доводит его до рабочего инструмента автоматизации. В статье разбираем реальные сценарии: логин и последующие запросы, работа с CSRF, сохранение и перезапись cookie, диагностика и встраивание в shell-скрипты и CI.

Cookie-jar, как curl хранит сессию

Базовая механика сводится к двум флагам брать и класть. Флаг -c файл (cookie-jar на запись) говорит curl сохранить все Set-Cookie из ответа в файл. Флаг -b файл (cookie на чтение) отправляет куки из указанного файла. Пишется файл в формате Netscape cookie file - обычный текст, который можно читать глазами и править руками. Комбинация -b cookies.txt -c cookies.txt в одной команде означает: загрузить имеющиеся, отправить уместные, обновить файл по новым Set-Cookie из ответа. Такой режим работы ближе всего к тому, как ведёт себя браузер.

Простейший сценарий логина: curl -c cookies.txt -X POST https://app.example.com/login -d "user=admin&password=secret" -d "_csrf=токен". Если сервер отвечает редиректом на личный кабинет, флаг -L проследует за ним. После команды cookies.txt уже содержит session-cookie, и все последующие запросы идут с -b cookies.txt -c cookies.txt. Проверка, что сессия реально установлена: curl -b cookies.txt https://app.example.com/api/me вернёт данные пользователя, а не 401.

Тонкость первая: curl отправляет из jar только те куки, которые подходят по domain, path и secure флагам, как и браузер. Если сервер поставил cookie с domain=.example.com, а вы обращаетесь на api.example.com - всё сработает; cookie с domain=example.com на поддомен уже не попадёт в запрос, и это правильное поведение, которое иногда принимают за баг curl.

Каскадные сценарии, логин, CSRF, защищённые запросы

Реальный API редко ограничивается логином. Приведу рабочий шаблон теста, который проходит полный путь пользователя - авторизация с формой, получение CSRF, вызов защищённого эндпоинта, проверка результата:

#!/bin/bash
set -euo pipefail
BASE=https://app.example.com
JAR=/tmp/cookies-$$.txt

1. Получаем страницу логина, чтобы собрать стартовые cookie и CSRF CSRF=$(curl -s -c "$JAR" "$BASE/login" | grep -oP 'name="_csrf" value="\K[^"]+')

2. Логинимся curl -s -b "$JAR" -c "$JAR" -o /dev/null -w "login: %{http_code}\n" \ -X POST "$BASE/login" \ --data-urlencode "user=$LOGIN" \ --data-urlencode "password=$PASSWORD" \ --data-urlencode "_csrf=$CSRF"

3. Дёргаем защищённый эндпоинт с актуальными cookie и свежим CSRF из cookie CSRF2=$(grep '_csrf' "$JAR" | awk '{print $NF}') curl -s -b "$JAR" -c "$JAR" \ -H "X-CSRF-Token: $CSRF2" \ -H "Content-Type: application/json" \ -d '{"title":"test"}' \ "$BASE/api/items" | jq .id

Ключевых приёмов здесь четыре. Во-первых, --data-urlencode вместо -d, чтобы спецсимволы пароля не ломали форму. Во-вторых, извлечение CSRF из HTML через grep -oP с заглядыванием вперёд - компактно и достаточно надёжно для тестов. В-третьих, актуальный CSRF берётся из cookie jar, потому что многие фреймворки кладут его и в форму, и в cookie, а валидация сравнивает пару. В-четвёртых, jq извлекает id созданного объекта для следующего шага теста.

Диагностика, что именно ушло в запросе и откуда 401

Когда сессия вдруг не работает, есть прямые инструменты разбирательства. Флаг -v покажет все заголовки запроса и ответа, включая строки Cookie и Set-Cookie: по ним сразу видно, ушёл ли session id и принял ли сервер новый. Флаг -w позволяет собрать компактный отчёт без лишнего шума: curl -o /dev/null -s -w "code=%{http_code} time=%{time_total} redirect=%{redirect_url}\n". Для редиректов полезен %{num_redirects} - выросшее с нуля число перенаправлений часто намекает, что логин не удался.

Вторая диагностическая группа это сам файл cookie-jar: его можно просто cat. Формат простой: домен, флаги, путь, secure, срок годности, имя и значение. Если кука исчезла - смотрите срок годности и флаг HttpOnly (curl его честно хранит: в файле строка помечается #HttpOnly_). Классическая ловушка здесь: сервер выставил cookie только после редиректа, а вы забыли -L, поэтому jar пустой. Вторая классика: сервер требует заголовок Origin или Referer для POST-запросов, и без него отвечает 403, хотя логин работал и cookie хорошие.

Отдельно стоит сказать о времени: сессии протухают. Если тест логина отлично работает, а серия из 50 запросов на 47-й возвращает 401 - вероятно короткий TTL сессии или rotation cookie на каждый запрос. Ротацию лечит именно комбинация -b и -c в одном вызове, чтобы файл обновлялся после каждого ответа.

Встраивание в скрипты и CI

В автоматизации красиво работают две идеи. Первая - одноразовый jar: JAR=$(mktemp) в начале скрипта и trap 'rm -f "$JAR"' EXIT гарантируют удаление сессионных данных с диска после прогона. Это важно, потому что cookie-jar это фактически пароль: кто владеет файлом, тот владеет сессией.

Вторая идея - проверки как код. Вместо визуального осмотра вывода в скрипте проверяются статус-коды и содержимое: [ "$CODE" = "200" ] || { echo fail; exit 1; }. Для JSON - jq -e '.status == "ok"' который вернёт ненулевой код при несовпадении. Такая связка даёт честный exit code, и CI-система корректно красит прогон зелёным или красным.

Для серии запросов с накоплением состояния (получить список, взять первый id, отредактировать, удалить) используется цепочка с jq между вызовами curl, и весь сценарий читается как плоский bash-скрипт на пятнадцать строк. Никакие Postman коллекции при этом не нужны, тест лежит в git рядом с кодом API и запускается из любой CI-джобы.

Когда curl не хватает и что ещё полезно знать

Справедливости ради: есть пределы. Если сессия опирается на исполнение JavaScript на странице (например, токен вычисляется скриптом), curl бессилен, и дорога ведёт к headless-браузеру. Если API полностью stateless на Bearer-токенах, cookie-jar вообще не нужен, достаточно -H "Authorization: Bearer ...". Но для классических сессионных веб-приложений - админок самописных сервисов, phpAdmin-подобных панелей, Jenkins, Gitea - связка curl с jar остаётся самым прозрачным способом автоматизации.

Несколько привычек делают скрипты с curl честными: всегда выставлять --fail, чтобы HTTP-ошибки давали ненулевой exit code; использовать --max-time на каждом вызове, потому что дефолтный таймаут curl бесконечный; логировать только статусы и id, а не тело ответов с персональными данными. И держите единый шаблон логина в общей библиотеке функций: когда endpoint поменяется, править придётся одно место, а не пятнадцать скриптов.

Итог простой: cookie-jar превращает curl из одноразового пистолета в полноценный инструмент работы с сессиями, и вместе с jq покрывает почти все задачи сквозного тестирования session-based API. Скрипт такого прогона читается коллегами, хранится в git и выполняется идентично на любой машине, включая CI-раннер, и это то самое сочетание простоты и предсказуемости, которое ценится в тестовой инфраструктуре больше всего.
## Формы, JSON и файлы, три разных тела запроса

Неправильно сформированное тело запроса даёт половину всех фантомных багов в тестах. Усвоить стоит три сценария. Обычная HTML-форма: --data-urlencode для каждого поля, заголовок Content-Type ставится сам как application/x-www-form-urlencoded. JSON: обязательно пара -H "Content-Type: application/json" и -d '{...}', потому что иначе сервер получит form-urlencoded и, в лучшем случае, ответит 400, в худшем молча проигнорирует поля. Файлы и multipart: флаг -F "file=@photo.jpg" -F "description=test" собирает multipart/form-data с файлом, и сервер видит то же, что от браузерной формы с enctype.

Для файлов полезно знать про -F "file=@-;filename=shot.png" < photo.jpg, когда данные приходят из пайпа: так тестируется загрузка без промежуточного файла на диске. А проверку скачанного контента делают с контрольной суммой: curl -s URL | sha256sum и сравнение с эталоном.

Работа с заголовками, браузерная маскировка и корректность

Некоторые сессионные приложения ведут себя по-разному в зависимости от User-Agent и заголовков. Тест, который должен имитировать браузер, добавляет: -A "Mozilla/5.0..." и при необходимости -H "Accept: text/html" и -H "Accept-Language: ru". Это не маскировка злодея, а честное воспроизведение условий, которые видит production пользователь.

Обратная ситуация - когда нужно сбросить заголовки-наследство: в длительных скриптах переменные окружения могут подмешиваться, поэтому критичные запросы собирают явно, а не надеются на умолчания. Ещё одна классика: HTTP-код 419 в Laravel-подобных приложениях или 403 в Django-подобных при правильном логине почти всегда означает, что CSRF-токен не совпал, и искать надо именно токен, а не права пользователя.

Проверка ограничений, rate limiting и обрывы сессии

Хороший сценарий тестирует не только happy path. Серия быстрых запросов в цикле вроде for i in $(seq 1 50); do curl -s -o /dev/null -w "%{http_code} " -b "$JAR" "$BASE/api/data"; done; echo показывает, в каком месте включается rate limiting и какой код он возвращает: 429 с заголовком Retry-After или просто затишье. Это важно знать до того, как прогон упадёт в продакшен-проверке посередине.

Отдельно полезен тест истечения сессии: после логина делается пауза sleep или меняется время на тестовом сервере, затем повторный запрос должен вернуть 401 или редирект на логин. Если же API спокойно отдаёт данные спустя сутки - у вас сессия без адекватного TTL, и это уже находка для безопасности, а не баг теста.

Соблюдаем гигиену секретов и логов

Cookie-jar и токены в скриптах это настоящие секреты, и обращаться с ними стоит соответственно. Пароли не хардкодятся в скрипт, а читаются из окружения или менеджера секретов. Файл jar держится с правами 600 и удаляется по завершении прогона; в CI он живёт в /tmp на эфемерном раннере и умирает вместе с ним. В логи выводятся статусы и идентификаторы сущностей, но не сами cookie и не тела ответов с чувствительными данными - иначе лог сборки становится источником утечки, которую ещё долго придётся разгребать.

С такими привычками набор скриптов на curl надёжно сопровождает API годами: их просто читать новичку команды, они не зависят от версии тяжёлых фреймворков тестирования, и они стабильно выполняются одинаково что у разработчика на ноутбуке, что на CI-агенте в облаке, где единственная гарантия это наличие bash, curl и jq.

Ещё три флага, которые выручают в сессионных сценариях

Первый - --resolve: когда тестируете staging-копию по реальному имени хоста на другом IP, вместо правки /etc/hosts пишите curl --resolve app.example.com:443:10.0.0.15 и весь сценарий, включая cookie с доменной привязкой, работает против тестовой площадки. Второй - --cookie-jar с ключом -k на стендах с самоподписанными сертификатами: допустимо для lab-окружения, но пометьте такие скрипты комментарием, чтобы привычка не протекла в production-проверки. Третий - --retry 3 --retry-delay 2 на нестабильных стендах: retry включается только на временные сетевые ошибки и не маскирует настоящие 500, если комбинировать его с --fail на уровне итоговых проверок. С этими флагами повседневная работа становится ремеслом с известным набором инструментов: вы точно знаете, что лежит в ящике, и не ищете отвёртку каждый раз заново.
И заключительная мысль: curl в связке с cookie-jar не заменяет фреймворки тестирования, но даёт надёжный базовый уровень, на котором даже временный скрипт ведёт себя предсказуемо, а это в отладке сессионных API ценится на вес золота.