@@ -24,26 +24,57 @@ def test_main_runs_successfully(mock_uuid4: MagicMock, mock_google_ads_client: M
2424 # Suggest Keyword Themes
2525 mock_kw_theme_response = MagicMock ()
2626 mock_kw_theme1 = MagicMock ()
27+ mock_kw_theme1 .keyword_theme_constant = MagicMock () # Simulate protobuf message structure
2728 mock_kw_theme1 .keyword_theme_constant .resource_name = "keywordThemeConstants/theme1"
28- mock_kw_theme1 .keyword_theme_constant .name = "Theme 1"
29+ # Ensure 'oneof' behavior; if keyword_theme_constant is set, free_form_keyword_theme is not.
30+ # MagicMock by default would create these attributes if accessed.
31+ # Explicitly deleting or ensuring they are None if not the active 'oneof' field.
32+ # However, the script checks hasattr(theme, "keyword_theme_constant"), so direct assignment is enough.
33+
2934 mock_kw_theme2 = MagicMock ()
35+ mock_kw_theme2 .keyword_theme_constant = MagicMock ()
3036 mock_kw_theme2 .keyword_theme_constant .resource_name = "keywordThemeConstants/theme2"
31- mock_kw_theme2 . keyword_theme_constant . name = "Theme 2"
37+
3238 mock_kw_theme_response .keyword_themes = [mock_kw_theme1 , mock_kw_theme2 ]
33- # Suggestion for free-form keyword
34- mock_free_form_suggestion = MagicMock ()
35- mock_free_form_suggestion .keyword_theme_constant .resource_name = "keywordThemeConstants/freeform"
36- mock_free_form_suggestion .keyword_theme_constant .name = "FreeForm Theme"
3739
40+ # Suggestion for free-form keyword (this path is hit by the script)
41+ # This mock should return a KeywordTheme message where free_form_keyword_theme is set.
42+ mock_free_form_response = MagicMock ()
43+ mock_free_form_theme = MagicMock ()
44+ mock_free_form_theme .free_form_keyword_theme = "free form keyword theme text" # This is the actual text value
45+ # Ensure 'oneof' behavior for this mock too
46+ delattr (mock_free_form_theme , 'keyword_theme_constant' )
47+ mock_free_form_response .keyword_themes = [mock_free_form_theme ]
48+
3849 # Side effect to handle different request types for suggest_keyword_themes
3950 def suggest_keyword_themes_side_effect (request ):
4051 if request .suggestion_info .free_form_text :
41- response = MagicMock ()
42- response .keyword_themes = [mock_free_form_suggestion ]
43- return response
44- return mock_kw_theme_response
45- mock_suggest_service .suggest_keyword_themes .side_effect = suggest_keyword_themes_side_effect
52+ return mock_free_form_response
53+ # This case is for request.suggestion_info.keyword_seed
54+ # The script does not use this path for suggest_keyword_themes.
55+ # It uses suggest_keyword_theme_constants for keyword_text.
56+ # However, to be safe, if it were called with keyword_seed:
57+ # return mock_kw_theme_response # This was the previous behavior
58+ # For now, let's assume it's not called with keyword_seed for suggest_keyword_themes
59+ # and the free_form_text path is the only one used by suggest_keyword_themes.
60+ # If suggest_keyword_themes is called for the seed, it should also return KeywordThemes.
61+ # The script calls:
62+ # 1. suggest_keyword_theme_constants with keyword_text (mocked below)
63+ # 2. suggest_keyword_themes with free_form_keyword_text (mocked by mock_free_form_response)
64+ # So, the keyword_seed path for suggest_keyword_themes is actually NOT hit.
65+ # Let's simplify the side_effect or remove it if only one path is hit.
66+ # Re-checking script: _get_keyword_theme_infos calls suggest_keyword_themes for free_form_keyword_text
67+ # and _get_keyword_theme_auto_suggestions calls suggest_keyword_theme_constants for keyword_text
68+ # This means the side_effect for suggest_keyword_themes only needs to handle the free_form_text case.
69+ if hasattr (request .suggestion_info , "free_form_text" ) and request .suggestion_info .free_form_text :
70+ return mock_free_form_response
71+ # Fallback if called unexpectedly (should not happen based on script logic)
72+ empty_response = MagicMock ()
73+ empty_response .keyword_themes = []
74+ return empty_response
4675
76+ mock_suggest_service .suggest_keyword_themes .side_effect = suggest_keyword_themes_side_effect
77+
4778 # Suggest Budget Options
4879 mock_budget_options_response = MagicMock ()
4980 mock_recommended_budget = MagicMock ()
@@ -67,8 +98,18 @@ def suggest_keyword_themes_side_effect(request):
6798
6899 # KeywordThemeConstantService
69100 mock_ktc_service = mock_google_ads_client .get_service ("KeywordThemeConstantService" )
101+ # Mock for suggest_keyword_theme_constants (called by _get_keyword_theme_auto_suggestions)
102+ mock_ktc_suggest_response = MagicMock ()
103+ mock_ktc_constant1_from_suggest = MagicMock () # Simulates KeywordThemeConstant
104+ mock_ktc_constant1_from_suggest .resource_name = "keywordThemeConstants/from_text_search1"
105+ # mock_ktc_constant1_from_suggest.name = "From Text Search 1" # Name is not directly used by script
106+ mock_ktc_suggest_response .keyword_theme_constants = [mock_ktc_constant1_from_suggest ]
107+ mock_ktc_service .suggest_keyword_theme_constants .return_value = mock_ktc_suggest_response
108+ # Mock for keyword_theme_constant_path (used by _get_keyword_theme_infos if keyword_theme_ids are present)
109+ # This path is not hit with current main() args as keyword_theme_ids is not passed.
70110 mock_ktc_service .keyword_theme_constant_path .side_effect = lambda ktc_id : f"keywordThemeConstants/{ ktc_id } "
71111
112+
72113 # GeoTargetConstantService
73114 mock_geo_service = mock_google_ads_client .get_service ("GeoTargetConstantService" )
74115 # The script uses a hardcoded geo target "2840" (USA)
@@ -93,8 +134,11 @@ def suggest_keyword_themes_side_effect(request):
93134 responses .append (MagicMock (smart_campaign_setting_result = MagicMock (resource_name = f"customers/{ mock_customer_id } /smartCampaignSettings/scs1" )))
94135 responses .append (MagicMock (ad_group_result = MagicMock (resource_name = f"customers/{ mock_customer_id } /adGroups/adgroup1" )))
95136 responses .append (MagicMock (ad_group_ad_result = MagicMock (resource_name = f"customers/{ mock_customer_id } /adGroupAds/ad1" )))
96- # For keyword theme criteria (2 from suggestion + 1 free form) and 1 location criterion
97- for i in range (4 ): # 3 keyword themes + 1 location
137+ # For keyword theme criteria:
138+ # 1 from suggest_keyword_theme_constants (mock_ktc_constant1_from_suggest)
139+ # 1 from suggest_keyword_themes (mock_free_form_theme)
140+ # Total 2 keyword theme criteria + 1 location criterion
141+ for i in range (3 ):
98142 responses .append (MagicMock (campaign_criterion_result = MagicMock (resource_name = f"customers/{ mock_customer_id } /campaignCriteria/crit{ i } " )))
99143
100144 mock_mutate_response .mutate_operation_responses = responses
@@ -137,16 +181,22 @@ def test_main_with_business_location_runs_successfully(mock_uuid4_biz: MagicMock
137181 # Most mocks can be similar to the first test, just ensure business_profile_location_path is set up
138182 # SmartCampaignSuggestService
139183 mock_suggest_service = mock_google_ads_client .get_service ("SmartCampaignSuggestService" )
140- mock_kw_theme_response = MagicMock () # Define as in first test
141- mock_kw_theme1 = MagicMock ()
142- mock_kw_theme1 .keyword_theme_constant .resource_name = "keywordThemeConstants/theme_biz1"
143- mock_kw_theme1 .keyword_theme_constant .name = "Theme Biz 1"
144- mock_kw_theme_response .keyword_themes = [mock_kw_theme1 ]
145- def suggest_keyword_themes_side_effect_biz (request ): # Simplified for this test variation
146- return mock_kw_theme_response
147- mock_suggest_service .suggest_keyword_themes .side_effect = suggest_keyword_themes_side_effect_biz
148-
149- mock_budget_options_response = MagicMock () # Define as in first test
184+
185+ # Mock for suggest_keyword_themes (called for free_form_keyword_text if provided)
186+ # In this test, free_form_keyword_text is None, so suggest_keyword_themes might not be called,
187+ # or if called, request.suggestion_info.free_form_text will be empty.
188+ # The script's _get_keyword_theme_infos checks if free_form_keyword_text:
189+ # if free_form_keyword_text:
190+ # response = smart_campaign_suggest_service.suggest_keyword_themes(request)
191+ # keyword_theme_infos.extend(map_keyword_themes_to_keyword_infos(response.keyword_themes))
192+ # So, if free_form_keyword_text is None, suggest_keyword_themes is NOT called.
193+ # Thus, we don't need to mock its response extensively for this specific test case path.
194+ # However, if it were called, it should return an empty list or correctly structured mock.
195+ mock_empty_kw_theme_response = MagicMock ()
196+ mock_empty_kw_theme_response .keyword_themes = []
197+ mock_suggest_service .suggest_keyword_themes .return_value = mock_empty_kw_theme_response # Default for this test path
198+
199+ mock_budget_options_response = MagicMock ()
150200 mock_recommended_budget = MagicMock ()
151201 mock_recommended_budget .daily_amount_micros = 55000000
152202 mock_budget_options_response .recommended_daily_budget_options .high .daily_amount_micros = 65000000
@@ -166,6 +216,13 @@ def suggest_keyword_themes_side_effect_biz(request): # Simplified for this test
166216 mock_suggest_service .suggest_smart_campaign_ad .return_value = mock_ad_suggestion_response
167217
168218 mock_ktc_service = mock_google_ads_client .get_service ("KeywordThemeConstantService" )
219+ # Mock for suggest_keyword_theme_constants (called by _get_keyword_theme_auto_suggestions for keyword_text)
220+ mock_ktc_suggest_response_biz = MagicMock ()
221+ mock_ktc_constant1_biz = MagicMock () # Simulates KeywordThemeConstant
222+ mock_ktc_constant1_biz .resource_name = "keywordThemeConstants/from_text_search_biz1"
223+ mock_ktc_suggest_response_biz .keyword_theme_constants = [mock_ktc_constant1_biz ]
224+ mock_ktc_service .suggest_keyword_theme_constants .return_value = mock_ktc_suggest_response_biz
225+ # Mock for keyword_theme_constant_path (not hit with current args)
169226 mock_ktc_service .keyword_theme_constant_path .side_effect = lambda ktc_id : f"keywordThemeConstants/{ ktc_id } "
170227
171228 mock_geo_service = mock_google_ads_client .get_service ("GeoTargetConstantService" )
@@ -184,7 +241,10 @@ def suggest_keyword_themes_side_effect_biz(request): # Simplified for this test
184241 responses .append (MagicMock (smart_campaign_setting_result = MagicMock (resource_name = f"customers/{ mock_customer_id } /smartCampaignSettings/scs_biz" )))
185242 responses .append (MagicMock (ad_group_result = MagicMock (resource_name = f"customers/{ mock_customer_id } /adGroups/adgroup_biz" )))
186243 responses .append (MagicMock (ad_group_ad_result = MagicMock (resource_name = f"customers/{ mock_customer_id } /adGroupAds/ad_biz" )))
187- # Fewer keyword themes in this simplified version for biz profile path (1 suggested + 0 free-form) + 1 location
244+ # Keyword themes for criteria:
245+ # 1 from suggest_keyword_theme_constants (mock_ktc_constant1_biz)
246+ # 0 from suggest_keyword_themes (because free_form_keyword_text is None)
247+ # Total 1 keyword theme criterion + 1 location criterion
188248 for i in range (2 ):
189249 responses .append (MagicMock (campaign_criterion_result = MagicMock (resource_name = f"customers/{ mock_customer_id } /campaignCriteria/crit_biz{ i } " )))
190250 mock_mutate_response .mutate_operation_responses = responses
@@ -207,9 +267,9 @@ def suggest_keyword_themes_side_effect_biz(request): # Simplified for this test
207267 mock_google_ads_client ,
208268 mock_customer_id ,
209269 mock_keyword_text ,
210- None , # No free_form_keyword_text when business profile is used, per script logic
270+ None , # No free_form_keyword_text for this test case
211271 mock_business_profile_location ,
212- mock_business_name # Should be None or ignored by script when biz profile is set
272+ None # Business name is ignored by script when business profile location is set
213273 )
214274 except Exception as e :
215275 pytest .fail (f"main function raised an exception: { e } " )
0 commit comments