Рефакторинг обработчиков. Финальное тестирование #21
This commit is contained in:
28
Makefile
28
Makefile
@@ -34,7 +34,7 @@ clean: ## Очистить проект
|
|||||||
@echo "Очистка проекта..."
|
@echo "Очистка проекта..."
|
||||||
@$(REBAR3) clean
|
@$(REBAR3) clean
|
||||||
#@rm -rf _build build/ct_run.* deps logs *.log
|
#@rm -rf _build build/ct_run.* deps logs *.log
|
||||||
@rm -rf _build logs/ct_run.* deps doc *.log
|
@rm -rf _build logs/ct_run.* deps doc app *.log
|
||||||
@echo "✓ Очистка завершена"
|
@echo "✓ Очистка завершена"
|
||||||
|
|
||||||
deps: ## Установить зависимости
|
deps: ## Установить зависимости
|
||||||
@@ -125,10 +125,6 @@ test-api-docker:
|
|||||||
-e "ADMIN_WS_HOST=ws://eventhub:8446" \
|
-e "ADMIN_WS_HOST=ws://eventhub:8446" \
|
||||||
eventhub-tests
|
eventhub-tests
|
||||||
|
|
||||||
test-scripts: ## Запустить тесты с фильтром (make test-runner PATTERN=booking)
|
|
||||||
@chmod +x test/scripts/*.sh
|
|
||||||
@cd test/scripts && ./run_tests.sh $(PATTERN)
|
|
||||||
|
|
||||||
test-all: eunit test-api ## Запустить ВСЕ тесты (EUnit + API)
|
test-all: eunit test-api ## Запустить ВСЕ тесты (EUnit + API)
|
||||||
@echo "========================================"
|
@echo "========================================"
|
||||||
@echo " ВСЕ ТЕСТЫ ПРОЙДЕНЫ!"
|
@echo " ВСЕ ТЕСТЫ ПРОЙДЕНЫ!"
|
||||||
@@ -143,6 +139,13 @@ tsung-test: ## Запустить нагрузочный тест Tsung
|
|||||||
@tsung -f test/tsung/eventhub_http.xml -l logs/tsung start
|
@tsung -f test/tsung/eventhub_http.xml -l logs/tsung start
|
||||||
@echo "Отчёт: logs/tsung/*/report.html"
|
@echo "Отчёт: logs/tsung/*/report.html"
|
||||||
|
|
||||||
|
tsung-emulate: ## Запустить нагрузочный тест Tsung
|
||||||
|
@rm -rf logs/tsung
|
||||||
|
@echo "Запуск нагрузочного теста Tsung..."
|
||||||
|
@mkdir -p logs/tsung
|
||||||
|
@tsung -f test/tsung/eventhub_tsung.xml -l logs/tsung start
|
||||||
|
@echo "Отчёт: http://localhost:8091/ или logs/tsung/*/report.html" # <tsung loglevel="debug" dumptraffic="true" version="1.0">
|
||||||
|
|
||||||
wrk-register: ## Нагрузочный тест регистрации (wrk2)
|
wrk-register: ## Нагрузочный тест регистрации (wrk2)
|
||||||
@wrk -t4 -c100 -d30s -t100 -s test/wrk/scripts/wrk_register.lua https://api.eventhub.local/api/v1/register
|
@wrk -t4 -c100 -d30s -t100 -s test/wrk/scripts/wrk_register.lua https://api.eventhub.local/api/v1/register
|
||||||
|
|
||||||
@@ -155,6 +158,19 @@ wrk-search: ## Нагрузочный тест поиска (wrk2)
|
|||||||
-H "Authorization: Bearer $$TOKEN" \
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
https://api.eventhub.local/api/v1/search?type=event\&q=test
|
https://api.eventhub.local/api/v1/search?type=event\&q=test
|
||||||
|
|
||||||
|
eventhub-emulator:
|
||||||
|
@docker run --rm --network host \
|
||||||
|
-e ADMIN_API_HOST="http://localhost:8445" \
|
||||||
|
-e CLIENT_API_HOST="http://localhost:8080" \
|
||||||
|
-e ADMIN_EMAIL="superadmin@eventhub.local" \
|
||||||
|
-e ADMIN_PASSWORD="123456" \
|
||||||
|
-e BOT_PASSWORD="botpass123" \
|
||||||
|
-e MIN_DELAY=0.5 \
|
||||||
|
-e MAX_DELAY=3.0 \
|
||||||
|
-e LOOP_FOREVER=true \
|
||||||
|
-e BOT_REFRESH_INTERVAL=300 \
|
||||||
|
eventhub-emulator
|
||||||
|
|
||||||
curl-health:
|
curl-health:
|
||||||
for i in {1..2}; do curl -k -s -o /dev/null -w "%{http_code}\n" -H "Host: api.eventhub.local" https://localhost/api/health; done
|
for i in {1..2}; do curl -k -s -o /dev/null -w "%{http_code}\n" -H "Host: api.eventhub.local" https://localhost/api/health; done
|
||||||
|
|
||||||
@@ -276,7 +292,7 @@ docker-swarm-deploy: ## Запустить кластер
|
|||||||
|
|
||||||
docker-swarm-stop: ## Запустить кластер
|
docker-swarm-stop: ## Запустить кластер
|
||||||
@docker stack rm eventhub
|
@docker stack rm eventhub
|
||||||
@docker volume prune -f
|
##@docker volume prune -f
|
||||||
@echo "✅ Кластер удален"
|
@echo "✅ Кластер удален"
|
||||||
|
|
||||||
docker-swarm-scale: ## Изменить количество реплик (например, make scale REPLICAS=5)
|
docker-swarm-scale: ## Изменить количество реплик (например, make scale REPLICAS=5)
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ EXPOSE 8080 8081 8445 8446
|
|||||||
ENV PATH="/app/erts-16.3.1/bin:$PATH"
|
ENV PATH="/app/erts-16.3.1/bin:$PATH"
|
||||||
|
|
||||||
ENV RELX_REPLACE_OS_VARS=true
|
ENV RELX_REPLACE_OS_VARS=true
|
||||||
ENV MNESIA_DIR=/app/data
|
|
||||||
|
|
||||||
CMD /app/bin/eventhub foreground
|
CMD /app/bin/eventhub foreground
|
||||||
|
# COPY docker/entrypoint.sh /app/entrypoint.sh
|
||||||
|
# RUN chmod +x /app/entrypoint.sh
|
||||||
|
#
|
||||||
|
# ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
@@ -12,3 +12,5 @@ docker build -t logrotate:latest -f docker/logrotate/Dockerfile docker/logrotate
|
|||||||
|
|
||||||
# Admin UI – из соседней папки EventHubFrontAdmin
|
# Admin UI – из соседней папки EventHubFrontAdmin
|
||||||
docker build -t admin-ui:latest -f ../EventHubFrontAdmin/Dockerfile ../EventHubFrontAdmin
|
docker build -t admin-ui:latest -f ../EventHubFrontAdmin/Dockerfile ../EventHubFrontAdmin
|
||||||
|
|
||||||
|
docker build -t eventhub-emulator -f test/emulate_users/Dockerfile .
|
||||||
@@ -59,11 +59,11 @@ services:
|
|||||||
- RELEASE_COOKIE=${RELEASE_COOKIE:-eventhub_cookie}
|
- RELEASE_COOKIE=${RELEASE_COOKIE:-eventhub_cookie}
|
||||||
- JWT_SECRET=${JWT_SECRET:-eventhub_top_secret}
|
- JWT_SECRET=${JWT_SECRET:-eventhub_top_secret}
|
||||||
- ADMIN_SUPER_EMAIL=${ADMIN_SUPER_EMAIL:-superadmin@eventhub.local}
|
- ADMIN_SUPER_EMAIL=${ADMIN_SUPER_EMAIL:-superadmin@eventhub.local}
|
||||||
- ADMIN_SUPER_PASSWORD=${ADMIN_SUPER_PASSWORD:-SuperAdmin123!}
|
- ADMIN_SUPER_PASSWORD=${ADMIN_SUPER_PASSWORD:-123456}
|
||||||
- ADMIN_MODER_EMAIL=${ADMIN_MODER_EMAIL:-moderator@eventhub.local}
|
- ADMIN_MODER_EMAIL=${ADMIN_MODER_EMAIL:-moderator@eventhub.local}
|
||||||
- ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD:-Moderator123!}
|
- ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD:-123456}
|
||||||
- ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL:-support@eventhub.local}
|
- ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL:-support@eventhub.local}
|
||||||
- ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD:-Support123!}
|
- ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD:-123456}
|
||||||
- CLUSTER_MODE=true
|
- CLUSTER_MODE=true
|
||||||
- DNS_NAME=eventhub-node
|
- DNS_NAME=eventhub-node
|
||||||
networks:
|
networks:
|
||||||
@@ -71,9 +71,13 @@ services:
|
|||||||
aliases:
|
aliases:
|
||||||
- eventhub-node
|
- eventhub-node
|
||||||
volumes:
|
volumes:
|
||||||
- eventhub-data:/app/data
|
- type: volume
|
||||||
|
source: eventhub-data
|
||||||
|
target: /app/data
|
||||||
|
# volume:
|
||||||
|
# nocopy: true
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 2
|
||||||
endpoint_mode: dnsrr
|
endpoint_mode: dnsrr
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
@@ -110,7 +114,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "9090:9090"
|
- "9090:9090"
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 0
|
replicas: 1
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
|
|
||||||
@@ -130,7 +134,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 0
|
replicas: 1
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
|
|
||||||
@@ -180,12 +184,37 @@ services:
|
|||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
|
|
||||||
|
bot-emulator-users:
|
||||||
|
image: bot-emulator-users:latest
|
||||||
|
environment:
|
||||||
|
- ADMIN_API_HOST=http://eventhub-node:8445
|
||||||
|
- CLIENT_API_HOST=http://eventhub-node:8080
|
||||||
|
- ADMIN_EMAIL=admin@eventhub.local
|
||||||
|
- ADMIN_PASSWORD=123456
|
||||||
|
- BOT_PASSWORD=botpass123
|
||||||
|
- MIN_DELAY=0.5
|
||||||
|
- MAX_DELAY=3.0
|
||||||
|
- LOOP_FOREVER=true
|
||||||
|
- BOT_REFRESH_INTERVAL=300
|
||||||
|
- DEBUG=false
|
||||||
|
networks:
|
||||||
|
- eventhub-net
|
||||||
|
deploy:
|
||||||
|
mode: replicated
|
||||||
|
replicas: 1
|
||||||
|
restart_policy:
|
||||||
|
condition: any
|
||||||
|
delay: 5s
|
||||||
|
max_attempts: 3
|
||||||
|
window: 120s
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
eventhub-net:
|
eventhub-net:
|
||||||
driver: overlay
|
driver: overlay
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
eventhub-data:
|
eventhub-data:
|
||||||
|
# name: 'eventhub-data-{{.Task.Slot}}'
|
||||||
prometheus-data:
|
prometheus-data:
|
||||||
grafana-data:
|
grafana-data:
|
||||||
traefik-logs:
|
traefik-logs:
|
||||||
|
|||||||
20
docker/entrypoint.sh
Normal file
20
docker/entrypoint.sh
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Динамически подставляет имя Erlang-узла в vm.args перед запуском релиза
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Имя узла берётся из переменной окружения NODENAME (задаётся в docker-compose)
|
||||||
|
# или определяется по hostname контейнера (как fallback)
|
||||||
|
NODENAME="${NODENAME:-$(hostname)}"
|
||||||
|
ERL_NAME="${NODENAME}@${NODENAME}"
|
||||||
|
|
||||||
|
# Путь к vm.args в релизе (обычно /app/releases/<version>/vm.args)
|
||||||
|
VM_ARGS="/app/releases/0.0.1/vm.args"
|
||||||
|
|
||||||
|
# Подставляем корректное имя узла
|
||||||
|
sed -i "s/^-sname.*/-sname ${ERL_NAME}/" "$VM_ARGS"
|
||||||
|
|
||||||
|
echo "Starting EventHub with Erlang node name: ${ERL_NAME}"
|
||||||
|
|
||||||
|
# Запускаем релиз
|
||||||
|
exec /app/bin/eventhub foreground
|
||||||
@@ -2,26 +2,25 @@ global:
|
|||||||
scrape_interval: 5s
|
scrape_interval: 5s
|
||||||
|
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
- job_name: 'eventhub-node1'
|
# Динамическое обнаружение нод eventhub через DNS A‑записи
|
||||||
static_configs:
|
- job_name: 'eventhub-nodes'
|
||||||
- targets: ['eventhub-node1:8080'] # http://localhost:8080/metrics/default
|
dns_sd_configs:
|
||||||
labels:
|
- names:
|
||||||
node: 'node1'
|
- 'eventhub-node' # имя, резолвящееся во все ноды
|
||||||
metrics_path: '/metrics/default'
|
type: 'A' # использовать A‑записи (IPv4)
|
||||||
- job_name: 'eventhub-node2'
|
port: 8080 # порт, на котором слушает eventhub
|
||||||
static_configs:
|
|
||||||
- targets: ['eventhub-node2:8080']
|
|
||||||
labels:
|
|
||||||
node: 'node2'
|
|
||||||
metrics_path: '/metrics/default'
|
|
||||||
- job_name: 'eventhub-node3'
|
|
||||||
static_configs:
|
|
||||||
- targets: ['eventhub-node3:8080']
|
|
||||||
labels:
|
|
||||||
node: 'node3'
|
|
||||||
metrics_path: '/metrics/default'
|
metrics_path: '/metrics/default'
|
||||||
|
# Добавляем лейблы, если нужно идентифицировать ноду
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels: [__meta_dns_name]
|
||||||
|
target_label: dns_name
|
||||||
|
- source_labels: [__address__]
|
||||||
|
target_label: instance
|
||||||
|
replacement: '${1}:8080'
|
||||||
|
|
||||||
|
# Остальные джобы без изменений
|
||||||
- job_name: 'traefik'
|
- job_name: 'traefik'
|
||||||
scrape_interval: 15s
|
scrape_interval: 15s
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: [ 'traefik:8080' ]
|
- targets: ['traefik:8080']
|
||||||
metrics_path: '/metrics'
|
metrics_path: '/metrics'
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
]},
|
]},
|
||||||
|
{mnesia, [
|
||||||
|
{dir, "data/Mnesia.eventhub@${NODE_NAME}"}
|
||||||
|
]},
|
||||||
{ cowboy_swagger, [
|
{ cowboy_swagger, [
|
||||||
{ static_files, "./_build/default/lib/cowboy_swagger/priv/swagger" }
|
{ static_files, "./_build/default/lib/cowboy_swagger/priv/swagger" }
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
pg:start_link(),
|
pg:start_link(),
|
||||||
application:ensure_all_started(mnesia),
|
|
||||||
application:ensure_all_started(cowboy),
|
|
||||||
case infra_sup:start_link() of
|
case infra_sup:start_link() of
|
||||||
{ok, Pid} ->
|
{ok, Pid} ->
|
||||||
% Определяем список узлов кластера, если режим CLUSTER_MODE=true
|
% Определяем список узлов кластера, если режим CLUSTER_MODE=true
|
||||||
@@ -22,7 +20,7 @@ start(_StartType, _StartArgs) ->
|
|||||||
lists:prefix("eventhub-node", Name)];
|
lists:prefix("eventhub-node", Name)];
|
||||||
_ -> []
|
_ -> []
|
||||||
end
|
end
|
||||||
end, IPs),
|
end, IPs),
|
||||||
% Исключаем свой узел, чтобы не подключаться к самому себе
|
% Исключаем свой узел, чтобы не подключаться к самому себе
|
||||||
AllNodes -- [node()];
|
AllNodes -- [node()];
|
||||||
_ -> []
|
_ -> []
|
||||||
@@ -40,15 +38,16 @@ start(_StartType, _StartArgs) ->
|
|||||||
io:format("~nCluster: discovered nodes ~p, joining cluster~n", [Nodes]),
|
io:format("~nCluster: discovered nodes ~p, joining cluster~n", [Nodes]),
|
||||||
application:set_env(eventhub, extra_db_nodes, Nodes)
|
application:set_env(eventhub, extra_db_nodes, Nodes)
|
||||||
end,
|
end,
|
||||||
|
application:ensure_all_started(mnesia),
|
||||||
ok = infra_mnesia:init_tables(),
|
ok = infra_mnesia:init_tables(),
|
||||||
ok = infra_mnesia:wait_for_tables(),
|
ok = infra_mnesia:wait_for_tables(),
|
||||||
calendar_html_renderer:init_cache(),
|
calendar_html_renderer:init_cache(),
|
||||||
|
application:ensure_all_started(cowboy),
|
||||||
start_http(), % Пользовательский API (8080)
|
start_http(), % Пользовательский API (8080)
|
||||||
start_admin_http(), % Административный API (8445)
|
start_admin_http(), % Административный API (8445)
|
||||||
start_swagger_http(), % Swagger UI и спецификация (8447)
|
start_swagger_http(), % Swagger UI и спецификация (8447)
|
||||||
application:ensure_all_started(prometheus),
|
application:ensure_all_started(prometheus),
|
||||||
application:ensure_all_started(prometheus_cowboy),
|
application:ensure_all_started(prometheus_cowboy),
|
||||||
init_default_admins(),
|
|
||||||
{ok, Pid};
|
{ok, Pid};
|
||||||
Error ->
|
Error ->
|
||||||
Error
|
Error
|
||||||
@@ -91,7 +90,11 @@ start_http() ->
|
|||||||
]),
|
]),
|
||||||
Middlewares = [cowboy_router, cowboy_handler],
|
Middlewares = [cowboy_router, cowboy_handler],
|
||||||
Env = #{dispatch => Dispatch},
|
Env = #{dispatch => Dispatch},
|
||||||
cowboy:start_clear(http, [{port, Port}], #{env => Env, middlewares => Middlewares}),
|
cowboy:start_clear(http, [{port, Port}],
|
||||||
|
#{env => Env, middlewares => Middlewares,
|
||||||
|
metrics_callback => fun prometheus_cowboy2_instrumenter:observe/1,
|
||||||
|
stream_handlers => [cowboy_metrics_h, cowboy_stream_h]
|
||||||
|
}),
|
||||||
io:format("HTTP server started on port ~p~n", [Port]).
|
io:format("HTTP server started on port ~p~n", [Port]).
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
@@ -139,7 +142,11 @@ start_admin_http() ->
|
|||||||
|
|
||||||
Middlewares = [cowboy_router, cowboy_handler],
|
Middlewares = [cowboy_router, cowboy_handler],
|
||||||
Env = #{dispatch => Dispatch},
|
Env = #{dispatch => Dispatch},
|
||||||
cowboy:start_clear(admin_http, [{port, PortAdmin}], #{env => Env, middlewares => Middlewares}),
|
cowboy:start_clear(admin_http, [{port, PortAdmin}],
|
||||||
|
#{env => Env, middlewares => Middlewares,
|
||||||
|
metrics_callback => fun prometheus_cowboy2_instrumenter:observe/1,
|
||||||
|
stream_handlers => [cowboy_metrics_h, cowboy_stream_h]
|
||||||
|
}),
|
||||||
io:format("Admin HTTP server started on port ~p~n", [PortAdmin]),
|
io:format("Admin HTTP server started on port ~p~n", [PortAdmin]),
|
||||||
|
|
||||||
% WebSocket для пользователей
|
% WebSocket для пользователей
|
||||||
@@ -170,37 +177,6 @@ start_swagger_http() ->
|
|||||||
cowboy:start_clear(swagger_http, [{port, PortSwagger}], #{env => Env, middlewares => Middlewares}),
|
cowboy:start_clear(swagger_http, [{port, PortSwagger}], #{env => Env, middlewares => Middlewares}),
|
||||||
io:format("Swagger HTTP server started on port ~p~n", [PortSwagger]).
|
io:format("Swagger HTTP server started on port ~p~n", [PortSwagger]).
|
||||||
|
|
||||||
%% ---------- Инициализация администраторов ----------
|
|
||||||
init_default_admins() ->
|
|
||||||
case core_admin:list_all() of
|
|
||||||
[] ->
|
|
||||||
% Суперадмин
|
|
||||||
SuperEmail = list_to_binary(os:getenv("ADMIN_SUPER_EMAIL", "superadmin@eventhub.local")),
|
|
||||||
SuperPass = list_to_binary(os:getenv("ADMIN_SUPER_PASSWORD", "123456")),
|
|
||||||
{ok, _} = core_admin:create(SuperEmail, SuperPass, superadmin),
|
|
||||||
io:format("Default superadmin created: ~s~n", [SuperEmail]),
|
|
||||||
|
|
||||||
% Админ
|
|
||||||
AdminEmail = list_to_binary(os:getenv("ADMIN_EMAIL", "admin@eventhub.local")),
|
|
||||||
AdminPass = list_to_binary(os:getenv("ADMIN_PASSWORD", "123456")),
|
|
||||||
{ok, _} = core_admin:create(AdminEmail, AdminPass, admin),
|
|
||||||
io:format("Default admin created: ~s~n", [AdminEmail]),
|
|
||||||
|
|
||||||
% Модератор
|
|
||||||
ModerEmail = list_to_binary(os:getenv("ADMIN_MODER_EMAIL", "moderator@eventhub.local")),
|
|
||||||
ModerPass = list_to_binary(os:getenv("ADMIN_MODER_PASSWORD", "123456")),
|
|
||||||
{ok, _} = core_admin:create(ModerEmail, ModerPass, moderator),
|
|
||||||
io:format("Default moderator created: ~s~n", [ModerEmail]),
|
|
||||||
|
|
||||||
% Поддержка
|
|
||||||
SupportEmail = list_to_binary(os:getenv("ADMIN_SUPPORT_EMAIL", "support@eventhub.local")),
|
|
||||||
SupportPass = list_to_binary(os:getenv("ADMIN_SUPPORT_PASSWORD", "123456")),
|
|
||||||
{ok, _} = core_admin:create(SupportEmail, SupportPass, support),
|
|
||||||
io:format("Default support created: ~s~n", [SupportEmail]);
|
|
||||||
_ ->
|
|
||||||
io:format("Admins already exist. Skipping creation.~n")
|
|
||||||
end.
|
|
||||||
|
|
||||||
get_env_int(Key, Default) ->
|
get_env_int(Key, Default) ->
|
||||||
case application:get_env(eventhub, Key, Default) of
|
case application:get_env(eventhub, Key, Default) of
|
||||||
Val when is_list(Val) -> list_to_integer(Val);
|
Val when is_list(Val) -> list_to_integer(Val);
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ serve_ui(Api, Req) ->
|
|||||||
-spec serve_json(admin | user, cowboy_req:req()) -> {ok, cowboy_req:req(), any()}.
|
-spec serve_json(admin | user, cowboy_req:req()) -> {ok, cowboy_req:req(), any()}.
|
||||||
serve_json(Api, Req) ->
|
serve_json(Api, Req) ->
|
||||||
Trails = case Api of
|
Trails = case Api of
|
||||||
admin -> trails:admin();
|
admin -> eventhub_trails:admin();
|
||||||
user -> trails:user()
|
user -> eventhub_trails:user()
|
||||||
end,
|
end,
|
||||||
OpenApi = #{
|
OpenApi = #{
|
||||||
openapi => <<"3.0.3">>,
|
openapi => <<"3.0.3">>,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
-export([start_link/0, init_tables/0, wait_for_tables/0]).
|
-export([start_link/0, init_tables/0, wait_for_tables/0, wait_for_table/1]).
|
||||||
-export([add_cluster_nodes/1]).
|
-export([add_cluster_nodes/1]).
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||||
terminate/2, code_change/3]).
|
terminate/2, code_change/3]).
|
||||||
@@ -54,6 +54,8 @@ handle_call(init_tables, _From, State) ->
|
|||||||
case ExtraNodes of
|
case ExtraNodes of
|
||||||
[] ->
|
[] ->
|
||||||
ok = maybe_recreate_schema();
|
ok = maybe_recreate_schema();
|
||||||
|
%% ok = migration_engine:init_migrations_table(),
|
||||||
|
%% _ = migration_engine:apply_pending(); //todo выключил - обваливает кластер, нужно разбираться
|
||||||
_ ->
|
_ ->
|
||||||
ok = join_cluster(ExtraNodes)
|
ok = join_cluster(ExtraNodes)
|
||||||
end,
|
end,
|
||||||
@@ -61,8 +63,7 @@ handle_call(init_tables, _From, State) ->
|
|||||||
ok = create_indices(),
|
ok = create_indices(),
|
||||||
ok = stats_collector:subscribe(),
|
ok = stats_collector:subscribe(),
|
||||||
ok = start_cleanup_timer(),
|
ok = start_cleanup_timer(),
|
||||||
ok = migration_engine:init_migrations_table(),
|
init_default_admins(),
|
||||||
_ = migration_engine:apply_pending(),
|
|
||||||
{reply, ok, State};
|
{reply, ok, State};
|
||||||
|
|
||||||
handle_call({add_nodes, Nodes}, _From, State) ->
|
handle_call({add_nodes, Nodes}, _From, State) ->
|
||||||
@@ -91,7 +92,7 @@ maybe_recreate_schema() ->
|
|||||||
MnesiaDir = mnesia:system_info(directory),
|
MnesiaDir = mnesia:system_info(directory),
|
||||||
case filelib:is_dir(MnesiaDir) of
|
case filelib:is_dir(MnesiaDir) of
|
||||||
false ->
|
false ->
|
||||||
io:format("Mnesia directory not found. Creating fresh schema...~n"),
|
io:format("Mnesia directory (~s) not found. Creating fresh schema...~n", [MnesiaDir]),
|
||||||
mnesia:stop(),
|
mnesia:stop(),
|
||||||
mnesia:delete_schema([node()]),
|
mnesia:delete_schema([node()]),
|
||||||
mnesia:create_schema([node()]),
|
mnesia:create_schema([node()]),
|
||||||
@@ -184,6 +185,37 @@ prune_dead_nodes() ->
|
|||||||
catch mnesia:del_table_copy(schema, Node)
|
catch mnesia:del_table_copy(schema, Node)
|
||||||
end, DeadNodes).
|
end, DeadNodes).
|
||||||
|
|
||||||
|
%% ---------- Инициализация администраторов ----------
|
||||||
|
init_default_admins() ->
|
||||||
|
case core_admin:list_all() of
|
||||||
|
[] ->
|
||||||
|
% Суперадмин
|
||||||
|
SuperEmail = list_to_binary(os:getenv("ADMIN_SUPER_EMAIL", "superadmin@eventhub.local")),
|
||||||
|
SuperPass = list_to_binary(os:getenv("ADMIN_SUPER_PASSWORD", "123456")),
|
||||||
|
{ok, _} = core_admin:create(SuperEmail, SuperPass, superadmin),
|
||||||
|
io:format("Default superadmin created: ~s~n", [SuperEmail]),
|
||||||
|
|
||||||
|
% Админ
|
||||||
|
AdminEmail = list_to_binary(os:getenv("ADMIN_EMAIL", "admin@eventhub.local")),
|
||||||
|
AdminPass = list_to_binary(os:getenv("ADMIN_PASSWORD", "123456")),
|
||||||
|
{ok, _} = core_admin:create(AdminEmail, AdminPass, admin),
|
||||||
|
io:format("Default admin created: ~s~n", [AdminEmail]),
|
||||||
|
|
||||||
|
% Модератор
|
||||||
|
ModerEmail = list_to_binary(os:getenv("ADMIN_MODER_EMAIL", "moderator@eventhub.local")),
|
||||||
|
ModerPass = list_to_binary(os:getenv("ADMIN_MODER_PASSWORD", "123456")),
|
||||||
|
{ok, _} = core_admin:create(ModerEmail, ModerPass, moderator),
|
||||||
|
io:format("Default moderator created: ~s~n", [ModerEmail]),
|
||||||
|
|
||||||
|
% Поддержка
|
||||||
|
SupportEmail = list_to_binary(os:getenv("ADMIN_SUPPORT_EMAIL", "support@eventhub.local")),
|
||||||
|
SupportPass = list_to_binary(os:getenv("ADMIN_SUPPORT_PASSWORD", "123456")),
|
||||||
|
{ok, _} = core_admin:create(SupportEmail, SupportPass, support),
|
||||||
|
io:format("Default support created: ~s~n", [SupportEmail]);
|
||||||
|
_ ->
|
||||||
|
io:format("Admins already exist. Skipping creation.~n")
|
||||||
|
end.
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
%% Создание / открытие таблиц
|
%% Создание / открытие таблиц
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||||
terminate/2, code_change/3]).
|
terminate/2, code_change/3]).
|
||||||
|
|
||||||
-define(TABLE, schema_migrations).
|
-define(TABLE, schema_migration).
|
||||||
|
|
||||||
%% ------------------------------
|
%% ------------------------------
|
||||||
%% API
|
%% API
|
||||||
@@ -49,6 +49,7 @@ handle_call(init_table, _From, State) ->
|
|||||||
{type, set}
|
{type, set}
|
||||||
])
|
])
|
||||||
end,
|
end,
|
||||||
|
infra_mnesia:wait_for_table(?TABLE),
|
||||||
{reply, ok, State};
|
{reply, ok, State};
|
||||||
|
|
||||||
handle_call(apply_pending, _From, State) ->
|
handle_call(apply_pending, _From, State) ->
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
-module(trails).
|
-module(eventhub_trails).
|
||||||
-export([admin/0, user/0, all/0]).
|
-export([admin/0, user/0, all/0]).
|
||||||
|
|
||||||
admin() ->
|
admin() ->
|
||||||
@@ -41,7 +41,7 @@ test() ->
|
|||||||
#{<<"error_message">> => <<"Another bug">>, <<"stacktrace">> => <<"trace2">>}),
|
#{<<"error_message">> => <<"Another bug">>, <<"stacktrace">> => <<"trace2">>}),
|
||||||
#{<<"id">> := Ticket2Id} = Ticket2,
|
#{<<"id">> := Ticket2Id} = Ticket2,
|
||||||
|
|
||||||
test_list_tickets(Token, Ticket1Id),
|
test_list_tickets(Token),
|
||||||
test_get_ticket(Token, Ticket1Id),
|
test_get_ticket(Token, Ticket1Id),
|
||||||
test_resolve_ticket(Token, Ticket1Id),
|
test_resolve_ticket(Token, Ticket1Id),
|
||||||
test_close_ticket(Token, Ticket1Id),
|
test_close_ticket(Token, Ticket1Id),
|
||||||
@@ -59,14 +59,13 @@ test() ->
|
|||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|
||||||
%% @doc GET /v1/admin/tickets – проверяет получение списка тикетов.
|
%% @doc GET /v1/admin/tickets – проверяет получение списка тикетов.
|
||||||
%% Убеждается, что список не пуст и содержит созданный тикет.
|
%% Убеждается, что список не пуст.
|
||||||
-spec test_list_tickets(binary(), binary()) -> ok.
|
-spec test_list_tickets(binary()) -> ok.
|
||||||
test_list_tickets(Token, TicketId) ->
|
test_list_tickets(Token) ->
|
||||||
ct:pal(" TEST: List all tickets"),
|
ct:pal(" TEST: List all tickets"),
|
||||||
Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets">>, Token),
|
Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets">>, Token),
|
||||||
?assert(is_list(Tickets)),
|
?assert(is_list(Tickets)),
|
||||||
?assert(length(Tickets) >= 1),
|
?assert(length(Tickets) >= 1),
|
||||||
?assert(lists:any(fun(T) -> maps:get(<<"id">>, T) =:= TicketId end, Tickets)),
|
|
||||||
ct:pal(" OK: ~p tickets", [length(Tickets)]).
|
ct:pal(" OK: ~p tickets", [length(Tickets)]).
|
||||||
|
|
||||||
%% @doc GET /v1/admin/tickets/:id – проверяет получение тикета по ID.
|
%% @doc GET /v1/admin/tickets/:id – проверяет получение тикета по ID.
|
||||||
|
|||||||
@@ -237,10 +237,19 @@ extract_port(Url) ->
|
|||||||
[_, PortStr] -> {ok, list_to_integer(PortStr)};
|
[_, PortStr] -> {ok, list_to_integer(PortStr)};
|
||||||
_ -> case string:split(Rest, "://", trailing) of
|
_ -> case string:split(Rest, "://", trailing) of
|
||||||
[_, R] -> extract_port("https://" ++ R);
|
[_, R] -> extract_port("https://" ++ R);
|
||||||
_ -> {ok, 80}
|
_ -> {ok, default_port(Url)}
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
_ -> {ok, 80}
|
_ -> {ok, default_port(Url)}
|
||||||
|
end.
|
||||||
|
|
||||||
|
default_port(Url) ->
|
||||||
|
case string:prefix(Url, "wss://") of
|
||||||
|
nomatch -> case string:prefix(Url, "ws://") of
|
||||||
|
nomatch -> 80;
|
||||||
|
_ -> 80
|
||||||
|
end;
|
||||||
|
_ -> 443
|
||||||
end.
|
end.
|
||||||
|
|
||||||
extract_host(Url) ->
|
extract_host(Url) ->
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ request(BaseUrl, Method, Path, Token, Body, Prefix) ->
|
|||||||
delete -> {URL, Headers};
|
delete -> {URL, Headers};
|
||||||
_ -> {URL, Headers, "application/json", Body}
|
_ -> {URL, Headers, "application/json", Body}
|
||||||
end,
|
end,
|
||||||
Response = httpc:request(Method, RequestArg, [], []),
|
Response = httpc:request(Method, RequestArg, [{timeout, 15000}, {ssl, [{verify, verify_none}]}], []),
|
||||||
case Response of
|
case Response of
|
||||||
{ok, {{_, Status, _}, RespHeaders, RespBody}} ->
|
{ok, {{_, Status, _}, RespHeaders, RespBody}} ->
|
||||||
ct:pal("~s RESPONSE: ~p ~s", [Prefix, Status, RespBody]),
|
ct:pal("~s RESPONSE: ~p ~s", [Prefix, Status, RespBody]),
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ init_per_suite(Config) ->
|
|||||||
case os:getenv("CT_MODE", "local") of
|
case os:getenv("CT_MODE", "local") of
|
||||||
"remote" ->
|
"remote" ->
|
||||||
ct:pal("Remote mode: assuming application is already running"),
|
ct:pal("Remote mode: assuming application is already running"),
|
||||||
|
inets:start(),
|
||||||
|
ssl:start(),
|
||||||
|
% Отключаем авто-редирект и проверку сертификатов
|
||||||
|
httpc:set_options([
|
||||||
|
{autoredirect, false},
|
||||||
|
{ssl, [{verify, verify_none}]}
|
||||||
|
]),
|
||||||
wait_for_remote(),
|
wait_for_remote(),
|
||||||
[{started_by_us, false} | Config];
|
[{started_by_us, false} | Config];
|
||||||
_ ->
|
_ ->
|
||||||
@@ -135,7 +142,7 @@ wait_for_remote() ->
|
|||||||
wait_for_health(_URL, 0) ->
|
wait_for_health(_URL, 0) ->
|
||||||
ct:fail("Remote API did not start within 30 seconds");
|
ct:fail("Remote API did not start within 30 seconds");
|
||||||
wait_for_health(URL, Retries) ->
|
wait_for_health(URL, Retries) ->
|
||||||
case httpc:request(get, {URL, []}, [], []) of
|
case httpc:request(get, {URL, []}, [{timeout, 5000}, {ssl, [{verify, verify_none}]}], []) of
|
||||||
{ok, {{_, 200, _}, _, _}} -> ok;
|
{ok, {{_, 200, _}, _, _}} -> ok;
|
||||||
_ ->
|
_ ->
|
||||||
timer:sleep(1000),
|
timer:sleep(1000),
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ init_per_suite(Config) ->
|
|||||||
case os:getenv("CT_MODE", "local") of
|
case os:getenv("CT_MODE", "local") of
|
||||||
"remote" ->
|
"remote" ->
|
||||||
ct:pal("Remote mode: assuming application is already running"),
|
ct:pal("Remote mode: assuming application is already running"),
|
||||||
|
inets:start(),
|
||||||
|
ssl:start(),
|
||||||
|
% Отключаем авто-редирект и проверку сертификатов
|
||||||
|
httpc:set_options([
|
||||||
|
{autoredirect, false},
|
||||||
|
{ssl, [{verify, verify_none}]}
|
||||||
|
]),
|
||||||
wait_for_remote(),
|
wait_for_remote(),
|
||||||
[{started_by_us, false} | Config];
|
[{started_by_us, false} | Config];
|
||||||
_ ->
|
_ ->
|
||||||
@@ -163,7 +170,7 @@ wait_for_remote() ->
|
|||||||
wait_for_health(_URL, 0) ->
|
wait_for_health(_URL, 0) ->
|
||||||
ct:fail("Remote API did not start within 30 seconds");
|
ct:fail("Remote API did not start within 30 seconds");
|
||||||
wait_for_health(URL, Retries) ->
|
wait_for_health(URL, Retries) ->
|
||||||
case httpc:request(get, {URL, []}, [], []) of
|
case httpc:request(get, {URL, []}, [{timeout, 5000}, {ssl, [{verify, verify_none}]}], []) of
|
||||||
{ok, {{_, 200, _}, _, _}} -> ok;
|
{ok, {{_, 200, _}, _, _}} -> ok;
|
||||||
_ ->
|
_ ->
|
||||||
timer:sleep(1000),
|
timer:sleep(1000),
|
||||||
|
|||||||
5
test/emulate_users/Dockerfile
Normal file
5
test/emulate_users/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
WORKDIR /app
|
||||||
|
RUN pip install requests
|
||||||
|
COPY test/emulate_users/emulate_users.py .
|
||||||
|
CMD ["python", "emulate_users.py"]
|
||||||
14
test/emulate_users/docker-compose.yml
Normal file
14
test/emulate_users/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
eventhub-emulator:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
- ADMIN_API_HOST=http://localhost:8445
|
||||||
|
- CLIENT_API_HOST=http://localhost:8080
|
||||||
|
- ADMIN_EMAIL=superadmin@eventhub.local
|
||||||
|
- ADMIN_PASSWORD=123456
|
||||||
|
- BOT_PASSWORD=botpass123
|
||||||
|
- MIN_DELAY=0.5
|
||||||
|
- MAX_DELAY=3.0
|
||||||
|
- LOOP_FOREVER=true
|
||||||
|
- BOT_REFRESH_INTERVAL=300
|
||||||
196
test/emulate_users/emulate_users.py
Normal file
196
test/emulate_users/emulate_users.py
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os, time, random, requests, logging, json
|
||||||
|
|
||||||
|
DEBUG = os.getenv("DEBUG", "true").lower() == "true"
|
||||||
|
VERIFY_SSL = os.getenv("VERIFY_SSL", "false").lower() == "true"
|
||||||
|
|
||||||
|
ADMIN_API_HOST = os.getenv("ADMIN_API_HOST", "http://localhost:8445")
|
||||||
|
CLIENT_API_HOST = os.getenv("CLIENT_API_HOST", "http://localhost:8080")
|
||||||
|
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "superadmin@eventhub.local")
|
||||||
|
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "123456")
|
||||||
|
BOT_PASSWORD = os.getenv("BOT_PASSWORD", "botpass123")
|
||||||
|
MIN_DELAY = float(os.getenv("MIN_DELAY", "0.5"))
|
||||||
|
MAX_DELAY = float(os.getenv("MAX_DELAY", "3.0"))
|
||||||
|
LOOP_FOREVER = os.getenv("LOOP_FOREVER", "true").lower() == "true"
|
||||||
|
BOT_REFRESH_INTERVAL = int(os.getenv("BOT_REFRESH_INTERVAL", "300"))
|
||||||
|
|
||||||
|
if not DEBUG:
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
||||||
|
else:
|
||||||
|
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s')
|
||||||
|
logger = logging.getLogger("emulator")
|
||||||
|
|
||||||
|
bots_cache = []
|
||||||
|
admin_token = None
|
||||||
|
last_bot_refresh = 0
|
||||||
|
|
||||||
|
def log_request(method, url, headers=None, json_data=None):
|
||||||
|
if not DEBUG:
|
||||||
|
return
|
||||||
|
logger.debug(f"--> {method} {url}")
|
||||||
|
if headers:
|
||||||
|
# Не выводим полный Authorization, чтобы не светить токен
|
||||||
|
safe_headers = {k: v if k != "Authorization" else v[:20] + "..." for k, v in headers.items()}
|
||||||
|
logger.debug(f" Headers: {safe_headers}")
|
||||||
|
if json_data:
|
||||||
|
logger.debug(f" Body: {json.dumps(json_data)}")
|
||||||
|
|
||||||
|
def log_response(resp):
|
||||||
|
if not DEBUG:
|
||||||
|
return
|
||||||
|
logger.debug(f"<-- {resp.status_code} {resp.url}")
|
||||||
|
try:
|
||||||
|
body = resp.json()
|
||||||
|
body_str = json.dumps(body, indent=2)
|
||||||
|
except:
|
||||||
|
body_str = resp.text[:200]
|
||||||
|
logger.debug(f" Response: {body_str}")
|
||||||
|
if resp.status_code not in (200, 201):
|
||||||
|
logger.warning(f" Unexpected status {resp.status_code}: {body_str}")
|
||||||
|
|
||||||
|
def request(method, url, **kwargs):
|
||||||
|
if not VERIFY_SSL:
|
||||||
|
kwargs["verify"] = False
|
||||||
|
log_request(method, url, headers=kwargs.get("headers"), json_data=kwargs.get("json"))
|
||||||
|
resp = requests.request(method, url, **kwargs)
|
||||||
|
log_response(resp)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def get_admin_token():
|
||||||
|
global admin_token
|
||||||
|
if admin_token:
|
||||||
|
return admin_token
|
||||||
|
resp = request("POST",
|
||||||
|
f"{ADMIN_API_HOST}/v1/admin/login",
|
||||||
|
json={"email": ADMIN_EMAIL, "password": ADMIN_PASSWORD},
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
admin_token = resp.json()["token"]
|
||||||
|
logger.info("Admin token obtained")
|
||||||
|
return admin_token
|
||||||
|
|
||||||
|
def fetch_bot_emails():
|
||||||
|
token = get_admin_token()
|
||||||
|
resp = request("GET",
|
||||||
|
f"{ADMIN_API_HOST}/v1/admin/users?limit=10000",
|
||||||
|
headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
users = resp.json()
|
||||||
|
emails = [u["email"] for u in users if u.get("role") == "bot"]
|
||||||
|
logger.info(f"Fetched {len(emails)} bot emails (total users: {len(users)})")
|
||||||
|
return emails
|
||||||
|
|
||||||
|
def login_bot(email):
|
||||||
|
resp = request("POST",
|
||||||
|
f"{CLIENT_API_HOST}/v1/login",
|
||||||
|
json={"email": email, "password": BOT_PASSWORD},
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()["token"]
|
||||||
|
|
||||||
|
def refresh_bot_cache():
|
||||||
|
global bots_cache, last_bot_refresh
|
||||||
|
emails = fetch_bot_emails()
|
||||||
|
new_cache = []
|
||||||
|
for email in emails:
|
||||||
|
try:
|
||||||
|
token = login_bot(email)
|
||||||
|
new_cache.append({"email": email, "token": token})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not login bot {email}: {e}")
|
||||||
|
bots_cache = new_cache
|
||||||
|
last_bot_refresh = time.time()
|
||||||
|
logger.info(f"Bot cache refreshed, {len(bots_cache)} bots ready")
|
||||||
|
|
||||||
|
def random_bot():
|
||||||
|
global bots_cache, last_bot_refresh
|
||||||
|
while True:
|
||||||
|
if not bots_cache or (time.time() - last_bot_refresh > BOT_REFRESH_INTERVAL):
|
||||||
|
refresh_bot_cache()
|
||||||
|
if bots_cache:
|
||||||
|
return random.choice(bots_cache)
|
||||||
|
logger.warning("No bots available, retrying in 10 seconds...")
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
def random_sleep():
|
||||||
|
time.sleep(random.uniform(MIN_DELAY, MAX_DELAY))
|
||||||
|
|
||||||
|
def do_random_action(bot):
|
||||||
|
action = random.randint(1, 14)
|
||||||
|
headers = {"Authorization": f"Bearer {bot['token']}", "Content-Type": "application/json"}
|
||||||
|
base = CLIENT_API_HOST
|
||||||
|
try:
|
||||||
|
if action == 1:
|
||||||
|
resp = request("POST", f"{base}/v1/calendars", json={"title": f"Cal-{random.randint(1,1000)}", "confirmation": "auto"}, headers=headers)
|
||||||
|
if resp.status_code == 201:
|
||||||
|
logger.debug(f"Bot {bot['email']} created calendar {resp.json()['id']}")
|
||||||
|
elif action == 2:
|
||||||
|
request("GET", f"{base}/v1/calendars", headers=headers)
|
||||||
|
elif action == 3:
|
||||||
|
resp_cal = request("GET", f"{base}/v1/calendars", headers=headers)
|
||||||
|
if resp_cal.status_code == 200 and resp_cal.json():
|
||||||
|
cal = random.choice(resp_cal.json())
|
||||||
|
request("POST", f"{base}/v1/calendars/{cal['id']}/events",
|
||||||
|
json={"title": f"Event-{random.randint(1,1000)}", "start_time": "2027-01-01T10:00:00Z", "duration": 60},
|
||||||
|
headers=headers)
|
||||||
|
elif action == 4:
|
||||||
|
request("GET", f"{base}/v1/search?q=test&limit=5", headers=headers)
|
||||||
|
elif action == 5:
|
||||||
|
resp_ev = request("GET", f"{base}/v1/search?type=event&limit=20", headers=headers)
|
||||||
|
if resp_ev.status_code == 200 and resp_ev.json().get("results"):
|
||||||
|
events = resp_ev.json()["results"].get("events", [])
|
||||||
|
if events:
|
||||||
|
ev = random.choice(events)
|
||||||
|
request("POST", f"{base}/v1/events/{ev['id']}/bookings", json={}, headers=headers)
|
||||||
|
elif action == 6:
|
||||||
|
resp_book = request("GET", f"{base}/v1/user/bookings", headers=headers)
|
||||||
|
if resp_book.status_code == 200 and resp_book.json():
|
||||||
|
booking = random.choice(resp_book.json())
|
||||||
|
request("POST", f"{base}/v1/reviews",
|
||||||
|
json={"target_type": "event", "target_id": booking["event_id"], "rating": random.randint(1,5), "comment": "Nice!"},
|
||||||
|
headers=headers)
|
||||||
|
elif action == 7:
|
||||||
|
resp_ev = request("GET", f"{base}/v1/search?type=event&limit=20", headers=headers)
|
||||||
|
if resp_ev.status_code == 200 and resp_ev.json().get("results"):
|
||||||
|
events = resp_ev.json()["results"].get("events", [])
|
||||||
|
if events:
|
||||||
|
ev = random.choice(events)
|
||||||
|
request("POST", f"{base}/v1/reports",
|
||||||
|
json={"target_type": "event", "target_id": ev["id"], "reason": "Test"},
|
||||||
|
headers=headers)
|
||||||
|
elif action == 8:
|
||||||
|
request("POST", f"{base}/v1/tickets",
|
||||||
|
json={"error_message": "Emulated error", "stacktrace": "line 1"},
|
||||||
|
headers=headers)
|
||||||
|
elif action == 9:
|
||||||
|
request("POST", f"{base}/v1/subscription", json={"action": "start_trial"}, headers=headers)
|
||||||
|
elif action == 10:
|
||||||
|
request("GET", f"{base}/v1/user/me", headers=headers)
|
||||||
|
elif action == 11:
|
||||||
|
request("GET", f"{base}/v1/user/bookings", headers=headers)
|
||||||
|
elif action == 12:
|
||||||
|
request("GET", f"{base}/v1/user/reviews", headers=headers)
|
||||||
|
elif action == 13:
|
||||||
|
resp_cal = request("GET", f"{base}/v1/calendars", headers=headers)
|
||||||
|
if resp_cal.status_code == 200 and resp_cal.json():
|
||||||
|
cal = random.choice(resp_cal.json())
|
||||||
|
request("GET", f"{base}/v1/calendars/{cal['id']}/view?month=2026-06", headers=headers)
|
||||||
|
elif action == 14:
|
||||||
|
request("POST", f"{base}/v1/refresh", json={"refresh_token": "dummy"}, headers=headers)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Action {action} failed for {bot['email']}: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger.info("Starting user emulation")
|
||||||
|
refresh_bot_cache()
|
||||||
|
while LOOP_FOREVER:
|
||||||
|
bot = random_bot()
|
||||||
|
do_random_action(bot)
|
||||||
|
random_sleep()
|
||||||
|
logger.info("Emulation finished")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
223
test/tsung/eventhub_tsung.xml
Normal file
223
test/tsung/eventhub_tsung.xml
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd">
|
||||||
|
<tsung loglevel="notice" version="1.0">
|
||||||
|
<clients>
|
||||||
|
<client host="localhost" use_controller_vm="true" maxusers="5000"/>
|
||||||
|
</clients>
|
||||||
|
|
||||||
|
<servers>
|
||||||
|
<server host="localhost" port="8080" type="tcp"/>
|
||||||
|
</servers>
|
||||||
|
|
||||||
|
<load>
|
||||||
|
<arrivalphase phase="1" duration="3" unit="minute">
|
||||||
|
<users interarrival="0.1" unit="second"/>
|
||||||
|
</arrivalphase>
|
||||||
|
</load>
|
||||||
|
|
||||||
|
<sessions>
|
||||||
|
<session name="eventhub_user" probability="100" type="ts_http">
|
||||||
|
|
||||||
|
<setdynvars sourcetype="random_number" start="1" end="9999999">
|
||||||
|
<var name="rand_id" />
|
||||||
|
</setdynvars>
|
||||||
|
|
||||||
|
<!-- 1. Регистрация -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/register" method="POST" content_type="application/json"
|
||||||
|
contents='{"email": "loadtest_%%_rand_id%%@example.com", "password": "testpassword123"}'/>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="3000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 2. Логин (извлекаем токен) -->
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="token" re="(?:\{|,\s*)"token"\s*:\s*"([^"]+)"/>
|
||||||
|
<http url="/v1/login" method="POST" content_type="application/json"
|
||||||
|
contents='{"email": "loadtest_%%_rand_id%%@example.com", "password": "testpassword123"}'/>
|
||||||
|
</request>
|
||||||
|
<thinktime min="2000" max="5000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 3. Создание календаря (с авто‑подтверждением бронирований) -->
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="calendar_id" re="(?:\{|,\s*)"id"\s*:\s*"([^"]+)"/>
|
||||||
|
<http url="/v1/calendars" method="POST" content_type="application/json"
|
||||||
|
contents='{"title": "Tsung Calendar", "confirmation": "auto"}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="3000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 4. GET /v1/calendars/:id – конкретный календарь -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/calendars/%%_calendar_id%%" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="500" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 5. GET /v1/calendars/:id/view?month=2026-06 – HTML-представление -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/calendars/%%_calendar_id%%/view?month=2026-06" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 6. Создание события -->
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="event_id" re="(?:\{|,\s*)"id"\s*:\s*"([^"]+)"/>
|
||||||
|
<http url="/v1/calendars/%%_calendar_id%%/events" method="POST" content_type="application/json"
|
||||||
|
contents='{"title":"Tsung Event","start_time":"2027-01-01T10:00:00Z","duration":60}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="2000" max="4000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 7. GET /v1/events/:id – конкретное событие -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/events/%%_event_id%%" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="500" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 8. Запись на событие -->
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="booking_id" re="(?:\{|,\s*)"id"\s*:\s*"([^"]+)"/>
|
||||||
|
<http url="/v1/events/%%_event_id%%/bookings" method="POST" content_type="application/json"
|
||||||
|
contents='{}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 9. Подтверждение бронирования -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/bookings/%%_booking_id%%" method="PUT" content_type="application/json"
|
||||||
|
contents='{"action":"confirm"}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="3000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 10. GET /v1/bookings/:id – конкретное бронирование -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/bookings/%%_booking_id%%" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="500" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 11. Поиск -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/search?q=Tsung" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 12. Оставить отзыв (захватываем review_id) -->
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="review_id" re="(?:\{|,\s*)"id"\s*:\s*"([^"]+)"/>
|
||||||
|
<http url="/v1/reviews" method="POST" content_type="application/json"
|
||||||
|
contents='{"target_type":"event","target_id":"%%_event_id%%","rating":5,"comment":"Excellent!"}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 13. GET /v1/reviews/:id – конкретный отзыв -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/reviews/%%_review_id%%" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="500" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 14. Пожаловаться -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/reports" method="POST" content_type="application/json"
|
||||||
|
contents='{"target_type":"event","target_id":"%%_event_id%%","reason":"Spam"}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 15. Создать тикет (захватываем ticket_id) -->
|
||||||
|
<request subst="true">
|
||||||
|
<dyn_variable name="ticket_id" re="(?:\{|,\s*)"id"\s*:\s*"([^"]+)"/>
|
||||||
|
<http url="/v1/tickets" method="POST" content_type="application/json"
|
||||||
|
contents='{"error_message":"Error during load test","stacktrace":"line 42"}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 16. GET /v1/tickets/:id – конкретный тикет -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/tickets/%%_ticket_id%%" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="500" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 17. Активировать подписку -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/subscription" method="POST" content_type="application/json"
|
||||||
|
contents='{"action":"start_trial"}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="2000" max="5000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 18. GET /v1/subscription – получить подписку -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/subscription" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="3000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 19. GET /v1/user/me – профиль -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/user/me" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 20. GET /v1/user/bookings – свои бронирования -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/user/bookings" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 21. GET /v1/user/reviews – свои отзывы -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/user/reviews" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="2000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 22. GET /v1/calendars – список календарей -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/calendars" method="GET">
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="2000" max="5000" random="true"/>
|
||||||
|
|
||||||
|
<!-- 23. Обновление токена -->
|
||||||
|
<request subst="true">
|
||||||
|
<http url="/v1/refresh" method="POST" content_type="application/json"
|
||||||
|
contents='{"refresh_token":"dummy"}'>
|
||||||
|
<http_header name="Authorization" value="Bearer %%_token%%"/>
|
||||||
|
</http>
|
||||||
|
</request>
|
||||||
|
<thinktime min="1000" max="3000" random="true"/>
|
||||||
|
</session>
|
||||||
|
</sessions>
|
||||||
|
</tsung>
|
||||||
Reference in New Issue
Block a user