План предварительного тестирования для бухгалтерского API
Сборник сценариев надежности для инженерных команд: дублирование записей, повторное использование идемпотентности, подписи вебхуков, неупорядоченные события, десятичные денежные суммы, области видимости (scopes), блокировка периодов и восстановление.
Статус 201 Created от бухгалтерского API не говорит практически ни о чем. Он подтверждает лишь то, что сервер ответил. Он не гарантирует, что существует ровно один инвойс, что дебет равен кредиту, что номер документа был выделен единожды или что повторная отправка после обрыва соединения не зарегистрирует вторую копию. Большинство производственных дублирований в бухгалтерии возникают именно из-за этого пробела: мутация зафиксировалась на сервере, в то время как клиент остался в неведении относительно результата и тихо повторил запрос с новым ключом.
Этот план предназначен для команд, оценивающих бухгалтерский API перед его интеграцией в реальный учет. В нем основное внимание уделяется поведению в аварийных ситуациях, а нештатным сценариям (happy paths). В каждом разделе ниже описано, что именно нужно тестировать, какие утверждения (assertions) проверять и что считается успешным прохождением. В качестве контекста реализации здесь выступает Nordlet, чей API использует токены с ограничением области видимости (scoped bearer keys), денежные суммы в виде десятичных строк (decimal-string amounts), ключи идемпотентности и подписанные вебхуки. Чтобы узнать точный синтаксис эндпоинтов и структуру ответов, обратитесь к разделам Соглашения API и Начало работы, а не полагайтесь на предположения.
Что на самом деле означает «успешное прохождение»
Тест доказывает безопасность бухгалтерской интеграции только тогда, когда он проверяет семантику, а не статус-коды. Для каждого сценария набор тестов должен подтверждать:
- Бизнес-эффект: существует ровно один клиент, инвойс, запись журнала (journal entry), выделенный номер или платежная транзакция.
- Целостность учета: дебет равен кредиту, а денежная точность сохраняется при сериализации.
- Поведение при повторе: повторный запрос возвращает исходный результат вместо создания новой записи.
- Надежность (durability): после фиксации операции в системе появляются аудиторские записи и события вебхуков.
- Отказы без побочных эффектов: отклоненные запросы не расходуют номера документов и не создают частичных проводок.
- Восстановление: временную ошибку можно повторить без дублирования или безвозвратной потери данных.
Рекомендации Google по работе с API четко проводят границу: безопасными для повтора являются только те операции, повторное выполнение которых приводит к тому же конечному состоянию, причем API должен сам указывать, какие именно операции идемпотентны, а не слепо повторять каждый неудачный запрос.
Настройка изолированной и наблюдаемой среды
Используйте тестовую компанию (sandbox), а не реальные учетные данные. Тестовая компания в Nordlet использует те же модули и поведение API, что и настоящая, но имеет специальную отметку тестовых данных, что делает ее безопасной для высоконагруженных тестов на дублирование и сбои. Этот флаг невозможно изменить после создания, поэтому реальные учетные записи нельзя перевести в статус тестовых задним числом. Обратите внимание, что использование песочницы тарифицируется точно так же, как и работа в реальной компании, поэтому рассчитывайте объем нагрузочных тестов в соответствии с вашим тарифом.
Прежде чем писать тесты, подготовьте:
- Выделенную тестовую компанию с параметром
isSandbox: true. - Тестовый ключ API с правами доступа только к тестируемым модулям.
- Эндпоинт вебхуков или локальный туннель, сохраняющий исходное тело запроса (raw body).
- Предустановленных клиентов, поставщиков и известный план счетов.
- Как минимум два учетных периода: один открытый, другой заблокированный.
- Тестовые часы или фикстуру дат, если это поддерживается реализацией.
- Локальное хранилище для фиксации каждой попытки запроса, ключа идемпотентности, хеша ответа, доставки вебхука и результатов сверки.
Не запускайте тесты на дублирование на общем промежуточном (staging) окружении. Забытый клиент или инвойс от предыдущего запуска сделает последующие проверки некорректными.
Ваша фикстура для каждой мутации должна фиксировать метод, эндпоинт, полное тело запроса, ключ идемпотентности, область видимости ключа, идентификатор клиентской корреляции, временные метки начала и окончания, статус ответа, заголовки, тело, ID запроса, а также факт обрыва соединения до получения ответа. Используйте фиксированные десятичные строки, например "75.0000", для сумм. Nordlet представляет денежные значения в виде десятичных строк с точностью до четырех знаков после запятой, и его пример инвойса использует именно этот формат.
Вторая часть подготовки - это инъекция сбоев (failure injection). Ваш тестовый каркас должен уметь: задерживать ответ, обрывать TCP-соединение после получения сервером запроса, возвращать синтетические ответы 429/500/502/503/504, возвращать не-2xx код от получателя вебхуков, доставлять один и тот же вебхук дважды, менять порядок событий, повреждать один байт в теле вебхука, изменять полезную нагрузку (payload) ключа идемпотентности при сохранении самого ключа, запускать два одинаковых запроса одновременно, а также блокировать период между настройкой и проведением документа (posting). Эти сценарии стоят дороже сотни стандартных тестов.
Зафиксируйте контракт перед написанием логики повторов
Сгенерируйте или получите OpenAPI-схему и сопоставьте ее с вашим клиентом. Nordlet публикует свою OpenAPI-спекуляцию в справочнике по API и генерирует типизированные SDK на ее основе, поэтому тесты, основанные на схеме, надежнее написанных вручную предположений.
Проверьте обязательные поля и типы данных, поля с десятичными строками, форматы дат и временных меток, разрешенные действия и области видимости, структуру конверта ошибок, схемы событий вебхуков, а также то, какие мутации принимают ключ идемпотентности и какие поля ответа идентифицируют созданный объект.
Критерии прохождения. Клиент локально отклоняет недействительные данные в соответствии со схемой. Ответы сервера соответствуют задокументированной схеме. Некорректные десятичные дроби, неверные даты и отсутствие обязательных полей вызывают структурированные ошибки валидации. Для детерминированных ошибок валидации повторные попытки (retries) не предпринимаются.
Типичная проблема. Клиент парсит "121.0000" в число с плавающей точкой двойной точности (binary float) и впоследствии сериализует сумму с небольшими искажениями. Целые числа проходят успешно, а строки с НДС и дробные количества - нет. Тестируйте денежные суммы как строки на всем пути от начала до конца.
Авторизация и изоляция тенантов
Используйте раздельные ключи для чтения и записи. Затем протестируйте всю матрицу: отсутствие ключа, недействительный ключ, действительный ключ с недостаточной областью видимости модуля, действительный ключ с правильной областью видимости, ключ, принадлежащий другой компании, и попытку прочитать объект из другой компании.
Утверждения:
- Отсутствие или недействительность учетных данных возвращает
401. - Действительный ключ без требуемых прав возвращает
403. - Никаких изменений в учете при таких сбоях не происходит.
- Успешный запрос не позволяет читать или изменять записи другой компании.
- Ошибки безопасности никогда не повторяются автоматически.
Критерий прохождения. Вы можете доказать корректность авторизации и изоляцию тенантов с помощью последующих проверок чтения, а не только по статус-кодам.
Проверка одной чистой транзакции
Запустите небольшой детерминированный сценарий: создайте или найдите тестового клиента, создайте инвойск с суммами в виде десятичных строк, проведите его, прочитайте инвойс и его бухгалтерские проводки, прочитайте запись аудита и получите вебхук. Пример из раздела «Начало работы» Nordlet показывает, что проведение инвойса выделяет сквозной номер, проверяет открытость периода и создает сбалансированную проводку в рамках одной транзакции.
Убедитесь, что инвойс достигает ожидаемого состояния, номер документа выделяется ровно один раз, дебет равен кредиту, НДС и итоговые суммы по строкам соответствуют правилу округления в большую сторону при равных условиях (half-up calculation), запись аудита содержит имя субъекта и описание изменений, вебхук в конечном итоге доставляется, а ответ API и последующий запрос на чтение согласованы в отношении ID инвойса и его статуса.
Стоит четко разделить понятия: доставка вебхука не является доказательством фиксации мутации. Основными подтверждениями транзакции служат ответ API и последующее чтение данных. Вебхук подтверждает лишь передачу уведомления внешним системам и тестируется отдельно.
Идемпотентность: тот же ключ, то же тело
Генерируйте один ключ на каждую логическую бизнес-операцию, а не на каждую попытку сетевого запроса. Отправьте мутацию с ключом K и телом B, сохраните созданный ID, а затем отправьте точно такой же запрос повторно.
Убедитесь, что второй ответ представляет ту же операцию, ID объекта идентичен, а в системе существует ровно один бизнес-объект, одна проводка, один номер документа и один финансовый результат. Повторный ответ должен содержать задокументированный заголовок повтора (в случае Nordlet - x-idempotent-replay: true), а журнал аудита не должен содержать следов второй мутации.
Два правила хранения меняют ожидания от цикла повторов, поэтому протестируйте их напрямую: сохраненный ответ 4xx воспроизводится как любой другой, в то время как 5xx не сохраняется вовсе, поэтому повтор после ошибки сервера приводит к повторному выполнению операции, а не к выдаче сохраненного ответа. Ключи также имеют срок действия (в Nordlet - 24 часа), что ограничивает время, в течение которого доступен повтор.
Документация Nordlet гарантирует, что добавление заголовка Idempotency-Key предотвращает создание дубликатов при повторных запросах, а соглашения API определяют точный заголовок повтора и требование идентичности ответа побайтово. Этот принцип совпадает с опубликованной моделью Stripe: ключ идентифицирует логическую операцию, повторы возвращают сохраненный результат, а повторное использование ключа с другими параметрами отклоняется, а не изменяется молчаливо.
Критерий прохождения. Независимо от того, сколько раз клиент отправляет один и тот же запрос в пределах окна повтора, создается ровно один переход финансового состояния.
Идемпотентность: тот же ключ, измененное тело
Отправьте исходный запрос с ключом K и телом B1, затем второй запрос с тем же ключом K и телом B2, где отличается хотя бы одна сумма, клиент, дата или строка.
Ожидаемый результат: запрос отклоняется с задокументированной ошибкой повторного использования ключа (Nordlet возвращает 422 idempotency_key_reuse), исходная запись остается нетронутой, никаких новых записей, номеров или изменений в аудите не появляется, а клиент не пытается «исправить» ситуацию генерацией нового ключа. Молчаливое принятие B2 сделало бы ключи повторов небезопасными.
Параллельные дублирующие запросы
Отправьте два одинаковых запроса с одинаковым ключом в один и тот же момент времени. В зависимости от задокументированного поведения, один запрос выполняется, в то время как второй получает конфликт выполнения (Nordlet возвращает 409 idempotency_in_progress), либо оба в конечном итоге получают одинаковый результат.
Убедитесь, что оба вызова не могут создать независимые финансовые последствия, выделяется не более одного номера документа, временный конфликт повторяется с тем же ключом, а не с новым, конечный ресурс является полным и доступным для чтения, а запись идемпотентности не зависает навсегда.
Типичная проблема. Библиотека повторов, создающая новый UUID для каждой попытки, превращает одну логическую операцию в несколько независимых записей и полностью сводит на нет идемпотентность.
Неопределенный результат после обрыва соединения
Это главная имитация реальной производственной среды. Отправьте мутацию с ключом K, дайте серверу зафиксировать ее, оборвите соединение до прихода ответа, зафиксируйте результат как неизвестный, затем повторите запрос с тем же ключом K и выполните сверку путем чтения ресурса и данных аудита.
Убедитесь, что повторный запрос возвращает исходный или задокументированный ответ повтора, существует ровно одно финансовое последствие, клиент отмечает операцию завершенной только после успешного повтора или чтения для сверки, и никакой новый ключ не генерируется. Таймаут не означает «ничего не произошло». Stripe определяет идемпотентность именно как защиту от сетевых сбоев, при которых сервер мог обработать мутацию до того, как клиент успел получить ответ.
Консервативная политика повторов
Решения о повторах зависят как от характера ошибки, так и от того, является ли операция идемпотентной, а не только от HTTP-метода.
Обычно допускают повтор с сохранением того же ключа: сброс соединения или таймаут после идемпотентной мутации, 408, 429 (после соблюдения заголовка retry-after), а также 502/503/504. Задокументированный ответ о выполнении операции (in-progress) можно повторить после короткой задержки.
Не подлежат повтору без сверки: ошибки валидации 400/422, ошибки аутентификации 401/403, 404 при отсутствии ресурса, бизнес-конфликты 409, не связанные с выполняющимся ключом, измененное тело запроса для существующего ключа, а также отказ из-за заблокированного периода.
Убедитесь в реализации экспоненциальной задержки с добавлением случайного разброса (jitter), задайте явные максимальные попытки и время ожидания, соблюдайте retry-after при ограничении частоты запросов, сохраняйте тот же ключ для повторов одной операции, используйте новый ключ для каждой новой операции и ведите логи, разделяя исходную попытку и повторы.
Протестируйте ограничение частоты запросов (rate limiting) отдельно. Превысьте задокументированный порог (по умолчанию Nordlet разрешает 300 запросов в минуту на один API-ключ, а некоторые неавторизованные эндпоинты имеют собственные более низкие лимиты на IP-адрес), а затем убедитесь, что сервер возвращает ответ об ограничении, клиент парсит retry-after как задержку, а не как ошибку, делает паузу вместо «бомбардировки» сервера, а при повторении ограниченной мутации использует исходный ключ. Заголовки x-ratelimit-* сообщают оставшийся лимит, что удобно использовать для предварительных проверок. Избегайте тестов лимитов во время проверки финансовых операций, если ваша тестовая среда не умеет отличать заблокированный из-за лимита запрос от успешно зафиксированного.
Верификация подписи вебхуков
Создайте получатель, сохраняющий исходные байты запроса. Для каждого события: сохраните сырое тело (raw body), прочитайте заголовок x-nordlet-signature, вычислите ожидаемый HMAC-SHA256 с использованием секретного ключа подписки, сравните их с помощью функции сравнения за постоянное время (constant-time function), парсите JSON только после успешной верификации, зарегистрируйте доставку и верните быстрый статус 2xx после надежного сохранения. Nordlet подписывает HMAC-хеш по сырому телу, отправляет его в виде sha256=<hex> и повторяет доставку с экспоненциальной задержкой. Рекомендации Stripe по вебхукам также подчеркивают необходимость проверки немодифицированного сырого тела с использованием сравнения за постоянное время.
Проведите негативные тесты: правильное тело и секрет, правильное тело с неверным секретом, изменение тела на один байт, повторно сериализованный JSON с измененными пробелами, отсутствие заголовока, некорректная подпись, повторная доставка старого события (если схема включает временную метку) и действительная подпись, доставленная не в ту подписку.
Критерии прохождения. Принимаются только правильное сырое тело и верный секрет. Недействительные подписи получают не-2xx статус. Некорректные данные никогда не попадают в бухгалтерскую очередь. Секретные ключи не попадают в обычные логи. Обработчик подтверждает получение только после надежной записи события.
Дубликаты и неупорядоченные вебхуки
Надежность доставки - это отдельная задача от идемпотентности API, и получатель также должен быть идемпотентным. Доставьте одно и то же событие дважды, доставьте событие sale_invoice.paid раньше sale_invoice.issued, верните 500 после сохранения, но до отправки ответа, и повторите событие, которое потребитель уже обработал.
Убедитесь, что повторные доставки не дублируют платежи, изменения статусов или проводки в журнале, что ключ дедупликации использует ID доставки или события провайдера, если таковой имеется (или задокументированный составной ключ, но никогда произвольную временную метку), что неупорядоченные события не перезаписывают более свежее состояние старым, что потребитель может запросить текущее состояние из API, когда порядка недостаточно, и что неудачные попытки потребителя направляются в очередь или в тупиковую очередь (dead-letter path). Stripe явно предупреждает, что порядок доставки не гарантируется, и рекомендует сохранять обработанные ID событий.
Держите эндпоинт простым: проверяйте структуру, верифицируйте подпись, сохраняйте или отправляйте в очередь, подтверждайте получение, возвращайте 2xx. Сверка и уведомления относятся к асинхронной обработке. Протестируйте это, заставив последующую обработку «заснуть» дольше таймаута доставки; корректный получатель все равно подтверждает прием после надежной постановки в очередь.
Открытые и заблокированные учетные периоды
Nordlet применяет блокировку периодов во время проведения документов для ручных журналов, потоков документов и импорта, поэтому протестируйте этот контроль в нескольких местах. Создайте эквивалентные документы с датами в открытом периоде, в заблокированном периоде, на границе непосредственно перед блокировкой, в первый день следующего открытого периода, а также с допустимой датой документа, но недействительной датой проводки (если API разделяет эти понятия).
Для случая с заблокированным периодом убедитесь, что запрос отклоняется с задокументированной ошибкой блокировки периода (в Nordlet это 409 conflict), никакие строки проводок не записываются, номер документа не расходуется, документ не переходит в статус проведенного, и вебхук об успехе не отправляется. Версия с открытым периодом должна завершиться успехом с теми же финансовыми значениями.
Тест на состояние гонки (Race test). Запустите проведение документа в открытом периоде, заблокируйте период прямо во время выполнения запроса и убедитесь, что фиксация не сможет обойти блокировку. Главное условие прохождения: проведенная проводка никогда не должна попадать в период, который был заблокирован до фиксации транзакции.
Аудит и сверка
Для каждой успешной мутации убедитесь, что бизнес-объект существует, бухгалтерская проводка создана с равными дебетом и кредитом, запись аудита существует, событие вебхука существует или доступно для получения, а запись о вашей операции ссылается на ID объекта провайдера, причем повторы ссылаются на ту же логическую операцию.
Для каждой отклоненной мутации убедитесь, что частичный документ не создан, номер не был израсходован (если контракт не разрешает резервирование), проводки в журнале отсутствуют, вебхук об успехе не отправлен, а ошибка содержит ID запроса для диагностики. Соглашения Nordlet описывают аудируемость мутаций и сбалансированность проводок как гарантии платформы, причем балансировка обеспечивается отложенным триггером базы данных в момент фиксации, а не только кодом приложения. Превратите эти гарантии в запросы сверки после тестов, вместо того чтобы доверять HTTP-ответу.
Основная тестовая матрица
| Область | Действие в тесте | Ожидаемое утверждение | Критерий прохождения |
|---|---|---|---|
| Аутентификация | Нет ключа, неверный ключ, неверная область видимости | Структурированные 401/403; нет побочных эффектов |
Все отклоненные попытки не создают записей в учете |
| Схема и деньги | Неверная десятичная дробь, дата, обязательное поле | Ошибка валидации на уровне полей | Повторы или частичная запись не происходят |
| Обычная проводка | Создать и провести инвойс в открытом периоде | Один документ, одна сбалансированная проводка, одна запись аудита | Состояние после чтения и бухгалтерский журнал совпадают |
| Повтор с тем же ключом | Повторить идентичный запрос, тот же ключ и тело | Исходный результат возвращен повторно; нет дубликатов | ID объекта, номер, проводка и аудит остаются единственными |
| Измененное тело, тот же ключ | Повторно использовать ключ с другой суммой | Задокументированный отказ из-за повторного использования ключа | Исходная запись не изменена; ничего нового не записано |
| Обрыв соединения | Зафиксировать, оборвать, повторить с тем же ключом | Повторный результат | Ровно одно финансовое последствие |
| Заблокированный период | Провести документ с датой в заблокированном периоде | Отказ из-за блокировки периода | Нет проводок, нет номера, нет вебхука об успехе |
| Подпись вебхука | Неверный секрет, измененное тело | Статус не-2xx от получателя | Принимаются только правильное сырое тело и верный секрет |
С чего начать в первую очередь
Если времени мало, запускайте тесты в порядке выявления самых дорогих ошибок. Начните с повтора после обрыва соединения, так как именно этот сценарий порождает большинство реальных дубликатов. Затем проверьте пару «тот же ключ» и «измененное тело» для идемпотентности. Далее проверьте заблокированные периоды, так как проводка в закрытом месяце создает проблемы с аудитом, которые всплывают слишком поздно. Верификация подписей и неупорядоченные вебхуки идут следом, а полная матрица схемы и авторизации может постоянно выполняться в CI, как только клиент стабилизируется.
Все остальное в этом плане поддерживает эти пять пунктов. Добейтесь их успешного прохождения на тестовой компании с проверками сверки для подтверждения побочных эффектов, и вы будете уверены в безопасности интеграции до того, как она коснется хотя бы одной реальной бухгалтерской книги. Вы можете создать тестовую компанию и ключ с ограниченными правами за пару минут в разделе Начало работы и проверить необходимые модули в списке возможностей.
FAQ
Достаточно ли ответа 2xx для подтверждения успешной записи в бухгалтерию?
Нет. Статус-код подтверждает лишь то, что сервер ответил, а не то, что созданы ровно один документ, одно выделение номера и одна сбалансированная проводка. Каждый тест мутации должен сопровождаться чтением объекта, его бухгалтерских проводок и записи аудита. Самый опасный случай - обратная ситуация: операция зафиксировалась, но клиент так и не получил ответ.
Стоит ли генерировать новый ключ идемпотентности при таймауте запроса?
Нет, именно эта ошибка вызывает большинство производственных дубликатов. Ключ идентифицирует логическую бизнес-операцию, а не сетевую попытку. Повторите запрос с тем же ключом, после чего выполните сверку путем чтения ресурса. Библиотека повторов, создающая новый UUID при каждой попытке, превращает один задуманный инвойс в несколько независимых записей.
Что произойдет, если повторно использовать ключ идемпотентности с другой полезной нагрузкой (payload)?
Корректный API отклонит такой запрос, вместо того чтобы молча применить новое тело. Nordlet возвращает 422 idempotency_key_reuse и оставляет исходную запись нетронутой. Ваш клиент должен рассматривать это как ошибку программирования, которую нужно исправить, а не как условие, которое можно обойти генерацией нового ключа.
Являются ли вебхуки заменой чтению созданного объекта?
Нет. Доставка вебхука подтверждает лишь отправку уведомления внешним системам, но не гарантирует, что транзакция зафиксировалась именно так, как вы ожидали, к тому же порядок доставки не гарантируется. Проверяйте подпись по сырому телу, дедуплицируйте по ID доставки или события и запрашивайте актуальное состояние из API всякий раз, когда порядка событий недостаточно для понимания того, что именно изменилось.
Как протестировать проведение документа в заблокированный бухгалтерский период?
Создайте эквивалентные документы в открытом и заблокированном периодах и убедитесь, что во втором случае запрос отклоняется без создания строк проводок, без расходования номера документа, без изменения состояния и без отправки вебхука об успехе. Затем запустите тест на состояние гонки: начните проведение в открытом периоде и заблокируйте период прямо во время выполнения запроса. Проведенная транзакция никогда не должна попадать в период, который был заблокирован до фиксации транзакции.
Можно ли запускать высоконагруженные тесты на дублирование в тестовой компании?
Да. Тестовая компания ведет себя как настоящая, имеет специальную пометку тестовых данных и удаляется сразу же по завершении, а не после окончания срока хранения, поэтому она идеально подходит для деструктивных тестов и тестов на дублирование. Использование песочницы все равно тарифицируется как реальное использование, поэтому планируйте объем запросов, а не запускайте их в бесконечных циклах.