From 27b0f1ef8b6abd92d13b1fbb87dcf86e8b35cd6e Mon Sep 17 00:00:00 2001 From: Mikhail Opryshko Date: Wed, 16 Sep 2026 13:31:09 +0300 Subject: [PATCH 1/2] feat: add AD groups and OpenID roles to user search results --- pybotx/client/users_api/user_from_search.py | 6 ++++++ pybotx/models/users.py | 8 +++++++- tests/fixtures/users_api.py | 6 ++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pybotx/client/users_api/user_from_search.py b/pybotx/client/users_api/user_from_search.py index 2c1c8ffb..9d487298 100644 --- a/pybotx/client/users_api/user_from_search.py +++ b/pybotx/client/users_api/user_from_search.py @@ -13,6 +13,8 @@ class BotXAPISearchUserResult(VerifiedPayloadBaseModel): user_huid: UUID ad_login: str | None = None ad_domain: str | None = None + ad_groups: list[str] = Field(default_factory=list) + openid_roles: list[str] = Field(default_factory=list) avatar: str | None = None avatar_preview: str | None = None name: str @@ -53,6 +55,8 @@ def to_domain(self) -> UserFromSearch: huid=self.result.user_huid, ad_login=self.result.ad_login, ad_domain=self.result.ad_domain, + ad_groups=self.result.ad_groups, + openid_roles=self.result.openid_roles, avatar=self.result.avatar, avatar_preview=self.result.avatar_preview, username=self.result.name, @@ -87,6 +91,8 @@ def to_domain(self) -> list[UserFromSearch]: huid=user.user_huid, ad_login=user.ad_login, ad_domain=user.ad_domain, + ad_groups=user.ad_groups, + openid_roles=user.openid_roles, avatar=user.avatar, avatar_preview=user.avatar_preview, username=user.name, diff --git a/pybotx/models/users.py b/pybotx/models/users.py index b0ca61ca..4f3b6a70 100644 --- a/pybotx/models/users.py +++ b/pybotx/models/users.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import datetime from uuid import UUID @@ -13,6 +13,10 @@ class UserFromSearch: huid: User huid. ad_login: User AD login. ad_domain: User AD domain. + ad_groups: User AD group names. An empty list means that the user has no + groups or the bot has no access to them. + openid_roles: User OpenID role names. An empty list means that the user + has no roles or the bot has no access to them. avatar: User avatar (link). avatar_preview: User avatar preview (link). username: User name. @@ -46,6 +50,8 @@ class UserFromSearch: emails: list[str] other_id: str | None user_kind: UserKinds + ad_groups: list[str] = field(default_factory=list) + openid_roles: list[str] = field(default_factory=list) avatar: str | None = None avatar_preview: str | None = None active: bool | None = None diff --git a/tests/fixtures/users_api.py b/tests/fixtures/users_api.py index d26b3368..4996633d 100644 --- a/tests/fixtures/users_api.py +++ b/tests/fixtures/users_api.py @@ -13,6 +13,8 @@ def user_from_search_with_data_json() -> dict[str, Any]: "user_huid": "6fafda2c-6505-57a5-a088-25ea5d1d0364", "ad_login": "ad_user_login", "ad_domain": "cts.com", + "ad_groups": ["employees", "developers", "vpn-users"], + "openid_roles": ["openid_user", "openid_profile"], "avatar": ( "https://test-cts.ru/uploads/profile_avatar/fa13e946-121b-52ce-aa92-677b51d19d83/" "3ebb64d5e2f54e1ab9a3f2ff8096f55a.jpg" @@ -50,6 +52,8 @@ def user_from_search_with_data() -> UserFromSearch: huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), ad_login="ad_user_login", ad_domain="cts.com", + ad_groups=["employees", "developers", "vpn-users"], + openid_roles=["openid_user", "openid_profile"], avatar=( "https://test-cts.ru/uploads/profile_avatar/fa13e946-121b-52ce-aa92-677b51d19d83/" "3ebb64d5e2f54e1ab9a3f2ff8096f55a.jpg" @@ -115,6 +119,8 @@ def user_from_search_without_data() -> UserFromSearch: huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), ad_login="ad_user_login", ad_domain="cts.com", + ad_groups=[], + openid_roles=[], avatar=None, avatar_preview=None, username="Bob", From 6d8e7c1b9ac692e3b914829363878960da5f38c9 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Wed, 16 Sep 2026 13:54:31 +0300 Subject: [PATCH 2/2] fix positional args --- pybotx/models/users.py | 14 +++++---- tests/models/test_users.py | 58 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 tests/models/test_users.py diff --git a/pybotx/models/users.py b/pybotx/models/users.py index 4f3b6a70..6116a1e2 100644 --- a/pybotx/models/users.py +++ b/pybotx/models/users.py @@ -13,10 +13,6 @@ class UserFromSearch: huid: User huid. ad_login: User AD login. ad_domain: User AD domain. - ad_groups: User AD group names. An empty list means that the user has no - groups or the bot has no access to them. - openid_roles: User OpenID role names. An empty list means that the user - has no roles or the bot has no access to them. avatar: User avatar (link). avatar_preview: User avatar preview (link). username: User name. @@ -38,6 +34,12 @@ class UserFromSearch: rts_id: User RTS id. created_at: User creation timestamp. updated_at: User update timestamp. + ad_groups: User AD group names. BotX returns them only when the bot has + allow_read_ad_groups enabled. An empty list does not distinguish + no group membership from unavailable data. + openid_roles: User OpenID role names. BotX returns them only when the bot + has allow_read_openid_roles enabled. An empty list does not + distinguish no roles from unavailable data. """ huid: UUID @@ -50,8 +52,6 @@ class UserFromSearch: emails: list[str] other_id: str | None user_kind: UserKinds - ad_groups: list[str] = field(default_factory=list) - openid_roles: list[str] = field(default_factory=list) avatar: str | None = None avatar_preview: str | None = None active: bool | None = None @@ -66,6 +66,8 @@ class UserFromSearch: rts_id: UUID | None = None created_at: datetime | None = None updated_at: datetime | None = None + ad_groups: list[str] = field(default_factory=list) + openid_roles: list[str] = field(default_factory=list) @dataclass(slots=True) diff --git a/tests/models/test_users.py b/tests/models/test_users.py new file mode 100644 index 00000000..8ade91d1 --- /dev/null +++ b/tests/models/test_users.py @@ -0,0 +1,58 @@ +from uuid import UUID + +from pybotx import UserFromSearch, UserKinds + + +def test__user_from_search__preserves_existing_positional_arguments() -> None: + user = UserFromSearch( + UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), + "alice", + "example.com", + "Alice", + None, + None, + None, + ["alice@example.com"], + None, + UserKinds.CTS_USER, + "https://example.com/avatar.png", + "https://example.com/avatar-preview.png", + ) + + assert user.avatar == "https://example.com/avatar.png" + assert user.avatar_preview == "https://example.com/avatar-preview.png" + assert user.ad_groups == [] + assert user.openid_roles == [] + + +def test__user_from_search__group_and_role_defaults_are_independent() -> None: + first = UserFromSearch( + huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), + ad_login=None, + ad_domain=None, + username="Alice", + company=None, + company_position=None, + department=None, + emails=[], + other_id=None, + user_kind=UserKinds.CTS_USER, + ) + second = UserFromSearch( + huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), + ad_login=None, + ad_domain=None, + username="Alice", + company=None, + company_position=None, + department=None, + emails=[], + other_id=None, + user_kind=UserKinds.CTS_USER, + ) + + first.ad_groups.append("employees") + first.openid_roles.append("reader") + + assert second.ad_groups == [] + assert second.openid_roles == []