Рефакторинг обработчиков. Финальное тестирование #21
This commit is contained in:
28
Makefile
28
Makefile
@@ -34,7 +34,7 @@ clean: ## Очистить проект
|
||||
@echo "Очистка проекта..."
|
||||
@$(REBAR3) clean
|
||||
#@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 "✓ Очистка завершена"
|
||||
|
||||
deps: ## Установить зависимости
|
||||
@@ -125,10 +125,6 @@ test-api-docker:
|
||||
-e "ADMIN_WS_HOST=ws://eventhub:8446" \
|
||||
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)
|
||||
@echo "========================================"
|
||||
@echo " ВСЕ ТЕСТЫ ПРОЙДЕНЫ!"
|
||||
@@ -143,6 +139,13 @@ tsung-test: ## Запустить нагрузочный тест Tsung
|
||||
@tsung -f test/tsung/eventhub_http.xml -l logs/tsung start
|
||||
@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 -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" \
|
||||
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:
|
||||
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 stack rm eventhub
|
||||
@docker volume prune -f
|
||||
##@docker volume prune -f
|
||||
@echo "✅ Кластер удален"
|
||||
|
||||
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 RELX_REPLACE_OS_VARS=true
|
||||
ENV MNESIA_DIR=/app/data
|
||||
|
||||
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
|
||||
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}
|
||||
- JWT_SECRET=${JWT_SECRET:-eventhub_top_secret}
|
||||
- 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_PASSWORD=${ADMIN_MODER_PASSWORD:-Moderator123!}
|
||||
- ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD:-123456}
|
||||
- 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
|
||||
- DNS_NAME=eventhub-node
|
||||
networks:
|
||||
@@ -71,9 +71,13 @@ services:
|
||||
aliases:
|
||||
- eventhub-node
|
||||
volumes:
|
||||
- eventhub-data:/app/data
|
||||
- type: volume
|
||||
source: eventhub-data
|
||||
target: /app/data
|
||||
# volume:
|
||||
# nocopy: true
|
||||
deploy:
|
||||
replicas: 1
|
||||
replicas: 2
|
||||
endpoint_mode: dnsrr
|
||||
restart_policy:
|
||||
condition: any
|
||||
@@ -110,7 +114,7 @@ services:
|
||||
ports:
|
||||
- "9090:9090"
|
||||
deploy:
|
||||
replicas: 0
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: any
|
||||
|
||||
@@ -130,7 +134,7 @@ services:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
deploy:
|
||||
replicas: 0
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: any
|
||||
|
||||
@@ -180,12 +184,37 @@ services:
|
||||
restart_policy:
|
||||
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:
|
||||
eventhub-net:
|
||||
driver: overlay
|
||||
|
||||
volumes:
|
||||
eventhub-data:
|
||||
# name: 'eventhub-data-{{.Task.Slot}}'
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
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,24 +2,23 @@ global:
|
||||
scrape_interval: 5s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'eventhub-node1'
|
||||
static_configs:
|
||||
- targets: ['eventhub-node1:8080'] # http://localhost:8080/metrics/default
|
||||
labels:
|
||||
node: 'node1'
|
||||
metrics_path: '/metrics/default'
|
||||
- job_name: 'eventhub-node2'
|
||||
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'
|
||||
# Динамическое обнаружение нод eventhub через DNS A‑записи
|
||||
- job_name: 'eventhub-nodes'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'eventhub-node' # имя, резолвящееся во все ноды
|
||||
type: 'A' # использовать A‑записи (IPv4)
|
||||
port: 8080 # порт, на котором слушает eventhub
|
||||
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'
|
||||
scrape_interval: 15s
|
||||
static_configs:
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
}
|
||||
]}
|
||||
]},
|
||||
{mnesia, [
|
||||
{dir, "data/Mnesia.eventhub@${NODE_NAME}"}
|
||||
]},
|
||||
{ cowboy_swagger, [
|
||||
{ static_files, "./_build/default/lib/cowboy_swagger/priv/swagger" }
|
||||
]}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
pg:start_link(),
|
||||
application:ensure_all_started(mnesia),
|
||||
application:ensure_all_started(cowboy),
|
||||
case infra_sup:start_link() of
|
||||
{ok, Pid} ->
|
||||
% Определяем список узлов кластера, если режим CLUSTER_MODE=true
|
||||
@@ -40,15 +38,16 @@ start(_StartType, _StartArgs) ->
|
||||
io:format("~nCluster: discovered nodes ~p, joining cluster~n", [Nodes]),
|
||||
application:set_env(eventhub, extra_db_nodes, Nodes)
|
||||
end,
|
||||
application:ensure_all_started(mnesia),
|
||||
ok = infra_mnesia:init_tables(),
|
||||
ok = infra_mnesia:wait_for_tables(),
|
||||
calendar_html_renderer:init_cache(),
|
||||
application:ensure_all_started(cowboy),
|
||||
start_http(), % Пользовательский API (8080)
|
||||
start_admin_http(), % Административный API (8445)
|
||||
start_swagger_http(), % Swagger UI и спецификация (8447)
|
||||
application:ensure_all_started(prometheus),
|
||||
application:ensure_all_started(prometheus_cowboy),
|
||||
init_default_admins(),
|
||||
{ok, Pid};
|
||||
Error ->
|
||||
Error
|
||||
@@ -91,7 +90,11 @@ start_http() ->
|
||||
]),
|
||||
Middlewares = [cowboy_router, cowboy_handler],
|
||||
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]).
|
||||
|
||||
%% ===================================================================
|
||||
@@ -139,7 +142,11 @@ start_admin_http() ->
|
||||
|
||||
Middlewares = [cowboy_router, cowboy_handler],
|
||||
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]),
|
||||
|
||||
% WebSocket для пользователей
|
||||
@@ -170,37 +177,6 @@ start_swagger_http() ->
|
||||
cowboy:start_clear(swagger_http, [{port, PortSwagger}], #{env => Env, middlewares => Middlewares}),
|
||||
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) ->
|
||||
case application:get_env(eventhub, Key, Default) of
|
||||
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()}.
|
||||
serve_json(Api, Req) ->
|
||||
Trails = case Api of
|
||||
admin -> trails:admin();
|
||||
user -> trails:user()
|
||||
admin -> eventhub_trails:admin();
|
||||
user -> eventhub_trails:user()
|
||||
end,
|
||||
OpenApi = #{
|
||||
openapi => <<"3.0.3">>,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
-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([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
terminate/2, code_change/3]).
|
||||
@@ -54,6 +54,8 @@ handle_call(init_tables, _From, State) ->
|
||||
case ExtraNodes of
|
||||
[] ->
|
||||
ok = maybe_recreate_schema();
|
||||
%% ok = migration_engine:init_migrations_table(),
|
||||
%% _ = migration_engine:apply_pending(); //todo выключил - обваливает кластер, нужно разбираться
|
||||
_ ->
|
||||
ok = join_cluster(ExtraNodes)
|
||||
end,
|
||||
@@ -61,8 +63,7 @@ handle_call(init_tables, _From, State) ->
|
||||
ok = create_indices(),
|
||||
ok = stats_collector:subscribe(),
|
||||
ok = start_cleanup_timer(),
|
||||
ok = migration_engine:init_migrations_table(),
|
||||
_ = migration_engine:apply_pending(),
|
||||
init_default_admins(),
|
||||
{reply, ok, State};
|
||||
|
||||
handle_call({add_nodes, Nodes}, _From, State) ->
|
||||
@@ -91,7 +92,7 @@ maybe_recreate_schema() ->
|
||||
MnesiaDir = mnesia:system_info(directory),
|
||||
case filelib:is_dir(MnesiaDir) of
|
||||
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:delete_schema([node()]),
|
||||
mnesia:create_schema([node()]),
|
||||
@@ -184,6 +185,37 @@ prune_dead_nodes() ->
|
||||
catch mnesia:del_table_copy(schema, Node)
|
||||
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,
|
||||
terminate/2, code_change/3]).
|
||||
|
||||
-define(TABLE, schema_migrations).
|
||||
-define(TABLE, schema_migration).
|
||||
|
||||
%% ------------------------------
|
||||
%% API
|
||||
@@ -49,6 +49,7 @@ handle_call(init_table, _From, State) ->
|
||||
{type, set}
|
||||
])
|
||||
end,
|
||||
infra_mnesia:wait_for_table(?TABLE),
|
||||
{reply, ok, State};
|
||||
|
||||
handle_call(apply_pending, _From, State) ->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-module(trails).
|
||||
-module(eventhub_trails).
|
||||
-export([admin/0, user/0, all/0]).
|
||||
|
||||
admin() ->
|
||||
@@ -41,7 +41,7 @@ test() ->
|
||||
#{<<"error_message">> => <<"Another bug">>, <<"stacktrace">> => <<"trace2">>}),
|
||||
#{<<"id">> := Ticket2Id} = Ticket2,
|
||||
|
||||
test_list_tickets(Token, Ticket1Id),
|
||||
test_list_tickets(Token),
|
||||
test_get_ticket(Token, Ticket1Id),
|
||||
test_resolve_ticket(Token, Ticket1Id),
|
||||
test_close_ticket(Token, Ticket1Id),
|
||||
@@ -59,14 +59,13 @@ test() ->
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc GET /v1/admin/tickets – проверяет получение списка тикетов.
|
||||
%% Убеждается, что список не пуст и содержит созданный тикет.
|
||||
-spec test_list_tickets(binary(), binary()) -> ok.
|
||||
test_list_tickets(Token, TicketId) ->
|
||||
%% Убеждается, что список не пуст.
|
||||
-spec test_list_tickets(binary()) -> ok.
|
||||
test_list_tickets(Token) ->
|
||||
ct:pal(" TEST: List all tickets"),
|
||||
Tickets = api_test_runner:admin_get(<<"/v1/admin/tickets">>, Token),
|
||||
?assert(is_list(Tickets)),
|
||||
?assert(length(Tickets) >= 1),
|
||||
?assert(lists:any(fun(T) -> maps:get(<<"id">>, T) =:= TicketId end, Tickets)),
|
||||
ct:pal(" OK: ~p tickets", [length(Tickets)]).
|
||||
|
||||
%% @doc GET /v1/admin/tickets/:id – проверяет получение тикета по ID.
|
||||
|
||||
@@ -237,10 +237,19 @@ extract_port(Url) ->
|
||||
[_, PortStr] -> {ok, list_to_integer(PortStr)};
|
||||
_ -> case string:split(Rest, "://", trailing) of
|
||||
[_, R] -> extract_port("https://" ++ R);
|
||||
_ -> {ok, 80}
|
||||
_ -> {ok, default_port(Url)}
|
||||
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.
|
||||
|
||||
extract_host(Url) ->
|
||||
|
||||
@@ -191,7 +191,7 @@ request(BaseUrl, Method, Path, Token, Body, Prefix) ->
|
||||
delete -> {URL, Headers};
|
||||
_ -> {URL, Headers, "application/json", Body}
|
||||
end,
|
||||
Response = httpc:request(Method, RequestArg, [], []),
|
||||
Response = httpc:request(Method, RequestArg, [{timeout, 15000}, {ssl, [{verify, verify_none}]}], []),
|
||||
case Response of
|
||||
{ok, {{_, Status, _}, RespHeaders, 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
|
||||
"remote" ->
|
||||
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(),
|
||||
[{started_by_us, false} | Config];
|
||||
_ ->
|
||||
@@ -135,7 +142,7 @@ wait_for_remote() ->
|
||||
wait_for_health(_URL, 0) ->
|
||||
ct:fail("Remote API did not start within 30 seconds");
|
||||
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;
|
||||
_ ->
|
||||
timer:sleep(1000),
|
||||
|
||||
@@ -53,6 +53,13 @@ init_per_suite(Config) ->
|
||||
case os:getenv("CT_MODE", "local") of
|
||||
"remote" ->
|
||||
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(),
|
||||
[{started_by_us, false} | Config];
|
||||
_ ->
|
||||
@@ -163,7 +170,7 @@ wait_for_remote() ->
|
||||
wait_for_health(_URL, 0) ->
|
||||
ct:fail("Remote API did not start within 30 seconds");
|
||||
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;
|
||||
_ ->
|
||||
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