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