44import ics
55import pytest
66
7- from couchers .email .calendar_events import create_host_request_ics
7+ from couchers .email .calendar_events import create_host_request_cancellation_ics , create_host_request_ics
88from couchers .i18n .context import LocalizationContext
99from couchers .proto import conversations_pb2 , requests_pb2
1010from couchers .proto .requests_pb2 import HostRequest
1111from couchers .utils import today
1212from tests .fixtures .db import generate_user
13- from tests .fixtures .misc import email_fields , mock_notification_email
13+ from tests .fixtures .misc import Moderator , email_fields , mock_notification_email
1414from tests .fixtures .sessions import requests_session
1515from tests .test_requests import valid_request_text
1616
@@ -20,47 +20,79 @@ def _(testconfig):
2020 pass
2121
2222
23- def test_ics_content ():
23+ def test_initial_ics_content ():
2424 host_request = HostRequest (
2525 host_request_id = 42 , from_date = "2000-01-01" , to_date = "2000-01-02" , hosting_city = "New York"
2626 )
2727
2828 ics = create_host_request_ics (
2929 host_request , other_name = "Bob" , hosting = True , loc_context = LocalizationContext .en_utc ()
3030 )
31- ics = ics .replace ("\r \n " , "\n " )
31+ assert _normalize_ics (ics ) == _normalize_ics ("""
32+ BEGIN:VCALENDAR
33+ VERSION:2.0
34+ PRODID:-//Couchers.org//Couchers//EN
35+ BEGIN:VEVENT
36+ SEQUENCE:0
37+ DTSTART;VALUE=DATE:20000101
38+ DTEND;VALUE=DATE:20000103
39+ DESCRIPTION:<stripped>/42
40+ LOCATION:New York
41+ SUMMARY:Hosting Bob
42+ UID:host_request.42@<stripped>
43+ URL:<stripped>/42
44+ END:VEVENT
45+ METHOD:PUBLISH
46+ END:VCALENDAR
47+ """ )
3248
33- # Strip the domain in the UID, which depends on environment variables
34- ics = re .sub (
35- r"^UID:.*@(.*)$" , lambda match : match [0 ].removesuffix (match [1 ]) + "<stripped>" , ics , flags = re .MULTILINE
36- )
3749
38- # Strip the domain in the URL and DESCRIPTION, which depends on environment variables
39- ics = re .sub (r"^(DESCRIPTION|URL):(.*)/(\d+)" , r"\1:<stripped>/\3" , ics , flags = re .MULTILINE )
50+ def test_cancellation_ics_content ():
51+ host_request = HostRequest (
52+ host_request_id = 42 , from_date = "2000-01-01" , to_date = "2000-01-02" , hosting_city = "New York"
53+ )
4054
41- assert (
42- ics
43- == """
55+ ics = create_host_request_cancellation_ics (
56+ host_request , other_name = "Bob" , hosting = True , loc_context = LocalizationContext .en_utc ()
57+ )
58+ assert _normalize_ics (ics ) == _normalize_ics ("""
4459BEGIN:VCALENDAR
4560VERSION:2.0
4661PRODID:-//Couchers.org//Couchers//EN
4762BEGIN:VEVENT
48- SEQUENCE:0
63+ SEQUENCE:1
4964DTSTART;VALUE=DATE:20000101
5065DTEND;VALUE=DATE:20000103
5166DESCRIPTION:<stripped>/42
5267LOCATION:New York
53- SUMMARY:Hosting Bob
68+ STATUS:CANCELLED
69+ SUMMARY:Cancelled: Hosting Bob
5470UID:host_request.42@<stripped>
5571URL:<stripped>/42
5672END:VEVENT
5773METHOD:PUBLISH
5874END:VCALENDAR
59- """ .strip ()
75+ """ )
76+
77+
78+ def _normalize_ics (ics : str ) -> str :
79+ # Normalize whitespace:
80+ # - The ics library produces '\r\n', in-code literals are '\n'
81+ # - In-code literals have start/end newlines and indentation.
82+ ics = ics .replace ("\r \n " , "\n " ).strip ()
83+
84+ # Strip the domain in the UID, which depends on environment variables
85+ ics = re .sub (
86+ r"^UID:.*@(.*)$" , lambda match : match [0 ].removesuffix (match [1 ]) + "<stripped>" , ics , flags = re .MULTILINE
6087 )
6188
89+ # Strip the domain in the URL and DESCRIPTION, which depends on environment variables
90+ ics = re .sub (r"^(DESCRIPTION|URL):(.*)/(\d+)" , r"\1:<stripped>/\3" , ics , flags = re .MULTILINE )
91+
92+ return ics
93+
6294
63- def test_host_request_attachments (db , moderator ):
95+ def test_host_request_attachments (db , moderator : Moderator ):
6496 host , host_token = generate_user (complete_profile = True )
6597 surfer , surfer_token = generate_user (complete_profile = True )
6698
@@ -100,6 +132,7 @@ def test_host_request_attachments(db, moderator):
100132 e = email_fields (mock )
101133 assert "accept" in e .subject and host .name in e .subject
102134 ics_event = _get_email_ics_attachment_calendar_event (e )
135+ assert _get_ics_event_sequence (ics_event ) == 0
103136 assert not ics_event .status
104137 assert ics_event .begin .date () == from_date
105138 assert ics_event .end .date () == (to_date + timedelta (days = 1 ))
@@ -120,6 +153,7 @@ def test_host_request_attachments(db, moderator):
120153 e = email_fields (mock )
121154 assert "confirm" in e .subject and surfer .name in e .subject
122155 ics_event = _get_email_ics_attachment_calendar_event (e )
156+ assert _get_ics_event_sequence (ics_event ) == 0
123157 assert not ics_event .status
124158 assert ics_event .name == f"Hosting { surfer .name } "
125159
@@ -137,6 +171,7 @@ def test_host_request_attachments(db, moderator):
137171 e = email_fields (mock )
138172 assert "cancel" in e .subject and surfer .name in e .subject
139173 ics_event = _get_email_ics_attachment_calendar_event (e )
174+ assert _get_ics_event_sequence (ics_event ) == 1
140175 assert ics_event .status == "CANCELLED"
141176
142177
@@ -147,3 +182,10 @@ def _get_email_ics_attachment_calendar_event(e) -> ics.Event:
147182 ics_calendar = ics .Calendar (ics_attachment .data .decode ("utf-8" ))
148183 assert len (ics_calendar .events ) == 1
149184 return next (iter (ics_calendar .events ))
185+
186+
187+ def _get_ics_event_sequence (event : ics .Event ) -> int | None :
188+ for x in event .extra :
189+ if x .name == "SEQUENCE" :
190+ return int (x .value )
191+ return None
0 commit comments