diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index dc8efc2..c56ebfd 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -1,6 +1,6 @@ - + ConstructorParameterNaming:CreateConfig.kt$CreateConfig.Builder$var long_url: String FunctionParameterNaming:Bitlinks.kt$Bitlinks$bitlink_id: String @@ -42,7 +42,6 @@ VariableNaming:CreateConfig.kt$CreateConfig$val group_guid = builder.group_guid VariableNaming:CreateConfig.kt$CreateConfig$val long_url = builder.long_url VariableNaming:CreateConfig.kt$CreateConfig.Builder$var group_guid: String = Constants.EMPTY - WildcardImport:BitlyTest.kt$import assertk.assertions.* - WildcardImport:BitlyTest.kt$import kotlin.test.* + WildcardImport:BitlinksTest.kt$import assertk.assertions.* diff --git a/src/main/kotlin/net/thauvin/erik/bitly/Utils.kt b/src/main/kotlin/net/thauvin/erik/bitly/Utils.kt index 17441e0..dca27c2 100644 --- a/src/main/kotlin/net/thauvin/erik/bitly/Utils.kt +++ b/src/main/kotlin/net/thauvin/erik/bitly/Utils.kt @@ -174,8 +174,9 @@ object Utils { return false } + /** - * Removes http(s) scheme from string. + * Removes the `http` or `https` schemes from a string. */ @JvmStatic fun String.removeHttp(): String { @@ -183,7 +184,7 @@ object Utils { } /** - * Builds the full API endpoint URL using the [Constants.API_BASE_URL]. + * Converts a path to an API endpoint URL using the [Constants.API_BASE_URL], unless a URL is already specified. */ @JvmStatic fun String.toEndPoint(): String { diff --git a/src/test/kotlin/net/thauvin/erik/bitly/BitlinksTest.kt b/src/test/kotlin/net/thauvin/erik/bitly/BitlinksTest.kt new file mode 100644 index 0000000..66de791 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/bitly/BitlinksTest.kt @@ -0,0 +1,338 @@ +/* + * BitlinksTest.kt + * + * Copyright 2020-2025 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.bitly + +import DisableOnCi +import assertk.all +import assertk.assertThat +import assertk.assertions.* +import net.thauvin.erik.bitly.config.CreateConfig +import net.thauvin.erik.bitly.config.UpdateConfig +import net.thauvin.erik.bitly.config.deeplinks.CreateDeeplinks +import net.thauvin.erik.bitly.config.deeplinks.UpdateDeeplinks +import net.thauvin.erik.bitly.config.deeplinks.enums.InstallType +import net.thauvin.erik.bitly.config.deeplinks.enums.Os +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable +import java.io.File +import java.util.logging.Level +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class BitlinksTest { + private val bitly = with(File("local.properties")) { + if (exists()) { + Bitly(toPath()) + } else { + Bitly() + } + } + private val longUrl = "https://erik.thauvin.net/blog" + private val shortUrl = "https://bit.ly/380ojFd" + + companion object { + @JvmStatic + @BeforeAll + fun before() { + with(Utils.logger) { + level = Level.FINE + } + } + } + + @Nested + @DisplayName("Bitlinks Tests") + inner class BitlinksTests { + @Test + fun `Clicks summary`() { + val bl = bitly.bitlinks() + val clicks = bl.clicks(shortUrl, unit = Units.MONTH, units = 1) + assertThat(bl.lastCallResponse).all { + prop(CallResponse::description).isEmpty() + prop(CallResponse::isSuccessful).isTrue() + prop(CallResponse::statusCode).isEqualTo(200) + } + assertThat(clicks.toInt()).isGreaterThanOrEqualTo(0) + } + + @Test + fun `Clicks summary as json`() { + val bl = bitly.bitlinks() + val clicks = bl.clicks(shortUrl, toJson = true) + assertThat(bl.lastCallResponse).all { + prop(CallResponse::description).isEmpty() + prop(CallResponse::isSuccessful).isTrue() + prop(CallResponse::statusCode).isEqualTo(200) + } + assertThat(clicks).startsWith("{\"unit_reference\":") + } + } + + @Nested + @DisplayName("Create Bitlink Tests") + inner class CreateBitlinkTests { + @Test + fun `Create bitlink`() { + assertThat(bitly.bitlinks().create(long_url = longUrl), "create(longUrl)") + .matches("https://\\w+.\\w{2}/\\w{7}".toRegex()) + assertEquals( + shortUrl, + bitly.bitlinks().create( + domain = "bit.ly", + title = "Erik's Blog", + tags = arrayOf("erik", "thauvin", "blog", "weblog"), + long_url = longUrl + ) + ) + } + + @Test + fun `Create bitlink with config`() { + var config = CreateConfig.Builder(longUrl).build() + assertThat(bitly.bitlinks().create(config), "create(config)") + .matches("https://\\w+.\\w{2}/\\w{7}".toRegex()) + + config = CreateConfig.Builder(longUrl) + .domain("bit.ly") + .title("Erik's Blog") + .tags(arrayOf("erik", "thauvin", "blog", "weblog")) + .build() + assertEquals( + shortUrl, + bitly.bitlinks().create(config) + ) + } + + @Test + fun `Create bitlink with deep links`() { + val bl = bitly.bitlinks() + val dl = CreateDeeplinks().apply { + app_uri_path("/store?id=123456") + install_type(InstallType.NO_INSTALL) + install_url("https://play.google.com/store/apps/details?id=com.bitly.app&hl=en_US") + } + + val config = CreateConfig.Builder(longUrl) + .deeplinks(dl) + .domain("bit.ly") + .build() + + assertThat(bl.create(config)).isEqualTo(Constants.EMPTY) + assertThat(bl.lastCallResponse.isUpgradeRequired).isTrue() + } + } + + @Nested + @DisplayName("Expand Test") + inner class ExpandTests { + @Test + fun `Expand as json`() { + assertTrue( + bitly.bitlinks().expand(shortUrl, toJson = true) + .startsWith("{\"created_at\":") + ) + } + + @Test + fun `Expand link`() { + assertEquals(longUrl, Bitlinks(bitly.accessToken).expand(shortUrl)) + } + } + + @Nested + @DisplayName("Shorten Tests") + inner class ShortenTests { + @Test + fun `Shorten as json`() { + assertTrue( + bitly.bitlinks().shorten(longUrl, toJson = true) + .startsWith("{\"created_at\":") + ) + } + + @Test + fun `Shorten last call response`() { + val bl = Bitlinks(bitly.accessToken) + bl.shorten(longUrl, domain = "bit.ly") + assertThat(bl.lastCallResponse, "shorten(longUrl)").all { + prop(CallResponse::body).contains("\"link\":\"$shortUrl\"") + prop(CallResponse::isSuccessful).isTrue() + prop(CallResponse::message).isEmpty() + prop(CallResponse::statusCode).isEqualTo(200) + } + + bl.shorten(shortUrl) + assertThat(bl.lastCallResponse, "shorten(shortUrl)").all { + prop(CallResponse::description).isEqualTo("The value provided is invalid.") + prop(CallResponse::isBadRequest).isTrue() + prop(CallResponse::isSuccessful).isFalse() + prop(CallResponse::message).isEqualTo("ALREADY_A_BITLY_LINK") + prop(CallResponse::statusCode).isEqualTo(400) + } + } + + @Test + fun `Shorten link`() { + assertEquals( + shortUrl, Bitlinks(bitly.accessToken) + .shorten(longUrl, domain = "bit.ly") + ) + } + + @Test + fun `Shorten with invalid domain`() { + val bl = bitly.bitlinks() + bl.shorten("https://www.examples.com", domain = "foo.com") + assertThat(bl.lastCallResponse).all { + prop(CallResponse::description).contains("invalid") + prop(CallResponse::isBadRequest).isTrue() + prop(CallResponse::isSuccessful).isFalse() + prop(CallResponse::message).isEqualTo("INVALID_ARG_DOMAIN") + } + } + } + + @Nested + @DisplayName("Update Bitlink Tests") + inner class UpdateBitlinkTests { + @Test + fun `Update bitlink`() { + val bl = bitly.bitlinks() + assertEquals( + Constants.TRUE, + bl.update( + shortUrl, title = "Erik's Weblog", tags = arrayOf("blog", "weblog"), archived = true + ) + ) + + assertThat(bl.update(shortUrl, tags = emptyArray(), toJson = true), "update(tags)") + .contains("\"tags\":[]") + } + + @Test + fun `Update bitlink with config`() { + val bl = bitly.bitlinks() + var config = UpdateConfig.Builder(shortUrl) + .archived(true) + .tags(arrayOf("blog", "weblog")) + .title("Erik's Weblog") + .build() + + assertEquals(Constants.TRUE, bl.update(config)) + + config = UpdateConfig.Builder(shortUrl) + .toJson(true) + .build() + + assertThat(bl.update(config), "update(tags)").contains("\"tags\":[]") + } + + @Test + fun `Update bitlink with deep links`() { + val bl = bitly.bitlinks() + val dl = UpdateDeeplinks().apply { + os(Os.ANDROID) + brand_guid("Ba1bc23dE4F") + } + val config = UpdateConfig.Builder(shortUrl) + .deeplinks(dl) + .build() + + assertThat(bl.update(config)).isEqualTo(Constants.FALSE) + assertThat(bl.lastCallResponse.isUpgradeRequired).isTrue() + } + + @Test + @DisableOnCi + fun `Update custom bitlink`() { + val bl = bitly.bitlinks() + assertEquals( + Constants.TRUE, + bl.updateCustom("https://thauv.in/2NwtljT", "thauv.in/2NwtljT") + ) + } + } + + @Nested + @DisplayName("Validation Tests") + inner class ValidationTests { + @Test + fun `Empty URL should not shorten`() { + assertEquals(Constants.EMPTY, bitly.bitlinks().shorten(Constants.EMPTY)) + } + + @Test + fun `Short URL should not shorten`() { + assertEquals(shortUrl, bitly.bitlinks().shorten(shortUrl)) + } + + @Test + fun `Token not specified with API call`() { + assertFailsWith(IllegalArgumentException::class, "Utils.call()") { + Utils.call("", "foo") + } + } + + @Test + @DisableOnCi + fun `Token not specified`() { + val test = Bitly() + + assertFailsWith(IllegalArgumentException::class) { + test.bitlinks().shorten(longUrl) + } + } + + @Test + @EnabledIfEnvironmentVariable(named = "CI", matches = "true") + fun `Token not specified on CI`() { + val test = Bitly(Constants.EMPTY) // to void picking up the environment variable + + assertFailsWith(IllegalArgumentException::class) { + test.bitlinks().shorten(longUrl) + } + } + + @Test + fun `Token should be valid`() { + val test = Bitly().apply { accessToken = "12345679" } + assertEquals( + "{\"message\":\"FORBIDDEN\"}", + test.bitlinks().shorten("https://erik.thauvin.net/blog", toJson = true) + ) + } + } +} diff --git a/src/test/kotlin/net/thauvin/erik/bitly/BitlyTest.kt b/src/test/kotlin/net/thauvin/erik/bitly/BitlyTest.kt index 2af5f86..3ad00cf 100644 --- a/src/test/kotlin/net/thauvin/erik/bitly/BitlyTest.kt +++ b/src/test/kotlin/net/thauvin/erik/bitly/BitlyTest.kt @@ -31,24 +31,22 @@ package net.thauvin.erik.bitly -import DisableOnCi import assertk.all import assertk.assertThat -import assertk.assertions.* -import net.thauvin.erik.bitly.Utils.isValidUrl +import assertk.assertions.contains +import assertk.assertions.isTrue +import assertk.assertions.prop import net.thauvin.erik.bitly.Utils.removeHttp import net.thauvin.erik.bitly.Utils.toEndPoint -import net.thauvin.erik.bitly.config.CreateConfig -import net.thauvin.erik.bitly.config.UpdateConfig -import net.thauvin.erik.bitly.config.deeplinks.CreateDeeplinks -import net.thauvin.erik.bitly.config.deeplinks.UpdateDeeplinks -import net.thauvin.erik.bitly.config.deeplinks.enums.InstallType -import net.thauvin.erik.bitly.config.deeplinks.enums.Os import org.json.JSONObject import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested import java.io.File import java.util.logging.Level -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class BitlyTest { private val bitly = with(File("local.properties")) { @@ -58,260 +56,8 @@ class BitlyTest { Bitly() } } - private val longUrl = "https://erik.thauvin.net/blog" private val shortUrl = "https://bit.ly/380ojFd" - @Test - fun `token should be specified`() { - val test = Bitly() - if (System.getenv("CI") == "true") { - test.accessToken = Constants.EMPTY - } - assertFailsWith(IllegalArgumentException::class) { - test.bitlinks().shorten(longUrl) - } - assertFailsWith(IllegalArgumentException::class, "Utils.call()") { - Utils.call("", "foo") - } - } - - @Test - fun `token should be valid`() { - val test = Bitly().apply { accessToken = "12345679" } - assertEquals( - "{\"message\":\"FORBIDDEN\"}", - test.bitlinks().shorten("https://erik.thauvin.net/blog", toJson = true) - ) - } - - @Test - fun `long url should be valid`() { - assertEquals(Constants.EMPTY, bitly.bitlinks().shorten(Constants.EMPTY)) - } - - @Test - fun `long url should not be short`() { - assertEquals(shortUrl, bitly.bitlinks().shorten(shortUrl)) - } - - @Test - fun `endPoint should be specified`() { - assertFailsWith(IllegalArgumentException::class, "bitly.call()") { - bitly.call("") - } - assertFailsWith(IllegalArgumentException::class, "Utils.call()") { - Utils.call("1234568", "") - } - } - - @Test - fun `endPoint conversion`() { - assertThat(Constants.API_BASE_URL.toEndPoint()).isEqualTo(Constants.API_BASE_URL) - assertThat("path".toEndPoint()).isEqualTo("${Constants.API_BASE_URL}/path") - assertThat("/path".toEndPoint()).isEqualTo("${Constants.API_BASE_URL}/path") - } - - @Test - fun `shorten = expand`() { - val shortUrl = bitly.bitlinks().shorten(longUrl, domain = "bit.ly") - assertEquals(longUrl, bitly.bitlinks().expand(shortUrl)) - } - - @Test - fun `shorten as json`() { - assertTrue(bitly.bitlinks().shorten(longUrl, toJson = true).startsWith("{\"created_at\":")) - } - - @Test - fun `get user`() { - assertThat(bitly.call("user", method = Methods.GET), "call(user)") - .prop(CallResponse::isSuccessful).isTrue() - assertThat(Utils.call(bitly.accessToken, "user".toEndPoint(), method = Methods.GET), "call(/user)").all { - prop(CallResponse::isSuccessful).isTrue() - prop(CallResponse::body).contains("login") - } - } - - @Test - fun `created by`() { - assertEquals( - "ethauvin", - JSONObject( - bitly.call( - "/bitlinks/${shortUrl.removeHttp()}", - method = Methods.GET - ).body - ).getString("created_by") - ) - } - - @Test - fun `bitlinks shorten`() { - assertEquals(shortUrl, Bitlinks(bitly.accessToken).shorten(longUrl, domain = "bit.ly")) - } - - @Test - fun `bitlinks expand`() { - assertEquals(longUrl, Bitlinks(bitly.accessToken).expand(shortUrl)) - } - - @Test - fun `bitlinks lastCallResponse`() { - val bl = Bitlinks(bitly.accessToken) - bl.shorten(longUrl, domain = "bit.ly") - assertThat(bl.lastCallResponse, "shorten(longUrl)").all { - prop(CallResponse::isSuccessful).isTrue() - prop(CallResponse::statusCode).isEqualTo(200) - prop(CallResponse::body).contains("\"link\":\"$shortUrl\"") - prop(CallResponse::message).isEmpty() - } - - bl.shorten(shortUrl) - assertThat(bl.lastCallResponse, "shorten(shortUrl)").all { - prop(CallResponse::isSuccessful).isFalse() - prop(CallResponse::statusCode).isEqualTo(400) - prop(CallResponse::isBadRequest).isTrue() - prop(CallResponse::message).isEqualTo("ALREADY_A_BITLY_LINK") - prop(CallResponse::description).isEqualTo("The value provided is invalid.") - } - } - - @Test - fun `clicks summary`() { - val bl = bitly.bitlinks() - assertThat(bl.clicks(shortUrl)).isNotEqualTo(Constants.EMPTY) - val clicks = bl.clicks(shortUrl, unit = Units.MONTH, units = 1) - assertThat(bl.lastCallResponse).all { - prop(CallResponse::isSuccessful).isTrue() - prop(CallResponse::statusCode).isEqualTo(200) - prop(CallResponse::description).isEmpty() - } - assertThat(clicks.toInt()).isGreaterThanOrEqualTo(0) - } - - @Test - fun `create bitlink`() { - assertThat(bitly.bitlinks().create(long_url = longUrl), "create(longUrl)") - .matches("https://\\w+.\\w{2}/\\w{7}".toRegex()) - assertEquals( - shortUrl, - bitly.bitlinks().create( - domain = "bit.ly", - title = "Erik's Blog", - tags = arrayOf("erik", "thauvin", "blog", "weblog"), - long_url = longUrl - ) - ) - } - - @Test - fun `create bitlink with config`() { - var config = CreateConfig.Builder(longUrl).build() - assertThat(bitly.bitlinks().create(config), "create(config)") - .matches("https://\\w+.\\w{2}/\\w{7}".toRegex()) - - config = CreateConfig.Builder(longUrl) - .domain("bit.ly") - .title("Erik's Blog") - .tags(arrayOf("erik", "thauvin", "blog", "weblog")) - .build() - assertEquals( - shortUrl, - bitly.bitlinks().create(config) - ) - } - - @Test - fun `create bitlink with deeplinks`() { - val bl = bitly.bitlinks() - val dl = CreateDeeplinks().apply { - install_type(InstallType.NO_INSTALL) - app_uri_path("/store?id=123456") - install_url("https://play.google.com/store/apps/details?id=com.bitly.app&hl=en_US") - } - - val config = CreateConfig.Builder(longUrl) - .domain("bit.ly") - .deeplinks(dl) - .build() - - assertThat(bl.create(config)).isEqualTo(Constants.EMPTY) - assertThat(bl.lastCallResponse.isUpgradeRequired).isTrue() - } - - @Test - fun `shorten with invalid domain`() { - val bl = bitly.bitlinks() - bl.shorten("https://www.examples.com", domain = "foo.com") - assertThat(bl.lastCallResponse).all { - prop(CallResponse::isSuccessful).isFalse() - prop(CallResponse::isBadRequest).isTrue() - prop(CallResponse::message).isEqualTo("INVALID_ARG_DOMAIN") - prop(CallResponse::description).contains("invalid") - } - } - - @Test - @DisableOnCi - fun `update custom bitlink`() { - val bl = bitly.bitlinks() - assertEquals( - Constants.TRUE, - bl.updateCustom("https://thauv.in/2NwtljT", "thauv.in/2NwtljT") - ) - } - - @Test - fun `update bitlink`() { - val bl = bitly.bitlinks() - assertEquals( - Constants.TRUE, - bl.update(shortUrl, title = "Erik's Weblog", tags = arrayOf("blog", "weblog"), archived = true) - ) - - assertThat(bl.update(shortUrl, tags = emptyArray(), toJson = true), "update(tags)") - .contains("\"tags\":[]") - } - - @Test - fun `update bitlink with config`() { - val bl = bitly.bitlinks() - var config = UpdateConfig.Builder(shortUrl) - .title("Erik's Weblog") - .tags(arrayOf("blog", "weblog")) - .archived(true) - .build() - - assertEquals(Constants.TRUE, bl.update(config)) - - config = UpdateConfig.Builder(shortUrl) - .toJson(true) - .build() - - assertThat(bl.update(config), "update(tags)").contains("\"tags\":[]") - } - - @Test - fun `update bitlink with deeplinks`() { - val bl = bitly.bitlinks() - val dl = UpdateDeeplinks().apply { - os(Os.ANDROID) - brand_guid("Ba1bc23dE4F") - } - val config = UpdateConfig.Builder(shortUrl) - .deeplinks(dl) - .build() - - assertThat(bl.update(config)).isEqualTo(Constants.FALSE) - assertThat(bl.lastCallResponse.isUpgradeRequired).isTrue() - } - - @Test - fun `validate URL`() { - assertTrue("https://www.example.com".isValidUrl(), "valid url") - assertFalse("this is a test".isValidUrl(), "invalid url") - } - companion object { @JvmStatic @BeforeAll @@ -321,4 +67,46 @@ class BitlyTest { } } } + + + @Nested + @DisplayName("API Call Tests") + inner class ApiCallTests { + @Test + fun `Created by`() { + assertEquals( + "ethauvin", + JSONObject( + bitly.call( + "/bitlinks/${shortUrl.removeHttp()}", + method = Methods.GET + ).body + ).getString("created_by") + ) + } + + @Test + fun `EndPoint should be specified`() { + assertFailsWith(IllegalArgumentException::class, "bitly.call()") { + bitly.call("") + } + assertFailsWith(IllegalArgumentException::class, "Utils.call()") { + Utils.call("1234568", "") + } + } + + @Test + fun `Get user`() { + assertThat(bitly.call("user", method = Methods.GET), "call(user)") + .prop(CallResponse::isSuccessful).isTrue() + assertThat( + Utils.call( + bitly.accessToken, "user".toEndPoint(), method = Methods.GET + ), "call(/user)" + ).all { + prop(CallResponse::isSuccessful).isTrue() + prop(CallResponse::body).contains("login") + } + } + } } diff --git a/src/test/kotlin/net/thauvin/erik/bitly/UtilsTests.kt b/src/test/kotlin/net/thauvin/erik/bitly/UtilsTests.kt new file mode 100644 index 0000000..a9a878b --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/bitly/UtilsTests.kt @@ -0,0 +1,114 @@ +/* + * UtilsTests.kt + * + * Copyright 2020-2025 Erik C. Thauvin (erik@thauvin.net) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.bitly + +import assertk.assertThat +import assertk.assertions.isEqualTo +import net.thauvin.erik.bitly.Utils.isValidUrl +import net.thauvin.erik.bitly.Utils.removeHttp +import net.thauvin.erik.bitly.Utils.toEndPoint +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class UtilsTests { + @Nested + @DisplayName("Endpoint Conversion Tests") + inner class EndpointConversionTests { + @Test + fun `Convert endpoint with empty string`() { + assertThat("".toEndPoint()).isEqualTo("") + } + + @Test + fun `Convert endpoint with full URL`() { + assertThat("https://example.com/path".toEndPoint()).isEqualTo("https://example.com/path") + } + + @Test + fun `Convert endpoint with leading slash`() { + assertThat("/path".toEndPoint()).isEqualTo("${Constants.API_BASE_URL}/path") + } + + @Test + fun `Convert endpoint with multiple path segments`() { + assertThat("/existing/path".toEndPoint()).isEqualTo("${Constants.API_BASE_URL}/existing/path") + } + + @Test + fun `Convert endpoint with no leading slash`() { + assertThat("path".toEndPoint()).isEqualTo("${Constants.API_BASE_URL}/path") + } + + @Test + fun `Convert endpoint with trailing slash`() { + assertThat("path/".toEndPoint()).isEqualTo("${Constants.API_BASE_URL}/path/") + } + } + + @Nested + @DisplayName("Remote HTTP Tests") + inner class RemoteHTTPTests { + @Test + @Suppress("HttpUrlsUsage") + fun `Remove HTTP`() { + assertThat("http://example.com".removeHttp()).isEqualTo("example.com") + } + + @Test + fun `Remove HTTPS`() { + assertThat("https://example.com".removeHttp()).isEqualTo("example.com") + } + + @Test + fun `Remove mixed case`() { + assertThat("HtTPs://EXAMPLE.Com".removeHttp()).isEqualTo("EXAMPLE.Com") + } + + @Test + fun `Remove no scheme`() { + assertThat("example.com".removeHttp()).isEqualTo("example.com") + } + } + + @Test + fun `Validate invalid URL`() { + assertFalse("this is a test".isValidUrl(), "invalid url") + } + + @Test + fun `Validate URL`() { + assertTrue("https://www.example.com".isValidUrl(), "valid url") + } +} diff --git a/src/test/kotlin/net/thauvin/erik/bitly/config/ConfigTest.kt b/src/test/kotlin/net/thauvin/erik/bitly/config/ConfigTest.kt index 1472d1e..53debb4 100644 --- a/src/test/kotlin/net/thauvin/erik/bitly/config/ConfigTest.kt +++ b/src/test/kotlin/net/thauvin/erik/bitly/config/ConfigTest.kt @@ -31,49 +31,69 @@ package net.thauvin.erik.bitly.config +import assertk.all import assertk.assertThat import assertk.assertions.isEqualTo +import assertk.assertions.isTrue +import assertk.assertions.prop +import net.thauvin.erik.bitly.Constants import net.thauvin.erik.bitly.config.deeplinks.CreateDeeplinks import net.thauvin.erik.bitly.config.deeplinks.UpdateDeeplinks import net.thauvin.erik.bitly.config.deeplinks.enums.InstallType import net.thauvin.erik.bitly.config.deeplinks.enums.Os import org.json.JSONObject +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested import kotlin.test.Test class ConfigTest { - @Test - fun `create config test`() { - val deeplinks = CreateDeeplinks().apply { - app_id("app_id") - install_type(InstallType.AUTO_INSTALL) - } + @Nested + @DisplayName("Build Configuration Tests") + inner class BuildConfigurationTests { + @Test + fun `Build create configuration`() { + val deeplinks = CreateDeeplinks().apply { + app_id("app_id") + install_type(InstallType.AUTO_INSTALL) + } - val config = CreateConfig.Builder("long_url") - .domain("domain") - .groupGuid("group_guid") - .title("title") - .tags(arrayOf("tag", "tag2")) - .deeplinks(deeplinks) - .build() + val config = CreateConfig.Builder("long_url") + .deeplinks(deeplinks) + .domain("domain") + .groupGuid("group_guid") + .tags(arrayOf("tag", "tag2")) + .title("title") + .build() - val map = mapOf( - "long_url" to config.long_url, - "domain" to config.domain, - "group_guid" to config.group_guid, - "title" to config.title, - "tags" to config.tags, - "deeplinks" to arrayOf(deeplinks.links()) - ) + assertThat(config).all { + prop(CreateConfig::deeplinks).isEqualTo(deeplinks) + prop(CreateConfig::domain).isEqualTo("domain") + prop(CreateConfig::group_guid).isEqualTo("group_guid") + prop(CreateConfig::long_url).isEqualTo("long_url") + prop(CreateConfig::tags).isEqualTo(arrayOf("tag", "tag2")) + prop(CreateConfig::title).isEqualTo("title") + prop(CreateConfig::toJson).isEqualTo(false) + } - assertThat(JSONObject(map).toString()).isEqualTo( - """ - {"group_guid":"group_guid","long_url":"long_url","title":"title","deeplinks":[{"app_id":"app_id","install_type":"auto_install"}],"domain":"domain","tags":["tag","tag2"]} + val map = mapOf( + "deeplinks" to arrayOf(deeplinks.links()), + "domain" to config.domain, + "group_guid" to config.group_guid, + "long_url" to config.long_url, + "tags" to config.tags, + "title" to config.title + ) + + assertThat(JSONObject(map).toString()).isEqualTo( + """ + {"group_guid":"group_guid","deeplinks":[{"app_id":"app_id","install_type":"auto_install"}],"long_url":"long_url","title":"title","domain":"domain","tags":["tag","tag2"]} """.trimIndent() - ) + ) + } } @Test - fun `update config test`() { + fun `Build update configuration`() { val deeplinks = UpdateDeeplinks().apply { os(Os.IOS) install_type(InstallType.PROMOTE_INSTALL) @@ -81,25 +101,123 @@ class ConfigTest { } val config = UpdateConfig.Builder("blink") - .title("title") .archived(true) - .tags(arrayOf("tag", "tag2")) .deeplinks(deeplinks) + .tags(arrayOf("tag", "tag2")) + .title("title") .build() + assertThat(config).all { + prop(UpdateConfig::archived).isTrue() + prop(UpdateConfig::bitlink).isEqualTo("blink") + prop(UpdateConfig::deeplinks).isEqualTo(deeplinks) + prop(UpdateConfig::tags).isEqualTo(arrayOf("tag", "tag2")) + prop(UpdateConfig::title).isEqualTo("title") + prop(UpdateConfig::toJson).isEqualTo(false) + } + val map = mapOf( - "bitlink" to config.bitlink, - "title" to config.title, "archived" to config.archived, + "bitlink" to config.bitlink, + "deeplinks" to arrayOf(deeplinks.links()), "tags" to config.tags, - "deeplinks" to arrayOf(deeplinks.links()) + "title" to config.title ) assertThat(JSONObject(map).toString()).isEqualTo( """ - {"archived":true,"bitlink":"blink","title":"title","deeplinks":[{"os":"ios","app_guid":"app_guid","install_type":"promote_install"}],"tags":["tag","tag2"]} + {"archived":true,"bitlink":"blink","deeplinks":[{"os":"ios","app_guid":"app_guid","install_type":"promote_install"}],"title":"title","tags":["tag","tag2"]} """.trimIndent() ) + } + @Nested + @DisplayName("Validate Configuration Tests") + inner class ValidateConfigurationTests { + @Test + fun `Validate create configuration`() { + val deeplinks = CreateDeeplinks().apply { + app_id("app_id") + install_type(InstallType.AUTO_INSTALL) + } + + val config = CreateConfig.Builder("long_url") + .deeplinks(deeplinks) + .domain("domain") + .groupGuid("group_guid") + .tags(arrayOf("tag", "tag2")) + .title("title") + .toJson(true) + + assertThat(config).all { + prop(CreateConfig.Builder::deeplinks).prop(CreateDeeplinks::links).isEqualTo(deeplinks.links()) + prop(CreateConfig.Builder::domain).isEqualTo("domain") + prop(CreateConfig.Builder::group_guid).isEqualTo("group_guid") + prop(CreateConfig.Builder::long_url).isEqualTo("long_url") + prop(CreateConfig.Builder::tags).isEqualTo(arrayOf("tag", "tag2")) + prop(CreateConfig.Builder::title).isEqualTo("title") + prop(CreateConfig.Builder::toJson).isTrue() + } + + config.longUrl("longer_url") + assertThat(config).prop(CreateConfig.Builder::long_url).isEqualTo("longer_url") + } + + @Test + fun `Validate create default configuration`() { + val config = CreateConfig.Builder("long_url") + + assertThat(config).all { + prop(CreateConfig.Builder::long_url).isEqualTo("long_url") + prop(CreateConfig.Builder::domain).isEqualTo(Constants.EMPTY) + prop(CreateConfig.Builder::group_guid).isEqualTo(Constants.EMPTY) + prop(CreateConfig.Builder::title).isEqualTo(Constants.EMPTY) + prop(CreateConfig.Builder::tags).isEqualTo(emptyArray()) + prop(CreateConfig.Builder::deeplinks).prop(CreateDeeplinks::links).isEqualTo(CreateDeeplinks().links()) + prop(CreateConfig.Builder::toJson).isEqualTo(false) + } + } + + @Test + fun `Validate update configuration`() { + val deeplinks = UpdateDeeplinks().apply { + os(Os.IOS) + install_type(InstallType.PROMOTE_INSTALL) + app_guid("app_guid") + } + + val config = UpdateConfig.Builder("bitlink") + .title("title") + .archived(true) + .tags(arrayOf("tag", "tag2")) + .deeplinks(deeplinks) + .toJson(true) + + assertThat(config).all { + prop(UpdateConfig.Builder::bitlink).isEqualTo("bitlink") + prop(UpdateConfig.Builder::title).isEqualTo("title") + prop(UpdateConfig.Builder::archived).isTrue() + prop(UpdateConfig.Builder::tags).isEqualTo(arrayOf("tag", "tag2")) + prop(UpdateConfig.Builder::deeplinks).isEqualTo(deeplinks) + prop(UpdateConfig.Builder::toJson).isTrue() + } + + config.bitlink("blink") + assertThat(config).prop(UpdateConfig.Builder::bitlink).isEqualTo("blink") + } + + @Test + fun `Validate update default configuration`() { + val config = UpdateConfig.Builder("bitlink") + + assertThat(config).all { + prop(UpdateConfig.Builder::bitlink).isEqualTo("bitlink") + prop(UpdateConfig.Builder::title).isEqualTo(Constants.EMPTY) + prop(UpdateConfig.Builder::archived).isEqualTo(false) + prop(UpdateConfig.Builder::tags).isEqualTo(emptyArray()) + prop(UpdateConfig.Builder::deeplinks).prop(UpdateDeeplinks::links).isEqualTo(UpdateDeeplinks().links()) + prop(UpdateConfig.Builder::toJson).isEqualTo(false) + } + } } } diff --git a/src/test/kotlin/net/thauvin/erik/bitly/config/deeplinks/DeeplinksTest.kt b/src/test/kotlin/net/thauvin/erik/bitly/config/deeplinks/DeeplinksTest.kt index c5dff39..9a0fcb4 100644 --- a/src/test/kotlin/net/thauvin/erik/bitly/config/deeplinks/DeeplinksTest.kt +++ b/src/test/kotlin/net/thauvin/erik/bitly/config/deeplinks/DeeplinksTest.kt @@ -31,11 +31,11 @@ package net.thauvin.erik.bitly.config.deeplinks +import assertk.all import assertk.assertThat -import assertk.assertions.contains -import assertk.assertions.doesNotContain import assertk.assertions.isEqualTo import assertk.assertions.isNull +import assertk.assertions.prop import net.thauvin.erik.bitly.config.deeplinks.enums.InstallType import net.thauvin.erik.bitly.config.deeplinks.enums.Os import org.json.JSONObject @@ -45,88 +45,92 @@ import java.time.ZonedDateTime class DeeplinksTest { @Test - fun `create test`() { + fun `Create deeplink`() { val deeplinks = CreateDeeplinks().apply { app_uri_path("app_uri_path") install_type(InstallType.NO_INSTALL) } - assertThat(deeplinks.install_url()).isNull() - deeplinks.install_url("install_url") - - assertThat(deeplinks.app_uri_path()).isEqualTo("app_uri_path") - assertThat(deeplinks.install_type()).isEqualTo(InstallType.NO_INSTALL) + assertThat(deeplinks).all { + prop(CreateDeeplinks::app_id).isNull() + prop(CreateDeeplinks::app_uri_path).isEqualTo("app_uri_path") + prop(CreateDeeplinks::install_type).isEqualTo(InstallType.NO_INSTALL) + prop(CreateDeeplinks::install_url).isNull() + prop(CreateDeeplinks::links).isEqualTo(deeplinks.links()) + } assertThat(JSONObject(deeplinks.links()).toString()).isEqualTo( """ - {"app_uri_path":"app_uri_path","install_type":"no_install","install_url":"install_url"} + {"app_uri_path":"app_uri_path","install_type":"no_install"} """.trimIndent() ) - deeplinks.install_type(InstallType.PROMOTE_INSTALL) deeplinks.app_id("app_id") + deeplinks.install_type(InstallType.PROMOTE_INSTALL) + deeplinks.install_url("install_url") - assertThat(deeplinks.app_id()).isEqualTo("app_id") - - assertThat(JSONObject(deeplinks.links()).toString()).apply { - doesNotContain(InstallType.NO_INSTALL.type) - contains(InstallType.PROMOTE_INSTALL.type) - contains("\"app_id\":\"app_id\"") + assertThat(deeplinks).all { + prop(CreateDeeplinks::app_id).isEqualTo("app_id") + prop(CreateDeeplinks::install_type).isEqualTo(InstallType.PROMOTE_INSTALL) + prop(CreateDeeplinks::install_url).isEqualTo("install_url") } + + assertThat(JSONObject(deeplinks.links()).toString()).isEqualTo( + """ + {"install_url":"install_url","app_id":"app_id","app_uri_path":"app_uri_path","install_type":"promote_install"} + """.trimIndent() + ) } @Test - fun `update test`() { + fun `Update deeplink`() { val deeplinks = UpdateDeeplinks().apply { app_guid("app_guid") - os(Os.IOS) - install_type(InstallType.NO_INSTALL) - guid("guid") - install_url("install_url") app_uri_path("app_uri_path") created("created") + guid("guid") + install_type(InstallType.NO_INSTALL) + install_url("install_url") modified("modified") + os(Os.IOS) } - assertThat(deeplinks.brand_guid()).isNull() + assertThat(deeplinks).all { + prop(UpdateDeeplinks::app_guid).isEqualTo("app_guid") + prop(UpdateDeeplinks::app_uri_path).isEqualTo("app_uri_path") + prop(UpdateDeeplinks::bitlink).isNull() + prop(UpdateDeeplinks::brand_guid).isNull() + prop(UpdateDeeplinks::created).isEqualTo("created") + prop(UpdateDeeplinks::guid).isEqualTo("guid") + prop(UpdateDeeplinks::install_type).isEqualTo(InstallType.NO_INSTALL) + prop(UpdateDeeplinks::install_url).isEqualTo("install_url") + prop(UpdateDeeplinks::links).isEqualTo(deeplinks.links()) + prop(UpdateDeeplinks::modified).isEqualTo("modified") + prop(UpdateDeeplinks::os).isEqualTo(Os.IOS) + } + + val zdt = ZonedDateTime.of(1997, 8, 29, 2, 14, 0, 0, ZoneId.of("US/Eastern")) + + deeplinks.bitlink("bitlink") deeplinks.brand_guid("brand_guid") + deeplinks.created(zdt) + deeplinks.install_type(InstallType.PROMOTE_INSTALL) + deeplinks.modified(zdt) + deeplinks.os(Os.ANDROID) - assertThat(deeplinks.app_uri_path()).isEqualTo("app_uri_path") - assertThat(deeplinks.install_url()).isEqualTo("install_url") - - assertThat(deeplinks.os()).isEqualTo(Os.IOS) - assertThat(deeplinks.install_type()).isEqualTo(InstallType.NO_INSTALL) - assertThat(deeplinks.app_guid()).isEqualTo("app_guid") - assertThat(deeplinks.modified()).isEqualTo("modified") - assertThat(deeplinks.brand_guid()).isEqualTo("brand_guid") - + assertThat(deeplinks).all { + prop(UpdateDeeplinks::bitlink).isEqualTo("bitlink") + prop(UpdateDeeplinks::brand_guid).isEqualTo("brand_guid") + prop(UpdateDeeplinks::created).isEqualTo("1997-08-29T02:14:00-0400") + prop(UpdateDeeplinks::install_type).isEqualTo(InstallType.PROMOTE_INSTALL) + prop(UpdateDeeplinks::modified).isEqualTo("1997-08-29T02:14:00-0400") + prop(UpdateDeeplinks::os).isEqualTo(Os.ANDROID) + } assertThat(JSONObject(deeplinks.links()).toString()).isEqualTo( """ - {"app_guid":"app_guid","install_url":"install_url","os":"ios","app_uri_path":"app_uri_path","created":"created","brand_guid":"brand_guid","guid":"guid","modified":"modified","install_type":"no_install"} + {"app_guid":"app_guid","install_url":"install_url","bitlink":"bitlink","os":"android","app_uri_path":"app_uri_path","created":"1997-08-29T02:14:00-0400","brand_guid":"brand_guid","guid":"guid","modified":"1997-08-29T02:14:00-0400","install_type":"promote_install"} """.trimIndent() ) - - deeplinks.install_type(InstallType.PROMOTE_INSTALL) - deeplinks.os(Os.ANDROID) - deeplinks.bitlink("bitlink") - - val zdt = ZonedDateTime.of(1997, 8, 29, 2, 14, 0, 0, ZoneId.of("US/Eastern")) - deeplinks.modified(zdt) - deeplinks.created(zdt) - - assertThat(deeplinks.bitlink()).isEqualTo("bitlink") - assertThat(deeplinks.created()).isEqualTo("1997-08-29T02:14:00-0400") - assertThat(deeplinks.modified()).isEqualTo("1997-08-29T02:14:00-0400") - - assertThat(JSONObject(deeplinks.links()).toString()).apply { - doesNotContain(InstallType.NO_INSTALL.type) - contains(InstallType.PROMOTE_INSTALL.type) - - doesNotContain(Os.IOS.type) - contains("\"os\":\"android\"") - - contains("\"bitlink\":\"bitlink\"") - } } }