fix bot-emulator-users
This commit is contained in:
@@ -71,6 +71,7 @@ test() ->
|
||||
|
||||
% ── Фильтрация ──
|
||||
test_filter_users_by_role(Token),
|
||||
test_filter_users_by_role_bot(Token), % новый тест
|
||||
test_filter_users_by_status(Token),
|
||||
test_filter_combined_role_status(Token),
|
||||
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],
|
||||
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 Фильтрация по статусу.
|
||||
-spec test_filter_users_by_status(binary()) -> ok.
|
||||
test_filter_users_by_status(Token) ->
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Эмулятор пользователей для EventHub.
|
||||
|
||||
Скрипт устойчив к отсутствию API при старте и во время работы –
|
||||
никогда не падает, переходит в режим ожидания и автоматически
|
||||
восстанавливает работу после появления сервера.
|
||||
"""
|
||||
|
||||
import os, time, random, requests, logging, json
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Настройки (можно переопределять через переменные окружения)
|
||||
# -------------------------------------------------------------------
|
||||
DEBUG = os.getenv("DEBUG", "true").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"))
|
||||
LOOP_FOREVER = os.getenv("LOOP_FOREVER", "true").lower() == "true"
|
||||
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:
|
||||
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 = []
|
||||
# -------------------------------------------------------------------
|
||||
# Состояние
|
||||
# -------------------------------------------------------------------
|
||||
bots_cache = [] # [{"email": ..., "token": ...}]
|
||||
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}")
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Безопасный HTTP-запрос
|
||||
# -------------------------------------------------------------------
|
||||
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
|
||||
"""
|
||||
Абсолютно безопасный HTTP-запрос.
|
||||
Возвращает объект ответа или None при любой ошибке.
|
||||
"""
|
||||
try:
|
||||
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():
|
||||
global admin_token
|
||||
if admin_token:
|
||||
@@ -63,37 +80,52 @@ def get_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")
|
||||
headers={"Content-Type": "application/json"})
|
||||
if resp is None or resp.status_code != 200:
|
||||
logger.warning("Failed to obtain admin token")
|
||||
return None
|
||||
admin_token = resp.json().get("token")
|
||||
if admin_token:
|
||||
logger.info("Admin token obtained")
|
||||
return admin_token
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Получение списка email'ов ботов (с фильтром role=bot)
|
||||
# -------------------------------------------------------------------
|
||||
def fetch_bot_emails():
|
||||
token = get_admin_token()
|
||||
if not token:
|
||||
return []
|
||||
resp = request("GET",
|
||||
f"{ADMIN_API_HOST}/v1/admin/users?limit=10000",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
resp.raise_for_status()
|
||||
f"{ADMIN_API_HOST}/v1/admin/users?role=bot&limit=10000",
|
||||
headers={"Authorization": f"Bearer {token}"})
|
||||
if resp is None or resp.status_code != 200:
|
||||
logger.warning("Failed to fetch bots")
|
||||
return []
|
||||
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
|
||||
return [u["email"] for u in users if u.get("role") == "bot"]
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Логин одного бота
|
||||
# -------------------------------------------------------------------
|
||||
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()
|
||||
headers={"Content-Type": "application/json"})
|
||||
if resp is None or resp.status_code != 200:
|
||||
raise RuntimeError(f"Bot login failed for {email}")
|
||||
return resp.json()["token"]
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Обновление кеша ботов
|
||||
# -------------------------------------------------------------------
|
||||
def refresh_bot_cache():
|
||||
global bots_cache, last_bot_refresh
|
||||
emails = fetch_bot_emails()
|
||||
if not emails:
|
||||
logger.warning("No bot emails fetched, keeping existing cache")
|
||||
return
|
||||
new_cache = []
|
||||
for email in emails:
|
||||
try:
|
||||
@@ -101,10 +133,16 @@ def refresh_bot_cache():
|
||||
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")
|
||||
if new_cache:
|
||||
bots_cache = new_cache
|
||||
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():
|
||||
global bots_cache, last_bot_refresh
|
||||
while True:
|
||||
@@ -115,9 +153,15 @@ def random_bot():
|
||||
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"}
|
||||
@@ -125,13 +169,13 @@ def do_random_action(bot):
|
||||
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:
|
||||
if resp and 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():
|
||||
if resp_cal and 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},
|
||||
@@ -140,31 +184,34 @@ def do_random_action(bot):
|
||||
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 resp_ev and resp_ev.status_code == 200:
|
||||
results = resp_ev.json().get("results", {})
|
||||
events = 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():
|
||||
if resp_book and 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 resp_ev and resp_ev.status_code == 200:
|
||||
results = resp_ev.json().get("results", {})
|
||||
events = 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)
|
||||
if random.random() < TICKET_CHANCE:
|
||||
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:
|
||||
@@ -175,7 +222,7 @@ def do_random_action(bot):
|
||||
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():
|
||||
if resp_cal and 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:
|
||||
@@ -183,12 +230,26 @@ def do_random_action(bot):
|
||||
except Exception as e:
|
||||
logger.error(f"Action {action} failed for {bot['email']}: {e}")
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Главный цикл
|
||||
# -------------------------------------------------------------------
|
||||
def main():
|
||||
logger.info("Starting user emulation")
|
||||
refresh_bot_cache()
|
||||
logger.info(f"Starting user emulation (startup delay: {STARTUP_DELAY}s)")
|
||||
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:
|
||||
bot = random_bot()
|
||||
do_random_action(bot)
|
||||
try:
|
||||
bot = random_bot()
|
||||
do_random_action(bot)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in main loop: {e}")
|
||||
random_sleep()
|
||||
logger.info("Emulation finished")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user