diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/integration/test-market.mjs | 60 | ||||
| -rw-r--r-- | tests/integration/test-price-discovery.mjs | 138 | ||||
| -rw-r--r-- | tests/unit/Makefile | 12 | ||||
| -rw-r--r-- | tests/unit/stubs/esp_wifi.h | 64 | ||||
| -rw-r--r-- | tests/unit/test_beacon_price.c | 132 | ||||
| -rw-r--r-- | tests/unit/test_market.c | 177 | ||||
| -rw-r--r-- | tests/unit/test_tollgate_client.c | 1 |
7 files changed, 581 insertions, 3 deletions
diff --git a/tests/integration/test-market.mjs b/tests/integration/test-market.mjs new file mode 100644 index 0000000..20f062f --- /dev/null +++ b/tests/integration/test-market.mjs | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | import { execSync } from 'child_process'; | ||
| 2 | |||
| 3 | const API_URL = `http://${process.env.TOLLGATE_IP || '10.192.45.1'}:2121`; | ||
| 4 | |||
| 5 | function run(cmd) { | ||
| 6 | try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); } | ||
| 7 | catch (e) { return e.stdout || null; } | ||
| 8 | } | ||
| 9 | |||
| 10 | function runJson(cmd) { | ||
| 11 | const out = run(cmd); | ||
| 12 | try { return out ? JSON.parse(out) : null; } | ||
| 13 | catch { return null; } | ||
| 14 | } | ||
| 15 | |||
| 16 | let passed = 0, failed = 0; | ||
| 17 | function assert(cond, msg) { | ||
| 18 | if (cond) { console.log(` PASS: ${msg}`); passed++; } | ||
| 19 | else { console.log(` FAIL: ${msg}`); failed++; } | ||
| 20 | } | ||
| 21 | |||
| 22 | console.log('=== test-market (GET /market) ===\n'); | ||
| 23 | |||
| 24 | console.log('--- /market endpoint responds ---'); | ||
| 25 | { | ||
| 26 | const data = runJson(`curl -s --connect-timeout 5 ${API_URL}/market`); | ||
| 27 | assert(data !== null, '/market returns valid JSON'); | ||
| 28 | assert(typeof data.count === 'number', `count is number (got ${data?.count})`); | ||
| 29 | assert(Array.isArray(data.entries), 'entries is array'); | ||
| 30 | } | ||
| 31 | |||
| 32 | console.log('\n--- /market entry structure ---'); | ||
| 33 | { | ||
| 34 | const data = runJson(`curl -s --connect-timeout 5 ${API_URL}/market`); | ||
| 35 | if (data && data.entries && data.entries.length > 0) { | ||
| 36 | const e = data.entries[0]; | ||
| 37 | assert(typeof e.bssid === 'string', `bssid is string (got ${e.bssid})`); | ||
| 38 | assert(typeof e.ssid === 'string', `ssid is string (got ${e.ssid})`); | ||
| 39 | assert(typeof e.rssi === 'number', `rssi is number (got ${e.rssi})`); | ||
| 40 | assert(typeof e.price_per_step === 'number', `price_per_step is number (got ${e.price_per_step})`); | ||
| 41 | assert(typeof e.step_size === 'number', `step_size is number (got ${e.step_size})`); | ||
| 42 | assert(typeof e.metric === 'string', `metric is string (got ${e.metric})`); | ||
| 43 | } else { | ||
| 44 | console.log(' SKIP: no entries found (scan may not have run yet)'); | ||
| 45 | } | ||
| 46 | } | ||
| 47 | |||
| 48 | console.log('\n--- /market with no discovered TollGates ---'); | ||
| 49 | { | ||
| 50 | const data = runJson(`curl -s --connect-timeout 5 ${API_URL}/market`); | ||
| 51 | if (data && data.count === 0) { | ||
| 52 | assert(data.entries.length === 0, 'empty entries array when count=0'); | ||
| 53 | console.log(' INFO: no nearby TollGates discovered yet (expected if only one board)'); | ||
| 54 | } else if (data && data.count > 0) { | ||
| 55 | console.log(` INFO: ${data.count} nearby TollGate(s) discovered`); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); | ||
| 60 | process.exit(failed > 0 ? 1 : 0); | ||
diff --git a/tests/integration/test-price-discovery.mjs b/tests/integration/test-price-discovery.mjs new file mode 100644 index 0000000..6762130 --- /dev/null +++ b/tests/integration/test-price-discovery.mjs | |||
| @@ -0,0 +1,138 @@ | |||
| 1 | import { execSync } from 'child_process'; | ||
| 2 | |||
| 3 | const BOARD_A_IP = process.env.TOLLGATE_IP || '10.185.47.1'; | ||
| 4 | const BOARD_B_IP = process.env.TOLLGATE_B_IP || process.env.TOLLGATE_IP_B || '10.192.45.1'; | ||
| 5 | const API_A = `http://${BOARD_A_IP}:2121`; | ||
| 6 | const API_B = `http://${BOARD_B_IP}:2121`; | ||
| 7 | |||
| 8 | function run(cmd) { | ||
| 9 | try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); } | ||
| 10 | catch (e) { return e.stdout || null; } | ||
| 11 | } | ||
| 12 | |||
| 13 | function runJson(cmd) { | ||
| 14 | const out = run(cmd); | ||
| 15 | try { return out ? JSON.parse(out) : null; } | ||
| 16 | catch { return null; } | ||
| 17 | } | ||
| 18 | |||
| 19 | let passed = 0, failed = 0; | ||
| 20 | function assert(cond, msg) { | ||
| 21 | if (cond) { console.log(` PASS: ${msg}`); passed++; } | ||
| 22 | else { console.log(` FAIL: ${msg}`); failed++; } | ||
| 23 | } | ||
| 24 | |||
| 25 | function canReach(url) { | ||
| 26 | const result = run(`curl -s --connect-timeout 3 --max-time 5 -o /dev/null -w "%{http_code}" ${url}`); | ||
| 27 | return result && result.trim() !== '000' && result.trim() !== ''; | ||
| 28 | } | ||
| 29 | |||
| 30 | console.log('=== test-price-discovery (two-board) ===\n'); | ||
| 31 | |||
| 32 | const reachA = canReach(`${API_A}/market`); | ||
| 33 | const reachB = canReach(`${API_B}/market`); | ||
| 34 | |||
| 35 | console.log(`Reachability: Board A=${reachA ? 'YES' : 'NO'}, Board B=${reachB ? 'YES' : 'NO'}\n`); | ||
| 36 | |||
| 37 | if (!reachA && !reachB) { | ||
| 38 | console.log('FATAL: Neither board reachable. Check TOLLGATE_IP and TOLLGATE_B_IP'); | ||
| 39 | process.exit(1); | ||
| 40 | } | ||
| 41 | |||
| 42 | console.log('--- Board A: market endpoint ---'); | ||
| 43 | { | ||
| 44 | if (reachA) { | ||
| 45 | const data = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_A}/market`); | ||
| 46 | assert(data !== null, 'Board A /market returns JSON'); | ||
| 47 | assert(typeof data?.count === 'number', `Board A count is ${data?.count}`); | ||
| 48 | if (data && data.entries) { | ||
| 49 | console.log(` Board A sees ${data.count} nearby TollGate(s):`); | ||
| 50 | for (const e of data.entries) { | ||
| 51 | console.log(` ${e.ssid} (BSSID: ${e.bssid}) — ${e.price_per_step} sats/step, RSSI: ${e.rssi}`); | ||
| 52 | } | ||
| 53 | } | ||
| 54 | } else { | ||
| 55 | console.log(' SKIP: Board A not reachable'); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | console.log('\n--- Board B: market endpoint ---'); | ||
| 60 | { | ||
| 61 | if (reachB) { | ||
| 62 | const data = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_B}/market`); | ||
| 63 | assert(data !== null, 'Board B /market returns JSON'); | ||
| 64 | assert(typeof data?.count === 'number', `Board B count is ${data?.count}`); | ||
| 65 | if (data && data.entries) { | ||
| 66 | console.log(` Board B sees ${data.count} nearby TollGate(s):`); | ||
| 67 | for (const e of data.entries) { | ||
| 68 | console.log(` ${e.ssid} (BSSID: ${e.bssid}) — ${e.price_per_step} sats/step, RSSI: ${e.rssi}`); | ||
| 69 | } | ||
| 70 | } | ||
| 71 | } else { | ||
| 72 | console.log(' SKIP: Board B not reachable'); | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 76 | console.log('\n--- Cross-discovery: Board A sees Board B ---'); | ||
| 77 | { | ||
| 78 | if (reachA) { | ||
| 79 | const mktA = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_A}/market`); | ||
| 80 | if (mktA && mktA.count > 0) { | ||
| 81 | const foundB = mktA.entries.some(e => | ||
| 82 | e.ssid.startsWith('TollGate-') && e.bssid !== '' && e.price_per_step > 0 | ||
| 83 | ); | ||
| 84 | assert(foundB, `Board A discovered another TollGate (count=${mktA.count})`); | ||
| 85 | } else { | ||
| 86 | console.log(' INFO: Board A has 0 entries. Scan may need more time.'); | ||
| 87 | } | ||
| 88 | } else { | ||
| 89 | console.log(' SKIP: Board A not reachable'); | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | console.log('\n--- Cross-discovery: Board B sees Board A ---'); | ||
| 94 | { | ||
| 95 | if (reachB) { | ||
| 96 | const mktB = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_B}/market`); | ||
| 97 | if (mktB && mktB.count > 0) { | ||
| 98 | const foundA = mktB.entries.some(e => | ||
| 99 | e.ssid.startsWith('TollGate-') && e.bssid !== '' && e.price_per_step > 0 | ||
| 100 | ); | ||
| 101 | assert(foundA, `Board B discovered another TollGate (count=${mktB.count})`); | ||
| 102 | } else { | ||
| 103 | console.log(' INFO: Board B has 0 entries. Scan may need more time.'); | ||
| 104 | } | ||
| 105 | } else { | ||
| 106 | console.log(' SKIP: Board B not reachable'); | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | console.log('\n--- Discovery data integrity ---'); | ||
| 111 | { | ||
| 112 | const boards = []; | ||
| 113 | if (reachA) { | ||
| 114 | const mktA = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_A}/market`); | ||
| 115 | if (mktA?.entries) boards.push({ name: 'A', data: mktA }); | ||
| 116 | } | ||
| 117 | if (reachB) { | ||
| 118 | const mktB = runJson(`curl -s --connect-timeout 5 --max-time 10 ${API_B}/market`); | ||
| 119 | if (mktB?.entries) boards.push({ name: 'B', data: mktB }); | ||
| 120 | } | ||
| 121 | |||
| 122 | for (const { name, data } of boards) { | ||
| 123 | for (const e of data.entries) { | ||
| 124 | assert(typeof e.price_per_step === 'number' && e.price_per_step > 0, | ||
| 125 | `Board ${name} entry has valid price (${e.price_per_step})`); | ||
| 126 | assert(typeof e.step_size === 'number' && e.step_size > 0, | ||
| 127 | `Board ${name} entry has valid step_size (${e.step_size})`); | ||
| 128 | assert(typeof e.metric === 'string' && e.metric.length > 0, | ||
| 129 | `Board ${name} entry has valid metric (${e.metric})`); | ||
| 130 | assert(typeof e.rssi === 'number', | ||
| 131 | `Board ${name} entry has valid RSSI (${e.rssi})`); | ||
| 132 | break; | ||
| 133 | } | ||
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 137 | console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); | ||
| 138 | process.exit(failed > 0 ? 1 : 0); | ||
diff --git a/tests/unit/Makefile b/tests/unit/Makefile index 6d13e4d..7bd3f1e 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 test_cvm_server test_display test_negentropy_adapter | 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 test_display test_negentropy_adapter test_beacon_price test_market |
| 26 | 26 | ||
| 27 | .PHONY: all test clean $(TESTS) | 27 | .PHONY: all test clean $(TESTS) |
| 28 | 28 | ||
| @@ -63,8 +63,8 @@ test_cashu: test_cashu.c $(REPO_ROOT)/main/cashu.c | |||
| 63 | test_session: test_session.c $(REPO_ROOT)/main/session.c $(REPO_ROOT)/main/cashu.c | 63 | test_session: test_session.c $(REPO_ROOT)/main/session.c $(REPO_ROOT)/main/cashu.c |
| 64 | $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/session.c $(REPO_ROOT)/main/cashu.c -o $@ $(LDFLAGS) | 64 | $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/session.c $(REPO_ROOT)/main/cashu.c -o $@ $(LDFLAGS) |
| 65 | 65 | ||
| 66 | test_tollgate_client: test_tollgate_client.c | 66 | test_tollgate_client: test_tollgate_client.c $(REPO_ROOT)/main/market.c $(REPO_ROOT)/main/beacon_price.c |
| 67 | $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) | 67 | $(CC) $(CFLAGS) -I $(REPO_ROOT)/main $< $(REPO_ROOT)/main/market.c $(REPO_ROOT)/main/beacon_price.c -o $@ $(LDFLAGS) |
| 68 | 68 | ||
| 69 | test_lnurl_pay: test_lnurl_pay.c | 69 | test_lnurl_pay: test_lnurl_pay.c |
| 70 | $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) | 70 | $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) |
| @@ -87,5 +87,11 @@ test_display: test_display.c | |||
| 87 | test_negentropy_adapter: test_negentropy_adapter.c | 87 | test_negentropy_adapter: test_negentropy_adapter.c |
| 88 | $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) | 88 | $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) |
| 89 | 89 | ||
| 90 | test_beacon_price: test_beacon_price.c $(REPO_ROOT)/main/beacon_price.c | ||
| 91 | $(CC) $(CFLAGS) -I $(REPO_ROOT)/main $< $(REPO_ROOT)/main/beacon_price.c -o $@ $(LDFLAGS) | ||
| 92 | |||
| 93 | test_market: test_market.c $(REPO_ROOT)/main/market.c $(REPO_ROOT)/main/beacon_price.c | ||
| 94 | $(CC) $(CFLAGS) -I $(REPO_ROOT)/main $< $(REPO_ROOT)/main/market.c $(REPO_ROOT)/main/beacon_price.c -o $@ $(LDFLAGS) | ||
| 95 | |||
| 90 | clean: | 96 | clean: |
| 91 | rm -f $(TESTS) $(SECP256K1_OBJ) | 97 | rm -f $(TESTS) $(SECP256K1_OBJ) |
diff --git a/tests/unit/stubs/esp_wifi.h b/tests/unit/stubs/esp_wifi.h index 6aa5787..5eb14bf 100644 --- a/tests/unit/stubs/esp_wifi.h +++ b/tests/unit/stubs/esp_wifi.h | |||
| @@ -2,6 +2,7 @@ | |||
| 2 | #define STUBS_ESP_WIFI_H | 2 | #define STUBS_ESP_WIFI_H |
| 3 | 3 | ||
| 4 | #include <stdint.h> | 4 | #include <stdint.h> |
| 5 | #include <stdbool.h> | ||
| 5 | #include <string.h> | 6 | #include <string.h> |
| 6 | #include "esp_err.h" | 7 | #include "esp_err.h" |
| 7 | 8 | ||
| @@ -37,4 +38,67 @@ static inline esp_err_t esp_wifi_set_config(int ifx, const wifi_config_t *cfg) { | |||
| 37 | static inline esp_err_t esp_wifi_set_mode(uint8_t mode) { (void)mode; return ESP_OK; } | 38 | static inline esp_err_t esp_wifi_set_mode(uint8_t mode) { (void)mode; return ESP_OK; } |
| 38 | static inline esp_err_t esp_wifi_start(void) { return ESP_OK; } | 39 | static inline esp_err_t esp_wifi_start(void) { return ESP_OK; } |
| 39 | 40 | ||
| 41 | #define WIFI_VENDOR_IE_ELEMENT_ID 0xDD | ||
| 42 | |||
| 43 | typedef enum { | ||
| 44 | WIFI_VND_IE_TYPE_BEACON, | ||
| 45 | WIFI_VND_IE_TYPE_PROBE_REQ, | ||
| 46 | WIFI_VND_IE_TYPE_PROBE_RESP, | ||
| 47 | WIFI_VND_IE_TYPE_ASSOC_REQ, | ||
| 48 | WIFI_VND_IE_TYPE_ASSOC_RESP, | ||
| 49 | } wifi_vendor_ie_type_t; | ||
| 50 | |||
| 51 | typedef enum { | ||
| 52 | WIFI_VND_IE_ID_0, | ||
| 53 | WIFI_VND_IE_ID_1, | ||
| 54 | } wifi_vendor_ie_id_t; | ||
| 55 | |||
| 56 | typedef struct { | ||
| 57 | uint8_t element_id; | ||
| 58 | uint8_t length; | ||
| 59 | uint8_t vendor_oui[3]; | ||
| 60 | uint8_t vendor_oui_type; | ||
| 61 | uint8_t payload[0]; | ||
| 62 | } vendor_ie_data_t; | ||
| 63 | |||
| 64 | typedef void (*esp_vendor_ie_cb_t)(void *ctx, wifi_vendor_ie_type_t type, const uint8_t sa[6], const vendor_ie_data_t *vnd_ie, int rssi); | ||
| 65 | |||
| 66 | static inline esp_err_t esp_wifi_set_vendor_ie(bool enable, wifi_vendor_ie_type_t type, wifi_vendor_ie_id_t idx, const void *vnd_ie) { (void)enable; (void)type; (void)idx; (void)vnd_ie; return ESP_OK; } | ||
| 67 | static inline esp_err_t esp_wifi_set_vendor_ie_cb(esp_vendor_ie_cb_t cb, void *ctx) { (void)cb; (void)ctx; return ESP_OK; } | ||
| 68 | |||
| 69 | #define WIFI_SCAN_TYPE_PASSIVE 0 | ||
| 70 | |||
| 71 | typedef struct { | ||
| 72 | uint8_t bssid[6]; | ||
| 73 | uint8_t ssid[33]; | ||
| 74 | uint8_t primary; | ||
| 75 | int second; | ||
| 76 | int8_t rssi; | ||
| 77 | int authmode; | ||
| 78 | } wifi_ap_record_t; | ||
| 79 | |||
| 80 | typedef struct { | ||
| 81 | uint8_t *ssid; | ||
| 82 | uint8_t *bssid; | ||
| 83 | uint8_t channel; | ||
| 84 | bool show_hidden; | ||
| 85 | int scan_type; | ||
| 86 | union { | ||
| 87 | struct { int min; int max; } active; | ||
| 88 | int passive; | ||
| 89 | } scan_time; | ||
| 90 | } wifi_scan_config_t; | ||
| 91 | |||
| 92 | static inline esp_err_t esp_wifi_scan_start(const wifi_scan_config_t *cfg, bool block) { (void)cfg; (void)block; return ESP_OK; } | ||
| 93 | static inline esp_err_t esp_wifi_scan_get_ap_num(uint16_t *n) { *n = 0; return ESP_OK; } | ||
| 94 | static inline esp_err_t esp_wifi_scan_get_ap_records(uint16_t *n, wifi_ap_record_t *records) { (void)records; *n = 0; return ESP_OK; } | ||
| 95 | |||
| 96 | #define WIFI_EVENT_SCAN_DONE 3 | ||
| 97 | |||
| 98 | typedef void *esp_event_handler_instance_t; | ||
| 99 | typedef const char *esp_event_base_t; | ||
| 100 | #define WIFI_EVENT "WIFI_EVENT" | ||
| 101 | |||
| 102 | static inline esp_err_t esp_event_handler_instance_register(esp_event_base_t a, int32_t b, void *c, void *d, esp_event_handler_instance_t *e) { (void)a; (void)b; (void)c; (void)d; (void)e; return ESP_OK; } | ||
| 103 | |||
| 40 | #endif | 104 | #endif |
diff --git a/tests/unit/test_beacon_price.c b/tests/unit/test_beacon_price.c new file mode 100644 index 0000000..9574478 --- /dev/null +++ b/tests/unit/test_beacon_price.c | |||
| @@ -0,0 +1,132 @@ | |||
| 1 | #include "test_framework.h" | ||
| 2 | #include "../../main/config.h" | ||
| 3 | #include "../../main/identity.h" | ||
| 4 | #include <string.h> | ||
| 5 | #include <stdio.h> | ||
| 6 | #include <mbedtls/sha256.h> | ||
| 7 | |||
| 8 | #include "../../main/beacon_price.h" | ||
| 9 | |||
| 10 | static tollgate_config_t g_test_config; | ||
| 11 | static tollgate_identity_t g_test_identity; | ||
| 12 | |||
| 13 | const tollgate_config_t *tollgate_config_get(void) { return &g_test_config; } | ||
| 14 | const tollgate_identity_t *identity_get(void) { return &g_test_identity; } | ||
| 15 | |||
| 16 | int main(void) | ||
| 17 | { | ||
| 18 | printf("=== test_beacon_price ===\n"); | ||
| 19 | |||
| 20 | memset(&g_test_config, 0, sizeof(g_test_config)); | ||
| 21 | strncpy(g_test_config.mint_url, "https://testnut.cashu.space", sizeof(g_test_config.mint_url) - 1); | ||
| 22 | strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1); | ||
| 23 | g_test_config.price_per_step = 21; | ||
| 24 | g_test_config.step_size_ms = 60000; | ||
| 25 | strncpy(g_test_config.nostr_geohash, "u281w0dfz", sizeof(g_test_config.nostr_geohash) - 1); | ||
| 26 | |||
| 27 | memset(&g_test_identity, 0, sizeof(g_test_identity)); | ||
| 28 | strncpy(g_test_identity.npub_hex, "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", 64); | ||
| 29 | g_test_identity.initialized = true; | ||
| 30 | |||
| 31 | printf("\n--- tollgate_price_payload_t size ---\n"); | ||
| 32 | { | ||
| 33 | ASSERT_EQ_INT(26, (int)TOLLGATE_IE_PAYLOAD_SIZE, "payload is 26 bytes"); | ||
| 34 | ASSERT_EQ_INT(32, (int)TOLLGATE_IE_TOTAL_SIZE, "total IE is 32 bytes"); | ||
| 35 | } | ||
| 36 | |||
| 37 | printf("\n--- beacon_price_hash_mint ---\n"); | ||
| 38 | { | ||
| 39 | uint8_t hash[4]; | ||
| 40 | beacon_price_hash_mint("https://testnut.cashu.space", hash); | ||
| 41 | |||
| 42 | uint8_t expected[32]; | ||
| 43 | mbedtls_sha256((const unsigned char *)"https://testnut.cashu.space", | ||
| 44 | strlen("https://testnut.cashu.space"), expected, 0); | ||
| 45 | ASSERT_MEM_EQ(expected, hash, 4, "mint_hash matches SHA-256 prefix"); | ||
| 46 | |||
| 47 | uint8_t hash2[4]; | ||
| 48 | beacon_price_hash_mint("https://other.mint.url", hash2); | ||
| 49 | ASSERT(memcmp(hash, hash2, 4) != 0, "different mint URLs produce different hashes"); | ||
| 50 | } | ||
| 51 | |||
| 52 | printf("\n--- beacon_price_hash_npub ---\n"); | ||
| 53 | { | ||
| 54 | uint8_t hash[4]; | ||
| 55 | beacon_price_hash_npub("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", hash); | ||
| 56 | |||
| 57 | uint8_t expected[32]; | ||
| 58 | mbedtls_sha256((const unsigned char *)"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", | ||
| 59 | 64, expected, 0); | ||
| 60 | ASSERT_MEM_EQ(expected, hash, 4, "npub_hash matches SHA-256 prefix"); | ||
| 61 | } | ||
| 62 | |||
| 63 | printf("\n--- beacon_price_build_ie (time metric) ---\n"); | ||
| 64 | { | ||
| 65 | tollgate_price_ie_t ie; | ||
| 66 | beacon_price_build_ie(&ie); | ||
| 67 | |||
| 68 | ASSERT_EQ_INT(0xDD, ie.element_id, "element_id is 0xDD"); | ||
| 69 | ASSERT_EQ_INT(4 + 26, ie.length, "length is 30 (4 header + 26 payload)"); | ||
| 70 | ASSERT_EQ_INT(0xC0, ie.vendor_oui[0], "OUI byte 0"); | ||
| 71 | ASSERT_EQ_INT(0xFF, ie.vendor_oui[1], "OUI byte 1"); | ||
| 72 | ASSERT_EQ_INT(0xEE, ie.vendor_oui[2], "OUI byte 2"); | ||
| 73 | ASSERT_EQ_INT(0x01, ie.vendor_oui_type, "OUI type is 0x01"); | ||
| 74 | |||
| 75 | ASSERT_EQ_INT(1, ie.payload.version, "version is 1"); | ||
| 76 | ASSERT_EQ_INT(0, ie.payload.metric, "metric is 0 (milliseconds)"); | ||
| 77 | ASSERT_EQ_INT(21, ie.payload.price_per_step, "price is 21"); | ||
| 78 | ASSERT_EQ_INT(60000, (int)ie.payload.step_size, "step_size is 60000"); | ||
| 79 | |||
| 80 | uint8_t expected_mint_hash[4]; | ||
| 81 | beacon_price_hash_mint("https://testnut.cashu.space", expected_mint_hash); | ||
| 82 | ASSERT_MEM_EQ(expected_mint_hash, ie.payload.mint_hash, 4, "mint_hash matches"); | ||
| 83 | |||
| 84 | ASSERT_EQ_INT(9, ie.payload.geohash_len, "geohash_len is 9"); | ||
| 85 | ASSERT(memcmp(ie.payload.geohash, "u281w0dfz", 9) == 0, "geohash matches"); | ||
| 86 | } | ||
| 87 | |||
| 88 | printf("\n--- beacon_price_build_ie (bytes metric) ---\n"); | ||
| 89 | { | ||
| 90 | strncpy(g_test_config.metric, "bytes", sizeof(g_test_config.metric) - 1); | ||
| 91 | g_test_config.step_size_bytes = 22020096; | ||
| 92 | g_test_config.price_per_step = 5; | ||
| 93 | |||
| 94 | tollgate_price_ie_t ie; | ||
| 95 | beacon_price_build_ie(&ie); | ||
| 96 | |||
| 97 | ASSERT_EQ_INT(1, ie.payload.metric, "metric is 1 (bytes)"); | ||
| 98 | ASSERT_EQ_INT(5, ie.payload.price_per_step, "price is 5"); | ||
| 99 | ASSERT_EQ_INT(22020096, (int)ie.payload.step_size, "step_size is 22020096 bytes"); | ||
| 100 | |||
| 101 | strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1); | ||
| 102 | g_test_config.step_size_ms = 60000; | ||
| 103 | g_test_config.price_per_step = 21; | ||
| 104 | } | ||
| 105 | |||
| 106 | printf("\n--- roundtrip: build → parse ---\n"); | ||
| 107 | { | ||
| 108 | tollgate_price_ie_t ie; | ||
| 109 | beacon_price_build_ie(&ie); | ||
| 110 | |||
| 111 | vendor_ie_data_t *vnd_ie = (vendor_ie_data_t *)&ie; | ||
| 112 | |||
| 113 | ASSERT(vnd_ie->length >= 4 + (int)TOLLGATE_IE_PAYLOAD_SIZE, "vendor IE length sufficient"); | ||
| 114 | |||
| 115 | const tollgate_price_payload_t *parsed = (const tollgate_price_payload_t *)vnd_ie->payload; | ||
| 116 | ASSERT_EQ_INT(1, parsed->version, "parsed version"); | ||
| 117 | ASSERT_EQ_INT(0, parsed->metric, "parsed metric"); | ||
| 118 | ASSERT_EQ_INT(21, parsed->price_per_step, "parsed price"); | ||
| 119 | ASSERT_EQ_INT(60000, (int)parsed->step_size, "parsed step_size"); | ||
| 120 | ASSERT_EQ_INT(9, parsed->geohash_len, "parsed geohash_len"); | ||
| 121 | } | ||
| 122 | |||
| 123 | printf("\n--- struct packing check ---\n"); | ||
| 124 | { | ||
| 125 | tollgate_price_ie_t ie; | ||
| 126 | memset(&ie, 0, sizeof(ie)); | ||
| 127 | int expected_size = 2 + 3 + 1 + 26; | ||
| 128 | ASSERT_EQ_INT(expected_size, (int)sizeof(tollgate_price_ie_t), "no padding in struct"); | ||
| 129 | } | ||
| 130 | |||
| 131 | TEST_SUMMARY(); | ||
| 132 | } | ||
diff --git a/tests/unit/test_market.c b/tests/unit/test_market.c new file mode 100644 index 0000000..c19d26e --- /dev/null +++ b/tests/unit/test_market.c | |||
| @@ -0,0 +1,177 @@ | |||
| 1 | #include "test_framework.h" | ||
| 2 | #include "../../main/beacon_price.h" | ||
| 3 | #include "../../main/market.h" | ||
| 4 | #include "../../main/config.h" | ||
| 5 | #include "../../main/identity.h" | ||
| 6 | #include <string.h> | ||
| 7 | #include <stdio.h> | ||
| 8 | #include <stdlib.h> | ||
| 9 | |||
| 10 | static tollgate_config_t g_test_config; | ||
| 11 | static tollgate_identity_t g_test_identity; | ||
| 12 | |||
| 13 | const tollgate_config_t *tollgate_config_get(void) { return &g_test_config; } | ||
| 14 | const tollgate_identity_t *identity_get(void) { return &g_test_identity; } | ||
| 15 | |||
| 16 | static void build_test_ie(tollgate_price_ie_t *ie, uint16_t price, uint32_t step, uint8_t metric, | ||
| 17 | const char *geohash, const char *mint_url, const char *npub_hex) | ||
| 18 | { | ||
| 19 | memset(ie, 0, sizeof(*ie)); | ||
| 20 | ie->element_id = 0xDD; | ||
| 21 | ie->length = 4 + TOLLGATE_IE_PAYLOAD_SIZE; | ||
| 22 | ie->vendor_oui[0] = TOLLGATE_OUI_0; | ||
| 23 | ie->vendor_oui[1] = TOLLGATE_OUI_1; | ||
| 24 | ie->vendor_oui[2] = TOLLGATE_OUI_2; | ||
| 25 | ie->vendor_oui_type = TOLLGATE_IE_TYPE; | ||
| 26 | |||
| 27 | ie->payload.version = TOLLGATE_IE_VERSION; | ||
| 28 | ie->payload.metric = metric; | ||
| 29 | ie->payload.price_per_step = price; | ||
| 30 | ie->payload.step_size = step; | ||
| 31 | |||
| 32 | if (mint_url) beacon_price_hash_mint(mint_url, ie->payload.mint_hash); | ||
| 33 | if (npub_hex) beacon_price_hash_npub(npub_hex, ie->payload.npub_hash); | ||
| 34 | |||
| 35 | uint8_t gh_len = (uint8_t)strnlen(geohash, TOLLGATE_IE_GEOHASH_MAX); | ||
| 36 | ie->payload.geohash_len = gh_len; | ||
| 37 | memcpy(ie->payload.geohash, geohash, gh_len); | ||
| 38 | } | ||
| 39 | |||
| 40 | static void reset_market(void) | ||
| 41 | { | ||
| 42 | market_t *m = (market_t *)market_get(); | ||
| 43 | memset(m, 0, sizeof(*m)); | ||
| 44 | } | ||
| 45 | |||
| 46 | int main(void) | ||
| 47 | { | ||
| 48 | printf("=== test_market ===\n"); | ||
| 49 | |||
| 50 | memset(&g_test_config, 0, sizeof(g_test_config)); | ||
| 51 | g_test_config.market_enabled = true; | ||
| 52 | g_test_config.market_scan_interval_s = 30; | ||
| 53 | strncpy(g_test_config.metric, "milliseconds", sizeof(g_test_config.metric) - 1); | ||
| 54 | |||
| 55 | memset(&g_test_identity, 0, sizeof(g_test_identity)); | ||
| 56 | strncpy(g_test_identity.npub_hex, "0000000000000000000000000000000000000000000000000000000000000001", 64); | ||
| 57 | g_test_identity.initialized = true; | ||
| 58 | |||
| 59 | printf("\n--- parse vendor IE (valid) ---\n"); | ||
| 60 | { | ||
| 61 | reset_market(); | ||
| 62 | tollgate_price_ie_t ie; | ||
| 63 | build_test_ie(&ie, 21, 60000, 0, "u281w0dfz", | ||
| 64 | "https://testnut.cashu.space", | ||
| 65 | "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"); | ||
| 66 | |||
| 67 | uint8_t bssid[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01}; | ||
| 68 | market_parse_vendor_ie(bssid, (vendor_ie_data_t *)&ie, -45); | ||
| 69 | |||
| 70 | const market_t *m = market_get(); | ||
| 71 | ASSERT_EQ_INT(1, m->count, "one entry added"); | ||
| 72 | ASSERT(m->entries[0].valid, "entry is valid"); | ||
| 73 | ASSERT_EQ_INT(21, m->entries[0].price_per_step, "price is 21"); | ||
| 74 | ASSERT_EQ_INT(60000, (int)m->entries[0].step_size, "step_size is 60000"); | ||
| 75 | ASSERT_EQ_INT(0, m->entries[0].metric, "metric is 0 (time)"); | ||
| 76 | ASSERT_EQ_INT(-45, m->entries[0].rssi, "rssi is -45"); | ||
| 77 | ASSERT(memcmp(m->entries[0].bssid, bssid, 6) == 0, "bssid matches"); | ||
| 78 | } | ||
| 79 | |||
| 80 | printf("\n--- parse vendor IE (ignore self) ---\n"); | ||
| 81 | { | ||
| 82 | reset_market(); | ||
| 83 | tollgate_price_ie_t ie; | ||
| 84 | build_test_ie(&ie, 21, 60000, 0, "u281w0dfz", | ||
| 85 | "https://testnut.cashu.space", | ||
| 86 | g_test_identity.npub_hex); | ||
| 87 | |||
| 88 | uint8_t bssid[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x02}; | ||
| 89 | market_parse_vendor_ie(bssid, (vendor_ie_data_t *)&ie, -50); | ||
| 90 | |||
| 91 | const market_t *m = market_get(); | ||
| 92 | ASSERT_EQ_INT(0, m->count, "self-entry ignored"); | ||
| 93 | } | ||
| 94 | |||
| 95 | printf("\n--- parse vendor IE (wrong OUI) ---\n"); | ||
| 96 | { | ||
| 97 | reset_market(); | ||
| 98 | tollgate_price_ie_t ie; | ||
| 99 | build_test_ie(&ie, 21, 60000, 0, "u281w0dfz", | ||
| 100 | "https://testnut.cashu.space", | ||
| 101 | "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"); | ||
| 102 | ie.vendor_oui[0] = 0x00; | ||
| 103 | |||
| 104 | uint8_t bssid[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x03}; | ||
| 105 | market_parse_vendor_ie(bssid, (vendor_ie_data_t *)&ie, -40); | ||
| 106 | |||
| 107 | const market_t *m = market_get(); | ||
| 108 | ASSERT_EQ_INT(0, m->count, "wrong OUI rejected"); | ||
| 109 | } | ||
| 110 | |||
| 111 | printf("\n--- market_find_cheapest ---\n"); | ||
| 112 | { | ||
| 113 | reset_market(); | ||
| 114 | |||
| 115 | tollgate_price_ie_t ie1, ie2, ie3; | ||
| 116 | build_test_ie(&ie1, 21, 60000, 0, "u281w0dfz", | ||
| 117 | "https://testnut.cashu.space", "aaa...npub1"); | ||
| 118 | build_test_ie(&ie2, 10, 60000, 0, "u281w0dfz", | ||
| 119 | "https://testnut.cashu.space", "bbb...npub2"); | ||
| 120 | build_test_ie(&ie3, 50, 60000, 0, "u281w0dfz", | ||
| 121 | "https://testnut.cashu.space", "ccc...npub3"); | ||
| 122 | |||
| 123 | uint8_t bssid1[6] = {0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; | ||
| 124 | uint8_t bssid2[6] = {0x02, 0x02, 0x02, 0x02, 0x02, 0x02}; | ||
| 125 | uint8_t bssid3[6] = {0x03, 0x03, 0x03, 0x03, 0x03, 0x03}; | ||
| 126 | |||
| 127 | market_parse_vendor_ie(bssid1, (vendor_ie_data_t *)&ie1, -45); | ||
| 128 | market_parse_vendor_ie(bssid2, (vendor_ie_data_t *)&ie2, -50); | ||
| 129 | market_parse_vendor_ie(bssid3, (vendor_ie_data_t *)&ie3, -55); | ||
| 130 | |||
| 131 | const market_t *m = market_get(); | ||
| 132 | ASSERT_EQ_INT(3, m->count, "three entries"); | ||
| 133 | |||
| 134 | strncpy((char *)m->entries[0].ssid, "TollGate-A", 32); | ||
| 135 | strncpy((char *)m->entries[1].ssid, "TollGate-B", 32); | ||
| 136 | strncpy((char *)m->entries[2].ssid, "TollGate-C", 32); | ||
| 137 | |||
| 138 | int cheapest = market_find_cheapest(); | ||
| 139 | ASSERT(cheapest >= 0, "found a cheapest entry"); | ||
| 140 | ASSERT_EQ_INT(10, m->entries[cheapest].price_per_step, "cheapest is 10 sats"); | ||
| 141 | } | ||
| 142 | |||
| 143 | printf("\n--- update existing entry ---\n"); | ||
| 144 | { | ||
| 145 | reset_market(); | ||
| 146 | tollgate_price_ie_t ie; | ||
| 147 | build_test_ie(&ie, 21, 60000, 0, "u281w0dfz", | ||
| 148 | "https://testnut.cashu.space", "npub1"); | ||
| 149 | uint8_t bssid[6] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; | ||
| 150 | |||
| 151 | market_parse_vendor_ie(bssid, (vendor_ie_data_t *)&ie, -45); | ||
| 152 | ASSERT_EQ_INT(1, market_get()->count, "first add"); | ||
| 153 | |||
| 154 | build_test_ie(&ie, 15, 60000, 0, "u281w0dfz", | ||
| 155 | "https://testnut.cashu.space", "npub1"); | ||
| 156 | market_parse_vendor_ie(bssid, (vendor_ie_data_t *)&ie, -47); | ||
| 157 | ASSERT_EQ_INT(1, market_get()->count, "update doesn't increase count"); | ||
| 158 | ASSERT_EQ_INT(15, market_get()->entries[0].price_per_step, "price updated to 15"); | ||
| 159 | } | ||
| 160 | |||
| 161 | printf("\n--- geohash preserved ---\n"); | ||
| 162 | { | ||
| 163 | reset_market(); | ||
| 164 | tollgate_price_ie_t ie; | ||
| 165 | build_test_ie(&ie, 21, 60000, 0, "u281w0dfz", | ||
| 166 | "https://testnut.cashu.space", "npub1"); | ||
| 167 | uint8_t bssid[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; | ||
| 168 | |||
| 169 | market_parse_vendor_ie(bssid, (vendor_ie_data_t *)&ie, -40); | ||
| 170 | |||
| 171 | const market_t *m = market_get(); | ||
| 172 | ASSERT(m->entries[0].valid, "entry valid"); | ||
| 173 | ASSERT_EQ_STR("u281w0dfz", m->entries[0].geohash, "geohash is u281w0dfz"); | ||
| 174 | } | ||
| 175 | |||
| 176 | TEST_SUMMARY(); | ||
| 177 | } | ||
diff --git a/tests/unit/test_tollgate_client.c b/tests/unit/test_tollgate_client.c index 686ad19..eebc747 100644 --- a/tests/unit/test_tollgate_client.c +++ b/tests/unit/test_tollgate_client.c | |||
| @@ -13,6 +13,7 @@ const tollgate_config_t *tollgate_config_get(void) { | |||
| 13 | 13 | ||
| 14 | uint64_t nucula_wallet_balance(void) { return 100; } | 14 | uint64_t nucula_wallet_balance(void) { return 100; } |
| 15 | esp_err_t nucula_wallet_send(uint64_t a, char *b, size_t c) { (void)a; (void)b; (void)c; return ESP_OK; } | 15 | esp_err_t nucula_wallet_send(uint64_t a, char *b, size_t c) { (void)a; (void)b; (void)c; return ESP_OK; } |
| 16 | const void *identity_get(void) { return NULL; } | ||
| 16 | 17 | ||
| 17 | #include "freertos/FreeRTOS.h" | 18 | #include "freertos/FreeRTOS.h" |
| 18 | 19 | ||