diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/integration/test-cvm.mjs | 94 | ||||
| -rw-r--r-- | tests/unit/Makefile | 5 | ||||
| -rw-r--r-- | tests/unit/test_cvm_server.c | 434 | ||||
| -rw-r--r-- | tests/unit/test_mcp_handler.c | 146 |
4 files changed, 678 insertions, 1 deletions
diff --git a/tests/integration/test-cvm.mjs b/tests/integration/test-cvm.mjs new file mode 100644 index 0000000..8deb6ec --- /dev/null +++ b/tests/integration/test-cvm.mjs | |||
| @@ -0,0 +1,94 @@ | |||
| 1 | import { execSync } from 'child_process'; | ||
| 2 | |||
| 3 | const IP = process.env.TOLLGATE_IP || '10.192.45.1'; | ||
| 4 | const RELAYS = ['wss://relay.damus.io', 'wss://nos.lol']; | ||
| 5 | |||
| 6 | let passed = 0, failed = 0; | ||
| 7 | |||
| 8 | function assert(condition, test) { | ||
| 9 | if (condition) { console.log(` \u2713 ${test}`); passed++; } | ||
| 10 | else { console.log(` \u2717 ${test}`); failed++; } | ||
| 11 | } | ||
| 12 | |||
| 13 | function nak(args, timeout = 10000) { | ||
| 14 | try { | ||
| 15 | return execSync(`timeout ${timeout / 1000} nak ${args}`, { | ||
| 16 | encoding: 'utf8', | ||
| 17 | stdio: ['pipe', 'pipe', 'pipe'], | ||
| 18 | timeout | ||
| 19 | }).trim(); | ||
| 20 | } catch (e) { | ||
| 21 | return e.stdout ? e.stdout.trim() : ''; | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } | ||
| 26 | |||
| 27 | async function runTests() { | ||
| 28 | console.log(`\n=== CVM Integration Tests (target: ${IP}) ===\n`); | ||
| 29 | |||
| 30 | const npub = nak(`key public a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2`); | ||
| 31 | const npubHex = npub.trim(); | ||
| 32 | console.log(`Board npub: ${npubHex}`); | ||
| 33 | |||
| 34 | const npubBech32 = nak(`encode npub ${npubHex}`).trim(); | ||
| 35 | console.log(`Board npub (bech32): ${npubBech32}`); | ||
| 36 | |||
| 37 | assert(npubHex.length === 64, 'npub hex is 64 chars'); | ||
| 38 | |||
| 39 | console.log('\n--- Test: Kind 11316 server announcement ---'); | ||
| 40 | for (const relay of RELAYS) { | ||
| 41 | console.log(` Querying ${relay}...`); | ||
| 42 | const result = nak(`req -k 11316 -a ${npubHex} -l 1 ${relay}`, 8000); | ||
| 43 | if (result.length > 0) { | ||
| 44 | assert(result.includes('"kind"') || result.includes('11316'), | ||
| 45 | `Kind 11316 found on ${relay}`); | ||
| 46 | if (result.includes('TollGate')) { | ||
| 47 | assert(true, `Announcement contains "TollGate"`); | ||
| 48 | } | ||
| 49 | } else { | ||
| 50 | console.log(` (no result from ${relay} — relay may be offline)`); | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | console.log('\n--- Test: Kind 11317 tools list ---'); | ||
| 55 | for (const relay of RELAYS) { | ||
| 56 | const result = nak(`req -k 11317 -a ${npubHex} -l 1 ${relay}`, 8000); | ||
| 57 | if (result.length > 0) { | ||
| 58 | assert(result.includes('"kind"') || result.includes('11317'), | ||
| 59 | `Kind 11317 found on ${relay}`); | ||
| 60 | if (result.includes('get_config') && result.includes('wallet_melt')) { | ||
| 61 | assert(true, `Tools list has expected tools`); | ||
| 62 | } | ||
| 63 | } else { | ||
| 64 | console.log(` (no result from ${relay} — relay may be offline)`); | ||
| 65 | } | ||
| 66 | } | ||
| 67 | |||
| 68 | console.log('\n--- Test: Kind 10002 relay list ---'); | ||
| 69 | for (const relay of RELAYS) { | ||
| 70 | const result = nak(`req -k 10002 -a ${npubHex} -l 1 ${relay}`, 8000); | ||
| 71 | if (result.length > 0) { | ||
| 72 | assert(result.includes('"kind"') || result.includes('10002'), | ||
| 73 | `Kind 10002 found on ${relay}`); | ||
| 74 | } else { | ||
| 75 | console.log(` (no result from ${relay} — relay may be offline)`); | ||
| 76 | } | ||
| 77 | } | ||
| 78 | |||
| 79 | console.log('\n--- Test: API get_config (control check) ---'); | ||
| 80 | try { | ||
| 81 | const apiResult = execSync(`curl -s http://${IP}:2121/usage`, { encoding: 'utf8', timeout: 5000 }); | ||
| 82 | assert(apiResult.length > 0, 'API /usage responds (board is reachable)'); | ||
| 83 | } catch (e) { | ||
| 84 | console.log(' (API not reachable — board may be offline or not flashed yet)'); | ||
| 85 | } | ||
| 86 | |||
| 87 | console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`); | ||
| 88 | process.exit(failed > 0 ? 1 : 0); | ||
| 89 | } | ||
| 90 | |||
| 91 | runTests().catch(e => { | ||
| 92 | console.error('Test error:', e.message); | ||
| 93 | process.exit(1); | ||
| 94 | }); | ||
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 | |||
| 22 | 22 | ||
| 23 | SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o | 23 | SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o |
| 24 | 24 | ||
| 25 | 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 | 25 | 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 |
| 26 | 26 | ||
| 27 | .PHONY: all test clean $(TESTS) | 27 | .PHONY: all test clean $(TESTS) |
| 28 | 28 | ||
| @@ -78,5 +78,8 @@ test_mcp_handler: test_mcp_handler.c $(REPO_ROOT)/main/mcp_handler.c | |||
| 78 | test_nip04: test_nip04.c $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ) | 78 | test_nip04: test_nip04.c $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ) |
| 79 | $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) $< $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ) -o $@ $(LDFLAGS) | 79 | $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) $< $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ) -o $@ $(LDFLAGS) |
| 80 | 80 | ||
| 81 | test_cvm_server: test_cvm_server.c | ||
| 82 | $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) | ||
| 83 | |||
| 81 | clean: | 84 | clean: |
| 82 | rm -f $(TESTS) $(SECP256K1_OBJ) | 85 | 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 @@ | |||
| 1 | #include "test_framework.h" | ||
| 2 | #include "cJSON.h" | ||
| 3 | #include <stdbool.h> | ||
| 4 | #include <string.h> | ||
| 5 | #include <stdio.h> | ||
| 6 | #include <stdlib.h> | ||
| 7 | |||
| 8 | static char *build_initialize_response_test(const char *request_id_str) | ||
| 9 | { | ||
| 10 | cJSON *response = cJSON_CreateObject(); | ||
| 11 | cJSON_AddStringToObject(response, "jsonrpc", "2.0"); | ||
| 12 | cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 0); | ||
| 13 | |||
| 14 | cJSON *result = cJSON_CreateObject(); | ||
| 15 | cJSON_AddStringToObject(result, "protocolVersion", "2025-07-02"); | ||
| 16 | |||
| 17 | cJSON *capabilities = cJSON_CreateObject(); | ||
| 18 | cJSON_AddItemToObject(capabilities, "tools", cJSON_CreateObject()); | ||
| 19 | cJSON_AddItemToObject(result, "capabilities", capabilities); | ||
| 20 | |||
| 21 | cJSON *serverInfo = cJSON_CreateObject(); | ||
| 22 | cJSON_AddStringToObject(serverInfo, "name", "TollGate"); | ||
| 23 | cJSON_AddStringToObject(serverInfo, "version", "1.0.0"); | ||
| 24 | cJSON_AddItemToObject(result, "serverInfo", serverInfo); | ||
| 25 | |||
| 26 | cJSON_AddItemToObject(response, "result", result); | ||
| 27 | |||
| 28 | char *json = cJSON_PrintUnformatted(response); | ||
| 29 | cJSON_Delete(response); | ||
| 30 | return json; | ||
| 31 | } | ||
| 32 | |||
| 33 | static char *build_tools_list_response_test(const char *request_id_str) | ||
| 34 | { | ||
| 35 | cJSON *response = cJSON_CreateObject(); | ||
| 36 | cJSON_AddStringToObject(response, "jsonrpc", "2.0"); | ||
| 37 | cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 1); | ||
| 38 | |||
| 39 | cJSON *result = cJSON_CreateObject(); | ||
| 40 | cJSON *tools = cJSON_CreateArray(); | ||
| 41 | |||
| 42 | const char *tool_names[] = { | ||
| 43 | "get_config", "set_config", "get_balance", "wallet_send", | ||
| 44 | "get_sessions", "get_usage", "set_payout", "set_metric", | ||
| 45 | "set_price", "wallet_melt" | ||
| 46 | }; | ||
| 47 | |||
| 48 | for (int i = 0; i < 10; i++) { | ||
| 49 | cJSON *tool = cJSON_CreateObject(); | ||
| 50 | cJSON_AddStringToObject(tool, "name", tool_names[i]); | ||
| 51 | cJSON_AddItemToArray(tools, tool); | ||
| 52 | } | ||
| 53 | |||
| 54 | cJSON_AddItemToObject(result, "tools", tools); | ||
| 55 | cJSON_AddItemToObject(response, "result", result); | ||
| 56 | |||
| 57 | char *json = cJSON_PrintUnformatted(response); | ||
| 58 | cJSON_Delete(response); | ||
| 59 | return json; | ||
| 60 | } | ||
| 61 | |||
| 62 | static char *build_tool_call_response_test(const char *request_id_str, | ||
| 63 | bool success, const char *result_or_error) | ||
| 64 | { | ||
| 65 | cJSON *response = cJSON_CreateObject(); | ||
| 66 | cJSON_AddStringToObject(response, "jsonrpc", "2.0"); | ||
| 67 | cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 2); | ||
| 68 | |||
| 69 | if (success) { | ||
| 70 | cJSON *result = cJSON_CreateObject(); | ||
| 71 | cJSON *content_arr = cJSON_CreateArray(); | ||
| 72 | cJSON *text_item = cJSON_CreateObject(); | ||
| 73 | cJSON_AddStringToObject(text_item, "type", "text"); | ||
| 74 | cJSON_AddStringToObject(text_item, "text", result_or_error); | ||
| 75 | cJSON_AddItemToArray(content_arr, text_item); | ||
| 76 | cJSON_AddItemToObject(result, "content", content_arr); | ||
| 77 | cJSON_AddBoolToObject(result, "isError", false); | ||
| 78 | cJSON_AddItemToObject(response, "result", result); | ||
| 79 | } else { | ||
| 80 | cJSON *error = cJSON_CreateObject(); | ||
| 81 | cJSON_AddNumberToObject(error, "code", -32603); | ||
| 82 | cJSON_AddStringToObject(error, "message", result_or_error); | ||
| 83 | cJSON_AddItemToObject(response, "error", error); | ||
| 84 | } | ||
| 85 | |||
| 86 | char *json = cJSON_PrintUnformatted(response); | ||
| 87 | cJSON_Delete(response); | ||
| 88 | return json; | ||
| 89 | } | ||
| 90 | |||
| 91 | static char *build_ping_response_test(const char *request_id_str) | ||
| 92 | { | ||
| 93 | cJSON *response = cJSON_CreateObject(); | ||
| 94 | cJSON_AddStringToObject(response, "jsonrpc", "2.0"); | ||
| 95 | cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 0); | ||
| 96 | cJSON *result = cJSON_CreateObject(); | ||
| 97 | cJSON_AddItemToObject(response, "result", result); | ||
| 98 | char *json = cJSON_PrintUnformatted(response); | ||
| 99 | cJSON_Delete(response); | ||
| 100 | return json; | ||
| 101 | } | ||
| 102 | |||
| 103 | static char *build_announcement_11316_test(void) | ||
| 104 | { | ||
| 105 | cJSON *ann = cJSON_CreateObject(); | ||
| 106 | cJSON_AddStringToObject(ann, "protocolVersion", "2025-07-02"); | ||
| 107 | |||
| 108 | cJSON *caps = cJSON_CreateObject(); | ||
| 109 | cJSON *tools = cJSON_CreateObject(); | ||
| 110 | cJSON_AddBoolToObject(tools, "listChanged", true); | ||
| 111 | cJSON_AddItemToObject(caps, "tools", tools); | ||
| 112 | cJSON_AddItemToObject(ann, "capabilities", caps); | ||
| 113 | |||
| 114 | cJSON *info = cJSON_CreateObject(); | ||
| 115 | cJSON_AddStringToObject(info, "name", "TollGate"); | ||
| 116 | cJSON_AddStringToObject(info, "version", "1.0.0"); | ||
| 117 | cJSON_AddItemToObject(ann, "serverInfo", info); | ||
| 118 | |||
| 119 | char *json = cJSON_PrintUnformatted(ann); | ||
| 120 | cJSON_Delete(ann); | ||
| 121 | return json; | ||
| 122 | } | ||
| 123 | |||
| 124 | static char *build_announcement_11317_test(void) | ||
| 125 | { | ||
| 126 | cJSON *root = cJSON_CreateObject(); | ||
| 127 | cJSON *tools = cJSON_CreateArray(); | ||
| 128 | |||
| 129 | const char *names[] = { | ||
| 130 | "get_config", "set_config", "get_balance", "wallet_send", | ||
| 131 | "get_sessions", "get_usage", "set_payout", "set_metric", | ||
| 132 | "set_price", "wallet_melt" | ||
| 133 | }; | ||
| 134 | |||
| 135 | for (int i = 0; i < 10; i++) { | ||
| 136 | cJSON *t = cJSON_CreateObject(); | ||
| 137 | cJSON_AddStringToObject(t, "name", names[i]); | ||
| 138 | cJSON_AddStringToObject(t, "description", "test"); | ||
| 139 | cJSON *schema = cJSON_CreateObject(); | ||
| 140 | cJSON_AddStringToObject(schema, "type", "object"); | ||
| 141 | cJSON_AddItemToObject(t, "inputSchema", schema); | ||
| 142 | cJSON_AddItemToArray(tools, t); | ||
| 143 | } | ||
| 144 | |||
| 145 | cJSON_AddItemToObject(root, "tools", tools); | ||
| 146 | char *json = cJSON_PrintUnformatted(root); | ||
| 147 | cJSON_Delete(root); | ||
| 148 | return json; | ||
| 149 | } | ||
| 150 | |||
| 151 | static char *build_relay_list_10002_test(void) | ||
| 152 | { | ||
| 153 | cJSON *tags = cJSON_CreateArray(); | ||
| 154 | const char *relays[] = {"wss://relay.damus.io", "wss://nos.lol"}; | ||
| 155 | for (int i = 0; i < 2; i++) { | ||
| 156 | cJSON *r = cJSON_CreateArray(); | ||
| 157 | cJSON_AddItemToArray(r, cJSON_CreateString("r")); | ||
| 158 | cJSON_AddItemToArray(r, cJSON_CreateString(relays[i])); | ||
| 159 | cJSON_AddItemToArray(tags, r); | ||
| 160 | } | ||
| 161 | char *json = cJSON_PrintUnformatted(tags); | ||
| 162 | cJSON_Delete(tags); | ||
| 163 | return json; | ||
| 164 | } | ||
| 165 | |||
| 166 | static bool parse_mcp_from_25910(const char *content, char *method_out, size_t method_max, | ||
| 167 | char *params_out, size_t params_max) | ||
| 168 | { | ||
| 169 | cJSON *msg = cJSON_Parse(content); | ||
| 170 | if (!msg) return false; | ||
| 171 | |||
| 172 | cJSON *method = cJSON_GetObjectItem(msg, "method"); | ||
| 173 | if (!method || !cJSON_IsString(method)) { | ||
| 174 | cJSON_Delete(msg); | ||
| 175 | return false; | ||
| 176 | } | ||
| 177 | |||
| 178 | strncpy(method_out, method->valuestring, method_max - 1); | ||
| 179 | |||
| 180 | cJSON *params = cJSON_GetObjectItem(msg, "params"); | ||
| 181 | if (params) { | ||
| 182 | char *pjson = cJSON_PrintUnformatted(params); | ||
| 183 | strncpy(params_out, pjson, params_max - 1); | ||
| 184 | cJSON_free(pjson); | ||
| 185 | } | ||
| 186 | |||
| 187 | cJSON_Delete(msg); | ||
| 188 | return true; | ||
| 189 | } | ||
| 190 | |||
| 191 | static void test_initialize_response(void) | ||
| 192 | { | ||
| 193 | printf("\n=== MCP initialize response ===\n"); | ||
| 194 | char *json = build_initialize_response_test("0"); | ||
| 195 | ASSERT(json != NULL, "response created"); | ||
| 196 | |||
| 197 | cJSON *root = cJSON_Parse(json); | ||
| 198 | ASSERT(root != NULL, "valid JSON"); | ||
| 199 | ASSERT_EQ_STR("2.0", cJSON_GetObjectItem(root, "jsonrpc")->valuestring, "jsonrpc version"); | ||
| 200 | ASSERT_EQ_INT(0, (int)cJSON_GetObjectItem(root, "id")->valuedouble, "id=0"); | ||
| 201 | |||
| 202 | cJSON *result = cJSON_GetObjectItem(root, "result"); | ||
| 203 | ASSERT(result != NULL, "has result"); | ||
| 204 | ASSERT_EQ_STR("2025-07-02", cJSON_GetObjectItem(result, "protocolVersion")->valuestring, "protocol version"); | ||
| 205 | |||
| 206 | cJSON *caps = cJSON_GetObjectItem(result, "capabilities"); | ||
| 207 | ASSERT(caps != NULL, "has capabilities"); | ||
| 208 | ASSERT(cJSON_GetObjectItem(caps, "tools") != NULL, "has tools capability"); | ||
| 209 | |||
| 210 | cJSON *info = cJSON_GetObjectItem(result, "serverInfo"); | ||
| 211 | ASSERT(info != NULL, "has serverInfo"); | ||
| 212 | ASSERT_EQ_STR("TollGate", cJSON_GetObjectItem(info, "name")->valuestring, "server name"); | ||
| 213 | ASSERT_EQ_STR("1.0.0", cJSON_GetObjectItem(info, "version")->valuestring, "server version"); | ||
| 214 | |||
| 215 | cJSON_Delete(root); | ||
| 216 | free(json); | ||
| 217 | } | ||
| 218 | |||
| 219 | static void test_tools_list_response(void) | ||
| 220 | { | ||
| 221 | printf("\n=== MCP tools/list response ===\n"); | ||
| 222 | char *json = build_tools_list_response_test("1"); | ||
| 223 | ASSERT(json != NULL, "response created"); | ||
| 224 | |||
| 225 | cJSON *root = cJSON_Parse(json); | ||
| 226 | ASSERT_EQ_STR("2.0", cJSON_GetObjectItem(root, "jsonrpc")->valuestring, "jsonrpc version"); | ||
| 227 | |||
| 228 | cJSON *result = cJSON_GetObjectItem(root, "result"); | ||
| 229 | cJSON *tools = cJSON_GetObjectItem(result, "tools"); | ||
| 230 | ASSERT(tools != NULL, "has tools array"); | ||
| 231 | ASSERT_EQ_INT(10, cJSON_GetArraySize(tools), "10 tools"); | ||
| 232 | |||
| 233 | ASSERT_EQ_STR("get_config", cJSON_GetObjectItem(cJSON_GetArrayItem(tools, 0), "name")->valuestring, "tool 0"); | ||
| 234 | ASSERT_EQ_STR("wallet_melt", cJSON_GetObjectItem(cJSON_GetArrayItem(tools, 9), "name")->valuestring, "tool 9"); | ||
| 235 | |||
| 236 | cJSON_Delete(root); | ||
| 237 | free(json); | ||
| 238 | } | ||
| 239 | |||
| 240 | static void test_tool_call_response_success(void) | ||
| 241 | { | ||
| 242 | printf("\n=== MCP tools/call success response ===\n"); | ||
| 243 | char *json = build_tool_call_response_test("2", true, "{\"balance\":500}"); | ||
| 244 | ASSERT(json != NULL, "response created"); | ||
| 245 | |||
| 246 | cJSON *root = cJSON_Parse(json); | ||
| 247 | cJSON *result = cJSON_GetObjectItem(root, "result"); | ||
| 248 | ASSERT(result != NULL, "has result"); | ||
| 249 | ASSERT(cJSON_GetObjectItem(result, "content") != NULL, "has content"); | ||
| 250 | ASSERT_EQ_INT(0, cJSON_GetObjectItem(result, "isError")->valueint, "isError=false"); | ||
| 251 | |||
| 252 | cJSON *content = cJSON_GetObjectItem(result, "content"); | ||
| 253 | cJSON *text = cJSON_GetArrayItem(content, 0); | ||
| 254 | ASSERT_EQ_STR("text", cJSON_GetObjectItem(text, "type")->valuestring, "content type=text"); | ||
| 255 | ASSERT(strstr(cJSON_GetObjectItem(text, "text")->valuestring, "balance") != NULL, "contains balance"); | ||
| 256 | |||
| 257 | cJSON_Delete(root); | ||
| 258 | free(json); | ||
| 259 | } | ||
| 260 | |||
| 261 | static void test_tool_call_response_error(void) | ||
| 262 | { | ||
| 263 | printf("\n=== MCP tools/call error response ===\n"); | ||
| 264 | char *json = build_tool_call_response_test("3", false, "Tool not found"); | ||
| 265 | ASSERT(json != NULL, "response created"); | ||
| 266 | |||
| 267 | cJSON *root = cJSON_Parse(json); | ||
| 268 | cJSON *error = cJSON_GetObjectItem(root, "error"); | ||
| 269 | ASSERT(error != NULL, "has error"); | ||
| 270 | ASSERT_EQ_INT(-32603, cJSON_GetObjectItem(error, "code")->valueint, "error code"); | ||
| 271 | ASSERT_EQ_STR("Tool not found", cJSON_GetObjectItem(error, "message")->valuestring, "error message"); | ||
| 272 | |||
| 273 | cJSON_Delete(root); | ||
| 274 | free(json); | ||
| 275 | } | ||
| 276 | |||
| 277 | static void test_ping_response(void) | ||
| 278 | { | ||
| 279 | printf("\n=== MCP ping response ===\n"); | ||
| 280 | char *json = build_ping_response_test("99"); | ||
| 281 | ASSERT(json != NULL, "response created"); | ||
| 282 | |||
| 283 | cJSON *root = cJSON_Parse(json); | ||
| 284 | ASSERT_EQ_STR("2.0", cJSON_GetObjectItem(root, "jsonrpc")->valuestring, "jsonrpc version"); | ||
| 285 | ASSERT(cJSON_GetObjectItem(root, "result") != NULL, "has result"); | ||
| 286 | |||
| 287 | cJSON_Delete(root); | ||
| 288 | free(json); | ||
| 289 | } | ||
| 290 | |||
| 291 | static void test_announcement_11316(void) | ||
| 292 | { | ||
| 293 | printf("\n=== Kind 11316 server announcement ===\n"); | ||
| 294 | char *json = build_announcement_11316_test(); | ||
| 295 | ASSERT(json != NULL, "announcement created"); | ||
| 296 | |||
| 297 | cJSON *root = cJSON_Parse(json); | ||
| 298 | ASSERT_EQ_STR("2025-07-02", cJSON_GetObjectItem(root, "protocolVersion")->valuestring, "protocol version"); | ||
| 299 | |||
| 300 | cJSON *caps = cJSON_GetObjectItem(root, "capabilities"); | ||
| 301 | ASSERT(cJSON_GetObjectItem(caps, "tools") != NULL, "has tools capability"); | ||
| 302 | |||
| 303 | cJSON *info = cJSON_GetObjectItem(root, "serverInfo"); | ||
| 304 | ASSERT_EQ_STR("TollGate", cJSON_GetObjectItem(info, "name")->valuestring, "name"); | ||
| 305 | ASSERT_EQ_STR("1.0.0", cJSON_GetObjectItem(info, "version")->valuestring, "version"); | ||
| 306 | |||
| 307 | cJSON_Delete(root); | ||
| 308 | free(json); | ||
| 309 | } | ||
| 310 | |||
| 311 | static void test_announcement_11317(void) | ||
| 312 | { | ||
| 313 | printf("\n=== Kind 11317 tools list ===\n"); | ||
| 314 | char *json = build_announcement_11317_test(); | ||
| 315 | ASSERT(json != NULL, "tools list created"); | ||
| 316 | |||
| 317 | cJSON *root = cJSON_Parse(json); | ||
| 318 | cJSON *tools = cJSON_GetObjectItem(root, "tools"); | ||
| 319 | ASSERT_EQ_INT(10, cJSON_GetArraySize(tools), "10 tools"); | ||
| 320 | |||
| 321 | cJSON *t0 = cJSON_GetArrayItem(tools, 0); | ||
| 322 | ASSERT_EQ_STR("get_config", cJSON_GetObjectItem(t0, "name")->valuestring, "tool 0 name"); | ||
| 323 | ASSERT(cJSON_GetObjectItem(t0, "inputSchema") != NULL, "tool has inputSchema"); | ||
| 324 | |||
| 325 | cJSON_Delete(root); | ||
| 326 | free(json); | ||
| 327 | } | ||
| 328 | |||
| 329 | static void test_relay_list_10002(void) | ||
| 330 | { | ||
| 331 | printf("\n=== Kind 10002 relay list ===\n"); | ||
| 332 | char *json = build_relay_list_10002_test(); | ||
| 333 | ASSERT(json != NULL, "relay list created"); | ||
| 334 | |||
| 335 | cJSON *tags = cJSON_Parse(json); | ||
| 336 | ASSERT(cJSON_IsArray(tags), "is array"); | ||
| 337 | ASSERT_EQ_INT(2, cJSON_GetArraySize(tags), "2 relay tags"); | ||
| 338 | |||
| 339 | cJSON *r0 = cJSON_GetArrayItem(tags, 0); | ||
| 340 | ASSERT_EQ_STR("r", cJSON_GetArrayItem(r0, 0)->valuestring, "tag type r"); | ||
| 341 | ASSERT_EQ_STR("wss://relay.damus.io", cJSON_GetArrayItem(r0, 1)->valuestring, "relay 0"); | ||
| 342 | |||
| 343 | cJSON *r1 = cJSON_GetArrayItem(tags, 1); | ||
| 344 | ASSERT_EQ_STR("wss://nos.lol", cJSON_GetArrayItem(r1, 1)->valuestring, "relay 1"); | ||
| 345 | |||
| 346 | cJSON_Delete(tags); | ||
| 347 | free(json); | ||
| 348 | } | ||
| 349 | |||
| 350 | static void test_mcp_parse_from_25910(void) | ||
| 351 | { | ||
| 352 | printf("\n=== Parse MCP from kind 25910 content ===\n"); | ||
| 353 | |||
| 354 | char method[64] = {0}; | ||
| 355 | char params[1024] = {0}; | ||
| 356 | |||
| 357 | bool ok = parse_mcp_from_25910( | ||
| 358 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"initialize\",\"params\":{}}", | ||
| 359 | method, sizeof(method), params, sizeof(params)); | ||
| 360 | ASSERT(ok, "parsed initialize"); | ||
| 361 | ASSERT_EQ_STR("initialize", method, "method=initialize"); | ||
| 362 | |||
| 363 | ok = parse_mcp_from_25910( | ||
| 364 | "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"get_config\"}}", | ||
| 365 | method, sizeof(method), params, sizeof(params)); | ||
| 366 | ASSERT(ok, "parsed tools/call"); | ||
| 367 | ASSERT_EQ_STR("tools/call", method, "method=tools/call"); | ||
| 368 | ASSERT(strstr(params, "get_config") != NULL, "params has get_config"); | ||
| 369 | |||
| 370 | ok = parse_mcp_from_25910("{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}", | ||
| 371 | method, sizeof(method), params, sizeof(params)); | ||
| 372 | ASSERT(ok, "parsed notification"); | ||
| 373 | ASSERT_EQ_STR("notifications/initialized", method, "method=notifications/initialized"); | ||
| 374 | |||
| 375 | ok = parse_mcp_from_25910("not json", method, sizeof(method), params, sizeof(params)); | ||
| 376 | ASSERT(!ok, "garbage rejected"); | ||
| 377 | |||
| 378 | ok = parse_mcp_from_25910("{\"jsonrpc\":\"2.0\"}", method, sizeof(method), params, sizeof(params)); | ||
| 379 | ASSERT(!ok, "missing method rejected"); | ||
| 380 | } | ||
| 381 | |||
| 382 | static void test_auth_check(void) | ||
| 383 | { | ||
| 384 | printf("\n=== Auth check logic ===\n"); | ||
| 385 | |||
| 386 | const char *owner = "d6bfe100d1600c0d8f769501676fc74c3809500bd131c8a549f88cf616c21f35"; | ||
| 387 | const char *other = "0000000000000000000000000000000000000000000000000000000000000001"; | ||
| 388 | |||
| 389 | ASSERT(strcmp(owner, owner) == 0, "owner matches self"); | ||
| 390 | ASSERT(strcmp(owner, other) != 0, "owner differs from other"); | ||
| 391 | ASSERT(strcmp(other, owner) != 0, "other differs from owner"); | ||
| 392 | ASSERT(NULL == NULL, "two NULLs match (for safety check)"); | ||
| 393 | } | ||
| 394 | |||
| 395 | static void test_25910_event_content_roundtrip(void) | ||
| 396 | { | ||
| 397 | printf("\n=== Kind 25910 content roundtrip ===\n"); | ||
| 398 | |||
| 399 | cJSON *request = cJSON_CreateObject(); | ||
| 400 | cJSON_AddStringToObject(request, "jsonrpc", "2.0"); | ||
| 401 | cJSON_AddNumberToObject(request, "id", 42); | ||
| 402 | cJSON_AddStringToObject(request, "method", "tools/call"); | ||
| 403 | cJSON *params = cJSON_CreateObject(); | ||
| 404 | cJSON_AddStringToObject(params, "name", "get_balance"); | ||
| 405 | cJSON_AddItemToObject(request, "params", params); | ||
| 406 | char *content = cJSON_PrintUnformatted(request); | ||
| 407 | cJSON_Delete(request); | ||
| 408 | |||
| 409 | char method[64] = {0}; | ||
| 410 | char params_out[1024] = {0}; | ||
| 411 | bool ok = parse_mcp_from_25910(content, method, sizeof(method), params_out, sizeof(params_out)); | ||
| 412 | ASSERT(ok, "roundtrip parse succeeded"); | ||
| 413 | ASSERT_EQ_STR("tools/call", method, "method preserved"); | ||
| 414 | ASSERT(strstr(params_out, "get_balance") != NULL, "tool name preserved"); | ||
| 415 | |||
| 416 | free(content); | ||
| 417 | } | ||
| 418 | |||
| 419 | int main(void) | ||
| 420 | { | ||
| 421 | printf("=== test_cvm_server ===\n"); | ||
| 422 | test_initialize_response(); | ||
| 423 | test_tools_list_response(); | ||
| 424 | test_tool_call_response_success(); | ||
| 425 | test_tool_call_response_error(); | ||
| 426 | test_ping_response(); | ||
| 427 | test_announcement_11316(); | ||
| 428 | test_announcement_11317(); | ||
| 429 | test_relay_list_10002(); | ||
| 430 | test_mcp_parse_from_25910(); | ||
| 431 | test_auth_check(); | ||
| 432 | test_25910_event_content_roundtrip(); | ||
| 433 | TEST_SUMMARY(); | ||
| 434 | } | ||
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 @@ | |||
| 1 | #include "test_framework.h" | 1 | #include "test_framework.h" |
| 2 | #include "mcp_handler.h" | 2 | #include "mcp_handler.h" |
| 3 | #include "config.h" | 3 | #include "config.h" |
| 4 | #include "session.h" | ||
| 4 | #include "nucula_wallet.h" | 5 | #include "nucula_wallet.h" |
| 5 | #include "cJSON.h" | 6 | #include "cJSON.h" |
| 6 | #include <string.h> | 7 | #include <string.h> |
| @@ -11,6 +12,7 @@ static uint64_t g_wallet_balance = 0; | |||
| 11 | static int g_wallet_proof_count = 0; | 12 | static int g_wallet_proof_count = 0; |
| 12 | static int g_wallet_send_rc = 0; | 13 | static int g_wallet_send_rc = 0; |
| 13 | static char g_wallet_send_token[256] = "cashuA_test_token"; | 14 | static char g_wallet_send_token[256] = "cashuA_test_token"; |
| 15 | static esp_err_t g_wallet_melt_rc = ESP_OK; | ||
| 14 | 16 | ||
| 15 | const tollgate_config_t *tollgate_config_get(void) { | 17 | const tollgate_config_t *tollgate_config_get(void) { |
| 16 | return &g_test_config; | 18 | return &g_test_config; |
| @@ -33,6 +35,23 @@ int nucula_wallet_send(uint64_t amount, char *token_out, size_t token_max) { | |||
| 33 | return g_wallet_send_rc; | 35 | return g_wallet_send_rc; |
| 34 | } | 36 | } |
| 35 | 37 | ||
| 38 | esp_err_t nucula_wallet_melt(const char *bolt11, uint64_t max_fee) { | ||
| 39 | (void)bolt11; | ||
| 40 | (void)max_fee; | ||
| 41 | return g_wallet_melt_rc; | ||
| 42 | } | ||
| 43 | |||
| 44 | static session_t g_test_sessions[SESSION_MAX_CLIENTS]; | ||
| 45 | static int g_test_session_count = 0; | ||
| 46 | |||
| 47 | session_t *cvm_get_sessions_array(void) { | ||
| 48 | return g_test_sessions; | ||
| 49 | } | ||
| 50 | |||
| 51 | int cvm_get_sessions_count(void) { | ||
| 52 | return SESSION_MAX_CLIENTS; | ||
| 53 | } | ||
| 54 | |||
| 36 | static void test_mcp_parse_tool(void) | 55 | static void test_mcp_parse_tool(void) |
| 37 | { | 56 | { |
| 38 | printf("\n=== MCP tool parsing ===\n"); | 57 | printf("\n=== MCP tool parsing ===\n"); |
| @@ -40,6 +59,12 @@ static void test_mcp_parse_tool(void) | |||
| 40 | ASSERT_EQ_INT(MCP_TOOL_SET_CONFIG, mcp_parse_tool("set_config"), "set_config"); | 59 | ASSERT_EQ_INT(MCP_TOOL_SET_CONFIG, mcp_parse_tool("set_config"), "set_config"); |
| 41 | ASSERT_EQ_INT(MCP_TOOL_GET_BALANCE, mcp_parse_tool("get_balance"), "get_balance"); | 60 | ASSERT_EQ_INT(MCP_TOOL_GET_BALANCE, mcp_parse_tool("get_balance"), "get_balance"); |
| 42 | ASSERT_EQ_INT(MCP_TOOL_WALLET_SEND, mcp_parse_tool("wallet_send"), "wallet_send"); | 61 | ASSERT_EQ_INT(MCP_TOOL_WALLET_SEND, mcp_parse_tool("wallet_send"), "wallet_send"); |
| 62 | ASSERT_EQ_INT(MCP_TOOL_GET_SESSIONS, mcp_parse_tool("get_sessions"), "get_sessions"); | ||
| 63 | ASSERT_EQ_INT(MCP_TOOL_GET_USAGE, mcp_parse_tool("get_usage"), "get_usage"); | ||
| 64 | ASSERT_EQ_INT(MCP_TOOL_SET_PAYOUT, mcp_parse_tool("set_payout"), "set_payout"); | ||
| 65 | ASSERT_EQ_INT(MCP_TOOL_SET_METRIC, mcp_parse_tool("set_metric"), "set_metric"); | ||
| 66 | ASSERT_EQ_INT(MCP_TOOL_SET_PRICE, mcp_parse_tool("set_price"), "set_price"); | ||
| 67 | ASSERT_EQ_INT(MCP_TOOL_WALLET_MELT, mcp_parse_tool("wallet_melt"), "wallet_melt"); | ||
| 43 | ASSERT_EQ_INT(MCP_TOOL_UNKNOWN, mcp_parse_tool("foo"), "unknown tool"); | 68 | ASSERT_EQ_INT(MCP_TOOL_UNKNOWN, mcp_parse_tool("foo"), "unknown tool"); |
| 44 | ASSERT_EQ_INT(MCP_TOOL_UNKNOWN, mcp_parse_tool(NULL), "NULL tool"); | 69 | ASSERT_EQ_INT(MCP_TOOL_UNKNOWN, mcp_parse_tool(NULL), "NULL tool"); |
| 45 | } | 70 | } |
| @@ -135,6 +160,121 @@ static void test_mcp_dispatch(void) | |||
| 135 | ASSERT(!resp.success, "NULL request dispatch fails"); | 160 | ASSERT(!resp.success, "NULL request dispatch fails"); |
| 136 | } | 161 | } |
| 137 | 162 | ||
| 163 | static void test_mcp_get_sessions(void) | ||
| 164 | { | ||
| 165 | printf("\n=== MCP get_sessions ===\n"); | ||
| 166 | memset(g_test_sessions, 0, sizeof(g_test_sessions)); | ||
| 167 | |||
| 168 | mcp_response_t resp = mcp_handle_get_sessions(); | ||
| 169 | ASSERT(resp.success, "get_sessions succeeds"); | ||
| 170 | cJSON *result = cJSON_Parse(resp.result_json); | ||
| 171 | ASSERT(result != NULL, "result is valid JSON array"); | ||
| 172 | ASSERT(cJSON_IsArray(result), "result is an array"); | ||
| 173 | ASSERT_EQ_INT(0, cJSON_GetArraySize(result), "empty sessions"); | ||
| 174 | cJSON_Delete(result); | ||
| 175 | |||
| 176 | g_test_sessions[0].active = true; | ||
| 177 | g_test_sessions[0].client_ip = 0x0100000A; | ||
| 178 | strncpy(g_test_sessions[0].mac, "AA:BB:CC:DD:EE:FF", sizeof(g_test_sessions[0].mac) - 1); | ||
| 179 | g_test_sessions[0].allotment_ms = 60000; | ||
| 180 | |||
| 181 | resp = mcp_handle_get_sessions(); | ||
| 182 | ASSERT(resp.success, "get_sessions with data succeeds"); | ||
| 183 | result = cJSON_Parse(resp.result_json); | ||
| 184 | ASSERT_EQ_INT(1, cJSON_GetArraySize(result), "one active session"); | ||
| 185 | cJSON *s = cJSON_GetArrayItem(result, 0); | ||
| 186 | ASSERT_EQ_STR("AA:BB:CC:DD:EE:FF", cJSON_GetObjectItem(s, "mac")->valuestring, "mac matches"); | ||
| 187 | cJSON_Delete(result); | ||
| 188 | g_test_sessions[0].active = false; | ||
| 189 | } | ||
| 190 | |||
| 191 | static void test_mcp_get_usage(void) | ||
| 192 | { | ||
| 193 | printf("\n=== MCP get_usage ===\n"); | ||
| 194 | memset(&g_test_config, 0, sizeof(g_test_config)); | ||
| 195 | strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1); | ||
| 196 | g_test_config.price_per_step = 21; | ||
| 197 | g_test_config.step_size_ms = 60000; | ||
| 198 | g_test_config.step_size_bytes = 22020096; | ||
| 199 | |||
| 200 | mcp_response_t resp = mcp_handle_get_usage(); | ||
| 201 | ASSERT(resp.success, "get_usage succeeds"); | ||
| 202 | cJSON *result = cJSON_Parse(resp.result_json); | ||
| 203 | ASSERT(result != NULL, "result is valid JSON"); | ||
| 204 | ASSERT_EQ_STR("milliseconds", cJSON_GetObjectItem(result, "metric")->valuestring, "metric matches"); | ||
| 205 | ASSERT_EQ_INT(21, cJSON_GetObjectItem(result, "price_per_step")->valueint, "price matches"); | ||
| 206 | cJSON_Delete(result); | ||
| 207 | } | ||
| 208 | |||
| 209 | static void test_mcp_set_payout(void) | ||
| 210 | { | ||
| 211 | printf("\n=== MCP set_payout ===\n"); | ||
| 212 | memset(&g_test_config, 0, sizeof(g_test_config)); | ||
| 213 | |||
| 214 | const char *params = "{\"enabled\":true,\"recipients\":[{\"lightning_address\":\"test@coinos.io\",\"factor\":0.5}]}"; | ||
| 215 | mcp_response_t resp = mcp_handle_set_payout(params); | ||
| 216 | ASSERT(resp.success, "set_payout succeeds"); | ||
| 217 | ASSERT(g_test_config.payout.enabled, "payout enabled"); | ||
| 218 | ASSERT_EQ_INT(1, g_test_config.payout.recipient_count, "1 recipient"); | ||
| 219 | ASSERT_EQ_STR("test@coinos.io", g_test_config.payout.recipients[0].lightning_address, "address matches"); | ||
| 220 | |||
| 221 | resp = mcp_handle_set_payout("not json"); | ||
| 222 | ASSERT(!resp.success, "invalid JSON fails"); | ||
| 223 | } | ||
| 224 | |||
| 225 | static void test_mcp_set_metric(void) | ||
| 226 | { | ||
| 227 | printf("\n=== MCP set_metric ===\n"); | ||
| 228 | memset(&g_test_config, 0, sizeof(g_test_config)); | ||
| 229 | |||
| 230 | mcp_response_t resp = mcp_handle_set_metric("{\"metric\":\"bytes\"}"); | ||
| 231 | ASSERT(resp.success, "set_metric bytes succeeds"); | ||
| 232 | ASSERT_EQ_STR("bytes", g_test_config.metric, "metric updated to bytes"); | ||
| 233 | |||
| 234 | resp = mcp_handle_set_metric("{\"metric\":\"milliseconds\"}"); | ||
| 235 | ASSERT(resp.success, "set_metric milliseconds succeeds"); | ||
| 236 | ASSERT_EQ_STR("milliseconds", g_test_config.metric, "metric updated to milliseconds"); | ||
| 237 | |||
| 238 | resp = mcp_handle_set_metric("{\"metric\":\"invalid\"}"); | ||
| 239 | ASSERT(!resp.success, "invalid metric rejected"); | ||
| 240 | |||
| 241 | resp = mcp_handle_set_metric("{}"); | ||
| 242 | ASSERT(!resp.success, "missing metric rejected"); | ||
| 243 | } | ||
| 244 | |||
| 245 | static void test_mcp_set_price(void) | ||
| 246 | { | ||
| 247 | printf("\n=== MCP set_price ===\n"); | ||
| 248 | memset(&g_test_config, 0, sizeof(g_test_config)); | ||
| 249 | g_test_config.price_per_step = 21; | ||
| 250 | |||
| 251 | mcp_response_t resp = mcp_handle_set_price("{\"price_per_step\":50}"); | ||
| 252 | ASSERT(resp.success, "set_price succeeds"); | ||
| 253 | ASSERT_EQ_INT(50, g_test_config.price_per_step, "price updated to 50"); | ||
| 254 | |||
| 255 | resp = mcp_handle_set_price("{\"price_per_step\":0}"); | ||
| 256 | ASSERT(!resp.success, "zero price rejected"); | ||
| 257 | |||
| 258 | resp = mcp_handle_set_price("{}"); | ||
| 259 | ASSERT(!resp.success, "missing price rejected"); | ||
| 260 | } | ||
| 261 | |||
| 262 | static void test_mcp_wallet_melt(void) | ||
| 263 | { | ||
| 264 | printf("\n=== MCP wallet_melt ===\n"); | ||
| 265 | g_wallet_melt_rc = ESP_OK; | ||
| 266 | |||
| 267 | mcp_response_t resp = mcp_handle_wallet_melt("{\"bolt11\":\"lnbc100n1...\"}"); | ||
| 268 | ASSERT(resp.success, "wallet_melt succeeds"); | ||
| 269 | |||
| 270 | g_wallet_melt_rc = ESP_FAIL; | ||
| 271 | resp = mcp_handle_wallet_melt("{\"bolt11\":\"lnbc100n1...\"}"); | ||
| 272 | ASSERT(!resp.success, "melt failure reported"); | ||
| 273 | |||
| 274 | resp = mcp_handle_wallet_melt("{}"); | ||
| 275 | ASSERT(!resp.success, "missing bolt11 fails"); | ||
| 276 | } | ||
| 277 | |||
| 138 | int main(void) | 278 | int main(void) |
| 139 | { | 279 | { |
| 140 | printf("=== test_mcp_handler ===\n"); | 280 | printf("=== test_mcp_handler ===\n"); |
| @@ -143,6 +283,12 @@ int main(void) | |||
| 143 | test_mcp_set_config(); | 283 | test_mcp_set_config(); |
| 144 | test_mcp_get_balance(); | 284 | test_mcp_get_balance(); |
| 145 | test_mcp_wallet_send(); | 285 | test_mcp_wallet_send(); |
| 286 | test_mcp_get_sessions(); | ||
| 287 | test_mcp_get_usage(); | ||
| 288 | test_mcp_set_payout(); | ||
| 289 | test_mcp_set_metric(); | ||
| 290 | test_mcp_set_price(); | ||
| 291 | test_mcp_wallet_melt(); | ||
| 146 | test_mcp_dispatch(); | 292 | test_mcp_dispatch(); |
| 147 | TEST_SUMMARY(); | 293 | TEST_SUMMARY(); |
| 148 | } | 294 | } |