diff --git a/bot/config.py b/bot/config.py index 8a79626..019b5ed 100644 --- a/bot/config.py +++ b/bot/config.py @@ -3,9 +3,36 @@ load_dotenv("../.env") + +def _require(name: str) -> str: + """Возвращает обязательную переменную окружения или бросает понятную ошибку.""" + value = os.environ.get(name, "").strip() + if not value: + raise RuntimeError(f"Не задана переменная окружения {name}") + return value + + +def _parse_admin_ids(raw: str) -> list[int]: + """Разбирает список id через запятую; пустые элементы пропускает.""" + ids: list[int] = [] + for item in raw.split(","): + item = item.strip() + if not item: + continue + try: + ids.append(int(item)) + except ValueError: + raise RuntimeError( + f"ADMIN_IDS: «{item}» не число (нужны id через запятую)" + ) from None + if not ids: + raise RuntimeError("ADMIN_IDS не содержит ни одного id") + return ids + + class Settings: - bot_token: str = os.environ["BOT_TOKEN"] - admin_ids: list[int] = [int(i) for i in os.environ["ADMIN_IDS"].split(",")] + bot_token: str = _require("BOT_TOKEN") + admin_ids: list[int] = _parse_admin_ids(_require("ADMIN_IDS")) database_url: str = os.getenv("DATABASE_URL", "sqlite:///./data/cards.db") cards_per_session: int = int(os.getenv("CARDS_PER_SESSION", "4")) # schedule_interval_hours: int = int(os.getenv("SCHEDULE_INTERVAL_HOURS", "5")) # не нужен при настройке почасового крона diff --git a/bot/tests/test_cards_db.py b/bot/tests/test_cards_db.py new file mode 100644 index 0000000..6bcdce6 --- /dev/null +++ b/bot/tests/test_cards_db.py @@ -0,0 +1,63 @@ +import os +import sys + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from cards import get_next_cards +from models import Card + + +def add_cards(db, count: int) -> None: + for i in range(1, count + 1): + db.add( + Card( + id=i, + question=f"Вопрос {i}", + answer=f"Ответ {i}", + category="Python", + tags="[]", + difficulty="easy", + ) + ) + db.commit() + + +@pytest.fixture +def db(): + engine = create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + Card.metadata.create_all(engine) + session = sessionmaker(bind=engine)() + try: + yield session + finally: + session.close() + engine.dispose() + + +class TestGetNextCardsRealDb: + def test_returns_cards_after_last_id_in_order(self, db): + add_cards(db, 6) + result = get_next_cards(db, 3, last_id=2) + assert [c.id for c in result] == [3, 4, 5] + + def test_wraps_around_to_start(self, db): + add_cards(db, 5) + result = get_next_cards(db, 3, last_id=4) + assert [c.id for c in result] == [5, 1, 2] + + def test_last_id_beyond_max_starts_from_beginning(self, db): + add_cards(db, 5) + result = get_next_cards(db, 3, last_id=100) + assert [c.id for c in result] == [1, 2, 3] + + def test_empty_db_returns_empty(self, db): + assert get_next_cards(db, 3, last_id=0) == [] diff --git a/bot/tests/test_config.py b/bot/tests/test_config.py new file mode 100644 index 0000000..3ed305c --- /dev/null +++ b/bot/tests/test_config.py @@ -0,0 +1,37 @@ +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import config + + +class TestRequire: + def test_returns_value(self, monkeypatch): + monkeypatch.setenv("TEST_REQUIRED_VAR", "abc") + assert config._require("TEST_REQUIRED_VAR") == "abc" + + def test_missing_variable_names_it(self, monkeypatch): + monkeypatch.delenv("TEST_REQUIRED_VAR", raising=False) + with pytest.raises(RuntimeError, match="TEST_REQUIRED_VAR"): + config._require("TEST_REQUIRED_VAR") + + def test_empty_variable_rejected(self, monkeypatch): + monkeypatch.setenv("TEST_REQUIRED_VAR", " ") + with pytest.raises(RuntimeError, match="TEST_REQUIRED_VAR"): + config._require("TEST_REQUIRED_VAR") + + +class TestParseAdminIds: + def test_valid_list(self): + assert config._parse_admin_ids("1, 22,333") == [1, 22, 333] + + def test_skips_empty_items(self): + assert config._parse_admin_ids("1,2,") == [1, 2] + + @pytest.mark.parametrize("raw", ["", " ", ",", "1,abc"]) + def test_invalid_rejected_with_name(self, raw): + with pytest.raises(RuntimeError, match="ADMIN_IDS"): + config._parse_admin_ids(raw)