webex-message-handler 0.6.13__tar.gz → 0.6.14__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.
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/PKG-INFO +10 -1
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/README.md +9 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/pyproject.toml +1 -1
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/device_manager.py +66 -3
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/tests/test_device_manager.py +132 -1
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/.gitignore +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/API.md +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/LICENSE +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/examples/basic_bot.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/__init__.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/errors.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/handler.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/id_utils.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/kms_client.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/logger.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/mention_parser.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/mercury_socket.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/message_decryptor.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/types.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/url_validation.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/test-proxy.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/tests/__init__.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/tests/conftest.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/tests/test_handler.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/tests/test_id_utils.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/tests/test_integration.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/tests/test_kms_client.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/tests/test_mention_parser.py +0 -0
- {webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/tests/test_message_decryptor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webex-message-handler
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.14
|
|
4
4
|
Summary: Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages without the full Webex SDK
|
|
5
5
|
Project-URL: Homepage, https://github.com/3rg0n/webex-message-handler
|
|
6
6
|
Project-URL: Repository, https://github.com/3rg0n/webex-message-handler
|
|
@@ -188,6 +188,15 @@ async def on_message(msg):
|
|
|
188
188
|
|
|
189
189
|
Resource types: `"MESSAGE"`, `"PEOPLE"`, `"ROOM"`.
|
|
190
190
|
|
|
191
|
+
## Region Discovery
|
|
192
|
+
|
|
193
|
+
Webex assigns each org to a service region (e.g. `wdm-a`, `wdm-r`). On connect,
|
|
194
|
+
the library discovers your org's correct WDM endpoint from the Webex U2C service
|
|
195
|
+
catalog and registers there automatically — no configuration needed. Registering
|
|
196
|
+
in the wrong region produces a socket that authorizes and completes the KMS
|
|
197
|
+
handshake but never receives that org's messages, so this is done for you. If
|
|
198
|
+
discovery fails for any reason, it falls back to the default region.
|
|
199
|
+
|
|
191
200
|
## Delivery Guarantees
|
|
192
201
|
|
|
193
202
|
This library provides **at-most-once** delivery semantics:
|
|
@@ -157,6 +157,15 @@ async def on_message(msg):
|
|
|
157
157
|
|
|
158
158
|
Resource types: `"MESSAGE"`, `"PEOPLE"`, `"ROOM"`.
|
|
159
159
|
|
|
160
|
+
## Region Discovery
|
|
161
|
+
|
|
162
|
+
Webex assigns each org to a service region (e.g. `wdm-a`, `wdm-r`). On connect,
|
|
163
|
+
the library discovers your org's correct WDM endpoint from the Webex U2C service
|
|
164
|
+
catalog and registers there automatically — no configuration needed. Registering
|
|
165
|
+
in the wrong region produces a socket that authorizes and completes the KMS
|
|
166
|
+
handshake but never receives that org's messages, so this is done for you. If
|
|
167
|
+
discovery fails for any reason, it falls back to the default region.
|
|
168
|
+
|
|
160
169
|
## Delivery Guarantees
|
|
161
170
|
|
|
162
171
|
This library provides **at-most-once** delivery semantics:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "webex-message-handler"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.14"
|
|
8
8
|
description = "Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages without the full Webex SDK"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -11,6 +11,7 @@ from .types import DeviceRegistration, FetchFunction, FetchRequest
|
|
|
11
11
|
from .url_validation import validate_webex_url
|
|
12
12
|
|
|
13
13
|
WDM_API_BASE = "https://wdm-a.wbx2.com/wdm/api/v1/devices"
|
|
14
|
+
U2C_CATALOG_URL = "https://u2c.wbx2.com/u2c/api/v1/catalog?format=hostmap"
|
|
14
15
|
|
|
15
16
|
_DEVICE_BODY = {
|
|
16
17
|
"deviceName": "webex-message-handler",
|
|
@@ -35,6 +36,66 @@ class DeviceManager:
|
|
|
35
36
|
self._logger: Logger = logger or noop_logger # type: ignore[assignment]
|
|
36
37
|
self._http_do = http_do
|
|
37
38
|
self._device_url: str | None = None
|
|
39
|
+
self._wdm_devices_url: str | None = None
|
|
40
|
+
|
|
41
|
+
async def _discover_wdm_base(self, token: str) -> str:
|
|
42
|
+
"""Discover the org-assigned WDM devices endpoint from U2C.
|
|
43
|
+
|
|
44
|
+
Caches the result. On any failure falls back to the hard-coded WDM_API_BASE
|
|
45
|
+
(never fatal), so a U2C outage degrades to today's behavior.
|
|
46
|
+
"""
|
|
47
|
+
if self._wdm_devices_url:
|
|
48
|
+
return self._wdm_devices_url
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
response = await self._http_do(
|
|
52
|
+
FetchRequest(
|
|
53
|
+
url=U2C_CATALOG_URL,
|
|
54
|
+
method="GET",
|
|
55
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if not response.ok:
|
|
60
|
+
self._logger.warning(
|
|
61
|
+
f"U2C discovery returned {response.status}; falling back to {WDM_API_BASE}"
|
|
62
|
+
)
|
|
63
|
+
self._wdm_devices_url = WDM_API_BASE
|
|
64
|
+
return self._wdm_devices_url
|
|
65
|
+
|
|
66
|
+
catalog = await response.json()
|
|
67
|
+
wdm = catalog.get("serviceLinks", {}).get("wdm")
|
|
68
|
+
|
|
69
|
+
if not wdm:
|
|
70
|
+
self._logger.warning(
|
|
71
|
+
f"U2C catalog has no wdm service link; falling back to {WDM_API_BASE}"
|
|
72
|
+
)
|
|
73
|
+
self._wdm_devices_url = WDM_API_BASE
|
|
74
|
+
return self._wdm_devices_url
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
validate_webex_url(wdm, "https")
|
|
78
|
+
except ValueError as exc:
|
|
79
|
+
self._logger.error(
|
|
80
|
+
f'U2C-discovered wdm URL "{wdm}" untrusted ({exc}); falling back to {WDM_API_BASE}'
|
|
81
|
+
)
|
|
82
|
+
self._wdm_devices_url = WDM_API_BASE
|
|
83
|
+
return self._wdm_devices_url
|
|
84
|
+
|
|
85
|
+
self._wdm_devices_url = wdm.rstrip("/") + "/devices"
|
|
86
|
+
self._logger.info(f"Discovered region-correct WDM endpoint: {self._wdm_devices_url}")
|
|
87
|
+
return self._wdm_devices_url
|
|
88
|
+
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
self._logger.warning(
|
|
91
|
+
f"U2C discovery failed ({exc}); falling back to {WDM_API_BASE}"
|
|
92
|
+
)
|
|
93
|
+
self._wdm_devices_url = WDM_API_BASE
|
|
94
|
+
return self._wdm_devices_url
|
|
95
|
+
|
|
96
|
+
def set_wdm_devices_url(self, url: str) -> None:
|
|
97
|
+
"""Pre-seed the discovered WDM devices endpoint (test helper)."""
|
|
98
|
+
self._wdm_devices_url = url
|
|
38
99
|
|
|
39
100
|
async def register(self, token: str) -> DeviceRegistration:
|
|
40
101
|
"""Register a new device with WDM.
|
|
@@ -75,7 +136,8 @@ class DeviceManager:
|
|
|
75
136
|
async def _create_device(self, token: str) -> DeviceRegistration:
|
|
76
137
|
"""Perform the raw POST /devices registration."""
|
|
77
138
|
try:
|
|
78
|
-
|
|
139
|
+
base = await self._discover_wdm_base(token)
|
|
140
|
+
create_url = f"{base}?includeUpstreamServices=all"
|
|
79
141
|
response = await self._http_do(
|
|
80
142
|
FetchRequest(
|
|
81
143
|
url=create_url,
|
|
@@ -159,10 +221,11 @@ class DeviceManager:
|
|
|
159
221
|
self._logger.warning(f"Could not list devices to reap: {exc}")
|
|
160
222
|
|
|
161
223
|
async def _list_devices(self, token: str) -> list[Any]:
|
|
162
|
-
"""Fetches the account's current WDM device registrations."""
|
|
224
|
+
"""Fetches the account's current WDM device registrations from the region-correct endpoint."""
|
|
225
|
+
base = await self._discover_wdm_base(token)
|
|
163
226
|
response = await self._http_do(
|
|
164
227
|
FetchRequest(
|
|
165
|
-
url=
|
|
228
|
+
url=base,
|
|
166
229
|
method="GET",
|
|
167
230
|
headers={"Authorization": f"Bearer {token}"},
|
|
168
231
|
)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
5
|
from tests.conftest import MockHttpDo
|
|
6
|
-
from webex_message_handler.device_manager import WDM_API_BASE, DeviceManager
|
|
6
|
+
from webex_message_handler.device_manager import U2C_CATALOG_URL, WDM_API_BASE, DeviceManager
|
|
7
7
|
from webex_message_handler.errors import AuthError, DeviceRegistrationError
|
|
8
8
|
|
|
9
9
|
MOCK_TOKEN = "test-token"
|
|
@@ -30,6 +30,7 @@ class TestRegister:
|
|
|
30
30
|
.add("POST", f"{WDM_API_BASE}?includeUpstreamServices=all", payload=MOCK_WDM_RESPONSE) # create
|
|
31
31
|
)
|
|
32
32
|
dm = DeviceManager(http_do=http_do)
|
|
33
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
33
34
|
result = await dm.register(MOCK_TOKEN)
|
|
34
35
|
|
|
35
36
|
assert result.web_socket_url == MOCK_WS_URL
|
|
@@ -45,6 +46,7 @@ class TestRegister:
|
|
|
45
46
|
.add("POST", f"{WDM_API_BASE}?includeUpstreamServices=all", status=401) # create fails with 401
|
|
46
47
|
)
|
|
47
48
|
dm = DeviceManager(http_do=http_do)
|
|
49
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
48
50
|
with pytest.raises(AuthError):
|
|
49
51
|
await dm.register(MOCK_TOKEN)
|
|
50
52
|
|
|
@@ -55,6 +57,7 @@ class TestRegister:
|
|
|
55
57
|
.add("POST", f"{WDM_API_BASE}?includeUpstreamServices=all", status=400) # create fails
|
|
56
58
|
)
|
|
57
59
|
dm = DeviceManager(http_do=http_do)
|
|
60
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
58
61
|
with pytest.raises(DeviceRegistrationError, match="Failed to register device"):
|
|
59
62
|
await dm.register(MOCK_TOKEN)
|
|
60
63
|
|
|
@@ -69,6 +72,7 @@ class TestRegister:
|
|
|
69
72
|
) # create fails
|
|
70
73
|
)
|
|
71
74
|
dm = DeviceManager(http_do=http_do)
|
|
75
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
72
76
|
with pytest.raises(DeviceRegistrationError):
|
|
73
77
|
await dm.register(MOCK_TOKEN)
|
|
74
78
|
|
|
@@ -83,6 +87,7 @@ class TestRefresh:
|
|
|
83
87
|
.add("PUT", MOCK_DEVICE_URL, payload=refreshed) # refresh
|
|
84
88
|
)
|
|
85
89
|
dm = DeviceManager(http_do=http_do)
|
|
90
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
86
91
|
await dm.register(MOCK_TOKEN)
|
|
87
92
|
result = await dm.refresh(MOCK_TOKEN)
|
|
88
93
|
|
|
@@ -102,6 +107,7 @@ class TestRefresh:
|
|
|
102
107
|
.add("PUT", MOCK_DEVICE_URL, status=401) # refresh fails
|
|
103
108
|
)
|
|
104
109
|
dm = DeviceManager(http_do=http_do)
|
|
110
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
105
111
|
await dm.register(MOCK_TOKEN)
|
|
106
112
|
with pytest.raises(AuthError):
|
|
107
113
|
await dm.refresh(MOCK_TOKEN)
|
|
@@ -114,6 +120,7 @@ class TestRefresh:
|
|
|
114
120
|
.add("PUT", MOCK_DEVICE_URL, status=500) # refresh fails
|
|
115
121
|
)
|
|
116
122
|
dm = DeviceManager(http_do=http_do)
|
|
123
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
117
124
|
await dm.register(MOCK_TOKEN)
|
|
118
125
|
with pytest.raises(DeviceRegistrationError):
|
|
119
126
|
await dm.refresh(MOCK_TOKEN)
|
|
@@ -128,6 +135,7 @@ class TestUnregister:
|
|
|
128
135
|
.add("DELETE", MOCK_DEVICE_URL, status=204) # unregister
|
|
129
136
|
)
|
|
130
137
|
dm = DeviceManager(http_do=http_do)
|
|
138
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
131
139
|
await dm.register(MOCK_TOKEN)
|
|
132
140
|
await dm.unregister(MOCK_TOKEN)
|
|
133
141
|
|
|
@@ -145,6 +153,7 @@ class TestUnregister:
|
|
|
145
153
|
.add("DELETE", MOCK_DEVICE_URL, status=401) # unregister fails
|
|
146
154
|
)
|
|
147
155
|
dm = DeviceManager(http_do=http_do)
|
|
156
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
148
157
|
await dm.register(MOCK_TOKEN)
|
|
149
158
|
with pytest.raises(AuthError):
|
|
150
159
|
await dm.unregister(MOCK_TOKEN)
|
|
@@ -157,6 +166,7 @@ class TestUnregister:
|
|
|
157
166
|
.add("DELETE", MOCK_DEVICE_URL, status=500) # unregister fails
|
|
158
167
|
)
|
|
159
168
|
dm = DeviceManager(http_do=http_do)
|
|
169
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
160
170
|
await dm.register(MOCK_TOKEN)
|
|
161
171
|
with pytest.raises(DeviceRegistrationError):
|
|
162
172
|
await dm.unregister(MOCK_TOKEN)
|
|
@@ -216,6 +226,7 @@ class TestDeviceReuseAndReaping:
|
|
|
216
226
|
.add("PUT", MOCK_DEVICE_URL, payload=MOCK_WDM_RESPONSE)
|
|
217
227
|
)
|
|
218
228
|
dm = DeviceManager(http_do=http_do)
|
|
229
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
219
230
|
result = await dm.register(MOCK_TOKEN)
|
|
220
231
|
|
|
221
232
|
assert result.device_url == MOCK_DEVICE_URL
|
|
@@ -249,6 +260,7 @@ class TestDeviceReuseAndReaping:
|
|
|
249
260
|
.add("POST", f"{WDM_API_BASE}?includeUpstreamServices=all", payload=MOCK_WDM_RESPONSE) # retry POST
|
|
250
261
|
)
|
|
251
262
|
dm = DeviceManager(http_do=http_do)
|
|
263
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
252
264
|
result = await dm.register(MOCK_TOKEN)
|
|
253
265
|
|
|
254
266
|
assert result.device_url == MOCK_DEVICE_URL
|
|
@@ -268,8 +280,127 @@ class TestDeviceReuseAndReaping:
|
|
|
268
280
|
.add("POST", f"{WDM_API_BASE}?includeUpstreamServices=all", payload=MOCK_WDM_RESPONSE) # create succeeds
|
|
269
281
|
)
|
|
270
282
|
dm = DeviceManager(http_do=http_do)
|
|
283
|
+
dm.set_wdm_devices_url(WDM_API_BASE)
|
|
271
284
|
result = await dm.register(MOCK_TOKEN)
|
|
272
285
|
|
|
273
286
|
assert result.device_url == MOCK_DEVICE_URL
|
|
274
287
|
# Should be: GET (list fails) + POST (create succeeds)
|
|
275
288
|
assert len(http_do.calls) == 2
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class TestU2CRegionDiscovery:
|
|
292
|
+
"""Tests for U2C region discovery (#27)."""
|
|
293
|
+
|
|
294
|
+
async def test_discovers_region_correct_wdm_endpoint_from_u2c(self):
|
|
295
|
+
"""Verify that register resolves the org-assigned WDM region from U2C."""
|
|
296
|
+
u2c_catalog = {"serviceLinks": {"wdm": "https://wdm-r.wbx2.com/wdm/api/v1"}}
|
|
297
|
+
regional_device_url = "https://wdm-r.wbx2.com/wdm/api/v1/devices/test-device-id"
|
|
298
|
+
regional_response = {**MOCK_WDM_RESPONSE, "url": regional_device_url}
|
|
299
|
+
|
|
300
|
+
http_do = (
|
|
301
|
+
MockHttpDo()
|
|
302
|
+
.add("GET", U2C_CATALOG_URL, payload=u2c_catalog) # U2C discovery
|
|
303
|
+
.add("GET", "https://wdm-r.wbx2.com/wdm/api/v1/devices", payload={"devices": []}) # list
|
|
304
|
+
.add(
|
|
305
|
+
"POST",
|
|
306
|
+
"https://wdm-r.wbx2.com/wdm/api/v1/devices?includeUpstreamServices=all",
|
|
307
|
+
payload=regional_response,
|
|
308
|
+
) # create
|
|
309
|
+
)
|
|
310
|
+
dm = DeviceManager(http_do=http_do)
|
|
311
|
+
result = await dm.register(MOCK_TOKEN)
|
|
312
|
+
|
|
313
|
+
assert result.device_url == regional_device_url
|
|
314
|
+
# Verify POST went to discovered regional endpoint, not hardcoded wdm-a
|
|
315
|
+
post_calls = [c for c in http_do.calls if c.method == "POST"]
|
|
316
|
+
assert "wdm-r.wbx2.com" in post_calls[0].url
|
|
317
|
+
|
|
318
|
+
async def test_caches_discovered_wdm_endpoint(self):
|
|
319
|
+
"""Verify that the discovered WDM endpoint is cached."""
|
|
320
|
+
u2c_catalog = {"serviceLinks": {"wdm": "https://wdm-r.wbx2.com/wdm/api/v1"}}
|
|
321
|
+
|
|
322
|
+
http_do = (
|
|
323
|
+
MockHttpDo()
|
|
324
|
+
.add("GET", U2C_CATALOG_URL, payload=u2c_catalog) # U2C discovery (first call)
|
|
325
|
+
.add("GET", "https://wdm-r.wbx2.com/wdm/api/v1/devices", payload={"devices": []}) # list
|
|
326
|
+
.add(
|
|
327
|
+
"POST",
|
|
328
|
+
"https://wdm-r.wbx2.com/wdm/api/v1/devices?includeUpstreamServices=all",
|
|
329
|
+
payload={**MOCK_WDM_RESPONSE, "url": "https://wdm-r.wbx2.com/wdm/api/v1/devices/id1"},
|
|
330
|
+
) # create
|
|
331
|
+
)
|
|
332
|
+
dm = DeviceManager(http_do=http_do)
|
|
333
|
+
|
|
334
|
+
# Register once; U2C should be called
|
|
335
|
+
await dm.register(MOCK_TOKEN)
|
|
336
|
+
u2c_calls = [c for c in http_do.calls if "u2c.wbx2.com" in c.url]
|
|
337
|
+
assert len(u2c_calls) == 1
|
|
338
|
+
|
|
339
|
+
# Pre-seed the cache
|
|
340
|
+
dm.set_wdm_devices_url("https://wdm-r.wbx2.com/wdm/api/v1/devices")
|
|
341
|
+
# Verify cache doesn't trigger another U2C call (would fail with assertion)
|
|
342
|
+
|
|
343
|
+
async def test_falls_back_to_wdm_a_when_u2c_returns_non_2xx(self):
|
|
344
|
+
"""Verify discovery is best-effort: non-2xx U2C falls back to wdm-a."""
|
|
345
|
+
http_do = (
|
|
346
|
+
MockHttpDo()
|
|
347
|
+
.add("GET", U2C_CATALOG_URL, status=500) # U2C discovery fails
|
|
348
|
+
.add("GET", WDM_API_BASE, payload={"devices": []}) # list uses fallback
|
|
349
|
+
.add("POST", f"{WDM_API_BASE}?includeUpstreamServices=all", payload=MOCK_WDM_RESPONSE) # create
|
|
350
|
+
)
|
|
351
|
+
dm = DeviceManager(http_do=http_do)
|
|
352
|
+
result = await dm.register(MOCK_TOKEN)
|
|
353
|
+
|
|
354
|
+
assert result.device_url == MOCK_DEVICE_URL
|
|
355
|
+
# Verify POST went to fallback wdm-a
|
|
356
|
+
post_calls = [c for c in http_do.calls if c.method == "POST"]
|
|
357
|
+
assert "wdm-a.wbx2.com" in post_calls[0].url
|
|
358
|
+
|
|
359
|
+
async def test_falls_back_to_wdm_a_when_u2c_has_no_wdm_link(self):
|
|
360
|
+
"""Verify that missing wdm link in U2C response falls back to wdm-a."""
|
|
361
|
+
u2c_catalog = {"serviceLinks": {}} # Missing wdm link
|
|
362
|
+
|
|
363
|
+
http_do = (
|
|
364
|
+
MockHttpDo()
|
|
365
|
+
.add("GET", U2C_CATALOG_URL, payload=u2c_catalog) # U2C discovery
|
|
366
|
+
.add("GET", WDM_API_BASE, payload={"devices": []}) # list uses fallback
|
|
367
|
+
.add("POST", f"{WDM_API_BASE}?includeUpstreamServices=all", payload=MOCK_WDM_RESPONSE) # create
|
|
368
|
+
)
|
|
369
|
+
dm = DeviceManager(http_do=http_do)
|
|
370
|
+
result = await dm.register(MOCK_TOKEN)
|
|
371
|
+
|
|
372
|
+
assert result.device_url == MOCK_DEVICE_URL
|
|
373
|
+
post_calls = [c for c in http_do.calls if c.method == "POST"]
|
|
374
|
+
assert "wdm-a.wbx2.com" in post_calls[0].url
|
|
375
|
+
|
|
376
|
+
async def test_rejects_untrusted_wdm_host_from_u2c_and_falls_back(self):
|
|
377
|
+
"""Verify that non-Webex wdm link from U2C is rejected and falls back."""
|
|
378
|
+
u2c_catalog = {"serviceLinks": {"wdm": "https://evil.attacker.com/wdm/api/v1"}}
|
|
379
|
+
|
|
380
|
+
http_do = (
|
|
381
|
+
MockHttpDo()
|
|
382
|
+
.add("GET", U2C_CATALOG_URL, payload=u2c_catalog) # U2C discovery
|
|
383
|
+
.add("GET", WDM_API_BASE, payload={"devices": []}) # list uses fallback
|
|
384
|
+
.add("POST", f"{WDM_API_BASE}?includeUpstreamServices=all", payload=MOCK_WDM_RESPONSE) # create
|
|
385
|
+
)
|
|
386
|
+
dm = DeviceManager(http_do=http_do)
|
|
387
|
+
result = await dm.register(MOCK_TOKEN)
|
|
388
|
+
|
|
389
|
+
assert result.device_url == MOCK_DEVICE_URL
|
|
390
|
+
post_calls = [c for c in http_do.calls if c.method == "POST"]
|
|
391
|
+
assert "wdm-a.wbx2.com" in post_calls[0].url
|
|
392
|
+
|
|
393
|
+
async def test_falls_back_to_wdm_a_on_u2c_fetch_error(self):
|
|
394
|
+
"""Verify that U2C fetch errors fall back to wdm-a."""
|
|
395
|
+
http_do = (
|
|
396
|
+
MockHttpDo()
|
|
397
|
+
.add("GET", U2C_CATALOG_URL, exc=ConnectionError("Network error")) # U2C discovery fails
|
|
398
|
+
.add("GET", WDM_API_BASE, payload={"devices": []}) # list uses fallback
|
|
399
|
+
.add("POST", f"{WDM_API_BASE}?includeUpstreamServices=all", payload=MOCK_WDM_RESPONSE) # create
|
|
400
|
+
)
|
|
401
|
+
dm = DeviceManager(http_do=http_do)
|
|
402
|
+
result = await dm.register(MOCK_TOKEN)
|
|
403
|
+
|
|
404
|
+
assert result.device_url == MOCK_DEVICE_URL
|
|
405
|
+
post_calls = [c for c in http_do.calls if c.method == "POST"]
|
|
406
|
+
assert "wdm-a.wbx2.com" in post_calls[0].url
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/__init__.py
RENAMED
|
File without changes
|
{webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/errors.py
RENAMED
|
File without changes
|
{webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/handler.py
RENAMED
|
File without changes
|
{webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/id_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/logger.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/src/webex_message_handler/types.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
|
{webex_message_handler-0.6.13 → webex_message_handler-0.6.14}/tests/test_message_decryptor.py
RENAMED
|
File without changes
|