Как я собрал этот блог и что при этом сломалось

Хотелось своего места для записей. Не аккаунта на чужой площадке, где однажды поменяют правила, а обычного сайта: пишу в браузере, нажимаю «опубликовать», текст появляется по адресу, который принадлежит мне.

Требования вышли скромные. Публичная часть с лентой и записями. Закрытая админка с логином. Черновики, теги, картинки, поиск, лента подписки. Всё это крутится в моём кластере, поэтому — контейнер и манифесты.

Как он устроен

Один сервер на Node, база в одном файле SQLite, шаблоны отдаются с сервера. Никакого отдельного фронтенда: страница приходит готовой.

Это звучит старомодно ровно до того момента, когда считаешь, во что обходится альтернатива. Отдельный фронтенд означает сборку, второй набор зависимостей, второй способ ошибиться при обновлении. Личному блогу с одним автором это не покупает ничего.

SQLite вместо сетевой базы — по той же логике. Резервная копия блога это файл. Перенести блог на другую машину — скопировать файл. При десяти записях в месяц разговор о масштабировании выглядит смешно.

Одно решение оказалось важнее прочих: предпросмотр в редакторе считает сервер тем же кодом, что и публикацию. Не второй разборчик разметки в браузере, а тот же самый. Если бы их было два, они однажды разошлись бы, и узнал бы я об этом из чужого письма: «у тебя там кавычки поехали».

Как я это делал

Не одним заходом. Сначала описание того, что строим, потом план из двадцати задач, и только потом код.

Каждую задачу делали по одному и тому же кругу: сначала падающий тест, потом код, потом отдельная проверка чужими глазами — не тем, кто писал. Проверяющий не видел, как код появился, и приходил к нему как посторонний.

Звучит бюрократично. На деле именно это и дало главный результат — но не тот, которого я ждал.

Что сломалось

Я ожидал, что ошибки будут в коде. Ошибки оказались в плане.

Тесты, которые ничего не проверяли. Их нашлось восемь. Все зелёные, все написаны раньше кода — и все проходили независимо от того, работает проверяемое или нет.

Самый показательный: тест обрезки длинного адреса записи. Он подавал строку из двухсот русских «а», а те превращаются в сплошные латинские a без единого дефиса. Тест проверял, что в конце не осталось дефиса — и был прав всегда, даже если убрать из кода строку, которая этот дефис убирает. Зелёная проверка, охраняющая пустоту.

Второй случай оказался ещё изящнее. Я исправил один такой тест — и написал новый, который проходил благодаря самому дефекту, который должен был ловить. Он брал минимум из трёх замеров, чтобы не реагировать на случайные задержки, и этим же минимумом отбрасывал разовую задержку, ради которой затевался.

С тех пор правило простое: исправление не принимается, пока не показано, что тест падает без него. Не «тест зелёный», а «тест краснеет ровно тогда, когда должен».

Форма входа выдавала логин по секундомеру. Если пользователя нет, проверка пароля пропускалась, и ответ приходил мгновенно. Если есть, но пароль неверный — уходили десятки миллисекунд на вычисление хеша. Этой разницы достаточно, чтобы перебором узнать мой логин, а дальше подбирать только пароль. Теперь несуществующий пользователь сверяется с заранее вычисленным хешем-приманкой и стоит ровно столько же.

Снимок базы мог оказаться обрубком. Если под убивают жёстко посреди копирования — а в кластере так заканчивается любая остановка, не уложившаяся в отведённое время, — на диске оставался недописанный файл с правильным именем. Для ротации он неотличим от настоящего: может вытеснить последнюю хорошую копию и быть выбранным при восстановлении. Лечится классически: пишем во временный файл и переименовываем. Переименование мгновенно, поэтому под именем снимка либо ничего, либо целый снимок.

И лучшая история — про удаление записи. Кнопка «Удалить» спрашивала подтверждение. Точнее, должна была: подтверждение стояло атрибутом прямо в разметке формы. А блог ставит на каждый ответ политику безопасности, запрещающую встроенные обработчики. Собственная защита отключала собственное подтверждение.

Запись удалялась молча, без вопроса. Восстановить — только из вчерашней копии.

Этот дефект пережил двадцать проверок и попался только на общем осмотре всей работы целиком. Он жил ровно в слепой зоне: политика приходит из одной задачи, кнопка из другой, а сторож, который ищет встроенные обработчики на страницах, обходил три адреса — и ни одного из админки.

Числа

Двадцать задач. Семьдесят три коммита. Сто сорок четыре теста. Тринадцать дефектов, найденных в плане, и восемь мнимых тестов — почти всё это исправлено до того, как я хоть раз открыл блог в браузере.

А первое, что сломалось при живом запуске, тестами не ловилось вовсе: приложение падало на старте с относительным путём к данным. Все тесты подставляли абсолютный, контейнер тоже — и единственной неработающей настройкой оказалась та, что написана в инструкции по запуску.

Мораль скучная и надёжная: проверять надо ровно то, что написано в документации, ровно так, как там написано.

Что дальше

Писать. Ради этого всё и затевалось.