приём и чтение записей сведены к одному контракту приложения
- адреса приложения переехали в своё пространство `/app/`, опрос готовности убран целиком: рубеж и причину остановки владелец узнаёт карточкой записи, текст — отдельным адресом названного вида - заведена единая точка отображения доменной ошибки и слой, приводящий к той же форме отказы библиотеки: тело несёт машиночитаемый код рядом с сообщением - у записи появились имя файла отправителя, длительность и размер своими колонками, а у ленты владельца — свой индекс: без него страница сканировала весь архив сервиса
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Приём записи по HTTP
|
||||
|
||||
Сервис SHALL принимать запись запросом `POST /app/audiorecords` с телом
|
||||
`multipart/form-data` и полем `audio` **только от узнанного отправителя**.
|
||||
Запрос без сессии MUST получать код `401`, и по нему MUST не заводиться ни файл,
|
||||
ни аудиозапись. Принятая запись от узнанного отправителя MUST быть сохранена и
|
||||
получить заведённую под неё аудиозапись на рубеже `uploaded`.
|
||||
|
||||
Приём стоит тем же адресом, что и список записей, и отличается от него только
|
||||
методом: он **заводит аудиозапись**, а не кладёт файл. Прежнее имя называло
|
||||
содержимое запроса, и по нему приём читался как отдельная от записи вещь — хотя
|
||||
запись он и создаёт.
|
||||
|
||||
Ответ MUST нести **список** заведённых записей и место под признак повторного
|
||||
файла у каждой, даже когда файл в запросе один. Форма согласована один раз и
|
||||
вперёд: приём, отдающий одну запись, пришлось бы переписывать вместе с приёмом
|
||||
нескольких файлов и с распознаванием повтора по содержимому, а экран загрузки —
|
||||
переделывать под вторую форму. Число файлов в запросе при этом остаётся прежним:
|
||||
меняется форма ответа, не число файлов.
|
||||
|
||||
Элемент списка MUST нести те же поля, что и карточка записи, плюс признак
|
||||
повторного файла полем `duplicate`: две формы одной вещи разошлись бы молча.
|
||||
Состав карточки нормирует capability `archive`.
|
||||
|
||||
Прежние имена полей ответа — `job_id` и `status` — MUST не употребляться: адрес
|
||||
опроса убран целиком, и идентификатор записи зовётся `id`. Это объявленная ломка
|
||||
публичного контракта: стадия проекта — стройка, на сервере данных нет, а внешней
|
||||
программы на прежнем контракте не существует — своего токена у неё не было.
|
||||
|
||||
Значение рубежа в ответе MUST принадлежать перечню рубежей конвейера и MUST не
|
||||
перечисляться этой нормой порознь: рубеж объявлен одним дескриптором, и
|
||||
перечисленный здесь второй раз он разошёлся бы с ним молча. Рубеж называет
|
||||
достигнутое, а не предстоящее, и `created` в перечне отсутствует вовсе.
|
||||
|
||||
Запись сверх потолка размера MUST отвергаться до заведения файла и аудиозаписи,
|
||||
и код с телом такого отказа нормирует capability `archive` наравне с прочими
|
||||
ветвями. Потолок применяется уже сегодня, а ответ на его срабатывание —
|
||||
самый частый отказ у человека на мобильной сети — прежде не был нормирован
|
||||
ничем и уходил телом ограничителя тела, мимо единой формы.
|
||||
|
||||
Отказ по отсутствию сессии наступает **раньше** чтения тела: запись, за которую
|
||||
не заплатит узнанный отправитель, не должна попасть даже в память.
|
||||
|
||||
Приём не судит о годности записи сам: расширение он берёт из имени файла, а
|
||||
пригодность содержимого узнаёт у источника метаданных.
|
||||
|
||||
Куда именно ложится принятая запись, приёму не принадлежит: раскладку выбирает
|
||||
хранилище, и нормирует её capability `storage`.
|
||||
|
||||
Владельцем принятой записи приём SHALL назначать предъявителя сессии. Обязательность
|
||||
владельца при этом MUST держаться и схемой хранилища: колонка владельца пустого
|
||||
значения не принимает вовсе, и норму эту держит capability `storage`. Проверка в
|
||||
приёме от этого не лишняя — она отвечает отправителю понятным отказом до того, как
|
||||
запись попадёт в память, а схема отвечала бы отказом сохранения после укладки
|
||||
файла.
|
||||
|
||||
Предъявитель, чья сессия не даёт учётной записи пользователя, MUST получать
|
||||
отказ `403` и MUST получать его **до чтения тела** — там же, где стоит отказ по
|
||||
отсутствию сессии. Сессия владельца панели — именно такой случай: узнан он всё
|
||||
же узнан, а записи в коллекции пользователей у него нет, и владельцем записи он
|
||||
стать не может.
|
||||
|
||||
Код здесь другой, чем у запроса без сессии, и это не оплошность: `401` значит
|
||||
«предъяви себя», а предъявитель себя предъявил. Утечки по разнице кодов нет —
|
||||
оба ответа говорят о самом спрашивающем, а не о том, какие записи заведены.
|
||||
|
||||
Отказ **после** укладки записи потребовал бы убрать уже сохранённый файл, а
|
||||
уборки файлов сервис не умеет вовсе: норма, обязывающая к недостижимому, не
|
||||
пишется.
|
||||
|
||||
#### Scenario: Запись принята
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **AND** отправитель предъявил сессию
|
||||
- **WHEN** программа шлёт `POST /app/audiorecords` с полем `audio`
|
||||
- **THEN** ответ имеет код `201`, а в теле лежит список из одного элемента
|
||||
- **AND** элемент несёт непустой `id`, поле `state` со значением `uploaded` и
|
||||
место под признак повторного файла
|
||||
- **AND** содержимое записи целиком лежит в хранилище одним файлом
|
||||
- **AND** владельцем заведённой аудиозаписи стоит предъявитель сессии
|
||||
|
||||
#### Scenario: Сессия не даёт учётной записи пользователя
|
||||
|
||||
- **GIVEN** предъявлена сессия владельца панели
|
||||
- **WHEN** он шлёт `POST /app/audiorecords` с полем `audio`
|
||||
- **THEN** ответ имеет код `403`
|
||||
- **AND** ни файла, ни аудиозаписи не заводится
|
||||
|
||||
#### Scenario: Сессии нет
|
||||
|
||||
- **WHEN** программа шлёт `POST /app/audiorecords` с полем `audio` без сессии
|
||||
- **THEN** ответ имеет код `401`
|
||||
- **AND** ни файла, ни аудиозаписи не заводится
|
||||
- **AND** тело ответа не несёт данных записи
|
||||
|
||||
#### Scenario: Поля с записью нет
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** программа шлёт `POST /app/audiorecords` без поля `audio`
|
||||
- **THEN** ответ имеет код `400` и сообщение об отсутствии записи
|
||||
- **AND** ни файла, ни аудиозаписи не заводится
|
||||
|
||||
#### Scenario: Размеру записи приём не судья
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **AND** отправитель предъявил сессию
|
||||
- **WHEN** программа шлёт запись нулевой длины
|
||||
- **THEN** ответ имеет код `201`: собственного порога по размеру у приёма нет
|
||||
|
||||
### Requirement: Имя файла в хранилище
|
||||
|
||||
Сервис SHALL сохранять принятую запись под собственным именем — идентификатором,
|
||||
к которому приписано расширение из имени файла отправителя. Имя, данное
|
||||
отправителем, MUST не попадать в **имя файла** в хранилище: оно приходит извне и
|
||||
содержимым своим приёму не подконтрольно.
|
||||
|
||||
Норма сужена: имя отправителя доходит теперь до самой аудиозаписи собственной
|
||||
колонкой — по нему человек узнаёт свою запись, — но не до имени файла и не до
|
||||
журнала. Что с ним делает приём, нормирует требование «Имя файла отправителя
|
||||
подписывает запись».
|
||||
|
||||
Расширения в присланном имени нет — сервис MUST подставить `.audio`, чтобы у
|
||||
файла в хранилище расширение было всегда.
|
||||
|
||||
Требование переживает смену раскладки. Умолчание хранилища, строящее имя из
|
||||
имени отправителя, MUST не применяться: имя отправителя в журнал не пишется по
|
||||
инварианту приватности, а изъятие из него кончается расширением — хвостом после
|
||||
последней точки.
|
||||
|
||||
#### Scenario: Расширение взято из имени отправителя
|
||||
|
||||
- **WHEN** программа шлёт запись с именем `test.mp3`
|
||||
- **THEN** имя файла в хранилище оканчивается на `.mp3`
|
||||
|
||||
#### Scenario: Имени без расширения назначено своё
|
||||
|
||||
- **WHEN** программа шлёт запись с именем `test` без расширения
|
||||
- **THEN** имя файла в хранилище оканчивается на `.audio`
|
||||
|
||||
#### Scenario: Имя отправителя в хранилище не попало
|
||||
|
||||
- **WHEN** программа шлёт запись с именем `секретное-слово.mp3`
|
||||
- **THEN** имя файла в хранилище не содержит `секретное-слово`
|
||||
- **AND** путь к этому файлу не содержит его тоже
|
||||
|
||||
### Requirement: Отказ чтения метаданных
|
||||
|
||||
Сервис SHALL отвечать отказом, когда источник метаданных не смог прочитать
|
||||
принятую запись. Ответ MUST иметь код `400`: причина отказа — присланная запись,
|
||||
а не сбой сервиса, и код, называющий место отказа вместо его причины, не говорит
|
||||
отправителю ничего. Сама причина MUST не попадать в тело ответа: она принадлежит
|
||||
журналу, а не отправителю.
|
||||
|
||||
Отображение этой ошибки в код и сообщение живёт одним местом на все адреса
|
||||
приложения; норму держит capability `archive`.
|
||||
|
||||
#### Scenario: Источник метаданных вернул ошибку
|
||||
|
||||
- **GIVEN** источник метаданных не может прочитать запись
|
||||
- **WHEN** программа шлёт `POST /app/audiorecords` с этой записью
|
||||
- **THEN** ответ имеет код `400` и несёт сообщение, пригодное человеку
|
||||
- **AND** аудиозаписи не заводится
|
||||
|
||||
### Requirement: Имя файла, данное отправителем, не попадает в журнал
|
||||
|
||||
Приём SHALL не писать имя файла, данное отправителем, ни в одну свою журнальную
|
||||
запись — ни на успешном пути, ни на пути отказа, где имя могло бы приехать
|
||||
текстом ошибки. Имя приходит извне вместе с записью и принадлежит содержимому
|
||||
личной переписки наравне с текстом расшифровки; журнал уезжает в собранные логи,
|
||||
откуда строку не убрать.
|
||||
|
||||
Запрет держится, хотя имя доходит теперь до самой записи: колонку записи видит
|
||||
один её владелец, а журнал — владелец сервиса и всякий, кому достались собранные
|
||||
логи.
|
||||
|
||||
Расширение, взятое из этого имени, в журнале остаётся собственным полем: по нему
|
||||
прослеживается путь записи. Что именно попадает в журнал ради прослеживаемости,
|
||||
нормирует требование ниже; наружу расширение выходит только приведённым к
|
||||
известному виду — этому отдано отдельное требование.
|
||||
|
||||
Оговорка про второй вход из требования ушла вместе с ним: имя, данное
|
||||
отправителем, доходит до сервиса единственным путём — приёмом по HTTP, — и
|
||||
сценарии судят именно его.
|
||||
|
||||
#### Scenario: Имя записи не видно в журнале принятой записи
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **WHEN** программа шлёт `POST /app/audiorecords` с записью, чья основа имени
|
||||
несёт опознаваемую строку при обычном расширении `.mp3`
|
||||
- **THEN** ни одна журнальная запись приёма этой строки не содержит
|
||||
- **AND** расширение `.mp3` в журнале допустимо
|
||||
|
||||
#### Scenario: Имя записи не видно в журнале при отказе приёма
|
||||
|
||||
- **GIVEN** источник метаданных не может прочитать запись
|
||||
- **WHEN** программа шлёт `POST /app/audiorecords` с записью, чья основа имени
|
||||
несёт опознаваемую строку
|
||||
- **THEN** ни одна журнальная запись приёма, включая запись об ошибке, этой
|
||||
строки не содержит
|
||||
|
||||
### Requirement: Журнал приёма прослеживает запись
|
||||
|
||||
Приём SHALL писать в журнал идентификатор заведённого файла, расширение принятой
|
||||
записи и её размер в байтах. По ним путь записи собирается отбором по журналу, и
|
||||
удаление имени отправителя прослеживаемости не отнимает.
|
||||
|
||||
Расширение засчитывается собственным полем журнальной строки. Имя, под которым
|
||||
файл лёг в хранилище, приём MUST в журнал не писать: это имя — последняя часть
|
||||
ссылки на скачивание, и записанное вместе с идентификатором записи оно собирает
|
||||
ссылку целиком. Норму держит capability `storage`.
|
||||
|
||||
#### Scenario: Идентификатор, расширение и размер на месте
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **WHEN** программа шлёт `POST /app/audiorecords` с записью
|
||||
- **THEN** журнал приёма несёт идентификатор заведённого файла, расширение
|
||||
принятой записи и её размер в байтах
|
||||
|
||||
#### Scenario: Имени файла в хранилище в журнале нет
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **WHEN** программа шлёт `POST /app/audiorecords` с записью
|
||||
- **THEN** имени, под которым файл лёг в хранилище, в журнале приёма нет
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Имя файла отправителя подписывает запись
|
||||
|
||||
Приём SHALL класть имя файла, данное отправителем, в собственную колонку
|
||||
аудиозаписи и MUST не класть его в колонку заголовка. По имени файла человек
|
||||
узнаёт свою запись до того, как у неё появится заголовок; заголовок же несёт
|
||||
название, которое дал человек либо посчитала языковая модель, и одной колонкой на
|
||||
оба смысла посчитанное название затирало бы то, по чему запись узнают, — а
|
||||
вернуть затёртое было бы неоткуда.
|
||||
|
||||
Колонка заголовка у принятой записи MUST оставаться пустой: приём заголовков не
|
||||
сочиняет.
|
||||
|
||||
Имя приходит извне и содержимым своим приёму не подконтрольно, поэтому приём
|
||||
MUST ограничивать его длину и MUST убирать из него управляющие знаки прежде, чем
|
||||
сохранить. Предел длины и перечень убираемого задаёт сервис, а не отправитель.
|
||||
|
||||
Приложение показывает заголовок, а имя файла подставляет, пока заголовка нет.
|
||||
|
||||
#### Scenario: Имя доходит до записи
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** он шлёт запись с именем `разговор.mp3`
|
||||
- **THEN** колонка имени файла у заведённой записи несёт `разговор.mp3`
|
||||
|
||||
#### Scenario: Заголовок принятой записи пуст
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** он шлёт запись с именем `разговор.mp3`
|
||||
- **THEN** колонка заголовка у заведённой записи пуста
|
||||
|
||||
#### Scenario: Длинное и грязное имя приходит обрезанным и очищенным
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** он шлёт запись, чьё имя длиннее предела и несёт управляющие знаки
|
||||
- **THEN** колонка имени файла несёт имя не длиннее предела
|
||||
- **AND** управляющих знаков в нём нет
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Опрос готовности задачи
|
||||
|
||||
**Reason**: Адрес опроса отвечал сразу на три вопроса — рубеж записи, время её
|
||||
заведения и текст расшифровки, — и держать два адреса на один вопрос не за чем.
|
||||
Карточка записи и её текст читаются теперь порознь: шестичасовая расшифровка
|
||||
иначе задерживает показ шапки записи на мобильной сети. Вместе с адресом уходят
|
||||
имена его полей: `job_id` зовётся `id`.
|
||||
|
||||
**Migration**: Рубеж, время заведения и признак остановки берутся карточкой
|
||||
записи — `GET /app/audiorecords/{id}`, — а текст расшифровки отдельным адресом
|
||||
`GET /app/audiorecords/{id}/text`. Оба нормирует capability `archive`.
|
||||
Переносить нечего: стадия проекта — стройка, данных на сервере нет, а внешней
|
||||
программы на прежнем контракте не существует — своего токена у неё не было.
|
||||
Reference in New Issue
Block a user