Перейти к основному содержимому

Кабинет на выдуманных данных: мок из OpenAPI прямо в Vite

· 7 мин

Личный кабинет Tuna — это Vue-приложение, которое ходит за данными в наш API по пути /v1. Чтобы открыть его локально и покликать страницы, обычно нужен поднятый бэкенд: Go-сервер, Postgres, Redis, S3, сиды. Для вёрстки это тяжело. Поэтому у кабинета есть лёгкий режим: yarn serve:mock — и dev-сервер сам отвечает на все запросы API, выдумывая данные по OpenAPI-спеке.

Никакого отдельного мок-сервера и ни одного написанного руками ответа: форму каждого ответа плагин берёт из swagger.json, а значения генерирует на лету.

Коротко
  • yarn serve:mock поднимает кабинет без бэкенда: Vite-плагин перехватывает /v1 и сам отвечает.
  • Форму ответа плагин берёт из OpenAPI-спеки (путь → схема ответа 200), значения генерирует по JSON Schema.
  • Каждый рефреш — новый случайный, но валидный по контракту набор: разные длины списков (включая пустые), статусы, роли, тексты. Это дёшево вскрывает состояния, которые на фиксированных сидах ловишь редко.
  • Побочный эффект: мок держит спеку честной. Пустая страница значит, что ручка не описана или описана не так, как отдаёт сервер.
Весь кабинет на моке: туннели, домены, порты, записи SSH, статистика команды, участники, аудит и сессии бастиона. Бэкенд не запущен.

Как это работает​

Кабинет собирается на Vite. У Vite dev-сервера есть цепочка middleware (обычный connect), и плагин вешается на префикс /v1:

vite-plugins/spec-mock.js
server.middlewares.use('/v1', (req, res, next) => {
const spec = load(); // swagger.json, один раз
const reqPath = '/v1' + req.url.split('?')[0];
const schema = responseSchema(spec, req.method, reqPath);
// ...генерируем ответ по schema и отдаём JSON
});

Дальше всё держится на спеке. У нас сервер на Go, аннотации go-swagger превращаются в swagger.json — полную (админскую) спеку со всеми ручками. Плагин находит путь запроса (с учётом path-параметров: /v1/domains/42 матчится на /v1/domains/{id}), берёт схему ответа 200/201 и строит по ней данные.

Генерация — рекурсивный обход JSON Schema:

  • enum → случайное значение из списка;
  • массив → 0–3 элемента (а для списков-страниц — пагинация через заголовки x-total / x-per-page);
  • объект → по каждому полю;
  • boolean / integer / number / строка.

Чтобы данные выглядели как настоящие, а не string string, для строк работают эвристики по имени поля:

vite-plugins/spec-mock.js
if (n.includes('email')) return `${pick(NAMES, k).toLowerCase()}@example.com`;
if (n.includes('avatar')) return `https://www.gravatar.com/avatar/...`;
if (n.includes('thumbnail')) return `https://picsum.photos/seed/${k % 1000}/320/200`;
if (n.includes('url')) return `https://${pick(WORDS, k)}.tuna.am`;
Отдельного процесса нет — это ещё один плагин в том же Vite dev-сервере, данные генерируются на каждый запрос.

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

Что для этого нужно​

Почти ничего, и в этом смысл. Нужна актуальная OpenAPI-спека (у нас она и так генерируется из аннотаций) и встроенный в Vite механизм middleware. Запуск — одна команда:

yarn serve:mock

Главное требование — не к инструменту, а к дисциплине: ответы API должны быть описаны в спеке. Нет описания ручки — плагину нечего генерировать, и страница приходит пустой.

Чем помогает​

Кабинет открывается за секунды и где угодно. Не нужны бэкенд, база, сиды, внешние сервисы. Фронтендер, дизайнер или ревьюер открывает ветку и сразу видит интерфейс.

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

  • пустые списки и пагинация (список может оказаться на 0 строк, на одну страницу или на две);
  • длинный текст в полях, где закладывались на короткий;
  • все значения enum — статусы, роли, типы — вперемешку;
  • необязательные поля то есть, то нет.
Пять рефрешей одной страницы: семь туннелей, один, снова пять. Каждый раз другие регионы, сервисы и баннеры.

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

Сложности подхода​

Данные, валидные по схеме, запросто бывают бессмысленными, и все доработки живут ровно на этом стыке:

  • Коды, которые UI превращает в названия. Поля вроде языков, ролей и локаций фронт мапит в человекочитаемые подписи. Случайная строка даёт пустые пункты в селектах — для таких полей в плагине есть точечные оверрайды с реальными кодами.
  • Связанные поля. Домен с редиректом и шлюз, который этот редирект выполняет, должны согласовываться. Случайная генерация их рассинхронизирует, поэтому для доменов и шлюзов мок отдаёт заготовленные ответы с заданными состояниями строк таблицы.
  • Глубина рекурсии. Генерация ограничена по глубине, иначе на циклических ссылках она бы не остановилась. Но и само ограничение дало побочные эффекты — о них ниже.
  • Отсутствие состояния. Мок по спеке — это stub: GET отдаёт свежую случайную выдумку, PATCH ничего не «запоминает». Для ручек, где это важно (например, флаги просмотренных баннеров у текущего участника), мы добавили маленькое in-memory состояние на время жизни dev-сервера: PATCH пишет присланные поля, GET их накладывает. Фактически stub местами дорастает до fake.

Что мок уже выявил​

  • Неописанные ручки. Пустые страницы подсветили ручки, которых не было в спеке. Мы дописали к ним аннотации, и спека стала полнее.
  • Описание, разошедшееся с реальностью. Один список был описан как массив, а сервер отдаёт по нему объект-словарь. Мок честно сгенерировал массив — и страница повела себя не так, как с настоящим API. Поправили описание под реальный ответ.
  • Хрупкое место на фронте. Экран отчёта падал на user.id: из-за ограничения глубины в массив попадал null-элемент, а код перебирал список без проверки. Формально это был баг генератора (его мы и починили — null-элементы теперь отсеиваются), но заодно это сигнал: граничный ввод нашёл место, где фронт слишком доверяет данным.
  • Ложное срабатывание эвристики. Проверка «в имени поля есть port» цепляла reports и new_reports — и в счётчиках отчётов появлялись «порты» в диапазоне 20000–60000. Такова цена эвристик по имени поля. Проверку сузили до точного совпадения port и суффикса _port.

Почему не взяли готовое​

  • Полный локальный стенд (Go-сервер + Postgres + Redis + сиды) у нас есть и остаётся основным инструментом для настоящего e2e и регресса. Но для «посмотреть вёрстку во всех состояниях» он тяжёлый: поднять, держать, пересобирать, а сиды фиксированы — редкие состояния всё равно заводить руками.
  • Фикстуры или хендлеры руками (условный MSW) пришлось бы писать на каждую из сотен ручек и чинить при каждом изменении API. Мок из спеки обновляется вместе со спекой сам.
  • Запись реальных ответов (record/replay) — это снимок одного состояния: ни вариативности, ни пустых списков, да ещё и с настоящими данными в дампах.
  • Готовый мок-сервер по OpenAPI (вроде Prism) — хорош как отдельный сервис. Но нам нужны были доменные доводки (коды→названия, согласованные строки таблиц, stateful-ручки, фикс тарифа для гейта раздела), и хотелось обойтись без лишнего процесса: ещё один плагин в том же Vite dev-сервере, с hot-reload и данными на лету. Небольшой свой плагин оказался проще, чем тянуть и обвязывать внешний сервер.

Подход не заменяет стенд и тесты — он закрывает узкую, но частую задачу: быстро посмотреть интерфейс на разнообразных данных и заодно не дать спеке разойтись с реальностью.

Оставьте отзыв​

Если вам нравится пользоваться Tuna, или наоборот вы недовольны чем-либо, то пожалуйста оставьте отзыв.

Новости​

Анонсы новых функций и статей публикуем в Telegram-канале. Подписывайтесь, чтобы ничего не пропустить.

Помощь​

Мы ценим наших пользователей и детально изучаем все обращения, если у вас возникли проблемы с tuna – обязательно свяжитесь с нами одним из способов: