/ tests / test_mcp_tools_meta_ads.py
test_mcp_tools_meta_ads.py
  1  """Meta Ads MCPツール定義・ハンドラーテスト
  2  
  3  ツール定義(inputSchema、requiredフィールド)とハンドラー(クライアントモック)の検証。
  4  """
  5  
  6  from __future__ import annotations
  7  
  8  import json
  9  from typing import Any
 10  from unittest.mock import AsyncMock, MagicMock, patch
 11  
 12  import pytest
 13  
 14  
 15  def _import_meta_ads_tools():
 16      from mureo.mcp import tools_meta_ads
 17  
 18      return tools_meta_ads
 19  
 20  
 21  def _import_handlers():
 22      from mureo.mcp import _handlers_meta_ads
 23  
 24      return _handlers_meta_ads
 25  
 26  
 27  # ---------------------------------------------------------------------------
 28  # ツール定義テスト
 29  # ---------------------------------------------------------------------------
 30  
 31  
 32  @pytest.mark.unit
 33  class TestMetaAdsToolDefinitions:
 34      """Meta Adsツール一覧が正しく定義されていることを検証する"""
 35  
 36      def test_tool_count(self) -> None:
 37          """全77ツールが定義されていること"""
 38          mod = _import_meta_ads_tools()
 39          assert len(mod.TOOLS) == 77
 40  
 41      def test_all_tool_names(self) -> None:
 42          """全ツール名がmeta_ads.で始まること"""
 43          mod = _import_meta_ads_tools()
 44          for tool in mod.TOOLS:
 45              assert tool.name.startswith("meta_ads."), f"不正なツール名: {tool.name}"
 46  
 47      def test_all_tools_have_input_schema(self) -> None:
 48          """全ツールにinputSchemaが定義されていること"""
 49          mod = _import_meta_ads_tools()
 50          for tool in mod.TOOLS:
 51              assert "type" in tool.inputSchema
 52              assert tool.inputSchema["type"] == "object"
 53              assert "properties" in tool.inputSchema
 54  
 55      @pytest.mark.parametrize(
 56          "tool_name,expected_required",
 57          [
 58              ("meta_ads.campaigns.list", []),
 59              ("meta_ads.campaigns.get", ["campaign_id"]),
 60              (
 61                  "meta_ads.campaigns.create",
 62                  ["name", "objective"],
 63              ),
 64              ("meta_ads.campaigns.update", ["campaign_id"]),
 65              ("meta_ads.ad_sets.list", []),
 66              (
 67                  "meta_ads.ad_sets.create",
 68                  ["campaign_id", "name"],
 69              ),
 70              ("meta_ads.ad_sets.update", ["ad_set_id"]),
 71              ("meta_ads.ads.list", []),
 72              (
 73                  "meta_ads.ads.create",
 74                  ["ad_set_id", "name", "creative_id"],
 75              ),
 76              ("meta_ads.ads.update", ["ad_id"]),
 77              ("meta_ads.insights.report", []),
 78              (
 79                  "meta_ads.insights.breakdown",
 80                  ["campaign_id"],
 81              ),
 82              ("meta_ads.audiences.list", []),
 83              ("meta_ads.audiences.create", ["name"]),
 84          ],
 85      )
 86      def test_required_fields(
 87          self, tool_name: str, expected_required: list[str]
 88      ) -> None:
 89          """各ツールのrequiredフィールドが正しいこと"""
 90          mod = _import_meta_ads_tools()
 91          tool = next((t for t in mod.TOOLS if t.name == tool_name), None)
 92          assert tool is not None, f"ツール {tool_name} が見つかりません"
 93          assert set(tool.inputSchema["required"]) == set(expected_required)
 94  
 95  
 96  # ---------------------------------------------------------------------------
 97  # ハンドラーテスト — ヘルパー
 98  # ---------------------------------------------------------------------------
 99  
100  
101  def _mock_meta_ads_context():
102      """Meta Ads認証情報とクライアントのモックを返す"""
103      mock_client = AsyncMock()
104      mock_creds = MagicMock()
105      return mock_creds, mock_client
106  
107  
108  # ---------------------------------------------------------------------------
109  # ハンドラーテスト — キャンペーン
110  # ---------------------------------------------------------------------------
111  
112  
113  @pytest.mark.unit
114  class TestMetaAdsCampaignHandlers:
115      """キャンペーン系ハンドラーテスト"""
116  
117      async def test_campaigns_list(self) -> None:
118          mod = _import_meta_ads_tools()
119          handlers = _import_handlers()
120          creds, client = _mock_meta_ads_context()
121          client.list_campaigns.return_value = [{"id": "1", "name": "Meta Camp"}]
122  
123          with (
124              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
125              patch.object(handlers, "create_meta_ads_client", return_value=client),
126          ):
127              result = await mod.handle_tool(
128                  "meta_ads.campaigns.list", {"account_id": "act_123"}
129              )
130  
131          client.list_campaigns.assert_awaited_once()
132          parsed = json.loads(result[0].text)
133          assert parsed[0]["id"] == "1"
134  
135      async def test_campaigns_get(self) -> None:
136          mod = _import_meta_ads_tools()
137          handlers = _import_handlers()
138          creds, client = _mock_meta_ads_context()
139          client.get_campaign.return_value = {"id": "456", "name": "Detail"}
140  
141          with (
142              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
143              patch.object(handlers, "create_meta_ads_client", return_value=client),
144          ):
145              result = await mod.handle_tool(
146                  "meta_ads.campaigns.get",
147                  {"account_id": "act_123", "campaign_id": "456"},
148              )
149  
150          client.get_campaign.assert_awaited_once_with("456")
151  
152      async def test_campaigns_create(self) -> None:
153          mod = _import_meta_ads_tools()
154          handlers = _import_handlers()
155          creds, client = _mock_meta_ads_context()
156          client.create_campaign.return_value = {"id": "789"}
157  
158          with (
159              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
160              patch.object(handlers, "create_meta_ads_client", return_value=client),
161          ):
162              result = await mod.handle_tool(
163                  "meta_ads.campaigns.create",
164                  {
165                      "account_id": "act_123",
166                      "name": "New Camp",
167                      "objective": "CONVERSIONS",
168                  },
169              )
170  
171          client.create_campaign.assert_awaited_once()
172  
173      async def test_campaigns_update(self) -> None:
174          mod = _import_meta_ads_tools()
175          handlers = _import_handlers()
176          creds, client = _mock_meta_ads_context()
177          client.update_campaign.return_value = {"success": True}
178  
179          with (
180              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
181              patch.object(handlers, "create_meta_ads_client", return_value=client),
182          ):
183              result = await mod.handle_tool(
184                  "meta_ads.campaigns.update",
185                  {"account_id": "act_123", "campaign_id": "456", "name": "Updated"},
186              )
187  
188          client.update_campaign.assert_awaited_once()
189  
190  
191  # ---------------------------------------------------------------------------
192  # ハンドラーテスト — 広告セット
193  # ---------------------------------------------------------------------------
194  
195  
196  @pytest.mark.unit
197  class TestMetaAdsAdSetHandlers:
198      """広告セット系ハンドラーテスト"""
199  
200      async def test_ad_sets_list(self) -> None:
201          mod = _import_meta_ads_tools()
202          handlers = _import_handlers()
203          creds, client = _mock_meta_ads_context()
204          client.list_ad_sets.return_value = [{"id": "10", "name": "AS1"}]
205  
206          with (
207              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
208              patch.object(handlers, "create_meta_ads_client", return_value=client),
209          ):
210              result = await mod.handle_tool(
211                  "meta_ads.ad_sets.list", {"account_id": "act_123"}
212              )
213  
214          client.list_ad_sets.assert_awaited_once()
215  
216      async def test_ad_sets_create(self) -> None:
217          mod = _import_meta_ads_tools()
218          handlers = _import_handlers()
219          creds, client = _mock_meta_ads_context()
220          client.create_ad_set.return_value = {"id": "20"}
221  
222          with (
223              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
224              patch.object(handlers, "create_meta_ads_client", return_value=client),
225          ):
226              result = await mod.handle_tool(
227                  "meta_ads.ad_sets.create",
228                  {
229                      "account_id": "act_123",
230                      "campaign_id": "456",
231                      "name": "New AdSet",
232                      "daily_budget": 5000,
233                  },
234              )
235  
236          client.create_ad_set.assert_awaited_once()
237  
238      async def test_ad_sets_update(self) -> None:
239          mod = _import_meta_ads_tools()
240          handlers = _import_handlers()
241          creds, client = _mock_meta_ads_context()
242          client.update_ad_set.return_value = {"success": True}
243  
244          with (
245              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
246              patch.object(handlers, "create_meta_ads_client", return_value=client),
247          ):
248              result = await mod.handle_tool(
249                  "meta_ads.ad_sets.update",
250                  {"account_id": "act_123", "ad_set_id": "20", "name": "Updated"},
251              )
252  
253          client.update_ad_set.assert_awaited_once()
254  
255  
256  # ---------------------------------------------------------------------------
257  # ハンドラーテスト — 広告
258  # ---------------------------------------------------------------------------
259  
260  
261  @pytest.mark.unit
262  class TestMetaAdsAdHandlers:
263      """広告系ハンドラーテスト"""
264  
265      async def test_ads_list(self) -> None:
266          mod = _import_meta_ads_tools()
267          handlers = _import_handlers()
268          creds, client = _mock_meta_ads_context()
269          client.list_ads.return_value = [{"id": "30", "name": "Ad1"}]
270  
271          with (
272              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
273              patch.object(handlers, "create_meta_ads_client", return_value=client),
274          ):
275              result = await mod.handle_tool(
276                  "meta_ads.ads.list", {"account_id": "act_123"}
277              )
278  
279          client.list_ads.assert_awaited_once()
280  
281      async def test_ads_create(self) -> None:
282          mod = _import_meta_ads_tools()
283          handlers = _import_handlers()
284          creds, client = _mock_meta_ads_context()
285          client.create_ad.return_value = {"id": "40"}
286  
287          with (
288              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
289              patch.object(handlers, "create_meta_ads_client", return_value=client),
290          ):
291              result = await mod.handle_tool(
292                  "meta_ads.ads.create",
293                  {
294                      "account_id": "act_123",
295                      "ad_set_id": "20",
296                      "name": "New Ad",
297                      "creative_id": "cr_999",
298                  },
299              )
300  
301          client.create_ad.assert_awaited_once()
302  
303      async def test_ads_update(self) -> None:
304          mod = _import_meta_ads_tools()
305          handlers = _import_handlers()
306          creds, client = _mock_meta_ads_context()
307          client.update_ad.return_value = {"success": True}
308  
309          with (
310              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
311              patch.object(handlers, "create_meta_ads_client", return_value=client),
312          ):
313              result = await mod.handle_tool(
314                  "meta_ads.ads.update",
315                  {"account_id": "act_123", "ad_id": "40", "name": "Updated Ad"},
316              )
317  
318          client.update_ad.assert_awaited_once()
319  
320  
321  # ---------------------------------------------------------------------------
322  # ハンドラーテスト — インサイト
323  # ---------------------------------------------------------------------------
324  
325  
326  @pytest.mark.unit
327  class TestMetaAdsInsightsHandlers:
328      """インサイト系ハンドラーテスト"""
329  
330      async def test_insights_report(self) -> None:
331          mod = _import_meta_ads_tools()
332          handlers = _import_handlers()
333          creds, client = _mock_meta_ads_context()
334          client.get_performance_report.return_value = [{"impressions": 1000}]
335  
336          with (
337              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
338              patch.object(handlers, "create_meta_ads_client", return_value=client),
339          ):
340              result = await mod.handle_tool(
341                  "meta_ads.insights.report", {"account_id": "act_123"}
342              )
343  
344          client.get_performance_report.assert_awaited_once()
345  
346      async def test_insights_breakdown(self) -> None:
347          mod = _import_meta_ads_tools()
348          handlers = _import_handlers()
349          creds, client = _mock_meta_ads_context()
350          client.get_breakdown_report.return_value = [{"age": "18-24"}]
351  
352          with (
353              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
354              patch.object(handlers, "create_meta_ads_client", return_value=client),
355          ):
356              result = await mod.handle_tool(
357                  "meta_ads.insights.breakdown",
358                  {"account_id": "act_123", "campaign_id": "456"},
359              )
360  
361          client.get_breakdown_report.assert_awaited_once()
362  
363  
364  # ---------------------------------------------------------------------------
365  # ハンドラーテスト — オーディエンス
366  # ---------------------------------------------------------------------------
367  
368  
369  @pytest.mark.unit
370  class TestMetaAdsAudienceHandlers:
371      """オーディエンス系ハンドラーテスト"""
372  
373      async def test_audiences_list(self) -> None:
374          mod = _import_meta_ads_tools()
375          handlers = _import_handlers()
376          creds, client = _mock_meta_ads_context()
377          client.list_custom_audiences.return_value = [{"id": "50", "name": "Aud1"}]
378  
379          with (
380              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
381              patch.object(handlers, "create_meta_ads_client", return_value=client),
382          ):
383              result = await mod.handle_tool(
384                  "meta_ads.audiences.list", {"account_id": "act_123"}
385              )
386  
387          client.list_custom_audiences.assert_awaited_once()
388  
389      async def test_audiences_create(self) -> None:
390          mod = _import_meta_ads_tools()
391          handlers = _import_handlers()
392          creds, client = _mock_meta_ads_context()
393          client.create_custom_audience.return_value = {"id": "60"}
394  
395          with (
396              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
397              patch.object(handlers, "create_meta_ads_client", return_value=client),
398          ):
399              result = await mod.handle_tool(
400                  "meta_ads.audiences.create",
401                  {
402                      "account_id": "act_123",
403                      "name": "New Audience",
404                      "subtype": "WEBSITE",
405                  },
406              )
407  
408          client.create_custom_audience.assert_awaited_once()
409  
410  
411  # ---------------------------------------------------------------------------
412  # エラーハンドリングテスト
413  # ---------------------------------------------------------------------------
414  
415  
416  @pytest.mark.unit
417  class TestMetaAdsErrorHandling:
418      """エラーハンドリングの検証"""
419  
420      async def test_missing_required_param(self) -> None:
421          """account_id未指定 + credentials.jsonにもない場合にエラーテキスト返却"""
422          mod = _import_meta_ads_tools()
423          with patch(
424              "mureo.mcp._handlers_meta_ads.load_meta_ads_credentials",
425              return_value=None,
426          ):
427              result = await mod.handle_tool("meta_ads.campaigns.list", {})
428              assert any("Credentials not found" in r.text for r in result)
429  
430      async def test_unknown_tool_raises_error(self) -> None:
431          """未知のツール名でValueErrorが発生"""
432          mod = _import_meta_ads_tools()
433          with pytest.raises(ValueError, match="Unknown"):
434              await mod.handle_tool("meta_ads.unknown.tool", {"account_id": "act_123"})
435  
436      async def test_no_credentials_returns_error_text(self) -> None:
437          """認証情報なしでエラーテキストを返す"""
438          handlers = _import_handlers()
439          mod = _import_meta_ads_tools()
440          with patch.object(handlers, "load_meta_ads_credentials", return_value=None):
441              result = await mod.handle_tool(
442                  "meta_ads.campaigns.list", {"account_id": "act_123"}
443              )
444          assert len(result) == 1
445          assert "Credentials not found" in result[0].text
446  
447      async def test_handler_api_error(self) -> None:
448          """API例外がTextContentエラーメッセージに変換されること"""
449          mod = _import_meta_ads_tools()
450          handlers = _import_handlers()
451          creds, client = _mock_meta_ads_context()
452          client.list_campaigns.side_effect = RuntimeError("Meta API接続エラー")
453  
454          with (
455              patch.object(handlers, "load_meta_ads_credentials", return_value=creds),
456              patch.object(handlers, "create_meta_ads_client", return_value=client),
457          ):
458              result = await mod.handle_tool(
459                  "meta_ads.campaigns.list", {"account_id": "act_123"}
460              )
461  
462          assert len(result) == 1
463          assert "API error" in result[0].text
464          assert "Meta API接続エラー" in result[0].text