Nordlet

← Блог

План за тестове преди производство за счетоводен API

Наръчник по надеждност за инженерни екипи: дублирани записи, преизползване на идемпотентност, подписи на уебхукове, събития извън ред, десетични суми, обхвати, заключени периоди и възстановяване.

Nordlet Team · · 16 мин четене

Отговор 201 Created от счетоводен API не ви дава почти никаква информация. Той само потвърждава, че сървърът е отговорил. Той не гарантира, че съществува точно една фактура, че дебитите се равняват на кредитите, че номерът на документа е бил заделен еднократно или че повторният опит след прекъсната връзка няма да запише второ копие. Повечето производствени счетоводни дублирания произтичат от тази празнина: промяна, която е била записана на сървъра, докато клиентът е бил в неизвестност относно изхода и тихо е повторил заявката с нов ключ.

Този план е предназначен за екипи, които оценяват счетоводен API, преди да го интегрират в реални счетоводни регистри. Той се фокусира върху поведението при грешки, а не върху идеалните сценарии (happy paths). Всяка област по-долу посочва какво да тествате, какво да твърдите като очакван резултат и какво се счита за успешно преминаване на теста. Контекстът на изпълнение тук е Nordlet, чийто API използва капсулирани (scoped) носещи ключове (bearer keys), суми като десетични низове (decimal-string), ключове за идемпотентност и подписани уебхукове. За точния синтаксис на крайната точка (endpoint) и договорите за отговор, моля, прочетете страниците с API конвенции и Първи стъпки, вместо да разчитате на предположения.

Какво всъщност означава „успешно преминаване“

Даден тест доказва, че счетоводната интеграция е безопасна само когато проверява семантиката, а не кодовете за състояние. За всеки сценарий тестовият пакет трябва да потвърждава:

  • Бизнес страничен ефект: съществува точно един клиент, фактура, счетоводна статия, номериране или платежна транзиция.
  • Счетоводна цялост (integrity): дебитите са равни на кредитите и паричната точност се запазва при сериализация.
  • Поведение при повторно изпълнение (replay): повторният опит връща оригиналния резултат, вместо нов запис.
  • Издръжливост (durability): след като операцията се запише, съществуват одиторски записи и събития от уебхукове.
  • Грешки без странични ефекти: отхвърлените заявки не консумират номера на документи и не записват частични редове в книгата (ledger).
  • Възстановяване: преходна грешка може да бъде повторена без дублиране или постоянна загуба на данни.

Ръководството за API на Google поставя границата ясно: само операции, чието повтаряне води до същото крайно състояние, са безопасни за повторен опит, като API трябва да идентифицира кои операции са идемпотентни, вместо сляпо да повтаря всяка грешка.

Настройка на изолирана и наблюдаема среда

Използвайте тестова фирма (sandbox), а не производствени регистри. Тестовата фирма в Nordlet използва същите модули и API поведение като реалната фирма и е обозначена като тестови данни, което я прави безопасна за тестове за дублиране и грешки с голям обем. Този флаг е неизменен след създаването, така че реалните регистри не могат да бъдат прекласифицирани като тестови данни по-късно. Имайте предвид, че използването на тестовата среда се отчита и таксува точно както използването в реална фирма, затова съобразете обема на тестовете си с ценовата листа, на която се намирате.

Преди да напишете който и да е тест, подгответе:

  • Специализирана тестова фирма с isSandbox: true.
  • Тестов API ключ, ограничен само до тестваните модули.
  • Крайна точка за уебхук или локален тунел, който запазва суровото тяло на заявката.
  • Предварително заредени клиенти, доставчици и познат индивидуален сметкоплан.
  • Най-малко два счетоводни периода: един отворен и един затворен.
  • Тестов часовник или времеви компонент (fixture), ако внедряването го поддържа.
  • Локално хранилище, което записва всеки опит за заявка, ключ за идемпотентност, хеш на отговора, доставка на уебхук и резултат от реконсилиация.

Не стартирайте тестове за дублиране срещу споделени данни за подготовка (staging). Изостанал клиент или фактура от предишно изпълнение прави последващите твърдения двусмислени.

Вашият компонент за всяка промяна трябва да улавя метода, крайната точка, пълното тяло, ключа за идемпотентност, обхвата на ключа, клиентския идентификатор за корелация, началното и крайното времево клеймо, кода на отговора, заглавките (headers), тялото, идентификатора на заявката (request ID) и това дали връзката е прекъснала преди пристигането на отговора. Използвайте фиксирани десетични низове като "75.0000" за парични суми. Nordlet представя паричните стойности като десетични низове с до четири знака след десетичната запетая, като примерната му фактура използва този формат.

Другата половина от настройката е инжектирането на грешки. Вашата тестова рамка трябва да може да забавя отговор, да прекъсва TCP връзка след като сървърът е получил заявката, да връща синтетични отговори 429/500/502/503/504, да връща грешка (не-2xx код) от получателя на уебхука, да доставя един и същ уебхук два пъти, да разменя реда на събитията, да поврежда един байт от тялото на уебхук, да променя полезния товар (payload) на ключа за идемпотентност, докато запазва същия ключ, да изпълнява две еднакви заявки едновременно и да заключва период между подготовката и публикуването (posting). Тези сценарии струват повече от сто примерни сценария с успешен изход.

Защитете договора (contract), преди да пишете логика за повторни опити

Генерирайте или изтеглете OpenAPI схемата и я сравнете с вашия клиент. Nordlet публикува своята OpenAPI спецификация в API документацията и генерира своите типизирани SDK от същата спецификация, така че тестовете, базирани на схема, превъзхождат ръчно писаните предположения.

Проверете задължителните полета и типове, полетата с десетични низове, форматите за дата и времево клеймо, разрешените действия и обхвати, формата на обвивката за грешки (error envelope), схемите на уебхук събитията, кои промени приемат ключ за идемпотентност и кои полета в отговора идентифицират създадения обект.

Критерии за успех. Клиентът отхвърля невалидни полезни товари локално там, където схемата го позволява. Отговорите на сървъра съответстват на документираната схема. Невалидните десетични знаци, лошите дати и липсващите задължителни полета генерират структурирани грешки при валидация. Не се предприема повторен опит за детерминистична грешка при валидация.

Режим на грешка, който трябва да бъде уловен. Клиент разпознава "121.0000" в двоично число с плаваща запетая и сериализира малко по-различна сума по-късно. Цялите числа минават успешно; редовете с ДДС и дробните количества се провалят. Тествайте паричните суми като низове от край до край.

Ототоризация и изолация между наематели (tenants)

Използвайте отделни ключове за четене и запис. След това тествайте цялата матрица: липсващ ключ, невалиден ключ, валиден ключ с недостатъчен обхват на модула, валиден ключ с правилния обхват, ключ, принадлежащ на друга фирма, и опит за четене на обект от различна фирма.

Твърдения за проверка (Assertions):

  • Липсващи или невалидни удостоверителни данни връщат 401.
  • Валиден ключ без изискваните права връща 403.
  • Не се създава счетоводен страничен ефект при нито една от двете грешки.
  • Успешна заявка не може да чете или променя записи на друга фирма.
  • Грешките свързани със сигурността никога не се повтарят автоматично.

Критерий за успех. Можете да докажете както коректността на ототоризацията, така и изолацията между наемателите с последващи тестови четения, а не само с кодове за състояние.

Докажете една чиста транзакция

Изпълнете малък, детерминистичен процес: създайте или намерете тестов клиент, създайте фактура със суми в десетични низове, издайте я, прочетете фактурата и нейния счетоводен ефект, прочетете одиторския запис и получете уебхука. Примерът за първи стъпки (Getting Started) на Nordlet показва издаване на фактура, което заделя пореден номер без празнини, проверява дали периодът е отворен и публикува балансирана счетоводна статия в същата транзакция.

Уверете се, че фактурата достигна очакваното състояние, номерът на документа е заделен точно веднъж, дебитите са равни на кредитите, ДДС-то и общите суми по редовете съответстват на очакваното закръгляване нагоре (half-up), одиторският запис посочва извършителя и промяната, уебхукът е успешно доставен в крайна сметка, а отговорът на API и последващото четпене съвпадат относно ID-то и състоянието на фактурата.

Едно разграничение, което си заслужава да бъде запазено: получаването на уебхук не е доказателство, че промяната е била записана. Отговорът на API заедно с последващото четене са основните ви транзакционни твърдения. Уебхукът доказна уведомлението надолу по веригата (downstream) и се тества отделно.

Идемпотентност: същ ключ, също тяло

Генерирайте по един ключ за всяка логическа бизнес операция, а не по един ключ за всеки мрежов опит. Изпратете промяната с ключ K и тяло B, запазете създаденото ID, след това изпратете същата заявка отново.

Уверете се, че вторият отговор представлява същата операция, ID-то на обекта е идентично и съществува един бизнес обект, едно записване, един номер на документ, един финансов ефект. Повтореният отговор трябва да носи документирания заглавен ред за повторение – x-idempotent-replay: true в случая на Nordlet – а одиторската следа не трябва да показва втора промяна.

Две правила за съхранение променят това, което вашият цикъл за повторни опити трябва да очаква, затова ги тествайте директно: съхраненият 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 и направете реконсилиация, като прочетете ресурса и данните от одит.

Уверете се, че повторният опит връща оригиналния или документирания отговор за повторение, съществува точно един финансов страничен ефект, клиентът отбелязва операцията като завършена само след успешно повторение или четене за реконсилиация и не се генерира нов ключ. Времевото изчакване (timeout) не означава „нищо не се е случило“. Stripe описва идемпотентността точно като защита срещу грешки във връзката, при които сървърът може да е обработил промяната, преди клиентът да успее да получи отговора.

Консервативна политика за повторни опити

Решенията за повторни опити зависят както от грешката, така и от това дали операцията е идемпотентна, а не само от HTTP метода.

Обикновено подлежащи на повторен опит, запазен същ ключ: нулиране на връзката или времево изчакване след идемпотентна промяна, 408, 429 след спазване на retry-after и 502/503/504. Документиран отговор за идемпотентност в ход може да бъде повторен след кратко забавяне.

Не подлежащи на повторен опит без реконсилиация: грешки при валидация 400/422, грешки при удостоверяване 401/403, 404 за липсващ ресурс, бизнес конфликти 409, които не са свързани с ключ в процес на изпълнение, променен полезен товар с наличен ключ и отхвърляне поради заключен период.

Уверете се в наличието на ограничено експоненциално забавяне със случайни отклонения (jitter), изрични максимални опити и изминало време, спазване на retry-after за лимитите на заявките (rate limits), запазване на същия ключ при повторни опити на една операция, нов ключ за всяка нова операция и логове, които разделят оригиналния опит от повторните.

Тествайте лимитирането на заявките самостоятелно. Пресечете документирания праг – по подразбиране Nordlet позволява 300 заявки на минута за API ключ, а някои неавтентикационни крайна точки носят свои собствени по-ниски лимити за адрес – след което потвърдете, че сървърът връща отговор за превишен лимит, клиентът разпознава retry-after като забавяне, а не като грешка, той прави пауза, вместо да атакува непрекъснато, а повторният опит за лимитирана промяна преизползва оригиналния ключ. Заглавките x-ratelimit-* докладват оставащия лимит, което е добра предварителна проверка. Дръжте тестовете за лимит на заявките далеч от финансовите твърдения, освен ако вашата тестова рамка не може да разграничи ограничен по лимит опит от записан такъв.

Проверка на подписа на уебхук (webhook signature)

Изградете получател, който запазва суровите байтове. За всяко събитие: уловете суровото тяло, прочетете заглавката x-nordlet-signature, изчислете очаквания HMAC-SHA256 с тайната на абонамента, сравнете с помощта на функция с постоянно време за изпълнение, анализирайте JSON само след проверка, запишете доставката и върнете бърз 2xx код след трайно приемане. Nordlet подписва HMAC върху суровото тяло, изпраща го като sha256=<hex> и повтаря доставите с експоненциално забавяне. Ръководството за уебхукове на Stripe набляга на проверката на непромененото сурово тяло чрез сравнение с постоянно време.

Изпълнете негативни тестове: правилно тяло и тайна, правилно тяло с грешна тайна, промяна на един байт в тялото, повторно сериализиран JSON с променени интервали, липсваща заглавка, деформиран подпис, повторно доставяне на старо събитие, ако схемата включва времево клеймо, и валиден подпис, доставен до грешен абонамент.

Критерии за успех. Приемат се само правилното сурово тяло и тайна. Невалидните подписи получават не-2xx код. Невалидните полезни товари никога не влизат в счетоводната опашка. Тайната се пази извън обикновените логове. Обработчикът потвърждава получаването само след като събитието е записано трайно.

Дублирани уебхукове и уебхукове извън ред

Надеждността на доставката е отделен проблем от идемпотентността на API и получателят също трябва да бъде идемпотентен. Доставете същото събитие два пъти, доставете събитие sale_invoice.paid преди събитие sale_invoice.issued, върнете 500 след запазване, но преди отговор, и повторете събитие, което потребителят вече е обработил.

Уверете се, че дублираните доставки не дублират плащания, промени в състоянието или счетоводни статии, че ключът за дедупликация използва идентификатора за доставка или събитие на доставчика, когато такъв съществува (или документиран комбиниран ключ, никога произволно времево клеймо), че събитията извън ред никога не презаписват по-ново състояние с по-старо, че потребителят може да извлече текущото състояние от API, когато редът е недостатъчен, и че неуспешните потребителски опити преминават през опашка или път за грешни съобщения (dead-letter). Stripe предупреждава изрично, че редът на доставка не е гарантиран и препоръчва съхраняването на обработени идентификатори на събития.

Поддържайте крайната точка лека: валидирайте формата, проверете подписа, запазете или поставете на опашка, потвърдете, върнете 2xx. Реконсилиацията и уведомленията принадлежат към асинхронната обработка. Тествайте го, като накарате downstream работата да заспи след времевия лимит за доставка; правилният получател все още потвърждава след трайно поставяне на опашка.

Отворени и заключени счетоводни периоди

Nordlet налага заключване на периоди по време на осчетоводяване в ръчни списания, документи и импорти, затова тествайте контрола на повече от едно място. Създайте еквивалентни документи с дата в отворен период, в заключен период, на границата точно преди заключването, в първия ден на следващия отворен период и с валидна дата на документа, но невалидна дата на осчетоводяване, ако API отделя двете.

За случая със заключен период, уверете се, че заявката е отхвърлена с документираната грешка за заключен период – конфликт 409 в Nordlet – и че не се записват редове в счетоводната книга, не се консумира номер на документ, нито един документ не преминава в състояние на публикуван и не се изпраща успешен уебхук за събитие. Версията за отворен период трябва да успее със същите финансови стойности.

Тест за състезателни условия (race condition). Започнете осчетоводяване срещу отворен период, заключете периода по време на заявката и потвърдете, че записа не може да заобиколи заключването. Критичното условие за успех: записано осчетоводяване никога не попада в период, който е бил заключен преди транзакцията да бъде завършена.

Одитируемост и реконсилиация

За всяка успешна промяна потвърдете, че бизнес обектът съществува, счетоводният запис съществува с дебити, равни на кредитите, одиторският запис съществува, събитието от уебхук съществува или може да бъде извлечено, а вашият запис за операция сочейки към ID-то на обекта на доставчика с повторни опити, препращащи към същата логическа операция.

За всяка отхвърлена промяна потвърдете, че няма частичен документ, не е консумиран номер, освен ако договорът не позволява запазване, няма запис в регистъра, няма успешен уебхук и грешката носи идентификатор на заявка за диагностика. Конвенциите на Nordlet описват одитируемостта на промените и балансираните счетоводни статии като платформени гаранции, като балансът се налага от отложен тригер на базата данни в момента на транзакцията, а не само от приложението. Превърнете тези гаранции в заявки за реконсилиация след теста, вместо да се доверявате на HTTP отговора.

Основна тестова матрица

Област Тестово действие Очаквано твърдение Критерий за успех
Удостоверяване Без ключ, невалиден ключ, недостатъчен обхват Структуриран 401/403; липса на страничен ефект Всички отхвърлени опити не създават счетоводен запис
Схема и пари Невалидно десетично число, дата, липсващо поле Грешка при валидация на ниво поле Не възниква повторен опит или частичен запис
Нормално осчетоводяване Създаване и издаване на фактура в отворен период Един документ, една балансирана статия, един одит Състоянието при четене и регистърът съвпадат
Повторение със същ ключ Повторение на еднаква промяна, същ ключ и тяло Оригиналният резултат е повторен; липса на дубликат ID на обект, номер, счетоводен ефект, одит остават уникални
Променено тяло, същ ключ Повторно използване на ключ с различна сума Документирано отхвърляне поради повторна употреба Оригиналният запис е непроменен; нищо ново не е записано
Прекъсната връзка Запис, прекъсване, повторение със същ ключ Повторен резултат Точно един финансов страничен ефект
Заключен период Публикуване на документ с дата в заключен период Отхвърляне поради заключен период Без статия, без номер, без успешен уебхук
Подпис на уебхук Грешна тайна, променено тяло Не-2xx от получателя Приети само правилно сурово тяло и тайна

С какво бих започнал първо

Ако времето е малко, изпълнете тестовете в реда, който улавя най-скъпоструващите грешки. Започнете с повторния опит при прекъсната връзка, защото този единствен сценарие произвежда повечето реални дублирания. След това двойката идемпотентност със същ ключ и променено тяло. След това заключените периоди, тъй като осчетоводяването в затворен месец е проблем с одита, който излиза наяве късно. Проверката на подписите и уебхуковете извън ред идват след това, а цялата матрица за схема и ототоризация може да се изпълнява непрекъснато в CI, след като клиентът се стабилизира.

Всичко останало в този план подкрепя тези пет точки. Направете ги зелени спрямо тестова фирма, с четене за реконсилиация, потвърждаващо страничните ефекти, и ще знаете, че интеграцията е безопасна, преди да докосне дори един единствен реален регистър. Можете да създадете тестова фирма и ограничен тестов ключ за няколко минути от Първи стъпки и да проверите от кои модули се нуждаете спрямо списъка с функции.

FAQ

Дали отговор 2xx е достатъчен, за да потвърди, че счетоводният запис е успешен?

Не. Кодът за състояние потвърждава, че сървърът е отговорил, а не че съществуват точно един документ, едно заделяне на номер и една балансирана счетоводна статия. Всеки тест за промяна трябва да бъде последван от четене на обекта, неговия счетоводен ефект и неговия одиторски запис. Опасният случай е обратната посока: операция, която е записана, докато клиентът изобщо не е получил отговор.

Трябва ли да генерирам нов ключ за идемпотентност, когато заявката получи времево изчакване (timeout)?

Не, и това е грешката, която причинява повечето дублирания в производство. Ключът идентифицира логическата бизнес операция, а не мрежовия опит. Повторете същия ключ, след което направете реконсилиация, като прочетете ресурса. Библиотека за повторни опити, която генерира ново UUID при всеки опит, превръща една предназначена фактура в няколко независими записа.

Какво се случва, ако използвам повторно ключ за идемпотентност с различен полезен товар?

Правилният API го отхвърля, вместо мълчаливо да приложи новото тяло. Nordlet връща 422 idempotency_key_reuse и оставя оригиналния запис недокоснат. Вашият клиент трябва да третира това като програмна грешка за отстраняване, а не като състояние, което да заобиколи чрез генериране на нов ключ.

Уебхуковете заместват ли четенето на създадения обект?

Не. Доставката на уебхук доказва уведомление надолу по веригата, а не че транзакцията се е записала така, както очаквате, като редът на доставка не е гарантиран. Проверете подписа върху суровото тяло, дедуплицирайте по идентификатор на доставка или събитие и извлечете текущото състояние от API, когато редът на събитията сам по себе си не е достатъчен, за да се реши какво се е променило.

Как да тествам осчетоводяване в заключен счетоводен период?

Създайте еквивалентни документи в отворен период и в заключен такъв и се уверете, че случаят със заключен период е отхвърлен без редове в счетоводната книга, без консумиран номер на документ, без преход на състоянието и без успешен уебхук. След това стартирайте състезателната версия: започнете осчетоводяване срещу отворен период и заключете периода по време на заявката. Записано осчетоводяване никога не трябва да попада в период, който е бил заключен преди транзакцията да се запише.

Мога ли да изпълнявам тестове за дублиране с голям обем в тестова фирма?

Да. Тестовата фирма се държи като реална, обозначена е като тестови данни и се изтрива незабавно, вместо след изтичане на срока за съхранение, така че тя е правилното място за деструктивни тестове и тестове за дублиране. Използването на тестовата среда все пак се отчита и таксува като реално използване, затова планирайте обема, вместо да циклите неопределено време.