import base64 import json import os import uuid from unittest.mock import MagicMock import EWSApiModule import exchangelib import pytest from EWSApiModule import ( EWSClient, GetSearchableMailboxes, create_folder, delete_attachments_for_message, delete_items, filter_dict_null, get_build_from_context, get_config_args_from_context, get_entry_for_object, get_expanded_group, get_folder, get_on_prem_build, get_out_of_office_state, get_searchable_mailboxes, handle_html, mark_item_as_read, move_item, move_item_between_mailboxes, recover_soft_delete_item, switch_hr_headers, ) from exchangelib import ( BASIC, DELEGATE, OAUTH2, Configuration, Credentials, EWSDateTime, EWSTimeZone, FileAttachment, Folder, Message, ) from exchangelib.attachments import AttachmentId from exchangelib.protocol import Protocol from exchangelib.settings import OofSettings from exchangelib.util import TNS from MicrosoftApiModule import AzureCloud, AzureCloudEndpoints """ Constants """ CLIENT_ID = "test_client_id" CLIENT_SECRET = "test_client_secret" ACCESS_TYPE = DELEGATE DEFAULT_TARGET_MAILBOX = "test@default_target_mailbox.com" EWS_SERVER = "http://test_ews_server.com" MAX_FETCH = 10 FOLDER = "test_folder" REQUEST_TIMEOUT = "120" VERSION_STR = "2013" BUILD = exchangelib.version.EXCHANGE_2013 VERSION = exchangelib.Version(BUILD) AUTH_TYPE = BASIC MSG_ID = "message_1" MSG_NO_SUBJECT = "message_2" DICSOVERY_EWS_SERVER = "https://auto-discovered-server.com" DISCOVERY_SERVER_BUILD = exchangelib.version.EXCHANGE_2016 DISCOVERY_VERSION = exchangelib.Version(DISCOVERY_SERVER_BUILD) """ Utilities """ def util_load_json(path): with open(path, encoding="utf-8") as f: return json.loads(f.read()) def assert_configs_equal(config1: Configuration, config2: Configuration): assert config1.credentials == config2.credentials assert config1.auth_type == config2.auth_type assert config1.version == config2.version assert config1.service_endpoint == config2.service_endpoint assert config1.server == config2.server @pytest.fixture() def client(): return EWSClient( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_type=ACCESS_TYPE, default_target_mailbox=DEFAULT_TARGET_MAILBOX, ews_server=EWS_SERVER, max_fetch=MAX_FETCH, auth_type=AUTH_TYPE, version=VERSION_STR, ) class MockAccount: class MockRights: def __init__(self, *args, **kwargs): self.read = True def __init__( self, primary_smtp_address, access_type, autodiscover, credentials=None, config=None, default_timezone=None, *args, **kwargs, ): self.primary_smtp_address = primary_smtp_address self.access_type = access_type self.autodiscover = autodiscover self.credentials = credentials self.config = config self.default_timezone = default_timezone if autodiscover: if not credentials: raise ValueError("Credentials must be provided for autodiscovery") config = Configuration( service_endpoint=DICSOVERY_EWS_SERVER, credentials=credentials, auth_type=AUTH_TYPE, version=DISCOVERY_VERSION, ) elif not config: raise ValueError("Autodiscovery is false and no config was provided") self.version = config.version self.protocol = Protocol(config=config) self.root = MagicMock() self.root.tois = MagicMock() def mock_floordiv(name): return self.root.tois self.root.tois.__floordiv__.side_effect = mock_floordiv self.root.effective_rights = MagicMock() self.root.effective_rights.read = True self.inbox = MagicMock() self.drafts = MagicMock() self.drafts.messages = { MSG_ID: Message(account=MagicMock(spec=exchangelib.Account), id=MSG_ID, subject="Test subject", body="Test body"), MSG_NO_SUBJECT: Message(account=MagicMock(spec=exchangelib.Account), id=MSG_ID, subject=None, body=None), } self.inbox.get = MagicMock(side_effect=lambda id: self.drafts.messages.get(id)) self.oof_settings = MagicMock( spec=OofSettings, state="Disabled", external_audience="All", start=EWSDateTime(2025, 2, 4, 8, 0, tzinfo=EWSTimeZone(key="UTC")), end=EWSDateTime(2025, 2, 5, 8, 0, tzinfo=EWSTimeZone(key="UTC")), internal_reply="reply_internal", external_reply="reply_external", ) self.recoverable_items_deletions = MagicMock() self.mock_deleted_messages = [ MagicMock(spec=Message, subject="Test Subject 1", id="id1", message_id="message1"), MagicMock(spec=Message, subject="Test Subject 2", id="id2", message_id="message2"), MagicMock(spec=Message, subject="Test Subject 3", id="id3", message_id="message3"), ] def mock_filter(message_id__in): output = MagicMock() output.all = lambda: [msg for msg in self.mock_deleted_messages if msg.message_id in message_id__in] return output self.recoverable_items_deletions.filter = mock_filter self.save_instance() def save_instance(self): pass def export(self, items): pass def upload(self, items): pass def bulk_delete(self, items): pass @pytest.fixture() def mock_account(mocker): mockAccount = mocker.MagicMock(wraps=MockAccount, instances=[]) mocker.patch("EWSApiModule.Account", mockAccount) mocker.patch.object(MockAccount, "save_instance", side_effect=lambda self: mockAccount.instances.append(self), autospec=True) return mockAccount """ Tests """ def test_client_configure_oauth(mocker): """ Given: - EWSClient configured with OAuth When: - Client is initialized Then: - The Credentials and Configuration objects are created correctly """ ACCESS_TOKEN = "test_access_token" class MockMSClient: def __init__(self, *args, **kwargs): pass def get_access_token(self): return ACCESS_TOKEN mocker.patch("EWSApiModule.MicrosoftClient", MockMSClient) azure_cloud = AzureCloud( origin="test_origin", name="test_name", abbreviation="test_abrv", endpoints=AzureCloudEndpoints(active_directory="", exchange_online="https://outlook.office365.com"), ) client = EWSClient( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_type=ACCESS_TYPE, default_target_mailbox=DEFAULT_TARGET_MAILBOX, ews_server=EWS_SERVER, max_fetch=MAX_FETCH, auth_type=OAUTH2, azure_cloud=azure_cloud, version="O365", ) credentials = client.credentials assert isinstance(credentials, exchangelib.OAuth2AuthorizationCodeCredentials) assert credentials.client_id == CLIENT_ID assert credentials.client_secret == CLIENT_SECRET assert credentials.access_token["access_token"] == ACCESS_TOKEN config = client.config assert config assert config.credentials == credentials assert config.auth_type == OAUTH2 assert config.version == exchangelib.Version(exchangelib.version.EXCHANGE_O365) assert config.service_endpoint == "https://outlook.office365.com/EWS/Exchange.asmx" def test_client_configure_onprem(mocker, client): """ Given: - EWSClient configured with auth_type other than OAUTH2 - EWS server is specified When: - Client is initialized Then: - The Credentials and Configuration objects are created correctly """ mocked_account = mocker.patch("EWSApiModule.Account") assert not client.auto_discover mocked_account.assert_not_called() assert client.server_build == exchangelib.version.EXCHANGE_2013 credentials = client.credentials assert isinstance(credentials, exchangelib.Credentials) assert credentials.username == CLIENT_ID assert credentials.password == CLIENT_SECRET config = client.config assert config assert config.credentials == credentials assert config.auth_type == AUTH_TYPE assert config.version == VERSION assert config.service_endpoint == EWS_SERVER def test_client_configure_onprem_autodiscover(mock_account): """ Given: - EWSClient configured with any auth_type other than OAUTH2 - EWS server is not specified - No previous clients were configured When: - Client is initialized Then: - The exchangelib auto-discover mechanism is used to configure the client - The Credentials, server and build are set correctly based on the auto-discovered configuration - Results are cached - subsequent client initializations should not trigger auto-discovery """ # First client, should trigger autodiscover client = EWSClient( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_type=ACCESS_TYPE, default_target_mailbox=DEFAULT_TARGET_MAILBOX, max_fetch=MAX_FETCH, ) assert client.auto_discover assert client.server_build == DISCOVERY_SERVER_BUILD assert client.ews_server == DICSOVERY_EWS_SERVER credentials = client.credentials assert isinstance(credentials, exchangelib.Credentials) assert credentials.username == CLIENT_ID assert credentials.password == CLIENT_SECRET mock_account.assert_called_once_with( primary_smtp_address=DEFAULT_TARGET_MAILBOX, autodiscover=True, access_type=ACCESS_TYPE, credentials=credentials, ) # Subsequent client, should not trigger autodiscover (No additional calls to MockAccount) client = EWSClient( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_type=ACCESS_TYPE, default_target_mailbox=DEFAULT_TARGET_MAILBOX, max_fetch=MAX_FETCH, ) assert client.auto_discover assert client.server_build == DISCOVERY_SERVER_BUILD assert client.ews_server == DICSOVERY_EWS_SERVER credentials = client.credentials assert isinstance(credentials, exchangelib.Credentials) assert credentials.username == CLIENT_ID assert credentials.password == CLIENT_SECRET mock_account.assert_called_once() def test_client_get_protocol(client): """ Given: - EWSClient is configured with EWS server When: - client.get_protocol is called Then: - The Protocol object is returned correctly """ expected_protocol = Protocol( config=Configuration( service_endpoint=EWS_SERVER, credentials=Credentials(username=CLIENT_ID, password=CLIENT_SECRET), auth_type=AUTH_TYPE, version=VERSION, ) ) assert client.get_protocol() == expected_protocol def test_client_get_protocol_autodiscover(mock_account): """ Given: - EWSClient is configured without EWS server When: - client.get_protocol is called Then: - The Protocol object is returned correctly based on the auto-discovered configuration """ expected_protocol = Protocol( config=Configuration( service_endpoint=DICSOVERY_EWS_SERVER, credentials=Credentials(username=CLIENT_ID, password=CLIENT_SECRET), auth_type=AUTH_TYPE, version=DISCOVERY_VERSION, ) ) client = EWSClient( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_type=ACCESS_TYPE, default_target_mailbox=DEFAULT_TARGET_MAILBOX, max_fetch=MAX_FETCH, auth_type=AUTH_TYPE, ) assert client.get_protocol() == expected_protocol @pytest.mark.parametrize("target_mailbox", [None, "test_target_mailbox"]) def test_client_get_account(client, mock_account, target_mailbox): """ Given: - EWSClient is configured with EWS server When: - client.get_account() is called Then: - The Account object is returned correctly """ time_zone = "test_tz" account = client.get_account(target_mailbox=target_mailbox, time_zone=time_zone) assert isinstance(account, MockAccount) expected_smtp_address = target_mailbox if target_mailbox else DEFAULT_TARGET_MAILBOX expected_config = Configuration( service_endpoint=EWS_SERVER, credentials=Credentials(username=CLIENT_ID, password=CLIENT_SECRET), auth_type=AUTH_TYPE, version=VERSION, ) assert account.primary_smtp_address == expected_smtp_address assert account.config assert_configs_equal(expected_config, account.config) assert account.access_type == ACCESS_TYPE assert account.default_timezone == time_zone assert not account.autodiscover def test_client_get_account_autodiscover(mock_account): """ Given: - EWSClient is configured without EWS server When: - client.get_account() is called Then: - The Account object is returned correctly based on the auto-discovered configuration """ time_zone = "test_tz" client = EWSClient( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_type=ACCESS_TYPE, default_target_mailbox=DEFAULT_TARGET_MAILBOX, max_fetch=MAX_FETCH, auth_type=AUTH_TYPE, ) account = client.get_account(time_zone=time_zone) assert isinstance(account, MockAccount) expected_smtp_address = DEFAULT_TARGET_MAILBOX expected_config = Configuration( service_endpoint=DICSOVERY_EWS_SERVER, credentials=Credentials(username=CLIENT_ID, password=CLIENT_SECRET), auth_type=AUTH_TYPE, version=DISCOVERY_VERSION, ) assert account.primary_smtp_address == expected_smtp_address assert account.config assert_configs_equal(expected_config, account.config) assert account.access_type == ACCESS_TYPE assert account.default_timezone == time_zone assert not account.autodiscover def test_client_get_items_from_mailbox(mocker, client): """ Given: - Configured EWSClient When: - client.get_items_from_mailbox is called Then: - Mailbox items are returned as expected """ mock_items = { "item_id_1": "item_1", "item_id_2": "item_2", "item_id_3": "item_3", } def mock_account_fetch(self, ids: list[exchangelib.items.Item]): mocked_items = [] for item in ids: id = item.id if id not in mock_items: raise ValueError(f"Item with ID {id} not found in mock items") mocked_item = mocker.MagicMock() mocked_item.id = id mocked_item.value = mock_items.get(id) mocked_items.append(mocked_item) return mocked_items mocker.patch.object(EWSApiModule.Account, "fetch", mock_account_fetch) items = client.get_items_from_mailbox(client.get_account(), list(mock_items.keys())) for item in items: assert item.id in mock_items assert item.value == mock_items[item.id] def test_client_get_item_from_mailbox(mocker, client): """ Given: - Configured EWSClient When: - client.get_item_from_mailbox is called Then: - The item is returned as expected """ mock_items = { "item_id_1": "item_1", "item_id_2": "item_2", "item_id_3": "item_3", } def mock_account_fetch(self, ids: list[exchangelib.items.Item]): mocked_items = [] for item in ids: id = item.id if id not in mock_items: raise ValueError(f"Item with ID {id} not found in mock items") mocked_item = mocker.MagicMock() mocked_item.id = id mocked_item.value = mock_items.get(id) mocked_items.append(mocked_item) return mocked_items mocker.patch.object(EWSApiModule.Account, "fetch", mock_account_fetch) item = client.get_item_from_mailbox(client.get_account(), list(mock_items.keys())[0]) assert item.id assert item.id == list(mock_items.keys())[0] assert item.value == mock_items[item.id] def test_client_get_attachments_for_item(mocker, client): """ Given: - Configured EWSClient When: - client.get_attachments_for_item is called Then: - The attachments for the item are returned as expected """ item_id = "item_id_1" attach_ids = ["attach_id_1", "attach_id_2", "attach_id_3"] mock_item = mocker.MagicMock() mock_item.id = item_id mock_item.attachments = [mocker.MagicMock(attachment_id=mocker.MagicMock(id=id)) for id in attach_ids] mocker.patch.object(EWSClient, "get_item_from_mailbox", return_value=mock_item) expected_attach_ids = attach_ids[:2] attachments = client.get_attachments_for_item(item_id, client.get_account(), expected_attach_ids) for attachment in attachments: assert attachment in mock_item.attachments assert attachment.attachment_id.id in expected_attach_ids @pytest.mark.parametrize( "folder_path, is_public, expected_is_public", [(FOLDER, True, True), (FOLDER, False, False), ("Calendar", True, False), ("Deleted Items", True, False)], ) def test_client_is_default_folder(folder_path, is_public, expected_is_public): """ Given: - Configured EWSClient When: - client.get_default_folder is called Then: - The return value indicates if the folder is a known public folder (i.e. default and public folder) """ client = EWSClient( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_type=ACCESS_TYPE, default_target_mailbox=DEFAULT_TARGET_MAILBOX, ews_server=EWS_SERVER, max_fetch=MAX_FETCH, auth_type=AUTH_TYPE, version=VERSION_STR, folder=FOLDER, is_public_folder=is_public, ) assert client.is_default_folder(folder_path) == expected_is_public @pytest.mark.parametrize("is_public", [True, False]) def test_client_is_default_folder_with_override(is_public): """ Given: - Configured EWSClient When: - client.get_default_folder is called with is_public argument Then: - The return value is overriden by the is_public argument """ client = EWSClient( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_type=ACCESS_TYPE, default_target_mailbox=DEFAULT_TARGET_MAILBOX, ews_server=EWS_SERVER, max_fetch=MAX_FETCH, auth_type=AUTH_TYPE, version=VERSION_STR, folder=FOLDER, is_public_folder=True, ) assert client.is_default_folder(FOLDER, is_public) == is_public def test_client_get_folder_by_path(mocker, mock_account): """ Given: - Configured EWSClient When: - client.get_folder_by_path is called Then: - The folder at the specified path is returned """ path = "Inbox/Subfolder/Test" client = EWSClient( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_type=ACCESS_TYPE, default_target_mailbox=DEFAULT_TARGET_MAILBOX, ews_server=EWS_SERVER, max_fetch=MAX_FETCH, auth_type=AUTH_TYPE, version=VERSION_STR, folder=FOLDER, is_public_folder=True, ) account = client.get_account() client.get_folder_by_path(path, account) expected_calls = [mocker.call(part) for part in path.split("/")] assert account.root.tois.__floordiv__.call_args_list == expected_calls # type: ignore def test_client_send_email(mocker, mock_account): """ Given: - A configured EWSClient instance When: - client.send_email is called Then: - The email is saved and sent successfully - The account field of the message is set """ send_and_save_mock = mocker.patch.object(EWSApiModule.Message, "send_and_save") message = Message( subject="Test subject", body="Test message", ) client = EWSClient( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_type=ACCESS_TYPE, default_target_mailbox=DEFAULT_TARGET_MAILBOX, ews_server=EWS_SERVER, max_fetch=MAX_FETCH, auth_type=AUTH_TYPE, version=VERSION_STR, folder=FOLDER, ) client.send_email(message) send_and_save_mock.assert_called_once() assert message.account @pytest.mark.parametrize("msg_type", [MSG_ID, MSG_NO_SUBJECT]) def test_client_reply_email(mocker, mock_account, msg_type): """ Given: - A configured EWSClient instance - Arguments for a reply email When: - client.reply_email is called Then: - The reply is created and sent successfully """ def mock_save(self, folder): folder.messages["reply_1"] = self return mocker.MagicMock(id="reply_1") mocker.patch.object(exchangelib.items.ReplyToItem, "save", mock_save) mocked_reply_send = mocker.patch.object(exchangelib.items.ReplyToItem, "send") client = EWSClient( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, access_type=ACCESS_TYPE, default_target_mailbox=DEFAULT_TARGET_MAILBOX, ews_server=EWS_SERVER, max_fetch=MAX_FETCH, auth_type=AUTH_TYPE, version=VERSION_STR, folder=FOLDER, ) reply_body = "This is a reply" reply_to = ["recipient@example.com"] reply_cc = ["cc_recipient@example.com"] reply_bcc = [] message = client.reply_email( in_reply_to=msg_type, to=reply_to, body=reply_body, subject="", bcc=reply_bcc, cc=reply_cc, html_body=None, attachments=[], ) assert isinstance(message, exchangelib.items.ReplyToItem) assert "Re:" in str(message.subject) assert message.new_body == reply_body assert message.to_recipients == reply_to assert message.cc_recipients == reply_cc assert message.bcc_recipients == reply_bcc mocked_reply_send.assert_called_once() def test_handle_html(mocker): """ Given: - HTML string containing inline images When: - Parsing the HTML string to incorporate the inline images Then: - Clean the HTML string and add the relevant references to image files """ mocker.patch.object(uuid, "uuid4", return_value="abcd1234") html_input = '
some textThis is a test email with attached images.
Another paragraph with an embedded image:
This paragraph has no image.
A link without an image """ expected_image_data = [ base64.b64decode("iVBORw0KGgoAAAANSUhEUgA=="), base64.b64decode("/9j/4AAQSkZJRgABAQEAYABgAAD/2w=="), ] expected_parsed_html = """This is a test email with attached images.
Another paragraph with an embedded image:
This paragraph has no image.
A link without an image """ clean_body, extracted_images = handle_html(html_content) assert clean_body == expected_parsed_html.format( image0_cid=extracted_images[0].content_id, image1_cid=extracted_images[1].content_id ) assert len(extracted_images) == 2 for i, image in enumerate(extracted_images): assert image.content == expected_image_data[i] def test_get_config_args_from_context(mocker): """ Given: - The integration context contains cached auto-discovery information When: - Trying to create a configuration object Then: - A configuration object is created based on the context information """ mocker.patch("EWSApiModule.get_build_from_context", return_value=BUILD) context = {"auth_type": "test_auth_type", "api_version": VERSION_STR, "service_endpoint": "test_service_endpoint"} credentials = Credentials(username=CLIENT_ID, password=CLIENT_SECRET) expected_args = { "credentials": credentials, "auth_type": context["auth_type"], "version": exchangelib.Version(BUILD, VERSION_STR), "service_endpoint": context["service_endpoint"], } config_args = get_config_args_from_context(context, credentials) assert config_args == expected_args def test_get_build_from_context(): """ Given: - The integration context contains cached auto-discovery information When: - Trying to get the discovered build data Then: - A Build object is returned based on the context information """ context = {"build": "10.0.10.1"} build = get_build_from_context(context) assert build == exchangelib.Build(10, 0, 10, 1) @pytest.mark.parametrize( "version, expected", [ ("2013", exchangelib.version.EXCHANGE_2013), ("2016", exchangelib.version.EXCHANGE_2016), ("2013_SP1", exchangelib.version.EXCHANGE_2013_SP1), ], ) def test_get_onprem_build(version, expected): """ Given: - A valid string representing a supported onprem exchange version When: - Converting the string to a Build object Then: - A Build object of the requested version is returned """ assert get_on_prem_build(version) == expected @pytest.mark.parametrize("version", [("2004"), ("test_version"), ("2003_SP1")]) def test_get_onprem_build_bad_version(version): """ Given: - An invalid string input to get_onprem_build When: - Converting the string to a Build object Then: - A ValueError should be raised """ with pytest.raises(ValueError): get_on_prem_build(version) def test_filter_dict_null(): """ Given: - Some dict When: - Dict has None values Then: - New dict is returned with the None values filtered out """ test_dict = { "some_val": 0, "bad_val": None, "another_val": "val", "another_bad_one": None, } expected_output = { "some_val": 0, "another_val": "val", } assert filter_dict_null(test_dict) == expected_output def test_switch_hr_headers(): """ Given: - A context object When: - Switching headers using a given header switch dict Then: - The keys that are present are switched """ test_context = { "willswitch": "1234", "wontswitch": "111", "alsoswitch": 5555, } header_changes = {"willswitch": "newkey", "alsoswitch": "annothernewkey", "doesnt_exiest": "doesnt break"} expected_output = {"annothernewkey": 5555, "newkey": "1234", "wontswitch": "111"} assert switch_hr_headers(test_context, header_changes) == expected_output def test_get_entry_for_object(): """ Given: - Results from a command When: - Creating the command output Then: - All empty values are filtered from the results object - Readable output table is created correctly with the requested swapped headers """ obj = [{"a": 1, "b": 2, "c": None, "d": 3}, {"a": 11, "b": None, "c": 5, "d": 6}, {"a": 3}] expected_output = [{"a": 1, "b": 2, "d": 3}, {"a": 11, "c": 5, "d": 6}, {"a": 3}] expected_hr = "### test\n|a|b|col|\n|---|---|---|\n| 1 | 2 | |\n| 11 | | 5 |\n| 3 | | |\n" entry = get_entry_for_object("test", "test_key", obj, headers=["a", "b", "col"], hr_header_changes={"c": "col"}) assert entry.readable_output == expected_hr assert entry.outputs == expected_output assert entry.outputs_prefix == "test_key" def test_get_entry_for_object_empty(): """ Given: - Results from a command When: - The result object is empty Then: - A message indicating there is no result is returned """ entry = get_entry_for_object("empty_obj", "test_key", {}) assert "There is no output" in entry.readable_output def test_delete_attachments_for_message(mocker, client): """ Given: - Id of some email - Ids of attachments to delete When: - Calling the delete-attachments command Then: - The requested attachments are deleted from the given email """ mock_items = { "itemid_1": [ FileAttachment(name="attach_1", content="test_content_1", attachment_id=AttachmentId(id="attach1")), FileAttachment(name="attach_2", content="test_content_2", attachment_id=AttachmentId(id="attach2")), ], "itemid_2": [], } mocker.patch.object( EWSClient, "get_attachments_for_item", side_effect=lambda item_id, _account, _attach_ids: mock_items.get(item_id, f"Item {item_id} not found"), ) attachment_detach_mock = mocker.patch.object(FileAttachment, "detach") expected_output = [ {"attachmentId": "attach1", "action": "deleted"}, {"attachmentId": "attach2", "action": "deleted"}, ] result = delete_attachments_for_message(client, {"item_id": "itemid_1"}) assert result[0].outputs == expected_output assert attachment_detach_mock.call_count == len(expected_output) def test_get_searchable_mailboxes(mocker, client): """ Given: - A configured EWS Client When: - The get_searchable_mailboxes function is called Then: - A list containing the relevant details for each searchable mailbox is returned """ from xml.etree import ElementTree as ET mock_elements = [ ET.fromstring(f"""