-
-
Notifications
You must be signed in to change notification settings - Fork 247
Open
Description
Hello,
TL;DR: I cannot get exchangelib to work with CBA, although oauth2 works.
I am already using exchangelib with oauth2, but would like to use certificates instead of tokens, because tokens have a limited lifetime.
I have researched related issues: #1183 #1093 #751 and Google.
Working unit test with oauth2:
from exchangelib import IMPERSONATION, OAUTH2, Account, Configuration, OAuth2Credentials
from exchangelib.version import EXCHANGE_O365, Version
CLIENT_ID = "xxx"
CLIENT_SECRET = "xxx"
TENANT_ID = "xxx"
imap_host = "outlook.office365.com"
mailbox_user = "user@example.com"
# authenticate using OAUTH2
credentials = OAuth2Credentials(
client_id=CLIENT_ID, tenant_id=TENANT_ID, client_secret=CLIENT_SECRET
)
# do not rely on autodiscover but specify explicit server name
config = Configuration(
server=imap_host, credentials=credentials,
version=Version(build=EXCHANGE_O365), auth_type=OAUTH2
)
def test_exchange_connection(logger):
logger.info("Logging in to mailbox")
# see: https://github.com/ecederstrand/exchangelib/issues/735
# use IMPERSONATION instead of DELEGATE: "ExchangeImpersonation SOAP header must be present for this type of OAuth token"
my_account = Account(
primary_smtp_address=mailbox_user,
config=config, autodiscover=False, access_type=IMPERSONATION
)
logger.info(f"my_account.root.tree(): {my_account.root.tree()}")
assert my_account.root.child_folder_count > 0
Attempted unit test for CBA:
from exchangelib.protocol import BaseProtocol
from exchangelib import Account, Configuration, DELEGATE, IMPERSONATION, CBA, TLSClientAuth
from exchangelib.version import EXCHANGE_O365, Version
imap_host = "outlook.office365.com"
mailbox_user = "user@example.com"
TLSClientAuth.cert_file = "mycert.pem"
BaseProtocol.HTTP_ADAPTER_CLS = TLSClientAuth
config = Configuration(server=imap_host, auth_type=CBA, version=Version(build=EXCHANGE_O365))
def test_exchange_connection(logger):
my_account = Account(
primary_smtp_address=mailbox_user,
config=config, autodiscover=False,
access_type=DELEGATE)
logger.info(f"my_account.root.child_folder_count: {dir(my_account.root.child_folder_count)}")
This results in a 401 error.
Relevant notes:
- I am not using autodiscover
- I already use certificates in Azure AD for MSAL with Python and that works
- The Python code uses a PEM file that combines both private key and public certificate - when using only the private key I had a loop of SSL errors
- I have tried DELEGATE and IMPERSONATION, not sure if that would make any difference
Is this the correct way or is there anything I should try?
Log below and thanks for looking.
============================= test session starts ============================== platform linux -- Python 3.12.8, pytest-7.4.4, pluggy-1.5.0 configfile: pytest.ini ----------------------------- live log collection ------------------------------ 2025-01-15 18:17:49 - DEBUG - spnego._gss - :55 - Python gssapi not available, cannot use any GSSAPIProxy protocols: No module named 'krb5' collected 1 item tests/test_exchange_cba.py::test_exchange_connection -------------------------------- live log call --------------------------------- 2025-01-15 18:17:49 - DEBUG - tzlocal - _get_localzone_name:123 - /etc/localtime found 2025-01-15 18:17:49 - DEBUG - tzlocal - _get_localzone_name:139 - 1 found: {'/etc/localtime is a symlink to': 'Iceland'} 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - __call__:408 - Waiting for _protocol_cache_lock 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - __call__:421 - Protocol __call__ cache miss. Adding key '('https://outlook.office365.com/EWS/Exchange.asmx', None)' 2025-01-15 18:17:49 - DEBUG - exchangelib.account - __init__:206 - Added account: user@example.com 2025-01-15 18:17:49 - INFO - tests.conftest - test_exchange_connection:29 - my_account: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_consume_item_service', 'access_type', 'ad_response', 'admin_audit_logs', 'affinity_cookie', 'archive_deleted_items', 'archive_inbox', 'archive_msg_folder_root', 'archive_recoverable_items_deletions', 'archive_recoverable_items_purges', 'archive_recoverable_items_root', 'archive_recoverable_items_versions', 'archive_root', 'bulk_archive', 'bulk_copy', 'bulk_create', 'bulk_delete', 'bulk_mark_as_junk', 'bulk_move', 'bulk_send', 'bulk_update', 'calendar', 'conflicts', 'contacts', 'conversation_history', 'create_rule', 'default_timezone', 'delegates', 'delete_rule', 'directory', 'domain', 'drafts', 'export', 'favorites', 'fetch', 'fetch_personas', 'fullname', 'identity', 'im_contact_list', 'inbox', 'journal', 'junk', 'local_failures', 'locale', 'mail_tips', 'msg_folder_root', 'my_contacts', 'notes', 'oof_settings', 'outbox', 'people_connect', 'primary_smtp_address', 'protocol', 'public_folders_root', 'pull_subscription', 'push_subscription', 'quick_contacts', 'recipient_cache', 'recoverable_items_deletions', 'recoverable_items_purges', 'recoverable_items_root', 'recoverable_items_versions', 'root', 'rules', 'search_folders', 'sent', 'server_failures', 'set_rule', 'streaming_subscription', 'subscribe_to_pull', 'subscribe_to_push', 'subscribe_to_streaming', 'sync_issues', 'tasks', 'todo_search', 'trash', 'unsubscribe', 'upload', 'version', 'voice_mail'] 2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _chunked_get_elements:278 - Processing chunk 1 containing 1 items 2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _get_response_xml:393 - Calling service GetFolder 2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _get_response_xml:395 - Trying API version Exchange2016 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - increase_poolsize:206 - Server outlook.office365.com: Increasing session pool size from 0 to 1 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - create_session:330 - Server outlook.office365.com: Created session 84582 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - get_session:249 - Server outlook.office365.com: Waiting for session 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - get_session:255 - Server outlook.office365.com: Got session 84582 2025-01-15 18:17:49 - DEBUG - exchangelib.util - post_ratelimited:811 - Session 84582 thread 140214710770560 timeout 120: POST'ing to https://outlook.office365.com/EWS/Exchange.asmx after 0s sleep 2025-01-15 18:17:49 - DEBUG - urllib3.connectionpool - _new_conn:1049 - Starting new HTTPS connection (1): outlook.office365.com:443 2025-01-15 18:17:50 - DEBUG - urllib3.connectionpool - _make_request:544 - https://outlook.office365.com:443 "POST /EWS/Exchange.asmx HTTP/1.1" 401 0 2025-01-15 18:17:50 - DEBUG - exchangelib.util - post_ratelimited:862 - Timeout: 120 Session: 84582 Thread: 140214710770560 Auth type: None URL: https://outlook.office365.com/EWS/Exchange.asmx HTTP adapter: Streaming: False Response time: 0.23992178899788996 Status code: 401 Request headers: {'User-Agent': 'exchangelib/5.5.0 (python-requests/2.32.3)', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'text/xml; charset=utf-8', 'X-AnchorMailbox': 'user@example.com', 'Content-Length': '1214'} Response headers: {'Content-Length': '0', 'Server': 'Microsoft-HTTPAPI/2.0', 'X-BEServer': 'AS8PR08MB6357', 'X-NanoProxy': '1,1', 'Request-Id': '3d6befc1-e20e-e7b9-1ac1-75aae8bdd77b', 'X-CalculatedFETarget': 'AS9PR07CU001.internal.outlook.com', 'X-BeSku': 'WCS6', 'X-BackEndHttpStatus': '401,401', 'BasicChallengeAdded': 'True', 'MS-CV': 'we9rPQ7iuecawXWq6L3Xew.1.1', 'X-CalculatedBETarget': 'AS8PR08MB6357.eurprd08.prod.outlook.com', 'X-DiagInfo': 'AS8PR08MB6357', 'X-FEEFZInfo': 'DHR', 'X-UserType': 'Business', 'X-FEProxyInfo': 'AS9PR07CA0001', 'X-FEServer': 'DUZPR01CA0137', 'X-Proxy-BackendServerStatus': '401', 'X-Proxy-RoutingCorrectness': '1', 'X-RUM-NotUpdateQueriedPath': '1', 'X-RUM-NotUpdateQueriedDbCopy': '1', 'X-RUM-Validated': '1', 'X-FirstHopCafeEFZ': 'DUB', 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', 'Set-Cookie': 'exchangecookie=a7a24814fef441248023763bc5787ec0; expires=Thu, 15-Jan-2026 18:17:50 GMT; path=/; secure; HttpOnly', 'WWW-Authenticate': 'Basic Realm=""', 'Date': 'Wed, 15 Jan 2025 18:17:49 GMT'} 2025-01-15 18:17:50 - DEBUG - exchangelib.util.xml - post_ratelimited:863 - Request XML: b'\nIdOnlyuser@example.comSMTPMailbox' Response XML: b'' 2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - retire_session:279 - Server outlook.office365.com: Retiring session 84582 2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - create_session:330 - Server outlook.office365.com: Created session 79172 2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - release_session:261 - Server outlook.office365.com: Releasing session 79172 FAILED [100%] =================================== FAILURES =================================== ___________________________ test_exchange_connection ___________________________ self = obj = cls = def __get__(self, obj, cls): if obj is None: return self obj_dict = obj.__dict__ name = self.func.__name__ with self.lock: try: # check if the value was computed before the lock was acquired > return obj_dict[name] E KeyError: 'root' ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/cached_property.py:63: KeyError During handling of the above exception, another exception occurred: logger = def test_exchange_connection(logger): my_account = Account( primary_smtp_address=mailbox_user, config=config, autodiscover=False, access_type=DELEGATE) logger.info(f"my_account: {dir(my_account)}") > logger.info(f"my_account.root.child_folder_count: {dir(my_account.root.child_folder_count)}") tests/test_exchange_cba.py:30: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/cached_property.py:67: in __get__ return obj_dict.setdefault(name, self.func(obj)) ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/account.py:350: in root return Root.get_distinguished(account=self) ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/folders/roots.py:145: in get_distinguished return cls._get_distinguished( ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/folders/base.py:226: in _get_distinguished return cls.resolve(account=folder.account, folder=folder) ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/folders/base.py:530: in resolve folders = list(FolderCollection(account=account, folders=[folder]).resolve()) ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/folders/collections.py:335: in resolve yield from self.__class__(account=self.account, folders=resolveable_folders).get_folders( ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/folders/collections.py:403: in get_folders yield from GetFolder(account=self.account).call( ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/get_folder.py:51: in _elems_to_objs for folder, elem in zip(self.folders, elems): ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/common.py:279: in _chunked_get_elements yield from self._get_elements(payload=payload_func(chunk, **kwargs)) ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/common.py:300: in _get_elements yield from self._response_generator(payload=payload) ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/common.py:263: in _response_generator response = self._get_response_xml(payload=payload) ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/common.py:396: in _get_response_xml r = self._get_response(payload=payload, api_version=api_version) ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/common.py:347: in _get_response r, session = post_ratelimited( ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/util.py:866: in post_ratelimited protocol.retry_policy.raise_response_errors(r) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = response = def raise_response_errors(self, response): if response.status_code == 200: # Response is OK return if response.status_code == 500 and response.content and is_xml(response.content): # Some genius at Microsoft thinks it's OK to send a valid SOAP response as an HTTP 500 log.debug("Got status code %s but trying to parse content anyway", response.status_code) return cas_error = response.headers.get("X-CasErrorCode") if cas_error: if cas_error.startswith("CAS error:"): # Remove unnecessary text cas_error = cas_error.split(":", 1)[1].strip() raise CASError(cas_error=cas_error, response=response) if response.status_code == 500 and ( b"The specified server version is invalid" in response.content or b"ErrorInvalidSchemaVersionForMailboxVersion" in response.content ): # Another way of communicating invalid schema versions raise ErrorInvalidSchemaVersionForMailboxVersion("Invalid server version") if response.headers.get("connection") == "close": # Connection closed. OK to retry. raise ErrorServerBusy("Caused by closed connection") if ( response.status_code == 302 and response.headers.get("location", "").lower() == "/ews/genericerrorpage.htm?aspxerrorpath=/ews/exchange.asmx" ): # Redirect to genericerrorpage.htm is ridiculous behaviour for random outages. OK to retry. # # Redirect to '/internalsite/internalerror.asp' or '/internalsite/initparams.aspx' is caused by e.g. TLS # certificate f*ckups on the Exchange server. We should not retry those. raise ErrorInternalServerTransientError(f"Caused by HTTP 302 redirect to {response.headers['location']}") if response.status_code in (301, 302): try: redirect_url = get_redirect_url(response=response, allow_relative=False) except RelativeRedirect as e: log.debug("Redirect not allowed but we were relative redirected (%s -> %s)", response.url, e.value) raise RedirectError(url=e.value) log.debug("Redirect not allowed but we were redirected ( (%s -> %s)", response.url, redirect_url) raise RedirectError(url=redirect_url) if b"The referenced account is currently locked out" in response.content: raise UnauthorizedError("The referenced account is currently locked out") if response.status_code == 401 and self.fail_fast: # This is a login failure > raise UnauthorizedError(f"Invalid credentials for {response.url}") E exchangelib.errors.UnauthorizedError: Invalid credentials for https://outlook.office365.com/EWS/Exchange.asmx ../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/protocol.py:727: UnauthorizedError ------------------------------ Captured log call ------------------------------- 2025-01-15 18:17:49 - DEBUG - tzlocal - _get_localzone_name:123 - /etc/localtime found 2025-01-15 18:17:49 - DEBUG - tzlocal - _get_localzone_name:139 - 1 found: {'/etc/localtime is a symlink to': 'Iceland'} 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - __call__:408 - Waiting for _protocol_cache_lock 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - __call__:421 - Protocol __call__ cache miss. Adding key '('https://outlook.office365.com/EWS/Exchange.asmx', None)' 2025-01-15 18:17:49 - DEBUG - exchangelib.account - __init__:206 - Added account: user@example.com 2025-01-15 18:17:49 - INFO - tests.conftest - test_exchange_connection:29 - my_account: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_consume_item_service', 'access_type', 'ad_response', 'admin_audit_logs', 'affinity_cookie', 'archive_deleted_items', 'archive_inbox', 'archive_msg_folder_root', 'archive_recoverable_items_deletions', 'archive_recoverable_items_purges', 'archive_recoverable_items_root', 'archive_recoverable_items_versions', 'archive_root', 'bulk_archive', 'bulk_copy', 'bulk_create', 'bulk_delete', 'bulk_mark_as_junk', 'bulk_move', 'bulk_send', 'bulk_update', 'calendar', 'conflicts', 'contacts', 'conversation_history', 'create_rule', 'default_timezone', 'delegates', 'delete_rule', 'directory', 'domain', 'drafts', 'export', 'favorites', 'fetch', 'fetch_personas', 'fullname', 'identity', 'im_contact_list', 'inbox', 'journal', 'junk', 'local_failures', 'locale', 'mail_tips', 'msg_folder_root', 'my_contacts', 'notes', 'oof_settings', 'outbox', 'people_connect', 'primary_smtp_address', 'protocol', 'public_folders_root', 'pull_subscription', 'push_subscription', 'quick_contacts', 'recipient_cache', 'recoverable_items_deletions', 'recoverable_items_purges', 'recoverable_items_root', 'recoverable_items_versions', 'root', 'rules', 'search_folders', 'sent', 'server_failures', 'set_rule', 'streaming_subscription', 'subscribe_to_pull', 'subscribe_to_push', 'subscribe_to_streaming', 'sync_issues', 'tasks', 'todo_search', 'trash', 'unsubscribe', 'upload', 'version', 'voice_mail'] 2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _chunked_get_elements:278 - Processing chunk 1 containing 1 items 2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _get_response_xml:393 - Calling service GetFolder 2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _get_response_xml:395 - Trying API version Exchange2016 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - increase_poolsize:206 - Server outlook.office365.com: Increasing session pool size from 0 to 1 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - create_session:330 - Server outlook.office365.com: Created session 84582 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - get_session:249 - Server outlook.office365.com: Waiting for session 2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - get_session:255 - Server outlook.office365.com: Got session 84582 2025-01-15 18:17:49 - DEBUG - exchangelib.util - post_ratelimited:811 - Session 84582 thread 140214710770560 timeout 120: POST'ing to https://outlook.office365.com/EWS/Exchange.asmx after 0s sleep 2025-01-15 18:17:49 - DEBUG - urllib3.connectionpool - _new_conn:1049 - Starting new HTTPS connection (1): outlook.office365.com:443 2025-01-15 18:17:50 - DEBUG - urllib3.connectionpool - _make_request:544 - https://outlook.office365.com:443 "POST /EWS/Exchange.asmx HTTP/1.1" 401 0 2025-01-15 18:17:50 - DEBUG - exchangelib.util - post_ratelimited:862 - Timeout: 120 Session: 84582 Thread: 140214710770560 Auth type: None URL: https://outlook.office365.com/EWS/Exchange.asmx HTTP adapter: Streaming: False Response time: 0.23992178899788996 Status code: 401 Request headers: {'User-Agent': 'exchangelib/5.5.0 (python-requests/2.32.3)', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'text/xml; charset=utf-8', 'X-AnchorMailbox': 'user@example.com', 'Content-Length': '1214'} Response headers: {'Content-Length': '0', 'Server': 'Microsoft-HTTPAPI/2.0', 'X-BEServer': 'AS8PR08MB6357', 'X-NanoProxy': '1,1', 'Request-Id': '3d6befc1-e20e-e7b9-1ac1-75aae8bdd77b', 'X-CalculatedFETarget': 'AS9PR07CU001.internal.outlook.com', 'X-BeSku': 'WCS6', 'X-BackEndHttpStatus': '401,401', 'BasicChallengeAdded': 'True', 'MS-CV': 'we9rPQ7iuecawXWq6L3Xew.1.1', 'X-CalculatedBETarget': 'AS8PR08MB6357.eurprd08.prod.outlook.com', 'X-DiagInfo': 'AS8PR08MB6357', 'X-FEEFZInfo': 'DHR', 'X-UserType': 'Business', 'X-FEProxyInfo': 'AS9PR07CA0001', 'X-FEServer': 'DUZPR01CA0137', 'X-Proxy-BackendServerStatus': '401', 'X-Proxy-RoutingCorrectness': '1', 'X-RUM-NotUpdateQueriedPath': '1', 'X-RUM-NotUpdateQueriedDbCopy': '1', 'X-RUM-Validated': '1', 'X-FirstHopCafeEFZ': 'DUB', 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', 'Set-Cookie': 'exchangecookie=a7a24814fef441248023763bc5787ec0; expires=Thu, 15-Jan-2026 18:17:50 GMT; path=/; secure; HttpOnly', 'WWW-Authenticate': 'Basic Realm=""', 'Date': 'Wed, 15 Jan 2025 18:17:49 GMT'} 2025-01-15 18:17:50 - DEBUG - exchangelib.util.xml - post_ratelimited:863 - Request XML: b'\nIdOnlyuser@example.comSMTPMailbox' Response XML: b'' 2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - retire_session:279 - Server outlook.office365.com: Retiring session 84582 2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - create_session:330 - Server outlook.office365.com: Created session 79172 2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - release_session:261 - Server outlook.office365.com: Releasing session 79172 =========================== short test summary info ============================ FAILED tests/test_exchange_cba.py::test_exchange_connection - exchangelib.err... !!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! ============================== 1 failed in 0.78s ===============================
Metadata
Metadata
Assignees
Labels
No labels