Skip to content

Commit 813a274

Browse files
Jack Kochclaude
andcommitted
Add edge case tests for get_emails_metadata_stream
- Test empty SORT response handling (lines 451-453) - Test empty page after pagination with SORT (line 463) - Test empty email_ids after split (lines 494-495) - Test empty page in fallback path (line 518) - Test logout error handling (lines 534-535) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b5dca80 commit 813a274

File tree

1 file changed

+153
-0
lines changed

1 file changed

+153
-0
lines changed

tests/test_email_client.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,159 @@ def uid_side_effect(cmd, uid_list, fetch_type):
10651065
# Should still get results via fallback
10661066
assert len(emails) == 2
10671067

1068+
@pytest.mark.asyncio
1069+
async def test_get_emails_stream_sort_empty_response(self, email_client):
1070+
"""Test handling of empty SORT response (covers 451-453)."""
1071+
mock_imap = AsyncMock()
1072+
mock_imap._client_task = asyncio.Future()
1073+
mock_imap._client_task.set_result(None)
1074+
mock_imap.wait_hello_from_server = AsyncMock()
1075+
mock_imap.login = AsyncMock()
1076+
mock_imap.select = AsyncMock()
1077+
mock_imap.logout = AsyncMock()
1078+
1079+
mock_protocol = MagicMock()
1080+
mock_protocol.capabilities = {"SORT", "IMAP4rev1"}
1081+
mock_imap.protocol = mock_protocol
1082+
1083+
# SORT returns empty/None response
1084+
mock_imap.uid = AsyncMock(return_value=(None, [None]))
1085+
1086+
with patch.object(email_client, "imap_class", return_value=mock_imap):
1087+
emails = []
1088+
async for email_data in email_client.get_emails_metadata_stream(page=1, page_size=10):
1089+
emails.append(email_data)
1090+
1091+
assert len(emails) == 0
1092+
1093+
@pytest.mark.asyncio
1094+
async def test_get_emails_stream_sort_empty_page(self, email_client):
1095+
"""Test handling when pagination results in empty page with SORT (covers 463)."""
1096+
mock_imap = AsyncMock()
1097+
mock_imap._client_task = asyncio.Future()
1098+
mock_imap._client_task.set_result(None)
1099+
mock_imap.wait_hello_from_server = AsyncMock()
1100+
mock_imap.login = AsyncMock()
1101+
mock_imap.select = AsyncMock()
1102+
mock_imap.logout = AsyncMock()
1103+
1104+
mock_protocol = MagicMock()
1105+
mock_protocol.capabilities = {"SORT", "IMAP4rev1"}
1106+
mock_imap.protocol = mock_protocol
1107+
1108+
# Only 2 emails, but request page 10
1109+
sort_response = [b"1 2"]
1110+
mock_imap.uid = AsyncMock(return_value=(None, sort_response))
1111+
1112+
with patch.object(email_client, "imap_class", return_value=mock_imap):
1113+
emails = []
1114+
async for email_data in email_client.get_emails_metadata_stream(page=10, page_size=10):
1115+
emails.append(email_data)
1116+
1117+
assert len(emails) == 0
1118+
1119+
@pytest.mark.asyncio
1120+
async def test_get_emails_stream_empty_email_ids_after_split(self, email_client):
1121+
"""Test handling when email_ids is empty after split (covers 494-495)."""
1122+
mock_imap = AsyncMock()
1123+
mock_imap._client_task = asyncio.Future()
1124+
mock_imap._client_task.set_result(None)
1125+
mock_imap.wait_hello_from_server = AsyncMock()
1126+
mock_imap.login = AsyncMock()
1127+
mock_imap.select = AsyncMock()
1128+
# Return non-empty response that splits to empty list
1129+
mock_imap.uid_search = AsyncMock(return_value=(None, [b" "]))
1130+
mock_imap.logout = AsyncMock()
1131+
1132+
mock_protocol = MagicMock()
1133+
mock_protocol.capabilities = set() # No SORT
1134+
mock_imap.protocol = mock_protocol
1135+
1136+
with patch.object(email_client, "imap_class", return_value=mock_imap):
1137+
emails = []
1138+
async for email_data in email_client.get_emails_metadata_stream(page=1, page_size=10):
1139+
emails.append(email_data)
1140+
1141+
assert len(emails) == 0
1142+
1143+
@pytest.mark.asyncio
1144+
async def test_get_emails_stream_fallback_empty_page(self, email_client):
1145+
"""Test handling when pagination results in empty page in fallback path (covers 518)."""
1146+
mock_imap = AsyncMock()
1147+
mock_imap._client_task = asyncio.Future()
1148+
mock_imap._client_task.set_result(None)
1149+
mock_imap.wait_hello_from_server = AsyncMock()
1150+
mock_imap.login = AsyncMock()
1151+
mock_imap.select = AsyncMock()
1152+
mock_imap.uid_search = AsyncMock(return_value=(None, [b"1 2"]))
1153+
mock_imap.logout = AsyncMock()
1154+
1155+
mock_protocol = MagicMock()
1156+
mock_protocol.capabilities = set() # No SORT
1157+
mock_imap.protocol = mock_protocol
1158+
1159+
date_response = [
1160+
b"1 FETCH (UID 1 BODY[HEADER.FIELDS (DATE)] {30}",
1161+
bytearray(b"Date: Mon, 1 Jan 2024 00:00:00 +0000\r\n"),
1162+
b"2 FETCH (UID 2 BODY[HEADER.FIELDS (DATE)] {30}",
1163+
bytearray(b"Date: Tue, 2 Jan 2024 00:00:00 +0000\r\n"),
1164+
]
1165+
1166+
mock_imap.uid = AsyncMock(return_value=(None, date_response))
1167+
1168+
with patch.object(email_client, "imap_class", return_value=mock_imap):
1169+
emails = []
1170+
# Request page 10 when only 2 emails exist
1171+
async for email_data in email_client.get_emails_metadata_stream(page=10, page_size=10):
1172+
emails.append(email_data)
1173+
1174+
assert len(emails) == 0
1175+
1176+
@pytest.mark.asyncio
1177+
async def test_get_emails_stream_logout_error(self, email_client):
1178+
"""Test handling of logout error (covers 534-535)."""
1179+
mock_imap = AsyncMock()
1180+
mock_imap._client_task = asyncio.Future()
1181+
mock_imap._client_task.set_result(None)
1182+
mock_imap.wait_hello_from_server = AsyncMock()
1183+
mock_imap.login = AsyncMock()
1184+
mock_imap.select = AsyncMock()
1185+
mock_imap.uid_search = AsyncMock(return_value=(None, [b"1"]))
1186+
# Logout raises an error
1187+
mock_imap.logout = AsyncMock(side_effect=Exception("Connection lost"))
1188+
1189+
mock_protocol = MagicMock()
1190+
mock_protocol.capabilities = set()
1191+
mock_imap.protocol = mock_protocol
1192+
1193+
date_response = [
1194+
b"1 FETCH (UID 1 BODY[HEADER.FIELDS (DATE)] {30}",
1195+
bytearray(b"Date: Mon, 1 Jan 2024 00:00:00 +0000\r\n"),
1196+
]
1197+
header_response = [
1198+
b"1 FETCH (UID 1 BODY[HEADER] {100}",
1199+
bytearray(
1200+
b"From: sender@example.com\r\nTo: recipient@example.com\r\nSubject: Test\r\nDate: Mon, 1 Jan 2024 00:00:00 +0000\r\n\r\n"
1201+
),
1202+
]
1203+
1204+
def uid_side_effect(cmd, uid_list, fetch_type):
1205+
if "HEADER.FIELDS" in fetch_type:
1206+
return (None, date_response)
1207+
else:
1208+
return (None, header_response)
1209+
1210+
mock_imap.uid = AsyncMock(side_effect=uid_side_effect)
1211+
1212+
with patch.object(email_client, "imap_class", return_value=mock_imap):
1213+
emails = []
1214+
# Should complete successfully despite logout error
1215+
async for email_data in email_client.get_emails_metadata_stream(page=1, page_size=10):
1216+
emails.append(email_data)
1217+
1218+
assert len(emails) == 1
1219+
assert emails[0]["email_id"] == "1"
1220+
10681221

10691222
class TestDeleteEmails:
10701223
"""Tests for delete_emails functionality."""

0 commit comments

Comments
 (0)