"""Fusion ACC-2.1 — connecteur MCP Flowdeck → TikTak (première paire). Le preset « tiktak » crée un connecteur MCP préconfiguré (URL du endpoint /mcp de TikTak, auth bearer = clé `tck_` chiffrée). Le probe parle le dialecte JSON-RPC réel de TikTak (initialize → protocolVersion 2025-03-26, tools/list avec ses trois outils en lecture, tools/call → content[0].text en JSON) ; les outils sont cachés puis exposés à l'agent comme `mcp_tiktak_*`. """ import asyncio import json import pytest from app.db import get_conn from app.services import connectors, mcp_client from app.services.importers import url_fetch from app.services.tool_registry import ToolRegistry @pytest.fixture(autouse=True) def _dns_monde_reel(monkeypatch): """L'environnement de test local résout les hôtes publics vers 198.18.0.0/15 (plage « reserved ») : la garde SSRF les refuse, alors qu'en production/CI tiktak.dracodev.net résout vers Cloudflare. On neutralise la résolution pour la création — le comportement du produit n'est pas modifié, seuls les tests s'affranchissent du DNS local (les tests v755/v757 échouent localement pour la même cause). """ monkeypatch.setattr(url_fetch, "_validate_url", lambda url: url.strip()) # Outils tels que TikTak les déclare (app/routers/sync.py, _OUTILS). OUTILS_TIKTAK = [ {"name": "list_today_tasks", "description": "Liste les tâches actives de l'utilisateur échéant aujourd'hui.", "inputSchema": {"type": "object", "properties": {}}}, {"name": "search_tasks", "description": "Recherche des tâches et des notes par mots.", "inputSchema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}, {"name": "get_task", "description": "Lit une tâche par son identifiant.", "inputSchema": {"type": "object", "properties": {"id": {"type": "integer"}}, "required": ["id"]}}, ] def _creer_tiktak(client, **kw): body = {"name": "TikTak", "kind": "tiktak", "url": "https://tiktak.dracodev.net/mcp", "secret": "tck_cle_de_test", "auth": "bearer", **kw} r = client.post("/api/agent/connectors", json=body) assert r.status_code == 200, r.text return r.json() def _faux_rpc_tiktak(appels, entetes): """Rejoue le serveur MCP de TikTak (POST /mcp, JSON-RPC).""" async def fake_rpc(url, payload, headers=None): assert url == "https://tiktak.dracodev.net/mcp" entetes.append(headers or {}) appels.append(payload.get("method")) method = payload.get("method") if method == "initialize": return {"result": {"protocolVersion": "2025-03-26", "capabilities": {"tools": {}}, "serverInfo": {"name": "tiktak", "version": "0.51.0"}}} if method == "tools/list": return {"result": {"tools": OUTILS_TIKTAK}} if method == "tools/call": params = payload.get("params") or {} if params.get("name") == "list_today_tasks": res = [{"id": 42, "title": "Appeler le dentiste", "list": "Perso"}] else: res = {"id": 42, "title": "Appeler le dentiste"} return {"result": {"content": [{"type": "text", "text": json.dumps(res, ensure_ascii=False)}]}} return {} return fake_rpc def test_preset_tiktak_et_secret_chiffre(client): row = _creer_tiktak(client) assert row["kind"] == "tiktak" and row["auth"] == "bearer" assert row["has_secret"] is True assert "tck_cle_de_test" not in json.dumps(row) # jamais renvoyée assert connectors.PRESETS["tiktak"]["url"] == "https://tiktak.dracodev.net/mcp" hdrs = connectors._headers(row["id"]) assert hdrs["Authorization"] == "Bearer tck_cle_de_test" # ce qu'attend TikTak def test_probe_tiktak_handshake_et_cache(client, monkeypatch): row = _creer_tiktak(client) appels, entetes = [], [] monkeypatch.setattr(mcp_client, "_rpc", _faux_rpc_tiktak(appels, entetes)) res = asyncio.run(connectors.probe(str(row["id"]))) assert res["status"] == "ok", res assert "3 outil(s)" in res["detail"] assert appels == ["initialize", "notifications/initialized", "tools/list"] assert entetes[0]["Authorization"] == "Bearer tck_cle_de_test" with get_conn() as conn: tools = conn.execute("SELECT tools_json FROM agent_connectors WHERE id=?", (row["id"],)).fetchone()["tools_json"] for nom in ("mcp_tiktak_list_today_tasks", "mcp_tiktak_search_tasks", "mcp_tiktak_get_task"): assert nom in tools def test_outils_tiktak_exposes_a_l_agent(client, monkeypatch): row = _creer_tiktak(client) monkeypatch.setattr(mcp_client, "_rpc", _faux_rpc_tiktak([], [])) asyncio.run(connectors.probe(str(row["id"]))) reg = ToolRegistry() schema = {t["name"]: t for t in reg.schema()} assert "mcp_tiktak_list_today_tasks" in schema assert schema["mcp_tiktak_search_tasks"]["parameters"]["required"] == ["query"] assert schema["mcp_tiktak_get_task"]["parameters"]["required"] == ["id"] res = asyncio.run(reg.execute("mcp_tiktak_list_today_tasks", {})) assert res.status == "success" assert "Appeler le dentiste" in res.data["text"] def test_tiktak_desactive_masque_ses_outils(client, monkeypatch): row = _creer_tiktak(client) monkeypatch.setattr(mcp_client, "_rpc", _faux_rpc_tiktak([], [])) asyncio.run(connectors.probe(str(row["id"]))) client.patch(f"/api/agent/connectors/{row['id']}", json={"enabled": False}) noms = {t["name"] for t in ToolRegistry().schema()} assert "mcp_tiktak_list_today_tasks" not in noms