From 6d2f1b2a04236de2da425f6082d6940455b76196 Mon Sep 17 00:00:00 2001 From: Oleg Date: Mon, 10 Aug 2026 15:23:43 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20last=5Factivity=5Ftime=20=D1=81=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B0=D0=BD=20=D0=BE=D0=BF=D1=86=D0=B8=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- maxapi/types/users.py | 3 +- tests/test_swagger_alignment.py | 89 +++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/maxapi/types/users.py b/maxapi/types/users.py index 1375e5a2..676daacb 100644 --- a/maxapi/types/users.py +++ b/maxapi/types/users.py @@ -26,6 +26,7 @@ class User(FetchableMixin, BaseModel, BotMixin, PeerShortcutMixin): username: Имя пользователя (ник). Может быть None. is_bot: Флаг, указывающий, является ли пользователь ботом. last_activity_time: Временная метка последней активности. + Может быть None для удалённых пользователей (DELETED USER). description: Описание пользователя. Может быть None. avatar_url: URL аватара пользователя. Может быть None. full_avatar_url: URL полного аватара пользователя. @@ -39,7 +40,7 @@ class User(FetchableMixin, BaseModel, BotMixin, PeerShortcutMixin): last_name: str | None = None username: str | None = None is_bot: bool - last_activity_time: int + last_activity_time: int | None = None description: str | None = None avatar_url: str | None = None full_avatar_url: str | None = None diff --git a/tests/test_swagger_alignment.py b/tests/test_swagger_alignment.py index b3e8b13b..56964983 100644 --- a/tests/test_swagger_alignment.py +++ b/tests/test_swagger_alignment.py @@ -11,6 +11,7 @@ from maxapi.methods.get_chat_by_link import GetChatByLink from maxapi.methods.get_messages import GetMessages from maxapi.methods.types.added_members_chat import AddedMembersChat +from maxapi.methods.types.getted_members_chat import GettedMembersChat from maxapi.types.attachments.attachment import ContactAttachmentPayload from maxapi.types.attachments.image import PhotoAttachmentRequestPayload from maxapi.types.attachments.video import Video @@ -102,6 +103,7 @@ def test_user_and_chat_admin_keep_swagger_compat_fields(): ) assert user.first_name == "Alice" + assert user.last_activity_time == 0 assert admin.alias == "owner" @@ -122,6 +124,7 @@ def test_chat_member_accepts_swagger_fields_with_nullable_permissions(): } ) + assert member.last_activity_time == 0 assert member.last_access_time == 10 assert member.is_owner is False assert member.is_admin is True @@ -130,6 +133,92 @@ def test_chat_member_accepts_swagger_fields_with_nullable_permissions(): assert member.alias == "moderator" +def test_chat_member_allows_missing_last_activity_time_for_deleted_user(): + """Issue #179: удалённые пользователи (DELETED USER) не содержат + last_activity_time в ответе API, но содержат last_access_time. + """ + + member = ChatMember.model_validate( + { + "user_id": 1, + "first_name": "DELETED USER", + "is_bot": False, + "name": "DELETED USER", + "last_access_time": 0, + } + ) + + assert member.last_activity_time is None + assert member.last_access_time == 0 + assert member.first_name == "DELETED USER" + assert member.user_id == 1 + + +def test_chat_member_still_requires_identity_fields(): + """Убедиться, что фикс #179 не ослабляет остальные обязательные + поля — падать должны только они, а не last_activity_time. + """ + + with pytest.raises(ValidationError) as exc_info: + ChatMember.model_validate( + {"last_access_time": 0, "name": "DELETED USER"} + ) + + missing_fields = {error["loc"][0] for error in exc_info.value.errors()} + assert missing_fields == {"user_id", "first_name", "is_bot"} + + +def test_user_allows_missing_last_activity_time(): + user = User.model_validate( + {"user_id": 1, "first_name": "DELETED USER", "is_bot": False} + ) + + assert user.last_activity_time is None + + +def test_getted_members_chat_accepts_deleted_and_normal_members(): + """Issue #179: get_chat_members не должен падать, если среди + участников чата есть удалённый пользователь. + """ + + raw = { + "members": [ + { + "user_id": 1, + "first_name": "Alice", + "last_name": "Smith", + "username": "alice", + "is_bot": False, + "last_activity_time": 1739184000000, + "name": "Alice Smith", + "last_access_time": 1739184000000, + "is_owner": True, + "is_admin": True, + "join_time": 1739000000000, + "permissions": ["write"], + "alias": None, + }, + { + "user_id": 2, + "first_name": "DELETED USER", + "is_bot": False, + "name": "DELETED USER", + "last_access_time": 0, + }, + ], + "marker": 42, + } + + result = GettedMembersChat(**raw) + + assert len(result.members) == 2 + assert result.members[0].last_activity_time == 1739184000000 + assert result.members[0].last_access_time == 1739184000000 + assert result.members[1].last_activity_time is None + assert result.members[1].last_access_time == 0 + assert result.marker == 42 + + def test_get_messages_requires_chat_id_or_message_ids(bot): with pytest.raises(ValueError, match="chat_id или message_ids"): GetMessages(bot=bot)