API запускает пересчёт баланса через GET, из за чего повторное обращение создаёт новую запись аудита. Какой...

API запускает пересчёт баланса через GET, из-за чего повторное обращение создаёт новую запись аудита. Какой HTTP-инвариант должен выявить интеграционный тест?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Интеграционный тест должен выявить нарушение свойства безопасности GET: обработка GET не должна изменять наблюдаемое бизнес-состояние сервера. Повторный GET может вернуть тот же или изменившийся результат из-за актуализации данных, но не должен запускать пересчёт, создавать аудитную запись или выполнять другую бизнес-операцию.

Исторический контекст

HTTP разделяет методы по смыслу, чтобы клиент, кэш, поисковый робот или другой автоматизированный участник мог понимать последствия запроса без знания внутренней реализации сервера. Свойство safe у метода означает, что запрос предназначен для чтения, а не для изменения состояния ресурса.

Это свойство необходимо для безопасного предварительного получения страниц, повторной загрузки и кэширования GET-запросов. Оно не означает отсутствия любых технических эффектов: сервер может вести обычные журналы, считать метрики или обновлять внутренний кэш, если это не является бизнес-изменением.

Постановка проблемы

Если GET запускает пересчёт или создаёт аудитную запись, сторонний компонент может непреднамеренно выполнить операцию при переходе по ссылке, предварительной загрузке или повторном запросе. Кэширование и автоматические повторы также начинают взаимодействовать с бизнес-операцией непредсказуемо.

Интеграционный тест, проверяющий только код ответа и тело, может пройти успешно, хотя состояние базы данных уже изменилось. Риск особенно высок, когда endpoint выглядит как чтение, но внутри вызывает командную логику.

Подробное решение

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

Важно различать safe и idempotent. Идемпотентная операция может изменять состояние, но повторение запроса приводит к тому же итоговому состоянию; безопасный GET вообще не должен запрашивать бизнес-изменение. Поэтому перенос операции на PUT не решит проблему автоматически: для запуска действия обычно уместнее POST, семантика которого допускает изменение состояния.

Тест не обязан требовать полного отсутствия любых изменений на сервере. Например, технический access log или счётчик мониторинга не нарушают безопасность метода, если они не являются частью бизнес-состояния и не влияют на результат операции. Граница должна быть явно определена в контракте API.

Ситуация из практики

Команда обнаружила endpoint GET для просмотра баланса. При каждом обращении сервис пересчитывал баланс и добавлял запись в историю операций. Сначала рассматривались два варианта: оставить GET и сделать пересчёт идемпотентным либо разделить чтение и команду.

Идемпотентность уменьшила бы риск повторного финансового изменения, но не устранила бы неверную семантику метода: кэш или робот всё равно мог бы неожиданно запускать пересчёт и создавать побочные бизнес-эффекты. Команда выбрала отдельную команду через POST, а GET оставила только для чтения текущего результата.

Интеграционный тест после этого проверял, что GET не изменяет баланс и историю аудита, а POST явно запускает пересчёт. Такой вариант сделал поведение предсказуемым для клиентов и инфраструктуры, а границу между чтением и изменением — проверяемой.

Что кандидаты часто упускают

  1. Разве GET нельзя повторять, если операция идемпотентна?

Идемпотентность и безопасность — разные свойства. Повторный PUT, например, может менять ресурс, но повторное выполнение приводит к тому же итоговому состоянию. Для GET требование строже: его назначение — получение представления, поэтому бизнес-изменение уже при первом вызове является нарушением семантики.

  1. Нужно ли сравнивать абсолютно всё состояние базы до и после GET?

Нет. Следует сравнивать бизнес-состояние, затрагиваемое контрактом: баланс, аудит, статусы и связанные сущности. Технические журналы, метрики и служебные обновления могут изменяться, поэтому проверка должна отделять допустимые инфраструктурные эффекты от изменений, которые видит бизнес или клиент.

  1. Можно ли оставить пересчёт внутри GET, если добавить специальный заголовок, запрещающий кэширование?

Запрет кэширования снижает один из рисков, но не исправляет семантику метода. GET всё ещё может быть вызван роботом, браузером, прокси или клиентом повторно. Команду следует сделать явно изменяющей состояние, обычно через POST, а интеграционным тестом проверить, что чтение и запуск операции имеют разные контракты.