Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def retrieve(self, key: str) -> Connection:
"""
spec = self._retrieve_spec(key)
response = self.request(spec.method, url=spec.endpoint)
return Connection.model_validate(response.json())
return self._select_connection(response.json(), key)

@traced(
name="connections_metadata",
Expand Down Expand Up @@ -280,7 +280,7 @@ async def retrieve_async(self, key: str) -> Connection:
"""
spec = self._retrieve_spec(key)
response = await self.request_async(spec.method, url=spec.endpoint)
return Connection.model_validate(response.json())
return self._select_connection(response.json(), key)

@traced(
name="connections_metadata",
Expand Down Expand Up @@ -571,6 +571,39 @@ def _retrieve_token_spec(
params={"tokenType": token_type.value},
)

def _select_connection(self, data: Any, key: str) -> Connection:
"""Validate a single-connection retrieve response, tolerating list bodies.

The Connections endpoint normally returns a single connection object, but it
can also answer with a list (e.g. when the key resolves to a filtered
collection). Validating a list against the single ``Connection`` model raises
``pydantic_core.ValidationError``; instead, select the connection whose ``id``
matches the requested key, falling back to the first entry when none match.

Args:
data: The deserialized JSON body of the retrieve response.
key: The connection key that was requested.

Returns:
Connection: The resolved connection.

Raises:
ValueError: If the response is a list but contains no connections.
"""
if isinstance(data, list):
if not data:
raise ValueError(f"No connection found for key '{key}'.")
matched = next(
(
item
for item in data
if isinstance(item, dict) and item.get("id") == key
),
data[0],
)
return Connection.model_validate(matched)
return Connection.model_validate(data)

def _parse_and_validate_list_response(self, response: Response) -> List[Connection]:
"""Parse and validate the list response from the API.

Expand Down
111 changes: 111 additions & 0 deletions packages/uipath-platform/tests/services/test_connections_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,117 @@ def test_retrieve(
== f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ConnectionsService.retrieve/{version}"
)

def test_retrieve_selects_matching_connection_from_list_response(
self,
httpx_mock: HTTPXMock,
service: ConnectionsService,
base_url: str,
org: str,
tenant: str,
) -> None:
"""Regression (PC-4777): tolerate a list body and select the matching connection.

The Connections endpoint can answer with a list of connections instead of a
single object. Validating that list against the single ``Connection`` model used
to raise ``pydantic_core.ValidationError`` (input_type=list); ``retrieve`` must
instead pick the connection whose id matches the requested key.
"""
connection_key = "1958b64e-9432-47aa-aaaa-000000000002"
httpx_mock.add_response(
url=f"{base_url}{org}{tenant}/connections_/api/v1/Connections/{connection_key}",
status_code=200,
json=[
{
"id": "1958b64e-9432-47aa-aaaa-000000000001",
"name": "Other Connection",
"elementInstanceId": 101,
},
{
"id": connection_key,
"name": "Target Connection",
"elementInstanceId": 102,
},
],
)

connection = service.retrieve(key=connection_key)

assert isinstance(connection, Connection)
assert connection.id == connection_key
assert connection.name == "Target Connection"
assert connection.element_instance_id == 102

@pytest.mark.anyio
async def test_retrieve_async_selects_matching_connection_from_list_response(
self,
httpx_mock: HTTPXMock,
service: ConnectionsService,
base_url: str,
org: str,
tenant: str,
) -> None:
"""Regression (PC-4777): async retrieve tolerates a list body.

This is the exact path from SRE-610701: ``invoke_activity_async`` ->
``retrieve_async`` received a list and ``Connection.model_validate`` raised.
"""
connection_key = "1958b64e-9432-47aa-aaaa-000000000002"
httpx_mock.add_response(
url=f"{base_url}{org}{tenant}/connections_/api/v1/Connections/{connection_key}",
status_code=200,
json=[
{
"id": "1958b64e-9432-47aa-aaaa-000000000001",
"name": "Other Connection",
"elementInstanceId": 101,
},
{
"id": connection_key,
"name": "Target Connection",
"elementInstanceId": 102,
},
],
)

connection = await service.retrieve_async(key=connection_key)

assert isinstance(connection, Connection)
assert connection.id == connection_key
assert connection.name == "Target Connection"
assert connection.element_instance_id == 102

def test_retrieve_falls_back_to_first_connection_when_no_key_match(
self,
httpx_mock: HTTPXMock,
service: ConnectionsService,
base_url: str,
org: str,
tenant: str,
) -> None:
"""A list with no id matching the key falls back to the first entry."""
connection_key = "unmatched-key"
httpx_mock.add_response(
url=f"{base_url}{org}{tenant}/connections_/api/v1/Connections/{connection_key}",
status_code=200,
json=[
{
"id": "first-id",
"name": "First Connection",
"elementInstanceId": 201,
},
{
"id": "second-id",
"name": "Second Connection",
"elementInstanceId": 202,
},
],
)

connection = service.retrieve(key=connection_key)

assert isinstance(connection, Connection)
assert connection.id == "first-id"

def test_metadata(
self,
httpx_mock: HTTPXMock,
Expand Down
Loading