slack-objects 0.1.1__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.2.0/.github/copilot-instructions.md +8 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/PKG-INFO +2 -1
- {slack_objects-0.1.1 → slack_objects-0.2.0}/README.md +2 -1
- slack_objects-0.2.0/pytest.ini +15 -0
- slack_objects-0.2.0/src/slack_objects/_version.py +24 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/client.py +1 -1
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/users.py +39 -8
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects.egg-info/PKG-INFO +2 -1
- {slack_objects-0.1.1 → 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.1 → slack_objects-0.2.0}/tests/SCIM/run_all_scim_users_live_tests.py +2 -1
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_update_attribute_live.py +15 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Smoke/users_smoke_test.py +19 -7
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/UnitTests/users_unit_test.py +262 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Users/run_all_users_live_tests.py +2 -1
- {slack_objects-0.1.1 → 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.1/.github/copilot-instructions.md +0 -5
- slack_objects-0.1.1/pytest.ini +0 -3
- slack_objects-0.1.1/src/slack_objects/_version.py +0 -34
- {slack_objects-0.1.1 → slack_objects-0.2.0}/.github/workflows/ci.yml +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/.github/workflows/publish-pypi.yml +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/.github/workflows/publish-testpypi.yml +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/.gitignore +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/LICENSE +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/pyproject.toml +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/setup.cfg +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/__init__.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/api_caller.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/base.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/config.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/conversations.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/files.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/idp_groups.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/messages.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/rate_limits.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/scim_base.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/usergroups.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects/workspaces.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects.egg-info/dependency_links.txt +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects.egg-info/requires.txt +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/src/slack_objects.egg-info/top_level.txt +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Examples/examples_config.example.json +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Examples/users_search_by_email_example.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Examples/users_test_AzureKeyVault.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/SCIM/conftest_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/SCIM/test_scim_idp_groups_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_create_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_deactivate_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_input_validation_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_make_guest_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_reactivate_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_update_email_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Smoke/_smoke_harness.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Smoke/api_caller_smoke_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Smoke/conversations_smoke_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Smoke/files_smoke_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Smoke/idp_groups_smoke_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Smoke/messages_smoke_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Smoke/run_all_smoke.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Smoke/security_smoke_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Smoke/usergroups_smoke_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Smoke/workspaces_smoke_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/UnitTests/config_unit_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/UnitTests/conversations_unit_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/UnitTests/files_unit_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/UnitTests/idp_groups_unit_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/UnitTests/messages_unit_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/UnitTests/rate_limits_unit_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/UnitTests/usergroups_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/UnitTests/workspaces_unit_test.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Users/conftest_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Users/test_users_admin_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Users/test_users_classification_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/Users/test_users_input_validation_live.py +0 -0
- {slack_objects-0.1.1 → slack_objects-0.2.0}/tests/live_test_config.example.json +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Copilot Instructions
|
|
2
|
+
|
|
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.
|
|
5
|
+
- Prefer consistency with the `Users.is_active`/`is_active_scim` pattern when structuring related methods (separate explicit SCIM method from higher-level orchestration).
|
|
6
|
+
|
|
7
|
+
## Testing Guidelines
|
|
8
|
+
- Unit test files in `tests/UnitTests` must follow the naming convention `*_unit_test.py` (not `test_*` prefix). The following files should adhere to this convention: `users_unit_test.py`, `conversations_unit_test.py`, `messages_unit_test.py`, `files_unit_test.py`, `workspaces_unit_test.py`, `idp_groups_unit_test.py`, `config_unit_test.py`, and two others with the `_unit_test.py` suffix.
|
|
@@ -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"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
23
|
+
|
|
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)
|
|
@@ -626,12 +649,20 @@ class Users(ScimMixin, SlackObjectBase):
|
|
|
626
649
|
def scim_update_user_attribute(
|
|
627
650
|
self,
|
|
628
651
|
*,
|
|
629
|
-
user_id: str,
|
|
652
|
+
user_id: Optional[str] = None,
|
|
630
653
|
attribute: str,
|
|
631
654
|
new_value: Any,
|
|
632
655
|
) -> ScimResponse:
|
|
633
|
-
"""SCIM PATCH Users/<id>
|
|
634
|
-
|
|
656
|
+
"""SCIM PATCH Users/<id> for simple scalar attributes.
|
|
657
|
+
|
|
658
|
+
For email updates, use :meth:`scim_update_email` instead — it handles
|
|
659
|
+
the version-specific payload formatting needed to avoid multi-primary
|
|
660
|
+
conflicts.
|
|
661
|
+
"""
|
|
662
|
+
uid = user_id or self.user_id
|
|
663
|
+
if not uid:
|
|
664
|
+
raise ValueError("scim_update_user_attribute requires user_id (passed or bound)")
|
|
665
|
+
validate_scim_id(uid, "user_id")
|
|
635
666
|
|
|
636
667
|
scim_version = self.cfg.scim_version
|
|
637
668
|
if scim_version == "v2":
|
|
@@ -644,7 +675,7 @@ class Users(ScimMixin, SlackObjectBase):
|
|
|
644
675
|
else:
|
|
645
676
|
raise NotImplementedError(f"Invalid SCIM version: {scim_version}")
|
|
646
677
|
|
|
647
|
-
return self._scim_request(path=f"Users/{
|
|
678
|
+
return self._scim_request(path=f"Users/{uid}", method="PATCH", payload=payload)
|
|
648
679
|
|
|
649
680
|
def scim_update_email(
|
|
650
681
|
self,
|
|
@@ -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
|
{slack_objects-0.1.1 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_update_attribute_live.py
RENAMED
|
@@ -190,6 +190,21 @@ class TestScimUpdateUserAttribute:
|
|
|
190
190
|
)
|
|
191
191
|
_pause()
|
|
192
192
|
|
|
193
|
+
# ----- bound user_id (no explicit user_id) -----
|
|
194
|
+
|
|
195
|
+
def test_update_attribute_bound_user(self, ctx, users):
|
|
196
|
+
"""Update attribute using bound user_id (no explicit user_id arg)."""
|
|
197
|
+
bound = users.with_user(ctx.active_member_id)
|
|
198
|
+
resp = bound.scim_update_user_attribute(
|
|
199
|
+
attribute="title",
|
|
200
|
+
new_value="Bound Test Title",
|
|
201
|
+
)
|
|
202
|
+
assert resp.ok, f"Expected ok for bound user: {resp.data}"
|
|
203
|
+
_pause()
|
|
204
|
+
|
|
205
|
+
bound.scim_update_user_attribute(attribute="title", new_value="")
|
|
206
|
+
_pause()
|
|
207
|
+
|
|
193
208
|
# ----- deactivated user -----
|
|
194
209
|
|
|
195
210
|
def test_update_attribute_deactivated_user(self, ctx, users):
|
|
@@ -110,17 +110,18 @@ def main() -> None:
|
|
|
110
110
|
CallSpec("get_user_id_from_email(miss)", lambda: users.get_user_id_from_email("missing@example.com")),
|
|
111
111
|
|
|
112
112
|
# Profile helpers
|
|
113
|
-
CallSpec("get_user_profile(
|
|
113
|
+
CallSpec("get_user_profile()", lambda: bound.get_user_profile()),
|
|
114
114
|
CallSpec("get_user_profile(by id)", lambda: users.get_user_profile("U1")),
|
|
115
|
-
CallSpec("set_user_profile_field(
|
|
115
|
+
CallSpec("set_user_profile_field()", lambda: bound.set_user_profile_field("status_text", "hello")),
|
|
116
116
|
CallSpec("set_user_profile_field(by id)", lambda: users.set_user_profile_field("status_text", "hello", user_id="U1")),
|
|
117
117
|
|
|
118
118
|
# Classification helpers (need attributes)
|
|
119
119
|
CallSpec("is_contingent_worker()", lambda: (_refresh_bound(), bound.is_contingent_worker())),
|
|
120
120
|
CallSpec("is_guest()", lambda: (_refresh_bound(), bound.is_guest())),
|
|
121
121
|
CallSpec("is_active()", lambda: (_refresh_bound(), bound.is_active())),
|
|
122
|
-
CallSpec("is_active(
|
|
122
|
+
CallSpec("is_active(by id)", lambda: (_refresh_bound(), bound.is_active(user_id="U2"))),
|
|
123
123
|
CallSpec("is_active_scim()", lambda: bound.is_active_scim()),
|
|
124
|
+
CallSpec("is_active_scim(by id)", lambda: bound.is_active_scim(user_id="U1")),
|
|
124
125
|
|
|
125
126
|
# Admin helpers
|
|
126
127
|
CallSpec(
|
|
@@ -128,14 +129,15 @@ def main() -> None:
|
|
|
128
129
|
lambda: bound.invite_user(channel_ids=["C1", "C2"], email="new@example.com", team_id="T1"),
|
|
129
130
|
),
|
|
130
131
|
CallSpec("wipe_all_sessions()", lambda: bound.wipe_all_sessions()),
|
|
132
|
+
CallSpec("wipe_all_sessions(by id)", lambda: bound.wipe_all_sessions(user_id="U1")),
|
|
131
133
|
CallSpec("add_to_workspace()", lambda: bound.add_to_workspace("U1", "T1")),
|
|
132
134
|
CallSpec("remove_from_workspace()", lambda: bound.remove_from_workspace("U1", "T1")),
|
|
133
135
|
CallSpec("add_to_conversation()", lambda: bound.add_to_conversation(["U1"], "C1")),
|
|
134
136
|
CallSpec("remove_from_conversation()", lambda: bound.remove_from_conversation("U1", "C1")),
|
|
135
137
|
|
|
136
138
|
# Discovery helper
|
|
137
|
-
CallSpec("get_channels(
|
|
138
|
-
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)),
|
|
139
141
|
|
|
140
142
|
# Authorization helper
|
|
141
143
|
CallSpec(
|
|
@@ -152,24 +154,34 @@ def main() -> None:
|
|
|
152
154
|
"set_guest_expiration_date()",
|
|
153
155
|
lambda: bound.set_guest_expiration_date("2030-01-01", workspace_id="T1"),
|
|
154
156
|
),
|
|
157
|
+
CallSpec(
|
|
158
|
+
"set_guest_expiration_date(by id)",
|
|
159
|
+
lambda: bound.set_guest_expiration_date("2030-01-01", user_id="U1", workspace_id="T1"),
|
|
160
|
+
),
|
|
155
161
|
|
|
156
162
|
# SCIM helpers (scim_version now comes from cfg, not a kwarg)
|
|
157
163
|
CallSpec("scim_create_user()", lambda: bound.scim_create_user("testuser", "test@example.com")),
|
|
158
164
|
CallSpec("scim_deactivate_user()", lambda: bound.scim_deactivate_user("U1")),
|
|
159
165
|
CallSpec("scim_reactivate_user()", lambda: bound.scim_reactivate_user()),
|
|
166
|
+
CallSpec("scim_reactivate_user(by id)", lambda: bound.scim_reactivate_user(user_id="U1")),
|
|
160
167
|
CallSpec(
|
|
161
|
-
"scim_update_user_attribute()",
|
|
168
|
+
"scim_update_user_attribute(by id)",
|
|
162
169
|
lambda: bound.scim_update_user_attribute(user_id="U1", attribute="active", new_value=False),
|
|
163
170
|
),
|
|
171
|
+
CallSpec(
|
|
172
|
+
"scim_update_user_attribute()",
|
|
173
|
+
lambda: bound.scim_update_user_attribute(attribute="active", new_value=False),
|
|
174
|
+
),
|
|
164
175
|
CallSpec(
|
|
165
176
|
"scim_update_email(by id)",
|
|
166
177
|
lambda: bound.scim_update_email(user_id="U1", new_email="newemail@example.com"),
|
|
167
178
|
),
|
|
168
179
|
CallSpec(
|
|
169
|
-
"scim_update_email(
|
|
180
|
+
"scim_update_email()",
|
|
170
181
|
lambda: bound.scim_update_email(new_email="newemail@example.com"),
|
|
171
182
|
),
|
|
172
183
|
CallSpec("make_multi_channel_guest()", lambda: bound.make_multi_channel_guest()),
|
|
184
|
+
CallSpec("make_multi_channel_guest(by id)", lambda: bound.make_multi_channel_guest(user_id="U1")),
|
|
173
185
|
|
|
174
186
|
# SCIM search primitives
|
|
175
187
|
CallSpec("scim_search_user_by_email()", lambda: bound.scim_search_user_by_email("test@example.com")),
|
|
@@ -573,3 +573,265 @@ class TestScimUpdateEmail:
|
|
|
573
573
|
users = _make_users()
|
|
574
574
|
with pytest.raises(ValueError):
|
|
575
575
|
users.scim_update_email(user_id="", new_email="x@example.com")
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
579
|
+
# scim_update_user_attribute
|
|
580
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
581
|
+
|
|
582
|
+
class TestScimUpdateUserAttribute:
|
|
583
|
+
"""scim_update_user_attribute — version-aware SCIM PATCH for scalar attributes."""
|
|
584
|
+
|
|
585
|
+
def test_v2_payload_structure(self):
|
|
586
|
+
"""v2 uses PatchOp schema with replace operation."""
|
|
587
|
+
users = _make_users()
|
|
588
|
+
bound = users.with_user("U1")
|
|
589
|
+
bound._scim_request = MagicMock(
|
|
590
|
+
return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
|
|
591
|
+
)
|
|
592
|
+
bound.scim_update_user_attribute(user_id="U1", attribute="active", new_value=False)
|
|
593
|
+
|
|
594
|
+
bound._scim_request.assert_called_once()
|
|
595
|
+
_, kwargs = bound._scim_request.call_args
|
|
596
|
+
op = kwargs["payload"]["Operations"][0]
|
|
597
|
+
assert op["op"] == "replace"
|
|
598
|
+
assert op["path"] == "active"
|
|
599
|
+
assert op["value"] is False
|
|
600
|
+
|
|
601
|
+
def test_v1_payload_structure(self):
|
|
602
|
+
"""v1 sets attribute directly on payload root."""
|
|
603
|
+
users = _make_users()
|
|
604
|
+
bound = users.with_user("U1")
|
|
605
|
+
object.__setattr__(bound.cfg, "scim_version", "v1")
|
|
606
|
+
bound._scim_request = MagicMock(
|
|
607
|
+
return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
|
|
608
|
+
)
|
|
609
|
+
bound.scim_update_user_attribute(user_id="U1", attribute="active", new_value=False)
|
|
610
|
+
|
|
611
|
+
bound._scim_request.assert_called_once()
|
|
612
|
+
_, kwargs = bound._scim_request.call_args
|
|
613
|
+
assert kwargs["payload"]["active"] is False
|
|
614
|
+
|
|
615
|
+
def test_falls_back_to_bound_user_id(self):
|
|
616
|
+
"""Omitting user_id uses self.user_id."""
|
|
617
|
+
users = _make_users()
|
|
618
|
+
bound = users.with_user("U1")
|
|
619
|
+
bound._scim_request = MagicMock(
|
|
620
|
+
return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
|
|
621
|
+
)
|
|
622
|
+
bound.scim_update_user_attribute(attribute="displayName", new_value="Alice")
|
|
623
|
+
|
|
624
|
+
bound._scim_request.assert_called_once()
|
|
625
|
+
_, kwargs = bound._scim_request.call_args
|
|
626
|
+
assert kwargs["path"] == "Users/U1"
|
|
627
|
+
|
|
628
|
+
def test_explicit_user_id_overrides_bound(self):
|
|
629
|
+
"""Passing user_id explicitly takes precedence over bound user_id."""
|
|
630
|
+
users = _make_users()
|
|
631
|
+
bound = users.with_user("U_IGNORED")
|
|
632
|
+
bound._scim_request = MagicMock(
|
|
633
|
+
return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
|
|
634
|
+
)
|
|
635
|
+
bound.scim_update_user_attribute(user_id="U_OTHER", attribute="active", new_value=True)
|
|
636
|
+
|
|
637
|
+
bound._scim_request.assert_called_once()
|
|
638
|
+
_, kwargs = bound._scim_request.call_args
|
|
639
|
+
assert kwargs["path"] == "Users/U_OTHER"
|
|
640
|
+
|
|
641
|
+
def test_no_user_id_raises(self):
|
|
642
|
+
"""Unbound instance with no user_id raises ValueError."""
|
|
643
|
+
users = _make_users()
|
|
644
|
+
with pytest.raises(ValueError, match="requires user_id"):
|
|
645
|
+
users.scim_update_user_attribute(attribute="active", new_value=False)
|
|
646
|
+
|
|
647
|
+
def test_invalid_scim_version_raises(self):
|
|
648
|
+
"""Unsupported scim_version raises NotImplementedError."""
|
|
649
|
+
users = _make_users()
|
|
650
|
+
bound = users.with_user("U1")
|
|
651
|
+
object.__setattr__(bound.cfg, "scim_version", "v99")
|
|
652
|
+
with pytest.raises(NotImplementedError, match="Invalid SCIM version"):
|
|
653
|
+
bound.scim_update_user_attribute(attribute="active", new_value=False)
|
|
654
|
+
|
|
655
|
+
def test_invalid_user_id_raises(self):
|
|
656
|
+
"""Path-traversal user_id is rejected by validate_scim_id."""
|
|
657
|
+
users = _make_users()
|
|
658
|
+
with pytest.raises(ValueError):
|
|
659
|
+
users.scim_update_user_attribute(user_id="../../bad", attribute="active", new_value=False)
|
|
660
|
+
|
|
661
|
+
def test_empty_user_id_raises(self):
|
|
662
|
+
"""Empty user_id is rejected by validate_scim_id."""
|
|
663
|
+
users = _make_users()
|
|
664
|
+
with pytest.raises(ValueError):
|
|
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)
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
# Copilot Instructions
|
|
2
|
-
|
|
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.
|
|
5
|
-
- Prefer consistency with the `Users.is_active`/`is_active_scim` pattern when structuring related methods (separate explicit SCIM method from higher-level orchestration).
|
slack_objects-0.1.1/pytest.ini
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# file generated by setuptools-scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
__all__ = [
|
|
5
|
-
"__version__",
|
|
6
|
-
"__version_tuple__",
|
|
7
|
-
"version",
|
|
8
|
-
"version_tuple",
|
|
9
|
-
"__commit_id__",
|
|
10
|
-
"commit_id",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
TYPE_CHECKING = False
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
from typing import Union
|
|
17
|
-
|
|
18
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
-
COMMIT_ID = Union[str, None]
|
|
20
|
-
else:
|
|
21
|
-
VERSION_TUPLE = object
|
|
22
|
-
COMMIT_ID = object
|
|
23
|
-
|
|
24
|
-
version: str
|
|
25
|
-
__version__: str
|
|
26
|
-
__version_tuple__: VERSION_TUPLE
|
|
27
|
-
version_tuple: VERSION_TUPLE
|
|
28
|
-
commit_id: COMMIT_ID
|
|
29
|
-
__commit_id__: COMMIT_ID
|
|
30
|
-
|
|
31
|
-
__version__ = version = '0.1.1'
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
33
|
-
|
|
34
|
-
__commit_id__ = commit_id = 'g74ff4e47f'
|
|
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.1 → slack_objects-0.2.0}/tests/SCIM/test_scim_users_input_validation_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
|
|
File without changes
|
|
File without changes
|