/ tests / test_mcp_tools_google_ads_extensions.py
test_mcp_tools_google_ads_extensions.py
  1  """Google Ads MCP拡張ツール ハンドラーテスト
  2  
  3  サイトリンク、コールアウト、コンバージョン、ターゲティング、変更履歴ハンドラーの検証。
  4  """
  5  
  6  from __future__ import annotations
  7  
  8  import json
  9  from unittest.mock import AsyncMock, MagicMock, patch
 10  
 11  import pytest
 12  
 13  
 14  def _import_google_ads_tools():
 15      from mureo.mcp import tools_google_ads
 16  
 17      return tools_google_ads
 18  
 19  
 20  def _import_handlers():
 21      from mureo.mcp import _handlers_google_ads
 22  
 23      return _handlers_google_ads
 24  
 25  
 26  def _mock_google_ads_context():
 27      """Google Ads認証情報とクライアントのモックを返す"""
 28      mock_client = AsyncMock()
 29      mock_creds = MagicMock()
 30      return mock_creds, mock_client
 31  
 32  
 33  # ---------------------------------------------------------------------------
 34  # ハンドラーテスト — サイトリンク
 35  # ---------------------------------------------------------------------------
 36  
 37  
 38  @pytest.mark.unit
 39  class TestGoogleAdsSitelinkHandlers:
 40      """サイトリンク系ハンドラーテスト"""
 41  
 42      async def test_sitelinks_list(self) -> None:
 43          mod = _import_google_ads_tools()
 44          creds, client = _mock_google_ads_context()
 45          client.list_sitelinks.return_value = [{"id": "1", "link_text": "Top"}]
 46  
 47          h = _import_handlers()
 48          with (
 49              patch.object(h, "load_google_ads_credentials", return_value=creds),
 50              patch.object(h, "create_google_ads_client", return_value=client),
 51          ):
 52              result = await mod.handle_tool(
 53                  "google_ads.sitelinks.list",
 54                  {"customer_id": "123", "campaign_id": "456"},
 55              )
 56  
 57          client.list_sitelinks.assert_awaited_once_with("456")
 58          parsed = json.loads(result[0].text)
 59          assert parsed[0]["link_text"] == "Top"
 60  
 61      async def test_sitelinks_create(self) -> None:
 62          mod = _import_google_ads_tools()
 63          creds, client = _mock_google_ads_context()
 64          client.create_sitelink.return_value = {"resource_name": "res"}
 65  
 66          h = _import_handlers()
 67          with (
 68              patch.object(h, "load_google_ads_credentials", return_value=creds),
 69              patch.object(h, "create_google_ads_client", return_value=client),
 70          ):
 71              result = await mod.handle_tool(
 72                  "google_ads.sitelinks.create",
 73                  {
 74                      "customer_id": "123",
 75                      "campaign_id": "456",
 76                      "link_text": "About",
 77                      "final_url": "https://example.com/about",
 78                  },
 79              )
 80  
 81          client.create_sitelink.assert_awaited_once()
 82          parsed = json.loads(result[0].text)
 83          assert "resource_name" in parsed
 84  
 85      async def test_sitelinks_remove(self) -> None:
 86          mod = _import_google_ads_tools()
 87          creds, client = _mock_google_ads_context()
 88          client.remove_sitelink.return_value = {"resource_name": "res"}
 89  
 90          h = _import_handlers()
 91          with (
 92              patch.object(h, "load_google_ads_credentials", return_value=creds),
 93              patch.object(h, "create_google_ads_client", return_value=client),
 94          ):
 95              result = await mod.handle_tool(
 96                  "google_ads.sitelinks.remove",
 97                  {"customer_id": "123", "campaign_id": "456", "asset_id": "789"},
 98              )
 99  
100          client.remove_sitelink.assert_awaited_once()
101          parsed = json.loads(result[0].text)
102          assert "resource_name" in parsed
103  
104  
105  # ---------------------------------------------------------------------------
106  # ハンドラーテスト — コールアウト
107  # ---------------------------------------------------------------------------
108  
109  
110  @pytest.mark.unit
111  class TestGoogleAdsCalloutHandlers:
112      """コールアウト系ハンドラーテスト"""
113  
114      async def test_callouts_list(self) -> None:
115          mod = _import_google_ads_tools()
116          creds, client = _mock_google_ads_context()
117          client.list_callouts.return_value = [{"id": "1", "text": "Free shipping"}]
118  
119          h = _import_handlers()
120          with (
121              patch.object(h, "load_google_ads_credentials", return_value=creds),
122              patch.object(h, "create_google_ads_client", return_value=client),
123          ):
124              result = await mod.handle_tool(
125                  "google_ads.callouts.list",
126                  {"customer_id": "123", "campaign_id": "456"},
127              )
128  
129          client.list_callouts.assert_awaited_once_with("456")
130          parsed = json.loads(result[0].text)
131          assert len(parsed) == 1
132  
133      async def test_callouts_create(self) -> None:
134          mod = _import_google_ads_tools()
135          creds, client = _mock_google_ads_context()
136          client.create_callout.return_value = {"resource_name": "res"}
137  
138          h = _import_handlers()
139          with (
140              patch.object(h, "load_google_ads_credentials", return_value=creds),
141              patch.object(h, "create_google_ads_client", return_value=client),
142          ):
143              result = await mod.handle_tool(
144                  "google_ads.callouts.create",
145                  {
146                      "customer_id": "123",
147                      "campaign_id": "456",
148                      "callout_text": "Free shipping",
149                  },
150              )
151  
152          client.create_callout.assert_awaited_once()
153          parsed = json.loads(result[0].text)
154          assert "resource_name" in parsed
155  
156      async def test_callouts_remove(self) -> None:
157          mod = _import_google_ads_tools()
158          creds, client = _mock_google_ads_context()
159          client.remove_callout.return_value = {"resource_name": "res"}
160  
161          h = _import_handlers()
162          with (
163              patch.object(h, "load_google_ads_credentials", return_value=creds),
164              patch.object(h, "create_google_ads_client", return_value=client),
165          ):
166              result = await mod.handle_tool(
167                  "google_ads.callouts.remove",
168                  {"customer_id": "123", "campaign_id": "456", "asset_id": "789"},
169              )
170  
171          client.remove_callout.assert_awaited_once()
172          parsed = json.loads(result[0].text)
173          assert "resource_name" in parsed
174  
175  
176  # ---------------------------------------------------------------------------
177  # ハンドラーテスト — コンバージョン
178  # ---------------------------------------------------------------------------
179  
180  
181  @pytest.mark.unit
182  class TestGoogleAdsConversionHandlers:
183      """コンバージョン系ハンドラーテスト"""
184  
185      async def test_conversions_list(self) -> None:
186          mod = _import_google_ads_tools()
187          creds, client = _mock_google_ads_context()
188          client.list_conversion_actions.return_value = [{"id": "1", "name": "Purchase"}]
189  
190          h = _import_handlers()
191          with (
192              patch.object(h, "load_google_ads_credentials", return_value=creds),
193              patch.object(h, "create_google_ads_client", return_value=client),
194          ):
195              result = await mod.handle_tool(
196                  "google_ads.conversions.list",
197                  {"customer_id": "123"},
198              )
199  
200          client.list_conversion_actions.assert_awaited_once()
201          parsed = json.loads(result[0].text)
202          assert len(parsed) == 1
203  
204      async def test_conversions_get(self) -> None:
205          mod = _import_google_ads_tools()
206          creds, client = _mock_google_ads_context()
207          client.get_conversion_action.return_value = {"id": "10", "name": "Purchase"}
208  
209          h = _import_handlers()
210          with (
211              patch.object(h, "load_google_ads_credentials", return_value=creds),
212              patch.object(h, "create_google_ads_client", return_value=client),
213          ):
214              result = await mod.handle_tool(
215                  "google_ads.conversions.get",
216                  {"customer_id": "123", "conversion_action_id": "10"},
217              )
218  
219          client.get_conversion_action.assert_awaited_once_with("10")
220          parsed = json.loads(result[0].text)
221          assert parsed["id"] == "10"
222  
223      async def test_conversions_performance(self) -> None:
224          mod = _import_google_ads_tools()
225          creds, client = _mock_google_ads_context()
226          client.get_conversion_performance.return_value = {"conversions": 50}
227  
228          h = _import_handlers()
229          with (
230              patch.object(h, "load_google_ads_credentials", return_value=creds),
231              patch.object(h, "create_google_ads_client", return_value=client),
232          ):
233              result = await mod.handle_tool(
234                  "google_ads.conversions.performance",
235                  {"customer_id": "123"},
236              )
237  
238          client.get_conversion_performance.assert_awaited_once()
239          parsed = json.loads(result[0].text)
240          assert parsed["conversions"] == 50
241  
242      async def test_conversions_create(self) -> None:
243          mod = _import_google_ads_tools()
244          creds, client = _mock_google_ads_context()
245          client.create_conversion_action.return_value = {"resource_name": "res"}
246  
247          h = _import_handlers()
248          with (
249              patch.object(h, "load_google_ads_credentials", return_value=creds),
250              patch.object(h, "create_google_ads_client", return_value=client),
251          ):
252              result = await mod.handle_tool(
253                  "google_ads.conversions.create",
254                  {"customer_id": "123", "name": "New Conversion"},
255              )
256  
257          client.create_conversion_action.assert_awaited_once()
258          parsed = json.loads(result[0].text)
259          assert "resource_name" in parsed
260  
261      async def test_conversions_update(self) -> None:
262          mod = _import_google_ads_tools()
263          creds, client = _mock_google_ads_context()
264          client.update_conversion_action.return_value = {"resource_name": "res"}
265  
266          h = _import_handlers()
267          with (
268              patch.object(h, "load_google_ads_credentials", return_value=creds),
269              patch.object(h, "create_google_ads_client", return_value=client),
270          ):
271              result = await mod.handle_tool(
272                  "google_ads.conversions.update",
273                  {
274                      "customer_id": "123",
275                      "conversion_action_id": "10",
276                      "name": "Updated",
277                  },
278              )
279  
280          client.update_conversion_action.assert_awaited_once()
281          parsed = json.loads(result[0].text)
282          assert "resource_name" in parsed
283  
284      async def test_conversions_remove(self) -> None:
285          mod = _import_google_ads_tools()
286          creds, client = _mock_google_ads_context()
287          client.remove_conversion_action.return_value = {"resource_name": "res"}
288  
289          h = _import_handlers()
290          with (
291              patch.object(h, "load_google_ads_credentials", return_value=creds),
292              patch.object(h, "create_google_ads_client", return_value=client),
293          ):
294              result = await mod.handle_tool(
295                  "google_ads.conversions.remove",
296                  {"customer_id": "123", "conversion_action_id": "10"},
297              )
298  
299          client.remove_conversion_action.assert_awaited_once()
300          parsed = json.loads(result[0].text)
301          assert "resource_name" in parsed
302  
303      async def test_conversions_tag(self) -> None:
304          mod = _import_google_ads_tools()
305          creds, client = _mock_google_ads_context()
306          client.get_conversion_action_tag.return_value = {"tag": "<script>...</script>"}
307  
308          h = _import_handlers()
309          with (
310              patch.object(h, "load_google_ads_credentials", return_value=creds),
311              patch.object(h, "create_google_ads_client", return_value=client),
312          ):
313              result = await mod.handle_tool(
314                  "google_ads.conversions.tag",
315                  {"customer_id": "123", "conversion_action_id": "10"},
316              )
317  
318          client.get_conversion_action_tag.assert_awaited_once_with("10")
319          parsed = json.loads(result[0].text)
320          assert "tag" in parsed
321  
322  
323  # ---------------------------------------------------------------------------
324  # ハンドラーテスト — ターゲティング
325  # ---------------------------------------------------------------------------
326  
327  
328  @pytest.mark.unit
329  class TestGoogleAdsTargetingHandlers:
330      """ターゲティング系ハンドラーテスト"""
331  
332      async def test_recommendations_list(self) -> None:
333          mod = _import_google_ads_tools()
334          creds, client = _mock_google_ads_context()
335          client.list_recommendations.return_value = [{"type": "KEYWORD"}]
336  
337          h = _import_handlers()
338          with (
339              patch.object(h, "load_google_ads_credentials", return_value=creds),
340              patch.object(h, "create_google_ads_client", return_value=client),
341          ):
342              result = await mod.handle_tool(
343                  "google_ads.recommendations.list",
344                  {"customer_id": "123"},
345              )
346  
347          client.list_recommendations.assert_awaited_once()
348          parsed = json.loads(result[0].text)
349          assert parsed[0]["type"] == "KEYWORD"
350  
351      async def test_recommendations_apply(self) -> None:
352          mod = _import_google_ads_tools()
353          creds, client = _mock_google_ads_context()
354          client.apply_recommendation.return_value = {"resource_name": "res"}
355  
356          h = _import_handlers()
357          with (
358              patch.object(h, "load_google_ads_credentials", return_value=creds),
359              patch.object(h, "create_google_ads_client", return_value=client),
360          ):
361              result = await mod.handle_tool(
362                  "google_ads.recommendations.apply",
363                  {
364                      "customer_id": "123",
365                      "resource_name": "customers/123/recommendations/456",
366                  },
367              )
368  
369          client.apply_recommendation.assert_awaited_once()
370          parsed = json.loads(result[0].text)
371          assert "resource_name" in parsed
372  
373      async def test_device_targeting_get(self) -> None:
374          mod = _import_google_ads_tools()
375          creds, client = _mock_google_ads_context()
376          client.get_device_targeting.return_value = {"devices": ["MOBILE", "DESKTOP"]}
377  
378          h = _import_handlers()
379          with (
380              patch.object(h, "load_google_ads_credentials", return_value=creds),
381              patch.object(h, "create_google_ads_client", return_value=client),
382          ):
383              result = await mod.handle_tool(
384                  "google_ads.device_targeting.get",
385                  {"customer_id": "123", "campaign_id": "456"},
386              )
387  
388          client.get_device_targeting.assert_awaited_once_with("456")
389          parsed = json.loads(result[0].text)
390          assert "MOBILE" in parsed["devices"]
391  
392      async def test_device_targeting_set(self) -> None:
393          mod = _import_google_ads_tools()
394          creds, client = _mock_google_ads_context()
395          client.set_device_targeting.return_value = {"resource_name": "res"}
396  
397          h = _import_handlers()
398          with (
399              patch.object(h, "load_google_ads_credentials", return_value=creds),
400              patch.object(h, "create_google_ads_client", return_value=client),
401          ):
402              result = await mod.handle_tool(
403                  "google_ads.device_targeting.set",
404                  {
405                      "customer_id": "123",
406                      "campaign_id": "456",
407                      "enabled_devices": ["MOBILE", "DESKTOP"],
408                  },
409              )
410  
411          client.set_device_targeting.assert_awaited_once()
412          parsed = json.loads(result[0].text)
413          assert "resource_name" in parsed
414  
415      async def test_bid_adjustments_get(self) -> None:
416          mod = _import_google_ads_tools()
417          creds, client = _mock_google_ads_context()
418          client.get_bid_adjustments.return_value = [{"criterion_id": "1"}]
419  
420          h = _import_handlers()
421          with (
422              patch.object(h, "load_google_ads_credentials", return_value=creds),
423              patch.object(h, "create_google_ads_client", return_value=client),
424          ):
425              result = await mod.handle_tool(
426                  "google_ads.bid_adjustments.get",
427                  {"customer_id": "123", "campaign_id": "456"},
428              )
429  
430          client.get_bid_adjustments.assert_awaited_once_with("456")
431          parsed = json.loads(result[0].text)
432          assert parsed[0]["criterion_id"] == "1"
433  
434      async def test_bid_adjustments_update(self) -> None:
435          mod = _import_google_ads_tools()
436          creds, client = _mock_google_ads_context()
437          client.update_bid_adjustment.return_value = {"resource_name": "res"}
438  
439          h = _import_handlers()
440          with (
441              patch.object(h, "load_google_ads_credentials", return_value=creds),
442              patch.object(h, "create_google_ads_client", return_value=client),
443          ):
444              result = await mod.handle_tool(
445                  "google_ads.bid_adjustments.update",
446                  {
447                      "customer_id": "123",
448                      "campaign_id": "456",
449                      "criterion_id": "1",
450                      "bid_modifier": 1.2,
451                  },
452              )
453  
454          client.update_bid_adjustment.assert_awaited_once()
455          parsed = json.loads(result[0].text)
456          assert "resource_name" in parsed
457  
458      async def test_location_targeting_list(self) -> None:
459          mod = _import_google_ads_tools()
460          creds, client = _mock_google_ads_context()
461          client.list_location_targeting.return_value = [{"location": "Tokyo"}]
462  
463          h = _import_handlers()
464          with (
465              patch.object(h, "load_google_ads_credentials", return_value=creds),
466              patch.object(h, "create_google_ads_client", return_value=client),
467          ):
468              result = await mod.handle_tool(
469                  "google_ads.location_targeting.list",
470                  {"customer_id": "123", "campaign_id": "456"},
471              )
472  
473          client.list_location_targeting.assert_awaited_once_with("456")
474          parsed = json.loads(result[0].text)
475          assert parsed[0]["location"] == "Tokyo"
476  
477      async def test_location_targeting_update(self) -> None:
478          mod = _import_google_ads_tools()
479          creds, client = _mock_google_ads_context()
480          client.update_location_targeting.return_value = {"resource_name": "res"}
481  
482          h = _import_handlers()
483          with (
484              patch.object(h, "load_google_ads_credentials", return_value=creds),
485              patch.object(h, "create_google_ads_client", return_value=client),
486          ):
487              result = await mod.handle_tool(
488                  "google_ads.location_targeting.update",
489                  {
490                      "customer_id": "123",
491                      "campaign_id": "456",
492                      "add_locations": [{"geo_target_id": "1009312"}],
493                  },
494              )
495  
496          client.update_location_targeting.assert_awaited_once()
497          parsed = json.loads(result[0].text)
498          assert "resource_name" in parsed
499  
500      async def test_schedule_targeting_list(self) -> None:
501          mod = _import_google_ads_tools()
502          creds, client = _mock_google_ads_context()
503          client.list_schedule_targeting.return_value = [{"day": "MONDAY"}]
504  
505          h = _import_handlers()
506          with (
507              patch.object(h, "load_google_ads_credentials", return_value=creds),
508              patch.object(h, "create_google_ads_client", return_value=client),
509          ):
510              result = await mod.handle_tool(
511                  "google_ads.schedule_targeting.list",
512                  {"customer_id": "123", "campaign_id": "456"},
513              )
514  
515          client.list_schedule_targeting.assert_awaited_once_with("456")
516          parsed = json.loads(result[0].text)
517          assert parsed[0]["day"] == "MONDAY"
518  
519      async def test_schedule_targeting_update(self) -> None:
520          mod = _import_google_ads_tools()
521          creds, client = _mock_google_ads_context()
522          client.update_schedule_targeting.return_value = {"resource_name": "res"}
523  
524          h = _import_handlers()
525          with (
526              patch.object(h, "load_google_ads_credentials", return_value=creds),
527              patch.object(h, "create_google_ads_client", return_value=client),
528          ):
529              result = await mod.handle_tool(
530                  "google_ads.schedule_targeting.update",
531                  {
532                      "customer_id": "123",
533                      "campaign_id": "456",
534                      "add_schedules": [
535                          {"day": "MONDAY", "start_hour": 9, "end_hour": 17}
536                      ],
537                  },
538              )
539  
540          client.update_schedule_targeting.assert_awaited_once()
541          parsed = json.loads(result[0].text)
542          assert "resource_name" in parsed
543  
544      async def test_change_history_list(self) -> None:
545          mod = _import_google_ads_tools()
546          creds, client = _mock_google_ads_context()
547          client.list_change_history.return_value = [{"change_type": "UPDATE"}]
548  
549          h = _import_handlers()
550          with (
551              patch.object(h, "load_google_ads_credentials", return_value=creds),
552              patch.object(h, "create_google_ads_client", return_value=client),
553          ):
554              result = await mod.handle_tool(
555                  "google_ads.change_history.list",
556                  {"customer_id": "123"},
557              )
558  
559          client.list_change_history.assert_awaited_once()
560          parsed = json.loads(result[0].text)
561          assert parsed[0]["change_type"] == "UPDATE"