ADR 0002 — Нормализация ошибок GitProvider
Контекст
Контрактной suite GitProviderContract (см. 0001) нужны
нормализованные и стабильные ошибки для 8 операций — в частности для 3
сценариев, названных в исходном запросе (репозиторий уже существует, ветка
не существует, доступ запрещён), плюс случай отсутствующей capability, который
никогда не может быть сырым crash.
В остальном коде уже есть соглашение об ошибках домена: каждая ошибка —
отдельный class X extends Error, с явно заданным this.name
и типизированными полями контекста (никогда общий enum code)
— см. apps/api/src/domain/git/git-provider-errors.ts
(GitProviderAuthError, InvalidOauthStateError, сосредоточенные на OAuth) и
ошибки машин состояний сессии/действия.
Решение
Без общего базового класса. Рассматривалось введение абстрактного GitError extends Error, к которому сошлись бы 6 новых классов (облегчило бы будущий
единый @Catch(GitError)), и отвергнуто: в этой сессии не регистрируется
ни одного нового HTTP-фильтра (ни один endpoint пока не открывает 8 операций —
это пункты 4-6 Фазы 2, будущие), так что общая база не имеет
немедленного применения и выбивалась бы из соглашения "без базы", уже установленного
во всём остальном домене, без конкретной причины сейчас.
Шесть отдельных классов в apps/api/src/domain/git/git-errors.ts (новый
файл, отдельный от git-provider-errors.ts для OAuth):
GitRepoAlreadyExistsError(repoId)GitRepoNotFoundError(repoId)GitBranchNotFoundError(repoId, ref)GitBranchAlreadyExistsError(repoId, branchName)— получается даром из семантики compare-and-swapgit update-ref, используемой вcreateBranch; не входил в исходный список из 3 сценариев, но это естественное поведение отказа в перезаписи существующей ветки.GitPermissionDeniedError(path)GitNotSupportedError(provider, operation)
Тест отказа в доступе и контейнеры, работающие от root. Dev-контейнеры
api работают от root
(docker/api/Dockerfile, без USER). Root игнорирует проверку
прав Unix (DAC), поэтому тест, который делает chmod(dir, 0o000) и
ожидает EACCES, ничего реального не воспроизводит при работе от root — тест
"прошёл" бы, не задействовав ни одного кода обработки ошибок, что
хуже, чем отсутствие теста. Рассматривалось (и отложено как непропорциональное для
области этой сессии) использование опций uid/gid из child_process
Node, чтобы намеренно сбросить привилегии перед попыткой операции, —
это потребовало бы предполагать конкретный непривилегированный uid (nobody или
подобный), присутствующий в каждом окружении, где запускается suite, и добавило бы
путь переопределения идентичности лишь ради тестов. Решение: suite
обнаруживает process.getuid?.() === 0 и пропускает (it.skipIf) тест
отказа в доступе при работе от root, с явным комментарием
в коде — никогда не притворяется, что прошёл.
Последствия
- В этой сессии не добавляется ни одного HTTP-фильтра (
@Catch(...)) — это остаётся на тот момент, когда будущая сессия откроет 8 операций через endpoint (пункты 4-6 Фазы 2). До тех пор эти 6 классов циркулируют лишь внутри процесса api (прямые вызовы provider, тесты). - В окружениях, где тесты запускаются не от root (напр.: CI,
настроенный без root, или будущий образ dev с не-root
USER), тест отказа в доступе начинает действительно задействоваться — никаких изменений кода для этого не нужно, только окружение.