diff --git a/src/fne/network/P25OTARService.cpp b/src/fne/network/P25OTARService.cpp index ba2169be..5fffc518 100644 --- a/src/fne/network/P25OTARService.cpp +++ b/src/fne/network/P25OTARService.cpp @@ -13,6 +13,7 @@ #include "common/Thread.h" #include "common/Utils.h" #include "network/TrafficNetwork.h" +#include "network/SubscriberOperationStore.h" #include "network/P25OTARService.h" #include "HostFNE.h" @@ -1043,6 +1044,8 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ switch (frame->getMessageId()) { case KMM_MessageType::CHANGEOVER_RSP: { + m_network->subscriberOperations().complete(SubscriberOperationStore::Protocol::P25, SubscriberOperationStore::Kind::KMM_CHANGEOVER, llId, 0U); + KMMChangeover* kmm = static_cast(frame.get()); LogInfoEx(LOG_P25, P25_KMM_STR ", %s, llId = %u, supersededKeysetId = $%02X, activeKeysetId = $%02X", kmm->toString().c_str(), llId, kmm->getSupersededKeysetId(), kmm->getActiveKeysetId()); @@ -1113,6 +1116,7 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ LogInfoEx(LOG_P25, P25_KMM_STR ", %s, no UKEK found for rekey request, llId = %u", kmm->toString().c_str(), llId); return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize, auth); } else { + m_network->subscriberOperations().start(SubscriberOperationStore::Protocol::P25, SubscriberOperationStore::Kind::KMM_REKEY, llId, 0U); if (m_network->m_kmfWarmStart) { // an authenticated Hello proves a common TEK already exists -- otherwise establish // a temporary security context before sending the Rekey Command @@ -1202,6 +1206,7 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ m_rsiInboundMessageNumber[kmm->getSrcLLId()] = kmm->KMMFrame::getMessageNumber(); LogInfoEx(LOG_P25, P25_KMM_STR ", Change-RSI MN resynchronization completed, RSI = %u, initialMN = %u", kmm->getSrcLLId(), initialMN); + m_network->subscriberOperations().complete(SubscriberOperationStore::Protocol::P25, SubscriberOperationStore::Kind::KMM_CHANGERSI, llId, 0U); return write_KMM_Rekey_Command(llId, kmm->getSrcLLId(), KMM_HelloFlag::REKEY_REQUEST_UKEK, payloadSize, auth, @@ -1312,6 +1317,8 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_ // if the rekey process is complete, issue a changeover command if (rekeyComplete && m_network->m_kmfChangeoverAfterRekey) { + m_network->subscriberOperations().complete(SubscriberOperationStore::Protocol::P25, SubscriberOperationStore::Kind::KMM_REKEY, llId, 0U); + // bryanb: should these be hardcoded? as this causes whatever SU on a DVM system to swap to keyset 1 static constexpr uint8_t SUPERSEDED_KEYSET_ID = 1U; static constexpr uint8_t ACTIVE_KEYSET_ID = 1U; @@ -2164,6 +2171,8 @@ UInt8Array P25OTARService::write_KMM_ChangeRSI_Command(uint32_t llId, uint32_t k it->second.deadline = monotonicMilliseconds() + WARM_START_TIMEOUT_MS; } + m_network->subscriberOperations().start(SubscriberOperationStore::Protocol::P25, SubscriberOperationStore::Kind::KMM_CHANGERSI, llId, 0U); + if (payloadSize != nullptr) *payloadSize = frameLength; @@ -2216,6 +2225,8 @@ UInt8Array P25OTARService::write_KMM_Changeover_Command(uint32_t llId, uint32_t return nullptr; } + m_network->subscriberOperations().start(SubscriberOperationStore::Protocol::P25, SubscriberOperationStore::Kind::KMM_CHANGEOVER, llId, 0U); + const uint16_t mn = reserveOutboundMessageNumbers(llId); // build the KMM Changeover command using the determined MAC TEK and message number diff --git a/src/fne/network/SubscriberOperationStore.cpp b/src/fne/network/SubscriberOperationStore.cpp index acb27889..a436618a 100644 --- a/src/fne/network/SubscriberOperationStore.cpp +++ b/src/fne/network/SubscriberOperationStore.cpp @@ -9,6 +9,8 @@ */ #include "fne/network/SubscriberOperationStore.h" +#include + using namespace network; // --------------------------------------------------------------------------- @@ -129,6 +131,32 @@ bool SubscriberOperationStore::get(uint64_t id, Operation& operation) return true; } +/* Retrieves the most recently started operations. */ + +std::vector SubscriberOperationStore::getLast(uint32_t count) +{ + const auto now = std::chrono::steady_clock::now(); + std::lock_guard lock(m_mutex); + + // expire records before taking the history snapshot + expireLocked(now); + + std::vector operations; + operations.reserve(std::min(count, m_operations.size())); + for (const auto& entry : m_operations) { + operations.push_back(entry.second.value); + } + + std::sort(operations.begin(), operations.end(), [](const Operation& lhs, const Operation& rhs) { + return lhs.id > rhs.id; + }); + if (operations.size() > count) { + operations.resize(count); + } + + return operations; +} + /* Retrieves the latest P25 status snapshot. */ bool SubscriberOperationStore::getP25Status(uint32_t subscriberId, Status& status) @@ -182,6 +210,12 @@ const char* SubscriberOperationStore::kindName(Kind kind) return "inhibit"; case Kind::UNINHIBIT: return "uninhibit"; + case Kind::KMM_REKEY: + return "kmm-rekey"; + case Kind::KMM_CHANGEOVER: + return "kmm-changeover"; + case Kind::KMM_CHANGERSI: + return "kmm-changersi"; } return "unknown"; diff --git a/src/fne/network/SubscriberOperationStore.h b/src/fne/network/SubscriberOperationStore.h index 33216541..5fd7abff 100644 --- a/src/fne/network/SubscriberOperationStore.h +++ b/src/fne/network/SubscriberOperationStore.h @@ -22,6 +22,7 @@ #include #include #include +#include namespace network { @@ -40,8 +41,8 @@ namespace network * @ingroup fne_network */ enum class Protocol : uint8_t { - DMR = 0U, //!< Digital Mobile Radio. - P25 = 1U //!< Project 25. + DMR = 0U, //!< Digital Mobile Radio. + P25 = 1U //!< Project 25. }; /** @@ -49,11 +50,15 @@ namespace network * @ingroup fne_network */ enum class Kind : uint8_t { - RADIO_CHECK = 0U, //!< Subscriber presence check. - STATUS_QUERY = 1U, //!< Subscriber status query. - MESSAGE_UPDATE = 2U,//!< Subscriber coded-message update. - INHIBIT = 3U, //!< Subscriber inhibit request. - UNINHIBIT = 4U //!< Subscriber uninhibit request. + RADIO_CHECK = 0U, //!< Subscriber presence check. + STATUS_QUERY = 1U, //!< Subscriber status query. + MESSAGE_UPDATE = 2U, //!< Subscriber coded-message update. + INHIBIT = 3U, //!< Subscriber inhibit request. + UNINHIBIT = 4U, //!< Subscriber uninhibit request. + + KMM_REKEY = 5U, //!< Key management rekey request. + KMM_CHANGEOVER = 6U, //!< Key management changeover request. + KMM_CHANGERSI = 7U //!< Key management change RSI request. }; /** @@ -61,10 +66,10 @@ namespace network * @ingroup fne_network */ enum class State : uint8_t { - PENDING = 0U, //!< Request has been transmitted and awaits a response. - SUCCEEDED = 1U, //!< A matching successful response was received. - REJECTED = 2U, //!< A matching negative response was received. - TIMED_OUT = 3U //!< No matching response arrived before the deadline. + PENDING = 0U, //!< Request has been transmitted and awaits a response. + SUCCEEDED = 1U, //!< A matching successful response was received. + REJECTED = 2U, //!< A matching negative response was received. + TIMED_OUT = 3U //!< No matching response arrived before the deadline. }; /** @@ -72,16 +77,16 @@ namespace network * @ingroup fne_network */ struct Operation { - uint64_t id; //!< FNE-local operation identifier. - Protocol protocol; //!< Air-interface protocol. - Kind kind; //!< Requested operation. - State state; //!< Current lifecycle state. - uint32_t targetId; //!< Target subscriber ID. - uint32_t peerId; //!< Requested or responding peer ID. - uint16_t requestValue; //!< Optional requested status/message value. - uint16_t resultValue; //!< Optional returned status value. - uint64_t createdAt; //!< Creation time in Unix milliseconds. - uint64_t completedAt; //!< Completion time in Unix milliseconds, or zero. + uint64_t id; //!< FNE-local operation identifier. + Protocol protocol; //!< Air-interface protocol. + Kind kind; //!< Requested operation. + State state; //!< Current lifecycle state. + uint32_t targetId; //!< Target subscriber ID. + uint32_t peerId; //!< Requested or responding peer ID. + uint16_t requestValue; //!< Optional requested status/message value. + uint16_t resultValue; //!< Optional returned status value. + uint64_t createdAt; //!< Creation time in Unix milliseconds. + uint64_t completedAt; //!< Completion time in Unix milliseconds, or zero. }; /** @@ -89,11 +94,11 @@ namespace network * @ingroup fne_network */ struct Status { - uint32_t subscriberId; //!< Reporting subscriber ID. - uint32_t peerId; //!< Peer that delivered the report. - uint16_t value; //!< Status value. - bool solicited; //!< True when matched to an active query. - uint64_t observedAt; //!< Observation time in Unix milliseconds. + uint32_t subscriberId; //!< Reporting subscriber ID. + uint32_t peerId; //!< Peer that delivered the report. + uint16_t value; //!< Status value. + bool solicited; //!< True when matched to an active query. + uint64_t observedAt; //!< Observation time in Unix milliseconds. }; /** @@ -141,6 +146,12 @@ namespace network * @returns bool True if the operation exists, otherwise false. */ bool get(uint64_t id, Operation& operation); + /** + * @brief Retrieves the most recently started operations. + * @param[in] count Maximum number of operations to retrieve. + * @returns std::vector Operation snapshots ordered newest first. + */ + std::vector getLast(uint32_t count); /** * @brief Retrieves the latest P25 status snapshot. * @param[in] subscriberId Subscriber ID. diff --git a/src/fne/network/callhandler/TagDMRData.cpp b/src/fne/network/callhandler/TagDMRData.cpp index f3e970f4..8f97401a 100644 --- a/src/fne/network/callhandler/TagDMRData.cpp +++ b/src/fne/network/callhandler/TagDMRData.cpp @@ -1129,6 +1129,18 @@ bool TagDMRData::processCSBK(uint8_t* buffer, uint32_t peerId, dmr::data::NetDat { lc::csbk::CSBK_EXT_FNCT* iosp = static_cast(csbk.get()); switch (iosp->getExtendedFunction()) { + case ExtendedFunctions::CHECK: + m_network->subscriberOperations().start(SubscriberOperationStore::Protocol::DMR, + SubscriberOperationStore::Kind::RADIO_CHECK, iosp->getDstId(), peerId); + return false; + case ExtendedFunctions::INHIBIT: + m_network->subscriberOperations().start(SubscriberOperationStore::Protocol::DMR, + SubscriberOperationStore::Kind::INHIBIT, iosp->getDstId(), peerId); + return false; + case ExtendedFunctions::UNINHIBIT: + m_network->subscriberOperations().start(SubscriberOperationStore::Protocol::DMR, + SubscriberOperationStore::Kind::UNINHIBIT, iosp->getDstId(), peerId); + return false; case ExtendedFunctions::CHECK_ACK: m_network->subscriberOperations().complete(SubscriberOperationStore::Protocol::DMR, SubscriberOperationStore::Kind::RADIO_CHECK, iosp->getDstId(), peerId); diff --git a/src/fne/network/callhandler/TagP25Data.cpp b/src/fne/network/callhandler/TagP25Data.cpp index 1ba3837e..742bf513 100644 --- a/src/fne/network/callhandler/TagP25Data.cpp +++ b/src/fne/network/callhandler/TagP25Data.cpp @@ -1453,6 +1453,18 @@ bool TagP25Data::processTSDUFrom(uint8_t* buffer, uint32_t peerId, uint8_t duid) lc::tsbk::IOSP_EXT_FNCT* iosp = static_cast(tsbk.get()); switch (iosp->getExtendedFunction()) { // Standard + case ExtendedFunctions::CHECK: + m_network->subscriberOperations().start(SubscriberOperationStore::Protocol::P25, + SubscriberOperationStore::Kind::RADIO_CHECK, iosp->getDstId(), peerId); + return false; + case ExtendedFunctions::INHIBIT: + m_network->subscriberOperations().start(SubscriberOperationStore::Protocol::P25, + SubscriberOperationStore::Kind::INHIBIT, iosp->getDstId(), peerId); + return false; + case ExtendedFunctions::UNINHIBIT: + m_network->subscriberOperations().start(SubscriberOperationStore::Protocol::P25, + SubscriberOperationStore::Kind::UNINHIBIT, iosp->getDstId(), peerId); + return false; case ExtendedFunctions::CHECK_ACK: m_network->subscriberOperations().complete(SubscriberOperationStore::Protocol::P25, SubscriberOperationStore::Kind::RADIO_CHECK, iosp->getDstId(), peerId); diff --git a/src/fne/network/packet/SubscriberOperation.cpp b/src/fne/network/packet/SubscriberOperation.cpp index 8ae11de6..6546c035 100644 --- a/src/fne/network/packet/SubscriberOperation.cpp +++ b/src/fne/network/packet/SubscriberOperation.cpp @@ -23,6 +23,10 @@ using namespace network; // Constants // --------------------------------------------------------------------------- +#define SOP_DMR "dmr" +#define SOP_P25 "p25" +#define SOP_NXDN "nxdn" + const uint32_t SUBSCRIBER_OPERATION_HEADER_LENGTH = 11U; // --------------------------------------------------------------------------- @@ -52,6 +56,8 @@ void MetadataNetwork::PacketHandler::subscriberOperation(TrafficNetwork* network const bool notFound = false; const bool found = true; const NET_SUBFUNC::ENUM subFunction = req->fneHeader.getSubFunction(); + + // helper function to write the response back to the peer const auto writeResponse = [network, mdNetwork, connection, subFunction, streamId](json::object responseObj) { json::value responseValue(responseObj); const std::string responsePayload = responseValue.serialize(); @@ -119,7 +125,7 @@ void MetadataNetwork::PacketHandler::subscriberOperation(TrafficNetwork* network SubscriberOperationStore::Protocol protocol; SubscriberOperationStore::Kind kind; uint16_t requestValue = 0U; - if (protocolName == "p25") { + if (protocolName == SOP_P25) { protocol = SubscriberOperationStore::Protocol::P25; // determine the kind of P25 operation requested @@ -144,7 +150,7 @@ void MetadataNetwork::PacketHandler::subscriberOperation(TrafficNetwork* network response["error"].set("unsupported P25 operation"); break; } - } else if (protocolName == "dmr") { + } else if (protocolName == SOP_DMR) { protocol = SubscriberOperationStore::Protocol::DMR; if (!request["slot"].is()) { response["found"].set(notFound); @@ -238,28 +244,61 @@ void MetadataNetwork::PacketHandler::subscriberOperation(TrafficNetwork* network case NET_SUBFUNC::SUBSCRIBER_OP_OPERATION: { response["type"].set("operation"); - if (!request["operationId"].is()) { + + // check if the request contains an operation ID and validate it + const bool hasOperationId = request.find("operationId") != request.end(); + if (hasOperationId && !request["operationId"].is()) { response["found"].set(notFound); response["error"].set("invalid operation ID"); break; } - const uint64_t operationId = request["operationId"].get(); - SubscriberOperationStore::Operation operation; - const bool found = operationId != 0U && network->subscriberOperations().get(operationId, operation); - response["found"].set(found); - response["operationId"].set(operationId); - if (found) { - response["protocol"].set(SubscriberOperationStore::protocolName(operation.protocol)); - response["operation"].set(SubscriberOperationStore::kindName(operation.kind)); - response["state"].set(SubscriberOperationStore::stateName(operation.state)); - response["targetId"].set(operation.targetId); - response["peerId"].set(operation.peerId); - response["requestValue"].set(operation.requestValue); - response["resultValue"].set(operation.resultValue); - response["createdAt"].set(operation.createdAt); - response["completedAt"].set(operation.completedAt); + // if an operation ID was provided, attempt to retrieve the corresponding operation from the store + if (hasOperationId) { + const uint64_t operationId = request["operationId"].get(); + SubscriberOperationStore::Operation operation; + + const bool found = operationId != 0U && network->subscriberOperations().get(operationId, operation); + response["found"].set(found); + response["operationId"].set(operationId); + if (found) { + response["protocol"].set(SubscriberOperationStore::protocolName(operation.protocol)); + response["operation"].set(SubscriberOperationStore::kindName(operation.kind)); + response["state"].set(SubscriberOperationStore::stateName(operation.state)); + response["targetId"].set(operation.targetId); + response["peerId"].set(operation.peerId); + response["requestValue"].set(operation.requestValue); + response["resultValue"].set(operation.resultValue); + response["createdAt"].set(operation.createdAt); + response["completedAt"].set(operation.completedAt); + } + } + + // retrieve the last 5 subscriber operations from the store and construct the JSON array + json::array operationHistory = json::array(); + const std::vector operations = network->subscriberOperations().getLast(5U); + for (const SubscriberOperationStore::Operation& historyOperation : operations) { + json::object operationObj = json::object(); + operationObj["operationId"].set(historyOperation.id); + operationObj["protocol"].set(SubscriberOperationStore::protocolName(historyOperation.protocol)); + operationObj["operation"].set(SubscriberOperationStore::kindName(historyOperation.kind)); + operationObj["state"].set(SubscriberOperationStore::stateName(historyOperation.state)); + operationObj["targetId"].set(historyOperation.targetId); + operationObj["peerId"].set(historyOperation.peerId); + operationObj["requestValue"].set(historyOperation.requestValue); + operationObj["resultValue"].set(historyOperation.resultValue); + operationObj["createdAt"].set(historyOperation.createdAt); + operationObj["completedAt"].set(historyOperation.completedAt); + operationHistory.push_back(json::value(operationObj)); } + + // if no specific operation ID was requested, indicate whether any history was found + if (!hasOperationId) { + const bool historyFound = !operations.empty(); + response["found"].set(historyFound); + } + + response["operations"].set(operationHistory); } break; @@ -272,12 +311,13 @@ void MetadataNetwork::PacketHandler::subscriberOperation(TrafficNetwork* network break; } + // retrieve the subscriber ID from the request and fetch the corresponding P25 status from the store const uint32_t subscriberId = request["subscriberId"].get(); SubscriberOperationStore::Status status; - const bool found = subscriberId != 0U && - network->subscriberOperations().getP25Status(subscriberId, status); + const bool found = subscriberId != 0U && network->subscriberOperations().getP25Status(subscriberId, status); + response["found"].set(found); - response["protocol"].set("p25"); + response["protocol"].set(SOP_P25); response["subscriberId"].set(subscriberId); if (found) { response["peerId"].set(status.peerId); diff --git a/src/fne/restapi/RESTAPI.cpp b/src/fne/restapi/RESTAPI.cpp index 59ff5665..18a447fe 100644 --- a/src/fne/restapi/RESTAPI.cpp +++ b/src/fne/restapi/RESTAPI.cpp @@ -724,6 +724,7 @@ void RESTAPI::initializeEndpoints() m_dispatcher.match(FNE_GET_GRANT_LIST).get(REST_API_BIND(RESTAPI::restAPI_GetGrantList, this)); m_dispatcher.match(FNE_GET_SPANNING_TREE).get(REST_API_BIND(RESTAPI::restAPI_GetSpanningTree, this)); + m_dispatcher.match(FNE_GET_RID_OPERATION_HISTORY, true).get(REST_API_BIND(RESTAPI::restAPI_GetRIDOperationHistory, this)); m_dispatcher.match(FNE_GET_RID_OPERATION, true).get(REST_API_BIND(RESTAPI::restAPI_GetRIDOperation, this)); /* @@ -3091,6 +3092,44 @@ void RESTAPI::restAPI_GetRIDOperation(const HTTPPayload& request, HTTPPayload& r response["resultValue"].set(operation.resultValue); response["createdAt"].set(operation.createdAt); response["completedAt"].set(operation.completedAt); + + reply.payload(response); +} + +/* REST API endpoint; retrieves recent subscriber operations. */ + +void RESTAPI::restAPI_GetRIDOperationHistory(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + // retrieve the requested count of recent operations from the URL and limit it to 64 if necessary + const uint64_t requestedCount = ::strtoull(match[1U].str().c_str(), nullptr, 10); + const uint32_t count = requestedCount > 64U ? 64U : static_cast(requestedCount); + const std::vector operations = m_network->subscriberOperations().getLast(count); + + // construct the JSON array of recent operations + json::array operationHistory = json::array(); + for (const SubscriberOperationStore::Operation& operation : operations) { + json::object operationObj = json::object(); + operationObj["operationId"].set(operation.id); + operationObj["protocol"].set(SubscriberOperationStore::protocolName(operation.protocol)); + operationObj["operation"].set(SubscriberOperationStore::kindName(operation.kind)); + operationObj["state"].set(SubscriberOperationStore::stateName(operation.state)); + operationObj["targetId"].set(operation.targetId); + operationObj["peerId"].set(operation.peerId); + operationObj["requestValue"].set(operation.requestValue); + operationObj["resultValue"].set(operation.resultValue); + operationObj["createdAt"].set(operation.createdAt); + operationObj["completedAt"].set(operation.completedAt); + operationHistory.push_back(json::value(operationObj)); + } + + // construct the JSON response with the operation history array + json::object response = json::object(); + setResponseDefaultStatus(response); + response["operations"].set(operationHistory); reply.payload(response); } diff --git a/src/fne/restapi/RESTAPI.h b/src/fne/restapi/RESTAPI.h index 0e750890..cc11e8e6 100644 --- a/src/fne/restapi/RESTAPI.h +++ b/src/fne/restapi/RESTAPI.h @@ -511,6 +511,14 @@ namespace fne_restapi */ void restAPI_GetRIDOperation(const HTTPPayload& request, HTTPPayload& reply, const restapi::RequestMatch& match); + /** + * @brief REST API endpoint; retrieves recent subscriber operations. + * @param[in] request HTTP request. + * @param[out] reply HTTP reply. + * @param[in] match HTTP request matcher. + */ + void restAPI_GetRIDOperationHistory(const HTTPPayload& request, HTTPPayload& reply, const restapi::RequestMatch& match); + /* ** Digital Mobile Radio */ diff --git a/src/fne/restapi/RESTDefines.h b/src/fne/restapi/RESTDefines.h index dc9932c5..29fdfd33 100644 --- a/src/fne/restapi/RESTDefines.h +++ b/src/fne/restapi/RESTDefines.h @@ -40,6 +40,7 @@ #define FNE_GET_RID_ALIAS_COMMIT "/rid/alias/commit" #define FNE_GET_RID_OPERATION_BASE "/rid/operation/" #define FNE_GET_RID_OPERATION "^/rid/operation/([0-9]+)$" +#define FNE_GET_RID_OPERATION_HISTORY "^/rid/operation/last/([0-9]+)$" #define FNE_GET_P25_RID_STATUS_BASE "/p25/rid/status/" #define FNE_GET_P25_RID_STATUS "^/p25/rid/status/([0-9]+)$" diff --git a/tests/fne/FNENetwork_Tests.cpp b/tests/fne/FNENetwork_Tests.cpp index b3998820..a9cfb7bf 100644 --- a/tests/fne/FNENetwork_Tests.cpp +++ b/tests/fne/FNENetwork_Tests.cpp @@ -120,6 +120,33 @@ TEST_CASE("FNE metadata returns subscriber operation and P25 status results", CHECK(operationResponse["operationId"].get() == operationId); CHECK(operationResponse["state"].get() == "succeeded"); CHECK(operationResponse["resultValue"].get() == 0x1234U); + REQUIRE(operationResponse["operations"].is()); + json::array& operations = operationResponse["operations"].get(); + REQUIRE(operations.size() == 1U); + REQUIRE(operations[0U].is()); + CHECK(operations[0U].get()["operationId"].get() == operationId); + + json::object historyRequest = json::object(); + const std::string historyJson = json::value(historyRequest).serialize(); + requestPayload.assign(11U + historyJson.length(), 0U); + ::memcpy(requestPayload.data() + 11U, historyJson.data(), historyJson.length()); + responsePayload.clear(); + + FNETestHooks::dispatchMetadata(harness.traffic, harness.metadata, + network::NET_FUNC::SUBSCRIBER_OP, network::NET_SUBFUNC::SUBSCRIBER_OP_OPERATION, + CONSOLE_PEER_ID, requestPayload); + + REQUIRE(responsePayload.size() > 11U); + json::value historyResponseValue; + REQUIRE(json::parse(historyResponseValue, + std::string(reinterpret_cast(responsePayload.data() + 11U), + responsePayload.size() - 11U)) + .empty()); + REQUIRE(historyResponseValue.is()); + json::object& historyResponse = historyResponseValue.get(); + CHECK(historyResponse["found"].get()); + REQUIRE(historyResponse["operations"].is()); + REQUIRE(historyResponse["operations"].get().size() == 1U); json::object statusRequest = json::object(); statusRequest["subscriberId"].set(SUBSCRIBER_ID); diff --git a/tests/fne/SubscriberOperationStore_Tests.cpp b/tests/fne/SubscriberOperationStore_Tests.cpp index d9955208..7377336f 100644 --- a/tests/fne/SubscriberOperationStore_Tests.cpp +++ b/tests/fne/SubscriberOperationStore_Tests.cpp @@ -11,6 +11,8 @@ #include +#include + using namespace network; TEST_CASE("FNE subscriber operations correlate protocol results", "[fne][subscriber-operations]") @@ -59,3 +61,24 @@ TEST_CASE("FNE subscriber operations isolate protocols and kinds", "[fne][subscr CHECK(dmrOperation.peerId == 12U); CHECK(p25Operation.state == SubscriberOperationStore::State::PENDING); } + +TEST_CASE("FNE subscriber operations return recent history newest first", "[fne][subscriber-operations]") +{ + SubscriberOperationStore store; + const uint64_t firstId = store.start(SubscriberOperationStore::Protocol::DMR, + SubscriberOperationStore::Kind::RADIO_CHECK, 1001U, 0U); + const uint64_t secondId = store.start(SubscriberOperationStore::Protocol::P25, + SubscriberOperationStore::Kind::INHIBIT, 1002U, 0U); + const uint64_t thirdId = store.start(SubscriberOperationStore::Protocol::P25, + SubscriberOperationStore::Kind::UNINHIBIT, 1003U, 0U); + + REQUIRE(firstId != 0U); + REQUIRE(secondId != 0U); + REQUIRE(thirdId != 0U); + + const std::vector operations = store.getLast(2U); + REQUIRE(operations.size() == 2U); + CHECK(operations[0U].id == thirdId); + CHECK(operations[1U].id == secondId); + CHECK(store.getLast(0U).empty()); +}