From d8a02d5ac2e695f0bc52fc2af15b26ed371d19ae Mon Sep 17 00:00:00 2001 From: Sylvia McLaughlin <85905333+sylviamclaughlin@users.noreply.github.com> Date: Thu, 25 Apr 2024 23:44:11 +0000 Subject: [PATCH 1/2] Adding functionality to attach the incident document to the meeting invite --- .../google_workspace/google_calendar.py | 16 ++++- app/modules/incident/incident_helper.py | 24 ++++++- app/modules/incident/schedule_retro.py | 4 ++ .../google_workspace/test_google_calendar.py | 64 +++++++++++++++++- .../modules/incident/test_incident_helper.py | 66 +++++++++++++++++-- .../modules/incident/test_schedule_retro.py | 2 + 6 files changed, 165 insertions(+), 11 deletions(-) diff --git a/app/integrations/google_workspace/google_calendar.py b/app/integrations/google_workspace/google_calendar.py index ce8232a4..70c635f9 100644 --- a/app/integrations/google_workspace/google_calendar.py +++ b/app/integrations/google_workspace/google_calendar.py @@ -48,7 +48,7 @@ def get_freebusy(time_min, time_max, items, **kwargs): @handle_google_api_errors -def insert_event(start, end, emails, title, **kwargs): +def insert_event(start, end, emails, title, incident_document, **kwargs): """Creates a new event in the specified calendars. Args: @@ -71,6 +71,19 @@ def insert_event(start, end, emails, title, **kwargs): "summary": title, "guestsCanModify": True, } + if incident_document: + body["attachments"] = [ + { + "fileUrl": f"https://docs.google.com/document/d/{incident_document}", + "mimeType": "application/vnd.google-apps.document", + "title": "Incident Document", + } + ] + else: + # Optionally handle the case where 'incident_document' is None or empty + # For example, remove 'attachments' from 'body' if it shouldn't exist without a valid document + body.pop("attachments", None) # This removes 'attachments' if it exists, does nothing if it doesn't + body.update({convert_string_to_camel_case(k): v for k, v in kwargs.items()}) if "delegated_user_email" in kwargs and kwargs["delegated_user_email"] is not None: delegated_user_email = kwargs["delegated_user_email"] @@ -86,6 +99,7 @@ def insert_event(start, end, emails, title, **kwargs): delegated_user_email=delegated_user_email, body=body, calendarId="primary", + supportsAttachments=True, ) return result.get("htmlLink") diff --git a/app/modules/incident/incident_helper.py b/app/modules/incident/incident_helper.py index b427b649..a53a6eb8 100644 --- a/app/modules/incident/incident_helper.py +++ b/app/modules/incident/incident_helper.py @@ -478,8 +478,30 @@ def schedule_incident_retro(client, body, ack): if "bot_id" not in response: user_emails.append(response["email"]) + # get the incident document + # get and update the incident document + document_id = "" + response = client.bookmarks_list(channel_id=channel_id) + if response["ok"]: + for item in range(len(response["bookmarks"])): + if response["bookmarks"][item]["title"] == "Incident report": + document_id = google_docs.extract_google_doc_id( + response["bookmarks"][item]["link"] + ) + else: + logging.warning( + "No bookmark link for the incident document found for channel %s", + channel_name, + ) + # convert the data to string so that we can send it as private metadata - data_to_send = json.dumps({"emails": user_emails, "topic": channel_topic}) + data_to_send = json.dumps( + { + "emails": user_emails, + "topic": channel_topic, + "incident_document": document_id, + } + ) blocks = { "type": "modal", diff --git a/app/modules/incident/schedule_retro.py b/app/modules/incident/schedule_retro.py index b068f3f7..9101fd97 100644 --- a/app/modules/incident/schedule_retro.py +++ b/app/modules/incident/schedule_retro.py @@ -26,6 +26,9 @@ def schedule_event(event_details, days): email = email.strip() items.append({"id": email}) + # get the incident document link + incident_document = json.loads(event_details).get("incident_document") + # Execute the query to find all the busy times for all the participants freebusy_result = get_freebusy(time_min, time_max, items) @@ -62,6 +65,7 @@ def schedule_event(event_details, days): first_available_end.isoformat(), emails, "Retro " + incident_name, + incident_document, **event_config, ) diff --git a/app/tests/integrations/google_workspace/test_google_calendar.py b/app/tests/integrations/google_workspace/test_google_calendar.py index 23168897..1b74defb 100644 --- a/app/tests/integrations/google_workspace/test_google_calendar.py +++ b/app/tests/integrations/google_workspace/test_google_calendar.py @@ -154,7 +154,8 @@ def test_insert_event_no_kwargs_no_delegated_email( end = start emails = ["test1@test.com", "test2@test.com"] title = "Test Event" - result = google_calendar.insert_event(start, end, emails, title) + 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", @@ -169,8 +170,16 @@ def test_insert_event_no_kwargs_no_delegated_email( "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", + } + ], }, calendarId="primary", + supportsAttachments=True ) assert not mock_convert_string_to_camel_case.called assert mock_os_environ_get.called_once_with("SRE_BOT_EMAIL") @@ -190,13 +199,15 @@ def test_insert_event_with_kwargs( end = start emails = ["test1@test.com", "test2@test.com"] title = "Test Event" + document_id = "test_document_id" kwargs = { "location": "Test Location", "description": "Test Description", "delegated_user_email": "test_custom_email", "time_zone": "Magic/Time_Zone", + "attachments": [{"fileUrl": "https://docs.google.com/document/d/test_document_id", "mimeType": "application/vnd.google-apps.document", "title": "Incident Document"}] } - result = google_calendar.insert_event(start, end, emails, title, **kwargs) + result = google_calendar.insert_event(start, end, emails, title, document_id, **kwargs) assert result == "test_link" mock_execute_google_api_call.assert_called_once_with( "calendar", @@ -214,12 +225,58 @@ def test_insert_event_with_kwargs( **kwargs, }, calendarId="primary", + supportsAttachments=True ) for key in kwargs: mock_convert_string_to_camel_case.assert_any_call(key) 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") +def test_insert_event_with_no_document( + 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_convert_string_to_camel_case.side_effect = ( + lambda x: x + ) # just return the same value + start = datetime.now() + end = start + emails = ["test1@test.com", "test2@test.com"] + title = "Test Event" + document_id = "" + kwargs = { + "location": "Test Location", + "description": "Test Description", + "delegated_user_email": "test_custom_email", + "time_zone": "Magic/Time_Zone", + } + result = google_calendar.insert_event(start, end, emails, title, document_id, **kwargs) + 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_custom_email", + body={ + "start": {"dateTime": start, "timeZone": "Magic/Time_Zone"}, + "end": {"dateTime": end, "timeZone": "Magic/Time_Zone"}, + "attendees": [{"email": email.strip()} for email in emails], + "summary": title, + "guestsCanModify": True, + **kwargs, + }, + calendarId="primary", + supportsAttachments=True + ) + for key in kwargs: + mock_convert_string_to_camel_case.assert_any_call(key) + + assert not mock_os_environ_get.called @patch("integrations.google_workspace.google_service.handle_google_api_errors") @patch("os.environ.get", return_value="test_email") @@ -237,7 +294,8 @@ def test_insert_event_api_call_error( end = start emails = ["test1@test.com", "test2@test.com"] title = "Test Event" - google_calendar.insert_event(start, end, emails, title) + document_id = "test_document_id" + google_calendar.insert_event(start, end, emails, title, document_id) assert ( "An unexpected error occurred in function 'insert_event': API call error" in caplog.text diff --git a/app/tests/modules/incident/test_incident_helper.py b/app/tests/modules/incident/test_incident_helper.py index 1a56de5e..6366d7ef 100644 --- a/app/tests/modules/incident/test_incident_helper.py +++ b/app/tests/modules/incident/test_incident_helper.py @@ -801,6 +801,15 @@ def test_schedule_incident_retro_successful_no_bots(): {"user": {"profile": {"email": "user1@example.com"}}}, {"user": {"profile": {"email": "user2@example.com"}}}, ] + mock_client.bookmarks_list.return_value = { + "ok": True, + "bookmarks": [ + { + "title": "Incident report", + "link": "https://docs.google.com/document/d/dummy_document_id/edit", + } + ], + } body = { "channel_id": "C1234567890", @@ -828,7 +837,7 @@ def test_schedule_incident_retro_successful_no_bots(): # Verify the modal payload contains the correct data expected_data = json.dumps( - {"emails": ["user1@example.com", "user2@example.com"], "topic": "Retro Topic"} + {"emails": ["user1@example.com", "user2@example.com"], "topic": "Retro Topic", "incident_document": "dummy_document_id"} ) assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data @@ -852,6 +861,15 @@ def test_schedule_incident_retro_successful_bots(): "user": {"profile": {"email": "user3@example.com", "bot_id": "B12345"}} }, # This simulates a bot user ] + mock_client.bookmarks_list.return_value = { + "ok": True, + "bookmarks": [ + { + "title": "Incident report", + "link": "https://docs.google.com/document/d/dummy_document_id/edit", + } + ], + } body = { "channel_id": "C1234567890", @@ -879,7 +897,7 @@ def test_schedule_incident_retro_successful_bots(): # Verify the modal payload contains the correct data expected_data = json.dumps( - {"emails": ["user1@example.com", "user2@example.com"], "topic": "Retro Topic"} + {"emails": ["user1@example.com", "user2@example.com"], "topic": "Retro Topic", "incident_document": "dummy_document_id"} ) assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data @@ -902,6 +920,15 @@ def test_schedule_incident_retro_successful_security_group(): "user": {"profile": {"email": "user3@example.com", "bot_id": "B12345"}} }, # This simulates a bot user ] + mock_client.bookmarks_list.return_value = { + "ok": True, + "bookmarks": [ + { + "title": "Incident report", + "link": "https://docs.google.com/document/d/dummy_document_id/edit", + } + ], + } body = { "channel_id": "C1234567890", @@ -929,7 +956,7 @@ def test_schedule_incident_retro_successful_security_group(): # Verify the modal payload contains the correct data expected_data = json.dumps( - {"emails": ["user2@example.com"], "topic": "Retro Topic"} + {"emails": ["user2@example.com"], "topic": "Retro Topic", "incident_document": "dummy_document_id"} ) assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data @@ -953,6 +980,15 @@ def test_schedule_incident_retro_successful_no_security_group(): "user": {"profile": {"email": "user3@example.com", "bot_id": "B12345"}} }, # This simulates a bot user ] + mock_client.bookmarks_list.return_value = { + "ok": True, + "bookmarks": [ + { + "title": "Incident report", + "link": "https://docs.google.com/document/d/dummy_document_id/edit", + } + ], + } body = { "channel_id": "C1234567890", @@ -980,7 +1016,7 @@ def test_schedule_incident_retro_successful_no_security_group(): # Verify the modal payload contains the correct data expected_data = json.dumps( - {"emails": ["user1@example.com", "user2@example.com"], "topic": "Retro Topic"} + {"emails": ["user1@example.com", "user2@example.com"], "topic": "Retro Topic", "incident_document": "dummy_document_id"} ) assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data @@ -995,6 +1031,15 @@ def test_schedule_incident_retro_with_no_users(): "channel": {"topic": {"value": "Retro Topic"}} } mock_client.users_info.side_effect = [] + mock_client.bookmarks_list.return_value = { + "ok": True, + "bookmarks": [ + { + "title": "Incident report", + "link": "https://docs.google.com/document/d/dummy_document_id/edit", + } + ], + } # Adjust the mock to simulate no users in the channel mock_client.conversations_members.return_value = {"members": []} @@ -1009,7 +1054,7 @@ def test_schedule_incident_retro_with_no_users(): incident_helper.schedule_incident_retro(mock_client, body, mock_ack) # construct the expected data object - expected_data = json.dumps({"emails": [], "topic": "Retro Topic"}) + expected_data = json.dumps({"emails": [], "topic": "Retro Topic", "incident_document": "dummy_document_id"}) # Assertions to validate behavior when no users are present in the channel assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data @@ -1021,6 +1066,15 @@ def test_schedule_incident_retro_with_no_topic(): mock_ack = MagicMock() mock_client.usergroups_users_list.return_value = {"users": ["U444444"]} mock_client.conversations_info.return_value = {"channel": {"topic": {"value": ""}}} + mock_client.bookmarks_list.return_value = { + "ok": True, + "bookmarks": [ + { + "title": "Incident report", + "link": "https://docs.google.com/document/d/dummy_document_id/edit", + } + ], + } mock_client.users_info.side_effect = [] # Adjust the mock to simulate no users in the channel @@ -1036,7 +1090,7 @@ def test_schedule_incident_retro_with_no_topic(): incident_helper.schedule_incident_retro(mock_client, body, mock_ack) # construct the expected data object and set the topic to a default one - expected_data = json.dumps({"emails": [], "topic": "Incident Retro"}) + expected_data = json.dumps({"emails": [], "topic": "Incident Retro", "incident_document": "dummy_document_id"}) # Assertions to validate behavior when no users are present in the channel assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data diff --git a/app/tests/modules/incident/test_schedule_retro.py b/app/tests/modules/incident/test_schedule_retro.py index f1a7197c..f0de7e0e 100644 --- a/app/tests/modules/incident/test_schedule_retro.py +++ b/app/tests/modules/incident/test_schedule_retro.py @@ -68,6 +68,7 @@ def test_schedule_event_successful( event_details_dict = json.loads(event_details) emails = event_details_dict["emails"] topic = event_details_dict["topic"] + document_id = event_details_dict.get("incident_document") # Call the function under test event_link = schedule_retro.schedule_event(event_details, mock_days) @@ -82,6 +83,7 @@ def test_schedule_event_successful( find_first_available_slot_mock.return_value[1].isoformat(), emails, "Retro " + topic, + document_id, description="This is a retro meeting to discuss incident: " + topic, conferenceData={ "createRequest": { From be0f1c4ac1f9da7b9017972c7753c824d3e3e16a Mon Sep 17 00:00:00 2001 From: Sylvia McLaughlin <85905333+sylviamclaughlin@users.noreply.github.com> Date: Thu, 25 Apr 2024 23:47:12 +0000 Subject: [PATCH 2/2] Formatting --- .../google_workspace/google_calendar.py | 6 ++-- .../google_workspace/test_google_calendar.py | 36 ++++++++++++------- .../modules/incident/test_incident_helper.py | 36 +++++++++++++++---- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/app/integrations/google_workspace/google_calendar.py b/app/integrations/google_workspace/google_calendar.py index 70c635f9..44cbce1f 100644 --- a/app/integrations/google_workspace/google_calendar.py +++ b/app/integrations/google_workspace/google_calendar.py @@ -82,8 +82,10 @@ def insert_event(start, end, emails, title, incident_document, **kwargs): else: # Optionally handle the case where 'incident_document' is None or empty # For example, remove 'attachments' from 'body' if it shouldn't exist without a valid document - body.pop("attachments", None) # This removes 'attachments' if it exists, does nothing if it doesn't - + body.pop( + "attachments", None + ) # This removes 'attachments' if it exists, does nothing if it doesn't + body.update({convert_string_to_camel_case(k): v for k, v in kwargs.items()}) if "delegated_user_email" in kwargs and kwargs["delegated_user_email"] is not None: delegated_user_email = kwargs["delegated_user_email"] diff --git a/app/tests/integrations/google_workspace/test_google_calendar.py b/app/tests/integrations/google_workspace/test_google_calendar.py index 1b74defb..4401769b 100644 --- a/app/tests/integrations/google_workspace/test_google_calendar.py +++ b/app/tests/integrations/google_workspace/test_google_calendar.py @@ -171,15 +171,15 @@ def test_insert_event_no_kwargs_no_delegated_email( "summary": title, "guestsCanModify": True, "attachments": [ - { - "fileUrl": f"https://docs.google.com/document/d/{document_id}", - "mimeType": "application/vnd.google-apps.document", - "title": "Incident Document", - } + { + "fileUrl": f"https://docs.google.com/document/d/{document_id}", + "mimeType": "application/vnd.google-apps.document", + "title": "Incident Document", + } ], }, calendarId="primary", - supportsAttachments=True + supportsAttachments=True, ) assert not mock_convert_string_to_camel_case.called assert mock_os_environ_get.called_once_with("SRE_BOT_EMAIL") @@ -205,9 +205,17 @@ def test_insert_event_with_kwargs( "description": "Test Description", "delegated_user_email": "test_custom_email", "time_zone": "Magic/Time_Zone", - "attachments": [{"fileUrl": "https://docs.google.com/document/d/test_document_id", "mimeType": "application/vnd.google-apps.document", "title": "Incident Document"}] + "attachments": [ + { + "fileUrl": "https://docs.google.com/document/d/test_document_id", + "mimeType": "application/vnd.google-apps.document", + "title": "Incident Document", + } + ], } - result = google_calendar.insert_event(start, end, emails, title, document_id, **kwargs) + result = google_calendar.insert_event( + start, end, emails, title, document_id, **kwargs + ) assert result == "test_link" mock_execute_google_api_call.assert_called_once_with( "calendar", @@ -225,13 +233,14 @@ def test_insert_event_with_kwargs( **kwargs, }, calendarId="primary", - supportsAttachments=True + supportsAttachments=True, ) for key in kwargs: mock_convert_string_to_camel_case.assert_any_call(key) 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") @@ -246,14 +255,16 @@ def test_insert_event_with_no_document( end = start emails = ["test1@test.com", "test2@test.com"] title = "Test Event" - document_id = "" + document_id = "" kwargs = { "location": "Test Location", "description": "Test Description", "delegated_user_email": "test_custom_email", "time_zone": "Magic/Time_Zone", } - result = google_calendar.insert_event(start, end, emails, title, document_id, **kwargs) + result = google_calendar.insert_event( + start, end, emails, title, document_id, **kwargs + ) assert result == "test_link" mock_execute_google_api_call.assert_called_once_with( "calendar", @@ -271,13 +282,14 @@ def test_insert_event_with_no_document( **kwargs, }, calendarId="primary", - supportsAttachments=True + supportsAttachments=True, ) for key in kwargs: mock_convert_string_to_camel_case.assert_any_call(key) assert not mock_os_environ_get.called + @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/modules/incident/test_incident_helper.py b/app/tests/modules/incident/test_incident_helper.py index 6366d7ef..5a4fba01 100644 --- a/app/tests/modules/incident/test_incident_helper.py +++ b/app/tests/modules/incident/test_incident_helper.py @@ -837,7 +837,11 @@ def test_schedule_incident_retro_successful_no_bots(): # Verify the modal payload contains the correct data expected_data = json.dumps( - {"emails": ["user1@example.com", "user2@example.com"], "topic": "Retro Topic", "incident_document": "dummy_document_id"} + { + "emails": ["user1@example.com", "user2@example.com"], + "topic": "Retro Topic", + "incident_document": "dummy_document_id", + } ) assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data @@ -897,7 +901,11 @@ def test_schedule_incident_retro_successful_bots(): # Verify the modal payload contains the correct data expected_data = json.dumps( - {"emails": ["user1@example.com", "user2@example.com"], "topic": "Retro Topic", "incident_document": "dummy_document_id"} + { + "emails": ["user1@example.com", "user2@example.com"], + "topic": "Retro Topic", + "incident_document": "dummy_document_id", + } ) assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data @@ -956,7 +964,11 @@ def test_schedule_incident_retro_successful_security_group(): # Verify the modal payload contains the correct data expected_data = json.dumps( - {"emails": ["user2@example.com"], "topic": "Retro Topic", "incident_document": "dummy_document_id"} + { + "emails": ["user2@example.com"], + "topic": "Retro Topic", + "incident_document": "dummy_document_id", + } ) assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data @@ -1016,7 +1028,11 @@ def test_schedule_incident_retro_successful_no_security_group(): # Verify the modal payload contains the correct data expected_data = json.dumps( - {"emails": ["user1@example.com", "user2@example.com"], "topic": "Retro Topic", "incident_document": "dummy_document_id"} + { + "emails": ["user1@example.com", "user2@example.com"], + "topic": "Retro Topic", + "incident_document": "dummy_document_id", + } ) assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data @@ -1054,7 +1070,9 @@ def test_schedule_incident_retro_with_no_users(): incident_helper.schedule_incident_retro(mock_client, body, mock_ack) # construct the expected data object - expected_data = json.dumps({"emails": [], "topic": "Retro Topic", "incident_document": "dummy_document_id"}) + expected_data = json.dumps( + {"emails": [], "topic": "Retro Topic", "incident_document": "dummy_document_id"} + ) # Assertions to validate behavior when no users are present in the channel assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data @@ -1090,7 +1108,13 @@ def test_schedule_incident_retro_with_no_topic(): incident_helper.schedule_incident_retro(mock_client, body, mock_ack) # construct the expected data object and set the topic to a default one - expected_data = json.dumps({"emails": [], "topic": "Incident Retro", "incident_document": "dummy_document_id"}) + expected_data = json.dumps( + { + "emails": [], + "topic": "Incident Retro", + "incident_document": "dummy_document_id", + } + ) # Assertions to validate behavior when no users are present in the channel assert ( mock_client.views_open.call_args[1]["view"]["private_metadata"] == expected_data