slack-objects 0.0.7__tar.gz → 0.0.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {slack_objects-0.0.7 → slack_objects-0.0.9}/.github/copilot-instructions.md +2 -1
- {slack_objects-0.0.7 → slack_objects-0.0.9}/PKG-INFO +1 -1
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/_version.py +3 -3
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/config.py +4 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/idp_groups.py +17 -8
- slack_objects-0.0.9/src/slack_objects/usergroups.py +105 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/users.py +27 -37
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects.egg-info/PKG-INFO +1 -1
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects.egg-info/SOURCES.txt +3 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Smoke/_smoke_harness.py +15 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Smoke/run_all_smoke.py +2 -0
- slack_objects-0.0.9/tests/Smoke/usergroups_smoke_test.py +58 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Smoke/users_smoke_test.py +2 -0
- slack_objects-0.0.9/tests/UnitTests/usergroups_test.py +192 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/UnitTests/users_unit_test.py +45 -2
- {slack_objects-0.0.7 → slack_objects-0.0.9}/.github/workflows/ci.yml +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/.github/workflows/publish-pypi.yml +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/.github/workflows/publish-testpypi.yml +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/.gitignore +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/.vs/VSWorkspaceState.json +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/.vs/slnx.sqlite +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/LICENSE +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/README.md +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/pyproject.toml +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/pytest.ini +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/setup.cfg +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/__init__.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/api_caller.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/base.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/client.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/conversations.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/files.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/messages.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/rate_limits.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/scim_base.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects/workspaces.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects.egg-info/dependency_links.txt +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects.egg-info/requires.txt +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/src/slack_objects.egg-info/top_level.txt +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Examples/examples_config.example.json +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Examples/users_search_by_email_example.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Examples/users_test_AzureKeyVault.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/SCIM/conftest_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/SCIM/run_all_scim_users_live_tests.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/SCIM/test_scim_idp_groups_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/SCIM/test_scim_users_create_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/SCIM/test_scim_users_deactivate_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/SCIM/test_scim_users_input_validation_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/SCIM/test_scim_users_make_guest_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/SCIM/test_scim_users_reactivate_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/SCIM/test_scim_users_update_attribute_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Smoke/api_caller_smoke_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Smoke/conversations_smoke_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Smoke/files_smoke_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Smoke/idp_groups_smoke_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Smoke/messages_smoke_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Smoke/security_smoke_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Smoke/workspaces_smoke_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/UnitTests/config_unit_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/UnitTests/conversations_unit_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/UnitTests/files_unit_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/UnitTests/idp_groups_unit_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/UnitTests/messages_unit_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/UnitTests/rate_limits_unit_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/UnitTests/workspaces_unit_test.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Users/conftest_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Users/run_all_users_live_tests.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Users/test_users_admin_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Users/test_users_classification_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Users/test_users_input_validation_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/Users/test_users_read_live.py +0 -0
- {slack_objects-0.0.7 → slack_objects-0.0.9}/tests/live_test_config.example.json +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Copilot Instructions
|
|
2
2
|
|
|
3
3
|
## Project Guidelines
|
|
4
|
-
- Follow Unix philosophy: build small, composable functions where higher-level methods leverage lower-level primitives. Don't mix concerns (e.g., don't embed identifier resolution inside a SCIM CRUD method). Keep signatures consistent across sibling methods.
|
|
4
|
+
- Follow Unix philosophy: build small, composable functions where higher-level methods leverage lower-level primitives. Don't mix concerns (e.g., don't embed identifier resolution inside a SCIM CRUD method). Keep signatures consistent across sibling methods.
|
|
5
|
+
- Prefer consistency with the `Users.is_active`/`is_active_scim` pattern when structuring related methods (separate explicit SCIM method from higher-level orchestration).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slack-objects
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.9
|
|
4
4
|
Summary: Opinionated, testable Python wrappers for Slack’s Web, Admin, and SCIM APIs, organized by object domain (users, conversations, messages, files, workspaces, and IdP groups). Designed for automation and administration workflows.
|
|
5
5
|
Author-email: "Marcos E. Mercado" <marcos_elias@hotmail.com>
|
|
6
6
|
Keywords: slack,objects,classes,slack objects,utilities,slack utilities,slack object types,slack types,types
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.9'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 9)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g82c79f1f4'
|
|
@@ -38,6 +38,9 @@ class SlackObjectsConfig:
|
|
|
38
38
|
user_token: Optional[str] = field(default=None, repr=False)
|
|
39
39
|
scim_token: Optional[str] = field(default=None, repr=False)
|
|
40
40
|
|
|
41
|
+
# Workspace ID — required for org-wide tokens when calling workspace-scoped Web APIs
|
|
42
|
+
team_id: Optional[str] = None
|
|
43
|
+
|
|
41
44
|
default_rate_tier: RateTier = RateTier.TIER_2
|
|
42
45
|
|
|
43
46
|
auth_idp_groups_read_access: dict[str, list[str]] = field(default_factory=dict)
|
|
@@ -59,6 +62,7 @@ class SlackObjectsConfig:
|
|
|
59
62
|
f"bot_token={_mask(self.bot_token)}, "
|
|
60
63
|
f"user_token={_mask(self.user_token)}, "
|
|
61
64
|
f"scim_token={_mask(self.scim_token)}, "
|
|
65
|
+
f"team_id={self.team_id}, "
|
|
62
66
|
f"default_rate_tier={self.default_rate_tier}, "
|
|
63
67
|
f"auth_idp_groups_read_access={self.auth_idp_groups_read_access}, "
|
|
64
68
|
f"auth_idp_groups_write_access={self.auth_idp_groups_write_access}, "
|
|
@@ -20,6 +20,7 @@ Design decisions
|
|
|
20
20
|
- SCIM REST calls are centralized in ScimMixin._scim_request(); all public methods call endpoint wrappers.
|
|
21
21
|
- Uses an injectable `requests.Session` (`scim_session`) so tests can pass a fake session.
|
|
22
22
|
- Keeps legacy output shapes: lists of dicts for groups and members.
|
|
23
|
+
- This module is SCIM-only. For Slack-native usergroups, see ``usergroups.py``.
|
|
23
24
|
"""
|
|
24
25
|
|
|
25
26
|
from dataclasses import dataclass, field
|
|
@@ -58,6 +59,15 @@ class IDP_groups(ScimMixin, SlackObjectBase):
|
|
|
58
59
|
scim_session=self.scim_session,
|
|
59
60
|
)
|
|
60
61
|
|
|
62
|
+
# ---------- identifier resolution ----------
|
|
63
|
+
|
|
64
|
+
def _resolve_group_id(self, group_id: Optional[str] = None) -> str:
|
|
65
|
+
"""Resolve group_id from argument or bound instance value."""
|
|
66
|
+
gid = group_id or self.group_id
|
|
67
|
+
if not gid:
|
|
68
|
+
raise ValueError("group_id is required (passed or bound)")
|
|
69
|
+
return gid
|
|
70
|
+
|
|
61
71
|
# ---------- endpoint wrappers (only these call _scim_request) ----------
|
|
62
72
|
|
|
63
73
|
def _scim_groups_list(self, *, count: int = 1000, start_index: Optional[int] = None) -> ScimResponse:
|
|
@@ -122,21 +132,20 @@ class IDP_groups(ScimMixin, SlackObjectBase):
|
|
|
122
132
|
|
|
123
133
|
def get_members(self, group_id: Optional[str] = None) -> List[Dict[str, str]]:
|
|
124
134
|
"""
|
|
125
|
-
Return
|
|
135
|
+
Return group members via SCIM (GET Groups/{id}) as a list of dicts `{'value': <user_id>, 'display': <name>}`.
|
|
126
136
|
|
|
127
|
-
|
|
137
|
+
This module is SCIM-only. For Slack-native usergroups, see
|
|
138
|
+
``Usergroups.get_members()`` in ``usergroups.py``.
|
|
128
139
|
"""
|
|
129
|
-
gid =
|
|
130
|
-
if not gid:
|
|
131
|
-
raise ValueError("get_members requires group_id (passed or bound)")
|
|
132
|
-
|
|
140
|
+
gid = self._resolve_group_id(group_id)
|
|
133
141
|
scim_resp = self._scim_group_get(gid)
|
|
134
|
-
# In the legacy scripts, group members are at `members` in the response body
|
|
135
142
|
return scim_resp.data.get("members", [])
|
|
136
143
|
|
|
137
144
|
def is_member(self, user_id: str, group_id: Optional[str] = None) -> bool:
|
|
138
145
|
"""
|
|
139
|
-
Return True if
|
|
146
|
+
Return True if ``user_id`` is a member of ``group_id`` (via SCIM).
|
|
147
|
+
|
|
148
|
+
Higher-level convenience that composes on ``get_members()``.
|
|
140
149
|
Preserves legacy semantics (scans the members list).
|
|
141
150
|
"""
|
|
142
151
|
members = self.get_members(group_id=group_id)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
slack_objects.usergroups
|
|
5
|
+
========================
|
|
6
|
+
|
|
7
|
+
Usergroups helper for the `slack-objects` package.
|
|
8
|
+
|
|
9
|
+
Purpose
|
|
10
|
+
-------
|
|
11
|
+
Manage Slack-native user groups via the Web API (usergroups.*).
|
|
12
|
+
These are distinct from IdP (SCIM) groups managed in ``idp_groups.py``.
|
|
13
|
+
|
|
14
|
+
Design decisions
|
|
15
|
+
----------------
|
|
16
|
+
- Only calls the Slack Web API via SlackApiCaller; no SCIM dependency.
|
|
17
|
+
- Keeps the same output shape as IDP_groups members
|
|
18
|
+
(``[{'value': <user_id>, 'display': ''}]``) so callers can swap sources.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any, Dict, List, Optional
|
|
23
|
+
|
|
24
|
+
from .base import SlackObjectBase
|
|
25
|
+
from .config import RateTier
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Usergroups(SlackObjectBase):
|
|
30
|
+
"""
|
|
31
|
+
Slack-native usergroups helper.
|
|
32
|
+
|
|
33
|
+
Factory usage:
|
|
34
|
+
slack = SlackObjectsClient(cfg)
|
|
35
|
+
ug = slack.usergroups() # unbound
|
|
36
|
+
bound = slack.usergroups("S0614TZR7") # bound to a usergroup_id
|
|
37
|
+
"""
|
|
38
|
+
usergroup_id: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
# ---------- factory ----------
|
|
41
|
+
|
|
42
|
+
def with_usergroup(self, usergroup_id: str) -> "Usergroups":
|
|
43
|
+
"""Return a new instance bound to a particular usergroup_id, sharing cfg/client/logger/api."""
|
|
44
|
+
return Usergroups(
|
|
45
|
+
cfg=self.cfg,
|
|
46
|
+
client=self.client,
|
|
47
|
+
logger=self.logger,
|
|
48
|
+
api=self.api,
|
|
49
|
+
usergroup_id=usergroup_id,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# ---------- identifier resolution ----------
|
|
53
|
+
|
|
54
|
+
def _resolve_usergroup_id(self, usergroup_id: Optional[str] = None) -> str:
|
|
55
|
+
"""Resolve usergroup_id from argument or bound instance value."""
|
|
56
|
+
ugid = usergroup_id or self.usergroup_id
|
|
57
|
+
if not ugid:
|
|
58
|
+
raise ValueError("usergroup_id is required (passed or bound)")
|
|
59
|
+
return ugid
|
|
60
|
+
|
|
61
|
+
# ---------- endpoint wrappers (only these call self.api.call) ----------
|
|
62
|
+
|
|
63
|
+
def _usergroups_list(self, **kwargs: Any) -> Dict[str, Any]:
|
|
64
|
+
"""Wrapper for usergroups.list."""
|
|
65
|
+
if self.cfg.team_id:
|
|
66
|
+
kwargs.setdefault("team_id", self.cfg.team_id)
|
|
67
|
+
return self.api.call(self.client, "usergroups.list", rate_tier=RateTier.TIER_2, **kwargs)
|
|
68
|
+
|
|
69
|
+
def _usergroups_users_list(self, usergroup_id: str) -> Dict[str, Any]:
|
|
70
|
+
"""Wrapper for usergroups.users.list."""
|
|
71
|
+
kwargs: Dict[str, Any] = {"usergroup": usergroup_id}
|
|
72
|
+
if self.cfg.team_id:
|
|
73
|
+
kwargs["team_id"] = self.cfg.team_id
|
|
74
|
+
return self.api.call(self.client, "usergroups.users.list", rate_tier=RateTier.TIER_2, **kwargs)
|
|
75
|
+
|
|
76
|
+
# ---------- public helpers ----------
|
|
77
|
+
|
|
78
|
+
def get_usergroups(self) -> List[Dict[str, Any]]:
|
|
79
|
+
"""Return all usergroups visible to the bot token (usergroups.list)."""
|
|
80
|
+
resp = self._usergroups_list()
|
|
81
|
+
return resp.get("usergroups", [])
|
|
82
|
+
|
|
83
|
+
def get_members(self, usergroup_id: Optional[str] = None) -> List[Dict[str, str]]:
|
|
84
|
+
"""
|
|
85
|
+
Return members of a Slack usergroup via the Web API (usergroups.users.list).
|
|
86
|
+
|
|
87
|
+
Returns the same shape as ``IDP_groups.get_members_scim()`` —
|
|
88
|
+
``[{'value': <user_id>, 'display': ''}]`` — so callers can swap
|
|
89
|
+
sources without changing downstream code.
|
|
90
|
+
|
|
91
|
+
Requires the ``usergroups:read`` bot scope.
|
|
92
|
+
For org-wide tokens, ``cfg.team_id`` must be set.
|
|
93
|
+
"""
|
|
94
|
+
ugid = self._resolve_usergroup_id(usergroup_id)
|
|
95
|
+
resp = self._usergroups_users_list(ugid)
|
|
96
|
+
users = resp.get("users", [])
|
|
97
|
+
return [{"value": uid, "display": ""} for uid in users]
|
|
98
|
+
|
|
99
|
+
def is_member(self, user_id: str, usergroup_id: Optional[str] = None) -> bool:
|
|
100
|
+
"""Return True if ``user_id`` is a member of the usergroup."""
|
|
101
|
+
members = self.get_members(usergroup_id=usergroup_id)
|
|
102
|
+
for member in members:
|
|
103
|
+
if member.get("value") == user_id:
|
|
104
|
+
return True
|
|
105
|
+
return False
|
|
@@ -268,7 +268,11 @@ class Users(ScimMixin, SlackObjectBase):
|
|
|
268
268
|
if not uid:
|
|
269
269
|
raise ValueError("is_active_scim requires user_id (passed or bound)")
|
|
270
270
|
|
|
271
|
-
scim_resp = self._scim_request(
|
|
271
|
+
scim_resp = self._scim_request(
|
|
272
|
+
path=f"Users/{uid}",
|
|
273
|
+
method="GET",
|
|
274
|
+
raise_for_status=False,
|
|
275
|
+
)
|
|
272
276
|
if not scim_resp.ok:
|
|
273
277
|
return False
|
|
274
278
|
return bool(scim_resp.data.get("active", False))
|
|
@@ -511,15 +515,27 @@ class Users(ScimMixin, SlackObjectBase):
|
|
|
511
515
|
|
|
512
516
|
identifier = identifier.strip()
|
|
513
517
|
|
|
518
|
+
# Normalize: strip leading '@' so "@U1234" falls into the user-ID
|
|
519
|
+
# path and "@alice" falls into the username path — no special stage needed.
|
|
520
|
+
if identifier.startswith("@"):
|
|
521
|
+
identifier = identifier.removeprefix("@")
|
|
522
|
+
|
|
514
523
|
# ── 1. Slack user ID ──────────────────────────────────────
|
|
515
524
|
if self._looks_like_user_id(identifier):
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
525
|
+
try:
|
|
526
|
+
resp = self.get_user_info(identifier)
|
|
527
|
+
if resp.get("ok"):
|
|
528
|
+
return identifier
|
|
529
|
+
except SlackApiError:
|
|
530
|
+
pass # expected for deactivated / not-found users
|
|
519
531
|
|
|
520
532
|
# Slow path: SCIM (active + deactivated)
|
|
521
533
|
self.logger.info("Web API miss for %s — falling back to SCIM lookup", identifier)
|
|
522
|
-
scim_resp = self._scim_request(
|
|
534
|
+
scim_resp = self._scim_request(
|
|
535
|
+
path=f"Users/{identifier}",
|
|
536
|
+
method="GET",
|
|
537
|
+
raise_for_status=False,
|
|
538
|
+
)
|
|
523
539
|
if scim_resp.ok and scim_resp.data.get("id"):
|
|
524
540
|
return scim_resp.data["id"]
|
|
525
541
|
|
|
@@ -528,12 +544,9 @@ class Users(ScimMixin, SlackObjectBase):
|
|
|
528
544
|
# ── 2. Email address ──────────────────────────────────────
|
|
529
545
|
if EMAIL_RE.match(identifier):
|
|
530
546
|
# Fast path: Web API (active users only)
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
return uid
|
|
535
|
-
except SlackApiError:
|
|
536
|
-
pass # expected for deactivated / not-found users
|
|
547
|
+
uid = self.get_user_id_from_email(identifier)
|
|
548
|
+
if uid:
|
|
549
|
+
return uid
|
|
537
550
|
|
|
538
551
|
# Slow path: SCIM (active + deactivated)
|
|
539
552
|
self.logger.info("Web API miss for %s — falling back to SCIM search", identifier)
|
|
@@ -543,32 +556,9 @@ class Users(ScimMixin, SlackObjectBase):
|
|
|
543
556
|
|
|
544
557
|
raise LookupError(f"No user found for email: {identifier}")
|
|
545
558
|
|
|
546
|
-
# ── 3.
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
# The caller may have passed "@U1234" — recognise the ID
|
|
551
|
-
# and verify via the cheaper Web API instead of SCIM.
|
|
552
|
-
if self._looks_like_user_id(stripped):
|
|
553
|
-
resp = self.get_user_info(stripped)
|
|
554
|
-
if resp.get("ok"):
|
|
555
|
-
return stripped
|
|
556
|
-
|
|
557
|
-
# Slow path: SCIM (active + deactivated)
|
|
558
|
-
self.logger.info("Web API miss for %s — falling back to SCIM lookup", stripped)
|
|
559
|
-
scim_resp = self._scim_request(path=f"Users/{stripped}", method="GET")
|
|
560
|
-
if scim_resp.ok and scim_resp.data.get("id"):
|
|
561
|
-
return scim_resp.data["id"]
|
|
562
|
-
|
|
563
|
-
raise LookupError(f"No user found for user ID: {stripped}")
|
|
564
|
-
|
|
565
|
-
uid = self._first_scim_user_id(self.scim_search_user_by_username(stripped))
|
|
566
|
-
if uid:
|
|
567
|
-
return uid
|
|
568
|
-
|
|
569
|
-
raise LookupError(f"No user found for username: @{stripped}")
|
|
570
|
-
|
|
571
|
-
# ── 4. Bare string — try as username via SCIM ─────────────
|
|
559
|
+
# ── 3. Bare string / username — SCIM only ─────────────────
|
|
560
|
+
# No Web API endpoint exists for username lookup; SCIM is the
|
|
561
|
+
# only option. This also handles "@alice" (already stripped above).
|
|
572
562
|
uid = self._first_scim_user_id(self.scim_search_user_by_username(identifier))
|
|
573
563
|
if uid:
|
|
574
564
|
return uid
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slack-objects
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.9
|
|
4
4
|
Summary: Opinionated, testable Python wrappers for Slack’s Web, Admin, and SCIM APIs, organized by object domain (users, conversations, messages, files, workspaces, and IdP groups). Designed for automation and administration workflows.
|
|
5
5
|
Author-email: "Marcos E. Mercado" <marcos_elias@hotmail.com>
|
|
6
6
|
Keywords: slack,objects,classes,slack objects,utilities,slack utilities,slack object types,slack types,types
|
|
@@ -21,6 +21,7 @@ src/slack_objects/idp_groups.py
|
|
|
21
21
|
src/slack_objects/messages.py
|
|
22
22
|
src/slack_objects/rate_limits.py
|
|
23
23
|
src/slack_objects/scim_base.py
|
|
24
|
+
src/slack_objects/usergroups.py
|
|
24
25
|
src/slack_objects/users.py
|
|
25
26
|
src/slack_objects/workspaces.py
|
|
26
27
|
src/slack_objects.egg-info/PKG-INFO
|
|
@@ -49,6 +50,7 @@ tests/Smoke/idp_groups_smoke_test.py
|
|
|
49
50
|
tests/Smoke/messages_smoke_test.py
|
|
50
51
|
tests/Smoke/run_all_smoke.py
|
|
51
52
|
tests/Smoke/security_smoke_test.py
|
|
53
|
+
tests/Smoke/usergroups_smoke_test.py
|
|
52
54
|
tests/Smoke/users_smoke_test.py
|
|
53
55
|
tests/Smoke/workspaces_smoke_test.py
|
|
54
56
|
tests/UnitTests/config_unit_test.py
|
|
@@ -57,6 +59,7 @@ tests/UnitTests/files_unit_test.py
|
|
|
57
59
|
tests/UnitTests/idp_groups_unit_test.py
|
|
58
60
|
tests/UnitTests/messages_unit_test.py
|
|
59
61
|
tests/UnitTests/rate_limits_unit_test.py
|
|
62
|
+
tests/UnitTests/usergroups_test.py
|
|
60
63
|
tests/UnitTests/users_unit_test.py
|
|
61
64
|
tests/UnitTests/workspaces_unit_test.py
|
|
62
65
|
tests/Users/conftest_live.py
|
|
@@ -190,6 +190,21 @@ class FakeWebClient:
|
|
|
190
190
|
"offset": "",
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
# ---------------------------
|
|
194
|
+
# Usergroups
|
|
195
|
+
# ---------------------------
|
|
196
|
+
if method == "usergroups.list":
|
|
197
|
+
return {
|
|
198
|
+
"ok": True,
|
|
199
|
+
"usergroups": [
|
|
200
|
+
{"id": "S0614TZR7", "name": "Team Admins", "handle": "admins"},
|
|
201
|
+
{"id": "S0615ABC8", "name": "Developers", "handle": "devs"},
|
|
202
|
+
],
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if method == "usergroups.users.list":
|
|
206
|
+
return {"ok": True, "users": ["U1", "U2"]}
|
|
207
|
+
|
|
193
208
|
# Default: ok True
|
|
194
209
|
return {"ok": True}
|
|
195
210
|
|
|
@@ -15,6 +15,7 @@ from tests.Smoke.idp_groups_smoke_test import main as idp_main
|
|
|
15
15
|
from tests.Smoke.workspaces_smoke_test import main as workspaces_main
|
|
16
16
|
from tests.Smoke.api_caller_smoke_test import main as api_caller_main
|
|
17
17
|
from tests.Smoke.security_smoke_test import main as security_main
|
|
18
|
+
from tests.Smoke.usergroups_smoke_test import main as usergroups_main
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def main() -> None:
|
|
@@ -26,6 +27,7 @@ def main() -> None:
|
|
|
26
27
|
workspaces_main()
|
|
27
28
|
api_caller_main()
|
|
28
29
|
security_main()
|
|
30
|
+
usergroups_main()
|
|
29
31
|
print("\n✅ All smoke tests completed successfully.")
|
|
30
32
|
|
|
31
33
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from slack_objects.config import SlackObjectsConfig, RateTier
|
|
6
|
+
from slack_objects.usergroups import Usergroups
|
|
7
|
+
|
|
8
|
+
from tests.Smoke._smoke_harness import (
|
|
9
|
+
FakeWebClient,
|
|
10
|
+
FakeApiCaller,
|
|
11
|
+
CallSpec,
|
|
12
|
+
run_smoke,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
logging.basicConfig(level=logging.INFO)
|
|
18
|
+
logger = logging.getLogger("smoke.usergroups")
|
|
19
|
+
|
|
20
|
+
cfg = SlackObjectsConfig(
|
|
21
|
+
bot_token="xoxb-fake",
|
|
22
|
+
default_rate_tier=RateTier.TIER_4,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
client = FakeWebClient()
|
|
26
|
+
api = FakeApiCaller(cfg)
|
|
27
|
+
|
|
28
|
+
ug = Usergroups(
|
|
29
|
+
cfg=cfg,
|
|
30
|
+
client=client,
|
|
31
|
+
api=api,
|
|
32
|
+
logger=logger,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
bound = ug.with_usergroup("S0614TZR7")
|
|
36
|
+
|
|
37
|
+
specs = [
|
|
38
|
+
# Factory helper
|
|
39
|
+
CallSpec("with_usergroup()", lambda: ug.with_usergroup("S0614TZR7")),
|
|
40
|
+
|
|
41
|
+
# Listing
|
|
42
|
+
CallSpec("get_usergroups()", lambda: ug.get_usergroups()),
|
|
43
|
+
|
|
44
|
+
# Members
|
|
45
|
+
CallSpec("get_members(by arg)", lambda: ug.get_members("S0614TZR7")),
|
|
46
|
+
CallSpec("get_members(bound)", lambda: bound.get_members()),
|
|
47
|
+
|
|
48
|
+
# Membership check
|
|
49
|
+
CallSpec("is_member(hit)", lambda: ug.is_member(user_id="U1", usergroup_id="S0614TZR7")),
|
|
50
|
+
CallSpec("is_member(miss)", lambda: ug.is_member(user_id="U_UNKNOWN", usergroup_id="S0614TZR7")),
|
|
51
|
+
CallSpec("is_member(bound)", lambda: bound.is_member(user_id="U2")),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
run_smoke("Usergroups smoke (all public methods)", specs)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|
|
@@ -169,6 +169,8 @@ def main() -> None:
|
|
|
169
169
|
|
|
170
170
|
# Identifier resolution
|
|
171
171
|
CallSpec("resolve_user_id(user_id)", lambda: bound.resolve_user_id("U1")),
|
|
172
|
+
CallSpec("resolve_user_id(email)", lambda: bound.resolve_user_id("found@example.com")),
|
|
173
|
+
CallSpec("resolve_user_id(@username)", lambda: bound.resolve_user_id("@testuser")),
|
|
172
174
|
]
|
|
173
175
|
|
|
174
176
|
run_smoke("Users smoke (all public methods)", specs)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for slack_objects.usergroups
|
|
3
|
+
=======================================
|
|
4
|
+
|
|
5
|
+
All Slack API calls are mocked via ``api.call`` to test logic in isolation.
|
|
6
|
+
|
|
7
|
+
Run: python -m pytest tests/UnitTests/usergroups_test.py -v --tb=short
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from unittest.mock import MagicMock
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from slack_objects.config import SlackObjectsConfig, RateTier
|
|
18
|
+
from slack_objects.api_caller import SlackApiCaller
|
|
19
|
+
from slack_objects.usergroups import Usergroups
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------- helpers ----------
|
|
23
|
+
|
|
24
|
+
def _make_usergroups(
|
|
25
|
+
*,
|
|
26
|
+
usergroup_id: str | None = None,
|
|
27
|
+
team_id: str | None = None,
|
|
28
|
+
) -> tuple[Usergroups, MagicMock]:
|
|
29
|
+
"""Return a ``Usergroups`` instance with a mocked ``api.call``."""
|
|
30
|
+
cfg = SlackObjectsConfig(bot_token="xoxb-fake", team_id=team_id)
|
|
31
|
+
client = MagicMock()
|
|
32
|
+
api = MagicMock(spec=SlackApiCaller)
|
|
33
|
+
ug = Usergroups(
|
|
34
|
+
cfg=cfg,
|
|
35
|
+
client=client,
|
|
36
|
+
api=api,
|
|
37
|
+
logger=logging.getLogger("test"),
|
|
38
|
+
usergroup_id=usergroup_id,
|
|
39
|
+
)
|
|
40
|
+
return ug, api.call
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------- factory ----------
|
|
44
|
+
|
|
45
|
+
class TestWithUsergroup:
|
|
46
|
+
def test_returns_new_bound_instance(self):
|
|
47
|
+
ug, _ = _make_usergroups()
|
|
48
|
+
bound = ug.with_usergroup("S0614TZR7")
|
|
49
|
+
assert bound.usergroup_id == "S0614TZR7"
|
|
50
|
+
assert bound is not ug
|
|
51
|
+
|
|
52
|
+
def test_shares_cfg_and_api(self):
|
|
53
|
+
ug, _ = _make_usergroups()
|
|
54
|
+
bound = ug.with_usergroup("S0614TZR7")
|
|
55
|
+
assert bound.cfg is ug.cfg
|
|
56
|
+
assert bound.api is ug.api
|
|
57
|
+
assert bound.client is ug.client
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------- identifier resolution ----------
|
|
61
|
+
|
|
62
|
+
class TestResolveUsergroupId:
|
|
63
|
+
def test_uses_passed_value(self):
|
|
64
|
+
ug, _ = _make_usergroups(usergroup_id="SBOUND")
|
|
65
|
+
assert ug._resolve_usergroup_id("SPASSED") == "SPASSED"
|
|
66
|
+
|
|
67
|
+
def test_falls_back_to_bound(self):
|
|
68
|
+
ug, _ = _make_usergroups(usergroup_id="SBOUND")
|
|
69
|
+
assert ug._resolve_usergroup_id() == "SBOUND"
|
|
70
|
+
|
|
71
|
+
def test_raises_when_missing(self):
|
|
72
|
+
ug, _ = _make_usergroups()
|
|
73
|
+
with pytest.raises(ValueError, match="usergroup_id is required"):
|
|
74
|
+
ug._resolve_usergroup_id()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------- endpoint wrappers ----------
|
|
78
|
+
|
|
79
|
+
class TestUsergroupsList:
|
|
80
|
+
def test_injects_team_id_when_configured(self):
|
|
81
|
+
ug, mock_call = _make_usergroups(team_id="T1234")
|
|
82
|
+
mock_call.return_value = {"ok": True, "usergroups": []}
|
|
83
|
+
ug._usergroups_list()
|
|
84
|
+
_, kwargs = mock_call.call_args
|
|
85
|
+
assert kwargs.get("team_id") == "T1234"
|
|
86
|
+
|
|
87
|
+
def test_omits_team_id_when_not_configured(self):
|
|
88
|
+
ug, mock_call = _make_usergroups(team_id=None)
|
|
89
|
+
mock_call.return_value = {"ok": True, "usergroups": []}
|
|
90
|
+
ug._usergroups_list()
|
|
91
|
+
_, kwargs = mock_call.call_args
|
|
92
|
+
assert "team_id" not in kwargs
|
|
93
|
+
|
|
94
|
+
def test_does_not_override_explicit_team_id(self):
|
|
95
|
+
ug, mock_call = _make_usergroups(team_id="TCFG")
|
|
96
|
+
mock_call.return_value = {"ok": True, "usergroups": []}
|
|
97
|
+
ug._usergroups_list(team_id="TEXPLICIT")
|
|
98
|
+
_, kwargs = mock_call.call_args
|
|
99
|
+
assert kwargs.get("team_id") == "TEXPLICIT"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TestUsergroupsUsersList:
|
|
103
|
+
def test_injects_team_id_when_configured(self):
|
|
104
|
+
ug, mock_call = _make_usergroups(team_id="T1234")
|
|
105
|
+
mock_call.return_value = {"ok": True, "users": []}
|
|
106
|
+
ug._usergroups_users_list("S123")
|
|
107
|
+
_, kwargs = mock_call.call_args
|
|
108
|
+
assert kwargs.get("team_id") == "T1234"
|
|
109
|
+
assert kwargs.get("usergroup") == "S123"
|
|
110
|
+
|
|
111
|
+
def test_omits_team_id_when_not_configured(self):
|
|
112
|
+
ug, mock_call = _make_usergroups(team_id=None)
|
|
113
|
+
mock_call.return_value = {"ok": True, "users": []}
|
|
114
|
+
ug._usergroups_users_list("S123")
|
|
115
|
+
_, kwargs = mock_call.call_args
|
|
116
|
+
assert "team_id" not in kwargs
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------- get_usergroups ----------
|
|
120
|
+
|
|
121
|
+
class TestGetUsergroups:
|
|
122
|
+
def test_returns_usergroups_list(self):
|
|
123
|
+
ug, mock_call = _make_usergroups()
|
|
124
|
+
mock_call.return_value = {
|
|
125
|
+
"ok": True,
|
|
126
|
+
"usergroups": [
|
|
127
|
+
{"id": "S1", "name": "admins"},
|
|
128
|
+
{"id": "S2", "name": "devs"},
|
|
129
|
+
],
|
|
130
|
+
}
|
|
131
|
+
result = ug.get_usergroups()
|
|
132
|
+
assert len(result) == 2
|
|
133
|
+
assert result[0]["id"] == "S1"
|
|
134
|
+
|
|
135
|
+
def test_returns_empty_list_on_missing_key(self):
|
|
136
|
+
ug, mock_call = _make_usergroups()
|
|
137
|
+
mock_call.return_value = {"ok": True}
|
|
138
|
+
assert ug.get_usergroups() == []
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------- get_members ----------
|
|
142
|
+
|
|
143
|
+
class TestGetMembers:
|
|
144
|
+
def test_returns_correct_shape(self):
|
|
145
|
+
ug, mock_call = _make_usergroups(usergroup_id="S123")
|
|
146
|
+
mock_call.return_value = {"ok": True, "users": ["U1", "U2", "U3"]}
|
|
147
|
+
members = ug.get_members()
|
|
148
|
+
assert members == [
|
|
149
|
+
{"value": "U1", "display": ""},
|
|
150
|
+
{"value": "U2", "display": ""},
|
|
151
|
+
{"value": "U3", "display": ""},
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
def test_accepts_explicit_usergroup_id(self):
|
|
155
|
+
ug, mock_call = _make_usergroups()
|
|
156
|
+
mock_call.return_value = {"ok": True, "users": ["U9"]}
|
|
157
|
+
members = ug.get_members(usergroup_id="SEXPLICIT")
|
|
158
|
+
assert members == [{"value": "U9", "display": ""}]
|
|
159
|
+
|
|
160
|
+
def test_empty_users(self):
|
|
161
|
+
ug, mock_call = _make_usergroups(usergroup_id="S123")
|
|
162
|
+
mock_call.return_value = {"ok": True, "users": []}
|
|
163
|
+
assert ug.get_members() == []
|
|
164
|
+
|
|
165
|
+
def test_raises_without_usergroup_id(self):
|
|
166
|
+
ug, _ = _make_usergroups()
|
|
167
|
+
with pytest.raises(ValueError, match="usergroup_id is required"):
|
|
168
|
+
ug.get_members()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------- is_member ----------
|
|
172
|
+
|
|
173
|
+
class TestIsMember:
|
|
174
|
+
def test_true_when_present(self):
|
|
175
|
+
ug, mock_call = _make_usergroups(usergroup_id="S123")
|
|
176
|
+
mock_call.return_value = {"ok": True, "users": ["U1", "U2"]}
|
|
177
|
+
assert ug.is_member("U2") is True
|
|
178
|
+
|
|
179
|
+
def test_false_when_absent(self):
|
|
180
|
+
ug, mock_call = _make_usergroups(usergroup_id="S123")
|
|
181
|
+
mock_call.return_value = {"ok": True, "users": ["U1", "U2"]}
|
|
182
|
+
assert ug.is_member("U999") is False
|
|
183
|
+
|
|
184
|
+
def test_accepts_explicit_usergroup_id(self):
|
|
185
|
+
ug, mock_call = _make_usergroups()
|
|
186
|
+
mock_call.return_value = {"ok": True, "users": ["UTARGET"]}
|
|
187
|
+
assert ug.is_member("UTARGET", usergroup_id="SEXPLICIT") is True
|
|
188
|
+
|
|
189
|
+
def test_raises_without_usergroup_id(self):
|
|
190
|
+
ug, _ = _make_usergroups()
|
|
191
|
+
with pytest.raises(ValueError, match="usergroup_id is required"):
|
|
192
|
+
ug.is_member("U1")
|
|
@@ -3,6 +3,7 @@ from typing import Any, Dict, Optional
|
|
|
3
3
|
from unittest.mock import MagicMock
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
+
from slack_sdk.errors import SlackApiError
|
|
6
7
|
|
|
7
8
|
from slack_objects.client import SlackObjectsClient
|
|
8
9
|
from slack_objects.config import SlackObjectsConfig, RateTier, USER_ID_RE
|
|
@@ -182,6 +183,9 @@ class TestResolveUserId:
|
|
|
182
183
|
)
|
|
183
184
|
with pytest.raises(LookupError, match="No user found for user ID"):
|
|
184
185
|
users.resolve_user_id("U00GHOST")
|
|
186
|
+
users._scim_request.assert_called_once_with(
|
|
187
|
+
path="Users/U00GHOST", method="GET", raise_for_status=False,
|
|
188
|
+
)
|
|
185
189
|
|
|
186
190
|
def test_email_active_user(self):
|
|
187
191
|
"""Active user email resolves via Web API (fast path)."""
|
|
@@ -221,7 +225,7 @@ class TestResolveUserId:
|
|
|
221
225
|
scim_resp = ScimResponse(ok=True, status_code=200, data={"Resources": []}, text="")
|
|
222
226
|
users.scim_search_user_by_username = MagicMock(return_value=scim_resp)
|
|
223
227
|
|
|
224
|
-
with pytest.raises(LookupError, match="No user found for
|
|
228
|
+
with pytest.raises(LookupError, match="No user found for identifier"):
|
|
225
229
|
users.resolve_user_id("@ghost")
|
|
226
230
|
|
|
227
231
|
def test_bare_username(self):
|
|
@@ -258,6 +262,43 @@ class TestResolveUserId:
|
|
|
258
262
|
)
|
|
259
263
|
with pytest.raises(LookupError, match="No user found for user ID"):
|
|
260
264
|
users.resolve_user_id("@U00GHOST")
|
|
265
|
+
users._scim_request.assert_called_once_with(
|
|
266
|
+
path="Users/U00GHOST", method="GET", raise_for_status=False,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def test_user_id_slack_api_error_falls_back_to_scim(self):
|
|
270
|
+
"""SlackApiError from users.info falls through to SCIM lookup."""
|
|
271
|
+
users = _make_users()
|
|
272
|
+
users.get_user_info = MagicMock(
|
|
273
|
+
side_effect=SlackApiError("users_not_found", response=MagicMock())
|
|
274
|
+
)
|
|
275
|
+
scim_resp = ScimResponse(
|
|
276
|
+
ok=True, status_code=200,
|
|
277
|
+
data={"id": "USCIM", "userName": "scim-user", "active": True},
|
|
278
|
+
text="",
|
|
279
|
+
)
|
|
280
|
+
users._scim_request = MagicMock(return_value=scim_resp)
|
|
281
|
+
|
|
282
|
+
assert users.resolve_user_id("USCIM") == "USCIM"
|
|
283
|
+
users._scim_request.assert_called_once_with(
|
|
284
|
+
path="Users/USCIM", method="GET", raise_for_status=False,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def test_user_id_slack_api_error_and_scim_miss_raises(self):
|
|
288
|
+
"""SlackApiError + SCIM miss still raises LookupError."""
|
|
289
|
+
users = _make_users()
|
|
290
|
+
users.get_user_info = MagicMock(
|
|
291
|
+
side_effect=SlackApiError("users_not_found", response=MagicMock())
|
|
292
|
+
)
|
|
293
|
+
users._scim_request = MagicMock(
|
|
294
|
+
return_value=ScimResponse(ok=False, status_code=404, data={}, text="")
|
|
295
|
+
)
|
|
296
|
+
with pytest.raises(LookupError, match="No user found for user ID"):
|
|
297
|
+
users.resolve_user_id("U00GHOST")
|
|
298
|
+
users._scim_request.assert_called_once_with(
|
|
299
|
+
path="Users/U00GHOST", method="GET", raise_for_status=False,
|
|
300
|
+
)
|
|
301
|
+
|
|
261
302
|
|
|
262
303
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
263
304
|
# is_user_authorized
|
|
@@ -373,7 +414,9 @@ class TestIsActiveScim:
|
|
|
373
414
|
return_value=ScimResponse(ok=True, status_code=200, data={"id": "U_OTHER", "active": True}, text=""),
|
|
374
415
|
)
|
|
375
416
|
assert bound.is_active_scim(user_id="U_OTHER") is True
|
|
376
|
-
bound._scim_request.assert_called_once_with(
|
|
417
|
+
bound._scim_request.assert_called_once_with(
|
|
418
|
+
path="Users/U_OTHER", method="GET", raise_for_status=False,
|
|
419
|
+
)
|
|
377
420
|
|
|
378
421
|
def test_no_user_id_raises(self):
|
|
379
422
|
"""Unbound instance with no user_id raises ValueError."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{slack_objects-0.0.7 → slack_objects-0.0.9}/tests/SCIM/test_scim_users_input_validation_live.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{slack_objects-0.0.7 → slack_objects-0.0.9}/tests/SCIM/test_scim_users_update_attribute_live.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|