2026-06-12 18:59:01 -03:00
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
|
|
|
|
#include "logos_protocol.h"
|
|
|
|
|
#include "logos_mock.h"
|
|
|
|
|
|
|
|
|
|
#include <QByteArray>
|
|
|
|
|
#include <QCoreApplication>
|
|
|
|
|
#include <QElapsedTimer>
|
|
|
|
|
#include <QVariant>
|
|
|
|
|
|
|
|
|
|
#include <atomic>
|
|
|
|
|
#include <string>
|
2026-07-22 17:44:52 -03:00
|
|
|
#include <thread>
|
2026-06-12 18:59:01 -03:00
|
|
|
|
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
|
|
|
|
|
|
// Exercises the consumer C ABI end-to-end over the mock transport — the
|
|
|
|
|
// same calls any non-C++ SDK makes. No C++ SDK types cross these tests'
|
|
|
|
|
// assertions: everything goes through lp_* and JSON strings.
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
struct LpClientGuard {
|
|
|
|
|
explicit LpClientGuard(lp_client* c) : client(c) {}
|
|
|
|
|
~LpClientGuard() { lp_client_destroy(client); }
|
|
|
|
|
lp_client* client;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
nlohmann::json parsed(const char* json)
|
|
|
|
|
{
|
|
|
|
|
return nlohmann::json::parse(json, nullptr, /*allow_exceptions=*/false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
class LpClientTest : public ::testing::Test {
|
|
|
|
|
protected:
|
|
|
|
|
void SetUp() override { m_mock = new LogosMockSetup(); }
|
|
|
|
|
void TearDown() override { delete m_mock; }
|
|
|
|
|
LogosMockSetup* m_mock = nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
TEST_F(LpClientTest, ModeRoundTrip)
|
|
|
|
|
{
|
|
|
|
|
// LogosMockSetup already switched to mock; the C ABI must see it.
|
|
|
|
|
EXPECT_STREQ(lp_get_mode(), "mock");
|
|
|
|
|
EXPECT_EQ(lp_set_mode("local"), LP_OK);
|
|
|
|
|
EXPECT_STREQ(lp_get_mode(), "local");
|
|
|
|
|
EXPECT_EQ(lp_set_mode("mock"), LP_OK);
|
|
|
|
|
EXPECT_EQ(lp_set_mode("bogus"), LP_ERR_INVALID_ARG);
|
|
|
|
|
EXPECT_EQ(lp_set_mode(nullptr), LP_ERR_INVALID_ARG);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(LpClientTest, InvokeReturnsMockedValue)
|
|
|
|
|
{
|
|
|
|
|
m_mock->when("test_module", "getValue").thenReturn(QVariant(42));
|
|
|
|
|
|
|
|
|
|
lp_client* client = lp_client_create("test_module", "origin", nullptr, nullptr);
|
|
|
|
|
ASSERT_NE(client, nullptr);
|
|
|
|
|
LpClientGuard guard(client);
|
|
|
|
|
|
|
|
|
|
char* result = nullptr;
|
|
|
|
|
char* error = nullptr;
|
|
|
|
|
ASSERT_EQ(lp_invoke(client, "getValue", nullptr, 0, &result, &error), LP_OK);
|
|
|
|
|
EXPECT_EQ(error, nullptr);
|
|
|
|
|
ASSERT_NE(result, nullptr);
|
|
|
|
|
|
|
|
|
|
nlohmann::json j = parsed(result);
|
|
|
|
|
ASSERT_TRUE(j.is_number());
|
|
|
|
|
EXPECT_DOUBLE_EQ(j.get<double>(), 42.0);
|
|
|
|
|
lp_string_free(result);
|
|
|
|
|
|
|
|
|
|
EXPECT_TRUE(m_mock->wasCalled("test_module", "getValue"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(LpClientTest, InvokePassesJsonArgs)
|
|
|
|
|
{
|
|
|
|
|
m_mock->when("mod", "echo").thenReturn(QVariant("hello back"));
|
|
|
|
|
|
|
|
|
|
lp_client* client = lp_client_create("mod", "origin", nullptr, nullptr);
|
|
|
|
|
ASSERT_NE(client, nullptr);
|
|
|
|
|
LpClientGuard guard(client);
|
|
|
|
|
|
|
|
|
|
char* result = nullptr;
|
|
|
|
|
ASSERT_EQ(lp_invoke(client, "echo", R"(["hello", 7, true])", 0, &result, nullptr),
|
|
|
|
|
LP_OK);
|
|
|
|
|
ASSERT_NE(result, nullptr);
|
|
|
|
|
EXPECT_EQ(parsed(result).get<std::string>(), "hello back");
|
|
|
|
|
lp_string_free(result);
|
|
|
|
|
|
|
|
|
|
const QVariantList args = m_mock->lastArgs("mod", "echo");
|
|
|
|
|
ASSERT_EQ(args.size(), 3);
|
|
|
|
|
EXPECT_EQ(args[0].toString(), "hello");
|
|
|
|
|
EXPECT_EQ(args[1].toLongLong(), 7);
|
|
|
|
|
EXPECT_EQ(args[2].toBool(), true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(LpClientTest, BytesRoundTripThroughTheCAbi)
|
|
|
|
|
{
|
|
|
|
|
// {"_bytes": base64url} in args must reach the callee as QByteArray,
|
|
|
|
|
// and QByteArray results must come back tagged — NUL included.
|
|
|
|
|
const QByteArray payload("x\0y", 3);
|
|
|
|
|
m_mock->when("mod", "store").thenReturn(QVariant(payload));
|
|
|
|
|
|
|
|
|
|
lp_client* client = lp_client_create("mod", "origin", nullptr, nullptr);
|
|
|
|
|
ASSERT_NE(client, nullptr);
|
|
|
|
|
LpClientGuard guard(client);
|
|
|
|
|
|
|
|
|
|
char* result = nullptr;
|
|
|
|
|
ASSERT_EQ(lp_invoke(client, "store", R"([{"_bytes":"eAB5"}])", 0, &result, nullptr),
|
|
|
|
|
LP_OK);
|
|
|
|
|
ASSERT_NE(result, nullptr);
|
|
|
|
|
|
|
|
|
|
nlohmann::json j = parsed(result);
|
|
|
|
|
ASSERT_TRUE(j.is_object());
|
|
|
|
|
ASSERT_TRUE(j.contains("_bytes"));
|
|
|
|
|
EXPECT_EQ(j["_bytes"].get<std::string>(), "eAB5"); // "x\0y" base64url
|
|
|
|
|
lp_string_free(result);
|
|
|
|
|
|
|
|
|
|
const QVariantList args = m_mock->lastArgs("mod", "store");
|
|
|
|
|
ASSERT_EQ(args.size(), 1);
|
|
|
|
|
EXPECT_EQ(args[0].toByteArray(), payload);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(LpClientTest, InvokeAsyncDeliversResult)
|
|
|
|
|
{
|
|
|
|
|
m_mock->when("mod", "compute").thenReturn(QVariant(7));
|
|
|
|
|
|
|
|
|
|
lp_client* client = lp_client_create("mod", "origin", nullptr, nullptr);
|
|
|
|
|
ASSERT_NE(client, nullptr);
|
|
|
|
|
LpClientGuard guard(client);
|
|
|
|
|
|
|
|
|
|
struct Capture {
|
|
|
|
|
std::atomic<bool> done{false};
|
|
|
|
|
int ok = 0;
|
|
|
|
|
std::string json;
|
|
|
|
|
} capture;
|
|
|
|
|
|
|
|
|
|
auto cb = [](int ok, const char* json, void* user_data) {
|
|
|
|
|
auto* c = static_cast<Capture*>(user_data);
|
|
|
|
|
c->ok = ok;
|
|
|
|
|
c->json = json ? json : "";
|
|
|
|
|
c->done = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ASSERT_EQ(lp_invoke_async(client, "compute", "[]", 0, cb, &capture), LP_OK);
|
|
|
|
|
|
|
|
|
|
QElapsedTimer timer;
|
|
|
|
|
timer.start();
|
|
|
|
|
while (!capture.done && timer.elapsed() < 5000)
|
|
|
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(capture.done) << "async result not delivered within 5s";
|
|
|
|
|
EXPECT_EQ(capture.ok, 1);
|
|
|
|
|
EXPECT_DOUBLE_EQ(parsed(capture.json.c_str()).get<double>(), 7.0);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 17:44:52 -03:00
|
|
|
TEST_F(LpClientTest, DestroyFromWorkerThreadIsSafe)
|
|
|
|
|
{
|
|
|
|
|
// A client share can outlive its creator inside a worker — a Rust event
|
|
|
|
|
// subscription moved into a bridge thread, say — so the last drop, and
|
|
|
|
|
// with it this destroy, can land on a thread that does not own the
|
|
|
|
|
// client's Qt objects. Destroying them there tore the transport's socket
|
|
|
|
|
// notifiers down cross-thread and faulted the owner's event loop.
|
|
|
|
|
//
|
|
|
|
|
// Scope: the mock transport has no sockets, so this pins the ABI contract
|
|
|
|
|
// (a foreign-thread destroy is legal, terminates, and leaves the client
|
|
|
|
|
// machinery usable) rather than reproducing the QtRO fault itself.
|
|
|
|
|
m_mock->when("mod", "compute").thenReturn(QVariant(7));
|
|
|
|
|
|
|
|
|
|
lp_client* client = lp_client_create("mod", "origin", nullptr, nullptr);
|
|
|
|
|
ASSERT_NE(client, nullptr);
|
|
|
|
|
|
|
|
|
|
std::thread worker([client]() { lp_client_destroy(client); });
|
|
|
|
|
worker.join();
|
|
|
|
|
|
|
|
|
|
// Let the owner thread run the teardown the destroy deferred to it.
|
|
|
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
|
|
|
|
|
|
|
|
|
// The machinery survives: a fresh client for the same target still works.
|
|
|
|
|
lp_client* revived = lp_client_create("mod", "origin", nullptr, nullptr);
|
|
|
|
|
ASSERT_NE(revived, nullptr);
|
|
|
|
|
LpClientGuard guard(revived);
|
|
|
|
|
|
|
|
|
|
char* result = nullptr;
|
|
|
|
|
ASSERT_EQ(lp_invoke(revived, "compute", "[]", 0, &result, nullptr), LP_OK);
|
|
|
|
|
ASSERT_NE(result, nullptr);
|
|
|
|
|
EXPECT_DOUBLE_EQ(parsed(result).get<double>(), 7.0);
|
|
|
|
|
lp_string_free(result);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 18:59:01 -03:00
|
|
|
TEST_F(LpClientTest, InvalidArgsJsonIsRejected)
|
|
|
|
|
{
|
|
|
|
|
lp_client* client = lp_client_create("mod", "origin", nullptr, nullptr);
|
|
|
|
|
ASSERT_NE(client, nullptr);
|
|
|
|
|
LpClientGuard guard(client);
|
|
|
|
|
|
|
|
|
|
char* result = nullptr;
|
|
|
|
|
char* error = nullptr;
|
|
|
|
|
EXPECT_EQ(lp_invoke(client, "m", "not json", 0, &result, &error),
|
|
|
|
|
LP_ERR_INVALID_ARG);
|
|
|
|
|
EXPECT_EQ(result, nullptr);
|
|
|
|
|
ASSERT_NE(error, nullptr);
|
|
|
|
|
nlohmann::json e = parsed(error);
|
|
|
|
|
EXPECT_EQ(e["code"].get<std::string>(), "invalid_args");
|
|
|
|
|
EXPECT_FALSE(e["message"].get<std::string>().empty());
|
|
|
|
|
lp_string_free(error);
|
|
|
|
|
|
|
|
|
|
// A JSON value that is not an array is rejected too.
|
|
|
|
|
EXPECT_EQ(lp_invoke(client, "m", R"({"a":1})", 0, nullptr, nullptr),
|
|
|
|
|
LP_ERR_INVALID_ARG);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(LpClientTest, NullArgumentsAreRejected)
|
|
|
|
|
{
|
|
|
|
|
EXPECT_EQ(lp_client_create(nullptr, "origin", nullptr, nullptr), nullptr);
|
|
|
|
|
EXPECT_EQ(lp_client_create("", "origin", nullptr, nullptr), nullptr);
|
|
|
|
|
EXPECT_EQ(lp_client_create("t", nullptr, nullptr, nullptr), nullptr);
|
|
|
|
|
|
|
|
|
|
char* error = nullptr;
|
|
|
|
|
EXPECT_EQ(lp_invoke(nullptr, "m", "[]", 0, nullptr, &error),
|
|
|
|
|
LP_ERR_INVALID_ARG);
|
|
|
|
|
ASSERT_NE(error, nullptr);
|
|
|
|
|
lp_string_free(error);
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(lp_invoke_async(nullptr, "m", "[]", 0,
|
|
|
|
|
[](int, const char*, void*) {}, nullptr),
|
|
|
|
|
LP_ERR_INVALID_ARG);
|
|
|
|
|
EXPECT_EQ(lp_subscribe(nullptr, "e", [](const char*, const char*, void*) {},
|
|
|
|
|
nullptr),
|
|
|
|
|
nullptr);
|
|
|
|
|
lp_unsubscribe(nullptr); // no-op
|
|
|
|
|
lp_client_destroy(nullptr); // no-op
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(LpClientTest, MalformedTransportJsonFailsCreate)
|
|
|
|
|
{
|
|
|
|
|
EXPECT_EQ(lp_client_create("t", "o", "not json", nullptr), nullptr);
|
|
|
|
|
EXPECT_EQ(lp_client_create("t", "o", nullptr, "not json"), nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(LpClientTest, TokenStoreRoundTrip)
|
|
|
|
|
{
|
|
|
|
|
EXPECT_EQ(lp_token_save("some_module", "tok-123"), LP_OK);
|
|
|
|
|
char* token = lp_token_get("some_module");
|
|
|
|
|
ASSERT_NE(token, nullptr);
|
|
|
|
|
EXPECT_STREQ(token, "tok-123");
|
|
|
|
|
lp_string_free(token);
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(lp_token_get("module_without_token"), nullptr);
|
|
|
|
|
EXPECT_EQ(lp_token_save(nullptr, "t"), LP_ERR_INVALID_ARG);
|
|
|
|
|
EXPECT_EQ(lp_token_get(nullptr), nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(LpClientTest, ProviderGroundworkSurface)
|
|
|
|
|
{
|
|
|
|
|
// Provider C ABI: constructible + callbacks registrable, but serving is
|
|
|
|
|
// explicitly deferred (LP_ERR_UNSUPPORTED) until module authoring lands.
|
|
|
|
|
lp_provider* provider = lp_provider_create("my_module", nullptr);
|
|
|
|
|
ASSERT_NE(provider, nullptr);
|
|
|
|
|
|
|
|
|
|
auto dispatch = [](const char*, const char*, void*) -> char* { return nullptr; };
|
|
|
|
|
EXPECT_EQ(lp_provider_register(provider, dispatch, nullptr, nullptr, nullptr),
|
|
|
|
|
LP_OK);
|
|
|
|
|
EXPECT_EQ(lp_provider_emit_event(provider, "e", "[]"), LP_ERR_UNSUPPORTED);
|
|
|
|
|
EXPECT_EQ(lp_provider_save_token(provider, "m", "t"), LP_ERR_UNSUPPORTED);
|
|
|
|
|
lp_provider_destroy(provider);
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(lp_provider_create(nullptr, nullptr), nullptr);
|
|
|
|
|
EXPECT_EQ(lp_provider_register(nullptr, dispatch, nullptr, nullptr, nullptr),
|
|
|
|
|
LP_ERR_INVALID_ARG);
|
|
|
|
|
lp_provider_destroy(nullptr); // no-op
|
|
|
|
|
}
|