This is an automated email from the ASF dual-hosted git repository.

dabla pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 328730cbcd9 Add Azure Analysis Services model refresh support (#71350)
328730cbcd9 is described below

commit 328730cbcd95e68d416a8d9f0d4335058d2b8802
Author: Aaron Chen <[email protected]>
AuthorDate: Mon Sep 7 16:13:27 2026 +0800

    Add Azure Analysis Services model refresh support (#71350)
    
    * Add Azure Analysis Services model refresh support
---
 .../azure/docs/connections/analysis_services.rst   |  63 +++
 .../azure/docs/operators/analysis_services.rst     |  85 ++++
 providers/microsoft/azure/provider.yaml            |  34 ++
 .../providers/microsoft/azure/get_provider_info.py |  40 ++
 .../microsoft/azure/hooks/analysis_services.py     | 300 +++++++++++++
 .../microsoft/azure/operators/analysis_services.py | 136 ++++++
 .../microsoft/azure/sensors/analysis_services.py   |  98 ++++
 .../microsoft/azure/triggers/analysis_services.py  | 182 ++++++++
 .../azure/example_azure_analysis_services.py       |  80 ++++
 .../azure/hooks/test_analysis_services.py          | 500 +++++++++++++++++++++
 .../azure/operators/test_analysis_services.py      | 218 +++++++++
 .../azure/sensors/test_analysis_services.py        | 122 +++++
 .../azure/triggers/test_analysis_services.py       | 336 ++++++++++++++
 13 files changed, 2194 insertions(+)

diff --git a/providers/microsoft/azure/docs/connections/analysis_services.rst 
b/providers/microsoft/azure/docs/connections/analysis_services.rst
new file mode 100644
index 00000000000..cb750ba444a
--- /dev/null
+++ b/providers/microsoft/azure/docs/connections/analysis_services.rst
@@ -0,0 +1,63 @@
+ .. Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+ ..   http://www.apache.org/licenses/LICENSE-2.0
+
+ .. Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+.. _howto/connection:azure_analysis_services:
+
+Microsoft Azure Analysis Services Connection
+============================================
+
+The Microsoft Azure Analysis Services connection type enables model refresh 
operations through the
+`asynchronous refresh REST API 
<https://learn.microsoft.com/en-us/analysis-services/azure-analysis-services/analysis-services-async-refresh>`__.
+
+The 
:class:`~airflow.providers.microsoft.azure.hooks.analysis_services.AzureAnalysisServicesHook`,
+:class:`~airflow.providers.microsoft.azure.operators.analysis_services.AzureAnalysisServicesRefreshOperator`,
+and 
:class:`~airflow.providers.microsoft.azure.sensors.analysis_services.AzureAnalysisServicesSensor`
+use this connection.
+
+Default Connection ID
+---------------------
+
+The default connection ID is ``azure_analysis_services_default``.
+
+Configuring the Connection
+--------------------------
+
+Region Endpoint
+    Specify the rollout endpoint in ``host``, for example 
``westus.asazure.windows.net``. Do not include
+    ``https://``, a port, the Analysis Services server name, or a path. You 
can find this endpoint in the
+    server's full name, such as 
``asazure://westus.asazure.windows.net/example-server``.
+
+Client ID
+    Specify the Microsoft Entra service principal application (client) ID in 
``login``.
+
+Client Secret
+    Specify the service principal client secret in ``password``.
+
+Tenant ID
+    Specify the Microsoft Entra tenant ID in the ``tenantId`` extra field.
+
+Azure Analysis Services currently requires the service principal to be a 
server administrator for
+asynchronous refresh REST API calls. Add it to the server administrator role 
using the format
+``app:<client-id>@<tenant-id>``. See `Add a service principal to the server 
administrator role
+<https://learn.microsoft.com/en-us/analysis-services/azure-analysis-services/analysis-services-addservprinc-admins>`__.
+
+This connection supports service principal client-secret authentication. 
Managed identity authentication is
+not supported because Azure Analysis Services does not support managed 
identities for these operations.
+
+.. spelling:word-list::
+
+    rollout
diff --git a/providers/microsoft/azure/docs/operators/analysis_services.rst 
b/providers/microsoft/azure/docs/operators/analysis_services.rst
new file mode 100644
index 00000000000..c809698a204
--- /dev/null
+++ b/providers/microsoft/azure/docs/operators/analysis_services.rst
@@ -0,0 +1,85 @@
+ .. Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+ ..   http://www.apache.org/licenses/LICENSE-2.0
+
+ .. Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+Microsoft Azure Analysis Services Operators
+===========================================
+
+Use these components to start and monitor asynchronous model refresh 
operations in Microsoft Azure Analysis
+Services. Configure an :ref:`Azure Analysis Services connection 
<howto/connection:azure_analysis_services>`
+before using them.
+
+Prerequisite Tasks
+------------------
+
+.. include:: /operators/_partials/prerequisite_tasks.rst
+
+.. _howto/operator:AzureAnalysisServicesRefreshOperator:
+
+Start a model refresh
+---------------------
+
+Use 
:class:`~airflow.providers.microsoft.azure.operators.analysis_services.AzureAnalysisServicesRefreshOperator`
+with ``wait_for_termination=False`` to start a refresh and immediately return 
its refresh ID.
+
+.. exampleinclude:: 
/../tests/system/microsoft/azure/example_azure_analysis_services.py
+    :language: python
+    :dedent: 4
+    :start-after: [START howto_operator_azure_analysis_services_refresh]
+    :end-before: [END howto_operator_azure_analysis_services_refresh]
+
+Wait for the refresh to finish
+------------------------------
+
+With the default ``wait_for_termination=True`` the operator also waits for the 
refresh to reach a
+terminal status. The ``timeout`` parameter limits that wait and starts once 
the refresh has been
+submitted, while ``request_timeout`` limits each individual REST request.
+
+.. exampleinclude:: 
/../tests/system/microsoft/azure/example_azure_analysis_services.py
+    :language: python
+    :dedent: 4
+    :start-after: [START 
howto_operator_azure_analysis_services_refresh_and_wait]
+    :end-before: [END howto_operator_azure_analysis_services_refresh_and_wait]
+
+.. note::
+
+    The operator and the sensor always run deferred. Both the request that 
starts the refresh and
+    the status polling are performed by the triggerer, so no worker slot is 
held while the model is
+    refreshing. A triggerer must be running in your deployment.
+
+.. _howto/sensor:AzureAnalysisServicesSensor:
+
+Wait with a sensor
+------------------
+
+The operator return value can be passed directly to
+:class:`~airflow.providers.microsoft.azure.sensors.analysis_services.AzureAnalysisServicesSensor`.
+This separates starting the refresh from waiting for it.
+
+.. exampleinclude:: 
/../tests/system/microsoft/azure/example_azure_analysis_services.py
+    :language: python
+    :dedent: 4
+    :start-after: [START howto_sensor_azure_analysis_services_refresh]
+    :end-before: [END howto_sensor_azure_analysis_services_refresh]
+
+Azure Analysis Services accepts only one active refresh for a model. A second 
request returns HTTP 409, so
+serialize refresh tasks that target the same model, for example with task 
dependencies or an Airflow pool.
+
+Reference
+---------
+
+For more information, see `Asynchronous refresh with the REST API
+<https://learn.microsoft.com/en-us/analysis-services/azure-analysis-services/analysis-services-async-refresh>`__.
diff --git a/providers/microsoft/azure/provider.yaml 
b/providers/microsoft/azure/provider.yaml
index 4c2f8112330..bb5c770251f 100644
--- a/providers/microsoft/azure/provider.yaml
+++ b/providers/microsoft/azure/provider.yaml
@@ -124,6 +124,12 @@ versions:
   - 1.0.0
 
 integrations:
+  - integration-name: Microsoft Azure Analysis Services
+    external-doc-url: 
https://learn.microsoft.com/en-us/analysis-services/azure-analysis-services/
+    how-to-guide:
+      - 
/docs/apache-airflow-providers-microsoft-azure/operators/analysis_services.rst
+    logo: /docs/integration-logos/Microsoft-Azure.png
+    tags: [azure]
   - integration-name: Microsoft Azure Batch
     external-doc-url: https://azure.microsoft.com/en-us/services/batch/
     how-to-guide:
@@ -215,6 +221,9 @@ integrations:
     tags: [azure]
 
 operators:
+  - integration-name: Microsoft Azure Analysis Services
+    python-modules:
+      - airflow.providers.microsoft.azure.operators.analysis_services
   - integration-name: Microsoft Azure Compute
     python-modules:
       - airflow.providers.microsoft.azure.operators.compute
@@ -256,6 +265,9 @@ operators:
       - airflow.providers.microsoft.azure.operators.powerbi
 
 sensors:
+  - integration-name: Microsoft Azure Analysis Services
+    python-modules:
+      - airflow.providers.microsoft.azure.sensors.analysis_services
   - integration-name: Microsoft Azure Compute
     python-modules:
       - airflow.providers.microsoft.azure.sensors.compute
@@ -277,6 +289,9 @@ filesystems:
   - airflow.providers.microsoft.azure.fs.msgraph
 
 hooks:
+  - integration-name: Microsoft Azure Analysis Services
+    python-modules:
+      - airflow.providers.microsoft.azure.hooks.analysis_services
   - integration-name: Microsoft Azure Compute
     python-modules:
       - airflow.providers.microsoft.azure.hooks.compute
@@ -329,6 +344,9 @@ hooks:
       - airflow.providers.microsoft.azure.hooks.powerbi
 
 triggers:
+  - integration-name: Microsoft Azure Analysis Services
+    python-modules:
+      - airflow.providers.microsoft.azure.triggers.analysis_services
   - integration-name: Microsoft Azure Batch
     python-modules:
       - airflow.providers.microsoft.azure.triggers.batch
@@ -389,6 +407,22 @@ transfers:
     python-module: airflow.providers.microsoft.azure.transfers.gcs_to_wasb
 
 connection-types:
+  - hook-class-name: 
airflow.providers.microsoft.azure.hooks.analysis_services.AzureAnalysisServicesHook
+    hook-name: "Azure Analysis Services"
+    connection-type: azure_analysis_services
+    ui-field-behaviour:
+      hidden-fields: ["schema", "port", "extra"]
+      relabeling:
+        host: Region Endpoint
+        login: Client ID
+        password: Client Secret
+      placeholders:
+        host: westus.asazure.windows.net
+    conn-fields:
+      tenantId:
+        label: Tenant ID
+        schema:
+          type: ["string", "null"]
   - hook-class-name: 
airflow.providers.microsoft.azure.hooks.base_azure.AzureBaseHook
     hook-name: "Azure"
     connection-type: azure
diff --git 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/get_provider_info.py
 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/get_provider_info.py
index 4e3ab2e9d30..f45dd5af9a6 100644
--- 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/get_provider_info.py
+++ 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/get_provider_info.py
@@ -27,6 +27,15 @@ def get_provider_info():
         "name": "Microsoft Azure",
         "description": "`Microsoft Azure <https://azure.microsoft.com/>`__\n",
         "integrations": [
+            {
+                "integration-name": "Microsoft Azure Analysis Services",
+                "external-doc-url": 
"https://learn.microsoft.com/en-us/analysis-services/azure-analysis-services/";,
+                "how-to-guide": [
+                    
"/docs/apache-airflow-providers-microsoft-azure/operators/analysis_services.rst"
+                ],
+                "logo": "/docs/integration-logos/Microsoft-Azure.png",
+                "tags": ["azure"],
+            },
             {
                 "integration-name": "Microsoft Azure Batch",
                 "external-doc-url": 
"https://azure.microsoft.com/en-us/services/batch/";,
@@ -149,6 +158,10 @@ def get_provider_info():
             },
         ],
         "operators": [
+            {
+                "integration-name": "Microsoft Azure Analysis Services",
+                "python-modules": 
["airflow.providers.microsoft.azure.operators.analysis_services"],
+            },
             {
                 "integration-name": "Microsoft Azure Compute",
                 "python-modules": 
["airflow.providers.microsoft.azure.operators.compute"],
@@ -203,6 +216,10 @@ def get_provider_info():
             },
         ],
         "sensors": [
+            {
+                "integration-name": "Microsoft Azure Analysis Services",
+                "python-modules": 
["airflow.providers.microsoft.azure.sensors.analysis_services"],
+            },
             {
                 "integration-name": "Microsoft Azure Compute",
                 "python-modules": 
["airflow.providers.microsoft.azure.sensors.compute"],
@@ -229,6 +246,10 @@ def get_provider_info():
             "airflow.providers.microsoft.azure.fs.msgraph",
         ],
         "hooks": [
+            {
+                "integration-name": "Microsoft Azure Analysis Services",
+                "python-modules": 
["airflow.providers.microsoft.azure.hooks.analysis_services"],
+            },
             {
                 "integration-name": "Microsoft Azure Compute",
                 "python-modules": 
["airflow.providers.microsoft.azure.hooks.compute"],
@@ -299,6 +320,10 @@ def get_provider_info():
             },
         ],
         "triggers": [
+            {
+                "integration-name": "Microsoft Azure Analysis Services",
+                "python-modules": 
["airflow.providers.microsoft.azure.triggers.analysis_services"],
+            },
             {
                 "integration-name": "Microsoft Azure Batch",
                 "python-modules": 
["airflow.providers.microsoft.azure.triggers.batch"],
@@ -379,6 +404,21 @@ def get_provider_info():
             },
         ],
         "connection-types": [
+            {
+                "hook-class-name": 
"airflow.providers.microsoft.azure.hooks.analysis_services.AzureAnalysisServicesHook",
+                "hook-name": "Azure Analysis Services",
+                "connection-type": "azure_analysis_services",
+                "ui-field-behaviour": {
+                    "hidden-fields": ["schema", "port", "extra"],
+                    "relabeling": {
+                        "host": "Region Endpoint",
+                        "login": "Client ID",
+                        "password": "Client Secret",
+                    },
+                    "placeholders": {"host": "westus.asazure.windows.net"},
+                },
+                "conn-fields": {"tenantId": {"label": "Tenant ID", "schema": 
{"type": ["string", "null"]}}},
+            },
             {
                 "hook-class-name": 
"airflow.providers.microsoft.azure.hooks.base_azure.AzureBaseHook",
                 "hook-name": "Azure",
diff --git 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/analysis_services.py
 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/analysis_services.py
new file mode 100644
index 00000000000..cdce36a4eb8
--- /dev/null
+++ 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/analysis_services.py
@@ -0,0 +1,300 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import asyncio
+from functools import cached_property
+from typing import TYPE_CHECKING, Any, Literal, get_args
+from urllib.parse import quote, unquote, urlsplit
+
+import httpx
+from azure.core.exceptions import AzureError
+from azure.identity.aio import ClientSecretCredential
+
+from airflow.providers.common.compat.sdk import AirflowException, BaseHook
+
+if TYPE_CHECKING:
+    from azure.core.credentials_async import AsyncTokenCredential
+
+    from airflow.sdk import Connection
+
+TOKEN_SCOPE = "https://*.asazure.windows.net/.default";
+
+RefreshType = Literal["full", "clearValues", "calculate", "dataOnly", 
"automatic", "defragment"]
+VALID_REFRESH_TYPES: frozenset[str] = frozenset(get_args(RefreshType))
+
+
+def _format_request_error(error: httpx.HTTPError) -> str:
+    # Only HTTPStatusError carries a response; transport errors have no such 
attribute.
+    response = getattr(error, "response", None)
+    response_body = getattr(response, "text", "").strip()[:1000]
+    response_detail = f"; response body: {response_body}" if response_body 
else ""
+    return f"{error}{response_detail}"
+
+
+class AzureAnalysisServicesRefreshStatus:
+    """Azure Analysis Services model refresh statuses."""
+
+    SUCCEEDED = "succeeded"
+    FAILED = "failed"
+    CANCELLED = "cancelled"
+    TIMED_OUT = "timedOut"
+    NOT_STARTED = "notStarted"
+    IN_PROGRESS = "inProgress"
+
+    FAILURE_STATUSES = frozenset({FAILED, CANCELLED, TIMED_OUT})
+    VALID_STATUSES = frozenset({SUCCEEDED, FAILED, CANCELLED, TIMED_OUT, 
NOT_STARTED, IN_PROGRESS})
+
+
+class AzureAnalysisServicesRefreshException(AirflowException):
+    """Indicate that an Azure Analysis Services model refresh operation 
failed."""
+
+
+class AzureAnalysisServicesHook(BaseHook):
+    """
+    Interact with the Azure Analysis Services asynchronous refresh REST API.
+
+    All request methods are asynchronous and are meant to be awaited from a 
trigger. Call
+    :meth:`aclose` when done so the HTTP client and the credential release 
their resources.
+
+    :param azure_analysis_services_conn_id: The Azure Analysis Services 
connection ID.
+    :param request_timeout: Timeout in seconds for each HTTP request.
+
+    The connection must define the region endpoint in ``host``, the service 
principal client ID in
+    ``login``, the client secret in ``password``, and the Microsoft Entra 
tenant ID in the
+    ``tenantId`` extra field.
+    """
+
+    conn_type: str = "azure_analysis_services"
+    conn_name_attr: str = "azure_analysis_services_conn_id"
+    default_conn_name: str = "azure_analysis_services_default"
+    hook_name: str = "Azure Analysis Services"
+
+    def __init__(
+        self,
+        azure_analysis_services_conn_id: str = default_conn_name,
+        request_timeout: float = 60,
+    ) -> None:
+        super().__init__()
+        if request_timeout <= 0:
+            raise ValueError("request_timeout must be greater than zero")
+        self.azure_analysis_services_conn_id = azure_analysis_services_conn_id
+        self.request_timeout = request_timeout
+        self._credential: AsyncTokenCredential | None = None
+        self._client: httpx.AsyncClient | None = None
+
+    @cached_property
+    def connection(self) -> Connection:
+        """Return the Azure Analysis Services connection."""
+        return self.get_connection(self.azure_analysis_services_conn_id)
+
+    @classmethod
+    def get_connection_form_widgets(cls) -> dict[str, Any]:
+        """Return connection widgets to add to the connection form."""
+        from flask_appbuilder.fieldwidgets import BS3TextFieldWidget
+        from flask_babel import lazy_gettext
+        from wtforms import StringField
+
+        return {
+            "tenantId": StringField(lazy_gettext("Tenant ID"), 
widget=BS3TextFieldWidget()),
+        }
+
+    @classmethod
+    def get_ui_field_behaviour(cls) -> dict[str, Any]:
+        """Return custom field behaviour for the connection form."""
+        return {
+            "hidden_fields": ["schema", "port", "extra"],
+            "relabeling": {
+                "host": "Region Endpoint",
+                "login": "Client ID",
+                "password": "Client Secret",
+            },
+            "placeholders": {
+                "host": "westus.asazure.windows.net",
+            },
+        }
+
+    def get_conn(self) -> httpx.AsyncClient:
+        """Return and cache the HTTP client used to communicate with Analysis 
Services."""
+        if self._client is None:
+            self._client = httpx.AsyncClient(timeout=self.request_timeout)
+        return self._client
+
+    def _get_credential(self) -> AsyncTokenCredential:
+        """Return and cache the service principal credential."""
+        if self._credential is not None:
+            return self._credential
+
+        connection = self.connection
+        tenant_id = connection.extra_dejson.get("tenantId")
+        if not connection.login:
+            raise ValueError("Client ID is required for Azure Analysis 
Services authentication")
+        if not connection.password:
+            raise ValueError("Client secret is required for Azure Analysis 
Services authentication")
+        if not isinstance(tenant_id, str) or not tenant_id:
+            raise ValueError("Tenant ID is required for Azure Analysis 
Services authentication")
+
+        self._credential = ClientSecretCredential(
+            tenant_id=tenant_id,
+            client_id=connection.login,
+            client_secret=connection.password,
+        )
+        return self._credential
+
+    async def aclose(self) -> None:
+        """Release the HTTP client and the credential."""
+        client = self._client
+        credential = self._credential
+        self._client = None
+        self._credential = None
+        try:
+            if client is not None:
+                await client.aclose()
+        finally:
+            if credential is not None:
+                await credential.close()
+
+    async def get_refresh_status(self, server_name: str, database: str, 
refresh_id: str) -> str:
+        """Return the validated status of an Azure Analysis Services model 
refresh."""
+        refresh_url = f"{self._get_refreshes_url(server_name, 
database)}/{quote(refresh_id, safe='')}"
+        try:
+            response = await self.get_conn().get(refresh_url, headers=await 
self._get_headers())
+            response.raise_for_status()
+        except httpx.HTTPError as error:
+            raise AzureAnalysisServicesRefreshException(
+                f"Failed to get status for Azure Analysis Services refresh 
{refresh_id}: "
+                f"{_format_request_error(error)}"
+            ) from error
+
+        try:
+            response_body = response.json()
+        except ValueError as error:
+            raise AzureAnalysisServicesRefreshException(
+                f"Azure Analysis Services returned a non-JSON status response 
for refresh {refresh_id}"
+            ) from error
+
+        if not isinstance(response_body, dict):
+            raise AzureAnalysisServicesRefreshException(
+                f"Azure Analysis Services returned an invalid status response 
for refresh {refresh_id}"
+            )
+        status = response_body.get("status")
+        if not isinstance(status, str) or status not in 
AzureAnalysisServicesRefreshStatus.VALID_STATUSES:
+            raise AzureAnalysisServicesRefreshException(
+                f"Azure Analysis Services returned unknown status {status!r} 
for refresh {refresh_id}"
+            )
+        return status
+
+    async def wait_for_refresh(
+        self, server_name: str, database: str, refresh_id: str, poke_interval: 
float
+    ) -> str:
+        """Poll until the refresh reaches a terminal status and return it."""
+        while True:
+            status = await self.get_refresh_status(
+                server_name=server_name,
+                database=database,
+                refresh_id=refresh_id,
+            )
+            self.log.info("Refresh %s status: %s", refresh_id, status)
+            if (
+                status == AzureAnalysisServicesRefreshStatus.SUCCEEDED
+                or status in 
AzureAnalysisServicesRefreshStatus.FAILURE_STATUSES
+            ):
+                return status
+            await asyncio.sleep(poke_interval)
+
+    async def trigger_refresh(
+        self, server_name: str, database: str, refresh_type: RefreshType = 
"full"
+    ) -> str:
+        """Trigger a model refresh and return its refresh ID."""
+        if refresh_type not in VALID_REFRESH_TYPES:
+            raise ValueError(
+                f"Invalid refresh_type {refresh_type!r}. Valid values are: 
{sorted(VALID_REFRESH_TYPES)}"
+            )
+
+        try:
+            response = await self.get_conn().post(
+                self._get_refreshes_url(server_name, database),
+                json={"Type": refresh_type},
+                headers=await self._get_headers(),
+            )
+            response.raise_for_status()
+        except httpx.HTTPError as error:
+            raise AzureAnalysisServicesRefreshException(
+                f"Failed to trigger an Azure Analysis Services model refresh: 
{_format_request_error(error)}"
+            ) from error
+
+        location = response.headers.get("Location")
+        if not location:
+            raise AzureAnalysisServicesRefreshException(
+                "Azure Analysis Services did not return a refresh ID in the 
Location header"
+            )
+        try:
+            location_parts = [part for part in 
urlsplit(location).path.split("/") if part]
+        except ValueError as error:
+            raise AzureAnalysisServicesRefreshException(
+                "Azure Analysis Services returned an invalid refresh Location 
header"
+            ) from error
+        if len(location_parts) < 2 or location_parts[-2] != "refreshes":
+            raise AzureAnalysisServicesRefreshException(
+                "Azure Analysis Services returned an invalid refresh Location 
header"
+            )
+        return unquote(location_parts[-1])
+
+    @staticmethod
+    def _assert_host(host: str) -> None:
+        parsed_host = urlsplit(f"//{host}")
+        # netloc, not .username/.port: those miss "@host" and ":0", and raise 
on ":abc".
+        if (
+            not host
+            or not parsed_host.hostname
+            or "@" in parsed_host.netloc
+            or ":" in parsed_host.netloc
+            or parsed_host.path
+            or parsed_host.query
+            or parsed_host.fragment
+        ):
+            raise ValueError(
+                "A valid region endpoint without a URL scheme, credentials, 
port, or path is "
+                "required in the Azure Analysis Services connection host"
+            )
+
+    def _get_base_url(self) -> str:
+        host = (self.connection.host or "").strip().rstrip("/")
+        self._assert_host(host)
+        return f"https://{host}";
+
+    async def _get_headers(self) -> dict[str, str]:
+        try:
+            token = await self._get_credential().get_token(TOKEN_SCOPE)
+        except AzureError as error:
+            raise AzureAnalysisServicesRefreshException(
+                "Failed to authenticate with Azure Analysis Services"
+            ) from error
+        return {
+            "Authorization": f"Bearer {token.token}",
+            "Content-Type": "application/json",
+        }
+
+    def _get_refreshes_url(self, server_name: str, database: str) -> str:
+        if not server_name:
+            raise ValueError("server_name must not be empty")
+        if not database:
+            raise ValueError("database must not be empty")
+        return (
+            f"{self._get_base_url()}/servers/{quote(server_name, safe='')}"
+            f"/models/{quote(database, safe='')}/refreshes"
+        )
diff --git 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/operators/analysis_services.py
 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/operators/analysis_services.py
new file mode 100644
index 00000000000..d78738bd8ff
--- /dev/null
+++ 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/operators/analysis_services.py
@@ -0,0 +1,136 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from collections.abc import Sequence
+from datetime import timedelta
+from typing import TYPE_CHECKING, Any
+
+from airflow.providers.common.compat.sdk import BaseOperator
+from airflow.providers.microsoft.azure.hooks.analysis_services import (
+    AzureAnalysisServicesHook,
+    RefreshType,
+)
+from airflow.providers.microsoft.azure.triggers.analysis_services import (
+    AzureAnalysisServicesRefreshTrigger,
+    validate_completed_refresh_event,
+    validate_refresh_event,
+)
+
+if TYPE_CHECKING:
+    from airflow.sdk import Context
+
+
+class AzureAnalysisServicesRefreshOperator(BaseOperator):
+    """
+    Trigger an Azure Analysis Services model refresh and optionally wait for 
completion.
+
+    The operator always runs deferred: both the request that starts the 
refresh and the status
+    polling happen in the triggerer, so no worker slot is held while the model 
is refreshing.
+    A triggerer must therefore be running in the deployment.
+
+    .. seealso::
+        For more information, see
+        :ref:`howto/operator:AzureAnalysisServicesRefreshOperator`.
+
+    :param server_name: The Analysis Services server name.
+    :param database: The model database name.
+    :param azure_analysis_services_conn_id: The Azure Analysis Services 
connection ID.
+    :param refresh_type: The processing type to request.
+    :param wait_for_termination: Wait for the refresh to reach a terminal 
status.
+    :param check_interval: Time in seconds between status requests.
+    :param timeout: Maximum time in seconds to wait for the refresh to 
complete. The clock starts
+        once the refresh has been submitted.
+    :param request_timeout: Timeout in seconds for each HTTP request.
+    """
+
+    template_fields: Sequence[str] = (
+        "azure_analysis_services_conn_id",
+        "server_name",
+        "database",
+        "refresh_type",
+    )
+    ui_color = "#0078d4"
+    ui_fgcolor = "#ffffff"
+
+    def __init__(
+        self,
+        *,
+        server_name: str,
+        database: str,
+        azure_analysis_services_conn_id: str = 
AzureAnalysisServicesHook.default_conn_name,
+        refresh_type: RefreshType = "full",
+        wait_for_termination: bool = True,
+        check_interval: float = 60,
+        timeout: float = 60 * 60 * 24 * 7,
+        request_timeout: float = 60,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        if check_interval <= 0:
+            raise ValueError("check_interval must be greater than zero")
+        if timeout <= 0:
+            raise ValueError("timeout must be greater than zero")
+        if request_timeout <= 0:
+            raise ValueError("request_timeout must be greater than zero")
+        self.server_name = server_name
+        self.database = database
+        self.azure_analysis_services_conn_id = azure_analysis_services_conn_id
+        self.refresh_type = refresh_type
+        self.wait_for_termination = wait_for_termination
+        self.check_interval = check_interval
+        self.timeout = timeout
+        self.request_timeout = request_timeout
+
+    def execute(self, context: Context) -> None:
+        """Defer to the trigger so the refresh is submitted off the worker."""
+        self.defer(
+            trigger=self._build_trigger(refresh_id=None),
+            method_name=self.handle_refresh.__name__,
+        )
+
+    def handle_refresh(self, context: Context, event: dict[str, Any] | None) 
-> str | None:
+        """Record the new refresh ID and defer again when the refresh has to 
be awaited."""
+        refresh_id = validate_refresh_event(event)
+        self.log.info("Triggered Azure Analysis Services refresh %s", 
refresh_id)
+        context["ti"].xcom_push(key=f"{self.task_id}.refresh_id", 
value=refresh_id)
+        if not self.wait_for_termination:
+            return refresh_id
+
+        # The timeout covers waiting for the refresh, so it starts once it has 
been submitted.
+        self.defer(
+            timeout=timedelta(seconds=self.timeout),
+            trigger=self._build_trigger(refresh_id=refresh_id),
+            method_name=self.execute_complete.__name__,
+        )
+
+    def _build_trigger(self, *, refresh_id: str | None) -> 
AzureAnalysisServicesRefreshTrigger:
+        return AzureAnalysisServicesRefreshTrigger(
+            conn_id=self.azure_analysis_services_conn_id,
+            server_name=self.server_name,
+            database=self.database,
+            refresh_id=refresh_id,
+            refresh_type=self.refresh_type,
+            poke_interval=self.check_interval,
+            request_timeout=self.request_timeout,
+        )
+
+    def execute_complete(self, context: Context, event: dict[str, Any] | None) 
-> str:
+        """Validate the terminal trigger event and return the refresh ID."""
+        refresh_id = validate_completed_refresh_event(event)
+        self.log.info("Azure Analysis Services refresh %s completed 
successfully", refresh_id)
+        return refresh_id
diff --git 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/sensors/analysis_services.py
 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/sensors/analysis_services.py
new file mode 100644
index 00000000000..2e3ce44ed35
--- /dev/null
+++ 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/sensors/analysis_services.py
@@ -0,0 +1,98 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from collections.abc import Sequence
+from datetime import timedelta
+from typing import TYPE_CHECKING, Any
+
+from airflow.providers.common.compat.sdk import BaseSensorOperator
+from airflow.providers.microsoft.azure.hooks.analysis_services import 
AzureAnalysisServicesHook
+from airflow.providers.microsoft.azure.triggers.analysis_services import (
+    AzureAnalysisServicesRefreshTrigger,
+    validate_completed_refresh_event,
+)
+
+if TYPE_CHECKING:
+    from airflow.sdk import Context
+
+
+class AzureAnalysisServicesSensor(BaseSensorOperator):
+    """
+    Wait for an Azure Analysis Services model refresh to finish.
+
+    The sensor always runs deferred: status polling happens in the triggerer, 
so no worker slot is
+    held while the model is refreshing. A triggerer must therefore be running 
in the deployment.
+
+    .. seealso::
+        For more information, see
+        :ref:`howto/sensor:AzureAnalysisServicesSensor`.
+
+    :param server_name: The Analysis Services server name.
+    :param database: The model database name.
+    :param refresh_id: The refresh operation ID to monitor.
+    :param azure_analysis_services_conn_id: The Azure Analysis Services 
connection ID.
+    :param request_timeout: Timeout in seconds for each HTTP request.
+    """
+
+    template_fields: Sequence[str] = (
+        "azure_analysis_services_conn_id",
+        "server_name",
+        "database",
+        "refresh_id",
+    )
+    ui_color = "#0078d4"
+    ui_fgcolor = "#ffffff"
+
+    def __init__(
+        self,
+        *,
+        server_name: str,
+        database: str,
+        refresh_id: str,
+        azure_analysis_services_conn_id: str = 
AzureAnalysisServicesHook.default_conn_name,
+        request_timeout: float = 60,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        if request_timeout <= 0:
+            raise ValueError("request_timeout must be greater than zero")
+        self.server_name = server_name
+        self.database = database
+        self.refresh_id = refresh_id
+        self.azure_analysis_services_conn_id = azure_analysis_services_conn_id
+        self.request_timeout = request_timeout
+
+    def execute(self, context: Context) -> None:
+        """Defer status polling to the triggerer."""
+        self.defer(
+            timeout=timedelta(seconds=self.timeout),
+            trigger=AzureAnalysisServicesRefreshTrigger(
+                conn_id=self.azure_analysis_services_conn_id,
+                server_name=self.server_name,
+                database=self.database,
+                refresh_id=self.refresh_id,
+                poke_interval=self.poke_interval,
+                request_timeout=self.request_timeout,
+            ),
+            method_name=self.execute_complete.__name__,
+        )
+
+    def execute_complete(self, context: Context, event: dict[str, Any] | None) 
-> None:
+        """Validate the terminal trigger event."""
+        refresh_id = validate_completed_refresh_event(event)
+        self.log.info("Azure Analysis Services refresh %s completed 
successfully", refresh_id)
diff --git 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/triggers/analysis_services.py
 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/triggers/analysis_services.py
new file mode 100644
index 00000000000..666859656fd
--- /dev/null
+++ 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/triggers/analysis_services.py
@@ -0,0 +1,182 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from collections.abc import AsyncIterator
+from typing import Any
+
+from airflow.providers.microsoft.azure.hooks.analysis_services import (
+    AzureAnalysisServicesHook,
+    AzureAnalysisServicesRefreshException,
+    AzureAnalysisServicesRefreshStatus,
+    RefreshType,
+)
+from airflow.triggers.base import BaseTrigger, TriggerEvent
+
+
+def validate_refresh_event(event: dict[str, Any] | None) -> str:
+    """Validate a trigger event and return its refresh ID."""
+    if not isinstance(event, dict):
+        raise AzureAnalysisServicesRefreshException(
+            "Did not receive a valid event from the Azure Analysis Services 
trigger"
+        )
+
+    # Errors are reported before the refresh ID is validated: a failed POST 
yields an event
+    # without one, and its message is the only useful diagnostic.
+    event_status = event.get("status")
+    if event_status == "error":
+        message = event.get("message")
+        if not isinstance(message, str) or not message:
+            message = "Azure Analysis Services refresh failed"
+        raise AzureAnalysisServicesRefreshException(message)
+    if event_status != "success":
+        raise AzureAnalysisServicesRefreshException(
+            f"Azure Analysis Services trigger returned unknown event status 
{event_status!r}"
+        )
+
+    refresh_id = event.get("refresh_id")
+    if not isinstance(refresh_id, str) or not refresh_id:
+        raise AzureAnalysisServicesRefreshException(
+            "Azure Analysis Services trigger event did not contain a valid 
refresh ID"
+        )
+    return refresh_id
+
+
+def validate_completed_refresh_event(event: dict[str, Any] | None) -> str:
+    """Validate a terminal trigger event and return the completed refresh 
ID."""
+    refresh_id = validate_refresh_event(event)
+
+    refresh_status = (event or {}).get("refresh_status")
+    if refresh_status != AzureAnalysisServicesRefreshStatus.SUCCEEDED:
+        raise AzureAnalysisServicesRefreshException(
+            f"Azure Analysis Services trigger returned unexpected refresh 
status {refresh_status!r}"
+        )
+    return refresh_id
+
+
+class AzureAnalysisServicesRefreshTrigger(BaseTrigger):
+    """
+    Poll an Azure Analysis Services model refresh until it reaches a terminal 
status.
+
+    When ``refresh_id`` is ``None`` the trigger starts a new refresh and 
yields its ID without
+    polling; the caller defers again with that ID to wait for completion. 
Serializing the actual
+    refresh ID is what makes the polling stage survive a triggerer restart.
+
+    :param conn_id: The Azure Analysis Services connection ID.
+    :param server_name: The Analysis Services server name.
+    :param database: The model database name.
+    :param refresh_id: The refresh operation ID to poll, or ``None`` to start 
a new refresh.
+    :param refresh_type: The refresh type used when starting a new refresh.
+    :param poke_interval: Time in seconds between status requests.
+    :param request_timeout: Timeout in seconds for each HTTP request.
+    """
+
+    def __init__(
+        self,
+        *,
+        conn_id: str,
+        server_name: str,
+        database: str,
+        refresh_id: str | None = None,
+        refresh_type: RefreshType = "full",
+        poke_interval: float = 60,
+        request_timeout: float = 60,
+    ) -> None:
+        super().__init__()
+        if poke_interval <= 0:
+            raise ValueError("poke_interval must be greater than zero")
+        if request_timeout <= 0:
+            raise ValueError("request_timeout must be greater than zero")
+        self.conn_id = conn_id
+        self.server_name = server_name
+        self.database = database
+        self.refresh_id = refresh_id
+        self.refresh_type = refresh_type
+        self.poke_interval = poke_interval
+        self.request_timeout = request_timeout
+
+    def serialize(self) -> tuple[str, dict[str, Any]]:
+        """Serialize the trigger arguments and classpath."""
+        return (
+            f"{self.__class__.__module__}.{self.__class__.__name__}",
+            {
+                "conn_id": self.conn_id,
+                "server_name": self.server_name,
+                "database": self.database,
+                "refresh_id": self.refresh_id,
+                "refresh_type": self.refresh_type,
+                "poke_interval": self.poke_interval,
+                "request_timeout": self.request_timeout,
+            },
+        )
+
+    async def run(self) -> AsyncIterator[TriggerEvent]:
+        """Start the refresh when needed, then delegate polling to the hook."""
+        hook = AzureAnalysisServicesHook(
+            azure_analysis_services_conn_id=self.conn_id,
+            request_timeout=self.request_timeout,
+        )
+        refresh_id = self.refresh_id
+        try:
+            if refresh_id is None:
+                refresh_id = await hook.trigger_refresh(
+                    server_name=self.server_name,
+                    database=self.database,
+                    refresh_type=self.refresh_type,
+                )
+                self.log.info("Triggered Azure Analysis Services refresh %s", 
refresh_id)
+                yield TriggerEvent(
+                    {
+                        "status": "success",
+                        "refresh_status": None,
+                        "message": f"Refresh {refresh_id} has been triggered",
+                        "refresh_id": refresh_id,
+                    }
+                )
+                return
+
+            status = await hook.wait_for_refresh(
+                server_name=self.server_name,
+                database=self.database,
+                refresh_id=refresh_id,
+                poke_interval=self.poke_interval,
+            )
+            is_success = status == AzureAnalysisServicesRefreshStatus.SUCCEEDED
+            yield TriggerEvent(
+                {
+                    "status": "success" if is_success else "error",
+                    "refresh_status": status,
+                    "message": (
+                        f"Refresh {refresh_id} completed successfully"
+                        if is_success
+                        else f"Refresh {refresh_id} finished with status 
{status}"
+                    ),
+                    "refresh_id": refresh_id,
+                }
+            )
+        except Exception as error:
+            message = str(error) or type(error).__name__
+            yield TriggerEvent(
+                {
+                    "status": "error",
+                    "refresh_status": None,
+                    "message": message,
+                    "refresh_id": refresh_id,
+                }
+            )
+        finally:
+            await hook.aclose()
diff --git 
a/providers/microsoft/azure/tests/system/microsoft/azure/example_azure_analysis_services.py
 
b/providers/microsoft/azure/tests/system/microsoft/azure/example_azure_analysis_services.py
new file mode 100644
index 00000000000..15ebda3bc6d
--- /dev/null
+++ 
b/providers/microsoft/azure/tests/system/microsoft/azure/example_azure_analysis_services.py
@@ -0,0 +1,80 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+from datetime import datetime
+from typing import TYPE_CHECKING, cast
+
+from airflow.providers.microsoft.azure.operators.analysis_services import (
+    AzureAnalysisServicesRefreshOperator,
+)
+from airflow.providers.microsoft.azure.sensors.analysis_services import 
AzureAnalysisServicesSensor
+from airflow.sdk import DAG
+
+if TYPE_CHECKING:
+    from airflow.providers.microsoft.azure.hooks.analysis_services import 
RefreshType
+
+DAG_ID = "example_azure_analysis_services"
+SERVER_NAME = os.environ.get("AZURE_ANALYSIS_SERVICES_SERVER_NAME", 
"testserver")
+DATABASE = os.environ.get("AZURE_ANALYSIS_SERVICES_DATABASE", "adventureworks")
+REFRESH_TYPE = cast("RefreshType", 
os.environ.get("AZURE_ANALYSIS_SERVICES_REFRESH_TYPE", "calculate"))
+
+with DAG(
+    dag_id=DAG_ID,
+    schedule=None,
+    start_date=datetime(2026, 1, 1),
+    catchup=False,
+    tags=["example", "azure", "analysis-services"],
+) as dag:
+    # [START howto_operator_azure_analysis_services_refresh]
+    start_refresh_without_wait = AzureAnalysisServicesRefreshOperator(
+        task_id="start_refresh_without_wait",
+        server_name=SERVER_NAME,
+        database=DATABASE,
+        refresh_type=REFRESH_TYPE,
+        wait_for_termination=False,
+    )
+    # [END howto_operator_azure_analysis_services_refresh]
+
+    # [START howto_sensor_azure_analysis_services_refresh]
+    wait_for_refresh = AzureAnalysisServicesSensor(
+        task_id="wait_for_refresh",
+        server_name=SERVER_NAME,
+        database=DATABASE,
+        refresh_id=start_refresh_without_wait.output,
+        poke_interval=10,
+        timeout=600,
+    )
+    # [END howto_sensor_azure_analysis_services_refresh]
+
+    # [START howto_operator_azure_analysis_services_refresh_and_wait]
+    refresh_and_wait = AzureAnalysisServicesRefreshOperator(
+        task_id="refresh_and_wait",
+        server_name=SERVER_NAME,
+        database=DATABASE,
+        refresh_type=REFRESH_TYPE,
+        check_interval=10,
+        timeout=600,
+    )
+    # [END howto_operator_azure_analysis_services_refresh_and_wait]
+
+    start_refresh_without_wait >> wait_for_refresh >> refresh_and_wait
+
+from tests_common.test_utils.system_tests import get_test_run  # noqa: E402
+
+test_run = get_test_run(dag)
diff --git 
a/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_analysis_services.py
 
b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_analysis_services.py
new file mode 100644
index 00000000000..c1b0d57d5ee
--- /dev/null
+++ 
b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_analysis_services.py
@@ -0,0 +1,500 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import json
+from unittest import mock
+
+import httpx
+import pytest
+from azure.core.credentials import AccessToken
+from azure.core.exceptions import ClientAuthenticationError
+
+from airflow.models import Connection
+from airflow.providers.microsoft.azure.hooks.analysis_services import (
+    TOKEN_SCOPE,
+    VALID_REFRESH_TYPES,
+    AzureAnalysisServicesHook,
+    AzureAnalysisServicesRefreshException,
+    AzureAnalysisServicesRefreshStatus,
+)
+
+CONN_ID = "azure_analysis_services_test"
+HOST = "westus.asazure.windows.net"
+SERVER_NAME = "testserver"
+DATABASE = "Adventure Works"
+REFRESH_ID = "refresh-id"
+POKE_INTERVAL = 5
+REQUEST_TIMEOUT = 30
+HEADERS = {"Authorization": "Bearer token", "Content-Type": "application/json"}
+MODULE = "airflow.providers.microsoft.azure.hooks.analysis_services"
+
+
+def build_response(
+    *, headers: dict[str, str] | None = None, body: object | None = None, 
text: str = ""
+) -> mock.Mock:
+    """Build a response mock with the requested headers and JSON body."""
+    response = mock.Mock(spec=httpx.Response)
+    response.headers = headers or {}
+    response.json.return_value = body
+    response.text = text
+    return response
+
+
+def build_client(client_class: mock.MagicMock) -> mock.MagicMock:
+    """Return the ``httpx.AsyncClient`` instance that the patched class hands 
to the hook."""
+    return client_class.return_value
+
+
+class TestAzureAnalysisServicesHook:
+    @pytest.fixture(autouse=True)
+    def setup_connection(self, create_mock_connection):
+        create_mock_connection(
+            Connection(
+                conn_id=CONN_ID,
+                conn_type="azure_analysis_services",
+                host=HOST,
+                login="client-id",
+                password="client-secret",
+                extra={"tenantId": "tenant-id"},
+            )
+        )
+
+    @pytest.mark.parametrize("request_timeout", [0, -1])
+    def test_rejects_invalid_request_timeout(self, request_timeout):
+        with pytest.raises(ValueError, match="request_timeout must be greater 
than zero"):
+            AzureAnalysisServicesHook(CONN_ID, request_timeout=request_timeout)
+
+    def test_defines_connection_form_widget(self):
+        pytest.importorskip("flask_appbuilder")
+        assert set(AzureAnalysisServicesHook.get_connection_form_widgets()) == 
{"tenantId"}
+
+    def test_defines_connection_ui_field_behaviour(self):
+        assert AzureAnalysisServicesHook.get_ui_field_behaviour() == {
+            "hidden_fields": ["schema", "port", "extra"],
+            "relabeling": {
+                "host": "Region Endpoint",
+                "login": "Client ID",
+                "password": "Client Secret",
+            },
+            "placeholders": {"host": "westus.asazure.windows.net"},
+        }
+
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    def test_get_conn_creates_and_caches_client(self, client_class):
+        hook = AzureAnalysisServicesHook(CONN_ID, 
request_timeout=REQUEST_TIMEOUT)
+
+        first_client = hook.get_conn()
+        second_client = hook.get_conn()
+
+        client_class.assert_called_once_with(timeout=REQUEST_TIMEOUT)
+        assert first_client is client_class.return_value
+        assert second_client is first_client
+
+    @mock.patch(f"{MODULE}.ClientSecretCredential", autospec=True)
+    def test_get_credential_creates_and_caches_credential(self, 
credential_class):
+        hook = AzureAnalysisServicesHook(CONN_ID)
+
+        first_credential = hook._get_credential()
+        second_credential = hook._get_credential()
+
+        credential_class.assert_called_once_with(
+            tenant_id="tenant-id",
+            client_id="client-id",
+            client_secret="client-secret",
+        )
+        assert first_credential is credential_class.return_value
+        assert second_credential is first_credential
+
+    @mock.patch(f"{MODULE}.BaseHook.get_connection", autospec=True)
+    def test_caches_connection(self, get_connection):
+        get_connection.return_value = Connection(
+            conn_id=CONN_ID,
+            conn_type="azure_analysis_services",
+            host=HOST,
+            login="client-id",
+            password="client-secret",
+            extra={"tenantId": "tenant-id"},
+        )
+        hook = AzureAnalysisServicesHook(CONN_ID)
+
+        first_connection = hook.connection
+        second_connection = hook.connection
+
+        get_connection.assert_called_once_with(CONN_ID)
+        assert second_connection is first_connection
+
+    @pytest.mark.parametrize(
+        ("login", "password", "extra", "message"),
+        [
+            (None, "secret", {"tenantId": "tenant"}, "Client ID is required"),
+            ("client", None, {"tenantId": "tenant"}, "Client secret is 
required"),
+            ("client", "secret", {}, "Tenant ID is required"),
+            ("client", "secret", {"tenantId": 123}, "Tenant ID is required"),
+        ],
+    )
+    def test_get_credential_requires_service_principal_fields(
+        self, create_mock_connection, login, password, extra, message
+    ):
+        create_mock_connection(
+            Connection(
+                conn_id="invalid-auth",
+                conn_type="azure_analysis_services",
+                host=HOST,
+                login=login,
+                password=password,
+                extra=extra,
+            )
+        )
+
+        with pytest.raises(ValueError, match=message):
+            AzureAnalysisServicesHook("invalid-auth")._get_credential()
+
+    @pytest.mark.asyncio
+    @mock.patch(f"{MODULE}.ClientSecretCredential", autospec=True)
+    async def test_get_headers_uses_literal_token_scope(self, 
credential_class):
+        credential_class.return_value.get_token.return_value = 
AccessToken("token", 0)
+
+        headers = await AzureAnalysisServicesHook(CONN_ID)._get_headers()
+
+        
credential_class.return_value.get_token.assert_awaited_once_with(TOKEN_SCOPE)
+        assert TOKEN_SCOPE == "https://*.asazure.windows.net/.default";
+        assert headers == HEADERS
+
+    @pytest.mark.asyncio
+    @mock.patch(f"{MODULE}.ClientSecretCredential", autospec=True)
+    async def test_get_headers_wraps_authentication_errors(self, 
credential_class):
+        credential_class.return_value.get_token.side_effect = 
ClientAuthenticationError("bad credential")
+
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="Failed to authenticate"):
+            await AzureAnalysisServicesHook(CONN_ID)._get_headers()
+
+    @pytest.mark.parametrize(
+        "host",
+        [
+            None,
+            "",
+            "https://westus.asazure.windows.net";,
+            "host/path",
+            "[email protected]",
+            "@evil.example",
+            "evil.example:9999",
+            "host:abc",
+            "evil.example:0",
+        ],
+    )
+    def test_rejects_invalid_region_endpoint(self, host, 
create_mock_connection):
+        create_mock_connection(
+            Connection(
+                conn_id="invalid-host",
+                conn_type="azure_analysis_services",
+                host=host,
+                login="client-id",
+                password="client-secret",
+                extra={"tenantId": "tenant-id"},
+            )
+        )
+
+        with pytest.raises(ValueError, match="valid region endpoint"):
+            AzureAnalysisServicesHook("invalid-host")._get_base_url()
+
+    def test_strips_trailing_slash_from_region_endpoint(self, 
create_mock_connection):
+        create_mock_connection(
+            Connection(
+                conn_id="trailing-slash",
+                conn_type="azure_analysis_services",
+                host=f"{HOST}/",
+                login="client-id",
+                password="client-secret",
+                extra={"tenantId": "tenant-id"},
+            )
+        )
+
+        assert AzureAnalysisServicesHook("trailing-slash")._get_base_url() == 
f"https://{HOST}";
+
+    @pytest.mark.parametrize(
+        ("server_name", "database", "message"), [("", "db", "server_name"), 
("s", "", "database")]
+    )
+    def test_rejects_empty_resource_names(self, server_name, database, 
message):
+        with pytest.raises(ValueError, match=message):
+            AzureAnalysisServicesHook(CONN_ID)._get_refreshes_url(server_name, 
database)
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize("refresh_type", sorted(VALID_REFRESH_TYPES))
+    @mock.patch.object(AzureAnalysisServicesHook, "_get_headers", 
autospec=True, return_value=HEADERS)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_trigger_refresh_posts_official_request_body(self, 
client_class, get_headers, refresh_type):
+        client = build_client(client_class)
+        client.post.return_value = build_response(
+            headers={
+                "Location": 
f"https://{HOST}/servers/{SERVER_NAME}/models/Adventure%20Works/refreshes/{REFRESH_ID}";
+            }
+        )
+        hook = AzureAnalysisServicesHook(CONN_ID, 
request_timeout=REQUEST_TIMEOUT)
+
+        result = await hook.trigger_refresh(SERVER_NAME, DATABASE, 
refresh_type)
+
+        assert result == REFRESH_ID
+        client_class.assert_called_once_with(timeout=REQUEST_TIMEOUT)
+        client.post.assert_awaited_once_with(
+            
f"https://{HOST}/servers/{SERVER_NAME}/models/Adventure%20Works/refreshes";,
+            json={"Type": refresh_type},
+            headers=HEADERS,
+        )
+
+    @pytest.mark.asyncio
+    async def test_trigger_refresh_rejects_unknown_type(self):
+        with pytest.raises(ValueError, match="Invalid refresh_type"):
+            await 
AzureAnalysisServicesHook(CONN_ID).trigger_refresh(SERVER_NAME, DATABASE, 
"invalid")
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize(
+        "location",
+        [None, "", 
f"https://{HOST}/servers/{SERVER_NAME}/models/{DATABASE}/refreshes/";, 
"https://[";],
+    )
+    @mock.patch.object(AzureAnalysisServicesHook, "_get_headers", 
autospec=True, return_value=HEADERS)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_trigger_refresh_rejects_invalid_location(self, 
client_class, get_headers, location):
+        build_client(client_class).post.return_value = build_response(
+            headers={"Location": location} if location is not None else {}
+        )
+
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="Location header"):
+            await 
AzureAnalysisServicesHook(CONN_ID).trigger_refresh(SERVER_NAME, DATABASE)
+
+    @pytest.mark.asyncio
+    @mock.patch.object(AzureAnalysisServicesHook, "_get_headers", 
autospec=True, return_value=HEADERS)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_trigger_refresh_decodes_refresh_id(self, client_class, 
get_headers):
+        build_client(client_class).post.return_value = build_response(
+            headers={"Location": 
f"https://{HOST}/models/model/refreshes/refresh%20id"}
+        )
+
+        result = await 
AzureAnalysisServicesHook(CONN_ID).trigger_refresh(SERVER_NAME, DATABASE)
+
+        assert result == "refresh id"
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize("error", [httpx.ConnectError("offline"), 
httpx.ReadTimeout("slow")])
+    @mock.patch.object(AzureAnalysisServicesHook, "_get_headers", 
autospec=True, return_value=HEADERS)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_trigger_refresh_wraps_request_errors(self, client_class, 
get_headers, error):
+        build_client(client_class).post.side_effect = error
+
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="Failed to trigger"):
+            await 
AzureAnalysisServicesHook(CONN_ID).trigger_refresh(SERVER_NAME, DATABASE)
+
+    @pytest.mark.asyncio
+    @mock.patch.object(AzureAnalysisServicesHook, "_get_headers", 
autospec=True, return_value=HEADERS)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def 
test_trigger_refresh_includes_truncated_http_error_response(self, client_class, 
get_headers):
+        response_text = "A refresh is already running. " + "x" * 1000
+        response = build_response(text=response_text)
+        response.raise_for_status.side_effect = httpx.HTTPStatusError(
+            "409 Client Error: Conflict", 
request=mock.Mock(spec=httpx.Request), response=response
+        )
+        build_client(client_class).post.return_value = response
+
+        with pytest.raises(AzureAnalysisServicesRefreshException) as error:
+            await 
AzureAnalysisServicesHook(CONN_ID).trigger_refresh(SERVER_NAME, DATABASE)
+
+        assert "409 Client Error: Conflict" in str(error.value)
+        assert f"response body: {response_text[:1000]}" in str(error.value)
+        assert response_text[:1001] not in str(error.value)
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize("status", 
sorted(AzureAnalysisServicesRefreshStatus.VALID_STATUSES))
+    @mock.patch.object(AzureAnalysisServicesHook, "_get_headers", 
autospec=True, return_value=HEADERS)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_get_refresh_status_returns_valid_status(self, client_class, 
get_headers, status):
+        client = build_client(client_class)
+        client.get.return_value = build_response(body={"status": status})
+        hook = AzureAnalysisServicesHook(CONN_ID, 
request_timeout=REQUEST_TIMEOUT)
+
+        result = await hook.get_refresh_status(SERVER_NAME, DATABASE, "refresh 
id")
+
+        assert result == status
+        client_class.assert_called_once_with(timeout=REQUEST_TIMEOUT)
+        client.get.assert_awaited_once_with(
+            
f"https://{HOST}/servers/{SERVER_NAME}/models/Adventure%20Works/refreshes/refresh%20id";,
+            headers=HEADERS,
+        )
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize(
+        ("body", "message"),
+        [
+            ([], "invalid status response"),
+            ({}, "unknown status None"),
+            ({"status": "unexpected"}, "unknown status 'unexpected'"),
+            ({"status": 1}, "unknown status 1"),
+        ],
+    )
+    @mock.patch.object(AzureAnalysisServicesHook, "_get_headers", 
autospec=True, return_value=HEADERS)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_get_refresh_status_rejects_invalid_response(
+        self, client_class, get_headers, body, message
+    ):
+        build_client(client_class).get.return_value = build_response(body=body)
+
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match=message):
+            await 
AzureAnalysisServicesHook(CONN_ID).get_refresh_status(SERVER_NAME, DATABASE, 
REFRESH_ID)
+
+    @pytest.mark.asyncio
+    @mock.patch.object(AzureAnalysisServicesHook, "_get_headers", 
autospec=True, return_value=HEADERS)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_get_refresh_status_rejects_non_json_response(self, 
client_class, get_headers):
+        response = build_response()
+        response.json.side_effect = json.JSONDecodeError("bad json", "", 0)
+        build_client(client_class).get.return_value = response
+
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="non-JSON"):
+            await 
AzureAnalysisServicesHook(CONN_ID).get_refresh_status(SERVER_NAME, DATABASE, 
REFRESH_ID)
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize("error", [httpx.ConnectError("offline"), 
httpx.ReadTimeout("slow")])
+    @mock.patch.object(AzureAnalysisServicesHook, "_get_headers", 
autospec=True, return_value=HEADERS)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_get_refresh_status_wraps_request_errors(self, client_class, 
get_headers, error):
+        build_client(client_class).get.side_effect = error
+
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="Failed to get status"):
+            await 
AzureAnalysisServicesHook(CONN_ID).get_refresh_status(SERVER_NAME, DATABASE, 
REFRESH_ID)
+
+    @pytest.mark.asyncio
+    @mock.patch.object(AzureAnalysisServicesHook, "_get_headers", 
autospec=True, return_value=HEADERS)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_get_refresh_status_includes_http_error_response(self, 
client_class, get_headers):
+        response = build_response(text='{"error":"model not found"}')
+        response.raise_for_status.side_effect = httpx.HTTPStatusError(
+            "500 Server Error", request=mock.Mock(spec=httpx.Request), 
response=response
+        )
+        build_client(client_class).get.return_value = response
+
+        with pytest.raises(AzureAnalysisServicesRefreshException) as error:
+            await 
AzureAnalysisServicesHook(CONN_ID).get_refresh_status(SERVER_NAME, DATABASE, 
REFRESH_ID)
+
+        assert "500 Server Error" in str(error.value)
+        assert 'response body: {"error":"model not found"}' in str(error.value)
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize(
+        "status",
+        [
+            AzureAnalysisServicesRefreshStatus.SUCCEEDED,
+            *sorted(AzureAnalysisServicesRefreshStatus.FAILURE_STATUSES),
+        ],
+    )
+    @mock.patch(f"{MODULE}.asyncio.sleep", new_callable=mock.AsyncMock)
+    @mock.patch.object(AzureAnalysisServicesHook, "get_refresh_status", 
autospec=True)
+    async def test_wait_for_refresh_returns_terminal_status(self, 
get_refresh_status, sleep, status):
+        get_refresh_status.return_value = status
+
+        result = await AzureAnalysisServicesHook(CONN_ID).wait_for_refresh(
+            SERVER_NAME, DATABASE, REFRESH_ID, POKE_INTERVAL
+        )
+
+        assert result == status
+        get_refresh_status.assert_awaited_once_with(
+            mock.ANY,
+            server_name=SERVER_NAME,
+            database=DATABASE,
+            refresh_id=REFRESH_ID,
+        )
+        sleep.assert_not_awaited()
+
+    @pytest.mark.asyncio
+    @mock.patch(f"{MODULE}.asyncio.sleep", new_callable=mock.AsyncMock)
+    @mock.patch.object(AzureAnalysisServicesHook, "get_refresh_status", 
autospec=True)
+    async def test_wait_for_refresh_polls_until_terminal_status(self, 
get_refresh_status, sleep):
+        get_refresh_status.side_effect = [
+            AzureAnalysisServicesRefreshStatus.NOT_STARTED,
+            AzureAnalysisServicesRefreshStatus.IN_PROGRESS,
+            AzureAnalysisServicesRefreshStatus.SUCCEEDED,
+        ]
+
+        result = await AzureAnalysisServicesHook(CONN_ID).wait_for_refresh(
+            SERVER_NAME, DATABASE, REFRESH_ID, POKE_INTERVAL
+        )
+
+        assert result == AzureAnalysisServicesRefreshStatus.SUCCEEDED
+        assert get_refresh_status.await_count == 3
+        assert sleep.await_args_list == [mock.call(POKE_INTERVAL), 
mock.call(POKE_INTERVAL)]
+
+    @pytest.mark.asyncio
+    @mock.patch.object(AzureAnalysisServicesHook, "_get_headers", 
autospec=True, return_value=HEADERS)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_reuses_single_async_client_across_calls(self, client_class, 
get_headers):
+        client = build_client(client_class)
+        client.get.return_value = build_response(
+            body={"status": AzureAnalysisServicesRefreshStatus.IN_PROGRESS}
+        )
+        hook = AzureAnalysisServicesHook(CONN_ID, 
request_timeout=REQUEST_TIMEOUT)
+
+        await hook.get_refresh_status(SERVER_NAME, DATABASE, REFRESH_ID)
+        await hook.get_refresh_status(SERVER_NAME, DATABASE, REFRESH_ID)
+
+        client_class.assert_called_once_with(timeout=REQUEST_TIMEOUT)
+        assert client.get.await_count == 2
+
+    @pytest.mark.asyncio
+    @mock.patch(f"{MODULE}.ClientSecretCredential", autospec=True)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_aclose_closes_client_and_credential(self, client_class, 
credential_class):
+        hook = AzureAnalysisServicesHook(CONN_ID)
+        client = hook.get_conn()
+        credential = hook._get_credential()
+
+        await hook.aclose()
+
+        client.aclose.assert_awaited_once_with()
+        credential.close.assert_awaited_once_with()
+        assert hook._client is None
+        assert hook._credential is None
+
+    @pytest.mark.asyncio
+    @mock.patch(f"{MODULE}.ClientSecretCredential", autospec=True)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_aclose_closes_credential_even_if_client_fails(self, 
client_class, credential_class):
+        hook = AzureAnalysisServicesHook(CONN_ID)
+        hook.get_conn().aclose.side_effect = RuntimeError("transport already 
gone")
+        credential = hook._get_credential()
+
+        with pytest.raises(RuntimeError, match="transport already gone"):
+            await hook.aclose()
+
+        credential.close.assert_awaited_once_with()
+        assert hook._client is None
+        assert hook._credential is None
+
+    @pytest.mark.asyncio
+    @mock.patch(f"{MODULE}.ClientSecretCredential", autospec=True)
+    @mock.patch(f"{MODULE}.httpx.AsyncClient", autospec=True)
+    async def test_aclose_is_idempotent(self, client_class, credential_class):
+        hook = AzureAnalysisServicesHook(CONN_ID)
+        client = hook.get_conn()
+        credential = hook._get_credential()
+
+        await hook.aclose()
+        await hook.aclose()
+
+        client.aclose.assert_awaited_once_with()
+        credential.close.assert_awaited_once_with()
+
+    @pytest.mark.asyncio
+    async def test_aclose_is_noop_when_nothing_created(self):
+        await AzureAnalysisServicesHook(CONN_ID).aclose()
diff --git 
a/providers/microsoft/azure/tests/unit/microsoft/azure/operators/test_analysis_services.py
 
b/providers/microsoft/azure/tests/unit/microsoft/azure/operators/test_analysis_services.py
new file mode 100644
index 00000000000..7bf216c50a3
--- /dev/null
+++ 
b/providers/microsoft/azure/tests/unit/microsoft/azure/operators/test_analysis_services.py
@@ -0,0 +1,218 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from datetime import timedelta
+from unittest import mock
+
+import pytest
+
+from airflow.providers.common.compat.sdk import TaskDeferred
+from airflow.providers.microsoft.azure.hooks.analysis_services import (
+    AzureAnalysisServicesHook,
+    AzureAnalysisServicesRefreshException,
+    AzureAnalysisServicesRefreshStatus,
+    RefreshType,
+)
+from airflow.providers.microsoft.azure.operators.analysis_services import (
+    AzureAnalysisServicesRefreshOperator,
+)
+from airflow.providers.microsoft.azure.triggers.analysis_services import (
+    AzureAnalysisServicesRefreshTrigger,
+)
+
+from tests_common.test_utils.operators.run_deferrable import execute_operator
+
+CONN_ID = "azure_analysis_services_test"
+SERVER_NAME = "testserver"
+DATABASE = "adventureworks"
+REFRESH_ID = "refresh-id"
+REFRESH_TYPE: RefreshType = "calculate"
+CHECK_INTERVAL = 5
+TIMEOUT = 120
+REQUEST_TIMEOUT = 30
+
+
+def build_operator(
+    *,
+    wait_for_termination: bool = True,
+    check_interval: float = CHECK_INTERVAL,
+    timeout: float = TIMEOUT,
+    request_timeout: float = REQUEST_TIMEOUT,
+) -> AzureAnalysisServicesRefreshOperator:
+    """Build an operator with standard test arguments."""
+    return AzureAnalysisServicesRefreshOperator(
+        task_id="refresh_model",
+        server_name=SERVER_NAME,
+        database=DATABASE,
+        azure_analysis_services_conn_id=CONN_ID,
+        refresh_type=REFRESH_TYPE,
+        wait_for_termination=wait_for_termination,
+        check_interval=check_interval,
+        timeout=timeout,
+        request_timeout=request_timeout,
+    )
+
+
+def build_context() -> dict:
+    """Build a context whose task instance records XCom pushes."""
+    return {"ti": mock.MagicMock()}
+
+
+def success_event(refresh_status: str | None) -> dict:
+    return {
+        "status": "success",
+        "refresh_status": refresh_status,
+        "message": "ok",
+        "refresh_id": REFRESH_ID,
+    }
+
+
+class TestAzureAnalysisServicesRefreshOperator:
+    @pytest.mark.parametrize(
+        ("check_interval", "timeout", "request_timeout", "message"),
+        [
+            (0, TIMEOUT, REQUEST_TIMEOUT, "check_interval"),
+            (CHECK_INTERVAL, 0, REQUEST_TIMEOUT, "timeout"),
+            (CHECK_INTERVAL, TIMEOUT, 0, "request_timeout"),
+        ],
+    )
+    def test_rejects_invalid_polling_arguments(self, check_interval, timeout, 
request_timeout, message):
+        with pytest.raises(ValueError, match=message):
+            build_operator(
+                check_interval=check_interval,
+                timeout=timeout,
+                request_timeout=request_timeout,
+            )
+
+    def test_defines_template_fields(self):
+        assert AzureAnalysisServicesRefreshOperator.template_fields == (
+            "azure_analysis_services_conn_id",
+            "server_name",
+            "database",
+            "refresh_type",
+        )
+
+    def test_execute_defers_without_refresh_id(self):
+        with pytest.raises(TaskDeferred) as deferred:
+            build_operator().execute(context=build_context())
+
+        trigger = deferred.value.trigger
+        assert isinstance(trigger, AzureAnalysisServicesRefreshTrigger)
+        assert trigger.refresh_id is None
+        assert trigger.refresh_type == REFRESH_TYPE
+        assert trigger.poke_interval == CHECK_INTERVAL
+        assert trigger.request_timeout == REQUEST_TIMEOUT
+        assert deferred.value.method_name == "handle_refresh"
+        # The timeout covers waiting for the refresh, so it only starts on the 
second deferral.
+        assert deferred.value.timeout is None
+
+    @mock.patch.object(AzureAnalysisServicesHook, "get_refresh_status", 
autospec=True)
+    @mock.patch.object(AzureAnalysisServicesHook, "trigger_refresh", 
autospec=True)
+    def test_execute_operator_full_lifecycle(self, trigger_refresh, 
get_refresh_status):
+        trigger_refresh.return_value = REFRESH_ID
+        get_refresh_status.return_value = 
AzureAnalysisServicesRefreshStatus.SUCCEEDED
+
+        result, events = execute_operator(build_operator())
+
+        assert result == REFRESH_ID
+        trigger_refresh.assert_awaited_once_with(
+            mock.ANY,
+            server_name=SERVER_NAME,
+            database=DATABASE,
+            refresh_type=REFRESH_TYPE,
+        )
+        get_refresh_status.assert_awaited_once_with(
+            mock.ANY,
+            server_name=SERVER_NAME,
+            database=DATABASE,
+            refresh_id=REFRESH_ID,
+        )
+        assert [event.payload for event in events] == [
+            {
+                "status": "success",
+                "refresh_status": None,
+                "message": f"Refresh {REFRESH_ID} has been triggered",
+                "refresh_id": REFRESH_ID,
+            },
+            {
+                "status": "success",
+                "refresh_status": AzureAnalysisServicesRefreshStatus.SUCCEEDED,
+                "message": f"Refresh {REFRESH_ID} completed successfully",
+                "refresh_id": REFRESH_ID,
+            },
+        ]
+
+    def test_handle_refresh_defers_again_with_refresh_id(self):
+        operator = build_operator()
+
+        with pytest.raises(TaskDeferred) as deferred:
+            operator.handle_refresh(context=build_context(), 
event=success_event(None))
+
+        trigger = deferred.value.trigger
+        # The serialised refresh ID is what lets polling survive a triggerer 
restart.
+        assert trigger.refresh_id == REFRESH_ID
+        assert trigger.serialize()[1]["refresh_id"] == REFRESH_ID
+        assert deferred.value.method_name == "execute_complete"
+
+    def test_handle_refresh_passes_timeout_to_polling_defer(self):
+        with pytest.raises(TaskDeferred) as deferred:
+            build_operator().handle_refresh(context=build_context(), 
event=success_event(None))
+
+        assert deferred.value.timeout == timedelta(seconds=TIMEOUT)
+
+    def test_handle_refresh_pushes_refresh_id_to_xcom(self):
+        operator = build_operator(wait_for_termination=False)
+        context = build_context()
+
+        operator.handle_refresh(context=context, event=success_event(None))
+
+        
context["ti"].xcom_push.assert_called_once_with(key="refresh_model.refresh_id", 
value=REFRESH_ID)
+
+    def test_handle_refresh_returns_without_waiting(self):
+        result = build_operator(wait_for_termination=False).handle_refresh(
+            context=build_context(), event=success_event(None)
+        )
+
+        assert result == REFRESH_ID
+
+    def test_handle_refresh_raises_on_error_event(self):
+        event = {
+            "status": "error",
+            "refresh_status": None,
+            "message": "Failed to trigger an Azure Analysis Services model 
refresh",
+            "refresh_id": None,
+        }
+
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="Failed to trigger"):
+            build_operator().handle_refresh(context=build_context(), 
event=event)
+
+    def test_execute_complete_returns_refresh_id(self):
+        result = build_operator().execute_complete(
+            context=build_context(),
+            event=success_event(AzureAnalysisServicesRefreshStatus.SUCCEEDED),
+        )
+
+        assert result == REFRESH_ID
+
+    @pytest.mark.parametrize(
+        "event",
+        [None, {"status": "success", "refresh_status": 
AzureAnalysisServicesRefreshStatus.FAILED}],
+    )
+    def test_execute_complete_rejects_malformed_event(self, event):
+        with pytest.raises(AzureAnalysisServicesRefreshException):
+            build_operator().execute_complete(context=build_context(), 
event=event)
diff --git 
a/providers/microsoft/azure/tests/unit/microsoft/azure/sensors/test_analysis_services.py
 
b/providers/microsoft/azure/tests/unit/microsoft/azure/sensors/test_analysis_services.py
new file mode 100644
index 00000000000..a86854aab70
--- /dev/null
+++ 
b/providers/microsoft/azure/tests/unit/microsoft/azure/sensors/test_analysis_services.py
@@ -0,0 +1,122 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from datetime import timedelta
+from unittest import mock
+
+import pytest
+
+from airflow.providers.common.compat.sdk import TaskDeferred
+from airflow.providers.microsoft.azure.hooks.analysis_services import (
+    AzureAnalysisServicesHook,
+    AzureAnalysisServicesRefreshException,
+    AzureAnalysisServicesRefreshStatus,
+)
+from airflow.providers.microsoft.azure.sensors.analysis_services import 
AzureAnalysisServicesSensor
+from airflow.providers.microsoft.azure.triggers.analysis_services import (
+    AzureAnalysisServicesRefreshTrigger,
+)
+
+from tests_common.test_utils.operators.run_deferrable import execute_operator
+
+CONN_ID = "azure_analysis_services_test"
+SERVER_NAME = "testserver"
+DATABASE = "adventureworks"
+REFRESH_ID = "refresh-id"
+POKE_INTERVAL = 5
+TIMEOUT = 120
+REQUEST_TIMEOUT = 30
+
+
+def build_sensor(*, request_timeout: float = REQUEST_TIMEOUT) -> 
AzureAnalysisServicesSensor:
+    """Build a sensor with standard test arguments."""
+    return AzureAnalysisServicesSensor(
+        task_id="wait_for_refresh",
+        server_name=SERVER_NAME,
+        database=DATABASE,
+        refresh_id=REFRESH_ID,
+        azure_analysis_services_conn_id=CONN_ID,
+        poke_interval=POKE_INTERVAL,
+        timeout=TIMEOUT,
+        request_timeout=request_timeout,
+    )
+
+
+class TestAzureAnalysisServicesSensor:
+    def test_rejects_invalid_request_timeout(self):
+        with pytest.raises(ValueError, match="request_timeout must be greater 
than zero"):
+            build_sensor(request_timeout=0)
+
+    def test_defines_template_fields(self):
+        assert AzureAnalysisServicesSensor.template_fields == (
+            "azure_analysis_services_conn_id",
+            "server_name",
+            "database",
+            "refresh_id",
+        )
+
+    def test_execute_defers_with_provided_refresh_id(self):
+        with pytest.raises(TaskDeferred) as deferred:
+            build_sensor().execute(context={})
+
+        trigger = deferred.value.trigger
+        assert isinstance(trigger, AzureAnalysisServicesRefreshTrigger)
+        assert trigger.refresh_id == REFRESH_ID
+        assert trigger.poke_interval == POKE_INTERVAL
+        assert trigger.request_timeout == REQUEST_TIMEOUT
+        assert deferred.value.method_name == "execute_complete"
+
+    def test_execute_passes_sensor_timeout_to_defer(self):
+        with pytest.raises(TaskDeferred) as deferred:
+            build_sensor().execute(context={})
+
+        assert deferred.value.timeout == timedelta(seconds=TIMEOUT)
+
+    @mock.patch.object(AzureAnalysisServicesHook, "get_refresh_status", 
autospec=True)
+    def test_execute_sensor_full_lifecycle(self, get_refresh_status):
+        get_refresh_status.return_value = 
AzureAnalysisServicesRefreshStatus.SUCCEEDED
+
+        result, events = execute_operator(build_sensor())
+
+        assert result is None
+        get_refresh_status.assert_awaited_once_with(
+            mock.ANY,
+            server_name=SERVER_NAME,
+            database=DATABASE,
+            refresh_id=REFRESH_ID,
+        )
+        assert [event.payload for event in events] == [
+            {
+                "status": "success",
+                "refresh_status": AzureAnalysisServicesRefreshStatus.SUCCEEDED,
+                "message": f"Refresh {REFRESH_ID} completed successfully",
+                "refresh_id": REFRESH_ID,
+            }
+        ]
+
+    @pytest.mark.parametrize(
+        "event",
+        [
+            None,
+            {"status": "success", "refresh_status": 
AzureAnalysisServicesRefreshStatus.FAILED},
+            {"status": "error", "message": "refresh failed", "refresh_id": 
REFRESH_ID},
+        ],
+    )
+    def test_execute_complete_rejects_malformed_event(self, event):
+        with pytest.raises(AzureAnalysisServicesRefreshException):
+            build_sensor().execute_complete(context={}, event=event)
diff --git 
a/providers/microsoft/azure/tests/unit/microsoft/azure/triggers/test_analysis_services.py
 
b/providers/microsoft/azure/tests/unit/microsoft/azure/triggers/test_analysis_services.py
new file mode 100644
index 00000000000..d4a64744eb1
--- /dev/null
+++ 
b/providers/microsoft/azure/tests/unit/microsoft/azure/triggers/test_analysis_services.py
@@ -0,0 +1,336 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest import mock
+
+import pytest
+
+from airflow.providers.microsoft.azure.hooks.analysis_services import (
+    AzureAnalysisServicesRefreshException,
+    AzureAnalysisServicesRefreshStatus,
+    RefreshType,
+)
+from airflow.providers.microsoft.azure.triggers.analysis_services import (
+    AzureAnalysisServicesRefreshTrigger,
+    validate_completed_refresh_event,
+    validate_refresh_event,
+)
+from airflow.triggers.base import TriggerEvent
+
+CONN_ID = "azure_analysis_services_test"
+SERVER_NAME = "testserver"
+DATABASE = "adventureworks"
+REFRESH_ID = "refresh-id"
+REFRESH_TYPE: RefreshType = "full"
+POKE_INTERVAL = 5
+REQUEST_TIMEOUT = 30
+MODULE = "airflow.providers.microsoft.azure.triggers.analysis_services"
+
+
+def build_trigger(refresh_id: str | None = REFRESH_ID) -> 
AzureAnalysisServicesRefreshTrigger:
+    """Build a trigger with standard test arguments."""
+    return AzureAnalysisServicesRefreshTrigger(
+        conn_id=CONN_ID,
+        server_name=SERVER_NAME,
+        database=DATABASE,
+        refresh_id=refresh_id,
+        refresh_type=REFRESH_TYPE,
+        poke_interval=POKE_INTERVAL,
+        request_timeout=REQUEST_TIMEOUT,
+    )
+
+
+class TestAzureAnalysisServicesRefreshTrigger:
+    def test_serializes_all_arguments(self):
+        trigger = build_trigger()
+
+        classpath, arguments = trigger.serialize()
+
+        assert (
+            classpath
+            == 
f"{AzureAnalysisServicesRefreshTrigger.__module__}.AzureAnalysisServicesRefreshTrigger"
+        )
+        assert arguments == {
+            "conn_id": CONN_ID,
+            "server_name": SERVER_NAME,
+            "database": DATABASE,
+            "refresh_id": REFRESH_ID,
+            "refresh_type": REFRESH_TYPE,
+            "poke_interval": POKE_INTERVAL,
+            "request_timeout": REQUEST_TIMEOUT,
+        }
+
+    def test_rejects_invalid_poke_interval(self):
+        with pytest.raises(ValueError, match="poke_interval must be greater 
than zero"):
+            AzureAnalysisServicesRefreshTrigger(
+                conn_id=CONN_ID,
+                server_name=SERVER_NAME,
+                database=DATABASE,
+                refresh_id=REFRESH_ID,
+                poke_interval=0,
+            )
+
+    def test_rejects_invalid_request_timeout(self):
+        with pytest.raises(ValueError, match="request_timeout must be greater 
than zero"):
+            AzureAnalysisServicesRefreshTrigger(
+                conn_id=CONN_ID,
+                server_name=SERVER_NAME,
+                database=DATABASE,
+                refresh_id=REFRESH_ID,
+                request_timeout=0,
+            )
+
+    @mock.patch(f"{MODULE}.AzureAnalysisServicesHook", autospec=True)
+    @pytest.mark.asyncio
+    async def test_emits_success_event(self, hook_class):
+        hook = hook_class.return_value
+        hook.wait_for_refresh.return_value = 
AzureAnalysisServicesRefreshStatus.SUCCEEDED
+
+        events = [event async for event in build_trigger().run()]
+
+        hook_class.assert_called_once_with(
+            azure_analysis_services_conn_id=CONN_ID,
+            request_timeout=REQUEST_TIMEOUT,
+        )
+        hook.wait_for_refresh.assert_awaited_once_with(
+            server_name=SERVER_NAME,
+            database=DATABASE,
+            refresh_id=REFRESH_ID,
+            poke_interval=POKE_INTERVAL,
+        )
+        assert events == [
+            TriggerEvent(
+                {
+                    "status": "success",
+                    "refresh_status": 
AzureAnalysisServicesRefreshStatus.SUCCEEDED,
+                    "message": f"Refresh {REFRESH_ID} completed successfully",
+                    "refresh_id": REFRESH_ID,
+                }
+            )
+        ]
+
+    @pytest.mark.parametrize("status", 
sorted(AzureAnalysisServicesRefreshStatus.FAILURE_STATUSES))
+    @mock.patch(f"{MODULE}.AzureAnalysisServicesHook", autospec=True)
+    @pytest.mark.asyncio
+    async def test_emits_error_event_for_failure_status(self, hook_class, 
status):
+        hook_class.return_value.wait_for_refresh.return_value = status
+
+        events = [event async for event in build_trigger().run()]
+
+        assert events == [
+            TriggerEvent(
+                {
+                    "status": "error",
+                    "refresh_status": status,
+                    "message": f"Refresh {REFRESH_ID} finished with status 
{status}",
+                    "refresh_id": REFRESH_ID,
+                }
+            )
+        ]
+
+    @mock.patch(f"{MODULE}.AzureAnalysisServicesHook", autospec=True)
+    @pytest.mark.asyncio
+    async def test_triggers_refresh_and_yields_without_polling(self, 
hook_class):
+        hook = hook_class.return_value
+        hook.trigger_refresh.return_value = REFRESH_ID
+
+        events = [event async for event in 
build_trigger(refresh_id=None).run()]
+
+        hook.trigger_refresh.assert_awaited_once_with(
+            server_name=SERVER_NAME,
+            database=DATABASE,
+            refresh_type=REFRESH_TYPE,
+        )
+        hook.wait_for_refresh.assert_not_awaited()
+        assert events == [
+            TriggerEvent(
+                {
+                    "status": "success",
+                    "refresh_status": None,
+                    "message": f"Refresh {REFRESH_ID} has been triggered",
+                    "refresh_id": REFRESH_ID,
+                }
+            )
+        ]
+
+    @mock.patch(f"{MODULE}.AzureAnalysisServicesHook", autospec=True)
+    @pytest.mark.asyncio
+    async def test_skips_trigger_when_refresh_id_provided(self, hook_class):
+        hook = hook_class.return_value
+        hook.wait_for_refresh.return_value = 
AzureAnalysisServicesRefreshStatus.SUCCEEDED
+
+        [event async for event in build_trigger().run()]
+
+        hook.trigger_refresh.assert_not_awaited()
+
+    @mock.patch(f"{MODULE}.AzureAnalysisServicesHook", autospec=True)
+    @pytest.mark.asyncio
+    async def test_emits_error_event_when_trigger_refresh_fails(self, 
hook_class):
+        hook_class.return_value.trigger_refresh.side_effect = 
AzureAnalysisServicesRefreshException(
+            "Failed to authenticate with Azure Analysis Services"
+        )
+
+        events = [event async for event in 
build_trigger(refresh_id=None).run()]
+
+        assert events == [
+            TriggerEvent(
+                {
+                    "status": "error",
+                    "refresh_status": None,
+                    "message": "Failed to authenticate with Azure Analysis 
Services",
+                    "refresh_id": None,
+                }
+            )
+        ]
+
+    @pytest.mark.parametrize("fails", [False, True])
+    @mock.patch(f"{MODULE}.AzureAnalysisServicesHook", autospec=True)
+    @pytest.mark.asyncio
+    async def test_closes_hook_on_exit(self, hook_class, fails):
+        hook = hook_class.return_value
+        if fails:
+            hook.wait_for_refresh.side_effect = 
AzureAnalysisServicesRefreshException("boom")
+        else:
+            hook.wait_for_refresh.return_value = 
AzureAnalysisServicesRefreshStatus.SUCCEEDED
+
+        [event async for event in build_trigger().run()]
+
+        hook.aclose.assert_awaited_once_with()
+
+    @mock.patch(f"{MODULE}.AzureAnalysisServicesHook", autospec=True)
+    @pytest.mark.asyncio
+    async def test_converts_hook_exception_to_error_event(self, hook_class):
+        hook_class.return_value.wait_for_refresh.side_effect = 
AzureAnalysisServicesRefreshException(
+            "API unavailable"
+        )
+
+        events = [event async for event in build_trigger().run()]
+
+        assert events == [
+            TriggerEvent(
+                {
+                    "status": "error",
+                    "refresh_status": None,
+                    "message": "API unavailable",
+                    "refresh_id": REFRESH_ID,
+                }
+            )
+        ]
+
+    @mock.patch(f"{MODULE}.AzureAnalysisServicesHook", autospec=True)
+    @pytest.mark.asyncio
+    async def test_uses_exception_class_name_when_message_is_empty(self, 
hook_class):
+        hook_class.return_value.wait_for_refresh.side_effect = RuntimeError()
+
+        events = [event async for event in build_trigger().run()]
+
+        assert events[0].payload["message"] == "RuntimeError"
+
+
+class TestValidateRefreshEvent:
+    def test_returns_refresh_id_for_success(self):
+        event = {
+            "status": "success",
+            "refresh_status": AzureAnalysisServicesRefreshStatus.SUCCEEDED,
+            "message": "completed",
+            "refresh_id": REFRESH_ID,
+        }
+
+        assert validate_refresh_event(event) == REFRESH_ID
+
+    @pytest.mark.parametrize("event", [None, [], "event"])
+    def test_rejects_non_dictionary_event(self, event):
+        with pytest.raises(AzureAnalysisServicesRefreshException, match="valid 
event"):
+            validate_refresh_event(event)
+
+    @pytest.mark.parametrize("refresh_id", [None, "", 1])
+    def test_rejects_invalid_refresh_id(self, refresh_id):
+        with pytest.raises(AzureAnalysisServicesRefreshException, match="valid 
refresh ID"):
+            validate_refresh_event({"status": "success", "refresh_id": 
refresh_id})
+
+    def test_raises_error_event_message(self):
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="refresh failed"):
+            validate_refresh_event(
+                {
+                    "status": "error",
+                    "refresh_status": 
AzureAnalysisServicesRefreshStatus.FAILED,
+                    "message": "refresh failed",
+                    "refresh_id": REFRESH_ID,
+                }
+            )
+
+    @pytest.mark.parametrize("message", [None, "", 1])
+    def test_raises_fallback_for_error_without_message(self, message):
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="refresh failed"):
+            validate_refresh_event(
+                {
+                    "status": "error",
+                    "refresh_status": 
AzureAnalysisServicesRefreshStatus.FAILED,
+                    "message": message,
+                    "refresh_id": REFRESH_ID,
+                }
+            )
+
+    def test_raises_error_message_when_refresh_id_absent(self):
+        # A POST that fails before a refresh exists yields no ID; the message 
is the only diagnostic.
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="Failed to authenticate"):
+            validate_refresh_event(
+                {
+                    "status": "error",
+                    "refresh_status": None,
+                    "message": "Failed to authenticate with Azure Analysis 
Services",
+                    "refresh_id": None,
+                }
+            )
+
+    def test_rejects_unknown_event_status(self):
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="unknown event status"):
+            validate_refresh_event({"status": "unknown", "refresh_id": 
REFRESH_ID})
+
+    def test_base_validator_accepts_null_refresh_status(self):
+        event = {"status": "success", "refresh_status": None, "refresh_id": 
REFRESH_ID}
+
+        assert validate_refresh_event(event) == REFRESH_ID
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="unexpected refresh status"):
+            validate_completed_refresh_event(event)
+
+    def test_completed_validator_returns_refresh_id(self):
+        assert (
+            validate_completed_refresh_event(
+                {
+                    "status": "success",
+                    "refresh_status": 
AzureAnalysisServicesRefreshStatus.SUCCEEDED,
+                    "refresh_id": REFRESH_ID,
+                }
+            )
+            == REFRESH_ID
+        )
+
+    @pytest.mark.parametrize(
+        "refresh_status",
+        [None, AzureAnalysisServicesRefreshStatus.IN_PROGRESS, 
AzureAnalysisServicesRefreshStatus.FAILED],
+    )
+    def test_completed_validator_rejects_non_success_refresh_status(self, 
refresh_status):
+        with pytest.raises(AzureAnalysisServicesRefreshException, 
match="unexpected refresh status"):
+            validate_completed_refresh_event(
+                {
+                    "status": "success",
+                    "refresh_status": refresh_status,
+                    "refresh_id": REFRESH_ID,
+                }
+            )

Reply via email to