diff --git a/app/integrations/google_workspace/google_calendar.py b/app/integrations/google_workspace/google_calendar.py index 607ac7e0..c0375117 100644 --- a/app/integrations/google_workspace/google_calendar.py +++ b/app/integrations/google_workspace/google_calendar.py @@ -8,7 +8,7 @@ execute_google_api_call, DEFAULT_DELEGATED_ADMIN_EMAIL, ) -from integrations.utils.api import convert_string_to_camel_case +from integrations.utils.api import convert_string_to_camel_case, generate_unique_id # Get the email for the SRE bot SRE_BOT_EMAIL = os.environ.get("SRE_BOT_EMAIL") @@ -71,6 +71,12 @@ def insert_event(start, end, emails, title, incident_document, **kwargs): "attendees": [{"email": email.strip()} for email in emails], "summary": title, "guestsCanModify": True, + "conferenceData": { + "createRequest": { + "requestId": generate_unique_id(), + "conferenceSolutionKey": {"type": "hangoutsMeet"}, + } + }, } if incident_document: body["attachments"] = [ @@ -103,6 +109,7 @@ def insert_event(start, end, emails, title, incident_document, **kwargs): body=body, calendarId="primary", supportsAttachments=True, + conferenceDataVersion=1, ) return result.get("htmlLink") diff --git a/app/integrations/utils/api.py b/app/integrations/utils/api.py index 7c4c52dd..c58ffd4e 100644 --- a/app/integrations/utils/api.py +++ b/app/integrations/utils/api.py @@ -1,5 +1,7 @@ """Utilities for API integrations.""" import re +import string +import random def convert_string_to_camel_case(snake_str): @@ -67,3 +69,18 @@ def convert_kwargs_to_pascal_case(kwargs): return [convert_kwargs_to_pascal_case(i) for i in kwargs] else: return kwargs + + +def generate_unique_id(): + # Define the characters to use in the ID + chars = string.ascii_lowercase + string.digits + + # Function to generate a segment of three characters + def generate_segment(): + return "".join(random.choices(chars, k=3)) + + # Generate the three segments and join them with hyphens + segments = [generate_segment() for _ in range(3)] + unique_id = "-".join(segments) + + return unique_id diff --git a/app/tests/integrations/google_workspace/test_google_calendar.py b/app/tests/integrations/google_workspace/test_google_calendar.py index eed028fe..371593c1 100644 --- a/app/tests/integrations/google_workspace/test_google_calendar.py +++ b/app/tests/integrations/google_workspace/test_google_calendar.py @@ -158,10 +158,15 @@ def test_get_freebusy_returns_object(mock_execute): @patch("os.environ.get", return_value="test_email") @patch("integrations.google_workspace.google_calendar.execute_google_api_call") @patch("integrations.google_workspace.google_calendar.convert_string_to_camel_case") +@patch("integrations.google_workspace.google_calendar.generate_unique_id") def test_insert_event_no_kwargs_no_delegated_email( - mock_convert_string_to_camel_case, mock_execute_google_api_call, mock_os_environ_get + mock_unique_id, + mock_convert_string_to_camel_case, + mock_execute_google_api_call, + mock_os_environ_get, ): mock_execute_google_api_call.return_value = {"htmlLink": "test_link"} + mock_unique_id.return_value = "abc-123-de4" start = datetime.now() end = start emails = ["test1@test.com", "test2@test.com"] @@ -189,9 +194,16 @@ def test_insert_event_no_kwargs_no_delegated_email( "title": "Incident Document", } ], + "conferenceData": { + "createRequest": { + "requestId": "abc-123-de4", + "conferenceSolutionKey": {"type": "hangoutsMeet"}, + } + }, }, calendarId="primary", supportsAttachments=True, + conferenceDataVersion=1, ) assert not mock_convert_string_to_camel_case.called assert mock_os_environ_get.called_once_with("SRE_BOT_EMAIL") @@ -200,10 +212,15 @@ def test_insert_event_no_kwargs_no_delegated_email( @patch("os.environ.get", return_value="test_email") @patch("integrations.google_workspace.google_calendar.execute_google_api_call") @patch("integrations.google_workspace.google_calendar.convert_string_to_camel_case") +@patch("integrations.google_workspace.google_calendar.generate_unique_id") def test_insert_event_with_kwargs( - mock_convert_string_to_camel_case, mock_execute_google_api_call, mock_os_environ_get + mock_unique_id, + mock_convert_string_to_camel_case, + mock_execute_google_api_call, + mock_os_environ_get, ): mock_execute_google_api_call.return_value = {"htmlLink": "test_link"} + mock_unique_id.return_value = "abc-123-de4" mock_convert_string_to_camel_case.side_effect = ( lambda x: x ) # just return the same value @@ -224,6 +241,12 @@ def test_insert_event_with_kwargs( "title": "Incident Document", } ], + "conferenceData": { + "createRequest": { + "requestId": "abc-123-de4", + "conferenceSolutionKey": {"type": "hangoutsMeet"}, + } + }, } result = google_calendar.insert_event( start, end, emails, title, document_id, **kwargs @@ -246,6 +269,7 @@ def test_insert_event_with_kwargs( }, calendarId="primary", supportsAttachments=True, + conferenceDataVersion=1, ) for key in kwargs: mock_convert_string_to_camel_case.assert_any_call(key) @@ -256,10 +280,15 @@ def test_insert_event_with_kwargs( @patch("os.environ.get", return_value="test_email") @patch("integrations.google_workspace.google_calendar.execute_google_api_call") @patch("integrations.google_workspace.google_calendar.convert_string_to_camel_case") +@patch("integrations.google_workspace.google_calendar.generate_unique_id") def test_insert_event_with_no_document( - mock_convert_string_to_camel_case, mock_execute_google_api_call, mock_os_environ_get + mock_unique_id, + mock_convert_string_to_camel_case, + mock_execute_google_api_call, + mock_os_environ_get, ): mock_execute_google_api_call.return_value = {"htmlLink": "test_link"} + mock_unique_id.return_value = "abc-123-de4" mock_convert_string_to_camel_case.side_effect = ( lambda x: x ) # just return the same value @@ -273,6 +302,12 @@ def test_insert_event_with_no_document( "description": "Test Description", "delegated_user_email": "test_custom_email", "time_zone": "Magic/Time_Zone", + "conferenceData": { + "createRequest": { + "requestId": "abc-123-de4", + "conferenceSolutionKey": {"type": "hangoutsMeet"}, + } + }, } result = google_calendar.insert_event( start, end, emails, title, document_id, **kwargs @@ -295,6 +330,7 @@ def test_insert_event_with_no_document( }, calendarId="primary", supportsAttachments=True, + conferenceDataVersion=1, ) for key in kwargs: mock_convert_string_to_camel_case.assert_any_call(key) @@ -302,6 +338,61 @@ def test_insert_event_with_no_document( assert not mock_os_environ_get.called +@patch("os.environ.get", return_value="test_email") +@patch("integrations.google_workspace.google_calendar.execute_google_api_call") +@patch("integrations.google_workspace.google_calendar.convert_string_to_camel_case") +@patch("integrations.google_workspace.google_calendar.generate_unique_id") +def test_insert_event_google_hangout_link_created( + mock_unique_id, + mock_convert_string_to_camel_case, + mock_execute_google_api_call, + mock_os_environ_get, +): + mock_execute_google_api_call.return_value = {"htmlLink": "test_link"} + mock_unique_id.return_value = "abc-123-de4" + start = datetime.now() + end = start + emails = ["test1@test.com", "test2@test.com"] + title = "Test Event" + document_id = "test_document_id" + result = google_calendar.insert_event(start, end, emails, title, document_id) + assert result == "test_link" + mock_execute_google_api_call.assert_called_once_with( + "calendar", + "v3", + "events", + "insert", + scopes=["https://www.googleapis.com/auth/calendar.events"], + delegated_user_email="test_email", + body={ + "start": {"dateTime": start, "timeZone": "America/New_York"}, + "end": {"dateTime": end, "timeZone": "America/New_York"}, + "attendees": [{"email": email.strip()} for email in emails], + "summary": title, + "guestsCanModify": True, + "attachments": [ + { + "fileUrl": f"https://docs.google.com/document/d/{document_id}", + "mimeType": "application/vnd.google-apps.document", + "title": "Incident Document", + } + ], + "conferenceData": { + "createRequest": { + "requestId": "abc-123-de4", + "conferenceSolutionKey": {"type": "hangoutsMeet"}, + } + }, + }, + calendarId="primary", + supportsAttachments=True, + conferenceDataVersion=1, + ) + assert mock_unique_id.called + assert mock_execute_google_api_call.contains("conferenceData") + assert mock_execute_google_api_call.contains(mock_unique_id.return_value) + + @patch("integrations.google_workspace.google_service.handle_google_api_errors") @patch("os.environ.get", return_value="test_email") @patch("integrations.google_workspace.google_calendar.execute_google_api_call") diff --git a/app/tests/integrations/utils/test_api.py b/app/tests/integrations/utils/test_api.py index b4a21a8f..2d39b3a8 100644 --- a/app/tests/integrations/utils/test_api.py +++ b/app/tests/integrations/utils/test_api.py @@ -1,4 +1,5 @@ import pytest +import string from integrations.utils.api import ( convert_string_to_camel_case, convert_dict_to_camel_case, @@ -6,6 +7,7 @@ convert_string_to_pascal_case, convert_dict_to_pascale_case, convert_kwargs_to_pascal_case, + generate_unique_id, ) @@ -211,3 +213,39 @@ def test_convert_kwargs_to_pascal_case_with_non_dict_non_list_kwargs(): def test_convert_kwargs_to_pascal_case_with_nested_list(): kwargs = [{"key": ["value1", "value2"]}] assert convert_kwargs_to_pascal_case(kwargs) == [{"Key": ["value1", "value2"]}] + + +def test_unique_id_format(): + """Test that the unique ID is in the correct format.""" + unique_id = generate_unique_id() + assert isinstance(unique_id, str), "Unique ID should be a string" + parts = unique_id.split("-") + assert len(parts) == 3, "Unique ID should have three parts separated by hyphens" + assert all( + len(part) == 3 for part in parts + ), "Each segment should be exactly 3 characters long" + + +def test_unique_id_characters(): + """Test that the unique ID contains only uppercase letters and digits.""" + unique_id = generate_unique_id() + allowed_chars = set(string.ascii_lowercase + string.digits) + assert all( + char in allowed_chars for char in unique_id.replace("-", "") + ), "All characters should be alphanumeric" + + +def test_unique_id_uniqueness(): + """Test that multiple IDs are unique from each other.""" + ids = {generate_unique_id() for _ in range(100)} + assert len(ids) == 100, "All generated IDs should be unique" + + +# Additional test to ensure no illegal characters or formats appear +def test_no_illegal_characters(): + """Ensure no lowecase or special characters are in the ID""" + unique_id = generate_unique_id() + illegal_chars = set(string.ascii_uppercase + string.punctuation) + assert not any( + char in illegal_chars for char in unique_id.replace("-", "") + ), "ID should not have uppercase or special characters"