## ADDED Requirements ### Requirement: Приложение отдаётся самим бинарником Сервис SHALL отдавать разметку приложения и её ресурсы из самого бинарника. Каталога с собранными файлами рядом с бинарником MUST не требоваться, и внешнего веб-сервера под раздачу MUST не заводиться. Причина в выкладке: сервис едет на сервер одним образом, и второй разворачиваемый артефакт рядом с ним завёл бы вторую точку, где выкладка расходится с собранным. Бинарник, которому нужен каталог рядом, отдаёт пустую страницу молча — каталог либо забыли положить, либо положили не тот, и различить это снаружи нечем. #### Scenario: Приложение открывается у бинарника без каталога рядом - **GIVEN** бинарник запущен в каталоге, где нет ничего, кроме его настроек - **WHEN** браузер спрашивает корень сервиса - **THEN** ответ имеет код `200` - **AND** тело ответа — разметка приложения #### Scenario: Ресурс приложения отдаётся оттуда же - **GIVEN** разметка приложения названа своим ресурсом - **WHEN** браузер спрашивает этот ресурс - **THEN** ответ имеет код `200` - **AND** тело ответа — содержимое ресурса ### Requirement: Сервис, поднятый без собранного приложения, говорит об этом Сервис SHALL отвечать кодом `503` на всяком пути, где он отдал бы разметку, если собранного приложения в нём нет, и MUST писать об этом строку в журнал при подъёме. Отвечать `404` и молчать он MUST не вправе: «приложения нет» и «такого адреса нет» — разные состояния, и первое чинится сборкой, а не поиском опечатки в адресе. Состояние это возможно только у собранного мимо набора проверок: и набор проверок, и сборка образа собирают приложение раньше бинарника. Проба здоровья при этом остаётся зелёной: она отвечает за то, работает ли сервис, а сервис в этом состоянии принимает записи и расшифровывает их — не работает только показ. #### Scenario: Пустая сборка отвечает отказом, а не отсутствием адреса - **GIVEN** бинарник собран без собранного приложения - **WHEN** браузер спрашивает корень сервиса - **THEN** ответ имеет код `503` #### Scenario: Отсутствие сборки видно в журнале - **GIVEN** бинарник собран без собранного приложения - **WHEN** сервис поднимается - **THEN** в журнале есть строка о том, что приложение не собрано #### Scenario: Проба здоровья остаётся зелёной - **GIVEN** бинарник собран без собранного приложения - **WHEN** запрос приходит на пробу здоровья - **THEN** ответ имеет код `200` ### Requirement: Неизвестный путь вне корней открывает приложение Сервис SHALL отдавать разметку приложения на всяком пути, который не принадлежит ни одному корню сервиса и не совпадает с отдельным адресом наблюдения. Корни перечислены поимённо — `/api` у хранилища, `/app` у приложения, `/auth` у входа, `/_` у панели, — отдельными адресами стоят `/health` и `/metrics`. Путь принадлежит корню, когда **совпадает с ним точно либо начинается им вместе с косой чертой**. Оба условия обязательны: по одному лишь префиксу корню `/app` достался бы посторонний `/apple`, а по одному лишь префиксу с косой чертой голый `/api` не достался бы никому и уехал бы разметкой. Путь, принадлежащий корню, MUST не проваливаться в приложение никогда: отказ контракта остаётся отказом контракта и уходит той формой, которой этот корень отвечает и сегодня. Иначе программа, ошибшаяся адресом под корнем приложения, получила бы разметку с кодом `200` вместо отказа с машиночитаемым кодом — и приняла бы её за ответ. Путь **под каталогом ресурсов** — тем, который наполняет сборщик, — разметкой не подменяется: не совпавший с файлом, он MUST отвечать `404`. Иначе разметка прежней сборки, назвавшая ресурс, которого в новой сборке уже нет, получает на него `200` и разметку вместо ресурса: браузер отвергнет её по типу содержимого, человек увидит пустой экран, а в кодах ответов сервиса не останется ничего. Открывающими страницу считаются `GET` и `HEAD`, и только они; прочие методы MUST отвечать `405`. #### Scenario: Обновление страницы посреди приложения открывает тот же экран - **GIVEN** приложение открыто на своём маршруте - **WHEN** браузер спрашивает этот путь заново - **THEN** ответ имеет код `200` - **AND** тело ответа — разметка приложения #### Scenario: Голый корень разметкой не подменяется - **WHEN** запрос приходит на путь, совпадающий с корнем сервиса точно и без косой черты - **THEN** тело ответа — не разметка приложения #### Scenario: Посторонний путь, начинающийся именем корня, открывает приложение - **WHEN** запрос приходит на путь, который начинается именем корня, но не отделён от него косой чертой - **THEN** ответ имеет код `200` - **AND** тело ответа — разметка приложения #### Scenario: Неизвестный путь под корнем приложения отвечает отказом - **GIVEN** человек вошёл и предъявил сессию - **WHEN** он спрашивает неизвестный путь под корнем приложения - **THEN** ответ имеет код `404` - **AND** тело ответа — отказ приложения с машиночитаемым кодом, а не разметка #### Scenario: Неизвестный путь под корнем приложения без сессии отвечает как все прочие его адреса - **WHEN** запрос приходит на неизвестный путь под корнем приложения без сессии - **THEN** ответ имеет код `401` - **AND** тело ответа — не разметка приложения #### Scenario: Неизвестный путь под корнем хранилища отвечает отказом - **WHEN** запрос приходит на неизвестный путь под корнем хранилища - **THEN** тело ответа — не разметка приложения #### Scenario: Несуществующий ресурс отвечает отсутствием, а не разметкой - **WHEN** браузер спрашивает под каталогом ресурсов файл, которого в сборке нет - **THEN** ответ имеет код `404` - **AND** тело ответа — не разметка приложения #### Scenario: Наблюдение приложением не подменяется - **WHEN** запрос приходит на пробу здоровья - **THEN** отвечает проба здоровья, а не приложение #### Scenario: Чужой метод отвечает отказом - **WHEN** на неизвестный путь вне корней приходит запрос методом, которым страницу не открывают - **THEN** ответ имеет код `405` #### Scenario: Проверка доступности разметку получает - **WHEN** разметку спрашивают методом `HEAD` - **THEN** ответ имеет код `200` ### Requirement: Обновлённое приложение доходит до браузера Сервис SHALL отдавать **ресурс из каталога, который наполняет сборщик**, с долгим сроком хранения и пометкой «неизменяемо», а всё прочее, включая разметку, — с требованием спрашивать заново. Признак — каталог, а не вид файла: имена в нём строит сборщик и несёт в них отпечаток содержимого, поэтому изменившийся ресурс приезжает под новым именем и прежний ответ устареть не может. Долгий срок MUST не доставаться файлу вне этого каталога. Разметка, иконка, манифест и всякий файл с постоянным именем меняются под тем же именем, и отозвать у браузера выданное «неизменяемо» нечем: выложенное обновление не дойдёт до того, кто уже открывал приложение, пока он не почистит хранилище браузера руками. Заметить это со стороны сервиса нечем — запросов он больше не увидит. Отпечаток в именах — свойство сборки, а не сервиса, и нормой его держит конвенция приложения, а не эта спека: сервис имён не выбирает и их нарушения не заметит. Здесь названо только то, что делает с ними сам сервис. #### Scenario: Ресурс сборщика отдаётся с долгим сроком - **WHEN** браузер спрашивает ресурс из каталога, который наполняет сборщик - **THEN** ответ несёт долгий срок хранения и пометку «неизменяемо» #### Scenario: Разметка спрашивается заново - **WHEN** браузер спрашивает разметку приложения - **THEN** ответ несёт требование спрашивать её заново - **AND** пометки «неизменяемо» в ответе нет #### Scenario: Файл с постоянным именем долгого срока не получает - **WHEN** браузер спрашивает файл сборки, лежащий вне каталога ресурсов - **THEN** ответ несёт требование спрашивать его заново ### Requirement: Путь, отданный приложению, в журнал не идёт Сервис SHALL записывать о таком запросе **исход из закрытого перечня** — разметка, ресурс, отказ — и длину пути, а самого пути MUST не записывать ни в один свой журнал. То же относится к меткам метрик. Причина в том, кто этот путь выбирает. До появления раздачи путь вне корней ловил отказ маршрутизатора; теперь он успешный ответ, и множеством его значений распоряжается спрашивающий — дословная запись сделала бы журнал местом, куда аноним пишет свой текст произвольной длины. Правило того же рода у сервиса уже есть: причина отказа, пришедшая от провайдера строкой запроса, приводится к перечню известных. Журналов при этом **два**: свой и журнал хранилища, куда библиотека кладёт путь целиком вместе с адресом отправителя. Требование относится к обоим. #### Scenario: Путь не доезжает до журнала - **WHEN** приходит запрос на путь вне корней сервиса - **THEN** записи о нём не несут этого пути - **AND** несут исход и длину пути #### Scenario: Длинный путь журнал не наполняет - **WHEN** приходит запрос на путь длиной в тысячу знаков - **THEN** записи о нём не растут вместе с длиной пути ### Requirement: Сервис объявляет, какая сборка приложения в нём вшита Сервис SHALL писать при подъёме отпечаток вшитой сборки. Он же MUST уходить меткой ответа с разметкой: сама разметка отдаётся с требованием спрашивать её заново, и без метки браузер получает полное тело вместо подтверждения — вшитый файл не несёт времени правки вовсе. Без отпечатка «не та сборка» неотличима от «той»: вне набора проверок порядок шагов ничем не задан, и бинарник собирается с тем, что лежало в каталоге с прошлого раза. Приложение при этом открывается и ведёт себя как прежняя версия, а искать причину человек идёт в код сервиса. #### Scenario: Отпечаток виден при подъёме - **WHEN** сервис поднимается с собранным приложением - **THEN** в журнале есть отпечаток вшитой сборки #### Scenario: Разметка несёт метку ответа - **WHEN** браузер спрашивает разметку приложения - **THEN** ответ несёт метку, по которой её можно спросить заново ### Requirement: Открытое приложение показывает вошедшего Приложение SHALL спрашивать сервис, кто вошёл, и показывать его имя. Отказ `401` MUST уводить ко входу: человек, ещё не вошедший, получает его на всяком адресе данных, и это его штатное состояние, а не поломка. Всякий **иной** отказ и сорванный запрос MUST ко входу не уводить, а показываться строкой о неудаче. Трактовка «любой отказ значит не вошёл» замкнула бы круг: приложение ушло бы ко входу, вход вернул бы человека в приложение, и на отказе сервиса или ограничителя частоты круг пошёл бы заново. Имя вошедшего берётся ответом сервиса, а не кукой сессии: кука недоступна скриптам страницы, и другого способа узнать вошедшего у приложения нет. Имени в ответе может не быть вовсе — тогда приложение MUST показать, что вход выполнен, и MUST не подставлять вместо имени адрес почты: его в ответе нет по норме `access`. #### Scenario: Вошедший виден - **GIVEN** человек вошёл и получил куку сессии - **WHEN** он открывает приложение - **THEN** приложение показывает его имя #### Scenario: Не вошедшему предлагается вход - **GIVEN** сессии у человека нет - **WHEN** он открывает приложение - **THEN** приложение ведёт его ко входу #### Scenario: Отказ сервиса ко входу не уводит - **GIVEN** сервис отвечает на вопрос о вошедшем отказом, который не является отсутствием сессии - **WHEN** человек открывает приложение - **THEN** приложение показывает строку о неудаче - **AND** ко входу оно не уводит #### Scenario: Учётная запись без имени - **GIVEN** человек вошёл, а имени у его учётной записи нет - **WHEN** он открывает приложение - **THEN** приложение показывает, что вход выполнен - **AND** адреса почты на экране нет