Files
Dario Gabriel LipicarandClaude Opus 5 4248520a7c refactor(api)!: rename version() to libstorageVersion()
`version` is now reserved for module identity. The generator injects
`version() -> tstr` from metadata.json for every module, and refuses a module
that declares the name with any other signature:

    storage_module.lidl: module 'storage_module' declares 'version() -> result',
    but 'version' is reserved for module identity and must be 'version() -> tstr'

That refusal is what currently blocks this module -- and everything downstream of
it -- from building against the current logos-module-builder.

This is a rename, not a removal, because the method is not the module's version
at all: it returns the version string of the underlying libstorage (Nim) library,
read from the live storage context. The module's OWN version was already exposed
separately as moduleVersion(), which is untouched.

So after this change the three are distinct and each says what it is:

    version()            -> tstr    injected from metadata.json (module identity)
    moduleVersion()      -> tstr    this module's version, from the build
    libstorageVersion()  -> result  the Nim library's version, from the node

moduleVersion() is left alone deliberately. It is now redundant with the injected
version(), but removing it is a second API break with no forcing reason, and it
belongs in its own change rather than riding along with an unblocking one.

BREAKING CHANGE: consumers calling storage_module.version() expecting the
libstorage string must call libstorageVersion(). The break is loud rather than
silent -- version() still exists, but returns a plain string instead of a
LogosResult, so a stale call site fails to compile rather than quietly reading
the wrong version. logos-storage-ui's StorageBackend::logVersion() is the one
in-tree caller and needs the companion one-line change.

Verified against the generated artifact rather than by inspection: the emitted
storage_module.lidl now carries `method libstorageVersion() -> result` and no
`method version()`, and logos-storage-ui -- the consumer whose qt-generator run
was failing -- builds clean against this module with the current
logos-module-builder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:50:13 -03:00

598 lines
17 KiB
C++

// Unit tests for StorageModuleImpl.
// All libstorage C functions are mocked at link time via mock_libstorage.cpp.
// Async mocks invoke the callback immediately so the condvar is signalled
// before waitSync's first check.
#include <logos_test.h>
#include "storage_module_plugin.h"
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// Helper: create an impl with a mocked, successfully initialized storage context.
static StorageModuleImpl* createInitializedImpl(LogosTestContext& t) {
t.mockCFunction("storage_new").returns(1);
auto* impl = new StorageModuleImpl();
LOGOS_ASSERT_TRUE(impl->init("{\"data-dir\":\"/tmp/test\"}"));
return impl;
}
// init
LOGOS_TEST(init_succeeds_when_storage_new_returns_context) {
auto t = LogosTestContext("storage_module");
t.mockCFunction("storage_new").returns(1);
StorageModuleImpl impl;
LOGOS_ASSERT_TRUE(impl.init("{\"data-dir\":\"/tmp/test\"}"));
LOGOS_ASSERT(t.cFunctionCalled("storage_new"));
}
LOGOS_TEST(init_fails_when_storage_new_returns_null) {
auto t = LogosTestContext("storage_module");
t.mockCFunction("storage_new").returns(0);
StorageModuleImpl impl;
LOGOS_ASSERT_FALSE(impl.init("{\"data-dir\":\"/tmp/test\"}"));
}
// version
LOGOS_TEST(libstorageVersion_returns_mocked_string) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_version").returns("1.2.3-test");
StdLogosResult vr = impl->libstorageVersion();
LOGOS_ASSERT_TRUE(vr.success);
LOGOS_ASSERT_EQ(vr.value.get<std::string>(), std::string("1.2.3-test"));
LOGOS_ASSERT(t.cFunctionCalled("storage_version"));
impl->destroy();
delete impl;
}
// The value is injected at build time from metadata.json; the fallback
// "0.0.0-dev" only survives when the build failed to pass the define, so
// asserting against it verifies the injection pipeline actually ran.
LOGOS_TEST(moduleVersion_is_injected_from_build) {
StorageModuleImpl impl;
std::string v = impl.moduleVersion();
LOGOS_ASSERT_FALSE(v.empty());
LOGOS_ASSERT(v != std::string("0.0.0-dev"));
}
// start / stop
LOGOS_TEST(start_returns_true_after_init) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
LOGOS_ASSERT_TRUE(impl->start());
LOGOS_ASSERT(t.cFunctionCalled("storage_start"));
impl->destroy();
delete impl;
}
LOGOS_TEST(start_returns_false_without_init) {
auto t = LogosTestContext("storage_module");
StorageModuleImpl impl;
LOGOS_ASSERT_FALSE(impl.start());
}
LOGOS_TEST(stop_fails_without_init) {
auto t = LogosTestContext("storage_module");
StorageModuleImpl impl;
LOGOS_ASSERT_FALSE(impl.stop().success);
}
LOGOS_TEST(stop_succeeds_after_init) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
LOGOS_ASSERT_TRUE(impl->stop().success);
LOGOS_ASSERT(t.cFunctionCalled("storage_stop"));
impl->destroy();
delete impl;
}
// destroy
LOGOS_TEST(destroy_without_init_returns_error) {
auto t = LogosTestContext("storage_module");
StorageModuleImpl impl;
// destroy() must not touch the C API when there is no context
LOGOS_ASSERT_FALSE(impl.destroy().success);
LOGOS_ASSERT(!t.cFunctionCalled("storage_destroy"));
}
LOGOS_TEST(destroy_succeeds_after_init) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
LOGOS_ASSERT_TRUE(impl->destroy().success);
LOGOS_ASSERT(t.cFunctionCalled("storage_close"));
LOGOS_ASSERT(t.cFunctionCalled("storage_destroy"));
delete impl;
}
// peerId / spr / dataDir
LOGOS_TEST(peerId_returns_mocked_value) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_peer_id").returns("QmTestPeerId123");
StdLogosResult r = impl->peerId();
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_EQ(r.value.get<std::string>(), std::string("QmTestPeerId123"));
impl->destroy();
delete impl;
}
LOGOS_TEST(spr_returns_mocked_value) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_spr").returns("spr:ABCD1234");
StdLogosResult r = impl->spr();
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_EQ(r.value.get<std::string>(), std::string("spr:ABCD1234"));
impl->destroy();
delete impl;
}
LOGOS_TEST(dataDir_returns_mocked_value) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_repo").returns("/tmp/test-data");
StdLogosResult r = impl->dataDir();
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_EQ(r.value.get<std::string>(), std::string("/tmp/test-data"));
impl->destroy();
delete impl;
}
LOGOS_TEST(peerId_returns_failure_without_init) {
auto t = LogosTestContext("storage_module");
StorageModuleImpl impl;
LOGOS_ASSERT_FALSE(impl.peerId().success);
}
// debug
LOGOS_TEST(debug_returns_parsed_map) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_debug")
.returns(R"({"id":"QmNode","addrs":[],"providerAddresses":[],"table":{}})");
StdLogosResult r = impl->debug();
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_TRUE(r.value.is_object());
LOGOS_ASSERT_FALSE(r.value.empty());
LOGOS_ASSERT_TRUE(r.value.contains("id"));
impl->destroy();
delete impl;
}
LOGOS_TEST(debug_fails_on_invalid_json) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_debug").returns("not json");
StdLogosResult r = impl->debug();
LOGOS_ASSERT_FALSE(r.success);
impl->destroy();
delete impl;
}
// collectMetrics
LOGOS_TEST(collectMetrics_returns_parsed_metrics) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_get_metrics")
.returns(R"({"metrics":[{"name":"test_metric","type":"gauge","help":"Test metric","value":1.0,"labels":{}}]})");
LogosMap r = impl->collectMetrics();
LOGOS_ASSERT_TRUE(r.is_object());
LOGOS_ASSERT_TRUE(r.contains("metrics"));
LOGOS_ASSERT_TRUE(r["metrics"].is_array());
LOGOS_ASSERT_EQ(static_cast<int>(r["metrics"].size()), 1);
LOGOS_ASSERT(t.cFunctionCalled("storage_get_metrics"));
impl->destroy();
delete impl;
}
LOGOS_TEST(collectMetrics_returns_empty_metrics_on_libstorage_error) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_get_metrics").returns(1);
LogosMap r = impl->collectMetrics();
LOGOS_ASSERT_TRUE(r.is_object());
LOGOS_ASSERT_TRUE(r.contains("metrics"));
LOGOS_ASSERT_TRUE(r["metrics"].is_array());
LOGOS_ASSERT_TRUE(r["metrics"].empty());
impl->destroy();
delete impl;
}
LOGOS_TEST(collectMetrics_returns_empty_metrics_on_invalid_json) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_get_metrics").returns("not json");
LogosMap r = impl->collectMetrics();
LOGOS_ASSERT_TRUE(r.is_object());
LOGOS_ASSERT_TRUE(r.contains("metrics"));
LOGOS_ASSERT_TRUE(r["metrics"].is_array());
LOGOS_ASSERT_TRUE(r["metrics"].empty());
impl->destroy();
delete impl;
}
LOGOS_TEST(collectMetrics_returns_empty_metrics_when_payload_is_array) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_get_metrics").returns(R"([])");
LogosMap r = impl->collectMetrics();
LOGOS_ASSERT_TRUE(r.is_object());
LOGOS_ASSERT_TRUE(r.contains("metrics"));
LOGOS_ASSERT_TRUE(r["metrics"].is_array());
LOGOS_ASSERT_TRUE(r["metrics"].empty());
impl->destroy();
delete impl;
}
LOGOS_TEST(collectMetrics_returns_empty_metrics_when_metrics_missing) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_get_metrics").returns(R"({"other":[]})");
LogosMap r = impl->collectMetrics();
LOGOS_ASSERT_TRUE(r.is_object());
LOGOS_ASSERT_TRUE(r.contains("metrics"));
LOGOS_ASSERT_TRUE(r["metrics"].is_array());
LOGOS_ASSERT_TRUE(r["metrics"].empty());
impl->destroy();
delete impl;
}
LOGOS_TEST(collectMetrics_returns_empty_metrics_when_metrics_is_not_array) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_get_metrics").returns(R"({"metrics":{}})");
LogosMap r = impl->collectMetrics();
LOGOS_ASSERT_TRUE(r.is_object());
LOGOS_ASSERT_TRUE(r.contains("metrics"));
LOGOS_ASSERT_TRUE(r["metrics"].is_array());
LOGOS_ASSERT_TRUE(r["metrics"].empty());
impl->destroy();
delete impl;
}
// updateLogLevel
LOGOS_TEST(updateLogLevel_returns_true_on_success) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
LOGOS_ASSERT_TRUE(impl->updateLogLevel("DEBUG").success);
LOGOS_ASSERT(t.cFunctionCalled("storage_log_level"));
impl->destroy();
delete impl;
}
// exists
LOGOS_TEST(exists_returns_true_when_cid_found) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_exists").returns("true");
StdLogosResult r = impl->exists("QmSomeCid");
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_TRUE(r.value.get<bool>());
impl->destroy();
delete impl;
}
LOGOS_TEST(exists_returns_false_when_cid_not_found) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_exists").returns("false");
StdLogosResult r = impl->exists("QmMissingCid");
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_FALSE(r.value.get<bool>());
impl->destroy();
delete impl;
}
// togglePrivateQueries
LOGOS_TEST(togglePrivateQueries_returns_previous_state) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_toggle_private_queries").returns("false");
StdLogosResult r = impl->togglePrivateQueries(true);
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_FALSE(r.value.get<bool>());
LOGOS_ASSERT(t.cFunctionCalled("storage_toggle_private_queries"));
impl->destroy();
delete impl;
}
LOGOS_TEST(togglePrivateQueries_maps_true_previous_state) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_toggle_private_queries").returns("true");
StdLogosResult r = impl->togglePrivateQueries(false);
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_TRUE(r.value.get<bool>());
impl->destroy();
delete impl;
}
// fetch / remove
LOGOS_TEST(fetch_calls_storage_fetch) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
LOGOS_ASSERT_TRUE(impl->fetch("QmSomeCid").success);
LOGOS_ASSERT(t.cFunctionCalled("storage_fetch"));
impl->destroy();
delete impl;
}
LOGOS_TEST(remove_dispatches_and_emits_event) {
logos_test::EventCapture events;
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
LOGOS_ASSERT_TRUE(impl->remove("QmSomeCid").success);
LOGOS_ASSERT(t.cFunctionCalled("storage_delete"));
LOGOS_ASSERT_TRUE(events.has("storageRemoveDone"));
impl->destroy();
delete impl;
}
// space
LOGOS_TEST(space_returns_parsed_map) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_space")
.returns(R"({"totalBlocks":100,"quotaMaxBytes":1000,"quotaUsedBytes":50,"quotaReservedBytes":10})");
StdLogosResult r = impl->space();
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_TRUE(r.value.is_object());
LOGOS_ASSERT_FALSE(r.value.empty());
LOGOS_ASSERT_TRUE(r.value.contains("totalBlocks"));
LOGOS_ASSERT_TRUE(r.value.contains("quotaMaxBytes"));
impl->destroy();
delete impl;
}
// manifests
LOGOS_TEST(manifests_returns_parsed_list) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_list")
.returns(R"([{"cid":"QmABC","manifest":{"treeCid":"QmTree","datasetSize":1024,"blockSize":64,"filename":"test.txt","mimetype":"text/plain"}}])");
StdLogosResult r = impl->manifests();
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_TRUE(r.value.is_array());
LOGOS_ASSERT_EQ(static_cast<int>(r.value.size()), 1);
impl->destroy();
delete impl;
}
// downloadManifest
LOGOS_TEST(downloadManifest_dispatches_and_emits_event) {
logos_test::EventCapture events;
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_download_manifest")
.returns(R"({"treeCid":"QmTree","datasetSize":2048,"blockSize":64,"filename":"data.bin","mimetype":"application/octet-stream"})");
StdLogosResult r = impl->downloadManifest("QmSomeCid");
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_TRUE(events.has("storageDownloadManifestDone"));
impl->destroy();
delete impl;
}
// uploadInit / uploadFinalize / uploadCancel
LOGOS_TEST(uploadInit_returns_session_id) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_upload_init").returns("session-abc-123");
StdLogosResult r = impl->uploadInit("test.txt", 65536);
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_EQ(r.value.get<std::string>(), std::string("session-abc-123"));
impl->destroy();
delete impl;
}
LOGOS_TEST(uploadFinalize_returns_cid) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
t.mockCFunction("storage_upload_finalize").returns("QmFinalCid");
StdLogosResult r = impl->uploadFinalize("session-abc-123");
LOGOS_ASSERT_TRUE(r.success);
LOGOS_ASSERT_EQ(r.value.get<std::string>(), std::string("QmFinalCid"));
LOGOS_ASSERT(t.cFunctionCalled("storage_upload_finalize"));
impl->destroy();
delete impl;
}
LOGOS_TEST(uploadCancel_returns_true) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
LOGOS_ASSERT_TRUE(impl->uploadCancel("session-abc-123").success);
LOGOS_ASSERT(t.cFunctionCalled("storage_upload_cancel"));
impl->destroy();
delete impl;
}
// uploadUrl input validation
LOGOS_TEST(uploadUrl_fails_with_nonexistent_file) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
StdLogosResult r = impl->uploadUrl("/nonexistent/path/file.txt", 65536);
LOGOS_ASSERT_FALSE(r.success);
impl->destroy();
delete impl;
}
LOGOS_TEST(uploadUrl_fails_with_zero_chunk_size) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
StdLogosResult r = impl->uploadUrl("/tmp/test.txt", 0);
LOGOS_ASSERT_FALSE(r.success);
impl->destroy();
delete impl;
}
// downloadCancel
LOGOS_TEST(downloadCancel_returns_true) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
LOGOS_ASSERT_TRUE(impl->downloadCancel("QmSomeCid").success);
LOGOS_ASSERT(t.cFunctionCalled("storage_download_cancel"));
impl->destroy();
delete impl;
}
LOGOS_TEST(downloadChunks_cancels_session_when_stream_fails) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
// storage_download_init succeeds, but the stream dispatch fails: the
// already-open session must be cancelled and the call must report failure.
t.mockCFunction("storage_download_stream").returns(1);
StdLogosResult r = impl->downloadChunks("QmSomeCid", false, 65536);
LOGOS_ASSERT_FALSE(r.success);
LOGOS_ASSERT(t.cFunctionCalled("storage_download_cancel"));
impl->destroy();
delete impl;
}
// connect
LOGOS_TEST(connect_fails_without_init) {
auto t = LogosTestContext("storage_module");
StorageModuleImpl impl;
LOGOS_ASSERT_FALSE(impl.connect("QmPeer", {"addr1"}).success);
}
LOGOS_TEST(connect_succeeds_after_init) {
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
LOGOS_ASSERT_TRUE(impl->connect("QmPeer", {"/ip4/127.0.0.1/tcp/1234"}).success);
LOGOS_ASSERT(t.cFunctionCalled("storage_connect"));
impl->destroy();
delete impl;
}
// Event wiring — typed `logos_events:` methods are forwarded to
// logos_test::recordEvent by tests/storage_events_test.cpp, so tests observe
// them via EventCapture. Construct the capture before the impl so it outlives
// any background thread the impl's destructor joins.
LOGOS_TEST(start_emits_storageStart_event) {
logos_test::EventCapture events;
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
LOGOS_ASSERT_TRUE(impl->start());
LOGOS_ASSERT_TRUE(events.has("storageStart"));
impl->destroy();
delete impl;
}
LOGOS_TEST(stop_emits_storageStop_event) {
logos_test::EventCapture events;
auto t = LogosTestContext("storage_module");
auto* impl = createInitializedImpl(t);
LOGOS_ASSERT_TRUE(impl->stop().success);
LOGOS_ASSERT_TRUE(events.has("storageStop"));
impl->destroy();
delete impl;
}