Skip to content

Repository files navigation

Room Booking Service

Тестовое задание на стажировку Backend в Avito.

Сервис позволяет администраторам создавать переговорки и расписания доступности, а пользователям просматривать свободные слоты, создавать и отменять бронирования.

Что реализовано

Обязательная часть:

  • POST /dummyLogin для получения тестового JWT по роли admin или user
  • GET /_info
  • создание переговорок только для admin
  • просмотр списка переговорок для admin и user
  • для каждой переговорки можно создать только одно расписание без возможности последующего изменения
  • генерация 30-минутных слотов на основе расписания
  • просмотр доступных слотов по переговорке и дате
  • создание брони только для user
  • идемпотентная отмена своей брони только для user
  • просмотр своих будущих броней
  • просмотр всех броней с пагинацией для admin
  • хранение и передача даты/времени в UTC
  • JWT-аутентификация и role-based access control
  • unit-тесты и integration-тесты

Дополнительно реализовано:

  • Makefile
  • Swagger-документация и Swagger UI
  • конфигурация линтера через .editorconfig
  • опциональная генерация conferenceLink при бронировании через мок внешнего сервиса

Important

С детальной информацией (checkboxes) по выполненным пунктам можно ознакомиться в EPIC в разделе Issues.

Стек

  • C# / ASP.NET Core 10
  • Entity Framework Core
  • PostgreSQL
  • Docker
  • xUnit
  • Testcontainers для integration-тестов
  • Swagger / Swashbuckle

Структура проекта

Дерево проекта

Посмотреть / Скрыть
test-backend-lkhorasandzhian/
│
├── .github/
│   └── workflows/
│       └── classroom.yml
│
├── coverage/
│   ├── percent_coverage.png
│   └── unit-test_coverage.xml
│
├── sql/
│   ├── clean.sql
│   └── seed.sql
│
├── src/
│   ├── RoomBookingService.Api/
│   │   ├── Contracts/
│   │   │   ├── Auth/
│   │   │   │   ├── DummyLoginRequest.cs
│   │   │   │   └── TokenResponse.cs
│   │   │   ├── Bookings/
│   │   │   │   ├── BookingResponse.cs
│   │   │   │   ├── BookingsListResponse.cs
│   │   │   │   ├── CancelBookingResponse.cs
│   │   │   │   ├── CreateBookingRequest.cs
│   │   │   │   ├── CreateBookingResponse.cs
│   │   │   │   ├── MyBookingsResponse.cs
│   │   │   │   └── PaginationResponse.cs
│   │   │   ├── Common/
│   │   │   │   └── ErrorResponse.cs
│   │   │   ├── Rooms/
│   │   │   │   ├── CreateRoomRequest.cs
│   │   │   │   ├── CreateRoomResponse.cs
│   │   │   │   ├── RoomListResponse.cs
│   │   │   │   └── RoomResponse.cs
│   │   │   ├── Schedules/
│   │   │   │   ├── CreateScheduleRequest.cs
│   │   │   │   ├── CreateScheduleResponse.cs
│   │   │   │   └── ScheduleResponse.cs
│   │   │   └── Slots/
│   │   │       ├── SlotResponse.cs
│   │   │       └── SlotsListResponse.cs
│   │   ├── Controllers/
│   │   │   ├── AuthController.cs
│   │   │   ├── BookingController.cs
│   │   │   ├── InfoController.cs
│   │   │   ├── RoomController.cs
│   │   │   ├── ScheduleController.cs
│   │   │   └── SlotController.cs
│   │   ├── Domain/
│   │   │   ├── Constants/
│   │   │   │   ├── BookingStatuses.cs
│   │   │   │   ├── Roles.cs
│   │   │   │   └── SystemUsers.cs
│   │   │   ├── Entities/
│   │   │   │   ├── Booking.cs
│   │   │   │   ├── Room.cs
│   │   │   │   ├── Schedule.cs
│   │   │   │   ├── Slot.cs
│   │   │   │   └── User.cs
│   │   │   └── Exceptions/
│   │   │       ├── BookingNotFoundException.cs
│   │   │       ├── ForbiddenBookingActionException.cs
│   │   │       ├── InvalidRequestException.cs
│   │   │       ├── InvalidScheduleException.cs
│   │   │       ├── RoomNotFoundException.cs
│   │   │       ├── ScheduleAlreadyExistsException.cs
│   │   │       ├── SlotAlreadyBookedException.cs
│   │   │       └── SlotNotFoundException.cs
│   │   ├── Infrastructure/
│   │   │   ├── Auth/
│   │   │   │   └── JwtOptions.cs
│   │   │   └── Persistence/
│   │   │       ├── Configurations/
│   │   │       │   ├── BookingConfiguration.cs
│   │   │       │   ├── RoomConfiguration.cs
│   │   │       │   ├── ScheduleConfiguration.cs
│   │   │       │   ├── SlotConfiguration.cs
│   │   │       │   └── UserConfiguration.cs
│   │   │       ├── Migrations/
│   │   │       │   ├── 20260405101259_InitialCreate.cs
│   │   │       │   ├── 20260405101259_InitialCreate.Designer.cs
│   │   │       │   └── AppDbContextModelSnapshot.cs
│   │   │       └── AppDbContext.cs
│   │   ├── Properties/
│   │   │   └── launchSettings.json
│   │   ├── Services/
│   │   │   ├── Auth/
│   │   │   │   ├── IJwtTokenService.cs
│   │   │   │   └── JwtTokenService.cs
│   │   │   ├── Bookings/
│   │   │   │   ├── BookingService.cs
│   │   │   │   └── IBookingService.cs
│   │   │   ├── Rooms/
│   │   │   │   ├── IRoomService.cs
│   │   │   │   └── RoomService.cs
│   │   │   ├── Schedules/
│   │   │   │   ├── IScheduleService.cs
│   │   │   │   └── ScheduleService.cs
│   │   │   └── Slots/
│   │   │       ├── ISlotService.cs
│   │   │       └── SlotService.cs
│   │   ├── appsettings.Development.json
│   │   ├── appsettings.json
│   │   ├── Program.cs
│   │   ├── RoomBookingService.Api.csproj
│   │   └── RoomBookingService.Api.csproj.user
│   └── RoomBookingService.Conference/
│       ├── Abstractions/
│       │   └── IConferenceLinkService.cs
│       ├── Errors/
│       │   └── ConferenceServiceUnavailableException.cs
│       ├── Models/
│       │   ├── ConferenceLinkRequest.cs
│       │   └── ConferenceLinkResult.cs
│       ├── Services/
│       │   └── FakeConferenceLinkService.cs
│       ├── ConferenceConstants.cs
│       └── RoomBookingService.Conference.csproj
│
├── tests/
│   ├── RoomBookingService.IntegrationTests/
│   │   ├── Common/
│   │   │   ├── TestConstants.cs
│   │   │   └── TestJwt.cs
│   │   ├── Contracts/
│   │   │   ├── Bookings/
│   │   │   │   ├── BookingDto.cs
│   │   │   │   ├── BookingResponse.cs
│   │   │   │   └── CreateBookingRequest.cs
│   │   │   ├── Rooms/
│   │   │   │   ├── CreateRoomRequest.cs
│   │   │   │   ├── RoomDto.cs
│   │   │   │   └── RoomResponse.cs
│   │   │   ├── Schedules/
│   │   │   │   └── CreateScheduleRequest.cs
│   │   │   └── Slots/
│   │   │       ├── SlotDto.cs
│   │   │       └── SlotsResponse.cs
│   │   ├── Fixtures/
│   │   │   ├── CustomWebApplicationFactory.cs
│   │   │   └── PostgresContainerFixture.cs
│   │   ├── Helpers/
│   │   │   ├── AuthHelper.cs
│   │   │   └── HttpClientExtensions.cs
│   │   ├── Tests/
│   │   │   └── Booking/
│   │   │       ├── BookingFlowTests.cs
│   │   │       ├── CancelBookingTests.cs
│   │   │       └── ConferenceLinkBookingTests.cs
│   │   ├── RoomBookingService.IntegrationTests.csproj
│   │   └── Usings.cs
│   └── RoomBookingService.UnitTests/
│       ├── Controllers/
│       │   ├── AuthControllerTests.cs
│       │   ├── BookingControllerTests.cs
│       │   ├── InfoControllerTests.cs
│       │   ├── RoomControllerTests.cs
│       │   ├── ScheduleControllerTests.cs
│       │   └── SlotControllerTests.cs
│       ├── Services/
│       │   ├── Auth/
│       │   │   └── JwtTokenServiceTests.cs
│       │   ├── Booking/
│       │   │   └── BookingServiceTests.cs
│       │   ├── Common/
│       │   │   └── TestDbContextFactory.cs
│       │   ├── Conference/
│       │   │   └── FakeConferenceLinkServiceTests.cs
│       │   ├── Rooms/
│       │   │   └── RoomServiceTests.cs
│       │   ├── Schedules/
│       │   │   └── ScheduleServiceTests.cs
│       │   └── Slots/
│       │       └── SlotServiceTests.cs
│       ├── Smoke/
│       │   └── ContractsAndConstantsSmokeTests.cs
│       └── RoomBookingService.UnitTests.csproj
│
├── .editorconfig
├── .env
├── .gitignore
├── api.yaml
├── docker-compose.yaml
├── Dockerfile
├── INSTRUCTIONS.md
├── Makefile
├── README.md
├── RoomBookingService.slnx
└── TASK.md

Описание структуры

  • src/RoomBookingService.Api - API, контроллеры, доменная логика, EF Core
  • src/RoomBookingService.Conference - мок внешнего conference-сервиса
  • tests/RoomBookingService.UnitTests - unit-тесты
  • tests/RoomBookingService.IntegrationTests - integration-тесты
  • sql/seed.sql - тестовые данные
  • sql/clean.sql - очистка БД
  • api.yaml - спецификация API из задания

Быстрый старт

Через Docker Compose

Требования:

  • Docker
  • Docker Compose
  • make или совместимый runner для Makefile

Запуск:

make up

После запуска сервис доступен по адресу:

  • API: http://localhost:8080
  • Swagger UI: http://localhost:8080/swagger
  • Health/info endpoint: http://localhost:8080/_info

Остановка:

make down

Полное удаление контейнеров и volume:

make remove

Локальный запуск без Docker

Требования:

  • .NET SDK 10
  • PostgreSQL

Нужно настроить строку подключения и JWT-секрет через переменные окружения или appsettings.

Запуск:

dotnet run --project src/RoomBookingService.Api/RoomBookingService.Api.csproj

Переменные окружения

Используются значения из .env:

APP_PORT=8080
POSTGRES_DB=room_booking_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
JWT__SECRETKEY=room_booking_super_secret_key_2026_very_safe

При старте приложения EF Core автоматически применяет миграции.

Тестовые пользователи

/dummyLogin возвращает JWT с фиксированными пользователями:

  • admin -> 11111111-1111-1111-1111-111111111111
  • user -> 22222222-2222-2222-2222-222222222222

Пример запроса:

POST /dummyLogin
Content-Type: application/json

{
  "role": "admin"
}

Тестовые данные

Заполнение БД:

make seed

Очистка БД:

make clean

Тестирование

Запуск основных тестов (unit + integration):

make test

Important

Для удобства все виды тестов были внедрены в качестве CI в dotnet_tests.yml.

Unit-тестирование

Запуск только unit-тестов:

make unit-test

Покрытие unit-тестов по сохраненному артефакту coverage/unit-test_coverage.xml: 78.72% по строкам.

Note

С детальной информацией по покрытию, а также скриншотами результатов можно ознакомиться в папке coverage.

Integration-тестирование

Запуск только integration-тестов:

make integration-test

Покрытые интеграционные сценарии:

  • создание переговорки -> создание расписания -> получение слотов -> создание брони
  • идемпотентная отмена брони
  • создание брони с conferenceLink

Load-тестирование

Запуск только load-тестов:

make workload-test

Для нагрузочного тестирования использовался NBomber со сценарием available_slots для самого нагруженного эндпоинта получения доступных слотов.

Параметры запуска:

  • профиль нагрузки: inject
  • интенсивность: 100 RPS
  • длительность: 60 секунд
  • warm up: 10 секунд
  • целевой сценарий: GET /rooms/{roomId}/slots/list

Результаты:

  • всего запросов: 6000
  • успешных запросов: 6000
  • ошибок: 0
  • фактический RPS: 100
  • средняя задержка (min latency): 9.49 ms
  • p50: 7.38 ms
  • p95: 21.73 ms
  • p99: 37.57 ms
  • максимальная задержка (max latency): 60.72 ms

Note

С детальной информацией по нагрузке, а также скриншотами результатов можно ознакомиться в папке workload.

Вывод:

  • при нагрузке 100 RPS эндпоинт отработал без ошибок
  • требование по ориентиру 200 ms для эндпоинта получения доступных слотов выполняется с запасом

Принятые решения

1. Как формируются слоты

Выбрал предгенерацию слотов и хранение их в БД.

При создании расписания сервис:

  • валидирует диапазон дней и времени
  • сохраняет расписание
  • сразу генерирует 30-минутные слоты
  • сохраняет слоты в таблицу slots со стабильными UUID

Сейчас слоты генерируются на ближайшие 7 дней. Это решение выбрано из условия задания, где сказано, что в 99.9% случаев пользователи смотрят слоты в пределах ближайших 7 дней.

Плюсы такого подхода:

  • быстрый GET /rooms/{roomId}/slots/list, потому что слоты уже материализованы
  • стабильные slotId, по которым можно безопасно бронировать
  • простая логика выборки свободных слотов

Ограничение текущей реализации: окно генерации создается один раз при создании расписания, поэтому после истечения этих 7 дней новые слоты автоматически не достраиваются

Если бы это был production-вариант, следующим шагом я бы добавил фоновый job, который поддерживает скользящее окно доступных слотов.

2. Защита от двойного бронирования

Защита сделана на двух уровнях:

  • в бизнес-логике перед созданием брони проверяется наличие активной брони на слот
  • в БД есть уникальный индекс на slotId только для записей со статусом active

Это не позволяет иметь две активные брони на один и тот же слот.

3. Обработка отмены брони

Отмена сделана идемпотентной:

  • если бронь активна, статус меняется на cancelled
  • если бронь уже отменена, сервис возвращает 200 OK и текущее состояние брони

4. Работа с conference link

Параметр createConferenceLink является опциональным.

Если он передан:

  • сервис вызывает мок внешнего conference-сервиса
  • при успехе ссылка сохраняется в booking.conferenceLink
  • при недоступности внешнего сервиса бронь все равно создается, а conferenceLink остается null

Это осознанный выбор: ссылка на конференцию является дополнительной возможностью, а не критичной частью бизнес-операции бронирования. Поэтому отказ внешней интеграции не должен ломать основное действие пользователя.

Бизнес-ограничения, которые соблюдаются

  • слот может иметь только одну активную бронь
  • слоты одной переговорки не пересекаются
  • без расписания переговорка недоступна для бронирования
  • нельзя создать бронь на слот в прошлом
  • admin не может создавать брони
  • пользователь может отменять только свою бронь
  • /bookings/my возвращает только будущие активные брони

API

Основной контракт сервиса основан на требованиях, предъявленных в api.yaml.

После запуска можно проверить API через Swagger: http://localhost:8080/swagger.

Avito Tech Backend Internship

Review Assignment Due Date

About

Сервис бронирования переговорных комнат со слотами, JWT и role-based доступом. Решение тестового задания Avito Backend Internship

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages