diff --git a/src/main/java/dev/tomr/hcloud/listener/ServerChangeListener.java b/src/main/java/dev/tomr/hcloud/listener/ServerChangeListener.java
index e490819..af85a78 100644
--- a/src/main/java/dev/tomr/hcloud/listener/ServerChangeListener.java
+++ b/src/main/java/dev/tomr/hcloud/listener/ServerChangeListener.java
@@ -19,13 +19,40 @@ public class ServerChangeListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
Server server = (Server) evt.getSource();
- if (evt.getPropertyName().equals("delete")) {
- logger.warn("Server delete has been called. Instructing Hetzner to delete");
- HetznerCloud.getInstance().getServiceManager().getServerService().deleteServerFromHetzner(server);
- } else {
- logger.info("Server changed: " + evt.getPropertyName());
- logger.info("Server: " + evt.getOldValue() + " -> " + evt.getNewValue());
- HetznerCloud.getInstance().getServiceManager().getServerService().serverNameOrLabelUpdate(evt.getPropertyName(), evt.getNewValue(), server);
+ String propertyName = evt.getPropertyName();
+
+ switch (propertyName) {
+ case "delete" -> {
+ logger.warn("Server delete has been called. Instructing Hetzner to delete");
+ HetznerCloud.getInstance().getServiceManager().getServerService().deleteServerFromHetzner(server);
+ }
+ case "shutdown" -> {
+ logger.info("Server shutdown has been called. Instructing Hetzner to shut the server down");
+ HetznerCloud.getInstance().getServiceManager().getServerService().shutdownServer(server);
+ }
+ case "poweroff" -> {
+ logger.info("Server poweroff has been called. Instructing Hetzner to power down the server");
+ logger.warn("This is a potentially destructive action!");
+ HetznerCloud.getInstance().getServiceManager().getServerService().powerOffServer(server);
+ }
+ case "poweron" -> {
+ logger.info("Server power on has been called. Instructing Hetzner to power up the server");
+ HetznerCloud.getInstance().getServiceManager().getServerService().powerOnServer(server);
+ }
+ case "reboot" -> {
+ logger.info("Server reboot has been called. Instructing Hetzner to reboot the server");
+ HetznerCloud.getInstance().getServiceManager().getServerService().rebootServer(server);
+ }
+ case "reset" -> {
+ logger.info("Server reset has been called. Instructing Hetzner to reset the server");
+ logger.warn("This is a potentially destructive action!");
+ HetznerCloud.getInstance().getServiceManager().getServerService().resetServer(server);
+ }
+ default -> {
+ logger.info("Server changed: {}", evt.getPropertyName());
+ logger.info("Server: {} -> {}", evt.getOldValue(), evt.getNewValue());
+ HetznerCloud.getInstance().getServiceManager().getServerService().serverNameOrLabelUpdate(evt.getPropertyName(), evt.getNewValue(), server);
+ }
}
}
}
diff --git a/src/main/java/dev/tomr/hcloud/resources/server/Server.java b/src/main/java/dev/tomr/hcloud/resources/server/Server.java
index dd18eb4..725c481 100644
--- a/src/main/java/dev/tomr/hcloud/resources/server/Server.java
+++ b/src/main/java/dev/tomr/hcloud/resources/server/Server.java
@@ -97,6 +97,8 @@ private void setupPropertyChangeListener() {
propertyChangeSupport.addPropertyChangeListener(HetznerCloud.getInstance().getListenerManager().getServerChangeListener());
}
+ // The following methods are for calling Actions on the server
+
/**
* Deletes a Server from Hetzner. Note, this is immediate and destructive. Ensure you want to delete the server before calling.
*/
@@ -104,6 +106,41 @@ public void delete() {
propertyChangeSupport.firePropertyChange("delete", null, null);
}
+ /**
+ * Sends a Shutdown request to the server by sending an ACPI request, which will instruct the OS to shut it down. Note that if you must> ensure the server is completely offline, you should also call .powerOff() to ensure the 'plug is pulled'.
+ * The server OS must support ACPI
+ */
+ public void shutdown() {
+ propertyChangeSupport.firePropertyChange("shutdown", null, null);
+ }
+
+ /**
+ * Sends a command to Power off the server. This is essentially 'pulling the plug' and could be destructive if programs are still running on the server. Only use if absolutely necessary
+ */
+ public void powerOff() {
+ propertyChangeSupport.firePropertyChange("poweroff", null, null);
+ }
+
+ /**
+ * Starts the Server by turning its power on
+ */
+ public void powerOn() {
+ propertyChangeSupport.firePropertyChange("poweron", null, null);
+ }
+
+ /**
+ * Sends a reboot request to the server by sending an ACPI request. The server OS must support ACPI
+ */
+ public void reboot() {
+ propertyChangeSupport.firePropertyChange("reboot", null, null);
+ }
+
+ /**
+ * Cuts power to the server and starts it again. Forcefully stops the server without giving the OS time to shut down. Should only be used if reboot does not work.
+ */
+ public void reset() {
+ propertyChangeSupport.firePropertyChange("reset", null, null);
+ }
// These are the current setters that will send an API request (PUT /servers) when actions begin to be added, they will also likely be triggered by setters
diff --git a/src/main/java/dev/tomr/hcloud/service/action/Action.java b/src/main/java/dev/tomr/hcloud/service/action/Action.java
new file mode 100644
index 0000000..18a757b
--- /dev/null
+++ b/src/main/java/dev/tomr/hcloud/service/action/Action.java
@@ -0,0 +1,15 @@
+package dev.tomr.hcloud.service.action;
+
+public enum Action {
+ SHUTDOWN("shutdown"),
+ POWEROFF("poweroff"),
+ POWERON("poweron"),
+ REBOOT("reboot"),
+ RESET("reset"),;
+
+ public final String path;
+
+ Action(String path) {
+ this.path = path;
+ }
+}
diff --git a/src/main/java/dev/tomr/hcloud/service/action/ActionService.java b/src/main/java/dev/tomr/hcloud/service/action/ActionService.java
index 5026250..7cc5131 100644
--- a/src/main/java/dev/tomr/hcloud/service/action/ActionService.java
+++ b/src/main/java/dev/tomr/hcloud/service/action/ActionService.java
@@ -49,6 +49,7 @@ public CompletableFuture waitForActionToComplete(Action action) {
futures.forEach((f) -> {
f.cancel(true);
});
+ scheduler.shutdownNow();
return completedAction.get();
});
}
diff --git a/src/main/java/dev/tomr/hcloud/service/server/ServerService.java b/src/main/java/dev/tomr/hcloud/service/server/ServerService.java
index 4b0ef32..5fc097f 100644
--- a/src/main/java/dev/tomr/hcloud/service/server/ServerService.java
+++ b/src/main/java/dev/tomr/hcloud/service/server/ServerService.java
@@ -23,9 +23,9 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Function;
import static dev.tomr.hcloud.http.RequestVerb.*;
+import static dev.tomr.hcloud.service.action.Action.*;
public class ServerService {
protected static final Logger logger = LogManager.getLogger();
@@ -127,6 +127,53 @@ public void deleteServerFromHetzner(Server server) {
}
}
+ public void shutdownServer(Server server) {
+ sendServerAction(server, SHUTDOWN);
+ }
+
+ public void powerOffServer(Server server) {
+ sendServerAction(server, POWEROFF);
+ }
+
+ public void powerOnServer(Server server) {
+ sendServerAction(server, POWERON);
+ }
+
+ public void rebootServer(Server server) {
+ sendServerAction(server, REBOOT);
+ }
+
+ public void resetServer(Server server) {
+ sendServerAction(server, RESET);
+ }
+
+ private void sendServerAction(Server server, dev.tomr.hcloud.service.action.Action givenAction) {
+ List hostAndKey = HetznerCloud.getInstance().getHttpDetails();
+ String httpUrl = String.format("%sservers/%d/actions/%s", hostAndKey.get(0), server.getId(), givenAction.path);
+ AtomicReference exceptionMsg = new AtomicReference<>();
+ try {
+ Action action = client.sendHttpRequest(ActionWrapper.class, httpUrl, POST, hostAndKey.get(1), "").getAction();
+ CompletableFuture completedActionFuture = serviceManager.getActionService().waitForActionToComplete(action).thenApplyAsync((completedAction) -> {
+ if (completedAction == null) {
+ throw new NullPointerException();
+ }
+ logger.info("Server {} at {}", givenAction.toString(), completedAction.getFinished());
+ return completedAction;
+ }).exceptionally((e) -> {
+ logger.error("Server {} failed", givenAction.toString());
+ logger.error(e.getMessage());
+ exceptionMsg.set(e.getMessage());
+ return null;
+ });
+ if (completedActionFuture.get() == null) {
+ throw new RuntimeException(exceptionMsg.get());
+ }
+ } catch (Exception e) {
+ logger.error("Failed to {} the Server", givenAction.toString());
+ throw new RuntimeException(e);
+ }
+ }
+
private void updateAllRemoteServers() {
Map newServerMap = new HashMap<>();
List hostAndKey = HetznerCloud.getInstance().getHttpDetails();
diff --git a/src/test/java/dev/tomr/hcloud/resources/server/ServerTest.java b/src/test/java/dev/tomr/hcloud/resources/server/ServerTest.java
index 92fd40a..a8ff57e 100644
--- a/src/test/java/dev/tomr/hcloud/resources/server/ServerTest.java
+++ b/src/test/java/dev/tomr/hcloud/resources/server/ServerTest.java
@@ -193,7 +193,6 @@ void callingSetNameSendsAnEventToTheServerChangeListener() {
assertNull(captor.getValue().getOldValue());
assertEquals("test", captor.getValue().getNewValue());
}
-
}
@Test
@@ -219,8 +218,130 @@ void callingDeleteSendsAnEventToTheServerChangeListener() {
verify(serverChangeListener, times(1)).propertyChange(captor.capture());
assertEquals("delete", captor.getValue().getPropertyName());
}
+ }
+
+ @Test
+ @DisplayName("calling shutdown sends an event to the ServerChangeListener")
+ void callingShutdownSendsAnEventToTheServerChangeListener() {
+ try (MockedStatic hetznerCloud = mockStatic(HetznerCloud.class)) {
+ HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
+ ServerChangeListener scl = new ServerChangeListener();
+ ServerChangeListener serverChangeListener = spy(scl);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+ ServiceManager serviceManager = mock(ServiceManager.class);
+ ServerService serverService = mock(ServerService.class);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);
+
+ hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
+ when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
+ when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
+ when(serviceManager.getServerService()).thenReturn(serverService);
+
+ Server server = new Server();
+ server.shutdown();
+ verify(serverChangeListener, times(1)).propertyChange(captor.capture());
+ assertEquals("shutdown", captor.getValue().getPropertyName());
+ }
+ }
+
+ @Test
+ @DisplayName("calling poweroff sends an event to the ServerChangeListener")
+ void callingPoweroffSendsAnEventToTheServerChangeListener() {
+ try (MockedStatic hetznerCloud = mockStatic(HetznerCloud.class)) {
+ HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
+ ServerChangeListener scl = new ServerChangeListener();
+ ServerChangeListener serverChangeListener = spy(scl);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+ ServiceManager serviceManager = mock(ServiceManager.class);
+ ServerService serverService = mock(ServerService.class);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);
+
+ hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
+ when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
+ when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
+ when(serviceManager.getServerService()).thenReturn(serverService);
+ Server server = new Server();
+ server.powerOff();
+ verify(serverChangeListener, times(1)).propertyChange(captor.capture());
+ assertEquals("poweroff", captor.getValue().getPropertyName());
+ }
}
+ @Test
+ @DisplayName("calling poweron sends an event to the ServerChangeListener")
+ void callingPowerOnSendsAnEventToTheServerChangeListener() {
+ try (MockedStatic hetznerCloud = mockStatic(HetznerCloud.class)) {
+ HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
+ ServerChangeListener scl = new ServerChangeListener();
+ ServerChangeListener serverChangeListener = spy(scl);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+ ServiceManager serviceManager = mock(ServiceManager.class);
+ ServerService serverService = mock(ServerService.class);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);
+
+ hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
+ when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
+ when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
+ when(serviceManager.getServerService()).thenReturn(serverService);
+
+ Server server = new Server();
+ server.powerOn();
+ verify(serverChangeListener, times(1)).propertyChange(captor.capture());
+ assertEquals("poweron", captor.getValue().getPropertyName());
+ }
+ }
+ @Test
+ @DisplayName("calling reboot sends an event to the ServerChangeListener")
+ void callingRebootSendsAnEventToTheServerChangeListener() {
+ try (MockedStatic hetznerCloud = mockStatic(HetznerCloud.class)) {
+ HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
+ ServerChangeListener scl = new ServerChangeListener();
+ ServerChangeListener serverChangeListener = spy(scl);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+ ServiceManager serviceManager = mock(ServiceManager.class);
+ ServerService serverService = mock(ServerService.class);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);
+
+ hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
+ when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
+ when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
+ when(serviceManager.getServerService()).thenReturn(serverService);
+
+ Server server = new Server();
+ server.reboot();
+ verify(serverChangeListener, times(1)).propertyChange(captor.capture());
+ assertEquals("reboot", captor.getValue().getPropertyName());
+ }
+ }
+
+ @Test
+ @DisplayName("calling reset sends an event to the ServerChangeListener")
+ void callingResetSendsAnEventToTheServerChangeListener() {
+ try (MockedStatic hetznerCloud = mockStatic(HetznerCloud.class)) {
+ HetznerCloud hetznerCloudMock = mock(HetznerCloud.class);
+ ServerChangeListener scl = new ServerChangeListener();
+ ServerChangeListener serverChangeListener = spy(scl);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+ ServiceManager serviceManager = mock(ServiceManager.class);
+ ServerService serverService = mock(ServerService.class);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PropertyChangeEvent.class);
+
+ hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock);
+ when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager);
+ when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener);
+ when(serviceManager.getServerService()).thenReturn(serverService);
+
+ Server server = new Server();
+ server.reset();
+ verify(serverChangeListener, times(1)).propertyChange(captor.capture());
+ assertEquals("reset", captor.getValue().getPropertyName());
+ }
+ }
}
\ No newline at end of file
diff --git a/src/test/java/dev/tomr/hcloud/service/action/ActionEnumTest.java b/src/test/java/dev/tomr/hcloud/service/action/ActionEnumTest.java
new file mode 100644
index 0000000..34af472
--- /dev/null
+++ b/src/test/java/dev/tomr/hcloud/service/action/ActionEnumTest.java
@@ -0,0 +1,33 @@
+package dev.tomr.hcloud.service.action;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ActionEnumTest {
+
+ @Test
+ @DisplayName("SHUTDOWN enum returns 'shutdown' for the path")
+ void shutdown() {
+ assertEquals("shutdown", Action.SHUTDOWN.path);
+ }
+
+ @Test
+ @DisplayName("POWEROFF enum returns 'poweroff' for the path")
+ void poweroff() {
+ assertEquals("poweroff", Action.POWEROFF.path);
+ }
+
+ @Test
+ @DisplayName("POWERON enum returns 'poweron' for the path")
+ void poweron() {
+ assertEquals("poweron", Action.POWERON.path);
+ }
+
+ @Test
+ @DisplayName("REBOOT enum returns 'reboot' for the path")
+ void reboot() {
+ assertEquals("reboot", Action.REBOOT.path);
+ }
+}
diff --git a/src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java b/src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java
index 61bc00d..05189f7 100644
--- a/src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java
+++ b/src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java
@@ -12,10 +12,7 @@
import dev.tomr.hcloud.resources.server.Server;
import dev.tomr.hcloud.service.ServiceManager;
import dev.tomr.hcloud.service.action.ActionService;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.*;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
@@ -610,7 +607,7 @@ void whenActionServiceThrowsDeleteServerAlsoThrows() throws IOException, Interru
}
@Test
- @DisplayName("When Action returned from Hetzner is Null, server service throws a null pointer exception")
+ @DisplayName("When Delete Action returned from Hetzner is Null, server service throws a null pointer exception")
void whenActionReturnedFromHetznerIsNullServerServiceThrowsANullPointer() throws IOException, InterruptedException, IllegalAccessException {
HetznerCloud hetznerCloud = mock(HetznerCloud.class);
HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
@@ -669,4 +666,223 @@ void deleteServerFromHetznerHandlesException() throws IOException, InterruptedEx
}
}
+ @Test
+ @DisplayName("Shutdown Server calls Hetzner and tracks the action")
+ void shutdownServerCallsHetznerAndTracksTheAction() throws IOException, InterruptedException, IllegalAccessException {
+ HetznerCloud hetznerCloud = mock(HetznerCloud.class);
+ HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+ ServiceManager serviceManager = mock(ServiceManager.class);
+ ActionService actionService = mock(ActionService.class);
+
+ try (MockedStatic hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
+ MockedStatic hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
+
+ Action action = new Action();
+ action.setFinished(Date.from(Instant.now()).toString());
+
+ hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
+ hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
+ when(hetznerCloud.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
+ when(serviceManager.getActionService()).thenReturn(actionService);
+ when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.completedFuture(action));
+
+ when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString(), anyString())).thenReturn(new ActionWrapper(action));
+
+ ServerService serverService = new ServerService(serviceManager);
+ serverService.shutdownServer(new Server());
+
+ verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), eq(RequestVerb.POST), eq("key1234"), eq(""));
+ verify(actionService, times(1)).waitForActionToComplete(any(Action.class));
+ }
+ }
+
+ @Test
+ @DisplayName("When httpclient throws, then shutdown Server also throws a Runtime exception")
+ void shutdownServerHandlesException() throws IOException, InterruptedException, IllegalAccessException {
+ HetznerCloud hetznerCloud = mock(HetznerCloud.class);
+ HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+
+ try (MockedStatic hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
+ MockedStatic hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
+
+ hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
+ hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
+ when(hetznerCloud.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
+
+ when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString(), anyString())).thenThrow(new IOException());
+
+ ServerService serverService = new ServerService();
+
+ RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> serverService.shutdownServer(new Server()));
+
+ verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), eq(RequestVerb.POST), eq("key1234"), eq(""));
+
+ assertTrue(runtimeException.getMessage().contains("IOException"));
+ }
+ }
+
+ @Test
+ @DisplayName("When Shutdown Action returned from Hetzner is Null, server service throws a null pointer exception")
+ void whenShutdownActionReturnedFromHetznerIsNullServerServiceThrowsANullPointer() throws IOException, InterruptedException, IllegalAccessException {
+ HetznerCloud hetznerCloud = mock(HetznerCloud.class);
+ HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+ ServiceManager serviceManager = mock(ServiceManager.class);
+ ActionService actionService = mock(ActionService.class);
+
+ try (MockedStatic hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
+ MockedStatic hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
+
+ hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
+ hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
+ when(hetznerCloud.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
+ when(serviceManager.getActionService()).thenReturn(actionService);
+
+ when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.completedFuture(null));
+
+ when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString(), anyString())).thenReturn(new ActionWrapper(new Action()));
+
+ ServerService serverService = new ServerService(serviceManager);
+
+ RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> serverService.shutdownServer(new Server()));
+
+ verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), eq(RequestVerb.POST), eq("key1234"), eq(""));
+ verify(actionService, times(1)).waitForActionToComplete(any(Action.class));
+
+ assertTrue(runtimeException.getMessage().contains("NullPointerException"));
+ }
+ }
+
+ @Test
+ @DisplayName("Poweroff Server calls Hetzner and tracks the action")
+ void powerOffServerCallsHetznerAndTracksTheAction() throws IOException, InterruptedException, IllegalAccessException {
+ HetznerCloud hetznerCloud = mock(HetznerCloud.class);
+ HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+ ServiceManager serviceManager = mock(ServiceManager.class);
+ ActionService actionService = mock(ActionService.class);
+
+ try (MockedStatic hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
+ MockedStatic hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
+
+ Action action = new Action();
+ action.setFinished(Date.from(Instant.now()).toString());
+
+ hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
+ hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
+ when(hetznerCloud.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
+ when(serviceManager.getActionService()).thenReturn(actionService);
+ when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.completedFuture(action));
+
+ when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString(), anyString())).thenReturn(new ActionWrapper(action));
+
+ ServerService serverService = new ServerService(serviceManager);
+ serverService.powerOffServer(new Server());
+
+ verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), eq(RequestVerb.POST), eq("key1234"), eq(""));
+ verify(actionService, times(1)).waitForActionToComplete(any(Action.class));
+ }
+ }
+
+ @Test
+ @DisplayName("PowerOn Server calls Hetzner and tracks the action")
+ void powerOnServerCallsHetznerAndTracksTheAction() throws IOException, InterruptedException, IllegalAccessException {
+ HetznerCloud hetznerCloud = mock(HetznerCloud.class);
+ HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+ ServiceManager serviceManager = mock(ServiceManager.class);
+ ActionService actionService = mock(ActionService.class);
+
+ try (MockedStatic hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
+ MockedStatic hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
+
+ Action action = new Action();
+ action.setFinished(Date.from(Instant.now()).toString());
+
+ hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
+ hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
+ when(hetznerCloud.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
+ when(serviceManager.getActionService()).thenReturn(actionService);
+ when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.completedFuture(action));
+
+ when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString(), anyString())).thenReturn(new ActionWrapper(action));
+
+ ServerService serverService = new ServerService(serviceManager);
+ serverService.powerOnServer(new Server());
+
+ verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), eq(RequestVerb.POST), eq("key1234"), eq(""));
+ verify(actionService, times(1)).waitForActionToComplete(any(Action.class));
+ }
+ }
+
+ @Test
+ @DisplayName("Reboot Server calls Hetzner and tracks the action")
+ void RebootServerCallsHetznerAndTracksTheAction() throws IOException, InterruptedException, IllegalAccessException {
+ HetznerCloud hetznerCloud = mock(HetznerCloud.class);
+ HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+ ServiceManager serviceManager = mock(ServiceManager.class);
+ ActionService actionService = mock(ActionService.class);
+
+ try (MockedStatic hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
+ MockedStatic hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
+
+ Action action = new Action();
+ action.setFinished(Date.from(Instant.now()).toString());
+
+ hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
+ hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
+ when(hetznerCloud.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
+ when(serviceManager.getActionService()).thenReturn(actionService);
+ when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.completedFuture(action));
+
+ when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString(), anyString())).thenReturn(new ActionWrapper(action));
+
+ ServerService serverService = new ServerService(serviceManager);
+ serverService.rebootServer(new Server());
+
+ verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), eq(RequestVerb.POST), eq("key1234"), eq(""));
+ verify(actionService, times(1)).waitForActionToComplete(any(Action.class));
+ }
+ }
+
+ @Test
+ @DisplayName("Reset Server calls Hetzner and tracks the action")
+ void ResetServerCallsHetznerAndTracksTheAction() throws IOException, InterruptedException, IllegalAccessException {
+ HetznerCloud hetznerCloud = mock(HetznerCloud.class);
+ HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
+ ListenerManager listenerManager = mock(ListenerManager.class);
+ ServiceManager serviceManager = mock(ServiceManager.class);
+ ActionService actionService = mock(ActionService.class);
+
+ try (MockedStatic hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
+ MockedStatic hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
+
+ Action action = new Action();
+ action.setFinished(Date.from(Instant.now()).toString());
+
+ hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
+ hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
+ when(hetznerCloud.getListenerManager()).thenReturn(listenerManager);
+ when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
+ when(serviceManager.getActionService()).thenReturn(actionService);
+ when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.completedFuture(action));
+
+ when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString(), anyString())).thenReturn(new ActionWrapper(action));
+
+ ServerService serverService = new ServerService(serviceManager);
+ serverService.resetServer(new Server());
+
+ verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), eq(RequestVerb.POST), eq("key1234"), eq(""));
+ verify(actionService, times(1)).waitForActionToComplete(any(Action.class));
+ }
+ }
}
\ No newline at end of file