From 9f6974b375766f10d01f846067f9268b90a2c922 Mon Sep 17 00:00:00 2001 From: Yan Bubenok Date: Fri, 31 Oct 2025 00:01:00 +0500 Subject: [PATCH 1/2] expand README; extend .gitignore --- .gitignore | 2 + README.md | 188 ++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 175 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 837f7f8..63888b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules .env .DS_Store +coverage/ +.nyc_output/ diff --git a/README.md b/README.md index 64223e6..2272de9 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,177 @@ +[![CI](https://github.com/iBubenok/booking-api/actions/workflows/ci.yml/badge.svg)](https://github.com/iBubenok/booking-api/actions/workflows/ci.yml) + # Booking API -Демо API для бронирования мест: -- запрет повторной брони одним пользователем на одно событие, -- защита от овербукинга. +Демо API бронирования мест: +- **один пользователь — одна бронь** на одно событие, +- **без овербукинга** (конкурентно-безопасно, блокировка `FOR UPDATE` + уникальный индекс). + +## Живой сервис + +**Base URL:** **https://booking-api-mwff.onrender.com** + +Быстрые ссылки: +- Домашняя страница — `/` +- Документация (Swagger UI) — `/docs` +- Health-check (app) — `/health` +- Health-check (DB) — `/health/db` +- Основной эндпоинт — `POST /api/bookings/reserve` + +--- + +## Проверка доступности + +Откройте в браузере: +- `/health` → ожидается: `{"status":"ok","db":"configured"}` +- `/health/db` → ожидается: `{"status":"ok"}` (при живой БД) или `503 {"status":"db-down"}` +- `/docs` → откроется Swagger UI (можно тестировать API из браузера) + +--- + +## Примеры запросов (curl) + +### 1) Первая бронь (ожидаем **201 Created**) + +```bash +curl -i -X POST https://booking-api-mwff.onrender.com/api/bookings/reserve \ + -H "Content-Type: application/json" \ + -d '{"event_id":1,"user_id":"user123"}' +```` + +Ожидается: + +* статус `HTTP/1.1 201` +* JSON с полями `booking` и `seats_left`. + +### 2) Повторная бронь тем же пользователем на то же событие (ожидаем **409 Conflict**) + +```bash +curl -i -X POST https://booking-api-mwff.onrender.com/api/bookings/reserve \ + -H "Content-Type: application/json" \ + -d '{"event_id":1,"user_id":"user123"}' +``` + +Ожидается: + +* статус `HTTP/1.1 409` +* JSON `{"error":"User already booked this event"}`. + +### 3) Продажа всех мест и отказ «sold out» (у события `#1` всего **3** места) + +```bash +# три уникальные брони (должны пройти: 201 201 201) +curl -i -X POST https://booking-api-mwff.onrender.com/api/bookings/reserve \ + -H "Content-Type: application/json" \ + -d '{"event_id":1,"user_id":"u2"}' + +curl -i -X POST https://booking-api-mwff.onrender.com/api/bookings/reserve \ + -H "Content-Type: application/json" \ + -d '{"event_id":1,"user_id":"u3"}' + +curl -i -X POST https://booking-api-mwff.onrender.com/api/bookings/reserve \ + -H "Content-Type: application/json" \ + -d '{"event_id":1,"user_id":"u4"}' + +# четвёртая попытка (ожидаем 409 sold out) +curl -i -X POST https://booking-api-mwff.onrender.com/api/bookings/reserve \ + -H "Content-Type: application/json" \ + -d '{"event_id":1,"user_id":"u5"}' +``` + +Ожидается: + +* первые три запроса вернут `201`, +* четвёртый — `409` и сообщение `Event is sold out`. + +### 4) Тест конкуренции (быстрый) + +Отправьте несколько запросов одновременно (через Swagger UI или скриптом) на одно и то же `event_id` с разными `user_id`. +Итог: число успешных бронирований **не превысит** `total_seats`, а лишние вернут `409`. + +--- + +## Поведение API и коды ответов + +* `POST /api/bookings/reserve` + + * **Вход**: JSON `{"event_id": number, "user_id": string}` + + * `event_id`: целое `> 0` + * `user_id`: непустая строка, **≤ 128** символов, допускаются только `A–Z a–z 0–9 _ - . : @` + * **Выход**: + + * `201 Created` — бронь создана, `{ booking, seats_left }` + * `400 Bad Request` — неверный формат входа + * `404 Not Found` — событие не найдено + * `409 Conflict` — повторная бронь того же пользователя **или** мест больше нет + * `503 Service Unavailable` — БД не настроена (`DATABASE_URL` не задан) + * `500 Internal Error` — непредвиденная ошибка +* **Ограничение частоты**: на `POST /api/bookings/reserve` действует rate-limit **30 запросов/мин** на IP (помогает от случайных/скриптовых «бурстов»). + +--- + +## Безопасность и сервисные возможности + +* `helmet` (HTTP-заголовки безопасности, настроен `crossOriginResourcePolicy` для Swagger UI) +* `cors` (разрешены кросс-доменные запросы) +* `express.json({ limit: '32kb' })` (лимит тела запроса) +* `morgan('combined')` (логирование запросов) +* `app.set('trust proxy', 1)` (корректно определяем IP клиента за прокси/балансировщиком — важно для rate-limit) +* **Graceful shutdown** по `SIGINT`/`SIGTERM` (корректное закрытие соединений с БД) +* **Fallback 404** JSON-ответом + +--- ## Быстрый старт (локально) -1) `npm ci` -2) Создайте `.env` из `.env.example` и укажите `DATABASE_URL` -3) `npm start` -4) Откройте `/docs` для Swagger UI и `/health` для healthcheck - -## Эндпоинты -- `POST /api/bookings/reserve` -- `GET /health` -- `GET /docs` - -## Миграции -`db/migrations.sql` (таблицы, индексы, сиды) + +1. Установите зависимости: + + ```bash + npm ci + ``` +2. Создайте `.env` на основе `.env.example` и задайте переменные: + + ```dotenv + PORT=3000 + DATABASE_URL=postgres://user:password@host:5432/dbname + # Включайте SSL для облачных БД (Neon/Render и т.п.) + DATABASE_SSL=true + ``` +3. Запуск: + + ```bash + npm start + ``` +4. Проверьте: + + * `GET http://localhost:3000/health` + * `GET http://localhost:3000/docs` + +### Миграции + +Выполните файл `db/migrations.sql` в вашей БД (создаёт таблицы, индексы и сид-данные). + +--- + +## Структура проекта + +``` +. +├─ db/ +│ └─ migrations.sql # таблицы, индексы, сиды +├─ __tests__/ # (опц.) автотесты +├─ openapi.yaml # спецификация OpenAPI для Swagger UI +├─ server.js # запуск приложения, graceful shutdown +├─ package.json +├─ .env.example +└─ README.md +``` + +--- + +## CI/CD + +* **CI**: GitHub Actions (`.github/workflows/ci.yml`) выполняет `npm ci` и `npm test` на каждый `push`/`PR`. +* **CD**: Render Blueprint (`render.yaml`) автодеплоит ветку `main`; `/health` используется как health-check. + +--- From 2c61d7fd213c574f4f12af23424fe30ec0c4cb4d Mon Sep 17 00:00:00 2001 From: Yan Bubenok Date: Fri, 31 Oct 2025 00:20:28 +0500 Subject: [PATCH 2/2] ci: run only on PRs to main and pushes to main; cancel superseded runs --- .github/workflows/ci.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5889a02..83aad9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,17 @@ name: CI on: - push: - branches: [ "**" ] + # Запускаем проверки для pull request'ов, целевая ветка — main pull_request: - branches: [ "**" ] + branches: [ "main" ] + # И дополнительно — для пушей в main (после merge), чтобы проверить уже то, что влито + push: + branches: [ "main" ] + +# Если быстро прилетает новый коммит в ту же ветку/PR — отменяем предыдущий прогон +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref || github.head_ref }} + cancel-in-progress: true jobs: test: