- убраны клиент бота, транспорт обновлений, отправитель сообщений, сборка входа при старте, секция настроек и зависимость go-telegram-bot-api; из конвейера ушла доставка ответа отправителю — исход виден опросом готовности. Колонки адресата и значение источника остались в схеме: применённые шаги не переписываются - шаг 202608140003 запрещает пустого владельца у аудиозаписи и у файла; существующие строки он не проверяет, и это принято сознательно — искать их надо запросом до выкладки - ревью нашло два пред-существующих дефекта, оба закрыты: пустой второй ответ распознавателя стирал сохранённую расшифровку, а пустая расшифровка перестала быть заметной вместе с убранной доставкой. Попутно поднят golang.org/x/image до v0.45.0 — красный шаг vulns, воспроизводился и на чистом master
19 KiB
MODIFIED Requirements
Requirement: Приём записи по HTTP
Сервис SHALL принимать запись от внешней программы запросом POST /api/audio с
телом multipart/form-data и полем audio только от узнанного отправителя.
Запрос без сессии MUST получать код 401, и по нему MUST не заводиться ни файл,
ни аудиозапись. Принятая запись от узнанного отправителя MUST быть сохранена и
получить заведённую под неё аудиозапись на рубеже uploaded; ответ MUST нести
идентификатор записи полем job_id и её рубеж полем status.
Значение рубежа в ответе изменилось: прежде приём отдавал created. Перечень
состояний назван проектом необратимым, и ломка объявлена прямо — состояние
теперь называет достигнутое, а не предстоящее, и created в новом перечне нет
вовсе.
Имена полей ответа нормативны и MUST остаться прежними: контракт HTTP API объявлен проектом необратимым, и переименование поля ломает внешнюю программу молча. Меняются значения поля рубежа, а не его имя.
Отказ по отсутствию сессии наступает раньше чтения тела: запись, за которую не заплатит узнанный отправитель, не должна попасть даже в память.
Приём не судит о годности записи сам: расширение он берёт из имени файла, а пригодность содержимого узнаёт у источника метаданных.
Куда именно ложится принятая запись, приёму не принадлежит: раскладку выбирает
хранилище, и нормирует её capability storage.
Владельцем принятой записи приём SHALL назначать предъявителя сессии. Обязательность
владельца при этом MUST держаться и схемой хранилища: колонка владельца пустого
значения не принимает вовсе, и норму эту держит capability storage. Проверка в
приёме от этого не лишняя — она отвечает отправителю понятным отказом до того, как
запись попадёт в память, а схема отвечала бы отказом сохранения после укладки
файла.
Предъявитель, чья сессия не даёт учётной записи пользователя, MUST получать
отказ 403 и MUST получать его до чтения тела — там же, где стоит отказ по
отсутствию сессии. Сессия владельца панели — именно такой случай: узнан он всё
же узнан, а записи в коллекции пользователей у него нет, и владельцем записи он
стать не может.
Код здесь другой, чем у запроса без сессии, и это не оплошность: 401 значит
«предъяви себя», а предъявитель себя предъявил. Утечки по разнице кодов нет —
оба ответа говорят о самом спрашивающем, а не о том, какие записи заведены.
Отказ после укладки записи потребовал бы убрать уже сохранённый файл, а уборки файлов сервис не умеет вовсе: норма, обязывающая к недостижимому, не пишется.
Scenario: Запись принята
- GIVEN источник метаданных читает запись и отдаёт её длительность
- AND отправитель предъявил сессию
- WHEN программа шлёт
POST /api/audioс полемaudio - THEN ответ имеет код
201, а в теле лежат непустойjob_idиstatusсо значениемuploaded - 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 называть вид, который
отдаёт: в поле transcription_text уходит сырая расшифровка, и только она.
Вычитанный текст этим полем MUST не подменяться — иначе значение поля менялось бы
у одной и той же записи от того, успел ли отработать необязательный шаг, а
контракт объявлен необратимым. Отдача «последнего записанного» текста MUST не
применяться: она делает ответ функцией порядка записи, а не состояния записи.
Перечень значений поля status MUST совпадать с перечнем рубежей конвейера:
uploaded, normalized, submitted, transcribed, done. Прежних значений
created, converted, transcribe, failed и dead в ответе MUST не быть.
Это объявленная ломка публичного контракта: рубеж называет достигнутое, а отказ
перестал быть состоянием.
Остановленная запись MUST отдавать рубеж, на котором она остановлена, и MUST
нести признак остановки отдельным полем halted со значением истины. Машинный
текст отказа MUST в ответ не попадать: он принадлежит журналу владельца сервиса,
а не отправителю. Этот адрес — единственное место, где отправитель узнаёт о
неудаче: доставки ответа отправителю у сервиса больше нет, и признак остановки
здесь несёт всю обязанность целиком.
Отказ без сессии MUST не зависеть от того, есть такая запись или нет: иначе по кодам ответа перебирается список заведённых записей.
Запись, принадлежащая другому, MUST отвечать тем же, чем отвечает неизвестный
идентификатор, — кодом 404 и тем же телом.
Scenario: Запись найдена
- GIVEN отправитель предъявил сессию
- WHEN он спрашивает рубеж своей записи
- THEN ответ имеет код
200и несётjob_id,statusиcreated_at - AND значение
statusпринадлежит перечню рубежей конвейера
Scenario: Запись остановлена
- GIVEN запись остановлена признаком на рубеже приведения
- WHEN владелец спрашивает её рубеж
- THEN поле
statusнесёт рубеж приведения - AND поле
haltedнесёт истину - AND машинного текста отказа в ответе нет
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и сообщение о ненайденной записи
Requirement: Имя файла, данное отправителем, не попадает в журнал
Приём SHALL не писать имя файла, данное отправителем, ни в одну свою журнальную запись — ни на успешном пути, ни на пути отказа, где имя могло бы приехать текстом ошибки. Имя приходит извне вместе с записью и принадлежит содержимому личной переписки наравне с текстом расшифровки; журнал уезжает в собранные логи, откуда строку не убрать.
Расширение, взятое из этого имени, в журнале остаётся собственным полем: по нему прослеживается путь записи. Что именно попадает в журнал ради прослеживаемости, нормирует требование ниже; наружу расширение выходит только приведённым к известному виду — этому отдано отдельное требование.
Оговорка про второй вход из требования ушла вместе с ним: имя, данное отправителем, доходит до сервиса единственным путём — приёмом по HTTP, — и сценарии судят именно его.
Scenario: Имя записи не видно в журнале принятой записи
- GIVEN источник метаданных читает запись и отдаёт её длительность
- WHEN программа шлёт
POST /api/audioс записью, чья основа имени несёт опознаваемую строку при обычном расширении.mp3 - THEN ни одна журнальная запись приёма этой строки не содержит
- AND расширение
.mp3в журнале допустимо
Scenario: Имя записи не видно в журнале при отказе приёма
- GIVEN источник метаданных не может прочитать запись
- WHEN программа шлёт
POST /api/audioс записью, чья основа имени несёт опознаваемую строку - THEN ни одна журнальная запись приёма, включая запись об ошибке, этой строки не содержит
Requirement: Поднятые входы видны наблюдателю
Сервис SHALL отдавать признак поднятости по каждому входу приёма отдельной метрикой и MUST выставлять метку только тому входу, который у сервиса есть. Метки убранного входа в метриках MUST не быть вовсе: признак со значением нуля читался бы как «вход есть, но не поднялся», то есть как поломка, а вечная единица рядом с ним — как исправность того, чего нет.
Проверяемое здесь одно — набор меток, и это честнее прежнего. Вход остался один, страница метрик отдаётся тем же сервером, что и приём, и значение нуля у единственной метки недостижимо: чтобы прочитать признак, надо дотянуться до входа, о котором он сообщает. Прежнее обоснование — «иначе потерянный вход не виден ничем» — было верно, пока входов было два; сегодня неподнятый вход виден неудачей чтения самих метрик.
Различать поднятый и неподнятый вход признак MUST снова, как только входов у сервиса станет больше одного.
Scenario: В метриках только оставшийся вход
- GIVEN сервис поднялся
- WHEN наблюдатель читает метрики
- THEN признак поднятости несёт метку входа HTTP со значением единицы
- AND метки убранного входа Telegram в метриках нет вовсе
REMOVED Requirements
Requirement: Признак включения решает, поднимается ли вход Telegram
Reason: Вход Telegram убран из сервиса целиком, и решать о его подъёме стало нечего. Настройки входа — признак включения, ключ доступа и срок ожидания обновлений — уходят из конфигурации вместе с ним.
Migration: Секцию [telegram] и ключ server.users_while_list — имя в коде
именно такое, с опечаткой, и в боевом файле искать надо его — из файла настроек
убрать руками. Оставленные ключи сервис пропускает молча — незнакомые
ключи разбор настроек не судит, — и потому файл, забытый как есть, поднимет
сервис без бота и без единого слова о том, что секция больше ничего не значит.
Записи с источником Telegram, если они в базе есть, остаются нетронутыми и
достаются своему владельцу; ответ в чат по ним не уходит. Возврат входа заводится
новым изменением вместе со связью чата и учётной записи.