Stage 3.4

This commit is contained in:
2026-04-20 16:40:44 +03:00
parent 42a047a938
commit b24cbc97f3
25 changed files with 2520 additions and 123 deletions

226
Makefile Normal file
View File

@@ -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

155
README.md
View File

@@ -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 |
---

102
src/core/core_booking.erl Normal file
View File

@@ -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}).

View File

@@ -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,

View File

@@ -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, []}
]}
]),

View File

@@ -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).

View File

@@ -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).

View File

@@ -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

View File

@@ -9,8 +9,14 @@ init(Req, Opts) ->
handle(Req, _Opts) ->
case cowboy_req:method(Req) of
<<"POST">> ->
case cowboy_req:has_body(Req) of
true ->
{ok, Body, Req1} = cowboy_req:read_body(Req),
case jsx:decode(Body, [return_maps]) of
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} ->
@@ -43,7 +49,14 @@ handle(Req, _Opts) ->
send_error(Req1, 401, <<"Invalid credentials">>)
end;
_ ->
send_error(Req1, 400, <<"Invalid request body">>)
send_error(Req1, 400, <<"Missing email or password">>)
catch
_:_ ->
send_error(Req1, 400, <<"Invalid JSON">>)
end
end;
false ->
send_error(Req, 400, <<"Missing request body">>)
end;
_ ->
send_error(Req, 405, <<"Method not allowed">>)

View File

@@ -9,8 +9,14 @@ init(Req, Opts) ->
handle(Req, _Opts) ->
case cowboy_req:method(Req) of
<<"POST">> ->
case cowboy_req:has_body(Req) of
true ->
{ok, Body, Req1} = cowboy_req:read_body(Req),
case jsx:decode(Body, [return_maps]) of
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 ->
@@ -28,12 +34,21 @@ handle(Req, _Opts) ->
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, <<"Invalid request body">>)
send_error(Req1, 400, <<"Missing email or password">>)
catch
_:_ ->
send_error(Req1, 400, <<"Invalid JSON">>)
end
end;
false ->
send_error(Req, 400, <<"Missing request body">>)
end;
_ ->
send_error(Req, 405, <<"Method not allowed">>)

View File

@@ -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).

189
src/logic/logic_booking.erl Normal file
View File

@@ -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.

View File

@@ -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;

View File

@@ -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 = <<UserId/binary, "@test.com">>,
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).

126
test/core_booking_tests.erl Normal file
View File

@@ -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).

View File

@@ -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)).

View File

@@ -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 = <<UserId/binary, "@test.com">>,
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)).

View File

@@ -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)).

View File

@@ -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)).

View File

@@ -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)).

50
test/scripts/test_all.sh Normal file
View File

@@ -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

View File

@@ -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 "============================================================"

View File

@@ -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 "============================================================"

View File

@@ -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 "============================================================"

View File

@@ -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 "============================================================"