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

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

commit f34569ee1bbc9bd424f8033a20c4bdd7f5ac4c6e
Author: Gabor Gyimesi <[email protected]>
AuthorDate: Thu Nov 20 14:33:13 2025 +0100

    MINIFICPP-2665 Move OPC UA tests to modular docker tests
    
    - Allow using multiple minifi containers, and use dictionaries for naming 
and storing containers instead of a list
    - Allow using host file resources in test minifi containers
    
    Closes #2052
    
    Signed-off-by: Marton Szasz <[email protected]>
---
 .../minifi_test_framework/containers/container.py  |  22 +++--
 .../minifi_test_framework/containers/host_file.py  |   4 +-
 .../containers/minifi_container.py                 |   4 +-
 .../src/minifi_test_framework/core/helpers.py      |   3 +-
 .../src/minifi_test_framework/core/hooks.py        |  45 +++++----
 .../core/minifi_test_context.py                    |  23 ++++-
 .../minifi_test_framework/steps/checking_steps.py  |  59 ++++++------
 .../steps/configuration_steps.py                   |  10 +-
 .../src/minifi_test_framework/steps/core_steps.py  |  27 ++++--
 .../steps/flow_building_steps.py                   | 101 ++++++++++++++-------
 docker/RunBehaveTests.sh                           |   3 +-
 docker/test/integration/cluster/ContainerStore.py  |   9 --
 .../cluster/DockerTestDirectoryBindings.py         |   1 -
 docker/test/integration/features/http.feature      |   2 +-
 docker/test/integration/features/https.feature     |   2 +-
 docker/test/integration/features/steps/steps.py    |  18 +---
 extensions/aws/tests/features/steps/steps.py       |  14 +--
 extensions/azure/tests/features/steps/steps.py     |  38 ++++----
 extensions/opc/tests/features/environment.py       |  28 ++++++
 .../opc/tests}/features/opcua.feature              |  84 ++++++++++-------
 .../features/resources}/opcua_client_cert.der      | Bin
 .../tests/features/resources}/opcua_client_key.der | Bin
 .../features/steps/opc_ua_server_container.py      |  32 +++----
 extensions/opc/tests/features/steps/steps.py       |  47 ++++++++++
 extensions/sql/tests/features/steps/steps.py       |  12 +--
 .../tests/features/steps/steps.py                  |   4 +-
 .../tests/features/steps/syslog_container.py       |   2 +-
 27 files changed, 375 insertions(+), 219 deletions(-)

diff --git a/behave_framework/src/minifi_test_framework/containers/container.py 
b/behave_framework/src/minifi_test_framework/containers/container.py
index d3a8316df..dc0cf7353 100644
--- a/behave_framework/src/minifi_test_framework/containers/container.py
+++ b/behave_framework/src/minifi_test_framework/containers/container.py
@@ -29,22 +29,25 @@ from minifi_test_framework.containers.host_file import 
HostFile
 
 
 class Container:
-    def __init__(self, image_name: str, container_name: str, network: Network):
+    def __init__(self, image_name: str, container_name: str, network: Network, 
command: str | None = None):
         self.image_name: str = image_name
         self.container_name: str = container_name
         self.network: Network = network
         self.user: str = "0:0"
-        self.client = docker.from_env()
+        self.client: docker.DockerClient = docker.from_env()
         self.container = None
         self.files: list[File] = []
         self.dirs: list[Directory] = []
         self.host_files: list[HostFile] = []
         self.volumes = {}
-        self.command = None
-        self._temp_dir = None
-        self.ports = None
+        self.command: str | None = command
+        self._temp_dir: tempfile.TemporaryDirectory | None = None
+        self.ports: dict[str, int] | None = None
         self.environment: list[str] = []
 
+    def add_host_file(self, host_path: str, container_path: str, mode: str = 
"ro"):
+        self.host_files.append(HostFile(container_path, host_path, mode))
+
     def deploy(self) -> bool:
         self._temp_dir = tempfile.TemporaryDirectory()
 
@@ -63,7 +66,7 @@ class Container:
                     temp_file.write(content)
             self.volumes[temp_path] = {"bind": directory.path, "mode": 
directory.mode}
         for host_file in self.host_files:
-            self.volumes[host_file.container_path] = {"bind": 
host_file.host_path, "mode": host_file.mode}
+            self.volumes[host_file.host_path] = {"bind": 
host_file.container_path, "mode": host_file.mode}
 
         try:
             existing_container = 
self.client.containers.get(self.container_name)
@@ -84,7 +87,10 @@ class Container:
 
     def clean_up(self):
         if self.container:
-            self.container.remove(force=True)
+            try:
+                self.container.remove(force=True)
+            except Exception as e:
+                logging.error(f"Error cleaning up container 
'{self.container_name}': {e}")
 
     def exec_run(self, command) -> tuple[int | None, str]:
         if self.container:
@@ -110,7 +116,7 @@ class Container:
         quoted_content = shlex.quote(expected_content)
         command = "sh -c {}".format(shlex.quote(f"grep -l -F -- 
{quoted_content} {directory_path}/*"))
 
-        exit_code, output = self.exec_run(command)
+        exit_code, _ = self.exec_run(command)
 
         return exit_code == 0
 
diff --git a/behave_framework/src/minifi_test_framework/containers/host_file.py 
b/behave_framework/src/minifi_test_framework/containers/host_file.py
index 9b59945d2..1e046f54c 100644
--- a/behave_framework/src/minifi_test_framework/containers/host_file.py
+++ b/behave_framework/src/minifi_test_framework/containers/host_file.py
@@ -16,7 +16,7 @@
 #
 
 class HostFile:
-    def __init__(self, path, host_path):
+    def __init__(self, path, host_path, mode="ro"):
         self.container_path = path
         self.host_path = host_path
-        self.mode = "ro"
+        self.mode = mode
diff --git 
a/behave_framework/src/minifi_test_framework/containers/minifi_container.py 
b/behave_framework/src/minifi_test_framework/containers/minifi_container.py
index 7c2661d44..805b04227 100644
--- a/behave_framework/src/minifi_test_framework/containers/minifi_container.py
+++ b/behave_framework/src/minifi_test_framework/containers/minifi_container.py
@@ -23,8 +23,8 @@ from .container import Container
 
 
 class MinifiContainer(Container):
-    def __init__(self, image_name: str, scenario_id: str, network: Network):
-        super().__init__(image_name, f"minifi-{scenario_id}", network)
+    def __init__(self, image_name: str, container_name: str, scenario_id: str, 
network: Network):
+        super().__init__(image_name, f"{container_name}-{scenario_id}", 
network)
         self.flow_config_str: str = ""
         self.flow_definition = FlowDefinition()
         self.properties: dict[str, str] = {}
diff --git a/behave_framework/src/minifi_test_framework/core/helpers.py 
b/behave_framework/src/minifi_test_framework/core/helpers.py
index 01fa506b0..3c551b8d4 100644
--- a/behave_framework/src/minifi_test_framework/core/helpers.py
+++ b/behave_framework/src/minifi_test_framework/core/helpers.py
@@ -27,9 +27,8 @@ from minifi_test_framework.core.minifi_test_context import 
MinifiTestContext
 
 def log_due_to_failure(context: MinifiTestContext | None):
     if context is not None:
-        for container in context.containers:
+        for container in context.containers.values():
             container.log_app_output()
-        context.minifi_container.log_app_output()
 
 
 def wait_for_condition(condition: Callable[[], bool], timeout_seconds: float, 
bail_condition: Callable[[], bool],
diff --git a/behave_framework/src/minifi_test_framework/core/hooks.py 
b/behave_framework/src/minifi_test_framework/core/hooks.py
index 0a4c33861..5513c9f51 100644
--- a/behave_framework/src/minifi_test_framework/core/hooks.py
+++ b/behave_framework/src/minifi_test_framework/core/hooks.py
@@ -17,13 +17,12 @@
 
 import logging
 import os
-
 import docker
+import types
+
 from behave.model import Scenario
-from behave.model import Step
 from behave.runner import Context
 
-from minifi_test_framework.containers.minifi_container import MinifiContainer
 from minifi_test_framework.core.minifi_test_context import MinifiTestContext
 
 
@@ -35,40 +34,52 @@ def get_minifi_container_image():
     return "apacheminificpp:behave"
 
 
+def inject_scenario_id(context: MinifiTestContext, step):
+    if "${scenario_id}" in step.name:
+        step.name = step.name.replace("${scenario_id}", context.scenario_id)
+    if getattr(step, "table", None):
+        for row in step.table:
+            row.cells = [cell.replace("${scenario_id}", context.scenario_id) 
if "${scenario_id}" in cell else cell for cell in row.cells]
+    if hasattr(step, "text") and step.text and "${scenario_id}" in step.text:
+        step.text = step.text.replace("${scenario_id}", context.scenario_id)
+
+
 def common_before_scenario(context: Context, scenario: Scenario):
     if not hasattr(context, "minifi_container_image"):
         context.minifi_container_image = get_minifi_container_image()
 
+    method_map = {
+        "get_or_create_minifi_container": 
MinifiTestContext.get_or_create_minifi_container,
+        "get_or_create_default_minifi_container": 
MinifiTestContext.get_or_create_default_minifi_container,
+        "get_minifi_container": MinifiTestContext.get_minifi_container,
+        "get_default_minifi_container": 
MinifiTestContext.get_default_minifi_container,
+    }
+    for attr, method in method_map.items():
+        if not hasattr(context, attr):
+            setattr(context, attr, types.MethodType(method, context))
+
     logging.info("Running scenario: %s", scenario)
     context.scenario_id = scenario.filename.rsplit("/", 1)[1].split(".")[0] + 
"-" + str(
         scenario.parent.scenarios.index(scenario))
     network_name = f"{context.scenario_id}-net"
     docker_client = docker.client.from_env()
+
     try:
         existing_network = docker_client.networks.get(network_name)
         logging.warning(f"Found existing network '{network_name}'. Removing it 
first.")
         existing_network.remove()
     except docker.errors.NotFound:
         pass  # No existing network found, which is good.
+
     context.network = docker_client.networks.create(network_name)
-    context.minifi_container = MinifiContainer(context.minifi_container_image, 
context.scenario_id, context.network)
-    context.containers = []
+    context.containers = {}
+    context.resource_dir = None
+
     for step in scenario.steps:
         inject_scenario_id(context, step)
 
 
 def common_after_scenario(context: MinifiTestContext, scenario: Scenario):
-    for container in context.containers:
+    for container in context.containers.values():
         container.clean_up()
-    context.minifi_container.clean_up()
     context.network.remove()
-
-
-def inject_scenario_id(context: MinifiTestContext, step: Step):
-    if "${scenario_id}" in step.name:
-        step.name = step.name.replace("${scenario_id}", context.scenario_id)
-    if step.table:
-        for row in step.table:
-            for i in range(len(row.cells)):
-                if "${scenario_id}" in row.cells[i]:
-                    row.cells[i] = row.cells[i].replace("${scenario_id}", 
context.scenario_id)
diff --git 
a/behave_framework/src/minifi_test_framework/core/minifi_test_context.py 
b/behave_framework/src/minifi_test_framework/core/minifi_test_context.py
index 6c7892c05..ab9110de0 100644
--- a/behave_framework/src/minifi_test_framework/core/minifi_test_context.py
+++ b/behave_framework/src/minifi_test_framework/core/minifi_test_context.py
@@ -21,9 +21,28 @@ from docker.models.networks import Network
 from minifi_test_framework.containers.container import Container
 from minifi_test_framework.containers.minifi_container import MinifiContainer
 
+DEFAULT_MINIFI_CONTAINER_NAME = "minifi-primary"
+
 
 class MinifiTestContext(Context):
-    minifi_container: MinifiContainer
-    containers: list[Container]
+    containers: dict[str, Container]
     scenario_id: str
     network: Network
+    minifi_container_image: str
+    resource_dir: str | None
+
+    def get_or_create_minifi_container(self, container_name: str) -> 
MinifiContainer:
+        if container_name not in self.containers:
+            self.containers[container_name] = 
MinifiContainer(self.minifi_container_image, container_name, self.scenario_id, 
self.network)
+        return self.containers[container_name]
+
+    def get_or_create_default_minifi_container(self) -> MinifiContainer:
+        return 
self.get_or_create_minifi_container(DEFAULT_MINIFI_CONTAINER_NAME)
+
+    def get_minifi_container(self, container_name: str) -> MinifiContainer:
+        if container_name not in self.containers:
+            raise KeyError(f"MiNiFi container '{container_name}' does not 
exist in the test context.")
+        return self.containers[container_name]
+
+    def get_default_minifi_container(self) -> MinifiContainer:
+        return self.get_minifi_container(DEFAULT_MINIFI_CONTAINER_NAME)
diff --git a/behave_framework/src/minifi_test_framework/steps/checking_steps.py 
b/behave_framework/src/minifi_test_framework/steps/checking_steps.py
index e4d46c7b4..231a2be89 100644
--- a/behave_framework/src/minifi_test_framework/steps/checking_steps.py
+++ b/behave_framework/src/minifi_test_framework/steps/checking_steps.py
@@ -23,7 +23,7 @@ from behave import then, step
 
 from minifi_test_framework.containers.http_proxy_container import HttpProxy
 from minifi_test_framework.core.helpers import wait_for_condition
-from minifi_test_framework.core.minifi_test_context import MinifiTestContext
+from minifi_test_framework.core.minifi_test_context import 
DEFAULT_MINIFI_CONTAINER_NAME, MinifiTestContext
 
 
 @then('there is a file with "{content}" content at {path} in less than 
{duration}')
@@ -31,8 +31,8 @@ def step_impl(context: MinifiTestContext, content: str, path: 
str, duration: str
     new_content = content.replace("\\n", "\n")
     timeout_in_seconds = humanfriendly.parse_timespan(duration)
     assert wait_for_condition(
-        condition=lambda: 
context.minifi_container.path_with_content_exists(path, new_content),
-        timeout_seconds=timeout_in_seconds, bail_condition=lambda: 
context.minifi_container.exited, context=context)
+        condition=lambda: 
context.get_default_minifi_container().path_with_content_exists(path, 
new_content),
+        timeout_seconds=timeout_in_seconds, bail_condition=lambda: 
context.get_default_minifi_container().exited, context=context)
 
 
 @then('there is a single file with "{content}" content in the "{directory}" 
directory in less than {duration}')
@@ -40,47 +40,52 @@ def step_impl(context: MinifiTestContext, content: str, 
directory: str, duration
     new_content = content.replace("\\n", "\n")
     timeout_in_seconds = humanfriendly.parse_timespan(duration)
     assert wait_for_condition(
-        condition=lambda: 
context.minifi_container.directory_has_single_file_with_content(directory, 
new_content),
-        timeout_seconds=timeout_in_seconds, bail_condition=lambda: 
context.minifi_container.exited, context=context)
+        condition=lambda: 
context.get_default_minifi_container().directory_has_single_file_with_content(directory,
 new_content),
+        timeout_seconds=timeout_in_seconds, bail_condition=lambda: 
context.get_default_minifi_container().exited, context=context)
 
 
-@then('at least one file with the content "{content}" is placed in the 
"{directory}" directory in less than {duration}')
-def step_impl(context: MinifiTestContext, content: str, directory: str, 
duration: str):
+@then('in the "{container_name}" container at least one file with the content 
"{content}" is placed in the "{directory}" directory in less than {duration}')
+def step_impl(context: MinifiTestContext, container_name: str, content: str, 
directory: str, duration: str):
     timeout_in_seconds = humanfriendly.parse_timespan(duration)
     assert wait_for_condition(
-        condition=lambda: 
context.minifi_container.directory_contains_file_with_content(directory, 
content),
-        timeout_seconds=timeout_in_seconds, bail_condition=lambda: 
context.minifi_container.exited, context=context)
+        condition=lambda: 
context.get_minifi_container(container_name).directory_contains_file_with_content(directory,
 content),
+        timeout_seconds=timeout_in_seconds, bail_condition=lambda: 
context.get_minifi_container(container_name).exited, context=context)
+
+
+@then('at least one file with the content "{content}" is placed in the 
"{directory}" directory in less than {duration}')
+def step_impl(context: MinifiTestContext, content: str, directory: str, 
duration: str):
+    context.execute_steps(f'then in the "{DEFAULT_MINIFI_CONTAINER_NAME}" 
container at least one file with the content "{content}" is placed in the 
"{directory}" directory in less than {duration}')
 
 
 @then('the Minifi logs do not contain the following message: "{message}" after 
{duration}')
 def step_impl(context: MinifiTestContext, message: str, duration: str):
     duration_seconds = humanfriendly.parse_timespan(duration)
     time.sleep(duration_seconds)
-    assert message not in context.minifi_container.get_logs()
+    assert message not in context.get_default_minifi_container().get_logs()
 
 
 @then("the Minifi logs do not contain errors")
 def step_impl(context: MinifiTestContext):
-    assert "[error]" not in context.minifi_container.get_logs() or 
context.minifi_container.log_app_output()
+    assert "[error]" not in context.get_default_minifi_container().get_logs() 
or context.get_default_minifi_container().log_app_output()
 
 
 @then("the Minifi logs do not contain warnings")
 def step_impl(context: MinifiTestContext):
-    assert "[warning]" not in context.minifi_container.get_logs() or 
context.minifi_container.log_app_output()
+    assert "[warning]" not in 
context.get_default_minifi_container().get_logs() or 
context.get_default_minifi_container().log_app_output()
 
 
 @then("the Minifi logs contain the following message: '{message}' in less than 
{duration}")
 @then('the Minifi logs contain the following message: "{message}" in less than 
{duration}')
 def step_impl(context: MinifiTestContext, message: str, duration: str):
     duration_seconds = humanfriendly.parse_timespan(duration)
-    assert wait_for_condition(condition=lambda: message in 
context.minifi_container.get_logs(),
-                              timeout_seconds=duration_seconds, 
bail_condition=lambda: context.minifi_container.exited,
+    assert wait_for_condition(condition=lambda: message in 
context.get_default_minifi_container().get_logs(),
+                              timeout_seconds=duration_seconds, 
bail_condition=lambda: context.get_default_minifi_container().exited,
                               context=context)
 
 
 @step('no errors were generated on the http-proxy regarding "{url}"')
 def step_impl(context: MinifiTestContext, url: str):
-    http_proxy_container = next(container for container in context.containers 
if isinstance(container, HttpProxy))
+    http_proxy_container = next(container for container in 
context.containers.values() if isinstance(container, HttpProxy))
     assert http_proxy_container.check_http_proxy_access(url) or 
http_proxy_container.log_app_output()
 
 
@@ -88,16 +93,16 @@ def step_impl(context: MinifiTestContext, url: str):
 @then('there is {num_str} file in the "{directory}" directory in less than 
{duration}')
 def step_impl(context: MinifiTestContext, num_str: str, directory: str, 
duration: str):
     duration_seconds = humanfriendly.parse_timespan(duration)
-    assert wait_for_condition(condition=lambda: 
context.minifi_container.get_number_of_files(directory) == int(num_str),
-                              timeout_seconds=duration_seconds, 
bail_condition=lambda: context.minifi_container.exited,
+    assert wait_for_condition(condition=lambda: 
context.get_default_minifi_container().get_number_of_files(directory) == 
int(num_str),
+                              timeout_seconds=duration_seconds, 
bail_condition=lambda: context.get_default_minifi_container().exited,
                               context=context)
 
 
 @then('there are at least {num_str} files is in the "{directory}" directory in 
less than {duration}')
 def step_impl(context: MinifiTestContext, num_str: str, directory: str, 
duration: str):
     duration_seconds = humanfriendly.parse_timespan(duration)
-    assert wait_for_condition(condition=lambda: 
context.minifi_container.get_number_of_files(directory) >= int(num_str),
-                              timeout_seconds=duration_seconds, 
bail_condition=lambda: context.minifi_container.exited,
+    assert wait_for_condition(condition=lambda: 
context.get_default_minifi_container().get_number_of_files(directory) >= 
int(num_str),
+                              timeout_seconds=duration_seconds, 
bail_condition=lambda: context.get_default_minifi_container().exited,
                               context=context)
 
 
@@ -105,8 +110,8 @@ def step_impl(context: MinifiTestContext, num_str: str, 
directory: str, duration
 def step_impl(context: MinifiTestContext, directory: str, regex_str: str, 
duration: str):
     duration_seconds = humanfriendly.parse_timespan(duration)
     assert wait_for_condition(
-        condition=lambda: 
context.minifi_container.directory_contains_file_with_regex(directory, 
regex_str),
-        timeout_seconds=duration_seconds, bail_condition=lambda: 
context.minifi_container.exited, context=context)
+        condition=lambda: 
context.get_default_minifi_container().directory_contains_file_with_regex(directory,
 regex_str),
+        timeout_seconds=duration_seconds, bail_condition=lambda: 
context.get_default_minifi_container().exited, context=context)
 
 
 @then('the contents of {directory} in less than {timeout} are: "{content_one}" 
and "{content_two}"')
@@ -115,8 +120,8 @@ def step_impl(context: MinifiTestContext, directory: str, 
timeout: str, content_
     c1 = content_one.replace("\\n", "\n")
     c2 = content_two.replace("\\n", "\n")
     contents_arr = [c1, c2]
-    assert wait_for_condition(condition=lambda: 
context.minifi_container.verify_file_contents(directory, contents_arr),
-                              timeout_seconds=timeout_seconds, 
bail_condition=lambda: context.minifi_container.exited,
+    assert wait_for_condition(condition=lambda: 
context.get_default_minifi_container().verify_file_contents(directory, 
contents_arr),
+                              timeout_seconds=timeout_seconds, 
bail_condition=lambda: context.get_default_minifi_container().exited,
                               context=context)
 
 
@@ -125,8 +130,8 @@ def step_impl(context: MinifiTestContext, directory: str, 
timeout: str, contents
     timeout_seconds = humanfriendly.parse_timespan(timeout)
     new_contents = contents.replace("\\n", "\n")
     contents_arr = new_contents.split(",")
-    assert wait_for_condition(condition=lambda: 
context.minifi_container.verify_file_contents(directory, contents_arr),
-                              timeout_seconds=timeout_seconds, 
bail_condition=lambda: context.minifi_container.exited,
+    assert wait_for_condition(condition=lambda: 
context.get_default_minifi_container().verify_file_contents(directory, 
contents_arr),
+                              timeout_seconds=timeout_seconds, 
bail_condition=lambda: context.get_default_minifi_container().exited,
                               context=context)
 
 
@@ -135,5 +140,5 @@ def step_impl(context: MinifiTestContext, directory: str, 
timeout: str, contents
 def step_impl(context: MinifiTestContext, content: str, directory: str, 
duration: str):
     timeout_in_seconds = humanfriendly.parse_timespan(duration)
     assert wait_for_condition(
-        condition=lambda: 
context.minifi_container.verify_path_with_json_content(directory, content),
-        timeout_seconds=timeout_in_seconds, bail_condition=lambda: 
context.minifi_container.exited, context=context)
+        condition=lambda: 
context.get_default_minifi_container().verify_path_with_json_content(directory, 
content),
+        timeout_seconds=timeout_in_seconds, bail_condition=lambda: 
context.get_default_minifi_container().exited, context=context)
diff --git 
a/behave_framework/src/minifi_test_framework/steps/configuration_steps.py 
b/behave_framework/src/minifi_test_framework/steps/configuration_steps.py
index 5eaeaa326..a9794936f 100644
--- a/behave_framework/src/minifi_test_framework/steps/configuration_steps.py
+++ b/behave_framework/src/minifi_test_framework/steps/configuration_steps.py
@@ -22,16 +22,16 @@ from minifi_test_framework.core.minifi_test_context import 
MinifiTestContext
 
 @step('MiNiFi configuration "{config_key}" is set to "{config_value}"')
 def step_impl(context: MinifiTestContext, config_key: str, config_value: str):
-    context.minifi_container.set_property(config_key, config_value)
+    context.get_or_create_default_minifi_container().set_property(config_key, 
config_value)
 
 
 @step("log metrics publisher is enabled in MiNiFi")
 def step_impl(context):
-    
context.minifi_container.set_property("nifi.metrics.publisher.LogMetricsPublisher.metrics",
 "RepositoryMetrics")
-    
context.minifi_container.set_property("nifi.metrics.publisher.LogMetricsPublisher.logging.interval",
 "1s")
-    context.minifi_container.set_property("nifi.metrics.publisher.class", 
"LogMetricsPublisher")
+    
context.get_or_create_default_minifi_container().set_property("nifi.metrics.publisher.LogMetricsPublisher.metrics",
 "RepositoryMetrics")
+    
context.get_or_create_default_minifi_container().set_property("nifi.metrics.publisher.LogMetricsPublisher.logging.interval",
 "1s")
+    
context.get_or_create_default_minifi_container().set_property("nifi.metrics.publisher.class",
 "LogMetricsPublisher")
 
 
 @step('log property "{log_property_key}" is set to "{log_property_value}"')
 def step_impl(context: MinifiTestContext, log_property_key: str, 
log_property_value: str):
-    context.minifi_container.set_log_property(log_property_key, 
log_property_value)
+    
context.get_or_create_default_minifi_container().set_log_property(log_property_key,
 log_property_value)
diff --git a/behave_framework/src/minifi_test_framework/steps/core_steps.py 
b/behave_framework/src/minifi_test_framework/steps/core_steps.py
index f4d55357b..552ea2d19 100644
--- a/behave_framework/src/minifi_test_framework/steps/core_steps.py
+++ b/behave_framework/src/minifi_test_framework/steps/core_steps.py
@@ -21,24 +21,24 @@ import string
 import os
 
 import humanfriendly
-from behave import when, step
+from behave import when, step, given
+
 from minifi_test_framework.containers.directory import Directory
 from minifi_test_framework.containers.file import File
-from minifi_test_framework.core.minifi_test_context import MinifiTestContext
+from minifi_test_framework.core.minifi_test_context import 
DEFAULT_MINIFI_CONTAINER_NAME, MinifiTestContext
 
 
 @when("both instances start up")
 @when("all instances start up")
 def step_impl(context: MinifiTestContext):
-    for container in context.containers:
-        assert container.deploy()
-    assert context.minifi_container.deploy() or 
context.minifi_container.log_app_output()
+    for container in context.containers.values():
+        assert container.deploy() or container.log_app_output()
     logging.debug("All instances started up")
 
 
 @when("the MiNiFi instance starts up")
 def step_impl(context):
-    assert context.minifi_container.deploy()
+    assert context.get_or_create_default_minifi_container().deploy()
     logging.debug("MiNiFi instance started up")
 
 
@@ -48,10 +48,21 @@ def step_impl(context: MinifiTestContext, directory: str, 
size: str):
     content = ''.join(random.choice(string.ascii_uppercase + string.digits) 
for _ in range(size))
     new_dir = Directory(directory)
     new_dir.files["input.txt"] = content
-    context.minifi_container.dirs.append(new_dir)
+    context.get_or_create_default_minifi_container().dirs.append(new_dir)
 
 
 @step('a file with filename "{file_name}" and content "{content}" is present 
in "{path}"')
 def step_impl(context: MinifiTestContext, file_name: str, content: str, path: 
str):
     new_content = content.replace("\\n", "\n")
-    context.minifi_container.files.append(File(os.path.join(path, file_name), 
new_content))
+    
context.get_or_create_default_minifi_container().files.append(File(os.path.join(path,
 file_name), new_content))
+
+
+@given('a host resource file "{filename}" is bound to the "{container_path}" 
path in the MiNiFi container "{container_name}"')
+def step_impl(context: MinifiTestContext, filename: str, container_path: str, 
container_name: str):
+    path = os.path.join(context.resource_dir, filename)
+    context.get_or_create_minifi_container(container_name).add_host_file(path, 
container_path)
+
+
+@given('a host resource file "{filename}" is bound to the "{container_path}" 
path in the MiNiFi container')
+def step_impl(context: MinifiTestContext, filename: str, container_path: str):
+    context.execute_steps(f"given a host resource file \"{filename}\" is bound 
to the \"{container_path}\" path in the MiNiFi container 
\"{DEFAULT_MINIFI_CONTAINER_NAME}\"")
diff --git 
a/behave_framework/src/minifi_test_framework/steps/flow_building_steps.py 
b/behave_framework/src/minifi_test_framework/steps/flow_building_steps.py
index a01ec10f9..a41d096c7 100644
--- a/behave_framework/src/minifi_test_framework/steps/flow_building_steps.py
+++ b/behave_framework/src/minifi_test_framework/steps/flow_building_steps.py
@@ -19,7 +19,7 @@ from behave import given, step
 
 from minifi_test_framework.containers.directory import Directory
 from minifi_test_framework.containers.http_proxy_container import HttpProxy
-from minifi_test_framework.core.minifi_test_context import MinifiTestContext
+from minifi_test_framework.core.minifi_test_context import 
DEFAULT_MINIFI_CONTAINER_NAME, MinifiTestContext
 from minifi_test_framework.minifi.connection import Connection
 from minifi_test_framework.minifi.controller_service import ControllerService
 from minifi_test_framework.minifi.funnel import Funnel
@@ -30,8 +30,8 @@ from minifi_test_framework.minifi.processor import Processor
 
 @given("a transient MiNiFi flow with a LogOnDestructionProcessor processor")
 def step_impl(context: MinifiTestContext):
-    context.minifi_container.command = ["/bin/sh", "-c", "timeout 10s 
./bin/minifi.sh run && sleep 100"]
-    context.minifi_container.flow_definition.add_processor(
+    context.get_or_create_default_minifi_container().command = ["/bin/sh", 
"-c", "timeout 10s ./bin/minifi.sh run && sleep 100"]
+    
context.get_or_create_default_minifi_container().flow_definition.add_processor(
         Processor("LogOnDestructionProcessor", "LogOnDestructionProcessor"))
 
 
@@ -41,7 +41,7 @@ def step_impl(context: MinifiTestContext, processor_type: 
str, processor_name: s
               property_value: str):
     processor = Processor(processor_type, processor_name)
     processor.add_property(property_name, property_value)
-    context.minifi_container.flow_definition.add_processor(processor)
+    
context.get_or_create_default_minifi_container().flow_definition.add_processor(processor)
 
 
 @step('a {processor_type} processor with the "{property_name}" property set to 
"{property_value}"')
@@ -50,75 +50,102 @@ def step_impl(context: MinifiTestContext, processor_type: 
str, property_name: st
         f'Given a {processor_type} processor with the name "{processor_type}" 
and the "{property_name}" property set to "{property_value}"')
 
 
+@step('a {processor_type} processor with the "{property_name}" property set to 
"{property_value}" in the "{minifi_container_name}" flow')
+def step_impl(context: MinifiTestContext, processor_type: str, property_name: 
str, property_value: str, minifi_container_name: str):
+    processor = Processor(processor_type, processor_type)
+    processor.add_property(property_name, property_value)
+    
context.get_or_create_minifi_container(minifi_container_name).flow_definition.add_processor(processor)
+
+
 @given('a {processor_type} processor with the name "{processor_name}"')
 def step_impl(context: MinifiTestContext, processor_type: str, processor_name: 
str):
     processor = Processor(processor_type, processor_name)
-    context.minifi_container.flow_definition.add_processor(processor)
+    
context.get_or_create_default_minifi_container().flow_definition.add_processor(processor)
+
+
+@given("a {processor_type} processor in the \"{minifi_container_name}\" flow")
+def step_impl(context: MinifiTestContext, processor_type: str, 
minifi_container_name: str):
+    processor = Processor(processor_type, processor_type)
+    
context.get_or_create_minifi_container(minifi_container_name).flow_definition.add_processor(processor)
 
 
 @given("a {processor_type} processor")
 def step_impl(context: MinifiTestContext, processor_type: str):
-    processor = Processor(processor_type, processor_type)
-    context.minifi_container.flow_definition.add_processor(processor)
+    context.execute_steps(f'given a {processor_type} processor in the 
"{DEFAULT_MINIFI_CONTAINER_NAME}" flow')
 
 
 @step('the "{property_name}" property of the {processor_name} processor is set 
to "{property_value}"')
 def step_impl(context: MinifiTestContext, property_name: str, processor_name: 
str, property_value: str):
-    processor = 
context.minifi_container.flow_definition.get_processor(processor_name)
+    processor = 
context.get_or_create_default_minifi_container().flow_definition.get_processor(processor_name)
     processor.add_property(property_name, property_value)
 
 
 @step('a Funnel with the name "{funnel_name}" is set up')
 def step_impl(context: MinifiTestContext, funnel_name: str):
-    context.minifi_container.flow_definition.add_funnel(Funnel(funnel_name))
+    
context.get_or_create_default_minifi_container().flow_definition.add_funnel(Funnel(funnel_name))
+
+
+@step('in the "{minifi_container_name}" flow the "{relationship_name}" 
relationship of the {source} processor is connected to the {target}')
+def step_impl(context: MinifiTestContext, relationship_name: str, source: str, 
target: str, minifi_container_name: str):
+    connection = Connection(source_name=source, 
source_relationship=relationship_name, target_name=target)
+    
context.get_or_create_minifi_container(minifi_container_name).flow_definition.add_connection(connection)
 
 
 @step('the "{relationship_name}" relationship of the {source} processor is 
connected to the {target}')
 def step_impl(context: MinifiTestContext, relationship_name: str, source: str, 
target: str):
-    connection = Connection(source_name=source, 
source_relationship=relationship_name, target_name=target)
-    context.minifi_container.flow_definition.add_connection(connection)
+    context.execute_steps(f'given in the "{DEFAULT_MINIFI_CONTAINER_NAME}" 
flow the "{relationship_name}" relationship of the {source} processor is 
connected to the {target}')
 
 
 @step('the Funnel with the name "{funnel_name}" is connected to the {target}')
 def step_impl(context: MinifiTestContext, funnel_name: str, target: str):
     connection = Connection(source_name=funnel_name, 
source_relationship="success", target_name=target)
-    context.minifi_container.flow_definition.add_connection(connection)
+    
context.get_or_create_default_minifi_container().flow_definition.add_connection(connection)
+
+
+@step("{processor_name}'s {relationship} relationship is auto-terminated in 
the \"{minifi_container_name}\" flow")
+def step_impl(context: MinifiTestContext, processor_name: str, relationship: 
str, minifi_container_name: str):
+    
context.get_or_create_minifi_container(minifi_container_name).flow_definition.get_processor(processor_name).auto_terminated_relationships.append(
+        relationship)
 
 
 @step("{processor_name}'s {relationship} relationship is auto-terminated")
 def step_impl(context: MinifiTestContext, processor_name: str, relationship: 
str):
-    
context.minifi_container.flow_definition.get_processor(processor_name).auto_terminated_relationships.append(
-        relationship)
+    context.execute_steps(f'given {processor_name}\'s {relationship} 
relationship is auto-terminated in the "{DEFAULT_MINIFI_CONTAINER_NAME}" flow')
 
 
 @given("a transient MiNiFi flow is set up")
 def step_impl(context: MinifiTestContext):
-    context.minifi_container.command = ["/bin/sh", "-c", "timeout 10s 
./bin/minifi.sh run && sleep 100"]
+    context.get_or_create_default_minifi_container().command = ["/bin/sh", 
"-c", "timeout 10s ./bin/minifi.sh run && sleep 100"]
 
 
 @step('the scheduling period of the {processor_name} processor is set to 
"{duration_str}"')
 def step_impl(context: MinifiTestContext, processor_name: str, duration_str: 
str):
-    
context.minifi_container.flow_definition.get_processor(processor_name).scheduling_period
 = duration_str
+    
context.get_or_create_default_minifi_container().flow_definition.get_processor(processor_name).scheduling_period
 = duration_str
 
 
 @given("parameter context name is set to '{context_name}'")
 def step_impl(context: MinifiTestContext, context_name: str):
-    
context.minifi_container.flow_definition.parameter_contexts.append(ParameterContext(context_name))
+    
context.get_or_create_default_minifi_container().flow_definition.parameter_contexts.append(ParameterContext(context_name))
 
 
 @step(
     "a non-sensitive parameter in the flow config called '{parameter_name}' 
with the value '{parameter_value}' in the parameter context '{context_name}'")
 def step_impl(context: MinifiTestContext, parameter_name: str, 
parameter_value: str, context_name: str):
-    parameter_context = 
context.minifi_container.flow_definition.get_parameter_context(context_name)
+    parameter_context = 
context.get_or_create_default_minifi_container().flow_definition.get_parameter_context(context_name)
     parameter_context.parameters.append(Parameter(parameter_name, 
parameter_value, False))
 
 
-@step('a directory at "{directory}" has a file with the content "{content}"')
-def step_impl(context: MinifiTestContext, directory: str, content: str):
+@step('a directory at "{directory}" has a file with the content "{content}" in 
the "{flow_name}" flow')
+def step_impl(context: MinifiTestContext, directory: str, content: str, 
flow_name: str):
     new_content = content.replace("\\n", "\n")
     new_dir = Directory(directory)
     new_dir.files["input.txt"] = new_content
-    context.minifi_container.dirs.append(new_dir)
+    context.get_or_create_minifi_container(flow_name).dirs.append(new_dir)
+
+
+@step('a directory at "{directory}" has a file with the content "{content}"')
+def step_impl(context: MinifiTestContext, directory: str, content: str):
+    context.execute_steps(f'given a directory at "{directory}" has a file with 
the content "{content}" in the "{DEFAULT_MINIFI_CONTAINER_NAME}" flow')
 
 
 @step('a directory at "{directory}" has a file ("{file_name}") with the 
content "{content}"')
@@ -126,19 +153,26 @@ def step_impl(context: MinifiTestContext, directory: str, 
file_name: str, conten
     new_content = content.replace("\\n", "\n")
     new_dir = Directory(directory)
     new_dir.files[file_name] = new_content
-    context.minifi_container.dirs.append(new_dir)
+    context.get_or_create_default_minifi_container().dirs.append(new_dir)
+
+
+@given("these processor properties are set in the \"{minifi_container_name}\" 
flow")
+def step_impl(context: MinifiTestContext, minifi_container_name: str):
+    for row in context.table:
+        processor = 
context.get_or_create_minifi_container(minifi_container_name).flow_definition.get_processor(row["processor
 name"])
+        processor.add_property(row["property name"], row["property value"])
 
 
 @given("these processor properties are set")
 def step_impl(context: MinifiTestContext):
     for row in context.table:
-        processor = 
context.minifi_container.flow_definition.get_processor(row["processor name"])
+        processor = 
context.get_or_create_default_minifi_container().flow_definition.get_processor(row["processor
 name"])
         processor.add_property(row["property name"], row["property value"])
 
 
 @step("the http proxy server is set up")
 def step_impl(context):
-    context.containers.append(HttpProxy(context))
+    context.containers["http-proxy"] = HttpProxy(context)
 
 
 @step("the processors are connected up as described here")
@@ -148,23 +182,28 @@ def step_impl(context: MinifiTestContext):
         dest_proc_name = row["destination name"]
         relationship = row["relationship name"]
         if dest_proc_name == "auto-terminated":
-            context.minifi_container.flow_definition.get_processor(
+            
context.get_or_create_default_minifi_container().flow_definition.get_processor(
                 
source_proc_name).auto_terminated_relationships.append(relationship)
         else:
             connection = Connection(source_name=row["source name"], 
source_relationship=relationship,
                                     target_name=row["destination name"])
-            context.minifi_container.flow_definition.add_connection(connection)
+            
context.get_or_create_default_minifi_container().flow_definition.add_connection(connection)
+
+
+@step("{processor_name} is EVENT_DRIVEN in the \"{minifi_container_name}\" 
flow")
+def step_impl(context: MinifiTestContext, processor_name: str, 
minifi_container_name: str):
+    processor = 
context.get_or_create_minifi_container(minifi_container_name).flow_definition.get_processor(processor_name)
+    processor.scheduling_strategy = "EVENT_DRIVEN"
 
 
 @step("{processor_name} is EVENT_DRIVEN")
 def step_impl(context: MinifiTestContext, processor_name: str):
-    processor = 
context.minifi_container.flow_definition.get_processor(processor_name)
-    processor.scheduling_strategy = "EVENT_DRIVEN"
+    context.execute_steps(f'given {processor_name} is EVENT_DRIVEN in the 
"{DEFAULT_MINIFI_CONTAINER_NAME}" flow')
 
 
 @step("{processor_name} is TIMER_DRIVEN with {scheduling_period} scheduling 
period")
 def step_impl(context: MinifiTestContext, processor_name: str, 
scheduling_period: int):
-    processor = 
context.minifi_container.flow_definition.get_processor(processor_name)
+    processor = 
context.get_or_create_default_minifi_container().flow_definition.get_processor(processor_name)
     processor.scheduling_strategy = "TIMER_DRIVEN"
     processor.scheduling_period = scheduling_period
 
@@ -173,11 +212,11 @@ def step_impl(context: MinifiTestContext, processor_name: 
str, scheduling_period
 @given("an {service_name} controller service is set up")
 def step_impl(context: MinifiTestContext, service_name: str):
     controller_service = ControllerService(class_name=service_name, 
service_name=service_name)
-    
context.minifi_container.flow_definition.controller_services.append(controller_service)
+    
context.get_or_create_default_minifi_container().flow_definition.controller_services.append(controller_service)
 
 
 @given('a {service_name} controller service is set up and the 
"{property_name}" property set to "{property_value}"')
 def step_impl(context: MinifiTestContext, service_name: str, property_name: 
str, property_value: str):
     controller_service = ControllerService(class_name=service_name, 
service_name=service_name)
     controller_service.add_property(property_name, property_value)
-    
context.minifi_container.flow_definition.controller_services.append(controller_service)
+    
context.get_or_create_default_minifi_container().flow_definition.controller_services.append(controller_service)
diff --git a/docker/RunBehaveTests.sh b/docker/RunBehaveTests.sh
index 4bac967f0..89d59f076 100755
--- a/docker/RunBehaveTests.sh
+++ b/docker/RunBehaveTests.sh
@@ -198,4 +198,5 @@ exec \
     "${docker_dir}/../extensions/aws/tests/features" \
     "${docker_dir}/../extensions/azure/tests/features" \
     "${docker_dir}/../extensions/sql/tests/features" \
-    "${docker_dir}/../extensions/llamacpp/tests/features"
+    "${docker_dir}/../extensions/llamacpp/tests/features" \
+    "${docker_dir}/../extensions/opc/tests/features"
diff --git a/docker/test/integration/cluster/ContainerStore.py 
b/docker/test/integration/cluster/ContainerStore.py
index 9b5cba6b3..a78b0712c 100644
--- a/docker/test/integration/cluster/ContainerStore.py
+++ b/docker/test/integration/cluster/ContainerStore.py
@@ -26,7 +26,6 @@ from .containers.FakeGcsServerContainer import 
FakeGcsServerContainer
 from .containers.HttpProxyContainer import HttpProxyContainer
 from .containers.PostgreSQLServerContainer import PostgreSQLServerContainer
 from .containers.MqttBrokerContainer import MqttBrokerContainer
-from .containers.OPCUAServerContainer import OPCUAServerContainer
 from .containers.SplunkContainer import SplunkContainer
 from .containers.ElasticsearchContainer import ElasticsearchContainer
 from .containers.OpensearchContainer import OpensearchContainer
@@ -184,14 +183,6 @@ class ContainerStore:
                                                                   
network=self.network,
                                                                   
image_store=self.image_store,
                                                                   
command=command))
-        elif engine == 'opcua-server':
-            return self.containers.setdefault(container_name,
-                                              
OPCUAServerContainer(feature_context=feature_context,
-                                                                   
name=container_name,
-                                                                   
vols=self.vols,
-                                                                   
network=self.network,
-                                                                   
image_store=self.image_store,
-                                                                   
command=command))
         elif engine == 'splunk':
             return self.containers.setdefault(container_name,
                                               
SplunkContainer(feature_context=feature_context,
diff --git a/docker/test/integration/cluster/DockerTestDirectoryBindings.py 
b/docker/test/integration/cluster/DockerTestDirectoryBindings.py
index 25add90ae..9834bc524 100644
--- a/docker/test/integration/cluster/DockerTestDirectoryBindings.py
+++ b/docker/test/integration/cluster/DockerTestDirectoryBindings.py
@@ -55,7 +55,6 @@ class DockerTestDirectoryBindings:
         # Add resources
         test_dir = os.environ['TEST_DIRECTORY']  # Based on DockerVerify.sh
         shutil.copytree(test_dir + "/resources/python", 
self.data_directories[self.feature_id]["resources_dir"] + "/python")
-        shutil.copytree(test_dir + "/resources/opcua", 
self.data_directories[self.feature_id]["resources_dir"] + "/opcua")
         shutil.copytree(test_dir + "/resources/lua", 
self.data_directories[self.feature_id]["resources_dir"] + "/lua")
         shutil.copytree(test_dir + "/resources/minifi", 
self.data_directories[self.feature_id]["minifi_config_dir"], dirs_exist_ok=True)
         shutil.copytree(test_dir + "/resources/minifi-controller", 
self.data_directories[self.feature_id]["resources_dir"] + "/minifi-controller")
diff --git a/docker/test/integration/features/http.feature 
b/docker/test/integration/features/http.feature
index 0cd01fbbb..19a9d3745 100644
--- a/docker/test/integration/features/http.feature
+++ b/docker/test/integration/features/http.feature
@@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-@CORE
+@ENABLE_CIVET
 Feature: Sending data using InvokeHTTP to a receiver using ListenHTTP
   In order to send and receive data via HTTP
   As a user of MiNiFi
diff --git a/docker/test/integration/features/https.feature 
b/docker/test/integration/features/https.feature
index f4ff9d246..863cc782e 100644
--- a/docker/test/integration/features/https.feature
+++ b/docker/test/integration/features/https.feature
@@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-@CORE
+@ENABLE_CIVET
 Feature: Transfer data from and to MiNiFi using HTTPS
 
   Background:
diff --git a/docker/test/integration/features/steps/steps.py 
b/docker/test/integration/features/steps/steps.py
index 590fa06d0..87285e409 100644
--- a/docker/test/integration/features/steps/steps.py
+++ b/docker/test/integration/features/steps/steps.py
@@ -680,17 +680,6 @@ def step_impl(context):
     context.test.acquire_container(context=context, name="postgresql-server", 
engine="postgresql-server")
 
 
-# OPC UA
-@given("an OPC UA server is set up")
-def step_impl(context):
-    context.test.acquire_container(context=context, name="opcua-server", 
engine="opcua-server")
-
-
-@given("an OPC UA server is set up with access control")
-def step_impl(context):
-    context.test.acquire_container(context=context, name="opcua-server", 
engine="opcua-server", 
command=["/opt/open62541/examples/access_control_server"])
-
-
 @when("the MiNiFi instance starts up")
 @when("both instances start up")
 @when("all instances start up")
@@ -917,7 +906,7 @@ def step_impl(context, blob_and_snapshot_count, 
timeout_seconds):
 
 # SQL
 @then("the query \"{query}\" returns {number_of_rows:d} rows in less than 
{timeout_seconds:d} seconds on the PostgreSQL server")
-def step_impl(context, query, number_of_rows, timeout_seconds):
+def step_impl(context, query: str, number_of_rows: int, timeout_seconds: int):
     
context.test.check_query_results(context.test.get_container_name_with_postfix("postgresql-server"),
 query, number_of_rows, timeout_seconds)
 
 
@@ -943,11 +932,6 @@ def step_impl(context, regex, duration):
     context.test.check_minifi_log_matches_regex(regex, 
humanfriendly.parse_timespan(duration))
 
 
-@then("the OPC UA server logs contain the following message: \"{log_message}\" 
in less than {duration}")
-def step_impl(context, log_message, duration):
-    context.test.check_container_log_contents("opcua-server", log_message, 
humanfriendly.parse_timespan(duration))
-
-
 # MQTT
 @then("the MQTT broker has a log line matching \"{log_pattern}\"")
 def step_impl(context, log_pattern):
diff --git a/extensions/aws/tests/features/steps/steps.py 
b/extensions/aws/tests/features/steps/steps.py
index e1d6c4cd7..6f8d1d757 100644
--- a/extensions/aws/tests/features/steps/steps.py
+++ b/extensions/aws/tests/features/steps/steps.py
@@ -46,32 +46,32 @@ def step_impl(context: MinifiTestContext, processor_name: 
str):
     processor.add_property('Proxy Username', '')
     processor.add_property('Proxy Password', '')
 
-    context.minifi_container.flow_definition.add_processor(processor)
+    
context.get_or_create_default_minifi_container().flow_definition.add_processor(processor)
 
 
 @step('a s3 server is set up in correspondence with the {processor_name}')
 @step('an s3 server is set up in correspondence with the {processor_name}')
 def step_impl(context: MinifiTestContext, processor_name: str):
-    context.containers.append(S3ServerContainer(context))
+    context.containers["s3-server"] = S3ServerContainer(context)
 
 
 @step('the object on the s3 server is "{object_data}"')
 def step_impl(context: MinifiTestContext, object_data: str):
-    s3_server_container = context.containers[0]
+    s3_server_container = context.containers["s3-server"]
     assert isinstance(s3_server_container, S3ServerContainer)
     assert s3_server_container.check_s3_server_object_data(object_data)
 
 
 @step('the object content type on the s3 server is "{content_type}" and the 
object metadata matches use metadata')
 def step_impl(context: MinifiTestContext, content_type: str):
-    s3_server_container = context.containers[0]
+    s3_server_container = context.containers["s3-server"]
     assert isinstance(s3_server_container, S3ServerContainer)
     assert s3_server_container.check_s3_server_object_metadata(content_type)
 
 
 @step("the object bucket on the s3 server is empty in less than 10 seconds")
 def step_impl(context):
-    s3_server_container = context.containers[0]
+    s3_server_container = context.containers["s3-server"]
     assert isinstance(s3_server_container, S3ServerContainer)
     assert wait_for_condition(
         condition=lambda: s3_server_container.is_s3_bucket_empty(),
@@ -80,7 +80,7 @@ def step_impl(context):
 
 @step("the object on the s3 server is present and matches the original hash")
 def step_impl(context):
-    s3_server_container = context.containers[0]
+    s3_server_container = context.containers["s3-server"]
 
     assert isinstance(s3_server_container, S3ServerContainer)
     assert 
s3_server_container.check_s3_server_object_hash(context.original_hash)
@@ -98,5 +98,5 @@ def step_impl(context):
     content = ''.join(random.choice(string.ascii_uppercase + string.digits) 
for _ in range(size))
     new_dir = Directory("/tmp/input")
     new_dir.files["input.txt"] = content
-    context.minifi_container.dirs.append(new_dir)
+    context.get_or_create_default_minifi_container().dirs.append(new_dir)
     context.original_hash = computeMD5hash(content)
diff --git a/extensions/azure/tests/features/steps/steps.py 
b/extensions/azure/tests/features/steps/steps.py
index 38721f99b..3331d7e71 100644
--- a/extensions/azure/tests/features/steps/steps.py
+++ b/extensions/azure/tests/features/steps/steps.py
@@ -37,41 +37,39 @@ def step_impl(context: MinifiTestContext, processor_name: 
str):
                            
f'DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint={hostname}:10000/devstoreaccount1;QueueEndpoint={hostname}:10001/devstoreaccount1;')
     processor.add_property('Blob', 'test-blob')
     processor.add_property('Create Container', 'true')
-    context.minifi_container.flow_definition.add_processor(processor)
+    
context.get_or_create_default_minifi_container().flow_definition.add_processor(processor)
 
 
 @step("an Azure storage server is set up")
 def step_impl(context):
-    context.containers.append(AzureServerContainer(context))
-    azure_server_container = context.containers[0]
-    assert isinstance(azure_server_container, AzureServerContainer)
-    assert azure_server_container.deploy()
+    context.containers["azure-storage-server"] = AzureServerContainer(context)
+    assert context.containers["azure-storage-server"].deploy()
 
 
 @then('the object on the Azure storage server is "{object_data}"')
 def step_impl(context: MinifiTestContext, object_data: str):
-    azure_server_container = context.containers[0]
+    azure_server_container = context.containers["azure-storage-server"]
     assert isinstance(azure_server_container, AzureServerContainer)
     assert azure_server_container.check_azure_storage_server_data(object_data)
 
 
 @step('test blob "{blob_name}" with the content "{data}" is created on Azure 
blob storage')
 def step_impl(context: MinifiTestContext, blob_name: str, data: str):
-    azure_server_container = context.containers[0]
+    azure_server_container = context.containers["azure-storage-server"]
     assert isinstance(azure_server_container, AzureServerContainer)
     assert azure_server_container.add_test_blob(blob_name, content=data)
 
 
 @step('test blob "{blob_name}" is created on Azure blob storage')
 def step_impl(context: MinifiTestContext, blob_name: str):
-    azure_server_container = context.containers[0]
+    azure_server_container = context.containers["azure-storage-server"]
     assert isinstance(azure_server_container, AzureServerContainer)
     assert azure_server_container.add_test_blob(blob_name)
 
 
 @step('test blob "{blob_name}" is created on Azure blob storage with a 
snapshot')
 def step_impl(context: MinifiTestContext, blob_name: str):
-    azure_server_container = context.containers[0]
+    azure_server_container = context.containers["azure-storage-server"]
     assert isinstance(azure_server_container, AzureServerContainer)
     assert azure_server_container.add_test_blob(blob_name, with_snapshot=True)
 
@@ -79,20 +77,22 @@ def step_impl(context: MinifiTestContext, blob_name: str):
 @then("the Azure blob storage becomes empty in {timeout_str}")
 def step_impl(context: MinifiTestContext, timeout_str: str):
     timeout_in_seconds = humanfriendly.parse_timespan(timeout_str)
-    azure_server_container = context.containers[0]
+    azure_server_container = context.containers["azure-storage-server"]
     assert isinstance(azure_server_container, AzureServerContainer)
-    assert wait_for_condition(condition=lambda: 
azure_server_container.check_azure_blob_storage_is_empty(),
-                              timeout_seconds=timeout_in_seconds,
-                              bail_condition=lambda: 
context.minifi_container.exited,
-                              context=context)
+    assert wait_for_condition(
+        condition=lambda: 
azure_server_container.check_azure_blob_storage_is_empty(),
+        timeout_seconds=timeout_in_seconds,
+        bail_condition=lambda: azure_server_container.exited,
+        context=context)
 
 
 @then("the blob and snapshot count becomes 1 in {timeout_str}")
 def step_impl(context: MinifiTestContext, timeout_str: str):
     timeout_in_seconds = humanfriendly.parse_timespan(timeout_str)
-    azure_server_container = context.containers[0]
+    azure_server_container = context.containers["azure-storage-server"]
     assert isinstance(azure_server_container, AzureServerContainer)
-    assert wait_for_condition(condition=lambda: 
azure_server_container.check_azure_blob_and_snapshot_count(1),
-                              timeout_seconds=timeout_in_seconds,
-                              bail_condition=lambda: 
context.minifi_container.exited,
-                              context=context)
+    assert wait_for_condition(
+        condition=lambda: 
azure_server_container.check_azure_blob_and_snapshot_count(1),
+        timeout_seconds=timeout_in_seconds,
+        bail_condition=lambda: azure_server_container.exited,
+        context=context)
diff --git a/extensions/opc/tests/features/environment.py 
b/extensions/opc/tests/features/environment.py
new file mode 100644
index 000000000..a02cce4c3
--- /dev/null
+++ b/extensions/opc/tests/features/environment.py
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+from minifi_test_framework.core.hooks import common_before_scenario
+from minifi_test_framework.core.hooks import common_after_scenario
+
+
+def before_scenario(context, scenario):
+    common_before_scenario(context, scenario)
+    context.resource_dir = os.path.join(os.path.dirname(__file__), 'resources')
+
+
+def after_scenario(context, scenario):
+    common_after_scenario(context, scenario)
diff --git a/docker/test/integration/features/opcua.feature 
b/extensions/opc/tests/features/opcua.feature
similarity index 69%
rename from docker/test/integration/features/opcua.feature
rename to extensions/opc/tests/features/opcua.feature
index 8f561f356..064e32d32 100644
--- a/docker/test/integration/features/opcua.feature
+++ b/extensions/opc/tests/features/opcua.feature
@@ -19,16 +19,16 @@ Feature: Putting and fetching data to OPC UA server
   As a user of MiNiFi
   I need to have PutOPCProcessor and FetchOPCProcessor
 
-  Background:
-    Given the content of "/tmp/output" is monitored
-
   Scenario Outline: Create and fetch data from an OPC UA node
     Given a GetFile processor with the "Input Directory" property set to 
"/tmp/input" in the "create-opc-ua-node" flow
-    And a file with the content "<Value>" is present in "/tmp/input"
+    And a directory at "/tmp/input" has a file with the content "<Value>" in 
the "create-opc-ua-node" flow
     And a PutOPCProcessor processor in the "create-opc-ua-node" flow
+    And PutOPCProcessor is EVENT_DRIVEN in the "create-opc-ua-node" flow
     And a FetchOPCProcessor processor in the "fetch-opc-ua-node" flow
     And a PutFile processor with the "Directory" property set to "/tmp/output" 
in the "fetch-opc-ua-node" flow
-    And these processor properties are set:
+    And PutFile's success relationship is auto-terminated in the 
"fetch-opc-ua-node" flow
+    And PutFile is EVENT_DRIVEN in the "fetch-opc-ua-node" flow
+    And these processor properties are set in the "create-opc-ua-node" flow
       | processor name    | property name               | property value       
                             |
       | PutOPCProcessor   | Parent node ID              | 85                   
                             |
       | PutOPCProcessor   | Parent node ID type         | Int                  
                             |
@@ -36,21 +36,23 @@ Feature: Putting and fetching data to OPC UA server
       | PutOPCProcessor   | Target node ID type         | Int                  
                             |
       | PutOPCProcessor   | Target node namespace index | 1                    
                             |
       | PutOPCProcessor   | Value type                  | <Value Type>         
                             |
-      | PutOPCProcessor   | OPC server endpoint         | 
opc.tcp://opcua-server-${feature_id}:4840/        |
+      | PutOPCProcessor   | OPC server endpoint         | 
opc.tcp://opcua-server-${scenario_id}:4840/       |
       | PutOPCProcessor   | Target node browse name     | testnodename         
                             |
+    And these processor properties are set in the "fetch-opc-ua-node" flow
+      | processor name    | property name               | property value       
                             |
       | FetchOPCProcessor | Node ID                     | 9999                 
                             |
       | FetchOPCProcessor | Node ID type                | Int                  
                             |
       | FetchOPCProcessor | Namespace index             | 1                    
                             |
-      | FetchOPCProcessor | OPC server endpoint         | 
opc.tcp://opcua-server-${feature_id}:4840/        |
+      | FetchOPCProcessor | OPC server endpoint         | 
opc.tcp://opcua-server-${scenario_id}:4840/       |
       | FetchOPCProcessor | Max depth                   | 1                    
                             |
 
-    And the "success" relationship of the GetFile processor is connected to 
the PutOPCProcessor
-    And the "success" relationship of the FetchOPCProcessor processor is 
connected to the PutFile
+    And in the "create-opc-ua-node" flow the "success" relationship of the 
GetFile processor is connected to the PutOPCProcessor
+    And in the "fetch-opc-ua-node" flow the "success" relationship of the 
FetchOPCProcessor processor is connected to the PutFile
 
     And an OPC UA server is set up
 
     When all instances start up
-    Then at least one flowfile with the content "<Value>" is placed in the 
monitored directory in less than 60 seconds
+    Then in the "fetch-opc-ua-node" container at least one file with the 
content "<Value>" is placed in the "/tmp/output" directory in less than 60 
seconds
 
   Examples: Topic names and formats to test
     | Value Type   | Value   |
@@ -61,11 +63,14 @@ Feature: Putting and fetching data to OPC UA server
 
   Scenario Outline: Update and fetch data from an OPC UA node
     Given a GetFile processor with the "Input Directory" property set to 
"/tmp/input" in the "update-opc-ua-node" flow
-    And a file with the content "<Value>" is present in "/tmp/input"
+    And a directory at "/tmp/input" has a file with the content "<Value>" in 
the "update-opc-ua-node" flow
     And a PutOPCProcessor processor in the "update-opc-ua-node" flow
+    And PutOPCProcessor is EVENT_DRIVEN in the "update-opc-ua-node" flow
     And a FetchOPCProcessor processor in the "fetch-opc-ua-node" flow
     And a PutFile processor with the "Directory" property set to "/tmp/output" 
in the "fetch-opc-ua-node" flow
-    And these processor properties are set:
+    And PutFile's success relationship is auto-terminated in the 
"fetch-opc-ua-node" flow
+    And PutFile is EVENT_DRIVEN in the "fetch-opc-ua-node" flow
+    And these processor properties are set in the "update-opc-ua-node" flow
       | processor name    | property name               | property value       
                             |
       | PutOPCProcessor   | Parent node ID              | 85                   
                             |
       | PutOPCProcessor   | Parent node ID type         | Int                  
                             |
@@ -73,21 +78,23 @@ Feature: Putting and fetching data to OPC UA server
       | PutOPCProcessor   | Target node ID type         | <Node ID Type>       
                             |
       | PutOPCProcessor   | Target node namespace index | 1                    
                             |
       | PutOPCProcessor   | Value type                  | <Value Type>         
                             |
-      | PutOPCProcessor   | OPC server endpoint         | 
opc.tcp://opcua-server-${feature_id}:4840/        |
+      | PutOPCProcessor   | OPC server endpoint         | 
opc.tcp://opcua-server-${scenario_id}:4840/       |
       | PutOPCProcessor   | Target node browse name     | testnodename         
                             |
+    And these processor properties are set in the "fetch-opc-ua-node" flow
+      | processor name    | property name               | property value       
                             |
       | FetchOPCProcessor | Node ID                     | <Node ID>            
                             |
       | FetchOPCProcessor | Node ID type                | <Node ID Type>       
                             |
       | FetchOPCProcessor | Namespace index             | 1                    
                             |
-      | FetchOPCProcessor | OPC server endpoint         | 
opc.tcp://opcua-server-${feature_id}:4840/        |
+      | FetchOPCProcessor | OPC server endpoint         | 
opc.tcp://opcua-server-${scenario_id}:4840/       |
       | FetchOPCProcessor | Max depth                   | 1                    
                             |
 
-    And the "success" relationship of the GetFile processor is connected to 
the PutOPCProcessor
-    And the "success" relationship of the FetchOPCProcessor processor is 
connected to the PutFile
+    And in the "update-opc-ua-node" flow the "success" relationship of the 
GetFile processor is connected to the PutOPCProcessor
+    And in the "fetch-opc-ua-node" flow the "success" relationship of the 
FetchOPCProcessor processor is connected to the PutFile
 
     And an OPC UA server is set up
 
     When all instances start up
-    Then at least one flowfile with the content "<Value>" is placed in the 
monitored directory in less than 60 seconds
+    Then in the "fetch-opc-ua-node" container at least one file with the 
content "<Value>" is placed in the "/tmp/output" directory in less than 60 
seconds
 
   # Node ids starting from 51000 are pre-defined demo node ids in the test 
server application (server_ctt) of the open62541 docker image. There is one 
nodeid defined
   # for each type supported by OPC UA. These demo nodes can be used for 
testing purposes. "the.answer" is also a pre-defined string id for the same 
testing purposes.
@@ -101,11 +108,18 @@ Feature: Putting and fetching data to OPC UA server
 
   Scenario: Create and fetch data from an OPC UA node through secure connection
     Given a GetFile processor with the "Input Directory" property set to 
"/tmp/input" in the "create-opc-ua-node" flow
-    And a file with the content "Test" is present in "/tmp/input"
+    And a directory at "/tmp/input" has a file with the content "Test" in the 
"create-opc-ua-node" flow
     And a PutOPCProcessor processor in the "create-opc-ua-node" flow
+    And PutOPCProcessor is EVENT_DRIVEN in the "create-opc-ua-node" flow
     And a FetchOPCProcessor processor in the "fetch-opc-ua-node" flow
     And a PutFile processor with the "Directory" property set to "/tmp/output" 
in the "fetch-opc-ua-node" flow
-    And these processor properties are set:
+    And PutFile's success relationship is auto-terminated in the 
"fetch-opc-ua-node" flow
+    And PutFile is EVENT_DRIVEN in the "fetch-opc-ua-node" flow
+    And a host resource file "opcua_client_cert.der" is bound to the 
"/tmp/resources/opcua/opcua_client_cert.der" path in the MiNiFi container 
"create-opc-ua-node"
+    And a host resource file "opcua_client_key.der" is bound to the 
"/tmp/resources/opcua/opcua_client_key.der" path in the MiNiFi container 
"create-opc-ua-node"
+    And a host resource file "opcua_client_cert.der" is bound to the 
"/tmp/resources/opcua/opcua_client_cert.der" path in the MiNiFi container 
"fetch-opc-ua-node"
+    And a host resource file "opcua_client_key.der" is bound to the 
"/tmp/resources/opcua/opcua_client_key.der" path in the MiNiFi container 
"fetch-opc-ua-node"
+    And these processor properties are set in the "create-opc-ua-node" flow
       | processor name    | property name                   | property value   
                                 |
       | PutOPCProcessor   | Parent node ID                  | 85               
                                 |
       | PutOPCProcessor   | Parent node ID type             | Int              
                                 |
@@ -113,38 +127,44 @@ Feature: Putting and fetching data to OPC UA server
       | PutOPCProcessor   | Target node ID type             | Int              
                                 |
       | PutOPCProcessor   | Target node namespace index     | 1                
                                 |
       | PutOPCProcessor   | Value type                      | String           
                                 |
-      | PutOPCProcessor   | OPC server endpoint             | 
opc.tcp://opcua-server-${feature_id}:4840/        |
+      | PutOPCProcessor   | OPC server endpoint             | 
opc.tcp://opcua-server-${scenario_id}:4840/       |
       | PutOPCProcessor   | Target node browse name         | testnodename     
                                 |
       | PutOPCProcessor   | Certificate path                | 
/tmp/resources/opcua/opcua_client_cert.der        |
       | PutOPCProcessor   | Key path                        | 
/tmp/resources/opcua/opcua_client_key.der         |
       | PutOPCProcessor   | Trusted server certificate path | 
/tmp/resources/opcua/opcua_client_cert.der        |
       | PutOPCProcessor   | Application URI                 | 
urn:open62541.server.application                  |
+    And these processor properties are set in the "fetch-opc-ua-node" flow
+      | processor name    | property name                   | property value   
                                 |
       | FetchOPCProcessor | Node ID                         | 9999             
                                 |
       | FetchOPCProcessor | Node ID type                    | Int              
                                 |
       | FetchOPCProcessor | Namespace index                 | 1                
                                 |
-      | FetchOPCProcessor | OPC server endpoint             | 
opc.tcp://opcua-server-${feature_id}:4840/        |
+      | FetchOPCProcessor | OPC server endpoint             | 
opc.tcp://opcua-server-${scenario_id}:4840/       |
       | FetchOPCProcessor | Max depth                       | 1                
                                 |
       | FetchOPCProcessor | Certificate path                | 
/tmp/resources/opcua/opcua_client_cert.der        |
       | FetchOPCProcessor | Key path                        | 
/tmp/resources/opcua/opcua_client_key.der         |
       | FetchOPCProcessor | Trusted server certificate path | 
/tmp/resources/opcua/opcua_client_cert.der        |
       | FetchOPCProcessor | Application URI                 | 
urn:open62541.server.application                  |
 
-    And the "success" relationship of the GetFile processor is connected to 
the PutOPCProcessor
-    And the "success" relationship of the FetchOPCProcessor processor is 
connected to the PutFile
+    And in the "create-opc-ua-node" flow the "success" relationship of the 
GetFile processor is connected to the PutOPCProcessor
+    And in the "fetch-opc-ua-node" flow the "success" relationship of the 
FetchOPCProcessor processor is connected to the PutFile
 
     And an OPC UA server is set up
 
     When all instances start up
-    Then at least one flowfile with the content "Test" is placed in the 
monitored directory in less than 60 seconds
+
+    Then in the "fetch-opc-ua-node" container at least one file with the 
content "Test" is placed in the "/tmp/output" directory in less than 60 seconds
     And the OPC UA server logs contain the following message: "SecureChannel 
opened with SecurityPolicy 
http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep"; in less than 
5 seconds
 
   Scenario: Create and fetch data from an OPC UA node through username and 
password authenticated connection
     Given a GetFile processor with the "Input Directory" property set to 
"/tmp/input" in the "create-opc-ua-node" flow
-    And a file with the content "Test" is present in "/tmp/input"
+    And a directory at "/tmp/input" has a file with the content "Test" in the 
"create-opc-ua-node" flow
     And a PutOPCProcessor processor in the "create-opc-ua-node" flow
+    And PutOPCProcessor is EVENT_DRIVEN in the "create-opc-ua-node" flow
     And a FetchOPCProcessor processor in the "fetch-opc-ua-node" flow
     And a PutFile processor with the "Directory" property set to "/tmp/output" 
in the "fetch-opc-ua-node" flow
-    And these processor properties are set:
+    And PutFile's success relationship is auto-terminated in the 
"fetch-opc-ua-node" flow
+    And PutFile is EVENT_DRIVEN in the "fetch-opc-ua-node" flow
+    And these processor properties are set in the "create-opc-ua-node" flow
       | processor name    | property name               | property value       
                             |
       | PutOPCProcessor   | Parent node ID              | 85                   
                             |
       | PutOPCProcessor   | Parent node ID type         | Int                  
                             |
@@ -152,22 +172,24 @@ Feature: Putting and fetching data to OPC UA server
       | PutOPCProcessor   | Target node ID type         | Int                  
                             |
       | PutOPCProcessor   | Target node namespace index | 1                    
                             |
       | PutOPCProcessor   | Value type                  | String               
                             |
-      | PutOPCProcessor   | OPC server endpoint         | 
opc.tcp://opcua-server-${feature_id}:4840/        |
+      | PutOPCProcessor   | OPC server endpoint         | 
opc.tcp://opcua-server-${scenario_id}:4840/       |
       | PutOPCProcessor   | Target node browse name     | testnodename         
                             |
       | PutOPCProcessor   | Username                    | peter                
                             |
       | PutOPCProcessor   | Password                    | peter123             
                             |
+    And these processor properties are set in the "fetch-opc-ua-node" flow
+      | processor name    | property name               | property value       
                             |
       | FetchOPCProcessor | Node ID                     | 9999                 
                             |
       | FetchOPCProcessor | Node ID type                | Int                  
                             |
       | FetchOPCProcessor | Namespace index             | 1                    
                             |
-      | FetchOPCProcessor | OPC server endpoint         | 
opc.tcp://opcua-server-${feature_id}:4840/        |
+      | FetchOPCProcessor | OPC server endpoint         | 
opc.tcp://opcua-server-${scenario_id}:4840/       |
       | FetchOPCProcessor | Max depth                   | 1                    
                             |
       | FetchOPCProcessor | Username                    | peter                
                             |
       | FetchOPCProcessor | Password                    | peter123             
                             |
 
-    And the "success" relationship of the GetFile processor is connected to 
the PutOPCProcessor
-    And the "success" relationship of the FetchOPCProcessor processor is 
connected to the PutFile
+    And in the "create-opc-ua-node" flow the "success" relationship of the 
GetFile processor is connected to the PutOPCProcessor
+    And in the "fetch-opc-ua-node" flow the "success" relationship of the 
FetchOPCProcessor processor is connected to the PutFile
 
     And an OPC UA server is set up with access control
 
     When all instances start up
-    Then at least one flowfile with the content "Test" is placed in the 
monitored directory in less than 60 seconds
+    Then in the "fetch-opc-ua-node" container at least one file with the 
content "Test" is placed in the "/tmp/output" directory in less than 60 seconds
diff --git a/docker/test/integration/resources/opcua/opcua_client_cert.der 
b/extensions/opc/tests/features/resources/opcua_client_cert.der
similarity index 100%
rename from docker/test/integration/resources/opcua/opcua_client_cert.der
rename to extensions/opc/tests/features/resources/opcua_client_cert.der
diff --git a/docker/test/integration/resources/opcua/opcua_client_key.der 
b/extensions/opc/tests/features/resources/opcua_client_key.der
similarity index 100%
rename from docker/test/integration/resources/opcua/opcua_client_key.der
rename to extensions/opc/tests/features/resources/opcua_client_key.der
diff --git a/docker/test/integration/cluster/containers/OPCUAServerContainer.py 
b/extensions/opc/tests/features/steps/opc_ua_server_container.py
similarity index 52%
rename from docker/test/integration/cluster/containers/OPCUAServerContainer.py
rename to extensions/opc/tests/features/steps/opc_ua_server_container.py
index fefb8759c..4973b9046 100644
--- a/docker/test/integration/cluster/containers/OPCUAServerContainer.py
+++ b/extensions/opc/tests/features/steps/opc_ua_server_container.py
@@ -13,27 +13,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
-import logging
-from .Container import Container
+from typing import List, Optional
+from minifi_test_framework.containers.container import Container
+from minifi_test_framework.core.helpers import wait_for_condition
+from minifi_test_framework.core.minifi_test_context import MinifiTestContext
 
 
 class OPCUAServerContainer(Container):
-    def __init__(self, feature_context, name, vols, network, image_store, 
command=None):
-        super().__init__(feature_context, name, 'opcua-server', vols, network, 
image_store, command)
-
-    def get_startup_finished_log_entry(self):
-        return "New DiscoveryUrl added: opc.tcp://"
+    def __init__(self, test_context: MinifiTestContext, command: 
Optional[List[str]] = None):
+        super().__init__("lordgamez/open62541:1.4.10", 
f"opcua-server-{test_context.scenario_id}", test_context.network, 
command=command)
 
     def deploy(self):
-        if not self.set_deployed():
-            return
-
-        logging.info('Creating and running OPC UA server docker container...')
-        self.client.containers.run(
-            "lordgamez/open62541:1.4.10",
-            detach=True,
-            name=self.name,
-            network=self.network.name,
-            entrypoint=self.command)
-        logging.info('Added container \'%s\'', self.name)
+        super().deploy()
+        finished_str = "New DiscoveryUrl added: opc.tcp://"
+        return wait_for_condition(
+            condition=lambda: finished_str in self.get_logs(),
+            timeout_seconds=15,
+            bail_condition=lambda: self.exited,
+            context=None)
diff --git a/extensions/opc/tests/features/steps/steps.py 
b/extensions/opc/tests/features/steps/steps.py
new file mode 100644
index 000000000..f9db97c8a
--- /dev/null
+++ b/extensions/opc/tests/features/steps/steps.py
@@ -0,0 +1,47 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import humanfriendly
+from behave import step, then
+
+from minifi_test_framework.steps import checking_steps        # noqa: F401
+from minifi_test_framework.steps import configuration_steps   # noqa: F401
+from minifi_test_framework.steps import core_steps            # noqa: F401
+from minifi_test_framework.steps import flow_building_steps   # noqa: F401
+from minifi_test_framework.core.minifi_test_context import MinifiTestContext
+from minifi_test_framework.core.helpers import wait_for_condition
+from opc_ua_server_container import OPCUAServerContainer
+
+
+@step("an OPC UA server is set up")
+def step_impl(context: MinifiTestContext):
+    context.containers["opcua-server"] = OPCUAServerContainer(context)
+
+
+@step("an OPC UA server is set up with access control")
+def step_impl(context: MinifiTestContext):
+    context.containers["opcua-server-access"] = OPCUAServerContainer(context, 
command=["/opt/open62541/examples/access_control_server"])
+
+
+@then("the OPC UA server logs contain the following message: \"{log_message}\" 
in less than {duration}")
+def step_impl(context, log_message, duration):
+    timeout_seconds = humanfriendly.parse_timespan(duration)
+    opcua_container = context.containers["opcua-server"]
+    assert isinstance(opcua_container, OPCUAServerContainer)
+    assert wait_for_condition(
+        condition=lambda: log_message in opcua_container.get_logs(),
+        timeout_seconds=timeout_seconds,
+        bail_condition=lambda: opcua_container.exited,
+        context=context)
diff --git a/extensions/sql/tests/features/steps/steps.py 
b/extensions/sql/tests/features/steps/steps.py
index 60a2059c3..db2f555e4 100644
--- a/extensions/sql/tests/features/steps/steps.py
+++ b/extensions/sql/tests/features/steps/steps.py
@@ -33,20 +33,20 @@ def step_impl(context: MinifiTestContext, processor_name: 
str, service_name: str
     odb_service = ControllerService(class_name="ODBCService", 
service_name=service_name)
     postgres_server_hostname = f"postgres-server-{context.scenario_id}"
     odb_service.add_property("Connection String", f"Driver={{PostgreSQL 
ANSI}};Server={postgres_server_hostname};Port=5432;Database=postgres;Uid=postgres;Pwd=password;")
-    
context.minifi_container.flow_definition.controller_services.append(odb_service)
-    processor = 
context.minifi_container.flow_definition.get_processor(processor_name)
+    
context.get_or_create_default_minifi_container().flow_definition.controller_services.append(odb_service)
+    processor = 
context.get_or_create_default_minifi_container().flow_definition.get_processor(processor_name)
     processor.add_property("DB Controller Service", "ODBCService")
 
 
 @step("a PostgreSQL server is set up")
 def step_impl(context):
-    context.containers.append(PostgresContainer(context))
+    context.containers["postgres-server"] = PostgresContainer(context)
 
 
-@then('the query "{query}" returns {rows} rows in less than {timeout_str} on 
the PostgreSQL server')
-def step_impl(context, query: str, rows: str, timeout_str: str):
+@then('the query "{query}" returns {rows:d} rows in less than {timeout_str} on 
the PostgreSQL server')
+def step_impl(context, query: str, rows: int, timeout_str: str):
     timeout_seconds = humanfriendly.parse_timespan(timeout_str)
-    postgres_container = context.containers[0]
+    postgres_container = context.containers["postgres-server"]
     assert isinstance(postgres_container, PostgresContainer)
     assert wait_for_condition(
         condition=lambda: postgres_container.check_query_results(query, 
int(rows)),
diff --git a/extensions/standard-processors/tests/features/steps/steps.py 
b/extensions/standard-processors/tests/features/steps/steps.py
index df76b292b..a53631e2e 100644
--- a/extensions/standard-processors/tests/features/steps/steps.py
+++ b/extensions/standard-processors/tests/features/steps/steps.py
@@ -27,9 +27,9 @@ from syslog_container import SyslogContainer
 
 @step("a Syslog client with TCP protocol is setup to send logs to minifi")
 def step_impl(context: MinifiTestContext):
-    context.containers.append(SyslogContainer("tcp", context))
+    context.containers["syslog-tcp"] = SyslogContainer("tcp", context)
 
 
 @step("a Syslog client with UDP protocol is setup to send logs to minifi")
 def step_impl(context):
-    context.containers.append(SyslogContainer("udp", context))
+    context.containers["syslog-udp"] = SyslogContainer("udp", context)
diff --git 
a/extensions/standard-processors/tests/features/steps/syslog_container.py 
b/extensions/standard-processors/tests/features/steps/syslog_container.py
index 15a3c9551..f2da956f5 100644
--- a/extensions/standard-processors/tests/features/steps/syslog_container.py
+++ b/extensions/standard-processors/tests/features/steps/syslog_container.py
@@ -21,4 +21,4 @@ from minifi_test_framework.containers.container import 
Container
 class SyslogContainer(Container):
     def __init__(self, protocol, context):
         super(SyslogContainer, self).__init__("ubuntu:24.04", 
f"syslog-{protocol}-{context.scenario_id}", context.network)
-        self.command = f'/bin/bash -c "echo Syslog {protocol} client started; 
while true; do logger --{protocol} -n minifi-{context.scenario_id} -P 514 
sample_log; sleep 1; done"'
+        self.command = f'/bin/bash -c "echo Syslog {protocol} client started; 
while true; do logger --{protocol} -n minifi-primary-{context.scenario_id} -P 
514 sample_log; sleep 1; done"'

Reply via email to