1+ # Copyright 2025 Google LLC
2+ #
3+ # Licensed under the Apache License, Version 2.0 (the "License");
4+ # you may not use this file except in compliance with the License.
5+ # You may obtain a copy of the License at
6+ #
7+ # http://www.apache.org/licenses/LICENSE-2.0
8+ #
9+ # Unless required by applicable law or agreed to in writing, software
10+ # distributed under the License is distributed on an "AS IS" BASIS,
11+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ # See the License for the specific language governing permissions and
13+ # limitations under the License.
14+
15+ from google .adk .utils .pydantic_v2_compatibility import (
16+ patch_types_for_pydantic_v2 ,
17+ create_robust_openapi_function ,
18+ __get_pydantic_core_schema__ ,
19+ )
20+ import pytest
21+ from unittest .mock import Mock , patch , MagicMock
22+ from fastapi import FastAPI
23+ import sys
24+ import logging
25+
26+
27+ class TestPydanticV2CompatibilityPatches :
28+ """Test suite for Pydantic v2 compatibility patches."""
29+
30+ def test_get_pydantic_core_schema_success (self ):
31+ """Test successful schema generation with valid handler."""
32+ mock_handler = Mock ()
33+ mock_handler .generate_schema .return_value = {"type" : "object" , "properties" : {}}
34+
35+ result = __get_pydantic_core_schema__ (str , mock_handler )
36+
37+ assert result == {"type" : "object" , "properties" : {}}
38+ mock_handler .generate_schema .assert_called_once_with (str )
39+
40+ def test_get_pydantic_core_schema_fallback (self ):
41+ """Test fallback schema when handler fails."""
42+ mock_handler = Mock ()
43+ mock_handler .generate_schema .side_effect = Exception ("Schema generation failed" )
44+
45+ result = __get_pydantic_core_schema__ (str , mock_handler )
46+
47+ expected_fallback = {
48+ "type" : "object" ,
49+ "properties" : {},
50+ "title" : "str" ,
51+ "_pydantic_v2_compat" : True
52+ }
53+ assert result == expected_fallback
54+
55+ def test_get_pydantic_core_schema_no_handler (self ):
56+ """Test schema generation when handler is None."""
57+ result = __get_pydantic_core_schema__ (str , None )
58+
59+ expected_fallback = {
60+ "type" : "object" ,
61+ "properties" : {},
62+ "title" : "str" ,
63+ "_pydantic_v2_compat" : True
64+ }
65+ assert result == expected_fallback
66+
67+ @patch ('google.adk.utils.pydantic_v2_compatibility.ClientSession' , create = True )
68+ def test_patch_types_for_pydantic_v2_success (self , mock_client_session ):
69+ """Test successful patching of types for Pydantic v2."""
70+ # Mock ClientSession class
71+ mock_client_session .__modify_schema__ = Mock ()
72+
73+ result = patch_types_for_pydantic_v2 ()
74+
75+ assert result is True
76+ # Verify that __get_pydantic_core_schema__ was added
77+ assert hasattr (mock_client_session , '__get_pydantic_core_schema__' )
78+ # Verify that __modify_schema__ was removed if it existed
79+ assert not hasattr (mock_client_session , '__modify_schema__' )
80+
81+ @patch ('google.adk.utils.pydantic_v2_compatibility.ClientSession' , side_effect = ImportError )
82+ def test_patch_types_for_pydantic_v2_import_error (self , mock_client_session ):
83+ """Test patching when ClientSession cannot be imported."""
84+ result = patch_types_for_pydantic_v2 ()
85+
86+ assert result is False
87+
88+ @patch ('google.adk.utils.pydantic_v2_compatibility.logger' )
89+ @patch ('google.adk.utils.pydantic_v2_compatibility.ClientSession' , create = True )
90+ def test_patch_types_for_pydantic_v2_exception_handling (self , mock_client_session , mock_logger ):
91+ """Test exception handling during patching."""
92+ # Make setattr raise an exception
93+ with patch ('builtins.setattr' , side_effect = Exception ("Patching failed" )):
94+ result = patch_types_for_pydantic_v2 ()
95+
96+ assert result is False
97+ mock_logger .error .assert_called ()
98+
99+ def test_create_robust_openapi_function_normal_operation (self ):
100+ """Test robust OpenAPI function under normal conditions."""
101+ mock_app = Mock (spec = FastAPI )
102+ mock_app .openapi .return_value = {"openapi" : "3.0.0" , "info" : {"title" : "Test API" }}
103+
104+ robust_openapi = create_robust_openapi_function (mock_app )
105+ result = robust_openapi ()
106+
107+ assert result == {"openapi" : "3.0.0" , "info" : {"title" : "Test API" }}
108+
109+ @patch ('google.adk.utils.pydantic_v2_compatibility.logger' )
110+ def test_create_robust_openapi_function_recursion_error (self , mock_logger ):
111+ """Test robust OpenAPI function handles RecursionError."""
112+ mock_app = Mock (spec = FastAPI )
113+ mock_app .openapi .side_effect = RecursionError ("Maximum recursion depth exceeded" )
114+
115+ robust_openapi = create_robust_openapi_function (mock_app )
116+ result = robust_openapi ()
117+
118+ # Should return fallback schema
119+ assert "openapi" in result
120+ assert "info" in result
121+ assert result ["info" ]["title" ] == "ADK Agent API"
122+ mock_logger .warning .assert_called ()
123+
124+ @patch ('google.adk.utils.pydantic_v2_compatibility.logger' )
125+ @patch ('google.adk.utils.pydantic_v2_compatibility.sys' )
126+ def test_create_robust_openapi_function_recursion_limit_handling (self , mock_sys , mock_logger ):
127+ """Test recursion limit handling in robust OpenAPI function."""
128+ mock_app = Mock (spec = FastAPI )
129+ mock_app .openapi .return_value = {"openapi" : "3.0.0" }
130+ mock_sys .getrecursionlimit .return_value = 1000
131+
132+ robust_openapi = create_robust_openapi_function (mock_app )
133+ result = robust_openapi ()
134+
135+ # Verify recursion limit was set
136+ mock_sys .setrecursionlimit .assert_called_with (500 )
137+ # Verify it was restored
138+ assert mock_sys .setrecursionlimit .call_count == 2
139+
140+ @patch ('google.adk.utils.pydantic_v2_compatibility.logger' )
141+ def test_create_robust_openapi_function_generic_exception (self , mock_logger ):
142+ """Test robust OpenAPI function handles generic exceptions."""
143+ mock_app = Mock (spec = FastAPI )
144+ mock_app .openapi .side_effect = Exception ("Generic error" )
145+
146+ robust_openapi = create_robust_openapi_function (mock_app )
147+ result = robust_openapi ()
148+
149+ # Should return fallback schema
150+ assert "openapi" in result
151+ assert "info" in result
152+ mock_logger .error .assert_called ()
153+
154+ @patch ('google.adk.utils.pydantic_v2_compatibility.logger' )
155+ def test_create_robust_openapi_function_attribute_error (self , mock_logger ):
156+ """Test robust OpenAPI function handles AttributeError."""
157+ mock_app = Mock ()
158+ # Remove openapi method to trigger AttributeError
159+ del mock_app .openapi
160+
161+ robust_openapi = create_robust_openapi_function (mock_app )
162+ result = robust_openapi ()
163+
164+ # Should return fallback schema
165+ assert "openapi" in result
166+ assert "info" in result
167+ mock_logger .error .assert_called ()
168+
169+ def test_robust_openapi_fallback_schema_structure (self ):
170+ """Test the structure of the fallback OpenAPI schema."""
171+ mock_app = Mock (spec = FastAPI )
172+ mock_app .openapi .side_effect = Exception ("Error" )
173+
174+ robust_openapi = create_robust_openapi_function (mock_app )
175+ result = robust_openapi ()
176+
177+ # Verify required OpenAPI structure
178+ assert result ["openapi" ] == "3.0.0"
179+ assert "info" in result
180+ assert result ["info" ]["title" ] == "ADK Agent API"
181+ assert result ["info" ]["version" ] == "1.0.0"
182+ assert "paths" in result
183+ assert "components" in result
184+ assert "schemas" in result ["components" ]
185+
186+ @patch ('google.adk.utils.pydantic_v2_compatibility.httpx' , create = True )
187+ def test_patch_httpx_client_success (self ):
188+ """Test successful patching of httpx Client."""
189+ mock_client = Mock ()
190+
191+ with patch ('google.adk.utils.pydantic_v2_compatibility.patch_types_for_pydantic_v2' ) as mock_patch :
192+ mock_patch .return_value = True
193+ result = patch_types_for_pydantic_v2 ()
194+
195+ assert result is True
196+
197+ def test_robust_openapi_preserves_successful_schema (self ):
198+ """Test that robust OpenAPI preserves successful schema generation."""
199+ mock_app = Mock (spec = FastAPI )
200+ expected_schema = {
201+ "openapi" : "3.0.0" ,
202+ "info" : {"title" : "Custom API" , "version" : "2.0.0" },
203+ "paths" : {"/test" : {"get" : {"summary" : "Test endpoint" }}},
204+ "components" : {"schemas" : {"TestModel" : {"type" : "object" }}}
205+ }
206+ mock_app .openapi .return_value = expected_schema
207+
208+ robust_openapi = create_robust_openapi_function (mock_app )
209+ result = robust_openapi ()
210+
211+ assert result == expected_schema
212+
213+ @patch ('google.adk.utils.pydantic_v2_compatibility.logger' )
214+ def test_create_robust_openapi_logs_errors_appropriately (self , mock_logger ):
215+ """Test that robust OpenAPI function logs errors with appropriate levels."""
216+ mock_app = Mock (spec = FastAPI )
217+
218+ # Test RecursionError logging
219+ mock_app .openapi .side_effect = RecursionError ("Recursion error" )
220+ robust_openapi = create_robust_openapi_function (mock_app )
221+ robust_openapi ()
222+ mock_logger .warning .assert_called ()
223+
224+ # Reset and test generic Exception logging
225+ mock_logger .reset_mock ()
226+ mock_app .openapi .side_effect = ValueError ("Generic error" )
227+ robust_openapi = create_robust_openapi_function (mock_app )
228+ robust_openapi ()
229+ mock_logger .error .assert_called ()
0 commit comments