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