refactor: use __len__ for MailDebugRecorder, str() size estimate, clearer tests, fix unused import
Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/5d2918de-630e-4a66-8355-1b036c620b1c Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
aab60d6866
commit
eeb3fa5c66
@@ -4,7 +4,6 @@ Supports both POP3 and IMAP protocols with secure connections.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json as _json
|
|
||||||
import poplib
|
import poplib
|
||||||
import re
|
import re
|
||||||
import smtplib
|
import smtplib
|
||||||
@@ -198,7 +197,7 @@ class MailDebugRecorder:
|
|||||||
if data:
|
if data:
|
||||||
entry["data"] = data
|
entry["data"] = data
|
||||||
try:
|
try:
|
||||||
entry_bytes = len(_json.dumps(entry, default=str))
|
entry_bytes = len(str(entry))
|
||||||
except Exception:
|
except Exception:
|
||||||
entry_bytes = 256 # conservative fallback
|
entry_bytes = 256 # conservative fallback
|
||||||
if (
|
if (
|
||||||
@@ -223,6 +222,9 @@ class MailDebugRecorder:
|
|||||||
def has_entries(self) -> bool:
|
def has_entries(self) -> bool:
|
||||||
return bool(self._entries)
|
return bool(self._entries)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._entries)
|
||||||
|
|
||||||
def as_details(self) -> Dict[str, Any]:
|
def as_details(self) -> Dict[str, Any]:
|
||||||
"""Return the trace as a dict suitable for storage in error_details."""
|
"""Return the trace as a dict suitable for storage in error_details."""
|
||||||
return {"trace": self._entries, "truncated": self._truncated}
|
return {"trace": self._entries, "truncated": self._truncated}
|
||||||
|
|||||||
@@ -479,7 +479,7 @@ async def process_mail_account(account_id: int):
|
|||||||
mail_account_id=account.id,
|
mail_account_id=account.id,
|
||||||
processing_run_id=run.id,
|
processing_run_id=run.id,
|
||||||
level="DEBUG",
|
level="DEBUG",
|
||||||
message=f"Connection trace ({len(_debug_recorder._entries)} entries)",
|
message=f"Connection trace ({len(_debug_recorder)} entries)",
|
||||||
success=True,
|
success=True,
|
||||||
error_details=_debug_recorder.as_details(),
|
error_details=_debug_recorder.as_details(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ class TestFormatConnectionErrorTimeout:
|
|||||||
def test_socket_timeout_includes_host_port(self):
|
def test_socket_timeout_includes_host_port(self):
|
||||||
exc = socket.timeout("timed out")
|
exc = socket.timeout("timed out")
|
||||||
msg = _fmt(exc, host="imap.gmx.net", port=993)
|
msg = _fmt(exc, host="imap.gmx.net", port=993)
|
||||||
assert "imap.gmx.net:993" in msg
|
# Verify both the host and timeout indicator appear in the message
|
||||||
|
assert "imap.gmx.net" in msg
|
||||||
|
assert "993" in msg
|
||||||
assert "timed out" in msg.lower() or "timeout" in msg.lower()
|
assert "timed out" in msg.lower() or "timeout" in msg.lower()
|
||||||
|
|
||||||
def test_asyncio_timeout_error(self):
|
def test_asyncio_timeout_error(self):
|
||||||
@@ -89,7 +91,9 @@ class TestFormatConnectionErrorRefused:
|
|||||||
def test_connection_refused_includes_host_port(self):
|
def test_connection_refused_includes_host_port(self):
|
||||||
exc = ConnectionRefusedError(111, "Connection refused")
|
exc = ConnectionRefusedError(111, "Connection refused")
|
||||||
msg = _fmt(exc, host="smtp.example.com", port=587)
|
msg = _fmt(exc, host="smtp.example.com", port=587)
|
||||||
assert "smtp.example.com:587" in msg
|
# Verify both host and port appear in the message
|
||||||
|
assert "smtp.example.com" in msg
|
||||||
|
assert "587" in msg
|
||||||
assert "refused" in msg.lower()
|
assert "refused" in msg.lower()
|
||||||
|
|
||||||
def test_connection_reset(self):
|
def test_connection_reset(self):
|
||||||
@@ -133,7 +137,9 @@ class TestFormatConnectionErrorPop3:
|
|||||||
def test_auth_error_identifies_auth_failure(self):
|
def test_auth_error_identifies_auth_failure(self):
|
||||||
exc = poplib.error_proto("-ERR authentication failed")
|
exc = poplib.error_proto("-ERR authentication failed")
|
||||||
msg = _format_connection_error(exc, "pop.example.com", 995, "POP3")
|
msg = _format_connection_error(exc, "pop.example.com", 995, "POP3")
|
||||||
assert "pop.example.com:995" in msg
|
# Verify both host and port appear in the message
|
||||||
|
assert "pop.example.com" in msg
|
||||||
|
assert "995" in msg
|
||||||
assert "Authentication" in msg or "authentication" in msg
|
assert "Authentication" in msg or "authentication" in msg
|
||||||
|
|
||||||
def test_generic_pop3_error(self):
|
def test_generic_pop3_error(self):
|
||||||
@@ -210,9 +216,9 @@ class TestMailDebugRecorder:
|
|||||||
details = rec.as_details()
|
details = rec.as_details()
|
||||||
assert details["truncated"]
|
assert details["truncated"]
|
||||||
# After truncation, recording new entries is a no-op
|
# After truncation, recording new entries is a no-op
|
||||||
prev_count = len(details["trace"])
|
prev_count = len(rec)
|
||||||
rec.record("after_truncate", "should be ignored")
|
rec.record("after_truncate", "should be ignored")
|
||||||
assert len(rec.as_details()["trace"]) == prev_count
|
assert len(rec) == prev_count
|
||||||
|
|
||||||
def test_entry_cap_triggers_truncation(self):
|
def test_entry_cap_triggers_truncation(self):
|
||||||
"""Recording more than _MAX_TRACE_ENTRIES entries truncates."""
|
"""Recording more than _MAX_TRACE_ENTRIES entries truncates."""
|
||||||
@@ -223,28 +229,24 @@ class TestMailDebugRecorder:
|
|||||||
# Total entries should be MAX + 1 (the truncation sentinel)
|
# Total entries should be MAX + 1 (the truncation sentinel)
|
||||||
assert len(rec.as_details()["trace"]) <= _MAX_TRACE_ENTRIES + 1
|
assert len(rec.as_details()["trace"]) <= _MAX_TRACE_ENTRIES + 1
|
||||||
|
|
||||||
def test_no_password_in_trace(self):
|
def test_no_password_in_trace_from_production_phases(self):
|
||||||
"""Passwords must never appear in any trace entry."""
|
"""Production instrumentation records only usernames and counts, never passwords.
|
||||||
|
|
||||||
|
The recorder itself has no auto-redaction; the contract is that callers
|
||||||
|
(the instrumented IMAP/POP3 code) must never pass credential data.
|
||||||
|
This test verifies the expected production-phase entries contain no
|
||||||
|
password strings.
|
||||||
|
"""
|
||||||
password = "s3cr3tP@ssw0rd!"
|
password = "s3cr3tP@ssw0rd!"
|
||||||
rec = MailDebugRecorder()
|
rec = MailDebugRecorder()
|
||||||
# Simulate what instrumented code records — only timing and counts
|
# Simulate the entries that production code actually records
|
||||||
rec.record("auth", f"Authenticated as user@example.com", {"elapsed_ms": 5})
|
rec.record("auth", f"Authenticated as user@example.com", {"elapsed_ms": 5})
|
||||||
rec.record("stat", "Mailbox has 3 messages", {"count": 3})
|
rec.record("stat", "Mailbox has 3 messages", {"count": 3})
|
||||||
# A bug might accidentally include the password — assert it doesn't
|
|
||||||
rec.record("bad_attempt", f"user={password}") # test robustness
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
serialised = json.dumps(rec.as_details())
|
serialised = json.dumps(rec.as_details())
|
||||||
# The entries only include the phase/msg/data we explicitly pass in
|
# The password must not appear in any of these entries
|
||||||
# production code — we just verify the test doesn't accidentally
|
assert password not in serialised
|
||||||
# redact by accident. More importantly, prod code never passes the
|
|
||||||
# actual password object, only usernames and counts.
|
|
||||||
# Here we did pass it deliberately, so it IS present — the real
|
|
||||||
# protection is in the instrumentation not passing it.
|
|
||||||
# This test documents that the recorder itself has no auto-redaction
|
|
||||||
# (the contract is that callers must not pass credentials).
|
|
||||||
assert serialised # non-empty serialisation
|
|
||||||
|
|
||||||
def test_timestamps_are_iso_format(self):
|
def test_timestamps_are_iso_format(self):
|
||||||
rec = MailDebugRecorder()
|
rec = MailDebugRecorder()
|
||||||
|
|||||||
Reference in New Issue
Block a user