Тестовое задание на стажировку Backend в Avito.
Сервис позволяет администраторам создавать переговорки и расписания доступности, а пользователям просматривать свободные слоты, создавать и отменять бронирования.
Обязательная часть:
POST /dummyLoginдля получения тестового JWT по ролиadminилиuserGET /_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 Coresrc/RoomBookingService.Conference- мок внешнего conference-сервисаtests/RoomBookingService.UnitTests- unit-тестыtests/RoomBookingService.IntegrationTests- integration-тестыsql/seed.sql- тестовые данныеsql/clean.sql- очистка БДapi.yaml- спецификация API из задания
Требования:
- 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Требования:
- .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-111111111111user->22222222-2222-2222-2222-222222222222
Пример запроса:
POST /dummyLogin
Content-Type: application/json
{
"role": "admin"
}Заполнение БД:
make seedОчистка БД:
make cleanЗапуск основных тестов (unit + integration):
make testImportant
Для удобства все виды тестов были внедрены в качестве CI в dotnet_tests.yml.
Запуск только unit-тестов:
make unit-testПокрытие unit-тестов по сохраненному артефакту coverage/unit-test_coverage.xml: 78.72% по строкам.
Note
С детальной информацией по покрытию, а также скриншотами результатов можно ознакомиться в папке coverage.
Запуск только integration-тестов:
make integration-testПокрытые интеграционные сценарии:
- создание переговорки -> создание расписания -> получение слотов -> создание брони
- идемпотентная отмена брони
- создание брони с
conferenceLink
Запуск только 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 msp95:21.73 msp99:37.57 ms- максимальная задержка (max latency):
60.72 ms
Note
С детальной информацией по нагрузке, а также скриншотами результатов можно ознакомиться в папке workload.
Вывод:
- при нагрузке
100 RPSэндпоинт отработал без ошибок - требование по ориентиру
200 msдля эндпоинта получения доступных слотов выполняется с запасом
Выбрал предгенерацию слотов и хранение их в БД.
При создании расписания сервис:
- валидирует диапазон дней и времени
- сохраняет расписание
- сразу генерирует 30-минутные слоты
- сохраняет слоты в таблицу
slotsсо стабильными UUID
Сейчас слоты генерируются на ближайшие 7 дней. Это решение выбрано из условия задания, где сказано, что в 99.9% случаев пользователи смотрят слоты в пределах ближайших 7 дней.
Плюсы такого подхода:
- быстрый
GET /rooms/{roomId}/slots/list, потому что слоты уже материализованы - стабильные
slotId, по которым можно безопасно бронировать - простая логика выборки свободных слотов
Ограничение текущей реализации: окно генерации создается один раз при создании расписания, поэтому после истечения этих 7 дней новые слоты автоматически не достраиваются
Если бы это был production-вариант, следующим шагом я бы добавил фоновый job, который поддерживает скользящее окно доступных слотов.
Защита сделана на двух уровнях:
- в бизнес-логике перед созданием брони проверяется наличие активной брони на слот
- в БД есть уникальный индекс на
slotIdтолько для записей со статусомactive
Это не позволяет иметь две активные брони на один и тот же слот.
Отмена сделана идемпотентной:
- если бронь активна, статус меняется на
cancelled - если бронь уже отменена, сервис возвращает
200 OKи текущее состояние брони
Параметр createConferenceLink является опциональным.
Если он передан:
- сервис вызывает мок внешнего conference-сервиса
- при успехе ссылка сохраняется в
booking.conferenceLink - при недоступности внешнего сервиса бронь все равно создается, а
conferenceLinkостаетсяnull
Это осознанный выбор: ссылка на конференцию является дополнительной возможностью, а не критичной частью бизнес-операции бронирования. Поэтому отказ внешней интеграции не должен ломать основное действие пользователя.
- слот может иметь только одну активную бронь
- слоты одной переговорки не пересекаются
- без расписания переговорка недоступна для бронирования
- нельзя создать бронь на слот в прошлом
adminне может создавать брони- пользователь может отменять только свою бронь
/bookings/myвозвращает только будущие активные брони
Основной контракт сервиса основан на требованиях, предъявленных в api.yaml.
После запуска можно проверить API через Swagger: http://localhost:8080/swagger.