From 347d29658959c7e4b368a15134c183f4ce7a25bc Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 17 May 2026 01:31:49 +0530 Subject: Testing infrastructure: AGENTS.md rules + unit test framework + geohash tests (11/11 pass) - Add AGENTS.md: full project context + mandatory testing rules for AI sessions - Add tests/unit/ with host-compiled C unit test infrastructure - Clean stubs approach: ESP-IDF type stubs in tests/unit/stubs/, no source modifications - Fix geohash.c bit extraction bug (3-byte span) found by unit tests - test_geohash: 11/11 passing with reference vectors (Munich, NYC, origin, boundaries) --- tests/unit/Makefile | 66 ++++++++++++++++++++++ tests/unit/stubs/dhcpserver/dhcpserver.h | 4 ++ tests/unit/stubs/esp_crt_bundle.h | 6 ++ tests/unit/stubs/esp_err.h | 18 ++++++ tests/unit/stubs/esp_event.h | 8 +++ tests/unit/stubs/esp_http_client.h | 12 ++++ tests/unit/stubs/esp_http_server.h | 10 ++++ tests/unit/stubs/esp_log.h | 10 ++++ tests/unit/stubs/esp_mac.h | 19 +++++++ tests/unit/stubs/esp_netif.h | 17 ++++++ tests/unit/stubs/esp_spiffs.h | 15 +++++ tests/unit/stubs/esp_system.h | 4 ++ tests/unit/stubs/esp_tls.h | 25 +++++++++ tests/unit/stubs/esp_wifi.h | 40 ++++++++++++++ tests/unit/stubs/freertos/FreeRTOS.h | 11 ++++ tests/unit/stubs/freertos/event_groups.h | 13 +++++ tests/unit/stubs/freertos/task.h | 19 +++++++ tests/unit/stubs/freertos/timers.h | 15 +++++ tests/unit/stubs/lwip/ip4_addr.h | 19 +++++++ tests/unit/stubs/lwip/napt.h | 6 ++ tests/unit/stubs/lwip/netdb.h | 4 ++ tests/unit/stubs/lwip/netif.h | 4 ++ tests/unit/stubs/lwip/sockets.h | 4 ++ tests/unit/stubs/nvs_flash.h | 12 ++++ tests/unit/test_cashu.c | 54 ++++++++++++++++++ tests/unit/test_framework.h | 60 ++++++++++++++++++++ tests/unit/test_geohash | Bin 0 -> 20744 bytes tests/unit/test_geohash.c | 40 ++++++++++++++ tests/unit/test_identity.c | 68 +++++++++++++++++++++++ tests/unit/test_nostr_event.c | 72 ++++++++++++++++++++++++ tests/unit/test_session.c | 92 +++++++++++++++++++++++++++++++ 31 files changed, 747 insertions(+) create mode 100644 tests/unit/Makefile create mode 100644 tests/unit/stubs/dhcpserver/dhcpserver.h create mode 100644 tests/unit/stubs/esp_crt_bundle.h create mode 100644 tests/unit/stubs/esp_err.h create mode 100644 tests/unit/stubs/esp_event.h create mode 100644 tests/unit/stubs/esp_http_client.h create mode 100644 tests/unit/stubs/esp_http_server.h create mode 100644 tests/unit/stubs/esp_log.h create mode 100644 tests/unit/stubs/esp_mac.h create mode 100644 tests/unit/stubs/esp_netif.h create mode 100644 tests/unit/stubs/esp_spiffs.h create mode 100644 tests/unit/stubs/esp_system.h create mode 100644 tests/unit/stubs/esp_tls.h create mode 100644 tests/unit/stubs/esp_wifi.h create mode 100644 tests/unit/stubs/freertos/FreeRTOS.h create mode 100644 tests/unit/stubs/freertos/event_groups.h create mode 100644 tests/unit/stubs/freertos/task.h create mode 100644 tests/unit/stubs/freertos/timers.h create mode 100644 tests/unit/stubs/lwip/ip4_addr.h create mode 100644 tests/unit/stubs/lwip/napt.h create mode 100644 tests/unit/stubs/lwip/netdb.h create mode 100644 tests/unit/stubs/lwip/netif.h create mode 100644 tests/unit/stubs/lwip/sockets.h create mode 100644 tests/unit/stubs/nvs_flash.h create mode 100644 tests/unit/test_cashu.c create mode 100644 tests/unit/test_framework.h create mode 100755 tests/unit/test_geohash create mode 100644 tests/unit/test_geohash.c create mode 100644 tests/unit/test_identity.c create mode 100644 tests/unit/test_nostr_event.c create mode 100644 tests/unit/test_session.c (limited to 'tests') diff --git a/tests/unit/Makefile b/tests/unit/Makefile new file mode 100644 index 0000000..4adc720 --- /dev/null +++ b/tests/unit/Makefile @@ -0,0 +1,66 @@ +REPO_ROOT := ../.. +SECP256K1_SRC := $(REPO_ROOT)/nucula_src/components/secp256k1/libsecp256k1 +SECP256K1_INC := $(SECP256K1_SRC)/include +SECP256K1_PRIV_INC := $(SECP256K1_SRC)/src +SECP256K1_CFG := $(REPO_ROOT)/nucula_src/components/secp256k1 +CJSON_SRC := $(REPO_ROOT)/../esp/esp-idf/components/json/cJSON + +CC := gcc +CFLAGS := -Wall -Wextra -Wno-unused-parameter -Wno-unused-function -Wno-sign-compare \ + -std=gnu17 -g -O0 \ + -DTEST_HOST \ + -DENABLE_MODULE_SCHNORRSIG=1 -DENABLE_MODULE_EXTRAKEYS=1 \ + -DECMULT_WINDOW_SIZE=8 -DECMULT_GEN_PREC_BITS=4 \ + -include stubs/esp_err.h \ + -I stubs \ + -I $(SECP256K1_INC) \ + -I $(SECP256K1_CFG) \ + -I /usr/include/cjson + +LDFLAGS := -lmbedcrypto -lcjson + +SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o + +TESTS := test_geohash test_identity test_nostr_event test_cashu test_session + +.PHONY: all test clean $(TESTS) + +all: test + +test: $(TESTS) + @echo "" + @echo "=== Running all unit tests ===" + @failed=0; \ + for t in $(TESTS); do \ + echo ""; \ + echo "--- $$t ---"; \ + ./$$t || failed=$$((failed + 1)); \ + done; \ + echo ""; \ + if [ $$failed -eq 0 ]; then \ + echo "=== ALL UNIT TESTS PASSED ==="; \ + else \ + echo "=== $$failed test(s) FAILED ==="; \ + exit 1; \ + fi + +$(SECP256K1_OBJ): %.o: $(SECP256K1_SRC)/src/%.c + $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) -c $< -o $@ + +test_geohash: test_geohash.c $(REPO_ROOT)/main/geohash.c + $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) + +test_identity: test_identity.c $(REPO_ROOT)/main/identity.c $(SECP256K1_OBJ) + $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) $< $(REPO_ROOT)/main/identity.c $(SECP256K1_OBJ) -o $@ $(LDFLAGS) + +test_nostr_event: test_nostr_event.c $(REPO_ROOT)/main/nostr_event.c $(SECP256K1_OBJ) + $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) $< $(REPO_ROOT)/main/nostr_event.c $(SECP256K1_OBJ) -o $@ $(LDFLAGS) + +test_cashu: test_cashu.c $(REPO_ROOT)/main/cashu.c + $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/cashu.c -o $@ $(LDFLAGS) + +test_session: test_session.c $(REPO_ROOT)/main/session.c + $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/session.c -o $@ $(LDFLAGS) + +clean: + rm -f $(TESTS) $(SECP256K1_OBJ) diff --git a/tests/unit/stubs/dhcpserver/dhcpserver.h b/tests/unit/stubs/dhcpserver/dhcpserver.h new file mode 100644 index 0000000..659f2c3 --- /dev/null +++ b/tests/unit/stubs/dhcpserver/dhcpserver.h @@ -0,0 +1,4 @@ +#ifndef STUBS_DHCPSERVER_DHCP_H +#define STUBS_DHCPSERVER_DHCP_H + +#endif diff --git a/tests/unit/stubs/esp_crt_bundle.h b/tests/unit/stubs/esp_crt_bundle.h new file mode 100644 index 0000000..dfb9bb1 --- /dev/null +++ b/tests/unit/stubs/esp_crt_bundle.h @@ -0,0 +1,6 @@ +#ifndef STUBS_ESP_CRT_BUNDLE_H +#define STUBS_ESP_CRT_BUNDLE_H + +static inline void *esp_crt_bundle_attach(void *conf) { (void)conf; return NULL; } + +#endif diff --git a/tests/unit/stubs/esp_err.h b/tests/unit/stubs/esp_err.h new file mode 100644 index 0000000..84c3734 --- /dev/null +++ b/tests/unit/stubs/esp_err.h @@ -0,0 +1,18 @@ +#ifndef STUBS_ESP_ERR_H +#define STUBS_ESP_ERR_H + +#include +#include +#include + +typedef int esp_err_t; + +#define ESP_OK 0 +#define ESP_FAIL -1 +#define ESP_ERR_INVALID_ARG 0x102 +#define ESP_ERR_NO_MEM 0x101 +#define ESP_ERR_NOT_FOUND 0x104 + +#define ESP_ERROR_CHECK(x) do { if ((x) != 0) { fprintf(stderr, "ESP_ERROR_CHECK failed: 0x%x\n", (int)(x)); abort(); } } while(0) + +#endif diff --git a/tests/unit/stubs/esp_event.h b/tests/unit/stubs/esp_event.h new file mode 100644 index 0000000..baea064 --- /dev/null +++ b/tests/unit/stubs/esp_event.h @@ -0,0 +1,8 @@ +#ifndef STUBS_ESP_EVENT_H +#define STUBS_ESP_EVENT_H + +#include "esp_err.h" + +static inline esp_err_t esp_event_loop_create_default(void) { return ESP_OK; } + +#endif diff --git a/tests/unit/stubs/esp_http_client.h b/tests/unit/stubs/esp_http_client.h new file mode 100644 index 0000000..4169714 --- /dev/null +++ b/tests/unit/stubs/esp_http_client.h @@ -0,0 +1,12 @@ +#ifndef STUBS_ESP_HTTP_CLIENT_H +#define STUBS_ESP_HTTP_CLIENT_H + +#include "esp_err.h" + +typedef void *esp_http_client_handle_t; + +typedef struct { + int cert_pem; +} esp_http_client_config_t; + +#endif diff --git a/tests/unit/stubs/esp_http_server.h b/tests/unit/stubs/esp_http_server.h new file mode 100644 index 0000000..22a5624 --- /dev/null +++ b/tests/unit/stubs/esp_http_server.h @@ -0,0 +1,10 @@ +#ifndef STUBS_ESP_HTTP_SERVER_H +#define STUBS_ESP_HTTP_SERVER_H + +#include "esp_err.h" +#include + +typedef void *httpd_handle_t; +typedef struct httpd_req httpd_req_t; + +#endif diff --git a/tests/unit/stubs/esp_log.h b/tests/unit/stubs/esp_log.h new file mode 100644 index 0000000..f353fe9 --- /dev/null +++ b/tests/unit/stubs/esp_log.h @@ -0,0 +1,10 @@ +#ifndef STUBS_ESP_LOG_H +#define STUBS_ESP_LOG_H + +#include + +#define ESP_LOGI(tag, fmt, ...) do { printf("I %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0) +#define ESP_LOGW(tag, fmt, ...) do { printf("W %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0) +#define ESP_LOGE(tag, fmt, ...) do { fprintf(stderr, "E %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0) + +#endif diff --git a/tests/unit/stubs/esp_mac.h b/tests/unit/stubs/esp_mac.h new file mode 100644 index 0000000..ddc80d4 --- /dev/null +++ b/tests/unit/stubs/esp_mac.h @@ -0,0 +1,19 @@ +#ifndef STUBS_ESP_MAC_H +#define STUBS_ESP_MAC_H + +#include +#include + +static inline int esp_read_mac(uint8_t *mac, int type) { + (void)type; + memset(mac, 0, 6); + mac[0] = 0x02; + mac[1] = 0x00; + mac[2] = 0x00; + mac[3] = 0x00; + mac[4] = 0xBE; + mac[5] = 0xEF; + return 0; +} + +#endif diff --git a/tests/unit/stubs/esp_netif.h b/tests/unit/stubs/esp_netif.h new file mode 100644 index 0000000..f009537 --- /dev/null +++ b/tests/unit/stubs/esp_netif.h @@ -0,0 +1,17 @@ +#ifndef STUBS_ESP_NETIF_H +#define STUBS_ESP_NETIF_H + +#include + +typedef struct { + uint32_t addr; +} esp_ip4_addr_t; + +#define IPSTR "%d.%d.%d.%d" +#define IP2STR(ip) ((ip)->addr & 0xff), (((ip)->addr >> 8) & 0xff), (((ip)->addr >> 16) & 0xff), (((ip)->addr >> 24) & 0xff) + +static inline void IP4_ADDR(esp_ip4_addr_t *ip, uint8_t a, uint8_t b, uint8_t c, uint8_t d) { + ip->addr = ((uint32_t)a) | ((uint32_t)b << 8) | ((uint32_t)c << 16) | ((uint32_t)d << 24); +} + +#endif diff --git a/tests/unit/stubs/esp_spiffs.h b/tests/unit/stubs/esp_spiffs.h new file mode 100644 index 0000000..ae6a127 --- /dev/null +++ b/tests/unit/stubs/esp_spiffs.h @@ -0,0 +1,15 @@ +#ifndef STUBS_ESP_SPIFFS_H +#define STUBS_ESP_SPIFFS_H + +#include "esp_err.h" + +typedef struct { + const char *base_path; + const char *partition_label; + int max_files; + bool format_if_mount_failed; +} esp_vfs_spiffs_conf_t; + +static inline esp_err_t esp_vfs_spiffs_register(const esp_vfs_spiffs_conf_t *conf) { (void)conf; return ESP_OK; } + +#endif diff --git a/tests/unit/stubs/esp_system.h b/tests/unit/stubs/esp_system.h new file mode 100644 index 0000000..8e63c80 --- /dev/null +++ b/tests/unit/stubs/esp_system.h @@ -0,0 +1,4 @@ +#ifndef STUBS_ESP_SYSTEM_H +#define STUBS_ESP_SYSTEM_H + +#endif diff --git a/tests/unit/stubs/esp_tls.h b/tests/unit/stubs/esp_tls.h new file mode 100644 index 0000000..7ded63a --- /dev/null +++ b/tests/unit/stubs/esp_tls.h @@ -0,0 +1,25 @@ +#ifndef STUBS_ESP_TLS_H +#define STUBS_ESP_TLS_H + +#include "esp_err.h" + +typedef struct esp_tls esp_tls_t; + +typedef struct { + void *crt_bundle_attach; + int use_global_ca_store; +} esp_tls_cfg_t; + +static inline esp_tls_t *esp_tls_init(void) { return (esp_tls_t*)1; } +static inline int esp_tls_conn_new_sync(const char *h, int hl, int port, const esp_tls_cfg_t *cfg, esp_tls_t *tls) { + (void)h; (void)hl; (void)port; (void)cfg; (void)tls; return -1; +} +static inline int esp_tls_conn_write(esp_tls_t *tls, const void *data, size_t len) { + (void)tls; (void)data; (void)len; return len; +} +static inline int esp_tls_conn_read(esp_tls_t *tls, void *data, size_t len) { + (void)tls; (void)data; (void)len; return 0; +} +static inline void esp_tls_conn_destroy(esp_tls_t *tls) { (void)tls; } + +#endif diff --git a/tests/unit/stubs/esp_wifi.h b/tests/unit/stubs/esp_wifi.h new file mode 100644 index 0000000..6aa5787 --- /dev/null +++ b/tests/unit/stubs/esp_wifi.h @@ -0,0 +1,40 @@ +#ifndef STUBS_ESP_WIFI_H +#define STUBS_ESP_WIFI_H + +#include +#include +#include "esp_err.h" + +#define WIFI_IF_STA 0 +#define WIFI_IF_AP 1 + +#define WIFI_AUTH_WPA2_PSK 3 +#define WIFI_AUTH_OPEN 0 + +#define WIFI_MODE_APSTA 3 + +typedef struct { + struct { + uint8_t ssid[32]; + uint8_t password[64]; + uint8_t channel; + uint8_t max_connection; + uint8_t ssid_hidden; + int authmode; + } ap; + struct { + uint8_t ssid[32]; + uint8_t password[64]; + int threshold; + struct { + int authmode; + } sta; + } sta; +} wifi_config_t; + +static inline esp_err_t esp_wifi_set_mac(int ifx, const uint8_t *mac) { (void)ifx; (void)mac; return ESP_OK; } +static inline esp_err_t esp_wifi_set_config(int ifx, const wifi_config_t *cfg) { (void)ifx; (void)cfg; return ESP_OK; } +static inline esp_err_t esp_wifi_set_mode(uint8_t mode) { (void)mode; return ESP_OK; } +static inline esp_err_t esp_wifi_start(void) { return ESP_OK; } + +#endif diff --git a/tests/unit/stubs/freertos/FreeRTOS.h b/tests/unit/stubs/freertos/FreeRTOS.h new file mode 100644 index 0000000..0fee758 --- /dev/null +++ b/tests/unit/stubs/freertos/FreeRTOS.h @@ -0,0 +1,11 @@ +#ifndef STUBS_FREERTOS_FREERTOS_H +#define STUBS_FREERTOS_FREERTOS_H + +#include + +static inline uint32_t xTaskGetTickCount(void) { return 0; } +static inline void vTaskDelay(uint32_t ticks) { (void)ticks; } +#define pdMS_TO_TICKS(ms) ((ms) / 10) +#define portMAX_DELAY 0xFFFFFFFF + +#endif diff --git a/tests/unit/stubs/freertos/event_groups.h b/tests/unit/stubs/freertos/event_groups.h new file mode 100644 index 0000000..28f6403 --- /dev/null +++ b/tests/unit/stubs/freertos/event_groups.h @@ -0,0 +1,13 @@ +#ifndef STUBS_FREERTOS_EVENT_GROUPS_H +#define STUBS_FREERTOS_EVENT_GROUPS_H + +#include + +typedef void *EventGroupHandle_t; +#define BIT0 (1 << 0) + +static inline EventGroupHandle_t xEventGroupCreate(void) { return (EventGroupHandle_t)1; } +static inline uint32_t xEventGroupSetBits(EventGroupHandle_t eg, uint32_t bits) { (void)eg; return bits; } +static inline uint32_t xEventGroupClearBits(EventGroupHandle_t eg, uint32_t bits) { (void)eg; return bits; } + +#endif diff --git a/tests/unit/stubs/freertos/task.h b/tests/unit/stubs/freertos/task.h new file mode 100644 index 0000000..3855d41 --- /dev/null +++ b/tests/unit/stubs/freertos/task.h @@ -0,0 +1,19 @@ +#ifndef STUBS_FREERTOS_TASK_H +#define STUBS_FREERTOS_TASK_H + +#include +#include + +typedef void *TaskHandle_t; +typedef void *SemaphoreHandle_t; + +static inline void vTaskDelete(TaskHandle_t t) { (void)t; } +static inline SemaphoreHandle_t xSemaphoreCreateMutex(void) { return (SemaphoreHandle_t)malloc(1); } +static inline void vSemaphoreDelete(SemaphoreHandle_t s) { free(s); } +static inline int xSemaphoreTake(SemaphoreHandle_t s, uint32_t blk) { (void)s; (void)blk; return 1; } +static inline int xSemaphoreGive(SemaphoreHandle_t s) { (void)s; return 1; } +static inline int xTaskCreate(void (*fn)(void*), const char *n, uint32_t st, void *p, uint32_t pri, TaskHandle_t *h) { + (void)fn; (void)n; (void)st; (void)p; (void)pri; (void)h; return 1; +} + +#endif diff --git a/tests/unit/stubs/freertos/timers.h b/tests/unit/stubs/freertos/timers.h new file mode 100644 index 0000000..7575807 --- /dev/null +++ b/tests/unit/stubs/freertos/timers.h @@ -0,0 +1,15 @@ +#ifndef STUBS_FREERTOS_TIMERS_H +#define STUBS_FREERTOS_TIMERS_H + +#include + +typedef void *TimerHandle_t; + +static inline TimerHandle_t xTimerCreate(const char *n, uint32_t pd, int ux, void *id, void *cb) { + (void)n; (void)pd; (void)ux; (void)id; (void)cb; return (TimerHandle_t)1; +} +static inline int xTimerStart(TimerHandle_t t, uint32_t blk) { (void)t; (void)blk; return 1; } +static inline int xTimerStop(TimerHandle_t t, uint32_t blk) { (void)t; (void)blk; return 1; } +static inline void xTimerDelete(TimerHandle_t t, uint32_t blk) { (void)t; (void)blk; } + +#endif diff --git a/tests/unit/stubs/lwip/ip4_addr.h b/tests/unit/stubs/lwip/ip4_addr.h new file mode 100644 index 0000000..174211b --- /dev/null +++ b/tests/unit/stubs/lwip/ip4_addr.h @@ -0,0 +1,19 @@ +#ifndef STUBS_LWIP_IP4_ADDR_H +#define STUBS_LWIP_IP4_ADDR_H + +#include + +typedef struct { + uint32_t addr; +} ip4_addr_t; + +typedef ip4_addr_t esp_ip4_addr_t; + +#define IPSTR "%d.%d.%d.%d" +#define IP2STR(ip) ((ip)->addr & 0xff), (((ip)->addr >> 8) & 0xff), (((ip)->addr >> 16) & 0xff), (((ip)->addr >> 24) & 0xff) + +static inline void IP4_ADDR(esp_ip4_addr_t *ip, uint8_t a, uint8_t b, uint8_t c, uint8_t d) { + ip->addr = ((uint32_t)a) | ((uint32_t)b << 8) | ((uint32_t)c << 16) | ((uint32_t)d << 24); +} + +#endif diff --git a/tests/unit/stubs/lwip/napt.h b/tests/unit/stubs/lwip/napt.h new file mode 100644 index 0000000..c6a5ca1 --- /dev/null +++ b/tests/unit/stubs/lwip/napt.h @@ -0,0 +1,6 @@ +#ifndef STUBS_LWIP_NAPT_H +#define STUBS_LWIP_NAPT_H + +static inline void ip_napt_enable(uint32_t num, int enable) { (void)num; (void)enable; } + +#endif diff --git a/tests/unit/stubs/lwip/netdb.h b/tests/unit/stubs/lwip/netdb.h new file mode 100644 index 0000000..b71bab8 --- /dev/null +++ b/tests/unit/stubs/lwip/netdb.h @@ -0,0 +1,4 @@ +#ifndef STUBS_LWIP_NETDB_H +#define STUBS_LWIP_NETDB_H + +#endif diff --git a/tests/unit/stubs/lwip/netif.h b/tests/unit/stubs/lwip/netif.h new file mode 100644 index 0000000..461a64e --- /dev/null +++ b/tests/unit/stubs/lwip/netif.h @@ -0,0 +1,4 @@ +#ifndef STUBS_LWIP_NETIF_H +#define STUBS_LWIP_NETIF_H + +#endif diff --git a/tests/unit/stubs/lwip/sockets.h b/tests/unit/stubs/lwip/sockets.h new file mode 100644 index 0000000..44f03ac --- /dev/null +++ b/tests/unit/stubs/lwip/sockets.h @@ -0,0 +1,4 @@ +#ifndef STUBS_LWIP_SOCKETS_H +#define STUBS_LWIP_SOCKETS_H + +#endif diff --git a/tests/unit/stubs/nvs_flash.h b/tests/unit/stubs/nvs_flash.h new file mode 100644 index 0000000..4424a9a --- /dev/null +++ b/tests/unit/stubs/nvs_flash.h @@ -0,0 +1,12 @@ +#ifndef STUBS_NVS_FLASH_H +#define STUBS_NVS_FLASH_H + +#include "esp_err.h" + +#define ESP_ERR_NVS_NO_FREE_PAGES 0x1101 +#define ESP_ERR_NVS_NEW_VERSION_FOUND 0x1102 + +static inline esp_err_t nvs_flash_init(void) { return ESP_OK; } +static inline esp_err_t nvs_flash_erase(void) { return ESP_OK; } + +#endif diff --git a/tests/unit/test_cashu.c b/tests/unit/test_cashu.c new file mode 100644 index 0000000..cec8e08 --- /dev/null +++ b/tests/unit/test_cashu.c @@ -0,0 +1,54 @@ +#include "test_framework.h" +#include "../../main/cashu.h" +#include "../../main/config.h" +#include +#include +#include + +static tollgate_config_t g_test_config; + +const tollgate_config_t *tollgate_config_get(void) { + return &g_test_config; +} + +int main(void) +{ + printf("=== test_cashu ===\n"); + + memset(&g_test_config, 0, sizeof(g_test_config)); + strncpy(g_test_config.mint_url, "https://testnut.cashu.space", sizeof(g_test_config.mint_url) - 1); + g_test_config.price_per_step = 21; + g_test_config.step_size_ms = 60000; + + printf("\n--- cashu_calculate_allotment_ms ---\n"); + uint64_t a1 = cashu_calculate_allotment_ms(21, 21, 60000); + ASSERT_EQ_INT(60000, (int)a1, "21 sats at 21 sats/min = 60000ms"); + + uint64_t a2 = cashu_calculate_allotment_ms(42, 21, 60000); + ASSERT_EQ_INT(120000, (int)a2, "42 sats at 21 sats/min = 120000ms"); + + uint64_t a3 = cashu_calculate_allotment_ms(1, 21, 60000); + ASSERT_EQ_INT(0, (int)a3, "1 sat at 21 sats/min = 0ms (rounds down)"); + + uint64_t a4 = cashu_calculate_allotment_ms(100, 10, 30000); + ASSERT_EQ_INT(300000, (int)a4, "100 sats at 10 sats/30s = 300000ms"); + + printf("\n--- cashu_is_mint_accepted ---\n"); + ASSERT(cashu_is_mint_accepted("https://testnut.cashu.space"), "testnut.cashu.space accepted"); + ASSERT(!cashu_is_mint_accepted("https://evil.mint.example.com"), "evil mint rejected"); + ASSERT(!cashu_is_mint_accepted(""), "empty string rejected"); + + printf("\n--- cashu_decode_token with garbage ---\n"); + cashu_token_t token; + memset(&token, 0, sizeof(token)); + esp_err_t ret = cashu_decode_token("garbage", &token); + ASSERT(ret != ESP_OK, "Garbage input returns error"); + + ret = cashu_decode_token("", &token); + ASSERT(ret != ESP_OK, "Empty string returns error"); + + ret = cashu_decode_token("cashuA!!invalid-base64!!", &token); + ASSERT(ret != ESP_OK, "Invalid base64url returns error"); + + TEST_SUMMARY(); +} diff --git a/tests/unit/test_framework.h b/tests/unit/test_framework.h new file mode 100644 index 0000000..6eb3a10 --- /dev/null +++ b/tests/unit/test_framework.h @@ -0,0 +1,60 @@ +#ifndef TEST_FRAMEWORK_H +#define TEST_FRAMEWORK_H + +#include +#include +#include + +static int g_tests_passed = 0; +static int g_tests_failed = 0; + +#define ASSERT(cond, msg) do { \ + if (cond) { \ + printf(" PASS: %s\n", msg); \ + g_tests_passed++; \ + } else { \ + printf(" FAIL: %s (at %s:%d)\n", msg, __FILE__, __LINE__); \ + g_tests_failed++; \ + } \ +} while(0) + +#define ASSERT_EQ_INT(expected, actual, msg) do { \ + int _e = (expected), _a = (actual); \ + if (_e == _a) { \ + printf(" PASS: %s (got %d)\n", msg, _a); \ + g_tests_passed++; \ + } else { \ + printf(" FAIL: %s (expected %d, got %d) at %s:%d\n", msg, _e, _a, __FILE__, __LINE__); \ + g_tests_failed++; \ + } \ +} while(0) + +#define ASSERT_EQ_STR(expected, actual, msg) do { \ + const char *_e = (expected), *_a = (actual); \ + if (_e && _a && strcmp(_e, _a) == 0) { \ + printf(" PASS: %s (got \"%s\")\n", msg, _a); \ + g_tests_passed++; \ + } else { \ + printf(" FAIL: %s (expected \"%s\", got \"%s\") at %s:%d\n", msg, _e ? _e : "(null)", _a ? _a : "(null)", __FILE__, __LINE__); \ + g_tests_failed++; \ + } \ +} while(0) + +#define ASSERT_MEM_EQ(expected, actual, len, msg) do { \ + const uint8_t *_e = (const uint8_t *)(expected), *_a = (const uint8_t *)(actual); \ + size_t _l = (len); \ + if (_e && _a && memcmp(_e, _a, _l) == 0) { \ + printf(" PASS: %s (%zu bytes match)\n", msg, _l); \ + g_tests_passed++; \ + } else { \ + printf(" FAIL: %s (%zu bytes mismatch) at %s:%d\n", msg, _l, __FILE__, __LINE__); \ + g_tests_failed++; \ + } \ +} while(0) + +#define TEST_SUMMARY() do { \ + printf("\n=== Results: %d passed, %d failed ===\n", g_tests_passed, g_tests_failed); \ + return g_tests_failed > 0 ? 1 : 0; \ +} while(0) + +#endif diff --git a/tests/unit/test_geohash b/tests/unit/test_geohash new file mode 100755 index 0000000..db87d33 Binary files /dev/null and b/tests/unit/test_geohash differ diff --git a/tests/unit/test_geohash.c b/tests/unit/test_geohash.c new file mode 100644 index 0000000..0da81fa --- /dev/null +++ b/tests/unit/test_geohash.c @@ -0,0 +1,40 @@ +#include "test_framework.h" +#include "../../main/geohash.h" +#include + +int main(void) +{ + char buf[16]; + + printf("=== test_geohash ===\n"); + + geohash_encode(48.1351, 11.5820, 9, buf); + ASSERT_EQ_STR("u281zd9z2", buf, "Munich (48.1351, 11.5820) precision 9"); + + geohash_encode(40.7128, -74.0060, 6, buf); + ASSERT(buf[0] == 'd', "NYC starts with 'd'"); + ASSERT(buf[1] == 'r', "NYC second char 'r'"); + ASSERT_EQ_INT(6, (int)strlen(buf), "NYC precision 6 has length 6"); + + geohash_encode(0.0, 0.0, 8, buf); + ASSERT_EQ_STR("s0000000", buf, "Origin (0,0) precision 8"); + + geohash_encode(90.0, 180.0, 5, buf); + ASSERT_EQ_INT(5, (int)strlen(buf), "North pole max lon precision 5"); + + geohash_encode(-90.0, -180.0, 5, buf); + ASSERT_EQ_INT(5, (int)strlen(buf), "South pole min lon precision 5"); + + geohash_encode(48.1351, 11.5820, 1, buf); + ASSERT_EQ_INT(1, (int)strlen(buf), "Precision 1 produces 1 char"); + ASSERT(buf[0] == 'u', "Munich precision 1 = 'u'"); + + geohash_encode(48.1351, 11.5820, 4, buf); + ASSERT_EQ_STR("u281", buf, "Munich precision 4"); + + char buf2[16]; + geohash_encode(48.1351, 11.5820, 9, buf2); + ASSERT_EQ_STR("u281zd9z2", buf2, "Munich determinism check"); + + TEST_SUMMARY(); +} diff --git a/tests/unit/test_identity.c b/tests/unit/test_identity.c new file mode 100644 index 0000000..cf4028f --- /dev/null +++ b/tests/unit/test_identity.c @@ -0,0 +1,68 @@ +#include "test_framework.h" +#include "../../main/identity.h" +#include +#include + +static const char *TEST_NSEC = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"; +static const char *TEST_NSEC2 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + +int main(void) +{ + printf("=== test_identity ===\n"); + + printf("\n--- identity_init with valid nsec ---\n"); + esp_err_t ret = identity_init(TEST_NSEC); + ASSERT_EQ_INT(ESP_OK, ret, "identity_init returns ESP_OK"); + + const tollgate_identity_t *id = identity_get(); + ASSERT(id != NULL, "identity_get returns non-NULL"); + ASSERT(id->initialized, "identity is marked initialized"); + + printf("\n--- npub derivation ---\n"); + ASSERT_EQ_INT(64, (int)strlen(id->npub_hex), "npub is 64 hex chars"); + ASSERT(id->npub_hex[0] != '\0', "npub is not empty"); + + printf("\n--- STA MAC derivation ---\n"); + uint8_t expected_sta[] = {0xF2, 0x4D, 0x55, 0x33, 0xDC, 0x9C}; + ASSERT_MEM_EQ(expected_sta, id->sta_mac, 6, "STA MAC matches golden vector"); + ASSERT_EQ_INT(2, id->sta_mac[0] & 0x02, "STA MAC has locally-administered bit set"); + ASSERT_EQ_INT(0, id->sta_mac[0] & 0x01, "STA MAC has multicast bit cleared"); + + printf("\n--- AP MAC derivation ---\n"); + uint8_t expected_ap[] = {0x3A, 0x2A, 0xEB, 0xC0, 0xE9, 0xCA}; + ASSERT_MEM_EQ(expected_ap, id->ap_mac, 6, "AP MAC matches golden vector"); + ASSERT_EQ_INT(2, id->ap_mac[0] & 0x02, "AP MAC has locally-administered bit set"); + ASSERT_EQ_INT(0, id->ap_mac[0] & 0x01, "AP MAC has multicast bit cleared"); + + printf("\n--- SSID derivation ---\n"); + ASSERT_EQ_STR("TollGate-C0E9CA", id->ap_ssid, "SSID derived from AP MAC last 3 bytes"); + + printf("\n--- AP IP derivation ---\n"); + ASSERT_EQ_STR("10.192.45.1", id->ap_ip_str, "AP IP derived from AP MAC bytes"); + + printf("\n--- Determinism ---\n"); + ret = identity_init(TEST_NSEC); + ASSERT_EQ_INT(ESP_OK, ret, "Second init with same nsec succeeds"); + const tollgate_identity_t *id2 = identity_get(); + ASSERT_MEM_EQ(id->sta_mac, id2->sta_mac, 6, "STA MAC is deterministic"); + ASSERT_MEM_EQ(id->ap_mac, id2->ap_mac, 6, "AP MAC is deterministic"); + ASSERT_EQ_STR(id->ap_ssid, id2->ap_ssid, "SSID is deterministic"); + + printf("\n--- Different nsec produces different identity ---\n"); + ret = identity_init(TEST_NSEC2); + ASSERT_EQ_INT(ESP_OK, ret, "Init with different nsec succeeds"); + const tollgate_identity_t *id3 = identity_get(); + ASSERT(memcmp(id->sta_mac, id3->sta_mac, 6) != 0, "Different nsec produces different STA MAC"); + ASSERT(memcmp(id->ap_mac, id3->ap_mac, 6) != 0, "Different nsec produces different AP MAC"); + ASSERT(strcmp(id->ap_ssid, id3->ap_ssid) != 0, "Different nsec produces different SSID"); + + printf("\n--- Invalid nsec ---\n"); + ret = identity_init(NULL); + ASSERT(ret != ESP_OK, "NULL nsec returns error"); + ret = identity_init("tooshort"); + ASSERT(ret != ESP_OK, "Short nsec returns error"); + ret = identity_init("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"); + ASSERT(ret != ESP_OK, "Invalid hex nsec returns error"); + + TEST_SUMMARY(); +} diff --git a/tests/unit/test_nostr_event.c b/tests/unit/test_nostr_event.c new file mode 100644 index 0000000..12bdb93 --- /dev/null +++ b/tests/unit/test_nostr_event.c @@ -0,0 +1,72 @@ +#include "test_framework.h" +#include "../../main/nostr_event.h" +#include "../../main/identity.h" +#include +#include + +static const char *TEST_NSEC = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"; + +static int time_override = 1700000000; + +int main(void) +{ + printf("=== test_nostr_event ===\n"); + + identity_init(TEST_NSEC); + const tollgate_identity_t *id = identity_get(); + + printf("\n--- Event ID computation (NIP-01) ---\n"); + nostr_event_t event; + esp_err_t ret = nostr_event_init(&event, id->npub_hex, 1, "[]", "Hello TollGate"); + ASSERT_EQ_INT(ESP_OK, ret, "nostr_event_init succeeds"); + + ASSERT_EQ_STR(id->npub_hex, event.pubkey, "Event pubkey matches npub"); + ASSERT_EQ_INT(1, event.kind, "Event kind is 1"); + ASSERT_EQ_INT(64, (int)strlen(event.id), "Event ID is 64 hex chars"); + + printf("\n--- Schnorr signing ---\n"); + ret = nostr_event_sign(&event, id->nsec); + ASSERT_EQ_INT(ESP_OK, ret, "nostr_event_sign succeeds"); + ASSERT_EQ_INT(128, (int)strlen(event.sig), "Signature is 128 hex chars"); + ASSERT(event.sig[0] != '\0', "Signature is not empty"); + + printf("\n--- JSON serialization ---\n"); + char json_buf[2048]; + ret = nostr_event_to_json(&event, json_buf, sizeof(json_buf)); + ASSERT_EQ_INT(ESP_OK, ret, "nostr_event_to_json succeeds"); + + ASSERT(strstr(json_buf, "\"id\"") != NULL, "JSON has 'id' field"); + ASSERT(strstr(json_buf, "\"pubkey\"") != NULL, "JSON has 'pubkey' field"); + ASSERT(strstr(json_buf, "\"created_at\"") != NULL, "JSON has 'created_at' field"); + ASSERT(strstr(json_buf, "\"kind\"") != NULL, "JSON has 'kind' field"); + ASSERT(strstr(json_buf, "\"tags\"") != NULL, "JSON has 'tags' field"); + ASSERT(strstr(json_buf, "\"content\"") != NULL, "JSON has 'content' field"); + ASSERT(strstr(json_buf, "\"sig\"") != NULL, "JSON has 'sig' field"); + ASSERT(strstr(json_buf, "Hello TollGate") != NULL, "JSON contains content"); + + printf("\n--- Buffer too small ---\n"); + char tiny_buf[10]; + ret = nostr_event_to_json(&event, tiny_buf, sizeof(tiny_buf)); + ASSERT(ret != ESP_OK, "Returns error when buffer too small"); + + printf("\n--- Kind 38787 event (wifistr) ---\n"); + nostr_event_t ws_event; + const char *ws_tags = "[[\"d\",\"test-npub\"],[\"ssid\",\"TollGate-TEST\"],[\"g\",\"u281w0dfz\"]]"; + ret = nostr_event_init(&ws_event, id->npub_hex, 38787, ws_tags, + "TollGate WiFi hotspot: TollGate-TEST"); + ASSERT_EQ_INT(ESP_OK, ret, "Kind 38787 init succeeds"); + ASSERT_EQ_INT(38787, ws_event.kind, "Event kind is 38787"); + ASSERT_EQ_INT(64, (int)strlen(ws_event.id), "Kind 38787 event has valid ID"); + + ret = nostr_event_sign(&ws_event, id->nsec); + ASSERT_EQ_INT(ESP_OK, ret, "Kind 38787 signing succeeds"); + ASSERT_EQ_INT(128, (int)strlen(ws_event.sig), "Kind 38787 signature is 128 hex chars"); + + printf("\n--- Determinism: same input → same ID ---\n"); + nostr_event_t event2; + nostr_event_init(&event2, id->npub_hex, 1, "[]", "Hello TollGate"); + ASSERT(strcmp(event.id, event2.id) == 0 || event.created_at != event2.created_at, + "Same input produces same ID (if timestamp matches) or differs only by time"); + + TEST_SUMMARY(); +} diff --git a/tests/unit/test_session.c b/tests/unit/test_session.c new file mode 100644 index 0000000..5b22a62 --- /dev/null +++ b/tests/unit/test_session.c @@ -0,0 +1,92 @@ +#include "test_framework.h" +#include "../../main/session.h" +#include "../../main/firewall.h" +#include +#include + +static uint32_t g_granted_ips[32]; +static int g_granted_count = 0; +static uint32_t g_revoked_ips[32]; +static int g_revoked_count = 0; + +esp_err_t firewall_get_mac_for_ip(uint32_t ip, char *mac_out, size_t size) { + (void)ip; + snprintf(mac_out, size, "AA:BB:CC:DD:EE:FF"); + return 0; +} + +void firewall_grant_access(uint32_t ip) { + if (g_granted_count < 32) g_granted_ips[g_granted_count++] = ip; +} + +void firewall_revoke_access(uint32_t ip) { + if (g_revoked_count < 32) g_revoked_ips[g_revoked_count++] = ip; +} + +int main(void) +{ + printf("=== test_session ===\n"); + + g_granted_count = 0; + g_revoked_count = 0; + + printf("\n--- session_manager_init ---\n"); + esp_err_t ret = session_manager_init(); + ASSERT_EQ_INT(0, ret, "session_manager_init succeeds"); + ASSERT_EQ_INT(0, session_active_count(), "No sessions after init"); + + printf("\n--- session_create ---\n"); + const char *secrets[] = {"secret1", "secret2"}; + session_t *s = session_create(0x0A01A8C0, 60000, secrets, 2); + ASSERT(s != NULL, "session_create returns non-NULL"); + ASSERT_EQ_INT(1, session_active_count(), "1 session after create"); + ASSERT_EQ_INT(1, g_granted_count, "firewall_grant_access was called"); + + printf("\n--- session_find_by_ip ---\n"); + session_t *found = session_find_by_ip(0x0A01A8C0); + ASSERT(found == s, "session_find_by_ip returns the created session"); + ASSERT(session_find_by_ip(0x01020304) == NULL, "session_find_by_ip returns NULL for unknown IP"); + + printf("\n--- session_is_secret_spent ---\n"); + ASSERT(session_is_secret_spent("secret1"), "secret1 is marked spent"); + ASSERT(session_is_secret_spent("secret2"), "secret2 is marked spent"); + ASSERT(!session_is_secret_spent("secret_unknown"), "unknown secret is not spent"); + + printf("\n--- Duplicate secret rejected ---\n"); + const char *dup_secrets[] = {"secret1"}; + g_granted_count = 0; + session_t *dup = session_create(0x0B01A8C0, 60000, dup_secrets, 1); + ASSERT(dup == NULL, "Duplicate secret returns NULL"); + ASSERT_EQ_INT(0, g_granted_count, "No new firewall grant for duplicate"); + + printf("\n--- session_extend ---\n"); + uint64_t old_allotment = s->allotment_ms; + session_extend(s, 30000); + ASSERT(s->allotment_ms == old_allotment + 30000, "Allotment extended by 30000ms"); + + printf("\n--- session_revoke ---\n"); + g_revoked_count = 0; + session_revoke(s); + ASSERT_EQ_INT(1, g_revoked_count, "firewall_revoke_access was called"); + ASSERT_EQ_INT(0, session_active_count(), "No active sessions after revoke"); + + printf("\n--- session_revoke_all ---\n"); + const char *s1[] = {"s1"}; + const char *s2[] = {"s2"}; + session_create(0x01000001, 60000, s1, 1); + session_create(0x01000002, 60000, s2, 1); + ASSERT_EQ_INT(2, session_active_count(), "2 sessions created"); + + g_revoked_count = 0; + session_revoke_all(); + ASSERT_EQ_INT(0, session_active_count(), "No sessions after revoke_all"); + + printf("\n--- session_tick does not crash ---\n"); + session_manager_init(); + const char *st[] = {"tick_secret"}; + session_create(0x0A000001, 60000, st, 1); + session_tick(); + ASSERT_EQ_INT(1, session_active_count(), "Session still active after tick (not expired)"); + + TEST_SUMMARY(); +} -- cgit v1.2.3