From 7d2ae4a69d72998048a7a428735084104679d4ac Mon Sep 17 00:00:00 2001 From: Logos Workspace Date: Wed, 6 May 2026 13:37:19 -0400 Subject: [PATCH] add tests add tests update readme --- .github/workflows/ci.yml | 45 ++ README.md | 33 +- tests/CMakeLists.txt | 50 ++ tests/main.cpp | 3 + tests/mocks/mock_logos_blockchain.cpp | 159 ++++++ tests/stubs/logos_blockchain.h | 131 +++++ tests/test_blockchain.cpp | 733 ++++++++++++++++++++++++++ 7 files changed, 1122 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 tests/CMakeLists.txt create mode 100644 tests/main.cpp create mode 100644 tests/mocks/mock_logos_blockchain.cpp create mode 100644 tests/stubs/logos_blockchain.h create mode 100644 tests/test_blockchain.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fcd7f85 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - uses: cachix/cachix-action@v15 + with: + name: logos-co + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + + - name: Build module + run: nix build + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - uses: cachix/cachix-action@v15 + with: + name: logos-co + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + + - name: Run unit tests + run: nix build '.#checks.x86_64-linux.unit-tests' -L diff --git a/README.md b/README.md index aceea33..1cca043 100644 --- a/README.md +++ b/README.md @@ -2,40 +2,9 @@ A Logos core module that wraps the [logos-blockchain](https://github.com/logos-blockchain/logos-blockchain) C bindings and ships the zk circuit binaries needed at runtime. -Built with [logos-module-builder](https://github.com/logos-co/logos-module-builder): `flake.nix`, `CMakeLists.txt`, and `metadata.json` are the only build-system files. The module-builder's `mkLogosModule` pulls the prebuilt `logos-blockchain-c` derivation (lib + header) in via `externalLibInputs`; the circuits directory is staged separately in `preConfigure` / `postInstall` because it's runtime data rather than a library. - ### Build and inspect ```bash -# In the workspace (preferred): -ws build logos-blockchain-module -ws build logos-blockchain-module --auto-local # pick up local dep overrides - -# Inspect the built plugin -lm ./result/lib/liblogos_blockchain_module_plugin.so - -# Standalone -nix build # -> result/lib/.so + result/lib/circuits/ -nix develop # cmake/ninja iteration +nix build '.#lgx' ``` -### Files - -- `flake.nix` — `mkLogosModule` call + circuits staging -- `CMakeLists.txt` — single `logos_module()` macro + C++20 bump -- `metadata.json` — module identity, deps, and `nix.external_libraries` -- `src/` — plugin sources (Q_OBJECT + Q_INVOKABLE API) - -### Troubleshooting - -#### Nix + IDE Integration -If your IDE reports that a file doesn't belong to the project or that files cannot be found, the CMake cache is likely missing the Nix-provided paths. This happens when the IDE runs CMake on its own, outside the Nix environment, leaving `LOGOS_CPP_SDK_ROOT` / `LOGOS_MODULE_BUILDER_ROOT` unset. - -To fix it: - -1. **Regenerate the cache from within the Nix shell** — this provides the required Nix paths and writes them into `build/CMakeCache.txt`: - ```bash - nix develop -c cmake -B build -GNinja - ``` - -2. **Reload the CMake project without resetting the cache.** On RustRover: open the CMake tool window (**View → Tool Windows → CMake**) and click **Reload** (↺). Resetting the cache would wipe the paths you just wrote. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..01a4ea8 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,50 @@ +cmake_minimum_required(VERSION 3.14) +project(BlockchainModuleTests LANGUAGES CXX) + +include(LogosTest) + +# Unit tests (mocked logos_blockchain) + +logos_test( + NAME blockchain_module_tests + MODULE_SOURCES + ../src/logos_blockchain_module.cpp + TEST_SOURCES + main.cpp + test_blockchain.cpp + MOCK_C_SOURCES + mocks/mock_logos_blockchain.cpp + EXTRA_INCLUDES + stubs +) + +# Integration tests (real logos_blockchain library) + +find_library(LIBLOGOS_BLOCKCHAIN_PATH + NAMES liblogos_blockchain.so liblogos_blockchain.dylib + PATHS ${CMAKE_CURRENT_SOURCE_DIR}/../lib + NO_DEFAULT_PATH) + +if(LIBLOGOS_BLOCKCHAIN_PATH) + message(STATUS "[BlockchainTests] logos_blockchain found: ${LIBLOGOS_BLOCKCHAIN_PATH} - building integration tests") + + logos_test( + NAME blockchain_module_integration_tests + MODULE_SOURCES + ../src/logos_blockchain_module.cpp + TEST_SOURCES + main.cpp + test_blockchain.cpp + EXTRA_INCLUDES + ../lib + EXTRA_LINK_LIBS + ${LIBLOGOS_BLOCKCHAIN_PATH} + ) + + get_filename_component(LIBLOGOS_BLOCKCHAIN_DIR "${LIBLOGOS_BLOCKCHAIN_PATH}" DIRECTORY) + set_target_properties(blockchain_module_integration_tests PROPERTIES + BUILD_RPATH "${LIBLOGOS_BLOCKCHAIN_DIR}" + ) +else() + message(STATUS "[BlockchainTests] logos_blockchain not found in ../lib - skipping integration tests") +endif() diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 0000000..93a8096 --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,3 @@ +#include + +LOGOS_TEST_MAIN() diff --git a/tests/mocks/mock_logos_blockchain.cpp b/tests/mocks/mock_logos_blockchain.cpp new file mode 100644 index 0000000..5374ff3 --- /dev/null +++ b/tests/mocks/mock_logos_blockchain.cpp @@ -0,0 +1,159 @@ +// Mock implementation of logos_blockchain C functions. +// Replaces the real Rust library at link time during unit tests. +// Return values are controlled via LogosCMockStore. + +#include +#include +#include +#include + +static char s_fakeNode = 0; +static CryptarchiaInfo s_fakeCryptarchiaInfo = {}; + +// Known-address mock storage (up to 4 addresses) +static uint8_t s_mockAddr0[32]; +static uint8_t s_mockAddr1[32]; +static uint8_t s_mockAddr2[32]; +static uint8_t s_mockAddr3[32]; +static uint8_t* s_mockAddrs[] = { s_mockAddr0, s_mockAddr1, s_mockAddr2, s_mockAddr3 }; + +extern "C" { + +bool is_ok(const OperationStatus* status) { + return status && *status == 0; +} + +OperationStatus generate_user_config(GenerateConfigArgs args) { + LOGOS_CMOCK_RECORD("generate_user_config"); + return LOGOS_CMOCK_RETURN(int, "generate_user_config"); +} + +NodeResult start_lb_node(const char* config_path, const char* deployment) { + LOGOS_CMOCK_RECORD("start_lb_node"); + int ok = LOGOS_CMOCK_RETURN(int, "start_lb_node"); + NodeResult result; + result.value = ok ? reinterpret_cast(&s_fakeNode) : nullptr; + result.error = ok ? 0 : 1; + return result; +} + +OperationStatus stop_node(LogosBlockchainNode* node) { + LOGOS_CMOCK_RECORD("stop_node"); + return 0; +} + +OperationStatus subscribe_to_new_blocks(LogosBlockchainNode* node, BlockCallback callback) { + LOGOS_CMOCK_RECORD("subscribe_to_new_blocks"); + return LOGOS_CMOCK_RETURN(int, "subscribe_to_new_blocks"); +} + +BalanceResult get_balance(LogosBlockchainNode* node, const uint8_t* address, const void* reserved) { + LOGOS_CMOCK_RECORD("get_balance"); + BalanceResult result; + result.value = static_cast(LOGOS_CMOCK_RETURN(int, "get_balance_value")); + result.error = LOGOS_CMOCK_RETURN(int, "get_balance_error"); + return result; +} + +TransferHashResult transfer_funds(LogosBlockchainNode* node, const TransferFundsArguments* args) { + LOGOS_CMOCK_RECORD("transfer_funds"); + TransferHashResult result; + memset(result.value, 0xAB, sizeof(Hash)); + result.error = LOGOS_CMOCK_RETURN(int, "transfer_funds_error"); + return result; +} + +KnownAddressesResult get_known_addresses(LogosBlockchainNode* node) { + LOGOS_CMOCK_RECORD("get_known_addresses"); + KnownAddressesResult result; + int err = LOGOS_CMOCK_RETURN(int, "get_known_addresses_error"); + result.error = err; + if (err == 0) { + int count = LOGOS_CMOCK_RETURN(int, "get_known_addresses_count"); + if (count > 4) count = 4; + memset(s_mockAddr0, 0x11, 32); + memset(s_mockAddr1, 0x22, 32); + memset(s_mockAddr2, 0x33, 32); + memset(s_mockAddr3, 0x44, 32); + result.value.addresses = s_mockAddrs; + result.value.len = static_cast(count); + } else { + result.value.addresses = nullptr; + result.value.len = 0; + } + return result; +} + +OperationStatus free_known_addresses(KnownAddresses addrs) { + LOGOS_CMOCK_RECORD("free_known_addresses"); + return 0; +} + +BlendHashResult blend_join_as_core_node( + LogosBlockchainNode* node, + const uint8_t* provider_id, + const uint8_t* zk_id, + const uint8_t* locked_note_id, + const char** locators, + size_t locators_count) +{ + LOGOS_CMOCK_RECORD("blend_join_as_core_node"); + BlendHashResult result; + memset(result.value, 0xCD, sizeof(Hash)); + result.error = LOGOS_CMOCK_RETURN(int, "blend_join_as_core_node_error"); + return result; +} + +StringResult get_block(LogosBlockchainNode* node, const HeaderId* header_id) { + LOGOS_CMOCK_RECORD("get_block"); + StringResult result; + const char* json = LOGOS_CMOCK_RETURN_STRING("get_block"); + result.value = json ? strdup(json) : nullptr; + result.error = LOGOS_CMOCK_RETURN(int, "get_block_error"); + return result; +} + +StringResult get_blocks(LogosBlockchainNode* node, uint64_t from_slot, uint64_t to_slot) { + LOGOS_CMOCK_RECORD("get_blocks"); + StringResult result; + const char* json = LOGOS_CMOCK_RETURN_STRING("get_blocks"); + result.value = json ? strdup(json) : nullptr; + result.error = LOGOS_CMOCK_RETURN(int, "get_blocks_error"); + return result; +} + +StringResult get_transaction(LogosBlockchainNode* node, const TxHash* tx_hash) { + LOGOS_CMOCK_RECORD("get_transaction"); + StringResult result; + const char* json = LOGOS_CMOCK_RETURN_STRING("get_transaction"); + result.value = json ? strdup(json) : nullptr; + result.error = LOGOS_CMOCK_RETURN(int, "get_transaction_error"); + return result; +} + +CryptarchiaInfoResult get_cryptarchia_info(LogosBlockchainNode* node) { + LOGOS_CMOCK_RECORD("get_cryptarchia_info"); + CryptarchiaInfoResult result; + memset(&s_fakeCryptarchiaInfo, 0, sizeof(s_fakeCryptarchiaInfo)); + s_fakeCryptarchiaInfo.slot = static_cast(LOGOS_CMOCK_RETURN(int, "cryptarchia_slot")); + s_fakeCryptarchiaInfo.height = static_cast(LOGOS_CMOCK_RETURN(int, "cryptarchia_height")); + s_fakeCryptarchiaInfo.mode = static_cast(LOGOS_CMOCK_RETURN(int, "cryptarchia_mode")); + memset(s_fakeCryptarchiaInfo.lib, 0xEE, 32); + memset(s_fakeCryptarchiaInfo.tip, 0xFF, 32); + result.value = &s_fakeCryptarchiaInfo; + result.error = LOGOS_CMOCK_RETURN(int, "get_cryptarchia_info_error"); + return result; +} + +OperationStatus free_cryptarchia_info(CryptarchiaInfo* info) { + LOGOS_CMOCK_RECORD("free_cryptarchia_info"); + return 0; +} + +OperationStatus free_cstring(char* s) { + LOGOS_CMOCK_RECORD("free_cstring"); + free(s); + return 0; +} + +} // extern "C" diff --git a/tests/stubs/logos_blockchain.h b/tests/stubs/logos_blockchain.h new file mode 100644 index 0000000..22d5f62 --- /dev/null +++ b/tests/stubs/logos_blockchain.h @@ -0,0 +1,131 @@ +// Stub header for logos_blockchain — provides the same declarations as the real +// Rust-generated header so that logos_blockchain_module sources compile in tests. + +#ifndef LOGOS_BLOCKCHAIN_H +#define LOGOS_BLOCKCHAIN_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// 32-byte hash/address types +typedef uint8_t Hash[32]; +typedef uint8_t HeaderId[32]; +typedef uint8_t TxHash[32]; + +// Opaque node handle +typedef struct LogosBlockchainNode LogosBlockchainNode; + +// Operation status (0 = OK) +typedef int OperationStatus; + +// Deployment enums +typedef enum { WellKnown, Custom } DeploymentType; +typedef enum { Devnet } WellKnownDeployment; + +// Consensus state enum +typedef enum { Bootstrapping, Online } State; + +// Deployment configuration +typedef struct { + DeploymentType deployment_type; + WellKnownDeployment well_known_deployment; + const char* custom_deployment_config_path; +} Deployment; + +// Arguments for generate_user_config +typedef struct { + const char** initial_peers; + const uint32_t* initial_peers_count; + const char* output; + const uint16_t* net_port; + const uint16_t* blend_port; + const char* http_addr; + const char* external_address; + const bool* no_public_ip_check; + const Deployment* deployment; + const char* state_path; +} GenerateConfigArgs; + +// Arguments for transfer_funds +typedef struct { + const HeaderId* optional_tip; + const uint8_t* change_public_key; + const uint8_t* const* funding_public_keys; + size_t funding_public_keys_len; + const uint8_t* recipient_public_key; + uint64_t amount; +} TransferFundsArguments; + +// Known addresses result container +typedef struct { + uint8_t** addresses; + size_t len; +} KnownAddresses; + +// Cryptarchia consensus info +typedef struct { + uint8_t lib[32]; + uint8_t tip[32]; + uint64_t slot; + uint64_t height; + State mode; +} CryptarchiaInfo; + +// Result types (C++ structured bindings decompose these) +typedef struct { LogosBlockchainNode* value; OperationStatus error; } NodeResult; +typedef struct { uint64_t value; OperationStatus error; } BalanceResult; +typedef struct { Hash value; OperationStatus error; } TransferHashResult; +typedef struct { KnownAddresses value; OperationStatus error; } KnownAddressesResult; +typedef struct { Hash value; OperationStatus error; } BlendHashResult; +typedef struct { char* value; OperationStatus error; } StringResult; +typedef struct { CryptarchiaInfo* value; OperationStatus error; } CryptarchiaInfoResult; + +// Block event callback +typedef void (*BlockCallback)(const char* block_json); + +// Status check +bool is_ok(const OperationStatus* status); + +// Lifecycle +OperationStatus generate_user_config(GenerateConfigArgs args); +NodeResult start_lb_node(const char* config_path, const char* deployment); +OperationStatus stop_node(LogosBlockchainNode* node); +OperationStatus subscribe_to_new_blocks(LogosBlockchainNode* node, BlockCallback callback); + +// Wallet +BalanceResult get_balance(LogosBlockchainNode* node, const uint8_t* address, const void* reserved); +TransferHashResult transfer_funds(LogosBlockchainNode* node, const TransferFundsArguments* args); +KnownAddressesResult get_known_addresses(LogosBlockchainNode* node); +OperationStatus free_known_addresses(KnownAddresses addrs); + +// Blend +BlendHashResult blend_join_as_core_node( + LogosBlockchainNode* node, + const uint8_t* provider_id, + const uint8_t* zk_id, + const uint8_t* locked_note_id, + const char** locators, + size_t locators_count); + +// Explorer +StringResult get_block(LogosBlockchainNode* node, const HeaderId* header_id); +StringResult get_blocks(LogosBlockchainNode* node, uint64_t from_slot, uint64_t to_slot); +StringResult get_transaction(LogosBlockchainNode* node, const TxHash* tx_hash); + +// Cryptarchia +CryptarchiaInfoResult get_cryptarchia_info(LogosBlockchainNode* node); +OperationStatus free_cryptarchia_info(CryptarchiaInfo* info); + +// Memory management +OperationStatus free_cstring(char* s); + +#ifdef __cplusplus +} +#endif + +#endif // LOGOS_BLOCKCHAIN_H diff --git a/tests/test_blockchain.cpp b/tests/test_blockchain.cpp new file mode 100644 index 0000000..3491450 --- /dev/null +++ b/tests/test_blockchain.cpp @@ -0,0 +1,733 @@ +// Unit tests for LogosBlockchainModule. +// All logos_blockchain C functions are mocked at link time via mock_logos_blockchain.cpp. + +#include +#include "logos_blockchain_module.h" + +#include +#include +#include + +// 64-char hex string = 32 bytes (valid address/hash) +static const QString VALID_HEX = QString(64, 'a'); +static const QString VALID_HEX_WITH_PREFIX = "0x" + QString(64, 'b'); + +// Helper: create a module with a running (mocked) node. +// Sets up circuits directory, mock LogosAPI, and calls start(). +static LogosBlockchainModule* createStartedModule(LogosTestContext& t, QTemporaryDir& tmpDir) { + QDir dir(tmpDir.path()); + dir.mkpath("circuits"); + QFile f(dir.filePath("circuits/dummy.bin")); + f.open(QIODevice::WriteOnly); + f.write("x"); + f.close(); + + t.api()->setProperty("modulePath", tmpDir.path()); + + auto* module = new LogosBlockchainModule(); + t.initLegacy(module); + + t.mockCFunction("start_lb_node").returns(1); + t.mockCFunction("subscribe_to_new_blocks").returns(0); + + int rc = module->start(tmpDir.filePath("config.json"), ""); + if (rc != 0) { + delete module; + return nullptr; + } + return module; +} + +// ============================================================================ +// Core metadata +// ============================================================================ + +LOGOS_TEST(name_returns_module_name) { + LogosBlockchainModule module; + LOGOS_ASSERT_EQ(module.name(), QString("liblogos_blockchain_module")); +} + +LOGOS_TEST(version_returns_module_version) { + LogosBlockchainModule module; + LOGOS_ASSERT_EQ(module.version(), QString("1.0.0")); +} + +// ============================================================================ +// generate_user_config +// ============================================================================ + +LOGOS_TEST(generate_user_config_returns_0_on_success) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + + t.mockCFunction("generate_user_config").returns(0); + + QVariantMap args; + args["output"] = "/tmp/test-config.json"; + LOGOS_ASSERT_EQ(module.generate_user_config(args), 0); + LOGOS_ASSERT(t.cFunctionCalled("generate_user_config")); +} + +LOGOS_TEST(generate_user_config_returns_1_on_failure) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + + t.mockCFunction("generate_user_config").returns(1); + + QVariantMap args; + LOGOS_ASSERT_EQ(module.generate_user_config(args), 1); +} + +LOGOS_TEST(generate_user_config_from_str_delegates_to_generate_user_config) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + + t.mockCFunction("generate_user_config").returns(0); + + LOGOS_ASSERT_EQ(module.generate_user_config_from_str(R"({"output":"/tmp/out.json"})"), 0); + LOGOS_ASSERT(t.cFunctionCalled("generate_user_config")); +} + +LOGOS_TEST(generate_user_config_with_all_fields) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + + t.mockCFunction("generate_user_config").returns(0); + + QVariantMap deployment; + deployment["well_known_deployment"] = "devnet"; + + QVariantMap args; + args["initial_peers"] = QStringList{"peer1", "peer2"}; + args["output"] = "/tmp/out.json"; + args["net_port"] = 9000; + args["blend_port"] = 9001; + args["http_addr"] = "0.0.0.0:8080"; + args["external_address"] = "1.2.3.4"; + args["no_public_ip_check"] = true; + args["deployment"] = deployment; + args["state_path"] = "/tmp/state"; + + LOGOS_ASSERT_EQ(module.generate_user_config(args), 0); +} + +// ============================================================================ +// No-node error paths — all methods should fail gracefully +// ============================================================================ + +LOGOS_TEST(stop_without_node_returns_1) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + LOGOS_ASSERT_EQ(module.stop(), 1); +} + +LOGOS_TEST(wallet_get_balance_without_node_returns_error) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + QString result = module.wallet_get_balance(VALID_HEX); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("not running")); +} + +LOGOS_TEST(wallet_transfer_funds_without_node_returns_error) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + QString result = module.wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "100", ""); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("not running")); +} + +LOGOS_TEST(wallet_transfer_funds_single_sender_without_node_returns_error) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + QString result = module.wallet_transfer_funds(VALID_HEX, VALID_HEX, VALID_HEX, "100", ""); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); +} + +LOGOS_TEST(wallet_get_known_addresses_without_node_returns_empty) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + LOGOS_ASSERT_TRUE(module.wallet_get_known_addresses().isEmpty()); +} + +LOGOS_TEST(blend_join_as_core_node_without_node_returns_error) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + QString result = module.blend_join_as_core_node(VALID_HEX, VALID_HEX, VALID_HEX, {"locator1"}); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("not running")); +} + +LOGOS_TEST(get_block_without_node_returns_error) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + LOGOS_ASSERT_TRUE(module.get_block(VALID_HEX).startsWith("Error:")); +} + +LOGOS_TEST(get_blocks_without_node_returns_error) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + LOGOS_ASSERT_TRUE(module.get_blocks(0, 10).startsWith("Error:")); +} + +LOGOS_TEST(get_transaction_without_node_returns_error) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + LOGOS_ASSERT_TRUE(module.get_transaction(VALID_HEX).startsWith("Error:")); +} + +LOGOS_TEST(get_cryptarchia_info_without_node_returns_error) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + LOGOS_ASSERT_TRUE(module.get_cryptarchia_info().startsWith("Error:")); +} + +// ============================================================================ +// Node lifecycle (start / stop) +// ============================================================================ + +LOGOS_TEST(start_succeeds_with_mocked_dependencies) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + LOGOS_ASSERT_TRUE(tmpDir.isValid()); + + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + LOGOS_ASSERT(t.cFunctionCalled("start_lb_node")); + LOGOS_ASSERT(t.cFunctionCalled("subscribe_to_new_blocks")); + delete module; +} + +LOGOS_TEST(start_returns_1_when_already_running) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + LOGOS_ASSERT_EQ(module->start("/tmp/config.json", ""), 1); + delete module; +} + +LOGOS_TEST(start_returns_2_without_logos_api) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + LOGOS_ASSERT_EQ(module.start("/tmp/config.json", ""), 2); +} + +LOGOS_TEST(stop_succeeds_with_running_node) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + LOGOS_ASSERT_EQ(module->stop(), 0); + LOGOS_ASSERT(t.cFunctionCalled("stop_node")); + delete module; +} + +// ============================================================================ +// Input validation (requires running node) +// ============================================================================ + +// wallet_get_balance validation + +LOGOS_TEST(wallet_get_balance_rejects_short_hex) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->wallet_get_balance("abcd"); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("64 hex")); + delete module; +} + +LOGOS_TEST(wallet_get_balance_rejects_long_hex) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->wallet_get_balance(QString(66, 'a')); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + delete module; +} + +LOGOS_TEST(wallet_get_balance_rejects_invalid_chars) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString hex = QString(62, 'a') + "zz"; + QString result = module->wallet_get_balance(hex); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + delete module; +} + +// wallet_transfer_funds validation + +LOGOS_TEST(wallet_transfer_funds_rejects_invalid_amount) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "not_a_number", ""); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("Invalid amount")); + delete module; +} + +LOGOS_TEST(wallet_transfer_funds_rejects_invalid_change_key) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->wallet_transfer_funds("bad", QStringList{VALID_HEX}, VALID_HEX, "100", ""); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("change_public_key")); + delete module; +} + +LOGOS_TEST(wallet_transfer_funds_rejects_invalid_recipient) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, "short", "100", ""); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("recipient_address")); + delete module; +} + +LOGOS_TEST(wallet_transfer_funds_rejects_empty_senders) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{}, VALID_HEX, "100", ""); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("sender")); + delete module; +} + +LOGOS_TEST(wallet_transfer_funds_rejects_invalid_sender_address) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{"bad_addr"}, VALID_HEX, "100", ""); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("sender")); + delete module; +} + +LOGOS_TEST(wallet_transfer_funds_rejects_invalid_optional_tip) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "100", "bad_tip"); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("tip")); + delete module; +} + +// blend_join_as_core_node validation + +LOGOS_TEST(blend_join_rejects_invalid_provider_id) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->blend_join_as_core_node("short", VALID_HEX, VALID_HEX, {}); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("provider_id")); + delete module; +} + +LOGOS_TEST(blend_join_rejects_invalid_zk_id) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->blend_join_as_core_node(VALID_HEX, "short", VALID_HEX, {}); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("zk_id")); + delete module; +} + +LOGOS_TEST(blend_join_rejects_invalid_locked_note_id) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->blend_join_as_core_node(VALID_HEX, VALID_HEX, "short", {}); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("locked_note_id")); + delete module; +} + +// get_block / get_transaction validation + +LOGOS_TEST(get_block_rejects_invalid_hex) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->get_block("tooshort"); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("64 hex")); + delete module; +} + +LOGOS_TEST(get_transaction_rejects_invalid_hex) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + QString result = module->get_transaction("bad"); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("64 hex")); + delete module; +} + +// ============================================================================ +// 0x prefix handling +// ============================================================================ + +LOGOS_TEST(wallet_get_balance_accepts_0x_prefix) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_balance_value").returns(42); + t.mockCFunction("get_balance_error").returns(0); + + QString result = module->wallet_get_balance(VALID_HEX_WITH_PREFIX); + LOGOS_ASSERT_EQ(result, QString("42")); + delete module; +} + +// ============================================================================ +// Success paths (requires running node + mocked C functions) +// ============================================================================ + +// Wallet + +LOGOS_TEST(wallet_get_balance_returns_balance_string) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_balance_value").returns(1000); + t.mockCFunction("get_balance_error").returns(0); + + QString result = module->wallet_get_balance(VALID_HEX); + LOGOS_ASSERT_EQ(result, QString("1000")); + LOGOS_ASSERT(t.cFunctionCalled("get_balance")); + delete module; +} + +LOGOS_TEST(wallet_get_balance_returns_error_on_ffi_failure) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_balance_error").returns(1); + + QString result = module->wallet_get_balance(VALID_HEX); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + delete module; +} + +LOGOS_TEST(wallet_transfer_funds_returns_tx_hash) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("transfer_funds_error").returns(0); + + QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "500", ""); + LOGOS_ASSERT_FALSE(result.startsWith("Error:")); + LOGOS_ASSERT_EQ(result.length(), 64); + LOGOS_ASSERT_TRUE(result.startsWith("ab")); + LOGOS_ASSERT(t.cFunctionCalled("transfer_funds")); + delete module; +} + +LOGOS_TEST(wallet_transfer_funds_with_optional_tip) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("transfer_funds_error").returns(0); + + QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "100", VALID_HEX); + LOGOS_ASSERT_FALSE(result.startsWith("Error:")); + LOGOS_ASSERT_EQ(result.length(), 64); + delete module; +} + +LOGOS_TEST(wallet_transfer_funds_returns_error_on_ffi_failure) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("transfer_funds_error").returns(1); + + QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "100", ""); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + delete module; +} + +LOGOS_TEST(wallet_transfer_funds_single_sender_overload) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("transfer_funds_error").returns(0); + + QString result = module->wallet_transfer_funds(VALID_HEX, VALID_HEX, VALID_HEX, "100", ""); + LOGOS_ASSERT_FALSE(result.startsWith("Error:")); + LOGOS_ASSERT(t.cFunctionCalled("transfer_funds")); + delete module; +} + +LOGOS_TEST(wallet_transfer_funds_multiple_senders) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("transfer_funds_error").returns(0); + + QStringList senders; + senders << VALID_HEX << VALID_HEX_WITH_PREFIX.mid(2); // two different addresses + QString result = module->wallet_transfer_funds(VALID_HEX, senders, VALID_HEX, "200", ""); + LOGOS_ASSERT_FALSE(result.startsWith("Error:")); + delete module; +} + +LOGOS_TEST(wallet_get_known_addresses_returns_addresses) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_known_addresses_error").returns(0); + t.mockCFunction("get_known_addresses_count").returns(2); + + QStringList addrs = module->wallet_get_known_addresses(); + LOGOS_ASSERT_EQ(addrs.size(), 2); + // Mock fills addr0 with 0x11 → hex "1111...11", addr1 with 0x22 → "2222...22" + LOGOS_ASSERT_EQ(addrs[0], QString(64, '1')); + LOGOS_ASSERT_EQ(addrs[1], QString(64, '2')); + LOGOS_ASSERT(t.cFunctionCalled("get_known_addresses")); + LOGOS_ASSERT(t.cFunctionCalled("free_known_addresses")); + delete module; +} + +LOGOS_TEST(wallet_get_known_addresses_returns_empty_on_ffi_failure) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_known_addresses_error").returns(1); + + QStringList addrs = module->wallet_get_known_addresses(); + LOGOS_ASSERT_TRUE(addrs.isEmpty()); + delete module; +} + +// Blend + +LOGOS_TEST(blend_join_as_core_node_returns_declaration_id) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("blend_join_as_core_node_error").returns(0); + + QStringList locators = {"locator1", "locator2"}; + QString result = module->blend_join_as_core_node(VALID_HEX, VALID_HEX, VALID_HEX, locators); + // Mock fills hash with 0xCD → hex "cdcd...cd" (64 chars) + LOGOS_ASSERT_EQ(result.length(), 64); + LOGOS_ASSERT_TRUE(result.startsWith("cd")); + LOGOS_ASSERT(t.cFunctionCalled("blend_join_as_core_node")); + delete module; +} + +LOGOS_TEST(blend_join_as_core_node_returns_error_on_ffi_failure) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("blend_join_as_core_node_error").returns(1); + + QString result = module->blend_join_as_core_node(VALID_HEX, VALID_HEX, VALID_HEX, {}); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + delete module; +} + +// Explorer + +LOGOS_TEST(get_block_returns_json_on_success) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_block").returns(R"({"slot":42,"data":"test"})"); + t.mockCFunction("get_block_error").returns(0); + + QString result = module->get_block(VALID_HEX); + LOGOS_ASSERT_TRUE(result.contains("slot")); + LOGOS_ASSERT_TRUE(result.contains("42")); + LOGOS_ASSERT(t.cFunctionCalled("get_block")); + LOGOS_ASSERT(t.cFunctionCalled("free_cstring")); + delete module; +} + +LOGOS_TEST(get_block_returns_error_on_ffi_failure) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_block_error").returns(1); + + QString result = module->get_block(VALID_HEX); + LOGOS_ASSERT_TRUE(result.startsWith("Error:")); + delete module; +} + +LOGOS_TEST(get_blocks_returns_json_on_success) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_blocks").returns(R"([{"slot":1},{"slot":2}])"); + t.mockCFunction("get_blocks_error").returns(0); + + QString result = module->get_blocks(1, 10); + LOGOS_ASSERT_TRUE(result.contains("slot")); + LOGOS_ASSERT(t.cFunctionCalled("get_blocks")); + delete module; +} + +LOGOS_TEST(get_blocks_returns_error_on_ffi_failure) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_blocks_error").returns(1); + + LOGOS_ASSERT_TRUE(module->get_blocks(0, 10).startsWith("Error:")); + delete module; +} + +LOGOS_TEST(get_transaction_returns_json_on_success) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_transaction").returns(R"({"hash":"abc","status":"confirmed"})"); + t.mockCFunction("get_transaction_error").returns(0); + + QString result = module->get_transaction(VALID_HEX); + LOGOS_ASSERT_TRUE(result.contains("confirmed")); + LOGOS_ASSERT(t.cFunctionCalled("get_transaction")); + LOGOS_ASSERT(t.cFunctionCalled("free_cstring")); + delete module; +} + +LOGOS_TEST(get_transaction_returns_error_on_ffi_failure) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_transaction_error").returns(1); + + LOGOS_ASSERT_TRUE(module->get_transaction(VALID_HEX).startsWith("Error:")); + delete module; +} + +// Cryptarchia + +LOGOS_TEST(get_cryptarchia_info_returns_json_on_success) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_cryptarchia_info_error").returns(0); + t.mockCFunction("cryptarchia_slot").returns(100); + t.mockCFunction("cryptarchia_height").returns(50); + t.mockCFunction("cryptarchia_mode").returns(1); // Online + + QString result = module->get_cryptarchia_info(); + LOGOS_ASSERT_FALSE(result.startsWith("Error:")); + LOGOS_ASSERT_TRUE(result.contains("slot")); + LOGOS_ASSERT_TRUE(result.contains("100")); + LOGOS_ASSERT_TRUE(result.contains("height")); + LOGOS_ASSERT_TRUE(result.contains("50")); + LOGOS_ASSERT_TRUE(result.contains("Online")); + LOGOS_ASSERT_TRUE(result.contains("lib")); + LOGOS_ASSERT_TRUE(result.contains("tip")); + LOGOS_ASSERT(t.cFunctionCalled("get_cryptarchia_info")); + LOGOS_ASSERT(t.cFunctionCalled("free_cryptarchia_info")); + delete module; +} + +LOGOS_TEST(get_cryptarchia_info_bootstrapping_mode) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_cryptarchia_info_error").returns(0); + t.mockCFunction("cryptarchia_mode").returns(0); // Bootstrapping + + QString result = module->get_cryptarchia_info(); + LOGOS_ASSERT_TRUE(result.contains("Bootstrapping")); + delete module; +} + +LOGOS_TEST(get_cryptarchia_info_returns_error_on_ffi_failure) { + auto t = LogosTestContext("blockchain_module"); + QTemporaryDir tmpDir; + auto* module = createStartedModule(t, tmpDir); + LOGOS_ASSERT_TRUE(module != nullptr); + + t.mockCFunction("get_cryptarchia_info_error").returns(1); + + LOGOS_ASSERT_TRUE(module->get_cryptarchia_info().startsWith("Error:")); + delete module; +}