You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dvmhost/tests/network/NetRPC.cpp

207 lines
6.1 KiB

// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Test Suite
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES FROM THIS FILE HEADER.
*
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
#include "common/json/json.h"
#include "common/network/NetRPC.h"
#include "common/network/RPCHeader.h"
#include <catch2/catch_test_macros.hpp>
#include <chrono>
#include <cstdint>
#include <string>
#include <thread>
using namespace network;
// ---------------------------------------------------------------------------
// Global Functions
// ---------------------------------------------------------------------------
/**
* @brief Finds an available loopback UDP port.
* @return uint16_t A free UDP port, or 0 on failure.
*/
static uint16_t reserveLoopbackPort()
{
#if defined(_WIN32)
SOCKET fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (fd == INVALID_SOCKET)
return 0U;
#else
int fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
return 0U;
#endif // defined(_WIN32)
sockaddr_in address = {};
address.sin_family = AF_INET;
address.sin_port = htons(0U);
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (::bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address)) < 0) {
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return 0U;
}
socklen_t addrLen = sizeof(address);
if (::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &addrLen) < 0) {
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return 0U;
}
#if defined(_WIN32)
::closesocket(fd);
#else
::close(fd);
#endif // defined(_WIN32)
return ntohs(address.sin_port);
}
/**
* @brief Pumps two NetRPC clocks until a condition is met.
* @param first The first NetRPC instance to clock.
* @param second The second NetRPC instance to clock.
* @param done A callable that returns true when the pumping should stop.
* @returns True if the pumping stopped because the condition was met, false if the maximum number of iterations was reached.
*/
static bool pumpRPCUntil(NetRPC& first, NetRPC& second, const std::function<bool()>& done)
{
for (uint32_t i = 0U; i < 120U; i++) {
first.clock(1U);
second.clock(1U);
if (done())
return true;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return false;
}
TEST_CASE("NetRPC defaultResponse populates status and message", "[network][rpc][netrpc]")
{
const uint16_t localPort = reserveLoopbackPort();
REQUIRE(localPort != 0U);
NetRPC rpc("127.0.0.1", localPort, localPort, "test-password", false);
json::object reply;
rpc.defaultResponse(reply, "invalid arguments", NetRPC::INVALID_ARGS);
REQUIRE(reply["status"].is<int>());
REQUIRE(reply["status"].get<int>() == (int)NetRPC::INVALID_ARGS);
REQUIRE(reply["message"].is<std::string>());
REQUIRE(reply["message"].get<std::string>() == "invalid arguments");
}
TEST_CASE("NetRPC register and unregister handler guards duplicate and out-of-range IDs", "[network][rpc][netrpc]")
{
const uint16_t localPort = reserveLoopbackPort();
REQUIRE(localPort != 0U);
NetRPC rpc("127.0.0.1", localPort, localPort, "test-password", false);
NetRPC::RPCType handler = [](json::object& request, json::object& reply) {
(void)request;
int ok = (int)NetRPC::OK;
reply["status"].set<int>(ok);
};
REQUIRE(rpc.registerHandler(0x0123U, handler));
REQUIRE_FALSE(rpc.registerHandler(0x0123U, handler));
REQUIRE(rpc.unregisterHandler(0x0123U));
REQUIRE_FALSE(rpc.unregisterHandler(0x0123U));
REQUIRE_FALSE(rpc.registerHandler(RPC_MAX_FUNC + 1U, handler));
REQUIRE_FALSE(rpc.unregisterHandler(RPC_MAX_FUNC + 1U));
}
TEST_CASE("NetRPC refuses to send requests to itself", "[network][rpc][netrpc]")
{
const uint16_t localPort = reserveLoopbackPort();
REQUIRE(localPort != 0U);
NetRPC rpc("127.0.0.1", localPort, localPort, "test-password", false);
json::object request;
request["op"].set<std::string>("self-test");
REQUIRE_FALSE(rpc.req(0x0100U, request, NetRPC::RPCType(), "127.0.0.1", localPort, false));
}
TEST_CASE("NetRPC request receives JSON reply from registered remote handler", "[network][rpc][netrpc]")
{
const uint16_t serverPort = reserveLoopbackPort();
const uint16_t clientPort = reserveLoopbackPort();
REQUIRE(serverPort != 0U);
REQUIRE(clientPort != 0U);
REQUIRE(serverPort != clientPort);
NetRPC server("127.0.0.1", serverPort, serverPort, "shared-password", false);
NetRPC client("127.0.0.1", clientPort, clientPort, "shared-password", false);
REQUIRE(server.open());
REQUIRE(client.open());
bool serverCalled = false;
bool clientReplyCalled = false;
std::string echoed;
REQUIRE(server.registerHandler(0x2222U, [&](json::object& request, json::object& reply) {
serverCalled = true;
std::string op = "";
if (request["op"].is<std::string>())
op = request["op"].get<std::string>();
int status = (int)NetRPC::OK;
reply["status"].set<int>(status);
reply["message"].set<std::string>("ok");
reply["echo"].set<std::string>(op);
}));
json::object request;
request["op"].set<std::string>("ping");
REQUIRE(client.req(0x2222U, request,
[&](json::object& response, json::object& unused) {
(void)unused;
if (!response["status"].is<int>())
return;
if (response["status"].get<int>() != (int)NetRPC::OK)
return;
if (!response["echo"].is<std::string>())
return;
echoed = response["echo"].get<std::string>();
clientReplyCalled = true;
},
"127.0.0.1", serverPort, false));
REQUIRE(pumpRPCUntil(server, client, [&]() { return clientReplyCalled; }));
REQUIRE(serverCalled);
REQUIRE(clientReplyCalled);
REQUIRE(echoed == "ping");
server.close();
client.close();
}

Powered by TurnKey Linux.