def setUpClass(cls): # There's no official Exchange server we can test against, and we can't really provide credentials for our # own test server to everyone on the Internet. Travis-CI uses the encrypted settings.yml.enc for testing. # # If you want to test against your own server and account, create your own settings.yml with credentials for # that server. 'settings.yml.sample' is provided as a template. try: with open(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'settings.yml')) as f: settings = safe_load(f) except FileNotFoundError: print('Skipping %s - no settings.yml file found' % cls.__name__) print('Copy settings.yml.sample to settings.yml and enter values for your test server') raise unittest.SkipTest('Skipping %s - no settings.yml file found' % cls.__name__) cls.settings = settings cls.verify_ssl = settings.get('verify_ssl', True) if not cls.verify_ssl: # Allow unverified TLS if requested in settings file BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter # Create an account shared by all tests tz = zoneinfo.ZoneInfo('Europe/Copenhagen') cls.retry_policy = FaultTolerance(max_wait=600) config = Configuration( server=settings['server'], credentials=Credentials(settings['username'], settings['password']), retry_policy=cls.retry_policy, ) cls.account = Account(primary_smtp_address=settings['account'], access_type=DELEGATE, config=config, locale='da_DK', default_timezone=tz)
def setUp(self): super().setUp() # Enable retries, to make tests more robust Autodiscovery.INITIAL_RETRY_POLICY = FaultTolerance(max_wait=5) Autodiscovery.RETRY_WAIT = 5 # Each test should start with a clean autodiscover cache clear_cache() # Some mocking helpers self.domain = get_domain(self.account.primary_smtp_address) self.dummy_ad_endpoint = 'https://%s/Autodiscover/Autodiscover.xml' % self.domain self.dummy_ews_endpoint = 'https://expr.example.com/EWS/Exchange.asmx' self.dummy_ad_response = b'''\ <?xml version="1.0" encoding="utf-8"?> <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"> <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"> <User> <AutoDiscoverSMTPAddress>%s</AutoDiscoverSMTPAddress> </User> <Account> <AccountType>email</AccountType> <Action>settings</Action> <Protocol> <Type>EXPR</Type> <EwsUrl>%s</EwsUrl> </Protocol> </Account> </Response> </Autodiscover>''' % (self.account.primary_smtp_address.encode(), self.dummy_ews_endpoint.encode()) self.dummy_ews_response = b'''\
def setUpClass(cls): # There's no official Exchange server we can test against, and we can't really provide credentials for our # own test server to everyone on the Internet. Travis-CI uses the encrypted settings.yml.enc for testing. # # If you want to test against your own server and account, create your own settings.yml with credentials for # that server. 'settings.yml.sample' is provided as a template. try: with open( os.path.join(os.path.dirname(os.path.dirname(__file__)), "settings.yml")) as f: settings = safe_load(f) except FileNotFoundError: print(f"Skipping {cls.__name__} - no settings.yml file found") print( "Copy settings.yml.sample to settings.yml and enter values for your test server" ) raise unittest.SkipTest( f"Skipping {cls.__name__} - no settings.yml file found") cls.settings = settings cls.verify_ssl = settings.get("verify_ssl", True) if not cls.verify_ssl: # Allow unverified TLS if requested in settings file BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter # Create an account shared by all tests cls.tz = zoneinfo.ZoneInfo("Europe/Copenhagen") cls.retry_policy = FaultTolerance(max_wait=600) cls.config = Configuration( server=settings["server"], credentials=Credentials(settings["username"], settings["password"]), retry_policy=cls.retry_policy, ) cls.account = cls.get_account()
def test_get_service_authtype(self, m): with self.assertRaises(TransportError) as e: _ = self.get_test_protocol(auth_type=None).auth_type self.assertEqual(e.exception.args[0], "XXX") with self.assertRaises(RateLimitError) as e: _ = self.get_test_protocol( auth_type=None, retry_policy=FaultTolerance(max_wait=0.5)).auth_type self.assertEqual(e.exception.args[0], "Max timeout reached")
def test_failed_login_via_account(self): Autodiscovery.INITIAL_RETRY_POLICY = FaultTolerance(max_wait=10) clear_cache() with self.assertRaises(AutoDiscoverFailed): Account( primary_smtp_address=self.account.primary_smtp_address, access_type=DELEGATE, credentials=Credentials( self.account.protocol.credentials.username, 'WRONG_PASSWORD'), autodiscover=True, locale='da_DK', )
def test_service_account_back_off(self): # Test back-off logic in FaultTolerance sa = FaultTolerance() # Initially, the value is None self.assertIsNone(sa.back_off_until) # Test a non-expired back off value in_a_while = datetime.datetime.now() + datetime.timedelta(seconds=10) sa.back_off_until = in_a_while self.assertEqual(sa.back_off_until, in_a_while) # Test an expired back off value sa.back_off_until = datetime.datetime.now() time.sleep(0.001) self.assertIsNone(sa.back_off_until) # Test the back_off() helper sa.back_off(10) # This is not a precise test. Assuming fast computers, there should be less than 1 second between the two lines. self.assertEqual( int( math.ceil((sa.back_off_until - datetime.datetime.now()).total_seconds())), 10) # Test expiry sa.back_off(0) time.sleep(0.001) self.assertIsNone(sa.back_off_until) # Test default value sa.back_off(None) self.assertEqual( int( math.ceil((sa.back_off_until - datetime.datetime.now()).total_seconds())), 60)
def setUp(self): super().setUp() # Enable retries, to make tests more robust Autodiscovery.INITIAL_RETRY_POLICY = FaultTolerance(max_wait=5) Autodiscovery.RETRY_WAIT = 5 # Each test should start with a clean autodiscover cache clear_cache() # Some mocking helpers self.domain = get_domain(self.account.primary_smtp_address) self.dummy_ad_endpoint = f"https://{self.domain}/Autodiscover/Autodiscover.xml" self.dummy_ews_endpoint = "https://expr.example.com/EWS/Exchange.asmx" self.dummy_ad_response = self.settings_xml(self.account.primary_smtp_address, self.dummy_ews_endpoint)
def test_error_too_many_objects_opened(self, m): # Test that we can parse ErrorTooManyObjectsOpened via ResponseMessage and return version = mock_version(build=EXCHANGE_2010) protocol = mock_protocol(version=version, service_endpoint='example.com') account = mock_account(version=version, protocol=protocol) ws = FindFolder(account=account, folders=[None]) xml = b'''\ <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Header> <h:ServerVersionInfo xmlns:h="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/exchange/services/2006/types" MajorVersion="15" MinorVersion="0" MajorBuildNumber="1497" MinorBuildNumber="6" Version="V2_23"/> </s:Header> <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <m:FindFolderResponse xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"> <m:ResponseMessages> <m:FindFolderResponseMessage ResponseClass="Error"> <m:MessageText>Too many concurrent connections opened.</m:MessageText> <m:ResponseCode>ErrorTooManyObjectsOpened</m:ResponseCode> <m:DescriptiveLinkKey>0</m:DescriptiveLinkKey> </m:FindFolderResponseMessage> </m:ResponseMessages> </m:FindFolderResponse> </s:Body> </s:Envelope>''' header, body = ws._get_soap_parts(response=MockResponse(xml)) # Just test that we can parse the error with self.assertRaises(ErrorTooManyObjectsOpened): list(ws._get_elements_in_response(response=ws._get_soap_messages(body=body))) # Test that it gets converted to an ErrorServerBusy exception. This happens deep inside EWSService methods # so it's easier to only mock the response. self.account.root # Needed to get past the GetFolder request m.post(self.account.protocol.service_endpoint, content=xml) self.account.protocol.config.retry_policy = FaultTolerance(max_wait=0) with self.assertRaises(ErrorServerBusy) as e: list(FolderCollection(account=self.account, folders=[self.account.root]).find_folders()) self.assertEqual(e.exception.back_off, None) # ErrorTooManyObjectsOpened has no BackOffMilliseconds value
def test_error_too_many_objects_opened(self, m): # Test that we can parse ErrorTooManyObjectsOpened via ResponseMessage and return version = mock_version(build=EXCHANGE_2010) protocol = mock_protocol(version=version, service_endpoint="example.com") account = mock_account(version=version, protocol=protocol) ws = FindFolder(account=account) xml = b"""\ <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body> <m:FindFolderResponse xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"> <m:ResponseMessages> <m:FindFolderResponseMessage ResponseClass="Error"> <m:MessageText>Too many concurrent connections opened.</m:MessageText> <m:ResponseCode>ErrorTooManyObjectsOpened</m:ResponseCode> <m:DescriptiveLinkKey>0</m:DescriptiveLinkKey> </m:FindFolderResponseMessage> </m:ResponseMessages> </m:FindFolderResponse> </s:Body> </s:Envelope>""" # Just test that we can parse the error with self.assertRaises(ErrorTooManyObjectsOpened): list(ws.parse(xml)) # Test that it gets converted to an ErrorServerBusy exception. This happens deep inside EWSService methods # so it's easier to only mock the response. self.account.root # Needed to get past the GetFolder request m.post(self.account.protocol.service_endpoint, content=xml) orig_policy = self.account.protocol.config.retry_policy try: self.account.protocol.config.retry_policy = FaultTolerance( max_wait=0) with self.assertRaises(ErrorServerBusy) as e: list( FolderCollection(account=self.account, folders=[self.account.root ]).find_folders()) self.assertEqual( e.exception.back_off, None ) # ErrorTooManyObjectsOpened has no BackOffMilliseconds value finally: self.account.protocol.config.retry_policy = orig_policy
def test_pickle(self): # Test that we can pickle various objects item = Message(folder=self.account.inbox, subject='XXX', categories=self.categories).save() attachment = FileAttachment(name='pickle_me.txt', content=b'') for o in ( FaultTolerance(max_wait=3600), self.account.protocol, attachment, self.account.root, self.account.inbox, self.account, item, ): with self.subTest(o=o): pickled_o = pickle.dumps(o) unpickled_o = pickle.loads(pickled_o) self.assertIsInstance(unpickled_o, type(o)) if not isinstance(o, (Account, Protocol, FaultTolerance)): # __eq__ is not defined on some classes self.assertEqual(o, unpickled_o)
def test_post_ratelimited(self): url = 'https://example.com' protocol = self.account.protocol orig_policy = protocol.config.retry_policy RETRY_WAIT = exchangelib.util.RETRY_WAIT MAX_REDIRECTS = exchangelib.util.MAX_REDIRECTS session = protocol.get_session() try: # Make sure we fail fast in error cases protocol.config.retry_policy = FailFast() # Test the straight, HTTP 200 path session.post = mock_post(url, 200, {}, 'foo') r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') self.assertEqual(r.content, b'foo') # Test exceptions raises by the POST request for err_cls in CONNECTION_ERRORS: session.post = mock_session_exception(err_cls) with self.assertRaises(err_cls): r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') # Test bad exit codes and headers session.post = mock_post(url, 401, {}) with self.assertRaises(UnauthorizedError): r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') session.post = mock_post(url, 999, {'connection': 'close'}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') session.post = mock_post( url, 302, { 'location': '/ews/genericerrorpage.htm?aspxerrorpath=/ews/exchange.asmx' }) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') session.post = mock_post(url, 503, {}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') # No redirect header session.post = mock_post(url, 302, {}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Redirect header to same location session.post = mock_post(url, 302, {'location': url}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Redirect header to relative location session.post = mock_post(url, 302, {'location': url + '/foo'}) with self.assertRaises(RedirectError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Redirect header to other location and allow_redirects=False session.post = mock_post(url, 302, {'location': 'https://contoso.com'}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Redirect header to other location and allow_redirects=True exchangelib.util.MAX_REDIRECTS = 0 session.post = mock_post(url, 302, {'location': 'https://contoso.com'}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='', allow_redirects=True) # CAS error session.post = mock_post(url, 999, {'X-CasErrorCode': 'AAARGH!'}) with self.assertRaises(CASError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Allow XML data in a non-HTTP 200 response session.post = mock_post(url, 500, {}, '<?xml version="1.0" ?><foo></foo>') r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') self.assertEqual(r.content, b'<?xml version="1.0" ?><foo></foo>') # Bad status_code and bad text session.post = mock_post(url, 999, {}) with self.assertRaises(TransportError): r, session = post_ratelimited(protocol=protocol, session=session, url=url, headers=None, data='') # Test rate limit exceeded exchangelib.util.RETRY_WAIT = 1 protocol.config.retry_policy = FaultTolerance( max_wait=0.5) # Fail after first RETRY_WAIT session.post = mock_post(url, 503, {'connection': 'close'}) # Mock renew_session to return the same session so the session object's 'post' method is still mocked protocol.renew_session = lambda s: s with self.assertRaises(RateLimitError) as rle: r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') self.assertEqual(rle.exception.status_code, 503) self.assertEqual(rle.exception.url, url) self.assertTrue(1 <= rle.exception.total_wait < 2) # One RETRY_WAIT plus some overhead # Test something larger than the default wait, so we retry at least once protocol.retry_policy.max_wait = 3 # Fail after second RETRY_WAIT session.post = mock_post(url, 503, {'connection': 'close'}) with self.assertRaises(RateLimitError) as rle: r, session = post_ratelimited(protocol=protocol, session=session, url='http://', headers=None, data='') self.assertEqual(rle.exception.status_code, 503) self.assertEqual(rle.exception.url, url) # We double the wait for each retry, so this is RETRY_WAIT + 2*RETRY_WAIT plus some overhead self.assertTrue(3 <= rle.exception.total_wait < 4, rle.exception.total_wait) finally: protocol.retire_session( session) # We have patched the session, so discard it # Restore patched attributes and functions protocol.config.retry_policy = orig_policy exchangelib.util.RETRY_WAIT = RETRY_WAIT exchangelib.util.MAX_REDIRECTS = MAX_REDIRECTS try: delattr(protocol, 'renew_session') except AttributeError: pass
def setUpClass(cls): super(AutodiscoverTest, cls).setUpClass() AutodiscoverProtocol.INITIAL_RETRY_POLICY = FaultTolerance(max_wait=30)