Stage 3.4
This commit is contained in:
226
Makefile
Normal file
226
Makefile
Normal 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
155
README.md
@@ -7,3 +7,158 @@ Build
|
|||||||
-----
|
-----
|
||||||
|
|
||||||
$ rebar3 compile
|
$ 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
102
src/core/core_booking.erl
Normal 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}).
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
-module(core_calendar).
|
-module(core_calendar).
|
||||||
-include("records.hrl").
|
-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]).
|
-export([generate_id/0]).
|
||||||
|
|
||||||
%% Создание календаря
|
%% Создание календаря
|
||||||
create(OwnerId, Title, Description) ->
|
create(OwnerId, Title, Description, Confirmation) ->
|
||||||
Id = generate_id(),
|
Id = generate_id(),
|
||||||
Calendar = #calendar{
|
Calendar = #calendar{
|
||||||
id = Id,
|
id = Id,
|
||||||
@@ -14,7 +14,7 @@ create(OwnerId, Title, Description) ->
|
|||||||
description = Description,
|
description = Description,
|
||||||
tags = [],
|
tags = [],
|
||||||
type = personal,
|
type = personal,
|
||||||
confirmation = manual,
|
confirmation = Confirmation,
|
||||||
rating_avg = 0.0,
|
rating_avg = 0.0,
|
||||||
rating_count = 0,
|
rating_count = 0,
|
||||||
status = active,
|
status = active,
|
||||||
|
|||||||
@@ -37,12 +37,15 @@ start_http() ->
|
|||||||
{"/v1/login", handler_login, []},
|
{"/v1/login", handler_login, []},
|
||||||
{"/v1/refresh", handler_refresh, []},
|
{"/v1/refresh", handler_refresh, []},
|
||||||
{"/v1/user/me", handler_user_me, []},
|
{"/v1/user/me", handler_user_me, []},
|
||||||
|
{"/v1/user/bookings", handler_user_bookings, []},
|
||||||
{"/v1/calendars", handler_calendars, []},
|
{"/v1/calendars", handler_calendars, []},
|
||||||
{"/v1/calendars/:id", handler_calendar_by_id, []},
|
{"/v1/calendars/:id", handler_calendar_by_id, []},
|
||||||
{"/v1/calendars/:calendar_id/events", handler_events, []},
|
{"/v1/calendars/:calendar_id/events", handler_events, []},
|
||||||
{"/v1/events/:id", handler_event_by_id, []},
|
{"/v1/events/:id", handler_event_by_id, []},
|
||||||
{"/v1/events/:id/occurrences", handler_event_occurrences, []},
|
{"/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, []}
|
||||||
]}
|
]}
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
|||||||
128
src/handlers/handler_booking_by_id.erl
Normal file
128
src/handlers/handler_booking_by_id.erl
Normal 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).
|
||||||
87
src/handlers/handler_bookings.erl
Normal file
87
src/handlers/handler_bookings.erl
Normal 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).
|
||||||
@@ -23,7 +23,8 @@ create_calendar(Req) ->
|
|||||||
case Decoded of
|
case Decoded of
|
||||||
#{<<"title">> := Title} ->
|
#{<<"title">> := Title} ->
|
||||||
Description = maps:get(<<"description">>, Decoded, <<"">>),
|
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} ->
|
{ok, Calendar} ->
|
||||||
Response = calendar_to_json(Calendar),
|
Response = calendar_to_json(Calendar),
|
||||||
send_json(Req2, 201, Response);
|
send_json(Req2, 201, Response);
|
||||||
@@ -45,6 +46,11 @@ create_calendar(Req) ->
|
|||||||
send_error(Req1, Code, Message)
|
send_error(Req1, Code, Message)
|
||||||
end.
|
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 - список календарей
|
%% GET /v1/calendars - список календарей
|
||||||
list_calendars(Req) ->
|
list_calendars(Req) ->
|
||||||
case handler_auth:authenticate(Req) of
|
case handler_auth:authenticate(Req) of
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ init(Req, Opts) ->
|
|||||||
handle(Req, _Opts) ->
|
handle(Req, _Opts) ->
|
||||||
case cowboy_req:method(Req) of
|
case cowboy_req:method(Req) of
|
||||||
<<"POST">> ->
|
<<"POST">> ->
|
||||||
|
case cowboy_req:has_body(Req) of
|
||||||
|
true ->
|
||||||
{ok, Body, Req1} = cowboy_req:read_body(Req),
|
{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} ->
|
#{<<"email">> := Email, <<"password">> := Password} ->
|
||||||
case core_user:get_by_email(Email) of
|
case core_user:get_by_email(Email) of
|
||||||
{ok, User} ->
|
{ok, User} ->
|
||||||
@@ -43,7 +49,14 @@ handle(Req, _Opts) ->
|
|||||||
send_error(Req1, 401, <<"Invalid credentials">>)
|
send_error(Req1, 401, <<"Invalid credentials">>)
|
||||||
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;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
send_error(Req, 405, <<"Method not allowed">>)
|
send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ init(Req, Opts) ->
|
|||||||
handle(Req, _Opts) ->
|
handle(Req, _Opts) ->
|
||||||
case cowboy_req:method(Req) of
|
case cowboy_req:method(Req) of
|
||||||
<<"POST">> ->
|
<<"POST">> ->
|
||||||
|
case cowboy_req:has_body(Req) of
|
||||||
|
true ->
|
||||||
{ok, Body, Req1} = cowboy_req:read_body(Req),
|
{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} ->
|
#{<<"email">> := Email, <<"password">> := Password} ->
|
||||||
case core_user:email_exists(Email) of
|
case core_user:email_exists(Email) of
|
||||||
true ->
|
true ->
|
||||||
@@ -28,12 +34,21 @@ handle(Req, _Opts) ->
|
|||||||
token => Token
|
token => Token
|
||||||
},
|
},
|
||||||
send_json(Req1, 201, Response);
|
send_json(Req1, 201, Response);
|
||||||
|
{error, email_exists} ->
|
||||||
|
send_error(Req1, 409, <<"Email already exists">>);
|
||||||
{error, _} ->
|
{error, _} ->
|
||||||
send_error(Req1, 500, <<"Internal server error">>)
|
send_error(Req1, 500, <<"Internal server error">>)
|
||||||
end
|
end
|
||||||
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;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
send_error(Req, 405, <<"Method not allowed">>)
|
send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
|||||||
55
src/handlers/handler_user_bookings.erl
Normal file
55
src/handlers/handler_user_bookings.erl
Normal 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
189
src/logic/logic_booking.erl
Normal 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.
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
-module(logic_calendar).
|
-module(logic_calendar).
|
||||||
-include("records.hrl").
|
-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]).
|
update_calendar/3, delete_calendar/2]).
|
||||||
-export([can_access/2, can_edit/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
|
case core_user:get_by_id(UserId) of
|
||||||
{ok, User} ->
|
{ok, User} ->
|
||||||
case User#user.status of
|
case User#user.status of
|
||||||
active ->
|
active ->
|
||||||
core_calendar:create(UserId, Title, Description);
|
core_calendar:create(UserId, Title, Description, Confirmation);
|
||||||
_ ->
|
_ ->
|
||||||
{error, user_inactive}
|
{error, user_inactive}
|
||||||
end;
|
end;
|
||||||
|
|||||||
148
test/booking_integration_tests.erl
Normal file
148
test/booking_integration_tests.erl
Normal 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
126
test/core_booking_tests.erl
Normal 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).
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
%% Setup и cleanup
|
|
||||||
setup() ->
|
setup() ->
|
||||||
mnesia:start(),
|
mnesia:start(),
|
||||||
mnesia:create_table(calendar, [
|
mnesia:create_table(calendar, [
|
||||||
@@ -16,7 +15,6 @@ cleanup(_) ->
|
|||||||
mnesia:stop(),
|
mnesia:stop(),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%% Группа тестов
|
|
||||||
core_calendar_test_() ->
|
core_calendar_test_() ->
|
||||||
{foreach,
|
{foreach,
|
||||||
fun setup/0,
|
fun setup/0,
|
||||||
@@ -29,18 +27,19 @@ core_calendar_test_() ->
|
|||||||
{"Delete calendar test", fun test_delete_calendar/0}
|
{"Delete calendar test", fun test_delete_calendar/0}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
%% Тесты
|
|
||||||
test_create_calendar() ->
|
test_create_calendar() ->
|
||||||
OwnerId = <<"owner123">>,
|
OwnerId = <<"owner123">>,
|
||||||
Title = <<"Test Calendar">>,
|
Title = <<"Test Calendar">>,
|
||||||
Description = <<"Test Description">>,
|
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(OwnerId, Calendar#calendar.owner_id),
|
||||||
?assertEqual(Title, Calendar#calendar.title),
|
?assertEqual(Title, Calendar#calendar.title),
|
||||||
?assertEqual(Description, Calendar#calendar.description),
|
?assertEqual(Description, Calendar#calendar.description),
|
||||||
?assertEqual(personal, Calendar#calendar.type),
|
?assertEqual(personal, Calendar#calendar.type),
|
||||||
|
?assertEqual(Confirmation, Calendar#calendar.confirmation),
|
||||||
?assertEqual(active, Calendar#calendar.status),
|
?assertEqual(active, Calendar#calendar.status),
|
||||||
?assert(is_binary(Calendar#calendar.id)),
|
?assert(is_binary(Calendar#calendar.id)),
|
||||||
?assert(Calendar#calendar.created_at =/= undefined),
|
?assert(Calendar#calendar.created_at =/= undefined),
|
||||||
@@ -48,7 +47,7 @@ test_create_calendar() ->
|
|||||||
|
|
||||||
test_get_by_id() ->
|
test_get_by_id() ->
|
||||||
OwnerId = <<"owner123">>,
|
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),
|
{ok, Found} = core_calendar:get_by_id(Calendar#calendar.id),
|
||||||
?assertEqual(Calendar#calendar.id, Found#calendar.id),
|
?assertEqual(Calendar#calendar.id, Found#calendar.id),
|
||||||
@@ -59,33 +58,33 @@ test_list_by_owner() ->
|
|||||||
OwnerId = <<"owner123">>,
|
OwnerId = <<"owner123">>,
|
||||||
OtherOwner = <<"other456">>,
|
OtherOwner = <<"other456">>,
|
||||||
|
|
||||||
{ok, _} = core_calendar:create(OwnerId, <<"Calendar 1">>, <<"">>),
|
{ok, _} = core_calendar:create(OwnerId, <<"Calendar 1">>, <<"">>, manual),
|
||||||
{ok, _} = core_calendar:create(OwnerId, <<"Calendar 2">>, <<"">>),
|
{ok, _} = core_calendar:create(OwnerId, <<"Calendar 2">>, <<"">>, auto),
|
||||||
{ok, _} = core_calendar:create(OtherOwner, <<"Other Calendar">>, <<"">>),
|
{ok, _} = core_calendar:create(OtherOwner, <<"Other Calendar">>, <<"">>, manual),
|
||||||
|
|
||||||
{ok, Calendars} = core_calendar:list_by_owner(OwnerId),
|
{ok, Calendars} = core_calendar:list_by_owner(OwnerId),
|
||||||
?assertEqual(2, length(Calendars)).
|
?assertEqual(2, length(Calendars)).
|
||||||
|
|
||||||
test_update_calendar() ->
|
test_update_calendar() ->
|
||||||
OwnerId = <<"owner123">>,
|
OwnerId = <<"owner123">>,
|
||||||
{ok, Calendar} = core_calendar:create(OwnerId, <<"Original">>, <<"">>),
|
{ok, Calendar} = core_calendar:create(OwnerId, <<"Original">>, <<"">>, manual),
|
||||||
timer:sleep(2000),
|
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),
|
{ok, Updated} = core_calendar:update(Calendar#calendar.id, Updates),
|
||||||
|
|
||||||
?assertEqual(<<"Updated">>, Updated#calendar.title),
|
?assertEqual(<<"Updated">>, Updated#calendar.title),
|
||||||
?assertEqual(<<"New Desc">>, Updated#calendar.description),
|
?assertEqual(<<"New Desc">>, Updated#calendar.description),
|
||||||
|
?assertEqual(auto, Updated#calendar.confirmation),
|
||||||
?assert(Updated#calendar.updated_at > Calendar#calendar.updated_at),
|
?assert(Updated#calendar.updated_at > Calendar#calendar.updated_at),
|
||||||
|
|
||||||
{error, not_found} = core_calendar:update(<<"nonexistent">>, Updates).
|
{error, not_found} = core_calendar:update(<<"nonexistent">>, Updates).
|
||||||
|
|
||||||
test_delete_calendar() ->
|
test_delete_calendar() ->
|
||||||
OwnerId = <<"owner123">>,
|
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),
|
{ok, Deleted} = core_calendar:delete(Calendar#calendar.id),
|
||||||
?assertEqual(deleted, Deleted#calendar.status),
|
?assertEqual(deleted, Deleted#calendar.status),
|
||||||
|
|
||||||
% Удалённый календарь не возвращается в списке активных
|
|
||||||
{ok, ActiveCalendars} = core_calendar:list_by_owner(OwnerId),
|
{ok, ActiveCalendars} = core_calendar:list_by_owner(OwnerId),
|
||||||
?assertEqual(0, length(ActiveCalendars)).
|
?assertEqual(0, length(ActiveCalendars)).
|
||||||
212
test/logic_booking_tests.erl
Normal file
212
test/logic_booking_tests.erl
Normal 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)).
|
||||||
@@ -51,17 +51,18 @@ test_create_calendar() ->
|
|||||||
UserId = create_test_user(),
|
UserId = create_test_user(),
|
||||||
Title = <<"Test Calendar">>,
|
Title = <<"Test Calendar">>,
|
||||||
Description = <<"Test Description">>,
|
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(UserId, Calendar#calendar.owner_id),
|
||||||
?assertEqual(Title, Calendar#calendar.title),
|
?assertEqual(Title, Calendar#calendar.title),
|
||||||
?assertEqual(personal, Calendar#calendar.type).
|
?assertEqual(personal, Calendar#calendar.type),
|
||||||
|
?assertEqual(Confirmation, Calendar#calendar.confirmation).
|
||||||
|
|
||||||
test_get_calendar() ->
|
test_get_calendar() ->
|
||||||
UserId = create_test_user(),
|
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
|
case logic_calendar:get_calendar(UserId, Calendar#calendar.id) of
|
||||||
{ok, Found} ->
|
{ok, Found} ->
|
||||||
?assertEqual(Calendar#calendar.id, Found#calendar.id);
|
?assertEqual(Calendar#calendar.id, Found#calendar.id);
|
||||||
@@ -69,77 +70,55 @@ test_get_calendar() ->
|
|||||||
?assert(false, {unexpected_result, Other})
|
?assert(false, {unexpected_result, Other})
|
||||||
end,
|
end,
|
||||||
|
|
||||||
% Другой пользователь не имеет доступа к personal календарю
|
|
||||||
OtherUserId = create_test_user(),
|
OtherUserId = create_test_user(),
|
||||||
?assertMatch({error, access_denied},
|
?assertMatch({error, access_denied},
|
||||||
logic_calendar:get_calendar(OtherUserId, Calendar#calendar.id)),
|
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).
|
|
||||||
|
|
||||||
test_list_calendars() ->
|
test_list_calendars() ->
|
||||||
UserId = create_test_user(),
|
UserId = create_test_user(),
|
||||||
{ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 1">>, <<"">>),
|
{ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 1">>, <<"">>, manual),
|
||||||
{ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 2">>, <<"">>),
|
{ok, _} = logic_calendar:create_calendar(UserId, <<"Calendar 2">>, <<"">>, auto),
|
||||||
|
|
||||||
{ok, Calendars} = logic_calendar:list_calendars(UserId),
|
{ok, Calendars} = logic_calendar:list_calendars(UserId),
|
||||||
?assertEqual(2, length(Calendars)).
|
?assertEqual(2, length(Calendars)).
|
||||||
|
|
||||||
test_update_calendar() ->
|
test_update_calendar() ->
|
||||||
UserId = create_test_user(),
|
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),
|
{ok, Updated} = logic_calendar:update_calendar(UserId, Calendar#calendar.id, Updates),
|
||||||
?assertEqual(<<"Updated">>, Updated#calendar.title),
|
?assertEqual(<<"Updated">>, Updated#calendar.title),
|
||||||
?assertEqual(commercial, Updated#calendar.type),
|
?assertEqual(commercial, Updated#calendar.type),
|
||||||
|
?assertEqual(auto, Updated#calendar.confirmation),
|
||||||
|
|
||||||
% Другой пользователь не может обновить
|
|
||||||
OtherUserId = create_test_user(),
|
OtherUserId = create_test_user(),
|
||||||
?assertMatch({error, access_denied},
|
?assertMatch({error, access_denied},
|
||||||
logic_calendar:update_calendar(OtherUserId, Calendar#calendar.id, Updates)).
|
logic_calendar:update_calendar(OtherUserId, Calendar#calendar.id, Updates)).
|
||||||
|
|
||||||
test_delete_calendar() ->
|
test_delete_calendar() ->
|
||||||
UserId = create_test_user(),
|
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),
|
{ok, Deleted} = logic_calendar:delete_calendar(UserId, Calendar#calendar.id),
|
||||||
?assertEqual(deleted, Deleted#calendar.status),
|
?assertEqual(deleted, Deleted#calendar.status),
|
||||||
|
|
||||||
% После удаления доступ запрещён
|
|
||||||
?assertMatch({error, access_denied}, logic_calendar:get_calendar(UserId, Calendar#calendar.id)).
|
?assertMatch({error, access_denied}, logic_calendar:get_calendar(UserId, Calendar#calendar.id)).
|
||||||
|
|
||||||
test_access_control() ->
|
test_access_control() ->
|
||||||
OwnerId = create_test_user(),
|
OwnerId = create_test_user(),
|
||||||
OtherId = create_test_user(),
|
OtherId = create_test_user(),
|
||||||
|
|
||||||
% Создаём personal календарь
|
{ok, PersonalCalendar} = logic_calendar:create_calendar(OwnerId, <<"Personal">>, <<"">>, manual),
|
||||||
{ok, PersonalCalendar} = logic_calendar:create_calendar(OwnerId, <<"Personal">>, <<"">>),
|
|
||||||
|
|
||||||
% Владелец может редактировать
|
|
||||||
?assert(logic_calendar:can_edit(OwnerId, PersonalCalendar)),
|
?assert(logic_calendar:can_edit(OwnerId, PersonalCalendar)),
|
||||||
|
|
||||||
% Другой пользователь не может редактировать
|
|
||||||
?assertNot(logic_calendar:can_edit(OtherId, PersonalCalendar)),
|
?assertNot(logic_calendar:can_edit(OtherId, PersonalCalendar)),
|
||||||
|
|
||||||
% Другой пользователь не может просматривать personal календарь
|
|
||||||
?assertNot(logic_calendar:can_access(OtherId, PersonalCalendar)),
|
?assertNot(logic_calendar:can_access(OtherId, PersonalCalendar)),
|
||||||
|
|
||||||
% Делаем календарь коммерческим
|
|
||||||
{ok, CommercialCalendar} = logic_calendar:update_calendar(OwnerId, PersonalCalendar#calendar.id, [{type, commercial}]),
|
{ok, CommercialCalendar} = logic_calendar:update_calendar(OwnerId, PersonalCalendar#calendar.id, [{type, commercial}]),
|
||||||
|
|
||||||
% Теперь другой пользователь может просматривать
|
|
||||||
?assert(logic_calendar:can_access(OtherId, CommercialCalendar)),
|
?assert(logic_calendar:can_access(OtherId, CommercialCalendar)),
|
||||||
|
|
||||||
% Но всё ещё не может редактировать
|
|
||||||
?assertNot(logic_calendar:can_edit(OtherId, CommercialCalendar)),
|
?assertNot(logic_calendar:can_edit(OtherId, CommercialCalendar)),
|
||||||
|
|
||||||
% Замораживаем календарь
|
|
||||||
{ok, Frozen} = core_calendar:update(CommercialCalendar#calendar.id, [{status, frozen}]),
|
{ok, Frozen} = core_calendar:update(CommercialCalendar#calendar.id, [{status, frozen}]),
|
||||||
|
|
||||||
% После заморозки доступ запрещён всем (кроме владельца для редактирования?)
|
|
||||||
?assertNot(logic_calendar:can_access(OtherId, Frozen)),
|
?assertNot(logic_calendar:can_access(OtherId, Frozen)),
|
||||||
?assertNot(logic_calendar:can_access(OwnerId, Frozen)).
|
?assertNot(logic_calendar:can_access(OwnerId, Frozen)).
|
||||||
@@ -56,13 +56,13 @@ create_test_user_and_calendar() ->
|
|||||||
},
|
},
|
||||||
mnesia:dirty_write(User),
|
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}.
|
{UserId, Calendar#calendar.id}.
|
||||||
|
|
||||||
test_create_recurring_event() ->
|
test_create_recurring_event() ->
|
||||||
{UserId, CalendarId} = create_test_user_and_calendar(),
|
{UserId, CalendarId} = create_test_user_and_calendar(),
|
||||||
Title = <<"Weekly Meeting">>,
|
Title = <<"Weekly Meeting">>,
|
||||||
StartTime = {{2026, 5, 1}, {10, 0, 0}},
|
StartTime = {{2026, 6, 1}, {10, 0, 0}},
|
||||||
Duration = 60,
|
Duration = 60,
|
||||||
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
|
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ test_create_recurring_event() ->
|
|||||||
test_create_recurring_invalid() ->
|
test_create_recurring_invalid() ->
|
||||||
{UserId, CalendarId} = create_test_user_and_calendar(),
|
{UserId, CalendarId} = create_test_user_and_calendar(),
|
||||||
Title = <<"Invalid">>,
|
Title = <<"Invalid">>,
|
||||||
StartTime = {{2026, 5, 1}, {10, 0, 0}},
|
StartTime = {{2026, 6, 1}, {10, 0, 0}},
|
||||||
Duration = 60,
|
Duration = 60,
|
||||||
InvalidRRule = #{<<"freq">> => <<"YEARLY">>, <<"interval">> => 1},
|
InvalidRRule = #{<<"freq">> => <<"YEARLY">>, <<"interval">> => 1},
|
||||||
|
|
||||||
@@ -85,17 +85,16 @@ test_create_recurring_invalid() ->
|
|||||||
|
|
||||||
test_get_occurrences() ->
|
test_get_occurrences() ->
|
||||||
{UserId, CalendarId} = create_test_user_and_calendar(),
|
{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},
|
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
|
||||||
|
|
||||||
{ok, Event} = logic_event:create_recurring_event(
|
{ok, Event} = logic_event:create_recurring_event(
|
||||||
UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule
|
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),
|
{ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd),
|
||||||
|
|
||||||
% 1, 8, 15, 22, 29 июня = 5 вхождений
|
|
||||||
?assertEqual(5, length(Occurrences)).
|
?assertEqual(5, length(Occurrences)).
|
||||||
|
|
||||||
test_cancel_occurrence() ->
|
test_cancel_occurrence() ->
|
||||||
@@ -107,7 +106,7 @@ test_cancel_occurrence() ->
|
|||||||
UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule
|
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).
|
{ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, CancelTime).
|
||||||
|
|
||||||
test_occurrences_with_cancelled() ->
|
test_occurrences_with_cancelled() ->
|
||||||
@@ -119,17 +118,14 @@ test_occurrences_with_cancelled() ->
|
|||||||
UserId, CalendarId, <<"Weekly">>, StartTime, 60, RRule
|
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),
|
{ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, CancelTime),
|
||||||
|
|
||||||
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),
|
{ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd),
|
||||||
|
|
||||||
% 1, 15, 22, 29 июня = 4 вхождения (одно отменено)
|
|
||||||
?assertEqual(4, length(Occurrences)),
|
?assertEqual(4, length(Occurrences)),
|
||||||
|
|
||||||
% Проверяем, что отменённого вхождения нет
|
|
||||||
Starts = [O || {virtual, O} <- Occurrences],
|
Starts = [O || {virtual, O} <- Occurrences],
|
||||||
?assertNot(lists:member(CancelTime, Starts)).
|
?assertNot(lists:member(CancelTime, Starts)).
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ create_test_user_and_calendar() ->
|
|||||||
},
|
},
|
||||||
mnesia:dirty_write(User),
|
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}.
|
{UserId, Calendar#calendar.id}.
|
||||||
|
|
||||||
test_create_event() ->
|
test_create_event() ->
|
||||||
@@ -99,10 +99,8 @@ test_delete_event() ->
|
|||||||
test_time_validation() ->
|
test_time_validation() ->
|
||||||
{UserId, CalendarId} = create_test_user_and_calendar(),
|
{UserId, CalendarId} = create_test_user_and_calendar(),
|
||||||
|
|
||||||
% Событие в прошлом
|
|
||||||
PastTime = {{2020, 1, 1}, {10, 0, 0}},
|
PastTime = {{2020, 1, 1}, {10, 0, 0}},
|
||||||
{error, event_in_past} = logic_event:create_event(UserId, CalendarId, <<"Past">>, PastTime, 60),
|
{error, event_in_past} = logic_event:create_event(UserId, CalendarId, <<"Past">>, PastTime, 60),
|
||||||
|
|
||||||
% Событие в будущем
|
|
||||||
FutureTime = {{2030, 1, 1}, {10, 0, 0}},
|
FutureTime = {{2030, 1, 1}, {10, 0, 0}},
|
||||||
?assertEqual(ok, logic_event:validate_event_time(FutureTime)).
|
?assertEqual(ok, logic_event:validate_event_time(FutureTime)).
|
||||||
50
test/scripts/test_all.sh
Normal file
50
test/scripts/test_all.sh
Normal 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
|
||||||
217
test/scripts/test_auth_api.sh
Normal file
217
test/scripts/test_auth_api.sh
Normal 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 "============================================================"
|
||||||
265
test/scripts/test_booking_api.sh
Normal file
265
test/scripts/test_booking_api.sh
Normal 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 "============================================================"
|
||||||
217
test/scripts/test_calendar_api.sh
Normal file
217
test/scripts/test_calendar_api.sh
Normal 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 "============================================================"
|
||||||
212
test/scripts/test_event_api.sh
Normal file
212
test/scripts/test_event_api.sh
Normal 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 "============================================================"
|
||||||
Reference in New Issue
Block a user