PR Checks / hadolint (pull_request) Successful in 18s
PR Checks / pytest (pull_request) Successful in 1m59s
Docker Build and Push / test (pull_request) Successful in 2m7s
Docker Build and Push / integration-test (pull_request) Successful in 2m14s
PR Checks / build-test (pull_request) Successful in 2m28s
Docker Build and Push / build (pull_request) Successful in 1m50s
Root cause of the CI 404s: test modules mutated os.environ at import time (test_server.py set COLLECTION_NAME='test', test_integration.py 'test_mcp_maildir'). With pytest's alphabetical import order, server.COLLECTION_NAME froze at whichever value was set when server was first imported, while the fixture read os.environ at runtime — often a different name (log proof: fixture created 'test', server queried 'test_mcp_maildir' → 404 on all 6 tests). conftest.py now sets QDRANT_URL/COLLECTION_NAME/EMBEDDING_MODEL_NAME once, before any test module import; env mutation removed from test files. Unit suite still green (43 passed, 6 deselected).
164 lines
5.9 KiB
Python
164 lines
5.9 KiB
Python
"""Unit tests for mcp-maildir server pure functions.
|
|
|
|
Env vars are set in tests/conftest.py (shared session-wide) — see the
|
|
conftest docstring for why test modules must not touch os.environ.
|
|
"""
|
|
|
|
from server import (
|
|
normalize_email_address,
|
|
payload_matches_participant,
|
|
format_search_result,
|
|
build_filter,
|
|
)
|
|
|
|
|
|
class TestNormalizeEmailAddress:
|
|
def test_empty_value(self):
|
|
assert normalize_email_address("") == ""
|
|
assert normalize_email_address(None) == ""
|
|
|
|
def test_simple_email(self):
|
|
assert normalize_email_address("user@example.com") == "user@example.com"
|
|
|
|
def test_display_name_with_email(self):
|
|
assert normalize_email_address("John Doe <john@example.com>") == "john@example.com"
|
|
|
|
def test_whitespace_and_case(self):
|
|
assert normalize_email_address(" USER@Example.COM ") == "user@example.com"
|
|
|
|
def test_invalid_email_fallback(self):
|
|
result = normalize_email_address("not-an-email")
|
|
# Returns stripped lowercase version of the input
|
|
assert result == "not-an-email"
|
|
|
|
|
|
class TestPayloadMatchesParticipant:
|
|
def test_sender_match_normalized(self):
|
|
payload = {"sender": "alice@example.com"}
|
|
assert payload_matches_participant(payload, "alice@example.com") is True
|
|
|
|
def test_receiver_match_normalized(self):
|
|
payload = {"receiver": "bob@example.com"}
|
|
assert payload_matches_participant(payload, "bob@example.com") is True
|
|
|
|
def test_sender_raw_match(self):
|
|
payload = {"sender_raw": "alice@other.com"}
|
|
assert payload_matches_participant(payload, "alice@other.com") is True
|
|
|
|
def test_no_match(self):
|
|
payload = {"sender": "alice@example.com", "receiver": "bob@example.com"}
|
|
assert payload_matches_participant(payload, "carol@example.com") is False
|
|
|
|
def test_display_name_in_sender(self):
|
|
payload = {"sender": "Alice <alice@example.com>"}
|
|
assert payload_matches_participant(payload, "alice@example.com") is True
|
|
|
|
def test_display_name_in_receiver(self):
|
|
payload = {"receiver": "Bob Smith <bob@example.com>"}
|
|
assert payload_matches_participant(payload, "bob@example.com") is True
|
|
|
|
def test_empty_payload(self):
|
|
assert payload_matches_participant({}, "someone@example.com") is False
|
|
|
|
def test_case_insensitive(self):
|
|
payload = {"sender": "ALICE@EXAMPLE.COM"}
|
|
assert payload_matches_participant(payload, "alice@example.com") is True
|
|
|
|
|
|
class TestFormatSearchResult:
|
|
def test_basic_formatting(self):
|
|
class MockPoint:
|
|
payload = {
|
|
"message_id": "<abc@def>",
|
|
"date": "2026-01-15T10:00:00",
|
|
"sender": "alice@example.com",
|
|
"receiver": "bob@example.com",
|
|
"subject": "Hello",
|
|
"attachments": ["file.pdf"],
|
|
}
|
|
score = 0.95
|
|
|
|
result = format_search_result(MockPoint())
|
|
assert result["message_id"] == "<abc@def>"
|
|
assert result["date"] == "2026-01-15T10:00:00"
|
|
assert result["sender"] == "alice@example.com"
|
|
assert result["receiver"] == "bob@example.com"
|
|
assert result["subject"] == "Hello"
|
|
assert result["attachments"] == ["file.pdf"]
|
|
assert result["score"] == 0.95
|
|
|
|
def test_empty_payload(self):
|
|
class MockPoint:
|
|
payload = None
|
|
score = None
|
|
|
|
result = format_search_result(MockPoint())
|
|
assert result["message_id"] is None
|
|
assert result["score"] is None
|
|
assert result["attachments"] == []
|
|
|
|
def test_missing_fields(self):
|
|
class MockPoint:
|
|
payload = {"message_id": "<123>"}
|
|
score = 0.5
|
|
|
|
result = format_search_result(MockPoint())
|
|
assert result["message_id"] == "<123>"
|
|
assert result["date"] is None
|
|
assert result["sender"] is None
|
|
assert result["receiver"] is None
|
|
assert result["subject"] is None
|
|
assert result["attachments"] == []
|
|
|
|
|
|
class TestBuildFilter:
|
|
def test_no_filters_returns_none(self):
|
|
assert build_filter(None, None, None) is None
|
|
|
|
def test_empty_strings_return_none(self):
|
|
assert build_filter("", "", "") is None
|
|
|
|
def test_participant_filter_is_should_clause_on_sender_and_receiver(self):
|
|
f = build_filter("alice@example.com", None, None)
|
|
assert f is not None
|
|
# The date conditions list must be absent and only the participant filter present
|
|
assert len(f.must) == 1
|
|
participant = f.must[0]
|
|
keys = [c.key for c in participant.should]
|
|
assert keys == ["sender", "receiver"]
|
|
assert all(c.match.value == "alice@example.com" for c in participant.should)
|
|
|
|
def test_participant_normalized_before_match(self):
|
|
f = build_filter("Alice <Alice@Example.COM>", None, None)
|
|
should = f.must[0].should
|
|
assert all(c.match.value == "alice@example.com" for c in should)
|
|
|
|
def test_date_range_only(self):
|
|
f = build_filter(None, "2026-01-01", "2026-06-30")
|
|
assert len(f.must) == 1
|
|
cond = f.must[0]
|
|
assert cond.key == "date"
|
|
assert cond.range.gte is not None
|
|
assert cond.range.lte is not None
|
|
|
|
def test_start_date_only(self):
|
|
f = build_filter(None, "2026-03-15", None)
|
|
cond = f.must[0]
|
|
assert cond.range.gte is not None
|
|
assert cond.range.lte is None
|
|
|
|
def test_end_date_only(self):
|
|
f = build_filter(None, None, "2026-03-15")
|
|
cond = f.must[0]
|
|
assert cond.range.gte is None
|
|
assert cond.range.lte is not None
|
|
|
|
def test_combined_participant_and_date(self):
|
|
f = build_filter("bob@example.org", "2026-01-01", "2026-12-31")
|
|
assert len(f.must) == 2
|
|
# One is the participant should-filter, the other the date range
|
|
kinds = sorted(
|
|
"participant" if getattr(c, "should", None) else "date" for c in f.must
|
|
)
|
|
assert kinds == ["date", "participant"]
|