|
29 | 29 | "managementUrls": {"id": "INSTANCEID"}, |
30 | 30 | }) |
31 | 31 |
|
| 32 | +_MANAGEMENT_QUERY = "taskHub=HostHub&connection=HostStorage&code=host-key" |
| 33 | +_MANAGEMENT_URLS = { |
| 34 | + "id": "INSTANCEID", |
| 35 | + "statusQueryGetUri": ( |
| 36 | + "http://internal-host/custom/manage/INSTANCEID?" |
| 37 | + f"{_MANAGEMENT_QUERY}"), |
| 38 | + "sendEventPostUri": ( |
| 39 | + "http://internal-host/custom/manage/INSTANCEID/raiseEvent/{eventName}?" |
| 40 | + f"{_MANAGEMENT_QUERY}"), |
| 41 | + "terminatePostUri": ( |
| 42 | + "http://internal-host/custom/manage/INSTANCEID/terminate?reason={text}&" |
| 43 | + f"{_MANAGEMENT_QUERY}"), |
| 44 | + "rewindPostUri": ( |
| 45 | + "http://internal-host/custom/manage/INSTANCEID/rewind?reason={text}&" |
| 46 | + f"{_MANAGEMENT_QUERY}"), |
| 47 | + "purgeHistoryDeleteUri": ( |
| 48 | + "http://internal-host/custom/manage/INSTANCEID?" |
| 49 | + f"{_MANAGEMENT_QUERY}"), |
| 50 | + "restartPostUri": ( |
| 51 | + "http://internal-host/custom/manage/INSTANCEID/restart?" |
| 52 | + f"{_MANAGEMENT_QUERY}"), |
| 53 | + "suspendPostUri": ( |
| 54 | + "http://internal-host/custom/manage/INSTANCEID/suspend?reason={text}&" |
| 55 | + f"{_MANAGEMENT_QUERY}"), |
| 56 | + "resumePostUri": ( |
| 57 | + "http://internal-host/custom/manage/INSTANCEID/resume?reason={text}&" |
| 58 | + f"{_MANAGEMENT_QUERY}"), |
| 59 | +} |
| 60 | + |
32 | 61 |
|
33 | 62 | def _make_client() -> df.DurableFunctionsClient: |
34 | 63 | return df.DurableFunctionsClient(_CLIENT_CONFIG) |
35 | 64 |
|
36 | 65 |
|
| 66 | +def _make_template_config() -> str: |
| 67 | + return json.dumps({ |
| 68 | + "taskHubName": "TestHub", |
| 69 | + "requiredQueryStringParameters": "code=fallback-key", |
| 70 | + "baseUrl": "http://fallback/runtime/webhooks/durabletask", |
| 71 | + "rpcBaseUrl": "http://localhost:8080/", |
| 72 | + "managementUrls": _MANAGEMENT_URLS, |
| 73 | + }) |
| 74 | + |
| 75 | + |
37 | 76 | def test_client_handles_null_max_grpc_message_size(): |
38 | 77 | # The Durable Functions host may send ``maxGrpcMessageSizeInBytes`` |
39 | 78 | # explicitly as ``null`` (not just omit it). ``dict.get(key, 0)`` returns |
@@ -160,6 +199,85 @@ async def test_create_http_management_payload_requires_instance_id(): |
160 | 199 | await client.close() |
161 | 200 |
|
162 | 201 |
|
| 202 | +@pytest.mark.parametrize( |
| 203 | + ("headers", "expected_origin"), |
| 204 | + [ |
| 205 | + ({}, "http://request-internal:7071"), |
| 206 | + ({"Forwarded": 'for=10.0.0.1;proto=https;host="public.example:8443"'}, |
| 207 | + "https://public.example:8443"), |
| 208 | + ({"X-Forwarded-Proto": "https", "X-Forwarded-Host": "proxy.example"}, |
| 209 | + "https://proxy.example"), |
| 210 | + ], |
| 211 | +) |
| 212 | +async def test_management_payload_uses_host_templates_and_external_origin( |
| 213 | + headers, expected_origin): |
| 214 | + config = _make_template_config() |
| 215 | + async_client = df.DurableFunctionsClient(config) |
| 216 | + sync_client = df.SyncDurableFunctionsClient(config) |
| 217 | + request = func.HttpRequest( |
| 218 | + method="POST", |
| 219 | + url="http://request-internal:7071/api/start", |
| 220 | + headers=headers, |
| 221 | + body=b"") |
| 222 | + instance_id = "folder/instance ?" |
| 223 | + encoded_instance_id = "folder%2Finstance%20%3F" |
| 224 | + |
| 225 | + try: |
| 226 | + async_payload = async_client.create_http_management_payload( |
| 227 | + request, instance_id) |
| 228 | + sync_payload = sync_client.create_http_management_payload( |
| 229 | + request, instance_id) |
| 230 | + |
| 231 | + assert async_payload == sync_payload |
| 232 | + assert async_payload["id"] == instance_id |
| 233 | + assert async_payload["statusQueryGetUri"] == ( |
| 234 | + f"{expected_origin}/custom/manage/{encoded_instance_id}?" |
| 235 | + f"{_MANAGEMENT_QUERY}") |
| 236 | + assert async_payload["sendEventPostUri"] == ( |
| 237 | + f"{expected_origin}/custom/manage/{encoded_instance_id}/" |
| 238 | + f"raiseEvent/{{eventName}}?{_MANAGEMENT_QUERY}") |
| 239 | + assert async_payload["terminatePostUri"] == ( |
| 240 | + f"{expected_origin}/custom/manage/{encoded_instance_id}/" |
| 241 | + f"terminate?reason={{text}}&{_MANAGEMENT_QUERY}") |
| 242 | + assert async_payload["rewindPostUri"] == ( |
| 243 | + f"{expected_origin}/custom/manage/{encoded_instance_id}/" |
| 244 | + f"rewind?reason={{text}}&{_MANAGEMENT_QUERY}") |
| 245 | + assert async_payload["purgeHistoryDeleteUri"] == ( |
| 246 | + async_payload["statusQueryGetUri"]) |
| 247 | + assert async_payload["restartPostUri"] == ( |
| 248 | + f"{expected_origin}/custom/manage/{encoded_instance_id}/" |
| 249 | + f"restart?{_MANAGEMENT_QUERY}") |
| 250 | + assert async_payload["suspendPostUri"] == ( |
| 251 | + f"{expected_origin}/custom/manage/{encoded_instance_id}/" |
| 252 | + f"suspend?reason={{text}}&{_MANAGEMENT_QUERY}") |
| 253 | + assert async_payload["resumePostUri"] == ( |
| 254 | + f"{expected_origin}/custom/manage/{encoded_instance_id}/" |
| 255 | + f"resume?reason={{text}}&{_MANAGEMENT_QUERY}") |
| 256 | + assert async_payload.urls == async_payload.to_json() |
| 257 | + |
| 258 | + async_response = async_client.create_check_status_response( |
| 259 | + request, instance_id) |
| 260 | + sync_response = sync_client.create_check_status_response( |
| 261 | + request, instance_id) |
| 262 | + assert json.loads(async_response.get_body()) == async_payload |
| 263 | + assert json.loads(sync_response.get_body()) == sync_payload |
| 264 | + assert json.loads(async_response.get_body())["rewindPostUri"] == ( |
| 265 | + async_payload["rewindPostUri"]) |
| 266 | + finally: |
| 267 | + await async_client.close() |
| 268 | + sync_client.close() |
| 269 | + |
| 270 | + |
| 271 | +async def test_management_payload_without_request_preserves_template_origin(): |
| 272 | + client = df.DurableFunctionsClient(_make_template_config()) |
| 273 | + try: |
| 274 | + payload = client.create_http_management_payload("instance") |
| 275 | + assert payload["statusQueryGetUri"] == ( |
| 276 | + f"http://internal-host/custom/manage/instance?{_MANAGEMENT_QUERY}") |
| 277 | + finally: |
| 278 | + await client.close() |
| 279 | + |
| 280 | + |
163 | 281 | # --------------------------------------------------------------------------- |
164 | 282 | # Deprecated client method aliases |
165 | 283 | # --------------------------------------------------------------------------- |
@@ -659,6 +777,8 @@ async def test_http_management_payload_is_mapping_like(): |
659 | 777 | payload = client.create_http_management_payload("inst1") |
660 | 778 | assert payload["id"] == "inst1" |
661 | 779 | assert "statusQueryGetUri" in payload |
| 780 | + assert "rewindPostUri" in payload |
| 781 | + assert payload.urls["rewindPostUri"] == payload.to_json()["rewindPostUri"] |
662 | 782 | assert "id" in list(payload.keys()) |
663 | 783 | assert dict(payload.items())["id"] == "inst1" |
664 | 784 | finally: |
|
0 commit comments