у записи появился владелец: чужую больше не отдают
- колонка `owner` связью с `users` в обеих коллекциях новым шагом схемы `202608140001`; чтение задачи сужено владельцем, и чужая, ничья и несуществующая дают один ответ; правило просмотра файлов сужено им же - приём по HTTP берёт владельца из сессии, а предъявителя без учётной записи пользователя отвергает до чтения тела: позже пришлось бы убирать уложенный файл, а уборки файлов сервис не умеет. Выборка воркера владельцем не сужается - удаление учётной записи с записями отвергается стражем, и вешает его сама сборка хранилища: сборка, забывшая его позвать, теряла защиту молча
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: У записи есть владелец, и чужую ей не отдают
|
||||
|
||||
Сервис SHALL заводить у каждой записи, принятой **по HTTP**, — владельца, то
|
||||
есть учётную запись, от имени которой запись принята, — и MUST отдавать данные
|
||||
такой записи только её владельцу. Владелец назначается один раз, при приёме, и
|
||||
MUST не меняться у записи, у которой владелец есть: совместного доступа, ролей и
|
||||
передачи записи другому сервис не знает. Оговорка не случайна — назначить
|
||||
владельца записи, у которой его нет, вправе задача, заводящая связь чата
|
||||
Telegram с учётной записью.
|
||||
|
||||
Владелец MUST браться из предъявленной сессии и ниоткуда больше. Владелец,
|
||||
пришедший полем запроса, дал бы всякому вошедшему право завести запись на чужое
|
||||
имя.
|
||||
|
||||
Обращение к чужой записи MUST быть неотличимо от обращения к несуществующей.
|
||||
Отдельный отказ «доступ запрещён» превращает опрос в перебор — по разнице
|
||||
ответов считывается, какие записи заведены, а идентификатор записи и есть то,
|
||||
что разграничение прячет. Каким именно ответом это выражено, нормирует
|
||||
capability `intake`: там живёт адрес опроса, и держатель нормы обязан быть один.
|
||||
|
||||
Пустой владелец MUST не совпадать ни с одной записью — ни со своей, ни с чужой,
|
||||
ни с ничьей. Правило записано со стороны **спрашивающего**, а не со стороны
|
||||
записи: обязательность владельца, которую держит одна лишь подпись метода, пустую
|
||||
строку пропускает, и первый же вызывающий без учётной записи получил бы ровно
|
||||
множество записей без владельца, то есть все записи бота.
|
||||
|
||||
Записи, принятые из Telegram, владельца не имеют: связи чата с учётной записью
|
||||
приложения сервис не ведёт. Такая запись MUST не доставаться по API никому —
|
||||
ответ на неё тот же, что и на несуществующую, — а её расшифровка уезжает
|
||||
отправителю в чат, как и прежде.
|
||||
|
||||
#### Scenario: Своя запись доступна
|
||||
|
||||
- **GIVEN** человек вошёл и принял запись
|
||||
- **WHEN** он спрашивает состояние этой записи своей сессией
|
||||
- **THEN** ответ несёт состояние записи
|
||||
|
||||
#### Scenario: Чужая запись неотличима от несуществующей
|
||||
|
||||
- **GIVEN** запись принята одним вошедшим
|
||||
- **WHEN** её состояние спрашивает другой вошедший
|
||||
- **THEN** ответ тот же, что и на неизвестный идентификатор, — и кодом, и телом
|
||||
|
||||
#### Scenario: Владельца не задают запросом
|
||||
|
||||
- **WHEN** запрос на приём записи несёт своё значение владельца
|
||||
- **THEN** владельцем принятой записи становится предъявитель сессии
|
||||
|
||||
#### Scenario: Запись из Telegram не достаётся по API
|
||||
|
||||
- **GIVEN** запись принята ботом
|
||||
- **WHEN** её состояние спрашивает вошедший человек
|
||||
- **THEN** ответ тот же, что и на неизвестный идентификатор
|
||||
|
||||
#### Scenario: Пустой владелец не открывает ничего
|
||||
|
||||
- **GIVEN** заведены три задачи: своя, чужая и принятая ботом
|
||||
- **WHEN** состояние каждой спрашивают с пустым владельцем
|
||||
- **THEN** ответ на все три тот же, что и на неизвестный идентификатор
|
||||
@@ -0,0 +1,136 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Приём записи по HTTP
|
||||
|
||||
Сервис SHALL принимать запись от внешней программы запросом `POST /api/audio` с
|
||||
телом `multipart/form-data` и полем `audio` **только от узнанного отправителя**.
|
||||
Запрос без сессии MUST получать код `401`, и по нему MUST не заводиться ни файл,
|
||||
ни задача расшифровки. Принятая запись от узнанного отправителя MUST быть
|
||||
сохранена и получить заведённую под неё задачу расшифровки в состоянии
|
||||
`created`; ответ MUST нести идентификатор задачи полем `job_id` и её состояние
|
||||
полем `status`.
|
||||
|
||||
Отказ по отсутствию сессии наступает **раньше** чтения тела: запись, за которую
|
||||
не заплатит узнанный отправитель, не должна попасть даже в память.
|
||||
|
||||
Имена полей ответа нормативны: контракт HTTP API объявлен проектом необратимым,
|
||||
и переименование поля ломает внешнюю программу молча. Появление отказа без
|
||||
сессии — намеренная ломка этого контракта: до неё приём стоял открытым наружу.
|
||||
|
||||
Приём не судит о годности записи сам: расширение он берёт из имени файла, а
|
||||
пригодность содержимого узнаёт у источника метаданных.
|
||||
|
||||
Куда именно ложится принятая запись, приёму не принадлежит: раскладку выбирает
|
||||
хранилище, и нормирует её capability `storage`.
|
||||
|
||||
Владельцем принятой записи приём SHALL назначать предъявителя сессии. Проверка
|
||||
стоит здесь, а не только в схеме хранилища: колонка владельца допускает пустое
|
||||
значение ради записей из Telegram, и приём по HTTP — то место, где
|
||||
обязательность держится.
|
||||
|
||||
Предъявитель, чья сессия не даёт учётной записи пользователя, MUST получать
|
||||
отказ `403` и MUST получать его **до чтения тела** — там же, где стоит отказ по
|
||||
отсутствию сессии. Сессия владельца панели — именно такой случай: узнан он всё
|
||||
же узнан, а записи в коллекции пользователей у него нет, и владельцем записи он
|
||||
стать не может.
|
||||
|
||||
Код здесь другой, чем у запроса без сессии, и это не оплошность: `401` значит
|
||||
«предъяви себя», а предъявитель себя предъявил. Утечки по разнице кодов нет —
|
||||
оба ответа говорят о самом спрашивающем, а не о том, какие записи заведены.
|
||||
|
||||
Отказ **после** укладки записи потребовал бы убрать уже сохранённый файл, а
|
||||
уборки файлов сервис не умеет вовсе: норма, обязывающая к недостижимому, не
|
||||
пишется.
|
||||
|
||||
#### Scenario: Запись принята
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **AND** отправитель предъявил сессию
|
||||
- **WHEN** программа шлёт `POST /api/audio` с полем `audio`
|
||||
- **THEN** ответ имеет код `201`, а в теле лежат непустой `job_id` и `status`
|
||||
со значением `created`
|
||||
- **AND** содержимое записи целиком лежит в хранилище одним файлом
|
||||
- **AND** владельцем заведённой задачи стоит предъявитель сессии
|
||||
|
||||
#### Scenario: Сессия не даёт учётной записи пользователя
|
||||
|
||||
- **GIVEN** предъявлена сессия владельца панели
|
||||
- **WHEN** он шлёт `POST /api/audio` с полем `audio`
|
||||
- **THEN** ответ имеет код `403`
|
||||
- **AND** ни файла, ни задачи не заводится
|
||||
|
||||
#### Scenario: Сессии нет
|
||||
|
||||
- **WHEN** программа шлёт `POST /api/audio` с полем `audio` без сессии
|
||||
- **THEN** ответ имеет код `401`
|
||||
- **AND** ни файла, ни задачи не заводится
|
||||
- **AND** тело ответа не несёт данных задачи
|
||||
|
||||
#### Scenario: Поля с записью нет
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** программа шлёт `POST /api/audio` без поля `audio`
|
||||
- **THEN** ответ имеет код `400` и сообщение об отсутствии записи
|
||||
- **AND** ни файла, ни задачи не заводится
|
||||
|
||||
#### Scenario: Размеру записи приём не судья
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **AND** отправитель предъявил сессию
|
||||
- **WHEN** программа шлёт запись нулевой длины
|
||||
- **THEN** ответ имеет код `201`: собственного порога по размеру у приёма нет
|
||||
|
||||
### Requirement: Опрос готовности задачи
|
||||
|
||||
Сервис SHALL отдавать состояние задачи расшифровки по запросу
|
||||
`GET /api/status/:id` **только её владельцу**. Запрос без сессии MUST получать
|
||||
код `401`, и тело такого ответа MUST не нести ни состояния задачи, ни текста
|
||||
расшифровки. Ответ владельцу MUST нести идентификатор полем `job_id`, состояние
|
||||
полем `status` и время заведения полем `created_at`, а текст расшифровки полем
|
||||
`transcription_text`, и это поле MUST отсутствовать в ответе, пока текста нет:
|
||||
пустая строка на месте отсутствующего текста читается как «расшифровка пуста».
|
||||
|
||||
Отказ без сессии MUST не зависеть от того, есть такая задача или нет: иначе по
|
||||
кодам ответа перебирается список заведённых задач.
|
||||
|
||||
Задача, принадлежащая другому, MUST отвечать тем же, чем отвечает неизвестный
|
||||
идентификатор, — кодом `404` и тем же телом. То же MUST относиться к задаче без
|
||||
владельца: запись, принятая ботом, по этому адресу не достаётся никому.
|
||||
|
||||
#### Scenario: Задача найдена
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** он спрашивает состояние своей задачи
|
||||
- **THEN** ответ имеет код `200` и несёт `job_id`, `status` и `created_at`
|
||||
|
||||
#### Scenario: Сессии нет
|
||||
|
||||
- **WHEN** программа спрашивает состояние заведённой задачи без сессии
|
||||
- **THEN** ответ имеет код `401`
|
||||
- **AND** тело ответа не несёт ни состояния задачи, ни текста расшифровки
|
||||
|
||||
#### Scenario: Без сессии неизвестная задача неотличима от заведённой
|
||||
|
||||
- **WHEN** программа без сессии спрашивает состояние заведённой задачи, а затем
|
||||
состояние по неизвестному идентификатору
|
||||
- **THEN** оба ответа имеют код `401`
|
||||
|
||||
#### Scenario: Чужая задача неотличима от неизвестной
|
||||
|
||||
- **GIVEN** задача заведена одним вошедшим
|
||||
- **WHEN** её состояние спрашивает другой вошедший
|
||||
- **THEN** ответ имеет код `404` и то же тело, что и ответ по неизвестному
|
||||
идентификатору
|
||||
- **AND** тело ответа не несёт ни состояния задачи, ни текста расшифровки
|
||||
|
||||
#### Scenario: Расшифровки ещё нет
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** он спрашивает состояние своей задачи, которая ещё не дошла до текста
|
||||
- **THEN** поля `transcription_text` в ответе нет вовсе
|
||||
|
||||
#### Scenario: Задачи с таким идентификатором нет
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** программа спрашивает состояние по неизвестному идентификатору
|
||||
- **THEN** ответ имеет код `404` и сообщение о ненайденной задаче
|
||||
@@ -0,0 +1,32 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Выборка воркера владельцем не сужается
|
||||
|
||||
Воркер SHALL брать задачи всех владельцев подряд и MUST не учитывать владельца
|
||||
при выборе очередной задачи. Задача без владельца — принятая ботом — MUST
|
||||
обрабатываться наравне с прочими.
|
||||
|
||||
Владелец решает, кому запись показывать, а не кому её считать. Сужение выборки
|
||||
владельцем остановило бы расшифровку записей бота вовсе, а записи остальных
|
||||
поставило бы в зависимость от того, кто первым завёл учётную запись.
|
||||
|
||||
Владелец задачи MUST переживать работу конвейера: шаг, сохраняющий свой
|
||||
результат, владельца не трогает и не затирает.
|
||||
|
||||
#### Scenario: Задачи двух владельцев проходят одним воркером
|
||||
|
||||
- **GIVEN** заведены задачи двух разных владельцев в одном состоянии
|
||||
- **WHEN** воркер забирает задачи этого состояния
|
||||
- **THEN** ему достаются обе, в порядке заведения
|
||||
|
||||
#### Scenario: Задача без владельца обрабатывается
|
||||
|
||||
- **GIVEN** заведена задача, принятая ботом, — без владельца
|
||||
- **WHEN** воркер забирает задачи её состояния
|
||||
- **THEN** она достаётся ему наравне с прочими
|
||||
|
||||
#### Scenario: Шаг конвейера владельца не затирает
|
||||
|
||||
- **GIVEN** задача с владельцем прошла шаг конвейера
|
||||
- **WHEN** шаг сохраняет свой результат
|
||||
- **THEN** владелец задачи остаётся прежним
|
||||
@@ -0,0 +1,199 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Владелец задачи лежит связью с учётной записью
|
||||
|
||||
Хранилище SHALL держать владельца задачи расшифровки отдельной колонкой — связью
|
||||
с учётной записью, — и эта колонка MUST не иметь умолчания: запись, чей владелец
|
||||
не назван, не достаётся никому по недосмотру схемы.
|
||||
|
||||
Колонка MUST допускать пустое значение, и это решение с названной ценой: записи,
|
||||
принятые ботом, владельца не имеют, потому что связи чата Telegram с учётной
|
||||
записью сервис не ведёт. Обязательность для приёма по HTTP держит сама
|
||||
capability `intake`, а не схема.
|
||||
|
||||
Колонка приезжает **новым шагом схемы**: применённый шаг не переписывается.
|
||||
Записей, заведённых до этого шага, сервис не переносит — проект заводится с
|
||||
чистого листа.
|
||||
|
||||
#### Scenario: Колонка появляется на пустой базе
|
||||
|
||||
- **WHEN** сервис поднимается на чистом каталоге данных
|
||||
- **THEN** у таблицы задач есть колонка владельца
|
||||
- **AND** умолчания у неё нет
|
||||
|
||||
### Requirement: Файл записи сужается владельцем наравне с задачей
|
||||
|
||||
Хранилище SHALL держать владельца и у файла записи — той же связью с учётной
|
||||
записью, тем же шагом схемы, — и правило просмотра файлов MUST пускать к файлу
|
||||
только его владельца. Прежнее правило пускало всякого узнанного, и знание
|
||||
идентификатора файловой записи равнялось праву скачать чужое аудио.
|
||||
|
||||
Без этого требования разграничение закрывает метаданные задачи и оставляет
|
||||
открытым содержимое — то самое, что оно и заведено прятать. Хуже самой дыры была
|
||||
бы отметка о закрытии: паспорт и модель угроз называют исполнителем этой работы
|
||||
именно эту задачу, и слово «закрыто» скрыло бы открытый путь.
|
||||
|
||||
Владелец файла MUST назначаться там же, где владелец задачи, — при приёме, из
|
||||
предъявленной сессии, — и MUST оставаться пустым у файлов, заведённых конвейером
|
||||
для записи без владельца.
|
||||
|
||||
Ссылка на файл в задаче переставляется каждым шагом конвейера, поэтому владелец
|
||||
файла MUST лежать своей колонкой, а не выводиться через задачу: исходная копия
|
||||
после конвертации не связана с задачей ничем.
|
||||
|
||||
Отказ наступает **на переходе по ссылке**, а не на выдаче токена файла: токен
|
||||
хранилище выдаёт на предъявителя, а не на файл, и о файле при выдаче не
|
||||
спрашивает вовсе. Требовать отказа при выдаче значит требовать механизма,
|
||||
которого нет, — а проверка, написанная под такое требование, зеленела бы, не
|
||||
касаясь пути, по которому аудио и уходит.
|
||||
|
||||
#### Scenario: Чужой файл не отдаётся
|
||||
|
||||
- **GIVEN** запись принята одним вошедшим
|
||||
- **WHEN** другой вошедший идёт по ссылке на файл этой записи со своим токеном
|
||||
- **THEN** содержимого он не получает
|
||||
|
||||
#### Scenario: Свой файл отдаётся
|
||||
|
||||
- **GIVEN** человек принял запись
|
||||
- **WHEN** он идёт по ссылке на файл своей записи со своим токеном
|
||||
- **THEN** содержимое отдаётся
|
||||
|
||||
#### Scenario: Файл записи из Telegram не отдаётся по API
|
||||
|
||||
- **GIVEN** запись принята ботом, и владельца у неё нет
|
||||
- **WHEN** вошедший человек идёт по ссылке на её файл со своим токеном
|
||||
- **THEN** содержимого он не получает
|
||||
|
||||
### Requirement: Учётная запись с записями не удаляется
|
||||
|
||||
Хранилище SHALL отвергать удаление учётной записи, у которой остались задачи
|
||||
расшифровки **либо файлы**. Отказ MUST называть причину, и MUST доезжать до
|
||||
спрашивающего: хранилище пропускает наружу только свою ошибку роутера, а всякую
|
||||
другую подменяет сообщением про обязательную связь — подсказкой, по которой
|
||||
владелец панели пойдёт удалять записи руками.
|
||||
|
||||
Считаются обе коллекции с владельцем. Файл переживает свою задачу: шаг конвейера
|
||||
заводит его до сохранения задачи, и потерянный захват оставляет файл с владельцем
|
||||
и без ссылки.
|
||||
|
||||
Запрет MUST ставить сама сборка хранилища, а не вызывающий: сборка, забывшая его
|
||||
позвать, теряет защиту молча — и теряла, пока запрет вешался отдельной строкой
|
||||
запуска, а окружение проверок его не ставило вовсе.
|
||||
|
||||
Удаление при этом не только панельное: умолчание библиотеки разрешает вошедшему
|
||||
удалить **свою** учётную запись запросом, так что запрет закрывает и публичную
|
||||
поверхность.
|
||||
|
||||
Требование заведено вместо прежнего «удаление не уносит задачи следом»: оно
|
||||
выглядело выполненным, а на деле хранилище при выключенном каскаде **снимает
|
||||
ссылку** — задачи остаются, но становятся ничьими, а ничья задача не достаётся
|
||||
по API никому. Архив человека исчезал бы молча и восстановлению не подлежал:
|
||||
прежнего владельца не остаётся нигде.
|
||||
|
||||
Цена требования названа прямо: владелец панели упирается в отказ, а способа
|
||||
удалить записи в сервисе пока нет вовсе — его приносит задача про удаление
|
||||
записи. До неё удаление учётной записи с записями невозможно, и это осознанный
|
||||
тупик, а не недосмотр.
|
||||
|
||||
#### Scenario: Удаление учётной записи с записями отвергается
|
||||
|
||||
- **GIVEN** у учётной записи есть задачи расшифровки
|
||||
- **WHEN** её удаляют
|
||||
- **THEN** удаление не проходит, а отказ называет причину
|
||||
- **AND** задачи и их владелец остаются прежними
|
||||
|
||||
#### Scenario: Учётная запись с одними файлами тоже не удаляется
|
||||
|
||||
- **GIVEN** у учётной записи остались файлы, но задач нет
|
||||
- **WHEN** её удаляют
|
||||
- **THEN** удаление не проходит, а владелец файлов остаётся прежним
|
||||
|
||||
#### Scenario: Учётная запись без записей удаляется
|
||||
|
||||
- **GIVEN** у учётной записи нет ни задач, ни файлов
|
||||
- **WHEN** её удаляют
|
||||
- **THEN** удаление проходит
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Файл отдаётся ссылкой
|
||||
|
||||
Сервис SHALL отдавать файл записи ссылкой, которую строит хранилище по самой
|
||||
записи, **и только узнанному отправителю**. Поле файла MUST быть помечено
|
||||
защищённым: без этого ссылка открывает запись любому, кто её знает, и знание
|
||||
ссылки становится правом. Отданный файл MUST совпадать с принятым по длине.
|
||||
|
||||
Одной пометки мало: защищённый файл судится **коротким токеном файла**, который
|
||||
узнанный отправитель берёт у хранилища, предъявив сессию, — и правилом просмотра
|
||||
коллекции. Правило MUST пускать только владельца файла: незаданное означает
|
||||
«только владелец панели», и тогда файла не получит и вошедший, а прежнее «всякий
|
||||
узнанный» отдавало чужое аудио тому, кто знает идентификатор записи.
|
||||
|
||||
Токен файла хранилище выдаёт **на предъявителя**, а не на файл, и о файле при
|
||||
выдаче не спрашивает. Значит владельца судит переход по ссылке, а не выдача
|
||||
токена: отказ наступает там, и требовать его от выдачи значит требовать
|
||||
механизма, которого нет.
|
||||
|
||||
Отсюда порядок для потребителя: сессия → токен файла → ссылка с этим токеном.
|
||||
Браузер с одной лишь кукой файла не получит, и это свойство хранилища, а не
|
||||
недосмотр.
|
||||
|
||||
Конвейер расшифровки этим не затронут: он читает файл из файловой системы
|
||||
хранилища, а не по ссылке.
|
||||
|
||||
Ссылка на несуществующую запись MUST отвечать отказом, а не пустым файлом.
|
||||
|
||||
**Ссылка сама по себе и есть право пройти по ней**, и потому она MUST не попадать
|
||||
ни в журнал, ни в метку метрики, ни в ответ отправителю. Имя, под которым файл
|
||||
лёг в хранилище, из журнала выводимо быть не должно: журнал уезжает в собранные
|
||||
логи, откуда строку не убрать, и оттуда ссылка на чужую запись работала бы
|
||||
бессрочно.
|
||||
|
||||
Защищённое поле сужает это право, но не отменяет запрета: право пройти теперь
|
||||
требует ещё и сессии, а строка журнала со ссылкой по-прежнему собирала бы
|
||||
половину ключа.
|
||||
|
||||
Отсюда требование к отказам: сообщение об отказе хранилища MUST не выходить за
|
||||
пределы хранилища дословно. Отказ чтения и отказ укладки называют ключ файла
|
||||
целиком, а отказ выгрузки во внешнее хранилище — полный адрес объекта; и то и
|
||||
другое кончается в журнале и собирает ссылку не хуже успешного пути.
|
||||
|
||||
Что именно журнал приёма пишет ради прослеживаемости, нормирует capability
|
||||
`intake`.
|
||||
|
||||
#### Scenario: Файл забирают по ссылке
|
||||
|
||||
- **GIVEN** запись принята и её файл лежит в хранилище
|
||||
- **AND** забирающий предъявил сессию и взял по ней токен файла
|
||||
- **WHEN** ссылку на файл запрашивают с этим токеном
|
||||
- **THEN** приходит тот же файл, и его длина совпадает с длиной принятого
|
||||
|
||||
#### Scenario: Без сессии файл не отдаётся
|
||||
|
||||
- **GIVEN** запись принята и её файл лежит в хранилище
|
||||
- **WHEN** ссылку на файл запрашивают без сессии
|
||||
- **THEN** приходит отказ, а содержимого записи в ответе нет
|
||||
|
||||
#### Scenario: Конвейер читает файл без сессии
|
||||
|
||||
- **GIVEN** запись принята и ждёт расшифровки
|
||||
- **WHEN** шаг конвейера берётся за неё
|
||||
- **THEN** файл читается из файловой системы хранилища и шаг проходит
|
||||
|
||||
#### Scenario: Ссылка ведёт в никуда
|
||||
|
||||
- **WHEN** запрашивают ссылку на запись, которой нет
|
||||
- **THEN** приходит отказ, а не пустой ответ
|
||||
|
||||
#### Scenario: По журналу ссылку не собрать
|
||||
|
||||
- **GIVEN** запись принята и прошла конвейер
|
||||
- **WHEN** читают журнал сервиса целиком
|
||||
- **THEN** имени, под которым файл лёг в хранилище, в нём нет
|
||||
|
||||
#### Scenario: Отказ чтения файла не называет его ключ
|
||||
|
||||
- **GIVEN** файл записи не читается из хранилища
|
||||
- **WHEN** шаг конвейера берётся за эту запись и отказывает
|
||||
- **THEN** отказ называет запись её идентификатором и не несёт имени файла
|
||||
Reference in New Issue
Block a user