Перейти к содержанию

Cookbook — Запустить тесты Payment Manager

Summary. Как запускать тесты PM локально: полный прогон, фильтрация по файлу/имени, watch-режим, integration, lint. Раскладка test/, шаблон нового теста, известные подсказки по troubleshooting.


Что делаем

Запускаем тесты Payment Manager. Тест-runner — Vitest (vitest run под капотом npm test). Конфиг — vitest.config.ts (корень проекта). Все тесты исполняются с фиксированным набором env-переменных (NODE_ENV=test, тестовые DATABASE_URL/REDIS_URL/TB_ADDRESS, тестовые SERVICE_SECRETS).

Тесты делятся на:

  • Юнит-тесты — изолированно проверяют функцию/хендлер с заглушками (test/<module>/<file>.test.ts).
  • Интеграционные — поднимают Fastify app через buildTestApp() и реальные сервисы (PostgreSQL/Redis/TigerBeetle) в Docker — в каталоге test/integration/.

Предусловия

  • Установлены зависимости: npm install.
  • Для integration тестов в фоне подняты PostgreSQL, Redis и TigerBeetle (см. ./run-locally.md). Юнит-тесты, использующие моки, можно запускать без сервисов — но любой тест, открывающий реальный коннект, упадёт без них.
  • Локальная БД test доступна по postgres://test:test@localhost:5432/test (значение из vitest.config.ts).

Шаги

1. Все тесты разом

npm test

Под капотом — vitest run (один проход, без watch). Это то, что гоняет CI. Используй перед коммитом / перед PR.

2. Фильтр по файлу

npx vitest run test/intent/confirm-handler.test.ts

Можно указать несколько путей через пробел или glob:

npx vitest run test/intent/
npx vitest run "test/limits/**/*.test.ts"

3. Фильтр по имени теста (-t)

npx vitest run -t "confirm should mark intent as SETTLED"

Полезно, когда нужен один конкретный сценарий из большого файла.

4. Watch mode

npm run test:watch

Эквивалентно npx vitest (без run). Перезапускает только затронутые тесты при сохранении файла. Внутри REPL: p — фильтр по пути, t — по имени, a — прогнать всё, q — выйти.

5. Coverage

В package.json отдельного скрипта нет, но установлен @vitest/coverage-v8 — можно запустить напрямую:

npx vitest run --coverage

Отчёт ложится в ./coverage/. Открыть coverage/index.html в браузере.

6. Lint (бонус)

Отдельного lint-скрипта в package.json сейчас нет; TypeScript-проверка делается через npm run build (т.е. tsc). Если в CI собирается — добавляй типовые ошибки в чек-лист перед PR:

npx tsc --noEmit

Раскладка test/

test/
├── helpers/                  # общие билдеры: buildTestApp(), signRequest(), seedDB()
│   ├── app.ts                # buildTestApp() + HMAC helper для интеграционных тестов
│   └── db.ts                 # фабрики для вставки тестовых данных
├── mocks/                    # in-memory моки внешних систем
│   └── ipps-mock-server.ts   # фейковый IPPS, поднимается внутри тестов
├── integration/              # интеграционные тесты на реальном app + сервисах
│   ├── invoice-flow.test.ts
│   └── notifications.test.ts
├── intent/                   # юнит-тесты intent-pipeline (handler, confirm, cancel, saga)
├── limits/                   # лимит-engine + policy evaluator
├── policies/                 # step-up policy routes/CRUD
├── rule-engine/              # fee/route rules
├── psp/                      # IPPS driver, psp-worker
├── ledger/                   # TigerBeetle wrappers, settlement-writer
├── channels/                 # single/two-phase каналы (INTERNAL_P2P, MERCHANT_INVOICE …)
├── jobs/                     # OutboxWorker, expirator, reconciler
├── workers/                  # фоновые воркеры (notify, saga-runner)
├── accounts/                 # резолвер аккаунтов, tb_account_map
├── operation-types/          # operation-registry, parseBody/resolveAccounts
├── admin/                    # admin-маршруты
├── shared/                   # утилиты (request-context, idempotency и т.п.)
├── hmac.test.ts              # глобальная проверка HMAC middleware
├── health.test.ts            # /healthz, /readyz
├── config.test.ts            # загрузка env / валидация
└── adminPlugin.test.ts       # admin-плагин Fastify

Правила именования:

  • Файл: <тестируемая-сущность>.test.ts. Не .spec.ts.
  • Папка совпадает с модулем в src/. Если такого модуля нет — стоп, скорее всего тест неуместен в этом каталоге.
  • Helpers — только в test/helpers/. Никаких re-export-цепочек из других тестов.
  • Моки внешних адаптеров (IPPS, QP, Notifications) — в test/mocks/.

Как добавить новый тест

Пример: пишем юнит-тест для функции validateInvoiceAmount из src/invoice/validate.ts.

  1. Создай файл test/invoice/validate.test.ts (папку invoice/ создай, если её нет).
  2. Минимальный скелет:
/**
 * Юнит-тесты validateInvoiceAmount: проверяем граничные значения min/max
 * и реакцию на отрицательные суммы.
 */
import { describe, it, expect } from 'vitest'
import { validateInvoiceAmount } from '../../src/invoice/validate.js'

describe('validateInvoiceAmount', () => {
  it('принимает сумму в допустимом диапазоне', () => {
    expect(() => validateInvoiceAmount(10000n)).not.toThrow()
  })

  it('отбрасывает отрицательную сумму', () => {
    expect(() => validateInvoiceAmount(-1n)).toThrow(/non-negative/)
  })
})
  1. Для интеграционного теста подними app через helper:
import { buildTestApp, signRequest } from '../helpers/app.js'

const app = await buildTestApp()
const res = await app.inject({
  method: 'POST',
  url: '/intents',
  headers: signRequest(body),
  payload: body,
})
expect(res.statusCode).toBe(200)
  1. Прогон одного файла:
npx vitest run test/invoice/validate.test.ts
  1. Перед PR — полный прогон npm test (CI делает то же самое).

Troubleshooting и известные подсказки

  • ECONNREFUSED 127.0.0.1:5432 / :6379 / :3000 — не подняты Postgres / Redis / TigerBeetle. Запусти локальный стек (см. ./run-locally.md) либо запускай только юнит-тесты конкретного модуля без реальных коннектов.
  • HMAC 401 invalid signature в интеграционных тестах — проверь, что используешь signRequest() из test/helpers/app.ts, а не самописную подпись. Секреты заданы в vitest.config.ts (поле SERVICE_SECRETS) — это тестовые значения, в проде они другие.
  • tx_history остался от прошлого прогона — интеграционные тесты должны чистить state в beforeEach через хелперы из test/helpers/db.ts. Если флак — добавь truncate соответствующих pm.* таблиц.
  • Висит навсегда — обычно забыт await app.close() после buildTestApp(). Vitest ждёт закрытия открытых хендлов.
  • Тест проходит локально, падает в CI — проверь, что не зависишь от текущего времени без vi.useFakeTimers() и от порядка тестов в файле.
  • Канал в тестах должен называться ADMIN (для админ-операций), а не выдуманные синонимы — это инвариант passport, не меняй его «по вкусу».

Связанные документы

  • ../testing/overview.md — стратегия тестирования, что юнит / что интеграция.
  • ./run-locally.md — поднять Postgres + Redis + TigerBeetle локально.
  • ./add-operation-type.md — чек-лист тестов при добавлении operationType.
  • PASSPORT.md — контрактные инварианты, которые тесты обязаны защищать.