Skip to content

Commit c756a1f

Browse files
committed
chore: Add supporting tests for Action Service
- Fixed checking the wrong action for null - Move getting the HTTP details to outside the completable future
1 parent 956e2a2 commit c756a1f

File tree

5 files changed

+229
-7
lines changed

5 files changed

+229
-7
lines changed

src/main/java/dev/tomr/hcloud/service/action/ActionService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ public ActionService() {
2323
}
2424

2525
public CompletableFuture<Action> waitForActionToComplete(Action action) {
26+
List<String> hostAndKey = HetznerCloud.getInstance().getHttpDetails();
2627
return CompletableFuture.supplyAsync(() -> {
2728
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
28-
List<String> hostAndKey = HetznerCloud.getInstance().getHttpDetails();
2929
List<Future<Action>> futures = new ArrayList<>();
3030
AtomicReference<Action> completedAction = new AtomicReference<>();
3131
futures.add(scheduler.schedule(createCheckCallable(action, hostAndKey), 0, TimeUnit.MILLISECONDS));

src/main/java/dev/tomr/hcloud/service/server/ServerService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public void deleteServerFromHetzner(Server server) {
107107
try {
108108
Action action = client.sendHttpRequest(ActionWrapper.class, httpUrl, DELETE, hostAndKey.get(1)).getAction();
109109
CompletableFuture<Action> completedActionFuture = serviceManager.getActionService().waitForActionToComplete(action).thenApplyAsync((completedAction) -> {
110-
if (action == null) {
110+
if (completedAction == null) {
111111
throw new NullPointerException();
112112
}
113113
logger.info("Server confirmed deleted at {}", completedAction.getFinished());
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package dev.tomr.hcloud.http.model;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
6+
import static org.junit.jupiter.api.Assertions.assertEquals;
7+
8+
public class ActionWrapperTest {
9+
10+
@Test
11+
@DisplayName("calling setAction set's the action")
12+
void setAction() {
13+
ActionWrapper actionWrapper = new ActionWrapper();
14+
Action action = new Action();
15+
action.setId(1);
16+
actionWrapper.setAction(action);
17+
assertEquals(action, actionWrapper.getAction());
18+
}
19+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,176 @@
11
package dev.tomr.hcloud.service.action;
22

3+
import dev.tomr.hcloud.HetznerCloud;
4+
import dev.tomr.hcloud.http.HetznerCloudHttpClient;
5+
import dev.tomr.hcloud.http.RequestVerb;
6+
import dev.tomr.hcloud.http.model.Action;
7+
import dev.tomr.hcloud.http.model.ActionWrapper;
8+
import dev.tomr.hcloud.http.model.Error;
9+
import org.junit.jupiter.api.DisplayName;
10+
import org.junit.jupiter.api.Test;
11+
import org.mockito.MockedStatic;
12+
13+
import java.io.IOException;
14+
import java.util.Date;
15+
import java.util.List;
16+
import java.util.concurrent.CompletableFuture;
17+
18+
import static org.junit.jupiter.api.Assertions.*;
19+
import static org.mockito.Mockito.*;
20+
321
public class ActionServiceTest {
22+
23+
@Test
24+
@DisplayName("Wait for action to complete returns a CompletableFuture with the finished action when the progress is 100 and finished has a date string")
25+
void waitForActionToCompleteReturnsWhenProgressIs100() throws IOException, InterruptedException, IllegalAccessException {
26+
HetznerCloud hetznerCloud = mock(HetznerCloud.class);
27+
HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
28+
29+
try (MockedStatic<HetznerCloud> hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
30+
MockedStatic<HetznerCloudHttpClient> hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
31+
32+
hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
33+
hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
34+
35+
when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
36+
37+
Action returnedAction = new Action();
38+
returnedAction.setProgress(100);
39+
returnedAction.setFinished(new Date().toString());
40+
returnedAction.setId(1);
41+
returnedAction.setCommand("delete_resource");
42+
43+
Action originalAction = new Action();
44+
originalAction.setId(1);
45+
originalAction.setCommand("delete_resource");
46+
47+
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(new ActionWrapper(returnedAction));
48+
49+
ActionService actionService = new ActionService();
50+
51+
CompletableFuture<Action> actionCompletableFuture = actionService.waitForActionToComplete(originalAction);
52+
53+
assertEquals(1, actionCompletableFuture.join().getId());
54+
}
55+
}
56+
57+
@Test
58+
@DisplayName("Wait for action to complete returns a CompletableFuture with the finished action after trying again when the action is not finished the first time")
59+
void waitForActionToCompleteReturnsACompletableFutureWithTheFinishedActionWithRetries() throws IOException, InterruptedException, IllegalAccessException {
60+
HetznerCloud hetznerCloud = mock(HetznerCloud.class);
61+
HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
62+
63+
try (MockedStatic<HetznerCloud> hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
64+
MockedStatic<HetznerCloudHttpClient> hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
65+
66+
hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
67+
hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
68+
69+
when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
70+
71+
Action returnedAction = new Action();
72+
returnedAction.setProgress(100);
73+
returnedAction.setFinished(new Date().toString());
74+
returnedAction.setId(1);
75+
returnedAction.setCommand("delete_resource");
76+
77+
Action unfinishedAction = new Action();
78+
unfinishedAction.setProgress(50);
79+
unfinishedAction.setId(1);
80+
unfinishedAction.setCommand("delete_resource");
81+
82+
Action originalAction = new Action();
83+
originalAction.setId(1);
84+
originalAction.setCommand("delete_resource");
85+
86+
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(
87+
new ActionWrapper(unfinishedAction),
88+
new ActionWrapper(unfinishedAction),
89+
new ActionWrapper(returnedAction));
90+
91+
ActionService actionService = new ActionService();
92+
93+
CompletableFuture<Action> actionCompletableFuture = actionService.waitForActionToComplete(originalAction);
94+
95+
assertEquals(1, actionCompletableFuture.join().getId());
96+
verify(hetznerCloudHttpClient, times(3)).sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString());
97+
}
98+
}
99+
100+
@Test
101+
@DisplayName("Wait for action to complete returns a CompletableFuture with the finished action after the first http request is 100 on progress, but not 'finished', the second is")
102+
void waitForActionToCompleteReturnsACompletableFutureWithTheFinishedActionWithABadReturnFirstTryGoodSecond() throws IOException, InterruptedException, IllegalAccessException {
103+
HetznerCloud hetznerCloud = mock(HetznerCloud.class);
104+
HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
105+
106+
try (MockedStatic<HetznerCloud> hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
107+
MockedStatic<HetznerCloudHttpClient> hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
108+
109+
hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
110+
hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
111+
112+
when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
113+
114+
Action returnedAction = new Action();
115+
returnedAction.setProgress(100);
116+
returnedAction.setFinished(new Date().toString());
117+
returnedAction.setId(1);
118+
returnedAction.setCommand("delete_resource");
119+
120+
Action unfinishedAction = new Action();
121+
unfinishedAction.setProgress(100);
122+
unfinishedAction.setId(1);
123+
unfinishedAction.setCommand("delete_resource");
124+
125+
Action originalAction = new Action();
126+
originalAction.setId(1);
127+
originalAction.setCommand("delete_resource");
128+
129+
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(
130+
new ActionWrapper(unfinishedAction),
131+
new ActionWrapper(returnedAction));
132+
133+
ActionService actionService = new ActionService();
134+
135+
CompletableFuture<Action> actionCompletableFuture = actionService.waitForActionToComplete(originalAction);
136+
137+
assertEquals(1, actionCompletableFuture.join().getId());
138+
verify(hetznerCloudHttpClient, times(2)).sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString());
139+
}
140+
}
141+
142+
@Test
143+
@DisplayName("Wait for action to complete throws a Runtime exception when the Hetzner Action has the error field present")
144+
void waitForActionToCompleteThrowsRuntimeWhenHetznerReturnsAnError() throws IOException, InterruptedException, IllegalAccessException {
145+
HetznerCloud hetznerCloud = mock(HetznerCloud.class);
146+
HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
147+
148+
try (MockedStatic<HetznerCloud> hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
149+
MockedStatic<HetznerCloudHttpClient> hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
150+
151+
hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
152+
hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
153+
154+
when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
155+
156+
157+
Action errorAction = new Action();
158+
errorAction.setId(1);
159+
errorAction.setCommand("delete_resource");
160+
errorAction.setError(new Error("HETZNER_01", "This is the error from Hetzner"));
161+
162+
Action originalAction = new Action();
163+
originalAction.setId(1);
164+
originalAction.setCommand("delete_resource");
165+
166+
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(new ActionWrapper(errorAction));
167+
168+
ActionService actionService = new ActionService();
169+
170+
RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> actionService.waitForActionToComplete(originalAction).join());
171+
172+
assertTrue(runtimeException.getMessage().contains("HETZNER_01"));
173+
assertTrue(runtimeException.getMessage().contains("This is the error from Hetzner"));
174+
}
175+
}
4176
}

src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import dev.tomr.hcloud.http.HetznerCloudHttpClient;
55
import dev.tomr.hcloud.http.RequestVerb;
66
import dev.tomr.hcloud.http.model.Action;
7+
import dev.tomr.hcloud.http.model.ActionWrapper;
78
import dev.tomr.hcloud.http.model.ServerDTO;
89
import dev.tomr.hcloud.http.model.ServerDTOList;
910
import dev.tomr.hcloud.listener.ListenerManager;
@@ -15,13 +16,10 @@
1516
import org.junit.jupiter.api.BeforeEach;
1617
import org.junit.jupiter.api.DisplayName;
1718
import org.junit.jupiter.api.Test;
18-
import org.mockito.ArgumentCaptor;
1919
import org.mockito.MockedStatic;
2020
import org.mockito.Mockito;
2121

2222
import java.io.IOException;
23-
import java.lang.reflect.Field;
24-
import java.time.Clock;
2523
import java.time.Instant;
2624
import java.util.Date;
2725
import java.util.List;
@@ -568,7 +566,7 @@ void deleteServerFromHetznerAndTracksTheAction() throws IOException, Interrupted
568566
when(serviceManager.getActionService()).thenReturn(actionService);
569567
when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.completedFuture(action));
570568

571-
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(new Action());
569+
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(new ActionWrapper(action));
572570

573571
ServerService serverService = new ServerService(serviceManager);
574572
serverService.deleteServerFromHetzner(new Server());
@@ -598,7 +596,7 @@ void whenActionServiceThrowsDeleteServerAlsoThrows() throws IOException, Interru
598596

599597
when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.failedFuture(new RuntimeException(new TimeoutException())));
600598

601-
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(new Action());
599+
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(new ActionWrapper(new Action()));
602600

603601
ServerService serverService = new ServerService(serviceManager);
604602

@@ -611,6 +609,39 @@ void whenActionServiceThrowsDeleteServerAlsoThrows() throws IOException, Interru
611609
}
612610
}
613611

612+
@Test
613+
@DisplayName("When Action returned from Hetzner is Null, server service throws a null pointer exception")
614+
void whenActionReturnedFromHetznerIsNullServerServiceThrowsANullPointer() throws IOException, InterruptedException, IllegalAccessException {
615+
HetznerCloud hetznerCloud = mock(HetznerCloud.class);
616+
HetznerCloudHttpClient hetznerCloudHttpClient = mock(HetznerCloudHttpClient.class);
617+
ListenerManager listenerManager = mock(ListenerManager.class);
618+
ServiceManager serviceManager = mock(ServiceManager.class);
619+
ActionService actionService = mock(ActionService.class);
620+
621+
try (MockedStatic<HetznerCloud> hetznerCloudMockedStatic = mockStatic(HetznerCloud.class);
622+
MockedStatic<HetznerCloudHttpClient> hetznerCloudHttpClientMockedStatic = mockStatic(HetznerCloudHttpClient.class)) {
623+
624+
hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient);
625+
hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud);
626+
when(hetznerCloud.getListenerManager()).thenReturn(listenerManager);
627+
when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234"));
628+
when(serviceManager.getActionService()).thenReturn(actionService);
629+
630+
when(actionService.waitForActionToComplete(any(Action.class))).thenReturn(CompletableFuture.completedFuture(null));
631+
632+
when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(new ActionWrapper(new Action()));
633+
634+
ServerService serverService = new ServerService(serviceManager);
635+
636+
RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> serverService.deleteServerFromHetzner(new Server()));
637+
638+
verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), eq(RequestVerb.DELETE), eq("key1234"));
639+
verify(actionService, times(1)).waitForActionToComplete(any(Action.class));
640+
641+
assertTrue(runtimeException.getMessage().contains("NullPointerException"));
642+
}
643+
}
644+
614645
@Test
615646
@DisplayName("When httpclient throws, then delete Server From Hetzner also throws a Runtime exception")
616647
void deleteServerFromHetznerHandlesException() throws IOException, InterruptedException, IllegalAccessException {

0 commit comments

Comments
 (0)