require "test_helper" class McpControllerTest < ActionDispatch::IntegrationTest setup do @token = ApiToken.issue!(user: users(:admin), name: "mcp-test") @marco = ApiToken.issue!(user: users(:marco), name: "mcp-marco") end test "rejects missing token" do post "/mcp", params: rpc("initialize"), as: :json, headers: mcp_headers(nil) assert_response :unauthorized end test "initialize advertises CRM tools" do post "/mcp", params: rpc("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "test", version: "1.0" } }), as: :json, headers: mcp_headers(@token.plaintext) assert_response :success names = Array(json_rpc.dig("result", "capabilities", "tools")).presence listed = tool_names_from_initialize.presence || fetch_tool_names assert_includes listed, "list_projects" assert_includes listed, "today" assert_includes listed, "create_activity" assert_includes listed, "check_mail_bounces" assert names || listed.any? end test "list_projects returns accessible projects" do result = call_tool("list_projects", {}, token: @marco.plaintext) codes = result.fetch("projects").map { |p| p["code"] } assert_includes codes, "matchlivetv" assert_not_includes codes, "riskmeter" end test "today requires project access" do result = call_tool("today", { project_code: "riskmeter" }, token: @marco.plaintext, expect_error: true) assert_equal "Progetto non accessibile", result["error"] end test "today returns tasks for matchlivetv" do result = call_tool("today", { project_code: "matchlivetv" }) ids = result.fetch("today_tasks").map { |t| t["id"] } assert_includes ids, tasks(:follow_up).id end test "allowed_hosts includes production APP_HOST and RAILS_ALLOWED_HOSTS" do previous_app = ENV["APP_HOST"] previous_allowed = ENV["RAILS_ALLOWED_HOSTS"] ENV["APP_HOST"] = "crm.eminux.it" ENV["RAILS_ALLOWED_HOSTS"] = "crm.eminux.it, 192.168.1.158, localhost" hosts = EminuxCrmMcp.allowed_hosts assert_includes hosts, "crm.eminux.it" assert_includes hosts, "192.168.1.158" assert_includes hosts, "localhost" ensure ENV["APP_HOST"] = previous_app ENV["RAILS_ALLOWED_HOSTS"] = previous_allowed end private def mcp_headers(plaintext) headers = { "Accept" => "application/json, text/event-stream", "MCP-Protocol-Version" => "2025-06-18" } headers["Authorization"] = "Bearer #{plaintext}" if plaintext.present? headers end def rpc(method, params = {}, id: 1) { jsonrpc: "2.0", id: id, method: method, params: params } end def json_rpc JSON.parse(response.body) end def initialize_mcp!(token) post "/mcp", params: rpc("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "test", version: "1.0" } }), as: :json, headers: mcp_headers(token) assert_response :success, response.body end def fetch_tool_names initialize_mcp!(@token.plaintext) post "/mcp", params: rpc("tools/list", {}, id: 2), as: :json, headers: mcp_headers(@token.plaintext) assert_response :success, response.body Array(json_rpc.dig("result", "tools")).map { |t| t["name"] } end def tool_names_from_initialize Array(json_rpc.dig("result", "tools")).map { |t| t["name"] } end def call_tool(name, arguments, token: @token.plaintext, expect_error: false) initialize_mcp!(token) post "/mcp", params: rpc("tools/call", { name: name, arguments: arguments }, id: 2), as: :json, headers: mcp_headers(token) assert_response :success, response.body payload = json_rpc text = payload.dig("result", "content", 0, "text") || payload.dig("result", "content", 0, :text) refute_nil text, payload.inspect parsed = JSON.parse(text) if expect_error assert payload.dig("result", "isError") || parsed["error"].present?, payload.inspect else assert_nil parsed["error"], payload.inspect end parsed end end