## 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`. Переносить нечего: стадия проекта — стройка, данных на сервере нет, а внешней программы на прежнем контракте не существует — своего токена у неё не было.