slack-objects 0.0.9__tar.gz → 0.1.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.
Files changed (73) hide show
  1. {slack_objects-0.0.9 → slack_objects-0.1.0}/.gitignore +4 -1
  2. {slack_objects-0.0.9 → slack_objects-0.1.0}/PKG-INFO +1 -1
  3. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/_version.py +3 -3
  4. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/users.py +45 -0
  5. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects.egg-info/PKG-INFO +1 -1
  6. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects.egg-info/SOURCES.txt +1 -2
  7. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/SCIM/conftest_live.py +5 -5
  8. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/SCIM/run_all_scim_users_live_tests.py +1 -0
  9. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/SCIM/test_scim_users_input_validation_live.py +14 -0
  10. slack_objects-0.1.0/tests/SCIM/test_scim_users_update_email_live.py +160 -0
  11. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Smoke/users_smoke_test.py +8 -0
  12. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/UnitTests/users_unit_test.py +94 -0
  13. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Users/conftest_live.py +5 -5
  14. slack_objects-0.0.9/.vs/VSWorkspaceState.json +0 -8
  15. slack_objects-0.0.9/.vs/slnx.sqlite +0 -0
  16. {slack_objects-0.0.9 → slack_objects-0.1.0}/.github/copilot-instructions.md +0 -0
  17. {slack_objects-0.0.9 → slack_objects-0.1.0}/.github/workflows/ci.yml +0 -0
  18. {slack_objects-0.0.9 → slack_objects-0.1.0}/.github/workflows/publish-pypi.yml +0 -0
  19. {slack_objects-0.0.9 → slack_objects-0.1.0}/.github/workflows/publish-testpypi.yml +0 -0
  20. {slack_objects-0.0.9 → slack_objects-0.1.0}/LICENSE +0 -0
  21. {slack_objects-0.0.9 → slack_objects-0.1.0}/README.md +0 -0
  22. {slack_objects-0.0.9 → slack_objects-0.1.0}/pyproject.toml +0 -0
  23. {slack_objects-0.0.9 → slack_objects-0.1.0}/pytest.ini +0 -0
  24. {slack_objects-0.0.9 → slack_objects-0.1.0}/setup.cfg +0 -0
  25. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/__init__.py +0 -0
  26. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/api_caller.py +0 -0
  27. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/base.py +0 -0
  28. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/client.py +0 -0
  29. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/config.py +0 -0
  30. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/conversations.py +0 -0
  31. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/files.py +0 -0
  32. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/idp_groups.py +0 -0
  33. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/messages.py +0 -0
  34. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/rate_limits.py +0 -0
  35. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/scim_base.py +0 -0
  36. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/usergroups.py +0 -0
  37. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects/workspaces.py +0 -0
  38. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects.egg-info/dependency_links.txt +0 -0
  39. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects.egg-info/requires.txt +0 -0
  40. {slack_objects-0.0.9 → slack_objects-0.1.0}/src/slack_objects.egg-info/top_level.txt +0 -0
  41. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Examples/examples_config.example.json +0 -0
  42. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Examples/users_search_by_email_example.py +0 -0
  43. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Examples/users_test_AzureKeyVault.py +0 -0
  44. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/SCIM/test_scim_idp_groups_live.py +0 -0
  45. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/SCIM/test_scim_users_create_live.py +0 -0
  46. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/SCIM/test_scim_users_deactivate_live.py +0 -0
  47. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/SCIM/test_scim_users_make_guest_live.py +0 -0
  48. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/SCIM/test_scim_users_reactivate_live.py +0 -0
  49. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/SCIM/test_scim_users_update_attribute_live.py +0 -0
  50. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Smoke/_smoke_harness.py +0 -0
  51. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Smoke/api_caller_smoke_test.py +0 -0
  52. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Smoke/conversations_smoke_test.py +0 -0
  53. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Smoke/files_smoke_test.py +0 -0
  54. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Smoke/idp_groups_smoke_test.py +0 -0
  55. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Smoke/messages_smoke_test.py +0 -0
  56. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Smoke/run_all_smoke.py +0 -0
  57. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Smoke/security_smoke_test.py +0 -0
  58. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Smoke/usergroups_smoke_test.py +0 -0
  59. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Smoke/workspaces_smoke_test.py +0 -0
  60. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/UnitTests/config_unit_test.py +0 -0
  61. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/UnitTests/conversations_unit_test.py +0 -0
  62. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/UnitTests/files_unit_test.py +0 -0
  63. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/UnitTests/idp_groups_unit_test.py +0 -0
  64. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/UnitTests/messages_unit_test.py +0 -0
  65. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/UnitTests/rate_limits_unit_test.py +0 -0
  66. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/UnitTests/usergroups_test.py +0 -0
  67. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/UnitTests/workspaces_unit_test.py +0 -0
  68. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Users/run_all_users_live_tests.py +0 -0
  69. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Users/test_users_admin_live.py +0 -0
  70. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Users/test_users_classification_live.py +0 -0
  71. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Users/test_users_input_validation_live.py +0 -0
  72. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/Users/test_users_read_live.py +0 -0
  73. {slack_objects-0.0.9 → slack_objects-0.1.0}/tests/live_test_config.example.json +0 -0
@@ -29,4 +29,7 @@ src/slack_objects/_version.py
29
29
  /tests/Examples/__pycache__
30
30
 
31
31
  # Examples config (contains Key Vault / secret names)
32
- /tests/Examples/examples_config.json
32
+ /tests/Examples/examples_config.json
33
+ /.vs/slnx.sqlite
34
+ /.vs/VSWorkspaceState.json
35
+ /src/slack_objects/.vs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slack-objects
3
- Version: 0.0.9
3
+ Version: 0.1.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
@@ -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.9'
32
- __version_tuple__ = version_tuple = (0, 0, 9)
31
+ __version__ = version = '0.1.0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0)
33
33
 
34
- __commit_id__ = commit_id = 'g82c79f1f4'
34
+ __commit_id__ = commit_id = 'gf712fe717'
@@ -646,6 +646,51 @@ class Users(ScimMixin, SlackObjectBase):
646
646
 
647
647
  return self._scim_request(path=f"Users/{user_id}", method="PATCH", payload=payload)
648
648
 
649
+ def scim_update_email(
650
+ self,
651
+ *,
652
+ user_id: Optional[str] = None,
653
+ new_email: str,
654
+ ) -> ScimResponse:
655
+ """Update a user's primary email address via SCIM PATCH.
656
+
657
+ This is intentionally separate from :meth:`scim_update_user_attribute`
658
+ because email updates require a structurally different payload in both
659
+ SCIM versions (value-filter path in v2, typed email array in v1).
660
+
661
+ Accepts a plain email-address string and builds the version-correct
662
+ payload automatically:
663
+ - v2: value-filter path ``emails[primary eq true].value`` to avoid
664
+ the "more than one primary element" conflict.
665
+ - v1: wraps the address in the standard SCIM email array.
666
+ """
667
+ uid = user_id or self.user_id
668
+ if not uid:
669
+ raise ValueError("scim_update_email requires user_id (passed or bound)")
670
+ validate_scim_id(uid, "user_id")
671
+
672
+ scim_version = self.cfg.scim_version
673
+ if scim_version == "v2":
674
+ payload = {
675
+ "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
676
+ "Operations": [
677
+ {
678
+ "op": "replace",
679
+ "path": "emails[primary eq true].value",
680
+ "value": new_email,
681
+ }
682
+ ],
683
+ }
684
+ elif scim_version == "v1":
685
+ payload = {
686
+ "schemas": ["urn:scim:schemas:core:1.0"],
687
+ "emails": [{"value": new_email, "type": "work", "primary": True}],
688
+ }
689
+ else:
690
+ raise NotImplementedError(f"Invalid SCIM version: {scim_version}")
691
+
692
+ return self._scim_request(path=f"Users/{uid}", method="PATCH", payload=payload)
693
+
649
694
  def make_multi_channel_guest(self, user_id: Optional[str] = None) -> ScimResponse:
650
695
  """Convert a user to a multi-channel guest via SCIM PATCH."""
651
696
  uid = user_id or self.user_id
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slack-objects
3
- Version: 0.0.9
3
+ Version: 0.1.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
@@ -7,8 +7,6 @@ pytest.ini
7
7
  .github/workflows/ci.yml
8
8
  .github/workflows/publish-pypi.yml
9
9
  .github/workflows/publish-testpypi.yml
10
- .vs/VSWorkspaceState.json
11
- .vs/slnx.sqlite
12
10
  src/slack_objects/__init__.py
13
11
  src/slack_objects/_version.py
14
12
  src/slack_objects/api_caller.py
@@ -42,6 +40,7 @@ tests/SCIM/test_scim_users_input_validation_live.py
42
40
  tests/SCIM/test_scim_users_make_guest_live.py
43
41
  tests/SCIM/test_scim_users_reactivate_live.py
44
42
  tests/SCIM/test_scim_users_update_attribute_live.py
43
+ tests/SCIM/test_scim_users_update_email_live.py
45
44
  tests/Smoke/_smoke_harness.py
46
45
  tests/Smoke/api_caller_smoke_test.py
47
46
  tests/Smoke/conversations_smoke_test.py
@@ -7,8 +7,8 @@ Test-user IDs and other identifiers are loaded from a JSON file.
7
7
  Setup
8
8
  -----
9
9
  1. Store your Slack tokens in Azure Key Vault:
10
- Bot-token-SB – xoxb-... bot token with users:read and users:read.email scopes
11
- User-token-SB – xoxp-... user token with admin scope (org-level admin + SCIM provisioning)
10
+ Bot-token-TEST – xoxb-... bot token with users:read and users:read.email scopes
11
+ User-token-TEST – xoxp-... user token with admin scope (org-level admin + SCIM provisioning)
12
12
 
13
13
  2. Set the ``_KEYVAULT_NAME`` constant in this file to your Azure Key Vault name.
14
14
 
@@ -51,9 +51,9 @@ _DEFAULT_CONFIG_PATH = _HERE.parent / "live_test_config.json"
51
51
  _KEYVAULT_NAME = "Slack-AI-assistant-KV"
52
52
 
53
53
  # Key Vault secret names
54
- _SECRET_BOT_TOKEN = "Bot-token-SB"
55
- _SECRET_USER_TOKEN = "User-token-SB"
56
- _SECRET_SCIM_TOKEN = "User-token-SB"
54
+ _SECRET_BOT_TOKEN = "Bot-token-TEST"
55
+ _SECRET_USER_TOKEN = "User-token-TEST"
56
+ _SECRET_SCIM_TOKEN = "User-token-TEST"
57
57
 
58
58
 
59
59
  # ---------------------------------------------------------------------------
@@ -25,6 +25,7 @@ _TEST_FILES = [
25
25
  "test_scim_users_input_validation_live.py", # no API calls
26
26
  "test_scim_users_reactivate_live.py", # read-mostly (no-ops on active users)
27
27
  "test_scim_users_update_attribute_live.py", # mutating, reversible
28
+ "test_scim_users_update_email_live.py", # mutating, reversible
28
29
  "test_scim_users_create_live.py", # self-contained (create + delete)
29
30
  "test_scim_users_deactivate_live.py", # mutating, reversible (strips workspaces)
30
31
  "test_scim_users_make_guest_live.py", # DESTRUCTIVE (disposable users only)
@@ -99,4 +99,18 @@ class TestScimInputValidation:
99
99
  with patch.object(users, "_scim_request", wraps=users._scim_request) as spy:
100
100
  with pytest.raises(ValueError):
101
101
  users.make_multi_channel_guest(bad_id)
102
+ spy.assert_not_called()
103
+
104
+ @pytest.mark.parametrize("bad_id", [
105
+ "../traversal",
106
+ "",
107
+ "id with spaces",
108
+ ])
109
+ def test_update_email_rejects_bad_ids(self, users, bad_id):
110
+ with patch.object(users, "_scim_request", wraps=users._scim_request) as spy:
111
+ with pytest.raises(ValueError):
112
+ users.scim_update_email(
113
+ user_id=bad_id,
114
+ new_email="test@example.com",
115
+ )
102
116
  spy.assert_not_called()
@@ -0,0 +1,160 @@
1
+ """
2
+ Live integration tests for scim_update_email.
3
+
4
+ MUTATING but REVERSIBLE — every email change is followed by a restore
5
+ to the original email in teardown.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from typing import Any, Dict, Optional
12
+
13
+ import pytest
14
+ import requests
15
+
16
+ from slack_objects.users import Users
17
+
18
+ from conftest_live import LiveTestContext, build_live_context
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Fixtures
22
+ # ---------------------------------------------------------------------------
23
+
24
+ _ctx: Optional[LiveTestContext] = None
25
+
26
+
27
+ def _get_ctx() -> LiveTestContext:
28
+ global _ctx
29
+ if _ctx is None:
30
+ _ctx = build_live_context()
31
+ return _ctx
32
+
33
+
34
+ @pytest.fixture(scope="module")
35
+ def ctx() -> LiveTestContext:
36
+ return _get_ctx()
37
+
38
+
39
+ @pytest.fixture(scope="module")
40
+ def users(ctx: LiveTestContext) -> Users:
41
+ return ctx.slack.users()
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Helpers
46
+ # ---------------------------------------------------------------------------
47
+
48
+ _SCIM_PAUSE = 4.0
49
+
50
+
51
+ def _pause():
52
+ time.sleep(_SCIM_PAUSE)
53
+
54
+
55
+ def _scim_get_user(users: Users, user_id: str) -> Dict[str, Any]:
56
+ resp = users._scim_request(path=f"Users/{user_id}", method="GET")
57
+ assert resp.ok, f"SCIM GET Users/{user_id} failed: {resp.data}"
58
+ return resp.data
59
+
60
+
61
+ def _get_primary_email(scim_data: Dict[str, Any]) -> str:
62
+ """Extract the primary email from a SCIM user resource."""
63
+ for email in scim_data.get("emails", []):
64
+ if email.get("primary", False):
65
+ return email.get("value", "")
66
+ return ""
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Tests
71
+ # ---------------------------------------------------------------------------
72
+
73
+ class TestScimUpdateEmail:
74
+ """scim_update_email — identifier × type matrix."""
75
+
76
+ # ----- active member -----
77
+
78
+ def test_update_email_active_member(self, ctx, users):
79
+ """Update primary email on an active member, verify, then restore."""
80
+ original_data = _scim_get_user(users, ctx.active_member_id)
81
+ original_email = _get_primary_email(original_data)
82
+ assert original_email, "Could not determine original email"
83
+ _pause()
84
+
85
+ new_email = f"scim-test-{int(time.time())}@example.com"
86
+ resp = users.scim_update_email(
87
+ user_id=ctx.active_member_id,
88
+ new_email=new_email,
89
+ )
90
+ assert resp.ok, f"Expected ok: {resp.data}"
91
+ _pause()
92
+
93
+ updated = _scim_get_user(users, ctx.active_member_id)
94
+ assert _get_primary_email(updated) == new_email
95
+ _pause()
96
+
97
+ # Teardown: restore original email
98
+ users.scim_update_email(
99
+ user_id=ctx.active_member_id,
100
+ new_email=original_email,
101
+ )
102
+ _pause()
103
+
104
+ # ----- active admin -----
105
+
106
+ def test_update_email_active_admin(self, ctx, users):
107
+ """Update primary email on an admin, verify, then restore."""
108
+ original_data = _scim_get_user(users, ctx.active_admin_id)
109
+ original_email = _get_primary_email(original_data)
110
+ assert original_email, "Could not determine original email"
111
+ _pause()
112
+
113
+ new_email = f"scim-admin-test-{int(time.time())}@example.com"
114
+ resp = users.scim_update_email(
115
+ user_id=ctx.active_admin_id,
116
+ new_email=new_email,
117
+ )
118
+ assert resp.ok, f"Expected ok for admin: {resp.data}"
119
+ _pause()
120
+
121
+ # Teardown: restore
122
+ users.scim_update_email(
123
+ user_id=ctx.active_admin_id,
124
+ new_email=original_email,
125
+ )
126
+ _pause()
127
+
128
+ # ----- bound user_id (no explicit user_id) -----
129
+
130
+ def test_update_email_bound_user(self, ctx, users):
131
+ """Update email using bound user_id (no explicit user_id arg)."""
132
+ bound = users.with_user(ctx.active_member_id)
133
+ original_data = _scim_get_user(bound, ctx.active_member_id)
134
+ original_email = _get_primary_email(original_data)
135
+ assert original_email, "Could not determine original email"
136
+ _pause()
137
+
138
+ new_email = f"scim-bound-test-{int(time.time())}@example.com"
139
+ resp = bound.scim_update_email(new_email=new_email)
140
+ assert resp.ok, f"Expected ok for bound user: {resp.data}"
141
+ _pause()
142
+
143
+ # Teardown: restore
144
+ bound.scim_update_email(new_email=original_email)
145
+ _pause()
146
+
147
+ # ----- deactivated user -----
148
+
149
+ def test_update_email_deactivated_user(self, ctx, users):
150
+ """Updating email on a deactivated user should fail or be a no-op."""
151
+ try:
152
+ resp = users.scim_update_email(
153
+ user_id=ctx.deactivated_user_id,
154
+ new_email="should-not-apply@example.com",
155
+ )
156
+ except requests.HTTPError as exc:
157
+ assert exc.response.status_code in (400, 403, 404), (
158
+ f"Unexpected status for deactivated user: {exc.response.status_code}"
159
+ )
160
+ _pause()
@@ -161,6 +161,14 @@ def main() -> None:
161
161
  "scim_update_user_attribute()",
162
162
  lambda: bound.scim_update_user_attribute(user_id="U1", attribute="active", new_value=False),
163
163
  ),
164
+ CallSpec(
165
+ "scim_update_email(by id)",
166
+ lambda: bound.scim_update_email(user_id="U1", new_email="newemail@example.com"),
167
+ ),
168
+ CallSpec(
169
+ "scim_update_email(bound)",
170
+ lambda: bound.scim_update_email(new_email="newemail@example.com"),
171
+ ),
164
172
  CallSpec("make_multi_channel_guest()", lambda: bound.make_multi_channel_guest()),
165
173
 
166
174
  # SCIM search primitives
@@ -479,3 +479,97 @@ class TestIsActiveWithUserId:
479
479
  bound.get_user_info = MagicMock()
480
480
  assert bound.is_active(user_id="U123") is True
481
481
  bound.get_user_info.assert_not_called()
482
+
483
+
484
+ # ═══════════════════════════════════════════════════════════════════════════
485
+ # scim_update_email
486
+ # ═══════════════════════════════════════════════════════════════════════════
487
+
488
+ class TestScimUpdateEmail:
489
+ """scim_update_email — version-aware SCIM PATCH for primary email."""
490
+
491
+ def test_v2_payload_uses_value_filter_path(self):
492
+ """v2 uses emails[primary eq true].value to avoid multi-primary conflict."""
493
+ users = _make_users()
494
+ bound = users.with_user("U1")
495
+ bound._scim_request = MagicMock(
496
+ return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
497
+ )
498
+ bound.scim_update_email(user_id="U1", new_email="new@example.com")
499
+
500
+ bound._scim_request.assert_called_once()
501
+ _, kwargs = bound._scim_request.call_args
502
+ op = kwargs["payload"]["Operations"][0]
503
+ assert op["op"] == "replace"
504
+ assert op["path"] == "emails[primary eq true].value"
505
+ assert op["value"] == "new@example.com"
506
+
507
+ def test_v1_payload_uses_email_array(self):
508
+ """v1 wraps address in standard SCIM email array with primary+type."""
509
+ users = _make_users()
510
+ bound = users.with_user("U1")
511
+ object.__setattr__(bound.cfg, "scim_version", "v1")
512
+ bound._scim_request = MagicMock(
513
+ return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
514
+ )
515
+ bound.scim_update_email(user_id="U1", new_email="new@example.com")
516
+
517
+ bound._scim_request.assert_called_once()
518
+ _, kwargs = bound._scim_request.call_args
519
+ emails = kwargs["payload"]["emails"]
520
+ assert len(emails) == 1
521
+ assert emails[0]["value"] == "new@example.com"
522
+ assert emails[0]["primary"] is True
523
+ assert emails[0]["type"] == "work"
524
+
525
+ def test_falls_back_to_bound_user_id(self):
526
+ """Omitting user_id uses self.user_id."""
527
+ users = _make_users()
528
+ bound = users.with_user("U1")
529
+ bound._scim_request = MagicMock(
530
+ return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
531
+ )
532
+ bound.scim_update_email(new_email="new@example.com")
533
+
534
+ bound._scim_request.assert_called_once()
535
+ _, kwargs = bound._scim_request.call_args
536
+ assert kwargs["path"] == "Users/U1"
537
+
538
+ def test_explicit_user_id_overrides_bound(self):
539
+ """Passing user_id explicitly takes precedence over bound user_id."""
540
+ users = _make_users()
541
+ bound = users.with_user("U_IGNORED")
542
+ bound._scim_request = MagicMock(
543
+ return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
544
+ )
545
+ bound.scim_update_email(user_id="U_OTHER", new_email="new@example.com")
546
+
547
+ bound._scim_request.assert_called_once()
548
+ _, kwargs = bound._scim_request.call_args
549
+ assert kwargs["path"] == "Users/U_OTHER"
550
+
551
+ def test_no_user_id_raises(self):
552
+ """Unbound instance with no user_id raises ValueError."""
553
+ users = _make_users()
554
+ with pytest.raises(ValueError, match="requires user_id"):
555
+ users.scim_update_email(new_email="x@example.com")
556
+
557
+ def test_invalid_scim_version_raises(self):
558
+ """Unsupported scim_version raises NotImplementedError."""
559
+ users = _make_users()
560
+ bound = users.with_user("U1")
561
+ object.__setattr__(bound.cfg, "scim_version", "v99")
562
+ with pytest.raises(NotImplementedError, match="Invalid SCIM version"):
563
+ bound.scim_update_email(new_email="x@example.com")
564
+
565
+ def test_invalid_user_id_raises(self):
566
+ """Path-traversal user_id is rejected by validate_scim_id."""
567
+ users = _make_users()
568
+ with pytest.raises(ValueError):
569
+ users.scim_update_email(user_id="../../bad", new_email="x@example.com")
570
+
571
+ def test_empty_user_id_raises(self):
572
+ """Empty user_id is rejected by validate_scim_id."""
573
+ users = _make_users()
574
+ with pytest.raises(ValueError):
575
+ users.scim_update_email(user_id="", new_email="x@example.com")
@@ -7,8 +7,8 @@ Test-user IDs and other identifiers are loaded from a JSON file.
7
7
  Setup
8
8
  -----
9
9
  1. Store your Slack tokens in Azure Key Vault:
10
- Bot-token-SB – xoxb-... bot token with users:read and users:read.email scopes
11
- User-token-SB – xoxp-... user token with admin scope
10
+ Bot-token-TEST – xoxb-... bot token with users:read and users:read.email scopes
11
+ User-token-TEST – xoxp-... user token with admin scope
12
12
 
13
13
  2. Set the ``_KEYVAULT_NAME`` constant in this file to your Azure Key Vault name.
14
14
 
@@ -48,9 +48,9 @@ _DEFAULT_CONFIG_PATH = _HERE.parent / "live_test_config.json"
48
48
  _KEYVAULT_NAME = "Slack-AI-assistant-KV"
49
49
 
50
50
  # Key Vault secret names
51
- _SECRET_BOT_TOKEN = "Bot-token-SB"
52
- _SECRET_USER_TOKEN = "User-token-SB"
53
- _SECRET_SCIM_TOKEN = "User-token-SB" # There's no separate SCIM token at the moment
51
+ _SECRET_BOT_TOKEN = "Bot-token-TEST"
52
+ _SECRET_USER_TOKEN = "User-token-TEST"
53
+ _SECRET_SCIM_TOKEN = "User-token-TEST" # There's no separate SCIM token at the moment
54
54
 
55
55
 
56
56
  # ---------------------------------------------------------------------------
@@ -1,8 +0,0 @@
1
- {
2
- "ExpandedNodes": [
3
- "",
4
- "\\src",
5
- "\\src\\slack-objects"
6
- ],
7
- "PreviewInSolutionExplorer": false
8
- }
Binary file
File without changes
File without changes
File without changes
File without changes