Skip to content

Commit f7a70d5

Browse files
author
Saurabh Badenkal
committed
Add coverage gap tests: timezone-aware timestamps, expand data, @OData stripping, whitespace IDs, multi-row NaN patterns
1 parent afbeafd commit f7a70d5

2 files changed

Lines changed: 101 additions & 0 deletions

File tree

tests/unit/test_dataframe_operations.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,54 @@ def test_create_normalizes_numpy_types_before_api(self):
433433
self.assertIsInstance(rec["createdon"], str)
434434
self.assertEqual(rec["createdon"], "2024-06-01T00:00:00")
435435

436+
def test_get_with_expand_includes_nested_data(self):
437+
"""get() with expand returns DataFrame including expanded navigation property data."""
438+
page = [
439+
{
440+
"accountid": "guid-1",
441+
"name": "Contoso",
442+
"primarycontactid": {"contactid": "c-1", "fullname": "John"},
443+
}
444+
]
445+
self.client._odata._get_multiple.return_value = iter([page])
446+
df = self.client.dataframe.get("account", expand=["primarycontactid"])
447+
self.assertEqual(len(df), 1)
448+
self.assertEqual(df.iloc[0]["name"], "Contoso")
449+
self.assertIsInstance(df.iloc[0]["primarycontactid"], dict)
450+
self.assertEqual(df.iloc[0]["primarycontactid"]["fullname"], "John")
451+
452+
def test_get_single_record_no_odata_keys(self):
453+
"""Single-record get strips @odata.* keys from the returned DataFrame."""
454+
self.client._odata._get.return_value = {
455+
"@odata.context": "https://example.crm.dynamics.com/$metadata#accounts/$entity",
456+
"@odata.etag": 'W/"123"',
457+
"accountid": "guid-1",
458+
"name": "Contoso",
459+
}
460+
df = self.client.dataframe.get("account", record_id="guid-1")
461+
self.assertNotIn("@odata.context", df.columns)
462+
self.assertNotIn("@odata.etag", df.columns)
463+
self.assertIn("name", df.columns)
464+
self.assertEqual(df.iloc[0]["name"], "Contoso")
465+
466+
def test_delete_whitespace_only_ids_rejected(self):
467+
"""Series containing whitespace-only strings raises ValueError."""
468+
ids = pd.Series(["guid-1", " ", "guid-3"])
469+
with self.assertRaises(ValueError) as ctx:
470+
self.client.dataframe.delete("account", ids)
471+
self.assertIn("invalid values", str(ctx.exception))
472+
self.assertIn("[1]", str(ctx.exception))
473+
474+
def test_update_with_timezone_aware_timestamps(self):
475+
"""Update correctly normalizes timezone-aware Timestamps."""
476+
ts = pd.Timestamp("2024-06-15 10:30:00", tz="UTC")
477+
df = pd.DataFrame([{"accountid": "guid-1", "lastonholdtime": ts}])
478+
self.client.dataframe.update("account", df, id_column="accountid")
479+
call_args = self.client._odata._update.call_args[0]
480+
changes = call_args[2]
481+
self.assertIsInstance(changes["lastonholdtime"], str)
482+
self.assertIn("2024-06-15T10:30:00", changes["lastonholdtime"])
483+
436484

437485
if __name__ == "__main__":
438486
unittest.main()

tests/unit/test_pandas_helpers.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,25 @@ def test_none_passthrough(self):
6666
result = _normalize_scalar(None)
6767
self.assertIsNone(result)
6868

69+
def test_timestamp_with_timezone(self):
70+
"""Timezone-aware pd.Timestamp is converted to ISO 8601 with tz offset."""
71+
ts = pd.Timestamp("2024-06-15 10:30:00", tz="UTC")
72+
result = _normalize_scalar(ts)
73+
self.assertIn("2024-06-15T10:30:00", result)
74+
self.assertIsInstance(result, str)
75+
76+
def test_numpy_int32(self):
77+
"""np.int32 is also converted to Python int."""
78+
result = _normalize_scalar(np.int32(7))
79+
self.assertIsInstance(result, int)
80+
self.assertEqual(result, 7)
81+
82+
def test_numpy_float32(self):
83+
"""np.float32 is also converted to Python float."""
84+
result = _normalize_scalar(np.float32(2.5))
85+
self.assertIsInstance(result, float)
86+
self.assertAlmostEqual(result, 2.5, places=5)
87+
6988

7089
class TestDataframeToRecords(unittest.TestCase):
7190
"""Unit tests for dataframe_to_records()."""
@@ -165,6 +184,40 @@ def test_mixed_types(self):
165184
self.assertEqual(rec["createdon"], "2024-06-01T00:00:00")
166185
self.assertNotIn("notes", rec)
167186

187+
def test_timezone_aware_timestamp(self):
188+
"""Timezone-aware Timestamp in DataFrame is converted to ISO string with tz."""
189+
ts = pd.Timestamp("2024-06-15 10:30:00", tz="US/Eastern")
190+
df = pd.DataFrame([{"createdon": ts}])
191+
result = dataframe_to_records(df)
192+
self.assertIn("2024-06-15T10:30:00", result[0]["createdon"])
193+
self.assertIsInstance(result[0]["createdon"], str)
194+
195+
def test_multiple_rows_some_with_nan(self):
196+
"""Multi-row DataFrame with mixed NaN positions drops correct keys per row."""
197+
df = pd.DataFrame(
198+
[
199+
{"name": "A", "phone": "555-0100", "city": None},
200+
{"name": "B", "phone": None, "city": "Seattle"},
201+
{"name": None, "phone": "555-0300", "city": "Portland"},
202+
]
203+
)
204+
result = dataframe_to_records(df)
205+
self.assertEqual(result[0], {"name": "A", "phone": "555-0100"})
206+
self.assertEqual(result[1], {"name": "B", "city": "Seattle"})
207+
self.assertEqual(result[2], {"phone": "555-0300", "city": "Portland"})
208+
209+
def test_multiple_rows_na_as_null(self):
210+
"""Multi-row DataFrame with na_as_null=True includes None for all missing values."""
211+
df = pd.DataFrame(
212+
[
213+
{"name": "A", "phone": None},
214+
{"name": None, "phone": "555-0200"},
215+
]
216+
)
217+
result = dataframe_to_records(df, na_as_null=True)
218+
self.assertEqual(result[0], {"name": "A", "phone": None})
219+
self.assertEqual(result[1], {"name": None, "phone": "555-0200"})
220+
168221

169222
if __name__ == "__main__":
170223
unittest.main()

0 commit comments

Comments
 (0)