diff --git a/app/integrations/google_workspace/google_calendar.py b/app/integrations/google_workspace/google_calendar.py index ce8232a4..44cbce1f 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,21 @@ 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 +101,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..4401769b 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,72 @@ 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, 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("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, **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,6 +282,7 @@ 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) @@ -237,7 +306,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..5a4fba01 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,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"} + { + "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 +865,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 +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"} + { + "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 +928,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 +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"} + { + "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 +992,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 +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"} + { + "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 +1047,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 +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"}) + 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 +1084,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 +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"}) + 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": {