приложение собрано каркасом и вшито в бинарник
- заведён каталог web/ — Vue 3, роутер пятой версии, сборка Vite; собранное вшивается через go:embed и раздаётся корневым маршрутом: разметка на неизвестном пути вне корней сервиса, отказ контракта внутри корня - перечень корней сервиса стал единой точкой и порождает регистрацию маршрутов, а не описывает её; журнал раздачи пишет исход и длину пути, но не сам путь - шаг front зовёт Node контейнером docker — Biome, юнит-тесты Vue и сборка входят в гейт, а в Dockerfile появилась ступень приложения
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
## 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** адреса почты на экране нет
|
||||
Reference in New Issue
Block a user