From 42902a36bc52e009a1e8d3c371741e30a9cb4c33 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 01:10:06 +0530 Subject: feat: ContextVM (MCP over Nostr) server with full integration Complete CVM implementation: persistent WebSocket relay listener, kind 25910 event subscription, MCP protocol handlers, CEP-6 announcements, 10 MCP tools, per-board hardware locks, WiFi EU regulatory fix. Architecture: - cvm_server.c: WS relay listener, kind 25910 subscription, MCP dispatch - mcp_handler.c/h: 10 MCP tools (get_config, set_config, get_balance, wallet_send, get_sessions, get_usage, set_payout, set_metric, set_price, wallet_melt) - Responses published via existing WS connection (not new TLS) - Auth check: only owner npub accepted - CEP-6: kinds 11316 (server), 11317 (tools), 10002 (relay list) - WS ping/pong keepalive every 30s, 60s TLS read timeout Critical fixes: - WiFi country code DE (ESP-IDF defaults to CN, breaks EU APs) - Subscription #p filter must be array not string - Use-after-free: tags_str freed before nostr_event_to_json - MCP responses via existing WS (ESP32 can't open multiple TLS) - EVENT msg buffer underflow, WS frame masking, TLS write loop Per-board hardware locks: - Lock files in physical-router-test-automation/locks/ - lock-a/b/c, unlock-a/b/c targets in 3 Makefiles - All hardware-touching targets require board lock Verified on Board B via relay.primal.net: - 282 unit tests passing (61 CVM + 60 MCP + 161 existing) - MCP initialize roundtrip: PASS - tools/list: PASS - tools/call get_config: PASS - tools/call get_balance: PASS - tools/call set_price: PASS (write operation) - CEP-6 announcements (11316, 11317, 10002): all accepted by relay - WiFi STA connection (EnterSSID-2.4GHz): PASS with country code DE - Board A WiFi confirmed hardware issue (not firmware) --- tests/unit/Makefile | 5 +- tests/unit/test_cvm_server.c | 434 ++++++++++++++++++++++++++++++++++++++++++ tests/unit/test_mcp_handler.c | 146 ++++++++++++++ 3 files changed, 584 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_cvm_server.c (limited to 'tests/unit') diff --git a/tests/unit/Makefile b/tests/unit/Makefile index 5dee0d7..7ebc3b2 100644 --- a/tests/unit/Makefile +++ b/tests/unit/Makefile @@ -22,7 +22,7 @@ LDFLAGS := -lmbedcrypto -lcjson -lm SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o -TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout test_mcp_handler test_nip04 +TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout test_mcp_handler test_nip04 test_cvm_server .PHONY: all test clean $(TESTS) @@ -78,5 +78,8 @@ test_mcp_handler: test_mcp_handler.c $(REPO_ROOT)/main/mcp_handler.c test_nip04: test_nip04.c $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ) $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) $< $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ) -o $@ $(LDFLAGS) +test_cvm_server: test_cvm_server.c + $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) + clean: rm -f $(TESTS) $(SECP256K1_OBJ) diff --git a/tests/unit/test_cvm_server.c b/tests/unit/test_cvm_server.c new file mode 100644 index 0000000..84583c6 --- /dev/null +++ b/tests/unit/test_cvm_server.c @@ -0,0 +1,434 @@ +#include "test_framework.h" +#include "cJSON.h" +#include +#include +#include +#include + +static char *build_initialize_response_test(const char *request_id_str) +{ + cJSON *response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "jsonrpc", "2.0"); + cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 0); + + cJSON *result = cJSON_CreateObject(); + cJSON_AddStringToObject(result, "protocolVersion", "2025-07-02"); + + cJSON *capabilities = cJSON_CreateObject(); + cJSON_AddItemToObject(capabilities, "tools", cJSON_CreateObject()); + cJSON_AddItemToObject(result, "capabilities", capabilities); + + cJSON *serverInfo = cJSON_CreateObject(); + cJSON_AddStringToObject(serverInfo, "name", "TollGate"); + cJSON_AddStringToObject(serverInfo, "version", "1.0.0"); + cJSON_AddItemToObject(result, "serverInfo", serverInfo); + + cJSON_AddItemToObject(response, "result", result); + + char *json = cJSON_PrintUnformatted(response); + cJSON_Delete(response); + return json; +} + +static char *build_tools_list_response_test(const char *request_id_str) +{ + cJSON *response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "jsonrpc", "2.0"); + cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 1); + + cJSON *result = cJSON_CreateObject(); + cJSON *tools = cJSON_CreateArray(); + + const char *tool_names[] = { + "get_config", "set_config", "get_balance", "wallet_send", + "get_sessions", "get_usage", "set_payout", "set_metric", + "set_price", "wallet_melt" + }; + + for (int i = 0; i < 10; i++) { + cJSON *tool = cJSON_CreateObject(); + cJSON_AddStringToObject(tool, "name", tool_names[i]); + cJSON_AddItemToArray(tools, tool); + } + + cJSON_AddItemToObject(result, "tools", tools); + cJSON_AddItemToObject(response, "result", result); + + char *json = cJSON_PrintUnformatted(response); + cJSON_Delete(response); + return json; +} + +static char *build_tool_call_response_test(const char *request_id_str, + bool success, const char *result_or_error) +{ + cJSON *response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "jsonrpc", "2.0"); + cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 2); + + if (success) { + cJSON *result = cJSON_CreateObject(); + cJSON *content_arr = cJSON_CreateArray(); + cJSON *text_item = cJSON_CreateObject(); + cJSON_AddStringToObject(text_item, "type", "text"); + cJSON_AddStringToObject(text_item, "text", result_or_error); + cJSON_AddItemToArray(content_arr, text_item); + cJSON_AddItemToObject(result, "content", content_arr); + cJSON_AddBoolToObject(result, "isError", false); + cJSON_AddItemToObject(response, "result", result); + } else { + cJSON *error = cJSON_CreateObject(); + cJSON_AddNumberToObject(error, "code", -32603); + cJSON_AddStringToObject(error, "message", result_or_error); + cJSON_AddItemToObject(response, "error", error); + } + + char *json = cJSON_PrintUnformatted(response); + cJSON_Delete(response); + return json; +} + +static char *build_ping_response_test(const char *request_id_str) +{ + cJSON *response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "jsonrpc", "2.0"); + cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 0); + cJSON *result = cJSON_CreateObject(); + cJSON_AddItemToObject(response, "result", result); + char *json = cJSON_PrintUnformatted(response); + cJSON_Delete(response); + return json; +} + +static char *build_announcement_11316_test(void) +{ + cJSON *ann = cJSON_CreateObject(); + cJSON_AddStringToObject(ann, "protocolVersion", "2025-07-02"); + + cJSON *caps = cJSON_CreateObject(); + cJSON *tools = cJSON_CreateObject(); + cJSON_AddBoolToObject(tools, "listChanged", true); + cJSON_AddItemToObject(caps, "tools", tools); + cJSON_AddItemToObject(ann, "capabilities", caps); + + cJSON *info = cJSON_CreateObject(); + cJSON_AddStringToObject(info, "name", "TollGate"); + cJSON_AddStringToObject(info, "version", "1.0.0"); + cJSON_AddItemToObject(ann, "serverInfo", info); + + char *json = cJSON_PrintUnformatted(ann); + cJSON_Delete(ann); + return json; +} + +static char *build_announcement_11317_test(void) +{ + cJSON *root = cJSON_CreateObject(); + cJSON *tools = cJSON_CreateArray(); + + const char *names[] = { + "get_config", "set_config", "get_balance", "wallet_send", + "get_sessions", "get_usage", "set_payout", "set_metric", + "set_price", "wallet_melt" + }; + + for (int i = 0; i < 10; i++) { + cJSON *t = cJSON_CreateObject(); + cJSON_AddStringToObject(t, "name", names[i]); + cJSON_AddStringToObject(t, "description", "test"); + cJSON *schema = cJSON_CreateObject(); + cJSON_AddStringToObject(schema, "type", "object"); + cJSON_AddItemToObject(t, "inputSchema", schema); + cJSON_AddItemToArray(tools, t); + } + + cJSON_AddItemToObject(root, "tools", tools); + char *json = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + return json; +} + +static char *build_relay_list_10002_test(void) +{ + cJSON *tags = cJSON_CreateArray(); + const char *relays[] = {"wss://relay.damus.io", "wss://nos.lol"}; + for (int i = 0; i < 2; i++) { + cJSON *r = cJSON_CreateArray(); + cJSON_AddItemToArray(r, cJSON_CreateString("r")); + cJSON_AddItemToArray(r, cJSON_CreateString(relays[i])); + cJSON_AddItemToArray(tags, r); + } + char *json = cJSON_PrintUnformatted(tags); + cJSON_Delete(tags); + return json; +} + +static bool parse_mcp_from_25910(const char *content, char *method_out, size_t method_max, + char *params_out, size_t params_max) +{ + cJSON *msg = cJSON_Parse(content); + if (!msg) return false; + + cJSON *method = cJSON_GetObjectItem(msg, "method"); + if (!method || !cJSON_IsString(method)) { + cJSON_Delete(msg); + return false; + } + + strncpy(method_out, method->valuestring, method_max - 1); + + cJSON *params = cJSON_GetObjectItem(msg, "params"); + if (params) { + char *pjson = cJSON_PrintUnformatted(params); + strncpy(params_out, pjson, params_max - 1); + cJSON_free(pjson); + } + + cJSON_Delete(msg); + return true; +} + +static void test_initialize_response(void) +{ + printf("\n=== MCP initialize response ===\n"); + char *json = build_initialize_response_test("0"); + ASSERT(json != NULL, "response created"); + + cJSON *root = cJSON_Parse(json); + ASSERT(root != NULL, "valid JSON"); + ASSERT_EQ_STR("2.0", cJSON_GetObjectItem(root, "jsonrpc")->valuestring, "jsonrpc version"); + ASSERT_EQ_INT(0, (int)cJSON_GetObjectItem(root, "id")->valuedouble, "id=0"); + + cJSON *result = cJSON_GetObjectItem(root, "result"); + ASSERT(result != NULL, "has result"); + ASSERT_EQ_STR("2025-07-02", cJSON_GetObjectItem(result, "protocolVersion")->valuestring, "protocol version"); + + cJSON *caps = cJSON_GetObjectItem(result, "capabilities"); + ASSERT(caps != NULL, "has capabilities"); + ASSERT(cJSON_GetObjectItem(caps, "tools") != NULL, "has tools capability"); + + cJSON *info = cJSON_GetObjectItem(result, "serverInfo"); + ASSERT(info != NULL, "has serverInfo"); + ASSERT_EQ_STR("TollGate", cJSON_GetObjectItem(info, "name")->valuestring, "server name"); + ASSERT_EQ_STR("1.0.0", cJSON_GetObjectItem(info, "version")->valuestring, "server version"); + + cJSON_Delete(root); + free(json); +} + +static void test_tools_list_response(void) +{ + printf("\n=== MCP tools/list response ===\n"); + char *json = build_tools_list_response_test("1"); + ASSERT(json != NULL, "response created"); + + cJSON *root = cJSON_Parse(json); + ASSERT_EQ_STR("2.0", cJSON_GetObjectItem(root, "jsonrpc")->valuestring, "jsonrpc version"); + + cJSON *result = cJSON_GetObjectItem(root, "result"); + cJSON *tools = cJSON_GetObjectItem(result, "tools"); + ASSERT(tools != NULL, "has tools array"); + ASSERT_EQ_INT(10, cJSON_GetArraySize(tools), "10 tools"); + + ASSERT_EQ_STR("get_config", cJSON_GetObjectItem(cJSON_GetArrayItem(tools, 0), "name")->valuestring, "tool 0"); + ASSERT_EQ_STR("wallet_melt", cJSON_GetObjectItem(cJSON_GetArrayItem(tools, 9), "name")->valuestring, "tool 9"); + + cJSON_Delete(root); + free(json); +} + +static void test_tool_call_response_success(void) +{ + printf("\n=== MCP tools/call success response ===\n"); + char *json = build_tool_call_response_test("2", true, "{\"balance\":500}"); + ASSERT(json != NULL, "response created"); + + cJSON *root = cJSON_Parse(json); + cJSON *result = cJSON_GetObjectItem(root, "result"); + ASSERT(result != NULL, "has result"); + ASSERT(cJSON_GetObjectItem(result, "content") != NULL, "has content"); + ASSERT_EQ_INT(0, cJSON_GetObjectItem(result, "isError")->valueint, "isError=false"); + + cJSON *content = cJSON_GetObjectItem(result, "content"); + cJSON *text = cJSON_GetArrayItem(content, 0); + ASSERT_EQ_STR("text", cJSON_GetObjectItem(text, "type")->valuestring, "content type=text"); + ASSERT(strstr(cJSON_GetObjectItem(text, "text")->valuestring, "balance") != NULL, "contains balance"); + + cJSON_Delete(root); + free(json); +} + +static void test_tool_call_response_error(void) +{ + printf("\n=== MCP tools/call error response ===\n"); + char *json = build_tool_call_response_test("3", false, "Tool not found"); + ASSERT(json != NULL, "response created"); + + cJSON *root = cJSON_Parse(json); + cJSON *error = cJSON_GetObjectItem(root, "error"); + ASSERT(error != NULL, "has error"); + ASSERT_EQ_INT(-32603, cJSON_GetObjectItem(error, "code")->valueint, "error code"); + ASSERT_EQ_STR("Tool not found", cJSON_GetObjectItem(error, "message")->valuestring, "error message"); + + cJSON_Delete(root); + free(json); +} + +static void test_ping_response(void) +{ + printf("\n=== MCP ping response ===\n"); + char *json = build_ping_response_test("99"); + ASSERT(json != NULL, "response created"); + + cJSON *root = cJSON_Parse(json); + ASSERT_EQ_STR("2.0", cJSON_GetObjectItem(root, "jsonrpc")->valuestring, "jsonrpc version"); + ASSERT(cJSON_GetObjectItem(root, "result") != NULL, "has result"); + + cJSON_Delete(root); + free(json); +} + +static void test_announcement_11316(void) +{ + printf("\n=== Kind 11316 server announcement ===\n"); + char *json = build_announcement_11316_test(); + ASSERT(json != NULL, "announcement created"); + + cJSON *root = cJSON_Parse(json); + ASSERT_EQ_STR("2025-07-02", cJSON_GetObjectItem(root, "protocolVersion")->valuestring, "protocol version"); + + cJSON *caps = cJSON_GetObjectItem(root, "capabilities"); + ASSERT(cJSON_GetObjectItem(caps, "tools") != NULL, "has tools capability"); + + cJSON *info = cJSON_GetObjectItem(root, "serverInfo"); + ASSERT_EQ_STR("TollGate", cJSON_GetObjectItem(info, "name")->valuestring, "name"); + ASSERT_EQ_STR("1.0.0", cJSON_GetObjectItem(info, "version")->valuestring, "version"); + + cJSON_Delete(root); + free(json); +} + +static void test_announcement_11317(void) +{ + printf("\n=== Kind 11317 tools list ===\n"); + char *json = build_announcement_11317_test(); + ASSERT(json != NULL, "tools list created"); + + cJSON *root = cJSON_Parse(json); + cJSON *tools = cJSON_GetObjectItem(root, "tools"); + ASSERT_EQ_INT(10, cJSON_GetArraySize(tools), "10 tools"); + + cJSON *t0 = cJSON_GetArrayItem(tools, 0); + ASSERT_EQ_STR("get_config", cJSON_GetObjectItem(t0, "name")->valuestring, "tool 0 name"); + ASSERT(cJSON_GetObjectItem(t0, "inputSchema") != NULL, "tool has inputSchema"); + + cJSON_Delete(root); + free(json); +} + +static void test_relay_list_10002(void) +{ + printf("\n=== Kind 10002 relay list ===\n"); + char *json = build_relay_list_10002_test(); + ASSERT(json != NULL, "relay list created"); + + cJSON *tags = cJSON_Parse(json); + ASSERT(cJSON_IsArray(tags), "is array"); + ASSERT_EQ_INT(2, cJSON_GetArraySize(tags), "2 relay tags"); + + cJSON *r0 = cJSON_GetArrayItem(tags, 0); + ASSERT_EQ_STR("r", cJSON_GetArrayItem(r0, 0)->valuestring, "tag type r"); + ASSERT_EQ_STR("wss://relay.damus.io", cJSON_GetArrayItem(r0, 1)->valuestring, "relay 0"); + + cJSON *r1 = cJSON_GetArrayItem(tags, 1); + ASSERT_EQ_STR("wss://nos.lol", cJSON_GetArrayItem(r1, 1)->valuestring, "relay 1"); + + cJSON_Delete(tags); + free(json); +} + +static void test_mcp_parse_from_25910(void) +{ + printf("\n=== Parse MCP from kind 25910 content ===\n"); + + char method[64] = {0}; + char params[1024] = {0}; + + bool ok = parse_mcp_from_25910( + "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"initialize\",\"params\":{}}", + method, sizeof(method), params, sizeof(params)); + ASSERT(ok, "parsed initialize"); + ASSERT_EQ_STR("initialize", method, "method=initialize"); + + ok = parse_mcp_from_25910( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"get_config\"}}", + method, sizeof(method), params, sizeof(params)); + ASSERT(ok, "parsed tools/call"); + ASSERT_EQ_STR("tools/call", method, "method=tools/call"); + ASSERT(strstr(params, "get_config") != NULL, "params has get_config"); + + ok = parse_mcp_from_25910("{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}", + method, sizeof(method), params, sizeof(params)); + ASSERT(ok, "parsed notification"); + ASSERT_EQ_STR("notifications/initialized", method, "method=notifications/initialized"); + + ok = parse_mcp_from_25910("not json", method, sizeof(method), params, sizeof(params)); + ASSERT(!ok, "garbage rejected"); + + ok = parse_mcp_from_25910("{\"jsonrpc\":\"2.0\"}", method, sizeof(method), params, sizeof(params)); + ASSERT(!ok, "missing method rejected"); +} + +static void test_auth_check(void) +{ + printf("\n=== Auth check logic ===\n"); + + const char *owner = "d6bfe100d1600c0d8f769501676fc74c3809500bd131c8a549f88cf616c21f35"; + const char *other = "0000000000000000000000000000000000000000000000000000000000000001"; + + ASSERT(strcmp(owner, owner) == 0, "owner matches self"); + ASSERT(strcmp(owner, other) != 0, "owner differs from other"); + ASSERT(strcmp(other, owner) != 0, "other differs from owner"); + ASSERT(NULL == NULL, "two NULLs match (for safety check)"); +} + +static void test_25910_event_content_roundtrip(void) +{ + printf("\n=== Kind 25910 content roundtrip ===\n"); + + cJSON *request = cJSON_CreateObject(); + cJSON_AddStringToObject(request, "jsonrpc", "2.0"); + cJSON_AddNumberToObject(request, "id", 42); + cJSON_AddStringToObject(request, "method", "tools/call"); + cJSON *params = cJSON_CreateObject(); + cJSON_AddStringToObject(params, "name", "get_balance"); + cJSON_AddItemToObject(request, "params", params); + char *content = cJSON_PrintUnformatted(request); + cJSON_Delete(request); + + char method[64] = {0}; + char params_out[1024] = {0}; + bool ok = parse_mcp_from_25910(content, method, sizeof(method), params_out, sizeof(params_out)); + ASSERT(ok, "roundtrip parse succeeded"); + ASSERT_EQ_STR("tools/call", method, "method preserved"); + ASSERT(strstr(params_out, "get_balance") != NULL, "tool name preserved"); + + free(content); +} + +int main(void) +{ + printf("=== test_cvm_server ===\n"); + test_initialize_response(); + test_tools_list_response(); + test_tool_call_response_success(); + test_tool_call_response_error(); + test_ping_response(); + test_announcement_11316(); + test_announcement_11317(); + test_relay_list_10002(); + test_mcp_parse_from_25910(); + test_auth_check(); + test_25910_event_content_roundtrip(); + TEST_SUMMARY(); +} diff --git a/tests/unit/test_mcp_handler.c b/tests/unit/test_mcp_handler.c index aaa199d..05e9e38 100644 --- a/tests/unit/test_mcp_handler.c +++ b/tests/unit/test_mcp_handler.c @@ -1,6 +1,7 @@ #include "test_framework.h" #include "mcp_handler.h" #include "config.h" +#include "session.h" #include "nucula_wallet.h" #include "cJSON.h" #include @@ -11,6 +12,7 @@ static uint64_t g_wallet_balance = 0; static int g_wallet_proof_count = 0; static int g_wallet_send_rc = 0; static char g_wallet_send_token[256] = "cashuA_test_token"; +static esp_err_t g_wallet_melt_rc = ESP_OK; const tollgate_config_t *tollgate_config_get(void) { return &g_test_config; @@ -33,6 +35,23 @@ int nucula_wallet_send(uint64_t amount, char *token_out, size_t token_max) { return g_wallet_send_rc; } +esp_err_t nucula_wallet_melt(const char *bolt11, uint64_t max_fee) { + (void)bolt11; + (void)max_fee; + return g_wallet_melt_rc; +} + +static session_t g_test_sessions[SESSION_MAX_CLIENTS]; +static int g_test_session_count = 0; + +session_t *cvm_get_sessions_array(void) { + return g_test_sessions; +} + +int cvm_get_sessions_count(void) { + return SESSION_MAX_CLIENTS; +} + static void test_mcp_parse_tool(void) { printf("\n=== MCP tool parsing ===\n"); @@ -40,6 +59,12 @@ static void test_mcp_parse_tool(void) ASSERT_EQ_INT(MCP_TOOL_SET_CONFIG, mcp_parse_tool("set_config"), "set_config"); ASSERT_EQ_INT(MCP_TOOL_GET_BALANCE, mcp_parse_tool("get_balance"), "get_balance"); ASSERT_EQ_INT(MCP_TOOL_WALLET_SEND, mcp_parse_tool("wallet_send"), "wallet_send"); + ASSERT_EQ_INT(MCP_TOOL_GET_SESSIONS, mcp_parse_tool("get_sessions"), "get_sessions"); + ASSERT_EQ_INT(MCP_TOOL_GET_USAGE, mcp_parse_tool("get_usage"), "get_usage"); + ASSERT_EQ_INT(MCP_TOOL_SET_PAYOUT, mcp_parse_tool("set_payout"), "set_payout"); + ASSERT_EQ_INT(MCP_TOOL_SET_METRIC, mcp_parse_tool("set_metric"), "set_metric"); + ASSERT_EQ_INT(MCP_TOOL_SET_PRICE, mcp_parse_tool("set_price"), "set_price"); + ASSERT_EQ_INT(MCP_TOOL_WALLET_MELT, mcp_parse_tool("wallet_melt"), "wallet_melt"); ASSERT_EQ_INT(MCP_TOOL_UNKNOWN, mcp_parse_tool("foo"), "unknown tool"); ASSERT_EQ_INT(MCP_TOOL_UNKNOWN, mcp_parse_tool(NULL), "NULL tool"); } @@ -135,6 +160,121 @@ static void test_mcp_dispatch(void) ASSERT(!resp.success, "NULL request dispatch fails"); } +static void test_mcp_get_sessions(void) +{ + printf("\n=== MCP get_sessions ===\n"); + memset(g_test_sessions, 0, sizeof(g_test_sessions)); + + mcp_response_t resp = mcp_handle_get_sessions(); + ASSERT(resp.success, "get_sessions succeeds"); + cJSON *result = cJSON_Parse(resp.result_json); + ASSERT(result != NULL, "result is valid JSON array"); + ASSERT(cJSON_IsArray(result), "result is an array"); + ASSERT_EQ_INT(0, cJSON_GetArraySize(result), "empty sessions"); + cJSON_Delete(result); + + g_test_sessions[0].active = true; + g_test_sessions[0].client_ip = 0x0100000A; + strncpy(g_test_sessions[0].mac, "AA:BB:CC:DD:EE:FF", sizeof(g_test_sessions[0].mac) - 1); + g_test_sessions[0].allotment_ms = 60000; + + resp = mcp_handle_get_sessions(); + ASSERT(resp.success, "get_sessions with data succeeds"); + result = cJSON_Parse(resp.result_json); + ASSERT_EQ_INT(1, cJSON_GetArraySize(result), "one active session"); + cJSON *s = cJSON_GetArrayItem(result, 0); + ASSERT_EQ_STR("AA:BB:CC:DD:EE:FF", cJSON_GetObjectItem(s, "mac")->valuestring, "mac matches"); + cJSON_Delete(result); + g_test_sessions[0].active = false; +} + +static void test_mcp_get_usage(void) +{ + printf("\n=== MCP get_usage ===\n"); + memset(&g_test_config, 0, sizeof(g_test_config)); + strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1); + g_test_config.price_per_step = 21; + g_test_config.step_size_ms = 60000; + g_test_config.step_size_bytes = 22020096; + + mcp_response_t resp = mcp_handle_get_usage(); + ASSERT(resp.success, "get_usage succeeds"); + cJSON *result = cJSON_Parse(resp.result_json); + ASSERT(result != NULL, "result is valid JSON"); + ASSERT_EQ_STR("milliseconds", cJSON_GetObjectItem(result, "metric")->valuestring, "metric matches"); + ASSERT_EQ_INT(21, cJSON_GetObjectItem(result, "price_per_step")->valueint, "price matches"); + cJSON_Delete(result); +} + +static void test_mcp_set_payout(void) +{ + printf("\n=== MCP set_payout ===\n"); + memset(&g_test_config, 0, sizeof(g_test_config)); + + const char *params = "{\"enabled\":true,\"recipients\":[{\"lightning_address\":\"test@coinos.io\",\"factor\":0.5}]}"; + mcp_response_t resp = mcp_handle_set_payout(params); + ASSERT(resp.success, "set_payout succeeds"); + ASSERT(g_test_config.payout.enabled, "payout enabled"); + ASSERT_EQ_INT(1, g_test_config.payout.recipient_count, "1 recipient"); + ASSERT_EQ_STR("test@coinos.io", g_test_config.payout.recipients[0].lightning_address, "address matches"); + + resp = mcp_handle_set_payout("not json"); + ASSERT(!resp.success, "invalid JSON fails"); +} + +static void test_mcp_set_metric(void) +{ + printf("\n=== MCP set_metric ===\n"); + memset(&g_test_config, 0, sizeof(g_test_config)); + + mcp_response_t resp = mcp_handle_set_metric("{\"metric\":\"bytes\"}"); + ASSERT(resp.success, "set_metric bytes succeeds"); + ASSERT_EQ_STR("bytes", g_test_config.metric, "metric updated to bytes"); + + resp = mcp_handle_set_metric("{\"metric\":\"milliseconds\"}"); + ASSERT(resp.success, "set_metric milliseconds succeeds"); + ASSERT_EQ_STR("milliseconds", g_test_config.metric, "metric updated to milliseconds"); + + resp = mcp_handle_set_metric("{\"metric\":\"invalid\"}"); + ASSERT(!resp.success, "invalid metric rejected"); + + resp = mcp_handle_set_metric("{}"); + ASSERT(!resp.success, "missing metric rejected"); +} + +static void test_mcp_set_price(void) +{ + printf("\n=== MCP set_price ===\n"); + memset(&g_test_config, 0, sizeof(g_test_config)); + g_test_config.price_per_step = 21; + + mcp_response_t resp = mcp_handle_set_price("{\"price_per_step\":50}"); + ASSERT(resp.success, "set_price succeeds"); + ASSERT_EQ_INT(50, g_test_config.price_per_step, "price updated to 50"); + + resp = mcp_handle_set_price("{\"price_per_step\":0}"); + ASSERT(!resp.success, "zero price rejected"); + + resp = mcp_handle_set_price("{}"); + ASSERT(!resp.success, "missing price rejected"); +} + +static void test_mcp_wallet_melt(void) +{ + printf("\n=== MCP wallet_melt ===\n"); + g_wallet_melt_rc = ESP_OK; + + mcp_response_t resp = mcp_handle_wallet_melt("{\"bolt11\":\"lnbc100n1...\"}"); + ASSERT(resp.success, "wallet_melt succeeds"); + + g_wallet_melt_rc = ESP_FAIL; + resp = mcp_handle_wallet_melt("{\"bolt11\":\"lnbc100n1...\"}"); + ASSERT(!resp.success, "melt failure reported"); + + resp = mcp_handle_wallet_melt("{}"); + ASSERT(!resp.success, "missing bolt11 fails"); +} + int main(void) { printf("=== test_mcp_handler ===\n"); @@ -143,6 +283,12 @@ int main(void) test_mcp_set_config(); test_mcp_get_balance(); test_mcp_wallet_send(); + test_mcp_get_sessions(); + test_mcp_get_usage(); + test_mcp_set_payout(); + test_mcp_set_metric(); + test_mcp_set_price(); + test_mcp_wallet_melt(); test_mcp_dispatch(); TEST_SUMMARY(); } -- cgit v1.2.3