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