kin: BATON-ARCH-013 Добавить keep-alive механизм для предотвращения cold start
This commit is contained in:
parent
5435d2006f
commit
12abac74f0
3 changed files with 837 additions and 0 deletions
|
|
@ -6,10 +6,13 @@ Acceptance criteria:
|
|||
2. Response body contains JSON with {"status": "ok"}.
|
||||
3. Endpoint does not require authorization (no token, no secret header needed).
|
||||
4. Keep-alive loop is started when APP_URL is set, and NOT started when APP_URL is unset.
|
||||
5. deploy/ contains valid systemd .service and .timer config files.
|
||||
6. README documents both hosting scenarios and keep-alive instructions.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("BOT_TOKEN", "test-bot-token")
|
||||
os.environ.setdefault("CHAT_ID", "-1001234567890")
|
||||
|
|
@ -23,6 +26,8 @@ import pytest
|
|||
|
||||
from tests.conftest import make_app_client, temp_db
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Criterion 1 & 2 & 3 — GET /health → 200 OK, {"status": "ok"}, no auth
|
||||
|
|
@ -48,6 +53,26 @@ async def test_health_returns_status_ok():
|
|||
assert data.get("status") == "ok"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_returns_timestamp():
|
||||
"""GET /health должен вернуть поле timestamp в JSON."""
|
||||
async with make_app_client() as client:
|
||||
response = await client.get("/health")
|
||||
|
||||
data = response.json()
|
||||
assert "timestamp" in data
|
||||
assert isinstance(data["timestamp"], int)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_no_auth_header_required():
|
||||
"""GET /health без заголовков авторизации должен вернуть 200 (не 401/403)."""
|
||||
async with make_app_client() as client:
|
||||
response = await client.get("/health")
|
||||
|
||||
assert response.status_code not in (401, 403)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Criterion 4 — keep-alive task lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -82,3 +107,100 @@ async def test_keepalive_not_started_when_app_url_unset():
|
|||
pass
|
||||
|
||||
assert not mock_loop.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keepalive_called_with_app_url_value():
|
||||
"""Keep-alive задача должна получить значение APP_URL в качестве аргумента."""
|
||||
from backend.main import app
|
||||
|
||||
with temp_db():
|
||||
with patch("backend.telegram.set_webhook", new_callable=AsyncMock):
|
||||
with patch("backend.config.APP_URL", "https://my-app.fly.dev"):
|
||||
with patch("backend.main._keep_alive_loop", new_callable=AsyncMock) as mock_loop:
|
||||
async with app.router.lifespan_context(app):
|
||||
pass
|
||||
|
||||
mock_loop.assert_called_once_with("https://my-app.fly.dev")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Criterion 5 — systemd config files in deploy/
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_keepalive_service_file_exists():
|
||||
"""Файл deploy/baton-keepalive.service должен существовать."""
|
||||
assert (PROJECT_ROOT / "deploy" / "baton-keepalive.service").exists()
|
||||
|
||||
|
||||
def test_keepalive_timer_file_exists():
|
||||
"""Файл deploy/baton-keepalive.timer должен существовать."""
|
||||
assert (PROJECT_ROOT / "deploy" / "baton-keepalive.timer").exists()
|
||||
|
||||
|
||||
def test_keepalive_service_has_oneshot_type():
|
||||
"""baton-keepalive.service должен содержать Type=oneshot."""
|
||||
content = (PROJECT_ROOT / "deploy" / "baton-keepalive.service").read_text()
|
||||
assert "Type=oneshot" in content
|
||||
|
||||
|
||||
def test_keepalive_service_pings_health():
|
||||
"""baton-keepalive.service должен вызывать curl с /health endpoint."""
|
||||
content = (PROJECT_ROOT / "deploy" / "baton-keepalive.service").read_text()
|
||||
assert "/health" in content
|
||||
assert "curl" in content
|
||||
|
||||
|
||||
def test_keepalive_timer_has_unit_active_sec():
|
||||
"""baton-keepalive.timer должен содержать OnUnitActiveSec."""
|
||||
content = (PROJECT_ROOT / "deploy" / "baton-keepalive.timer").read_text()
|
||||
assert "OnUnitActiveSec" in content
|
||||
|
||||
|
||||
def test_keepalive_timer_has_install_section():
|
||||
"""baton-keepalive.timer должен содержать секцию [Install] с WantedBy=timers.target."""
|
||||
content = (PROJECT_ROOT / "deploy" / "baton-keepalive.timer").read_text()
|
||||
assert "[Install]" in content
|
||||
assert "timers.target" in content
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Criterion 6 — README documents hosting scenarios and keep-alive
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_readme_documents_selfhosting_scenario():
|
||||
"""README должен описывать вариант self-hosting (VPS)."""
|
||||
content = (PROJECT_ROOT / "README.md").read_text()
|
||||
assert "самохост" in content.lower() or "vps" in content.lower() or "Self" in content
|
||||
|
||||
|
||||
def test_readme_documents_fly_io_scenario():
|
||||
"""README должен описывать вариант хостинга Fly.io."""
|
||||
content = (PROJECT_ROOT / "README.md").read_text()
|
||||
assert "fly.io" in content.lower()
|
||||
|
||||
|
||||
def test_readme_documents_cron_keepalive():
|
||||
"""README должен содержать инструкцию по настройке cron для keep-alive."""
|
||||
content = (PROJECT_ROOT / "README.md").read_text()
|
||||
assert "cron" in content.lower() or "crontab" in content.lower()
|
||||
|
||||
|
||||
def test_readme_documents_systemd_keepalive():
|
||||
"""README должен содержать инструкцию по настройке systemd timer для keep-alive."""
|
||||
content = (PROJECT_ROOT / "README.md").read_text()
|
||||
assert "systemd" in content.lower()
|
||||
|
||||
|
||||
def test_readme_documents_uptimerobot():
|
||||
"""README должен содержать секцию UptimeRobot как внешний watchdog."""
|
||||
content = (PROJECT_ROOT / "README.md").read_text()
|
||||
assert "uptimerobot" in content.lower()
|
||||
|
||||
|
||||
def test_readme_documents_app_url_env_var():
|
||||
"""README должен упоминать переменную APP_URL для keep-alive."""
|
||||
content = (PROJECT_ROOT / "README.md").read_text()
|
||||
assert "APP_URL" in content
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue