def test_protocol_default_values(self): # Test that retry_policy and auth_type always get a value regardless of how we create an Account c = Credentials(self.settings["username"], self.settings["password"]) a = Account( self.account.primary_smtp_address, autodiscover=False, config=Configuration( server=self.settings["server"], credentials=c, ), ) self.assertIsNotNone(a.protocol.auth_type) self.assertIsNotNone(a.protocol.retry_policy) a = Account( self.account.primary_smtp_address, autodiscover=True, config=Configuration( server=self.settings["server"], credentials=c, ), ) self.assertIsNotNone(a.protocol.auth_type) self.assertIsNotNone(a.protocol.retry_policy) a = Account(self.account.primary_smtp_address, autodiscover=True, credentials=c) self.assertIsNotNone(a.protocol.auth_type) self.assertIsNotNone(a.protocol.retry_policy)
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 test_decrease_poolsize(self): # Test increasing and decreasing the pool size max_connections = 3 protocol = Protocol(config=Configuration( service_endpoint='https://example.com/Foo.asmx', credentials=Credentials(get_random_string(8), get_random_string( 8)), auth_type=NTLM, version=Version(Build(15, 1)), retry_policy=FailFast(), max_connections=max_connections, )) self.assertEqual(protocol._session_pool.qsize(), 0) self.assertEqual(protocol.session_pool_size, 0) protocol.increase_poolsize() protocol.increase_poolsize() protocol.increase_poolsize() with self.assertRaises(SessionPoolMaxSizeReached): protocol.increase_poolsize() self.assertEqual(protocol._session_pool.qsize(), max_connections) self.assertEqual(protocol.session_pool_size, max_connections) protocol.decrease_poolsize() protocol.decrease_poolsize() with self.assertRaises(SessionPoolMinSizeReached): protocol.decrease_poolsize() self.assertEqual(protocol._session_pool.qsize(), 1)
def test_protocol_instance_caching(self, m): # Verify that we get the same Protocol instance for the same combination of (endpoint, credentials) user, password = get_random_string(8), get_random_string(8) config = Configuration(service_endpoint='https://example.com/Foo.asmx', credentials=Credentials(user, password), auth_type=NTLM, version=Version(Build(15, 1)), retry_policy=FailFast()) # Test CachingProtocol.__getitem__ with self.assertRaises(KeyError): _ = Protocol[config] base_p = Protocol(config=config) self.assertEqual(base_p, Protocol[config][0]) # Make sure we always return the same item when creating a Protocol with the same endpoint and creds for _ in range(10): p = Protocol(config=config) self.assertEqual(base_p, p) self.assertEqual(id(base_p), id(p)) self.assertEqual(hash(base_p), hash(p)) self.assertEqual(id(base_p._session_pool), id(p._session_pool)) # Test CachingProtocol.__delitem__ del Protocol[config] with self.assertRaises(KeyError): _ = Protocol[config] # Make sure we get a fresh instance after we cleared the cache p = Protocol(config=config) self.assertNotEqual(base_p, p) Protocol.clear_cache()
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_autodiscover_cache(self, m): # Mock the default endpoint that we test in step 1 of autodiscovery m.post(self.dummy_ad_endpoint, status_code=200, content=self.dummy_ad_response) # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post(self.dummy_ews_endpoint, status_code=200) discovery = Autodiscovery( email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, ) # Not cached self.assertNotIn(discovery._cache_key, autodiscover_cache) discovery.discover() # Now it's cached self.assertIn(discovery._cache_key, autodiscover_cache) # Make sure the cache can be looked by value, not by id(). This is important for multi-threading/processing self.assertIn( (self.account.primary_smtp_address.split('@')[1], Credentials(self.account.protocol.credentials.username, self.account.protocol.credentials.password), True), autodiscover_cache) # Poison the cache with a failing autodiscover endpoint. discover() must handle this and rebuild the cache autodiscover_cache[discovery._cache_key] = AutodiscoverProtocol( config=Configuration( service_endpoint= 'https://example.com/Autodiscover/Autodiscover.xml', credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, retry_policy=FailFast(), )) m.post('https://example.com/Autodiscover/Autodiscover.xml', status_code=404) discovery.discover() self.assertIn(discovery._cache_key, autodiscover_cache) # Make sure that the cache is actually used on the second call to discover() _orig = discovery._step_1 def _mock(slf, *args, **kwargs): raise NotImplementedError() discovery._step_1 = MethodType(_mock, discovery) discovery.discover() # Fake that another thread added the cache entry into the persistent storage but we don't have it in our # in-memory cache. The cache should work anyway. autodiscover_cache._protocols.clear() discovery.discover() discovery._step_1 = _orig # Make sure we can delete cache entries even though we don't have it in our in-memory cache autodiscover_cache._protocols.clear() del autodiscover_cache[discovery._cache_key] # This should also work if the cache does not contain the entry anymore del autodiscover_cache[discovery._cache_key]
def test_autodiscover_from_account(self, m): # Test that autodiscovery via account creation works # Mock the default endpoint that we test in step 1 of autodiscovery m.post(self.dummy_ad_endpoint, status_code=200, content=self.dummy_ad_response) # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post(self.dummy_ews_endpoint, status_code=200, content=self.dummy_ews_response) self.assertEqual(len(autodiscover_cache), 0) account = Account( primary_smtp_address=self.account.primary_smtp_address, config=Configuration( credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, ), autodiscover=True, locale='da_DK', ) self.assertEqual(account.primary_smtp_address, self.account.primary_smtp_address) self.assertEqual(account.protocol.service_endpoint.lower(), self.dummy_ews_endpoint.lower()) # Make sure cache is full self.assertEqual(len(autodiscover_cache), 1) self.assertTrue((account.domain, self.account.protocol.credentials, True) in autodiscover_cache) # Test that autodiscover works with a full cache account = Account( primary_smtp_address=self.account.primary_smtp_address, config=Configuration( credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, ), autodiscover=True, locale='da_DK', ) self.assertEqual(account.primary_smtp_address, self.account.primary_smtp_address) # Test cache manipulation key = (account.domain, self.account.protocol.credentials, True) self.assertTrue(key in autodiscover_cache) del autodiscover_cache[key] self.assertFalse(key in autodiscover_cache)
def test_protocol_instance_caching(self, m): # Verify that we get the same Protocol instance for the same combination of (endpoint, credentials) user, password = get_random_string(8), get_random_string(8) base_p = Protocol(config=Configuration( service_endpoint='https://example.com/Foo.asmx', credentials=Credentials(user, password), auth_type=NTLM, version=Version(Build(15, 1)), retry_policy=FailFast() )) for _ in range(10): p = Protocol(config=Configuration( service_endpoint='https://example.com/Foo.asmx', credentials=Credentials(user, password), auth_type=NTLM, version=Version(Build(15, 1)), retry_policy=FailFast() )) self.assertEqual(base_p, p) self.assertEqual(id(base_p), id(p)) self.assertEqual(hash(base_p), hash(p)) self.assertEqual(id(base_p._session_pool), id(p._session_pool)) Protocol.clear_cache()
def test_session(self, m): protocol = Protocol(config=Configuration( service_endpoint='https://example.com/Foo.asmx', credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, version=Version(Build(15, 1)), retry_policy=FailFast() )) session = protocol.create_session() new_session = protocol.renew_session(session) self.assertNotEqual(id(session), id(new_session))
def get_test_protocol(**kwargs): return AutodiscoverProtocol( config=Configuration( service_endpoint=kwargs.get("service_endpoint", "https://example.com/Autodiscover/Autodiscover.xml"), credentials=kwargs.get("credentials", Credentials(get_random_string(8), get_random_string(8))), auth_type=kwargs.get("auth_type", NTLM), retry_policy=kwargs.get("retry_policy", FailFast()), ) )
def test_hardcode_all(self, m): # Test that we can hardcode everything without having a working server. This is useful if neither tasting or # guessing missing values works. Configuration( server="example.com", credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, version=Version(build=Build(15, 1, 2, 3), api_version="foo"), )
def test_magic(self): config = Configuration( server="example.com", credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, version=Version(build=Build(15, 1, 2, 3), api_version="foo"), ) # Just test that these work str(config) repr(config)
def test_init(self): with self.assertRaises(TypeError) as e: Protocol(config="XXX") self.assertEqual( e.exception.args[0], "'config' 'XXX' must be of type <class 'exchangelib.configuration.Configuration'>" ) with self.assertRaises(AttributeError) as e: Protocol(config=Configuration()) self.assertEqual(e.exception.args[0], "'config.service_endpoint' must be set")
def test_pickle(self): # Test that we can pickle, repr and str Protocols o = Protocol(config=Configuration( service_endpoint='https://example.com/Foo.asmx', credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, version=Version(Build(15, 1)), retry_policy=FailFast() )) pickled_o = pickle.dumps(o) unpickled_o = pickle.loads(pickled_o) self.assertIsInstance(unpickled_o, type(o)) self.assertEqual(repr(o), repr(unpickled_o)) self.assertEqual(str(o), str(unpickled_o))
def test_close_autodiscover_connections(self, m): # A live test that we can close TCP connections c = Credentials(get_random_string(8), get_random_string(8)) autodiscover_cache[('example.com', c)] = AutodiscoverProtocol( config=Configuration( service_endpoint= 'https://example.com/Autodiscover/Autodiscover.xml', credentials=c, auth_type=NTLM, retry_policy=FailFast(), )) self.assertEqual(len(autodiscover_cache), 1) close_connections()
def test_autodiscover_direct_gc(self, m): # Test garbage collection of the autodiscover cache c = Credentials(get_random_string(8), get_random_string(8)) autodiscover_cache[('example.com', c)] = AutodiscoverProtocol( config=Configuration( service_endpoint= 'https://example.com/Autodiscover/Autodiscover.xml', credentials=c, auth_type=NTLM, retry_policy=FailFast(), )) self.assertEqual(len(autodiscover_cache), 1) autodiscover_cache.__del__()
def get_test_protocol(**kwargs): return Protocol(config=Configuration( server=kwargs.get("server"), service_endpoint=kwargs.get( "service_endpoint", f"https://{get_random_hostname()}/Foo.asmx"), credentials=kwargs.get( "credentials", Credentials(get_random_string(8), get_random_string(8))), auth_type=kwargs.get("auth_type", NTLM), version=kwargs.get("version", Version(Build(15, 1))), retry_policy=kwargs.get("retry_policy", FailFast()), max_connections=kwargs.get("max_connections"), ))
def test_tzlocal_failure(self, m): a = Account( primary_smtp_address=self.account.primary_smtp_address, access_type=DELEGATE, config=Configuration( service_endpoint=self.account.protocol.service_endpoint, credentials=Credentials(self.account.protocol.credentials.username, "WRONG_PASSWORD"), version=self.account.version, auth_type=self.account.protocol.auth_type, retry_policy=self.retry_policy, ), autodiscover=False, ) self.assertEqual(a.default_timezone, UTC)
def test_close(self): # Don't use example.com here - it does not resolve or answer on all ISPs proc = psutil.Process() ip_addresses = { info[4][0] for info in socket.getaddrinfo('httpbin.org', 80, socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_IP) } def conn_count(): return len( [p for p in proc.connections() if p.raddr[0] in ip_addresses]) self.assertGreater(len(ip_addresses), 0) protocol = Protocol(config=Configuration( service_endpoint='http://httpbin.org', credentials=Credentials(get_random_string(8), get_random_string( 8)), auth_type=NOAUTH, version=Version(Build(15, 1)), retry_policy=FailFast(), max_connections=3)) # Merely getting a session should not create conections session = protocol.get_session() self.assertEqual(conn_count(), 0) # Open one URL - we have 1 connection session.get('http://httpbin.org') self.assertEqual(conn_count(), 1) # Open the same URL - we should still have 1 connection session.get('http://httpbin.org') self.assertEqual(conn_count(), 1) # Open some more connections s2 = protocol.get_session() s2.get('http://httpbin.org') s3 = protocol.get_session() s3.get('http://httpbin.org') self.assertEqual(conn_count(), 3) # Releasing the sessions does not close the connections protocol.release_session(session) protocol.release_session(s2) protocol.release_session(s3) self.assertEqual(conn_count(), 3) # But closing explicitly does protocol.close() self.assertEqual(conn_count(), 0)
def test_init(self): with self.assertRaises(ValueError) as e: Configuration(credentials='foo') self.assertEqual(e.exception.args[0], "'credentials' 'foo' must be a Credentials instance") with self.assertRaises(AttributeError) as e: Configuration(server='foo', service_endpoint='bar') self.assertEqual( e.exception.args[0], "Only one of 'server' or 'service_endpoint' must be provided") with self.assertRaises(ValueError) as e: Configuration(auth_type='foo') self.assertEqual( e.exception.args[0], "'auth_type' 'foo' must be one of %s" % ', '.join("'%s'" % k for k in sorted(AUTH_TYPE_MAP))) with self.assertRaises(ValueError) as e: Configuration(version='foo') self.assertEqual(e.exception.args[0], "'version' 'foo' must be a Version instance") with self.assertRaises(ValueError) as e: Configuration(retry_policy='foo') self.assertEqual( e.exception.args[0], "'retry_policy' 'foo' must be a RetryPolicy instance")
def test_magic(self, m): # Just test we don't fail when calling repr() and str(). Insert a dummy cache entry for testing c = Credentials(get_random_string(8), get_random_string(8)) autodiscover_cache[('example.com', c)] = AutodiscoverProtocol( config=Configuration( service_endpoint= 'https://example.com/Autodiscover/Autodiscover.xml', credentials=c, auth_type=NTLM, retry_policy=FailFast(), )) self.assertEqual(len(autodiscover_cache), 1) str(autodiscover_cache) repr(autodiscover_cache) for protocol in autodiscover_cache._protocols.values(): str(protocol) repr(protocol)
def test_login_failure_and_credentials_update(self): # Create an account that does not need to create any connections account = Account( primary_smtp_address=self.account.primary_smtp_address, access_type=DELEGATE, config=Configuration( service_endpoint=self.account.protocol.service_endpoint, credentials=Credentials(self.account.protocol.credentials.username, 'WRONG_PASSWORD'), version=self.account.version, auth_type=self.account.protocol.auth_type, retry_policy=self.retry_policy, ), autodiscover=False, locale='da_DK', ) # Should fail when credentials are wrong, but UnauthorizedError is caught and retried. Mock the needed methods import exchangelib.util _orig1 = exchangelib.util._may_retry_on_error _orig2 = exchangelib.util._raise_response_errors def _mock1(response, retry_policy, wait): if response.status_code == 401: return False return _orig1(response, retry_policy, wait) def _mock2(response, protocol): if response.status_code == 401: raise UnauthorizedError('Invalid credentials for %s' % response.url) return _orig2(response, protocol) exchangelib.util._may_retry_on_error = _mock1 exchangelib.util._raise_response_errors = _mock2 try: with self.assertRaises(UnauthorizedError): account.root.refresh() finally: exchangelib.util._may_retry_on_error = _orig1 exchangelib.util._raise_response_errors = _orig2 # Cannot update from Configuration object with self.assertRaises(AttributeError): account.protocol.config.credentials = self.account.protocol.credentials # Should succeed after credentials update account.protocol.credentials = self.account.protocol.credentials account.root.refresh()
def test_init(self): with self.assertRaises(TypeError) as e: Configuration(credentials="foo") self.assertEqual( e.exception.args[0], "'credentials' 'foo' must be of type <class 'exchangelib.credentials.BaseCredentials'>" ) with self.assertRaises(ValueError) as e: Configuration(credentials=None, auth_type=NTLM) self.assertEqual( e.exception.args[0], "Auth type 'NTLM' was detected but no credentials were provided") with self.assertRaises(AttributeError) as e: Configuration(server="foo", service_endpoint="bar") self.assertEqual( e.exception.args[0], "Only one of 'server' or 'service_endpoint' must be provided") with self.assertRaises(ValueError) as e: Configuration(auth_type="foo") self.assertEqual( e.exception.args[0], f"'auth_type' 'foo' must be one of {sorted(AUTH_TYPE_MAP)}") with self.assertRaises(TypeError) as e: Configuration(version="foo") self.assertEqual( e.exception.args[0], "'version' 'foo' must be of type <class 'exchangelib.version.Version'>" ) with self.assertRaises(TypeError) as e: Configuration(retry_policy="foo") self.assertEqual( e.exception.args[0], "'retry_policy' 'foo' must be of type <class 'exchangelib.protocol.RetryPolicy'>" ) with self.assertRaises(TypeError) as e: Configuration(max_connections="foo") self.assertEqual( e.exception.args[0], "'max_connections' 'foo' must be of type <class 'int'>") self.assertEqual( Configuration().server, None) # Test that property works when service_endpoint is None
def test_login_failure_and_credentials_update(self): # Create an account that does not need to create any connections account = Account( primary_smtp_address=self.account.primary_smtp_address, access_type=DELEGATE, config=Configuration( service_endpoint=self.account.protocol.service_endpoint, credentials=Credentials( self.account.protocol.credentials.username, 'WRONG_PASSWORD'), version=self.account.version, auth_type=self.account.protocol.auth_type, retry_policy=self.retry_policy, ), autodiscover=False, locale='da_DK', ) # Should fail when credentials are wrong, but UnauthorizedError is caught and retried. Mock the needed methods class Mock1(FaultTolerance): def may_retry_on_error(self, response, wait): if response.status_code == 401: return False return super().may_retry_on_error(response, wait) def raise_response_errors(self, response): if response.status_code == 401: raise UnauthorizedError('Invalid credentials for %s' % response.url) return super().raise_response_errors(response) try: account.protocol.config.retry_policy = Mock1() with self.assertRaises(UnauthorizedError): account.root.refresh() finally: account.protocol.config.retry_policy = self.retry_policy # Cannot update from Configuration object with self.assertRaises(AttributeError): account.protocol.config.credentials = self.account.protocol.credentials # Should succeed after credentials update account.protocol.credentials = self.account.protocol.credentials account.root.refresh()
def initEventStorage(): # Open sqlite database, up to now it is used to store calendarNames / Outlook categories and Mattermost groups in which calendar events should be posted. # The calendar events are stored in an Outlook calendar and also in the sqlite database up to now. try: conn = sqlite3.connect(calendarSettings['DatabaseName']) cursor = conn.cursor() except Exception as e: print( "Could not open sqlite database due to following exception:\n {0} \n {1}" .format(e.__doc__, e.message)) # Open outlook calendar try: config = Configuration(username=outlookSettings['Username'], password=outlookSettings['Password']) account = Account(primary_smtp_address=outlookSettings['Email'], config=config, autodiscover=True, access_type=DELEGATE) except Exception as e: print( "Could not open Outlook calendar due to following exception:\n {0} \n {1}" .format(e.__doc__, e.message)) # Set timezone needed later, note: only Copenhagen is mapped but it is the same timezone as Berlin tz = EWSTimeZone.timezone('Europe/Copenhagen') # Create table which contains subcalendars # calendarName: Name which is refered to add an event, calendarMattermostGroup: group in which events should be published # calendarName is rewritten in an Outlook calendar category to be stored in Outlook calendar try: createCalendarTable = "CREATE TABLE IF NOT EXISTS 'calendarTable' (id INTEGER PRIMARY KEY AUTOINCREMENT, calendarName TEXT, calendarMattermostGroup TEXT)" conn.execute(createCalendarTable) except Exception as e: print( "Could not access / create sqlite calendar due to following exception:\n {0} \n {1}" .format(e.__doc__, e.message)) return conn, cursor, tz, account
def test_autodiscover_failure(self): # A live test that errors can be raised. Here, we try to aútodiscover a non-existing email address if not self.settings.get('autodiscover_server'): self.skipTest( "Skipping %s - no 'autodiscover_server' entry in settings.yml" % self.__class__.__name__) # Autodiscovery may take a long time. Prime the cache with the autodiscover server from the config file ad_endpoint = 'https://%s/Autodiscover/Autodiscover.xml' % self.settings[ 'autodiscover_server'] cache_key = (self.domain, self.account.protocol.credentials) autodiscover_cache[cache_key] = AutodiscoverProtocol( config=Configuration( service_endpoint=ad_endpoint, credentials=self.account.protocol.credentials, auth_type=NTLM, retry_policy=self.retry_policy, )) with self.assertRaises(ErrorNonExistentMailbox): exchangelib.autodiscover.discovery.discover( email='XXX.' + self.account.primary_smtp_address, credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, )