"""ICS calendar adapter tests: parsing semantics plus sample-data ingestion.""" from __future__ import annotations from pathlib import Path from helpers import blob from pmp.adapters.ics_calendar import ICSCalendarAdapter # A deliberately tricky minimal calendar: # - event 1: folded DESCRIPTION (RFC 5545 line folding), escaped comma in # LOCATION, explicit UTC start/end # - event 2: all-day event (VALUE=DATE) MINIMAL_ICS = ( "BEGIN:VCALENDAR\n" "VERSION:2.0\n" "PRODID:-//PMP Test Suite//EN\n" "BEGIN:VEVENT\n" "UID:evt-1@pmp-test\n" "DTSTART:20250310T090000Z\n" "DTEND:20250310T093000Z\n" "SUMMARY:Dentist appointment\n" "LOCATION:Smile Clinic\\, Berlin\n" "DESCRIPTION:Bring insurance card and arrive ten minutes early to fill in\n" " the intake form.\n" "END:VEVENT\n" "BEGIN:VEVENT\n" "UID:evt-2@pmp-test\n" "DTSTART;VALUE=DATE:20250312\n" "SUMMARY:Conference day\n" "END:VEVENT\n" "END:VCALENDAR\n" ) def _write_ics(tmp_path: Path) -> Path: path = tmp_path / "test.ics" path.write_text(MINIMAL_ICS, encoding="utf-8") return path def test_parses_both_events(tmp_path: Path): items = list(ICSCalendarAdapter().ingest(_write_ics(tmp_path))) assert len(items) == 2 combined = blob([item.content for item in items]) assert "Dentist appointment" in combined assert "Conference day" in combined def test_event_uids_surface_in_evidence(tmp_path: Path): items = list(ICSCalendarAdapter().ingest(_write_ics(tmp_path))) for uid in ("evt-1@pmp-test", "evt-2@pmp-test"): assert any( uid in blob(item.content) or uid in str(item.source_ref) for item in items ), f"event UID {uid} must appear in evidence content or source_ref" def test_folded_description_is_unfolded(tmp_path: Path): items = list(ICSCalendarAdapter().ingest(_write_ics(tmp_path))) combined = blob([item.content for item in items]) # If RFC 5545 unfolding were missing, the continuation line would be # dropped or mangled and this phrase would not survive intact. assert "intake form" in combined def test_escaped_location_text_survives(tmp_path: Path): items = list(ICSCalendarAdapter().ingest(_write_ics(tmp_path))) combined = blob([item.content for item in items]) assert "Smile Clinic" in combined def test_all_day_event_date_captured(tmp_path: Path): items = list(ICSCalendarAdapter().ingest(_write_ics(tmp_path))) combined = blob([item.content for item in items]) assert ("20250312" in combined) or ("2025-03-12" in combined) def test_items_carry_provenance_fields(tmp_path: Path): path = _write_ics(tmp_path) for item in ICSCalendarAdapter().ingest(path): assert item.content, "evidence content must be non-empty" assert item.source_ref, "evidence must carry a source reference" assert "calendar" in str(item.source_type).lower() def test_ingest_is_deterministic(tmp_path: Path): path = _write_ics(tmp_path) first = [blob(item.content) for item in ICSCalendarAdapter().ingest(path)] second = [blob(item.content) for item in ICSCalendarAdapter().ingest(path)] assert first == second def test_sample_calendars_ingest(samples_dir: Path): adapter = ICSCalendarAdapter() personal = list(adapter.ingest(samples_dir / "calendar" / "avery-personal.ics")) work = list(adapter.ingest(samples_dir / "calendar" / "avery-work.ics")) assert len(personal) >= 1 assert len(work) >= 1 items = personal + work assert len(items) >= 2 # Every sample event becomes a distinct, non-empty piece of evidence. rendered = [blob(item.content) for item in items] assert all(rendered) assert len(set(rendered)) == len(rendered)