From 70a11417500faee1961c63feb6c4442785c69bec Mon Sep 17 00:00:00 2001 From: Ryan Harg Date: Wed, 27 Nov 2024 11:20:54 +0100 Subject: [PATCH] Restructuring and more tests --- src/main/java/de/rpr/ddnsclient/Updater.java | 10 +-- .../ddnsclient/lookup/CurlProcessFactory.java | 35 ++++++++++ .../{IpProvider.java => PublicIpLookup.java} | 34 ++++++---- .../java/de/rpr/ddnsclient/UpdaterTest.java | 22 +++--- .../ddnsclient/lookup/PublicIpLookupTest.java | 67 +++++++++++++++++++ 5 files changed, 139 insertions(+), 29 deletions(-) create mode 100644 src/main/java/de/rpr/ddnsclient/lookup/CurlProcessFactory.java rename src/main/java/de/rpr/ddnsclient/lookup/{IpProvider.java => PublicIpLookup.java} (74%) create mode 100644 src/test/java/de/rpr/ddnsclient/lookup/PublicIpLookupTest.java diff --git a/src/main/java/de/rpr/ddnsclient/Updater.java b/src/main/java/de/rpr/ddnsclient/Updater.java index b2b4c20..7ec107c 100644 --- a/src/main/java/de/rpr/ddnsclient/Updater.java +++ b/src/main/java/de/rpr/ddnsclient/Updater.java @@ -3,7 +3,7 @@ package de.rpr.ddnsclient; import de.rpr.ddnsclient.dyndns.DynDns; import de.rpr.ddnsclient.dyndns.DynDnsRouter; import de.rpr.ddnsclient.lookup.DnsResolver; -import de.rpr.ddnsclient.lookup.IpProvider; +import de.rpr.ddnsclient.lookup.PublicIpLookup; import de.rpr.ddnsclient.model.Config; import de.rpr.ddnsclient.model.DyndnsAuth; import de.rpr.ddnsclient.model.IPs; @@ -23,7 +23,7 @@ public class Updater { private static final Logger log = Logger.getLogger(Updater.class); private final DynDnsRouter dynDnsRouter; - private final IpProvider ipProvider; + private final PublicIpLookup publicIpLookup; private final DnsResolver dnsResolver; private final Duration backoff; @@ -33,12 +33,12 @@ public class Updater { @Inject public Updater(DynDnsRouter dynDnsRouter, - IpProvider ipProvider, + PublicIpLookup publicIpLookup, DnsResolver dnsResolver, Config config, @ConfigProperty(name = "ddnsclient.backoff-duration:300s") Duration backoff) { this.dynDnsRouter = dynDnsRouter; - this.ipProvider = ipProvider; + this.publicIpLookup = publicIpLookup; this.dnsResolver = dnsResolver; this.config = config; this.backoff = backoff; @@ -51,7 +51,7 @@ public class Updater { throw new IllegalStateException("Missing configuration"); } - IPs publicIps = ipProvider.getPublicIps(); + IPs publicIps = publicIpLookup.get(); log.debugf("Public ips - v4: %s, v6: %s", publicIps.v4(), publicIps.v6()); config.forEach(cfg -> { diff --git a/src/main/java/de/rpr/ddnsclient/lookup/CurlProcessFactory.java b/src/main/java/de/rpr/ddnsclient/lookup/CurlProcessFactory.java new file mode 100644 index 0000000..10db361 --- /dev/null +++ b/src/main/java/de/rpr/ddnsclient/lookup/CurlProcessFactory.java @@ -0,0 +1,35 @@ +package de.rpr.ddnsclient.lookup; + +import jakarta.enterprise.context.ApplicationScoped; + +import java.io.IOException; + +@ApplicationScoped +class CurlProcessFactory { + enum IpClass { + V4, + V6 + } + + // Workaround: Can't get Mockito 5 to mock final classes + static class ProcessBuilderWrapper { + private final ProcessBuilder processBuilder; + + ProcessBuilderWrapper(String... args) { + this.processBuilder = new ProcessBuilder(args); + } + + Process start() throws IOException { + return processBuilder.start(); + } + } + + ProcessBuilderWrapper create(String provider, IpClass ipClass) { + if (ipClass == IpClass.V4) { + return new ProcessBuilderWrapper("curl", "--ipv4", provider); + } else { + return new ProcessBuilderWrapper("curl", "--ipv6", provider); + } + + } +} diff --git a/src/main/java/de/rpr/ddnsclient/lookup/IpProvider.java b/src/main/java/de/rpr/ddnsclient/lookup/PublicIpLookup.java similarity index 74% rename from src/main/java/de/rpr/ddnsclient/lookup/IpProvider.java rename to src/main/java/de/rpr/ddnsclient/lookup/PublicIpLookup.java index b410509..890ba97 100644 --- a/src/main/java/de/rpr/ddnsclient/lookup/IpProvider.java +++ b/src/main/java/de/rpr/ddnsclient/lookup/PublicIpLookup.java @@ -17,9 +17,20 @@ import java.util.NoSuchElementException; import java.util.Optional; @ApplicationScoped -public class IpProvider { +public class PublicIpLookup { - private static final Logger log = Logger.getLogger(IpProvider.class); + private static final Logger log = Logger.getLogger(PublicIpLookup.class); + + private final CurlProcessFactory curlProcessFactory; + private final Optional configuredProvider; + + PublicIpLookup( + CurlProcessFactory curlProcessFactory, + @ConfigProperty(name = "ddnsclient.ip-provider") Optional configuredProvider + ) { + this.curlProcessFactory = curlProcessFactory; + this.configuredProvider = configuredProvider; + } enum Providers { IFCONFIG_ME("ifconfig.me", "ifconfig.me"), @@ -50,9 +61,6 @@ public class IpProvider { } } - @ConfigProperty(name = "ddnsclient.ip-provider") - Optional configuredProvider; - private Optional provider = Optional.empty(); void onStart(@Observes StartupEvent event) { @@ -63,11 +71,11 @@ public class IpProvider { } ); } catch (Exception e) { - Quarkus.asyncExit(1); + throw new IllegalStateException("Unknow provider configured!", e); } } - public IPs getPublicIps() { + public IPs get() { log.trace("Retrieving public ips."); try { @@ -76,9 +84,9 @@ public class IpProvider { IPs.Builder ipsBuilder = new IPs.Builder(); try { - Process curl = new ProcessBuilder("curl", "--ipv4", provider).start(); - curl.waitFor(); - InputStream inputStream = curl.getInputStream(); + Process curlProcess = curlProcessFactory.create(provider, CurlProcessFactory.IpClass.V4).start(); + curlProcess.waitFor(); + InputStream inputStream = curlProcess.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String v4 = reader.readLine(); log.tracef("Retrieved ipv4: %s", v4); @@ -88,9 +96,9 @@ public class IpProvider { } try { - Process curl = new ProcessBuilder("curl", "--ipv6", provider).start(); - curl.waitFor(); - InputStream inputStream = curl.getInputStream(); + Process curlProcess = curlProcessFactory.create(provider, CurlProcessFactory.IpClass.V6).start(); + curlProcess.waitFor(); + InputStream inputStream = curlProcess.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String v6 = reader.readLine(); log.tracef("Retrieved ipv6: %s", v6); diff --git a/src/test/java/de/rpr/ddnsclient/UpdaterTest.java b/src/test/java/de/rpr/ddnsclient/UpdaterTest.java index 87d9983..3df0d6c 100644 --- a/src/test/java/de/rpr/ddnsclient/UpdaterTest.java +++ b/src/test/java/de/rpr/ddnsclient/UpdaterTest.java @@ -4,7 +4,7 @@ import de.rpr.ddnsclient.dyndns.Ddnss; import de.rpr.ddnsclient.dyndns.DynDns; import de.rpr.ddnsclient.dyndns.DynDnsRouter; import de.rpr.ddnsclient.lookup.DnsResolver; -import de.rpr.ddnsclient.lookup.IpProvider; +import de.rpr.ddnsclient.lookup.PublicIpLookup; import de.rpr.ddnsclient.model.Config; import de.rpr.ddnsclient.model.DyndnsAuth; import de.rpr.ddnsclient.model.IPs; @@ -27,7 +27,7 @@ class UpdaterTest { @Mock DynDnsRouter dynDnsRouter; @Mock - IpProvider ipProvider; + PublicIpLookup publicIpLookup; @Mock DnsResolver dnsResolver; @@ -40,7 +40,7 @@ class UpdaterTest { @Test void should_throw_exception_if_config_is_empty() { Config config = new Config(); - Updater updater = new Updater(dynDnsRouter, ipProvider, dnsResolver, config, backoff); + Updater updater = new Updater(dynDnsRouter, publicIpLookup, dnsResolver, config, backoff); Assertions.assertThrows(IllegalStateException.class, updater::run); } @@ -48,7 +48,7 @@ class UpdaterTest { void should_process_all_config_entries() { IPs ips = new IPs("ipv4", "ipv6"); - when(ipProvider.getPublicIps()).thenReturn(ips); + when(publicIpLookup.get()).thenReturn(ips); when(dnsResolver.resolve(any())).thenReturn(ips); Config multipleConfigEntries = new Config() {{ @@ -56,7 +56,7 @@ class UpdaterTest { add(new Value("ddnss", "host2.example.org", null, null, "token")); }}; - Updater updater = new Updater(dynDnsRouter, ipProvider, dnsResolver, multipleConfigEntries, backoff); + Updater updater = new Updater(dynDnsRouter, publicIpLookup, dnsResolver, multipleConfigEntries, backoff); updater.run(); ArgumentCaptor hostnameCaptor = ArgumentCaptor.forClass(String.class); @@ -71,10 +71,10 @@ class UpdaterTest { IPs ips = new IPs("ipv4", "ipv6"); - when(ipProvider.getPublicIps()).thenReturn(ips); + when(publicIpLookup.get()).thenReturn(ips); when(dnsResolver.resolve("example.org")).thenReturn(ips); - Updater updater = new Updater(dynDnsRouter, ipProvider, dnsResolver, ddnssConfig, backoff); + Updater updater = new Updater(dynDnsRouter, publicIpLookup, dnsResolver, ddnssConfig, backoff); updater.run(); verify(dynDnsRouter, never()).get(any()); @@ -89,10 +89,10 @@ class UpdaterTest { when(dynDnsRouter.get("ddnss")).thenReturn(ddnss); IPs publicIps = new IPs("ipv4", "ipv6"); - when(ipProvider.getPublicIps()).thenReturn(publicIps); + when(publicIpLookup.get()).thenReturn(publicIps); when(dnsResolver.resolve("example.org")).thenReturn(new IPs("registered_ipv4", "registered_ipv6")); - Updater updater = new Updater(dynDnsRouter, ipProvider, dnsResolver, ddnssConfig, backoff); + Updater updater = new Updater(dynDnsRouter, publicIpLookup, dnsResolver, ddnssConfig, backoff); updater.run(); verify(ddnss).update( @@ -110,10 +110,10 @@ class UpdaterTest { when(dynDnsRouter.get("ddnss")).thenReturn(ddnss); IPs publicIps = new IPs("ipv4", "ipv6"); - when(ipProvider.getPublicIps()).thenReturn(publicIps); + when(publicIpLookup.get()).thenReturn(publicIps); when(dnsResolver.resolve("example.org")).thenReturn(new IPs("registered_ipv4", "registered_ipv6")); - Updater updater = new Updater(dynDnsRouter, ipProvider, dnsResolver, ddnssConfig, backoff); + Updater updater = new Updater(dynDnsRouter, publicIpLookup, dnsResolver, ddnssConfig, backoff); updater.run(); updater.run(); diff --git a/src/test/java/de/rpr/ddnsclient/lookup/PublicIpLookupTest.java b/src/test/java/de/rpr/ddnsclient/lookup/PublicIpLookupTest.java new file mode 100644 index 0000000..dc2433c --- /dev/null +++ b/src/test/java/de/rpr/ddnsclient/lookup/PublicIpLookupTest.java @@ -0,0 +1,67 @@ +package de.rpr.ddnsclient.lookup; + +import de.rpr.ddnsclient.model.IPs; +import io.quarkus.runtime.StartupEvent; +import org.apache.commons.io.IOUtils; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + + +@ExtendWith(MockitoExtension.class) +class PublicIpLookupTest { + + @Mock + CurlProcessFactory curlProcessFactory; + + @Test + void unknown_provider_should_throw_exception() { + PublicIpLookup lookup = new PublicIpLookup(curlProcessFactory, Optional.of("unknown")); + Assertions.assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> lookup.onStart(new StartupEvent())); + } + + @Test + void valid_provider_should_be_configurable() { + PublicIpLookup lookup = new PublicIpLookup(curlProcessFactory, Optional.of("ifconfig.me")); + lookup.onStart(new StartupEvent()); + } + + @Test + void should_use_configured_provider_for_lookups() { + Process v4CurlProcess = mock(Process.class); + when(v4CurlProcess.getInputStream()).thenReturn(IOUtils.toInputStream("ipv4", "UTF-8")); + + CurlProcessFactory.ProcessBuilderWrapper v4CurlProcessBuilderWrapper = mock(CurlProcessFactory.ProcessBuilderWrapper.class, invocationOnMock -> v4CurlProcess); + when(curlProcessFactory.create(any(), eq(CurlProcessFactory.IpClass.V4))).thenReturn(v4CurlProcessBuilderWrapper); + + Process v6CurlProcess = mock(Process.class); + when(v6CurlProcess.getInputStream()).thenReturn(IOUtils.toInputStream("ipv6", "UTF-8")); + + CurlProcessFactory.ProcessBuilderWrapper v6CurlProcessBuilderWrapper = mock(CurlProcessFactory.ProcessBuilderWrapper.class, invocationOnMock -> v6CurlProcess); + when(curlProcessFactory.create(any(), eq(CurlProcessFactory.IpClass.V6))).thenReturn(v6CurlProcessBuilderWrapper); + + PublicIpLookup lookup = new PublicIpLookup(curlProcessFactory, Optional.of("ifconfig.me")); + lookup.onStart(new StartupEvent()); + IPs result = lookup.get(); + + assertThat(result).isEqualTo(new IPs("ipv4", "ipv6")); + + ArgumentCaptor providerCaptor = ArgumentCaptor.forClass(String.class); + verify(curlProcessFactory, times(2)).create(providerCaptor.capture(), any()); + + assertThat(providerCaptor.getAllValues()).containsExactly("ifconfig.me", "ifconfig.me"); + } +} \ No newline at end of file