Skip to content

Commit d19e316

Browse files
committed
Update unit tests to confirm absence of unwarranted OAuth events
1 parent 86d542f commit d19e316

1 file changed

Lines changed: 62 additions & 78 deletions

File tree

tests/unittests/auth/test_toolset_auth.py

Lines changed: 62 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
from typing import Optional
1818
from unittest.mock import AsyncMock
19-
from unittest.mock import MagicMock
2019
from unittest.mock import Mock
2120
from unittest.mock import patch
2221

@@ -28,12 +27,8 @@
2827
from google.adk.auth.auth_credential import AuthCredential
2928
from google.adk.auth.auth_credential import AuthCredentialTypes
3029
from google.adk.auth.auth_credential import OAuth2Auth
31-
from google.adk.auth.auth_preprocessor import TOOLSET_AUTH_CREDENTIAL_ID_PREFIX
3230
from google.adk.auth.auth_tool import AuthConfig
33-
from google.adk.auth.auth_tool import AuthToolArguments
3431
from google.adk.flows.llm_flows.base_llm_flow import _resolve_toolset_auth
35-
from google.adk.flows.llm_flows.base_llm_flow import BaseLlmFlow
36-
from google.adk.flows.llm_flows.base_llm_flow import TOOLSET_AUTH_CREDENTIAL_ID_PREFIX as FLOW_PREFIX
3732
from google.adk.flows.llm_flows.functions import build_auth_request_event
3833
from google.adk.flows.llm_flows.functions import REQUEST_EUC_FUNCTION_CALL_NAME
3934
from google.adk.tools.base_tool import BaseTool
@@ -85,15 +80,6 @@ def create_oauth2_auth_config() -> AuthConfig:
8580
)
8681

8782

88-
class TestToolsetAuthPrefixConstant:
89-
"""Test that prefix constants are consistent."""
90-
91-
def test_prefix_constants_match(self):
92-
"""Ensure auth_preprocessor and base_llm_flow use the same prefix."""
93-
assert TOOLSET_AUTH_CREDENTIAL_ID_PREFIX == FLOW_PREFIX
94-
assert TOOLSET_AUTH_CREDENTIAL_ID_PREFIX == "_adk_toolset_auth_"
95-
96-
9783
class TestResolveToolsetAuth:
9884
"""Tests for _resolve_toolset_auth method in BaseLlmFlow."""
9985

@@ -121,19 +107,12 @@ def mock_agent(self):
121107
return agent
122108

123109
@pytest.mark.asyncio
124-
async def test_no_tools_returns_no_events(
125-
self, mock_invocation_context, mock_agent
126-
):
127-
"""Test that no events are yielded when agent has no tools."""
110+
async def test_no_tools_completes(self, mock_invocation_context, mock_agent):
111+
"""Test that resolve completes without side effects when agent has no tools."""
128112
mock_agent.tools = []
129113

130-
events = []
131-
async for event in _resolve_toolset_auth(
132-
mock_invocation_context, mock_agent
133-
):
134-
events.append(event)
114+
await _resolve_toolset_auth(mock_invocation_context, mock_agent)
135115

136-
assert len(events) == 0
137116
assert mock_invocation_context.end_invocation is False
138117

139118
@pytest.mark.asyncio
@@ -144,13 +123,8 @@ async def test_toolset_without_auth_config_skipped(
144123
toolset = MockToolset(auth_config=None)
145124
mock_agent.tools = [toolset]
146125

147-
events = []
148-
async for event in _resolve_toolset_auth(
149-
mock_invocation_context, mock_agent
150-
):
151-
events.append(event)
126+
await _resolve_toolset_auth(mock_invocation_context, mock_agent)
152127

153-
assert len(events) == 0
154128
assert mock_invocation_context.end_invocation is False
155129

156130
@pytest.mark.asyncio
@@ -162,7 +136,6 @@ async def test_toolset_with_credential_available_populates_config(
162136
toolset = MockToolset(auth_config=auth_config)
163137
mock_agent.tools = [toolset]
164138

165-
# Mock CredentialManager to return a credential
166139
mock_credential = AuthCredential(
167140
auth_type=AuthCredentialTypes.OAUTH2,
168141
oauth2=OAuth2Auth(access_token="test-token"),
@@ -175,23 +148,21 @@ async def test_toolset_with_credential_available_populates_config(
175148
mock_manager.get_auth_credential = AsyncMock(return_value=mock_credential)
176149
MockCredentialManager.return_value = mock_manager
177150

178-
events = []
179-
async for event in _resolve_toolset_auth(
180-
mock_invocation_context, mock_agent
181-
):
182-
events.append(event)
151+
await _resolve_toolset_auth(mock_invocation_context, mock_agent)
183152

184-
# No auth request events - credential was available
185-
assert len(events) == 0
186153
assert mock_invocation_context.end_invocation is False
187-
# Credential should be populated in auth_config
188154
assert auth_config.exchanged_auth_credential == mock_credential
189155

190156
@pytest.mark.asyncio
191-
async def test_toolset_without_credential_yields_auth_event(
157+
async def test_toolset_without_credential_defers_auth(
192158
self, mock_invocation_context, mock_agent
193159
):
194-
"""Test that auth request event is yielded when credential not available."""
160+
"""Test that auth is deferred when credential is not available.
161+
162+
When no credential is found, _resolve_toolset_auth should not interrupt
163+
the invocation. Auth will be handled on demand by ToolAuthHandler when
164+
a tool is actually invoked.
165+
"""
195166
auth_config = create_oauth2_auth_config()
196167
toolset = MockToolset(auth_config=auth_config)
197168
mock_agent.tools = [toolset]
@@ -203,37 +174,16 @@ async def test_toolset_without_credential_yields_auth_event(
203174
mock_manager.get_auth_credential = AsyncMock(return_value=None)
204175
MockCredentialManager.return_value = mock_manager
205176

206-
events = []
207-
async for event in _resolve_toolset_auth(
208-
mock_invocation_context, mock_agent
209-
):
210-
events.append(event)
211-
212-
# Should yield one auth request event
213-
assert len(events) == 1
214-
assert mock_invocation_context.end_invocation is True
177+
await _resolve_toolset_auth(mock_invocation_context, mock_agent)
215178

216-
# Check event structure
217-
event = events[0]
218-
assert event.invocation_id == "test-invocation-id"
219-
assert event.author == "test-agent"
220-
assert event.content is not None
221-
assert len(event.content.parts) == 1
222-
223-
# Check function call
224-
fc = event.content.parts[0].function_call
225-
assert fc.name == REQUEST_EUC_FUNCTION_CALL_NAME
226-
# The args use camelCase aliases from the pydantic model
227-
assert fc.args["functionCallId"].startswith(
228-
TOOLSET_AUTH_CREDENTIAL_ID_PREFIX
229-
)
230-
assert "MockToolset" in fc.args["functionCallId"]
179+
assert mock_invocation_context.end_invocation is False
180+
assert auth_config.exchanged_auth_credential is None
231181

232182
@pytest.mark.asyncio
233-
async def test_multiple_toolsets_needing_auth(
183+
async def test_multiple_toolsets_without_credentials_defers_auth(
234184
self, mock_invocation_context, mock_agent
235185
):
236-
"""Test that multiple toolsets needing auth yield multiple function calls."""
186+
"""Test that multiple toolsets without credentials do not interrupt."""
237187
auth_config1 = create_oauth2_auth_config()
238188
auth_config2 = create_oauth2_auth_config()
239189
toolset1 = MockToolset(auth_config=auth_config1)
@@ -247,17 +197,51 @@ async def test_multiple_toolsets_needing_auth(
247197
mock_manager.get_auth_credential = AsyncMock(return_value=None)
248198
MockCredentialManager.return_value = mock_manager
249199

250-
events = []
251-
async for event in _resolve_toolset_auth(
252-
mock_invocation_context, mock_agent
253-
):
254-
events.append(event)
255-
256-
# Should yield one event with multiple function calls
257-
# But since both toolsets have same class name, they'll have same ID
258-
# and only one will be in pending_auth_requests (dict overwrites)
259-
assert len(events) == 1
260-
assert mock_invocation_context.end_invocation is True
200+
await _resolve_toolset_auth(mock_invocation_context, mock_agent)
201+
202+
assert mock_invocation_context.end_invocation is False
203+
204+
@pytest.mark.asyncio
205+
async def test_mixed_toolsets_populates_available_credentials(
206+
self, mock_invocation_context, mock_agent
207+
):
208+
"""Test that credentials are populated when available, without interrupt.
209+
210+
When one toolset has credentials and another does not, the available
211+
credential should be populated while the missing one is deferred.
212+
"""
213+
auth_config_with_cred = create_oauth2_auth_config()
214+
auth_config_without_cred = create_oauth2_auth_config()
215+
toolset_with_cred = MockToolset(auth_config=auth_config_with_cred)
216+
toolset_without_cred = MockToolset(auth_config=auth_config_without_cred)
217+
mock_agent.tools = [toolset_with_cred, toolset_without_cred]
218+
219+
mock_credential = AuthCredential(
220+
auth_type=AuthCredentialTypes.OAUTH2,
221+
oauth2=OAuth2Auth(access_token="test-token"),
222+
)
223+
224+
call_count = 0
225+
226+
async def side_effect(*args, **kwargs):
227+
nonlocal call_count
228+
call_count += 1
229+
if call_count == 1:
230+
return mock_credential
231+
return None
232+
233+
with patch(
234+
"google.adk.flows.llm_flows.base_llm_flow.CredentialManager"
235+
) as MockCredentialManager:
236+
mock_manager = AsyncMock()
237+
mock_manager.get_auth_credential = AsyncMock(side_effect=side_effect)
238+
MockCredentialManager.return_value = mock_manager
239+
240+
await _resolve_toolset_auth(mock_invocation_context, mock_agent)
241+
242+
assert mock_invocation_context.end_invocation is False
243+
assert auth_config_with_cred.exchanged_auth_credential == mock_credential
244+
assert auth_config_without_cred.exchanged_auth_credential is None
261245

262246

263247
class TestAuthPreprocessorToolsetAuthSkip:

0 commit comments

Comments
 (0)