From 73ee65f5efeb2030f2144e4b724d4b5e4ee2b053 Mon Sep 17 00:00:00 2001 From: Tom Roman Date: Sun, 10 Nov 2024 01:10:34 +0000 Subject: [PATCH 1/4] feat: start work on auto caching and accessing servers --- .../java/dev/tomr/hcloud/HetznerCloud.java | 26 ++++++++- .../tomr/hcloud/http/model/ServerDTOList.java | 37 +++++++++++++ .../hcloud/resources/common/CreatedFrom.java | 6 +- .../hcloud/resources/common/Datacenter.java | 3 + .../hcloud/resources/common/Deprecation.java | 3 + .../tomr/hcloud/resources/common/Image.java | 3 + .../dev/tomr/hcloud/resources/common/Iso.java | 3 + .../hcloud/resources/common/Location.java | 3 + .../resources/common/PlacementGroup.java | 3 + .../tomr/hcloud/resources/common/Price.java | 10 ++-- .../hcloud/resources/common/PriceDetails.java | 2 + .../hcloud/resources/common/Protection.java | 2 + .../hcloud/resources/common/ServerType.java | 2 + .../hcloud/resources/common/ServerTypes.java | 2 + .../tomr/hcloud/service/ServiceManager.java | 2 +- .../hcloud/service/server/ServerService.java | 55 +++++++++++++++++-- .../converter/ServerConverterUtilTest.java | 2 +- .../http/model/ServerDTOBuilderTest.java | 2 +- .../tomr/hcloud/http/model/ServerDTOTest.java | 2 +- .../hcloud/resources/common/PriceTest.java | 4 +- .../resources/common/ServerTypeTest.java | 2 +- .../hcloud/resources/server/ServerTest.java | 2 +- .../service/server/ServerServiceTest.java | 2 +- 23 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 src/main/java/dev/tomr/hcloud/http/model/ServerDTOList.java diff --git a/src/main/java/dev/tomr/hcloud/HetznerCloud.java b/src/main/java/dev/tomr/hcloud/HetznerCloud.java index 41e7f5a..93ed3a5 100644 --- a/src/main/java/dev/tomr/hcloud/HetznerCloud.java +++ b/src/main/java/dev/tomr/hcloud/HetznerCloud.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import dev.tomr.hcloud.listener.ListenerManager; +import dev.tomr.hcloud.resources.server.Server; import dev.tomr.hcloud.service.ServiceManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -14,10 +15,10 @@ public class HetznerCloud { protected static final Logger logger = LogManager.getLogger(); - private static final ListenerManager listenerManager = ListenerManager.getInstance(); - private static final ServiceManager serviceManager = ServiceManager.getInstance(); + private static ListenerManager listenerManager = ListenerManager.getInstance(); + private static ServiceManager serviceManager = ServiceManager.getInstance(); private static final ObjectMapper objectMapper = new ObjectMapper(); - private static String HETZNER_CLOUD_HOST = "https://api.hetzner.cloud/v1/"; + private static final String HETZNER_CLOUD_HOST = "https://api.hetzner.cloud/v1/"; private static HetznerCloud instance; @@ -32,6 +33,7 @@ public HetznerCloud(String apiKey) { this.apiKey = apiKey; this.host = HETZNER_CLOUD_HOST; instance = this; + ServiceManager.getInstance().getServerService().forceRefreshServersCache(); } /** @@ -43,6 +45,7 @@ public HetznerCloud(String apiKey, String host) { this.apiKey = apiKey; this.host = host; instance = this; + ServiceManager.getInstance().getServerService().forceRefreshServersCache(); } /** @@ -96,4 +99,21 @@ public static ServiceManager getServiceManager() { public List getHttpDetails() { return List.of(host, apiKey); } + + /** + * Whether we have an API Key supplied + * @return true if one is present, false if not + */ + public boolean hasApiKey() { + return apiKey != null && !apiKey.isEmpty(); + } + + /** + * Get a Hetzner Server Instance from the local cache + * @param id ID of the server + * @return A server object from the local cache + */ + public Server getServer(Integer id) { + return serviceManager.getServerService().getServer(id); + } } diff --git a/src/main/java/dev/tomr/hcloud/http/model/ServerDTOList.java b/src/main/java/dev/tomr/hcloud/http/model/ServerDTOList.java new file mode 100644 index 0000000..7a370ca --- /dev/null +++ b/src/main/java/dev/tomr/hcloud/http/model/ServerDTOList.java @@ -0,0 +1,37 @@ +package dev.tomr.hcloud.http.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import dev.tomr.hcloud.http.HetznerJsonObject; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ServerDTOList extends HetznerJsonObject { + private Object meta; + private List servers; + + public ServerDTOList() {} + + public ServerDTOList(Object meta, List servers) { + this.meta = meta; + this.servers = servers; + } + + public Object getMeta() { + return meta; + } + + public List getServers() { + return servers; + } + + public void setMeta(Object meta) { + this.meta = meta; + } + + public void setServers(List servers) { + this.servers = servers; + } +} diff --git a/src/main/java/dev/tomr/hcloud/resources/common/CreatedFrom.java b/src/main/java/dev/tomr/hcloud/resources/common/CreatedFrom.java index c1dac2f..c73693b 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/CreatedFrom.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/CreatedFrom.java @@ -9,13 +9,15 @@ public CreatedFrom(int id, String name) { this.name = name; } + public CreatedFrom() { + } + public int getId() { return id; } - public CreatedFrom setId(int id) { + public void setId(int id) { this.id = id; - return this; } public String getName() { diff --git a/src/main/java/dev/tomr/hcloud/resources/common/Datacenter.java b/src/main/java/dev/tomr/hcloud/resources/common/Datacenter.java index 6e1b615..191ba54 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/Datacenter.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/Datacenter.java @@ -18,6 +18,9 @@ public Datacenter(String description, Integer id, Location location, String name this.serverTypes = serverTypes; } + public Datacenter() { + } + public String getDescription() { return description; } diff --git a/src/main/java/dev/tomr/hcloud/resources/common/Deprecation.java b/src/main/java/dev/tomr/hcloud/resources/common/Deprecation.java index 6c03419..d82f08b 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/Deprecation.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/Deprecation.java @@ -14,6 +14,9 @@ public Deprecation(Date announced, Date unavailableAfter) { this.unavailableAfter = unavailableAfter; } + public Deprecation() { + } + public Date getAnnounced() { return announced; } diff --git a/src/main/java/dev/tomr/hcloud/resources/common/Image.java b/src/main/java/dev/tomr/hcloud/resources/common/Image.java index c3bc8a7..e9c5c65 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/Image.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/Image.java @@ -53,6 +53,9 @@ public Image(String architecture, Object boundTo, Date created, CreatedFrom crea this.type = type; } + public Image() { + } + public String getArchitecture() { return architecture; } diff --git a/src/main/java/dev/tomr/hcloud/resources/common/Iso.java b/src/main/java/dev/tomr/hcloud/resources/common/Iso.java index 8f1ae1d..5d40f2b 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/Iso.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/Iso.java @@ -20,6 +20,9 @@ public Iso(String architecture, Map deprecation, String descriptio this.type = type; } + public Iso() { + } + public String getArchitecture() { return architecture; } diff --git a/src/main/java/dev/tomr/hcloud/resources/common/Location.java b/src/main/java/dev/tomr/hcloud/resources/common/Location.java index 94a5b6d..0380f05 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/Location.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/Location.java @@ -24,6 +24,9 @@ public Location(String city, String country, String description, Integer id, dou this.networkZone = networkZone; } + public Location() { + } + public String getCity() { return city; } diff --git a/src/main/java/dev/tomr/hcloud/resources/common/PlacementGroup.java b/src/main/java/dev/tomr/hcloud/resources/common/PlacementGroup.java index 9a1b612..7b96344 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/PlacementGroup.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/PlacementGroup.java @@ -21,6 +21,9 @@ public PlacementGroup(Date created, int id, Map labels, String n this.type = type; } + public PlacementGroup() { + } + public Date getCreated() { return created; } diff --git a/src/main/java/dev/tomr/hcloud/resources/common/Price.java b/src/main/java/dev/tomr/hcloud/resources/common/Price.java index 72af6b9..71e0790 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/Price.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/Price.java @@ -4,7 +4,7 @@ public class Price{ @JsonProperty("included_traffic") - private Integer includedTraffic; + private Long includedTraffic; private String location; @JsonProperty("price_hourly") private PriceDetails priceHourly; @@ -13,7 +13,7 @@ public class Price{ @JsonProperty("price_per_tb_traffic") private PriceDetails pricePerTbTraffic; - public Price(Integer includedTraffic, String location, PriceDetails priceHourly, PriceDetails priceMonthly, PriceDetails pricePerTbTraffic) { + public Price(Long includedTraffic, String location, PriceDetails priceHourly, PriceDetails priceMonthly, PriceDetails pricePerTbTraffic) { this.includedTraffic = includedTraffic; this.location = location; this.priceHourly = priceHourly; @@ -21,11 +21,13 @@ public Price(Integer includedTraffic, String location, PriceDetails priceHourly, this.pricePerTbTraffic = pricePerTbTraffic; } - public Integer getIncludedTraffic() { + public Price() {} + + public Long getIncludedTraffic() { return includedTraffic; } - public void setIncludedTraffic(Integer includedTraffic) { + public void setIncludedTraffic(Long includedTraffic) { this.includedTraffic = includedTraffic; } diff --git a/src/main/java/dev/tomr/hcloud/resources/common/PriceDetails.java b/src/main/java/dev/tomr/hcloud/resources/common/PriceDetails.java index 2fa4e03..afe09f3 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/PriceDetails.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/PriceDetails.java @@ -9,6 +9,8 @@ public PriceDetails(String gross, String net) { this.net = net; } + public PriceDetails() {} + public String getGross() { return gross; } diff --git a/src/main/java/dev/tomr/hcloud/resources/common/Protection.java b/src/main/java/dev/tomr/hcloud/resources/common/Protection.java index 739a5f0..54ae08d 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/Protection.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/Protection.java @@ -12,6 +12,8 @@ public Protection(boolean delete, boolean rebuild) { this.rebuild = rebuild; } + public Protection() {} + public boolean isDelete() { return delete; } diff --git a/src/main/java/dev/tomr/hcloud/resources/common/ServerType.java b/src/main/java/dev/tomr/hcloud/resources/common/ServerType.java index 7c73f03..231e721 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/ServerType.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/ServerType.java @@ -38,6 +38,8 @@ public ServerType(String architecture, Integer cores, String cpuType, boolean de this.storageType = storageType; } + public ServerType() {} + public String getArchitecture() { return architecture; } diff --git a/src/main/java/dev/tomr/hcloud/resources/common/ServerTypes.java b/src/main/java/dev/tomr/hcloud/resources/common/ServerTypes.java index a97cbec..4b9a605 100644 --- a/src/main/java/dev/tomr/hcloud/resources/common/ServerTypes.java +++ b/src/main/java/dev/tomr/hcloud/resources/common/ServerTypes.java @@ -16,6 +16,8 @@ public ServerTypes(List available, List availableForMigration, this.supported = supported; } + public ServerTypes() {} + public List getAvailable() { return available; } diff --git a/src/main/java/dev/tomr/hcloud/service/ServiceManager.java b/src/main/java/dev/tomr/hcloud/service/ServiceManager.java index bbae807..8ebf057 100644 --- a/src/main/java/dev/tomr/hcloud/service/ServiceManager.java +++ b/src/main/java/dev/tomr/hcloud/service/ServiceManager.java @@ -13,8 +13,8 @@ public class ServiceManager { private ExecutorService executor; private ServiceManager() { - this.serverService = new ServerService(); instance = this; + this.serverService = new ServerService(this); } /** 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 1bda750..b34d52b 100644 --- a/src/main/java/dev/tomr/hcloud/service/server/ServerService.java +++ b/src/main/java/dev/tomr/hcloud/service/server/ServerService.java @@ -4,36 +4,55 @@ import dev.tomr.hcloud.http.HetznerCloudHttpClient; import dev.tomr.hcloud.http.converter.ServerConverterUtil; import dev.tomr.hcloud.http.model.ServerDTO; +import dev.tomr.hcloud.http.model.ServerDTOList; import dev.tomr.hcloud.resources.server.Server; import dev.tomr.hcloud.service.ServiceManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.IOException; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import static dev.tomr.hcloud.http.RequestVerb.GET; import static dev.tomr.hcloud.http.RequestVerb.PUT; public class ServerService { protected static final Logger logger = LogManager.getLogger(); - private final HetznerCloudHttpClient client; + private final HetznerCloudHttpClient client = HetznerCloudHttpClient.getInstance(); private final ServiceManager serviceManager; private final ConcurrentHashMap updatedFields = new ConcurrentHashMap<>(); private Server updatedServer; private CompletableFuture updatedServerFuture; + private Map remoteServers = new HashMap<>(); + /** * Creates a new {@code ServerService} instance */ public ServerService() { - client = HetznerCloudHttpClient.getInstance(); - serviceManager = HetznerCloud.getServiceManager(); + this.serviceManager = HetznerCloud.getServiceManager(); + } + + public ServerService(ServiceManager serviceManager) { + this.serviceManager = serviceManager; + } + + public void forceRefreshServersCache() { + if (HetznerCloud.getInstance().hasApiKey()) { + updateAllRemoteServers(); + } else { + logger.warn("No API Key supplied, not refreshing servers. Consider refreshing again, when you have a key!"); + } } /** @@ -77,6 +96,34 @@ public void cancelServerNameOrLabelUpdate() { updatedFields.clear(); } + private void updateAllRemoteServers() { + Map newServerMap = new HashMap<>(); + List hostAndKey = HetznerCloud.getInstance().getHttpDetails(); + String httpUrl = String.format("%sservers", hostAndKey.get(0)); + ServerDTOList serverDTOList = null; + try { + serverDTOList = client.sendHttpRequest(ServerDTOList.class, httpUrl, GET, hostAndKey.get(1)); + } catch (IOException | InterruptedException | IllegalAccessException e) { + logger.error("Failed to refresh all remote servers!"); + throw new RuntimeException(e); + } + serverDTOList.getServers().forEach(serverDTO -> { + newServerMap.put(Date.from(Instant.now()), ServerConverterUtil.transformServerDTOToServer(serverDTO)); + logger.info(serverDTO.getId()); + }); + remoteServers = newServerMap; + } + + public Server getServer(Integer id) { + for (var entry : remoteServers.entrySet()) { + if (entry.getValue().getId().equals(id)) { + return entry.getValue(); + } + } + logger.warn("Server with id {} not found", id); + return null; + } + private CompletableFuture scheduleHttpRequest(String host, String apiKey) { return CompletableFuture.runAsync(() -> { try { @@ -94,7 +141,7 @@ private CompletableFuture scheduleHttpRequest(String host, String apiKey) removeUnchangedFields(serverDTO); info = ref.get(); logger.info(info); - String endpoint = host + "server/" + updatedServer.getId(); + String endpoint = host + "servers/" + updatedServer.getId(); client.sendHttpRequest(ServerDTO.class, endpoint, PUT, apiKey, HetznerCloud.getObjectMapper().writeValueAsString(serverDTO)); } else { throw new RuntimeException("No updated values??"); diff --git a/src/test/java/dev/tomr/hcloud/http/converter/ServerConverterUtilTest.java b/src/test/java/dev/tomr/hcloud/http/converter/ServerConverterUtilTest.java index 5c91922..3d95ca0 100644 --- a/src/test/java/dev/tomr/hcloud/http/converter/ServerConverterUtilTest.java +++ b/src/test/java/dev/tomr/hcloud/http/converter/ServerConverterUtilTest.java @@ -27,7 +27,7 @@ public class ServerConverterUtilTest { null, 2048, "", - List.of(new Price(0, "london", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))), + List.of(new Price(0L, "london", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))), "" ); private Iso ISO = new Iso("", Map.of(), "", 1, "", ""); diff --git a/src/test/java/dev/tomr/hcloud/http/model/ServerDTOBuilderTest.java b/src/test/java/dev/tomr/hcloud/http/model/ServerDTOBuilderTest.java index 8d13bde..088057f 100644 --- a/src/test/java/dev/tomr/hcloud/http/model/ServerDTOBuilderTest.java +++ b/src/test/java/dev/tomr/hcloud/http/model/ServerDTOBuilderTest.java @@ -28,7 +28,7 @@ void serverDTOBuilderCreatesAServerDTOCorrectly() { null, 2048, "", - List.of(new Price(0, "london", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))), + List.of(new Price(0L, "london", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))), "" ); Iso ISO = new Iso("", Map.of(), "", 1, "", ""); diff --git a/src/test/java/dev/tomr/hcloud/http/model/ServerDTOTest.java b/src/test/java/dev/tomr/hcloud/http/model/ServerDTOTest.java index 1082c87..b2c04a3 100644 --- a/src/test/java/dev/tomr/hcloud/http/model/ServerDTOTest.java +++ b/src/test/java/dev/tomr/hcloud/http/model/ServerDTOTest.java @@ -174,7 +174,7 @@ void setServerType() { null, 2048, "", - List.of(new Price(0, "london", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))), + List.of(new Price(0L, "london", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))), "" ); serverDTO.setServerType(serverType); diff --git a/src/test/java/dev/tomr/hcloud/resources/common/PriceTest.java b/src/test/java/dev/tomr/hcloud/resources/common/PriceTest.java index f82a9b6..d96c270 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/PriceTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/PriceTest.java @@ -11,12 +11,12 @@ class PriceTest { @BeforeEach void setUp() { - price = new Price(0, null, null, null, null); + price = new Price(00L, null, null, null, null); } @Test void setIncludedTraffic() { - price.setIncludedTraffic(1); + price.setIncludedTraffic(1L); assertEquals(1, price.getIncludedTraffic()); } diff --git a/src/test/java/dev/tomr/hcloud/resources/common/ServerTypeTest.java b/src/test/java/dev/tomr/hcloud/resources/common/ServerTypeTest.java index 127801c..370eff8 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/ServerTypeTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/ServerTypeTest.java @@ -88,7 +88,7 @@ void setName() { @Test void setPrices() { - List prices = List.of(new Price(0, "", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))); + List prices = List.of(new Price(0L, "", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))); serverType.setPrices(prices); assertEquals(prices, serverType.getPrices()); } 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 b684eb2..0239de0 100644 --- a/src/test/java/dev/tomr/hcloud/resources/server/ServerTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/server/ServerTest.java @@ -38,7 +38,7 @@ void serverCanBeInstantiatedAndValuesSetCorrectly() { null, 2048, "", - List.of(new Price(0, "london", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))), + List.of(new Price(0L, "london", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))), "" ); Iso ISO = new Iso("", Map.of(), "", 1, "", ""); 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 a5668b2..da6d98e 100644 --- a/src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java +++ b/src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java @@ -36,7 +36,7 @@ class ServerServiceTest { null, 2048, "", - List.of(new Price(0, "london", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))), + List.of(new Price(0L, "london", new PriceDetails("", ""), new PriceDetails("", ""), new PriceDetails("", ""))), "" ); private Iso ISO = new Iso("", Map.of(), "", 1, "", ""); From d3a177e793f6cccf180084c23fcc861b41d28fd5 Mon Sep 17 00:00:00 2001 From: Tom Roman Date: Wed, 13 Nov 2024 20:54:45 +0000 Subject: [PATCH 2/4] chore: Add missing default constructors and tests --- .../dev/tomr/hcloud/resources/common/CreatedFromTest.java | 8 +++++++- .../dev/tomr/hcloud/resources/common/DatacenterTest.java | 7 +++++++ .../dev/tomr/hcloud/resources/common/DeprecationTest.java | 6 ++++++ .../java/dev/tomr/hcloud/resources/common/ImageTest.java | 6 ++++++ .../java/dev/tomr/hcloud/resources/common/IsoTest.java | 6 ++++++ .../dev/tomr/hcloud/resources/common/LocationTest.java | 6 ++++++ .../tomr/hcloud/resources/common/PlacementGroupTest.java | 6 ++++++ .../tomr/hcloud/resources/common/PriceDetailsTest.java | 6 ++++++ .../java/dev/tomr/hcloud/resources/common/PriceTest.java | 6 ++++++ .../dev/tomr/hcloud/resources/common/ProtectionTest.java | 6 ++++++ .../dev/tomr/hcloud/resources/common/ServerTypeTest.java | 6 ++++++ .../dev/tomr/hcloud/resources/common/ServerTypesTest.java | 6 ++++++ 12 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/test/java/dev/tomr/hcloud/resources/common/CreatedFromTest.java b/src/test/java/dev/tomr/hcloud/resources/common/CreatedFromTest.java index bd9f73c..d33c145 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/CreatedFromTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/CreatedFromTest.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; public class CreatedFromTest { @@ -25,4 +25,10 @@ void setName() { createdFrom.setName("test"); assertEquals("test", createdFrom.getName()); } + + @Test + void defaultConstructor() { + createdFrom = new CreatedFrom(); + assertNotNull(createdFrom); + } } diff --git a/src/test/java/dev/tomr/hcloud/resources/common/DatacenterTest.java b/src/test/java/dev/tomr/hcloud/resources/common/DatacenterTest.java index 2d7da26..1afa94d 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/DatacenterTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/DatacenterTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Date; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -47,4 +48,10 @@ void setServerTypes() { datacenter.setServerTypes(serverTypes); assertEquals(serverTypes, datacenter.getServerTypes()); } + + @Test + void defaultConstructor() { + Datacenter datacenter = new Datacenter(); + assertNotNull(datacenter); + } } \ No newline at end of file diff --git a/src/test/java/dev/tomr/hcloud/resources/common/DeprecationTest.java b/src/test/java/dev/tomr/hcloud/resources/common/DeprecationTest.java index ae6feaf..06cf53e 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/DeprecationTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/DeprecationTest.java @@ -30,4 +30,10 @@ void setUnavailableAfter() { deprecation.setUnavailableAfter(date); assertEquals(date, deprecation.getUnavailableAfter()); } + + @Test + void defaultConstructor() { + Deprecation deprecation = new Deprecation(); + assertNotNull(deprecation); + } } \ No newline at end of file diff --git a/src/test/java/dev/tomr/hcloud/resources/common/ImageTest.java b/src/test/java/dev/tomr/hcloud/resources/common/ImageTest.java index 6735f56..f0eea69 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/ImageTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/ImageTest.java @@ -129,4 +129,10 @@ void setType() { image.setType("type"); assertEquals("type", image.getType()); } + + @Test + void defaultConstructor() { + Image image = new Image(); + assertNotNull(image); + } } \ No newline at end of file diff --git a/src/test/java/dev/tomr/hcloud/resources/common/IsoTest.java b/src/test/java/dev/tomr/hcloud/resources/common/IsoTest.java index cbdeb56..8a316be 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/IsoTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/IsoTest.java @@ -54,4 +54,10 @@ void setType() { iso.setType("test"); assertEquals("test", iso.getType()); } + + @Test + void defaultConstructor() { + Iso iso = new Iso(); + assertNotNull(iso); + } } \ No newline at end of file diff --git a/src/test/java/dev/tomr/hcloud/resources/common/LocationTest.java b/src/test/java/dev/tomr/hcloud/resources/common/LocationTest.java index 5e4a3a4..4e87335 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/LocationTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/LocationTest.java @@ -61,4 +61,10 @@ void setNetworkZone() { location.setNetworkZone("test"); assertEquals("test", location.getNetworkZone()); } + + @Test + void defaultConstructor() { + Location location = new Location(); + assertNotNull(location); + } } \ No newline at end of file diff --git a/src/test/java/dev/tomr/hcloud/resources/common/PlacementGroupTest.java b/src/test/java/dev/tomr/hcloud/resources/common/PlacementGroupTest.java index cb4513f..154d424 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/PlacementGroupTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/PlacementGroupTest.java @@ -54,4 +54,10 @@ void setType() { placementGroup.setType("test"); assertEquals("test", placementGroup.getType()); } + + @Test + void defaultConstructor() { + PlacementGroup placementGroup = new PlacementGroup(); + assertNotNull(placementGroup); + } } \ No newline at end of file diff --git a/src/test/java/dev/tomr/hcloud/resources/common/PriceDetailsTest.java b/src/test/java/dev/tomr/hcloud/resources/common/PriceDetailsTest.java index 63a022c..3f70ce2 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/PriceDetailsTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/PriceDetailsTest.java @@ -25,4 +25,10 @@ void setNet() { priceDetails.setNet("net"); assertEquals("net", priceDetails.getNet()); } + + @Test + void defaultConstructor() { + PriceDetails priceDetails = new PriceDetails(); + assertNotNull(priceDetails); + } } \ No newline at end of file diff --git a/src/test/java/dev/tomr/hcloud/resources/common/PriceTest.java b/src/test/java/dev/tomr/hcloud/resources/common/PriceTest.java index d96c270..531ec46 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/PriceTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/PriceTest.java @@ -46,4 +46,10 @@ void setPricePerTbTraffic() { price.setPricePerTbTraffic(priceDetails); assertEquals(priceDetails, price.getPricePerTbTraffic()); } + + @Test + void defaultConstructor() { + Price price = new Price(); + assertNotNull(price); + } } \ No newline at end of file diff --git a/src/test/java/dev/tomr/hcloud/resources/common/ProtectionTest.java b/src/test/java/dev/tomr/hcloud/resources/common/ProtectionTest.java index 833dccc..a57e61d 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/ProtectionTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/ProtectionTest.java @@ -25,4 +25,10 @@ void setRebuild() { protection.setRebuild(true); assertTrue(protection.isRebuild()); } + + @Test + void defaultConstructor() { + Protection protection = new Protection(); + assertNotNull(protection); + } } \ No newline at end of file diff --git a/src/test/java/dev/tomr/hcloud/resources/common/ServerTypeTest.java b/src/test/java/dev/tomr/hcloud/resources/common/ServerTypeTest.java index 370eff8..239ac3e 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/ServerTypeTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/ServerTypeTest.java @@ -98,4 +98,10 @@ void setStorageType() { serverType.setStorageType("storage"); assertEquals("storage", serverType.getStorageType()); } + + @Test + void defaultConstructor() { + ServerType serverType = new ServerType(); + assertNotNull(serverType); + } } \ No newline at end of file diff --git a/src/test/java/dev/tomr/hcloud/resources/common/ServerTypesTest.java b/src/test/java/dev/tomr/hcloud/resources/common/ServerTypesTest.java index beba752..37f80f8 100644 --- a/src/test/java/dev/tomr/hcloud/resources/common/ServerTypesTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/common/ServerTypesTest.java @@ -33,4 +33,10 @@ void setSupported() { serverTypes.setSupported(List.of(1, 2)); assertEquals(List.of(1, 2), serverTypes.getSupported()); } + + @Test + void defaultConstructor() { + ServerTypes serverTypes = new ServerTypes(); + assertNotNull(serverTypes); + } } \ No newline at end of file From 775ca7aed53dc2621c8c04eca739b9259a56d214 Mon Sep 17 00:00:00 2001 From: Tom Roman Date: Wed, 13 Nov 2024 20:56:10 +0000 Subject: [PATCH 3/4] chore: refactor how ServerService handles caching --- .../java/dev/tomr/hcloud/HetznerCloud.java | 6 +- .../tomr/hcloud/resources/server/Server.java | 10 +-- .../hcloud/service/server/ServerService.java | 11 ++- .../dev/tomr/hcloud/HetznerCloudTest.java | 79 +++++++++++++++++++ .../hcloud/http/model/ServerDTOListTest.java | 37 +++++++++ .../service/server/ServerServiceTest.java | 39 ++++++++- 6 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 src/test/java/dev/tomr/hcloud/http/model/ServerDTOListTest.java diff --git a/src/main/java/dev/tomr/hcloud/HetznerCloud.java b/src/main/java/dev/tomr/hcloud/HetznerCloud.java index 93ed3a5..38bf144 100644 --- a/src/main/java/dev/tomr/hcloud/HetznerCloud.java +++ b/src/main/java/dev/tomr/hcloud/HetznerCloud.java @@ -15,8 +15,8 @@ public class HetznerCloud { protected static final Logger logger = LogManager.getLogger(); - private static ListenerManager listenerManager = ListenerManager.getInstance(); - private static ServiceManager serviceManager = ServiceManager.getInstance(); + private static final ListenerManager listenerManager = ListenerManager.getInstance(); + private static final ServiceManager serviceManager = ServiceManager.getInstance(); private static final ObjectMapper objectMapper = new ObjectMapper(); private static final String HETZNER_CLOUD_HOST = "https://api.hetzner.cloud/v1/"; @@ -33,7 +33,6 @@ public HetznerCloud(String apiKey) { this.apiKey = apiKey; this.host = HETZNER_CLOUD_HOST; instance = this; - ServiceManager.getInstance().getServerService().forceRefreshServersCache(); } /** @@ -45,7 +44,6 @@ public HetznerCloud(String apiKey, String host) { this.apiKey = apiKey; this.host = host; instance = this; - ServiceManager.getInstance().getServerService().forceRefreshServersCache(); } /** 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 6c9205f..ae7c3ed 100644 --- a/src/main/java/dev/tomr/hcloud/resources/server/Server.java +++ b/src/main/java/dev/tomr/hcloud/resources/server/Server.java @@ -22,7 +22,7 @@ public class Server implements Serializable { private Iso iso; private Map labels; private List loadBalancers; // need to figure this out - private boolean locked; + private Boolean locked; private String name; private PlacementGroup placementGroup; private Long primaryDiskSize; @@ -30,7 +30,7 @@ public class Server implements Serializable { private List privateNet; private Protection protection; private Object publicNet; - private boolean rescueEnabled; + private Boolean rescueEnabled; private ServerType serverType; private String status; private List volumes; @@ -67,7 +67,7 @@ public Server() { * @param status Status of the Server * @param volumes Attached Volumes */ - public Server(Integer id, String backupWindow, String created, Datacenter datacenter, Image image, Long includedTraffic, Long ingoingTraffic, Long outgoingTraffic, Iso iso, Map labels, List loadBalancers, boolean locked, String name, PlacementGroup placementGroup, Long primaryDiskSize, List privateNet, Protection protection, Object publicNet, boolean rescueEnabled, ServerType serverType, String status, List volumes) { + public Server(Integer id, String backupWindow, String created, Datacenter datacenter, Image image, Long includedTraffic, Long ingoingTraffic, Long outgoingTraffic, Iso iso, Map labels, List loadBalancers, Boolean locked, String name, PlacementGroup placementGroup, Long primaryDiskSize, List privateNet, Protection protection, Object publicNet, Boolean rescueEnabled, ServerType serverType, String status, List volumes) { this.id = id; this.backupWindow = backupWindow; this.created = created; @@ -177,7 +177,7 @@ public List getLoadBalancers() { return loadBalancers; } - public boolean isLocked() { + public Boolean isLocked() { return locked; } @@ -201,7 +201,7 @@ public Object getPublicNet() { return publicNet; } - public boolean isRescueEnabled() { + public Boolean isRescueEnabled() { return rescueEnabled; } 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 b34d52b..c6dc26d 100644 --- a/src/main/java/dev/tomr/hcloud/service/server/ServerService.java +++ b/src/main/java/dev/tomr/hcloud/service/server/ServerService.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -35,6 +36,7 @@ public class ServerService { private CompletableFuture updatedServerFuture; private Map remoteServers = new HashMap<>(); + private Date lastFullRefresh; /** * Creates a new {@code ServerService} instance @@ -109,12 +111,13 @@ private void updateAllRemoteServers() { } serverDTOList.getServers().forEach(serverDTO -> { newServerMap.put(Date.from(Instant.now()), ServerConverterUtil.transformServerDTOToServer(serverDTO)); - logger.info(serverDTO.getId()); }); remoteServers = newServerMap; + lastFullRefresh = new Date(); } public Server getServer(Integer id) { + verifyCacheUpToDate(); for (var entry : remoteServers.entrySet()) { if (entry.getValue().getId().equals(id)) { return entry.getValue(); @@ -160,4 +163,10 @@ private void removeUnchangedFields(ServerDTO serverDTO) { serverDTO.setName(null); } } + + private void verifyCacheUpToDate() { + if (lastFullRefresh == null || lastFullRefresh.before(Date.from(Instant.now().minus(10, ChronoUnit.MINUTES)))) { + forceRefreshServersCache(); + } + } } diff --git a/src/test/java/dev/tomr/hcloud/HetznerCloudTest.java b/src/test/java/dev/tomr/hcloud/HetznerCloudTest.java index 3fee9e9..b2ecc80 100644 --- a/src/test/java/dev/tomr/hcloud/HetznerCloudTest.java +++ b/src/test/java/dev/tomr/hcloud/HetznerCloudTest.java @@ -1,15 +1,20 @@ package dev.tomr.hcloud; import dev.tomr.hcloud.listener.ListenerManager; +import dev.tomr.hcloud.resources.server.Server; import dev.tomr.hcloud.service.ServiceManager; +import dev.tomr.hcloud.service.server.ServerService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import java.lang.reflect.Field; import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; class HetznerCloudTest { @@ -77,4 +82,78 @@ void callingGetHttpDetailsWillReturnTheHostAndApikeyInAList() { assertEquals(List.of("https://api.hetzner.cloud/v1/", "apiKey"), instance.getHttpDetails()); } + @Test + @DisplayName("Calling hasApiKey returns true for present key") + void callingHasApiKeyReturnsTrueForPresentKey() { + HetznerCloud instance = HetznerCloud.getInstance(); + instance.setApiKey("apiKey"); + assertTrue(instance.hasApiKey()); + } + + @Test + @DisplayName("Calling hasApiKey returns false for empty value") + void callingHasApiKeyReturnsFalseForEmptyValue() { + HetznerCloud instance = HetznerCloud.getInstance(); + instance.setApiKey(""); + assertFalse(instance.hasApiKey()); + } + + @Test + @DisplayName("Calling hasApiKey returns false for null value") + void callingHasApiKeyReturnsFalseForNullValue() { + HetznerCloud instance = HetznerCloud.getInstance(); + instance.setApiKey(null); + assertFalse(instance.hasApiKey()); + } + + @Test + @DisplayName("calling getServer calls ServerManager for the server") + void callingGetServerCallsServerManagerForTheServer() { + try (MockedStatic serviceManagerMockedStatic = mockStatic(ServiceManager.class); + MockedStatic listenerManagerMockedStatic = mockStatic(ListenerManager.class)) { + + ServiceManager serviceManager = mock(ServiceManager.class); + ServerService serverService = mock(ServerService.class); + ListenerManager listenerManager = mock(ListenerManager.class); + + serviceManagerMockedStatic.when(ServiceManager::getInstance).thenReturn(serviceManager); + listenerManagerMockedStatic.when(ListenerManager::getInstance).thenReturn(listenerManager); + + when(serviceManager.getServerService()).thenReturn(serverService); + + Server server = new Server(1, + "backupWindow", + "created", + null, + null, + 1L, + 1L, + 1L, + null, + Map.of("label", "value"), + List.of(), + false, + "name", + null, + 1L, + List.of(), + null, + new Object(), + false, + null, + "healthy", + List.of(1)); + + HetznerCloud instance = HetznerCloud.getInstance(); + + when(serverService.getServer(anyInt())).thenReturn(server); + + Server serverUnderTest = instance.getServer(1); + + assertEquals(server, serverUnderTest); + verify(serverService, times(1)).getServer(1); + } + + } + } \ No newline at end of file diff --git a/src/test/java/dev/tomr/hcloud/http/model/ServerDTOListTest.java b/src/test/java/dev/tomr/hcloud/http/model/ServerDTOListTest.java new file mode 100644 index 0000000..d460df5 --- /dev/null +++ b/src/test/java/dev/tomr/hcloud/http/model/ServerDTOListTest.java @@ -0,0 +1,37 @@ +package dev.tomr.hcloud.http.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ServerDTOListTest { + + @Test + @DisplayName("getters work as expected") + void gettersWorkAsExpected() { + Object o = new Object(); + List serverDTOList = new ArrayList<>(); + ServerDTOList list = new ServerDTOList(o, serverDTOList); + + assertEquals(o, list.getMeta()); + assertEquals(serverDTOList, list.getServers()); + } + + @Test + @DisplayName("Setters work as expected") + void settersWorkAsExpected() { + Object o = new Object(); + List serverDTOList = new ArrayList<>(); + ServerDTOList list = new ServerDTOList(); + + list.setMeta(o); + list.setServers(serverDTOList); + + assertEquals(o, list.getMeta()); + assertEquals(serverDTOList, list.getServers()); + } +} 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 da6d98e..f9dde3f 100644 --- a/src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java +++ b/src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java @@ -4,6 +4,7 @@ import dev.tomr.hcloud.http.HetznerCloudHttpClient; import dev.tomr.hcloud.http.RequestVerb; import dev.tomr.hcloud.http.model.ServerDTO; +import dev.tomr.hcloud.http.model.ServerDTOList; import dev.tomr.hcloud.listener.ListenerManager; import dev.tomr.hcloud.resources.common.*; import dev.tomr.hcloud.resources.server.Server; @@ -109,7 +110,7 @@ void taskIsScheduledWhenServerNameOrLabelUpdateCalledForFirstTime() { serverService.serverNameOrLabelUpdate("name", "name", server); - verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/server/1"), any(RequestVerb.class), eq("key1234"), eq("{\"name\":\"name\"}")); + verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/servers/1"), any(RequestVerb.class), eq("key1234"), eq("{\"name\":\"name\"}")); } catch (IOException | InterruptedException | IllegalAccessException e) { throw new RuntimeException(e); } @@ -164,7 +165,7 @@ void taskIsScheduledWhenServerNameOrLabelUpdateCalledForFirstTimeWithLabels() { serverService.serverNameOrLabelUpdate("labels", Map.of("label", "value"), server); - verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/server/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"}}")); + verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/servers/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"}}")); } catch (IOException | InterruptedException | IllegalAccessException e) { throw new RuntimeException(e); } @@ -220,7 +221,7 @@ void taskUsesExtraFieldsChangedAfterFirstInvocation() { serverService.serverNameOrLabelUpdate("name", "name", server); serverService.serverNameOrLabelUpdate("labels", Map.of("l", "v"), server); - verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/server/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"},\"name\":\"name\"}")); + verify(hetznerCloudHttpClient, timeout(2000).times(1)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/servers/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"},\"name\":\"name\"}")); } catch (IOException | InterruptedException | IllegalAccessException e) { throw new RuntimeException(e); } @@ -277,7 +278,7 @@ void whenHttpClientThrowsGracefully() { when(hetznerCloudHttpClient.sendHttpRequest(any(), any(), any(), any(), any())).thenThrow(IOException.class); - verify(hetznerCloudHttpClient, timeout(2000).times(0)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/server/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"},\"name\":\"name\"}")); + verify(hetznerCloudHttpClient, timeout(2000).times(0)).sendHttpRequest(eq(ServerDTO.class), eq("http://host/servers/1"), any(RequestVerb.class), eq("key1234"), eq("{\"labels\":{\"label\":\"value\"},\"name\":\"name\"}")); verify(serviceManager, timeout(2000).times(1)).closeExecutor(); } catch (IOException | InterruptedException | IllegalAccessException e) { throw new RuntimeException(e); @@ -341,4 +342,34 @@ void cancelMethodPreventsTheRequestBeingSent() { throw new RuntimeException(e); } } + + @Test + @DisplayName("getServer returns a server from the cache") + void getServerReturnsAServerFromTheCache() 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)) { + ServerDTOList serverDTOList = new ServerDTOList(); + ServerDTO serverDTO = new ServerDTO(); + serverDTO.setName("name"); + serverDTO.setId(1); + serverDTOList.setServers(List.of(serverDTO)); + + hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient); + hetznerCloudMockedStatic.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); + hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud); + when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234")); + when(hetznerCloud.hasApiKey()).thenReturn(true); + when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(serverDTOList); + + ServerService serverService = new ServerService(); + + Server server = serverService.getServer(1); + + assertEquals(serverDTO.getName(), server.getName()); + } + } } \ No newline at end of file From 6221532731eddc68a7b3f2df0544224c4b61fe25 Mon Sep 17 00:00:00 2001 From: Tom Roman Date: Sun, 17 Nov 2024 16:02:15 +0000 Subject: [PATCH 4/4] fix: rework Service and Listener Manager to not be static in HetznerCloud --- .gitignore | 3 +- .../java/dev/tomr/hcloud/HetznerCloud.java | 9 +- .../hcloud/listener/ServerChangeListener.java | 2 +- .../tomr/hcloud/resources/server/Server.java | 2 +- .../hcloud/service/server/ServerService.java | 4 +- .../dev/tomr/hcloud/HetznerCloudTest.java | 4 +- .../listener/ServerChangeListenerTest.java | 6 +- .../hcloud/resources/server/ServerTest.java | 23 ++- .../service/server/ServerServiceTest.java | 192 +++++++++++++++++- 9 files changed, 213 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index b1a77ad..c7aa09f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ bin/ ### Mac OS ### .DS_Store -.idea \ No newline at end of file +.idea +/src/test/java/dev/tomr/hcloud/component/TestApp.java diff --git a/src/main/java/dev/tomr/hcloud/HetznerCloud.java b/src/main/java/dev/tomr/hcloud/HetznerCloud.java index 38bf144..386d930 100644 --- a/src/main/java/dev/tomr/hcloud/HetznerCloud.java +++ b/src/main/java/dev/tomr/hcloud/HetznerCloud.java @@ -15,13 +15,14 @@ public class HetznerCloud { protected static final Logger logger = LogManager.getLogger(); - private static final ListenerManager listenerManager = ListenerManager.getInstance(); - private static final ServiceManager serviceManager = ServiceManager.getInstance(); private static final ObjectMapper objectMapper = new ObjectMapper(); private static final String HETZNER_CLOUD_HOST = "https://api.hetzner.cloud/v1/"; private static HetznerCloud instance; + private final ListenerManager listenerManager = ListenerManager.getInstance(); + private final ServiceManager serviceManager = ServiceManager.getInstance(); + private String apiKey; private final String host; @@ -78,7 +79,7 @@ public static ObjectMapper getObjectMapper() { * Get the internal {@code ListenerManager}. End users do not need to interact with the {@code ListenerManager} * @return The {@code ListenerManager} Instance */ - public static ListenerManager getListenerManager() { + public ListenerManager getListenerManager() { return listenerManager; } @@ -86,7 +87,7 @@ public static ListenerManager getListenerManager() { * Get the internal {@code ServiceManager}. End users do not need to interact with the {@code ServiceManager} * @return The {@code ServiceManager} Instance */ - public static ServiceManager getServiceManager() { + public ServiceManager getServiceManager() { return serviceManager; } diff --git a/src/main/java/dev/tomr/hcloud/listener/ServerChangeListener.java b/src/main/java/dev/tomr/hcloud/listener/ServerChangeListener.java index 94c9fb7..658fd44 100644 --- a/src/main/java/dev/tomr/hcloud/listener/ServerChangeListener.java +++ b/src/main/java/dev/tomr/hcloud/listener/ServerChangeListener.java @@ -21,6 +21,6 @@ public void propertyChange(PropertyChangeEvent evt) { Server server = (Server) evt.getSource(); logger.info("Server changed: " + evt.getPropertyName()); logger.info("Server: " + evt.getOldValue() + " -> " + evt.getNewValue()); - HetznerCloud.getServiceManager().getServerService().serverNameOrLabelUpdate(evt.getPropertyName(), evt.getNewValue(), server); + 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 ae7c3ed..4f77c89 100644 --- a/src/main/java/dev/tomr/hcloud/resources/server/Server.java +++ b/src/main/java/dev/tomr/hcloud/resources/server/Server.java @@ -94,7 +94,7 @@ public Server(Integer id, String backupWindow, String created, Datacenter datace } private void setupPropertyChangeListener() { - propertyChangeSupport.addPropertyChangeListener(HetznerCloud.getListenerManager().getServerChangeListener()); + propertyChangeSupport.addPropertyChangeListener(HetznerCloud.getInstance().getListenerManager().getServerChangeListener()); } 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 c6dc26d..dcfdb4a 100644 --- a/src/main/java/dev/tomr/hcloud/service/server/ServerService.java +++ b/src/main/java/dev/tomr/hcloud/service/server/ServerService.java @@ -42,7 +42,7 @@ public class ServerService { * Creates a new {@code ServerService} instance */ public ServerService() { - this.serviceManager = HetznerCloud.getServiceManager(); + this.serviceManager = HetznerCloud.getInstance().getServiceManager(); } public ServerService(ServiceManager serviceManager) { @@ -113,7 +113,7 @@ private void updateAllRemoteServers() { newServerMap.put(Date.from(Instant.now()), ServerConverterUtil.transformServerDTOToServer(serverDTO)); }); remoteServers = newServerMap; - lastFullRefresh = new Date(); + lastFullRefresh = Date.from(Instant.now()); } public Server getServer(Integer id) { diff --git a/src/test/java/dev/tomr/hcloud/HetznerCloudTest.java b/src/test/java/dev/tomr/hcloud/HetznerCloudTest.java index b2ecc80..6b6f2c1 100644 --- a/src/test/java/dev/tomr/hcloud/HetznerCloudTest.java +++ b/src/test/java/dev/tomr/hcloud/HetznerCloudTest.java @@ -64,14 +64,14 @@ void callingSetApiKeyUpdatesTheAPIKey() throws IllegalAccessException, NoSuchFie @DisplayName("Calling getListenerManager returns the listener manager") void callingGetListenerManagerReturnsTheListenerManager() { ListenerManager listenerManager = ListenerManager.getInstance(); - assertEquals(listenerManager, HetznerCloud.getListenerManager()); + assertEquals(listenerManager, HetznerCloud.getInstance().getListenerManager()); } @Test @DisplayName("Calling getServiceManager returns the service manager") void callingGetServiceManagerReturnsTheServiceManager() { ServiceManager serviceManager = ServiceManager.getInstance(); - assertEquals(serviceManager, HetznerCloud.getServiceManager()); + assertEquals(serviceManager, HetznerCloud.getInstance().getServiceManager()); } @Test diff --git a/src/test/java/dev/tomr/hcloud/listener/ServerChangeListenerTest.java b/src/test/java/dev/tomr/hcloud/listener/ServerChangeListenerTest.java index 5484396..9c021da 100644 --- a/src/test/java/dev/tomr/hcloud/listener/ServerChangeListenerTest.java +++ b/src/test/java/dev/tomr/hcloud/listener/ServerChangeListenerTest.java @@ -21,6 +21,7 @@ public class ServerChangeListenerTest { @Test void verifyServerChangeListenerCalledWhenSetterIsSet() { try (MockedStatic hetznerCloud = mockStatic(HetznerCloud.class)) { + HetznerCloud hetznerCloudMock = mock(HetznerCloud.class); ServerChangeListener scl = new ServerChangeListener(); ServerChangeListener serverChangeListener = spy(scl); ListenerManager listenerManager = mock(ListenerManager.class); @@ -28,10 +29,11 @@ void verifyServerChangeListenerCalledWhenSetterIsSet() { ServerService serverService = mock(ServerService.class); ArgumentCaptor captor = ArgumentCaptor.forClass(PropertyChangeEvent.class); - hetznerCloud.when(HetznerCloud::getServiceManager).thenReturn(serviceManager); - hetznerCloud.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); + hetznerCloud.when(HetznerCloud::getInstance).thenReturn(hetznerCloudMock); when(listenerManager.getServerChangeListener()).thenReturn(serverChangeListener); when(serviceManager.getServerService()).thenReturn(serverService); + when(hetznerCloudMock.getListenerManager()).thenReturn(listenerManager); + when(hetznerCloudMock.getServiceManager()).thenReturn(serviceManager); Server server = new Server(); server.setName("test"); 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 0239de0..7580dc0 100644 --- a/src/test/java/dev/tomr/hcloud/resources/server/ServerTest.java +++ b/src/test/java/dev/tomr/hcloud/resources/server/ServerTest.java @@ -114,8 +114,10 @@ void serverCanBeInstantiatedAndValuesSetCorrectly() { void callingSetLabelsUpdatesLabels() { try (MockedStatic hetznerCloudMockedStatic = mockStatic(HetznerCloud.class); MockedStatic listenerManagerMockedStatic = mockStatic(ListenerManager.class)) { + HetznerCloud hetznerCloud = mock(HetznerCloud.class); ListenerManager listenerManager = mock(ListenerManager.class); - hetznerCloudMockedStatic.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); + hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud); + when(hetznerCloud.getListenerManager()).thenReturn(listenerManager); Server server = new Server(); server.setLabels(Map.of("label", "value")); @@ -130,13 +132,15 @@ void callingSetLabelsSendsAnEventToTheServerChangeListener() { try (MockedStatic hetznerCloud = mockStatic(HetznerCloud.class)) { ServerChangeListener scl = new ServerChangeListener(); ServerChangeListener serverChangeListener = spy(scl); + HetznerCloud hetznerCloudMock = mock(HetznerCloud.class); ListenerManager listenerManager = mock(ListenerManager.class); ServiceManager serviceManager = mock(ServiceManager.class); ServerService serverService = mock(ServerService.class); ArgumentCaptor captor = ArgumentCaptor.forClass(PropertyChangeEvent.class); - hetznerCloud.when(HetznerCloud::getServiceManager).thenReturn(serviceManager); - hetznerCloud.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); + 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); @@ -153,10 +157,11 @@ void callingSetLabelsSendsAnEventToTheServerChangeListener() { @Test @DisplayName("calling setName updates the name") void callingSetNameUpdatesTheName() { - try (MockedStatic hetznerCloudMockedStatic = mockStatic(HetznerCloud.class); - MockedStatic listenerManagerMockedStatic = mockStatic(ListenerManager.class)) { + try (MockedStatic hetznerCloudMockedStatic = mockStatic(HetznerCloud.class)) { + HetznerCloud hetznerCloud = mock(HetznerCloud.class); ListenerManager listenerManager = mock(ListenerManager.class); - hetznerCloudMockedStatic.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); + hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud); + when(hetznerCloud.getListenerManager()).thenReturn(listenerManager); Server server = new Server(); server.setName("name"); assertEquals("name", server.getName()); @@ -167,6 +172,7 @@ void callingSetNameUpdatesTheName() { @DisplayName("calling setName sends an event to the ServerChangeListener") void callingSetNameSendsAnEventToTheServerChangeListener() { try (MockedStatic hetznerCloud = mockStatic(HetznerCloud.class)) { + HetznerCloud hetznerCloudMock = mock(HetznerCloud.class); ServerChangeListener scl = new ServerChangeListener(); ServerChangeListener serverChangeListener = spy(scl); ListenerManager listenerManager = mock(ListenerManager.class); @@ -174,8 +180,9 @@ void callingSetNameSendsAnEventToTheServerChangeListener() { ServerService serverService = mock(ServerService.class); ArgumentCaptor captor = ArgumentCaptor.forClass(PropertyChangeEvent.class); - hetznerCloud.when(HetznerCloud::getServiceManager).thenReturn(serviceManager); - hetznerCloud.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); + 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); 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 f9dde3f..aab886a 100644 --- a/src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java +++ b/src/test/java/dev/tomr/hcloud/service/server/ServerServiceTest.java @@ -9,13 +9,17 @@ import dev.tomr.hcloud.resources.common.*; import dev.tomr.hcloud.resources.server.Server; import dev.tomr.hcloud.service.ServiceManager; +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.mockito.ArgumentCaptor; import org.mockito.MockedStatic; +import org.mockito.Mockito; import java.io.IOException; import java.lang.reflect.Field; +import java.time.Clock; import java.time.Instant; import java.util.Date; import java.util.List; @@ -61,6 +65,23 @@ class ServerServiceTest { "", ""); + private MockedStatic instantMockedStatic; + + private void mockInstant(long time) { + Instant instant = Instant.ofEpochSecond(time); + instantMockedStatic.when(Instant::now).thenReturn(instant); + } + + @BeforeEach + void setUp() { + instantMockedStatic = mockStatic(Instant.class, Mockito.CALLS_REAL_METHODS); + } + + @AfterEach + void after() { + instantMockedStatic.close(); + } + @Test @DisplayName("Task is scheduled when serverNameOrLabelUpdate called for first time with name supplied") void taskIsScheduledWhenServerNameOrLabelUpdateCalledForFirstTime() { @@ -78,8 +99,9 @@ void taskIsScheduledWhenServerNameOrLabelUpdateCalledForFirstTime() { serviceManagerMockedStatic.when(ServiceManager::getInstance).thenReturn(serviceManager); hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud); - hetznerCloudMockedStatic.when(HetznerCloud::getServiceManager).thenReturn(serviceManager); - hetznerCloudMockedStatic.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); + + when(hetznerCloud.getServiceManager()).thenReturn(serviceManager); + when(hetznerCloud.getListenerManager()).thenReturn(listenerManager); when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234")); @@ -133,8 +155,9 @@ void taskIsScheduledWhenServerNameOrLabelUpdateCalledForFirstTimeWithLabels() { serviceManagerMockedStatic.when(ServiceManager::getInstance).thenReturn(serviceManager); hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud); - hetznerCloudMockedStatic.when(HetznerCloud::getServiceManager).thenReturn(serviceManager); - hetznerCloudMockedStatic.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); + + when(hetznerCloud.getServiceManager()).thenReturn(serviceManager); + when(hetznerCloud.getListenerManager()).thenReturn(listenerManager); when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234")); @@ -188,8 +211,9 @@ void taskUsesExtraFieldsChangedAfterFirstInvocation() { serviceManagerMockedStatic.when(ServiceManager::getInstance).thenReturn(serviceManager); hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud); - hetznerCloudMockedStatic.when(HetznerCloud::getServiceManager).thenReturn(serviceManager); - hetznerCloudMockedStatic.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); + + when(hetznerCloud.getServiceManager()).thenReturn(serviceManager); + when(hetznerCloud.getListenerManager()).thenReturn(listenerManager); when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234")); @@ -244,8 +268,9 @@ void whenHttpClientThrowsGracefully() { serviceManagerMockedStatic.when(ServiceManager::getInstance).thenReturn(serviceManager); hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud); - hetznerCloudMockedStatic.when(HetznerCloud::getServiceManager).thenReturn(serviceManager); - hetznerCloudMockedStatic.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); + + when(hetznerCloud.getServiceManager()).thenReturn(serviceManager); + when(hetznerCloud.getListenerManager()).thenReturn(listenerManager); when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234")); @@ -303,8 +328,9 @@ void cancelMethodPreventsTheRequestBeingSent() { serviceManagerMockedStatic.when(ServiceManager::getInstance).thenReturn(serviceManager); hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud); - hetznerCloudMockedStatic.when(HetznerCloud::getServiceManager).thenReturn(serviceManager); - hetznerCloudMockedStatic.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); + + when(hetznerCloud.getServiceManager()).thenReturn(serviceManager); + when(hetznerCloud.getListenerManager()).thenReturn(listenerManager); when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234")); @@ -359,8 +385,8 @@ void getServerReturnsAServerFromTheCache() throws IOException, InterruptedExcept serverDTOList.setServers(List.of(serverDTO)); hetznerCloudHttpClientMockedStatic.when(HetznerCloudHttpClient::getInstance).thenReturn(hetznerCloudHttpClient); - hetznerCloudMockedStatic.when(HetznerCloud::getListenerManager).thenReturn(listenerManager); hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud); + when(hetznerCloud.getListenerManager()).thenReturn(listenerManager); when(hetznerCloud.getHttpDetails()).thenReturn(List.of("http://host/", "key1234")); when(hetznerCloud.hasApiKey()).thenReturn(true); when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(serverDTOList); @@ -372,4 +398,148 @@ void getServerReturnsAServerFromTheCache() throws IOException, InterruptedExcept assertEquals(serverDTO.getName(), server.getName()); } } + + @Test + @DisplayName("getServer returns a server from the cache when multiple are in the cache") + void getServerReturnsAServerFromTheCacheWithMultipleCached() 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)) { + ServerDTOList serverDTOList = new ServerDTOList(); + ServerDTO serverDTO = new ServerDTO(); + serverDTO.setName("name"); + serverDTO.setId(1); + ServerDTO serverDTO2 = new ServerDTO(); + serverDTO2.setName("name2"); + serverDTO2.setId(2); + serverDTOList.setServers(List.of(serverDTO2, serverDTO)); + + 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(hetznerCloud.hasApiKey()).thenReturn(true); + when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(serverDTOList); + + ServerService serverService = new ServerService(); + + Server server = serverService.getServer(2); + + assertEquals(serverDTO2.getName(), server.getName()); + } + } + + @Test + @DisplayName("getServer returns null when it's not found") + void getServerReturnsNullWhenItIsNotFound() 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)) { + ServerDTOList serverDTOList = new ServerDTOList(); + ServerDTO serverDTO = new ServerDTO(); + serverDTO.setName("name"); + serverDTO.setId(20); + serverDTOList.setServers(List.of(serverDTO)); + + 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(hetznerCloud.hasApiKey()).thenReturn(true); + when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(serverDTOList); + + ServerService serverService = new ServerService(); + + Server server = serverService.getServer(1); + + assertNull(server); + } + } + + @Test + @DisplayName("If no API key is present, refreshing the cache does nothing") + void ifNoAPIKeyPresentRefreshCacheDoesNothing() { + HetznerCloud hetznerCloud = mock(HetznerCloud.class); + + try (MockedStatic hetznerCloudMockedStatic = mockStatic(HetznerCloud.class)) { + hetznerCloudMockedStatic.when(HetznerCloud::getInstance).thenReturn(hetznerCloud); + ServerService serverService = new ServerService(); + assertDoesNotThrow(serverService::forceRefreshServersCache); + } + } + + @Test + @DisplayName("When http client throws, updateAllRemoteServers also throws a Runtime Exception") + void whenHttpClientThrowsRuntimeException() 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(hetznerCloud.hasApiKey()).thenReturn(true); + when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenThrow(IOException.class); + + ServerService serverService = new ServerService(); + + RuntimeException runtimeException = assertThrows(RuntimeException.class, () -> serverService.getServer(1)); + + assertTrue(runtimeException.getMessage().contains("IOException")); + + } + } + + @Test + @DisplayName("Cache is refreshed if it was last refreshed over 10 minutes ago") + void cacheIsRefreshedIfItWasLastRefreshedOver10MinutesAgo() throws IOException, InterruptedException, IllegalAccessException { + mockInstant(1731856842); + 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)) { + ServerDTOList serverDTOList = new ServerDTOList(); + ServerDTO serverDTO = new ServerDTO(); + serverDTO.setName("name"); + serverDTO.setId(1); + serverDTOList.setServers(List.of(serverDTO)); + + 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(hetznerCloud.hasApiKey()).thenReturn(true); + when(hetznerCloudHttpClient.sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString())).thenReturn(serverDTOList); + + ServerService serverService = new ServerService(); + serverService.getServer(1); + + verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString()); + + mockInstant(1731857082); + + serverService.getServer(1); + verify(hetznerCloudHttpClient, times(1)).sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString()); + + mockInstant(1731858342); + + serverService.getServer(1); + + verify(hetznerCloudHttpClient, times(2)).sendHttpRequest(any(), anyString(), any(RequestVerb.class), anyString()); + } + + } + } \ No newline at end of file