Files
glchat/deploy/smoke.sh
T
grendervill 463021161a test(smoke): проверки API Фазы 1 в smoke-тесте
Добавлен блок [2b]: `/api/v1/instance` с созданным главным сервером,
наличие ручек серверов в OpenAPI, 401 на `/users/@me` без сессии, отказ
регистрации на пустое тело и отклонение обычного GET на `/gateway`
(апгрейд обязателен).
2026-09-19 21:56:47 +03:00

277 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# glchat: smoke-тест после установки (AGENT.md 10.9).
# Фаза 0 проверяет фундамент: HTTPS-заглушка, meta, healthy, порты, fail2ban.
set -uo pipefail
GLCHAT_ETC_DIR="${GLCHAT_ETC_DIR:-/opt/glchat}"
ENV_FILE="${GLCHAT_ETC_DIR}/.env"
PASSED=0
FAILED=0
SKIPPED=0
pass() {
printf ' \033[32m✓\033[0m %s\n' "$*"
PASSED=$((PASSED + 1))
}
fail() {
printf ' \033[31m✗\033[0m %s\n' "$*"
FAILED=$((FAILED + 1))
}
skip() {
printf ' \033[33m–\033[0m %s\n' "$*"
SKIPPED=$((SKIPPED + 1))
}
if [ -f "$ENV_FILE" ]; then
# shellcheck disable=SC1090
set -a && . "$ENV_FILE" && set +a
fi
DOMAIN="${DOMAIN:-}"
APP_PORT="${APP_PORT:-8080}"
APP_BIND="${GLCHAT_APP_BIND:-127.0.0.1}"
case "$APP_BIND" in 0.0.0.0 | "") APP_BIND="127.0.0.1" ;; esac
APP_URL="http://${APP_BIND}:${APP_PORT}"
PROXY_MODE="${PROXY_MODE:-embedded}"
LIVEKIT_TCP_PORT="${LIVEKIT_TCP_PORT:-7881}"
LIVEKIT_UDP_PORT="${LIVEKIT_UDP_PORT:-7882}"
TURN_ENABLED="${TURN_ENABLED:-true}"
TURN_UDP_PORT="${TURN_UDP_PORT:-3478}"
TURN_TLS_PORT="${TURN_TLS_PORT:-5349}"
SKIP_TLS="${SKIP_TLS:-0}"
LIVEKIT_PUBLISH="${LIVEKIT_PUBLISH:-true}"
ENABLE_FAIL2BAN="${FAIL2BAN_ENABLED:-true}"
printf '\n=== glchat smoke-тест ===\n'
printf '\n[1] Приложение\n'
if curl -fsS "${APP_URL}/healthz" >/dev/null; then
pass "/healthz отвечает 200 (${APP_URL})"
else
fail "/healthz недоступен на ${APP_URL}"
fi
ready="$(curl -fsS "${APP_URL}/readyz" 2>/dev/null || true)"
if printf '%s' "$ready" | grep -q '"status":"ready"'; then
pass "/readyz: база данных готова"
else
fail "/readyz: ${ready:-нет ответа}"
fi
meta="$(curl -fsS "${APP_URL}/api/v1/meta" 2>/dev/null || true)"
if printf '%s' "$meta" | grep -q '"api_version":"v1"'; then
version="$(printf '%s' "$meta" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')"
pass "/api/v1/meta отвечает, версия ${version}"
else
fail "/api/v1/meta недоступен"
fi
printf '\n[2] Веб-клиент\n'
index="$(curl -fsS "${APP_URL}/" 2>/dev/null || true)"
if printf '%s' "$index" | grep -qi '<div id="root"'; then
pass "заглушка клиента отдаётся с /"
elif printf '%s' "$index" | grep -qi 'web client bundle is not present'; then
fail "в образе нет собранного веб-клиента"
else
fail "некорректный ответ на /"
fi
printf '\n[2b] API Фазы 1 (аккаунты, права, gateway)\n'
instance="$(curl -fsS "${APP_URL}/api/v1/instance" 2>/dev/null || true)"
if printf '%s' "$instance" | grep -q '"registration_enabled"'; then
if printf '%s' "$instance" | grep -q '"main_guild_id":"[0-9]'; then
pass "/api/v1/instance отвечает, главный сервер создан"
else
fail "/api/v1/instance: главный сервер не создан (выполните bootstrap-admin)"
fi
else
fail "/api/v1/instance недоступен"
fi
openapi="$(curl -fsS "${APP_URL}/api/v1/openapi.json" 2>/dev/null || true)"
if printf '%s' "$openapi" | grep -q '"/guilds"'; then
pass "/api/v1/openapi.json содержит ручки серверов"
else
fail "/api/v1/openapi.json без ручек Фазы 1"
fi
me_status="$(curl -s -o /dev/null -w '%{http_code}' "${APP_URL}/api/v1/users/@me" 2>/dev/null || echo 000)"
if [ "$me_status" = "401" ]; then
pass "/api/v1/users/@me без сессии отдаёт 401"
else
fail "/api/v1/users/@me без сессии вернул ${me_status}, ожидался 401"
fi
register_status="$(curl -s -o /dev/null -w '%{http_code}' -X POST "${APP_URL}/api/v1/auth/register" \
-H 'Content-Type: application/json' -d '{}' 2>/dev/null || echo 000)"
case "$register_status" in
4*) pass "регистрация защищена от пустого запроса (${register_status})" ;;
*) fail "некорректный ответ регистрации на пустое тело: ${register_status}" ;;
esac
gateway_status="$(curl -s -o /dev/null -w '%{http_code}' "${APP_URL}/gateway" 2>/dev/null || echo 000)"
if [ "$gateway_status" = "200" ]; then
fail "/gateway отвечает 200 на обычный GET: апгрейд обязателен"
else
pass "/gateway без апгрейда отклонён (${gateway_status})"
fi
printf '\n[3] Публичный доступ\n'
if [ "$SKIP_TLS" = "1" ]; then
skip "TLS отключён (--skip-tls): публичная проверка пропущена"
elif [ "$PROXY_MODE" = "external" ]; then
if curl -fsS --max-time 10 "https://${DOMAIN}/api/v1/meta" >/dev/null 2>&1; then
pass "https://${DOMAIN}/api/v1/meta отвечает через внешний прокси"
else
fail "https://${DOMAIN} недоступен: добавьте блок из ${GLCHAT_ETC_DIR}/external-proxy.conf во внешний прокси"
fi
else
if curl -fsS --max-time 15 "https://${DOMAIN}/api/v1/meta" >/dev/null 2>&1; then
pass "https://${DOMAIN}/api/v1/meta отвечает (сертификат валиден)"
else
fail "https://${DOMAIN} недоступен (проверьте DNS и логи Caddy)"
fi
fi
printf '\n[4] Внутренние порты закрыты снаружи\n'
public_ip="$(curl -fsS --max-time 5 https://api.ipify.org 2>/dev/null || true)"
if [ -z "$public_ip" ]; then
skip "внешний IP не определён: пропускаем проверку публикации портов"
else
for port in "$APP_PORT" 7880; do
if timeout 4 bash -c "</dev/tcp/${public_ip}/${port}" 2>/dev/null; then
fail "порт ${port} доступен снаружи (${public_ip})"
else
pass "порт ${port} недоступен снаружи"
fi
done
fi
printf '\n[5] Открытые медиапорты\n'
if [ "$LIVEKIT_PUBLISH" = "false" ] || [ "${SKIP_TLS}" = "1" ]; then
skip "порты LiveKit не публиковались в этом режиме"
else
for spec in "${LIVEKIT_TCP_PORT}/tcp" "${LIVEKIT_UDP_PORT}/udp"; do
port="${spec%%/*}"
if ss -tulnH 2>/dev/null | grep -q ":${port} "; then
pass "слушает ${spec}"
else
fail "не слушает ${spec}"
fi
done
if [ "$TURN_ENABLED" = "true" ]; then
for spec in "${TURN_UDP_PORT}/udp" "${TURN_TLS_PORT}/tcp"; do
port="${spec%%/*}"
if ss -tulnH 2>/dev/null | grep -q ":${port} "; then
pass "слушает ${spec}"
else
fail "не слушает ${spec} (TURN не запустился: проверьте cert_file в livekit.yaml)"
fi
done
# TURN/TLS должен отдавать валидный сертификат на своём домене.
turn_domain="$(grep -A3 '^turn:' "${GLCHAT_ETC_DIR}/livekit/livekit.yaml" 2>/dev/null |
awk '/domain:/ {print $2; exit}')"
if [ -n "$turn_domain" ] && command -v openssl >/dev/null 2>&1; then
handshake="$(echo | timeout 10 openssl s_client -connect "${APP_BIND}:${TURN_TLS_PORT}" \
-servername "$turn_domain" 2>&1 || true)"
if printf '%s' "$handshake" | grep -q 'Verify return code: 0 (ok)'; then
pass "TURN/TLS: сертификат ${turn_domain} принят (verify ok)"
elif printf '%s' "$handshake" | grep -q 'BEGIN CERTIFICATE'; then
# Сертификат есть, но самоподписанный/недоверенный: для тестов это допустимо.
skip "TURN/TLS: сертификат не проходит проверку доверия (${turn_domain})"
else
fail "TURN/TLS: TLS-хендшейк на ${APP_BIND}:${TURN_TLS_PORT} не прошёл"
fi
fi
fi
fi
printf '\n[6] UFW и DOCKER-USER\n'
if command -v ufw >/dev/null 2>&1; then
if ufw status 2>/dev/null | grep -q 'Status: active'; then
pass "UFW активен"
if [ "$PROXY_MODE" = "external" ]; then
ufw status | grep -q "${LIVEKIT_TCP_PORT}/tcp" && pass "UFW: ${LIVEKIT_TCP_PORT}/tcp разрешён" || fail "UFW: нет правила для ${LIVEKIT_TCP_PORT}/tcp"
ufw status | grep -q "${LIVEKIT_UDP_PORT}/udp" && pass "UFW: ${LIVEKIT_UDP_PORT}/udp разрешён" || fail "UFW: нет правила для ${LIVEKIT_UDP_PORT}/udp"
fi
else
fail "UFW не активен"
fi
else
skip "ufw не установлен"
fi
if command -v iptables >/dev/null 2>&1 && iptables -nL DOCKER-USER >/dev/null 2>&1; then
if iptables -nL DOCKER-USER 2>/dev/null | grep -q 'glchat-block-internal'; then
pass "DOCKER-USER: внутренние порты заблокированы"
else
fail "DOCKER-USER: нет правил glchat"
fi
else
skip "цепочка DOCKER-USER недоступна"
fi
printf '\n[7] fail2ban\n'
if [ "$ENABLE_FAIL2BAN" = "true" ] && command -v fail2ban-client >/dev/null 2>&1; then
jails="$(fail2ban-client status 2>/dev/null | sed -n 's/.*Jail list:\s*//p')"
for jail in sshd glchat-auth recidive; do
if printf '%s' "$jails" | grep -q "$jail"; then
pass "jail ${jail} активен"
else
fail "jail ${jail} не активен"
fi
done
else
skip "fail2ban не включён"
fi
printf '\n[8] Автозапуск\n'
if systemctl is-enabled glchat.service >/dev/null 2>&1; then
pass "glchat.service включён"
else
fail "glchat.service не включён в автозапуск"
fi
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^glchat-app$'; then
restart_policy="$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' glchat-app 2>/dev/null)"
if [ "$restart_policy" = "unless-stopped" ]; then
pass "политика перезапуска контейнера: unless-stopped"
else
fail "политика перезапуска: ${restart_policy:-unknown}"
fi
else
fail "контейнер glchat-app не запущен"
fi
printf '\n[9] Обновление сертификата TURN\n'
if [ "$TURN_ENABLED" = "true" ] && systemctl cat glchat-cert-refresh.service >/dev/null 2>&1; then
if systemctl start glchat-cert-refresh.service 2>/dev/null; then
pass "glchat-cert-refresh отработал (сертификат TURN актуален)"
else
fail "glchat-cert-refresh не запускается"
fi
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q glchat-livekit; then
pass "LiveKit работает после проверки сертификата"
else
fail "LiveKit не запущен после проверки сертификата"
fi
else
skip "обновление сертификата TURN не настроено"
fi
printf '\n[10] Бэкап\n'
if GLCHAT_ETC_DIR="$GLCHAT_ETC_DIR" bash "${GLCHAT_ETC_DIR}/backup.sh" >/tmp/glchat-smoke-backup.log 2>&1; then
last_backup="$(ls -1t "${GLCHAT_ETC_DIR}/data/backups"/glchat-*.tar.gz* 2>/dev/null | head -1)"
pass "бэкап создан: $(basename "${last_backup:-unknown}")"
# Шифрование проверяем по содержимому, а не по имени файла.
case "$(head -c 18 "${last_backup}" 2>/dev/null)" in
age-encryption.org*) pass "бэкап зашифрован age" ;;
*) skip "бэкап без age-шифрования: задайте AGE_RECIPIENT (установщик генерирует его сам)" ;;
esac
else
fail "бэкап не создан (см. /tmp/glchat-smoke-backup.log)"
fi
printf '\n=== Итог: %d ok, %d ошибок, %d пропущено ===\n\n' "$PASSED" "$FAILED" "$SKIPPED"
if [ "$FAILED" -gt 0 ]; then
exit 1
fi
exit 0