Stage 4
This commit is contained in:
67
Makefile
67
Makefile
@@ -27,7 +27,7 @@ help: ## Показать это сообщение
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
compile: ## Скомпилировать проект
|
compile: ## Скомпилировать проект
|
||||||
@echo "Компиляция проекта..."
|
@echo "Компиляция проекта..."
|
||||||
@$(REBAR3) compile
|
@$(REBAR3) clean compile
|
||||||
@echo "✓ Компиляция завершена"
|
@echo "✓ Компиляция завершена"
|
||||||
|
|
||||||
clean: ## Очистить проект
|
clean: ## Очистить проект
|
||||||
@@ -57,6 +57,21 @@ run: ## Запустить приложение (foreground)
|
|||||||
@echo "Запуск приложения..."
|
@echo "Запуск приложения..."
|
||||||
@$(REBAR3) shell --sname $(SNAME)
|
@$(REBAR3) shell --sname $(SNAME)
|
||||||
|
|
||||||
|
test-server: ## Запустить тестовый сервер в фоне
|
||||||
|
@echo "Cleaning old data..."
|
||||||
|
@rm -rf Mnesia.*
|
||||||
|
@pkill -f "beam.*eventhub_test" 2>/dev/null || true
|
||||||
|
@echo "Starting server..."
|
||||||
|
@rebar3 shell --sname eventhub_test </dev/null > /tmp/eventhub_test.log 2>&1 &
|
||||||
|
@echo "PID: $$!"
|
||||||
|
@for i in 1 2 3 4 5 6 7 8 9 10; do \
|
||||||
|
if curl -s http://localhost:8080/health | grep -q "ok"; then \
|
||||||
|
echo "✓ Server ready at http://localhost:8080"; \
|
||||||
|
break; \
|
||||||
|
fi; \
|
||||||
|
sleep 1; \
|
||||||
|
done
|
||||||
|
|
||||||
stop: ## Остановить приложение
|
stop: ## Остановить приложение
|
||||||
@echo "Остановка приложения..."
|
@echo "Остановка приложения..."
|
||||||
@pkill -f "rebar3 shell --sname $(SNAME)" || true
|
@pkill -f "rebar3 shell --sname $(SNAME)" || true
|
||||||
@@ -72,27 +87,52 @@ test: eunit ## Запустить все тесты (алиас для eunit)
|
|||||||
|
|
||||||
eunit: ## Запустить EUnit тесты
|
eunit: ## Запустить EUnit тесты
|
||||||
@echo "Запуск EUnit тестов..."
|
@echo "Запуск EUnit тестов..."
|
||||||
@pkill -f "beam.*$(SNAME)" 2>/dev/null || true
|
|
||||||
@$(REBAR3) eunit --sname $(SNAME)_test
|
@$(REBAR3) eunit --sname $(SNAME)_test
|
||||||
|
|
||||||
eunit-module: ## Запустить тесты для модуля (make eunit-module MODULE=core_calendar_tests)
|
eunit-module: ## Запустить тесты для модуля (make eunit-module MODULE=core_calendar_tests)
|
||||||
@echo "Запуск тестов для модуля $(MODULE)..."
|
@echo "Запуск тестов для модуля $(MODULE)..."
|
||||||
@pkill -f "beam.*$(SNAME)" 2>/dev/null || true
|
|
||||||
@$(REBAR3) eunit --sname $(SNAME)_test --module=$(MODULE)
|
@$(REBAR3) eunit --sname $(SNAME)_test --module=$(MODULE)
|
||||||
|
|
||||||
eunit-verbose: ## Запустить EUnit тесты с подробным выводом
|
eunit-verbose: ## Запустить EUnit тесты с подробным выводом
|
||||||
@echo "Запуск EUnit тестов (verbose)..."
|
@echo "Запуск EUnit тестов (verbose)..."
|
||||||
@pkill -f "beam.*$(SNAME)" 2>/dev/null || true
|
|
||||||
@$(REBAR3) eunit --sname $(SNAME)_test --verbose
|
@$(REBAR3) eunit --sname $(SNAME)_test --verbose
|
||||||
|
|
||||||
test-api: ## Запустить API тесты
|
test-search-unit: ## Запустить unit-тесты поиска
|
||||||
@echo "Запуск API тестов..."
|
@echo "Запуск unit-тестов поиска (logic)..."
|
||||||
@if ! curl -s http://localhost:8080/health > /dev/null 2>&1; then \
|
@$(REBAR3) eunit --sname test_search1 --module=logic_search_tests
|
||||||
echo "✗ Сервер не запущен. Выполните 'make run' в другом терминале"; \
|
|
||||||
exit 1; \
|
test-search-handler: ## Запустить handler тесты поиска
|
||||||
fi
|
@echo "Запуск handler тестов поиска..."
|
||||||
|
@$(REBAR3) eunit --sname test_search2 --module=handler_search_tests
|
||||||
|
|
||||||
|
test-api: ## Запустить API тесты (авто-запуск сервера)
|
||||||
|
@./test/scripts/run_tests.sh
|
||||||
|
|
||||||
|
test-full: ## Полный цикл тестирования
|
||||||
|
@./test/scripts/run_tests.sh $(PATTERN)
|
||||||
|
|
||||||
|
test-full-search: ## Полный цикл для поиска
|
||||||
|
@./test/scripts/run_tests.sh search
|
||||||
|
|
||||||
|
test-full-booking: ## Полный цикл для бронирований
|
||||||
|
@./test/scripts/run_tests.sh booking
|
||||||
|
|
||||||
|
test-api-existing: ## Запустить API тесты на уже работающем сервере
|
||||||
@chmod +x test/scripts/*.sh
|
@chmod +x test/scripts/*.sh
|
||||||
@cd test/scripts && ./test_all.sh
|
@cd test/scripts && ./test_runner.sh -s $(PATTERN)
|
||||||
|
|
||||||
|
test-server-stop: ## Остановить тестовый сервер
|
||||||
|
@pkill -f "beam.*eventhub" 2>/dev/null || true
|
||||||
|
@echo "✓ Servers stopped"
|
||||||
|
@rm -rf Mnesia.* 2>/dev/null || true
|
||||||
|
|
||||||
|
test-runner: ## Запустить тесты с фильтром (make test-runner PATTERN=booking)
|
||||||
|
@chmod +x test/scripts/*.sh
|
||||||
|
@cd test/scripts && ./test_runner.sh $(PATTERN)
|
||||||
|
|
||||||
|
test-quick: ## Запустить тесты используя уже запущенный сервер
|
||||||
|
@chmod +x test/scripts/*.sh
|
||||||
|
@cd test/scripts && ./test_runner.sh -s $(PATTERN)
|
||||||
|
|
||||||
test-auth: ## Запустить тесты аутентификации
|
test-auth: ## Запустить тесты аутентификации
|
||||||
@chmod +x test/scripts/test_auth_api.sh
|
@chmod +x test/scripts/test_auth_api.sh
|
||||||
@@ -110,7 +150,9 @@ test-booking: ## Запустить тесты бронирований
|
|||||||
@chmod +x test/scripts/test_booking_api.sh
|
@chmod +x test/scripts/test_booking_api.sh
|
||||||
@./test/scripts/test_booking_api.sh
|
@./test/scripts/test_booking_api.sh
|
||||||
|
|
||||||
test-all: eunit test-api ## Запустить ВСЕ тесты (EUnit + API)
|
test-all: eunit ## Запустить ВСЕ тесты (EUnit + API)
|
||||||
|
@sleep 1
|
||||||
|
make test-api
|
||||||
@echo "========================================"
|
@echo "========================================"
|
||||||
@echo " ВСЕ ТЕСТЫ ПРОЙДЕНЫ!"
|
@echo " ВСЕ ТЕСТЫ ПРОЙДЕНЫ!"
|
||||||
@echo "========================================"
|
@echo "========================================"
|
||||||
@@ -130,7 +172,6 @@ xref: ## Запустить Xref (кросс-ссылки)
|
|||||||
|
|
||||||
cover: ## Запустить тесты с покрытием кода
|
cover: ## Запустить тесты с покрытием кода
|
||||||
@echo "Запуск тестов с покрытием..."
|
@echo "Запуск тестов с покрытием..."
|
||||||
@pkill -f "beam.*$(SNAME)" 2>/dev/null || true
|
|
||||||
@$(REBAR3) eunit --sname $(SNAME)_test --cover
|
@$(REBAR3) eunit --sname $(SNAME)_test --cover
|
||||||
@$(REBAR3) cover --verbose
|
@$(REBAR3) cover --verbose
|
||||||
@echo "✓ Отчёт о покрытии в _build/test/cover/"
|
@echo "✓ Отчёт о покрытии в _build/test/cover/"
|
||||||
|
|||||||
@@ -31,12 +31,15 @@ Build
|
|||||||
## 🧪 Тестирование
|
## 🧪 Тестирование
|
||||||
|
|
||||||
| Команда | Описание |
|
| Команда | Описание |
|
||||||
|---------|----------|
|
|---------|-------------------------------------------------------------------------|
|
||||||
| `make test` | Алиас для `make eunit` |
|
| `make test` | Алиас для `make eunit` |
|
||||||
| `make eunit` | Запустить все EUnit тесты |
|
| `make eunit` | Запустить все EUnit тесты |
|
||||||
| `make eunit-verbose` | EUnit тесты с подробным выводом |
|
| `make eunit-verbose` | EUnit тесты с подробным выводом |
|
||||||
| `make eunit-module MODULE=имя` | Тесты для конкретного модуля |
|
| `make eunit-module MODULE=имя` | Тесты для конкретного модуля |
|
||||||
| `make test-api` | Запустить API тесты (сервер должен быть запущен) |
|
| `make test-api` | Запустить все API тесты (сервер запустится и остановится автоматически) |
|
||||||
|
| `make test-quick` | Запустить API тесты с существующим сервером |
|
||||||
|
| `make test-runner PATTERN=booking` | Запустить API тесты бронирований |
|
||||||
|
| `cd test/scripts && ./test_runner.sh -v auth` | Запустить тесты в verbose режиме |
|
||||||
| `make test-auth` | Только тесты аутентификации |
|
| `make test-auth` | Только тесты аутентификации |
|
||||||
| `make test-calendar` | Только тесты календарей |
|
| `make test-calendar` | Только тесты календарей |
|
||||||
| `make test-event` | Только тесты событий |
|
| `make test-event` | Только тесты событий |
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
-module(core_user).
|
-module(core_user).
|
||||||
|
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
-export([create/2, get_by_id/1, get_by_email/1, update/2, delete/1]).
|
-export([create/2, get_by_id/1, get_by_email/1, update/2, delete/1]).
|
||||||
-export([email_exists/1]).
|
-export([email_exists/1]).
|
||||||
|
-export([generate_id/0]).
|
||||||
|
|
||||||
%% Создание пользователя
|
%% Создание пользователя
|
||||||
create(Email, Password) ->
|
create(Email, Password) ->
|
||||||
% Сначала проверяем, существует ли email
|
% Проверяем, существует ли email
|
||||||
case email_exists(Email) of
|
case email_exists(Email) of
|
||||||
true ->
|
true ->
|
||||||
{error, email_exists};
|
{error, email_exists};
|
||||||
false ->
|
false ->
|
||||||
Id = generate_id(),
|
Id = generate_id(),
|
||||||
{ok, PasswordHash} = logic_auth:hash_password(Password),
|
{ok, PasswordHash} = logic_auth:hash_password(Password),
|
||||||
|
|
||||||
|
% Определяем роль: первый пользователь становится админом
|
||||||
|
Role = case mnesia:dirty_match_object(#user{_ = '_'}) of
|
||||||
|
[] -> admin;
|
||||||
|
_ -> user
|
||||||
|
end,
|
||||||
|
|
||||||
User = #user{
|
User = #user{
|
||||||
id = Id,
|
id = Id,
|
||||||
email = Email,
|
email = Email,
|
||||||
password_hash = PasswordHash,
|
password_hash = PasswordHash,
|
||||||
role = user,
|
role = Role,
|
||||||
status = active,
|
status = active,
|
||||||
created_at = calendar:universal_time(),
|
created_at = calendar:universal_time(),
|
||||||
updated_at = calendar:universal_time()
|
updated_at = calendar:universal_time()
|
||||||
@@ -42,7 +49,7 @@ get_by_id(Id) ->
|
|||||||
[User] -> {ok, User}
|
[User] -> {ok, User}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение пользователя по email (через индекс позже)
|
%% Получение пользователя по email
|
||||||
get_by_email(Email) ->
|
get_by_email(Email) ->
|
||||||
Match = #user{email = Email, _ = '_'},
|
Match = #user{email = Email, _ = '_'},
|
||||||
case mnesia:dirty_match_object(Match) of
|
case mnesia:dirty_match_object(Match) of
|
||||||
@@ -82,11 +89,15 @@ delete(Id) ->
|
|||||||
%% Внутренние функции
|
%% Внутренние функции
|
||||||
generate_id() ->
|
generate_id() ->
|
||||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
||||||
apply_updates(User, Updates) ->
|
|
||||||
lists:foldl(fun({Field, Value}, U) -> set_field(Field, Value, U) end, User, Updates).
|
|
||||||
|
|
||||||
set_field(email, Value, User) -> User#user{email = Value, updated_at = calendar:universal_time()};
|
apply_updates(User, Updates) ->
|
||||||
set_field(password_hash, Value, User) -> User#user{password_hash = Value, updated_at = calendar:universal_time()};
|
Updated = lists:foldl(fun({Field, Value}, U) ->
|
||||||
set_field(role, Value, User) -> User#user{role = Value, updated_at = calendar:universal_time()};
|
set_field(Field, Value, U)
|
||||||
set_field(status, Value, User) -> User#user{status = Value, updated_at = calendar:universal_time()};
|
end, User, Updates),
|
||||||
set_field(_, _, User) -> User.
|
Updated#user{updated_at = calendar:universal_time()}.
|
||||||
|
|
||||||
|
set_field(email, Value, U) -> U#user{email = Value};
|
||||||
|
set_field(password_hash, Value, U) -> U#user{password_hash = Value};
|
||||||
|
set_field(role, Value, U) when Value =:= user; Value =:= admin -> U#user{role = Value};
|
||||||
|
set_field(status, Value, U) when Value =:= active; Value =:= frozen; Value =:= deleted -> U#user{status = Value};
|
||||||
|
set_field(_, _, U) -> U.
|
||||||
@@ -4,19 +4,14 @@
|
|||||||
-export([start/2, stop/1]).
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
% Запускаем Mnesia
|
|
||||||
application:ensure_all_started(mnesia),
|
application:ensure_all_started(mnesia),
|
||||||
application:ensure_all_started(cowboy),
|
application:ensure_all_started(cowboy),
|
||||||
|
|
||||||
case infra_sup:start_link() of
|
case infra_sup:start_link() of
|
||||||
{ok, Pid} ->
|
{ok, Pid} ->
|
||||||
% Инициализируем таблицы и ждем готовности
|
|
||||||
ok = infra_mnesia:init_tables(),
|
ok = infra_mnesia:init_tables(),
|
||||||
ok = infra_mnesia:wait_for_tables(),
|
ok = infra_mnesia:wait_for_tables(),
|
||||||
|
|
||||||
% Запускаем HTTP-сервер
|
|
||||||
start_http(),
|
start_http(),
|
||||||
|
|
||||||
{ok, Pid};
|
{ok, Pid};
|
||||||
Error ->
|
Error ->
|
||||||
Error
|
Error
|
||||||
@@ -25,11 +20,9 @@ start(_StartType, _StartArgs) ->
|
|||||||
stop(_State) ->
|
stop(_State) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%% Internal functions
|
|
||||||
start_http() ->
|
start_http() ->
|
||||||
Port = application:get_env(eventhub, http_port, 8080),
|
Port = application:get_env(eventhub, http_port, 8080),
|
||||||
|
|
||||||
% Настройка маршрутов
|
|
||||||
Dispatch = cowboy_router:compile([
|
Dispatch = cowboy_router:compile([
|
||||||
{'_', [
|
{'_', [
|
||||||
{"/health", handler_health, []},
|
{"/health", handler_health, []},
|
||||||
@@ -38,6 +31,7 @@ start_http() ->
|
|||||||
{"/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/user/bookings", handler_user_bookings, []},
|
||||||
|
{"/v1/search", handler_search, []},
|
||||||
{"/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, []},
|
||||||
@@ -49,7 +43,6 @@ start_http() ->
|
|||||||
]}
|
]}
|
||||||
]),
|
]),
|
||||||
|
|
||||||
% Настройка middleware
|
|
||||||
Middlewares = [
|
Middlewares = [
|
||||||
cowboy_router,
|
cowboy_router,
|
||||||
cowboy_handler
|
cowboy_handler
|
||||||
|
|||||||
@@ -24,9 +24,16 @@ create_calendar(Req) ->
|
|||||||
#{<<"title">> := Title} ->
|
#{<<"title">> := Title} ->
|
||||||
Description = maps:get(<<"description">>, Decoded, <<"">>),
|
Description = maps:get(<<"description">>, Decoded, <<"">>),
|
||||||
Confirmation = parse_confirmation(maps:get(<<"confirmation">>, Decoded, <<"manual">>)),
|
Confirmation = parse_confirmation(maps:get(<<"confirmation">>, Decoded, <<"manual">>)),
|
||||||
|
Tags = maps:get(<<"tags">>, Decoded, []),
|
||||||
|
Type = parse_type(maps:get(<<"type">>, Decoded, <<"personal">>)),
|
||||||
|
|
||||||
case logic_calendar:create_calendar(UserId, Title, Description, Confirmation) of
|
case logic_calendar:create_calendar(UserId, Title, Description, Confirmation) of
|
||||||
{ok, Calendar} ->
|
{ok, Calendar} ->
|
||||||
Response = calendar_to_json(Calendar),
|
% Обновляем теги и тип
|
||||||
|
Updates = [{tags, Tags}, {type, Type}],
|
||||||
|
core_calendar:update(Calendar#calendar.id, Updates),
|
||||||
|
{ok, Updated} = core_calendar:get_by_id(Calendar#calendar.id),
|
||||||
|
Response = calendar_to_json(Updated),
|
||||||
send_json(Req2, 201, Response);
|
send_json(Req2, 201, Response);
|
||||||
{error, user_inactive} ->
|
{error, user_inactive} ->
|
||||||
send_error(Req2, 403, <<"User account is not active">>);
|
send_error(Req2, 403, <<"User account is not active">>);
|
||||||
@@ -51,6 +58,10 @@ parse_confirmation(<<"manual">>) -> manual;
|
|||||||
parse_confirmation(#{<<"timeout">> := N}) when is_integer(N), N > 0 -> {timeout, N};
|
parse_confirmation(#{<<"timeout">> := N}) when is_integer(N), N > 0 -> {timeout, N};
|
||||||
parse_confirmation(_) -> manual.
|
parse_confirmation(_) -> manual.
|
||||||
|
|
||||||
|
parse_type(<<"personal">>) -> personal;
|
||||||
|
parse_type(<<"commercial">>) -> commercial;
|
||||||
|
parse_type(_) -> personal.
|
||||||
|
|
||||||
%% GET /v1/calendars - список календарей
|
%% GET /v1/calendars - список календарей
|
||||||
list_calendars(Req) ->
|
list_calendars(Req) ->
|
||||||
case handler_auth:authenticate(Req) of
|
case handler_auth:authenticate(Req) of
|
||||||
|
|||||||
@@ -43,9 +43,8 @@ update_event(Req) ->
|
|||||||
try jsx:decode(Body, [return_maps]) of
|
try jsx:decode(Body, [return_maps]) of
|
||||||
UpdatesMap when is_map(UpdatesMap) ->
|
UpdatesMap when is_map(UpdatesMap) ->
|
||||||
Updates = maps:to_list(UpdatesMap),
|
Updates = maps:to_list(UpdatesMap),
|
||||||
% Преобразуем строку времени в datetime если есть
|
UpdatesWithTypes = convert_fields(Updates),
|
||||||
UpdatesWithTime = convert_time_field(Updates),
|
case logic_event:update_event(UserId, EventId, UpdatesWithTypes) of
|
||||||
case logic_event:update_event(UserId, EventId, UpdatesWithTime) of
|
|
||||||
{ok, Event} ->
|
{ok, Event} ->
|
||||||
Response = event_to_json(Event),
|
Response = event_to_json(Event),
|
||||||
send_json(Req2, 200, Response);
|
send_json(Req2, 200, Response);
|
||||||
@@ -88,13 +87,22 @@ delete_event(Req) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
%% Вспомогательные функции
|
%% Вспомогательные функции
|
||||||
convert_time_field(Updates) ->
|
convert_fields(Updates) ->
|
||||||
lists:map(fun
|
lists:map(fun
|
||||||
({start_time, Value}) when is_binary(Value) ->
|
({start_time, Value}) when is_binary(Value) ->
|
||||||
case parse_datetime(Value) of
|
case parse_datetime(Value) of
|
||||||
{ok, DateTime} -> {start_time, DateTime};
|
{ok, DateTime} -> {start_time, DateTime};
|
||||||
_ -> {start_time, Value}
|
_ -> {start_time, Value}
|
||||||
end;
|
end;
|
||||||
|
({location, Value}) when is_map(Value) ->
|
||||||
|
case Value of
|
||||||
|
#{<<"lat">> := Lat, <<"lon">> := Lon} ->
|
||||||
|
Address = maps:get(<<"address">>, Value, <<"">>),
|
||||||
|
{location, #location{address = Address, lat = Lat, lon = Lon}};
|
||||||
|
_ -> {location, undefined}
|
||||||
|
end;
|
||||||
|
({tags, Value}) when is_list(Value) ->
|
||||||
|
{tags, Value};
|
||||||
(Other) -> Other
|
(Other) -> Other
|
||||||
end, Updates).
|
end, Updates).
|
||||||
|
|
||||||
@@ -125,6 +133,16 @@ event_to_json(Event) ->
|
|||||||
#{address => Addr, lat => Lat, lon => Lon}
|
#{address => Addr, lat => Lat, lon => Lon}
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
RecurrenceJson = case Event#event.recurrence_rule of
|
||||||
|
undefined -> null;
|
||||||
|
Rule ->
|
||||||
|
Decoded = jsx:decode(Rule, [return_maps]),
|
||||||
|
case Decoded of
|
||||||
|
Map when is_map(Map) -> Map;
|
||||||
|
{ok, Map} -> Map
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
#{
|
#{
|
||||||
id => Event#event.id,
|
id => Event#event.id,
|
||||||
calendar_id => Event#event.calendar_id,
|
calendar_id => Event#event.calendar_id,
|
||||||
@@ -133,6 +151,9 @@ event_to_json(Event) ->
|
|||||||
event_type => Event#event.event_type,
|
event_type => Event#event.event_type,
|
||||||
start_time => datetime_to_iso8601(Event#event.start_time),
|
start_time => datetime_to_iso8601(Event#event.start_time),
|
||||||
duration => Event#event.duration,
|
duration => Event#event.duration,
|
||||||
|
recurrence => RecurrenceJson,
|
||||||
|
master_id => Event#event.master_id,
|
||||||
|
is_instance => Event#event.is_instance,
|
||||||
specialist_id => Event#event.specialist_id,
|
specialist_id => Event#event.specialist_id,
|
||||||
location => LocationJson,
|
location => LocationJson,
|
||||||
tags => Event#event.tags,
|
tags => Event#event.tags,
|
||||||
|
|||||||
@@ -27,13 +27,19 @@ create_event(Req) ->
|
|||||||
<<"duration">> := Duration} ->
|
<<"duration">> := Duration} ->
|
||||||
case parse_datetime(StartTimeStr) of
|
case parse_datetime(StartTimeStr) of
|
||||||
{ok, StartTime} ->
|
{ok, StartTime} ->
|
||||||
|
% Парсим location если есть
|
||||||
|
Location = parse_location(maps:get(<<"location">>, Decoded, undefined)),
|
||||||
|
|
||||||
% Проверяем, есть ли правило повторения
|
% Проверяем, есть ли правило повторения
|
||||||
case maps:get(<<"recurrence">>, Decoded, undefined) of
|
case maps:get(<<"recurrence">>, Decoded, undefined) of
|
||||||
undefined ->
|
undefined ->
|
||||||
% Одиночное событие
|
% Одиночное событие
|
||||||
case logic_event:create_event(UserId, CalendarId, Title, StartTime, Duration) of
|
case logic_event:create_event(UserId, CalendarId, Title, StartTime, Duration) of
|
||||||
{ok, Event} ->
|
{ok, Event} ->
|
||||||
Response = event_to_json(Event),
|
% Обновляем location и capacity если нужно
|
||||||
|
update_event_fields(Event#event.id, Location, Decoded),
|
||||||
|
{ok, UpdatedEvent} = core_event:get_by_id(Event#event.id),
|
||||||
|
Response = event_to_json(UpdatedEvent),
|
||||||
send_json(Req2, 201, Response);
|
send_json(Req2, 201, Response);
|
||||||
{error, access_denied} ->
|
{error, access_denied} ->
|
||||||
send_error(Req2, 403, <<"Access denied">>);
|
send_error(Req2, 403, <<"Access denied">>);
|
||||||
@@ -48,7 +54,9 @@ create_event(Req) ->
|
|||||||
% Повторяющееся событие
|
% Повторяющееся событие
|
||||||
case logic_event:create_recurring_event(UserId, CalendarId, Title, StartTime, Duration, RRule) of
|
case logic_event:create_recurring_event(UserId, CalendarId, Title, StartTime, Duration, RRule) of
|
||||||
{ok, Event} ->
|
{ok, Event} ->
|
||||||
Response = event_to_json(Event),
|
update_event_fields(Event#event.id, Location, Decoded),
|
||||||
|
{ok, UpdatedEvent} = core_event:get_by_id(Event#event.id),
|
||||||
|
Response = event_to_json(UpdatedEvent),
|
||||||
send_json(Req2, 201, Response);
|
send_json(Req2, 201, Response);
|
||||||
{error, invalid_rrule} ->
|
{error, invalid_rrule} ->
|
||||||
send_error(Req2, 400, <<"Invalid recurrence rule">>);
|
send_error(Req2, 400, <<"Invalid recurrence rule">>);
|
||||||
@@ -83,14 +91,12 @@ list_events(Req) ->
|
|||||||
case handler_auth:authenticate(Req) of
|
case handler_auth:authenticate(Req) of
|
||||||
{ok, UserId, Req1} ->
|
{ok, UserId, Req1} ->
|
||||||
CalendarId = cowboy_req:binding(calendar_id, Req1),
|
CalendarId = cowboy_req:binding(calendar_id, Req1),
|
||||||
% Проверяем параметры запроса для диапазона дат
|
|
||||||
Qs = cowboy_req:parse_qs(Req1),
|
Qs = cowboy_req:parse_qs(Req1),
|
||||||
From = proplists:get_value(<<"from">>, Qs, undefined),
|
From = proplists:get_value(<<"from">>, Qs, undefined),
|
||||||
To = proplists:get_value(<<"to">>, Qs, undefined),
|
To = proplists:get_value(<<"to">>, Qs, undefined),
|
||||||
|
|
||||||
case logic_event:list_events(UserId, CalendarId) of
|
case logic_event:list_events(UserId, CalendarId) of
|
||||||
{ok, Events} ->
|
{ok, Events} ->
|
||||||
% Если указан диапазон, разворачиваем повторяющиеся события
|
|
||||||
Response = case {From, To} of
|
Response = case {From, To} of
|
||||||
{undefined, undefined} ->
|
{undefined, undefined} ->
|
||||||
[event_to_json(E) || E <- Events];
|
[event_to_json(E) || E <- Events];
|
||||||
@@ -111,7 +117,43 @@ list_events(Req) ->
|
|||||||
send_error(Req1, Code, Message)
|
send_error(Req1, Code, Message)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Разворачивание повторяющихся событий в диапазоне
|
%% Вспомогательные функции
|
||||||
|
update_event_fields(EventId, Location, Decoded) ->
|
||||||
|
Updates = [],
|
||||||
|
Updates1 = case Location of
|
||||||
|
undefined -> Updates;
|
||||||
|
_ -> [{location, Location} | Updates]
|
||||||
|
end,
|
||||||
|
Updates2 = case maps:get(<<"capacity">>, Decoded, undefined) of
|
||||||
|
undefined -> Updates1;
|
||||||
|
Cap -> [{capacity, Cap} | Updates1]
|
||||||
|
end,
|
||||||
|
Updates3 = case maps:get(<<"tags">>, Decoded, undefined) of
|
||||||
|
undefined -> Updates2;
|
||||||
|
Tags -> [{tags, Tags} | Updates2]
|
||||||
|
end,
|
||||||
|
Updates4 = case maps:get(<<"description">>, Decoded, undefined) of
|
||||||
|
undefined -> Updates3;
|
||||||
|
Desc -> [{description, Desc} | Updates3]
|
||||||
|
end,
|
||||||
|
Updates5 = case maps:get(<<"online_link">>, Decoded, undefined) of
|
||||||
|
undefined -> Updates4;
|
||||||
|
Link -> [{online_link, Link} | Updates4]
|
||||||
|
end,
|
||||||
|
if Updates5 /= [] -> core_event:update(EventId, Updates5);
|
||||||
|
true -> ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
parse_location(undefined) -> undefined;
|
||||||
|
parse_location(LocationMap) when is_map(LocationMap) ->
|
||||||
|
case LocationMap of
|
||||||
|
#{<<"lat">> := Lat, <<"lon">> := Lon} ->
|
||||||
|
Address = maps:get(<<"address">>, LocationMap, <<"">>),
|
||||||
|
#location{address = Address, lat = Lat, lon = Lon};
|
||||||
|
_ -> undefined
|
||||||
|
end;
|
||||||
|
parse_location(_) -> undefined.
|
||||||
|
|
||||||
expand_recurring_events(UserId, Events, From, To) ->
|
expand_recurring_events(UserId, Events, From, To) ->
|
||||||
lists:flatmap(fun(Event) ->
|
lists:flatmap(fun(Event) ->
|
||||||
case Event#event.event_type of
|
case Event#event.event_type of
|
||||||
@@ -148,7 +190,6 @@ parse_datetime_binary(Str) ->
|
|||||||
{ok, Dt} = parse_datetime(Str),
|
{ok, Dt} = parse_datetime(Str),
|
||||||
Dt.
|
Dt.
|
||||||
|
|
||||||
%% Вспомогательные функции
|
|
||||||
parse_datetime(Str) ->
|
parse_datetime(Str) ->
|
||||||
try
|
try
|
||||||
[DateStr, TimeStr] = string:split(Str, "T"),
|
[DateStr, TimeStr] = string:split(Str, "T"),
|
||||||
@@ -178,7 +219,12 @@ event_to_json(Event) ->
|
|||||||
|
|
||||||
RecurrenceJson = case Event#event.recurrence_rule of
|
RecurrenceJson = case Event#event.recurrence_rule of
|
||||||
undefined -> null;
|
undefined -> null;
|
||||||
Rule -> jsx:decode(Rule, [return_maps])
|
Rule ->
|
||||||
|
Decoded = jsx:decode(Rule, [return_maps]),
|
||||||
|
case Decoded of
|
||||||
|
Map when is_map(Map) -> Map;
|
||||||
|
{ok, Map} -> Map
|
||||||
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
#{
|
#{
|
||||||
|
|||||||
104
src/handlers/handler_search.erl
Normal file
104
src/handlers/handler_search.erl
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
-module(handler_search).
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
-export([init/2]).
|
||||||
|
|
||||||
|
init(Req, Opts) ->
|
||||||
|
handle(Req, Opts).
|
||||||
|
|
||||||
|
handle(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"GET">> -> search(Req);
|
||||||
|
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
search(Req) ->
|
||||||
|
case handler_auth:authenticate(Req) of
|
||||||
|
{ok, UserId, Req1} ->
|
||||||
|
Qs = cowboy_req:parse_qs(Req1),
|
||||||
|
|
||||||
|
Type = proplists:get_value(<<"type">>, Qs, undefined),
|
||||||
|
Query = proplists:get_value(<<"q">>, Qs, undefined),
|
||||||
|
|
||||||
|
Params = parse_params(Qs),
|
||||||
|
|
||||||
|
case logic_search:search(Type, Query, UserId, Params) of
|
||||||
|
{ok, Total, Results} ->
|
||||||
|
Response = #{
|
||||||
|
total => Total,
|
||||||
|
limit => maps:get(limit, Params, 20),
|
||||||
|
offset => maps:get(offset, Params, 0),
|
||||||
|
results => Results
|
||||||
|
},
|
||||||
|
send_json(Req1, 200, Response);
|
||||||
|
{error, _} ->
|
||||||
|
send_error(Req1, 500, <<"Search failed">>)
|
||||||
|
end;
|
||||||
|
{error, Code, Message, Req1} ->
|
||||||
|
send_error(Req1, Code, Message)
|
||||||
|
end.
|
||||||
|
|
||||||
|
parse_params(Qs) ->
|
||||||
|
Params = #{
|
||||||
|
limit => parse_int_param(Qs, <<"limit">>, 20),
|
||||||
|
offset => parse_int_param(Qs, <<"offset">>, 0),
|
||||||
|
tags => proplists:get_value(<<"tags">>, Qs),
|
||||||
|
sort => proplists:get_value(<<"sort">>, Qs, <<"start_time">>),
|
||||||
|
order => proplists:get_value(<<"order">>, Qs, <<"asc">>)
|
||||||
|
},
|
||||||
|
|
||||||
|
Params1 = case {parse_float_param(Qs, <<"lat">>), parse_float_param(Qs, <<"lon">>)} of
|
||||||
|
{{ok, Lat}, {ok, Lon}} ->
|
||||||
|
Radius = parse_int_param(Qs, <<"radius">>, 10),
|
||||||
|
Params#{lat => Lat, lon => Lon, radius => Radius};
|
||||||
|
_ -> Params
|
||||||
|
end,
|
||||||
|
|
||||||
|
Params2 = case {parse_datetime_param(Qs, <<"from">>), parse_datetime_param(Qs, <<"to">>)} of
|
||||||
|
{{ok, From}, {ok, To}} ->
|
||||||
|
Params1#{from => From, to => To};
|
||||||
|
{{ok, From}, error} ->
|
||||||
|
Params1#{from => From};
|
||||||
|
{error, {ok, To}} ->
|
||||||
|
Params1#{to => To};
|
||||||
|
_ -> Params1
|
||||||
|
end,
|
||||||
|
|
||||||
|
Params2.
|
||||||
|
|
||||||
|
parse_int_param(Qs, Key, Default) ->
|
||||||
|
case proplists:get_value(Key, Qs) of
|
||||||
|
undefined -> Default;
|
||||||
|
Val -> binary_to_integer(Val)
|
||||||
|
end.
|
||||||
|
|
||||||
|
parse_float_param(Qs, Key) ->
|
||||||
|
case proplists:get_value(Key, Qs) of
|
||||||
|
undefined -> error;
|
||||||
|
Val -> {ok, binary_to_float(Val)}
|
||||||
|
end.
|
||||||
|
|
||||||
|
parse_datetime_param(Qs, Key) ->
|
||||||
|
case proplists:get_value(Key, Qs) of
|
||||||
|
undefined -> error;
|
||||||
|
Val ->
|
||||||
|
try
|
||||||
|
[DateStr, TimeStr] = string:split(Val, "T"),
|
||||||
|
TimeStrNoZ = string:trim(TimeStr, trailing, "Z"),
|
||||||
|
|
||||||
|
[Y, M, D] = [binary_to_integer(X) || X <- string:split(DateStr, "-", all)],
|
||||||
|
[H, Min, S] = [binary_to_integer(X) || X <- string:split(TimeStrNoZ, ":", all)],
|
||||||
|
|
||||||
|
{ok, {{Y, M, D}, {H, Min, S}}}
|
||||||
|
catch
|
||||||
|
_:_ -> error
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
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).
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
-module(logic_calendar).
|
-module(logic_calendar).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
-export([create_calendar/4, get_calendar/2, list_calendars/1,
|
-export([create_calendar/3, 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]).
|
||||||
|
|
||||||
%% Создание календаря
|
%% Создание календаря с политикой по умолчанию (manual)
|
||||||
|
create_calendar(UserId, Title, Description) ->
|
||||||
|
create_calendar(UserId, Title, Description, manual).
|
||||||
|
|
||||||
|
%% Создание календаря с указанной политикой подтверждения
|
||||||
create_calendar(UserId, Title, Description, Confirmation) ->
|
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} ->
|
||||||
@@ -41,7 +45,6 @@ update_calendar(UserId, CalendarId, Updates) ->
|
|||||||
{ok, Calendar} ->
|
{ok, Calendar} ->
|
||||||
case can_edit(UserId, Calendar) of
|
case can_edit(UserId, Calendar) of
|
||||||
true ->
|
true ->
|
||||||
% Валидация обновлений
|
|
||||||
ValidUpdates = validate_updates(Updates),
|
ValidUpdates = validate_updates(Updates),
|
||||||
core_calendar:update(CalendarId, ValidUpdates);
|
core_calendar:update(CalendarId, ValidUpdates);
|
||||||
false ->
|
false ->
|
||||||
@@ -66,13 +69,20 @@ delete_calendar(UserId, CalendarId) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
%% Проверка прав доступа (просмотр)
|
%% Проверка прав доступа (просмотр)
|
||||||
can_access(UserId, #calendar{owner_id = UserId, status = active}) -> true;
|
can_access(UserId, #calendar{owner_id = UserId, status = active}) ->
|
||||||
can_access(_UserId, #calendar{type = commercial, status = active}) -> true;
|
true;
|
||||||
can_access(_UserId, _) -> false.
|
can_access(_UserId, #calendar{type = commercial, status = active}) ->
|
||||||
|
true;
|
||||||
|
can_access(_UserId, _) ->
|
||||||
|
false.
|
||||||
|
|
||||||
%% Проверка прав редактирования
|
%% Проверка прав редактирования
|
||||||
can_edit(UserId, #calendar{owner_id = UserId, status = active}) -> true;
|
can_edit(UserId, #calendar{owner_id = UserId, status = active}) ->
|
||||||
can_edit(_, _) -> false.
|
true;
|
||||||
|
can_edit(_UserId, #calendar{owner_id = _OwnerId}) ->
|
||||||
|
false;
|
||||||
|
can_edit(_, _) ->
|
||||||
|
false.
|
||||||
|
|
||||||
%% Валидация полей обновления
|
%% Валидация полей обновления
|
||||||
validate_updates(Updates) ->
|
validate_updates(Updates) ->
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
-export([create_event/5, create_recurring_event/6, get_event/2, list_events/2,
|
-export([create_event/5, create_recurring_event/6, get_event/2, list_events/2,
|
||||||
update_event/3, delete_event/2]).
|
update_event/3, delete_event/2]).
|
||||||
-export([validate_event_time/1, get_occurrences/3, cancel_occurrence/3]).
|
-export([validate_event_time/1, validate_event_time/2, get_occurrences/3, cancel_occurrence/3]).
|
||||||
-export([materialize_for_booking/3]).
|
-export([materialize_for_booking/3]).
|
||||||
|
|
||||||
%% Создание одиночного события
|
%% Создание одиночного события
|
||||||
@@ -12,7 +12,7 @@ create_event(UserId, CalendarId, Title, StartTime, Duration) ->
|
|||||||
{ok, Calendar} ->
|
{ok, Calendar} ->
|
||||||
case logic_calendar:can_edit(UserId, Calendar) of
|
case logic_calendar:can_edit(UserId, Calendar) of
|
||||||
true ->
|
true ->
|
||||||
case validate_event_time(StartTime) of
|
case validate_event_time(StartTime, UserId) of
|
||||||
ok ->
|
ok ->
|
||||||
core_event:create(CalendarId, Title, StartTime, Duration);
|
core_event:create(CalendarId, Title, StartTime, Duration);
|
||||||
{error, _} = Error ->
|
{error, _} = Error ->
|
||||||
@@ -31,7 +31,7 @@ create_recurring_event(UserId, CalendarId, Title, StartTime, Duration, RRule) ->
|
|||||||
{ok, Calendar} ->
|
{ok, Calendar} ->
|
||||||
case logic_calendar:can_edit(UserId, Calendar) of
|
case logic_calendar:can_edit(UserId, Calendar) of
|
||||||
true ->
|
true ->
|
||||||
case validate_event_time(StartTime) of
|
case validate_event_time(StartTime, UserId) of
|
||||||
ok ->
|
ok ->
|
||||||
case logic_recurrence:validate_rrule(RRule) of
|
case logic_recurrence:validate_rrule(RRule) of
|
||||||
true ->
|
true ->
|
||||||
@@ -49,7 +49,6 @@ create_recurring_event(UserId, CalendarId, Title, StartTime, Duration, RRule) ->
|
|||||||
Error
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Получение вхождений повторяющегося события в диапазоне
|
|
||||||
%% Получение вхождений повторяющегося события в диапазоне
|
%% Получение вхождений повторяющегося события в диапазоне
|
||||||
get_occurrences(UserId, MasterId, RangeEnd) ->
|
get_occurrences(UserId, MasterId, RangeEnd) ->
|
||||||
case get_event(UserId, MasterId) of
|
case get_event(UserId, MasterId) of
|
||||||
@@ -61,18 +60,12 @@ get_occurrences(UserId, MasterId, RangeEnd) ->
|
|||||||
end,
|
end,
|
||||||
{ok, ParsedRule} = logic_recurrence:parse_rrule(RRuleMap),
|
{ok, ParsedRule} = logic_recurrence:parse_rrule(RRuleMap),
|
||||||
|
|
||||||
% Генерируем вхождения
|
|
||||||
Occurrences = logic_recurrence:generate_occurrences(
|
Occurrences = logic_recurrence:generate_occurrences(
|
||||||
Event#event.start_time, ParsedRule, RangeEnd
|
Event#event.start_time, ParsedRule, RangeEnd
|
||||||
),
|
),
|
||||||
|
|
||||||
% Получаем исключения
|
|
||||||
Exceptions = get_exceptions(MasterId),
|
Exceptions = get_exceptions(MasterId),
|
||||||
|
|
||||||
% Фильтруем отменённые вхождения
|
|
||||||
ValidOccurrences = filter_cancelled(Occurrences, Exceptions),
|
ValidOccurrences = filter_cancelled(Occurrences, Exceptions),
|
||||||
|
|
||||||
% Проверяем материализованные вхождения (могут иметь изменения)
|
|
||||||
FinalOccurrences = merge_materialized(MasterId, ValidOccurrences),
|
FinalOccurrences = merge_materialized(MasterId, ValidOccurrences),
|
||||||
|
|
||||||
{ok, FinalOccurrences};
|
{ok, FinalOccurrences};
|
||||||
@@ -90,7 +83,6 @@ cancel_occurrence(UserId, MasterId, OccurrenceStart) ->
|
|||||||
{ok, Calendar} ->
|
{ok, Calendar} ->
|
||||||
case logic_calendar:can_edit(UserId, Calendar) of
|
case logic_calendar:can_edit(UserId, Calendar) of
|
||||||
true ->
|
true ->
|
||||||
% Добавляем исключение
|
|
||||||
Exception = #recurrence_exception{
|
Exception = #recurrence_exception{
|
||||||
master_id = MasterId,
|
master_id = MasterId,
|
||||||
original_start = OccurrenceStart,
|
original_start = OccurrenceStart,
|
||||||
@@ -144,7 +136,7 @@ update_event(UserId, EventId, Updates) ->
|
|||||||
{ok, Calendar} ->
|
{ok, Calendar} ->
|
||||||
case logic_calendar:can_edit(UserId, Calendar) of
|
case logic_calendar:can_edit(UserId, Calendar) of
|
||||||
true ->
|
true ->
|
||||||
ValidUpdates = validate_updates(Updates),
|
ValidUpdates = validate_updates(Updates, UserId),
|
||||||
core_event:update(EventId, ValidUpdates);
|
core_event:update(EventId, ValidUpdates);
|
||||||
false ->
|
false ->
|
||||||
{error, access_denied}
|
{error, access_denied}
|
||||||
@@ -175,36 +167,53 @@ delete_event(UserId, EventId) ->
|
|||||||
Error
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Валидация времени события
|
%% Валидация времени события (без учёта пользователя)
|
||||||
validate_event_time(StartTime) ->
|
validate_event_time(StartTime) ->
|
||||||
|
validate_event_time(StartTime, undefined).
|
||||||
|
|
||||||
|
%% Валидация времени события с учётом роли пользователя
|
||||||
|
validate_event_time(StartTime, UserId) ->
|
||||||
|
case is_admin(UserId) of
|
||||||
|
true ->
|
||||||
|
ok;
|
||||||
|
false ->
|
||||||
Now = calendar:universal_time(),
|
Now = calendar:universal_time(),
|
||||||
case StartTime > Now of
|
case StartTime > Now of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> {error, event_in_past}
|
false -> {error, event_in_past}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Проверка, является ли пользователь администратором
|
||||||
|
is_admin(undefined) -> false;
|
||||||
|
is_admin(UserId) ->
|
||||||
|
case core_user:get_by_id(UserId) of
|
||||||
|
{ok, User} -> User#user.role =:= admin;
|
||||||
|
_ -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Внутренние функции
|
%% Внутренние функции
|
||||||
validate_updates(Updates) ->
|
validate_updates(Updates, UserId) ->
|
||||||
lists:filter(fun validate_update/1, Updates).
|
lists:filter(fun(Update) -> validate_update(Update, UserId) end, Updates).
|
||||||
|
|
||||||
validate_update({title, Value}) when is_binary(Value) -> true;
|
validate_update({title, Value}, _) when is_binary(Value) -> true;
|
||||||
validate_update({description, Value}) when is_binary(Value) -> true;
|
validate_update({description, Value}, _) when is_binary(Value) -> true;
|
||||||
validate_update({start_time, Value}) ->
|
validate_update({start_time, Value}, UserId) ->
|
||||||
case validate_event_time(Value) of
|
case validate_event_time(Value, UserId) of
|
||||||
ok -> true;
|
ok -> true;
|
||||||
_ -> false
|
_ -> false
|
||||||
end;
|
end;
|
||||||
validate_update({duration, Value}) when is_integer(Value), Value > 0 -> true;
|
validate_update({duration, Value}, _) when is_integer(Value), Value > 0 -> true;
|
||||||
validate_update({specialist_id, Value}) when is_binary(Value) -> true;
|
validate_update({specialist_id, Value}, _) when is_binary(Value) -> true;
|
||||||
validate_update({location, Value}) ->
|
validate_update({location, Value}, _) ->
|
||||||
case Value of
|
case Value of
|
||||||
#location{} -> true;
|
#location{} -> true;
|
||||||
_ -> false
|
_ -> false
|
||||||
end;
|
end;
|
||||||
validate_update({tags, Value}) when is_list(Value) -> true;
|
validate_update({tags, Value}, _) when is_list(Value) -> true;
|
||||||
validate_update({capacity, Value}) when is_integer(Value), Value > 0 -> true;
|
validate_update({capacity, Value}, _) when is_integer(Value), Value > 0 -> true;
|
||||||
validate_update({online_link, Value}) when is_binary(Value) -> true;
|
validate_update({online_link, Value}, _) when is_binary(Value) -> true;
|
||||||
validate_update(_) -> false.
|
validate_update(_, _) -> false.
|
||||||
|
|
||||||
get_exceptions(MasterId) ->
|
get_exceptions(MasterId) ->
|
||||||
Match = #recurrence_exception{master_id = MasterId, _ = '_'},
|
Match = #recurrence_exception{master_id = MasterId, _ = '_'},
|
||||||
|
|||||||
241
src/logic/logic_search.erl
Normal file
241
src/logic/logic_search.erl
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
-module(logic_search).
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
-export([search/4]).
|
||||||
|
|
||||||
|
-define(DEFAULT_LIMIT, 20).
|
||||||
|
-define(MAX_LIMIT, 100).
|
||||||
|
-define(EARTH_RADIUS_KM, 6371.0).
|
||||||
|
|
||||||
|
%% Поиск событий и календарей
|
||||||
|
search(Type, Query, UserId, Params) ->
|
||||||
|
Limit = min(maps:get(limit, Params, ?DEFAULT_LIMIT), ?MAX_LIMIT),
|
||||||
|
Offset = maps:get(offset, Params, 0),
|
||||||
|
|
||||||
|
case Type of
|
||||||
|
<<"event">> -> search_events(Query, UserId, Params, Limit, Offset);
|
||||||
|
<<"calendar">> -> search_calendars(Query, UserId, Params, Limit, Offset);
|
||||||
|
_ -> search_all(Query, UserId, Params, Limit, Offset)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% ============ Поиск событий ============
|
||||||
|
search_events(Query, UserId, Params, Limit, Offset) ->
|
||||||
|
AllEvents = get_all_events(),
|
||||||
|
AccessibleEvents = filter_accessible_events(AllEvents, UserId),
|
||||||
|
Filtered = apply_event_filters(AccessibleEvents, Query, Params),
|
||||||
|
Sorted = sort_events(Filtered, Params),
|
||||||
|
Paginated = paginate(Sorted, Limit, Offset),
|
||||||
|
|
||||||
|
{ok, length(Filtered), format_events(Paginated)}.
|
||||||
|
|
||||||
|
%% ============ Поиск календарей ============
|
||||||
|
search_calendars(Query, UserId, Params, Limit, Offset) ->
|
||||||
|
AllCalendars = get_all_calendars(),
|
||||||
|
AccessibleCalendars = filter_accessible_calendars(AllCalendars, UserId),
|
||||||
|
Filtered = apply_calendar_filters(AccessibleCalendars, Query, Params),
|
||||||
|
Paginated = paginate(Filtered, Limit, Offset),
|
||||||
|
|
||||||
|
{ok, length(Filtered), format_calendars(Paginated)}.
|
||||||
|
|
||||||
|
%% ============ Поиск всего ============
|
||||||
|
search_all(Query, UserId, Params, Limit, Offset) ->
|
||||||
|
{ok, EventsTotal, Events} = search_events(Query, UserId, Params, Limit, Offset),
|
||||||
|
{ok, CalendarsTotal, Calendars} = search_calendars(Query, UserId, Params, Limit, Offset),
|
||||||
|
|
||||||
|
{ok, EventsTotal + CalendarsTotal, #{
|
||||||
|
events => Events,
|
||||||
|
calendars => Calendars
|
||||||
|
}}.
|
||||||
|
|
||||||
|
%% ============ Получение данных ============
|
||||||
|
get_all_events() ->
|
||||||
|
Match = #event{status = active, is_instance = false, _ = '_'},
|
||||||
|
mnesia:dirty_match_object(Match).
|
||||||
|
|
||||||
|
get_all_calendars() ->
|
||||||
|
Match = #calendar{status = active, _ = '_'},
|
||||||
|
mnesia:dirty_match_object(Match).
|
||||||
|
|
||||||
|
%% ============ Фильтрация по доступности ============
|
||||||
|
filter_accessible_events(Events, UserId) ->
|
||||||
|
lists:filter(fun(Event) ->
|
||||||
|
case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||||
|
{ok, Calendar} ->
|
||||||
|
CanAccess = logic_calendar:can_access(UserId, Calendar),
|
||||||
|
case CanAccess of
|
||||||
|
false ->
|
||||||
|
io:format("Access denied for user ~p to calendar ~p (type: ~p, owner: ~p, status: ~p)~n",
|
||||||
|
[UserId, Calendar#calendar.id, Calendar#calendar.type,
|
||||||
|
Calendar#calendar.owner_id, Calendar#calendar.status]);
|
||||||
|
true -> ok
|
||||||
|
end,
|
||||||
|
CanAccess;
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end, Events).
|
||||||
|
|
||||||
|
filter_accessible_calendars(Calendars, UserId) ->
|
||||||
|
lists:filter(fun(Calendar) ->
|
||||||
|
logic_calendar:can_access(UserId, Calendar)
|
||||||
|
end, Calendars).
|
||||||
|
|
||||||
|
%% ============ Применение фильтров ============
|
||||||
|
apply_event_filters(Events, Query, Params) ->
|
||||||
|
Events1 = filter_by_text(Events, Query),
|
||||||
|
Events2 = filter_by_tags(Events1, Params),
|
||||||
|
Events3 = filter_by_date_range(Events2, Params),
|
||||||
|
filter_by_location(Events3, Params).
|
||||||
|
|
||||||
|
apply_calendar_filters(Calendars, Query, Params) ->
|
||||||
|
Calendars1 = filter_by_text(Calendars, Query),
|
||||||
|
filter_by_tags(Calendars1, Params).
|
||||||
|
|
||||||
|
filter_by_text(Items, undefined) -> Items;
|
||||||
|
filter_by_text(Items, <<>>) -> Items;
|
||||||
|
filter_by_text(Items, Query) ->
|
||||||
|
QueryLower = string:lowercase(Query),
|
||||||
|
lists:filter(fun(Item) ->
|
||||||
|
Title = get_title(Item),
|
||||||
|
Description = get_description(Item),
|
||||||
|
string:find(string:lowercase(Title), QueryLower) =/= nomatch orelse
|
||||||
|
string:find(string:lowercase(Description), QueryLower) =/= nomatch
|
||||||
|
end, Items).
|
||||||
|
|
||||||
|
filter_by_tags(Items, Params) ->
|
||||||
|
case maps:get(tags, Params, undefined) of
|
||||||
|
undefined -> Items;
|
||||||
|
TagsStr ->
|
||||||
|
Tags = [string:trim(T) || T <- string:split(TagsStr, ",", all)],
|
||||||
|
lists:filter(fun(Item) ->
|
||||||
|
ItemTags = get_tags(Item),
|
||||||
|
has_any_tag(ItemTags, Tags)
|
||||||
|
end, Items)
|
||||||
|
end.
|
||||||
|
|
||||||
|
filter_by_date_range(Events, Params) ->
|
||||||
|
From = maps:get(from, Params, undefined),
|
||||||
|
To = maps:get(to, Params, undefined),
|
||||||
|
|
||||||
|
case {From, To} of
|
||||||
|
{undefined, undefined} -> Events;
|
||||||
|
_ ->
|
||||||
|
lists:filter(fun(Event) ->
|
||||||
|
StartTime = Event#event.start_time,
|
||||||
|
(From =:= undefined orelse StartTime >= From) andalso
|
||||||
|
(To =:= undefined orelse StartTime =< To)
|
||||||
|
end, Events)
|
||||||
|
end.
|
||||||
|
|
||||||
|
filter_by_location(Events, Params) ->
|
||||||
|
case {maps:get(lat, Params, undefined), maps:get(lon, Params, undefined)} of
|
||||||
|
{undefined, _} -> Events;
|
||||||
|
{_, undefined} -> Events;
|
||||||
|
{Lat, Lon} ->
|
||||||
|
Radius = maps:get(radius, Params, 10),
|
||||||
|
lists:filter(fun(Event) ->
|
||||||
|
case Event#event.location of
|
||||||
|
undefined -> false;
|
||||||
|
#location{lat = EventLat, lon = EventLon} ->
|
||||||
|
distance(Lat, Lon, EventLat, EventLon) =< Radius
|
||||||
|
end
|
||||||
|
end, Events)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% ============ Вспомогательные функции ============
|
||||||
|
get_title(#event{title = Title}) -> Title;
|
||||||
|
get_title(#calendar{title = Title}) -> Title.
|
||||||
|
|
||||||
|
get_description(#event{description = Desc}) -> Desc;
|
||||||
|
get_description(#calendar{description = Desc}) -> Desc.
|
||||||
|
|
||||||
|
get_tags(#event{tags = Tags}) -> Tags;
|
||||||
|
get_tags(#calendar{tags = Tags}) -> Tags.
|
||||||
|
|
||||||
|
has_any_tag(ItemTags, SearchTags) ->
|
||||||
|
lists:any(fun(Tag) -> lists:member(Tag, ItemTags) end, SearchTags).
|
||||||
|
|
||||||
|
%% ============ Гео-вычисления ============
|
||||||
|
distance(Lat1, Lon1, Lat2, Lon2) ->
|
||||||
|
DLat = deg_to_rad(Lat2 - Lat1),
|
||||||
|
DLon = deg_to_rad(Lon2 - Lon1),
|
||||||
|
|
||||||
|
A = math:sin(DLat / 2) * math:sin(DLat / 2) +
|
||||||
|
math:cos(deg_to_rad(Lat1)) * math:cos(deg_to_rad(Lat2)) *
|
||||||
|
math:sin(DLon / 2) * math:sin(DLon / 2),
|
||||||
|
|
||||||
|
C = 2 * math:atan2(math:sqrt(A), math:sqrt(1 - A)),
|
||||||
|
|
||||||
|
?EARTH_RADIUS_KM * C.
|
||||||
|
|
||||||
|
deg_to_rad(Deg) -> Deg * math:pi() / 180.
|
||||||
|
|
||||||
|
%% ============ Сортировка ============
|
||||||
|
sort_events(Events, Params) ->
|
||||||
|
SortBy = maps:get(sort, Params, <<"start_time">>),
|
||||||
|
Order = maps:get(order, Params, <<"asc">>),
|
||||||
|
|
||||||
|
Sorted = case SortBy of
|
||||||
|
<<"start_time">> ->
|
||||||
|
lists:sort(fun(A, B) -> A#event.start_time =< B#event.start_time end, Events);
|
||||||
|
<<"rating">> ->
|
||||||
|
lists:sort(fun(A, B) -> A#event.rating_avg >= B#event.rating_avg end, Events);
|
||||||
|
<<"created_at">> ->
|
||||||
|
lists:sort(fun(A, B) -> A#event.created_at =< B#event.created_at end, Events);
|
||||||
|
_ -> Events
|
||||||
|
end,
|
||||||
|
|
||||||
|
case Order of
|
||||||
|
<<"desc">> -> lists:reverse(Sorted);
|
||||||
|
_ -> Sorted
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% ============ Пагинация ============
|
||||||
|
paginate(List, Limit, Offset) ->
|
||||||
|
lists:sublist(List, Offset + 1, Limit).
|
||||||
|
|
||||||
|
%% ============ Форматирование ============
|
||||||
|
format_events(Events) ->
|
||||||
|
lists:map(fun format_event/1, Events).
|
||||||
|
|
||||||
|
format_event(Event) ->
|
||||||
|
Location = case Event#event.location of
|
||||||
|
undefined -> null;
|
||||||
|
#location{address = Addr, lat = Lat, lon = Lon} ->
|
||||||
|
#{address => Addr, lat => Lat, lon => Lon}
|
||||||
|
end,
|
||||||
|
|
||||||
|
#{
|
||||||
|
id => Event#event.id,
|
||||||
|
calendar_id => Event#event.calendar_id,
|
||||||
|
title => Event#event.title,
|
||||||
|
description => Event#event.description,
|
||||||
|
event_type => Event#event.event_type,
|
||||||
|
start_time => datetime_to_iso8601(Event#event.start_time),
|
||||||
|
duration => Event#event.duration,
|
||||||
|
location => Location,
|
||||||
|
tags => Event#event.tags,
|
||||||
|
capacity => Event#event.capacity,
|
||||||
|
rating_avg => Event#event.rating_avg,
|
||||||
|
rating_count => Event#event.rating_count,
|
||||||
|
status => Event#event.status
|
||||||
|
}.
|
||||||
|
|
||||||
|
format_calendars(Calendars) ->
|
||||||
|
lists:map(fun format_calendar/1, Calendars).
|
||||||
|
|
||||||
|
format_calendar(Calendar) ->
|
||||||
|
#{
|
||||||
|
id => Calendar#calendar.id,
|
||||||
|
owner_id => Calendar#calendar.owner_id,
|
||||||
|
title => Calendar#calendar.title,
|
||||||
|
description => Calendar#calendar.description,
|
||||||
|
type => Calendar#calendar.type,
|
||||||
|
tags => Calendar#calendar.tags,
|
||||||
|
rating_avg => Calendar#calendar.rating_avg,
|
||||||
|
rating_count => Calendar#calendar.rating_count,
|
||||||
|
status => Calendar#calendar.status
|
||||||
|
}.
|
||||||
|
|
||||||
|
datetime_to_iso8601({{Y, M, D}, {H, Min, S}}) ->
|
||||||
|
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||||
|
[Y, M, D, H, Min, S])).
|
||||||
23
test/handler_search_tests.erl
Normal file
23
test/handler_search_tests.erl
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
-module(handler_search_tests).
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
handler_search_test_() ->
|
||||||
|
case is_server_running() of
|
||||||
|
true ->
|
||||||
|
[
|
||||||
|
{"Search API requires authentication", fun test_search_requires_auth/0}
|
||||||
|
];
|
||||||
|
false ->
|
||||||
|
io:format("Skipping handler tests: server not running~n"),
|
||||||
|
[]
|
||||||
|
end.
|
||||||
|
|
||||||
|
is_server_running() ->
|
||||||
|
case httpc:request(get, {"http://localhost:8080/health", []}, [], [{timeout, 1000}]) of
|
||||||
|
{ok, {{_, 200, _}, _, _}} -> true;
|
||||||
|
_ -> false
|
||||||
|
end.
|
||||||
|
|
||||||
|
test_search_requires_auth() ->
|
||||||
|
{ok, {{_, 401, _}, _, Body}} = httpc:request(get, {"http://localhost:8080/v1/search?type=event", []}, [], []),
|
||||||
|
?assertMatch(#{<<"error">> := _}, jsx:decode(Body, [return_maps])).
|
||||||
249
test/logic_search_tests.erl
Normal file
249
test/logic_search_tests.erl
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
-module(logic_search_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()]}
|
||||||
|
]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
cleanup(_) ->
|
||||||
|
mnesia:delete_table(event),
|
||||||
|
mnesia:delete_table(calendar),
|
||||||
|
mnesia:delete_table(user),
|
||||||
|
mnesia:stop(),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
logic_search_test_() ->
|
||||||
|
{foreach,
|
||||||
|
fun setup/0,
|
||||||
|
fun cleanup/1,
|
||||||
|
[
|
||||||
|
{"Search events by text", fun test_search_events_by_text/0},
|
||||||
|
{"Search events by tags", fun test_search_events_by_tags/0},
|
||||||
|
{"Search events by date range", fun test_search_events_by_date/0},
|
||||||
|
{"Search events by location", fun test_search_events_by_location/0},
|
||||||
|
{"Combined search", fun test_combined_search/0},
|
||||||
|
{"Search calendars", fun test_search_calendars/0},
|
||||||
|
{"Search all", fun test_search_all/0},
|
||||||
|
{"Pagination", fun test_pagination/0},
|
||||||
|
{"Sorting", fun test_sorting/0},
|
||||||
|
{"Access control in search", fun test_access_control/0},
|
||||||
|
{"Empty search results", fun test_empty_search/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, Type, Tags) ->
|
||||||
|
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test Calendar">>, <<"Description">>, manual),
|
||||||
|
core_calendar:update(Calendar#calendar.id, [{type, Type}, {tags, Tags}]),
|
||||||
|
{ok, Updated} = core_calendar:get_by_id(Calendar#calendar.id),
|
||||||
|
Updated#calendar.id.
|
||||||
|
|
||||||
|
create_test_event(CalendarId, Title, Description, StartTime, Tags, Location) ->
|
||||||
|
{ok, Event} = core_event:create(CalendarId, Title, StartTime, 60),
|
||||||
|
Updates = [{description, Description}, {tags, Tags}],
|
||||||
|
Updates2 = case Location of
|
||||||
|
undefined -> Updates;
|
||||||
|
_ -> [{location, Location} | Updates]
|
||||||
|
end,
|
||||||
|
core_event:update(Event#event.id, Updates2),
|
||||||
|
{ok, Updated} = core_event:get_by_id(Event#event.id),
|
||||||
|
Updated#event.id.
|
||||||
|
|
||||||
|
%% Тесты
|
||||||
|
test_search_events_by_text() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, personal, []),
|
||||||
|
|
||||||
|
StartTime = {{2026, 6, 1}, {10, 0, 0}},
|
||||||
|
create_test_event(CalendarId, <<"Python Workshop">>, <<"Learn Python">>, StartTime, [], undefined),
|
||||||
|
create_test_event(CalendarId, <<"JavaScript Conference">>, <<"JS for everyone">>, StartTime, [], undefined),
|
||||||
|
create_test_event(CalendarId, <<"Yoga Class">>, <<"Morning yoga">>, StartTime, [], undefined),
|
||||||
|
|
||||||
|
Params = #{},
|
||||||
|
{ok, Total, Results} = logic_search:search(<<"event">>, <<"Python">>, OwnerId, Params),
|
||||||
|
?assertEqual(1, Total),
|
||||||
|
?assertEqual(1, length(Results)),
|
||||||
|
|
||||||
|
{ok, Total2, _Results2} = logic_search:search(<<"event">>, <<"conference">>, OwnerId, Params),
|
||||||
|
?assertEqual(1, Total2).
|
||||||
|
|
||||||
|
test_search_events_by_tags() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, personal, []),
|
||||||
|
|
||||||
|
StartTime = {{2026, 6, 1}, {10, 0, 0}},
|
||||||
|
create_test_event(CalendarId, <<"Event 1">>, <<"">>, StartTime, [<<"python">>, <<"workshop">>], undefined),
|
||||||
|
create_test_event(CalendarId, <<"Event 2">>, <<"">>, StartTime, [<<"javascript">>, <<"conference">>], undefined),
|
||||||
|
create_test_event(CalendarId, <<"Event 3">>, <<"">>, StartTime, [<<"python">>, <<"advanced">>], undefined),
|
||||||
|
|
||||||
|
% Поиск по одному тегу
|
||||||
|
Params = #{tags => <<"python">>},
|
||||||
|
{ok, Total, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
|
||||||
|
?assertEqual(2, Total), % Event 1 и Event 3
|
||||||
|
|
||||||
|
% Поиск по тегу, который есть только у одного события
|
||||||
|
Params2 = #{tags => <<"workshop">>},
|
||||||
|
{ok, Total2, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params2),
|
||||||
|
?assertEqual(1, Total2), % Только Event 1
|
||||||
|
|
||||||
|
% Поиск по тегу, которого нет
|
||||||
|
Params3 = #{tags => <<"nonexistent">>},
|
||||||
|
{ok, Total3, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params3),
|
||||||
|
?assertEqual(0, Total3).
|
||||||
|
|
||||||
|
test_search_events_by_date() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, personal, []),
|
||||||
|
|
||||||
|
create_test_event(CalendarId, <<"June Event">>, <<"">>, {{2026, 6, 15}, {10, 0, 0}}, [], undefined),
|
||||||
|
create_test_event(CalendarId, <<"July Event">>, <<"">>, {{2026, 7, 15}, {10, 0, 0}}, [], undefined),
|
||||||
|
create_test_event(CalendarId, <<"August Event">>, <<"">>, {{2026, 8, 15}, {10, 0, 0}}, [], undefined),
|
||||||
|
|
||||||
|
Params = #{from => {{2026, 6, 1}, {0, 0, 0}}, to => {{2026, 6, 30}, {23, 59, 59}}},
|
||||||
|
{ok, Total, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
|
||||||
|
?assertEqual(1, Total).
|
||||||
|
|
||||||
|
test_search_events_by_location() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, personal, []),
|
||||||
|
|
||||||
|
StartTime = {{2026, 6, 1}, {10, 0, 0}},
|
||||||
|
MoscowLoc = #location{address = <<"Moscow">>, lat = 55.7558, lon = 37.6173},
|
||||||
|
SpbLoc = #location{address = <<"SPb">>, lat = 59.9343, lon = 30.3351},
|
||||||
|
|
||||||
|
create_test_event(CalendarId, <<"Moscow Event">>, <<"">>, StartTime, [], MoscowLoc),
|
||||||
|
create_test_event(CalendarId, <<"SPb Event">>, <<"">>, StartTime, [], SpbLoc),
|
||||||
|
|
||||||
|
Params = #{lat => 55.7558, lon => 37.6173, radius => 10},
|
||||||
|
{ok, Total, _Results} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
|
||||||
|
?assertEqual(1, Total),
|
||||||
|
|
||||||
|
Params2 = #{lat => 59.9343, lon => 30.3351, radius => 10},
|
||||||
|
{ok, Total2, _} = logic_search:search(<<"event">>, undefined, OwnerId, Params2),
|
||||||
|
?assertEqual(1, Total2).
|
||||||
|
|
||||||
|
test_combined_search() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, personal, []),
|
||||||
|
|
||||||
|
StartTime = {{2026, 6, 15}, {10, 0, 0}},
|
||||||
|
create_test_event(CalendarId, <<"Python Workshop">>, <<"Learn Python">>, StartTime, [<<"python">>, <<"workshop">>], undefined),
|
||||||
|
create_test_event(CalendarId, <<"Python Advanced">>, <<"Advanced Python">>, {{2026, 7, 15}, {10, 0, 0}}, [<<"python">>, <<"advanced">>], undefined),
|
||||||
|
|
||||||
|
Params = #{from => {{2026, 6, 1}, {0, 0, 0}}, to => {{2026, 6, 30}, {23, 59, 59}}, tags => <<"python">>},
|
||||||
|
{ok, Total, _} = logic_search:search(<<"event">>, <<"Workshop">>, OwnerId, Params),
|
||||||
|
?assertEqual(1, Total).
|
||||||
|
|
||||||
|
test_search_calendars() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
create_test_calendar(OwnerId, personal, [<<"tech">>, <<"programming">>]),
|
||||||
|
create_test_calendar(OwnerId, commercial, [<<"yoga">>, <<"wellness">>]),
|
||||||
|
|
||||||
|
Params = #{tags => <<"tech">>},
|
||||||
|
{ok, Total, _} = logic_search:search(<<"calendar">>, undefined, OwnerId, Params),
|
||||||
|
?assertEqual(1, Total),
|
||||||
|
|
||||||
|
{ok, Total2, _} = logic_search:search(<<"calendar">>, <<"Calendar">>, OwnerId, #{}),
|
||||||
|
?assertEqual(2, Total2).
|
||||||
|
|
||||||
|
test_search_all() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, personal, []),
|
||||||
|
StartTime = {{2026, 6, 1}, {10, 0, 0}},
|
||||||
|
create_test_event(CalendarId, <<"Test Event">>, <<"">>, StartTime, [], undefined),
|
||||||
|
|
||||||
|
{ok, Total, Results} = logic_search:search(undefined, <<"Test">>, OwnerId, #{}),
|
||||||
|
?assert(Total >= 2),
|
||||||
|
?assert(maps:is_key(events, Results)),
|
||||||
|
?assert(maps:is_key(calendars, Results)).
|
||||||
|
|
||||||
|
test_pagination() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, personal, []),
|
||||||
|
StartTime = {{2026, 6, 1}, {10, 0, 0}},
|
||||||
|
|
||||||
|
lists:foreach(fun(I) ->
|
||||||
|
Title = iolist_to_binary(["Event ", integer_to_binary(I)]),
|
||||||
|
create_test_event(CalendarId, Title, <<"">>, StartTime, [], undefined)
|
||||||
|
end, [1, 2, 3, 4, 5]),
|
||||||
|
|
||||||
|
Params = #{limit => 2, offset => 0},
|
||||||
|
{ok, Total, Results} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
|
||||||
|
?assertEqual(5, Total),
|
||||||
|
?assertEqual(2, length(Results)),
|
||||||
|
|
||||||
|
Params2 = #{limit => 2, offset => 2},
|
||||||
|
{ok, _, Results2} = logic_search:search(<<"event">>, undefined, OwnerId, Params2),
|
||||||
|
?assertEqual(2, length(Results2)),
|
||||||
|
|
||||||
|
Params3 = #{limit => 2, offset => 4},
|
||||||
|
{ok, _, Results3} = logic_search:search(<<"event">>, undefined, OwnerId, Params3),
|
||||||
|
?assertEqual(1, length(Results3)).
|
||||||
|
|
||||||
|
test_sorting() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, personal, []),
|
||||||
|
|
||||||
|
create_test_event(CalendarId, <<"Early">>, <<"">>, {{2026, 6, 1}, {10, 0, 0}}, [], undefined),
|
||||||
|
create_test_event(CalendarId, <<"Late">>, <<"">>, {{2026, 6, 30}, {10, 0, 0}}, [], undefined),
|
||||||
|
|
||||||
|
Params = #{sort => <<"start_time">>, order => <<"asc">>},
|
||||||
|
{ok, _, [First | _]} = logic_search:search(<<"event">>, undefined, OwnerId, Params),
|
||||||
|
?assertMatch(#{title := <<"Early">>}, First),
|
||||||
|
|
||||||
|
Params2 = #{sort => <<"start_time">>, order => <<"desc">>},
|
||||||
|
{ok, _, [First2 | _]} = logic_search:search(<<"event">>, undefined, OwnerId, Params2),
|
||||||
|
?assertMatch(#{title := <<"Late">>}, First2).
|
||||||
|
|
||||||
|
test_access_control() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
OtherId = create_test_user(user),
|
||||||
|
|
||||||
|
PersonalId = create_test_calendar(OwnerId, personal, []),
|
||||||
|
CommercialId = create_test_calendar(OwnerId, commercial, []),
|
||||||
|
|
||||||
|
StartTime = {{2026, 6, 1}, {10, 0, 0}},
|
||||||
|
create_test_event(PersonalId, <<"Personal Event">>, <<"">>, StartTime, [], undefined),
|
||||||
|
create_test_event(CommercialId, <<"Public Event">>, <<"">>, StartTime, [], undefined),
|
||||||
|
|
||||||
|
% Другой пользователь видит только коммерческие события
|
||||||
|
{ok, Total, Results} = logic_search:search(<<"event">>, undefined, OtherId, #{}),
|
||||||
|
?assertEqual(1, Total),
|
||||||
|
[Event] = Results,
|
||||||
|
?assertMatch(#{title := <<"Public Event">>}, Event).
|
||||||
|
|
||||||
|
test_empty_search() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, personal, []),
|
||||||
|
StartTime = {{2026, 6, 1}, {10, 0, 0}},
|
||||||
|
create_test_event(CalendarId, <<"Test">>, <<"">>, StartTime, [], undefined),
|
||||||
|
|
||||||
|
{ok, Total, Results} = logic_search:search(<<"event">>, <<"nonexistent">>, OwnerId, #{}),
|
||||||
|
?assertEqual(0, Total),
|
||||||
|
?assertEqual([], Results).
|
||||||
50
test/scripts/run_tests.sh
Normal file
50
test/scripts/run_tests.sh
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo " FULL TEST CYCLE"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
# Остановка старых процессов
|
||||||
|
echo "[1/4] Stopping old servers..."
|
||||||
|
pkill -f "beam.*eventhub" 2>/dev/null || true
|
||||||
|
rm -rf Mnesia.*
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Запуск сервера в фоне
|
||||||
|
echo "[2/4] Starting server..."
|
||||||
|
./test/scripts/start_server_bg.sh
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to start server"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Запуск тестов
|
||||||
|
echo "[3/4] Running tests..."
|
||||||
|
chmod +x test/scripts/*.sh
|
||||||
|
cd test/scripts
|
||||||
|
|
||||||
|
if [ -n "$1" ]; then
|
||||||
|
./test_runner.sh -s "$1"
|
||||||
|
else
|
||||||
|
./test_runner.sh -s
|
||||||
|
fi
|
||||||
|
|
||||||
|
TEST_RESULT=$?
|
||||||
|
|
||||||
|
# Остановка сервера
|
||||||
|
echo ""
|
||||||
|
echo "[4/4] Stopping server..."
|
||||||
|
pkill -f "beam.*eventhub" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
if [ $TEST_RESULT -eq 0 ]; then
|
||||||
|
echo "🎉 ALL TESTS PASSED!"
|
||||||
|
else
|
||||||
|
echo "❌ TESTS FAILED"
|
||||||
|
fi
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
exit $TEST_RESULT
|
||||||
39
test/scripts/start_server_bg.sh
Normal file
39
test/scripts/start_server_bg.sh
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
echo "PROJECT_ROOT: $PROJECT_ROOT"
|
||||||
|
# Очистка
|
||||||
|
echo "Stopping old processes..."
|
||||||
|
pkill beam 2>/dev/null || true
|
||||||
|
rm -rf Mnesia.*
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Компиляция
|
||||||
|
echo "Compiling..."
|
||||||
|
rebar3 compile > /dev/null 2>&1
|
||||||
|
|
||||||
|
# Запуск в фоне через erl
|
||||||
|
echo "Starting server in background..."
|
||||||
|
erl -sname eventhub_test \
|
||||||
|
-pa _build/default/lib/*/ebin \
|
||||||
|
-eval "application:ensure_all_started(eventhub)" \
|
||||||
|
-noshell \
|
||||||
|
-detached
|
||||||
|
|
||||||
|
# Ждём запуска
|
||||||
|
echo "Waiting for server..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
if curl -s http://localhost:8080/health 2>/dev/null | grep -q "ok"; then
|
||||||
|
echo "✓ Server ready at http://localhost:8080"
|
||||||
|
pgrep -f "beam.*eventhub_test"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo -n "."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✗ Server failed to start"
|
||||||
|
pkill beam 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
@@ -1,50 +1,298 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
echo "============================================================"
|
RED='\033[0;31m'
|
||||||
echo " EVENTHUB FULL API TEST SUITE"
|
GREEN='\033[0;32m'
|
||||||
echo "============================================================"
|
YELLOW='\033[1;33m'
|
||||||
echo ""
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPTS_DIR/../.." && pwd)"
|
||||||
# Проверяем, что сервер запущен
|
BASE_URL="http://localhost:8080"
|
||||||
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
|
PASSED=0
|
||||||
FAILED=0
|
FAILED=0
|
||||||
|
SKIPPED=0
|
||||||
|
SERVER_STARTED=false
|
||||||
|
SERVER_PID=""
|
||||||
|
|
||||||
run_test() {
|
# ============================================================================
|
||||||
|
# Вспомогательные функции
|
||||||
|
# ============================================================================
|
||||||
|
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"; }
|
||||||
|
log_step() { echo -e "${CYAN}[STEP]${NC} $1"; }
|
||||||
|
|
||||||
|
# Очистка при выходе
|
||||||
|
cleanup() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "▶ Running $1..."
|
log_info "Cleaning up..."
|
||||||
if bash "$SCRIPTS_DIR/$1"; then
|
|
||||||
((PASSED++))
|
if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
echo "✅ $1 PASSED"
|
log_info "Stopping server (PID: $SERVER_PID)..."
|
||||||
else
|
kill "$SERVER_PID" 2>/dev/null
|
||||||
((FAILED++))
|
wait "$SERVER_PID" 2>/dev/null
|
||||||
echo "❌ $1 FAILED"
|
|
||||||
|
# Убеждаемся, что все beam процессы остановлены
|
||||||
|
pkill -f "beam.*eventhub" 2>/dev/null || true
|
||||||
|
log_success "Server stopped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Удаляем временные файлы
|
||||||
|
rm -f /tmp/eventhub_test_*.log 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Обработчик сигналов
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# Проверка порта
|
||||||
|
check_port() {
|
||||||
|
local port=$1
|
||||||
|
if lsof -i ":$port" > /dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":$port "; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ожидание запуска сервера
|
||||||
|
wait_for_server() {
|
||||||
|
local max_attempts=30
|
||||||
|
local attempt=0
|
||||||
|
|
||||||
|
log_info "Waiting for server to start..."
|
||||||
|
|
||||||
|
while [ $attempt -lt $max_attempts ]; do
|
||||||
|
if curl -s "$BASE_URL/health" | grep -q "ok"; then
|
||||||
|
log_success "Server is ready (took $attempt seconds)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
((attempt++))
|
||||||
|
echo -n "."
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
log_error "Server failed to start within $max_attempts seconds"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Запуск сервера
|
||||||
|
start_server() {
|
||||||
|
echo -e "${CYAN}[STEP]${NC} Starting EventHub server..."
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
if [ ! -f "rebar.config" ]; then
|
||||||
|
echo -e "${RED}[ERROR]${NC} rebar.config not found in $(pwd)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}[INFO]${NC} Project root: $(pwd)"
|
||||||
|
|
||||||
|
# Очищаем старые данные
|
||||||
|
rm -rf Mnesia.* 2>/dev/null
|
||||||
|
pkill -f "beam.*eventhub_test" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Компилируем
|
||||||
|
echo -e "${BLUE}[INFO]${NC} Compiling..."
|
||||||
|
rebar3 compile > /dev/null 2>&1
|
||||||
|
|
||||||
|
# Запускаем сервер через erl (более надёжно для фона)
|
||||||
|
LOG_FILE="/tmp/eventhub_test_server.log"
|
||||||
|
echo -e "${BLUE}[INFO]${NC} Starting server..."
|
||||||
|
|
||||||
|
# Запускаем в фоне с перенаправлением вывода
|
||||||
|
rebar3 shell --sname eventhub_test </dev/null > "$LOG_FILE" 2>&1 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Даём процессу время запуститься
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Проверяем, жив ли процесс
|
||||||
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
|
echo -e "${RED}[ERROR]${NC} Server process died immediately"
|
||||||
|
echo -e "${YELLOW}[INFO]${NC} Check log: $LOG_FILE"
|
||||||
|
cat "$LOG_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}[INFO]${NC} Server PID: $SERVER_PID"
|
||||||
|
|
||||||
|
# Ждём готовности
|
||||||
|
for i in {1..30}; do
|
||||||
|
echo -n "."
|
||||||
|
if curl -s "http://localhost:8080/health" 2>/dev/null | grep -q "ok"; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} Server ready at http://localhost:8080"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}[ERROR]${NC} Server died during startup"
|
||||||
|
echo -e "${YELLOW}[INFO]${NC} Last lines of log:"
|
||||||
|
tail -30 "$LOG_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}[ERROR]${NC} Server failed to respond"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Остановка сервера
|
||||||
|
stop_server() {
|
||||||
|
if [ "$SERVER_STARTED" = true ] && [ -n "$SERVER_PID" ]; then
|
||||||
|
log_step "Stopping EventHub server..."
|
||||||
|
|
||||||
|
# Останавливаем нашу ноду
|
||||||
|
if kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
|
kill "$SERVER_PID" 2>/dev/null
|
||||||
|
wait "$SERVER_PID" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Останавливаем все связанные beam процессы
|
||||||
|
pkill -f "beam.*eventhub_test" 2>/dev/null || true
|
||||||
|
|
||||||
|
SERVER_STARTED=false
|
||||||
|
log_success "Server stopped"
|
||||||
|
sleep 2
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
run_test "test_auth_api.sh"
|
# Проверка, запущен ли сервер
|
||||||
run_test "test_calendar_api.sh"
|
is_server_running() {
|
||||||
run_test "test_event_api.sh"
|
curl -s "$BASE_URL/health" | grep -q "ok"
|
||||||
run_test "test_booking_api.sh"
|
}
|
||||||
|
|
||||||
echo ""
|
# Запуск одного тестового скрипта
|
||||||
echo "============================================================"
|
run_test_script() {
|
||||||
echo " TEST SUMMARY"
|
local script_path=$1
|
||||||
echo "============================================================"
|
local script_name=$(basename "$script_path")
|
||||||
echo "Passed: $PASSED"
|
|
||||||
echo "Failed: $FAILED"
|
|
||||||
echo "============================================================"
|
|
||||||
|
|
||||||
if [ $FAILED -eq 0 ]; then
|
echo ""
|
||||||
echo "🎉 ALL TESTS PASSED!"
|
echo "============================================================"
|
||||||
exit 0
|
echo -e "${CYAN}[RUNNING]${NC} $script_name"
|
||||||
else
|
echo "============================================================"
|
||||||
echo "❌ SOME TESTS FAILED"
|
|
||||||
|
# Даём скрипту права на выполнение
|
||||||
|
chmod +x "$script_path" 2>/dev/null
|
||||||
|
|
||||||
|
# Запускаем тест
|
||||||
|
if bash "$script_path"; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}[PASSED]${NC} $script_name"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}[FAILED]${NC} $script_name"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Главная логика
|
||||||
|
# ============================================================================
|
||||||
|
main() {
|
||||||
|
echo "============================================================"
|
||||||
|
echo " EVENTHUB FULL API TEST SUITE"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Проверяем, не запущен ли уже сервер
|
||||||
|
if is_server_running; then
|
||||||
|
log_warning "Server is already running on port 8080"
|
||||||
|
read -p "Use existing server? [Y/n]: " USE_EXISTING
|
||||||
|
if [[ "$USE_EXISTING" =~ ^[Nn] ]]; then
|
||||||
|
log_error "Please stop the existing server first: make stop"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
log_info "Using existing server"
|
||||||
|
else
|
||||||
|
# Проверяем, свободен ли порт
|
||||||
|
if check_port 8080; then
|
||||||
|
log_error "Port 8080 is in use by another process"
|
||||||
|
log_info "Please free the port or stop the other process"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Запускаем сервер
|
||||||
|
if ! start_server; then
|
||||||
|
log_error "Failed to start server"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Server is ready at $BASE_URL"
|
||||||
|
|
||||||
|
# Получаем список всех тестовых скриптов
|
||||||
|
TEST_SCRIPTS=$(find "$SCRIPTS_DIR" -maxdepth 1 -name "test_*.sh" ! -name "test_all.sh" ! -name "test_runner.sh" -type f | sort)
|
||||||
|
|
||||||
|
if [ -z "$TEST_SCRIPTS" ]; then
|
||||||
|
log_warning "No test scripts found in $SCRIPTS_DIR"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Found test scripts:"
|
||||||
|
for script in $TEST_SCRIPTS; do
|
||||||
|
echo " - $(basename "$script")"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Счётчики времени
|
||||||
|
START_TIME=$(date +%s)
|
||||||
|
|
||||||
|
# Запускаем все тесты
|
||||||
|
for script in $TEST_SCRIPTS; do
|
||||||
|
if run_test_script "$script"; then
|
||||||
|
((PASSED++))
|
||||||
|
else
|
||||||
|
((FAILED++))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
END_TIME=$(date +%s)
|
||||||
|
DURATION=$((END_TIME - START_TIME))
|
||||||
|
|
||||||
|
# Останавливаем сервер, если мы его запускали
|
||||||
|
if [ "$SERVER_STARTED" = true ]; then
|
||||||
|
echo ""
|
||||||
|
stop_server
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Итоговый отчёт
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo " TEST SUMMARY"
|
||||||
|
echo "============================================================"
|
||||||
|
echo -e "Total scripts: $((PASSED + FAILED))"
|
||||||
|
echo -e "${GREEN}Passed: $PASSED${NC}"
|
||||||
|
echo -e "${RED}Failed: $FAILED${NC}"
|
||||||
|
if [ $SKIPPED -gt 0 ]; then
|
||||||
|
echo -e "${YELLOW}Skipped: $SKIPPED${NC}"
|
||||||
|
fi
|
||||||
|
echo -e "Duration: ${DURATION}s"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
if [ $FAILED -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}🎉 ALL TESTS PASSED!${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}❌ SOME TESTS FAILED${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Запуск
|
||||||
|
main "$@"
|
||||||
|
exit $?
|
||||||
228
test/scripts/test_runner.sh
Normal file
228
test/scripts/test_runner.sh
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPTS_DIR/../.." && pwd)"
|
||||||
|
BASE_URL="http://localhost:8080"
|
||||||
|
|
||||||
|
SERVER_STARTED=false
|
||||||
|
SERVER_PID=""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Функции
|
||||||
|
# ============================================================================
|
||||||
|
cleanup() {
|
||||||
|
if [ "$SERVER_STARTED" = true ] && [ -n "$SERVER_PID" ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}[INFO]${NC} Stopping server..."
|
||||||
|
kill "$SERVER_PID" 2>/dev/null
|
||||||
|
wait "$SERVER_PID" 2>/dev/null
|
||||||
|
pkill -f "beam.*eventhub_test" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 [options] [pattern]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " -h, --help Show this help"
|
||||||
|
echo " -l, --list List available test scripts"
|
||||||
|
echo " -v, --verbose Verbose output"
|
||||||
|
echo " -s, --server Use existing server (don't start/stop)"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 Run all tests"
|
||||||
|
echo " $0 auth Run tests matching 'auth'"
|
||||||
|
echo " $0 booking Run tests matching 'booking'"
|
||||||
|
echo " $0 -l List all test scripts"
|
||||||
|
echo " $0 -s Use already running server"
|
||||||
|
}
|
||||||
|
|
||||||
|
list_scripts() {
|
||||||
|
echo "Available test scripts:"
|
||||||
|
find "$SCRIPTS_DIR" -maxdepth 1 -name "test_*.sh" ! -name "test_all.sh" ! -name "test_runner.sh" -type f | sort | while read script; do
|
||||||
|
name=$(basename "$script")
|
||||||
|
echo " - $name"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
start_server() {
|
||||||
|
echo -e "${CYAN}[STEP]${NC} Starting EventHub server..."
|
||||||
|
|
||||||
|
# Переходим в корень проекта
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
# Проверяем, что мы в правильной директории
|
||||||
|
if [ ! -f "rebar.config" ]; then
|
||||||
|
echo -e "${RED}[ERROR]${NC} rebar.config not found in $(pwd)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}[INFO]${NC} Project root: $(pwd)"
|
||||||
|
|
||||||
|
# Компилируем если нужно
|
||||||
|
if [ ! -d "_build" ]; then
|
||||||
|
echo -e "${BLUE}[INFO]${NC} Compiling project..."
|
||||||
|
rebar3 compile
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Запускаем сервер
|
||||||
|
LOG_FILE="/tmp/eventhub_test_server.log"
|
||||||
|
echo -e "${BLUE}[INFO]${NC} Starting server, log: $LOG_FILE"
|
||||||
|
|
||||||
|
rebar3 shell --sname eventhub_test > "$LOG_FILE" 2>&1 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
echo -e "${BLUE}[INFO]${NC} Server PID: $SERVER_PID"
|
||||||
|
|
||||||
|
# Ждём готовности
|
||||||
|
for i in {1..30}; do
|
||||||
|
echo -n "."
|
||||||
|
if curl -s "http://localhost:8080/health" 2>/dev/null | grep -q "ok"; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} Server ready (took $i seconds)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем, не умер ли процесс
|
||||||
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}[ERROR]${NC} Server process died"
|
||||||
|
echo -e "${YELLOW}[INFO]${NC} Last 20 lines of log:"
|
||||||
|
tail -20 "$LOG_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}[ERROR]${NC} Server failed to start within 30 seconds"
|
||||||
|
echo -e "${YELLOW}[INFO]${NC} Last 20 lines of log:"
|
||||||
|
tail -20 "$LOG_FILE"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Парсинг аргументов
|
||||||
|
# ============================================================================
|
||||||
|
VERBOSE=false
|
||||||
|
USE_EXISTING=false
|
||||||
|
PATTERN=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-l|--list)
|
||||||
|
list_scripts
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-v|--verbose)
|
||||||
|
VERBOSE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-s|--server)
|
||||||
|
USE_EXISTING=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
PATTERN="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Главная логика
|
||||||
|
# ============================================================================
|
||||||
|
echo "============================================================"
|
||||||
|
echo " EVENTHUB TEST RUNNER"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$USE_EXISTING" = false ]; then
|
||||||
|
if ! start_server; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
SERVER_STARTED=true
|
||||||
|
else
|
||||||
|
if ! curl -s "$BASE_URL/health" | grep -q "ok"; then
|
||||||
|
echo -e "${RED}[ERROR]${NC} Server is not running"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} Using existing server"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Находим тесты
|
||||||
|
if [ -n "$PATTERN" ]; then
|
||||||
|
TEST_SCRIPTS=$(find "$SCRIPTS_DIR" -maxdepth 1 -name "test_*${PATTERN}*.sh" ! -name "test_all.sh" ! -name "test_runner.sh" -type f | sort)
|
||||||
|
else
|
||||||
|
TEST_SCRIPTS=$(find "$SCRIPTS_DIR" -maxdepth 1 -name "test_*.sh" ! -name "test_all.sh" ! -name "test_runner.sh" -type f | sort)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$TEST_SCRIPTS" ]; then
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} No test scripts found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}[INFO]${NC} Running tests:"
|
||||||
|
for script in $TEST_SCRIPTS; do
|
||||||
|
echo " - $(basename "$script")"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
PASSED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
for script in $TEST_SCRIPTS; do
|
||||||
|
script_name=$(basename "$script")
|
||||||
|
echo "============================================================"
|
||||||
|
echo -e "${CYAN}[RUNNING]${NC} $script_name"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
if $VERBOSE; then
|
||||||
|
bash "$script"
|
||||||
|
EXIT_CODE=$?
|
||||||
|
else
|
||||||
|
bash "$script"
|
||||||
|
EXIT_CODE=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $EXIT_CODE -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}[PASSED]${NC} $script_name"
|
||||||
|
((PASSED++))
|
||||||
|
else
|
||||||
|
echo -e "${RED}[FAILED]${NC} $script_name"
|
||||||
|
((FAILED++))
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo " TEST SUMMARY"
|
||||||
|
echo "============================================================"
|
||||||
|
echo -e "Scripts run: $((PASSED + FAILED))"
|
||||||
|
echo -e "${GREEN}Passed: $PASSED${NC}"
|
||||||
|
echo -e "${RED}Failed: $FAILED${NC}"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
if [ $FAILED -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}🎉 ALL TESTS PASSED!${NC}"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ SOME TESTS FAILED${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
393
test/scripts/test_search_api.sh
Normal file
393
test/scripts/test_search_api.sh
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
#!/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
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
url_encode() {
|
||||||
|
echo -n "$1" | sed 's/ /%20/g;s/,/%2C/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo " EVENTHUB SEARCH 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="search_owner_$(date +%s)@example.com"
|
||||||
|
OWNER_PASSWORD="owner123"
|
||||||
|
|
||||||
|
log_info "Creating calendar owner..."
|
||||||
|
response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OWNER_EMAIL\",\"password\":\"$OWNER_PASSWORD\"}" "")
|
||||||
|
OWNER_TOKEN=$(extract_json "$response" "token")
|
||||||
|
OWNER_ID=$(extract_json "$response" "id")
|
||||||
|
|
||||||
|
if [ -z "$OWNER_TOKEN" ]; then
|
||||||
|
log_error "Failed to create owner"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_success "Owner created: $OWNER_EMAIL"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "STEP 2: Create calendar with tags"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Creating calendar..."
|
||||||
|
response=$(http_post "$BASE_URL/v1/calendars" \
|
||||||
|
"{\"title\":\"Tech Events Calendar\",\"description\":\"Calendar for technology events and workshops\",\"tags\":[\"tech\",\"programming\",\"workshop\"]}" "$OWNER_TOKEN")
|
||||||
|
CALENDAR_ID=$(extract_json "$response" "id")
|
||||||
|
log_success "Calendar created with tags: $CALENDAR_ID"
|
||||||
|
|
||||||
|
# Добавляем теги через обновление
|
||||||
|
http_put "$BASE_URL/v1/calendars/$CALENDAR_ID" "{\"tags\":[\"tech\",\"programming\",\"workshop\"]}" "$OWNER_TOKEN" > /dev/null
|
||||||
|
log_success "Calendar created with tags: $CALENDAR_ID"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "STEP 3: Create events with different properties"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
# Функция для создания события
|
||||||
|
create_event() {
|
||||||
|
local title=$1
|
||||||
|
local description=$2
|
||||||
|
local start_time=$3
|
||||||
|
local tags=$4
|
||||||
|
local lat=$5
|
||||||
|
local lon=$6
|
||||||
|
local address=$7
|
||||||
|
|
||||||
|
local location_json="null"
|
||||||
|
if [ -n "$lat" ] && [ -n "$lon" ]; then
|
||||||
|
location_json="{\"address\":\"$address\",\"lat\":$lat,\"lon\":$lon}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tags_json="[]"
|
||||||
|
if [ -n "$tags" ]; then
|
||||||
|
tags_json="$tags"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local data="{\"title\":\"$title\",\"description\":\"$description\",\"start_time\":\"$start_time\",\"duration\":60,\"tags\":$tags_json"
|
||||||
|
if [ "$location_json" != "null" ]; then
|
||||||
|
data="$data,\"location\":$location_json"
|
||||||
|
fi
|
||||||
|
data="$data}"
|
||||||
|
|
||||||
|
response=$(http_post "$BASE_URL/v1/calendars/$CALENDAR_ID/events" "$data" "$OWNER_TOKEN")
|
||||||
|
local event_id=$(extract_json "$response" "id")
|
||||||
|
|
||||||
|
echo "$event_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info "Creating Python Workshop event..."
|
||||||
|
WORKSHOP_ID=$(create_event "Python Workshop" "Learn Python programming basics" "2026-06-01T10:00:00Z" \
|
||||||
|
"[\"python\",\"workshop\",\"programming\"]" "55.7558" "37.6173" "Moscow, Russia")
|
||||||
|
log_success "Created: $WORKSHOP_ID"
|
||||||
|
|
||||||
|
log_info "Creating JavaScript Conference event..."
|
||||||
|
JS_ID=$(create_event "JavaScript Conference" "Annual JS conference for developers" "2026-06-15T09:00:00Z" \
|
||||||
|
"[\"javascript\",\"conference\",\"web\"]" "55.7558" "37.6173" "Moscow, Russia")
|
||||||
|
log_success "Created: $JS_ID"
|
||||||
|
|
||||||
|
log_info "Creating Yoga Class event (no tags)..."
|
||||||
|
YOGA_ID=$(create_event "Yoga Class" "Morning yoga session" "2026-06-10T08:00:00Z" \
|
||||||
|
"" "" "" "")
|
||||||
|
log_success "Created: $YOGA_ID"
|
||||||
|
|
||||||
|
log_info "Creating Tech Meetup in another city..."
|
||||||
|
MEETUP_ID=$(create_event "Tech Meetup" "Networking for tech professionals" "2026-06-20T18:00:00Z" \
|
||||||
|
"[\"networking\",\"tech\"]" "59.9343" "30.3351" "Saint Petersburg, Russia")
|
||||||
|
log_success "Created: $MEETUP_ID"
|
||||||
|
|
||||||
|
log_info "Creating past event..."
|
||||||
|
PAST_ID=$(create_event "Past Event" "This event already happened" "2020-01-01T10:00:00Z" \
|
||||||
|
"[\"past\"]" "" "" "")
|
||||||
|
log_success "Created: $PAST_ID"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "TEST 1: Search by text query"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Searching for 'Python'..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&q=Python" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q "Python Workshop"; then
|
||||||
|
log_success "Found Python Workshop"
|
||||||
|
else
|
||||||
|
log_error "Python Workshop not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Searching for 'conference'..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&q=conference" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q "JavaScript Conference"; then
|
||||||
|
log_success "Found JavaScript Conference"
|
||||||
|
else
|
||||||
|
log_error "JavaScript Conference not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "TEST 2: Search by tags"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Searching for events with tag 'python'..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&tags=python" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q "Python Workshop"; then
|
||||||
|
log_success "Found Python Workshop by tag"
|
||||||
|
else
|
||||||
|
log_error "Python Workshop not found by tag"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Searching for multiple tags 'tech,workshop'..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&tags=tech,workshop" "$OWNER_TOKEN")
|
||||||
|
log_success "Multiple tag search completed"
|
||||||
|
|
||||||
|
log_info "Searching for tag 'yoga' (should be empty)..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&tags=yoga" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q '"total":0'; then
|
||||||
|
log_success "Yoga tag correctly returned no results (no tags on event)"
|
||||||
|
else
|
||||||
|
log_warning "Yoga event has no tags, but might appear in results"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "TEST 3: Search by date range"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Searching events in June 2026..."
|
||||||
|
FROM="2026-06-01T00:00:00Z"
|
||||||
|
TO="2026-06-30T23:59:59Z"
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&from=$FROM&to=$TO" "$OWNER_TOKEN")
|
||||||
|
|
||||||
|
if echo "$response" | grep -q "Python Workshop"; then
|
||||||
|
log_success "Found June events"
|
||||||
|
else
|
||||||
|
log_error "June events not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Searching past events only..."
|
||||||
|
FROM="2019-01-01T00:00:00Z"
|
||||||
|
TO="2021-01-01T00:00:00Z"
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&from=$FROM&to=$TO" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q "Past Event"; then
|
||||||
|
log_success "Found past event"
|
||||||
|
else
|
||||||
|
log_error "Past event not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "TEST 4: Geo-location search"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Searching events within 5km of Moscow center..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&lat=55.7558&lon=37.6173&radius=5" "$OWNER_TOKEN")
|
||||||
|
|
||||||
|
if echo "$response" | grep -q "Python Workshop"; then
|
||||||
|
log_success "Found Moscow events"
|
||||||
|
else
|
||||||
|
log_error "Moscow events not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Searching events within 1km of Moscow (should find fewer)..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&lat=55.7558&lon=37.6173&radius=1" "$OWNER_TOKEN")
|
||||||
|
log_success "Radius search completed"
|
||||||
|
|
||||||
|
log_info "Searching events in Saint Petersburg..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&lat=59.9343&lon=30.3351&radius=10" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q "Tech Meetup"; then
|
||||||
|
log_success "Found Saint Petersburg event"
|
||||||
|
else
|
||||||
|
log_error "Saint Petersburg event not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "TEST 5: Combined search"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Search: text 'Python' + tag 'workshop'..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&q=Python&tags=workshop" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q "Python Workshop"; then
|
||||||
|
log_success "Combined text+tag search successful"
|
||||||
|
else
|
||||||
|
log_error "Combined search failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Search: tag 'javascript' + date range..."
|
||||||
|
FROM="2026-06-01T00:00:00Z"
|
||||||
|
TO="2026-06-30T23:59:59Z"
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&tags=javascript&from=$FROM&to=$TO" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q "JavaScript Conference"; then
|
||||||
|
log_success "Combined tag+date search successful"
|
||||||
|
else
|
||||||
|
log_error "Combined search failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "TEST 6: Pagination"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Search with limit=2..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&limit=2" "$OWNER_TOKEN")
|
||||||
|
COUNT=$(echo "$response" | grep -o "\"id\"" | wc -l)
|
||||||
|
if [ "$COUNT" -le 2 ]; then
|
||||||
|
log_success "Pagination limit works (got $COUNT results)"
|
||||||
|
else
|
||||||
|
log_error "Pagination limit failed (got $COUNT results)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Search with offset=2..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&limit=2&offset=2" "$OWNER_TOKEN")
|
||||||
|
log_success "Pagination offset works"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "TEST 7: Sorting"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Sort by start_time ascending..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&sort=start_time&order=asc" "$OWNER_TOKEN")
|
||||||
|
log_success "Sort ascending completed"
|
||||||
|
|
||||||
|
log_info "Sort by start_time descending..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&sort=start_time&order=desc" "$OWNER_TOKEN")
|
||||||
|
log_success "Sort descending completed"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "TEST 8: Calendar search"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Searching calendars by text..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=calendar&q=Tech" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q "Tech Events Calendar"; then
|
||||||
|
log_success "Found calendar by text"
|
||||||
|
else
|
||||||
|
log_error "Calendar not found by text"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Searching calendars by tag..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=calendar&tags=tech" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q "Tech Events Calendar"; then
|
||||||
|
log_success "Found calendar by tag"
|
||||||
|
else
|
||||||
|
log_error "Calendar not found by tag"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "TEST 9: Search all (events + calendars)"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Searching all (no type specified)..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?q=Tech" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q "events" && echo "$response" | grep -q "calendars"; then
|
||||||
|
log_success "All search returned both events and calendars"
|
||||||
|
else
|
||||||
|
log_warning "All search may not have returned both types"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "TEST 10: Empty search results"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Searching for non-existent text..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&q=nonexistenttext12345" "$OWNER_TOKEN")
|
||||||
|
if echo "$response" | grep -q '"total":0'; then
|
||||||
|
log_success "Empty search handled correctly"
|
||||||
|
else
|
||||||
|
log_error "Empty search not handled correctly"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "============================================================"
|
||||||
|
log_info "TEST 11: Commercial calendar visibility"
|
||||||
|
log_info "============================================================"
|
||||||
|
|
||||||
|
log_info "Creating commercial calendar..."
|
||||||
|
response=$(http_post "$BASE_URL/v1/calendars" \
|
||||||
|
"{\"title\":\"Public Commercial Calendar\",\"type\":\"commercial\"}" "$OWNER_TOKEN")
|
||||||
|
COMMERCIAL_ID=$(extract_json "$response" "id")
|
||||||
|
log_success "Commercial calendar created: $COMMERCIAL_ID"
|
||||||
|
|
||||||
|
log_info "Creating event in commercial calendar..."
|
||||||
|
response=$(http_post "$BASE_URL/v1/calendars/$COMMERCIAL_ID/events" \
|
||||||
|
"{\"title\":\"Public Event\",\"start_time\":\"2026-06-01T10:00:00Z\",\"duration\":60}" "$OWNER_TOKEN")
|
||||||
|
log_success "Public event created"
|
||||||
|
|
||||||
|
log_info "Creating another user to test visibility..."
|
||||||
|
OTHER_EMAIL="search_other_$(date +%s)@example.com"
|
||||||
|
response=$(http_post "$BASE_URL/v1/register" "{\"email\":\"$OTHER_EMAIL\",\"password\":\"test123\"}" "")
|
||||||
|
OTHER_TOKEN=$(extract_json "$response" "token")
|
||||||
|
|
||||||
|
log_info "Other user searching for public event..."
|
||||||
|
response=$(http_get "$BASE_URL/v1/search?type=event&q=Public" "$OTHER_TOKEN")
|
||||||
|
if echo "$response" | grep -q "Public Event"; then
|
||||||
|
log_success "Other user can see public event in commercial calendar"
|
||||||
|
else
|
||||||
|
log_error "Other user cannot see public event"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
log_success "SEARCH API TESTS COMPLETED!"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
echo "Summary of created resources:"
|
||||||
|
echo " Owner: $OWNER_EMAIL"
|
||||||
|
echo " Calendar: $CALENDAR_ID"
|
||||||
|
echo " Commercial Calendar: $COMMERCIAL_ID"
|
||||||
|
echo " Events created: 5"
|
||||||
|
echo ""
|
||||||
Reference in New Issue
Block a user