MicrosoftApiModule

Common Microsoft code that will be appended into each Microsoft integration when it's deployed

python · ApiModules

Details

IDMicrosoftApiModule
Languagepython
From Version5.0.0
Docker Imagedemisto/crypto:1.0.0.111961
Tagsinfra server

README

To use the common Microsoft API logic (authentication and API requests), run the following command to import the MicrosoftApiModule.

def main():
    ...


from MicrosoftApiModule import *  # noqa: E402

if __name__ in ["builtins", "__main__"]:
    main()

Then, the MicrosoftClient will be available for usage. For examples, see the Microsoft Graph Listener or Microsoft Graph Mail integrations.

import demistomock as demisto
import freezegun
import pytest
from MicrosoftApiModule import *
from requests import Response

from collections.abc import Iterable

TOKEN = "dummy_token"
TENANT = "dummy_tenant"
REFRESH_TOKEN = "dummy_refresh"
AUTH_ID = "dummy_auth_id"
ENC_KEY = "dummy_enc_key"
TOKEN_URL = "mock://dummy_url"
APP_NAME = "ms-graph-mail-listener"
BASE_URL = "https://graph.microsoft.com/v1.0/"
OK_CODES = (200, 201, 202)

CLIENT_ID = "dummy_client"
CLIENT_SECRET = "dummy_secret"
APP_URL = "https://login.microsoftonline.com/dummy_tenant/oauth2/v2.0/token"
SCOPE = "https://graph.microsoft.com/.default"
RESOURCE = "https://defender.windows.com/shtak"
RESOURCES = ["https://resource1.com", "https://resource2.com"]
FREEZE_STR_DATE = "1970-01-01 00:00:00"


def oproxy_client_tenant():
    tenant_id = TENANT
    auth_id = f"{AUTH_ID}@{TOKEN_URL}"
    enc_key = ENC_KEY
    app_name = APP_NAME
    base_url = BASE_URL
    ok_codes = OK_CODES

    return MicrosoftClient(
        self_deployed=False,
        auth_id=auth_id,
        enc_key=enc_key,
        app_name=app_name,
        tenant_id=tenant_id,
        base_url=base_url,
        verify=True,
        proxy=False,
        ok_codes=ok_codes,
    )


def oproxy_client_multi_resource():
    tenant_id = TENANT
    auth_id = f"{AUTH_ID}@{TOKEN_URL}"
    enc_key = ENC_KEY
    app_name = APP_NAME
    base_url = BASE_URL
    ok_codes = OK_CODES

    return MicrosoftClient(
        self_deployed=False,
        auth_id=auth_id,
        enc_key=enc_key,
        app_name=app_name,
        tenant_id=tenant_id,
        base_url=base_url,
        verify=True,
        proxy=False,
        ok_codes=ok_codes,
        multi_resource=True,
        resources=["https://resource1.com", "https://resource2.com"],
    )


def oproxy_client_refresh():
    refresh_token = REFRESH_TOKEN  # represents the refresh token from the integration context
    auth_id = f"{AUTH_ID}@{TOKEN_URL}"
    enc_key = ENC_KEY
    app_name = APP_NAME
    base_url = BASE_URL
    ok_codes = OK_CODES

    return MicrosoftClient(
        self_deployed=False,
        auth_id=auth_id,
        enc_key=enc_key,
        app_name=app_name,
        refresh_token=refresh_token,
        base_url=base_url,
        verify=True,
        proxy=False,
        ok_codes=ok_codes,
    )


def self_deployed_client():
    tenant_id = TENANT
    client_id = CLIENT_ID
    client_secret = CLIENT_SECRET
    base_url = BASE_URL
    resource = RESOURCE
    ok_codes = OK_CODES

    return MicrosoftClient(
        self_deployed=True,
        tenant_id=tenant_id,
        auth_id=client_id,
        enc_key=client_secret,
        resource=resource,
        base_url=base_url,
        verify=True,
        proxy=False,
        ok_codes=ok_codes,
    )


def self_deployed_client_multi_resource():
    tenant_id = TENANT
    client_id = CLIENT_ID
    client_secret = CLIENT_SECRET
    base_url = BASE_URL
    resources = RESOURCES
    ok_codes = OK_CODES

    return MicrosoftClient(
        self_deployed=True,
        tenant_id=tenant_id,
        auth_id=client_id,
        enc_key=client_secret,
        resources=resources,
        multi_resource=True,
        base_url=base_url,
        verify=True,
        proxy=False,
        ok_codes=ok_codes,
    )


def retry_on_rate_limit_client(retry_on_rate_limit: bool):
    tenant_id = TENANT
    client_id = CLIENT_ID
    client_secret = CLIENT_SECRET
    base_url = BASE_URL
    resource = RESOURCE
    ok_codes = OK_CODES

    return MicrosoftClient(
        self_deployed=True,
        tenant_id=tenant_id,
        auth_id=client_id,
        enc_key=client_secret,
        resource=resource,
        base_url=base_url,
        verify=True,
        proxy=False,
        ok_codes=ok_codes,
        retry_on_rate_limit=retry_on_rate_limit,
    )


@pytest.mark.parametrize(
    "error_content, status_code, expected_response",
    [
        (b'{"error":{"code":"code","message":"message"}}', 401, "code: message"),
        (
            b'{"error": "invalid_grant", "error_description": "AADSTS700082: The refresh token has expired due to inactivity.'
            b"\\u00a0The token was issued on 2023-02-06T12:26:14.6448497Z and was inactive for 90.00:00:00."
            b'\\r\\nTrace ID: test\\r\\nCorrelation ID: test\\r\\nTimestamp: 2023-07-02 06:40:26Z", '
            b'"error_codes": [700082], "timestamp": "2023-07-02 06:40:26Z", "trace_id": "test", "correlation_id": "test",'
            b' "error_uri": "https://login.microsoftonline.com/error?code=700082"}',
            400,
            "invalid_grant. \nThe refresh token has expired due to inactivity.\xa0The token was issued on "
            "2023-02-06T12:26:14.6448497Z and was inactive for 90.00:00:00.\nGot the following error codes from Microsoft: [700082]."  # noqa: E501
            "\nYou can run the ***command_prefix-auth-reset*** "
            "command to reset the authentication process.",
        ),
        (
            b'{"error": "invalid_resource", "error_description": "AADSTS500011: The resource principal named '
            b"https://security.microsoft.us was not found in the tenant named x Inc.. This can happen if the "
            b"application has not been installed by the administrator of the tenant or consented to by any user "
            b"in the tenant. You might have sent your authentication request to the wrong tenant. "
            b"Trace ID: test Correlation ID: test "
            b'Timestamp: 2025-02-26 14:27:01Z", "error_codes": [500011], "timestamp": "2025-02-26 14:27:01Z", '
            b'"trace_id": "test", "correlation_id": "test", "error_uri": "https://login.microsoftonline.us/error?code=500011"}',
            400,
            "invalid_resource. \nThe resource principal named https://security.microsoft.us was not found in the tenant named x "
            "Inc.. 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.",
        ),
        (
            b'{"error": "invalid_client", "error_description": "AADSTS7000215: Invalid client secret provided. '
            b"Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret "
            b"added to app 'c23324ec-b4a9-466d-9198-28d3efa07ee8'. Trace ID: 41b4a1eb-4fb9-4336-8e3f-fc1268861a00 "
            b'Correlation ID: 8e6ff592-76ca-4312-9662-b5088e5fe5fe Timestamp: 2025-03-04 08:35:11Z", "error_codes": [500011], '
            b'"timestamp": "2025-02-26 14:27:01Z", "trace_id": "test", "correlation_id": "test", "error_uri": '
            b'"https://login.microsoftonline.us/error?code=500011"}',
            400,
            "invalid_client. \nInvalid client secret provided. Ensure the secret being sent in the request is the client secret "
            "value, not the client secret ID, for a secret added to app 'c23324ec-b4a9-466d-9198-28d3efa07ee8'.",
        ),
    ],
)
def test_error_parser(mocker, error_content, status_code, expected_response):
    """
    Given:
        - The error_content, status_code, and expected_response for testing the error_parser function.
    When:
        - The error_parser function is called with the given error_content and status_code.
    Then:
        - Assert that the response from the error_parser matches the expected_response.
    """
    mocker.patch.object(demisto, "error")
    client = self_deployed_client()
    err = Response()
    err.status_code = status_code
    err._content = error_content
    response = client.error_parser(err)
    assert response == expected_response


def test_raise_authentication_error(mocker):
    """
    Given:
        - The error_content, status_code, and expected_response for testing the _raise_authentication_error function.
    When:
        - The _raise_authentication_error function is called with the given error_content and status_code.
    Then:
        - Assert that the response from the _raise_authentication_error matches the expected_response.
    """
    mocker.patch.object(demisto, "error")
    client = oproxy_client_tenant()
    err = Response()
    err.status_code = 401
    error_content_str = (
        "Error: failed to get access token with err: "
        '{"error":"invalid_grant","error_description":"AADSTS700003: Device object was not found in the '
        "tenant 'test' directory.\\r\\nTrace ID: test\\r\\nCorrelation ID: test\\r\\n"
        'Timestamp: 2023-07-20 12:03:53Z","error_codes":[700003],"timestamp":"2023-07-20 12:03:53Z",'
        '"trace_id":"test","correlation_id":"test",'
        '"error_uri":"https://login.microsoftonline.com/error?code=700003","suberror":'
        '"device_authentication_failed","claims":"{\\"access_token\\":{\\"capolids\\":'
        '{\\"essential\\":true,\\"values\\":[\\"test\\"]}}}"}'
    )
    err._content = error_content_str.encode("utf-8")
    err.reason = "test reason"
    expected_msg = (
        "Error in Microsoft authorization. Status: 401, body: invalid_grant. \nDevice object was not found in "
        "the tenant 'test' directory."
    )
    with pytest.raises(Exception, match=expected_msg):
        client._raise_authentication_error(err)


def test_page_not_found_error(mocker):
    """
    Given:
        - The http_request command for making MS API calls.
    When:
        - The response returned is a 404 response.
    Then:
        - Validate that the exception is handled in the http_request function of MicrosoftClient.
    """
    error_404 = Response()
    error_404._content = (
        b'{"error": {"code": "Request_ResourceNotFound", "message": "Resource "NotExistingUser does not exist."}}'
    )
    error_404.status_code = 404
    client = self_deployed_client()
    mocker.patch.object(BaseClient, "_http_request", return_value=error_404)
    mocker.patch.object(client, "get_access_token")

    with pytest.raises(NotFoundError):
        client.http_request()


def test_epoch_seconds(mocker):
    from datetime import datetime as date_time

    mocker.patch.object(MicrosoftClient, "_get_utcnow", return_value=date_time(2019, 12, 24, 14, 12, 0, 586636))
    mocker.patch.object(MicrosoftClient, "_get_utc_from_timestamp", return_value=date_time(1970, 1, 1, 0, 0))
    integer = MicrosoftClient.epoch_seconds()
    assert integer == 1577196720


@pytest.mark.parametrize(
    "client, tokens, context",
    [
        (
            oproxy_client_refresh(),
            (TOKEN, 3600, REFRESH_TOKEN),
            {"access_token": TOKEN, "valid_until": 3605, "current_refresh_token": REFRESH_TOKEN},
        ),
        (oproxy_client_tenant(), (TOKEN, 3600, ""), {"access_token": TOKEN, "valid_until": 3605, "current_refresh_token": ""}),
        (
            self_deployed_client(),
            (TOKEN, 3600, REFRESH_TOKEN),
            {"access_token": TOKEN, "valid_until": 3605, "current_refresh_token": REFRESH_TOKEN},
        ),
    ],
)
def test_get_access_token_no_context(mocker, client, tokens, context):
    mocker.patch.object(demisto, "getIntegrationContext", return_value={})
    mocker.patch.object(demisto, "setIntegrationContext")

    mocker.patch.object(client, "_oproxy_authorize", return_value=tokens)
    mocker.patch.object(client, "_get_self_deployed_token", return_value=tokens)
    mocker.patch.object(client, "epoch_seconds", return_value=10)

    # Arrange
    token = client.get_access_token()

    integration_context = demisto.setIntegrationContext.call_args[0][0]

    # Assert
    assert token == TOKEN
    assert integration_context == context


@pytest.mark.parametrize(
    "client, tokens, context",
    [
        (
            oproxy_client_refresh(),
            (TOKEN, 3600, REFRESH_TOKEN),
            {"access_token": TOKEN, "valid_until": 3605, "current_refresh_token": REFRESH_TOKEN},
        ),
        (
            oproxy_client_tenant(),
            (TOKEN, 3600, ""),
            {"access_token": TOKEN, "valid_until": 3605, "current_refresh_token": REFRESH_TOKEN},
        ),
        (
            self_deployed_client(),
            (TOKEN, 3600, REFRESH_TOKEN),
            {"access_token": TOKEN, "valid_until": 3605, "current_refresh_token": REFRESH_TOKEN},
        ),
    ],
)
def test_get_access_token_with_context_valid(mocker, client, tokens, context):
    # Set
    mocker.patch.object(demisto, "getIntegrationContext", return_value=context)
    mocker.patch.object(demisto, "setIntegrationContext")

    mocker.patch.object(client, "_oproxy_authorize", return_value=tokens)
    mocker.patch.object(client, "_get_self_deployed_token", return_value=tokens)
    mocker.patch.object(client, "epoch_seconds", return_value=3600)

    # Arrange
    token = client.get_access_token()

    set_context_count = demisto.setIntegrationContext.call_count
    auth_call_oproxy = client._oproxy_authorize.call_count
    auth_call_self_deployed = client._get_self_deployed_token.call_count

    # Assert
    assert set_context_count == 0
    assert auth_call_oproxy == 0
    assert auth_call_self_deployed == 0
    assert token == TOKEN


@pytest.mark.parametrize(
    "client, tokens, context_invalid, context_valid",
    [
        (
            oproxy_client_refresh(),
            (TOKEN, 3600, REFRESH_TOKEN),
            {"access_token": TOKEN, "valid_until": 3605, "current_refresh_token": REFRESH_TOKEN},
            {"access_token": TOKEN, "valid_until": 8595, "current_refresh_token": REFRESH_TOKEN},
        ),
        (
            oproxy_client_tenant(),
            (TOKEN, 3600, ""),
            {"access_token": TOKEN, "valid_until": 3605, "current_refresh_token": REFRESH_TOKEN},
            {"access_token": TOKEN, "valid_until": 8595, "current_refresh_token": ""},
        ),
        (
            self_deployed_client(),
            (TOKEN, 3600, ""),
            {"access_token": TOKEN, "valid_until": 3605, "current_refresh_token": ""},
            {"access_token": TOKEN, "valid_until": 8595, "current_refresh_token": ""},
        ),
    ],
)
def test_get_access_token_with_context_invalid(mocker, client, tokens, context_invalid, context_valid):
    # Set
    mocker.patch.object(demisto, "getIntegrationContext", return_value=context_invalid)
    mocker.patch.object(demisto, "setIntegrationContext")

    mocker.patch.object(client, "_oproxy_authorize", return_value=tokens)
    mocker.patch.object(client, "_get_self_deployed_token", return_value=tokens)
    mocker.patch.object(client, "epoch_seconds", side_effect=[4000, 5000])

    # Arrange
    token = client.get_access_token()

    integration_context = demisto.setIntegrationContext.call_args[0][0]

    # Assert
    assert token == TOKEN
    assert integration_context == context_valid


@pytest.mark.parametrize(
    "client, enc_content, tokens, res",
    [
        (oproxy_client_tenant(), TENANT, {"access_token": TOKEN, "expires_in": 3600}, (TOKEN, 3600, "")),
        (
            oproxy_client_refresh(),
            REFRESH_TOKEN,
            {"access_token": TOKEN, "expires_in": 3600, "refresh_token": REFRESH_TOKEN},
            (TOKEN, 3600, REFRESH_TOKEN),
        ),
    ],
)
def test_oproxy_request(mocker, requests_mock, client, enc_content, tokens, res):
    def get_encrypted(content, key):
        return content + key

    # Set
    body = {
        "app_name": APP_NAME,
        "registration_id": AUTH_ID,
        "encrypted_token": enc_content + ENC_KEY,
        "scope": None,
        "resource": "",
    }
    mocker.patch.object(client, "_add_info_headers")
    mocker.patch.object(client, "get_encrypted", side_effect=get_encrypted)
    requests_mock.post(TOKEN_URL, json=tokens)

    # Arrange
    req_res = client._oproxy_authorize()
    req_body = requests_mock._adapter.last_request.json()
    assert req_body == body
    assert req_res == res


def test_self_deployed_request(requests_mock):
    import urllib

    # Set
    client = self_deployed_client()

    body = {
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "grant_type": "client_credentials",
        "scope": SCOPE,
        "resource": RESOURCE,
    }

    requests_mock.post(APP_URL, json={"access_token": TOKEN, "expires_in": "3600"})

    # Arrange
    req_res = client._get_self_deployed_token()
    req_body = requests_mock._adapter.last_request._request.body
    assert req_body == urllib.parse.urlencode(body)
    assert req_res == (TOKEN, 3600, "")


def test_oproxy_use_resource(mocker):
    """
    Given:
        multi_resource client
    When
        When configuration is oproxy authentication type and multi resource
    Then
        Verify post request is using resource value
    """
    resource = "https://resource2.com"
    client = oproxy_client_multi_resource()
    context = {"access_token": TOKEN}

    mocked_post = mocker.patch("requests.post", json=context, status_code=200, ok=True)
    mocker.patch.object(client, "get_encrypted", return_value="encrypt")

    client._oproxy_authorize(resource)
    assert resource == mocked_post.call_args_list[0][1]["json"]["resource"]


@pytest.mark.parametrize("resource", ["https://resource1.com", "https://resource2.com"])
def test_self_deployed_multi_resource(requests_mock, resource):
    """
    Given:
        multi_resource client.
    When
        When configuration is client credentials authentication type and multi resource.
    Then
        Verify access token for each resource.
    """
    client = self_deployed_client_multi_resource()
    requests_mock.post(APP_URL, json={"access_token": TOKEN, "expires_in": "3600"})

    req_res = client._get_self_deployed_token()
    assert req_res == ("", 3600, "")
    assert client.resource_to_access_token[resource] == TOKEN


@pytest.mark.parametrize("azure_cloud_name", ["com", "gcc", "gcc-high", "dod", "de", "cn"])
def test_national_endpoints(mocker, azure_cloud_name):
    """
    Given:
        self-deployed client
    When:
        Configuring the client with different national endpoints
    Then:
        Verify that the token_retrieval_url and the scope are set correctly
    """
    tenant_id = TENANT
    auth_id = f"{AUTH_ID}@{TOKEN_URL}"
    enc_key = ENC_KEY
    app_name = APP_NAME
    ok_codes = OK_CODES
    azure_cloud = AZURE_CLOUDS[azure_cloud_name]
    client = MicrosoftClient(
        self_deployed=True,
        auth_id=auth_id,
        enc_key=enc_key,
        app_name=app_name,
        tenant_id=tenant_id,
        base_url=BASE_URL,
        verify=True,
        proxy=False,
        ok_codes=ok_codes,
        azure_cloud=azure_cloud,
    )

    assert client.azure_ad_endpoint == TOKEN_RETRIEVAL_ENDPOINTS[client.azure_cloud.abbreviation]
    assert client.scope == f"{GRAPH_ENDPOINTS[client.azure_cloud.abbreviation]}/.default"


def test_retry_on_rate_limit(requests_mock, mocker):
    """
    Given:
        self-deployed client with retry_on_rate_limit=True
    When:
        Response from http request is 429 rate limit
    Then:
        Verify that a ScheduledCommand is returend with relevant details
    """
    client = retry_on_rate_limit_client(True)
    requests_mock.post(APP_URL, json={"access_token": TOKEN, "expires_in": "3600"})

    requests_mock.get("https://graph.microsoft.com/v1.0/test_id", status_code=429, json={"content": "Rate limit reached!"})

    mocker.patch("CommonServerPython.is_demisto_version_ge", return_value=True)
    mocker.patch("MicrosoftApiModule.is_demisto_version_ge", return_value=True)
    mocker.patch.object(demisto, "command", return_value="testing_command")
    mocker.patch.object(demisto, "results")
    mocker.patch.object(sys, "exit")
    mocker.patch.object(demisto, "callingContext", {"context": {"ExecutedCommands": [{"moduleBrand": "msgraph"}]}})

    client.http_request(method="GET", url_suffix="test_id")
    retry_results: ScheduledCommand = demisto.results.call_args[0][0]
    assert retry_results.get("PollingCommand") == "testing_command"
    assert retry_results.get("PollingArgs") == {"ran_once_flag": True}

    metric_results = demisto.results.call_args_list[0][0][0]
    assert metric_results.get("Contents") == "Metrics reported successfully."
    assert metric_results.get("APIExecutionMetrics") == [{"Type": "QuotaError", "APICallsCount": 1}]


def test_fail_on_retry_on_rate_limit(requests_mock, mocker):
    """
    Given:
        client with retry_on_rate_limit=True and where 'first_run_flag' set to True in args
    When:
        Response from http request is 429 rate limit
    Then:
        Return Error as we  already retried rerunning the command
    """
    client = retry_on_rate_limit_client(True)
    requests_mock.post(APP_URL, json={"access_token": TOKEN, "expires_in": "3600"})

    requests_mock.get("https://graph.microsoft.com/v1.0/test_id", status_code=429, json={"content": "Rate limit reached!"})

    mocker.patch("CommonServerPython.is_demisto_version_ge", return_value=True)
    mocker.patch("MicrosoftApiModule.is_demisto_version_ge", return_value=True)
    mocker.patch.object(demisto, "command", return_value="testing_command")
    mocker.patch.object(demisto, "args", return_value={"ran_once_flag": True})
    mocker.patch.object(demisto, "results")
    mocker.patch.object(sys, "exit")
    mocker.patch.object(demisto, "callingContext", {"context": {"ExecutedCommands": [{"moduleBrand": "msgraph"}]}})

    with pytest.raises(DemistoException, match=r"Rate limit reached!"):
        client.http_request(method="GET", url_suffix="test_id")


def test_rate_limit_when_retry_is_false(requests_mock):
    """
    Given:
        self-deployed client with retry_on_rate_limit=False
    When:
        Response from http request is 429 rate limit
    Then:
        Verify that a regular error is returned and not a ScheduledCommand
    """
    client = retry_on_rate_limit_client(False)
    requests_mock.post(APP_URL, json={"access_token": TOKEN, "expires_in": "3600"})

    requests_mock.get("https://graph.microsoft.com/v1.0/test_id", status_code=429, json={"content": "Rate limit reached!"})

    with pytest.raises(DemistoException, match="Error in API call \[429\]"):
        client.http_request(method="GET", url_suffix="test_id")


@pytest.mark.parametrize(
    "response, result",
    [
        (200, [{"Type": "Successful", "APICallsCount": 1}]),
        (429, [{"Type": "QuotaError", "APICallsCount": 1}]),
        (500, [{"Type": "GeneralError", "APICallsCount": 1}]),
    ],
)
def test_create_api_metrics(mocker, response, result):
    """
    Test create_api_metrics function, make sure metrics are reported according to the response
    """
    mocker.patch.object(demisto, "results")
    mocker.patch("CommonServerPython.is_demisto_version_ge", return_value=True)
    mocker.patch("MicrosoftApiModule.is_demisto_version_ge", return_value=True)
    mocker.patch.object(demisto, "callingContext", {"context": {"ExecutedCommands": [{"moduleBrand": "msgraph"}]}})
    MicrosoftClient.create_api_metrics(response)

    metric_results = demisto.results.call_args_list[0][0][0]
    assert metric_results.get("Contents") == "Metrics reported successfully."
    assert metric_results.get("APIExecutionMetrics") == result


def test_general_error_metrics(requests_mock, mocker):
    "When we activate the retry mechanism, and we recieve a general error, it's metric should be recorded"
    client = retry_on_rate_limit_client(True)
    requests_mock.post(APP_URL, json={"access_token": TOKEN, "expires_in": "3600"})

    requests_mock.get("https://graph.microsoft.com/v1.0/test_id", status_code=500, json={"content": "General Error!"})

    mocker.patch("CommonServerPython.is_demisto_version_ge", return_value=True)
    mocker.patch("MicrosoftApiModule.is_demisto_version_ge", return_value=True)
    mocker.patch.object(demisto, "command", return_value="testing_command")
    mocker.patch.object(demisto, "results")

    with pytest.raises(DemistoException):
        client.http_request(method="GET", url_suffix="test_id")
    metric_results = demisto.results.call_args_list[0][0][0]
    assert metric_results.get("Contents") == "Metrics reported successfully."
    assert metric_results.get("APIExecutionMetrics") == [{"Type": "GeneralError", "APICallsCount": 1}]


@pytest.mark.parametrize(argnames="client_id", argvalues=["test_client_id", None])
def test_get_token_managed_identities(requests_mock, mocker, client_id):
    """
    Given:
        managed identity client id or None
    When:
        get access token
    Then:
        Verify that the result are as expected
    """
    test_token = "test_token"
    import MicrosoftApiModule

    mock_token = {"access_token": test_token, "expires_in": "86400"}

    get_mock = requests_mock.get(MANAGED_IDENTITIES_TOKEN_URL, json=mock_token)
    mocker.patch.object(MicrosoftApiModule, "get_integration_context", return_value={})

    client = self_deployed_client()
    client.managed_identities_resource_uri = Resources.graph
    client.managed_identities_client_id = client_id or MANAGED_IDENTITIES_SYSTEM_ASSIGNED

    assert test_token == client.get_access_token()
    qs = get_mock.last_request.qs
    assert qs["resource"] == [Resources.graph]
    assert (client_id and qs["client_id"] == [client_id]) or "client_id" not in qs


def test_get_token_managed_identities__error(requests_mock, mocker):
    """
    Given:
        managed identity client id
    When:
        get access token
    Then:
        Verify that the result are as expected
    """

    import MicrosoftApiModule

    mock_token = {"error_description": "test_error_description"}
    requests_mock.get(MANAGED_IDENTITIES_TOKEN_URL, json=mock_token)
    mocker.patch.object(MicrosoftApiModule, "return_error", side_effect=Exception())
    mocker.patch.object(MicrosoftApiModule, "get_integration_context", return_value={})

    client = self_deployed_client()
    client.managed_identities_client_id = "test_client_id"
    client.managed_identities_resource_uri = Resources.graph

    with pytest.raises(Exception):
        client.get_access_token()

    err_message = "Error in Microsoft authorization with Azure Managed Identities"
    assert err_message in MicrosoftApiModule.return_error.call_args[0][0]


args = {"test": "test_arg_value"}
params = {"test": "test_param_value", "test_unique": "test_arg2_value"}


def test_get_from_args_or_params__when_the_key_exists_in_args_and_params():
    """
    Given:
        args and params with the same key in both
    When:
        get value from args or params is called
    Then:
        Verify that the result are as expected = the value from args is returned
    """

    assert get_from_args_or_params(args, params, "test") == "test_arg_value"


def test_get_from_args_or_params__when_the_key_exists_only_in_params():
    """
    Given:
        args and params with the requested key exist only in params
    When:
        get value from args or params is called
    Then:
        Verify that the result are as expected = the value from params
    """
    assert get_from_args_or_params(args, params, "test_unique") == "test_arg2_value"


def test_get_from_args_or_params__when_the_key_dose_not_exists():
    """
    Given:
        args and params
    When:
        get value from args or params is called with a key that dose not exist
    Then:
        Verify that the correct error message is raising
    """
    with pytest.raises(Exception) as e:
        get_from_args_or_params(args, params, "mock")
    assert (
        e.value.args[0] == "No mock was provided. Please provide a mock either in the instance "
        "configuration or as a command argument."
    )


def test_azure_tag_formatter__with_valid_input():
    """
    Given:
        A valid json as a string
    When:
        azure_tag_formatter is called
    Then:
        Verify that the result are as expected
    """
    assert azure_tag_formatter('{"key":"value"}') == "tagName eq 'key' and tagValue eq 'value'"


def test_azure_tag_formatter__with_invalid_input():
    """
    Given:
        A invalid json as a string
    When:
        azure_tag_formatter is called
    Then:
        Verify that the correct error message is raising
    """
    with pytest.raises(Exception) as e:
        azure_tag_formatter('{"key:value"}')
    assert e.value.args[0] == 'Invalid tag format, please use the following format: \'{"key_name":"value_name"}\''


def test_reset_auth(mocker):
    """
    Given:
        -
    When:
        - Calling function reset_auth.
    Then:
        - Ensure the output are as expected.
    """
    from MicrosoftApiModule import reset_auth

    expected_output = (
        "Authorization was reset successfully. Please regenerate the credentials, "
        "and then click **Test** to validate the credentials and connection."
    )

    mocker.patch.object(demisto, "getIntegrationContext", return_value={"test"})
    mocker.patch.object(demisto, "setIntegrationContext")

    result = reset_auth()

    assert result.readable_output == expected_output
    assert demisto.getIntegrationContext.call_count == 1
    assert demisto.setIntegrationContext.call_count == 1
    assert demisto.setIntegrationContext.call_args[0][0] == {}
    assert result


def test_generate_login_url():
    """
    Given:
        - Self-deployed are true and auth code are the auth flow
    When:
        - Calling function generate_login_url
     Then:
        - Ensure the generated url are as expected.
    """
    from MicrosoftApiModule import generate_login_url

    client = self_deployed_client()

    result = generate_login_url(client)

    expected_url = (
        f"[login URL](https://login.microsoftonline.com/{TENANT}/oauth2/v2.0/authorize?"
        f"response_type=code&scope=offline_access%20https://graph.microsoft.com/.default"
        f"&client_id={CLIENT_ID}&redirect_uri=https://localhost/myapp)"
    )
    assert expected_url in result.readable_output, "Login URL is incorrect"


@pytest.mark.parametrize(
    "params, expected_resource_manager, expected_active_directory, expected_microsoft_graph_resource_id",
    [
        (
            {"azure_cloud": "Germany"},
            "https://management.microsoftazure.de",
            "https://login.microsoftonline.de",
            "https://graph.microsoft.de",
        ),
        (
            {"azure_cloud": "Custom", "server_url": "mock_url"},
            "mock_url",
            "https://login.microsoftonline.com",
            "https://graph.microsoft.com/",
        ),
        (
            {"azure_ad_endpoint": "mock_endpoint"},
            "https://management.azure.com/",
            "mock_endpoint",
            "https://graph.microsoft.com/",
        ),
        ({"url": "mock_url"}, "https://management.azure.com/", "https://login.microsoftonline.com", "mock_url"),
        ({}, "https://management.azure.com/", "https://login.microsoftonline.com", "https://graph.microsoft.com/"),
    ],
)
def test_get_azure_cloud(params, expected_resource_manager, expected_active_directory, expected_microsoft_graph_resource_id):
    """
    Given:
        - params with different azure_cloud values
        case 1: azure_cloud = Germany
        case 2: azure_cloud = Custom and server_url
        case 3: azure_cloud = None. azure_ad_endpoint in params
        case 4: azure_cloud = None. url in params
        case 5 : params is empty
    When:
        - Calling function get_azure_cloud
    Then:
        - Ensure the generated url are as expected.
    """
    from MicrosoftApiModule import get_azure_cloud

    assert get_azure_cloud(params=params, integration_name="test").endpoints.resource_manager == expected_resource_manager
    assert get_azure_cloud(params=params, integration_name="test").endpoints.active_directory == expected_active_directory
    assert (
        get_azure_cloud(params=params, integration_name="test").endpoints.microsoft_graph_resource_id
        == expected_microsoft_graph_resource_id
    )


@freezegun.freeze_time(FREEZE_STR_DATE)
def test_should_delay_true():
    """
    Given:
        - Mocked context with later next request time than the current time.
    When:
        - Calling the function should_delay_request.
     Then:
        - Ensure the function return the expected value.
    """
    from datetime import datetime

    from MicrosoftApiModule import should_delay_request

    mocked_next_request_time = datetime.strptime(FREEZE_STR_DATE, "%Y-%m-%d %H:%M:%S").timestamp() + 1.0
    excepted_error = f"The request will be delayed until {datetime.fromtimestamp(mocked_next_request_time)}"
    with pytest.raises(Exception) as e:
        should_delay_request(mocked_next_request_time)
    assert str(e.value) == excepted_error


@freezegun.freeze_time(FREEZE_STR_DATE)
def test_should_delay_false():
    """
    Given:
        - Mocked context with next request time equal to the current time.
    When:
        - Calling the function should_delay_request.
     Then:
        - Ensure the function return with no error.
    """
    from datetime import datetime

    from MicrosoftApiModule import should_delay_request

    mocked_next_request_time = datetime.strptime(FREEZE_STR_DATE, "%Y-%m-%d %H:%M:%S").timestamp()
    should_delay_request(mocked_next_request_time)


@freezegun.freeze_time(FREEZE_STR_DATE)
@pytest.mark.parametrize("mocked_next_request_time,excepted", [(2, 4.0), (3, 8.0), (6, 64.0)])
def test_calculate_next_request_time(mocked_next_request_time, excepted):
    """
    Given:
        - Mocked context with next request time equal to the current time.
    When:
        - Calling the function should_delay_request.
     Then:
        - Ensure the function return with no error.
    """
    from MicrosoftApiModule import calculate_next_request_time

    assert calculate_next_request_time(mocked_next_request_time) == excepted


@freezegun.freeze_time(FREEZE_STR_DATE)
@pytest.mark.parametrize(
    "mocked_delay_request_counter,excepted",
    [
        ({"delay_request_counter": 5}, {"next_request_time": 32.0, "delay_request_counter": 6}),
        ({"delay_request_counter": 6}, {"next_request_time": 64.0, "delay_request_counter": 7}),
        ({"delay_request_counter": 7}, {"next_request_time": 64.0, "delay_request_counter": 7}),
    ],
)
def test_oproxy_authorize_retry_mechanism(mocker, capfd, mocked_delay_request_counter, excepted):
    """
    Given:
        - Mocked context with next request time equal to the current time.
    When:
        - Calling the _oproxy_authorize function.
     Then:
        - Ensure the function return with no error and the context has been set with the right values.
    """
    from datetime import datetime

    # pytest raises a warning when there is error in the stderr, this is a workaround to disable it
    with capfd.disabled():
        client = oproxy_client_refresh()
        error = Response()
        error.status_code = 400
        error.reason = "Bad Request"
        mocked_next_request_time = {"next_request_time": datetime.strptime(FREEZE_STR_DATE, "%Y-%m-%d %H:%M:%S").timestamp()}

        mocked_context = mocked_next_request_time | mocked_delay_request_counter
        mocker.patch.object(demisto, "getIntegrationContext", return_value=mocked_context)
        mocker.patch.object(client, "_oproxy_authorize_build_request", return_value=error)
        res = mocker.patch.object(demisto, "setIntegrationContext")

        with pytest.raises(Exception):
            client._oproxy_authorize()
        assert res.call_args[0][0] == excepted


def test_http_request_status_list_to_retry_parameter(requests_mock, mocker):
    """
    Test that MicrosoftClient passes status_list_to_retry parameter correctly as a list.

    This test verifies that the retry mechanism receives the correct parameter format
    to prevent TypeError: argument of type 'int' is not iterable.

    Given:
        - MicrosoftClient configured for self-deployed authentication
    When:
        - Making an HTTP request through http_request method
    Then:
        - Verify that status_list_to_retry is passed as an Iterable
    """

    client = self_deployed_client()
    api_url = f"{BASE_URL}test-endpoint"
    requests_mock.get(api_url, status_code=200, json={"success": True})
    requests_mock.post(APP_URL, json={"access_token": TOKEN, "expires_in": "3600"})

    captured_params = {}

    def capture_http_request_params(self, *args, **kwargs):
        from unittest.mock import Mock

        captured_params.update(kwargs)
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"success": True}
        return mock_response

    mocker.patch.object(BaseClient, "_http_request", side_effect=capture_http_request_params)
    client.http_request("GET", "test-endpoint")

    assert "status_list_to_retry" in captured_params, "status_list_to_retry parameter not found"

    status_list = captured_params["status_list_to_retry"]
    assert isinstance(
        status_list, Iterable
    ), f"status_list_to_retry should be an Iterable, got {type(status_list)}: {status_list}"