mirror of
https://github.com/logos-co/logos-storage-module.git
synced 2026-08-31 12:21:10 +00:00
`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>
649 lines
22 KiB
C++
649 lines
22 KiB
C++
// Integration tests for StorageModuleImpl - uses the REAL libstorage library.
|
|
// No mocking. These tests start an actual storage node, upload/download real data,
|
|
// and verify end-to-end behavior.
|
|
//
|
|
// Requires libstorage to be available in ../lib at build time.
|
|
// Skipped automatically when libstorage is not found.
|
|
|
|
#include <logos_test.h>
|
|
#include "storage_module_plugin.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
|
|
namespace fs = std::filesystem;
|
|
using json = nlohmann::json;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// base64 decode — mirrors the base64Encode helper in storage_module_plugin.cpp.
|
|
// Chunk data arriving in storageDownloadProgress events is base64-encoded so
|
|
// that arbitrary binary bytes can be safely embedded in a JSON string.
|
|
// ---------------------------------------------------------------------------
|
|
static std::string base64Decode(const std::string& in) {
|
|
static const int kDecTable[256] = {
|
|
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
|
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
|
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
|
|
52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,
|
|
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
|
|
15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
|
|
-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
|
|
41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,
|
|
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
|
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
|
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
|
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
|
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
|
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
|
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
|
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
|
};
|
|
std::string out;
|
|
out.reserve((in.size() / 4) * 3);
|
|
int val = 0, bits = -8;
|
|
for (unsigned char c : in) {
|
|
if (kDecTable[c] == -1) break;
|
|
val = (val << 6) + kDecTable[c];
|
|
bits += 6;
|
|
if (bits >= 0) {
|
|
out += static_cast<char>((val >> bits) & 0xFF);
|
|
bits -= 8;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static const int DEFAULT_TIMEOUT_MS = 3000;
|
|
static const int START_TIMEOUT_MS = 5000;
|
|
static const std::string LOG_FILENAME = "storage.log";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// EventWaiter - replaces QEventLoop + storageResponse signal.
|
|
// Collects events emitted via the typed `logos_events:` methods on
|
|
// StorageModuleImpl. The Qt-free test forwarders in
|
|
// `tests/storage_events_test.cpp` route each event through
|
|
// logos_test::recordEvent(name, payload); here we install a ScopedEventSink
|
|
// that fans them into the same (name, data) pair the existing assertions read.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Events are identified by the typed `logos_events:` method, exactly as the
|
|
// plugin emits them in storage_module_plugin.cpp (e.g.
|
|
// `&StorageModuleImpl::storageUploadDone`). The Qt-free forwarders in
|
|
// storage_events_test.cpp record each one under its method name, so this is
|
|
// the single place that maps the typed identity back to that name.
|
|
using StorageEvent = void (StorageModuleImpl::*)(const std::string&);
|
|
|
|
static const char* eventName(StorageEvent e) {
|
|
if (e == &StorageModuleImpl::storageStart) return "storageStart";
|
|
if (e == &StorageModuleImpl::storageStop) return "storageStop";
|
|
if (e == &StorageModuleImpl::storageConnect) return "storageConnect";
|
|
if (e == &StorageModuleImpl::storageUploadProgress) return "storageUploadProgress";
|
|
if (e == &StorageModuleImpl::storageUploadDone) return "storageUploadDone";
|
|
if (e == &StorageModuleImpl::storageDownloadProgress) return "storageDownloadProgress";
|
|
if (e == &StorageModuleImpl::storageDownloadDone) return "storageDownloadDone";
|
|
if (e == &StorageModuleImpl::storageDownloadManifestDone) return "storageDownloadManifestDone";
|
|
if (e == &StorageModuleImpl::storageRemoveDone) return "storageRemoveDone";
|
|
|
|
return "";
|
|
}
|
|
|
|
struct EventWaiter {
|
|
std::mutex mtx;
|
|
std::condition_variable cv;
|
|
std::string lastEventName;
|
|
std::string lastEventData;
|
|
bool received = false;
|
|
std::unique_ptr<logos_test::ScopedEventSink> sink;
|
|
|
|
// Install — must be called once per impl instance. Replaces any previous
|
|
// sink installed by an earlier EventWaiter / collectDownloadChunks call.
|
|
// Destroy the old sink *before* creating the new one: ScopedEventSink is
|
|
// RAII over a single global slot, so constructing the replacement first
|
|
// and tearing the old one down after would leave the old destructor
|
|
// clearing the slot we just registered — silently dropping every event of
|
|
// the next node (the cause of restart timeouts).
|
|
void install(StorageModuleImpl* /*impl*/) {
|
|
sink.reset();
|
|
sink = std::make_unique<logos_test::ScopedEventSink>(
|
|
[this](const std::string& name, const std::string& data) {
|
|
std::unique_lock<std::mutex> lock(mtx);
|
|
lastEventName = name;
|
|
lastEventData = data;
|
|
received = true;
|
|
cv.notify_all();
|
|
});
|
|
}
|
|
|
|
// Reset before waiting for the next event.
|
|
void reset() {
|
|
std::unique_lock<std::mutex> lock(mtx);
|
|
received = false;
|
|
lastEventName.clear();
|
|
lastEventData.clear();
|
|
}
|
|
|
|
// Wait for the given typed event within timeoutMs.
|
|
bool waitFor(StorageEvent event, int timeoutMs) {
|
|
const std::string name = eventName(event);
|
|
std::unique_lock<std::mutex> lock(mtx);
|
|
bool ok = cv.wait_for(lock, std::chrono::milliseconds(timeoutMs),
|
|
[&] { return received && lastEventName == name; });
|
|
return ok;
|
|
}
|
|
|
|
// Non-blocking: returns the last event data string (must be called under lock or after wait).
|
|
std::string data() {
|
|
std::unique_lock<std::mutex> lock(mtx);
|
|
return lastEventData;
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared impl instance - restarted before each test.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static StorageModuleImpl* g_impl = nullptr;
|
|
static fs::path g_dataDir;
|
|
static EventWaiter g_waiter;
|
|
|
|
static void ensureRestarted(const json& extraConfig = json::object()) {
|
|
if (g_impl) {
|
|
g_impl->stop();
|
|
g_waiter.reset();
|
|
g_waiter.waitFor(&StorageModuleImpl::storageStop, DEFAULT_TIMEOUT_MS);
|
|
g_impl->destroy();
|
|
delete g_impl;
|
|
g_impl = nullptr;
|
|
g_dataDir.clear();
|
|
}
|
|
|
|
g_dataDir = fs::temp_directory_path() /
|
|
("logos-storage-integration-test-" +
|
|
std::to_string(
|
|
std::chrono::steady_clock::now().time_since_epoch().count()));
|
|
fs::create_directories(g_dataDir);
|
|
|
|
std::string logFile = (g_dataDir / LOG_FILENAME).string();
|
|
|
|
g_impl = new StorageModuleImpl();
|
|
g_waiter.install(g_impl);
|
|
|
|
json cfg = {
|
|
{"data-dir", g_dataDir.string()},
|
|
{"log-level", "DEBUG"},
|
|
{"nat", "extip:127.0.0.1"},
|
|
{"log-file", logFile},
|
|
};
|
|
cfg.update(extraConfig);
|
|
std::string config = cfg.dump();
|
|
|
|
if (!g_impl->init(config)) {
|
|
throw LogosTestFailure("Failed to init storage impl.");
|
|
}
|
|
|
|
g_waiter.reset();
|
|
if (!g_impl->start()) {
|
|
throw LogosTestFailure("Failed to start storage impl.");
|
|
}
|
|
|
|
if (!g_waiter.waitFor(&StorageModuleImpl::storageStart, START_TIMEOUT_MS)) {
|
|
throw LogosTestFailure("Storage node did not start within timeout.");
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper: write a file and upload it, returning the CID.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static std::string uploadContent(const std::string& content,
|
|
const std::string& filename) {
|
|
fs::path filePath = g_dataDir / filename;
|
|
std::ofstream f(filePath, std::ios::binary);
|
|
f.write(content.data(), static_cast<std::streamsize>(content.size()));
|
|
f.close();
|
|
|
|
g_waiter.reset();
|
|
StdLogosResult sr = g_impl->uploadUrl(filePath.string(), 65536);
|
|
if (!sr.success) return {};
|
|
|
|
if (!g_waiter.waitFor(&StorageModuleImpl::storageUploadDone, DEFAULT_TIMEOUT_MS)) return {};
|
|
|
|
// Extract CID from JSON payload: {"success":true,"sessionId":"...","cid":"..."}
|
|
std::string d = g_waiter.data();
|
|
auto cidPos = d.find("\"cid\":\"");
|
|
if (cidPos == std::string::npos) return {};
|
|
cidPos += 7;
|
|
auto cidEnd = d.find('"', cidPos);
|
|
if (cidEnd == std::string::npos) return {};
|
|
return d.substr(cidPos, cidEnd - cidPos);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper: collect download chunks until storageDownloadDone.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static std::string collectDownloadChunks(int timeoutMs) {
|
|
std::string collected;
|
|
std::mutex m;
|
|
std::condition_variable cv;
|
|
bool done = false;
|
|
bool success = false;
|
|
|
|
// Replace the global EventWaiter sink for this call only; restored at
|
|
// the end via g_waiter.install(g_impl).
|
|
logos_test::ScopedEventSink localSink(
|
|
[&](const std::string& name, const std::string& data) {
|
|
if (name == "storageDownloadProgress") {
|
|
// Extract chunk field from JSON payload.
|
|
// The chunk is base64-encoded; decode it before accumulating.
|
|
auto pos = data.find("\"chunk\":\"");
|
|
if (pos != std::string::npos) {
|
|
pos += 9;
|
|
auto end = data.find('"', pos);
|
|
if (end != std::string::npos) {
|
|
collected += base64Decode(data.substr(pos, end - pos));
|
|
}
|
|
}
|
|
} else if (name == "storageDownloadDone") {
|
|
success = data.find("\"success\":true") != std::string::npos;
|
|
std::unique_lock<std::mutex> lock(m);
|
|
done = true;
|
|
cv.notify_all();
|
|
}
|
|
});
|
|
|
|
std::unique_lock<std::mutex> lock(m);
|
|
cv.wait_for(lock, std::chrono::milliseconds(timeoutMs),
|
|
[&] { return done; });
|
|
|
|
// Restore normal waiter.
|
|
g_waiter.install(g_impl);
|
|
|
|
return success ? collected : std::string();
|
|
}
|
|
|
|
// integration_version
|
|
|
|
LOGOS_TEST(init_multiple_times) {
|
|
std::string g_dataDir = fs::temp_directory_path() /
|
|
("logos-storage-integration-test-" +
|
|
std::to_string(
|
|
std::chrono::steady_clock::now().time_since_epoch().count()));
|
|
|
|
g_impl = new StorageModuleImpl();
|
|
g_waiter.install(g_impl);
|
|
|
|
json cfg = {
|
|
{"data-dir", g_dataDir},
|
|
{"log-level", "DEBUG"},
|
|
{"nat", "extip:127.0.0.1"},
|
|
};
|
|
|
|
std::string config = cfg.dump();
|
|
|
|
if (!g_impl->init(config)) {
|
|
throw LogosTestFailure("Failed to init storage impl.");
|
|
}
|
|
|
|
// It will not re-initialize if already initialized.
|
|
// So the init function should return false to indicate that
|
|
// no init was done but the call itself should not fail.
|
|
if (g_impl->init(config)) {
|
|
throw LogosTestFailure("Failed to init storage impl.");
|
|
}
|
|
|
|
// The call to destroy should succeed.
|
|
StdLogosResult result = g_impl->destroy();
|
|
if (!result.success) {
|
|
throw LogosTestFailure("Failed to destroy storage impl.");
|
|
}
|
|
|
|
delete g_impl;
|
|
g_impl = nullptr;
|
|
}
|
|
|
|
// integration_libstorageVersion
|
|
|
|
LOGOS_TEST(integration_libstorageVersion) {
|
|
ensureRestarted();
|
|
StdLogosResult r = g_impl->libstorageVersion();
|
|
LOGOS_ASSERT_TRUE(r.success);
|
|
LOGOS_ASSERT_FALSE(r.value.get<std::string>().empty());
|
|
}
|
|
|
|
// integration_dataDir
|
|
|
|
LOGOS_TEST(integration_dataDir) {
|
|
ensureRestarted();
|
|
StdLogosResult r = g_impl->dataDir();
|
|
LOGOS_ASSERT_TRUE(r.success);
|
|
LOGOS_ASSERT_EQ(r.value.get<std::string>(), g_dataDir.string());
|
|
}
|
|
|
|
// integration_peerId
|
|
|
|
LOGOS_TEST(integration_peerId) {
|
|
ensureRestarted();
|
|
StdLogosResult r = g_impl->peerId();
|
|
LOGOS_ASSERT_TRUE(r.success);
|
|
LOGOS_ASSERT_FALSE(r.value.get<std::string>().empty());
|
|
}
|
|
|
|
// integration_debug
|
|
|
|
LOGOS_TEST(integration_debug) {
|
|
ensureRestarted();
|
|
StdLogosResult r = g_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"));
|
|
LOGOS_ASSERT_TRUE(r.value.contains("addrs"));
|
|
LOGOS_ASSERT_TRUE(r.value.contains("providerAddresses"));
|
|
LOGOS_ASSERT_TRUE(r.value.contains("table"));
|
|
}
|
|
|
|
// integration_collectMetrics
|
|
|
|
LOGOS_TEST(integration_collectMetrics) {
|
|
ensureRestarted();
|
|
LogosMap r = g_impl->collectMetrics();
|
|
LOGOS_ASSERT_TRUE(r.is_object());
|
|
LOGOS_ASSERT_TRUE(r.contains("metrics"));
|
|
LOGOS_ASSERT_TRUE(r["metrics"].is_array());
|
|
if (!r["metrics"].empty()) {
|
|
const auto& metric = r["metrics"][0];
|
|
LOGOS_ASSERT_TRUE(metric.is_object());
|
|
LOGOS_ASSERT_TRUE(metric.contains("name"));
|
|
LOGOS_ASSERT_TRUE(metric.contains("type"));
|
|
LOGOS_ASSERT_TRUE(metric.contains("help"));
|
|
LOGOS_ASSERT_TRUE(metric.contains("value"));
|
|
LOGOS_ASSERT_TRUE(metric.contains("labels"));
|
|
LOGOS_ASSERT_TRUE(metric["name"].is_string());
|
|
LOGOS_ASSERT_TRUE(metric["type"].is_string());
|
|
LOGOS_ASSERT_TRUE(metric["help"].is_string());
|
|
LOGOS_ASSERT_TRUE(metric["value"].is_number());
|
|
LOGOS_ASSERT_TRUE(metric["labels"].is_object());
|
|
}
|
|
}
|
|
|
|
// integration_spr
|
|
|
|
LOGOS_TEST(integration_spr) {
|
|
ensureRestarted();
|
|
StdLogosResult r = g_impl->spr();
|
|
LOGOS_ASSERT_TRUE(r.success);
|
|
LOGOS_ASSERT_FALSE(r.value.get<std::string>().empty());
|
|
}
|
|
|
|
// integration_uploadFile
|
|
|
|
LOGOS_TEST(integration_uploadFile) {
|
|
ensureRestarted();
|
|
std::string cid = uploadContent("Hello, Logos Storage!", "test_upload.txt");
|
|
LOGOS_ASSERT_FALSE(cid.empty());
|
|
}
|
|
|
|
// integration_uploadWorkflowManual
|
|
|
|
LOGOS_TEST(integration_uploadWorkflowManual) {
|
|
ensureRestarted();
|
|
|
|
fs::path filePath = g_dataDir / "test_manual_upload.txt";
|
|
std::string content = "Hello, Logos Storage! Manual upload test.";
|
|
std::ofstream f(filePath, std::ios::binary);
|
|
f.write(content.data(), static_cast<std::streamsize>(content.size()));
|
|
f.close();
|
|
|
|
StdLogosResult initR = g_impl->uploadInit(filePath.string(), 65536);
|
|
LOGOS_ASSERT_TRUE(initR.success);
|
|
std::string sid = initR.value.get<std::string>();
|
|
LOGOS_ASSERT_FALSE(sid.empty());
|
|
|
|
LOGOS_ASSERT_TRUE(g_impl->uploadChunk(sid, content).success);
|
|
|
|
StdLogosResult finalizeR = g_impl->uploadFinalize(sid);
|
|
LOGOS_ASSERT_TRUE(finalizeR.success);
|
|
LOGOS_ASSERT_FALSE(finalizeR.value.get<std::string>().empty());
|
|
}
|
|
|
|
// integration_downloadFile
|
|
|
|
LOGOS_TEST(integration_downloadFile) {
|
|
ensureRestarted();
|
|
|
|
std::string content = "Hello, Logos Download Test!";
|
|
std::string cid = uploadContent(content, "test_download.txt");
|
|
LOGOS_ASSERT_FALSE(cid.empty());
|
|
|
|
fs::path downloadPath = g_dataDir / "test_download_result.txt";
|
|
|
|
g_waiter.reset();
|
|
StdLogosResult dlR = g_impl->downloadToUrl(cid, downloadPath.string(), false, 65536);
|
|
LOGOS_ASSERT_TRUE(dlR.success);
|
|
|
|
LOGOS_ASSERT_TRUE(g_waiter.waitFor(&StorageModuleImpl::storageDownloadDone, DEFAULT_TIMEOUT_MS));
|
|
|
|
std::ifstream in(downloadPath, std::ios::binary);
|
|
std::string downloaded((std::istreambuf_iterator<char>(in)),
|
|
std::istreambuf_iterator<char>());
|
|
LOGOS_ASSERT_EQ(downloaded, content);
|
|
}
|
|
|
|
// integration_downloadChunks
|
|
|
|
LOGOS_TEST(integration_downloadChunks) {
|
|
ensureRestarted();
|
|
|
|
std::string content = "Hello, Logos Chunks Download Test!";
|
|
std::string cid = uploadContent(content, "test_chunks_src.txt");
|
|
LOGOS_ASSERT_FALSE(cid.empty());
|
|
|
|
// collectDownloadChunks installs a local ScopedEventSink for the duration
|
|
// of the call and restores the global EventWaiter sink before returning.
|
|
StdLogosResult dlR = g_impl->downloadChunks(cid, false, 65536);
|
|
LOGOS_ASSERT_TRUE(dlR.success);
|
|
|
|
std::string downloaded = collectDownloadChunks(DEFAULT_TIMEOUT_MS);
|
|
LOGOS_ASSERT_FALSE(downloaded.empty());
|
|
LOGOS_ASSERT_EQ(downloaded, content);
|
|
}
|
|
|
|
// integration_exists
|
|
|
|
LOGOS_TEST(integration_exists) {
|
|
ensureRestarted();
|
|
|
|
std::string cid = uploadContent("Hello, Logos Exists Test!", "test_exists_src.txt");
|
|
LOGOS_ASSERT_FALSE(cid.empty());
|
|
|
|
StdLogosResult r = g_impl->exists(cid);
|
|
LOGOS_ASSERT_TRUE(r.success);
|
|
LOGOS_ASSERT_TRUE(r.value.get<bool>());
|
|
}
|
|
|
|
// integration_fetch
|
|
|
|
LOGOS_TEST(integration_fetch) {
|
|
ensureRestarted();
|
|
|
|
std::string cid = uploadContent("Hello, Logos Fetch Test!", "test_fetch_src.txt");
|
|
LOGOS_ASSERT_FALSE(cid.empty());
|
|
|
|
LOGOS_ASSERT_TRUE(g_impl->fetch(cid).success);
|
|
}
|
|
|
|
// integration_remove
|
|
|
|
LOGOS_TEST(integration_remove) {
|
|
ensureRestarted();
|
|
|
|
std::string cid = uploadContent("Hello, Logos Remove Test!", "test_remove_src.txt");
|
|
LOGOS_ASSERT_FALSE(cid.empty());
|
|
|
|
StdLogosResult e1 = g_impl->exists(cid);
|
|
LOGOS_ASSERT_TRUE(e1.success);
|
|
LOGOS_ASSERT_TRUE(e1.value.get<bool>());
|
|
|
|
g_waiter.reset();
|
|
LOGOS_ASSERT_TRUE(g_impl->remove(cid).success);
|
|
|
|
LOGOS_ASSERT_TRUE(g_waiter.waitFor(&StorageModuleImpl::storageRemoveDone, DEFAULT_TIMEOUT_MS));
|
|
|
|
json payload = json::parse(g_waiter.data());
|
|
LOGOS_ASSERT_TRUE(payload["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(payload["cid"].get<std::string>(), cid);
|
|
|
|
StdLogosResult e2 = g_impl->exists(cid);
|
|
LOGOS_ASSERT_TRUE(e2.success);
|
|
LOGOS_ASSERT_FALSE(e2.value.get<bool>());
|
|
}
|
|
|
|
// integration_space
|
|
|
|
LOGOS_TEST(integration_space) {
|
|
ensureRestarted();
|
|
|
|
StdLogosResult r = g_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"));
|
|
LOGOS_ASSERT_TRUE(r.value.contains("quotaUsedBytes"));
|
|
LOGOS_ASSERT_TRUE(r.value.contains("quotaReservedBytes"));
|
|
}
|
|
|
|
// integration_manifests
|
|
|
|
LOGOS_TEST(integration_manifests) {
|
|
ensureRestarted();
|
|
|
|
std::string content = "Hello, Logos Manifests Test!";
|
|
std::string cid = uploadContent(content, "test_manifests_src.txt");
|
|
LOGOS_ASSERT_FALSE(cid.empty());
|
|
|
|
StdLogosResult lr = g_impl->manifests();
|
|
LOGOS_ASSERT_TRUE(lr.success);
|
|
LOGOS_ASSERT_TRUE(lr.value.is_array());
|
|
LOGOS_ASSERT_FALSE(lr.value.empty());
|
|
|
|
bool found = false;
|
|
for (const auto& entry : lr.value) {
|
|
if (!entry.contains("cid") || entry["cid"].get<std::string>() != cid) continue;
|
|
found = true;
|
|
LOGOS_ASSERT_TRUE(entry.contains("treeCid"));
|
|
LOGOS_ASSERT_FALSE(entry["treeCid"].get<std::string>().empty());
|
|
break;
|
|
}
|
|
LOGOS_ASSERT_TRUE(found);
|
|
}
|
|
|
|
// integration_downloadManifest
|
|
|
|
LOGOS_TEST(integration_downloadManifest) {
|
|
ensureRestarted();
|
|
|
|
std::string content = "Hello, Logos DownloadManifest Test!";
|
|
std::string cid = uploadContent(content, "test_download_manifest_src.txt");
|
|
LOGOS_ASSERT_FALSE(cid.empty());
|
|
|
|
g_waiter.reset();
|
|
StdLogosResult mr = g_impl->downloadManifest(cid);
|
|
LOGOS_ASSERT_TRUE(mr.success);
|
|
|
|
LOGOS_ASSERT_TRUE(g_waiter.waitFor(&StorageModuleImpl::storageDownloadManifestDone, DEFAULT_TIMEOUT_MS));
|
|
|
|
json payload = json::parse(g_waiter.data());
|
|
LOGOS_ASSERT_TRUE(payload["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(payload["cid"].get<std::string>(), cid);
|
|
const auto& manifest = payload["manifest"];
|
|
LOGOS_ASSERT_TRUE(manifest.is_object());
|
|
LOGOS_ASSERT_TRUE(manifest.contains("treeCid"));
|
|
LOGOS_ASSERT_TRUE(manifest.contains("datasetSize"));
|
|
}
|
|
|
|
// integration_updateLogLevel
|
|
|
|
LOGOS_TEST(integration_updateLogLevel) {
|
|
ensureRestarted();
|
|
|
|
LOGOS_ASSERT_TRUE(g_impl->updateLogLevel("TRACE").success);
|
|
|
|
// Upload a file to generate TRACE logs.
|
|
uploadContent("Hello, Logos Log Level Test!", "test_loglevel_src.txt");
|
|
|
|
fs::path logFile = g_dataDir / LOG_FILENAME;
|
|
std::ifstream in(logFile);
|
|
std::string logContent((std::istreambuf_iterator<char>(in)),
|
|
std::istreambuf_iterator<char>());
|
|
|
|
LOGOS_ASSERT_FALSE(logContent.empty());
|
|
LOGOS_ASSERT_TRUE(logContent.find("TRC") != std::string::npos);
|
|
}
|
|
|
|
// integration_togglePrivateQueries_withoutMix
|
|
//
|
|
// This test verifies that private queries cannot be enabled without Mix being
|
|
// configured.
|
|
|
|
LOGOS_TEST(integration_togglePrivateQueries_withoutMix) {
|
|
ensureRestarted();
|
|
|
|
StdLogosResult sprRes = g_impl->spr();
|
|
LOGOS_ASSERT_TRUE(sprRes.success);
|
|
std::string proxySpr = sprRes.value.get<std::string>();
|
|
|
|
ensureRestarted({
|
|
{"mix-enabled", false},
|
|
{"dht-mix-proxy", json::array({proxySpr})},
|
|
});
|
|
|
|
// Enabling fails: Mix is not configured.
|
|
StdLogosResult on = g_impl->togglePrivateQueries(true);
|
|
LOGOS_ASSERT_FALSE(on.success);
|
|
|
|
// Disabling is always allowed and reports the previous state (off).
|
|
StdLogosResult off = g_impl->togglePrivateQueries(false);
|
|
LOGOS_ASSERT_TRUE(off.success);
|
|
LOGOS_ASSERT_FALSE(off.value.get<bool>());
|
|
}
|
|
|
|
// integration_togglePrivateQueries_withMixEnabled
|
|
//
|
|
// This test verifies that private queries can be enabled and disabled when Mix
|
|
// is configured.
|
|
|
|
LOGOS_TEST(integration_togglePrivateQueries_withMixEnabled) {
|
|
ensureRestarted();
|
|
|
|
StdLogosResult sprRes = g_impl->spr();
|
|
LOGOS_ASSERT_TRUE(sprRes.success);
|
|
std::string proxySpr = sprRes.value.get<std::string>();
|
|
|
|
ensureRestarted({
|
|
{"mix-enabled", true},
|
|
{"dht-mix-proxy", json::array({proxySpr})},
|
|
});
|
|
|
|
// Mix configured: private queries default on, so the first disable reports
|
|
// the previous state as on.
|
|
StdLogosResult off = g_impl->togglePrivateQueries(false);
|
|
LOGOS_ASSERT_TRUE(off.success);
|
|
LOGOS_ASSERT_TRUE(off.value.get<bool>());
|
|
|
|
// Re-enabling now succeeds (Mix is configured) and reports previous = off.
|
|
StdLogosResult on = g_impl->togglePrivateQueries(true);
|
|
LOGOS_ASSERT_TRUE(on.success);
|
|
LOGOS_ASSERT_FALSE(on.value.get<bool>());
|
|
}
|