fix bot-emulator-users

This commit is contained in:
2026-05-28 21:55:34 +03:00
parent d2483ada15
commit 732e7c81b4
5 changed files with 161 additions and 70 deletions
+1 -1
View File
@@ -13,4 +13,4 @@ docker build -t logrotate:latest -f docker/logrotate/Dockerfile docker/logrotate
# Admin UI – из соседней папки EventHubFrontAdmin # Admin UI – из соседней папки EventHubFrontAdmin
docker build -t eventhub-admin-ui:latest -f ../EventHubFrontAdmin/Dockerfile ../EventHubFrontAdmin docker build -t eventhub-admin-ui:latest -f ../EventHubFrontAdmin/Dockerfile ../EventHubFrontAdmin
docker build -t eventhub-emulator -f test/emulate_users/Dockerfile . docker build -t bot-emulator-users:latest -f test/emulate_users/Dockerfile .
+6 -2
View File
@@ -74,8 +74,6 @@ services:
- type: volume - type: volume
source: eventhub-data source: eventhub-data
target: /app/data target: /app/data
# volume:
# nocopy: true
deploy: deploy:
replicas: 1 replicas: 1
endpoint_mode: dnsrr endpoint_mode: dnsrr
@@ -89,6 +87,8 @@ services:
image: eventhub-admin-ui:latest image: eventhub-admin-ui:latest
networks: networks:
- eventhub-net - eventhub-net
depends_on:
- eventhub
deploy: deploy:
replicas: 1 replicas: 1
restart_policy: restart_policy:
@@ -196,9 +196,13 @@ services:
- MAX_DELAY=3.0 - MAX_DELAY=3.0
- LOOP_FOREVER=true - LOOP_FOREVER=true
- BOT_REFRESH_INTERVAL=300 - BOT_REFRESH_INTERVAL=300
- TICKET_CHANCE=0.07
- STARTUP_DELAY=30
- DEBUG=false - DEBUG=false
networks: networks:
- eventhub-net - eventhub-net
depends_on:
- eventhub
deploy: deploy:
mode: replicated mode: replicated
replicas: 1 replicas: 1
+10 -2
View File
@@ -101,8 +101,16 @@ list_users(Req) ->
-spec parse_user_filters(cowboy_req:req()) -> map(). -spec parse_user_filters(cowboy_req:req()) -> map().
parse_user_filters(Req) -> parse_user_filters(Req) ->
Qs = cowboy_req:parse_qs(Req), Qs = cowboy_req:parse_qs(Req),
RoleBin = proplists:get_value(<<"role">>, Qs),
StatusBin = proplists:get_value(<<"status">>, Qs),
#{ #{
role => proplists:get_value(<<"role">>, Qs), role => convert_to_atom(RoleBin),
status => proplists:get_value(<<"status">>, Qs), status => convert_to_atom(StatusBin),
q => proplists:get_value(<<"q">>, Qs) q => proplists:get_value(<<"q">>, Qs)
}. }.
convert_to_atom(undefined) -> undefined;
convert_to_atom(Bin) when is_binary(Bin) ->
try binary_to_existing_atom(Bin, utf8)
catch error:badarg -> Bin
end.
+18
View File
@@ -71,6 +71,7 @@ test() ->
% ── Фильтрация ── % ── Фильтрация ──
test_filter_users_by_role(Token), test_filter_users_by_role(Token),
test_filter_users_by_role_bot(Token), % новый тест
test_filter_users_by_status(Token), test_filter_users_by_status(Token),
test_filter_combined_role_status(Token), test_filter_combined_role_status(Token),
test_filter_users_by_search(Token, User2Email), test_filter_users_by_search(Token, User2Email),
@@ -149,6 +150,23 @@ test_filter_users_by_role(Token) ->
[?assertEqual(<<"bot">>, maps:get(<<"role">>, U)) || U <- Users], [?assertEqual(<<"bot">>, maps:get(<<"role">>, U)) || U <- Users],
ct:pal(" OK: ~p bot users", [length(Users)]). ct:pal(" OK: ~p bot users", [length(Users)]).
%% @doc Фильтрация по роли bot с отдельным пользователем (гарантирует наличие).
-spec test_filter_users_by_role_bot(binary()) -> ok.
test_filter_users_by_role_bot(Token) ->
ct:pal(" TEST: Filter users by role=bot (dedicated user)"),
% Создаём пользователя и сразу назначаем ему роль bot
Email = api_test_runner:unique_email(<<"botfilter">>),
UserTok = api_test_runner:register_and_login(Email, <<"testpass">>),
#{<<"id">> := BotId} = api_test_runner:client_get(<<"/v1/user/me">>, UserTok),
Path = <<"/v1/admin/users/", BotId/binary>>,
api_test_runner:admin_put(Path, Token, #{<<"role">> => <<"bot">>}),
% Теперь фильтр должен вернуть хотя бы этого пользователя
Users = api_test_runner:admin_get(<<"/v1/admin/users?role=bot">>, Token),
?assert(is_list(Users)),
?assert(length(Users) >= 1),
[?assertEqual(<<"bot">>, maps:get(<<"role">>, U)) || U <- Users],
ct:pal(" OK: ~p bot users", [length(Users)]).
%% @doc Фильтрация по статусу. %% @doc Фильтрация по статусу.
-spec test_filter_users_by_status(binary()) -> ok. -spec test_filter_users_by_status(binary()) -> ok.
test_filter_users_by_status(Token) -> test_filter_users_by_status(Token) ->
+125 -64
View File
@@ -1,6 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
Эмулятор пользователей для EventHub.
Скрипт устойчив к отсутствию API при старте и во время работы –
никогда не падает, переходит в режим ожидания и автоматически
восстанавливает работу после появления сервера.
"""
import os, time, random, requests, logging, json import os, time, random, requests, logging, json
# -------------------------------------------------------------------
# Настройки (можно переопределять через переменные окружения)
# -------------------------------------------------------------------
DEBUG = os.getenv("DEBUG", "true").lower() == "true" DEBUG = os.getenv("DEBUG", "true").lower() == "true"
VERIFY_SSL = os.getenv("VERIFY_SSL", "false").lower() == "true" VERIFY_SSL = os.getenv("VERIFY_SSL", "false").lower() == "true"
@@ -13,49 +24,55 @@ MIN_DELAY = float(os.getenv("MIN_DELAY", "0.5"))
MAX_DELAY = float(os.getenv("MAX_DELAY", "3.0")) MAX_DELAY = float(os.getenv("MAX_DELAY", "3.0"))
LOOP_FOREVER = os.getenv("LOOP_FOREVER", "true").lower() == "true" LOOP_FOREVER = os.getenv("LOOP_FOREVER", "true").lower() == "true"
BOT_REFRESH_INTERVAL = int(os.getenv("BOT_REFRESH_INTERVAL", "300")) BOT_REFRESH_INTERVAL = int(os.getenv("BOT_REFRESH_INTERVAL", "300"))
TICKET_CHANCE = float(os.getenv("TICKET_CHANCE", "0.07"))
STARTUP_DELAY = int(os.getenv("STARTUP_DELAY", "30"))
# -------------------------------------------------------------------
# Логирование
# -------------------------------------------------------------------
if not DEBUG: if not DEBUG:
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
else: else:
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s') logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger("emulator") logger = logging.getLogger("emulator")
bots_cache = [] # -------------------------------------------------------------------
# Состояние
# -------------------------------------------------------------------
bots_cache = [] # [{"email": ..., "token": ...}]
admin_token = None admin_token = None
last_bot_refresh = 0 last_bot_refresh = 0
def log_request(method, url, headers=None, json_data=None): # -------------------------------------------------------------------
if not DEBUG: # Безопасный HTTP-запрос
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): def request(method, url, **kwargs):
if not VERIFY_SSL: """
kwargs["verify"] = False Абсолютно безопасный HTTP-запрос.
log_request(method, url, headers=kwargs.get("headers"), json_data=kwargs.get("json")) Возвращает объект ответа или None при любой ошибке.
resp = requests.request(method, url, **kwargs) """
log_response(resp) try:
return resp if not VERIFY_SSL:
kwargs["verify"] = False
if DEBUG:
logger.debug(f"--> {method} {url}")
resp = requests.request(method, url, **kwargs)
if DEBUG:
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}")
return resp
except Exception as e:
logger.warning(f"Request failed ({method} {url}): {e}")
return None
# -------------------------------------------------------------------
# Получение административного токена
# -------------------------------------------------------------------
def get_admin_token(): def get_admin_token():
global admin_token global admin_token
if admin_token: if admin_token:
@@ -63,37 +80,52 @@ def get_admin_token():
resp = request("POST", resp = request("POST",
f"{ADMIN_API_HOST}/v1/admin/login", f"{ADMIN_API_HOST}/v1/admin/login",
json={"email": ADMIN_EMAIL, "password": ADMIN_PASSWORD}, json={"email": ADMIN_EMAIL, "password": ADMIN_PASSWORD},
headers={"Content-Type": "application/json"} headers={"Content-Type": "application/json"})
) if resp is None or resp.status_code != 200:
resp.raise_for_status() logger.warning("Failed to obtain admin token")
admin_token = resp.json()["token"] return None
logger.info("Admin token obtained") admin_token = resp.json().get("token")
if admin_token:
logger.info("Admin token obtained")
return admin_token return admin_token
# -------------------------------------------------------------------
# Получение списка email'ов ботов (с фильтром role=bot)
# -------------------------------------------------------------------
def fetch_bot_emails(): def fetch_bot_emails():
token = get_admin_token() token = get_admin_token()
if not token:
return []
resp = request("GET", resp = request("GET",
f"{ADMIN_API_HOST}/v1/admin/users?limit=10000", f"{ADMIN_API_HOST}/v1/admin/users?role=bot&limit=10000",
headers={"Authorization": f"Bearer {token}"} headers={"Authorization": f"Bearer {token}"})
) if resp is None or resp.status_code != 200:
resp.raise_for_status() logger.warning("Failed to fetch bots")
return []
users = resp.json() users = resp.json()
emails = [u["email"] for u in users if u.get("role") == "bot"] return [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): def login_bot(email):
resp = request("POST", resp = request("POST",
f"{CLIENT_API_HOST}/v1/login", f"{CLIENT_API_HOST}/v1/login",
json={"email": email, "password": BOT_PASSWORD}, json={"email": email, "password": BOT_PASSWORD},
headers={"Content-Type": "application/json"} headers={"Content-Type": "application/json"})
) if resp is None or resp.status_code != 200:
resp.raise_for_status() raise RuntimeError(f"Bot login failed for {email}")
return resp.json()["token"] return resp.json()["token"]
# -------------------------------------------------------------------
# Обновление кеша ботов
# -------------------------------------------------------------------
def refresh_bot_cache(): def refresh_bot_cache():
global bots_cache, last_bot_refresh global bots_cache, last_bot_refresh
emails = fetch_bot_emails() emails = fetch_bot_emails()
if not emails:
logger.warning("No bot emails fetched, keeping existing cache")
return
new_cache = [] new_cache = []
for email in emails: for email in emails:
try: try:
@@ -101,10 +133,16 @@ def refresh_bot_cache():
new_cache.append({"email": email, "token": token}) new_cache.append({"email": email, "token": token})
except Exception as e: except Exception as e:
logger.warning(f"Could not login bot {email}: {e}") logger.warning(f"Could not login bot {email}: {e}")
bots_cache = new_cache if new_cache:
last_bot_refresh = time.time() bots_cache = new_cache
logger.info(f"Bot cache refreshed, {len(bots_cache)} bots ready") last_bot_refresh = time.time()
logger.info(f"Bot cache refreshed, {len(bots_cache)} bots ready")
else:
logger.warning("No bots could be logged in, keeping existing cache")
# -------------------------------------------------------------------
# Получение случайного бота (с автоматическим обновлением кеша)
# -------------------------------------------------------------------
def random_bot(): def random_bot():
global bots_cache, last_bot_refresh global bots_cache, last_bot_refresh
while True: while True:
@@ -115,9 +153,15 @@ def random_bot():
logger.warning("No bots available, retrying in 10 seconds...") logger.warning("No bots available, retrying in 10 seconds...")
time.sleep(10) time.sleep(10)
# -------------------------------------------------------------------
# Случайная задержка между действиями
# -------------------------------------------------------------------
def random_sleep(): def random_sleep():
time.sleep(random.uniform(MIN_DELAY, MAX_DELAY)) time.sleep(random.uniform(MIN_DELAY, MAX_DELAY))
# -------------------------------------------------------------------
# Выполнение случайного действия от имени бота
# -------------------------------------------------------------------
def do_random_action(bot): def do_random_action(bot):
action = random.randint(1, 14) action = random.randint(1, 14)
headers = {"Authorization": f"Bearer {bot['token']}", "Content-Type": "application/json"} headers = {"Authorization": f"Bearer {bot['token']}", "Content-Type": "application/json"}
@@ -125,13 +169,13 @@ def do_random_action(bot):
try: try:
if action == 1: if action == 1:
resp = request("POST", f"{base}/v1/calendars", json={"title": f"Cal-{random.randint(1,1000)}", "confirmation": "auto"}, headers=headers) resp = request("POST", f"{base}/v1/calendars", json={"title": f"Cal-{random.randint(1,1000)}", "confirmation": "auto"}, headers=headers)
if resp.status_code == 201: if resp and resp.status_code == 201:
logger.debug(f"Bot {bot['email']} created calendar {resp.json()['id']}") logger.debug(f"Bot {bot['email']} created calendar {resp.json()['id']}")
elif action == 2: elif action == 2:
request("GET", f"{base}/v1/calendars", headers=headers) request("GET", f"{base}/v1/calendars", headers=headers)
elif action == 3: elif action == 3:
resp_cal = request("GET", f"{base}/v1/calendars", headers=headers) resp_cal = request("GET", f"{base}/v1/calendars", headers=headers)
if resp_cal.status_code == 200 and resp_cal.json(): if resp_cal and resp_cal.status_code == 200 and resp_cal.json():
cal = random.choice(resp_cal.json()) cal = random.choice(resp_cal.json())
request("POST", f"{base}/v1/calendars/{cal['id']}/events", 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}, json={"title": f"Event-{random.randint(1,1000)}", "start_time": "2027-01-01T10:00:00Z", "duration": 60},
@@ -140,31 +184,34 @@ def do_random_action(bot):
request("GET", f"{base}/v1/search?q=test&limit=5", headers=headers) request("GET", f"{base}/v1/search?q=test&limit=5", headers=headers)
elif action == 5: elif action == 5:
resp_ev = request("GET", f"{base}/v1/search?type=event&limit=20", headers=headers) 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"): if resp_ev and resp_ev.status_code == 200:
events = resp_ev.json()["results"].get("events", []) results = resp_ev.json().get("results", {})
events = results.get("events", [])
if events: if events:
ev = random.choice(events) ev = random.choice(events)
request("POST", f"{base}/v1/events/{ev['id']}/bookings", json={}, headers=headers) request("POST", f"{base}/v1/events/{ev['id']}/bookings", json={}, headers=headers)
elif action == 6: elif action == 6:
resp_book = request("GET", f"{base}/v1/user/bookings", headers=headers) resp_book = request("GET", f"{base}/v1/user/bookings", headers=headers)
if resp_book.status_code == 200 and resp_book.json(): if resp_book and resp_book.status_code == 200 and resp_book.json():
booking = random.choice(resp_book.json()) booking = random.choice(resp_book.json())
request("POST", f"{base}/v1/reviews", request("POST", f"{base}/v1/reviews",
json={"target_type": "event", "target_id": booking["event_id"], "rating": random.randint(1,5), "comment": "Nice!"}, json={"target_type": "event", "target_id": booking["event_id"], "rating": random.randint(1,5), "comment": "Nice!"},
headers=headers) headers=headers)
elif action == 7: elif action == 7:
resp_ev = request("GET", f"{base}/v1/search?type=event&limit=20", headers=headers) 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"): if resp_ev and resp_ev.status_code == 200:
events = resp_ev.json()["results"].get("events", []) results = resp_ev.json().get("results", {})
events = results.get("events", [])
if events: if events:
ev = random.choice(events) ev = random.choice(events)
request("POST", f"{base}/v1/reports", request("POST", f"{base}/v1/reports",
json={"target_type": "event", "target_id": ev["id"], "reason": "Test"}, json={"target_type": "event", "target_id": ev["id"], "reason": "Test"},
headers=headers) headers=headers)
elif action == 8: elif action == 8:
request("POST", f"{base}/v1/tickets", if random.random() < TICKET_CHANCE:
json={"error_message": "Emulated error", "stacktrace": "line 1"}, request("POST", f"{base}/v1/tickets",
headers=headers) json={"error_message": "Emulated error", "stacktrace": "line 1"},
headers=headers)
elif action == 9: elif action == 9:
request("POST", f"{base}/v1/subscription", json={"action": "start_trial"}, headers=headers) request("POST", f"{base}/v1/subscription", json={"action": "start_trial"}, headers=headers)
elif action == 10: elif action == 10:
@@ -175,7 +222,7 @@ def do_random_action(bot):
request("GET", f"{base}/v1/user/reviews", headers=headers) request("GET", f"{base}/v1/user/reviews", headers=headers)
elif action == 13: elif action == 13:
resp_cal = request("GET", f"{base}/v1/calendars", headers=headers) resp_cal = request("GET", f"{base}/v1/calendars", headers=headers)
if resp_cal.status_code == 200 and resp_cal.json(): if resp_cal and resp_cal.status_code == 200 and resp_cal.json():
cal = random.choice(resp_cal.json()) cal = random.choice(resp_cal.json())
request("GET", f"{base}/v1/calendars/{cal['id']}/view?month=2026-06", headers=headers) request("GET", f"{base}/v1/calendars/{cal['id']}/view?month=2026-06", headers=headers)
elif action == 14: elif action == 14:
@@ -183,12 +230,26 @@ def do_random_action(bot):
except Exception as e: except Exception as e:
logger.error(f"Action {action} failed for {bot['email']}: {e}") logger.error(f"Action {action} failed for {bot['email']}: {e}")
# -------------------------------------------------------------------
# Главный цикл
# -------------------------------------------------------------------
def main(): def main():
logger.info("Starting user emulation") logger.info(f"Starting user emulation (startup delay: {STARTUP_DELAY}s)")
refresh_bot_cache() time.sleep(STARTUP_DELAY)
# Бесконечно ждём, пока не появятся боты
while True:
refresh_bot_cache()
if bots_cache:
break
logger.info("Waiting 10 seconds before retry...")
time.sleep(10)
while LOOP_FOREVER: while LOOP_FOREVER:
bot = random_bot() try:
do_random_action(bot) bot = random_bot()
do_random_action(bot)
except Exception as e:
logger.error(f"Unexpected error in main loop: {e}")
random_sleep() random_sleep()
logger.info("Emulation finished") logger.info("Emulation finished")