feat: Add CalendarInvitePanel to display invite details in mail app
- Create CalendarInvitePanel widget showing event summary, time, location, organizer, and attendees with accept/decline/tentative buttons - Add is_calendar_email() to notification_detector for detecting invite emails - Add get_raw_message() to himalaya client for exporting full MIME content - Refactor calendar_parser.py with proper icalendar parsing (METHOD at VCALENDAR level, not VEVENT) - Integrate calendar panel into ContentContainer.display_content flow - Update tests for new calendar parsing API - Minor: fix today's header style in calendar WeekGrid
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
"""Unit tests for calendar email detection and ICS parsing."""
|
||||
|
||||
import pytest
|
||||
from src.mail.utils import calendar_parser
|
||||
from src.mail.utils.calendar_parser import (
|
||||
parse_ics_content,
|
||||
parse_calendar_from_raw_message,
|
||||
extract_ics_from_mime,
|
||||
is_cancelled_event,
|
||||
is_event_request,
|
||||
ParsedCalendarEvent,
|
||||
)
|
||||
from src.mail.notification_detector import is_calendar_email
|
||||
|
||||
|
||||
@@ -44,58 +51,91 @@ class TestICSParsing:
|
||||
"""Test ICS file parsing."""
|
||||
|
||||
def test_parse_cancellation_ics(self):
|
||||
"""Test parsing of cancellation ICS from test fixture."""
|
||||
import base64
|
||||
from pathlib import Path
|
||||
"""Test parsing of cancellation ICS."""
|
||||
ics_content = """BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
METHOD:CANCEL
|
||||
BEGIN:VEVENT
|
||||
UID:test-uid-001
|
||||
SUMMARY:Technical Refinement Meeting
|
||||
DTSTART:20251230T140000Z
|
||||
DTEND:20251230T150000Z
|
||||
STATUS:CANCELLED
|
||||
ORGANIZER;CN=Test Organizer:mailto:organizer@example.com
|
||||
ATTENDEE;CN=Test Attendee:mailto:attendee@example.com
|
||||
END:VEVENT
|
||||
END:VCALENDAR"""
|
||||
|
||||
fixture_path = Path(
|
||||
"tests/fixtures/test_mailbox/INBOX/cur/17051226-calendar-invite-001.test:2"
|
||||
)
|
||||
if not fixture_path.exists():
|
||||
pytest.skip("Test fixture file not found")
|
||||
return
|
||||
|
||||
with open(fixture_path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
event = parse_calendar_part(content)
|
||||
event = parse_ics_content(ics_content)
|
||||
assert event is not None
|
||||
assert is_cancelled_event(event) is True
|
||||
assert event.method == "CANCEL"
|
||||
assert event.summary == "Technical Refinement Meeting"
|
||||
|
||||
def test_parse_invite_ics(self):
|
||||
"""Test parsing of invite ICS from test fixture."""
|
||||
import base64
|
||||
from pathlib import Path
|
||||
"""Test parsing of invite/request ICS."""
|
||||
ics_content = """BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Test//Test//EN
|
||||
METHOD:REQUEST
|
||||
BEGIN:VEVENT
|
||||
UID:test-uid-002
|
||||
SUMMARY:Team Standup
|
||||
DTSTART:20251230T100000Z
|
||||
DTEND:20251230T103000Z
|
||||
STATUS:CONFIRMED
|
||||
ORGANIZER;CN=Test Organizer:mailto:organizer@example.com
|
||||
ATTENDEE;CN=Test Attendee:mailto:attendee@example.com
|
||||
LOCATION:Conference Room A
|
||||
END:VEVENT
|
||||
END:VCALENDAR"""
|
||||
|
||||
fixture_path = Path(
|
||||
"tests/fixtures/test_mailbox/INBOX/cur/17051226-calendar-invite-001.test:2"
|
||||
)
|
||||
if not fixture_path.exists():
|
||||
pytest.skip("Test fixture file not found")
|
||||
return
|
||||
|
||||
with open(fixture_path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
event = parse_calendar_part(content)
|
||||
event = parse_ics_content(ics_content)
|
||||
assert event is not None
|
||||
assert is_event_request(event) is True
|
||||
assert event.method == "REQUEST"
|
||||
assert event.summary == "Technical Refinement Meeting"
|
||||
assert event.summary == "Team Standup"
|
||||
assert event.location == "Conference Room A"
|
||||
|
||||
def test_invalid_ics(self):
|
||||
"""Test parsing of invalid ICS content."""
|
||||
event = parse_calendar_part("invalid ics content")
|
||||
|
||||
event = parse_ics_content("invalid ics content")
|
||||
assert event is None # Should return None for invalid ICS
|
||||
|
||||
def test_base64_decoding(self):
|
||||
"""Test base64 decoding of ICS attachment."""
|
||||
# Test that we can decode base64
|
||||
encoded = "BASE64ENCODED_I_TEST"
|
||||
import base64
|
||||
def test_extract_ics_from_mime(self):
|
||||
"""Test extraction of ICS from raw MIME message."""
|
||||
raw_message = """From: organizer@example.com
|
||||
To: attendee@example.com
|
||||
Subject: Meeting Invite
|
||||
Content-Type: multipart/mixed; boundary="boundary123"
|
||||
|
||||
decoded = base64.b64decode(encoded)
|
||||
assert decoded == encoded
|
||||
--boundary123
|
||||
Content-Type: text/plain
|
||||
|
||||
You have been invited to a meeting.
|
||||
|
||||
--boundary123
|
||||
Content-Type: text/calendar
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:REQUEST
|
||||
BEGIN:VEVENT
|
||||
UID:mime-test-001
|
||||
SUMMARY:MIME Test Meeting
|
||||
DTSTART:20251230T140000Z
|
||||
DTEND:20251230T150000Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
--boundary123--
|
||||
"""
|
||||
ics = extract_ics_from_mime(raw_message)
|
||||
assert ics is not None
|
||||
assert "BEGIN:VCALENDAR" in ics
|
||||
assert "MIME Test Meeting" in ics
|
||||
|
||||
event = parse_ics_content(ics)
|
||||
assert event is not None
|
||||
assert event.summary == "MIME Test Meeting"
|
||||
assert event.method == "REQUEST"
|
||||
|
||||
Reference in New Issue
Block a user