Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions bot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")) # не нужен при настройке почасового крона
Expand Down
63 changes: 63 additions & 0 deletions bot/tests/test_cards_db.py
Original file line number Diff line number Diff line change
@@ -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) == []
37 changes: 37 additions & 0 deletions bot/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -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)
Loading