Repository: eclipse-bluechi/bluechi (language: python)

Issue:
Add e2e test scenario for proxy
## e2e testing for proxy scenario

Service A on node bar, dependent on service B on node foo

Send with hirtectl command to start service A on node bar
Service B on node foo, is down, once service B is up,
Service A status is up and running
Add e2e test scenario for proxy
## e2e testing for proxy scenario

Service A on node bar, dependent on service B on node foo

Send with hirtectl command to start service A on node bar
Service B on node foo, is down, once service B is up,
Service A status is up and running


Discussion / hints:
The following description shows a few test cases that should be covered: 

## Basic setup

agent foo --> manager <-- agent bar

## Basic Units

```INI
# complex-sleep.service
[Unit]
Wants=hirte-proxy@bar_simple-sleep.service
After=hirte-proxy@bar_simple-sleep.service

[Service]
ExecStart=/usr/bin/sleep infinity
```

```INI
# simple-sleep.service
[Unit]
Description=Just sleeping

[Service]
Type=simple
ExecStart=/usr/bin/sleep infinity
```

## Test Cases

The state of units should be verified using `hirtectl list-units` to query (and filter) units and check their status. 

### (A) Starting a proxy service :heavy_check_mark:  (https://github.com/containers/hirte/pull/290)

_Steps:_

1. Using `hirtectl`, start `complex-sleep.service` on `agent foo`
2. Verify state

_Expected Result:_

| agent | service | state |
|---|---|---|
| foo | compolex-sleep.service | active |
| foo | hirte-proxy@bar_compolex-sleep.service | active |
| bar | simple-sleep.service | active |
| bar | hirte-dep@simple-sleep.service | active |

    
### (B) Stopping a proxy service on the source node :heavy_check_mark: (https://github.com/containers/hirte/pull/297)

_Prerequisites:_ 

- Steps from (A)

_Steps:_

1. Using `hirtectl`, stop `complex-sleep.service` on `agent foo`
2. Verify state 

_Expected Result:_

| agent | service | state |
|---|---|---|
| foo | compolex-sleep.service | inactive |
| foo | hirte-proxy@bar_compolex-sleep.service | inactive |
| bar | simple-sleep.service | inactive |
| bar | hirte-dep@simple-sleep.service | inactive |


### (C) Stopping the actual (dependency) service on the target node

_Prerequisites:_ 

- Steps from (A)

_Steps:_

1. Using `hirtectl`, stop `simple-sleep.service` on `agent bar`
2. Verify state 

_Expected Result:_

| agent | service | state |
|---|---|---|
| foo | compolex-sleep.service | inactive |
| foo | hirte-proxy@bar_compolex-sleep.service | inactive |
| bar | simple-sleep.service | inactive |
| bar | hirte-dep@simple-sleep.service | inactive |


### (D) Stopping the proxy (dependency) service on the target node

_Prerequisites:_ 

- Steps from (A)

_Steps:_

1. Using `hirtectl`, stop `hirte-dep@simple-sleep.service` on `agent bar`
2. Verify state 

_Expected Result:_

| agent | service | state |
|---|---|---|
| foo | compolex-sleep.service | inactive |
| foo | hirte-proxy@bar_compolex-sleep.service | inactive |
| bar | simple-sleep.service | active |
| bar | hirte-dep@simple-sleep.service | inactive |

### (E) Stopping the proxy (dependency) service on the target node with StopWhenUnneeded=yes

_Prerequisites:_ 

- Modify `simple-sleep.service`:
    - add `StopWhenUnneeded=yes` to the `[Unit]` section
    - systemctl daemon-reload
- Steps from (A)

_Steps:_

1. Using `hirtectl`, stop `hirte-dep@simple-sleep.service` on `agent bar`
2. Verify state 

_Expected Result:_

| agent | service | state |
|---|---|---|
| foo | compolex-sleep.service | inactive |
| foo | hirte-proxy@bar_compolex-sleep.service | inactive |
| bar | simple-sleep.service | inactive |
| bar | hirte-dep@simple-sleep.service | inactive |

This is the basics, you want to also test other edge cases:

Depend on a service that doesn't exist should fail.
Depend on a service with a typo in the .service 
Depend on a service where the ExecStart=command fails.
Have two services on a node depend on a proxy, and ensure lifetimes still work
Have two services on two nodes depend on the same target service, ensure lifetimes still work.



By "typo in the .service" i mean that the target service will have a typo inside the file, which will cause a different kind of failure codepath when loading it.

@engelmi Do you plan to work on it? 
The following description shows a few test cases that should be covered: 

## Basic setup

agent foo --> manager <-- agent bar

## Basic Units

```INI
# complex-sleep.service
[Unit]
Wants=hirte-proxy@bar_simple-sleep.service
After=hirte-proxy@bar_simple-sleep.service

[Service]
ExecStart=/usr/bin/sleep infinity
```

```INI
# simple-sleep.service
[Unit]
Description=Just sleeping

[Service]
Type=simple
ExecStart=/usr/bin/sleep infinity
```

## Test Cases

The state of units should be verified using `hirtectl list-units` to query (and filter) units and check their status. 

### (A) Starting a proxy service :heavy_check_mark:  (https://github.com/containers/hirte/pull/290)

_Steps:_

1. Using `hirtectl`, start `complex-sleep.service` on `agent foo`
2. Verify state

_Expected Result:_

| agent | service | state |
|---|---|---|
| foo | compolex-sleep.service | active |
| foo | hirte-proxy@bar_compolex-sleep.service | active |
| bar | simple-sleep.service | active |
| bar | hirte-dep@simple-sleep.service | active |

    
### (B) Stopping a proxy service on the source node :heavy_check_mark: (https://github.com/containers/hirte/pull/297)

_Prerequisites:_ 

- Steps from (A)

_Steps:_

1. Using `hirtectl`, stop `complex-sleep.service` on `agent foo`
2. Verify state 

_Expected Result:_

| agent | service | state |
|---|---|---|
| foo | compolex-sleep.service | inactive |
| foo | hirte-proxy@bar_compolex-sleep.service | inactive |
| bar | simple-sleep.service | inactive |
| bar | hirte-dep@simple-sleep.service | inactive |


### (C) Stopping the actual (dependency) service on the target node

_Prerequisites:_ 

- Steps from (A)

_Steps:_

1. Using `hirtectl`, stop `simple-sleep.service` on `agent bar`
2. Verify state 

_Expected Result:_

| agent | service | state |
|---|---|---|
| foo | compolex-sleep.service | inactive |
| foo | hirte-proxy@bar_compolex-sleep.service | inactive |
| bar | simple-sleep.service | inactive |
| bar | hirte-dep@simple-sleep.service | inactive |


### (D) Stopping the proxy (dependency) service on the target node

_Prerequisites:_ 

- Steps from (A)

_Steps:_

1. Using `hirtectl`, stop `hirte-dep@simple-sleep.service` on `agent bar`
2. Verify state 

_Expected Result:_

| agent | service | state |
|---|---|---|
| foo | compolex-sleep.service | inactive |
| foo | hirte-proxy@bar_compolex-sleep.service | inactive |
| bar | simple-sleep.service | active |
| bar | hirte-dep@simple-sleep.service | inactive |

### (E) Stopping the proxy (dependency) service on the target node with StopWhenUnneeded=yes

_Prerequisites:_ 

- Modify `simple-sleep.service`:
    - add `StopWhenUnneeded=yes` to the `[Unit]` section
    - systemctl daemon-reload
- Steps from (A)

_Steps:_

1. Using `hirtectl`, stop `hirte-dep@simple-sleep.service` on `agent bar`
2. Verify state 

_Expected Result:_

| agent | service | state |
|---|---|---|
| foo | compolex-sleep.service | inactive |
| foo | hirte-proxy@bar_compolex-sleep.service | inactive |
| bar | simple-sleep.service | inactive |
| bar | hirte-dep@simple-sleep.service | inactive |

This is the basics, you want to also test other edge cases:

Depend on a service that doesn't exist should fail.
Depend on a service with a typo in the .service 
Depend on a service where the ExecStart=command fails.
Have two services on a node depend on a proxy, and ensure lifetimes still work
Have two services on two nodes depend on the same target service, ensure lifetimes still work.



By "typo in the .service" i mean that the target service will have a typo inside the file, which will cause a different kind of failure codepath when loading it.

@engelmi Do you plan to work on it? 

Fix patch:
diff --git a/Makefile b/Makefile
index dbcce9be64..753e1bc4b6 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ DESTDIR ?=
 BUILDDIR=builddir
 
 CODESPELL_PARAMS=\
-	-S Makefile,imgtype,copy,AUTHORS,bin,.git,CHANGELOG.md,changelog.txt,.cirrus.yml,"*.xz,*.gz,*.tar,*.tgz,*ico,*.png,*.1,*.5,*.orig,*.rej,*.xml,*xsl",build.ninja,intro-targets.json \
+	-S Makefile,imgtype,copy,AUTHORS,bin,.git,CHANGELOG.md,changelog.txt,.cirrus.yml,"*.xz,*.gz,*.tar,*.tgz,*ico,*.png,*.1,*.5,*.orig,*.rej,*.xml,*xsl",build.ninja,intro-targets.json,./tests/tests/tier0/proxy-service-fails-on-typo-in-file/systemd/simple.service \
 	-L keypair,flate,uint,iff,od,ERRO
 
 build:


Test patch:
diff --git a/tests/hirte_test/container.py b/tests/hirte_test/container.py
index 14a8dace7d..ab8b4e6095 100644
--- a/tests/hirte_test/container.py
+++ b/tests/hirte_test/container.py
@@ -2,6 +2,7 @@
 
 import io
 import os
+import time
 import tarfile
 
 from podman.domain.containers import Container
@@ -84,32 +85,68 @@ def copy_systemd_service(self, service_file_name: str, source_dir: str, target_d
         self.create_file(target_dir, service_file_name, content)
         self.systemctl_daemon_reload()
 
+    def service_is_active(self, unit_name: str) -> bool:
+        result, _ = self.exec_run(f"systemctl is-active {unit_name}")
+        return result == 0
 
-class HirteControllerContainer(HirteContainer):
+    def get_unit_state(self, unit_name: str) -> str:
+        _, output = self.exec_run(f"systemctl is-active {unit_name}")
+        return output
 
-    def __init__(self, container: Container, config: HirteControllerConfig) -> None:
-        super().__init__(container, config)
+    def wait_for_unit_state_to_be(self, unit_name: str, expected_state: str, timeout: float = 2.0) -> bool:
+        latest_state = ""
 
-    def wait_for_hirte(self):
-        result = 1
-        while result != 0:
-            result, _ = self.exec_run("systemctl is-active hirte")
+        start = time.time()
+        while (time.time() - start) < timeout:
+            latest_state = self.get_unit_state(unit_name)
+            if latest_state == expected_state:
+                return True
 
-    def copy_systemd_service(self, service_file_name: str, source_dir: str, target_dir):
-        super().copy_systemd_service(service_file_name, source_dir, target_dir)
-        self.wait_for_hirte()
+        print(f"Timeout while waiting for {unit_name} to reach state {expected_state}. \
+              Latest state: {latest_state}")
+        return False
 
 
 class HirteNodeContainer(HirteContainer):
 
     def __init__(self, container: Container, config: HirteNodeConfig) -> None:
         super().__init__(container, config)
+        self.node_name = config.node_name
 
     def wait_for_hirte_agent(self):
-        result = 1
-        while result != 0:
-            result, _ = self.exec_run("systemctl is-active hirte-agent")
+        should_wait = True
+        while should_wait:
+            should_wait = not self.service_is_active("hirte-agent")
 
     def copy_systemd_service(self, service_file_name: str, source_dir: str, target_dir):
         super().copy_systemd_service(service_file_name, source_dir, target_dir)
         self.wait_for_hirte_agent()
+
+
+class HirteControllerContainer(HirteContainer):
+
+    def __init__(self, container: Container, config: HirteControllerConfig) -> None:
+        super().__init__(container, config)
+
+    def wait_for_hirte(self):
+        should_wait = True
+        while should_wait:
+            should_wait = not self.service_is_active("hirte")
+
+    def copy_systemd_service(self, service_file_name: str, source_dir: str, target_dir):
+        super().copy_systemd_service(service_file_name, source_dir, target_dir)
+        self.wait_for_hirte()
+
+    def start_unit(self, node_name: str, unit_name: str) -> None:
+        print(f"Starting unit {unit_name} on node {node_name}")
+
+        result, output = self.exec_run(f"hirtectl start {node_name} {unit_name}")
+        if result != 0:
+            raise Exception(f"Failed to start service {unit_name} on node {node_name}: {output}")
+
+    def stop_unit(self, node_name: str, unit_name: str) -> None:
+        print(f"Stopping unit {unit_name} on node {node_name}")
+
+        result, output = self.exec_run(f"hirtectl stop {node_name} {unit_name}")
+        if result != 0:
+            raise Exception(f"Failed to start service {unit_name} on node {node_name}: {output}")
diff --git a/tests/hirte_test/test.py b/tests/hirte_test/test.py
index 75c226c3bf..2cb9e4ed4a 100644
--- a/tests/hirte_test/test.py
+++ b/tests/hirte_test/test.py
@@ -53,6 +53,7 @@ def setup(self) -> Tuple[bool, Tuple[HirteControllerContainer, Dict[str, HirteNo
                 ports={self.hirte_ctrl_svc_port: self.hirte_ctrl_host_port},
                 networks={self.hirte_network_name: {}},
             )
+            c.wait(condition="running")
 
             ctrl_container = HirteControllerContainer(c, self.hirte_controller_config)
             ctrl_container.exec_run('systemctl start hirte')
@@ -66,10 +67,13 @@ def setup(self) -> Tuple[bool, Tuple[HirteControllerContainer, Dict[str, HirteNo
                     detach=True,
                     networks={self.hirte_network_name: {}},
                 )
+                c.wait(condition="running")
+
                 node = HirteNodeContainer(c, cfg)
+                node_container[cfg.node_name] = node
+
                 node.exec_run('systemctl start hirte-agent')
 
-                node_container[cfg.node_name] = node
         except Exception as ex:
             success = False
             print(f"Failed to setup hirte container: {ex}")
diff --git a/tests/hirte_test/util.py b/tests/hirte_test/util.py
index 08a06eafe6..91b5936fd1 100644
--- a/tests/hirte_test/util.py
+++ b/tests/hirte_test/util.py
@@ -23,3 +23,11 @@ def get_primary_ip() -> str:
     finally:
         s.close()
     return ip
+
+
+def assemble_hirte_dep_service_name(unit_name: str) -> str:
+    return f"hirte-dep@{unit_name}"
+
+
+def assemble_hirte_proxy_service_name(node_name: str, unit_name: str) -> str:
+    return f"hirte-proxy@{node_name}_{unit_name}"
diff --git a/tests/tests/tier0/proxy-service-fails-on-execstart/main.fmf b/tests/tests/tier0/proxy-service-fails-on-execstart/main.fmf
new file mode 100644
index 0000000000..a3f6a290f4
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-fails-on-execstart/main.fmf
@@ -0,0 +1,6 @@
+summary: Test if proxy service fails on failure of ExecStart of dependency service
+
+tier: 0
+
+test: |
+  pytest -svv
diff --git a/tests/tests/tier0/proxy-service-fails-on-execstart/systemd/requesting.service b/tests/tests/tier0/proxy-service-fails-on-execstart/systemd/requesting.service
new file mode 100644
index 0000000000..58e4ff769d
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-fails-on-execstart/systemd/requesting.service
@@ -0,0 +1,7 @@
+[Unit]
+Wants=hirte-proxy@node-bar_simple.service
+After=hirte-proxy@node-bar_simple.service
+
+[Service]
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-fails-on-execstart/systemd/simple.service b/tests/tests/tier0/proxy-service-fails-on-execstart/systemd/simple.service
new file mode 100644
index 0000000000..a1ab4a3450
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-fails-on-execstart/systemd/simple.service
@@ -0,0 +1,7 @@
+[Unit]
+Description=Attempting to just sleep, but fail
+
+[Service]
+Type=simple
+ExecStart=exit 1
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-fails-on-execstart/test_proxy_service_fails_on_execstart.py b/tests/tests/tier0/proxy-service-fails-on-execstart/test_proxy_service_fails_on_execstart.py
new file mode 100644
index 0000000000..6ecefe9c21
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-fails-on-execstart/test_proxy_service_fails_on_execstart.py
@@ -0,0 +1,62 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import pytest
+from typing import Dict
+
+from hirte_test.config import HirteControllerConfig, HirteNodeConfig
+from hirte_test.container import HirteControllerContainer, HirteNodeContainer
+from hirte_test.test import HirteTest
+from hirte_test.util import assemble_hirte_dep_service_name, assemble_hirte_proxy_service_name
+
+
+node_foo_name = "node-foo"
+node_bar_name = "node-bar"
+
+requesting_service = "requesting.service"
+simple_service = "simple.service"
+
+
+def verify_proxy_start_failed(foo: HirteNodeContainer, bar: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "active")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "inactive")
+
+    assert bar.wait_for_unit_state_to_be(simple_service, "failed")
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "inactive")
+
+
+def exec(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
+    foo = nodes[node_foo_name]
+    bar = nodes[node_bar_name]
+
+    source_dir = "systemd"
+    target_dir = os.path.join("/", "etc", "systemd", "system")
+
+    foo.copy_systemd_service(requesting_service, source_dir, target_dir)
+    bar.copy_systemd_service(simple_service, source_dir, target_dir)
+
+    ctrl.start_unit(node_foo_name, requesting_service)
+    verify_proxy_start_failed(foo, bar)
+
+
+@pytest.mark.timeout(10)
+def test_proxy_service_fails_on_execstart(
+        hirte_test: HirteTest,
+        hirte_ctrl_default_config: HirteControllerConfig,
+        hirte_node_default_config: HirteNodeConfig):
+
+    node_foo_cfg = hirte_node_default_config.deep_copy()
+    node_foo_cfg.node_name = node_foo_name
+
+    node_bar_cfg = hirte_node_default_config.deep_copy()
+    node_bar_cfg.node_name = node_bar_name
+
+    hirte_ctrl_default_config.allowed_node_names = [node_foo_name, node_bar_name]
+
+    hirte_test.set_hirte_controller_config(hirte_ctrl_default_config)
+    hirte_test.add_hirte_node_config(node_foo_cfg)
+    hirte_test.add_hirte_node_config(node_bar_cfg)
+
+    hirte_test.run(exec)
diff --git a/tests/tests/tier0/proxy-service-fails-on-non-existent-service/main.fmf b/tests/tests/tier0/proxy-service-fails-on-non-existent-service/main.fmf
new file mode 100644
index 0000000000..4c9c3f4cbf
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-fails-on-non-existent-service/main.fmf
@@ -0,0 +1,6 @@
+summary: Test if proxy service fails on requesting non-existent dependency service
+
+tier: 0
+
+test: |
+  pytest -svv
diff --git a/tests/tests/tier0/proxy-service-fails-on-non-existent-service/systemd/requesting.service b/tests/tests/tier0/proxy-service-fails-on-non-existent-service/systemd/requesting.service
new file mode 100644
index 0000000000..58e4ff769d
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-fails-on-non-existent-service/systemd/requesting.service
@@ -0,0 +1,7 @@
+[Unit]
+Wants=hirte-proxy@node-bar_simple.service
+After=hirte-proxy@node-bar_simple.service
+
+[Service]
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-fails-on-non-existent-service/test_proxy_service_fails_on_non_existent_service.py b/tests/tests/tier0/proxy-service-fails-on-non-existent-service/test_proxy_service_fails_on_non_existent_service.py
new file mode 100644
index 0000000000..b4b5553e9c
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-fails-on-non-existent-service/test_proxy_service_fails_on_non_existent_service.py
@@ -0,0 +1,56 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import pytest
+from typing import Dict
+
+from hirte_test.config import HirteControllerConfig, HirteNodeConfig
+from hirte_test.container import HirteControllerContainer, HirteNodeContainer
+from hirte_test.test import HirteTest
+from hirte_test.util import assemble_hirte_proxy_service_name
+
+
+node_foo_name = "node-foo"
+node_bar_name = "node-bar"
+
+requesting_service = "requesting.service"
+simple_service = "simple.service"
+
+
+def verify_proxy_start_failed(foo: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "active")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "failed")
+
+
+def exec(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
+    foo = nodes[node_foo_name]
+
+    source_dir = os.path.join(".", "systemd")
+    target_dir = os.path.join("/", "etc", "systemd", "system")
+
+    foo.copy_systemd_service(requesting_service, source_dir, target_dir)
+
+    ctrl.start_unit(node_foo_name, requesting_service)
+    verify_proxy_start_failed(foo)
+
+
+@pytest.mark.timeout(10)
+def test_proxy_service_fails_on_non_existent_service(
+        hirte_test: HirteTest,
+        hirte_ctrl_default_config: HirteControllerConfig,
+        hirte_node_default_config: HirteNodeConfig):
+
+    node_foo_cfg = hirte_node_default_config.deep_copy()
+    node_foo_cfg.node_name = node_foo_name
+
+    node_bar_cfg = hirte_node_default_config.deep_copy()
+    node_bar_cfg.node_name = node_bar_name
+
+    hirte_ctrl_default_config.allowed_node_names = [node_foo_name, node_bar_name]
+
+    hirte_test.set_hirte_controller_config(hirte_ctrl_default_config)
+    hirte_test.add_hirte_node_config(node_foo_cfg)
+    hirte_test.add_hirte_node_config(node_bar_cfg)
+
+    hirte_test.run(exec)
diff --git a/tests/tests/tier0/proxy-service-fails-on-typo-in-file/main.fmf b/tests/tests/tier0/proxy-service-fails-on-typo-in-file/main.fmf
new file mode 100644
index 0000000000..f01bcdf345
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-fails-on-typo-in-file/main.fmf
@@ -0,0 +1,6 @@
+summary: Test if proxy service fails if dependency service can't be loaded due to typo
+
+tier: 0
+
+test: |
+  pytest -svv
diff --git a/tests/tests/tier0/proxy-service-fails-on-typo-in-file/systemd/requesting.service b/tests/tests/tier0/proxy-service-fails-on-typo-in-file/systemd/requesting.service
new file mode 100644
index 0000000000..58e4ff769d
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-fails-on-typo-in-file/systemd/requesting.service
@@ -0,0 +1,7 @@
+[Unit]
+Wants=hirte-proxy@node-bar_simple.service
+After=hirte-proxy@node-bar_simple.service
+
+[Service]
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-fails-on-typo-in-file/systemd/simple.service b/tests/tests/tier0/proxy-service-fails-on-typo-in-file/systemd/simple.service
new file mode 100644
index 0000000000..9adc2dab54
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-fails-on-typo-in-file/systemd/simple.service
@@ -0,0 +1,7 @@
+[Unit]
+Description=Just sleeping
+
+[Sevice]
+Type=simple
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-fails-on-typo-in-file/test_proxy_service_fails_on_typo_in_file.py b/tests/tests/tier0/proxy-service-fails-on-typo-in-file/test_proxy_service_fails_on_typo_in_file.py
new file mode 100644
index 0000000000..1276259401
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-fails-on-typo-in-file/test_proxy_service_fails_on_typo_in_file.py
@@ -0,0 +1,62 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import pytest
+from typing import Dict
+
+from hirte_test.config import HirteControllerConfig, HirteNodeConfig
+from hirte_test.container import HirteControllerContainer, HirteNodeContainer
+from hirte_test.test import HirteTest
+from hirte_test.util import assemble_hirte_dep_service_name, assemble_hirte_proxy_service_name
+
+
+node_foo_name = "node-foo"
+node_bar_name = "node-bar"
+
+requesting_service = "requesting.service"
+simple_service = "simple.service"
+
+
+def verify_proxy_start_failed(foo: HirteNodeContainer, bar: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "active")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "failed")
+
+    assert bar.wait_for_unit_state_to_be(simple_service, "inactive")
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "inactive")
+
+
+def exec(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
+    foo = nodes[node_foo_name]
+    bar = nodes[node_bar_name]
+
+    source_dir = "systemd"
+    target_dir = os.path.join("/", "etc", "systemd", "system")
+
+    foo.copy_systemd_service(requesting_service, source_dir, target_dir)
+    bar.copy_systemd_service(simple_service, source_dir, target_dir)
+
+    ctrl.start_unit(node_foo_name, requesting_service)
+    verify_proxy_start_failed(foo, bar)
+
+
+@pytest.mark.timeout(10)
+def test_proxy_service_fails_on_typo_in_file(
+        hirte_test: HirteTest,
+        hirte_ctrl_default_config: HirteControllerConfig,
+        hirte_node_default_config: HirteNodeConfig):
+
+    node_foo_cfg = hirte_node_default_config.deep_copy()
+    node_foo_cfg.node_name = node_foo_name
+
+    node_bar_cfg = hirte_node_default_config.deep_copy()
+    node_bar_cfg.node_name = node_bar_name
+
+    hirte_ctrl_default_config.allowed_node_names = [node_foo_name, node_bar_name]
+
+    hirte_test.set_hirte_controller_config(hirte_ctrl_default_config)
+    hirte_test.add_hirte_node_config(node_foo_cfg)
+    hirte_test.add_hirte_node_config(node_bar_cfg)
+
+    hirte_test.run(exec)
diff --git a/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/main.fmf b/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/main.fmf
new file mode 100644
index 0000000000..027cb7e019
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/main.fmf
@@ -0,0 +1,6 @@
+summary: Test proper lifetimes of all services when multiple services on multiple nodes request the same service on the same node
+
+tier: 0
+
+test: |
+  pytest -svv
diff --git a/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/systemd/requesting.service b/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/systemd/requesting.service
new file mode 100644
index 0000000000..58e4ff769d
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/systemd/requesting.service
@@ -0,0 +1,7 @@
+[Unit]
+Wants=hirte-proxy@node-bar_simple.service
+After=hirte-proxy@node-bar_simple.service
+
+[Service]
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/systemd/simple.service b/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/systemd/simple.service
new file mode 100644
index 0000000000..9ca13ed786
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/systemd/simple.service
@@ -0,0 +1,8 @@
+[Unit]
+Description=Just sleeping
+StopWhenUnneeded=yes
+
+[Service]
+Type=simple
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/test_proxy_service_multiple_services_multiple_nodes.py b/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/test_proxy_service_multiple_services_multiple_nodes.py
new file mode 100644
index 0000000000..57d67d8b67
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-multiple-services-multiple-nodes/test_proxy_service_multiple_services_multiple_nodes.py
@@ -0,0 +1,83 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import pytest
+from typing import Dict
+
+from hirte_test.config import HirteControllerConfig, HirteNodeConfig
+from hirte_test.container import HirteControllerContainer, HirteNodeContainer
+from hirte_test.test import HirteTest
+from hirte_test.util import assemble_hirte_dep_service_name, assemble_hirte_proxy_service_name
+
+
+node_foo1_name = "node-foo-1"
+node_foo2_name = "node-foo-2"
+node_bar_name = "node-bar"
+
+requesting_service = "requesting.service"
+simple_service = "simple.service"
+
+
+def exec(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
+    foo1 = nodes[node_foo1_name]
+    foo2 = nodes[node_foo2_name]
+    bar = nodes[node_bar_name]
+
+    source_dir = "systemd"
+    target_dir = os.path.join("/", "etc", "systemd", "system")
+
+    foo1.copy_systemd_service(requesting_service, source_dir, target_dir)
+    foo2.copy_systemd_service(requesting_service, source_dir, target_dir)
+    bar.copy_systemd_service(simple_service, source_dir, target_dir)
+
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+
+    ctrl.start_unit(node_foo1_name, requesting_service)
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "active")
+    assert foo1.wait_for_unit_state_to_be(requesting_service, "active")
+    assert foo1.wait_for_unit_state_to_be(hirte_proxy_service, "active")
+
+    ctrl.start_unit(node_foo2_name, requesting_service)
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "active")
+    assert foo2.wait_for_unit_state_to_be(requesting_service, "active")
+    assert foo2.wait_for_unit_state_to_be(hirte_proxy_service, "active")
+
+    ctrl.stop_unit(node_foo1_name, requesting_service)
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "active")
+    assert foo1.wait_for_unit_state_to_be(requesting_service, "inactive")
+    assert foo1.wait_for_unit_state_to_be(hirte_proxy_service, "inactive")
+
+    ctrl.stop_unit(node_foo2_name, requesting_service)
+    assert bar.wait_for_unit_state_to_be(simple_service, "inactive")
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "inactive")
+    assert foo1.wait_for_unit_state_to_be(requesting_service, "inactive")
+    assert foo1.wait_for_unit_state_to_be(hirte_proxy_service, "inactive")
+
+
+@pytest.mark.timeout(15)
+def test_proxy_service_multiple_services_multiple_nodes(
+        hirte_test: HirteTest,
+        hirte_ctrl_default_config: HirteControllerConfig,
+        hirte_node_default_config: HirteNodeConfig):
+
+    node_foo1_cfg = hirte_node_default_config.deep_copy()
+    node_foo1_cfg.node_name = node_foo1_name
+
+    node_foo2_cfg = hirte_node_default_config.deep_copy()
+    node_foo2_cfg.node_name = node_foo2_name
+
+    node_bar_cfg = hirte_node_default_config.deep_copy()
+    node_bar_cfg.node_name = node_bar_name
+
+    hirte_ctrl_default_config.allowed_node_names = [node_foo1_name, node_foo2_name, node_bar_name]
+
+    hirte_test.set_hirte_controller_config(hirte_ctrl_default_config)
+    hirte_test.add_hirte_node_config(node_foo1_cfg)
+    hirte_test.add_hirte_node_config(node_foo2_cfg)
+    hirte_test.add_hirte_node_config(node_bar_cfg)
+
+    hirte_test.run(exec)
diff --git a/tests/tests/tier0/proxy-service-multiple-services-one-node/main.fmf b/tests/tests/tier0/proxy-service-multiple-services-one-node/main.fmf
new file mode 100644
index 0000000000..254d0c3476
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-multiple-services-one-node/main.fmf
@@ -0,0 +1,6 @@
+summary: Test proper lifetimes of all services when multiple services on the same node request the same service on the same node
+
+tier: 0
+
+test: |
+  pytest -svv
diff --git a/tests/tests/tier0/proxy-service-multiple-services-one-node/systemd/requesting-the-first.service b/tests/tests/tier0/proxy-service-multiple-services-one-node/systemd/requesting-the-first.service
new file mode 100644
index 0000000000..58e4ff769d
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-multiple-services-one-node/systemd/requesting-the-first.service
@@ -0,0 +1,7 @@
+[Unit]
+Wants=hirte-proxy@node-bar_simple.service
+After=hirte-proxy@node-bar_simple.service
+
+[Service]
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-multiple-services-one-node/systemd/requesting-the-second.service b/tests/tests/tier0/proxy-service-multiple-services-one-node/systemd/requesting-the-second.service
new file mode 100644
index 0000000000..58e4ff769d
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-multiple-services-one-node/systemd/requesting-the-second.service
@@ -0,0 +1,7 @@
+[Unit]
+Wants=hirte-proxy@node-bar_simple.service
+After=hirte-proxy@node-bar_simple.service
+
+[Service]
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-multiple-services-one-node/systemd/simple.service b/tests/tests/tier0/proxy-service-multiple-services-one-node/systemd/simple.service
new file mode 100644
index 0000000000..9ca13ed786
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-multiple-services-one-node/systemd/simple.service
@@ -0,0 +1,8 @@
+[Unit]
+Description=Just sleeping
+StopWhenUnneeded=yes
+
+[Service]
+Type=simple
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-multiple-services-one-node/test_proxy_service_multiple_services_one_node.py b/tests/tests/tier0/proxy-service-multiple-services-one-node/test_proxy_service_multiple_services_one_node.py
new file mode 100644
index 0000000000..314bfa6da6
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-multiple-services-one-node/test_proxy_service_multiple_services_one_node.py
@@ -0,0 +1,78 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import pytest
+from typing import Dict
+
+from hirte_test.config import HirteControllerConfig, HirteNodeConfig
+from hirte_test.container import HirteControllerContainer, HirteNodeContainer
+from hirte_test.test import HirteTest
+from hirte_test.util import assemble_hirte_dep_service_name, assemble_hirte_proxy_service_name
+
+
+node_foo_name = "node-foo"
+node_bar_name = "node-bar"
+
+requesting_the_first_service = "requesting-the-first.service"
+requesting_the_second_service = "requesting-the-second.service"
+simple_service = "simple.service"
+
+
+def exec(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
+    foo = nodes[node_foo_name]
+    bar = nodes[node_bar_name]
+
+    source_dir = "systemd"
+    target_dir = os.path.join("/", "etc", "systemd", "system")
+
+    foo.copy_systemd_service(requesting_the_first_service, source_dir, target_dir)
+    foo.copy_systemd_service(requesting_the_second_service, source_dir, target_dir)
+    bar.copy_systemd_service(simple_service, source_dir, target_dir)
+
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+
+    ctrl.start_unit(node_foo_name, requesting_the_first_service)
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "active")
+    assert foo.wait_for_unit_state_to_be(requesting_the_first_service, "active")
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "active")
+
+    ctrl.start_unit(node_foo_name, requesting_the_second_service)
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "active")
+    assert foo.wait_for_unit_state_to_be(requesting_the_second_service, "active")
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "active")
+
+    ctrl.stop_unit(node_foo_name, requesting_the_first_service)
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "active")
+    assert foo.wait_for_unit_state_to_be(requesting_the_first_service, "inactive")
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "active")
+
+    ctrl.stop_unit(node_foo_name, requesting_the_second_service)
+    assert bar.wait_for_unit_state_to_be(simple_service, "inactive")
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "inactive")
+    assert foo.wait_for_unit_state_to_be(requesting_the_second_service, "inactive")
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "inactive")
+
+
+@pytest.mark.timeout(15)
+def test_proxy_service_multiple_services_one_node(
+        hirte_test: HirteTest,
+        hirte_ctrl_default_config: HirteControllerConfig,
+        hirte_node_default_config: HirteNodeConfig):
+
+    node_foo_cfg = hirte_node_default_config.deep_copy()
+    node_foo_cfg.node_name = node_foo_name
+
+    node_bar_cfg = hirte_node_default_config.deep_copy()
+    node_bar_cfg.node_name = node_bar_name
+
+    hirte_ctrl_default_config.allowed_node_names = [node_foo_name, node_bar_name]
+
+    hirte_test.set_hirte_controller_config(hirte_ctrl_default_config)
+    hirte_test.add_hirte_node_config(node_foo_cfg)
+    hirte_test.add_hirte_node_config(node_bar_cfg)
+
+    hirte_test.run(exec)
diff --git a/tests/tests/tier0/proxy-service-start/systemd/requesting.service b/tests/tests/tier0/proxy-service-start/systemd/requesting.service
index 4398b73f10..58e4ff769d 100644
--- a/tests/tests/tier0/proxy-service-start/systemd/requesting.service
+++ b/tests/tests/tier0/proxy-service-start/systemd/requesting.service
@@ -4,4 +4,4 @@ After=hirte-proxy@node-bar_simple.service
 
 [Service]
 ExecStart=/bin/true
-RemainAfterExit=yes
\ No newline at end of file
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-start/systemd/simple.service b/tests/tests/tier0/proxy-service-start/systemd/simple.service
index 46e3b1e6aa..acacd10fca 100644
--- a/tests/tests/tier0/proxy-service-start/systemd/simple.service
+++ b/tests/tests/tier0/proxy-service-start/systemd/simple.service
@@ -4,4 +4,4 @@ Description=Just sleeping
 [Service]
 Type=simple
 ExecStart=/bin/true
-RemainAfterExit=yes
\ No newline at end of file
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-start/test_proxy_service_start.py b/tests/tests/tier0/proxy-service-start/test_proxy_service_start.py
index 967f4ba516..225921afe7 100644
--- a/tests/tests/tier0/proxy-service-start/test_proxy_service_start.py
+++ b/tests/tests/tier0/proxy-service-start/test_proxy_service_start.py
@@ -1,6 +1,5 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 
-import re
 import os
 import pytest
 from typing import Dict
@@ -8,38 +7,38 @@
 from hirte_test.config import HirteControllerConfig, HirteNodeConfig
 from hirte_test.container import HirteControllerContainer, HirteNodeContainer
 from hirte_test.test import HirteTest
+from hirte_test.util import assemble_hirte_dep_service_name, assemble_hirte_proxy_service_name
 
 
 node_foo_name = "node-foo"
 node_bar_name = "node-bar"
 
+requesting_service = "requesting.service"
+simple_service = "simple.service"
 
-def verify_proxy_start(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
+
+def verify_proxy_start(foo: HirteNodeContainer, bar: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "active")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "active")
+
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "active")
+
+
+def exec(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
     foo = nodes[node_foo_name]
     bar = nodes[node_bar_name]
 
     source_dir = "systemd"
     target_dir = os.path.join("/", "etc", "systemd", "system")
-    requesting_service = "requesting.service"
-    simple_service = "simple.service"
 
     foo.copy_systemd_service(requesting_service, source_dir, target_dir)
     bar.copy_systemd_service(simple_service, source_dir, target_dir)
 
-    result, output = ctrl.exec_run(f"hirtectl start {node_foo_name} {requesting_service}")
-    if result != 0:
-        raise Exception(f"Failed to start requesting service on node foo: {output}")
-
-    # verify the units have been loaded
-    result, output = ctrl.exec_run(f"hirtectl list-units {node_bar_name} | grep {requesting_service}")
-    if result != 0:
-        raise Exception(f"Failed to list units for node bar: {output}")
-    assert re.search("active", output) is not None
-
-    result, output = ctrl.exec_run(f"hirtectl list-units {node_foo_name} | grep {simple_service}")
-    if result != 0:
-        raise Exception(f"Failed to list units for node foo: {output}")
-    assert re.search("active", output) is not None
+    ctrl.start_unit(node_foo_name, requesting_service)
+    verify_proxy_start(foo, bar)
 
 
 @pytest.mark.timeout(10)
@@ -60,4 +59,4 @@ def test_proxy_service_start(
     hirte_test.add_hirte_node_config(node_foo_cfg)
     hirte_test.add_hirte_node_config(node_bar_cfg)
 
-    hirte_test.run(verify_proxy_start)
+    hirte_test.run(exec)
diff --git a/tests/tests/tier0/proxy-service-stop-hirte-dep/main.fmf b/tests/tests/tier0/proxy-service-stop-hirte-dep/main.fmf
new file mode 100644
index 0000000000..3730eb0686
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-hirte-dep/main.fmf
@@ -0,0 +1,6 @@
+summary: Test if stopping the hirte-dep service results in a stopped hirte-proxy service, but both actual services are still running
+
+tier: 0
+
+test: |
+  pytest -svv
diff --git a/tests/tests/tier0/proxy-service-stop-hirte-dep/systemd/requesting.service b/tests/tests/tier0/proxy-service-stop-hirte-dep/systemd/requesting.service
new file mode 100644
index 0000000000..58e4ff769d
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-hirte-dep/systemd/requesting.service
@@ -0,0 +1,7 @@
+[Unit]
+Wants=hirte-proxy@node-bar_simple.service
+After=hirte-proxy@node-bar_simple.service
+
+[Service]
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-stop-hirte-dep/systemd/simple.service b/tests/tests/tier0/proxy-service-stop-hirte-dep/systemd/simple.service
new file mode 100644
index 0000000000..acacd10fca
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-hirte-dep/systemd/simple.service
@@ -0,0 +1,7 @@
+[Unit]
+Description=Just sleeping
+
+[Service]
+Type=simple
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-stop-hirte-dep/test_proxy_service_stop_hirte_dep.py b/tests/tests/tier0/proxy-service-stop-hirte-dep/test_proxy_service_stop_hirte_dep.py
new file mode 100644
index 0000000000..46385887ad
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-hirte-dep/test_proxy_service_stop_hirte_dep.py
@@ -0,0 +1,74 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import pytest
+from typing import Dict
+
+from hirte_test.config import HirteControllerConfig, HirteNodeConfig
+from hirte_test.container import HirteControllerContainer, HirteNodeContainer
+from hirte_test.test import HirteTest
+from hirte_test.util import assemble_hirte_dep_service_name, assemble_hirte_proxy_service_name
+
+
+node_foo_name = "node-foo"
+node_bar_name = "node-bar"
+
+requesting_service = "requesting.service"
+simple_service = "simple.service"
+
+
+def verify_proxy_start(foo: HirteNodeContainer, bar: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "active")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "active")
+
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "active")
+
+
+def verify_proxy_stop(foo: HirteNodeContainer, bar: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "active")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "active")
+
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "inactive")
+
+
+def exec(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
+    foo = nodes[node_foo_name]
+    bar = nodes[node_bar_name]
+
+    source_dir = os.path.join(".", "systemd")
+    target_dir = os.path.join("/", "etc", "systemd", "system")
+
+    foo.copy_systemd_service(requesting_service, source_dir, target_dir)
+    bar.copy_systemd_service(simple_service, source_dir, target_dir)
+
+    ctrl.start_unit(node_foo_name, requesting_service)
+    verify_proxy_start(foo, bar)
+    ctrl.stop_unit(node_bar_name, assemble_hirte_dep_service_name(simple_service))
+    verify_proxy_stop(foo, bar)
+
+
+@pytest.mark.timeout(10)
+def test_proxy_service_stop_hirte_dep(
+        hirte_test: HirteTest,
+        hirte_ctrl_default_config: HirteControllerConfig,
+        hirte_node_default_config: HirteNodeConfig):
+
+    node_foo_cfg = hirte_node_default_config.deep_copy()
+    node_foo_cfg.node_name = node_foo_name
+
+    node_bar_cfg = hirte_node_default_config.deep_copy()
+    node_bar_cfg.node_name = node_bar_name
+
+    hirte_ctrl_default_config.allowed_node_names = [node_foo_name, node_bar_name]
+
+    hirte_test.set_hirte_controller_config(hirte_ctrl_default_config)
+    hirte_test.add_hirte_node_config(node_foo_cfg)
+    hirte_test.add_hirte_node_config(node_bar_cfg)
+
+    hirte_test.run(exec)
diff --git a/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/main.fmf b/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/main.fmf
new file mode 100644
index 0000000000..abb2c1f3eb
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/main.fmf
@@ -0,0 +1,6 @@
+summary: Test if stopping the requesting service results in a stopped dependency service if its marked with StopWhenUnneeded
+
+tier: 0
+
+test: |
+  pytest -svv
diff --git a/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/systemd/requesting.service b/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/systemd/requesting.service
new file mode 100644
index 0000000000..58e4ff769d
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/systemd/requesting.service
@@ -0,0 +1,7 @@
+[Unit]
+Wants=hirte-proxy@node-bar_simple.service
+After=hirte-proxy@node-bar_simple.service
+
+[Service]
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/systemd/simple.service b/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/systemd/simple.service
new file mode 100644
index 0000000000..9ca13ed786
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/systemd/simple.service
@@ -0,0 +1,8 @@
+[Unit]
+Description=Just sleeping
+StopWhenUnneeded=yes
+
+[Service]
+Type=simple
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/test_proxy_service_stop_requesting_with_unneeded.py b/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/test_proxy_service_stop_requesting_with_unneeded.py
new file mode 100644
index 0000000000..b766c9c6aa
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-requesting-with-unneeded/test_proxy_service_stop_requesting_with_unneeded.py
@@ -0,0 +1,74 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import pytest
+from typing import Dict
+
+from hirte_test.config import HirteControllerConfig, HirteNodeConfig
+from hirte_test.container import HirteControllerContainer, HirteNodeContainer
+from hirte_test.test import HirteTest
+from hirte_test.util import assemble_hirte_dep_service_name, assemble_hirte_proxy_service_name
+
+
+node_foo_name = "node-foo"
+node_bar_name = "node-bar"
+
+requesting_service = "requesting.service"
+simple_service = "simple.service"
+
+
+def verify_proxy_start(foo: HirteNodeContainer, bar: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "active")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "active")
+
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "active")
+
+
+def verify_proxy_stop(foo: HirteNodeContainer, bar: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "inactive")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "inactive")
+
+    assert bar.wait_for_unit_state_to_be(simple_service, "inactive")
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "inactive")
+
+
+def exec(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
+    foo = nodes[node_foo_name]
+    bar = nodes[node_bar_name]
+
+    source_dir = os.path.join(".", "systemd")
+    target_dir = os.path.join("/", "etc", "systemd", "system")
+
+    foo.copy_systemd_service(requesting_service, source_dir, target_dir)
+    bar.copy_systemd_service(simple_service, source_dir, target_dir)
+
+    ctrl.start_unit(node_foo_name, requesting_service)
+    verify_proxy_start(foo, bar)
+    ctrl.stop_unit(node_foo_name, requesting_service)
+    verify_proxy_stop(foo, bar)
+
+
+@pytest.mark.timeout(10)
+def test_proxy_service_stop_requesting(
+        hirte_test: HirteTest,
+        hirte_ctrl_default_config: HirteControllerConfig,
+        hirte_node_default_config: HirteNodeConfig):
+
+    node_foo_cfg = hirte_node_default_config.deep_copy()
+    node_foo_cfg.node_name = node_foo_name
+
+    node_bar_cfg = hirte_node_default_config.deep_copy()
+    node_bar_cfg.node_name = node_bar_name
+
+    hirte_ctrl_default_config.allowed_node_names = [node_foo_name, node_bar_name]
+
+    hirte_test.set_hirte_controller_config(hirte_ctrl_default_config)
+    hirte_test.add_hirte_node_config(node_foo_cfg)
+    hirte_test.add_hirte_node_config(node_bar_cfg)
+
+    hirte_test.run(exec)
diff --git a/tests/tests/tier0/proxy-service-stop-requesting/main.fmf b/tests/tests/tier0/proxy-service-stop-requesting/main.fmf
index 613ad95b10..a46142b0d1 100644
--- a/tests/tests/tier0/proxy-service-stop-requesting/main.fmf
+++ b/tests/tests/tier0/proxy-service-stop-requesting/main.fmf
@@ -1,4 +1,4 @@
-summary: Test if requesting.service's dependency on simple.service from node bar can be resolved
+summary: Test if stopping the requesting service results in a stopped hirte-proxy and hirte-dep service, but dependency service is still running
 
 tier: 0
 
diff --git a/tests/tests/tier0/proxy-service-stop-requesting/systemd/requesting.service b/tests/tests/tier0/proxy-service-stop-requesting/systemd/requesting.service
index 4398b73f10..58e4ff769d 100644
--- a/tests/tests/tier0/proxy-service-stop-requesting/systemd/requesting.service
+++ b/tests/tests/tier0/proxy-service-stop-requesting/systemd/requesting.service
@@ -4,4 +4,4 @@ After=hirte-proxy@node-bar_simple.service
 
 [Service]
 ExecStart=/bin/true
-RemainAfterExit=yes
\ No newline at end of file
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-stop-requesting/systemd/simple.service b/tests/tests/tier0/proxy-service-stop-requesting/systemd/simple.service
index 46e3b1e6aa..acacd10fca 100644
--- a/tests/tests/tier0/proxy-service-stop-requesting/systemd/simple.service
+++ b/tests/tests/tier0/proxy-service-stop-requesting/systemd/simple.service
@@ -4,4 +4,4 @@ Description=Just sleeping
 [Service]
 Type=simple
 ExecStart=/bin/true
-RemainAfterExit=yes
\ No newline at end of file
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-stop-requesting/test_proxy_service_stop_requesting.py b/tests/tests/tier0/proxy-service-stop-requesting/test_proxy_service_stop_requesting.py
index 5fbf9dd946..40e59a37bf 100644
--- a/tests/tests/tier0/proxy-service-stop-requesting/test_proxy_service_stop_requesting.py
+++ b/tests/tests/tier0/proxy-service-stop-requesting/test_proxy_service_stop_requesting.py
@@ -1,6 +1,5 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 
-import re
 import os
 import pytest
 from typing import Dict
@@ -8,6 +7,7 @@
 from hirte_test.config import HirteControllerConfig, HirteNodeConfig
 from hirte_test.container import HirteControllerContainer, HirteNodeContainer
 from hirte_test.test import HirteTest
+from hirte_test.util import assemble_hirte_dep_service_name, assemble_hirte_proxy_service_name
 
 
 node_foo_name = "node-foo"
@@ -17,54 +17,40 @@
 simple_service = "simple.service"
 
 
-def setup_and_start_proxy(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
-    foo = nodes[node_foo_name]
-    bar = nodes[node_bar_name]
-
-    source_dir = os.path.join(".", "systemd")
-    target_dir = os.path.join("/", "etc", "systemd", "system")
+def verify_proxy_start(foo: HirteNodeContainer, bar: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "active")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "active")
 
-    foo.copy_systemd_service(requesting_service, source_dir, target_dir)
-    bar.copy_systemd_service(simple_service, source_dir, target_dir)
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "active")
 
-    result, output = ctrl.exec_run(f"hirtectl start {node_foo_name} {requesting_service}")
-    if result != 0:
-        raise Exception(f"Failed to start requesting service on node foo: {output}")
 
+def verify_proxy_stop(foo: HirteNodeContainer, bar: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "inactive")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "inactive")
 
-def verify_proxies_running(ctrl: HirteControllerContainer):
-    result, output = ctrl.exec_run(f"hirtectl list-units {node_bar_name} | grep {requesting_service}")
-    if result != 0:
-        raise Exception(f"Failed to list units for node bar: {output}")
-    assert re.search("active", output) is not None
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "inactive")
 
-    result, output = ctrl.exec_run(f"hirtectl list-units {node_foo_name} | grep {simple_service}")
-    if result != 0:
-        raise Exception(f"Failed to list units for node foo: {output}")
-    assert re.search("active", output) is not None
 
+def exec(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
+    foo = nodes[node_foo_name]
+    bar = nodes[node_bar_name]
 
-def stop_requesting_service(ctrl: HirteControllerContainer):
-    result, output = ctrl.exec_run(f"hirtectl stop {node_foo_name} requesting.service")
-    if result != 0:
-        raise Exception(f"Failed to start requesting service on node foo: {output}")
+    source_dir = os.path.join(".", "systemd")
+    target_dir = os.path.join("/", "etc", "systemd", "system")
 
+    foo.copy_systemd_service(requesting_service, source_dir, target_dir)
+    bar.copy_systemd_service(simple_service, source_dir, target_dir)
 
-def exec(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
-    setup_and_start_proxy(ctrl, nodes)
-    verify_proxies_running(ctrl)
-    stop_requesting_service(ctrl)
-
-    # verify requesting service is stopped, but simple service is still running
-    result, output = ctrl.exec_run(f"hirtectl list-units {node_bar_name} | grep {requesting_service}")
-    if result != 0:
-        raise Exception(f"Failed to list units for node bar: {output}")
-    assert re.search("inactive", output) is not None
-
-    result, output = ctrl.exec_run(f"hirtectl list-units {node_foo_name} | grep {simple_service}")
-    if result != 0:
-        raise Exception(f"Failed to list units for node foo: {output}")
-    assert re.search("active", output) is not None
+    ctrl.start_unit(node_foo_name, requesting_service)
+    verify_proxy_start(foo, bar)
+    ctrl.stop_unit(node_foo_name, requesting_service)
+    verify_proxy_stop(foo, bar)
 
 
 @pytest.mark.timeout(10)
diff --git a/tests/tests/tier0/proxy-service-stop-target/main.fmf b/tests/tests/tier0/proxy-service-stop-target/main.fmf
new file mode 100644
index 0000000000..f1c32b33e5
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-target/main.fmf
@@ -0,0 +1,6 @@
+summary: Test if stopping the dependency service results in a stopped hirte-dep and hirte-proxy service, but the requesting service is still running
+
+tier: 0
+
+test: |
+  pytest -svv
diff --git a/tests/tests/tier0/proxy-service-stop-target/systemd/requesting.service b/tests/tests/tier0/proxy-service-stop-target/systemd/requesting.service
new file mode 100644
index 0000000000..58e4ff769d
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-target/systemd/requesting.service
@@ -0,0 +1,7 @@
+[Unit]
+Wants=hirte-proxy@node-bar_simple.service
+After=hirte-proxy@node-bar_simple.service
+
+[Service]
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-stop-target/systemd/simple.service b/tests/tests/tier0/proxy-service-stop-target/systemd/simple.service
new file mode 100644
index 0000000000..acacd10fca
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-target/systemd/simple.service
@@ -0,0 +1,7 @@
+[Unit]
+Description=Just sleeping
+
+[Service]
+Type=simple
+ExecStart=/bin/true
+RemainAfterExit=yes
diff --git a/tests/tests/tier0/proxy-service-stop-target/test_proxy_service_stop_target.py b/tests/tests/tier0/proxy-service-stop-target/test_proxy_service_stop_target.py
new file mode 100644
index 0000000000..e0d29981c2
--- /dev/null
+++ b/tests/tests/tier0/proxy-service-stop-target/test_proxy_service_stop_target.py
@@ -0,0 +1,74 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import os
+import pytest
+from typing import Dict
+
+from hirte_test.config import HirteControllerConfig, HirteNodeConfig
+from hirte_test.container import HirteControllerContainer, HirteNodeContainer
+from hirte_test.test import HirteTest
+from hirte_test.util import assemble_hirte_dep_service_name, assemble_hirte_proxy_service_name
+
+
+node_foo_name = "node-foo"
+node_bar_name = "node-bar"
+
+requesting_service = "requesting.service"
+simple_service = "simple.service"
+
+
+def verify_proxy_start(foo: HirteNodeContainer, bar: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "active")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "active")
+
+    assert bar.wait_for_unit_state_to_be(simple_service, "active")
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "active")
+
+
+def verify_proxy_stop(foo: HirteNodeContainer, bar: HirteNodeContainer):
+    assert foo.wait_for_unit_state_to_be(requesting_service, "active")
+    hirte_proxy_service = assemble_hirte_proxy_service_name(node_bar_name, simple_service)
+    assert foo.wait_for_unit_state_to_be(hirte_proxy_service, "inactive")
+
+    assert bar.wait_for_unit_state_to_be(simple_service, "inactive")
+    hirte_dep_service = assemble_hirte_dep_service_name(simple_service)
+    assert bar.wait_for_unit_state_to_be(hirte_dep_service, "inactive")
+
+
+def exec(ctrl: HirteControllerContainer, nodes: Dict[str, HirteNodeContainer]):
+    foo = nodes[node_foo_name]
+    bar = nodes[node_bar_name]
+
+    source_dir = os.path.join(".", "systemd")
+    target_dir = os.path.join("/", "etc", "systemd", "system")
+
+    foo.copy_systemd_service(requesting_service, source_dir, target_dir)
+    bar.copy_systemd_service(simple_service, source_dir, target_dir)
+
+    ctrl.start_unit(node_foo_name, requesting_service)
+    verify_proxy_start(foo, bar)
+    ctrl.stop_unit(node_bar_name, simple_service)
+    verify_proxy_stop(foo, bar)
+
+
+@pytest.mark.timeout(10)
+def test_proxy_service_stop_target(
+        hirte_test: HirteTest,
+        hirte_ctrl_default_config: HirteControllerConfig,
+        hirte_node_default_config: HirteNodeConfig):
+
+    node_foo_cfg = hirte_node_default_config.deep_copy()
+    node_foo_cfg.node_name = node_foo_name
+
+    node_bar_cfg = hirte_node_default_config.deep_copy()
+    node_bar_cfg.node_name = node_bar_name
+
+    hirte_ctrl_default_config.allowed_node_names = [node_foo_name, node_bar_name]
+
+    hirte_test.set_hirte_controller_config(hirte_ctrl_default_config)
+    hirte_test.add_hirte_node_config(node_foo_cfg)
+    hirte_test.add_hirte_node_config(node_bar_cfg)
+
+    hirte_test.run(exec)