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