upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-19 01:10:06 +0530
committerYour Name <you@example.com>2026-05-19 01:10:06 +0530
commit42902a36bc52e009a1e8d3c371741e30a9cb4c33 (patch)
tree46db33710a3650b2267933a8375d3598af11319a /tests
parentfe7c3be2fd9d464dbc837d1913409d2691bd50f5 (diff)
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)
Diffstat (limited to 'tests')
-rw-r--r--tests/integration/test-cvm.mjs94
-rw-r--r--tests/unit/Makefile5
-rw-r--r--tests/unit/test_cvm_server.c434
-rw-r--r--tests/unit/test_mcp_handler.c146
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 @@
1import { execSync } from 'child_process';
2
3const IP = process.env.TOLLGATE_IP || '10.192.45.1';
4const RELAYS = ['wss://relay.damus.io', 'wss://nos.lol'];
5
6let passed = 0, failed = 0;
7
8function assert(condition, test) {
9 if (condition) { console.log(` \u2713 ${test}`); passed++; }
10 else { console.log(` \u2717 ${test}`); failed++; }
11}
12
13function 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
25function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
26
27async 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
91runTests().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
23SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o 23SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o
24 24
25TESTS := 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 25TESTS := 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
78test_nip04: test_nip04.c $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ) 78test_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
81test_cvm_server: test_cvm_server.c
82 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
83
81clean: 84clean:
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
8static 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
33static 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
62static 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
91static 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
103static 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
124static 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
151static 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
166static 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
191static 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
219static 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
240static 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
261static 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
277static 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
291static 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
311static 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
329static 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
350static 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
382static 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
395static 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
419int 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;
11static int g_wallet_proof_count = 0; 12static int g_wallet_proof_count = 0;
12static int g_wallet_send_rc = 0; 13static int g_wallet_send_rc = 0;
13static char g_wallet_send_token[256] = "cashuA_test_token"; 14static char g_wallet_send_token[256] = "cashuA_test_token";
15static esp_err_t g_wallet_melt_rc = ESP_OK;
14 16
15const tollgate_config_t *tollgate_config_get(void) { 17const 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
38esp_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
44static session_t g_test_sessions[SESSION_MAX_CLIENTS];
45static int g_test_session_count = 0;
46
47session_t *cvm_get_sessions_array(void) {
48 return g_test_sessions;
49}
50
51int cvm_get_sessions_count(void) {
52 return SESSION_MAX_CLIENTS;
53}
54
36static void test_mcp_parse_tool(void) 55static 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
163static 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
191static 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
209static 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
225static 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
245static 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
262static 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
138int main(void) 278int 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}