Skip to content

Commit fe79ea3

Browse files
committed
test: cover new column get/update + typed-read paths (sync + async) for diff coverage
1 parent c4a5faf commit fe79ea3

4 files changed

Lines changed: 304 additions & 0 deletions

File tree

‎tests/unit/aio/data/test_async_odata_internal.py‎

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,3 +1893,162 @@ async def test_none_context_no_parentheses(self):
18931893
client = _AsyncODataClient(auth, "https://example.crm.dynamics.com", config=config)
18941894
headers = await client._headers()
18951895
assert "(" not in headers["User-Agent"]
1896+
1897+
1898+
class TestAsyncRetrieveMetadataChanges:
1899+
"""_AsyncODataClient._retrieve_metadata_changes / _get_column typed (#203)."""
1900+
1901+
def _client_with_attrs(self, entities):
1902+
client = _make_client()
1903+
client._request_metadata_with_retry = AsyncMock(return_value=_resp(json_data={"EntityMetadata": entities}))
1904+
return client
1905+
1906+
async def test_typed_read_single_request_with_projection_and_criteria(self):
1907+
client = self._client_with_attrs(
1908+
[
1909+
{
1910+
"LogicalName": "account",
1911+
"Attributes": [
1912+
{
1913+
"@odata.type": "#Microsoft.Dynamics.CRM.StringAttributeMetadata",
1914+
"LogicalName": "emailaddress1",
1915+
"MaxLength": 100,
1916+
}
1917+
],
1918+
}
1919+
]
1920+
)
1921+
attrs = await client._retrieve_metadata_changes("account", attribute_logical_name="emailaddress1")
1922+
assert len(attrs) == 1
1923+
assert attrs[0]["MaxLength"] == 100
1924+
call = client._request_metadata_with_retry.call_args
1925+
assert call.args[0] == "get"
1926+
assert "RetrieveMetadataChanges(Query=@p1)" in call.args[1]
1927+
query = json.loads(call.kwargs["params"]["@p1"])
1928+
assert query["Criteria"]["Conditions"][0]["Value"]["Value"] == "account"
1929+
assert query["AttributeQuery"]["Criteria"]["Conditions"][0]["Value"]["Value"] == "emailaddress1"
1930+
assert "MaxLength" in query["AttributeQuery"]["Properties"]["PropertyNames"]
1931+
1932+
async def test_table_not_found_raises(self):
1933+
client = self._client_with_attrs([])
1934+
with pytest.raises(MetadataError):
1935+
await client._retrieve_metadata_changes("nope")
1936+
1937+
async def test_get_column_typed_returns_single_attr(self):
1938+
client = _make_client()
1939+
client._retrieve_metadata_changes = AsyncMock(return_value=[{"LogicalName": "emailaddress1", "MaxLength": 100}])
1940+
col = await client._get_column("account", "emailaddress1", typed=True)
1941+
assert col["MaxLength"] == 100
1942+
1943+
async def test_get_column_typed_empty_returns_none(self):
1944+
client = _make_client()
1945+
client._retrieve_metadata_changes = AsyncMock(return_value=[])
1946+
assert await client._get_column("account", "ghost", typed=True) is None
1947+
1948+
1949+
class TestAsyncGetColumn:
1950+
"""_AsyncODataClient._get_column non-typed paths (#203)."""
1951+
1952+
def _client(self):
1953+
client = _make_client()
1954+
client._get_entity_by_table_schema_name = AsyncMock(return_value={"MetadataId": "ent-1"})
1955+
client._request = AsyncMock(return_value=_resp(json_data={"LogicalName": "emailaddress1", "MaxLength": 100}))
1956+
return client
1957+
1958+
async def test_untyped_returns_json(self):
1959+
client = self._client()
1960+
col = await client._get_column("account", "emailaddress1")
1961+
assert col["MaxLength"] == 100
1962+
assert "Attributes(LogicalName='emailaddress1')" in client._request.call_args.args[1]
1963+
1964+
async def test_untyped_with_select_projects(self):
1965+
client = self._client()
1966+
await client._get_column("account", "emailaddress1", select=["MaxLength"])
1967+
assert client._request.call_args.kwargs["params"]["$select"] == "MaxLength"
1968+
1969+
async def test_untyped_404_returns_none(self):
1970+
client = self._client()
1971+
client._request = AsyncMock(side_effect=HttpError("nope", 404))
1972+
assert await client._get_column("account", "ghost") is None
1973+
1974+
async def test_untyped_other_error_propagates(self):
1975+
client = self._client()
1976+
client._request = AsyncMock(side_effect=HttpError("boom", 500))
1977+
with pytest.raises(HttpError):
1978+
await client._get_column("account", "emailaddress1")
1979+
1980+
async def test_untyped_table_not_found_raises(self):
1981+
client = self._client()
1982+
client._get_entity_by_table_schema_name = AsyncMock(return_value=None)
1983+
with pytest.raises(MetadataError):
1984+
await client._get_column("nope", "emailaddress1")
1985+
1986+
1987+
class TestAsyncUpdateAttribute:
1988+
"""_AsyncODataClient._update_attribute (#202)."""
1989+
1990+
def _client(self):
1991+
client = _make_client()
1992+
client._get_entity_by_table_schema_name = AsyncMock(return_value={"MetadataId": "ent-1"})
1993+
client._request = AsyncMock(
1994+
return_value=_resp(
1995+
json_data={
1996+
"@odata.type": "#Microsoft.Dynamics.CRM.MemoAttributeMetadata",
1997+
"MetadataId": "attr-1",
1998+
"SchemaName": "new_Comment",
1999+
}
2000+
)
2001+
)
2002+
client._execute_raw = AsyncMock()
2003+
return client
2004+
2005+
async def test_update_issues_put_with_odata_type_and_override(self):
2006+
client = self._client()
2007+
await client._update_attribute(
2008+
"new_Feedback", "new_Comment", {"max_length": 4000, "display_name": "Customer Comment"}
2009+
)
2010+
client._execute_raw.assert_awaited_once()
2011+
req = client._execute_raw.call_args.args[0]
2012+
assert req.method == "PUT"
2013+
assert "Attributes(attr-1)" in req.url
2014+
body = json.loads(req.body)
2015+
assert body["@odata.type"] == "Microsoft.Dynamics.CRM.MemoAttributeMetadata"
2016+
assert body["MetadataId"] == "attr-1"
2017+
assert body["MaxLength"] == 4000
2018+
assert body["DisplayName"]["LocalizedLabels"][0]["Label"] == "Customer Comment"
2019+
2020+
async def test_update_unknown_key_raises(self):
2021+
client = self._client()
2022+
with pytest.raises(ValueError):
2023+
await client._update_attribute("new_Feedback", "new_Comment", {"bogus": 1})
2024+
2025+
async def test_update_empty_overrides_raises(self):
2026+
client = self._client()
2027+
with pytest.raises(TypeError):
2028+
await client._update_attribute("new_Feedback", "new_Comment", {})
2029+
2030+
async def test_update_table_not_found_raises(self):
2031+
client = self._client()
2032+
client._get_entity_by_table_schema_name = AsyncMock(return_value=None)
2033+
with pytest.raises(MetadataError):
2034+
await client._update_attribute("nope", "new_Comment", {"max_length": 10})
2035+
2036+
async def test_update_column_get_404_raises_metadata_error(self):
2037+
client = self._client()
2038+
client._request = AsyncMock(side_effect=HttpError("not found", 404))
2039+
with pytest.raises(MetadataError):
2040+
await client._update_attribute("new_Feedback", "ghost", {"max_length": 10})
2041+
client._execute_raw.assert_not_awaited()
2042+
2043+
async def test_update_column_get_500_propagates(self):
2044+
client = self._client()
2045+
client._request = AsyncMock(side_effect=HttpError("boom", 500))
2046+
with pytest.raises(HttpError):
2047+
await client._update_attribute("new_Feedback", "new_Comment", {"max_length": 10})
2048+
2049+
async def test_update_column_missing_odata_type_raises(self):
2050+
client = self._client()
2051+
client._request = AsyncMock(return_value=_resp(json_data={"SchemaName": "new_Comment"}))
2052+
with pytest.raises(MetadataError):
2053+
await client._update_attribute("new_Feedback", "new_Comment", {"max_length": 10})
2054+
client._execute_raw.assert_not_awaited()

‎tests/unit/aio/test_async_tables.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,41 @@ async def test_list_table_relationships(self, async_client, mock_od):
312312
result = await async_client.tables.list_table_relationships("account")
313313
mock_od._list_table_relationships.assert_called_once_with("account", filter=None, select=None)
314314
assert result == [{"SchemaName": "new_Dept_Emp"}]
315+
316+
317+
class TestAsyncTableGetUpdateColumn:
318+
async def test_get_column(self, async_client, mock_od):
319+
"""get_column() delegates to _get_column with defaults and returns the dict."""
320+
mock_od._get_column.return_value = {"LogicalName": "emailaddress1", "MaxLength": 100}
321+
result = await async_client.tables.get_column("account", "emailaddress1")
322+
mock_od._get_column.assert_called_once_with("account", "emailaddress1", typed=False, select=None)
323+
assert result["MaxLength"] == 100
324+
325+
async def test_get_column_typed_with_select(self, async_client, mock_od):
326+
"""get_column() forwards typed and select."""
327+
mock_od._get_column.return_value = None
328+
await async_client.tables.get_column("account", "emailaddress1", typed=True, select=["MaxLength"])
329+
mock_od._get_column.assert_called_once_with("account", "emailaddress1", typed=True, select=["MaxLength"])
330+
331+
async def test_update_column(self, async_client, mock_od):
332+
"""update_column() delegates to _update_attribute and returns the column name."""
333+
mock_od._update_attribute.return_value = "new_Comment"
334+
result = await async_client.tables.update_column("new_Feedback", "new_Comment", {"max_length": 4000})
335+
mock_od._update_attribute.assert_called_once_with("new_Feedback", "new_Comment", {"max_length": 4000})
336+
assert result == "new_Comment"
337+
338+
async def test_update_columns(self, async_client, mock_od):
339+
"""update_columns() updates each column and returns the names in order."""
340+
result = await async_client.tables.update_columns(
341+
"new_Feedback",
342+
{"new_Comment": {"max_length": 4000}, "new_Rating": {"max_value": 10}},
343+
)
344+
assert result == ["new_Comment", "new_Rating"]
345+
assert mock_od._update_attribute.await_count == 2
346+
347+
async def test_update_columns_empty_raises(self, async_client, mock_od):
348+
"""update_columns() rejects an empty mapping before touching the client."""
349+
import pytest
350+
351+
with pytest.raises(TypeError):
352+
await async_client.tables.update_columns("new_Feedback", {})

‎tests/unit/data/test_odata_internal.py‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3189,6 +3189,65 @@ def test_update_table_not_found_raises(self):
31893189
with self.assertRaises(MetadataError):
31903190
self.od._update_attribute("nope", "new_Comment", {"max_length": 10})
31913191

3192+
def test_update_column_get_404_raises_metadata_error(self):
3193+
"""A 404 fetching the existing attribute -> MetadataError (column not found)."""
3194+
self.od._request = MagicMock(side_effect=HttpError("not found", 404))
3195+
with self.assertRaises(MetadataError):
3196+
self.od._update_attribute("new_Feedback", "ghost", {"max_length": 10})
3197+
self.od._execute_raw.assert_not_called()
3198+
3199+
def test_update_column_get_500_propagates(self):
3200+
"""A non-404 error fetching the existing attribute propagates unchanged."""
3201+
self.od._request = MagicMock(side_effect=HttpError("boom", 500))
3202+
with self.assertRaises(HttpError):
3203+
self.od._update_attribute("new_Feedback", "new_Comment", {"max_length": 10})
3204+
3205+
def test_update_column_missing_odata_type_raises(self):
3206+
"""Existing attribute lacking @odata.type/MetadataId -> MetadataError, no PUT."""
3207+
self.od._request = MagicMock(return_value=_mock_response(json_data={"SchemaName": "new_Comment"}))
3208+
with self.assertRaises(MetadataError):
3209+
self.od._update_attribute("new_Feedback", "new_Comment", {"max_length": 10})
3210+
self.od._execute_raw.assert_not_called()
3211+
3212+
3213+
class TestGetColumn(unittest.TestCase):
3214+
"""Unit tests for _ODataClient._get_column (typed and non-typed paths, #203)."""
3215+
3216+
def setUp(self):
3217+
self.od = _make_odata_client()
3218+
self.od._get_entity_by_table_schema_name = MagicMock(return_value={"MetadataId": "ent-1"})
3219+
self.od._request = MagicMock(
3220+
return_value=_mock_response(json_data={"LogicalName": "emailaddress1", "MaxLength": 100})
3221+
)
3222+
3223+
def test_untyped_returns_json(self):
3224+
col = self.od._get_column("account", "emailaddress1")
3225+
self.assertEqual(col["MaxLength"], 100)
3226+
url = self.od._request.call_args.args[1]
3227+
self.assertIn("Attributes(LogicalName='emailaddress1')", url)
3228+
3229+
def test_untyped_with_select_projects(self):
3230+
self.od._get_column("account", "emailaddress1", select=["MaxLength"])
3231+
self.assertEqual(self.od._request.call_args.kwargs["params"]["$select"], "MaxLength")
3232+
3233+
def test_untyped_404_returns_none(self):
3234+
self.od._request = MagicMock(side_effect=HttpError("nope", 404))
3235+
self.assertIsNone(self.od._get_column("account", "ghost"))
3236+
3237+
def test_untyped_other_error_propagates(self):
3238+
self.od._request = MagicMock(side_effect=HttpError("boom", 500))
3239+
with self.assertRaises(HttpError):
3240+
self.od._get_column("account", "emailaddress1")
3241+
3242+
def test_untyped_table_not_found_raises(self):
3243+
self.od._get_entity_by_table_schema_name = MagicMock(return_value=None)
3244+
with self.assertRaises(MetadataError):
3245+
self.od._get_column("nope", "emailaddress1")
3246+
3247+
def test_typed_empty_returns_none(self):
3248+
self.od._retrieve_metadata_changes = MagicMock(return_value=[])
3249+
self.assertIsNone(self.od._get_column("account", "ghost", typed=True))
3250+
31923251

31933252
class TestRetrieveMetadataChanges(unittest.TestCase):
31943253
"""Unit tests for _ODataClient._retrieve_metadata_changes / _get_column typed (#203)."""

‎tests/unit/test_tables_operations.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,54 @@ def test_list_columns_with_select_and_filter(self):
462462
typed=False,
463463
)
464464

465+
# ------------------------------------------------- get_column / update_column
466+
467+
def test_get_column(self):
468+
"""get_column() delegates to _get_column with defaults and returns the dict."""
469+
expected = {"LogicalName": "emailaddress1", "MaxLength": 100}
470+
self.client._odata._get_column.return_value = expected
471+
472+
result = self.client.tables.get_column("account", "emailaddress1")
473+
474+
self.client._odata._get_column.assert_called_once_with("account", "emailaddress1", typed=False, select=None)
475+
self.assertEqual(result, expected)
476+
477+
def test_get_column_typed_with_select(self):
478+
"""get_column() forwards typed and select."""
479+
self.client._odata._get_column.return_value = None
480+
481+
self.client.tables.get_column("account", "emailaddress1", typed=True, select=["MaxLength"])
482+
483+
self.client._odata._get_column.assert_called_once_with(
484+
"account", "emailaddress1", typed=True, select=["MaxLength"]
485+
)
486+
487+
def test_update_column(self):
488+
"""update_column() delegates to _update_attribute and returns the column name."""
489+
self.client._odata._update_attribute.return_value = "new_Comment"
490+
491+
result = self.client.tables.update_column("new_Feedback", "new_Comment", {"max_length": 4000})
492+
493+
self.client._odata._update_attribute.assert_called_once_with(
494+
"new_Feedback", "new_Comment", {"max_length": 4000}
495+
)
496+
self.assertEqual(result, "new_Comment")
497+
498+
def test_update_columns(self):
499+
"""update_columns() updates each column and returns the names in order."""
500+
result = self.client.tables.update_columns(
501+
"new_Feedback",
502+
{"new_Comment": {"max_length": 4000}, "new_Rating": {"max_value": 10}},
503+
)
504+
505+
self.assertEqual(result, ["new_Comment", "new_Rating"])
506+
self.assertEqual(self.client._odata._update_attribute.call_count, 2)
507+
508+
def test_update_columns_empty_raises(self):
509+
"""update_columns() rejects an empty mapping before touching the client."""
510+
with self.assertRaises(TypeError):
511+
self.client.tables.update_columns("new_Feedback", {})
512+
465513
# ------------------------------------------------- list_relationships
466514

467515
def test_list_relationships(self):

0 commit comments

Comments
 (0)