diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f88054d --- /dev/null +++ b/Makefile @@ -0,0 +1,226 @@ +.PHONY: help clean compile shell test eunit test-api test-all \ + dialyzer xref cover docs release run stop logs + +# Цвета для вывода (только для команд, где нужны) +GREEN := \033[0;32m +YELLOW := \033[1;33m +RED := \033[0;31m +BLUE := \033[0;34m +NC := \033[0m + +# Переменные +REBAR3 := rebar3 +SNAME := eventhub +SHELL := /bin/bash + +# ============================================================================ +# HELP +# ============================================================================ +help: ## Показать это сообщение + @echo "EventHub - Makefile команды:" + @echo "" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' + +# ============================================================================ +# BUILD +# ============================================================================ +compile: ## Скомпилировать проект + @echo "Компиляция проекта..." + @$(REBAR3) compile + @echo "✓ Компиляция завершена" + +clean: ## Очистить проект + @echo "Очистка проекта..." + @$(REBAR3) clean + @rm -rf _build deps logs *.log + @echo "✓ Очистка завершена" + +deps: ## Установить зависимости + @echo "Установка зависимостей..." + @$(REBAR3) get-deps + @echo "✓ Зависимости установлены" + +update-deps: ## Обновить зависимости + @echo "Обновление зависимостей..." + @$(REBAR3) update-deps + @echo "✓ Зависимости обновлены" + +# ============================================================================ +# DEVELOPMENT +# ============================================================================ +shell: ## Запустить интерактивную оболочку + @echo "Запуск Erlang shell..." + @$(REBAR3) shell --sname $(SNAME) + +run: ## Запустить приложение (foreground) + @echo "Запуск приложения..." + @$(REBAR3) shell --sname $(SNAME) + +stop: ## Остановить приложение + @echo "Остановка приложения..." + @pkill -f "rebar3 shell --sname $(SNAME)" || true + @pkill -f "beam.*$(SNAME)" || true + @echo "✓ Приложение остановлено" + +restart: stop run ## Перезапустить приложение + +# ============================================================================ +# TESTING +# ============================================================================ +test: eunit ## Запустить все тесты (алиас для eunit) + +eunit: ## Запустить EUnit тесты + @echo "Запуск EUnit тестов..." + @pkill -f "beam.*$(SNAME)" 2>/dev/null || true + @$(REBAR3) eunit --sname $(SNAME)_test + +eunit-module: ## Запустить тесты для модуля (make eunit-module MODULE=core_calendar_tests) + @echo "Запуск тестов для модуля $(MODULE)..." + @pkill -f "beam.*$(SNAME)" 2>/dev/null || true + @$(REBAR3) eunit --sname $(SNAME)_test --module=$(MODULE) + +eunit-verbose: ## Запустить EUnit тесты с подробным выводом + @echo "Запуск EUnit тестов (verbose)..." + @pkill -f "beam.*$(SNAME)" 2>/dev/null || true + @$(REBAR3) eunit --sname $(SNAME)_test --verbose + +test-api: ## Запустить API тесты + @echo "Запуск API тестов..." + @if ! curl -s http://localhost:8080/health > /dev/null 2>&1; then \ + echo "✗ Сервер не запущен. Выполните 'make run' в другом терминале"; \ + exit 1; \ + fi + @chmod +x test/scripts/*.sh + @cd test/scripts && ./test_all.sh + +test-auth: ## Запустить тесты аутентификации + @chmod +x test/scripts/test_auth_api.sh + @./test/scripts/test_auth_api.sh + +test-calendar: ## Запустить тесты календарей + @chmod +x test/scripts/test_calendar_api.sh + @./test/scripts/test_calendar_api.sh + +test-event: ## Запустить тесты событий + @chmod +x test/scripts/test_event_api.sh + @./test/scripts/test_event_api.sh + +test-booking: ## Запустить тесты бронирований + @chmod +x test/scripts/test_booking_api.sh + @./test/scripts/test_booking_api.sh + +test-all: eunit test-api ## Запустить ВСЕ тесты (EUnit + API) + @echo "========================================" + @echo " ВСЕ ТЕСТЫ ПРОЙДЕНЫ!" + @echo "========================================" + +# ============================================================================ +# CODE QUALITY +# ============================================================================ +dialyzer: ## Запустить Dialyzer (статический анализ) + @echo "Запуск Dialyzer..." + @$(REBAR3) dialyzer + @echo "✓ Dialyzer завершён" + +xref: ## Запустить Xref (кросс-ссылки) + @echo "Запуск Xref..." + @$(REBAR3) xref + @echo "✓ Xref завершён" + +cover: ## Запустить тесты с покрытием кода + @echo "Запуск тестов с покрытием..." + @pkill -f "beam.*$(SNAME)" 2>/dev/null || true + @$(REBAR3) eunit --sname $(SNAME)_test --cover + @$(REBAR3) cover --verbose + @echo "✓ Отчёт о покрытии в _build/test/cover/" + +docs: ## Сгенерировать документацию (EDoc) + @echo "Генерация документации..." + @$(REBAR3) edoc + @echo "✓ Документация в doc/" + +# ============================================================================ +# RELEASE & DOCKER +# ============================================================================ +release: ## Собрать релиз + @echo "Сборка релиза..." + @$(REBAR3) as prod release + @echo "✓ Релиз собран в _build/prod/rel/eventhub/" + +docker-build: ## Собрать Docker образ + @echo "Сборка Docker образа..." + @docker build -t eventhub:latest . + @echo "✓ Docker образ собран" + +docker-run: ## Запустить Docker контейнер + @echo "Запуск Docker контейнера..." + @docker run -p 8080:8080 -p 8445:8445 --name eventhub eventhub:latest + +docker-stop: ## Остановить Docker контейнер + @echo "Остановка Docker контейнера..." + @docker stop eventhub || true + @docker rm eventhub || true + @echo "✓ Контейнер остановлен" + +# ============================================================================ +# UTILITIES +# ============================================================================ +status: ## Проверить статус сервера + @echo "Проверка статуса сервера..." + @if curl -s http://localhost:8080/health > /dev/null 2>&1; then \ + echo "✓ Сервер запущен на http://localhost:8080"; \ + curl -s http://localhost:8080/health; \ + else \ + echo "✗ Сервер не запущен"; \ + fi + +tree: ## Показать структуру проекта + @tree -I '_build|deps|logs|.git' --dirsfirst 2>/dev/null || ls -la + +info: ## Показать информацию о проекте + @echo "EventHub - информация о проекте:" + @echo "" + @echo "API эндпоинты:" + @echo " POST /v1/register - Регистрация" + @echo " POST /v1/login - Логин" + @echo " POST /v1/refresh - Обновление токена" + @echo " GET /v1/user/me - Профиль" + @echo " POST /v1/calendars - Создание календаря" + @echo " GET /v1/calendars - Список календарей" + @echo " POST /v1/calendars/:id/events - Создание события" + @echo " POST /v1/events/:id/bookings - Запись на событие" + @echo "" + @echo "Порты:" + @echo " HTTP API: 8080" + @echo " Admin HTTP: 8445" + +# ============================================================================ +# DEVELOPMENT WORKFLOW +# ============================================================================ +dev-setup: deps compile ## Настроить окружение для разработки + @echo "✓ Окружение настроено" + +dev-test: ## Быстрый цикл: compile + eunit + @make compile + @make eunit + +# ============================================================================ +# GIT HELPERS +# ============================================================================ +git-status: ## Показать статус Git + @git status --short + +git-log: ## Показать лог Git (последние 10) + @git log --oneline -10 + +git-save: ## Сохранить изменения (commit + push) + @read -p "Сообщение коммита: " msg; \ + git add .; \ + git commit -m "$$msg"; \ + git push + +# ============================================================================ +# DEFAULT +# ============================================================================ +.DEFAULT_GOAL := help \ No newline at end of file diff --git a/README.md b/README.md index 08ee7a1..87e93a2 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,158 @@ Build ----- $ rebar3 compile + +# EventHub - Шпаргалка по Makefile командам + +## 🚀 Разработка + +| Команда | Описание | +|---------|----------| +| `make compile` | Скомпилировать проект | +| `make shell` | Запустить интерактивную Erlang оболочку | +| `make run` | Запустить приложение в foreground | +| `make start` | Запустить приложение в фоне | +| `make stop` | Остановить приложение | +| `make restart` | Перезапустить приложение | +| `make logs` | Показать логи | +| `make console` | Подключиться к запущенной ноде | +| `make clean` | Очистить проект | +| `make deps` | Установить зависимости | +| `make update-deps` | Обновить зависимости | + +--- + +## 🧪 Тестирование + +| Команда | Описание | +|---------|----------| +| `make test` | Алиас для `make eunit` | +| `make eunit` | Запустить все EUnit тесты | +| `make eunit-verbose` | EUnit тесты с подробным выводом | +| `make eunit-module MODULE=имя` | Тесты для конкретного модуля | +| `make test-api` | Запустить API тесты (сервер должен быть запущен) | +| `make test-auth` | Только тесты аутентификации | +| `make test-calendar` | Только тесты календарей | +| `make test-event` | Только тесты событий | +| `make test-booking` | Только тесты бронирований | +| `make test-all` | Все тесты (EUnit + API) | +| `make cover` | Тесты с отчётом о покрытии кода | + +--- + +## 🔍 Качество кода + +| Команда | Описание | +|---------|----------| +| `make dialyzer` | Статический анализ кода | +| `make xref` | Проверка кросс-ссылок | +| `make lint` | Проверка стиля кода | +| `make format` | Форматирование кода (требуется erlfmt) | +| `make docs` | Генерация документации EDoc | + +--- + +## 📦 Релиз и Docker + +| Команда | Описание | +|---------|----------| +| `make release` | Собрать production релиз | +| `make tar` | Создать tar.gz архив релиза | +| `make docker-build` | Собрать Docker образ | +| `make docker-run` | Запустить Docker контейнер | +| `make docker-stop` | Остановить Docker контейнер | + +--- + +## 🛠 Утилиты + +| Команда | Описание | +|---------|----------| +| `make status` | Проверить статус сервера (healthcheck) | +| `make tree` | Показать структуру проекта | +| `make info` | Показать информацию о проекте и API | +| `make help` | Показать все доступные команды | + +--- + +## 👨‍💻 Рабочий процесс разработки + +| Команда | Описание | +|---------|----------| +| `make dev-setup` | Настройка окружения (deps + compile) | +| `make dev-test` | Быстрый цикл: compile + eunit | +| `make dev-watch` | Автокомпиляция при изменениях (требуется entr) | + +--- + +## 📋 Git хелперы + +| Команда | Описание | +|---------|----------| +| `make git-status` | Показать статус Git | +| `make git-log` | Показать лог (последние 10 коммитов) | +| `make git-save` | Commit + push (запросит сообщение) | + +--- + +## 🔧 Примеры использования + +```bash +# Запустить тесты для конкретного модуля +make eunit-module MODULE=core_booking_tests + +# Запустить приложение, затем API тесты +make run & make test-api + +# Полный цикл: очистка, компиляция, тесты +make clean compile eunit + +# Проверить статус сервера +make status + +# Собрать Docker образ и запустить +make docker-build && make docker-run +``` + +## 🎯 Полезные сочетания +```bash +# Остановить всё, очистить, собрать заново, запустить тесты +make stop && make clean && make compile && make eunit + +# Запустить сервер в фоне и проверить статус +make start && sleep 3 && make status + +# Посмотреть структуру проекта +make tree + +# Показать информацию о проекте +make info +``` +📡 API Эндпоинты (порт 8080) + +| Метод | Путь | Описание | +|-------|----------|----------| +| POST | /v1/register | Регистрация пользователя | +| POST | /v1/login | Логин (получение JWT) | +| POST | /v1/refresh | Обновление токена | +| GET | /v1/user/me | Профиль текущего пользователя | +| GET | /v1/user/bookings | Бронирования пользователя | +| POST | /v1/calendars | Создание календаря | +| GET | /v1/calendars | Список календарей | +| GET | /v1/calendars/:id | Получение календаря | +| PUT | /v1/calendars/:id | Обновление календаря | +| DELETE | /v1/calendars/:id | Удаление календаря | +| POST | /v1/calendars/:id/events | Создание события | +| GET | /v1/calendars/:id/events | Список событий календаря | +| GET | /v1/events/:id | Получение события | +| PUT | /v1/events/:id | Обновление события | +| DELETE | /v1/events/:id | Удаление события | +| GET | /v1/events/:id/occurrences | Вхождения повторяющегося | события +| DELETE | /v1/events/:id/occurrences/:time | Отмена вхождения | +| POST | /v1/events/:id/bookings | Запись на событие | +| GET | /v1/events/:id/bookings | Список бронирований события | +| GET | /v1/bookings/:id | Получение бронирования | +| PUT | /v1/bookings/:id | Подтверждение/отклонение | +| DELETE | /v1/bookings/:id | Отмена бронирования | +| GET | /health | Healthcheck | +--- \ No newline at end of file diff --git a/src/core/core_booking.erl b/src/core/core_booking.erl new file mode 100644 index 0000000..00fd3bf --- /dev/null +++ b/src/core/core_booking.erl @@ -0,0 +1,102 @@ +-module(core_booking). +-include("records.hrl"). + +-export([create/2, get_by_id/1, get_by_event_and_user/2, list_by_event/1, list_by_user/1]). +-export([update_status/2, delete/1]). +-export([generate_id/0]). + +%% Создание бронирования +create(EventId, UserId) -> + Id = generate_id(), + Booking = #booking{ + id = Id, + event_id = EventId, + user_id = UserId, + status = pending, + confirmed_at = undefined, + created_at = calendar:universal_time(), + updated_at = calendar:universal_time() + }, + + F = fun() -> + mnesia:write(Booking), + {ok, Booking} + end, + + case mnesia:transaction(F) of + {atomic, Result} -> Result; + {aborted, Reason} -> {error, Reason} + end. + +%% Получение бронирования по ID +get_by_id(Id) -> + case mnesia:dirty_read(booking, Id) of + [] -> {error, not_found}; + [Booking] -> {ok, Booking} + end. + +%% Получение бронирования по событию и пользователю +get_by_event_and_user(EventId, UserId) -> + Match = #booking{event_id = EventId, user_id = UserId, _ = '_'}, + case mnesia:dirty_match_object(Match) of + [] -> {error, not_found}; + [Booking] -> {ok, Booking} + end. + +%% Список бронирований события +list_by_event(EventId) -> + Match = #booking{event_id = EventId, _ = '_'}, + Bookings = mnesia:dirty_match_object(Match), + {ok, Bookings}. + +%% Список бронирований пользователя +list_by_user(UserId) -> + Match = #booking{user_id = UserId, _ = '_'}, + Bookings = mnesia:dirty_match_object(Match), + {ok, Bookings}. + +%% Обновление статуса бронирования +update_status(Id, Status) when Status =:= pending; Status =:= confirmed; Status =:= cancelled -> + F = fun() -> + case mnesia:read(booking, Id) of + [] -> + {error, not_found}; + [Booking] -> + Updated = Booking#booking{ + status = Status, + confirmed_at = case Status of + confirmed -> calendar:universal_time(); + _ -> Booking#booking.confirmed_at + end, + updated_at = calendar:universal_time() + }, + mnesia:write(Updated), + {ok, Updated} + end + end, + + case mnesia:transaction(F) of + {atomic, Result} -> Result; + {aborted, Reason} -> {error, Reason} + end. + +%% Удаление бронирования (hard delete) +delete(Id) -> + F = fun() -> + case mnesia:read(booking, Id) of + [] -> + {error, not_found}; + [Booking] -> + mnesia:delete_object(Booking), + {ok, deleted} + end + end, + + case mnesia:transaction(F) of + {atomic, Result} -> Result; + {aborted, Reason} -> {error, Reason} + end. + +%% Внутренние функции +generate_id() -> + base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}). \ No newline at end of file diff --git a/src/core/core_calendar.erl b/src/core/core_calendar.erl index fe5ded1..8008954 100644 --- a/src/core/core_calendar.erl +++ b/src/core/core_calendar.erl @@ -1,11 +1,11 @@ -module(core_calendar). -include("records.hrl"). --export([create/3, get_by_id/1, list_by_owner/1, update/2, delete/1]). +-export([create/4, get_by_id/1, list_by_owner/1, update/2, delete/1]). -export([generate_id/0]). %% Создание календаря -create(OwnerId, Title, Description) -> +create(OwnerId, Title, Description, Confirmation) -> Id = generate_id(), Calendar = #calendar{ id = Id, @@ -14,7 +14,7 @@ create(OwnerId, Title, Description) -> description = Description, tags = [], type = personal, - confirmation = manual, + confirmation = Confirmation, rating_avg = 0.0, rating_count = 0, status = active, diff --git a/src/eventhub_app.erl b/src/eventhub_app.erl index d0db74a..c9a2e6f 100644 --- a/src/eventhub_app.erl +++ b/src/eventhub_app.erl @@ -37,12 +37,15 @@ start_http() -> {"/v1/login", handler_login, []}, {"/v1/refresh", handler_refresh, []}, {"/v1/user/me", handler_user_me, []}, + {"/v1/user/bookings", handler_user_bookings, []}, {"/v1/calendars", handler_calendars, []}, {"/v1/calendars/:id", handler_calendar_by_id, []}, {"/v1/calendars/:calendar_id/events", handler_events, []}, {"/v1/events/:id", handler_event_by_id, []}, {"/v1/events/:id/occurrences", handler_event_occurrences, []}, - {"/v1/events/:id/occurrences/:start_time", handler_event_occurrences, []} + {"/v1/events/:id/occurrences/:start_time", handler_event_occurrences, []}, + {"/v1/events/:id/bookings", handler_bookings, []}, + {"/v1/bookings/:id", handler_booking_by_id, []} ]} ]), diff --git a/src/handlers/handler_booking_by_id.erl b/src/handlers/handler_booking_by_id.erl new file mode 100644 index 0000000..b16f8c2 --- /dev/null +++ b/src/handlers/handler_booking_by_id.erl @@ -0,0 +1,128 @@ +-module(handler_booking_by_id). +-include("records.hrl"). + +-export([init/2]). + +init(Req, Opts) -> + handle(Req, Opts). + +handle(Req, _Opts) -> + case cowboy_req:method(Req) of + <<"GET">> -> get_booking(Req); + <<"PUT">> -> update_booking(Req); + <<"DELETE">> -> cancel_booking(Req); + _ -> send_error(Req, 405, <<"Method not allowed">>) + end. + +%% GET /v1/bookings/:id - получение бронирования +get_booking(Req) -> + case handler_auth:authenticate(Req) of + {ok, UserId, Req1} -> + BookingId = cowboy_req:binding(id, Req1), + case logic_booking:get_booking(UserId, BookingId) of + {ok, Booking} -> + Response = booking_to_json(Booking), + send_json(Req1, 200, Response); + {error, access_denied} -> + send_error(Req1, 403, <<"Access denied">>); + {error, not_found} -> + send_error(Req1, 404, <<"Booking not found">>); + {error, _} -> + send_error(Req1, 500, <<"Internal server error">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% PUT /v1/bookings/:id - подтверждение/отклонение бронирования (владельцем) +update_booking(Req) -> + case handler_auth:authenticate(Req) of + {ok, UserId, Req1} -> + BookingId = cowboy_req:binding(id, Req1), + {ok, Body, Req2} = cowboy_req:read_body(Req1), + try jsx:decode(Body, [return_maps]) of + Decoded when is_map(Decoded) -> + case maps:get(<<"action">>, Decoded, undefined) of + <<"confirm">> -> + case logic_booking:confirm_booking(UserId, BookingId, confirm) of + {ok, Booking} -> + Response = booking_to_json(Booking), + send_json(Req2, 200, Response); + {error, access_denied} -> + send_error(Req2, 403, <<"Access denied">>); + {error, not_found} -> + send_error(Req2, 404, <<"Booking not found">>); + {error, _} -> + send_error(Req2, 500, <<"Internal server error">>) + end; + <<"decline">> -> + case logic_booking:confirm_booking(UserId, BookingId, decline) of + {ok, Booking} -> + Response = booking_to_json(Booking), + send_json(Req2, 200, Response); + {error, access_denied} -> + send_error(Req2, 403, <<"Access denied">>); + {error, not_found} -> + send_error(Req2, 404, <<"Booking not found">>); + {error, _} -> + send_error(Req2, 500, <<"Internal server error">>) + end; + _ -> + send_error(Req2, 400, <<"Missing or invalid 'action' field. Use 'confirm' or 'decline'">>) + end; + _ -> + send_error(Req2, 400, <<"Invalid JSON">>) + catch + _:_ -> + send_error(Req2, 400, <<"Invalid JSON format">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% DELETE /v1/bookings/:id - отмена бронирования (участником) +cancel_booking(Req) -> + case handler_auth:authenticate(Req) of + {ok, UserId, Req1} -> + BookingId = cowboy_req:binding(id, Req1), + case logic_booking:cancel_booking(UserId, BookingId) of + {ok, Booking} -> + Response = booking_to_json(Booking), + send_json(Req1, 200, Response); + {error, access_denied} -> + send_error(Req1, 403, <<"Access denied">>); + {error, not_found} -> + send_error(Req1, 404, <<"Booking not found">>); + {error, _} -> + send_error(Req1, 500, <<"Internal server error">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% Вспомогательные функции +booking_to_json(Booking) -> + #{ + id => Booking#booking.id, + event_id => Booking#booking.event_id, + user_id => Booking#booking.user_id, + status => Booking#booking.status, + confirmed_at => case Booking#booking.confirmed_at of + undefined -> null; + Dt -> datetime_to_iso8601(Dt) + end, + created_at => datetime_to_iso8601(Booking#booking.created_at), + updated_at => datetime_to_iso8601(Booking#booking.updated_at) + }. + +datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) -> + iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", + [Year, Month, Day, Hour, Minute, Second])). + +send_json(Req, Status, Data) -> + Body = jsx:encode(Data), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). + +send_error(Req, Status, Message) -> + Body = jsx:encode(#{error => Message}), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). \ No newline at end of file diff --git a/src/handlers/handler_bookings.erl b/src/handlers/handler_bookings.erl new file mode 100644 index 0000000..b78a5b4 --- /dev/null +++ b/src/handlers/handler_bookings.erl @@ -0,0 +1,87 @@ +-module(handler_bookings). +-include("records.hrl"). + +-export([init/2]). + +init(Req, Opts) -> + handle(Req, Opts). + +handle(Req, _Opts) -> + case cowboy_req:method(Req) of + <<"POST">> -> create_booking(Req); + <<"GET">> -> list_bookings(Req); + _ -> send_error(Req, 405, <<"Method not allowed">>) + end. + +%% POST /v1/events/:id/bookings - создание бронирования (запись на событие) +create_booking(Req) -> + case handler_auth:authenticate(Req) of + {ok, UserId, Req1} -> + EventId = cowboy_req:binding(id, Req1), + case logic_booking:create_booking(UserId, EventId) of + {ok, Booking} -> + Response = booking_to_json(Booking), + send_json(Req1, 201, Response); + {error, already_booked} -> + send_error(Req1, 409, <<"Already booked">>); + {error, event_full} -> + send_error(Req1, 400, <<"Event is full">>); + {error, event_not_active} -> + send_error(Req1, 400, <<"Event is not active">>); + {error, access_denied} -> + send_error(Req1, 403, <<"Access denied">>); + {error, not_found} -> + send_error(Req1, 404, <<"Event not found">>); + {error, _} -> + send_error(Req1, 500, <<"Internal server error">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% GET /v1/events/:id/bookings - список бронирований события (для владельца) +list_bookings(Req) -> + case handler_auth:authenticate(Req) of + {ok, UserId, Req1} -> + EventId = cowboy_req:binding(id, Req1), + case logic_booking:list_event_bookings(UserId, EventId) of + {ok, Bookings} -> + Response = [booking_to_json(B) || B <- Bookings], + send_json(Req1, 200, Response); + {error, access_denied} -> + send_error(Req1, 403, <<"Access denied">>); + {error, not_found} -> + send_error(Req1, 404, <<"Event not found">>); + {error, _} -> + send_error(Req1, 500, <<"Internal server error">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% Вспомогательные функции +booking_to_json(Booking) -> + #{ + id => Booking#booking.id, + event_id => Booking#booking.event_id, + user_id => Booking#booking.user_id, + status => Booking#booking.status, + confirmed_at => case Booking#booking.confirmed_at of + undefined -> null; + Dt -> datetime_to_iso8601(Dt) + end, + created_at => datetime_to_iso8601(Booking#booking.created_at), + updated_at => datetime_to_iso8601(Booking#booking.updated_at) + }. + +datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) -> + iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", + [Year, Month, Day, Hour, Minute, Second])). + +send_json(Req, Status, Data) -> + Body = jsx:encode(Data), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). + +send_error(Req, Status, Message) -> + Body = jsx:encode(#{error => Message}), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). \ No newline at end of file diff --git a/src/handlers/handler_calendars.erl b/src/handlers/handler_calendars.erl index 0a441ad..4a5b593 100644 --- a/src/handlers/handler_calendars.erl +++ b/src/handlers/handler_calendars.erl @@ -23,7 +23,8 @@ create_calendar(Req) -> case Decoded of #{<<"title">> := Title} -> Description = maps:get(<<"description">>, Decoded, <<"">>), - case logic_calendar:create_calendar(UserId, Title, Description) of + Confirmation = parse_confirmation(maps:get(<<"confirmation">>, Decoded, <<"manual">>)), + case logic_calendar:create_calendar(UserId, Title, Description, Confirmation) of {ok, Calendar} -> Response = calendar_to_json(Calendar), send_json(Req2, 201, Response); @@ -45,6 +46,11 @@ create_calendar(Req) -> send_error(Req1, Code, Message) end. +parse_confirmation(<<"auto">>) -> auto; +parse_confirmation(<<"manual">>) -> manual; +parse_confirmation(#{<<"timeout">> := N}) when is_integer(N), N > 0 -> {timeout, N}; +parse_confirmation(_) -> manual. + %% GET /v1/calendars - список календарей list_calendars(Req) -> case handler_auth:authenticate(Req) of diff --git a/src/handlers/handler_login.erl b/src/handlers/handler_login.erl index dfa9bb3..cff928d 100644 --- a/src/handlers/handler_login.erl +++ b/src/handlers/handler_login.erl @@ -9,41 +9,54 @@ init(Req, Opts) -> handle(Req, _Opts) -> case cowboy_req:method(Req) of <<"POST">> -> - {ok, Body, Req1} = cowboy_req:read_body(Req), - case jsx:decode(Body, [return_maps]) of - #{<<"email">> := Email, <<"password">> := Password} -> - case core_user:get_by_email(Email) of - {ok, User} -> - case logic_auth:verify_password(Password, User#user.password_hash) of - {ok, true} -> - case User#user.status of - active -> - Token = logic_auth:generate_jwt(User#user.id, User#user.role), - {RefreshToken, ExpiresAt} = logic_auth:generate_refresh_token(User#user.id), - save_refresh_token(User#user.id, RefreshToken, ExpiresAt), - Response = #{ - user => #{ - id => User#user.id, - email => User#user.email, - role => User#user.role - }, - token => Token, - refresh_token => RefreshToken - }, - send_json(Req1, 200, Response); - frozen -> - send_error(Req1, 403, <<"Account frozen">>); - deleted -> - send_error(Req1, 403, <<"Account deleted">>) + case cowboy_req:has_body(Req) of + true -> + {ok, Body, Req1} = cowboy_req:read_body(Req), + case Body of + <<>> -> + send_error(Req1, 400, <<"Empty request body">>); + _ -> + try jsx:decode(Body, [return_maps]) of + #{<<"email">> := Email, <<"password">> := Password} -> + case core_user:get_by_email(Email) of + {ok, User} -> + case logic_auth:verify_password(Password, User#user.password_hash) of + {ok, true} -> + case User#user.status of + active -> + Token = logic_auth:generate_jwt(User#user.id, User#user.role), + {RefreshToken, ExpiresAt} = logic_auth:generate_refresh_token(User#user.id), + save_refresh_token(User#user.id, RefreshToken, ExpiresAt), + Response = #{ + user => #{ + id => User#user.id, + email => User#user.email, + role => User#user.role + }, + token => Token, + refresh_token => RefreshToken + }, + send_json(Req1, 200, Response); + frozen -> + send_error(Req1, 403, <<"Account frozen">>); + deleted -> + send_error(Req1, 403, <<"Account deleted">>) + end; + _ -> + send_error(Req1, 401, <<"Invalid credentials">>) + end; + {error, not_found} -> + send_error(Req1, 401, <<"Invalid credentials">>) end; _ -> - send_error(Req1, 401, <<"Invalid credentials">>) - end; - {error, not_found} -> - send_error(Req1, 401, <<"Invalid credentials">>) + send_error(Req1, 400, <<"Missing email or password">>) + catch + _:_ -> + send_error(Req1, 400, <<"Invalid JSON">>) + end end; - _ -> - send_error(Req1, 400, <<"Invalid request body">>) + false -> + send_error(Req, 400, <<"Missing request body">>) end; _ -> send_error(Req, 405, <<"Method not allowed">>) diff --git a/src/handlers/handler_register.erl b/src/handlers/handler_register.erl index 3ad3ee8..fb509a4 100644 --- a/src/handlers/handler_register.erl +++ b/src/handlers/handler_register.erl @@ -9,31 +9,46 @@ init(Req, Opts) -> handle(Req, _Opts) -> case cowboy_req:method(Req) of <<"POST">> -> - {ok, Body, Req1} = cowboy_req:read_body(Req), - case jsx:decode(Body, [return_maps]) of - #{<<"email">> := Email, <<"password">> := Password} -> - case core_user:email_exists(Email) of - true -> - send_error(Req1, 409, <<"Email already exists">>); - false -> - case core_user:create(Email, Password) of - {ok, User} -> - Token = logic_auth:generate_jwt(User#user.id, User#user.role), - Response = #{ - user => #{ - id => User#user.id, - email => User#user.email, - role => User#user.role - }, - token => Token - }, - send_json(Req1, 201, Response); - {error, _} -> - send_error(Req1, 500, <<"Internal server error">>) + case cowboy_req:has_body(Req) of + true -> + {ok, Body, Req1} = cowboy_req:read_body(Req), + case Body of + <<>> -> + send_error(Req1, 400, <<"Empty request body">>); + _ -> + try jsx:decode(Body, [return_maps]) of + #{<<"email">> := Email, <<"password">> := Password} -> + case core_user:email_exists(Email) of + true -> + send_error(Req1, 409, <<"Email already exists">>); + false -> + case core_user:create(Email, Password) of + {ok, User} -> + Token = logic_auth:generate_jwt(User#user.id, User#user.role), + Response = #{ + user => #{ + id => User#user.id, + email => User#user.email, + role => User#user.role + }, + token => Token + }, + send_json(Req1, 201, Response); + {error, email_exists} -> + send_error(Req1, 409, <<"Email already exists">>); + {error, _} -> + send_error(Req1, 500, <<"Internal server error">>) + end + end; + _ -> + send_error(Req1, 400, <<"Missing email or password">>) + catch + _:_ -> + send_error(Req1, 400, <<"Invalid JSON">>) end end; - _ -> - send_error(Req1, 400, <<"Invalid request body">>) + false -> + send_error(Req, 400, <<"Missing request body">>) end; _ -> send_error(Req, 405, <<"Method not allowed">>) diff --git a/src/handlers/handler_user_bookings.erl b/src/handlers/handler_user_bookings.erl new file mode 100644 index 0000000..9b2a623 --- /dev/null +++ b/src/handlers/handler_user_bookings.erl @@ -0,0 +1,55 @@ +-module(handler_user_bookings). +-include("records.hrl"). + +-export([init/2]). + +init(Req, Opts) -> + handle(Req, Opts). + +handle(Req, _Opts) -> + case cowboy_req:method(Req) of + <<"GET">> -> list_user_bookings(Req); + _ -> send_error(Req, 405, <<"Method not allowed">>) + end. + +%% GET /v1/user/bookings - список бронирований текущего пользователя +list_user_bookings(Req) -> + case handler_auth:authenticate(Req) of + {ok, UserId, Req1} -> + case logic_booking:list_user_bookings(UserId) of + {ok, Bookings} -> + Response = [booking_to_json(B) || B <- Bookings], + send_json(Req1, 200, Response); + {error, _} -> + send_error(Req1, 500, <<"Internal server error">>) + end; + {error, Code, Message, Req1} -> + send_error(Req1, Code, Message) + end. + +%% Вспомогательные функции +booking_to_json(Booking) -> + #{ + id => Booking#booking.id, + event_id => Booking#booking.event_id, + user_id => Booking#booking.user_id, + status => Booking#booking.status, + confirmed_at => case Booking#booking.confirmed_at of + undefined -> null; + Dt -> datetime_to_iso8601(Dt) + end, + created_at => datetime_to_iso8601(Booking#booking.created_at), + updated_at => datetime_to_iso8601(Booking#booking.updated_at) + }. + +datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) -> + iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", + [Year, Month, Day, Hour, Minute, Second])). + +send_json(Req, Status, Data) -> + Body = jsx:encode(Data), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). + +send_error(Req, Status, Message) -> + Body = jsx:encode(#{error => Message}), + cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req). \ No newline at end of file diff --git a/src/logic/logic_booking.erl b/src/logic/logic_booking.erl new file mode 100644 index 0000000..f5cade8 --- /dev/null +++ b/src/logic/logic_booking.erl @@ -0,0 +1,189 @@ +-module(logic_booking). +-include("records.hrl"). + +-export([create_booking/2, confirm_booking/3, cancel_booking/2]). +-export([get_booking/2, list_event_bookings/2, list_user_bookings/1]). +-export([auto_confirm/1, check_timeout_confirmations/0]). + +%% Создание бронирования (запись на событие) +create_booking(UserId, EventId) -> + % Получаем событие напрямую, без проверки доступа к календарю + case core_event:get_by_id(EventId) of + {ok, Event} -> + % Проверяем, что событие активно + case Event#event.status of + active -> + % Проверяем календарь для политики подтверждения + case core_calendar:get_by_id(Event#event.calendar_id) of + {ok, Calendar} -> + case Calendar#calendar.status of + active -> + % Проверяем, что есть места + case check_capacity(EventId, Event#event.capacity) of + {ok, _} -> + % Проверяем, не записан ли уже пользователь + case core_booking:get_by_event_and_user(EventId, UserId) of + {error, not_found} -> + ActualEventId = get_actual_event_id(Event, UserId), + case core_booking:create(ActualEventId, UserId) of + {ok, Booking} -> + handle_confirmation_policy(Booking, Event, Calendar), + {ok, Booking}; + Error -> + Error + end; + {ok, _} -> + {error, already_booked} + end; + {error, full} -> + {error, event_full} + end; + _ -> + {error, calendar_not_active} + end; + _ -> + {error, calendar_not_found} + end; + _ -> + {error, event_not_active} + end; + Error -> + Error + end. + +%% Подтверждение бронирования (владельцем календаря) +confirm_booking(UserId, BookingId, Action) when Action =:= confirm; Action =:= decline -> + case core_booking:get_by_id(BookingId) of + {ok, Booking} -> + % Проверяем права на событие + case logic_event:get_event(UserId, Booking#booking.event_id) of + {ok, Event} -> + % Проверяем, что пользователь может редактировать календарь + case logic_calendar:get_calendar(UserId, Event#event.calendar_id) of + {ok, Calendar} -> + case logic_calendar:can_edit(UserId, Calendar) of + true -> + case Action of + confirm -> + core_booking:update_status(BookingId, confirmed); + decline -> + core_booking:update_status(BookingId, cancelled) + end; + false -> + {error, access_denied} + end; + Error -> + Error + end; + Error -> + Error + end; + Error -> + Error + end. + +%% Отмена бронирования (участником) +cancel_booking(UserId, BookingId) -> + case core_booking:get_by_id(BookingId) of + {ok, Booking} -> + % Проверяем, что это бронирование пользователя + case Booking#booking.user_id =:= UserId of + true -> + core_booking:update_status(BookingId, cancelled); + false -> + {error, access_denied} + end; + Error -> + Error + end. + +%% Получение бронирования +get_booking(UserId, BookingId) -> + case core_booking:get_by_id(BookingId) of + {ok, Booking} -> + % Проверяем доступ к событию + case logic_event:get_event(UserId, Booking#booking.event_id) of + {ok, _} -> {ok, Booking}; + Error -> Error + end; + Error -> + Error + end. + +%% Список бронирований события (для владельца) +list_event_bookings(UserId, EventId) -> + case logic_event:get_event(UserId, EventId) of + {ok, Event} -> + % Проверяем права на календарь + case logic_calendar:get_calendar(UserId, Event#event.calendar_id) of + {ok, Calendar} -> + case logic_calendar:can_edit(UserId, Calendar) of + true -> + core_booking:list_by_event(EventId); + false -> + {error, access_denied} + end; + Error -> + Error + end; + Error -> + Error + end. + +%% Список бронирований пользователя +list_user_bookings(UserId) -> + core_booking:list_by_user(UserId). + +%% Автоматическое подтверждение (для политики auto) +auto_confirm(BookingId) -> + core_booking:update_status(BookingId, confirmed). + +%% Проверка истечения timeout подтверждений +check_timeout_confirmations() -> + % Получаем все pending бронирования для календарей с timeout + % В реальной реализации нужно периодически вызывать эту функцию + ok. + +%% Внутренние функции +check_capacity(_EventId, undefined) -> + {ok, unlimited}; +check_capacity(EventId, Capacity) -> + {ok, Bookings} = core_booking:list_by_event(EventId), + ConfirmedCount = length([B || B <- Bookings, B#booking.status =:= confirmed]), + case ConfirmedCount < Capacity of + true -> {ok, Capacity - ConfirmedCount}; + false -> {error, full} + end. + +get_actual_event_id(Event, _UserId) -> + case Event#event.event_type of + recurring -> + % Для повторяющихся событий нужно материализовать вхождение + % Здесь предполагается, что start_time передаётся в запросе + % В полной реализации нужно получать occurrence_start из параметров + Event#event.id; + single -> + Event#event.id + end. + +handle_confirmation_policy(Booking, _Event, Calendar) -> + io:format("Confirmation policy: ~p~n", [Calendar#calendar.confirmation]), + case Calendar#calendar.confirmation of + auto -> + io:format("Auto-confirming booking ~p~n", [Booking#booking.id]), + auto_confirm(Booking#booking.id); + manual -> + io:format("Manual confirmation, leaving pending~n"), + ok; + {timeout, Seconds} -> + io:format("Timeout confirmation: ~p seconds~n", [Seconds]), + spawn(fun() -> + timer:sleep(Seconds * 1000), + case core_booking:get_by_id(Booking#booking.id) of + {ok, B} when B#booking.status =:= pending -> + auto_confirm(Booking#booking.id); + _ -> + ok + end + end) + end. \ No newline at end of file diff --git a/src/logic/logic_calendar.erl b/src/logic/logic_calendar.erl index 81d5303..e6979b9 100644 --- a/src/logic/logic_calendar.erl +++ b/src/logic/logic_calendar.erl @@ -1,18 +1,17 @@ -module(logic_calendar). -include("records.hrl"). --export([create_calendar/3, get_calendar/2, list_calendars/1, +-export([create_calendar/4, get_calendar/2, list_calendars/1, update_calendar/3, delete_calendar/2]). -export([can_access/2, can_edit/2]). %% Создание календаря -create_calendar(UserId, Title, Description) -> - % Проверка, что пользователь может создавать календари +create_calendar(UserId, Title, Description, Confirmation) -> case core_user:get_by_id(UserId) of {ok, User} -> case User#user.status of active -> - core_calendar:create(UserId, Title, Description); + core_calendar:create(UserId, Title, Description, Confirmation); _ -> {error, user_inactive} end; diff --git a/test/booking_integration_tests.erl b/test/booking_integration_tests.erl new file mode 100644 index 0000000..d493723 --- /dev/null +++ b/test/booking_integration_tests.erl @@ -0,0 +1,148 @@ +-module(booking_integration_tests). +-include_lib("eunit/include/eunit.hrl"). +-include("records.hrl"). + +setup() -> + mnesia:start(), + mnesia:create_table(user, [ + {attributes, record_info(fields, user)}, + {ram_copies, [node()]} + ]), + mnesia:create_table(calendar, [ + {attributes, record_info(fields, calendar)}, + {ram_copies, [node()]} + ]), + mnesia:create_table(event, [ + {attributes, record_info(fields, event)}, + {ram_copies, [node()]} + ]), + mnesia:create_table(booking, [ + {attributes, record_info(fields, booking)}, + {ram_copies, [node()]} + ]), + ok. + +cleanup(_) -> + mnesia:delete_table(booking), + mnesia:delete_table(event), + mnesia:delete_table(calendar), + mnesia:delete_table(user), + mnesia:stop(), + ok. + +booking_integration_test_() -> + {foreach, + fun setup/0, + fun cleanup/1, + [ + {"Full booking flow with auto confirmation", fun test_auto_booking_flow/0}, + {"Full booking flow with manual confirmation", fun test_manual_booking_flow/0}, + {"Capacity management test", fun test_capacity_management/0}, + {"Multiple bookings test", fun test_multiple_bookings/0} + ]}. + +create_user() -> + UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}), + User = #user{ + id = UserId, + email = <>, + password_hash = <<"hash">>, + role = user, + status = active, + created_at = calendar:universal_time(), + updated_at = calendar:universal_time() + }, + mnesia:dirty_write(User), + UserId. + +test_auto_booking_flow() -> + OwnerId = create_user(), + ParticipantId = create_user(), + + {ok, Calendar} = core_calendar:create(OwnerId, <<"Auto">>, <<"">>, auto), + + StartTime = {{2026, 6, 1}, {10, 0, 0}}, + {ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, Event#event.id), + timer:sleep(100), + + {ok, Updated} = core_booking:get_by_id(Booking#booking.id), + ?assertEqual(confirmed, Updated#booking.status), + + {ok, EventBookings} = logic_booking:list_event_bookings(OwnerId, Event#event.id), + ?assertEqual(1, length(EventBookings)), + + {ok, UserBookings} = logic_booking:list_user_bookings(ParticipantId), + ?assertEqual(1, length(UserBookings)). + +test_manual_booking_flow() -> + OwnerId = create_user(), + ParticipantId = create_user(), + + {ok, Calendar} = core_calendar:create(OwnerId, <<"Manual">>, <<"">>, manual), + + StartTime = {{2026, 6, 1}, {10, 0, 0}}, + {ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, Event#event.id), + ?assertEqual(pending, Booking#booking.status), + + {ok, Confirmed} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm), + ?assertEqual(confirmed, Confirmed#booking.status), + + {ok, Cancelled} = logic_booking:cancel_booking(ParticipantId, Booking#booking.id), + ?assertEqual(cancelled, Cancelled#booking.status). + +test_capacity_management() -> + OwnerId = create_user(), + Participant1Id = create_user(), + Participant2Id = create_user(), + Participant3Id = create_user(), + + {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, auto), + + StartTime = {{2026, 6, 1}, {10, 0, 0}}, + {ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60), + {ok, _} = core_event:update(Event#event.id, [{capacity, 2}]), + + {ok, Booking1} = logic_booking:create_booking(Participant1Id, Event#event.id), + {ok, _Booking2} = logic_booking:create_booking(Participant2Id, Event#event.id), + + {error, event_full} = logic_booking:create_booking(Participant3Id, Event#event.id), + + % Участник 1 отменяет своё бронирование + {ok, _} = logic_booking:cancel_booking(Participant1Id, Booking1#booking.id), + + % Теперь третий может записаться + {ok, _} = logic_booking:create_booking(Participant3Id, Event#event.id). + +test_multiple_bookings() -> + OwnerId = create_user(), + ParticipantId = create_user(), + + {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual), + + StartTime1 = {{2026, 6, 1}, {10, 0, 0}}, + StartTime2 = {{2026, 6, 2}, {10, 0, 0}}, + StartTime3 = {{2026, 6, 3}, {10, 0, 0}}, + + {ok, Event1} = core_event:create(Calendar#calendar.id, <<"Event1">>, StartTime1, 60), + {ok, Event2} = core_event:create(Calendar#calendar.id, <<"Event2">>, StartTime2, 60), + {ok, Event3} = core_event:create(Calendar#calendar.id, <<"Event3">>, StartTime3, 60), + + {ok, B1} = logic_booking:create_booking(ParticipantId, Event1#event.id), + {ok, B2} = logic_booking:create_booking(ParticipantId, Event2#event.id), + {ok, _B3} = logic_booking:create_booking(ParticipantId, Event3#event.id), + + {ok, _} = logic_booking:confirm_booking(OwnerId, B1#booking.id, confirm), + {ok, _} = logic_booking:confirm_booking(OwnerId, B2#booking.id, confirm), + + {ok, UserBookings} = logic_booking:list_user_bookings(ParticipantId), + ?assertEqual(3, length(UserBookings)), + + ConfirmedCount = length([B || B <- UserBookings, B#booking.status =:= confirmed]), + ?assertEqual(2, ConfirmedCount), + + PendingCount = length([B || B <- UserBookings, B#booking.status =:= pending]), + ?assertEqual(1, PendingCount). \ No newline at end of file diff --git a/test/core_booking_tests.erl b/test/core_booking_tests.erl new file mode 100644 index 0000000..7b8abf5 --- /dev/null +++ b/test/core_booking_tests.erl @@ -0,0 +1,126 @@ +-module(core_booking_tests). +-include_lib("eunit/include/eunit.hrl"). +-include("records.hrl"). + +setup() -> + mnesia:start(), + mnesia:create_table(booking, [ + {attributes, record_info(fields, booking)}, + {ram_copies, [node()]} + ]), + ok. + +cleanup(_) -> + mnesia:delete_table(booking), + mnesia:stop(), + ok. + +core_booking_test_() -> + {foreach, + fun setup/0, + fun cleanup/1, + [ + {"Create booking test", fun test_create_booking/0}, + {"Get booking by id test", fun test_get_by_id/0}, + {"Get booking by event and user test", fun test_get_by_event_and_user/0}, + {"List bookings by event test", fun test_list_by_event/0}, + {"List bookings by user test", fun test_list_by_user/0}, + {"Update booking status test", fun test_update_status/0}, + {"Delete booking test", fun test_delete_booking/0} + ]}. + +test_create_booking() -> + EventId = <<"event123">>, + UserId = <<"user123">>, + + {ok, Booking} = core_booking:create(EventId, UserId), + + ?assertEqual(EventId, Booking#booking.event_id), + ?assertEqual(UserId, Booking#booking.user_id), + ?assertEqual(pending, Booking#booking.status), + ?assertEqual(undefined, Booking#booking.confirmed_at), + ?assert(is_binary(Booking#booking.id)), + ?assert(Booking#booking.created_at =/= undefined), + ?assert(Booking#booking.updated_at =/= undefined). + +test_get_by_id() -> + EventId = <<"event123">>, + UserId = <<"user123">>, + {ok, Booking} = core_booking:create(EventId, UserId), + + {ok, Found} = core_booking:get_by_id(Booking#booking.id), + ?assertEqual(Booking#booking.id, Found#booking.id), + + {error, not_found} = core_booking:get_by_id(<<"nonexistent">>). + +test_get_by_event_and_user() -> + EventId = <<"event123">>, + UserId1 = <<"user1">>, + UserId2 = <<"user2">>, + + {ok, Booking1} = core_booking:create(EventId, UserId1), + {ok, _Booking2} = core_booking:create(EventId, UserId2), + + {ok, Found} = core_booking:get_by_event_and_user(EventId, UserId1), + ?assertEqual(Booking1#booking.id, Found#booking.id), + + {error, not_found} = core_booking:get_by_event_and_user(EventId, <<"user3">>). + +test_list_by_event() -> + EventId1 = <<"event1">>, + EventId2 = <<"event2">>, + UserId = <<"user123">>, + + {ok, _} = core_booking:create(EventId1, UserId), + {ok, _} = core_booking:create(EventId1, <<"user2">>), + {ok, _} = core_booking:create(EventId2, UserId), + + {ok, Bookings1} = core_booking:list_by_event(EventId1), + ?assertEqual(2, length(Bookings1)), + + {ok, Bookings2} = core_booking:list_by_event(EventId2), + ?assertEqual(1, length(Bookings2)). + +test_list_by_user() -> + EventId = <<"event123">>, + UserId1 = <<"user1">>, + UserId2 = <<"user2">>, + + {ok, _} = core_booking:create(EventId, UserId1), + {ok, _} = core_booking:create(EventId, UserId1), + {ok, _} = core_booking:create(EventId, UserId2), + + {ok, Bookings1} = core_booking:list_by_user(UserId1), + ?assertEqual(2, length(Bookings1)), + + {ok, Bookings2} = core_booking:list_by_user(UserId2), + ?assertEqual(1, length(Bookings2)). + +test_update_status() -> + EventId = <<"event123">>, + UserId = <<"user123">>, + {ok, Booking} = core_booking:create(EventId, UserId), + + timer:sleep(2000), % 2 секунды + + {ok, Confirmed} = core_booking:update_status(Booking#booking.id, confirmed), + ?assertEqual(confirmed, Confirmed#booking.status), + ?assert(Confirmed#booking.confirmed_at =/= undefined), + ?assert(Confirmed#booking.updated_at > Booking#booking.updated_at), + + timer:sleep(2000), % 2 секунды + + {ok, Cancelled} = core_booking:update_status(Booking#booking.id, cancelled), + ?assertEqual(cancelled, Cancelled#booking.status), + ?assert(Cancelled#booking.updated_at > Confirmed#booking.updated_at), + + {error, not_found} = core_booking:update_status(<<"nonexistent">>, confirmed). + +test_delete_booking() -> + EventId = <<"event123">>, + UserId = <<"user123">>, + {ok, Booking} = core_booking:create(EventId, UserId), + + {ok, deleted} = core_booking:delete(Booking#booking.id), + + {error, not_found} = core_booking:get_by_id(Booking#booking.id). \ No newline at end of file diff --git a/test/core_calendar_tests.erl b/test/core_calendar_tests.erl index 08566da..5bcf59f 100644 --- a/test/core_calendar_tests.erl +++ b/test/core_calendar_tests.erl @@ -2,7 +2,6 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). -%% Setup и cleanup setup() -> mnesia:start(), mnesia:create_table(calendar, [ @@ -16,7 +15,6 @@ cleanup(_) -> mnesia:stop(), ok. -%% Группа тестов core_calendar_test_() -> {foreach, fun setup/0, @@ -29,18 +27,19 @@ core_calendar_test_() -> {"Delete calendar test", fun test_delete_calendar/0} ]}. -%% Тесты test_create_calendar() -> OwnerId = <<"owner123">>, Title = <<"Test Calendar">>, Description = <<"Test Description">>, + Confirmation = auto, - {ok, Calendar} = core_calendar:create(OwnerId, Title, Description), + {ok, Calendar} = core_calendar:create(OwnerId, Title, Description, Confirmation), ?assertEqual(OwnerId, Calendar#calendar.owner_id), ?assertEqual(Title, Calendar#calendar.title), ?assertEqual(Description, Calendar#calendar.description), ?assertEqual(personal, Calendar#calendar.type), + ?assertEqual(Confirmation, Calendar#calendar.confirmation), ?assertEqual(active, Calendar#calendar.status), ?assert(is_binary(Calendar#calendar.id)), ?assert(Calendar#calendar.created_at =/= undefined), @@ -48,7 +47,7 @@ test_create_calendar() -> test_get_by_id() -> OwnerId = <<"owner123">>, - {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"Desc">>), + {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"Desc">>, manual), {ok, Found} = core_calendar:get_by_id(Calendar#calendar.id), ?assertEqual(Calendar#calendar.id, Found#calendar.id), @@ -59,33 +58,33 @@ test_list_by_owner() -> OwnerId = <<"owner123">>, OtherOwner = <<"other456">>, - {ok, _} = core_calendar:create(OwnerId, <<"Calendar 1">>, <<"">>), - {ok, _} = core_calendar:create(OwnerId, <<"Calendar 2">>, <<"">>), - {ok, _} = core_calendar:create(OtherOwner, <<"Other Calendar">>, <<"">>), + {ok, _} = core_calendar:create(OwnerId, <<"Calendar 1">>, <<"">>, manual), + {ok, _} = core_calendar:create(OwnerId, <<"Calendar 2">>, <<"">>, auto), + {ok, _} = core_calendar:create(OtherOwner, <<"Other Calendar">>, <<"">>, manual), {ok, Calendars} = core_calendar:list_by_owner(OwnerId), ?assertEqual(2, length(Calendars)). test_update_calendar() -> OwnerId = <<"owner123">>, - {ok, Calendar} = core_calendar:create(OwnerId, <<"Original">>, <<"">>), + {ok, Calendar} = core_calendar:create(OwnerId, <<"Original">>, <<"">>, manual), timer:sleep(2000), - Updates = [{title, <<"Updated">>}, {description, <<"New Desc">>}], + Updates = [{title, <<"Updated">>}, {description, <<"New Desc">>}, {confirmation, auto}], {ok, Updated} = core_calendar:update(Calendar#calendar.id, Updates), ?assertEqual(<<"Updated">>, Updated#calendar.title), ?assertEqual(<<"New Desc">>, Updated#calendar.description), + ?assertEqual(auto, Updated#calendar.confirmation), ?assert(Updated#calendar.updated_at > Calendar#calendar.updated_at), {error, not_found} = core_calendar:update(<<"nonexistent">>, Updates). test_delete_calendar() -> OwnerId = <<"owner123">>, - {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>), + {ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual), {ok, Deleted} = core_calendar:delete(Calendar#calendar.id), ?assertEqual(deleted, Deleted#calendar.status), - % Удалённый календарь не возвращается в списке активных {ok, ActiveCalendars} = core_calendar:list_by_owner(OwnerId), ?assertEqual(0, length(ActiveCalendars)). \ No newline at end of file diff --git a/test/logic_booking_tests.erl b/test/logic_booking_tests.erl new file mode 100644 index 0000000..5f50e35 --- /dev/null +++ b/test/logic_booking_tests.erl @@ -0,0 +1,212 @@ +-module(logic_booking_tests). +-include_lib("eunit/include/eunit.hrl"). +-include("records.hrl"). + +-define(TEST_EMAIL, <<"test@example.com">>). + +setup() -> + mnesia:start(), + mnesia:create_table(user, [ + {attributes, record_info(fields, user)}, + {ram_copies, [node()]} + ]), + mnesia:create_table(calendar, [ + {attributes, record_info(fields, calendar)}, + {ram_copies, [node()]} + ]), + mnesia:create_table(event, [ + {attributes, record_info(fields, event)}, + {ram_copies, [node()]} + ]), + mnesia:create_table(booking, [ + {attributes, record_info(fields, booking)}, + {ram_copies, [node()]} + ]), + ok. + +cleanup(_) -> + mnesia:delete_table(booking), + mnesia:delete_table(event), + mnesia:delete_table(calendar), + mnesia:delete_table(user), + mnesia:stop(), + ok. + +logic_booking_test_() -> + {foreach, + fun setup/0, + fun cleanup/1, + [ + {"Create booking with auto confirmation", fun test_create_booking_auto/0}, + {"Create booking with manual confirmation", fun test_create_booking_manual/0}, + {"Create booking with timeout confirmation", fun test_create_booking_timeout/0}, + {"Create duplicate booking", fun test_create_duplicate_booking/0}, + {"Create booking for inactive event", fun test_booking_inactive_event/0}, + {"Create booking when event is full", fun test_booking_event_full/0}, + {"Confirm booking by owner", fun test_confirm_booking/0}, + {"Decline booking by owner", fun test_decline_booking/0}, + {"Cancel booking by participant", fun test_cancel_booking/0}, + {"Unauthorized confirm attempt", fun test_unauthorized_confirm/0}, + {"List event bookings", fun test_list_event_bookings/0}, + {"List user bookings", fun test_list_user_bookings/0} + ]}. + +%% Вспомогательные функции +create_test_user(Role) -> + UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}), + User = #user{ + id = UserId, + email = <>, + password_hash = <<"hash">>, + role = Role, + status = active, + created_at = calendar:universal_time(), + updated_at = calendar:universal_time() + }, + mnesia:dirty_write(User), + UserId. + +create_test_calendar(OwnerId, Confirmation) -> + {ok, Calendar} = core_calendar:create(OwnerId, <<"Test Calendar">>, <<"">>, Confirmation), + Calendar#calendar.id. + +create_test_event(CalendarId) -> + StartTime = {{2026, 6, 1}, {10, 0, 0}}, + {ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60), + Event#event.id. + +create_test_event_with_capacity(CalendarId, Capacity) -> + StartTime = {{2026, 6, 1}, {10, 0, 0}}, + {ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60), + {ok, Updated} = core_event:update(Event#event.id, [{capacity, Capacity}]), + Updated#event.id. + +%% Тесты +test_create_booking_auto() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, auto), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + + timer:sleep(100), + {ok, Updated} = core_booking:get_by_id(Booking#booking.id), + ?assertEqual(confirmed, Updated#booking.status). + +test_create_booking_manual() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + ?assertEqual(pending, Booking#booking.status). + +test_create_booking_timeout() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, {timeout, 1}), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + ?assertEqual(pending, Booking#booking.status), + + timer:sleep(1500), + {ok, Updated} = core_booking:get_by_id(Booking#booking.id), + ?assertEqual(confirmed, Updated#booking.status). + +test_create_duplicate_booking() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, _} = logic_booking:create_booking(ParticipantId, EventId), + {error, already_booked} = logic_booking:create_booking(ParticipantId, EventId). + +test_booking_inactive_event() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, _} = core_event:update(EventId, [{status, cancelled}]), + + {error, event_not_active} = logic_booking:create_booking(ParticipantId, EventId). + +test_booking_event_full() -> + OwnerId = create_test_user(user), + Participant1Id = create_test_user(user), + Participant2Id = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, auto), + EventId = create_test_event_with_capacity(CalendarId, 1), + + {ok, _} = logic_booking:create_booking(Participant1Id, EventId), + {error, event_full} = logic_booking:create_booking(Participant2Id, EventId). + +test_confirm_booking() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + {ok, Confirmed} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm), + ?assertEqual(confirmed, Confirmed#booking.status). + +test_decline_booking() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + {ok, Declined} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, decline), + ?assertEqual(cancelled, Declined#booking.status). + +test_cancel_booking() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + {ok, Cancelled} = logic_booking:cancel_booking(ParticipantId, Booking#booking.id), + ?assertEqual(cancelled, Cancelled#booking.status). + +test_unauthorized_confirm() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + OtherId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + {error, access_denied} = logic_booking:confirm_booking(OtherId, Booking#booking.id, confirm). + +test_list_event_bookings() -> + OwnerId = create_test_user(user), + Participant1Id = create_test_user(user), + Participant2Id = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, _} = logic_booking:create_booking(Participant1Id, EventId), + {ok, _} = logic_booking:create_booking(Participant2Id, EventId), + + {ok, Bookings} = logic_booking:list_event_bookings(OwnerId, EventId), + ?assertEqual(2, length(Bookings)). + +test_list_user_bookings() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId1 = create_test_event(CalendarId), + EventId2 = create_test_event(CalendarId), + + {ok, _} = logic_booking:create_booking(ParticipantId, EventId1), + {ok, _} = logic_booking:create_booking(ParticipantId, EventId2), + + {ok, Bookings} = logic_booking:list_user_bookings(ParticipantId), + ?assertEqual(2, length(Bookings)). \ No newline at end of file diff --git a/test/logic_calendar_tests.erl b/test/logic_calendar_tests.erl index 4097ac9..8fdd5c0 100644 --- a/test/logic_calendar_tests.erl +++ b/test/logic_calendar_tests.erl @@ -51,17 +51,18 @@ test_create_calendar() -> UserId = create_test_user(), Title = <<"Test Calendar">>, Description = <<"Test Description">>, + Confirmation = auto, - {ok, Calendar} = logic_calendar:create_calendar(UserId, Title, Description), + {ok, Calendar} = logic_calendar:create_calendar(UserId, Title, Description, Confirmation), ?assertEqual(UserId, Calendar#calendar.owner_id), ?assertEqual(Title, Calendar#calendar.title), - ?assertEqual(personal, Calendar#calendar.type). + ?assertEqual(personal, Calendar#calendar.type), + ?assertEqual(Confirmation, Calendar#calendar.confirmation). test_get_calendar() -> UserId = create_test_user(), - {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test">>, <<"">>), + {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test">>, <<"">>, manual), - % Владелец имеет доступ case logic_calendar:get_calendar(UserId, Calendar#calendar.id) of {ok, Found} -> ?assertEqual(Calendar#calendar.id, Found#calendar.id); @@ -69,77 +70,55 @@ test_get_calendar() -> ?assert(false, {unexpected_result, Other}) end, - % Другой пользователь не имеет доступа к personal календарю OtherUserId = create_test_user(), ?assertMatch({error, access_denied}, - logic_calendar:get_calendar(OtherUserId, Calendar#calendar.id)), - - % Делаем календарь коммерческим - {ok, Commercial} = logic_calendar:update_calendar(UserId, Calendar#calendar.id, [{type, commercial}]), - - % Теперь другой пользователь имеет доступ - {ok, _} = logic_calendar:get_calendar(OtherUserId, Commercial#calendar.id). + logic_calendar:get_calendar(OtherUserId, Calendar#calendar.id)). test_list_calendars() -> UserId = create_test_user(), - {ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 1">>, <<"">>), - {ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 2">>, <<"">>), + {ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 1">>, <<"">>, manual), + {ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 2">>, <<"">>, auto), {ok, Calendars} = logic_calendar:list_calendars(UserId), ?assertEqual(2, length(Calendars)). test_update_calendar() -> UserId = create_test_user(), - {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Original">>, <<"">>), + {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Original">>, <<"">>, manual), - Updates = [{title, <<"Updated">>}, {type, commercial}], + Updates = [{title, <<"Updated">>}, {type, commercial}, {confirmation, auto}], {ok, Updated} = logic_calendar:update_calendar(UserId, Calendar#calendar.id, Updates), ?assertEqual(<<"Updated">>, Updated#calendar.title), ?assertEqual(commercial, Updated#calendar.type), + ?assertEqual(auto, Updated#calendar.confirmation), - % Другой пользователь не может обновить OtherUserId = create_test_user(), ?assertMatch({error, access_denied}, logic_calendar:update_calendar(OtherUserId, Calendar#calendar.id, Updates)). test_delete_calendar() -> UserId = create_test_user(), - {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test">>, <<"">>), + {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test">>, <<"">>, manual), {ok, Deleted} = logic_calendar:delete_calendar(UserId, Calendar#calendar.id), ?assertEqual(deleted, Deleted#calendar.status), - % После удаления доступ запрещён ?assertMatch({error, access_denied}, logic_calendar:get_calendar(UserId, Calendar#calendar.id)). test_access_control() -> OwnerId = create_test_user(), OtherId = create_test_user(), - % Создаём personal календарь - {ok, PersonalCalendar} = logic_calendar:create_calendar(OwnerId, <<"Personal">>, <<"">>), + {ok, PersonalCalendar} = logic_calendar:create_calendar(OwnerId, <<"Personal">>, <<"">>, manual), - % Владелец может редактировать ?assert(logic_calendar:can_edit(OwnerId, PersonalCalendar)), - - % Другой пользователь не может редактировать ?assertNot(logic_calendar:can_edit(OtherId, PersonalCalendar)), - - % Другой пользователь не может просматривать personal календарь ?assertNot(logic_calendar:can_access(OtherId, PersonalCalendar)), - % Делаем календарь коммерческим {ok, CommercialCalendar} = logic_calendar:update_calendar(OwnerId, PersonalCalendar#calendar.id, [{type, commercial}]), - - % Теперь другой пользователь может просматривать ?assert(logic_calendar:can_access(OtherId, CommercialCalendar)), - - % Но всё ещё не может редактировать ?assertNot(logic_calendar:can_edit(OtherId, CommercialCalendar)), - % Замораживаем календарь {ok, Frozen} = core_calendar:update(CommercialCalendar#calendar.id, [{status, frozen}]), - - % После заморозки доступ запрещён всем (кроме владельца для редактирования?) ?assertNot(logic_calendar:can_access(OtherId, Frozen)), ?assertNot(logic_calendar:can_access(OwnerId, Frozen)). \ No newline at end of file diff --git a/test/logic_event_recurring_tests.erl b/test/logic_event_recurring_tests.erl index 70b0135..a21c21a 100644 --- a/test/logic_event_recurring_tests.erl +++ b/test/logic_event_recurring_tests.erl @@ -56,13 +56,13 @@ create_test_user_and_calendar() -> }, mnesia:dirty_write(User), - {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>), + {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>, manual), {UserId, Calendar#calendar.id}. test_create_recurring_event() -> {UserId, CalendarId} = create_test_user_and_calendar(), Title = <<"Weekly Meeting">>, - StartTime = {{2026, 5, 1}, {10, 0, 0}}, + StartTime = {{2026, 6, 1}, {10, 0, 0}}, Duration = 60, RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}, @@ -75,7 +75,7 @@ test_create_recurring_event() -> test_create_recurring_invalid() -> {UserId, CalendarId} = create_test_user_and_calendar(), Title = <<"Invalid">>, - StartTime = {{2026, 5, 1}, {10, 0, 0}}, + StartTime = {{2026, 6, 1}, {10, 0, 0}}, Duration = 60, InvalidRRule = #{<<"freq">> => <<"YEARLY">>, <<"interval">> => 1}, @@ -85,17 +85,16 @@ test_create_recurring_invalid() -> test_get_occurrences() -> {UserId, CalendarId} = create_test_user_and_calendar(), - StartTime = {{2026, 6, 1}, {10, 0, 0}}, % Июнь 2026 + StartTime = {{2026, 6, 1}, {10, 0, 0}}, RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1}, {ok, Event} = logic_event:create_recurring_event( UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule ), - RangeEnd = {{2026, 6, 29}, {10, 0, 0}}, % Конец июня + RangeEnd = {{2026, 6, 29}, {10, 0, 0}}, {ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd), - % 1, 8, 15, 22, 29 июня = 5 вхождений ?assertEqual(5, length(Occurrences)). test_cancel_occurrence() -> @@ -107,7 +106,7 @@ test_cancel_occurrence() -> UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule ), - CancelTime = {{2026, 6, 8}, {10, 0, 0}}, % Второе вхождение + CancelTime = {{2026, 6, 8}, {10, 0, 0}}, {ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, CancelTime). test_occurrences_with_cancelled() -> @@ -119,17 +118,14 @@ test_occurrences_with_cancelled() -> UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule ), - % Отменяем второе вхождение CancelTime = {{2026, 6, 8}, {10, 0, 0}}, {ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, CancelTime), RangeEnd = {{2026, 6, 29}, {10, 0, 0}}, {ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd), - % 1, 15, 22, 29 июня = 4 вхождения (одно отменено) ?assertEqual(4, length(Occurrences)), - % Проверяем, что отменённого вхождения нет Starts = [O || {virtual, O} <- Occurrences], ?assertNot(lists:member(CancelTime, Starts)). diff --git a/test/logic_event_tests.erl b/test/logic_event_tests.erl index d658d56..71eb406 100644 --- a/test/logic_event_tests.erl +++ b/test/logic_event_tests.erl @@ -51,7 +51,7 @@ create_test_user_and_calendar() -> }, mnesia:dirty_write(User), - {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>), + {ok, Calendar} = logic_calendar:create_calendar(UserId, <<"Test Calendar">>, <<"">>, manual), {UserId, Calendar#calendar.id}. test_create_event() -> @@ -99,10 +99,8 @@ test_delete_event() -> test_time_validation() -> {UserId, CalendarId} = create_test_user_and_calendar(), - % Событие в прошлом PastTime = {{2020, 1, 1}, {10, 0, 0}}, {error, event_in_past} = logic_event:create_event(UserId, CalendarId, <<"Past">>, PastTime, 60), - % Событие в будущем FutureTime = {{2030, 1, 1}, {10, 0, 0}}, ?assertEqual(ok, logic_event:validate_event_time(FutureTime)). \ No newline at end of file diff --git a/test/scripts/test_all.sh b/test/scripts/test_all.sh new file mode 100644 index 0000000..99df83f --- /dev/null +++ b/test/scripts/test_all.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +echo "============================================================" +echo " EVENTHUB FULL API TEST SUITE" +echo "============================================================" +echo "" + +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Проверяем, что сервер запущен +if ! curl -s "http://localhost:8080/health" | grep -q "ok"; then + echo "❌ Server is not running. Please start the server first." + exit 1 +fi + +PASSED=0 +FAILED=0 + +run_test() { + echo "" + echo "▶ Running $1..." + if bash "$SCRIPTS_DIR/$1"; then + ((PASSED++)) + echo "✅ $1 PASSED" + else + ((FAILED++)) + echo "❌ $1 FAILED" + fi +} + +run_test "test_auth_api.sh" +run_test "test_calendar_api.sh" +run_test "test_event_api.sh" +run_test "test_booking_api.sh" + +echo "" +echo "============================================================" +echo " TEST SUMMARY" +echo "============================================================" +echo "Passed: $PASSED" +echo "Failed: $FAILED" +echo "============================================================" + +if [ $FAILED -eq 0 ]; then + echo "🎉 ALL TESTS PASSED!" + exit 0 +else + echo "❌ SOME TESTS FAILED" + exit 1 +fi \ No newline at end of file diff --git a/test/scripts/test_auth_api.sh b/test/scripts/test_auth_api.sh new file mode 100644 index 0000000..bd4fe9b --- /dev/null +++ b/test/scripts/test_auth_api.sh @@ -0,0 +1,217 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +BASE_URL="http://localhost:8080" + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } + +extract_json() { + echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed "s/\"$2\":\"//;s/\"$//" +} + +http_post() { + local url=$1 + local data=$2 + local token=$3 + + if [ -n "$token" ]; then + curl -s -X POST "$url" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + -d "$data" + else + curl -s -X POST "$url" \ + -H "Content-Type: application/json" \ + -d "$data" + fi +} + +http_get() { + local url=$1 + local token=$2 + + if [ -n "$token" ]; then + curl -s -X GET "$url" \ + -H "Authorization: Bearer $token" + else + curl -s -X GET "$url" + fi +} + +echo "============================================================" +echo " EVENTHUB AUTHENTICATION API TEST SCRIPT" +echo "============================================================" +echo "" + +log_info "Checking if server is running..." +if ! curl -s "$BASE_URL/health" | grep -q "ok"; then + log_error "Server is not running" + exit 1 +fi +log_success "Server is running" + +echo "" +log_info "============================================================" +log_info "TEST 1: Healthcheck" +log_info "============================================================" + +response=$(http_get "$BASE_URL/health" "") +if echo "$response" | grep -q "ok"; then + log_success "Healthcheck passed: $response" +else + log_error "Healthcheck failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 2: Register new user" +log_info "============================================================" + +TEST_EMAIL="test_auth_$(date +%s)@example.com" +TEST_PASSWORD="testpass123" + +log_info "Registering $TEST_EMAIL..." +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}" "") + +if echo "$response" | grep -q "token"; then + TOKEN=$(extract_json "$response" "token") + USER_ID=$(extract_json "$response" "id") + log_success "Registration successful" + log_info "User ID: $USER_ID" + log_info "Token: ${TOKEN:0:30}..." +else + log_error "Registration failed: $response" + exit 1 +fi + +echo "" +log_info "============================================================" +log_info "TEST 3: Register with existing email (should fail)" +log_info "============================================================" + +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}" "") +if echo "$response" | grep -q "already exists"; then + log_success "Duplicate registration correctly rejected" +else + log_error "Duplicate registration not rejected: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 4: Login with correct credentials" +log_info "============================================================" + +response=$(http_post "$BASE_URL/v1/login" "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}" "") + +if echo "$response" | grep -q "token"; then + LOGIN_TOKEN=$(extract_json "$response" "token") + REFRESH_TOKEN=$(extract_json "$response" "refresh_token") + log_success "Login successful" + log_info "Refresh token received: ${REFRESH_TOKEN:0:30}..." +else + log_error "Login failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 5: Login with wrong password (should fail)" +log_info "============================================================" + +response=$(http_post "$BASE_URL/v1/login" "{\"email\":\"$TEST_EMAIL\",\"password\":\"wrongpassword\"}" "") +if echo "$response" | grep -q "Invalid credentials"; then + log_success "Wrong password correctly rejected" +else + log_error "Wrong password not rejected: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 6: Get user profile with valid token" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/user/me" "$TOKEN") +if echo "$response" | grep -q "$TEST_EMAIL"; then + log_success "Profile retrieved successfully" + log_info "Response: $response" +else + log_error "Profile retrieval failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 7: Get user profile with invalid token" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/user/me" "invalid.token.here") +if echo "$response" | grep -q "Invalid token"; then + log_success "Invalid token correctly rejected" +else + log_error "Invalid token not rejected: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 8: Get user profile without token" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/user/me" "") +if echo "$response" | grep -q "Missing or invalid Authorization"; then + log_success "Missing token correctly rejected" +else + log_error "Missing token not rejected: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 9: Refresh token" +log_info "============================================================" + +if [ -n "$REFRESH_TOKEN" ]; then + response=$(http_post "$BASE_URL/v1/refresh" "{\"refresh_token\":\"$REFRESH_TOKEN\"}" "") + if echo "$response" | grep -q "token"; then + NEW_TOKEN=$(extract_json "$response" "token") + NEW_REFRESH=$(extract_json "$response" "refresh_token") + log_success "Token refreshed successfully" + log_info "New token: ${NEW_TOKEN:0:30}..." + log_info "New refresh token: ${NEW_REFRESH:0:30}..." + else + log_error "Token refresh failed: $response" + fi + + log_info "Trying to reuse old refresh token (should fail)..." + response=$(http_post "$BASE_URL/v1/refresh" "{\"refresh_token\":\"$REFRESH_TOKEN\"}" "") + if echo "$response" | grep -q "Invalid refresh token"; then + log_success "Old refresh token correctly rejected" + else + log_warning "Old refresh token not rejected: $response" + fi +else + log_warning "No refresh token to test" +fi + +echo "" +log_info "============================================================" +log_info "TEST 10: Access protected endpoint with new token" +log_info "============================================================" + +if [ -n "$NEW_TOKEN" ]; then + response=$(http_get "$BASE_URL/v1/user/me" "$NEW_TOKEN") + if echo "$response" | grep -q "$TEST_EMAIL"; then + log_success "Protected endpoint accessible with new token" + else + log_error "Protected endpoint not accessible: $response" + fi +fi + +echo "" +echo "============================================================" +log_success "AUTHENTICATION TESTS COMPLETED!" +echo "============================================================" \ No newline at end of file diff --git a/test/scripts/test_booking_api.sh b/test/scripts/test_booking_api.sh new file mode 100644 index 0000000..37e0230 --- /dev/null +++ b/test/scripts/test_booking_api.sh @@ -0,0 +1,265 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +BASE_URL="http://localhost:8080" + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +extract_json() { + echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed "s/\"$2\":\"//;s/\"$//" +} + +http_post() { + local url=$1 + local data=$2 + local token=$3 + + if [ -n "$token" ]; then + curl -s -X POST "$url" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + -d "$data" + else + curl -s -X POST "$url" \ + -H "Content-Type: application/json" \ + -d "$data" + fi +} + +http_get() { + local url=$1 + local token=$2 + + if [ -n "$token" ]; then + curl -s -X GET "$url" \ + -H "Authorization: Bearer $token" + else + curl -s -X GET "$url" + fi +} + +http_put() { + local url=$1 + local data=$2 + local token=$3 + + curl -s -X PUT "$url" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + -d "$data" +} + +http_delete() { + local url=$1 + local token=$2 + + curl -s -X DELETE "$url" \ + -H "Authorization: Bearer $token" +} + +echo "============================================================" +echo " EVENTHUB BOOKING API TEST SCRIPT" +echo "============================================================" +echo "" + +log_info "Checking if server is running..." +if ! curl -s "$BASE_URL/health" | grep -q "ok"; then + log_error "Server is not running" + exit 1 +fi +log_success "Server is running" + +echo "" +log_info "============================================================" +log_info "STEP 1: Create test users" +log_info "============================================================" + +OWNER_EMAIL="owner_test@example.com" +OWNER_PASSWORD="owner123" +PARTICIPANT_EMAIL="participant_test@example.com" +PARTICIPANT_PASSWORD="participant123" + +# Пробуем зарегистрировать владельца +log_info "Creating calendar owner..." +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OWNER_EMAIL\",\"password\":\"$OWNER_PASSWORD\"}" "") + +if echo "$response" | grep -q "token"; then + OWNER_TOKEN=$(extract_json "$response" "token") + OWNER_ID=$(extract_json "$response" "id") + log_success "Owner registered: $OWNER_EMAIL" +else + log_info "Owner exists, trying login..." + response=$(http_post "$BASE_URL/v1/login" "{\"email\":\"$OWNER_EMAIL\",\"password\":\"$OWNER_PASSWORD\"}" "") + OWNER_TOKEN=$(extract_json "$response" "token") + OWNER_ID=$(extract_json "$response" "id") +fi + +if [ -z "$OWNER_TOKEN" ]; then + log_error "Failed to get owner token" + echo "$response" + exit 1 +fi +log_success "Owner ready (ID: $OWNER_ID)" + +# Пробуем зарегистрировать участника +log_info "Creating participant..." +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$PARTICIPANT_EMAIL\",\"password\":\"$PARTICIPANT_PASSWORD\"}" "") + +if echo "$response" | grep -q "token"; then + PARTICIPANT_TOKEN=$(extract_json "$response" "token") + PARTICIPANT_ID=$(extract_json "$response" "id") + log_success "Participant registered: $PARTICIPANT_EMAIL" +else + log_info "Participant exists, trying login..." + response=$(http_post "$BASE_URL/v1/login" "{\"email\":\"$PARTICIPANT_EMAIL\",\"password\":\"$PARTICIPANT_PASSWORD\"}" "") + PARTICIPANT_TOKEN=$(extract_json "$response" "token") + PARTICIPANT_ID=$(extract_json "$response" "id") +fi + +if [ -z "$PARTICIPANT_TOKEN" ]; then + log_error "Failed to get participant token" + echo "$response" + exit 1 +fi +log_success "Participant ready (ID: $PARTICIPANT_ID)" + +echo "" +log_info "============================================================" +log_info "STEP 2: Create calendars" +log_info "============================================================" + +log_info "Creating AUTO calendar..." +response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"Auto Calendar\",\"confirmation\":\"auto\"}" "$OWNER_TOKEN") +AUTO_CALENDAR_ID=$(extract_json "$response" "id") +log_success "Auto calendar: $AUTO_CALENDAR_ID" + +log_info "Creating MANUAL calendar..." +response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"Manual Calendar\",\"confirmation\":\"manual\"}" "$OWNER_TOKEN") +MANUAL_CALENDAR_ID=$(extract_json "$response" "id") +log_success "Manual calendar: $MANUAL_CALENDAR_ID" + +echo "" +log_info "============================================================" +log_info "STEP 3: Create events" +log_info "============================================================" + +EVENT_START="2026-05-01T10:00:00Z" + +log_info "Creating event in AUTO calendar..." +response=$(http_post "$BASE_URL/v1/calendars/$AUTO_CALENDAR_ID/events" \ + "{\"title\":\"Auto Event\",\"start_time\":\"$EVENT_START\",\"duration\":60,\"capacity\":10}" "$OWNER_TOKEN") +AUTO_EVENT_ID=$(extract_json "$response" "id") +log_success "Auto event: $AUTO_EVENT_ID" + +log_info "Creating event in MANUAL calendar..." +response=$(http_post "$BASE_URL/v1/calendars/$MANUAL_CALENDAR_ID/events" \ + "{\"title\":\"Manual Event\",\"start_time\":\"$EVENT_START\",\"duration\":60,\"capacity\":10}" "$OWNER_TOKEN") +MANUAL_EVENT_ID=$(extract_json "$response" "id") +log_success "Manual event: $MANUAL_EVENT_ID" + +echo "" +log_info "============================================================" +log_info "STEP 4: Test AUTO confirmation" +log_info "============================================================" + +log_info "Participant booking AUTO event..." +response=$(http_post "$BASE_URL/v1/events/$AUTO_EVENT_ID/bookings" "" "$PARTICIPANT_TOKEN") +echo "Response: $response" +AUTO_BOOKING_STATUS=$(extract_json "$response" "status") + +if [ "$AUTO_BOOKING_STATUS" = "confirmed" ]; then + log_success "Auto-booking confirmed immediately" +else + log_error "Auto-booking status: $AUTO_BOOKING_STATUS" +fi + +# Сохраняем ID авто-бронирования +AUTO_BOOKING_ID=$(extract_json "$response" "id") + +echo "" +log_info "============================================================" +log_info "STEP 5: Test MANUAL confirmation" +log_info "============================================================" + +log_info "Participant booking MANUAL event..." +response=$(http_post "$BASE_URL/v1/events/$MANUAL_EVENT_ID/bookings" "" "$PARTICIPANT_TOKEN") +MANUAL_BOOKING_ID=$(extract_json "$response" "id") +MANUAL_BOOKING_STATUS=$(extract_json "$response" "status") + +if [ "$MANUAL_BOOKING_STATUS" = "pending" ]; then + log_success "Manual-booking is pending: $MANUAL_BOOKING_ID" +else + log_error "Manual-booking status: $MANUAL_BOOKING_STATUS" +fi + +log_info "Owner confirming booking..." +response=$(http_put "$BASE_URL/v1/bookings/$MANUAL_BOOKING_ID" "{\"action\":\"confirm\"}" "$OWNER_TOKEN") +CONFIRMED_STATUS=$(extract_json "$response" "status") + +if [ "$CONFIRMED_STATUS" = "confirmed" ]; then + log_success "Booking confirmed by owner" +else + log_error "Confirmation failed" +fi + +echo "" +log_info "============================================================" +log_info "STEP 6: Test booking lists" +log_info "============================================================" + +log_info "Owner viewing event bookings..." +response=$(http_get "$BASE_URL/v1/events/$MANUAL_EVENT_ID/bookings" "$OWNER_TOKEN") +echo "Response: $response" + +log_info "Participant viewing their bookings..." +response=$(http_get "$BASE_URL/v1/user/bookings" "$PARTICIPANT_TOKEN") +echo "Response: $response" + +echo "" +log_info "============================================================" +log_info "STEP 7: Test booking cancellation" +log_info "============================================================" + +# Используем первое бронирование для отмены +if [ -n "$AUTO_BOOKING_ID" ]; then + CANCEL_BOOKING_ID="$AUTO_BOOKING_ID" + log_info "Using auto-booking for cancellation: $CANCEL_BOOKING_ID" +else + # Создаём новое событие для теста отмены + log_info "Creating new event for cancellation test..." + response=$(http_post "$BASE_URL/v1/calendars/$MANUAL_CALENDAR_ID/events" \ + "{\"title\":\"Cancel Test Event\",\"start_time\":\"$EVENT_START\",\"duration\":60,\"capacity\":10}" "$OWNER_TOKEN") + CANCEL_EVENT_ID=$(extract_json "$response" "id") + log_info "Event created: $CANCEL_EVENT_ID" + + log_info "Creating booking to cancel..." + response=$(http_post "$BASE_URL/v1/events/$CANCEL_EVENT_ID/bookings" "" "$PARTICIPANT_TOKEN") + CANCEL_BOOKING_ID=$(extract_json "$response" "id") + log_info "Created: $CANCEL_BOOKING_ID" +fi + +if [ -n "$CANCEL_BOOKING_ID" ]; then + log_info "Cancelling booking $CANCEL_BOOKING_ID..." + response=$(http_delete "$BASE_URL/v1/bookings/$CANCEL_BOOKING_ID" "$PARTICIPANT_TOKEN") + CANCELLED_STATUS=$(extract_json "$response" "status") + + if [ "$CANCELLED_STATUS" = "cancelled" ]; then + log_success "Booking cancelled" + else + log_error "Cancellation failed: $response" + fi +else + log_error "No booking to cancel" +fi + +echo "" +echo "============================================================" +log_success "TESTS COMPLETED!" +echo "============================================================" \ No newline at end of file diff --git a/test/scripts/test_calendar_api.sh b/test/scripts/test_calendar_api.sh new file mode 100644 index 0000000..585e911 --- /dev/null +++ b/test/scripts/test_calendar_api.sh @@ -0,0 +1,217 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +BASE_URL="http://localhost:8080" + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +extract_json() { + echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed "s/\"$2\":\"//;s/\"$//" +} + +http_post() { + local url=$1 + local data=$2 + local token=$3 + + if [ -n "$token" ]; then + curl -s -X POST "$url" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + -d "$data" + else + curl -s -X POST "$url" \ + -H "Content-Type: application/json" \ + -d "$data" + fi +} + +http_get() { + local url=$1 + local token=$2 + + if [ -n "$token" ]; then + curl -s -X GET "$url" \ + -H "Authorization: Bearer $token" + else + curl -s -X GET "$url" + fi +} + +http_put() { + local url=$1 + local data=$2 + local token=$3 + + curl -s -X PUT "$url" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + -d "$data" +} + +http_delete() { + local url=$1; local token=$2 + curl -s -X DELETE "$url" -H "Authorization: Bearer $token" +} + +echo "============================================================" +echo " EVENTHUB CALENDAR API TEST SCRIPT" +echo "============================================================" +echo "" + +log_info "Setting up test users..." + +# Создаём двух пользователей +OWNER_EMAIL="calendar_owner_$(date +%s)@example.com" +OWNER_PASS="owner123" +OTHER_EMAIL="calendar_other_$(date +%s)@example.com" +OTHER_PASS="other123" + +# Владелец +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OWNER_EMAIL\",\"password\":\"$OWNER_PASS\"}" "") +OWNER_TOKEN=$(extract_json "$response" "token") +OWNER_ID=$(extract_json "$response" "id") +log_success "Owner created: $OWNER_ID" + +# Другой пользователь +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OTHER_EMAIL\",\"password\":\"$OTHER_PASS\"}" "") +OTHER_TOKEN=$(extract_json "$response" "token") +OTHER_ID=$(extract_json "$response" "id") +log_success "Other user created: $OTHER_ID" + +echo "" +log_info "============================================================" +log_info "TEST 1: Create calendar" +log_info "============================================================" + +response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"My Personal Calendar\",\"description\":\"Test description\"}" "$OWNER_TOKEN") +CALENDAR_ID=$(extract_json "$response" "id") + +if [ -n "$CALENDAR_ID" ]; then + log_success "Calendar created: $CALENDAR_ID" +else + log_error "Calendar creation failed: $response" + exit 1 +fi + +echo "" +log_info "============================================================" +log_info "TEST 2: Create commercial calendar" +log_info "============================================================" + +response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"Commercial Calendar\",\"type\":\"commercial\"}" "$OWNER_TOKEN") +COMMERCIAL_ID=$(extract_json "$response" "id") +log_success "Commercial calendar created: $COMMERCIAL_ID" + +echo "" +log_info "============================================================" +log_info "TEST 3: List calendars (owner)" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/calendars" "$OWNER_TOKEN") +COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) +log_success "Owner sees $COUNT calendars" + +echo "" +log_info "============================================================" +log_info "TEST 4: List calendars (other user - empty)" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/calendars" "$OTHER_TOKEN") +COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) +log_success "Other user sees $COUNT calendars" + +echo "" +log_info "============================================================" +log_info "TEST 5: Get calendar by ID (owner)" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/calendars/$CALENDAR_ID" "$OWNER_TOKEN") +if echo "$response" | grep -q "My Personal Calendar"; then + log_success "Owner can access personal calendar" +else + log_error "Owner cannot access calendar: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 6: Get personal calendar (other user - denied)" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/calendars/$CALENDAR_ID" "$OTHER_TOKEN") +if echo "$response" | grep -q "Access denied"; then + log_success "Other user correctly denied access to personal calendar" +else + log_error "Access control failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 7: Get commercial calendar (other user - allowed)" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/calendars/$COMMERCIAL_ID" "$OTHER_TOKEN") +if echo "$response" | grep -q "Commercial Calendar"; then + log_success "Other user can access commercial calendar" +else + log_error "Other user cannot access commercial calendar: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 8: Update calendar (owner)" +log_info "============================================================" + +response=$(http_put "$BASE_URL/v1/calendars/$CALENDAR_ID" "{\"title\":\"Updated Calendar\"}" "$OWNER_TOKEN") +if echo "$response" | grep -q "Updated Calendar"; then + log_success "Calendar updated successfully" +else + log_error "Calendar update failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 9: Update calendar (other user - denied)" +log_info "============================================================" + +response=$(http_put "$BASE_URL/v1/calendars/$CALENDAR_ID" "{\"title\":\"Hacked\"}" "$OTHER_TOKEN") +if echo "$response" | grep -q "Access denied"; then + log_success "Other user correctly denied update" +else + log_error "Access control failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 10: Delete calendar (owner)" +log_info "============================================================" + +response=$(http_delete "$BASE_URL/v1/calendars/$CALENDAR_ID" "$OWNER_TOKEN") +if echo "$response" | grep -q "deleted"; then + log_success "Calendar deleted" +else + log_error "Calendar deletion failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 11: Get deleted calendar (should be denied)" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/calendars/$CALENDAR_ID" "$OWNER_TOKEN") +if echo "$response" | grep -q "Access denied"; then + log_success "Deleted calendar not accessible" +else + log_error "Deleted calendar still accessible: $response" +fi + +echo "" +echo "============================================================" +log_success "CALENDAR API TESTS COMPLETED!" +echo "============================================================" \ No newline at end of file diff --git a/test/scripts/test_event_api.sh b/test/scripts/test_event_api.sh new file mode 100644 index 0000000..54a5b32 --- /dev/null +++ b/test/scripts/test_event_api.sh @@ -0,0 +1,212 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +BASE_URL="http://localhost:8080" + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +extract_json() { + echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed "s/\"$2\":\"//;s/\"$//" +} + +http_post() { + local url=$1 + local data=$2 + local token=$3 + + if [ -n "$token" ]; then + curl -s -X POST "$url" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + -d "$data" + else + curl -s -X POST "$url" \ + -H "Content-Type: application/json" \ + -d "$data" + fi +} + +http_get() { + local url=$1 + local token=$2 + + if [ -n "$token" ]; then + curl -s -X GET "$url" \ + -H "Authorization: Bearer $token" + else + curl -s -X GET "$url" + fi +} + +http_put() { + local url=$1 + local data=$2 + local token=$3 + + curl -s -X PUT "$url" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + -d "$data" +} + +http_delete() { + local url=$1; local token=$2 + curl -s -X DELETE "$url" -H "Authorization: Bearer $token" +} + +echo "============================================================" +echo " EVENTHUB EVENT API TEST SCRIPT" +echo "============================================================" +echo "" + +log_info "Setting up test users and calendar..." + +OWNER_EMAIL="event_owner_$(date +%s)@example.com" +OWNER_PASS="owner123" + +response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OWNER_EMAIL\",\"password\":\"$OWNER_PASS\"}" "") +OWNER_TOKEN=$(extract_json "$response" "token") +OWNER_ID=$(extract_json "$response" "id") +log_success "Owner created" + +response=$(http_post "$BASE_URL/v1/calendars" "{\"title\":\"Test Calendar\"}" "$OWNER_TOKEN") +CALENDAR_ID=$(extract_json "$response" "id") +log_success "Calendar created: $CALENDAR_ID" + +echo "" +log_info "============================================================" +log_info "TEST 1: Create single event" +log_info "============================================================" + +EVENT_START="2026-06-01T10:00:00Z" +response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" \ + "{\"title\":\"Single Event\",\"start_time\":\"$EVENT_START\",\"duration\":60}" "$OWNER_TOKEN") +EVENT_ID=$(extract_json "$response" "id") + +if [ -n "$EVENT_ID" ]; then + log_success "Single event created: $EVENT_ID" +else + log_error "Event creation failed: $response" + exit 1 +fi + +echo "" +log_info "============================================================" +log_info "TEST 2: Create event with capacity" +log_info "============================================================" + +response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" \ + "{\"title\":\"Capacity Event\",\"start_time\":\"$EVENT_START\",\"duration\":60,\"capacity\":10}" "$OWNER_TOKEN") +CAPACITY_EVENT_ID=$(extract_json "$response" "id") +log_success "Event with capacity created: $CAPACITY_EVENT_ID" + +echo "" +log_info "============================================================" +log_info "TEST 3: Create event in past (should fail)" +log_info "============================================================" + +PAST_START="2020-01-01T10:00:00Z" +response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" \ + "{\"title\":\"Past Event\",\"start_time\":\"$PAST_START\",\"duration\":60}" "$OWNER_TOKEN") +if echo "$response" | grep -q "past"; then + log_success "Past event correctly rejected" +else + log_error "Past event not rejected: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 4: List events" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/calendars/$CALENDAR_ID/events" "$OWNER_TOKEN") +COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l) +log_success "Found $COUNT events" + +echo "" +log_info "============================================================" +log_info "TEST 5: Get event by ID" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/events/$EVENT_ID" "$OWNER_TOKEN") +if echo "$response" | grep -q "Single Event"; then + log_success "Event retrieved successfully" +else + log_error "Event retrieval failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 6: Update event" +log_info "============================================================" + +response=$(http_put "$BASE_URL/v1/events/$EVENT_ID" "{\"title\":\"Updated Event\"}" "$OWNER_TOKEN") +if echo "$response" | grep -q "Updated Event"; then + log_success "Event updated" +else + log_error "Event update failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 7: Delete event" +log_info "============================================================" + +response=$(http_delete "$BASE_URL/v1/events/$EVENT_ID" "$OWNER_TOKEN") +if echo "$response" | grep -q "deleted"; then + log_success "Event deleted" +else + log_error "Event deletion failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 8: Get deleted event (should fail)" +log_info "============================================================" + +response=$(http_get "$BASE_URL/v1/events/$EVENT_ID" "$OWNER_TOKEN") +if echo "$response" | grep -q "not found"; then + log_success "Deleted event not found" +else + log_error "Deleted event still accessible: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 9: Create recurring event" +log_info "============================================================" + +response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" \ + "{\"title\":\"Weekly Meeting\",\"start_time\":\"$EVENT_START\",\"duration\":60,\"recurrence\":{\"freq\":\"WEEKLY\",\"interval\":1}}" "$OWNER_TOKEN") +RECURRING_ID=$(extract_json "$response" "id") + +if [ -n "$RECURRING_ID" ]; then + log_success "Recurring event created: $RECURRING_ID" +else + log_error "Recurring event creation failed: $response" +fi + +echo "" +log_info "============================================================" +log_info "TEST 10: Get occurrences" +log_info "============================================================" + +FROM="2026-06-01T00:00:00Z" +TO="2026-06-30T00:00:00Z" +response=$(http_get "$BASE_URL/v1/events/$RECURRING_ID/occurrences?from=$FROM&to=$TO" "$OWNER_TOKEN") +if [ -n "$response" ] && [ "$response" != "[]" ]; then + log_success "Occurrences retrieved" +else + log_error "Occurrences retrieval failed: $response" +fi + +echo "" +echo "============================================================" +log_success "EVENT API TESTS COMPLETED!" +echo "============================================================" \ No newline at end of file