From 80a05b7f736269be14444aff3ca1c32e26a34bc1 Mon Sep 17 00:00:00 2001 From: Eric Bariaux <375613+ebariaux@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:00:10 +0200 Subject: [PATCH 1/3] Extract function to create Url from host (domain), add tests and implement logic similar to iOS --- GenericApp/app/build.gradle | 4 + .../app/ui/HostSelectionFragment.kt | 7 +- .../java/io/openremote/app/util/UrlUtils.kt | 25 ++++ .../io/openremote/app/util/UrlUtilsTest.kt | 136 ++++++++++++++++++ 4 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 GenericApp/app/src/test/java/io/openremote/app/util/UrlUtilsTest.kt diff --git a/GenericApp/app/build.gradle b/GenericApp/app/build.gradle index 52886d0..855dcd5 100644 --- a/GenericApp/app/build.gradle +++ b/GenericApp/app/build.gradle @@ -74,6 +74,10 @@ dependencies { implementation 'com.google.firebase:firebase-messaging-ktx' implementation 'com.google.android.material:material:1.12.0' implementation project(':ORLib') + + // Unit testing framework + testImplementation 'junit:junit:4.13.2' + testImplementation "org.robolectric:robolectric:4.11.1" } task sourcesJar(type: Jar) { diff --git a/GenericApp/app/src/main/java/io/openremote/app/ui/HostSelectionFragment.kt b/GenericApp/app/src/main/java/io/openremote/app/ui/HostSelectionFragment.kt index 5c4141f..1331c73 100644 --- a/GenericApp/app/src/main/java/io/openremote/app/ui/HostSelectionFragment.kt +++ b/GenericApp/app/src/main/java/io/openremote/app/ui/HostSelectionFragment.kt @@ -51,12 +51,7 @@ class HostSelectionFragment : Fragment() { private fun connectToHost(host: String) { parentActivity.binding.progressBar.visibility = View.VISIBLE - val url = when { - URLUtil.isValidUrl(host) -> host.plus("/api/master") - UrlUtils.isIpAddress(host) -> "https://${host}/api/master" - !UrlUtils.startsWithHttp(host) && UrlUtils.endsWithTld(host) -> "https://${host}/api/master" - else -> "https://${host}.openremote.app/api/master" - } + val url = UrlUtils.hostToUrl(host).plus("/api/master") parentActivity.apiManager = ApiManager(url) parentActivity.apiManager.getConsoleConfig { statusCode, consoleConfig, error -> when (statusCode) { diff --git a/GenericApp/app/src/main/java/io/openremote/app/util/UrlUtils.kt b/GenericApp/app/src/main/java/io/openremote/app/util/UrlUtils.kt index 7d8f1e2..1376538 100644 --- a/GenericApp/app/src/main/java/io/openremote/app/util/UrlUtils.kt +++ b/GenericApp/app/src/main/java/io/openremote/app/util/UrlUtils.kt @@ -1,5 +1,7 @@ package io.openremote.app.util +import android.webkit.URLUtil + object UrlUtils { fun isIpAddress(url: String): Boolean { val ipPattern = Regex( @@ -7,14 +9,37 @@ object UrlUtils { ) return ipPattern.matches(url) } + + fun isIpV6NoScheme(url: String): Boolean { + val ipv6Pattern = Regex( + "^(?:([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6}))$" + ) + return ipv6Pattern.matches(url) + } + fun startsWithHttp(url: String): Boolean { return url.startsWith("http://") || url.startsWith("https://") } + fun startsWithScheme(url: String): Boolean { + val schemePattern = Regex("^[a-zA-Z]+://.*$") + return schemePattern.matches(url) + } + fun endsWithTld(url: String): Boolean { val tldPattern = Regex( "(?:[a-zA-Z]*\\.)+([a-zA-Z]+)(?:\\/.*)?" ) return tldPattern.matches(url) } + + fun hostToUrl(host: String): String { + return when { + isIpV6NoScheme(host) -> "https://[${host}]" + startsWithScheme(host) -> + if (host.contains(".") || host.contains("[")) host else "${host}.openremote.app" + (host.contains(".") || host.contains("[")) -> "https://${host}" + else -> "https://${host}.openremote.app" + } + } } \ No newline at end of file diff --git a/GenericApp/app/src/test/java/io/openremote/app/util/UrlUtilsTest.kt b/GenericApp/app/src/test/java/io/openremote/app/util/UrlUtilsTest.kt new file mode 100644 index 0000000..05dcb1c --- /dev/null +++ b/GenericApp/app/src/test/java/io/openremote/app/util/UrlUtilsTest.kt @@ -0,0 +1,136 @@ +import io.openremote.app.util.UrlUtils +import org.junit.Test +import org.junit.Assert.assertEquals + +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33], manifest=Config.NONE) +class UrlUtilsTest { + + @Test + fun fqdnWithScheme() { + assertEquals("http://www.example.com", UrlUtils.hostToUrl("http://www.example.com")) + assertEquals("https://www.example.com", UrlUtils.hostToUrl("https://www.example.com")) + } + + @Test + fun fqdnWithNonWebScheme() { + assertEquals("ftp://www.example.com", UrlUtils.hostToUrl("ftp://www.example.com")) + } + + @Test + fun fqdnNoScheme() { + assertEquals("https://www.example.com", UrlUtils.hostToUrl("www.example.com")) + } + + @Test + fun fqdnAndPortWithScheme() { + assertEquals("http://www.example.com:8080", UrlUtils.hostToUrl("http://www.example.com:8080")) + assertEquals("https://www.example.com:443", UrlUtils.hostToUrl("https://www.example.com:443")) + } + + @Test + fun fqdnAndPortWithNonWebScheme() { + assertEquals("ftp://www.example.com:21", UrlUtils.hostToUrl("ftp://www.example.com:21")) + } + + @Test + fun fqdnAndPortNoScheme() { + assertEquals("https://www.example.com:8080", UrlUtils.hostToUrl("www.example.com:8080")) + } + + @Test + fun hostnameNoScheme() { + assertEquals("https://example.openremote.app", UrlUtils.hostToUrl("example")) + } + + @Test + fun ipAddressWithScheme() { + assertEquals("http://192.168.1.1", UrlUtils.hostToUrl("http://192.168.1.1")) + } + + @Test + fun ipAddressWithNonWebScheme() { + assertEquals("ftp://192.168.1.1", UrlUtils.hostToUrl("ftp://192.168.1.1")) + } + + @Test + fun ipAddressAndPortWithScheme() { + assertEquals("http://192.168.1.1:8080", UrlUtils.hostToUrl("http://192.168.1.1:8080")) + } + + @Test + fun ipAddressAndPortWithNonWebScheme() { + assertEquals("ftp://192.168.1.1:25", UrlUtils.hostToUrl("ftp://192.168.1.1:25")) + } + + @Test + fun ipAddressAndInvalidPortWithScheme() { + assertEquals("http://192.168.1.1:InvalidPort", UrlUtils.hostToUrl("http://192.168.1.1:InvalidPort")) + } + + @Test + fun ipAddressNoScheme() { + assertEquals("https://192.168.1.1", UrlUtils.hostToUrl("192.168.1.1")) + } + + @Test + fun ipAddressAndPortNoScheme() { + assertEquals("https://192.168.1.1:8080", UrlUtils.hostToUrl("192.168.1.1:8080")) + } + + @Test + fun ipv6AddressWithScheme() { + assertEquals("http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + UrlUtils.hostToUrl("http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]")) + } + + @Test + fun ipv6AddressAndPortWithScheme() { + assertEquals("http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080", + UrlUtils.hostToUrl("http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080")) + } + + @Test + fun ipv6AddressNoScheme() { + assertEquals("https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + UrlUtils.hostToUrl("2001:0db8:85a3:0000:0000:8a2e:0370:7334")) + assertEquals("https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + UrlUtils.hostToUrl("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]")) + } + + @Test + fun ipv6AddressAndPortNoScheme() { + assertEquals("https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080", + UrlUtils.hostToUrl("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080")) + } + + @Test + fun ipv6CompressedAddressWithScheme() { + assertEquals("http://[2001:db8:85a3::8a2e:370:7334]", + UrlUtils.hostToUrl("http://[2001:db8:85a3::8a2e:370:7334]")) + } + + @Test + fun ipv6CompressedAddressAndPortWithScheme() { + assertEquals("http://[2001:db8:85a3::8a2e:370:7334]:8080", + UrlUtils.hostToUrl("http://[2001:db8:85a3::8a2e:370:7334]:8080")) + } + + @Test + fun ipv6CompressedAddressNoScheme() { + assertEquals("https://[2001:db8:85a3::8a2e:370:7334]", + UrlUtils.hostToUrl("2001:db8:85a3::8a2e:370:7334")) + assertEquals("https://[2001:db8:85a3::8a2e:370:7334]", + UrlUtils.hostToUrl("[2001:db8:85a3::8a2e:370:7334]")) + } + + @Test + fun ipv6CompressedAddressAndPortNoScheme() { + assertEquals("https://[2001:db8:85a3::8a2e:370:7334]:8080", + UrlUtils.hostToUrl("[2001:db8:85a3::8a2e:370:7334]:8080")) + } +} \ No newline at end of file From 8c44629297d30450fa3b2ba0ca5766453a878f3f Mon Sep 17 00:00:00 2001 From: Eric Bariaux <375613+ebariaux@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:01:29 +0200 Subject: [PATCH 2/3] Was using Roboelectric because original implementation used function from Android SDK, not the case anymore --- GenericApp/app/build.gradle | 1 - .../src/test/java/io/openremote/app/util/UrlUtilsTest.kt | 6 ------ 2 files changed, 7 deletions(-) diff --git a/GenericApp/app/build.gradle b/GenericApp/app/build.gradle index 855dcd5..1fd0ec2 100644 --- a/GenericApp/app/build.gradle +++ b/GenericApp/app/build.gradle @@ -77,7 +77,6 @@ dependencies { // Unit testing framework testImplementation 'junit:junit:4.13.2' - testImplementation "org.robolectric:robolectric:4.11.1" } task sourcesJar(type: Jar) { diff --git a/GenericApp/app/src/test/java/io/openremote/app/util/UrlUtilsTest.kt b/GenericApp/app/src/test/java/io/openremote/app/util/UrlUtilsTest.kt index 05dcb1c..e4488b7 100644 --- a/GenericApp/app/src/test/java/io/openremote/app/util/UrlUtilsTest.kt +++ b/GenericApp/app/src/test/java/io/openremote/app/util/UrlUtilsTest.kt @@ -2,12 +2,6 @@ import io.openremote.app.util.UrlUtils import org.junit.Test import org.junit.Assert.assertEquals -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -@Config(sdk = [33], manifest=Config.NONE) class UrlUtilsTest { @Test From 4e62d71dc556915f61b14313cf7ac4d63dfff121 Mon Sep 17 00:00:00 2001 From: Eric Bariaux <375613+ebariaux@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:03:29 +0200 Subject: [PATCH 3/3] Remove unused functions --- .../java/io/openremote/app/util/UrlUtils.kt | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/GenericApp/app/src/main/java/io/openremote/app/util/UrlUtils.kt b/GenericApp/app/src/main/java/io/openremote/app/util/UrlUtils.kt index 1376538..86fac3c 100644 --- a/GenericApp/app/src/main/java/io/openremote/app/util/UrlUtils.kt +++ b/GenericApp/app/src/main/java/io/openremote/app/util/UrlUtils.kt @@ -1,15 +1,6 @@ package io.openremote.app.util -import android.webkit.URLUtil - object UrlUtils { - fun isIpAddress(url: String): Boolean { - val ipPattern = Regex( - "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:\\d{1,5})?$" - ) - return ipPattern.matches(url) - } - fun isIpV6NoScheme(url: String): Boolean { val ipv6Pattern = Regex( "^(?:([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6}))$" @@ -17,22 +8,11 @@ object UrlUtils { return ipv6Pattern.matches(url) } - fun startsWithHttp(url: String): Boolean { - return url.startsWith("http://") || url.startsWith("https://") - } - fun startsWithScheme(url: String): Boolean { val schemePattern = Regex("^[a-zA-Z]+://.*$") return schemePattern.matches(url) } - fun endsWithTld(url: String): Boolean { - val tldPattern = Regex( - "(?:[a-zA-Z]*\\.)+([a-zA-Z]+)(?:\\/.*)?" - ) - return tldPattern.matches(url) - } - fun hostToUrl(host: String): String { return when { isIpV6NoScheme(host) -> "https://[${host}]"