From 7ac4864a97c94550586c999f55fa25edaa9a9908 Mon Sep 17 00:00:00 2001 From: PoAn Yang Date: Sat, 13 Jun 2026 09:16:14 +0900 Subject: [PATCH 1/3] fix(test_wasb.py): SAS token tests failing with azure-storage-blob 12.30.0 Signed-off-by: PoAn Yang --- .../unit/microsoft/azure/hooks/test_wasb.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py index 40f72229950f1..15b8157fd06f6 100644 --- a/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py +++ b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py @@ -45,6 +45,13 @@ ACCESS_KEY_STRING = "AccountName=name;skdkskd" PROXIES = {"http": "http_proxy_uri", "https": "https_proxy_uri"} +# A SAS token is a query string (not a path segment). Use a representative token and the +# equivalent full SAS URL so the resulting BlobServiceClient.url carries it in the query. +# This is what azure-storage-blob actually uses to sign requests, and is stable across SDK +# versions. +SAS_TOKEN = "?sv=2021-08-06&ss=b&srt=co&sp=r&sig=samplesignature" +HTTPS_SAS_TOKEN = f"https://login.blob.core.windows.net/{SAS_TOKEN}" + @pytest.fixture def mocked_blob_service_client(): @@ -147,24 +154,24 @@ def setup_method(self, create_mock_connections): conn_id="sas_conn_id", conn_type=self.connection_type, login=self.login, - extra={"sas_token": "token", "proxies": self.proxies}, + extra={"sas_token": SAS_TOKEN, "proxies": self.proxies}, ), Connection( conn_id=self.extra__wasb__sas_conn_id, conn_type=self.connection_type, login=self.login, - extra={"extra__wasb__sas_token": "token", "proxies": self.proxies}, + extra={"extra__wasb__sas_token": SAS_TOKEN, "proxies": self.proxies}, ), Connection( conn_id=self.http_sas_conn_id, conn_type=self.connection_type, - extra={"sas_token": "https://login.blob.core.windows.net/token", "proxies": self.proxies}, + extra={"sas_token": HTTPS_SAS_TOKEN, "proxies": self.proxies}, ), Connection( conn_id=self.extra__wasb__http_sas_conn_id, conn_type=self.connection_type, extra={ - "extra__wasb__sas_token": "https://login.blob.core.windows.net/token", + "extra__wasb__sas_token": HTTPS_SAS_TOKEN, "proxies": self.proxies, }, ), @@ -364,7 +371,9 @@ def test_sas_token_connection(self, conn_id_str, extra_key): assert conn.url.startswith("https://") if hook_conn.login: assert hook_conn.login in conn.url - assert conn.url.endswith(sas_token + "/") + # The SAS token must be carried in the URL query string so the SDK signs requests with + # it. + assert sas_token.lstrip("?") in conn.url @pytest.mark.parametrize( argnames="conn_id_str", From 2d1628f45ddb11e14cea7018429a5e0be3573302 Mon Sep 17 00:00:00 2001 From: PoAn Yang Date: Sat, 13 Jun 2026 13:52:58 +0900 Subject: [PATCH 2/3] refactor: add SAS link and update test_sas_token_connection assertion Signed-off-by: PoAn Yang --- .../providers/microsoft/azure/hooks/wasb.py | 7 +++-- .../unit/microsoft/azure/hooks/test_wasb.py | 31 +++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/wasb.py b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/wasb.py index dabd7280e3d2d..c363c227690ed 100644 --- a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/wasb.py +++ b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/wasb.py @@ -30,6 +30,7 @@ import os from typing import TYPE_CHECKING, Any, cast +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import HttpResponseError, ResourceExistsError, ResourceNotFoundError from azure.identity import ClientSecretCredential from azure.identity.aio import ( @@ -203,7 +204,9 @@ def get_conn(self) -> BlobServiceClient: if sas_token: if sas_token.startswith("https"): return BlobServiceClient(account_url=sas_token, **extra) - return BlobServiceClient(account_url=f"{account_url.rstrip('/')}/{sas_token}", **extra) + return BlobServiceClient( + account_url=account_url, credential=AzureSasCredential(sas_token), **extra + ) # Fall back to old auth (password) or use managed identity if not provided. credential: str | TokenCredential | None = conn.password @@ -671,7 +674,7 @@ async def get_async_conn(self) -> AsyncBlobServiceClient: self.blob_service_client = AsyncBlobServiceClient(account_url=sas_token, **extra) else: self.blob_service_client = AsyncBlobServiceClient( - account_url=f"{account_url.rstrip('/')}/{sas_token}", **extra + account_url=account_url, credential=AzureSasCredential(sas_token), **extra ) return self.blob_service_client diff --git a/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py index 15b8157fd06f6..0551467fcb3eb 100644 --- a/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py +++ b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py @@ -23,6 +23,7 @@ from unittest.mock import create_autospec import pytest +from azure.core.credentials import AzureSasCredential from azure.core.exceptions import ResourceNotFoundError from azure.storage.blob import BlobServiceClient, ContainerClient from azure.storage.blob._models import BlobProperties @@ -45,12 +46,10 @@ ACCESS_KEY_STRING = "AccountName=name;skdkskd" PROXIES = {"http": "http_proxy_uri", "https": "https_proxy_uri"} -# A SAS token is a query string (not a path segment). Use a representative token and the -# equivalent full SAS URL so the resulting BlobServiceClient.url carries it in the query. -# This is what azure-storage-blob actually uses to sign requests, and is stable across SDK -# versions. -SAS_TOKEN = "?sv=2021-08-06&ss=b&srt=co&sp=r&sig=samplesignature" -HTTPS_SAS_TOKEN = f"https://login.blob.core.windows.net/{SAS_TOKEN}" +# https://learn.microsoft.com/en-us/rest/api/storageservices/create-service-sas +# The hook wraps plain SAS tokens in AzureSasCredential for the SDK to sign requests. +SAS_TOKEN = "sv=2021-08-06&ss=b&srt=sco&sp=r&sig=samplesignature" +HTTPS_SAS_TOKEN = f"https://login.blob.core.windows.net/?{SAS_TOKEN}" @pytest.fixture @@ -311,8 +310,12 @@ def test_sas_token_provided_and_active_directory_id_used_as_host( self, mocked_connection, mocked_blob_service_client ): WasbHook(wasb_conn_id="testconn").get_conn() + called_credential = mocked_blob_service_client.call_args[1]["credential"] + assert isinstance(called_credential, AzureSasCredential) + assert called_credential.signature == "SAStoken" mocked_blob_service_client.assert_called_once_with( - account_url="https://testaccountname.blob.core.windows.net/SAStoken", + account_url="https://testaccountname.blob.core.windows.net/", + credential=called_credential, sas_token="SAStoken", ) @@ -368,12 +371,14 @@ def test_sas_token_connection(self, conn_id_str, extra_key): hook_conn = hook.get_connection(hook.conn_id) sas_token = hook_conn.extra_dejson[extra_key] assert isinstance(conn, BlobServiceClient) - assert conn.url.startswith("https://") - if hook_conn.login: - assert hook_conn.login in conn.url - # The SAS token must be carried in the URL query string so the SDK signs requests with - # it. - assert sas_token.lstrip("?") in conn.url + if sas_token.startswith("https"): + # HTTPS SAS URL: the full URL is passed to BlobServiceClient as-is; + # credential handling is left to the SDK. + assert conn.url.startswith("https://") + else: + # Plain SAS token: the hook wraps it in AzureSasCredential. + assert isinstance(conn.credential, AzureSasCredential) + assert conn.credential.signature == sas_token @pytest.mark.parametrize( argnames="conn_id_str", From 6ccaf1845e61feffde69e7727385ab0c2bc4db74 Mon Sep 17 00:00:00 2001 From: PoAn Yang Date: Sat, 13 Jun 2026 17:42:17 +0900 Subject: [PATCH 3/3] test: assert sas_token in conn.url Signed-off-by: PoAn Yang --- .../azure/tests/unit/microsoft/azure/hooks/test_wasb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py index 0551467fcb3eb..45670510944ca 100644 --- a/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py +++ b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_wasb.py @@ -374,7 +374,7 @@ def test_sas_token_connection(self, conn_id_str, extra_key): if sas_token.startswith("https"): # HTTPS SAS URL: the full URL is passed to BlobServiceClient as-is; # credential handling is left to the SDK. - assert conn.url.startswith("https://") + assert sas_token in conn.url else: # Plain SAS token: the hook wraps it in AzureSasCredential. assert isinstance(conn.credential, AzureSasCredential)