Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-msal for openSUSE:Factory 
checked in at 2024-10-08 17:24:47
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-msal (Old)
 and      /work/SRC/openSUSE:Factory/.python-msal.new.19354 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-msal"

Tue Oct  8 17:24:47 2024 rev:24 rq:1206228 version:1.31.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-msal/python-msal.changes  2024-08-01 
22:04:46.329314831 +0200
+++ /work/SRC/openSUSE:Factory/.python-msal.new.19354/python-msal.changes       
2024-10-08 17:25:54.364778092 +0200
@@ -1,0 +2,11 @@
+Wed Oct  2 09:57:49 UTC 2024 - John Paul Adrian Glaubitz 
<adrian.glaub...@suse.com>
+
+- Update to version 1.31.0
+  * Integration with Broker-on-Mac in (#596)
+  * Change Managed Identity detection logic on Arc in (#731)
+  * Managed Identity supports CAE in (#730)
+  * Support Managed Identity on Azure Container
+    Instance (ACI) with Resource id in (#741)
+  * Other refactoring in (#740)
+
+-------------------------------------------------------------------

Old:
----
  msal-1.30.0.tar.gz

New:
----
  msal-1.31.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-msal.spec ++++++
--- /var/tmp/diff_new_pack.jfPE7h/_old  2024-10-08 17:25:54.788795788 +0200
+++ /var/tmp/diff_new_pack.jfPE7h/_new  2024-10-08 17:25:54.788795788 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-msal
-Version:        1.30.0
+Version:        1.31.0
 Release:        0
 Summary:        Microsoft Authentication Library (MSAL) for Python
 License:        MIT

++++++ msal-1.30.0.tar.gz -> msal-1.31.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/PKG-INFO new/msal-1.31.0/PKG-INFO
--- old/msal-1.30.0/PKG-INFO    2024-07-17 06:01:39.363946200 +0200
+++ new/msal-1.31.0/PKG-INFO    2024-09-07 00:20:40.805118800 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: msal
-Version: 1.30.0
+Version: 1.31.0
 Summary: The Microsoft Authentication Library (MSAL) for Python library 
enables your app to access the Microsoft Cloud by supporting authentication of 
users with Microsoft Azure Active Directory accounts (AAD) and Microsoft 
Accounts (MSA) using industry standard OAuth2 and OpenID Connect.
 Home-page: 
https://github.com/AzureAD/microsoft-authentication-library-for-python
 Author: Microsoft Corporation
@@ -27,9 +27,10 @@
 License-File: LICENSE
 Requires-Dist: requests<3,>=2.0.0
 Requires-Dist: PyJWT[crypto]<3,>=1.0.0
-Requires-Dist: cryptography<45,>=2.5
+Requires-Dist: cryptography<46,>=2.5
 Provides-Extra: broker
-Requires-Dist: pymsalruntime<0.17,>=0.13.2; (python_version >= "3.6" and 
platform_system == "Windows") and extra == "broker"
+Requires-Dist: pymsalruntime<0.18,>=0.14; (python_version >= "3.6" and 
platform_system == "Windows") and extra == "broker"
+Requires-Dist: pymsalruntime<0.18,>=0.17; (python_version >= "3.8" and 
platform_system == "Darwin") and extra == "broker"
 
 # Microsoft Authentication Library (MSAL) for Python
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/msal/__main__.py 
new/msal-1.31.0/msal/__main__.py
--- old/msal-1.30.0/msal/__main__.py    2024-07-17 06:01:34.000000000 +0200
+++ new/msal-1.31.0/msal/__main__.py    2024-09-07 00:20:35.000000000 +0200
@@ -299,6 +299,7 @@
         authority=authority,
         instance_discovery=instance_discovery,
         enable_broker_on_windows=enable_broker,
+        enable_broker_on_mac=enable_broker,
         enable_pii_log=enable_pii_log,
         token_cache=global_cache,
         ) if not is_cca else msal.ConfidentialClientApplication(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/msal/application.py 
new/msal-1.31.0/msal/application.py
--- old/msal-1.30.0/msal/application.py 2024-07-17 06:01:34.000000000 +0200
+++ new/msal-1.31.0/msal/application.py 2024-09-07 00:20:35.000000000 +0200
@@ -21,11 +21,16 @@
 
 
 # The __init__.py will import this. Not the other way around.
-__version__ = "1.30.0"  # When releasing, also check and bump our 
dependencies's versions if needed
+__version__ = "1.31.0"  # When releasing, also check and bump our 
dependencies's versions if needed
 
 logger = logging.getLogger(__name__)
 _AUTHORITY_TYPE_CLOUDSHELL = "CLOUDSHELL"
 
+def _init_broker(enable_pii_log):  # Make it a function to allow mocking
+    from . import broker  # Trigger Broker's initialization, lazily
+    if enable_pii_log:
+        broker._enable_pii_log()
+
 def extract_certs(public_cert_content):
     # Parses raw public certificate file contents and returns a list of strings
     # Usage: headers = {"x5c": extract_certs(open("my_cert.pem").read())}
@@ -411,9 +416,11 @@
             (STS) what this client is capable for,
             so STS can decide to turn on certain features.
             For example, if client is capable to handle *claims challenge*,
-            STS can then issue CAE access tokens to resources
-            knowing when the resource emits *claims challenge*
-            the client will be capable to handle.
+            STS may issue
+            `Continuous Access Evaluation (CAE) 
<https://learn.microsoft.com/entra/identity/conditional-access/concept-continuous-access-evaluation>`_
+            access tokens to resources,
+            knowing that when the resource emits a *claims challenge*
+            the client will be able to handle those challenges.
 
             Implementation details:
             Client capability is implemented using "claims" parameter on the 
wire,
@@ -638,20 +645,28 @@
         if allow_broker:
             warnings.warn(
                 "allow_broker is deprecated. "
-                "Please use PublicClientApplication(..., 
enable_broker_on_windows=True)",
+                "Please use PublicClientApplication(..., "
+                "enable_broker_on_windows=True, "
+                "enable_broker_on_mac=...)",
                 DeprecationWarning)
-        self._enable_broker = self._enable_broker or (
+        opted_in_for_broker = (
+            self._enable_broker  # True means Opted-in from PCA
+            or (
             # When we started the broker project on Windows platform,
             # the allow_broker was meant to be cross-platform. Now we realize
             # that other platforms have different redirect_uri requirements,
             # so the old allow_broker is deprecated and will only for Windows.
             allow_broker and sys.platform == "win32")
-        if (self._enable_broker and not is_confidential_app
-                and not self.authority.is_adfs and not self.authority._is_b2c):
+        )
+        self._enable_broker = (  # This same variable will also store the state
+            opted_in_for_broker
+            and not is_confidential_app
+            and not self.authority.is_adfs
+            and not self.authority._is_b2c
+        )
+        if self._enable_broker:
             try:
-                from . import broker  # Trigger Broker's initialization
-                if enable_pii_log:
-                    broker._enable_pii_log()
+                _init_broker(enable_pii_log)
             except RuntimeError:
                 self._enable_broker = False
                 logger.exception(
@@ -1879,7 +1894,7 @@
 
         .. note::
 
-            You may set enable_broker_on_windows to True.
+            You may set enable_broker_on_windows and/or enable_broker_on_mac 
to True.
 
             **What is a broker, and why use it?**
 
@@ -1905,9 +1920,11 @@
 
                * ``ms-appx-web://Microsoft.AAD.BrokerPlugin/your_client_id``
                  if your app is expected to run on Windows 10+
+               * ``msauth.com.msauth.unsignedapp://auth``
+                 if your app is expected to run on Mac
 
             2. installed broker dependency,
-               e.g. ``pip install msal[broker]>=1.25,<2``.
+               e.g. ``pip install msal[broker]>=1.31,<2``.
 
             3. tested with ``acquire_token_interactive()`` and 
``acquire_token_silent()``.
 
@@ -1939,12 +1956,21 @@
             This parameter defaults to None, which means MSAL will not utilize 
a broker.
 
             New in MSAL Python 1.25.0.
+
+        :param boolean enable_broker_on_mac:
+            This setting is only effective if your app is running on Mac.
+            This parameter defaults to None, which means MSAL will not utilize 
a broker.
+
+            New in MSAL Python 1.31.0.
         """
         if client_credential is not None:
             raise ValueError("Public Client should not possess credentials")
         # Using kwargs notation for now. We will switch to keyword-only 
arguments.
         enable_broker_on_windows = kwargs.pop("enable_broker_on_windows", 
False)
-        self._enable_broker = enable_broker_on_windows and sys.platform == 
"win32"
+        enable_broker_on_mac = kwargs.pop("enable_broker_on_mac", False)
+        self._enable_broker = bool(
+            enable_broker_on_windows and sys.platform == "win32"
+            or enable_broker_on_mac and sys.platform == "darwin")
         super(PublicClientApplication, self).__init__(
             client_id, client_credential=None, **kwargs)
 
@@ -2022,14 +2048,22 @@
             New in version 1.15.
 
         :param int parent_window_handle:
-            Required if your app is running on Windows and opted in to use 
broker.
+            OPTIONAL.
+
+            * If your app does not opt in to use broker,
+              you do not need to provide a ``parent_window_handle`` here.
+
+            * If your app opts in to use broker,
+              ``parent_window_handle`` is required.
 
-            If your app is a GUI app,
-            you are recommended to also provide its window handle,
-            so that the sign in UI window will properly pop up on top of your 
window.
+              - If your app is a GUI app running on Windows or Mac system,
+                you are required to also provide its window handle,
+                so that the sign-in window will pop up on top of your window.
+              - If your app is a console app running on Windows or Mac system,
+                you can use a placeholder
+                ``PublicClientApplication.CONSOLE_WINDOW_HANDLE``.
 
-            If your app is a console app (most Python scripts are console 
apps),
-            you can use a placeholder value 
``msal.PublicClientApplication.CONSOLE_WINDOW_HANDLE``.
+            Most Python scripts are console apps.
 
             New in version 1.20.0.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/msal/broker.py 
new/msal-1.31.0/msal/broker.py
--- old/msal-1.30.0/msal/broker.py      2024-07-17 06:01:34.000000000 +0200
+++ new/msal-1.31.0/msal/broker.py      2024-09-07 00:20:35.000000000 +0200
@@ -1,9 +1,9 @@
 """This module is an adaptor to the underlying broker.
 It relies on PyMsalRuntime which is the package providing broker's 
functionality.
 """
-from threading import Event
 import json
 import logging
+import sys
 import time
 import uuid
 
@@ -35,14 +35,12 @@
     pass
 
 
-class _CallbackData:
-    def __init__(self):
-        self.signal = Event()
-        self.result = None
-
-    def complete(self, result):
-        self.signal.set()
-        self.result = result
+_redirect_uri_on_mac = "msauth.com.msauth.unsignedapp://auth"  # Note:
+    # On Mac, the native Python has a team_id which links to bundle id
+    # com.apple.python3 however it won't give Python scripts better security.
+    # Besides, the homebrew-installed Pythons have no team_id
+    # so they have to use a generic placeholder anyway.
+    # The v-team chose to combine two situations into using same placeholder.
 
 
 def _convert_error(error, client_id):
@@ -52,8 +50,9 @@
             or "AADSTS7000218" in context  # This "request body must contain 
... client_secret" is just a symptom of current app has no WAM redirect_uri
             ):
         raise RedirectUriError(  # This would be seen by either the app 
developer or end user
-            "MsalRuntime won't work unless this one more redirect_uri is 
registered to current app: "
-            "ms-appx-web://Microsoft.AAD.BrokerPlugin/{}".format(client_id))
+            "MsalRuntime needs the current app to register these redirect_uri "
+            "(1) ms-appx-web://Microsoft.AAD.BrokerPlugin/{} (2) {}".format(
+            client_id, _redirect_uri_on_mac))
         # OTOH, AAD would emit other errors when other error handling branch 
was hit first,
         # so, the AADSTS50011/RedirectUriError is not guaranteed to happen.
     return {
@@ -70,8 +69,8 @@
 
 
 def _read_account_by_id(account_id, correlation_id):
-    """Return an instance of MSALRuntimeAccount, or log error and return 
None"""
-    callback_data = _CallbackData()
+    """Return an instance of MSALRuntimeError or MSALRuntimeAccount, or None"""
+    callback_data = pymsalruntime.CallbackData()
     pymsalruntime.read_account_by_id(
         account_id,
         correlation_id,
@@ -142,7 +141,7 @@
         params.set_pop_params(
             auth_scheme._http_method, auth_scheme._url.netloc, 
auth_scheme._url.path,
             auth_scheme._nonce)
-    callback_data = _CallbackData()
+    callback_data = pymsalruntime.CallbackData()
     for k, v in kwargs.items():  # This can be used to support domain_hint, 
max_age, etc.
         if v is not None:
             params.set_additional_parameter(k, str(v))
@@ -169,9 +168,12 @@
         **kwargs):
     params = pymsalruntime.MSALRuntimeAuthParameters(client_id, authority)
     params.set_requested_scopes(scopes)
-    
params.set_redirect_uri("https://login.microsoftonline.com/common/oauth2/nativeclient";)
-        # This default redirect_uri value is not currently used by the broker
+    params.set_redirect_uri(
+        _redirect_uri_on_mac if sys.platform == "darwin" else
+        "https://login.microsoftonline.com/common/oauth2/nativeclient";
+        # This default redirect_uri value is not currently used by WAM
         # but it is required by the MSAL.cpp to be set to a non-empty valid 
URI.
+    )
     if prompt:
         if prompt == "select_account":
             if login_hint:
@@ -198,7 +200,7 @@
             params.set_additional_parameter(k, str(v))
     if claims:
         params.set_decoded_claims(claims)
-    callback_data = _CallbackData()
+    callback_data = pymsalruntime.CallbackData(is_interactive=True)
     pymsalruntime.signin_interactively(
         parent_window_handle or pymsalruntime.get_console_window() or 
pymsalruntime.get_desktop_window(),  # Since pymsalruntime 0.2+
         params,
@@ -231,7 +233,7 @@
     for k, v in kwargs.items():  # This can be used to support domain_hint, 
max_age, etc.
         if v is not None:
             params.set_additional_parameter(k, str(v))
-    callback_data = _CallbackData()
+    callback_data = pymsalruntime.CallbackData()
     pymsalruntime.acquire_token_silently(
         params,
         correlation_id,
@@ -247,7 +249,7 @@
     account = _read_account_by_id(account_id, correlation_id)
     if account is None:
         return
-    callback_data = _CallbackData()
+    callback_data = pymsalruntime.CallbackData()
     pymsalruntime.signout_silently(  # New in PyMsalRuntime 0.7
         client_id,
         correlation_id,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/msal/managed_identity.py 
new/msal-1.31.0/msal/managed_identity.py
--- old/msal-1.30.0/msal/managed_identity.py    2024-07-17 06:01:34.000000000 
+0200
+++ new/msal-1.31.0/msal/managed_identity.py    2024-09-07 00:20:35.000000000 
+0200
@@ -10,7 +10,7 @@
 import time
 from urllib.parse import urlparse  # Python 3+
 from collections import UserDict  # Python 3+
-from typing import Union  # Needed in Python 3.7 & 3.8
+from typing import Optional, Union  # Needed in Python 3.7 & 3.8
 from .token_cache import TokenCache
 from .individual_cache import _IndividualCache as IndividualCache
 from .throttled_http_client import ThrottledHttpClientBase, RetryAfterParser
@@ -40,14 +40,15 @@
 
     _types_mapping = {  # Maps type name in configuration to type name on wire
         CLIENT_ID: "client_id",
-        RESOURCE_ID: "mi_res_id",
+        RESOURCE_ID: "msi_res_id",  # VM's IMDS prefers msi_res_id 
https://github.com/Azure/azure-rest-api-specs/blob/dba6ed1f03bda88ac6884c0a883246446cc72495/specification/imds/data-plane/Microsoft.InstanceMetadataService/stable/2018-10-01/imds.json#L233-L239
         OBJECT_ID: "object_id",
     }
 
     @classmethod
     def is_managed_identity(cls, unknown):
-        return isinstance(unknown, ManagedIdentity) or (
-            isinstance(unknown, dict) and cls.ID_TYPE in unknown)
+        return (isinstance(unknown, ManagedIdentity)
+            or cls.is_system_assigned(unknown)
+            or cls.is_user_assigned(unknown))
 
     @classmethod
     def is_system_assigned(cls, unknown):
@@ -144,7 +145,10 @@
         (like what a ``PublicClientApplication`` does),
         not a token with application permissions for an app.
     """
-    _instance, _tenant = socket.getfqdn(), "managed_identity"  # Placeholders
+    __instance, _tenant = None, "managed_identity"  # Placeholders
+    _TOKEN_SOURCE = "token_source"
+    _TOKEN_SOURCE_IDP = "identity_provider"
+    _TOKEN_SOURCE_CACHE = "cache"
 
     def __init__(
         self,
@@ -214,6 +218,9 @@
                 )
             token = client.acquire_token_for_client("resource")
         """
+        if not ManagedIdentity.is_managed_identity(managed_identity):
+            raise ManagedIdentityError(
+                f"Incorrect managed_identity: {managed_identity}")
         self._managed_identity = managed_identity
         self._http_client = _ThrottledHttpClient(
             # This class only throttles excess token acquisition requests.
@@ -232,12 +239,36 @@
         )
         self._token_cache = token_cache or TokenCache()
 
-    def acquire_token_for_client(self, *, resource):  # We may support scope 
in the future
+    def _get_instance(self):
+        if self.__instance is None:
+            self.__instance = socket.getfqdn()  # Moved from class definition 
to here
+        return self.__instance
+
+    def acquire_token_for_client(
+        self,
+        *,
+        resource: str,  # If/when we support scope, resource will become 
optional
+        claims_challenge: Optional[str] = None,
+    ):
         """Acquire token for the managed identity.
 
         The result will be automatically cached.
         Subsequent calls will automatically search from cache first.
 
+        :param resource: The resource for which the token is acquired.
+
+        :param claims_challenge:
+            Optional.
+            It is a string representation of a JSON object
+            (which contains lists of claims being requested).
+
+            The tenant admin may choose to revoke all Managed Identity tokens,
+            and then a *claims challenge* will be returned by the target 
resource,
+            as a `claims_challenge` directive in the `www-authenticate` header,
+            even if the app developer did not opt in for the "CP1" client 
capability.
+            Upon receiving a `claims_challenge`, MSAL will skip a token cache 
read,
+            and will attempt to acquire a new token.
+
         .. note::
 
             Known issue: When an Azure VM has only one user-assigned managed 
identity,
@@ -250,19 +281,18 @@
         access_token_from_cache = None
         client_id_in_cache = self._managed_identity.get(
             ManagedIdentity.ID, "SYSTEM_ASSIGNED_MANAGED_IDENTITY")
-        if True:  # Does not offer an "if not force_refresh" option, because
-                  # there would be built-in token cache in the service side 
anyway
+        now = time.time()
+        if not claims_challenge:  # Then attempt token cache search
             matches = self._token_cache.find(
                 self._token_cache.CredentialType.ACCESS_TOKEN,
                 target=[resource],
                 query=dict(
                     client_id=client_id_in_cache,
-                    environment=self._instance,
+                    environment=self._get_instance(),
                     realm=self._tenant,
                     home_account_id=None,
                 ),
             )
-            now = time.time()
             for entry in matches:
                 expires_in = int(entry["expires_on"]) - now
                 if expires_in < 5*60:  # Then consider it expired
@@ -272,6 +302,7 @@
                     "access_token": entry["secret"],
                     "token_type": entry.get("token_type", "Bearer"),
                     "expires_in": int(expires_in),  # OAuth2 specs defines it 
as int
+                    self._TOKEN_SOURCE: self._TOKEN_SOURCE_CACHE,
                 }
                 if "refresh_on" in entry:
                     access_token_from_cache["refresh_on"] = 
int(entry["refresh_on"])
@@ -287,13 +318,15 @@
                 self._token_cache.add(dict(
                     client_id=client_id_in_cache,
                     scope=[resource],
-                    token_endpoint="https://{}/{}".format(self._instance, 
self._tenant),
+                    token_endpoint="https://{}/{}".format(
+                        self._get_instance(), self._tenant),
                     response=result,
                     params={},
                     data={},
                 ))
                 if "refresh_in" in result:
                     result["refresh_on"] = int(now + result["refresh_in"])
+                result[self._TOKEN_SOURCE] = self._TOKEN_SOURCE_IDP
             if (result and "error" not in result) or (not 
access_token_from_cache):
                 return result
         except:  # The exact HTTP exception is transportation-layer dependent
@@ -310,6 +343,17 @@
     return scope  # There is no much else we can do here
 
 
+def _get_arc_endpoint():
+    if "IDENTITY_ENDPOINT" in os.environ and "IMDS_ENDPOINT" in os.environ:
+        return os.environ["IDENTITY_ENDPOINT"]
+    if (  # Defined in 
https://msazure.visualstudio.com/One/_wiki/wikis/One.wiki/233012/VM-Extension-Authoring-for-Arc?anchor=determining-which-endpoint-to-use
+        sys.platform == "linux" and 
os.path.exists("/var/opt/azcmagent/bin/himds")
+        or sys.platform == "win32" and os.path.exists(os.path.expandvars(
+            r"%ProgramFiles%\AzureConnectedMachineAgent\himds.exe"))
+    ):
+        return "http://localhost:40342/metadata/identity/oauth2/token";
+
+
 APP_SERVICE = object()
 AZURE_ARC = object()
 CLOUD_SHELL = object()  # In MSAL Python, token acquisition was done by
@@ -332,7 +376,7 @@
         return APP_SERVICE
     if "MSI_ENDPOINT" in os.environ and "MSI_SECRET" in os.environ:
         return MACHINE_LEARNING
-    if "IDENTITY_ENDPOINT" in os.environ and "IMDS_ENDPOINT" in os.environ:
+    if _get_arc_endpoint():
         return AZURE_ARC
     if _is_running_in_cloud_shell():
         return CLOUD_SHELL
@@ -374,24 +418,21 @@
             managed_identity,
             resource,
         )
-    if "IDENTITY_ENDPOINT" in os.environ and "IMDS_ENDPOINT" in os.environ:
+    arc_endpoint = _get_arc_endpoint()
+    if arc_endpoint:
         if ManagedIdentity.is_user_assigned(managed_identity):
             raise ManagedIdentityError(  # Note: Azure Identity for Python 
raised exception too
                 "Invalid managed_identity parameter. "
                 "Azure Arc supports only system-assigned managed identity, "
                 "See also "
                 
"https://learn.microsoft.com/en-us/azure/service-fabric/configure-existing-cluster-enable-managed-identity-token-service";)
-        return _obtain_token_on_arc(
-            http_client,
-            os.environ["IDENTITY_ENDPOINT"],
-            resource,
-        )
+        return _obtain_token_on_arc(http_client, arc_endpoint, resource)
     return _obtain_token_on_azure_vm(http_client, managed_identity, resource)
 
 
-def _adjust_param(params, managed_identity):
+def _adjust_param(params, managed_identity, types_mapping=None):
     # Modify the params dict in place
-    id_name = ManagedIdentity._types_mapping.get(
+    id_name = (types_mapping or ManagedIdentity._types_mapping).get(
         managed_identity.get(ManagedIdentity.ID_TYPE))
     if id_name:
         params[id_name] = managed_identity[ManagedIdentity.ID]
@@ -418,7 +459,7 @@
                 "resource": payload.get("resource"),
                 "token_type": payload.get("token_type", "Bearer"),
                 }
-        return payload  # Typically an error, but it is undefined in the doc 
above
+        return payload  # It would be {"error": ..., "error_description": ...} 
according to 
https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#error-handling
     except json.decoder.JSONDecodeError:
         logger.debug("IMDS emits unexpected payload: %s", resp.text)
         raise
@@ -438,7 +479,12 @@
         "api-version": "2019-08-01",
         "resource": resource,
         }
-    _adjust_param(params, managed_identity)
+    _adjust_param(params, managed_identity, types_mapping={
+        ManagedIdentity.CLIENT_ID: "client_id",
+        ManagedIdentity.RESOURCE_ID: "mi_res_id",  # App Service's resource id 
uses "mi_res_id"
+        ManagedIdentity.OBJECT_ID: "object_id",
+    })
+
     resp = http_client.get(
         endpoint,
         params=params,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/msal/token_cache.py 
new/msal-1.31.0/msal/token_cache.py
--- old/msal-1.30.0/msal/token_cache.py 2024-07-17 06:01:34.000000000 +0200
+++ new/msal-1.31.0/msal/token_cache.py 2024-09-07 00:20:35.000000000 +0200
@@ -363,11 +363,14 @@
 
     This class does NOT actually persist the cache on disk/db/etc..
     Depending on your need,
-    the following simple recipe for file-based persistence may be sufficient::
+    the following simple recipe for file-based, unencrypted persistence may be 
sufficient::
 
         import os, atexit, msal
         cache_filename = os.path.join(  # Persist cache into this file
-            os.getenv("XDG_RUNTIME_DIR", ""),  # Automatically wipe out the 
cache from Linux when user's ssh session ends. See also 
https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/690
+            os.getenv(
+                # Automatically wipe out the cache from Linux when user's ssh 
session ends.
+                # See also 
https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/690
+                "XDG_RUNTIME_DIR", ""),
             "my_cache.bin")
         cache = msal.SerializableTokenCache()
         if os.path.exists(cache_filename):
@@ -380,6 +383,10 @@
         app = msal.ClientApplication(..., token_cache=cache)
         ...
 
+    Alternatively, you may use a more sophisticated cache persistence library,
+    `MSAL Extensions 
<https://github.com/AzureAD/microsoft-authentication-extensions-for-python>`_,
+    which provides token cache persistence with encryption, and more.
+
     :var bool has_state_changed:
         Indicates whether the cache state in the memory has changed since last
         :func:`~serialize` or :func:`~deserialize` call.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/msal.egg-info/PKG-INFO 
new/msal-1.31.0/msal.egg-info/PKG-INFO
--- old/msal-1.30.0/msal.egg-info/PKG-INFO      2024-07-17 06:01:39.000000000 
+0200
+++ new/msal-1.31.0/msal.egg-info/PKG-INFO      2024-09-07 00:20:40.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: msal
-Version: 1.30.0
+Version: 1.31.0
 Summary: The Microsoft Authentication Library (MSAL) for Python library 
enables your app to access the Microsoft Cloud by supporting authentication of 
users with Microsoft Azure Active Directory accounts (AAD) and Microsoft 
Accounts (MSA) using industry standard OAuth2 and OpenID Connect.
 Home-page: 
https://github.com/AzureAD/microsoft-authentication-library-for-python
 Author: Microsoft Corporation
@@ -27,9 +27,10 @@
 License-File: LICENSE
 Requires-Dist: requests<3,>=2.0.0
 Requires-Dist: PyJWT[crypto]<3,>=1.0.0
-Requires-Dist: cryptography<45,>=2.5
+Requires-Dist: cryptography<46,>=2.5
 Provides-Extra: broker
-Requires-Dist: pymsalruntime<0.17,>=0.13.2; (python_version >= "3.6" and 
platform_system == "Windows") and extra == "broker"
+Requires-Dist: pymsalruntime<0.18,>=0.14; (python_version >= "3.6" and 
platform_system == "Windows") and extra == "broker"
+Requires-Dist: pymsalruntime<0.18,>=0.17; (python_version >= "3.8" and 
platform_system == "Darwin") and extra == "broker"
 
 # Microsoft Authentication Library (MSAL) for Python
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/msal.egg-info/requires.txt 
new/msal-1.31.0/msal.egg-info/requires.txt
--- old/msal-1.30.0/msal.egg-info/requires.txt  2024-07-17 06:01:39.000000000 
+0200
+++ new/msal-1.31.0/msal.egg-info/requires.txt  2024-09-07 00:20:40.000000000 
+0200
@@ -1,8 +1,11 @@
 requests<3,>=2.0.0
 PyJWT[crypto]<3,>=1.0.0
-cryptography<45,>=2.5
+cryptography<46,>=2.5
 
 [broker]
 
 [broker:python_version >= "3.6" and platform_system == "Windows"]
-pymsalruntime<0.17,>=0.13.2
+pymsalruntime<0.18,>=0.14
+
+[broker:python_version >= "3.8" and platform_system == "Darwin"]
+pymsalruntime<0.18,>=0.17
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/setup.cfg new/msal-1.31.0/setup.cfg
--- old/msal-1.30.0/setup.cfg   2024-07-17 06:01:39.363946200 +0200
+++ new/msal-1.31.0/setup.cfg   2024-09-07 00:20:40.805118800 +0200
@@ -39,11 +39,12 @@
        
        PyJWT[crypto]>=1.0.0,<3
        
-       cryptography>=2.5,<45
+       cryptography>=2.5,<46
 
 [options.extras_require]
 broker = 
-       pymsalruntime>=0.13.2,<0.17; python_version>='3.6' and 
platform_system=='Windows'
+       pymsalruntime>=0.14,<0.18; python_version>='3.6' and 
platform_system=='Windows'
+       pymsalruntime>=0.17,<0.18; python_version>='3.8' and 
platform_system=='Darwin'
 
 [options.packages.find]
 exclude = 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/tests/test_application.py 
new/msal-1.31.0/tests/test_application.py
--- old/msal-1.30.0/tests/test_application.py   2024-07-17 06:01:34.000000000 
+0200
+++ new/msal-1.31.0/tests/test_application.py   2024-09-07 00:20:35.000000000 
+0200
@@ -1,11 +1,16 @@
 # Note: Since Aug 2019 we move all e2e tests into test_e2e.py,
 # so this test_application file contains only unit tests without dependency.
+import json
+import logging
 import sys
 import time
-from msal.application import *
-from msal.application import _str2bytes
+from unittest.mock import patch, Mock
 import msal
-from msal.application import _merge_claims_challenge_and_capabilities
+from msal.application import (
+    extract_certs,
+    ClientApplication, PublicClientApplication, ConfidentialClientApplication,
+    _str2bytes, _merge_claims_challenge_and_capabilities,
+)
 from tests import unittest
 from tests.test_token_cache import build_id_token, build_response
 from tests.http_client import MinimalHttpClient, MinimalResponse
@@ -722,3 +727,63 @@
         self._test_client_id_should_be_a_valid_scope("client_id", [])
         self._test_client_id_should_be_a_valid_scope("client_id", ["foo"])
 
+
+@patch("sys.platform", new="darwin")  # Pretend running on Mac.
+@patch("msal.authority.tenant_discovery", new=Mock(return_value={
+    "authorization_endpoint": "https://contoso.com/placeholder";,
+    "token_endpoint": "https://contoso.com/placeholder";,
+    }))
+@patch("msal.application._init_broker", new=Mock())  # Allow testing without 
pymsalruntime
+class TestBrokerFallback(unittest.TestCase):
+
+    def test_broker_should_be_disabled_by_default(self):
+        app = msal.PublicClientApplication(
+            "client_id",
+            authority="https://login.microsoftonline.com/common";,
+            )
+        self.assertFalse(app._enable_broker)
+
+    def test_broker_should_be_enabled_when_opted_in(self):
+        app = msal.PublicClientApplication(
+            "client_id",
+            authority="https://login.microsoftonline.com/common";,
+            enable_broker_on_mac=True,
+            )
+        self.assertTrue(app._enable_broker)
+
+    def test_should_fallback_to_non_broker_when_using_adfs(self):
+        app = msal.PublicClientApplication(
+            "client_id",
+            authority="https://contoso.com/adfs";,
+            #instance_discovery=False,  # Automatically skipped when detected 
ADFS
+            enable_broker_on_mac=True,
+            )
+        self.assertFalse(app._enable_broker)
+
+    def test_should_fallback_to_non_broker_when_using_b2c(self):
+        app = msal.PublicClientApplication(
+            "client_id",
+            authority="https://contoso.b2clogin.com/contoso/policy";,
+            #instance_discovery=False,  # Automatically skipped when detected 
B2C
+            enable_broker_on_mac=True,
+            )
+        self.assertFalse(app._enable_broker)
+
+    def test_should_use_broker_when_disabling_instance_discovery(self):
+        app = msal.PublicClientApplication(
+            "client_id",
+            authority="https://contoso.com/path";,
+            instance_discovery=False,  # Need this for a generic authority url
+            enable_broker_on_mac=True,
+            )
+        # TODO: Shall we bypass broker when opted out of instance discovery?
+        self.assertTrue(app._enable_broker)  # Current implementation enables 
broker
+
+    def test_should_fallback_to_non_broker_when_using_oidc_authority(self):
+        app = msal.PublicClientApplication(
+            "client_id",
+            oidc_authority="https://contoso.com/path";,
+            enable_broker_on_mac=True,
+            )
+        self.assertFalse(app._enable_broker)
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/tests/test_e2e.py 
new/msal-1.31.0/tests/test_e2e.py
--- old/msal-1.30.0/tests/test_e2e.py   2024-07-17 06:01:34.000000000 +0200
+++ new/msal-1.31.0/tests/test_e2e.py   2024-09-07 00:20:35.000000000 +0200
@@ -16,6 +16,7 @@
 import json
 import time
 import unittest
+from urllib.parse import urlparse, parse_qs
 import sys
 try:
     from unittest.mock import patch, ANY
@@ -171,6 +172,7 @@
             client_id,
             client_credential=None,
             authority="https://login.microsoftonline.com/common";,
+            oidc_authority=None,
             scopes=["https://graph.microsoft.com/.default";],  # Microsoft Graph
             http_client=None,
             azure_region=None,
@@ -180,6 +182,7 @@
                 client_id,
                 client_credential=client_credential,
                 authority=authority,
+                oidc_authority=oidc_authority,
                 azure_region=azure_region,
                 http_client=http_client or MinimalHttpClient(),
             )
@@ -193,20 +196,24 @@
             return msal.PublicClientApplication(
                 client_id,
                 authority=authority,
+                oidc_authority=oidc_authority,
                 http_client=http_client or MinimalHttpClient(),
                 enable_broker_on_windows=broker_available,
+                enable_broker_on_mac=broker_available,
                 )
 
     def _test_username_password(self,
             authority=None, client_id=None, username=None, password=None, 
scope=None,
+            oidc_authority=None,
             client_secret=None,  # Since MSAL 1.11, confidential client has 
ROPC too
             azure_region=None,
             http_client=None,
             auth_scheme=None,
             **ignored):
-        assert authority and client_id and username and password and scope
+        assert client_id and username and password and scope and (
+            authority or oidc_authority)
         self.app = self._build_app(
-            client_id, authority=authority,
+            client_id, authority=authority, oidc_authority=oidc_authority,
             http_client=http_client,
             azure_region=azure_region,  # Regional endpoint does not support 
ROPC.
                 # Here we just use it to test a regional app won't break ROPC.
@@ -227,9 +234,14 @@
         os.getenv("TRAVIS"),  # It is set when running on TravisCI or Github 
Actions
         "Although it is doable, we still choose to skip device flow to save 
time")
     def _test_device_flow(
-            self, client_id=None, authority=None, scope=None, **ignored):
-        assert client_id and authority and scope
-        self.app = self._build_app(client_id, authority=authority)
+        self,
+        *,
+        client_id=None, authority=None, oidc_authority=None, scope=None,
+        **ignored
+    ):
+        assert client_id and scope and (authority or oidc_authority)
+        self.app = self._build_app(
+            client_id, authority=authority, oidc_authority=oidc_authority)
         flow = self.app.initiate_device_flow(scopes=scope)
         assert "user_code" in flow, "DF does not seem to be provisioned: 
%s".format(
             json.dumps(flow, indent=4))
@@ -253,7 +265,8 @@
 
     @unittest.skipIf(os.getenv("TRAVIS"), "Browser automation is not yet 
implemented")
     def _test_acquire_token_interactive(
-            self, client_id=None, authority=None, scope=None, port=None,
+            self, *, client_id=None, authority=None, scope=None, port=None,
+            oidc_authority=None,
             username=None, lab_name=None,
             username_uri="",  # Unnecessary if you provided username and 
lab_name
             data=None,  # Needed by ssh-cert feature
@@ -261,8 +274,9 @@
             enable_msa_passthrough=None,
             auth_scheme=None,
             **ignored):
-        assert client_id and authority and scope
-        self.app = self._build_app(client_id, authority=authority)
+        assert client_id and scope and (authority or oidc_authority)
+        self.app = self._build_app(
+            client_id, authority=authority, oidc_authority=oidc_authority)
         logger.info(_get_hint(  # Useful when testing broker which shows no 
welcome_template
             username=username, lab_name=lab_name, username_uri=username_uri))
         result = self.app.acquire_token_interactive(
@@ -680,10 +694,13 @@
 
     def _test_acquire_token_by_client_secret(
             self, client_id=None, client_secret=None, authority=None, 
scope=None,
+            oidc_authority=None,
             **ignored):
-        assert client_id and client_secret and authority and scope
+        assert client_id and client_secret and scope and (
+            authority or oidc_authority)
         self.app = msal.ConfidentialClientApplication(
             client_id, client_credential=client_secret, authority=authority,
+            oidc_authority=oidc_authority,
             http_client=MinimalHttpClient())
         result = self.app.acquire_token_for_client(scope)
         self.assertIsNotNone(result.get("access_token"), "Got %s instead" % 
result)
@@ -1004,14 +1021,18 @@
     @classmethod
     def setUpClass(cls):
         super(CiamTestCase, cls).setUpClass()
-        cls.user = cls.get_lab_user(federationProvider="ciam")
+        cls.user = cls.get_lab_user(
+            #federationProvider="ciam",  # This line would return ciam2 tenant
+            federationProvider="ciamcud", signinAudience="AzureAdMyOrg",  # 
ciam6
+        )
         # FYI: Only single- or multi-tenant CIAM app can have other-than-OIDC
         # delegated permissions on Microsoft Graph.
         cls.app_config = cls.get_lab_app_object(cls.user["client_id"])
 
     def test_ciam_acquire_token_interactive(self):
         self._test_acquire_token_interactive(
-            authority=self.app_config["authority"],
+            authority=self.app_config.get("authority"),
+            oidc_authority=self.app_config.get("oidc_authority"),
             client_id=self.app_config["appId"],
             scope=self.app_config["scopes"],
             username=self.user["username"],
@@ -1019,13 +1040,18 @@
             )
 
     def test_ciam_acquire_token_for_client(self):
+        raw_url = self.app_config["clientSecret"]
+        secret_url = urlparse(raw_url)
+        if secret_url.query:  # Ciam2 era has a query param Secret=name
+            secret_name = parse_qs(secret_url.query)["Secret"][0]
+        else:  # Ciam6 era has a URL path that ends with the secret name
+            secret_name = secret_url.path.split("/")[-1]
+        logger.info('Detected secret name "%s" from "%s"', secret_name, 
raw_url)
         self._test_acquire_token_by_client_secret(
             client_id=self.app_config["appId"],
-            client_secret=self.get_lab_user_secret(
-                self.app_config["clientSecret"].split("=")[-1]),
-            authority=self.app_config["authority"],
-            #scope=["{}/.default".format(self.app_config["appId"])],  # 
AADSTS500207: The account type can't be used for the resource you're trying to 
access.
-            #scope=["api://{}/.default".format(self.app_config["appId"])],  # 
AADSTS500011: The resource principal named 
api://ced781e7-bdb0-4c99-855c-d3bacddea88a was not found in the tenant named 
MSIDLABCIAM2. This can happen if the application has not been installed by the 
administrator of the tenant or consented to by any user in the tenant. You 
might have sent your authentication request to the wrong tenant.
+            client_secret=self.get_lab_user_secret(secret_name),
+            authority=self.app_config.get("authority"),
+            oidc_authority=self.app_config.get("oidc_authority"),
             scope=self.app_config["scopes"],  # It shall ends with "/.default"
             )
 
@@ -1038,21 +1064,35 @@
         # and enabling "Allow public client flows".
         # Otherwise it would hit AADSTS7000218.
         self._test_username_password(
-            authority=self.app_config["authority"],
+            authority=self.app_config.get("authority"),
+            oidc_authority=self.app_config.get("oidc_authority"),
             client_id=self.app_config["appId"],
             username=self.user["username"],
             password=self.get_lab_user_secret(self.user["lab_name"]),
             scope=self.app_config["scopes"],
             )
 
+    @unittest.skip("""As of Aug 2024, in both ciam2 and ciam6, sign-in fails 
with
+AADSTS500208: The domain is not a valid login domain for the account type.""")
     def test_ciam_device_flow(self):
         self._test_device_flow(
-            authority=self.app_config["authority"],
+            authority=self.app_config.get("authority"),
+            oidc_authority=self.app_config.get("oidc_authority"),
             client_id=self.app_config["appId"],
             scope=self.app_config["scopes"],
             )
 
 
+class CiamCudTestCase(CiamTestCase):
+    @classmethod
+    def setUpClass(cls):
+        super(CiamCudTestCase, cls).setUpClass()
+        cls.app_config["authority"] = None
+        cls.app_config["oidc_authority"] = (
+            # Derived from 
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.63.0/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/CiamIntegrationTests.cs#L156
+            
"https://login.msidlabsciam.com/fe362aec-5d43-45d1-b730-9755e60dc3b9/v2.0";)
+
+
 class WorldWideRegionalEndpointTestCase(LabBasedTestCase):
     region = "westus"
     timeout = 2  # Short timeout makes this test case responsive on non-VM
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/msal-1.30.0/tests/test_mi.py 
new/msal-1.31.0/tests/test_mi.py
--- old/msal-1.30.0/tests/test_mi.py    2024-07-17 06:01:34.000000000 +0200
+++ new/msal-1.31.0/tests/test_mi.py    2024-09-07 00:20:35.000000000 +0200
@@ -61,6 +61,14 @@
             http_client=requests.Session(),
             )
 
+    def test_error_out_on_invalid_input(self):
+        with self.assertRaises(ManagedIdentityError):
+            ManagedIdentityClient({"foo": "bar"}, 
http_client=requests.Session())
+        with self.assertRaises(ManagedIdentityError):
+            ManagedIdentityClient(
+                {"ManagedIdentityIdType": "undefined", "Id": "foo"},
+                http_client=requests.Session())
+
     def assertCacheStatus(self, app):
         cache = app._token_cache._cache
         self.assertEqual(1, len(cache.get("AccessToken", [])), "Should have 1 
AT")
@@ -82,20 +90,17 @@
         self.assertTrue(
             is_subdict_of(expected_result, result),  # We will test refresh_on 
later
             "Should obtain a token response")
+        self.assertTrue(result["token_source"], "identity_provider")
         self.assertEqual(expires_in, result["expires_in"], "Should have 
expected expires_in")
         if expires_in >= 7200:
             expected_refresh_on = int(time.time() + expires_in / 2)
             self.assertTrue(
                 expected_refresh_on - 1 <= result["refresh_on"] <= 
expected_refresh_on + 1,
                 "Should have a refresh_on time around the middle of the 
token's life")
-        self.assertEqual(
-            result["access_token"],
-            
app.acquire_token_for_client(resource=resource).get("access_token"),
-            "Should hit the same token from cache")
-
-        self.assertCacheStatus(app)
 
         result = app.acquire_token_for_client(resource=resource)
+        self.assertCacheStatus(app)
+        self.assertEqual("cache", result["token_source"], "Should hit cache")
         self.assertEqual(
             call_count, mocked_http.call_count,
             "No new call to the mocked http should be made for a cache hit")
@@ -110,6 +115,9 @@
                 expected_refresh_on - 5 < result["refresh_on"] <= 
expected_refresh_on,
                 "Should have a refresh_on time around the middle of the 
token's life")
 
+        result = app.acquire_token_for_client(resource=resource, 
claims_challenge="foo")
+        self.assertEqual("identity_provider", result["token_source"], "Should 
miss cache")
+
 
 class VmTestCase(ClientTestCase):
 
@@ -131,6 +139,22 @@
                 json.loads(raw_error), 
self.app.acquire_token_for_client(resource="R"))
             self.assertEqual({}, self.app._token_cache._cache)
 
+    def test_vm_resource_id_parameter_should_be_msi_res_id(self):
+        app = ManagedIdentityClient(
+            {"ManagedIdentityIdType": "ResourceId", "Id": "1234"},
+            http_client=requests.Session(),
+            )
+        with patch.object(app._http_client, "get", 
return_value=MinimalResponse(
+            status_code=200,
+            text='{"access_token": "AT", "expires_in": 3600, "resource": "R"}',
+        )) as mocked_method:
+            app.acquire_token_for_client(resource="R")
+            mocked_method.assert_called_with(
+                'http://169.254.169.254/metadata/identity/oauth2/token',
+                params={'api-version': '2018-02-01', 'resource': 'R', 
'msi_res_id': '1234'},
+                headers={'Metadata': 'true'},
+                )
+
 
 @patch.dict(os.environ, {"IDENTITY_ENDPOINT": "http://localhost";, 
"IDENTITY_HEADER": "foo"})
 class AppServiceTestCase(ClientTestCase):
@@ -156,6 +180,22 @@
             }, self.app.acquire_token_for_client(resource="R"))
             self.assertEqual({}, self.app._token_cache._cache)
 
+    def test_app_service_resource_id_parameter_should_be_mi_res_id(self):
+        app = ManagedIdentityClient(
+            {"ManagedIdentityIdType": "ResourceId", "Id": "1234"},
+            http_client=requests.Session(),
+            )
+        with patch.object(app._http_client, "get", 
return_value=MinimalResponse(
+            status_code=200,
+            text='{"access_token": "AT", "expires_on": 12345, "resource": 
"R"}',
+        )) as mocked_method:
+            app.acquire_token_for_client(resource="R")
+            mocked_method.assert_called_with(
+                'http://localhost',
+                params={'api-version': '2019-08-01', 'resource': 'R', 
'mi_res_id': '1234'},
+                headers={'X-IDENTITY-HEADER': 'foo', 'Metadata': 'true'},
+                )
+
 
 @patch.dict(os.environ, {"MSI_ENDPOINT": "http://localhost";, "MSI_SECRET": 
"foo"})
 class MachineLearningTestCase(ClientTestCase):
@@ -241,6 +281,9 @@
         "WWW-Authenticate": "Basic realm=/tmp/foo",
         })
 
+    def test_error_out_on_invalid_input(self, mocked_stat):
+        return super(ArcTestCase, self).test_error_out_on_invalid_input()
+
     def test_happy_path(self, mocked_stat):
         expires_in = 1234
         with patch.object(self.app._http_client, "get", side_effect=[
@@ -249,7 +292,8 @@
                 status_code=200,
                 text='{"access_token": "AT", "expires_in": "%s", "resource": 
"R"}' % expires_in,
                 ),
-        ]) as mocked_method:
+            ] * 2,  # Duplicate a pair of mocks for _test_happy_path()'s CAE 
check
+        ) as mocked_method:
             try:
                 self._test_happy_path(self.app, mocked_method, expires_in)
                 mocked_stat.assert_called_with(os.path.join(

Reply via email to