slack-objects 0.1.2__tar.gz → 0.2.0__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.1.2 → slack_objects-0.2.0}/PKG-INFO +2 -1
- {slack_objects-0.1.2 → slack_objects-0.2.0}/README.md +2 -1
- slack_objects-0.2.0/pytest.ini +15 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/_version.py +3 -3
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/client.py +1 -1
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/users.py +27 -4
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects.egg-info/PKG-INFO +2 -1
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects.egg-info/SOURCES.txt +3 -0
- slack_objects-0.2.0/src/slack_objects.egg-info/scm_file_list.json +68 -0
- slack_objects-0.2.0/src/slack_objects.egg-info/scm_version.json +8 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/SCIM/run_all_scim_users_live_tests.py +2 -1
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Smoke/users_smoke_test.py +2 -2
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/UnitTests/users_unit_test.py +172 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Users/run_all_users_live_tests.py +2 -1
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Users/test_users_read_live.py +20 -9
- slack_objects-0.2.0/tests/conftest.py +31 -0
- slack_objects-0.1.2/pytest.ini +0 -3
- {slack_objects-0.1.2 → slack_objects-0.2.0}/.github/copilot-instructions.md +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/.github/workflows/ci.yml +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/.github/workflows/publish-pypi.yml +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/.github/workflows/publish-testpypi.yml +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/.gitignore +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/LICENSE +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/pyproject.toml +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/setup.cfg +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/__init__.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/api_caller.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/base.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/config.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/conversations.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/files.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/idp_groups.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/messages.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/rate_limits.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/scim_base.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/usergroups.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects/workspaces.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects.egg-info/dependency_links.txt +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects.egg-info/requires.txt +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/src/slack_objects.egg-info/top_level.txt +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Examples/examples_config.example.json +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Examples/users_search_by_email_example.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Examples/users_test_AzureKeyVault.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/SCIM/conftest_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/SCIM/test_scim_idp_groups_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_create_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_deactivate_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_input_validation_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_make_guest_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_reactivate_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_update_attribute_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_update_email_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Smoke/_smoke_harness.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Smoke/api_caller_smoke_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Smoke/conversations_smoke_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Smoke/files_smoke_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Smoke/idp_groups_smoke_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Smoke/messages_smoke_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Smoke/run_all_smoke.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Smoke/security_smoke_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Smoke/usergroups_smoke_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Smoke/workspaces_smoke_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/UnitTests/config_unit_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/UnitTests/conversations_unit_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/UnitTests/files_unit_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/UnitTests/idp_groups_unit_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/UnitTests/messages_unit_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/UnitTests/rate_limits_unit_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/UnitTests/usergroups_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/UnitTests/workspaces_unit_test.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Users/conftest_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Users/test_users_admin_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Users/test_users_classification_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/Users/test_users_input_validation_live.py +0 -0
- {slack_objects-0.1.2 → slack_objects-0.2.0}/tests/live_test_config.example.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slack-objects
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -198,3 +198,4 @@ explicit per-call tier → method-specific override → prefix rule → `default
|
|
|
198
198
|
- `is_user_authorized` supports IdP-group-based authorization checks with configurable read/write access levels
|
|
199
199
|
- SCIM user operations are available on `Users`: create, deactivate, reactivate, update attributes, update email, and convert to multi-channel guest
|
|
200
200
|
- Discovery API is used for `Users.get_channels` and `Conversations.get_members` (requires appropriate token scopes)
|
|
201
|
+
- `Users.get_channels(include_channels_user_left=True)` and `Conversations.get_members(include_members_who_left=True)` opt into historical data; both default to `False` so the common case makes fewer paginated calls
|
|
@@ -176,4 +176,5 @@ explicit per-call tier → method-specific override → prefix rule → `default
|
|
|
176
176
|
- `resolve_user_id` accepts flexible identifiers (user ID, email, or @username) and verifies existence via Web API + SCIM fallback
|
|
177
177
|
- `is_user_authorized` supports IdP-group-based authorization checks with configurable read/write access levels
|
|
178
178
|
- SCIM user operations are available on `Users`: create, deactivate, reactivate, update attributes, update email, and convert to multi-channel guest
|
|
179
|
-
- Discovery API is used for `Users.get_channels` and `Conversations.get_members` (requires appropriate token scopes)
|
|
179
|
+
- Discovery API is used for `Users.get_channels` and `Conversations.get_members` (requires appropriate token scopes)
|
|
180
|
+
- `Users.get_channels(include_channels_user_left=True)` and `Conversations.get_members(include_members_who_left=True)` opt into historical data; both default to `False` so the common case makes fewer paginated calls
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[pytest]
|
|
2
|
+
pythonpath =
|
|
3
|
+
src
|
|
4
|
+
|
|
5
|
+
# Test tiers
|
|
6
|
+
# ----------
|
|
7
|
+
# live: hits real Azure Key Vault + real Slack APIs. Applied automatically to
|
|
8
|
+
# every '*_live.py' file by tests/conftest.py. Some live tests (tests/SCIM)
|
|
9
|
+
# mutate real users, so they are NOT run by default.
|
|
10
|
+
markers =
|
|
11
|
+
live: requires real credentials and makes live Slack API calls (deselected by default)
|
|
12
|
+
|
|
13
|
+
# Deselect the live tier by default so a bare 'pytest' is safe to run.
|
|
14
|
+
# Override with '-m live' (only live tests) or '-m "live or not live"' (everything).
|
|
15
|
+
addopts = -m "not live"
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g7fa644b5a'
|
|
@@ -18,7 +18,7 @@ class SlackObjectsClient:
|
|
|
18
18
|
Owns config, Slack client, and rate-limited API caller.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
def __init__(self, cfg: SlackObjectsConfig, logger: logging.Logger
|
|
21
|
+
def __init__(self, cfg: SlackObjectsConfig, logger: Optional[logging.Logger] = None):
|
|
22
22
|
self.cfg = cfg
|
|
23
23
|
self.logger = logger or logging.getLogger("slack-objects")
|
|
24
24
|
|
|
@@ -404,19 +404,39 @@ class Users(ScimMixin, SlackObjectBase):
|
|
|
404
404
|
|
|
405
405
|
# ---------- discovery helper ----------
|
|
406
406
|
|
|
407
|
-
def get_channels(self, user_id: str,
|
|
407
|
+
def get_channels(self, user_id: str, include_channels_user_left: bool = False) -> List[Dict[str, Any]]:
|
|
408
408
|
"""
|
|
409
409
|
discovery.user.conversations, paginated by offset.
|
|
410
410
|
|
|
411
|
+
Args:
|
|
412
|
+
user_id: The Slack user whose conversations should be listed.
|
|
413
|
+
include_channels_user_left: When True, also return channels the user
|
|
414
|
+
has left (those with a non-zero ``date_left``). Mirrors
|
|
415
|
+
``Conversations.get_members(include_members_who_left=...)``.
|
|
416
|
+
|
|
417
|
+
Note:
|
|
418
|
+
Slack only returns channels the user has left when ``include_historical``
|
|
419
|
+
is sent, so this flag controls the *request* and not just local filtering.
|
|
420
|
+
Leaving it False keeps the common case cheaper: Slack returns fewer
|
|
421
|
+
channels, which also means fewer paginated calls.
|
|
422
|
+
|
|
411
423
|
Returns:
|
|
412
424
|
- If errors occur: a list of error dicts (legacy behavior preserved)
|
|
413
|
-
- Else:
|
|
425
|
+
- Else: the user's channels, including left channels only when
|
|
426
|
+
``include_channels_user_left`` is True
|
|
414
427
|
"""
|
|
415
428
|
current_channels: List[Dict[str, Any]] = []
|
|
416
429
|
all_channels: List[Dict[str, Any]] = []
|
|
417
430
|
errors: List[Dict[str, Any]] = []
|
|
418
431
|
|
|
419
|
-
payload: Dict[str, Any] = {
|
|
432
|
+
payload: Dict[str, Any] = {
|
|
433
|
+
"user": user_id,
|
|
434
|
+
"limit": 1000,
|
|
435
|
+
# Sent explicitly in both states so the behavior does not depend on
|
|
436
|
+
# Slack's server-side default. The Slack SDK serializes booleans to
|
|
437
|
+
# "0"/"1", so False is transmitted correctly.
|
|
438
|
+
"include_historical": include_channels_user_left,
|
|
439
|
+
}
|
|
420
440
|
|
|
421
441
|
while True:
|
|
422
442
|
resp = self._discovery_user_conversations(payload)
|
|
@@ -428,6 +448,7 @@ class Users(ScimMixin, SlackObjectBase):
|
|
|
428
448
|
channels = resp.get("channels") or []
|
|
429
449
|
for ch in channels:
|
|
430
450
|
all_channels.append(ch)
|
|
451
|
+
# date_left is 0 for channels the user is still a member of.
|
|
431
452
|
if ch.get("date_left", 0) == 0:
|
|
432
453
|
current_channels.append(ch)
|
|
433
454
|
|
|
@@ -440,7 +461,9 @@ class Users(ScimMixin, SlackObjectBase):
|
|
|
440
461
|
if errors:
|
|
441
462
|
return errors # preserve legacy behavior
|
|
442
463
|
|
|
443
|
-
|
|
464
|
+
# The filter is a safety net: when include_historical is False Slack
|
|
465
|
+
# should not return left channels in the first place.
|
|
466
|
+
return all_channels if include_channels_user_left else current_channels
|
|
444
467
|
|
|
445
468
|
# ============================================================
|
|
446
469
|
# SCIM public methods (use inherited ScimMixin._scim_request)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slack-objects
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -198,3 +198,4 @@ explicit per-call tier → method-specific override → prefix rule → `default
|
|
|
198
198
|
- `is_user_authorized` supports IdP-group-based authorization checks with configurable read/write access levels
|
|
199
199
|
- SCIM user operations are available on `Users`: create, deactivate, reactivate, update attributes, update email, and convert to multi-channel guest
|
|
200
200
|
- Discovery API is used for `Users.get_channels` and `Conversations.get_members` (requires appropriate token scopes)
|
|
201
|
+
- `Users.get_channels(include_channels_user_left=True)` and `Conversations.get_members(include_members_who_left=True)` opt into historical data; both default to `False` so the common case makes fewer paginated calls
|
|
@@ -26,7 +26,10 @@ src/slack_objects.egg-info/PKG-INFO
|
|
|
26
26
|
src/slack_objects.egg-info/SOURCES.txt
|
|
27
27
|
src/slack_objects.egg-info/dependency_links.txt
|
|
28
28
|
src/slack_objects.egg-info/requires.txt
|
|
29
|
+
src/slack_objects.egg-info/scm_file_list.json
|
|
30
|
+
src/slack_objects.egg-info/scm_version.json
|
|
29
31
|
src/slack_objects.egg-info/top_level.txt
|
|
32
|
+
tests/conftest.py
|
|
30
33
|
tests/live_test_config.example.json
|
|
31
34
|
tests/Examples/examples_config.example.json
|
|
32
35
|
tests/Examples/users_search_by_email_example.py
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
"pyproject.toml",
|
|
4
|
+
"LICENSE",
|
|
5
|
+
"README.md",
|
|
6
|
+
"pytest.ini",
|
|
7
|
+
".gitignore",
|
|
8
|
+
".github/copilot-instructions.md",
|
|
9
|
+
".github/workflows/publish-testpypi.yml",
|
|
10
|
+
".github/workflows/publish-pypi.yml",
|
|
11
|
+
".github/workflows/ci.yml",
|
|
12
|
+
"tests/live_test_config.example.json",
|
|
13
|
+
"tests/conftest.py",
|
|
14
|
+
"tests/UnitTests/usergroups_test.py",
|
|
15
|
+
"tests/UnitTests/rate_limits_unit_test.py",
|
|
16
|
+
"tests/UnitTests/files_unit_test.py",
|
|
17
|
+
"tests/UnitTests/idp_groups_unit_test.py",
|
|
18
|
+
"tests/UnitTests/messages_unit_test.py",
|
|
19
|
+
"tests/UnitTests/workspaces_unit_test.py",
|
|
20
|
+
"tests/UnitTests/users_unit_test.py",
|
|
21
|
+
"tests/UnitTests/conversations_unit_test.py",
|
|
22
|
+
"tests/UnitTests/config_unit_test.py",
|
|
23
|
+
"tests/SCIM/test_scim_users_make_guest_live.py",
|
|
24
|
+
"tests/SCIM/test_scim_users_update_email_live.py",
|
|
25
|
+
"tests/SCIM/test_scim_users_reactivate_live.py",
|
|
26
|
+
"tests/SCIM/conftest_live.py",
|
|
27
|
+
"tests/SCIM/test_scim_users_update_attribute_live.py",
|
|
28
|
+
"tests/SCIM/test_scim_users_input_validation_live.py",
|
|
29
|
+
"tests/SCIM/run_all_scim_users_live_tests.py",
|
|
30
|
+
"tests/SCIM/test_scim_users_create_live.py",
|
|
31
|
+
"tests/SCIM/test_scim_users_deactivate_live.py",
|
|
32
|
+
"tests/SCIM/test_scim_idp_groups_live.py",
|
|
33
|
+
"tests/Smoke/idp_groups_smoke_test.py",
|
|
34
|
+
"tests/Smoke/security_smoke_test.py",
|
|
35
|
+
"tests/Smoke/api_caller_smoke_test.py",
|
|
36
|
+
"tests/Smoke/run_all_smoke.py",
|
|
37
|
+
"tests/Smoke/_smoke_harness.py",
|
|
38
|
+
"tests/Smoke/users_smoke_test.py",
|
|
39
|
+
"tests/Smoke/files_smoke_test.py",
|
|
40
|
+
"tests/Smoke/workspaces_smoke_test.py",
|
|
41
|
+
"tests/Smoke/conversations_smoke_test.py",
|
|
42
|
+
"tests/Smoke/messages_smoke_test.py",
|
|
43
|
+
"tests/Smoke/usergroups_smoke_test.py",
|
|
44
|
+
"tests/Users/test_users_read_live.py",
|
|
45
|
+
"tests/Users/conftest_live.py",
|
|
46
|
+
"tests/Users/test_users_input_validation_live.py",
|
|
47
|
+
"tests/Users/run_all_users_live_tests.py",
|
|
48
|
+
"tests/Users/test_users_classification_live.py",
|
|
49
|
+
"tests/Users/test_users_admin_live.py",
|
|
50
|
+
"tests/Examples/users_search_by_email_example.py",
|
|
51
|
+
"tests/Examples/users_test_AzureKeyVault.py",
|
|
52
|
+
"tests/Examples/examples_config.example.json",
|
|
53
|
+
"src/slack_objects/workspaces.py",
|
|
54
|
+
"src/slack_objects/usergroups.py",
|
|
55
|
+
"src/slack_objects/conversations.py",
|
|
56
|
+
"src/slack_objects/messages.py",
|
|
57
|
+
"src/slack_objects/users.py",
|
|
58
|
+
"src/slack_objects/config.py",
|
|
59
|
+
"src/slack_objects/__init__.py",
|
|
60
|
+
"src/slack_objects/client.py",
|
|
61
|
+
"src/slack_objects/files.py",
|
|
62
|
+
"src/slack_objects/api_caller.py",
|
|
63
|
+
"src/slack_objects/scim_base.py",
|
|
64
|
+
"src/slack_objects/rate_limits.py",
|
|
65
|
+
"src/slack_objects/base.py",
|
|
66
|
+
"src/slack_objects/idp_groups.py"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
@@ -49,7 +49,8 @@ def main() -> int:
|
|
|
49
49
|
print(f"{'═' * 70}\n")
|
|
50
50
|
|
|
51
51
|
rc = subprocess.call(
|
|
52
|
-
|
|
52
|
+
# '-m live' re-enables the live tier, which pytest.ini deselects by default.
|
|
53
|
+
[sys.executable, "-m", "pytest", "-m", "live", str(filepath), *extra_args],
|
|
53
54
|
cwd=str(_HERE.parent.parent), # workspace root
|
|
54
55
|
)
|
|
55
56
|
results[filename] = rc
|
|
@@ -136,8 +136,8 @@ def main() -> None:
|
|
|
136
136
|
CallSpec("remove_from_conversation()", lambda: bound.remove_from_conversation("U1", "C1")),
|
|
137
137
|
|
|
138
138
|
# Discovery helper
|
|
139
|
-
CallSpec("get_channels(
|
|
140
|
-
CallSpec("get_channels(
|
|
139
|
+
CallSpec("get_channels(current only)", lambda: bound.get_channels("U1")),
|
|
140
|
+
CallSpec("get_channels(including left)", lambda: bound.get_channels("U1", include_channels_user_left=True)),
|
|
141
141
|
|
|
142
142
|
# Authorization helper
|
|
143
143
|
CallSpec(
|
|
@@ -663,3 +663,175 @@ class TestScimUpdateUserAttribute:
|
|
|
663
663
|
users = _make_users()
|
|
664
664
|
with pytest.raises(ValueError):
|
|
665
665
|
users.scim_update_user_attribute(user_id="", attribute="active", new_value=False)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
669
|
+
# get_channels (discovery.user.conversations)
|
|
670
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
671
|
+
|
|
672
|
+
class FakeDiscoveryWebClient:
|
|
673
|
+
"""Fake discovery.user.conversations that honors include_historical.
|
|
674
|
+
|
|
675
|
+
Mirrors real Slack behavior confirmed against the Discovery API: channels the
|
|
676
|
+
user has left are returned ONLY when include_historical is truthy. The Slack
|
|
677
|
+
SDK serializes booleans to "0"/"1", so this accepts both forms.
|
|
678
|
+
|
|
679
|
+
Also records every payload it receives so tests can assert on the request.
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
CURRENT = [
|
|
683
|
+
{"id": "C001", "date_joined": 1665776256, "date_left": 0},
|
|
684
|
+
{"id": "C002", "date_joined": 1665776257, "date_left": 0},
|
|
685
|
+
]
|
|
686
|
+
LEFT = [
|
|
687
|
+
{"id": "C900", "date_joined": 1665776258, "date_left": 1700000000},
|
|
688
|
+
]
|
|
689
|
+
|
|
690
|
+
def __init__(self, paginate: bool = False):
|
|
691
|
+
self.payloads: list = []
|
|
692
|
+
self.paginate = paginate
|
|
693
|
+
|
|
694
|
+
@staticmethod
|
|
695
|
+
def _is_truthy(value) -> bool:
|
|
696
|
+
# Accept True, "1", and 1; the SDK converts booleans to "0"/"1".
|
|
697
|
+
return value in (True, 1, "1")
|
|
698
|
+
|
|
699
|
+
def api_call(self, method: str, json=None, params=None):
|
|
700
|
+
payload = json if json is not None else (params or {})
|
|
701
|
+
|
|
702
|
+
if method != "discovery.user.conversations":
|
|
703
|
+
return {"ok": True}
|
|
704
|
+
|
|
705
|
+
self.payloads.append(dict(payload))
|
|
706
|
+
|
|
707
|
+
include_historical = self._is_truthy(payload.get("include_historical"))
|
|
708
|
+
channels = list(self.CURRENT)
|
|
709
|
+
if include_historical:
|
|
710
|
+
channels += self.LEFT
|
|
711
|
+
|
|
712
|
+
# Emit two pages on the first call when pagination is being exercised.
|
|
713
|
+
if self.paginate and "offset" not in payload:
|
|
714
|
+
return {"ok": True, "channels": channels, "offset": "page2"}
|
|
715
|
+
|
|
716
|
+
return {"ok": True, "channels": channels}
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
class FakeDiscoveryErrorWebClient:
|
|
720
|
+
"""Fake that always fails, to exercise the legacy error-list behavior."""
|
|
721
|
+
|
|
722
|
+
def api_call(self, method: str, json=None, params=None):
|
|
723
|
+
if method == "discovery.user.conversations":
|
|
724
|
+
return {"ok": False, "error": "user_not_found"}
|
|
725
|
+
return {"ok": True}
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _make_users_with_client(web_client):
|
|
729
|
+
"""Helper: build a Users instance wired to a specific fake web client."""
|
|
730
|
+
cfg = SlackObjectsConfig(
|
|
731
|
+
bot_token="xoxb-fake",
|
|
732
|
+
user_token="xoxp-fake",
|
|
733
|
+
scim_token="xoxp-fake",
|
|
734
|
+
default_rate_tier=RateTier.TIER_3,
|
|
735
|
+
)
|
|
736
|
+
slack = SlackObjectsClient(cfg, logger=logging.getLogger("test"))
|
|
737
|
+
slack.web_client = web_client
|
|
738
|
+
slack.api = FakeApiCaller(cfg)
|
|
739
|
+
slack._users = None
|
|
740
|
+
return slack.users()
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
class TestGetChannels:
|
|
744
|
+
"""get_channels — include_channels_user_left controls the request, not just filtering."""
|
|
745
|
+
|
|
746
|
+
def test_default_does_not_request_historical(self):
|
|
747
|
+
"""Default call must send include_historical=False.
|
|
748
|
+
|
|
749
|
+
This is the efficiency guarantee: the common case should not make Slack
|
|
750
|
+
return channels the user left only for them to be filtered out locally.
|
|
751
|
+
"""
|
|
752
|
+
fake = FakeDiscoveryWebClient()
|
|
753
|
+
users = _make_users_with_client(fake)
|
|
754
|
+
|
|
755
|
+
users.get_channels("U1")
|
|
756
|
+
|
|
757
|
+
assert fake.payloads[0]["include_historical"] is False
|
|
758
|
+
|
|
759
|
+
def test_include_left_requests_historical(self):
|
|
760
|
+
"""include_channels_user_left=True must send include_historical=True."""
|
|
761
|
+
fake = FakeDiscoveryWebClient()
|
|
762
|
+
users = _make_users_with_client(fake)
|
|
763
|
+
|
|
764
|
+
users.get_channels("U1", include_channels_user_left=True)
|
|
765
|
+
|
|
766
|
+
assert fake.payloads[0]["include_historical"] is True
|
|
767
|
+
|
|
768
|
+
def test_default_returns_only_current_channels(self):
|
|
769
|
+
"""Default call returns just the channels the user is still in."""
|
|
770
|
+
users = _make_users_with_client(FakeDiscoveryWebClient())
|
|
771
|
+
|
|
772
|
+
result = users.get_channels("U1")
|
|
773
|
+
|
|
774
|
+
assert [c["id"] for c in result] == ["C001", "C002"]
|
|
775
|
+
|
|
776
|
+
def test_include_left_returns_current_and_left(self):
|
|
777
|
+
"""include_channels_user_left=True returns left channels too."""
|
|
778
|
+
users = _make_users_with_client(FakeDiscoveryWebClient())
|
|
779
|
+
|
|
780
|
+
result = users.get_channels("U1", include_channels_user_left=True)
|
|
781
|
+
|
|
782
|
+
assert [c["id"] for c in result] == ["C001", "C002", "C900"]
|
|
783
|
+
|
|
784
|
+
def test_include_left_returns_more_than_default(self):
|
|
785
|
+
"""The two modes must not return identical data (the original bug)."""
|
|
786
|
+
users = _make_users_with_client(FakeDiscoveryWebClient())
|
|
787
|
+
|
|
788
|
+
current = users.get_channels("U1")
|
|
789
|
+
including_left = users.get_channels("U1", include_channels_user_left=True)
|
|
790
|
+
|
|
791
|
+
assert len(including_left) > len(current)
|
|
792
|
+
|
|
793
|
+
def test_request_includes_user_and_limit(self):
|
|
794
|
+
"""Payload keeps the documented user/limit parameters."""
|
|
795
|
+
fake = FakeDiscoveryWebClient()
|
|
796
|
+
users = _make_users_with_client(fake)
|
|
797
|
+
|
|
798
|
+
users.get_channels("U1")
|
|
799
|
+
|
|
800
|
+
assert fake.payloads[0]["user"] == "U1"
|
|
801
|
+
assert fake.payloads[0]["limit"] == 1000
|
|
802
|
+
|
|
803
|
+
def test_pagination_follows_offset(self):
|
|
804
|
+
"""An offset in the response triggers another call and results accumulate."""
|
|
805
|
+
fake = FakeDiscoveryWebClient(paginate=True)
|
|
806
|
+
users = _make_users_with_client(fake)
|
|
807
|
+
|
|
808
|
+
result = users.get_channels("U1")
|
|
809
|
+
|
|
810
|
+
assert len(fake.payloads) == 2
|
|
811
|
+
assert fake.payloads[1]["offset"] == "page2"
|
|
812
|
+
assert len(result) == 4 # two current channels per page
|
|
813
|
+
|
|
814
|
+
def test_pagination_preserves_historical_flag(self):
|
|
815
|
+
"""include_historical must persist across paginated requests."""
|
|
816
|
+
fake = FakeDiscoveryWebClient(paginate=True)
|
|
817
|
+
users = _make_users_with_client(fake)
|
|
818
|
+
|
|
819
|
+
users.get_channels("U1", include_channels_user_left=True)
|
|
820
|
+
|
|
821
|
+
assert all(p["include_historical"] is True for p in fake.payloads)
|
|
822
|
+
|
|
823
|
+
def test_api_error_returns_error_list(self):
|
|
824
|
+
"""Legacy behavior: API failures come back as a list of error dicts."""
|
|
825
|
+
users = _make_users_with_client(FakeDiscoveryErrorWebClient())
|
|
826
|
+
|
|
827
|
+
result = users.get_channels("U1")
|
|
828
|
+
|
|
829
|
+
assert isinstance(result, list)
|
|
830
|
+
assert result[0]["message"] == "user_not_found"
|
|
831
|
+
|
|
832
|
+
def test_active_only_no_longer_accepted(self):
|
|
833
|
+
"""The old active_only parameter was replaced and must not silently work."""
|
|
834
|
+
users = _make_users_with_client(FakeDiscoveryWebClient())
|
|
835
|
+
|
|
836
|
+
with pytest.raises(TypeError):
|
|
837
|
+
users.get_channels("U1", active_only=False)
|
|
@@ -45,7 +45,8 @@ def main() -> int:
|
|
|
45
45
|
print(f"{'═' * 70}\n")
|
|
46
46
|
|
|
47
47
|
rc = subprocess.call(
|
|
48
|
-
|
|
48
|
+
# '-m live' re-enables the live tier, which pytest.ini deselects by default.
|
|
49
|
+
[sys.executable, "-m", "pytest", "-m", "live", str(filepath)] + extra_args
|
|
49
50
|
)
|
|
50
51
|
results[filename] = rc
|
|
51
52
|
|
|
@@ -311,22 +311,33 @@ class TestGetChannels:
|
|
|
311
311
|
assert isinstance(result, list)
|
|
312
312
|
_pause()
|
|
313
313
|
|
|
314
|
-
def
|
|
315
|
-
"""
|
|
316
|
-
|
|
314
|
+
def test_current_only_subset_of_including_left(self, ctx, users):
|
|
315
|
+
"""Default (current only) should return ≤ include_channels_user_left=True."""
|
|
316
|
+
current = users.get_channels(ctx.active_member_id)
|
|
317
317
|
_pause()
|
|
318
|
-
|
|
318
|
+
including_left = users.get_channels(ctx.active_member_id, include_channels_user_left=True)
|
|
319
319
|
_pause()
|
|
320
|
-
assert len(
|
|
321
|
-
f"
|
|
320
|
+
assert len(current) <= len(including_left), (
|
|
321
|
+
f"current only ({len(current)}) should be ≤ including left ({len(including_left)})"
|
|
322
322
|
)
|
|
323
323
|
|
|
324
|
-
def
|
|
325
|
-
"""
|
|
326
|
-
result = users.get_channels(ctx.active_member_id,
|
|
324
|
+
def test_including_left_returns_list(self, ctx, users):
|
|
325
|
+
"""include_channels_user_left=True should still return a list."""
|
|
326
|
+
result = users.get_channels(ctx.active_member_id, include_channels_user_left=True)
|
|
327
327
|
assert isinstance(result, list)
|
|
328
328
|
_pause()
|
|
329
329
|
|
|
330
|
+
def test_default_excludes_left_channels(self, ctx, users):
|
|
331
|
+
"""Default call must not return channels the user has left.
|
|
332
|
+
|
|
333
|
+
Guards the regression where include_historical was never sent: the flag
|
|
334
|
+
controls the request, so a default call should contain no date_left != 0.
|
|
335
|
+
"""
|
|
336
|
+
result = users.get_channels(ctx.active_member_id)
|
|
337
|
+
left = [c for c in result if c.get("date_left", 0) != 0]
|
|
338
|
+
assert not left, f"Default call returned {len(left)} channel(s) the user left"
|
|
339
|
+
_pause()
|
|
340
|
+
|
|
330
341
|
|
|
331
342
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
332
343
|
# 6. refresh
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared pytest configuration for the whole test suite.
|
|
3
|
+
|
|
4
|
+
The suite has two tiers:
|
|
5
|
+
|
|
6
|
+
Offline tier - tests/UnitTests/* (fakes only, no network, no credentials)
|
|
7
|
+
Live tier - tests/**/*_live.py (real Azure Key Vault tokens + real Slack API calls)
|
|
8
|
+
|
|
9
|
+
Every live test file follows the '*_live.py' naming convention, so instead of
|
|
10
|
+
decorating hundreds of tests by hand, this hook marks them automatically at
|
|
11
|
+
collection time.
|
|
12
|
+
|
|
13
|
+
pytest.ini deselects the 'live' marker by default, which makes a bare 'pytest'
|
|
14
|
+
run safe: it collects only the offline tier. The live tier is opt-in, since some
|
|
15
|
+
of it (notably tests/SCIM) performs mutating operations against real users.
|
|
16
|
+
|
|
17
|
+
To run the live tier explicitly:
|
|
18
|
+
|
|
19
|
+
pytest -m live tests/Users/test_users_read_live.py
|
|
20
|
+
python tests/Users/run_all_users_live_tests.py
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def pytest_collection_modifyitems(config, items):
|
|
27
|
+
"""Automatically apply the 'live' marker to tests in *_live.py files."""
|
|
28
|
+
for item in items:
|
|
29
|
+
# item.path is the file the test was collected from.
|
|
30
|
+
if item.path is not None and item.path.name.endswith("_live.py"):
|
|
31
|
+
item.add_marker(pytest.mark.live)
|
slack_objects-0.1.2/pytest.ini
DELETED
|
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.1.2 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_input_validation_live.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{slack_objects-0.1.2 → slack_objects-0.2.0}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|