231 lines
5.5 KiB
Markdown
231 lines
5.5 KiB
Markdown
# Отправка СМС
|
||
|
||
## На ВМ выполните:
|
||
|
||
cd /opt/han-chat/backend
|
||
umask 077
|
||
|
||
read -r -p "Тестовый номер в E.164 (+79...): " TEST_PHONE
|
||
|
||
CHALLENGE_ID=$(python3 -c 'import uuid; print(uuid.uuid4())')
|
||
OTP_CODE=$(python3 -c 'import secrets; print(f"{secrets.randbelow(1000000):06d}")')
|
||
|
||
SMS_TOKEN=$(python3 - <<'PY'
|
||
from pathlib import Path
|
||
|
||
for line in Path(".env").read_text().splitlines():
|
||
if line.startswith("SMS_SERVICE_TOKEN="):
|
||
print(line.split("=", 1)[1].strip().strip("\"'"))
|
||
break
|
||
else:
|
||
raise SystemExit("SMS_SERVICE_TOKEN отсутствует")
|
||
PY
|
||
)
|
||
|
||
export TEST_PHONE CHALLENGE_ID OTP_CODE SMS_TOKEN
|
||
REQUEST_FILE=$(mktemp)
|
||
|
||
python3 - "$REQUEST_FILE" <<'PY'
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
payload = {
|
||
"idempotency_key": f"ops:smoke:{os.environ['CHALLENGE_ID']}",
|
||
"template_code": "auth_otp",
|
||
"locale": "ru",
|
||
"phone_e164": os.environ["TEST_PHONE"],
|
||
"substitutions": {
|
||
"code": os.environ["OTP_CODE"],
|
||
"ttl_min": "1",
|
||
},
|
||
"customer_ref": os.environ["CHALLENGE_ID"],
|
||
"message_ttl_sec": 60,
|
||
}
|
||
|
||
with open(sys.argv[1], "w", encoding="utf-8") as file:
|
||
json.dump(payload, file, ensure_ascii=False)
|
||
PY
|
||
|
||
|
||
## Создайте функцию отправки:
|
||
|
||
send_sms_smoke() {
|
||
docker compose --env-file .env --profile ops run --rm --no-deps \
|
||
--user 0:0 \
|
||
--entrypoint sh \
|
||
-e SMS_TOKEN \
|
||
-v "$REQUEST_FILE:/tmp/sms-request.json:ro" \
|
||
toolbox -ec '
|
||
curl -sS \
|
||
-w "\nHTTP %{http_code}\n" \
|
||
-X POST \
|
||
-H "Authorization: Bearer $SMS_TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-H "X-Request-ID: ops-sms-smoke" \
|
||
--data-binary @/tmp/sms-request.json \
|
||
http://sms-service:8080/internal/sms/v1/send
|
||
'
|
||
}
|
||
|
||
## Отправка:
|
||
send_sms_smoke
|
||
|
||
Ожидается:
|
||
HTTP 202 и JSON с sms_message_id.
|
||
|
||
## Проверьте журнал:
|
||
SELECT
|
||
id,
|
||
phone_masked,
|
||
send_status,
|
||
delivery_status,
|
||
provider_message_id,
|
||
provider_error_code,
|
||
attempt_count,
|
||
created_at
|
||
FROM sms.sms_outbound_message
|
||
ORDER BY created_at DESC
|
||
LIMIT 5;
|
||
|
||
## После проверки удалите секретные данные:
|
||
shred -u "$REQUEST_FILE" 2>/dev/null || rm -f "$REQUEST_FILE"
|
||
unset SMS_TOKEN OTP_CODE TEST_PHONE CHALLENGE_ID REQUEST_FILE
|
||
|
||
# Тесты
|
||
|
||
Выполняйте на ВМ из `/opt/han-chat/backend`.
|
||
|
||
### 1. Проверить запрет публичного internal API
|
||
|
||
```bash
|
||
PUBLIC_WEB_URL=$(python3 - <<'PY'
|
||
from pathlib import Path
|
||
for line in Path(".env").read_text().splitlines():
|
||
if line.startswith("PUBLIC_WEB_URL="):
|
||
print(line.split("=", 1)[1].strip().strip("\"'"))
|
||
break
|
||
PY
|
||
)
|
||
|
||
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
|
||
"$PUBLIC_WEB_URL/internal/sms/v1/messages/00000000-0000-0000-0000-000000000000"
|
||
```
|
||
|
||
Ожидается:
|
||
|
||
```text
|
||
HTTP 404
|
||
```
|
||
|
||
### 2. Проверить callback с неправильного IP
|
||
|
||
```bash
|
||
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
|
||
-X POST \
|
||
-H 'Content-Type: application/json' \
|
||
--data '[]' \
|
||
"$PUBLIC_WEB_URL/callbacks/idgtl/sms"
|
||
```
|
||
|
||
Ожидается:
|
||
|
||
```text
|
||
HTTP 403
|
||
```
|
||
|
||
Заголовок `X-Forwarded-For` не должен позволять обойти ограничение.
|
||
|
||
### 3. Проверить Basic auth внутри Docker-сети
|
||
|
||
Получите credentials из `.env`:
|
||
|
||
```bash
|
||
CB_USER=$(python3 - <<'PY'
|
||
from pathlib import Path
|
||
for line in Path(".env").read_text().splitlines():
|
||
if line.startswith("IDGTL_SMS_CALLBACK_USERNAME="):
|
||
print(line.split("=", 1)[1].strip().strip("\"'"))
|
||
break
|
||
PY
|
||
)
|
||
|
||
CB_PASS=$(python3 - <<'PY'
|
||
from pathlib import Path
|
||
for line in Path(".env").read_text().splitlines():
|
||
if line.startswith("IDGTL_SMS_CALLBACK_PASSWORD="):
|
||
print(line.split("=", 1)[1].strip().strip("\"'"))
|
||
break
|
||
PY
|
||
)
|
||
|
||
export CB_USER CB_PASS
|
||
```
|
||
|
||
Неверные credentials:
|
||
|
||
```bash
|
||
docker compose --env-file .env --profile ops run --rm --no-deps \
|
||
--entrypoint sh toolbox -ec '
|
||
curl -sS -o /dev/null -w "HTTP %{http_code}\n" \
|
||
-u invalid:invalid \
|
||
-H "Content-Type: application/json" \
|
||
--data "[]" \
|
||
http://sms-service:8080/callbacks/idgtl/sms
|
||
'
|
||
```
|
||
|
||
Ожидается `HTTP 401`.
|
||
|
||
Правильные credentials:
|
||
|
||
```bash
|
||
docker compose --env-file .env --profile ops run --rm --no-deps \
|
||
--entrypoint sh -e CB_USER -e CB_PASS toolbox -ec '
|
||
curl -sS -o /dev/null -w "HTTP %{http_code}\n" \
|
||
-u "$CB_USER:$CB_PASS" \
|
||
-H "Content-Type: application/json" \
|
||
--data "[]" \
|
||
http://sms-service:8080/callbacks/idgtl/sms
|
||
'
|
||
```
|
||
|
||
Ожидается `HTTP 422`: авторизация прошла, но пустой callback-массив невалиден.
|
||
|
||
После проверки:
|
||
|
||
```bash
|
||
unset CB_USER CB_PASS
|
||
```
|
||
|
||
### 4. Проверить реальный callback Direct
|
||
|
||
После тестовой SMS:
|
||
|
||
```sql
|
||
SELECT
|
||
id,
|
||
send_status,
|
||
delivery_status,
|
||
provider_message_id,
|
||
callback_last_at,
|
||
sent_at,
|
||
delivered_at
|
||
FROM sms.sms_outbound_message
|
||
ORDER BY created_at DESC
|
||
LIMIT 5;
|
||
```
|
||
|
||
Успешный реальный callback подтверждается:
|
||
|
||
- `callback_last_at IS NOT NULL`;
|
||
- `delivery_status = sent` или `delivered`;
|
||
- заполняются `sent_at`/`delivered_at`.
|
||
|
||
Дополнительно:
|
||
|
||
```bash
|
||
docker compose --env-file .env logs --since=30m nginx sms-service
|
||
```
|
||
|
||
Для callback должен быть ответ `204`. Только реальный запрос Direct может полноценно подтвердить IP allowlist. |