This is an automated email from the ASF dual-hosted git repository.

fgerlits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new d860695ae MINIFICPP-2900 Fix leak of NodeID allocation in OPC 
processors (#2261)
d860695ae is described below

commit d860695ae9553ac5971da16120819f500c2ff654
Author: Gabor Gyimesi <[email protected]>
AuthorDate: Thu Sep 17 12:58:35 2026 +0200

    MINIFICPP-2900 Fix leak of NodeID allocation in OPC processors (#2261)
---
 .../opc/include/{opcbase.h => BaseOPCProcessor.h}  |  2 +-
 .../include/{fetchopc.h => FetchOPCProcessor.h}    |  6 +--
 extensions/opc/include/{opc.h => OPCCommon.h}      | 39 ++++++++++++++++-
 .../opc/include/{putopc.h => PutOPCProcessor.h}    |  8 ++--
 .../opc/src/{opcbase.cpp => BaseOPCProcessor.cpp}  |  4 +-
 .../src/{fetchopc.cpp => FetchOPCProcessor.cpp}    | 14 +++---
 extensions/opc/src/{opc.cpp => OPCCommon.cpp}      |  8 ++--
 .../opc/src/{putopc.cpp => PutOPCProcessor.cpp}    | 51 +++++++++++-----------
 ...ocessorTests.cpp => FetchOPCProcessorTests.cpp} | 26 ++++++++++-
 extensions/opc/tests/OpcUaTestServer.h             | 30 +++++++++++++
 ...ProcessorTests.cpp => PutOPCProcessorTests.cpp} | 12 ++---
 11 files changed, 143 insertions(+), 57 deletions(-)

diff --git a/extensions/opc/include/opcbase.h 
b/extensions/opc/include/BaseOPCProcessor.h
similarity index 99%
rename from extensions/opc/include/opcbase.h
rename to extensions/opc/include/BaseOPCProcessor.h
index b3c02e18d..9ec4afe00 100644
--- a/extensions/opc/include/opcbase.h
+++ b/extensions/opc/include/BaseOPCProcessor.h
@@ -22,7 +22,7 @@
 #include <utility>
 #include <vector>
 
-#include "opc.h"
+#include "OPCCommon.h"
 #include "core/ProcessorImpl.h"
 #include "core/ProcessSession.h"
 #include "minifi-cpp/core/PropertyDefinition.h"
diff --git a/extensions/opc/include/fetchopc.h 
b/extensions/opc/include/FetchOPCProcessor.h
similarity index 97%
rename from extensions/opc/include/fetchopc.h
rename to extensions/opc/include/FetchOPCProcessor.h
index 4c3669fc0..0bf5abd3a 100644
--- a/extensions/opc/include/fetchopc.h
+++ b/extensions/opc/include/FetchOPCProcessor.h
@@ -22,8 +22,8 @@
 #include <utility>
 #include <vector>
 
-#include "opc.h"
-#include "opcbase.h"
+#include "OPCCommon.h"
+#include "BaseOPCProcessor.h"
 #include "minifi-cpp/FlowFileRecord.h"
 #include "core/ProcessSession.h"
 #include "minifi-cpp/core/Property.h"
@@ -149,7 +149,7 @@ class FetchOPCProcessor final : public BaseOPCProcessor {
 
   uint64_t max_depth_ = 0;
   LazyModeOptions lazy_mode_ = LazyModeOptions::Off;
-  std::vector<UA_NodeId> translated_node_ids_;  // Only used when user 
provides path, path->nodeid translation is only done once
+  std::vector<opc::NodeId> translated_node_ids_;  // Only used when user 
provides path, path->nodeid translation is only done once
 };
 
 }  // namespace org::apache::nifi::minifi::processors
diff --git a/extensions/opc/include/opc.h b/extensions/opc/include/OPCCommon.h
similarity index 77%
rename from extensions/opc/include/opc.h
rename to extensions/opc/include/OPCCommon.h
index 8a4973515..df780de4b 100644
--- a/extensions/opc/include/opc.h
+++ b/extensions/opc/include/OPCCommon.h
@@ -58,6 +58,43 @@ enum class OPCNodeDataType{
   String
 };
 
+// RAII owner for a UA_NodeId that calls UA_NodeId_clear to free node id 
allocation
+class NodeId {
+ public:
+  NodeId() = default;
+  explicit NodeId(UA_NodeId id) noexcept : id_(id) {}  // takes ownership of 
an already-built node id
+  NodeId(const NodeId&) = delete;
+  NodeId& operator=(const NodeId&) = delete;
+  NodeId(NodeId&& other) noexcept : id_(other.id_) { other.id_ = 
UA_NODEID_NULL; }
+  NodeId& operator=(NodeId&& other) noexcept {
+    if (this != &other) {
+      UA_NodeId_clear(&id_);
+      id_ = other.id_;
+      other.id_ = UA_NODEID_NULL;
+    }
+    return *this;
+  }
+  ~NodeId() noexcept { UA_NodeId_clear(&id_); }
+
+  static NodeId copyOf(const UA_NodeId& id) {
+    NodeId result;
+    UA_NodeId_copy(&id, &result.id_);
+    return result;
+  }
+
+  operator const UA_NodeId&() const noexcept { return id_; }  // 
NOLINT(google-explicit-constructor) implicit passthrough to the C API is 
intended
+  [[nodiscard]] const UA_NodeId& get() const noexcept { return id_; }
+
+  // Returns a pointer to the (cleared) node id for an open62541 out-parameter 
to write a freshly created node id into.
+  UA_NodeId* receive() noexcept {
+    UA_NodeId_clear(&id_);
+    return &id_;
+  }
+
+ private:
+  UA_NodeId id_ = UA_NODEID_NULL;
+};
+
 struct NodeData;
 
 class Client;
@@ -73,7 +110,7 @@ class Client {
   UA_ReferenceDescription * getNodeReference(UA_NodeId node_id);
   void traverse(UA_NodeId node_id, const std::function<NodeFoundCallBackFunc>& 
cb, const std::string& base_path = "", uint64_t max_depth = 0, bool fetch_root 
= true);
   bool exists(UA_NodeId node_id);
-  UA_StatusCode translateBrowsePathsToNodeIdsRequest(const std::string& path, 
std::vector<UA_NodeId>& found_node_ids, int32_t namespace_index,
+  UA_StatusCode translateBrowsePathsToNodeIdsRequest(const std::string& path, 
std::vector<NodeId>& found_node_ids, int32_t namespace_index,
     const std::vector<UA_UInt32>& path_reference_types, const 
std::shared_ptr<core::logging::Logger>& logger);
 
   template<typename T>
diff --git a/extensions/opc/include/putopc.h 
b/extensions/opc/include/PutOPCProcessor.h
similarity index 96%
rename from extensions/opc/include/putopc.h
rename to extensions/opc/include/PutOPCProcessor.h
index 5aba702b8..2ee4bb9d8 100644
--- a/extensions/opc/include/putopc.h
+++ b/extensions/opc/include/PutOPCProcessor.h
@@ -22,8 +22,8 @@
 #include <utility>
 #include <vector>
 
-#include "opc.h"
-#include "opcbase.h"
+#include "OPCCommon.h"
+#include "BaseOPCProcessor.h"
 #include "minifi-cpp/FlowFileRecord.h"
 #include "core/ProcessSession.h"
 #include "minifi-cpp/core/Property.h"
@@ -118,11 +118,11 @@ class PutOPCProcessor final : public BaseOPCProcessor {
 
  private:
   bool readParentNodeId();
-  std::expected<std::pair<bool, UA_NodeId>, std::string> 
configureTargetNode(core::ProcessContext& context, core::FlowFile& flow_file) 
const;
+  std::expected<std::pair<bool, opc::NodeId>, std::string> 
configureTargetNode(core::ProcessContext& context, core::FlowFile& flow_file) 
const;
   void updateNode(const UA_NodeId& target_node, const std::string& contentstr, 
core::ProcessSession& session, const std::shared_ptr<core::FlowFile>& 
flow_file) const;
   void createNode(const UA_NodeId& target_node, const std::string& contentstr, 
core::ProcessContext& context, core::ProcessSession& session, const 
std::shared_ptr<core::FlowFile>& flow_file) const;
 
-  UA_NodeId parent_node_id_{};
+  opc::NodeId parent_node_id_;
   opc::OPCNodeDataType node_data_type_{};
   UA_UInt32 create_node_reference_type_ = UA_NS0ID_HASCOMPONENT;
 };
diff --git a/extensions/opc/src/opcbase.cpp 
b/extensions/opc/src/BaseOPCProcessor.cpp
similarity index 99%
rename from extensions/opc/src/opcbase.cpp
rename to extensions/opc/src/BaseOPCProcessor.cpp
index e7ff5a73e..a39cc7416 100644
--- a/extensions/opc/src/opcbase.cpp
+++ b/extensions/opc/src/BaseOPCProcessor.cpp
@@ -18,8 +18,8 @@
 #include <memory>
 #include <string>
 
-#include "opc.h"
-#include "opcbase.h"
+#include "OPCCommon.h"
+#include "BaseOPCProcessor.h"
 #include "minifi-cpp/FlowFileRecord.h"
 #include "core/ProcessSession.h"
 #include "core/Core.h"
diff --git a/extensions/opc/src/fetchopc.cpp 
b/extensions/opc/src/FetchOPCProcessor.cpp
similarity index 95%
rename from extensions/opc/src/fetchopc.cpp
rename to extensions/opc/src/FetchOPCProcessor.cpp
index 032417484..4fc4b1caa 100644
--- a/extensions/opc/src/fetchopc.cpp
+++ b/extensions/opc/src/FetchOPCProcessor.cpp
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-#include "fetchopc.h"
+#include "FetchOPCProcessor.h"
 
 #include <list>
 #include <memory>
@@ -24,7 +24,7 @@
 #include "minifi-cpp/core/ProcessContext.h"
 #include "core/ProcessSession.h"
 #include "core/Resource.h"
-#include "opc.h"
+#include "OPCCommon.h"
 #include "utils/Enum.h"
 #include "utils/StringUtils.h"
 #include "utils/ProcessorConfigUtils.h"
@@ -77,14 +77,12 @@ void FetchOPCProcessor::onTrigger(core::ProcessContext& 
context, core::ProcessSe
   };
 
   if (id_type_ != opc::OPCNodeIDType::Path) {
-    UA_NodeId my_id;
-    my_id.namespaceIndex = namespace_idx_;
+    const auto namespace_index = gsl::narrow_cast<UA_UInt16>(namespace_idx_);
+    opc::NodeId my_id;
     if (id_type_ == opc::OPCNodeIDType::Int) {
-      my_id.identifierType = UA_NODEIDTYPE_NUMERIC;
-      my_id.identifier.numeric = std::stoi(node_id_);  // 
NOLINT(cppcoreguidelines-pro-type-union-access)
+      my_id = opc::NodeId{UA_NODEID_NUMERIC(namespace_index, 
std::stoi(node_id_))};
     } else if (id_type_ == opc::OPCNodeIDType::String) {
-      my_id.identifierType = UA_NODEIDTYPE_STRING;
-      my_id.identifier.string = UA_STRING_ALLOC(node_id_.c_str());  // 
NOLINT(cppcoreguidelines-pro-type-union-access)
+      my_id = opc::NodeId{UA_NODEID_STRING_ALLOC(namespace_index, 
node_id_.c_str())};
     } else {
       logger_->log_error("Unhandled id type: '{}'. No flowfiles are 
generated.", magic_enum::enum_underlying(id_type_));
       context.yield();
diff --git a/extensions/opc/src/opc.cpp b/extensions/opc/src/OPCCommon.cpp
similarity index 99%
rename from extensions/opc/src/opc.cpp
rename to extensions/opc/src/OPCCommon.cpp
index b66664529..8bf836a4a 100644
--- a/extensions/opc/src/opc.cpp
+++ b/extensions/opc/src/OPCCommon.cpp
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-#include "opc.h"
+#include "OPCCommon.h"
 
 #include <cstdlib>
 #include <memory>
@@ -357,7 +357,7 @@ bool Client::exists(UA_NodeId node_id) {
   return retval;
 }
 
-UA_StatusCode Client::translateBrowsePathsToNodeIdsRequest(const std::string& 
path, std::vector<UA_NodeId>& found_node_ids, int32_t namespace_index,
+UA_StatusCode Client::translateBrowsePathsToNodeIdsRequest(const std::string& 
path, std::vector<NodeId>& found_node_ids, int32_t namespace_index,
     const std::vector<UA_UInt32>& path_reference_types, const 
std::shared_ptr<core::logging::Logger>& logger) {
   logger->log_trace("Trying to find node ids for {}", path.c_str());
 
@@ -414,9 +414,7 @@ UA_StatusCode 
Client::translateBrowsePathsToNodeIdsRequest(const std::string& pa
     UA_BrowsePathResult res = response.results[i];
     for (size_t j = 0; j < res.targetsSize; ++j) {
       found_data = true;
-      UA_NodeId resultId;
-      UA_NodeId_copy(&res.targets[j].targetId.nodeId, &resultId);
-      found_node_ids.push_back(resultId);
+      found_node_ids.push_back(NodeId::copyOf(res.targets[j].targetId.nodeId));
     }
   }
 
diff --git a/extensions/opc/src/putopc.cpp 
b/extensions/opc/src/PutOPCProcessor.cpp
similarity index 86%
rename from extensions/opc/src/putopc.cpp
rename to extensions/opc/src/PutOPCProcessor.cpp
index e253d6765..b4636e0ef 100644
--- a/extensions/opc/src/putopc.cpp
+++ b/extensions/opc/src/PutOPCProcessor.cpp
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-#include "putopc.h"
+#include "PutOPCProcessor.h"
 
 #include <memory>
 #include <string>
@@ -23,7 +23,7 @@
 #include "minifi-cpp/core/ProcessContext.h"
 #include "core/ProcessSession.h"
 #include "core/Resource.h"
-#include "opc.h"
+#include "OPCCommon.h"
 #include "utils/StringUtils.h"
 #include "utils/ProcessorConfigUtils.h"
 
@@ -60,7 +60,7 @@ void PutOPCProcessor::onSchedule(core::ProcessContext& 
context, core::ProcessSes
 
 bool PutOPCProcessor::readParentNodeId() {
   if (id_type_ == opc::OPCNodeIDType::Path) {
-    std::vector<UA_NodeId> translated_node_ids;
+    std::vector<opc::NodeId> translated_node_ids;
     if (connection_->translateBrowsePathsToNodeIdsRequest(node_id_, 
translated_node_ids, namespace_idx_, path_reference_types_, logger_) !=
         UA_STATUSCODE_GOOD) {
       logger_->log_error("Failed to translate {} to node id, no flow files 
will be put", node_id_.c_str());
@@ -69,16 +69,14 @@ bool PutOPCProcessor::readParentNodeId() {
       logger_->log_error("{} was translated to multiple node ids, no flow 
files will be put", node_id_.c_str());
       return false;
     } else {
-      parent_node_id_ = translated_node_ids[0];
+      parent_node_id_ = std::move(translated_node_ids[0]);
     }
   } else {
-    parent_node_id_.namespaceIndex = namespace_idx_;
+    const auto namespace_index = gsl::narrow_cast<UA_UInt16>(namespace_idx_);
     if (id_type_ == opc::OPCNodeIDType::Int) {
-      parent_node_id_.identifierType = UA_NODEIDTYPE_NUMERIC;
-      parent_node_id_.identifier.numeric = std::stoi(node_id_);  // 
NOLINT(cppcoreguidelines-pro-type-union-access)
+      parent_node_id_ = opc::NodeId{UA_NODEID_NUMERIC(namespace_index, 
std::stoi(node_id_))};
     } else {  // idType_ == opc::OPCNodeIDType::String
-      parent_node_id_.identifierType = UA_NODEIDTYPE_STRING;
-      parent_node_id_.identifier.string = UA_STRING_ALLOC(node_id_.c_str());  
// NOLINT(cppcoreguidelines-pro-type-union-access)
+      parent_node_id_ = opc::NodeId{UA_NODEID_STRING_ALLOC(namespace_index, 
node_id_.c_str())};
     }
     if (!connection_->exists(parent_node_id_)) {
       logger_->log_error("Parent node doesn't exist, no flow files will be 
put");
@@ -88,7 +86,7 @@ bool PutOPCProcessor::readParentNodeId() {
   return true;
 }
 
-std::expected<std::pair<bool, UA_NodeId>, std::string> 
PutOPCProcessor::configureTargetNode(core::ProcessContext& context, 
core::FlowFile& flow_file) const {
+std::expected<std::pair<bool, opc::NodeId>, std::string> 
PutOPCProcessor::configureTargetNode(core::ProcessContext& context, 
core::FlowFile& flow_file) const {
   const auto namespaceidx = context.getProperty(TargetNodeNameSpaceIndex, 
&flow_file).value_or("");
   if (namespaceidx.empty()) {
     return std::unexpected{fmt::format("Flowfile {} had no target namespace 
index specified, routing to failure!", flow_file.getUUIDStr())};
@@ -113,24 +111,25 @@ std::expected<std::pair<bool, UA_NodeId>, std::string> 
PutOPCProcessor::configur
                                     flow_file.getUUIDStr(), target_id_type)};
   }
 
-  UA_NodeId target_node;
-  target_node.namespaceIndex = nsi;
+  const auto namespace_index = gsl::narrow_cast<UA_UInt16>(nsi);
+  opc::NodeId target_node;
   if (target_id_type == "Int") {
-    target_node.identifierType = UA_NODEIDTYPE_NUMERIC;
+    int32_t numeric_id = 0;
     try {
-      target_node.identifier.numeric = std::stoi(target_id);  // 
NOLINT(cppcoreguidelines-pro-type-union-access)
+      numeric_id = std::stoi(target_id);
     } catch (const std::exception&) {
       return std::unexpected{fmt::format("Flowfile {}: target node ID is not a 
valid integer: {}. Routing to failure!",
                                       flow_file.getUUIDStr(), target_id)};
     }
+    target_node = opc::NodeId{UA_NODEID_NUMERIC(namespace_index, numeric_id)};
   } else if (target_id_type == "String") {
-    target_node.identifierType = UA_NODEIDTYPE_STRING;
-    target_node.identifier.string = UA_STRING_ALLOC(target_id.c_str());  // 
NOLINT(cppcoreguidelines-pro-type-union-access)
+    target_node = opc::NodeId{UA_NODEID_STRING_ALLOC(namespace_index, 
target_id.c_str())};
   } else {
     return std::unexpected{fmt::format("Flowfile {}: target node ID type is 
invalid: {}. Routing to failure!",
                                     flow_file.getUUIDStr(), target_id_type)};
   }
-  return std::make_pair(connection_->exists(target_node), target_node);
+  const bool target_node_exists = connection_->exists(target_node);
+  return std::make_pair(target_node_exists, std::move(target_node));
 }
 
 void PutOPCProcessor::updateNode(const UA_NodeId& target_node, const 
std::string& contentstr, core::ProcessSession& session, const 
std::shared_ptr<core::FlowFile>& flow_file) const {
@@ -211,31 +210,31 @@ void PutOPCProcessor::createNode(const UA_NodeId& 
target_node, const std::string
 
   try {
     UA_StatusCode sc = 0;
-    UA_NodeId result_node;
+    opc::NodeId result_node;
     switch (node_data_type_) {
       case opc::OPCNodeDataType::Int64: {
         int64_t value = std::stoll(contentstr);
-        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, &result_node);
+        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, result_node.receive());
         break;
       }
       case opc::OPCNodeDataType::UInt64: {
         uint64_t value = std::stoull(contentstr);
-        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, &result_node);
+        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, result_node.receive());
         break;
       }
       case opc::OPCNodeDataType::Int32: {
         int32_t value = std::stoi(contentstr);
-        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, &result_node);
+        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, result_node.receive());
         break;
       }
       case opc::OPCNodeDataType::UInt32: {
         uint32_t value = std::stoul(contentstr);
-        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, &result_node);
+        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, result_node.receive());
         break;
       }
       case opc::OPCNodeDataType::Boolean: {
         if (auto contentstr_parsed = utils::string::toBool(contentstr)) {
-          sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, contentstr_parsed.value(), 
&result_node);
+          sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, contentstr_parsed.value(), 
result_node.receive());
         } else {
           throw std::runtime_error("Content cannot be converted to bool");
         }
@@ -243,16 +242,16 @@ void PutOPCProcessor::createNode(const UA_NodeId& 
target_node, const std::string
       }
       case opc::OPCNodeDataType::Float: {
         float value = std::stof(contentstr);
-        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, &result_node);
+        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, result_node.receive());
         break;
       }
       case opc::OPCNodeDataType::Double: {
         double value = std::stod(contentstr);
-        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, &result_node);
+        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, value, result_node.receive());
         break;
       }
       case opc::OPCNodeDataType::String: {
-        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, contentstr, &result_node);
+        sc = connection_->add_node(parent_node_id_, target_node, 
create_node_reference_type_, browse_name, contentstr, result_node.receive());
         break;
       }
       default:
diff --git a/extensions/opc/tests/FetchOpcProcessorTests.cpp 
b/extensions/opc/tests/FetchOPCProcessorTests.cpp
similarity index 91%
rename from extensions/opc/tests/FetchOpcProcessorTests.cpp
rename to extensions/opc/tests/FetchOPCProcessorTests.cpp
index 09da30ae0..98c335e35 100644
--- a/extensions/opc/tests/FetchOpcProcessorTests.cpp
+++ b/extensions/opc/tests/FetchOPCProcessorTests.cpp
@@ -19,7 +19,7 @@
 #include "unit/Catch.h"
 #include "OpcUaTestServer.h"
 #include "unit/SingleProcessorTestController.h"
-#include "include/fetchopc.h"
+#include "include/FetchOPCProcessor.h"
 #include "unit/TestUtils.h"
 
 namespace org::apache::nifi::minifi::test {
@@ -94,6 +94,30 @@ TEST_CASE("Test fetching using custom reference type id 
path", "[fetchopcprocess
   CHECK(controller.plan->getContent(flow_file) == "4");
 }
 
+TEST_CASE("Test fetching using string node id", "[fetchopcprocessor]") {
+  OpcUaTestServer server(4841);
+  server.start();
+  SingleProcessorTestController 
controller{minifi::test::utils::make_processor<processors::FetchOPCProcessor>("FetchOPCProcessor")};
+  auto fetch_opc_processor = controller.getProcessor();
+  
REQUIRE(fetch_opc_processor->setProperty(processors::FetchOPCProcessor::OPCServerEndPoint.name,
 "opc.tcp://127.0.0.1:4841/"));
+  
REQUIRE(fetch_opc_processor->setProperty(processors::FetchOPCProcessor::NodeIDType.name,
 "String"));
+  
REQUIRE(fetch_opc_processor->setProperty(processors::FetchOPCProcessor::NodeID.name,
 "the.answer.node"));
+  
REQUIRE(fetch_opc_processor->setProperty(processors::FetchOPCProcessor::NameSpaceIndex.name,
 std::to_string(server.getNamespaceIndex())));
+
+  const auto results = controller.trigger();
+  REQUIRE(results.at(processors::FetchOPCProcessor::Failure).empty());
+  REQUIRE(results.at(processors::FetchOPCProcessor::Success).size() == 1);
+  auto flow_file = results.at(processors::FetchOPCProcessor::Success)[0];
+  CHECK(flow_file->getAttribute("Browsename") == "StringNode");
+  CHECK(flow_file->getAttribute("Datasize") == "4");
+  CHECK(flow_file->getAttribute("Full path") == "/StringNode");
+  CHECK(flow_file->getAttribute("NodeID") == "the.answer.node");
+  CHECK(flow_file->getAttribute("NodeID type") == "string");
+  CHECK(flow_file->getAttribute("Typename") == "Int32");
+  CHECK(flow_file->getAttribute("Sourcetimestamp"));
+  CHECK(controller.plan->getContent(flow_file) == "42");
+}
+
 TEST_CASE("Test missing path reference types", "[fetchopcprocessor]") {
   SingleProcessorTestController 
controller{minifi::test::utils::make_processor<processors::FetchOPCProcessor>("FetchOPCProcessor")};
   auto fetch_opc_processor = controller.getProcessor();
diff --git a/extensions/opc/tests/OpcUaTestServer.h 
b/extensions/opc/tests/OpcUaTestServer.h
index dfb0372e5..191e806cc 100644
--- a/extensions/opc/tests/OpcUaTestServer.h
+++ b/extensions/opc/tests/OpcUaTestServer.h
@@ -73,6 +73,8 @@ class OpcUaTestServer {
     node_ids_["Simulator/Default/Device1/INT3"] = int3_node;
     auto int4_node = addIntVariable("INT4", int3_node, 4);
     node_ids_["Simulator/Default/Device1/INT4"] = int4_node;
+
+    addStringVariable("StringNode", "the.answer.node", UA_NODEID_NUMERIC(0, 
UA_NS0ID_OBJECTSFOLDER), 42);
   }
 
   void start() {
@@ -179,6 +181,34 @@ class OpcUaTestServer {
     return node_id;
   }
 
+  void addStringVariable(const char* name, const char* node_id_str, UA_NodeId 
parent, UA_Int32 value) {
+    UA_VariableAttributes attr = UA_VariableAttributes_default;
+    attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", name);
+    attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
+
+    UA_Variant_setScalar(&attr.value, &value, &UA_TYPES[UA_TYPES_INT32]);
+
+    UA_NodeId requested_id = UA_NODEID_STRING_ALLOC(ns_index_, node_id_str);
+    auto status = UA_Server_addVariableNode(server_,
+        requested_id,
+        parent,
+        UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
+        UA_QUALIFIEDNAME(ns_index_, const_cast<char*>(name)),
+        UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
+        attr,
+        nullptr,
+        nullptr);
+
+    UA_NodeId_clear(&requested_id);
+
+    if (status != UA_STATUSCODE_GOOD) {
+      UA_LocalizedText_clear(&attr.displayName);
+      throw std::runtime_error("Failed to add string variable node");
+    }
+
+    UA_LocalizedText_clear(&attr.displayName);
+  }
+
   void ensureConnection() {
     REQUIRE(utils::verifyEventHappenedInPollTime(
       5s,
diff --git a/extensions/opc/tests/PutOpcProcessorTests.cpp 
b/extensions/opc/tests/PutOPCProcessorTests.cpp
similarity index 98%
rename from extensions/opc/tests/PutOpcProcessorTests.cpp
rename to extensions/opc/tests/PutOPCProcessorTests.cpp
index 583dc0cf4..7197d05c7 100644
--- a/extensions/opc/tests/PutOpcProcessorTests.cpp
+++ b/extensions/opc/tests/PutOPCProcessorTests.cpp
@@ -19,7 +19,7 @@
 #include "unit/Catch.h"
 #include "OpcUaTestServer.h"
 #include "unit/SingleProcessorTestController.h"
-#include "include/putopc.h"
+#include "include/PutOPCProcessor.h"
 #include "utils/StringUtils.h"
 #include "unit/TestUtils.h"
 #include "minifi-cpp/utils/gsl.h"
@@ -39,7 +39,7 @@ struct NodeData {
 void verifyCreatedNode(const NodeData& expected_node, 
SingleProcessorTestController& controller) {
   auto client = minifi::opc::Client::createClient(controller.getLogger(), "", 
{}, {}, {});
   REQUIRE(client->connect("opc.tcp://127.0.0.1:4840/") == UA_STATUSCODE_GOOD);
-  std::vector<UA_NodeId> found_node_ids;
+  std::vector<opc::NodeId> found_node_ids;
   std::vector<UA_UInt32> reference_types;
 
   if (!expected_node.path_reference_types.empty()) {
@@ -60,14 +60,14 @@ void verifyCreatedNode(const NodeData& expected_node, 
SingleProcessorTestControl
   }, 100ms));
 
   REQUIRE(found_node_ids.size() == 1);
-  REQUIRE(found_node_ids[0].namespaceIndex == expected_node.namespace_index);
-  REQUIRE(found_node_ids[0].identifierType == UA_NODEIDTYPE_NUMERIC);
-  REQUIRE(found_node_ids[0].identifier.numeric == expected_node.node_id);  // 
NOLINT(cppcoreguidelines-pro-type-union-access)
+  REQUIRE(found_node_ids[0].get().namespaceIndex == 
expected_node.namespace_index);
+  REQUIRE(found_node_ids[0].get().identifierType == UA_NODEIDTYPE_NUMERIC);
+  REQUIRE(found_node_ids[0].get().identifier.numeric == 
expected_node.node_id);  // NOLINT(cppcoreguidelines-pro-type-union-access)
 
   UA_ReferenceDescription ref_desc;
   ref_desc.isForward = true;
   ref_desc.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NODEIDTYPE_NUMERIC);
-  ref_desc.nodeId.nodeId = found_node_ids[0];
+  ref_desc.nodeId.nodeId = found_node_ids[0].get();
   ref_desc.browseName = UA_QUALIFIEDNAME_ALLOC(expected_node.namespace_index, 
expected_node.browse_name.c_str());
   ref_desc.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", 
expected_node.browse_name.c_str());
   const auto ref_desc_guard = gsl::finally([&ref_desc] {

Reply via email to