Skip to content

Commit 9896d8e

Browse files
authored
[keyvault] Fix azure-mgmt-keyvault vaults.list() api-version for next-link pagination (#45955)
* Fix azure-mgmt-keyvault vaults.list() api-version for next-link pagination The legacy vaults.list() API requires api-version=2015-11-01 for both initial and next-link requests. After SDK regeneration, the next-link handler was using self._config.api_version (2026-02-01) instead of the hardcoded 2015-11-01. Fixed in both sync and async operations. Added mock unit tests to catch this regression in CI. * Update test_key_vault_management_vaults_operations_test.py
1 parent a37ee0c commit 9896d8e

5 files changed

Lines changed: 163 additions & 3 deletions

File tree

sdk/keyvault/azure-mgmt-keyvault/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Release History
22

3+
## 14.0.1 (2026-03-27)
4+
5+
### Bugs Fixed
6+
7+
- Fix wrong replacement about api-version for `next_link` of API `VaultsOperations.list`
8+
39
## 14.0.0 (2026-03-18)
410

511
### Breaking Changes

sdk/keyvault/azure-mgmt-keyvault/azure/mgmt/keyvault/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
77
# --------------------------------------------------------------------------
88

9-
VERSION = "14.0.0"
9+
VERSION = "14.0.1"

sdk/keyvault/azure-mgmt-keyvault/azure/mgmt/keyvault/aio/operations/_operations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1462,7 +1462,7 @@ def prepare_request(next_link=None):
14621462
for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items()
14631463
}
14641464
)
1465-
_next_request_params["api-version"] = self._config.api_version
1465+
_next_request_params["api-version"] = api_version
14661466
_request = HttpRequest(
14671467
"GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params
14681468
)

sdk/keyvault/azure-mgmt-keyvault/azure/mgmt/keyvault/operations/_operations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2808,7 +2808,7 @@ def prepare_request(next_link=None):
28082808
for key, value in urllib.parse.parse_qs(_parsed_next_link.query).items()
28092809
}
28102810
)
2811-
_next_request_params["api-version"] = self._config.api_version
2811+
_next_request_params["api-version"] = api_version
28122812
_request = HttpRequest(
28132813
"GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params
28142814
)

sdk/keyvault/azure-mgmt-keyvault/tests/test_unittest.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
# Code generated by Microsoft (R) AutoRest Code Generator.
66
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
77
# --------------------------------------------------------------------------
8+
import json
9+
from unittest.mock import AsyncMock, Mock
10+
11+
import pytest
12+
13+
from azure.mgmt.keyvault import KeyVaultManagementClient
14+
from azure.mgmt.keyvault.aio import KeyVaultManagementClient as AsyncKeyVaultManagementClient
815
from azure.mgmt.keyvault.models import Key, KeyAttributes, KeyProperties, PrivateLinkResource
16+
from azure.core.pipeline.transport import HttpTransport, AsyncHttpTransport
917
from azure.core.serialization import attribute_list
1018

1119

@@ -52,3 +60,149 @@ def test_private_link_resource_initialization():
5260
)
5361
assert plr.group_id == "vault"
5462
assert plr.properties.group_id == "vault"
63+
64+
65+
def test_vaults_list_api_version():
66+
"""Verify vaults.list() uses api-version=2015-11-01 for both initial and next-link requests.
67+
68+
This API is a legacy paging API where api-version must be "2015-11-01" for all requests.
69+
After SDK regeneration, 2 lines need to be customized per:
70+
https://github.com/Azure/azure-sdk-for-python/pull/43559/commits/6e09d1d513da26c55c7960442f0f20f5e59e149a
71+
"""
72+
captured_urls = []
73+
call_count = 0
74+
75+
class MockTransport(HttpTransport):
76+
def open(self):
77+
pass
78+
79+
def close(self):
80+
pass
81+
82+
def __exit__(self, *args):
83+
pass
84+
85+
def send(self, request, **kwargs):
86+
nonlocal call_count
87+
call_count += 1
88+
captured_urls.append(request.url)
89+
90+
response = Mock()
91+
response.status_code = 200
92+
response.headers = {"Content-Type": "application/json"}
93+
response.content_type = "application/json"
94+
95+
if call_count == 1:
96+
# First response includes a nextLink WITHOUT api-version,
97+
# simulating real service behavior
98+
body = {
99+
"value": [{"id": "1", "name": "vault1", "type": "Microsoft.KeyVault/vaults", "location": "eastus"}],
100+
"nextLink": "https://management.azure.com/subscriptions/fake-sub-id/resources"
101+
"?$filter=resourceType+eq+%27Microsoft.KeyVault%2Fvaults%27&$skiptoken=page2",
102+
}
103+
else:
104+
body = {
105+
"value": [{"id": "2", "name": "vault2", "type": "Microsoft.KeyVault/vaults", "location": "eastus"}],
106+
}
107+
108+
response.json.return_value = body
109+
response.text.return_value = json.dumps(body)
110+
content = json.dumps(body).encode("utf-8")
111+
response.read.return_value = content
112+
response.content = content
113+
response.is_closed = False
114+
response.is_stream_consumed = False
115+
return response
116+
117+
mock_credential = Mock()
118+
mock_credential.get_token.return_value = Mock(token="fake-token", expires_on=9999999999)
119+
120+
client = KeyVaultManagementClient(
121+
credential=mock_credential,
122+
subscription_id="fake-sub-id",
123+
base_url="https://management.azure.com",
124+
transport=MockTransport(),
125+
policies=[],
126+
)
127+
128+
result = list(client.vaults.list())
129+
assert len(result) == 2
130+
assert call_count == 2
131+
132+
for url in captured_urls:
133+
assert (
134+
url.count("api-version=2015-11-01") == 1
135+
), f"api-version=2015-11-01 must appear exactly once in URL: {url}"
136+
assert url.count("api-version") == 1, f"api-version query parameter is duplicated in URL: {url}"
137+
138+
139+
@pytest.mark.asyncio
140+
async def test_vaults_list_api_version_async():
141+
"""Verify async vaults.list() uses api-version=2015-11-01 for both initial and next-link requests."""
142+
captured_urls = []
143+
call_count = 0
144+
145+
class MockAsyncTransport(AsyncHttpTransport):
146+
async def open(self):
147+
pass
148+
149+
async def close(self):
150+
pass
151+
152+
async def __aexit__(self, *args):
153+
pass
154+
155+
async def send(self, request, **kwargs):
156+
nonlocal call_count
157+
call_count += 1
158+
captured_urls.append(request.url)
159+
160+
response = Mock()
161+
response.status_code = 200
162+
response.headers = {"Content-Type": "application/json"}
163+
response.content_type = "application/json"
164+
165+
if call_count == 1:
166+
body = {
167+
"value": [{"id": "1", "name": "vault1", "type": "Microsoft.KeyVault/vaults", "location": "eastus"}],
168+
"nextLink": "https://management.azure.com/subscriptions/fake-sub-id/resources"
169+
"?$filter=resourceType+eq+%27Microsoft.KeyVault%2Fvaults%27&$skiptoken=page2",
170+
}
171+
else:
172+
body = {
173+
"value": [{"id": "2", "name": "vault2", "type": "Microsoft.KeyVault/vaults", "location": "eastus"}],
174+
}
175+
176+
response.json.return_value = body
177+
response.text.return_value = json.dumps(body)
178+
content = json.dumps(body).encode("utf-8")
179+
response.read = AsyncMock(return_value=content)
180+
response.content = content
181+
response.is_closed = False
182+
response.is_stream_consumed = False
183+
return response
184+
185+
mock_credential = Mock()
186+
mock_credential.get_token = AsyncMock(return_value=Mock(token="fake-token", expires_on=9999999999))
187+
188+
client = AsyncKeyVaultManagementClient(
189+
credential=mock_credential,
190+
subscription_id="fake-sub-id",
191+
base_url="https://management.azure.com",
192+
transport=MockAsyncTransport(),
193+
policies=[],
194+
)
195+
196+
result = []
197+
async for item in client.vaults.list():
198+
result.append(item)
199+
await client.close()
200+
201+
assert len(result) == 2
202+
assert call_count == 2
203+
204+
for url in captured_urls:
205+
assert (
206+
url.count("api-version=2015-11-01") == 1
207+
), f"api-version=2015-11-01 must appear exactly once in URL: {url}"
208+
assert url.count("api-version") == 1, f"api-version query parameter is duplicated in URL: {url}"

0 commit comments

Comments
 (0)