Skip to content

Fix/backround clear magic tokens#17

Merged
Fl1riX merged 14 commits into
mainfrom
fix/backround-clear-magic-tokens
May 22, 2026
Merged

Fix/backround clear magic tokens#17
Fl1riX merged 14 commits into
mainfrom
fix/backround-clear-magic-tokens

Conversation

@Fl1riX
Copy link
Copy Markdown
Owner

@Fl1riX Fl1riX commented May 22, 2026

Summary by Sourcery

Добавлены пользовательские роли и поддержка блокировок, уточнена работа с Telegram magic token, а также изменены URL-адреса в потоке запуска бота и соответствующие тесты.

New Features:

  • Добавлено поле роли пользователя и модель Ban с ограничениями и валидацией для управления блокировками пользователей.
  • Связаны magic tokens с пользователями через отношение и добавлено их отображение в модели User.

Bug Fixes:

  • Модель MagicTokens переименована в MagicToken и обновлены все использования для обеспечения консистентной работы с токенами.
  • Исправлен некорректный импорт сессии базы данных в задаче очистки magic token.
  • Предотвращён краш обработчика запуска бота, когда message.from_user равно None.
  • Исправлена опечатка в сообщении проверочного утверждения (assertion) для обновления пароля.

Enhancements:

  • Используются временные метки с поддержкой таймзоны и более безопасные фабрики значений по умолчанию во всех моделях и логике валидации.
  • Добавлены более подробные docstring и отношения для основных моделей (User, Service, Appointment, MagicToken).
  • Ужесточены отношения и ограничения для Service, Appointment и Ban, включая уникальность активной блокировки и логику валидации.
  • Обновлены URL-адреса login-link на использование localhost и упрощён ответ обработчика start за счёт отправки прямой ссылки для входа.

Tests:

  • Обновлены интеграционные тесты с учётом переименованной модели MagicToken и нового поведения.
  • Добавлены модульные тесты для хеширования пароля и создания JWT access token.
Original summary in English

Summary by Sourcery

Introduce user roles and banning support, refine Telegram magic token handling, and adjust bot start flow URLs and tests.

New Features:

  • Add user role field and a Ban model with constraints and validation to manage user bans.
  • Link magic tokens to users via a relationship and expose them on the User model.

Bug Fixes:

  • Rename MagicTokens model to MagicToken and update all usages to ensure consistent token handling.
  • Fix incorrect database session import in the magic token cleanup task.
  • Avoid crashing in the bot start handler when message.from_user is None.
  • Correct typo in test assertion message for password updates.

Enhancements:

  • Use timezone-aware datetimes with safer default factories across models and validation logic.
  • Add richer docstrings and relationships for core models (User, Service, Appointment, MagicToken).
  • Tighten Service, Appointment, and Ban relationships and constraints, including active-ban uniqueness and validation logic.
  • Update login-link URLs to use localhost and simplify the start handler response by sending a direct login link.

Tests:

  • Update integration tests to use the renamed MagicToken model and new behavior.
  • Add unit tests for password hashing and JWT access token creation.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 22, 2026

Руководство для ревьюера

Рефакторинг и расширение модели данных SQLAlchemy (users, magic tokens, bans, services, appointments), исправление обработки и очистки magic‑токенов, корректировка стартового сценария Telegram‑бота и добавление модульных тестов для JWT/паролей и мелких исправлений в тестах.

Диаграмма последовательностей для Telegram /start с magic‑token ссылкой

sequenceDiagram
  actor TgUser
  participant Bot as TelegramBot
  participant TgLinkService
  participant DB

  TgUser->>Bot: /start
  Bot->>Bot: start_handler(message)
  Bot->>TgLinkService: is_telegram_linked(user_id)
  TgLinkService->>DB: SELECT User by telegram_id
  DB-->>TgLinkService: User or None
  TgLinkService-->>Bot: connected flag
  alt not connected
    Bot->>TgLinkService: generate_magic_token(user_id)
    TgLinkService->>DB: INSERT MagicToken
    DB-->>TgLinkService: MagicToken
    TgLinkService-->>Bot: token
    Bot-->>TgUser: message with URL /auth/login-link?token=...
  else already connected
    Bot-->>TgUser: message about existing link
  end
Loading

Диаграмма связей сущностей для обновлённых моделей user, magic token, appointment, service и ban

erDiagram
  User {
    int id
    int telegram_id
    string username
    string phone
    string email
    string role
  }

  MagicToken {
    int id
    string token
    datetime expires_at
    bool used
    datetime created_at
    int user_id
  }

  Service {
    int id
    string name
    int price
    string duration
    int entrepreneur_id
  }

  Appointment {
    int id
    datetime date
    int service_id
    int entrepreneur_id
    int user_id
  }

  Ban {
    int id
    string reason
    int user_id
    int banned_by
    int revoked_by
    datetime banned_at
    datetime expires_at
    datetime revoked_at
  }

  User ||--o{ MagicToken : has
  User ||--o{ Service : offers
  User ||--o{ Appointment : books
  Service ||--o{ Appointment : includes
  User ||--o{ Ban : receives
Loading

Изменения по файлам

Change Details Files
Уточнение моделей User, Appointment, Service и MagicToken и добавление модели Ban с валидацией и ограничениями целостности на уровне БД.
  • Перевод различных значений по умолчанию для DateTime на лямбды с учётом часового пояса с использованием datetime.now(timezone.utc).
  • Добавление Enum для role, учёт часового пояса в telegram_linked_at и добавление связи magic_tokens в User; добавление связи bans для поддержки блокировок пользователей.
  • Уточнение связей между Service и Appointment явным указанием foreign_keys и улучшение docstring’ов/комментариев.
  • Переименование модели MagicTokens в MagicToken, добавление внешнего ключа user_id и обратной связи к User, а также перевод created_at на учёт часового пояса с лямбдой по умолчанию.
  • Добавление новой модели Ban с внешними ключами на пользователей, check‑ограничениями, обеспечивающими корректную логику по времени блокировок, частичным уникальным индексом, гарантирующим только один активный бан на пользователя, а также валидаторами для banned_at/expires_at/revoked_at и свойством is_active.
src/infrastructure/db/models.py
Обновление сервисов и задач, использующих magic‑токены, для работы с переименованной моделью MagicToken и исправление импорта DB‑сессии.
  • Обновление TgLinkService для импорта и использования MagicToken вместо MagicTokens в type hints, запросах и создании экземпляров.
  • Корректировка запросов для magic‑токенов так, чтобы они ссылались на новые поля модели MagicToken, сохраняя текущую семантику (неиспользованный и не истёкший).
  • Исправление cleanup_telegram_tokens: импорт SessionLocal из корректного модуля и удаление через модель MagicToken с правильной проверкой истечения срока.
src/domain/services/tg_link_service.py
src/infrastructure/tasks/cleanup_magic_tokens.py
Корректировка сценария start‑handler’а Telegram‑бота и URL‑адресов входа для токен‑базированной аутентификации.
  • Замена assert для message.from_user на явную проверку на None с логированием ошибки и ранним выходом вместо генерации AssertionError.
  • Временный обход inline‑клавиатуры в start_handler и отправка прямой HTTP‑ссылки для входа, содержащей magic‑токен.
  • Обновление start_keyboards.get_link_keyboard для использования localhost вместо 127.0.0.1 в качестве хоста для login‑link.
src/presentation/bot/handlers/start.py
src/presentation/bot/keyboards/start_keyboards.py
Приведение тестов в соответствие с переименованием модели и изменениями поведения, а также добавление модульных тестов для JWT/помощников по паролям и небольшое исправление утверждения.
  • Замена MagicTokens на MagicToken во всех интеграционных тестах, которые создают или запрашивают magic‑токены.
  • Исправление опечатки в тексте сообщения assert в интеграционном тесте авторизации для обновлённых паролей.
  • Добавление модульных тестов для хеширования и проверки паролей и для создания JWT‑access токенов (базовые проверки формы/длины), плюс заглушка для тестирования декодирования токенов.
tests/integration/test_services/test_tg_link.py
tests/integration/test_services/test_auth.py
tests/unit/test_auth/test_jwt.py

Подсказки и команды

Взаимодействие с Sourcery

  • Запустить новый обзор: Оставьте комментарий @sourcery-ai review в pull request.
  • Продолжить обсуждение: Отвечайте напрямую на комментарии обзора от Sourcery.
  • Создать GitHub‑issue из комментария обзора: Попросите Sourcery создать issue из комментария обзора, ответив на него. Также можно ответить на комментарий обзора @sourcery-ai issue, чтобы создать issue на его основе.
  • Сгенерировать заголовок pull request: Напишите @sourcery-ai в любом месте заголовка pull request, чтобы сгенерировать заголовок в любой момент. Также можно оставить комментарий @sourcery-ai title в pull request, чтобы (пере)сгенерировать заголовок в любой момент.
  • Сгенерировать краткое описание pull request: Напишите @sourcery-ai summary в любом месте тела pull request, чтобы сгенерировать описание PR в нужном месте. Также можно оставить комментарий @sourcery-ai summary в pull request, чтобы (пере)сгенерировать описание в любой момент.
  • Сгенерировать руководство для ревьюера: Оставьте комментарий @sourcery-ai guide в pull request, чтобы (пере)сгенерировать руководство для ревьюера в любой момент.
  • Разрешить все комментарии Sourcery: Оставьте комментарий @sourcery-ai resolve в pull request, чтобы пометить все комментарии Sourcery как разрешённые. Полезно, если вы уже обработали все комментарии и не хотите их больше видеть.
  • Отклонить все обзоры Sourcery: Оставьте комментарий @sourcery-ai dismiss в pull request, чтобы отклонить все существующие обзоры Sourcery. Особенно полезно, если вы хотите начать с чистого листа — не забудьте затем оставить комментарий @sourcery-ai review, чтобы запустить новый обзор!

Настройка вашего опыта

Зайдите в вашу панель управления, чтобы:

  • Включать или отключать функции обзора, такие как автоматически сгенерированное Sourcery описание pull request, руководство для ревьюера и другие.
  • Изменить язык обзора.
  • Добавлять, удалять или редактировать пользовательские инструкции для ревью.
  • Настраивать другие параметры обзора.

Получение помощи

Original review guide in English

Reviewer's Guide

Refactors and extends the SQLAlchemy data model (users, magic tokens, bans, services, appointments), fixes magic-token handling and cleanup, adjusts the Telegram bot start flow, and adds JWT/password unit tests and minor test fixes.

Sequence diagram for Telegram /start flow with magic token link

sequenceDiagram
  actor TgUser
  participant Bot as TelegramBot
  participant TgLinkService
  participant DB

  TgUser->>Bot: /start
  Bot->>Bot: start_handler(message)
  Bot->>TgLinkService: is_telegram_linked(user_id)
  TgLinkService->>DB: SELECT User by telegram_id
  DB-->>TgLinkService: User or None
  TgLinkService-->>Bot: connected flag
  alt not connected
    Bot->>TgLinkService: generate_magic_token(user_id)
    TgLinkService->>DB: INSERT MagicToken
    DB-->>TgLinkService: MagicToken
    TgLinkService-->>Bot: token
    Bot-->>TgUser: message with URL /auth/login-link?token=...
  else already connected
    Bot-->>TgUser: message about existing link
  end
Loading

Entity relationship diagram for updated user, magic token, appointment, service, and ban models

erDiagram
  User {
    int id
    int telegram_id
    string username
    string phone
    string email
    string role
  }

  MagicToken {
    int id
    string token
    datetime expires_at
    bool used
    datetime created_at
    int user_id
  }

  Service {
    int id
    string name
    int price
    string duration
    int entrepreneur_id
  }

  Appointment {
    int id
    datetime date
    int service_id
    int entrepreneur_id
    int user_id
  }

  Ban {
    int id
    string reason
    int user_id
    int banned_by
    int revoked_by
    datetime banned_at
    datetime expires_at
    datetime revoked_at
  }

  User ||--o{ MagicToken : has
  User ||--o{ Service : offers
  User ||--o{ Appointment : books
  Service ||--o{ Appointment : includes
  User ||--o{ Ban : receives
Loading

File-Level Changes

Change Details Files
Refine User, Appointment, Service, and MagicToken models and introduce Ban model with validation and DB-level integrity constraints.
  • Switch various DateTime defaults to timezone-aware lambdas using datetime.now(timezone.utc).
  • Add role Enum, telegram_linked_at timezone awareness, and a magic_tokens relationship to User; add a bans relationship to support user bans.
  • Clarify Service and Appointment relationships by explicitly setting foreign_keys and improve docstrings/comments.
  • Rename MagicTokens model to MagicToken, add user_id FK and relationship back to User, and make created_at timezone-aware with a lambda default.
  • Add new Ban model with FK links to users, check constraints enforcing ban timing logic, a partial unique index ensuring only one active ban per user, and validators for banned_at/expires_at/revoked_at plus an is_active property.
src/infrastructure/db/models.py
Update services and tasks that use magic tokens to work with the renamed MagicToken model and correct DB session import.
  • Update TgLinkService to import and use MagicToken instead of MagicTokens in type hints, queries, and instance creation.
  • Adjust magic token queries to reference the new MagicToken model fields consistently, keeping existing semantics (unused & not expired).
  • Fix cleanup_telegram_tokens to import SessionLocal from the correct module and delete using the MagicToken model with proper expiry check.
src/domain/services/tg_link_service.py
src/infrastructure/tasks/cleanup_magic_tokens.py
Adjust Telegram bot start handler flow and login link URLs for token-based login.
  • Replace assert on message.from_user with an explicit None check, logging an error and returning early instead of raising AssertionError.
  • Temporarily bypass the inline keyboard in start_handler and send a direct HTTP login-link URL containing the magic token.
  • Update start_keyboards.get_link_keyboard to use localhost instead of 127.0.0.1 as the login-link host.
src/presentation/bot/handlers/start.py
src/presentation/bot/keyboards/start_keyboards.py
Align tests with model renaming and behavior changes and add unit tests for JWT/password helpers and a minor assertion fix.
  • Replace MagicTokens with MagicToken in all integration tests that create or query magic tokens.
  • Fix typo in integration auth test assertion message for updated passwords.
  • Add unit tests for hashing and verifying passwords and for creating JWT access tokens (basic shape/length checks), plus a stub for token decode testing.
tests/integration/test_services/test_tg_link.py
tests/integration/test_services/test_auth.py
tests/unit/test_auth/test_jwt.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Привет — я нашёл 5 проблем и оставил немного высокоуровневого фидбэка:

  • В TgLinkService.check_magic_token и нескольких тестах всё ещё используется datetime.now() без таймзоны, в то время как expires_at определён как DateTime(timezone=True) в других местах; имеет смысл везде последовательно использовать datetime.now(timezone.utc), чтобы избежать тонких багов, связанных с таймзонами, и несоответствий с БД.
  • В новой модели Ban определено несколько связей с User (moderator, user, revoker), но только user связан через back_populates; имеет смысл добавить соответствующие связи или подсказки overlaps на User, чтобы избежать неоднозначности/варнингов SQLAlchemy при нескольких FK на одну и ту же таблицу.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `TgLinkService.check_magic_token` and some tests you still use `datetime.now()` without timezone while `expires_at` is defined as `DateTime(timezone=True)` elsewhere; consider consistently using `datetime.now(timezone.utc)` to avoid subtle timezone bugs and mismatches with the DB.
- The new `Ban` model defines several relationships to `User` (`moderator`, `user`, `revoker`) but only `user` is connected via `back_populates`; you may want to add corresponding relationships or `overlaps` hints on `User` to avoid SQLAlchemy relationship ambiguity/warning with multiple FKs to the same table.

## Individual Comments

### Comment 1
<location path="src/domain/services/tg_link_service.py" line_range="60" />
<code_context>
-            MagicTokens.used == False  # noqa: E712
+        result = await db.execute(select(MagicToken).where(
+            MagicToken.token == token,
+            MagicToken.expires_at > datetime.now(),
+            MagicToken.used == False  # noqa: E712
         ))
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Use timezone‑aware `datetime` when comparing with a timezone‑aware DB column.

`expires_at` is `DateTime(timezone=True)`, but this comparison uses a naive `datetime.now()`. To match the column type and avoid timezone issues, use a timezone‑aware value here (e.g. `datetime.now(timezone.utc)`) as done elsewhere in the codebase.

Suggested implementation:

```python
        result = await db.execute(select(MagicToken).where(
            MagicToken.token == token,
            MagicToken.expires_at > datetime.now(timezone.utc),
            MagicToken.used == False  # noqa: E712
        ))

```

1. At the top of `src/domain/services/tg_link_service.py`, ensure you have:
   `from datetime import datetime, timezone`
   If `datetime` is already imported (e.g. `from datetime import datetime`), extend that import to include `timezone` instead of adding a separate conflicting import.
</issue_to_address>

### Comment 2
<location path="src/domain/services/tg_link_service.py" line_range="37-40" />
<code_context>
         -> Null
         """
-        link_token = MagicTokens(
+        link_token = MagicToken(
             telegram_id=telegram_id,
             token=token,
             expires_at=expires_at
</code_context>
<issue_to_address>
**issue (bug_risk):** `MagicToken` is instantiated with `telegram_id`, but the model no longer appears to define that column.

In the updated `MagicToken` model, only `id`, `token`, `expires_at`, `used`, `created_at`, and `user_id` are mapped, so `telegram_id` will either be an unmapped attribute or cause an error at runtime. If `telegram_id` is still required, it should be reintroduced as a mapped column; otherwise, drop this argument and adjust the flow to use `user_id` or another appropriate field instead.
</issue_to_address>

### Comment 3
<location path="tests/unit/test_auth/test_jwt.py" line_range="7-20" />
<code_context>
+    create_access_token
+)
+
+def test_hash_password(): 
+    """Тестирование хэширования паролей"""
+    password = "password_123"
+    wrong_password = "password"
+    
+    wrong_hash = hash_password(wrong_password)
+    hash1 = hash_password(password)
+    hash2 = hash_password(password)
+    
+    assert verify_password(password, hash1) is True, "Пароли не совпадают"
+    assert verify_password(password, hash2) is True, "Пароли не совпадают"
+    
+    assert verify_password(wrong_hash, hash1) is False, "Верный и не верный пароли совпадают"
+    assert verify_password(wrong_hash, hash2) is False, "Верный и не верный пароли совпадают"
+
+def test_create_access_token():
</code_context>
<issue_to_address>
**suggestion (testing):** Усилить тест хэширования пароля: проверить рандомизацию хэша и дополнительные негативные кейсы

Сейчас тест проверяет только успешную и неуспешную верификацию. Предлагаю дополнить его:

1) Явно зафиксировать рандомизацию хэша (при наличии соли):
```python
hash1 = hash_password(password)
hash2 = hash_password(password)
assert hash1 != hash2
```

2) Проверять негативный кейс именно с неверным *plain*-паролем, а не только с `wrong_hash`, чтобы очевидно проверялся контракт `verify_password(plain, hashed)`:
```python
assert verify_password(wrong_password, hash1) is False
```

3) Опционально добавить кейсы для пустого и очень длинного пароля, чтобы убедиться, что реализация не падает на граничных значениях.

```suggestion
def test_hash_password(): 
    """Тестирование хэширования паролей"""
    password = "password_123"
    wrong_password = "password"

    wrong_hash = hash_password(wrong_password)
    hash1 = hash_password(password)
    hash2 = hash_password(password)

    # 1) Рандомизация хэша (наличие соли)
    assert hash1 != hash2, "Хэши одинаковы, возможно отсутствует соль"

    # 2) Позитивная проверка: верный пароль проходит верификацию
    assert verify_password(password, hash1) is True, "Пароли не совпадают"
    assert verify_password(password, hash2) is True, "Пароли не совпадают"

    # 2) Негативный кейс: неверный plain-пароль не проходит верификацию
    assert verify_password(wrong_password, hash1) is False, "Неверный пароль проходит верификацию"
    assert verify_password(wrong_password, hash2) is False, "Неверный пароль проходит верификацию"

    # Дополнительный негативный кейс: неверный хэш не проходит верификацию
    assert verify_password(wrong_hash, hash1) is False, "Верный и неверный пароли дают совпадающий результат"
    assert verify_password(wrong_hash, hash2) is False, "Верный и неверный пароли дают совпадающий результат"

    # 3) Граничные значения: пустой и очень длинный пароль
    empty_password = ""
    long_password = "x" * 1024

    empty_hash = hash_password(empty_password)
    long_hash = hash_password(long_password)

    # Пустой пароль хэшируется и корректно верифицируется
    assert verify_password(empty_password, empty_hash) is True, "Пустой пароль не проходит верификацию"
    assert verify_password(wrong_password, empty_hash) is False, "Неверный пароль проходит верификацию для пустого пароля"

    # Очень длинный пароль хэшируется и корректно верифицируется
    assert verify_password(long_password, long_hash) is True, "Длинный пароль не проходит верификацию"
    assert verify_password(wrong_password, long_hash) is False, "Неверный пароль проходит верификацию для длинного пароля"
```
</issue_to_address>

### Comment 4
<location path="tests/unit/test_auth/test_jwt.py" line_range="22-29" />
<code_context>
+    assert verify_password(wrong_hash, hash1) is False, "Верный и не верный пароли совпадают"
+    assert verify_password(wrong_hash, hash2) is False, "Верный и не верный пароли совпадают"
+
+def test_create_access_token():
+    """Тестирование создания JWT токена"""
+    
+    data = {"sub": 1234}
+    token = create_access_token(data)
+    
+    assert isinstance(token, str)
+    assert len(token) > 100, "Токен слишком длинный"
+    
+def test_decode_token():
</code_context>
<issue_to_address>
**suggestion (testing):** Уточнить проверки для JWT: не только длина, но и содержимое/распаковка токена

Сейчас тест лишь проверяет, что `create_access_token` возвращает достаточно длинную строку, и при этом сообщение об ошибке противоречит условию (`len(token) > 100`, но текст: "Токен слишком длинный"). Лучше либо поправить текст на "Токен слишком короткий", либо отказаться от проверки длины и вместо этого декодировать токен тем же механизмом, что и в приложении, и проверить, что:
- `sub` присутствует и совпадает с переданным значением;
- есть ожидаемые клеймы (например, `exp`).
Так тест будет действительно проверять корректность и структуру JWT, а не только форму.

Suggested implementation:

```python
def test_create_access_token():
    """Тестирование создания JWT токена"""

    data = {"sub": 1234}
    token = create_access_token(data)

    assert isinstance(token, str)
    assert len(token) > 100, "Токен слишком короткий"

    # Проверяем, что токен корректно декодируется тем же механизмом, что и в приложении
    payload = decode_access_token(token)

    assert "sub" in payload, "В токене отсутствует claim 'sub'"
    assert payload["sub"] == data["sub"], "Claim 'sub' в токене не совпадает с исходными данными"
    assert "exp" in payload, "В токене отсутствует claim 'exp' (время истечения)"

def test_decode_token():
    """Тестирование расшифровки токена"""
    data = {"sub": 5678}
    token = create_access_token(data)

    payload = decode_access_token(token)

    assert payload["sub"] == data["sub"], "Claim 'sub' в расшифрованном токене не совпадает с исходными данными"
    assert "exp" in payload, "В расшифрованном токене отсутствует claim 'exp'"

```

1. В начале файла необходимо импортировать функцию `decode_access_token` из того же модуля, где она используется в приложении (например, `from app.auth.jwt import decode_access_token` или аналогично принятой структуре проекта).
2. Если в кодовой базе вместо `decode_access_token` используется другая функция/утилита для разбора JWT, замените вызовы `decode_access_token(...)` в тестах на фактическую функцию декодирования и скорректируйте импорт.
3. Если `create_access_token` возвращает токен, в котором `sub` хранится как строка, возможно, потребуется привести типы в ассертах (`str(data["sub"])`) в обоих тестах.
</issue_to_address>

### Comment 5
<location path="tests/unit/test_auth/test_jwt.py" line_range="31-32" />
<code_context>
+    assert isinstance(token, str)
+    assert len(token) > 100, "Токен слишком длинный"
+    
+def test_decode_token():
+    """Тестирование расшифровки токена"""
+    
+    
</code_context>
<issue_to_address>
**issue (testing):** Пустой `test_decode_token` создает ложное ощущение покрытия и не проверяет декодирование JWT

Сейчас `test_decode_token` не содержит ни одной проверки и всегда проходит.

Предлагаю либо:
- реализовать тест: сгенерировать токен, декодировать его и проверить ключевые поля (`sub`, срок действия и т.п.), а также реакцию на невалидный/просроченный токен;
- либо временно удалить тест, пока он не будет полноценно реализован.

Минимальный набор: один happy-path (валидный токен декодируется корректно) и один негативный сценарий (например, подделанный токен приводит к ожидаемому исключению/результату).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Original comment in English

Hey - I've found 5 issues, and left some high level feedback:

  • In TgLinkService.check_magic_token and some tests you still use datetime.now() without timezone while expires_at is defined as DateTime(timezone=True) elsewhere; consider consistently using datetime.now(timezone.utc) to avoid subtle timezone bugs and mismatches with the DB.
  • The new Ban model defines several relationships to User (moderator, user, revoker) but only user is connected via back_populates; you may want to add corresponding relationships or overlaps hints on User to avoid SQLAlchemy relationship ambiguity/warning with multiple FKs to the same table.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `TgLinkService.check_magic_token` and some tests you still use `datetime.now()` without timezone while `expires_at` is defined as `DateTime(timezone=True)` elsewhere; consider consistently using `datetime.now(timezone.utc)` to avoid subtle timezone bugs and mismatches with the DB.
- The new `Ban` model defines several relationships to `User` (`moderator`, `user`, `revoker`) but only `user` is connected via `back_populates`; you may want to add corresponding relationships or `overlaps` hints on `User` to avoid SQLAlchemy relationship ambiguity/warning with multiple FKs to the same table.

## Individual Comments

### Comment 1
<location path="src/domain/services/tg_link_service.py" line_range="60" />
<code_context>
-            MagicTokens.used == False  # noqa: E712
+        result = await db.execute(select(MagicToken).where(
+            MagicToken.token == token,
+            MagicToken.expires_at > datetime.now(),
+            MagicToken.used == False  # noqa: E712
         ))
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Use timezone‑aware `datetime` when comparing with a timezone‑aware DB column.

`expires_at` is `DateTime(timezone=True)`, but this comparison uses a naive `datetime.now()`. To match the column type and avoid timezone issues, use a timezone‑aware value here (e.g. `datetime.now(timezone.utc)`) as done elsewhere in the codebase.

Suggested implementation:

```python
        result = await db.execute(select(MagicToken).where(
            MagicToken.token == token,
            MagicToken.expires_at > datetime.now(timezone.utc),
            MagicToken.used == False  # noqa: E712
        ))

```

1. At the top of `src/domain/services/tg_link_service.py`, ensure you have:
   `from datetime import datetime, timezone`
   If `datetime` is already imported (e.g. `from datetime import datetime`), extend that import to include `timezone` instead of adding a separate conflicting import.
</issue_to_address>

### Comment 2
<location path="src/domain/services/tg_link_service.py" line_range="37-40" />
<code_context>
         -> Null
         """
-        link_token = MagicTokens(
+        link_token = MagicToken(
             telegram_id=telegram_id,
             token=token,
             expires_at=expires_at
</code_context>
<issue_to_address>
**issue (bug_risk):** `MagicToken` is instantiated with `telegram_id`, but the model no longer appears to define that column.

In the updated `MagicToken` model, only `id`, `token`, `expires_at`, `used`, `created_at`, and `user_id` are mapped, so `telegram_id` will either be an unmapped attribute or cause an error at runtime. If `telegram_id` is still required, it should be reintroduced as a mapped column; otherwise, drop this argument and adjust the flow to use `user_id` or another appropriate field instead.
</issue_to_address>

### Comment 3
<location path="tests/unit/test_auth/test_jwt.py" line_range="7-20" />
<code_context>
+    create_access_token
+)
+
+def test_hash_password(): 
+    """Тестирование хэширования паролей"""
+    password = "password_123"
+    wrong_password = "password"
+    
+    wrong_hash = hash_password(wrong_password)
+    hash1 = hash_password(password)
+    hash2 = hash_password(password)
+    
+    assert verify_password(password, hash1) is True, "Пароли не совпадают"
+    assert verify_password(password, hash2) is True, "Пароли не совпадают"
+    
+    assert verify_password(wrong_hash, hash1) is False, "Верный и не верный пароли совпадают"
+    assert verify_password(wrong_hash, hash2) is False, "Верный и не верный пароли совпадают"
+
+def test_create_access_token():
</code_context>
<issue_to_address>
**suggestion (testing):** Усилить тест хэширования пароля: проверить рандомизацию хэша и дополнительные негативные кейсы

Сейчас тест проверяет только успешную и неуспешную верификацию. Предлагаю дополнить его:

1) Явно зафиксировать рандомизацию хэша (при наличии соли):
```python
hash1 = hash_password(password)
hash2 = hash_password(password)
assert hash1 != hash2
```

2) Проверять негативный кейс именно с неверным *plain*-паролем, а не только с `wrong_hash`, чтобы очевидно проверялся контракт `verify_password(plain, hashed)`:
```python
assert verify_password(wrong_password, hash1) is False
```

3) Опционально добавить кейсы для пустого и очень длинного пароля, чтобы убедиться, что реализация не падает на граничных значениях.

```suggestion
def test_hash_password(): 
    """Тестирование хэширования паролей"""
    password = "password_123"
    wrong_password = "password"

    wrong_hash = hash_password(wrong_password)
    hash1 = hash_password(password)
    hash2 = hash_password(password)

    # 1) Рандомизация хэша (наличие соли)
    assert hash1 != hash2, "Хэши одинаковы, возможно отсутствует соль"

    # 2) Позитивная проверка: верный пароль проходит верификацию
    assert verify_password(password, hash1) is True, "Пароли не совпадают"
    assert verify_password(password, hash2) is True, "Пароли не совпадают"

    # 2) Негативный кейс: неверный plain-пароль не проходит верификацию
    assert verify_password(wrong_password, hash1) is False, "Неверный пароль проходит верификацию"
    assert verify_password(wrong_password, hash2) is False, "Неверный пароль проходит верификацию"

    # Дополнительный негативный кейс: неверный хэш не проходит верификацию
    assert verify_password(wrong_hash, hash1) is False, "Верный и неверный пароли дают совпадающий результат"
    assert verify_password(wrong_hash, hash2) is False, "Верный и неверный пароли дают совпадающий результат"

    # 3) Граничные значения: пустой и очень длинный пароль
    empty_password = ""
    long_password = "x" * 1024

    empty_hash = hash_password(empty_password)
    long_hash = hash_password(long_password)

    # Пустой пароль хэшируется и корректно верифицируется
    assert verify_password(empty_password, empty_hash) is True, "Пустой пароль не проходит верификацию"
    assert verify_password(wrong_password, empty_hash) is False, "Неверный пароль проходит верификацию для пустого пароля"

    # Очень длинный пароль хэшируется и корректно верифицируется
    assert verify_password(long_password, long_hash) is True, "Длинный пароль не проходит верификацию"
    assert verify_password(wrong_password, long_hash) is False, "Неверный пароль проходит верификацию для длинного пароля"
```
</issue_to_address>

### Comment 4
<location path="tests/unit/test_auth/test_jwt.py" line_range="22-29" />
<code_context>
+    assert verify_password(wrong_hash, hash1) is False, "Верный и не верный пароли совпадают"
+    assert verify_password(wrong_hash, hash2) is False, "Верный и не верный пароли совпадают"
+
+def test_create_access_token():
+    """Тестирование создания JWT токена"""
+    
+    data = {"sub": 1234}
+    token = create_access_token(data)
+    
+    assert isinstance(token, str)
+    assert len(token) > 100, "Токен слишком длинный"
+    
+def test_decode_token():
</code_context>
<issue_to_address>
**suggestion (testing):** Уточнить проверки для JWT: не только длина, но и содержимое/распаковка токена

Сейчас тест лишь проверяет, что `create_access_token` возвращает достаточно длинную строку, и при этом сообщение об ошибке противоречит условию (`len(token) > 100`, но текст: "Токен слишком длинный"). Лучше либо поправить текст на "Токен слишком короткий", либо отказаться от проверки длины и вместо этого декодировать токен тем же механизмом, что и в приложении, и проверить, что:
- `sub` присутствует и совпадает с переданным значением;
- есть ожидаемые клеймы (например, `exp`).
Так тест будет действительно проверять корректность и структуру JWT, а не только форму.

Suggested implementation:

```python
def test_create_access_token():
    """Тестирование создания JWT токена"""

    data = {"sub": 1234}
    token = create_access_token(data)

    assert isinstance(token, str)
    assert len(token) > 100, "Токен слишком короткий"

    # Проверяем, что токен корректно декодируется тем же механизмом, что и в приложении
    payload = decode_access_token(token)

    assert "sub" in payload, "В токене отсутствует claim 'sub'"
    assert payload["sub"] == data["sub"], "Claim 'sub' в токене не совпадает с исходными данными"
    assert "exp" in payload, "В токене отсутствует claim 'exp' (время истечения)"

def test_decode_token():
    """Тестирование расшифровки токена"""
    data = {"sub": 5678}
    token = create_access_token(data)

    payload = decode_access_token(token)

    assert payload["sub"] == data["sub"], "Claim 'sub' в расшифрованном токене не совпадает с исходными данными"
    assert "exp" in payload, "В расшифрованном токене отсутствует claim 'exp'"

```

1. В начале файла необходимо импортировать функцию `decode_access_token` из того же модуля, где она используется в приложении (например, `from app.auth.jwt import decode_access_token` или аналогично принятой структуре проекта).
2. Если в кодовой базе вместо `decode_access_token` используется другая функция/утилита для разбора JWT, замените вызовы `decode_access_token(...)` в тестах на фактическую функцию декодирования и скорректируйте импорт.
3. Если `create_access_token` возвращает токен, в котором `sub` хранится как строка, возможно, потребуется привести типы в ассертах (`str(data["sub"])`) в обоих тестах.
</issue_to_address>

### Comment 5
<location path="tests/unit/test_auth/test_jwt.py" line_range="31-32" />
<code_context>
+    assert isinstance(token, str)
+    assert len(token) > 100, "Токен слишком длинный"
+    
+def test_decode_token():
+    """Тестирование расшифровки токена"""
+    
+    
</code_context>
<issue_to_address>
**issue (testing):** Пустой `test_decode_token` создает ложное ощущение покрытия и не проверяет декодирование JWT

Сейчас `test_decode_token` не содержит ни одной проверки и всегда проходит.

Предлагаю либо:
- реализовать тест: сгенерировать токен, декодировать его и проверить ключевые поля (`sub`, срок действия и т.п.), а также реакцию на невалидный/просроченный токен;
- либо временно удалить тест, пока он не будет полноценно реализован.

Минимальный набор: один happy-path (валидный токен декодируется корректно) и один негативный сценарий (например, подделанный токен приводит к ожидаемому исключению/результату).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Repository owner deleted a comment from sourcery-ai Bot May 22, 2026
Repository owner deleted a comment from sourcery-ai Bot May 22, 2026
Repository owner deleted a comment from sourcery-ai Bot May 22, 2026
Repository owner deleted a comment from sourcery-ai Bot May 22, 2026
Repository owner deleted a comment from sourcery-ai Bot May 22, 2026
@Fl1riX Fl1riX merged commit e6a1d51 into main May 22, 2026
3 checks passed
@Fl1riX Fl1riX deleted the fix/backround-clear-magic-tokens branch May 22, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant