Skip to content

Commit 5884fe9

Browse files
Jack Kochclaude
andcommitted
Test: Add edge case tests for _batch_fetch_headers
Sync local-dev with PR ai-zerolab#111 tests covering all edge cases: - Skips non-bytes items in response - Skips items without BODY[HEADER] marker - Handles truncated responses - Handles non-bytearray content - Handles non-bytes UID item - Handles missing UID in response - Handles mixed valid/invalid items Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1f915c9 commit 5884fe9

File tree

1 file changed

+148
-0
lines changed

1 file changed

+148
-0
lines changed

tests/test_email_client.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,3 +867,151 @@ async def test_batch_fetch_headers_preserves_uid_mapping(self, email_client):
867867
assert len(result) == 2
868868
assert result["100"]["subject"] == "First"
869869
assert result["200"]["subject"] == "Second"
870+
871+
@pytest.mark.asyncio
872+
async def test_batch_fetch_headers_skips_non_bytes_items(self, email_client):
873+
"""Test that _batch_fetch_headers skips non-bytes items in response."""
874+
mock_imap = AsyncMock()
875+
mock_imap.uid = AsyncMock(
876+
return_value=(
877+
None,
878+
[
879+
"not bytes", # Should be skipped
880+
b"1 FETCH (BODY[HEADER] {50}",
881+
bytearray(b"From: a@test.com\r\nSubject: Test\r\n\r\n"),
882+
b" UID 100)",
883+
],
884+
)
885+
)
886+
887+
result = await email_client._batch_fetch_headers(mock_imap, ["100"])
888+
889+
assert "100" in result
890+
assert result["100"]["subject"] == "Test"
891+
892+
@pytest.mark.asyncio
893+
async def test_batch_fetch_headers_skips_items_without_body_header(self, email_client):
894+
"""Test that _batch_fetch_headers skips bytes without BODY[HEADER]."""
895+
mock_imap = AsyncMock()
896+
mock_imap.uid = AsyncMock(
897+
return_value=(
898+
None,
899+
[
900+
b"some other data", # No BODY[HEADER], should be skipped
901+
b"1 FETCH (BODY[HEADER] {50}",
902+
bytearray(b"From: a@test.com\r\nSubject: Test\r\n\r\n"),
903+
b" UID 100)",
904+
],
905+
)
906+
)
907+
908+
result = await email_client._batch_fetch_headers(mock_imap, ["100"])
909+
910+
assert "100" in result
911+
assert result["100"]["subject"] == "Test"
912+
913+
@pytest.mark.asyncio
914+
async def test_batch_fetch_headers_skips_truncated_response(self, email_client):
915+
"""Test that _batch_fetch_headers skips when i+2 >= len(data)."""
916+
mock_imap = AsyncMock()
917+
mock_imap.uid = AsyncMock(
918+
return_value=(
919+
None,
920+
[
921+
b"1 FETCH (BODY[HEADER] {50}",
922+
bytearray(b"From: a@test.com\r\nSubject: Test\r\n\r\n"),
923+
# Missing UID line (i+2 doesn't exist)
924+
],
925+
)
926+
)
927+
928+
result = await email_client._batch_fetch_headers(mock_imap, ["100"])
929+
930+
# Should return empty dict since response is truncated
931+
assert result == {}
932+
933+
@pytest.mark.asyncio
934+
async def test_batch_fetch_headers_skips_non_bytearray_content(self, email_client):
935+
"""Test that _batch_fetch_headers skips when data[i+1] is not bytearray."""
936+
mock_imap = AsyncMock()
937+
mock_imap.uid = AsyncMock(
938+
return_value=(
939+
None,
940+
[
941+
b"1 FETCH (BODY[HEADER] {50}",
942+
b"not a bytearray", # Should be bytearray, not bytes
943+
b" UID 100)",
944+
],
945+
)
946+
)
947+
948+
result = await email_client._batch_fetch_headers(mock_imap, ["100"])
949+
950+
# Should return empty dict since content is not bytearray
951+
assert result == {}
952+
953+
@pytest.mark.asyncio
954+
async def test_batch_fetch_headers_skips_non_bytes_uid_item(self, email_client):
955+
"""Test that _batch_fetch_headers skips when data[i+2] is not bytes."""
956+
mock_imap = AsyncMock()
957+
mock_imap.uid = AsyncMock(
958+
return_value=(
959+
None,
960+
[
961+
b"1 FETCH (BODY[HEADER] {50}",
962+
bytearray(b"From: a@test.com\r\nSubject: Test\r\n\r\n"),
963+
12345, # Not bytes, should result in uid_item = None
964+
],
965+
)
966+
)
967+
968+
result = await email_client._batch_fetch_headers(mock_imap, ["100"])
969+
970+
# Should return empty dict since UID item is not bytes
971+
assert result == {}
972+
973+
@pytest.mark.asyncio
974+
async def test_batch_fetch_headers_skips_missing_uid_in_response(self, email_client):
975+
"""Test that _batch_fetch_headers skips when UID regex doesn't match."""
976+
mock_imap = AsyncMock()
977+
mock_imap.uid = AsyncMock(
978+
return_value=(
979+
None,
980+
[
981+
b"1 FETCH (BODY[HEADER] {50}",
982+
bytearray(b"From: a@test.com\r\nSubject: Test\r\n\r\n"),
983+
b" NO_UID_HERE)", # No UID in this line
984+
],
985+
)
986+
)
987+
988+
result = await email_client._batch_fetch_headers(mock_imap, ["100"])
989+
990+
# Should return empty dict since UID regex doesn't match
991+
assert result == {}
992+
993+
@pytest.mark.asyncio
994+
async def test_batch_fetch_headers_handles_mixed_valid_invalid(self, email_client):
995+
"""Test that _batch_fetch_headers processes valid items and skips invalid ones."""
996+
mock_imap = AsyncMock()
997+
mock_imap.uid = AsyncMock(
998+
return_value=(
999+
None,
1000+
[
1001+
# Invalid: truncated (no UID line)
1002+
b"1 FETCH (BODY[HEADER] {50}",
1003+
bytearray(b"From: bad@test.com\r\nSubject: Bad\r\n\r\n"),
1004+
# Valid email
1005+
b"2 FETCH (BODY[HEADER] {50}",
1006+
bytearray(b"From: good@test.com\r\nSubject: Good\r\n\r\n"),
1007+
b" UID 200)",
1008+
],
1009+
)
1010+
)
1011+
1012+
result = await email_client._batch_fetch_headers(mock_imap, ["100", "200"])
1013+
1014+
# Only the valid email should be in results
1015+
assert len(result) == 1
1016+
assert "200" in result
1017+
assert result["200"]["subject"] == "Good"

0 commit comments

Comments
 (0)