fix(loadtest): verify через admin verification-token, убрать token из register. Refs EventHub/EventHubBack#30

This commit is contained in:
2026-07-15 00:12:32 +03:00
parent e8b90606c3
commit 6bba96c36e
6 changed files with 125 additions and 47 deletions
+3
View File
@@ -30,6 +30,9 @@ docker/.env.stage
/Mnesia.*/ /Mnesia.*/
/doc/ /doc/
# Локальные учётки admin для Tsung IFT (см. test/tsung/ift-admin.csv.example)
/test/tsung/ift-admin.csv
# Traefik TLS private keys (generated locally for dev/staging). # Traefik TLS private keys (generated locally for dev/staging).
# Никогда не коммитим приватный ключ в репозиторий. # Никогда не коммитим приватный ключ в репозиторий.
docker/traefik/certs/*.key docker/traefik/certs/*.key
+3 -12
View File
@@ -78,23 +78,14 @@ register(Req) ->
% Создаём верификационный токен и отправляем письмо % Создаём верификационный токен и отправляем письмо
{ok, VerificationToken, _Expires} = core_verification:create_token(User#user.id), {ok, VerificationToken, _Expires} = core_verification:create_token(User#user.id),
logic_email:send_verification_email(Email, VerificationToken), logic_email:send_verification_email(Email, VerificationToken),
BaseUser = #{ Response = #{
user => #{
id => User#user.id, id => User#user.id,
email => User#user.email, email => User#user.email,
role => atom_to_binary(User#user.role, utf8), role => atom_to_binary(User#user.role, utf8),
status => atom_to_binary(User#user.status, utf8) % <<"pending">> status => atom_to_binary(User#user.status, utf8) % <<"pending">>
},
%% На стендах (не prod) отдаём verification_token для loadtest/E2E
%% без доступа к почте (IFT EVENTHUB_ENV=stage).
Response = case os:getenv("EVENTHUB_ENV") of
"prod" ->
#{user => BaseUser};
_ ->
#{
user => BaseUser,
verification_token => VerificationToken
} }
end, },
handler_utils:send_json(Req1, 201, Response); handler_utils:send_json(Req1, 201, Response);
{error, email_exists} -> {error, email_exists} ->
handler_utils:send_error(Req1, 409, <<"Email already exists">>); handler_utils:send_error(Req1, 409, <<"Email already exists">>);
+33 -23
View File
@@ -6,42 +6,53 @@
|------|------------| |------|------------|
| `eventhub_ift_smoke.xml` | Короткий smoke на IFT после prepare | | `eventhub_ift_smoke.xml` | Короткий smoke на IFT после prepare |
| `eventhub_ift.xml` | Mix: `auth_browse` 60% + `create_flow` 40%, фазы warmup/ramp/peak | | `eventhub_ift.xml` | Mix: `auth_browse` 60% + `create_flow` 40%, фазы warmup/ramp/peak |
| `eventhub_tsung.xml` | Legacy localhost (thinktime там в секундах — не использовать для IFT) | | `eventhub_tsung.xml` | Legacy localhost (не для IFT) |
| `eventhub_http.xml` | Legacy короткий localhost | | `eventhub_http.xml` | Legacy короткий localhost |
Цель IFT: **HTTPS** `api.ift.eventhub.local:443` (Traefik). Цель: **HTTPS** `api.ift.eventhub.local` (+ `admin-api.ift.eventhub.local` для verify).
Auth path: `POST /v1/register``POST /v1/verify``POST /v1/login`. ### Auth path
На стендах (`EVENTHUB_ENV``prod`) register отдаёт `verification_token` (без почты).
1. `POST /v1/admin/login` (admin-api) — учётка из `ift-admin.csv`
2. `POST /v1/register``user.id`
3. `GET /v1/admin/users/:id/verification-token` — спец. admin-метод
4. `POST /v1/verify`
5. `POST /v1/login` → user JWT
## Подготовка учётки admin
```bash
# из /opt/eventhub-ift/.env или локальной копии
cp test/tsung/ift-admin.csv.example test/tsung/ift-admin.csv
# одна строка без комментариев: email;password
printf '%s;%s\n' "$ADMIN_SUPER_EMAIL" "$ADMIN_SUPER_PASSWORD" > test/tsung/ift-admin.csv
```
`ift-admin.csv` в `.gitignore`.
## Перед прогоном (хост IFT) ## Перед прогоном (хост IFT)
```bash ```bash
# 1 нода (default)
EVENTHUB_REPLICAS=1 bash /opt/eventhub-ift/devops/scripts/ift-loadtest-prepare.sh EVENTHUB_REPLICAS=1 bash /opt/eventhub-ift/devops/scripts/ift-loadtest-prepare.sh
# EVENTHUB_REPLICAS=N — больше нод
# или N нод
EVENTHUB_REPLICAS=2 bash /opt/eventhub-ift/devops/scripts/ift-loadtest-prepare.sh
``` ```
Оставляет `traefik` + `eventhub×N`, гасит admin-ui / bot / observability / fallback. Оставляет `traefik` + `eventhub×N`.
## Дех-машина (WSL) ## Дех-машина (WSL)
1. DNS: `api.ift.eventhub.local` → IP IFT (тот же, что в OpenWRT). 1. DNS: `api.ift.eventhub.local` и `admin-api.ift.eventhub.local` → IP IFT.
2. Установка: `sudo apt install tsung` (или [документация Tsung](http://tsung.erlang-projects.org/)). 2. `sudo apt install tsung`
3. TLS: самоподписанный сертификат стенда — импортировать CA из `EventHubDevOps/ift/traefik/certs` в trust store WSL, либо временно положить CA в `/usr/local/share/ca-certificates/` и `update-ca-certificates`. Проверка: `curl -sf https://api.ift.eventhub.local/health`. 3. TLS: доверить CA стенда (`EventHubDevOps/ift/traefik/certs`). Проверка:
4. Запуск из корня `EventHubBack`: `curl -sf https://api.ift.eventhub.local/health`
4. Из корня `EventHubBack`:
```bash ```bash
# smoke (~1 мин)
tsung -f test/tsung/eventhub_ift_smoke.xml start tsung -f test/tsung/eventhub_ift_smoke.xml start
# полный прогон (~6 мин фаз)
tsung -f test/tsung/eventhub_ift.xml start tsung -f test/tsung/eventhub_ift.xml start
``` ```
Отчёт: `~/.tsung/log/<timestamp>/``tsung_stats.pl` / HTML. Отчёт: `~/.tsung/log/<timestamp>/`.
## После прогона ## После прогона
@@ -51,11 +62,10 @@ bash /opt/eventhub-ift/devops/scripts/ift-loadtest-restore.sh
## Настройка интенсивности ## Настройка интенсивности
В `eventhub_ift.xml` блок `<load>`: `interarrival` (сек между стартами пользователей) и `duration` фаз. В `eventhub_ift.xml` блок `<load>`: `interarrival` и `duration` фаз.
`thinktime`**секунды** (не мс). `thinktime`**секунды**.
## Что сознательно убрано из hot path ## Замечания
- dummy `/v1/refresh` - Каждый VU делает admin login — это нагрузка и на admin-api; при необходимости позже вынести общий admin JWT в csv.
- reports / tickets (шум модерации) - Из hot path убраны: dummy refresh, reports/tickets, завышенные thinktime старого `eventhub_tsung.xml`.
- завышенные thinktime (старый `eventhub_tsung.xml`)
+51 -4
View File
@@ -2,8 +2,8 @@
<!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd"> <!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd">
<!-- <!--
IFT load mix: auth_browse 60% + create_flow 40%. IFT load mix: auth_browse 60% + create_flow 40%.
Auth: register → verify (verification_token на стендах) → login. Verify через admin GET /v1/admin/users/:id/verification-token.
Перед прогоном: EVENTHUB_REPLICAS=1 bash …/ift-loadtest-prepare.sh Нужен test/tsung/ift-admin.csv (см. ift-admin.csv.example).
thinktime — секунды. thinktime — секунды.
--> -->
<tsung loglevel="notice" version="1.0"> <tsung loglevel="notice" version="1.0">
@@ -29,22 +29,46 @@
<options> <options>
<option name="ssl_reuse" value="true"/> <option name="ssl_reuse" value="true"/>
<option name="file_server" id="admincred" value="test/tsung/ift-admin.csv"/>
</options> </options>
<sessions> <sessions>
<session name="auth_browse" probability="60" type="ts_http"> <session name="auth_browse" probability="60" type="ts_http">
<setdynvars sourcetype="file" fileid="admincred" delimiter=";" order="iter">
<var name="admin_email"/>
<var name="admin_password"/>
</setdynvars>
<setdynvars sourcetype="random_number" start="100000" end="9999999"> <setdynvars sourcetype="random_number" start="100000" end="9999999">
<var name="rand_id"/> <var name="rand_id"/>
</setdynvars> </setdynvars>
<request subst="true"> <request subst="true">
<dyn_variable name="vtoken" re="&quot;verification_token&quot;\s*:\s*&quot;([^&quot;]+)"/> <dyn_variable name="admin_token" re="&quot;token&quot;\s*:\s*&quot;([^&quot;]+)"/>
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="https://admin-api.ift.eventhub.local/v1/admin/login" method="POST"
content_type="application/json"
contents='{"email":"%%_admin_email%%","password":"%%_admin_password%%"}'/>
</request>
<thinktime min="1" max="2" random="true"/>
<request subst="true">
<dyn_variable name="user_id" re="&quot;id&quot;\s*:\s*&quot;([^&quot;]+)"/>
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match> <match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="/v1/register" method="POST" content_type="application/json" <http url="/v1/register" method="POST" content_type="application/json"
contents='{"email":"lt_b_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/> contents='{"email":"lt_b_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/>
</request> </request>
<thinktime min="1" max="2" random="true"/> <thinktime min="1" max="2" random="true"/>
<request subst="true">
<dyn_variable name="vtoken" re="&quot;token&quot;\s*:\s*&quot;([^&quot;]+)"/>
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="https://admin-api.ift.eventhub.local/v1/admin/users/%%_user_id%%/verification-token"
method="GET">
<http_header name="Authorization" value="Bearer %%_admin_token%%"/>
</http>
</request>
<thinktime value="1"/>
<request subst="true"> <request subst="true">
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match> <match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="/v1/verify" method="POST" content_type="application/json" <http url="/v1/verify" method="POST" content_type="application/json"
@@ -93,6 +117,10 @@
</session> </session>
<session name="create_flow" probability="40" type="ts_http"> <session name="create_flow" probability="40" type="ts_http">
<setdynvars sourcetype="file" fileid="admincred" delimiter=";" order="iter">
<var name="admin_email"/>
<var name="admin_password"/>
</setdynvars>
<setdynvars sourcetype="random_number" start="100000" end="9999999"> <setdynvars sourcetype="random_number" start="100000" end="9999999">
<var name="rand_id"/> <var name="rand_id"/>
</setdynvars> </setdynvars>
@@ -104,13 +132,32 @@
</setdynvars> </setdynvars>
<request subst="true"> <request subst="true">
<dyn_variable name="vtoken" re="&quot;verification_token&quot;\s*:\s*&quot;([^&quot;]+)"/> <dyn_variable name="admin_token" re="&quot;token&quot;\s*:\s*&quot;([^&quot;]+)"/>
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="https://admin-api.ift.eventhub.local/v1/admin/login" method="POST"
content_type="application/json"
contents='{"email":"%%_admin_email%%","password":"%%_admin_password%%"}'/>
</request>
<thinktime min="1" max="2" random="true"/>
<request subst="true">
<dyn_variable name="user_id" re="&quot;id&quot;\s*:\s*&quot;([^&quot;]+)"/>
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match> <match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="/v1/register" method="POST" content_type="application/json" <http url="/v1/register" method="POST" content_type="application/json"
contents='{"email":"lt_c_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/> contents='{"email":"lt_c_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/>
</request> </request>
<thinktime min="1" max="2" random="true"/> <thinktime min="1" max="2" random="true"/>
<request subst="true">
<dyn_variable name="vtoken" re="&quot;token&quot;\s*:\s*&quot;([^&quot;]+)"/>
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="https://admin-api.ift.eventhub.local/v1/admin/users/%%_user_id%%/verification-token"
method="GET">
<http_header name="Authorization" value="Bearer %%_admin_token%%"/>
</http>
</request>
<thinktime value="1"/>
<request subst="true"> <request subst="true">
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match> <match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="/v1/verify" method="POST" content_type="application/json" <http url="/v1/verify" method="POST" content_type="application/json"
+30 -4
View File
@@ -1,9 +1,9 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd"> <!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd">
<!-- <!--
IFT smoke: register → verify → login → calendar/event. IFT smoke: register → admin verification-token → verify → login → calendar/event.
Требует EVENTHUB_ENV≠prod (в ответе register есть verification_token). Нужен test/tsung/ift-admin.csv (см. ift-admin.csv.example).
Запуск: tsung -f test/tsung/eventhub_ift_smoke.xml start Запуск из корня EventHubBack: tsung -f test/tsung/eventhub_ift_smoke.xml start
--> -->
<tsung loglevel="notice" version="1.0"> <tsung loglevel="notice" version="1.0">
<clients> <clients>
@@ -22,10 +22,15 @@
<options> <options>
<option name="ssl_reuse" value="true"/> <option name="ssl_reuse" value="true"/>
<option name="file_server" id="admincred" value="test/tsung/ift-admin.csv"/>
</options> </options>
<sessions> <sessions>
<session name="ift_smoke" probability="100" type="ts_http"> <session name="ift_smoke" probability="100" type="ts_http">
<setdynvars sourcetype="file" fileid="admincred" delimiter=";" order="iter">
<var name="admin_email"/>
<var name="admin_password"/>
</setdynvars>
<setdynvars sourcetype="random_number" start="100000" end="9999999"> <setdynvars sourcetype="random_number" start="100000" end="9999999">
<var name="rand_id"/> <var name="rand_id"/>
</setdynvars> </setdynvars>
@@ -33,14 +38,35 @@
<var name="sec"/> <var name="sec"/>
</setdynvars> </setdynvars>
<!-- admin login (admin-api host) -->
<request subst="true"> <request subst="true">
<dyn_variable name="vtoken" re="&quot;verification_token&quot;\s*:\s*&quot;([^&quot;]+)"/> <dyn_variable name="admin_token" re="&quot;token&quot;\s*:\s*&quot;([^&quot;]+)"/>
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="https://admin-api.ift.eventhub.local/v1/admin/login" method="POST"
content_type="application/json"
contents='{"email":"%%_admin_email%%","password":"%%_admin_password%%"}'/>
</request>
<thinktime value="1"/>
<request subst="true">
<dyn_variable name="user_id" re="&quot;id&quot;\s*:\s*&quot;([^&quot;]+)"/>
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match> <match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="/v1/register" method="POST" content_type="application/json" <http url="/v1/register" method="POST" content_type="application/json"
contents='{"email":"lt_smoke_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/> contents='{"email":"lt_smoke_%%_rand_id%%@loadtest.local","password":"testpassword123"}'/>
</request> </request>
<thinktime value="1"/> <thinktime value="1"/>
<!-- GET /v1/admin/users/:id/verification-token -->
<request subst="true">
<dyn_variable name="vtoken" re="&quot;token&quot;\s*:\s*&quot;([^&quot;]+)"/>
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="https://admin-api.ift.eventhub.local/v1/admin/users/%%_user_id%%/verification-token"
method="GET">
<http_header name="Authorization" value="Bearer %%_admin_token%%"/>
</http>
</request>
<thinktime value="1"/>
<request subst="true"> <request subst="true">
<match do="abort" when="nomatch">HTTP/1.[0-1] 20</match> <match do="abort" when="nomatch">HTTP/1.[0-1] 20</match>
<http url="/v1/verify" method="POST" content_type="application/json" <http url="/v1/verify" method="POST" content_type="application/json"
+1
View File
@@ -0,0 +1 @@
admin@example.com;change-me