aissia/tests/mcp/MCPClientTests.cpp

393 lines
12 KiB
C++

/**
* @file MCPClientTests.cpp
* @brief Integration tests for MCPClient (15 TI)
*/
#include <catch2/catch_test_macros.hpp>
#include "shared/mcp/MCPClient.hpp"
#include "mocks/MockTransport.hpp"
#include <fstream>
#include <filesystem>
using namespace aissia::mcp;
using namespace aissia::tests;
using json = nlohmann::json;
// ============================================================================
// Helper: Create test config file
// ============================================================================
std::string createTestConfigFile(const json& config) {
std::string path = "test_mcp_config.json";
std::ofstream file(path);
file << config.dump(2);
file.close();
return path;
}
void cleanupTestConfigFile(const std::string& path) {
std::filesystem::remove(path);
}
// ============================================================================
// TI_CLIENT_001: Load config valid
// ============================================================================
TEST_CASE("TI_CLIENT_001_LoadConfigValid", "[mcp][client]") {
json config = {
{"servers", {
{"test_server", {
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
{"args", json::array({"server.py"})},
{"enabled", true}
}}
}}
};
auto path = createTestConfigFile(config);
MCPClient client;
bool loaded = client.loadConfig(path);
REQUIRE(loaded == true);
cleanupTestConfigFile(path);
}
// ============================================================================
// TI_CLIENT_002: Load config invalid
// ============================================================================
TEST_CASE("TI_CLIENT_002_LoadConfigInvalid", "[mcp][client]") {
// Create file with invalid JSON
std::string path = "invalid_config.json";
std::ofstream file(path);
file << "{ invalid json }";
file.close();
MCPClient client;
bool loaded = client.loadConfig(path);
REQUIRE(loaded == false);
cleanupTestConfigFile(path);
}
// ============================================================================
// TI_CLIENT_003: Load config missing file
// ============================================================================
TEST_CASE("TI_CLIENT_003_LoadConfigMissingFile", "[mcp][client]") {
MCPClient client;
bool loaded = client.loadConfig("nonexistent_file.json");
REQUIRE(loaded == false);
}
// ============================================================================
// TI_CLIENT_004: ConnectAll starts servers
// ============================================================================
TEST_CASE("TI_CLIENT_004_ConnectAllStartsServers", "[mcp][client]") {
// Use the real mock MCP server fixture
MCPClient client;
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
if (loaded) {
int connected = client.connectAll();
// Should connect to enabled servers
REQUIRE(connected >= 0);
client.disconnectAll();
} else {
// Skip if fixture not available
SUCCEED();
}
}
// ============================================================================
// TI_CLIENT_005: ConnectAll skips disabled
// ============================================================================
TEST_CASE("TI_CLIENT_005_ConnectAllSkipsDisabled", "[mcp][client]") {
json config = {
{"servers", {
{"enabled_server", {
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
{"args", json::array({"tests/fixtures/echo_server.py"})},
{"enabled", true}
}},
{"disabled_server", {
{"command", "nonexistent"},
{"enabled", false}
}}
}}
};
auto path = createTestConfigFile(config);
MCPClient client;
client.loadConfig(path);
int connected = client.connectAll();
// disabled_server should not be connected
REQUIRE(client.isConnected("disabled_server") == false);
client.disconnectAll();
cleanupTestConfigFile(path);
}
// ============================================================================
// TI_CLIENT_006: Connect single server
// ============================================================================
TEST_CASE("TI_CLIENT_006_ConnectSingleServer", "[mcp][client]") {
json config = {
{"servers", {
{"server1", {
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
{"args", json::array({"tests/fixtures/echo_server.py"})},
{"enabled", true}
}},
{"server2", {
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
{"args", json::array({"tests/fixtures/echo_server.py"})},
{"enabled", true}
}}
}}
};
auto path = createTestConfigFile(config);
MCPClient client;
client.loadConfig(path);
// Connect only server1
bool connected = client.connect("server1");
REQUIRE(connected == true);
REQUIRE(client.isConnected("server1") == true);
REQUIRE(client.isConnected("server2") == false);
client.disconnectAll();
cleanupTestConfigFile(path);
}
// ============================================================================
// TI_CLIENT_007: Disconnect single server
// ============================================================================
TEST_CASE("TI_CLIENT_007_DisconnectSingleServer", "[mcp][client]") {
json config = {
{"servers", {
{"server1", {
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
{"args", json::array({"tests/fixtures/echo_server.py"})},
{"enabled", true}
}}
}}
};
auto path = createTestConfigFile(config);
MCPClient client;
client.loadConfig(path);
client.connect("server1");
REQUIRE(client.isConnected("server1") == true);
client.disconnect("server1");
REQUIRE(client.isConnected("server1") == false);
cleanupTestConfigFile(path);
}
// ============================================================================
// TI_CLIENT_008: DisconnectAll cleans up
// ============================================================================
TEST_CASE("TI_CLIENT_008_DisconnectAllCleansUp", "[mcp][client]") {
json config = {
{"servers", {
{"server1", {
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
{"args", json::array({"tests/fixtures/echo_server.py"})},
{"enabled", true}
}},
{"server2", {
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
{"args", json::array({"tests/fixtures/echo_server.py"})},
{"enabled", true}
}}
}}
};
auto path = createTestConfigFile(config);
MCPClient client;
client.loadConfig(path);
client.connectAll();
client.disconnectAll();
REQUIRE(client.isConnected("server1") == false);
REQUIRE(client.isConnected("server2") == false);
REQUIRE(client.getConnectedServers().empty() == true);
cleanupTestConfigFile(path);
}
// ============================================================================
// TI_CLIENT_009: ListAllTools aggregates
// ============================================================================
TEST_CASE("TI_CLIENT_009_ListAllToolsAggregates", "[mcp][client]") {
MCPClient client;
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
if (loaded) {
client.connectAll();
auto tools = client.listAllTools();
// Should have tools from mock server
REQUIRE(tools.size() >= 0);
client.disconnectAll();
} else {
SUCCEED();
}
}
// ============================================================================
// TI_CLIENT_010: Tool name prefixed
// ============================================================================
TEST_CASE("TI_CLIENT_010_ToolNamePrefixed", "[mcp][client]") {
MCPClient client;
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
if (loaded) {
client.connectAll();
auto tools = client.listAllTools();
bool hasPrefix = false;
for (const auto& tool : tools) {
if (tool.name.find(":") != std::string::npos) {
hasPrefix = true;
break;
}
}
if (!tools.empty()) {
REQUIRE(hasPrefix == true);
}
client.disconnectAll();
} else {
SUCCEED();
}
}
// ============================================================================
// TI_CLIENT_011: CallTool routes to server
// ============================================================================
TEST_CASE("TI_CLIENT_011_CallToolRoutesToServer", "[mcp][client]") {
MCPClient client;
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
if (loaded) {
client.connectAll();
auto tools = client.listAllTools();
if (!tools.empty()) {
// Call the first available tool
auto result = client.callTool(tools[0].name, json::object());
// Should get some result (success or error)
REQUIRE(result.content.size() >= 0);
}
client.disconnectAll();
} else {
SUCCEED();
}
}
// ============================================================================
// TI_CLIENT_012: CallTool invalid name
// ============================================================================
TEST_CASE("TI_CLIENT_012_CallToolInvalidName", "[mcp][client]") {
MCPClient client;
client.loadConfig("tests/fixtures/mock_mcp.json");
client.connectAll();
auto result = client.callTool("nonexistent:tool", json::object());
REQUIRE(result.isError == true);
client.disconnectAll();
}
// ============================================================================
// TI_CLIENT_013: CallTool disconnected server
// ============================================================================
TEST_CASE("TI_CLIENT_013_CallToolDisconnectedServer", "[mcp][client]") {
MCPClient client;
// Don't connect any servers
auto result = client.callTool("server:tool", json::object());
REQUIRE(result.isError == true);
}
// ============================================================================
// TI_CLIENT_014: ToolCount accurate
// ============================================================================
TEST_CASE("TI_CLIENT_014_ToolCountAccurate", "[mcp][client]") {
MCPClient client;
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
if (loaded) {
client.connectAll();
size_t count = client.toolCount();
auto tools = client.listAllTools();
REQUIRE(count == tools.size());
client.disconnectAll();
} else {
SUCCEED();
}
}
// ============================================================================
// TI_CLIENT_015: IsConnected accurate
// ============================================================================
TEST_CASE("TI_CLIENT_015_IsConnectedAccurate", "[mcp][client]") {
json config = {
{"servers", {
{"test_server", {
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
{"args", json::array({"tests/fixtures/echo_server.py"})},
{"enabled", true}
}}
}}
};
auto path = createTestConfigFile(config);
MCPClient client;
client.loadConfig(path);
// Not connected yet
REQUIRE(client.isConnected("test_server") == false);
// Connect
client.connect("test_server");
REQUIRE(client.isConnected("test_server") == true);
// Disconnect
client.disconnect("test_server");
REQUIRE(client.isConnected("test_server") == false);
cleanupTestConfigFile(path);
}