From 269049c762b1858966632b8d0749bbfd947fccc2 Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Fri, 3 Jan 2025 12:59:28 -0500 Subject: [PATCH 01/13] add HttpConnectivityTest --- .../connectivity/convention/Multiplatform.kt | 12 +- connectivity-http/build.gradle.kts | 4 + .../connectivity/HttpConnectivityOptions.kt | 13 +- .../HttpConnectivityOptionsTest.kt | 184 ++++++++++++++++++ gradle/libs.versions.toml | 2 + 5 files changed, 203 insertions(+), 12 deletions(-) create mode 100644 connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/HttpConnectivityOptionsTest.kt diff --git a/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt b/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt index fd884c1..7dac91b 100644 --- a/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt +++ b/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt @@ -38,13 +38,13 @@ fun Project.configureMultiplatform( } } +@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) internal fun KotlinMultiplatformExtension.configurePlatforms( platforms: Set = Platforms.All, name: String, ) { applyDefaultHierarchyTemplate() - @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") optIn.add("dev.jordond.connectivity.InternalConnectivityApi") @@ -70,11 +70,10 @@ internal fun KotlinMultiplatformExtension.configurePlatforms( tvosArm64() } - // TODO: Not currently supported by ktor-wasm02 -// if (platforms.contains(Platform.Linux)) { -// linuxX64() -// linuxArm64() -// } + if (platforms.contains(Platform.Linux)) { + linuxX64() + linuxArm64() + } if (platforms.contains(Platform.Js)) { js { @@ -118,6 +117,7 @@ internal fun KotlinMultiplatformExtension.configurePlatforms( sourceSets.commonTest.dependencies { implementation(kotlin("test")) + implementation(project.libs.findLibrary("kotlinx-coroutines-test").get()) implementation(project.libs.findLibrary("kotest-assertions").get()) } } \ No newline at end of file diff --git a/connectivity-http/build.gradle.kts b/connectivity-http/build.gradle.kts index 720664e..fb6e60f 100644 --- a/connectivity-http/build.gradle.kts +++ b/connectivity-http/build.gradle.kts @@ -19,6 +19,10 @@ kotlin { api(libs.ktor.client.core) } + commonTest.dependencies { + implementation(libs.ktor.client.mock) + } + androidMain.dependencies { implementation(libs.kotlinx.coroutines.android) implementation(libs.ktor.client.android) diff --git a/connectivity-http/src/commonMain/kotlin/dev/jordond/connectivity/HttpConnectivityOptions.kt b/connectivity-http/src/commonMain/kotlin/dev/jordond/connectivity/HttpConnectivityOptions.kt index 6847c58..885caa9 100644 --- a/connectivity-http/src/commonMain/kotlin/dev/jordond/connectivity/HttpConnectivityOptions.kt +++ b/connectivity-http/src/commonMain/kotlin/dev/jordond/connectivity/HttpConnectivityOptions.kt @@ -23,7 +23,7 @@ public class HttpConnectivityOptions( public val method: HttpMethod = DEFAULT_HTTP_METHOD, public val timeoutMs: Long = DEFAULT_TIMEOUT, public val pollingIntervalMs: Long = DEFAULT_POLLING_INTERVAL_MS, - public val onPollResult: ((result: PollResult) -> Unit)? = null + public val onPollResult: ((result: PollResult) -> Unit)? = null, ) { /** @@ -122,6 +122,7 @@ public class HttpConnectivityOptions( options = options, urls = urls, port = port, + method = method, timeoutMs = timeoutMs, pollingIntervalMs = pollingIntervalMs, onPollResult = onPoll, @@ -130,11 +131,11 @@ public class HttpConnectivityOptions( public companion object { - private const val DEFAULT_PORT = 443 - private val DEFAULT_HTTP_METHOD = HttpMethod.Get - private const val DEFAULT_TIMEOUT = 2000L - private const val DEFAULT_POLLING_INTERVAL_MS = (60 * 1000L) * 5 - private val DEFAULT_URLS = listOf("google.com", "github.com", "bing.com") + internal const val DEFAULT_PORT = 443 + internal val DEFAULT_HTTP_METHOD = HttpMethod.Get + internal const val DEFAULT_TIMEOUT = 2000L + internal const val DEFAULT_POLLING_INTERVAL_MS = (60 * 1000L) * 5 + internal val DEFAULT_URLS = listOf("google.com", "github.com", "bing.com") /** * Builds a new [HttpConnectivityOptions] instance using a builder pattern. diff --git a/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/HttpConnectivityOptionsTest.kt b/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/HttpConnectivityOptionsTest.kt new file mode 100644 index 0000000..6df9110 --- /dev/null +++ b/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/HttpConnectivityOptionsTest.kt @@ -0,0 +1,184 @@ +package dev.jordond.connectivity + +import dev.jordond.connectivity.HttpConnectivityOptions.Companion.DEFAULT_HTTP_METHOD +import dev.jordond.connectivity.HttpConnectivityOptions.Companion.DEFAULT_POLLING_INTERVAL_MS +import dev.jordond.connectivity.HttpConnectivityOptions.Companion.DEFAULT_PORT +import dev.jordond.connectivity.HttpConnectivityOptions.Companion.DEFAULT_TIMEOUT +import dev.jordond.connectivity.HttpConnectivityOptions.Companion.DEFAULT_URLS +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.ints.shouldBeExactly +import io.kotest.matchers.longs.shouldBeExactly +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.ktor.http.HttpMethod +import kotlin.test.Test + +class HttpConnectivityOptionsTest { + + @Test + fun verifyDefaultValues() { + val options = HttpConnectivityOptions.build { } + + options.options.autoStart.shouldBeFalse() + options.urls shouldBeEqual DEFAULT_URLS + options.port shouldBeExactly DEFAULT_PORT + options.method shouldBeEqual DEFAULT_HTTP_METHOD + options.timeoutMs shouldBeExactly DEFAULT_TIMEOUT + options.pollingIntervalMs shouldBeExactly DEFAULT_POLLING_INTERVAL_MS + options.onPollResult.shouldBeNull() + } + + @Test + fun canSetAutoStart() { + val options = HttpConnectivityOptions.build { + autoStart = true + } + + options.options.autoStart.shouldBeTrue() + } + + @Test + fun canAddSingleUrl() { + val options = HttpConnectivityOptions.build { + url("test.com") + } + + options.urls shouldBeEqual listOf("test.com") + } + + @Test + fun canSetMultipleUrlsUsingVararg() { + val options = HttpConnectivityOptions.build { + urls("test1.com", "test2.com") + } + + options.urls shouldBeEqual listOf("test1.com", "test2.com") + } + + @Test + fun canSetUrlsUsingList() { + val urlList = listOf("test1.com", "test2.com", "test3.com") + val options = HttpConnectivityOptions.build { + urls(urlList) + } + + options.urls shouldBeEqual urlList + } + + @Test + fun canSetPort() { + val options = HttpConnectivityOptions.build { + port = 8080 + } + + options.port shouldBeExactly 8080 + } + + @Test + fun canSetHttpMethod() { + val options = HttpConnectivityOptions.build { + method = HttpMethod.Post + } + + options.method shouldBeEqual HttpMethod.Post + } + + @Test + fun canSetTimeout() { + val options = HttpConnectivityOptions.build { + timeoutMs = 5000L + } + + options.timeoutMs shouldBeExactly 5000L + } + + @Test + fun canSetPollingInterval() { + val options = HttpConnectivityOptions.build { + pollingIntervalMs = 10000L + } + + options.pollingIntervalMs shouldBeExactly 10000L + } + + @Test + fun canSetOnPollResultCallback() { + var callbackCalled = false + val options = HttpConnectivityOptions.build { + onPollResult { callbackCalled = true } + } + + options.onPollResult.shouldNotBeNull() + options.onPollResult?.invoke(PollResult.Error(Exception())) + callbackCalled.shouldBeTrue() + } + + @Test + fun secondsExtensionConvertsCorrectly() { + val options = HttpConnectivityOptions.build { + timeoutMs = 5.seconds + } + + options.timeoutMs shouldBeExactly 5000L + } + + @Test + fun minutesExtensionConvertsCorrectly() { + val options = HttpConnectivityOptions.build { + pollingIntervalMs = 2.minutes + } + + options.pollingIntervalMs shouldBeExactly 120000L + } + + @Test + fun multipleConfigurationsAreAppliedCorrectly() { + val options = HttpConnectivityOptions.build { + autoStart = true + url("test.com") + port = 8080 + method = HttpMethod.Post + timeoutMs = 5.seconds + pollingIntervalMs = 1.minutes + } + + options.options.autoStart.shouldBeTrue() + options.urls shouldBeEqual listOf("test.com") + options.port shouldBeExactly 8080 + options.method shouldBeEqual HttpMethod.Post + options.timeoutMs shouldBeExactly 5000L + options.pollingIntervalMs shouldBeExactly 60000L + } + + @Test + fun urlsAreClearedWhenSettingNewUrls() { + val options = HttpConnectivityOptions.build { + url("test1.com") + urls("test2.com", "test3.com") + } + + options.urls shouldBeEqual listOf("test2.com", "test3.com") + } + + @Test + fun httpExtensionFunctionBuilder() { + val options = ConnectivityOptions.http { + autoStart = true + url("test.com") + port = 8080 + method = HttpMethod.Post + timeoutMs = 5.seconds + pollingIntervalMs = 1.minutes + } + + options.options.autoStart.shouldBeTrue() + options.urls shouldBeEqual listOf("test.com") + options.port shouldBeExactly 8080 + options.method shouldBeEqual HttpMethod.Post + options.timeoutMs shouldBeExactly 5000L + options.pollingIntervalMs shouldBeExactly 60000L + } + +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2c89e69..b72e3f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,11 +36,13 @@ androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "and kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-core-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } From a8345d9a248912075887e64921d4215006488241 Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Fri, 3 Jan 2025 16:00:11 -0500 Subject: [PATCH 02/13] add test for HttpConnectivity --- connectivity-http/build.gradle.kts | 1 + .../dev/jordond/connectivity/PollResult.kt | 10 +- .../internal/HttpConnectivityTest.kt | 190 ++++++++++++++++++ 3 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/internal/HttpConnectivityTest.kt diff --git a/connectivity-http/build.gradle.kts b/connectivity-http/build.gradle.kts index fb6e60f..0748b0e 100644 --- a/connectivity-http/build.gradle.kts +++ b/connectivity-http/build.gradle.kts @@ -21,6 +21,7 @@ kotlin { commonTest.dependencies { implementation(libs.ktor.client.mock) + implementation(libs.kermit) } androidMain.dependencies { diff --git a/connectivity-http/src/commonMain/kotlin/dev/jordond/connectivity/PollResult.kt b/connectivity-http/src/commonMain/kotlin/dev/jordond/connectivity/PollResult.kt index d359e51..1f7977c 100644 --- a/connectivity-http/src/commonMain/kotlin/dev/jordond/connectivity/PollResult.kt +++ b/connectivity-http/src/commonMain/kotlin/dev/jordond/connectivity/PollResult.kt @@ -2,6 +2,7 @@ package dev.jordond.connectivity import dev.drewhamilton.poko.Poko import io.ktor.client.statement.HttpResponse +import io.ktor.http.isSuccess /** * Represents the result of a poll operation. @@ -14,7 +15,14 @@ public sealed interface PollResult { * @property response The [HttpResponse] received from the poll operation. */ @Poko - public class Response(public val response: HttpResponse) : PollResult + public class Response(public val response: HttpResponse) : PollResult { + + public val isSuccess: Boolean + get() = response.status.isSuccess() + + public val isFailure: Boolean + get() = !isSuccess + } /** * Represents an error that occurred during the poll operation. diff --git a/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/internal/HttpConnectivityTest.kt b/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/internal/HttpConnectivityTest.kt new file mode 100644 index 0000000..ee8f6d0 --- /dev/null +++ b/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/internal/HttpConnectivityTest.kt @@ -0,0 +1,190 @@ +package dev.jordond.connectivity.internal + +import dev.jordond.connectivity.Connectivity +import dev.jordond.connectivity.HttpConnectivityOptions +import dev.jordond.connectivity.PollResult +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.engine.mock.respondError +import io.ktor.client.engine.mock.respondOk +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.HttpStatusCode.Companion.InternalServerError +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class HttpConnectivityTest { + + private lateinit var scope: TestScope + private lateinit var mockEngine: MockEngine + private lateinit var httpClient: HttpClient + + @BeforeTest + fun setup() { + scope = TestScope() + mockEngine = MockEngine { respondOk() } + httpClient = HttpClient(mockEngine) + } + + @AfterTest + fun cleanup() { + httpClient.close() + mockEngine.close() + scope.cancel() + } + + @Test + fun shouldStartMonitoringWhenAutoStartIsTrue() = scope.runTest { + testConnectivity(configure = { autoStart = true }) { connectivity -> + connectivity.monitoring.value.shouldBeTrue() + } + } + + @Test + fun shouldNotStartMonitoringWhenAutoStartIsFalse() = scope.runTest { + testConnectivity(configure = { autoStart = false }) { connectivity -> + connectivity.monitoring.value.shouldBeFalse() + } + } + + @Test + fun shouldEmitConnectedStatusWhenRequestSucceeds() = scope.runTest { + testConnectivity { connectivity -> + connectivity.start() + + val status = connectivity.statusUpdates.first() + status.shouldBeInstanceOf() + } + } + + @Test + fun shouldEmitDisconnectedStatusWhenRequestFails() = scope.runTest { + mockEngine = MockEngine { _ -> + respondError(InternalServerError) + } + httpClient = HttpClient(mockEngine) + + testConnectivity { connectivity -> + connectivity.start() + + val status = connectivity.statusUpdates.first() + status.shouldBeInstanceOf() + } + } + + @Test + fun shouldTryMultipleUrlsUntilSuccess() = scope.runTest { + mockEngine = MockEngine { request -> + if (request.url.host.contains("failed")) { + respondError(InternalServerError) + } else { + respond(content = "OK", status = HttpStatusCode.OK) + } + } + httpClient = HttpClient(mockEngine) + + testConnectivity( + configure = { urls("failed.com", "failed2.com", "success.com") }, + ) { connectivity -> + val status = connectivity.status() + status.shouldBeInstanceOf() + } + } + + @Test + fun shouldTryMultipleUrlsUntilFailure() = scope.runTest { + mockEngine = MockEngine { + respond(content = "Err", status = InternalServerError) + } + httpClient = HttpClient(mockEngine) + + testConnectivity( + configure = { urls("failed.com", "failed2.com", "failed3.com") }, + ) { connectivity -> + val status = connectivity.status() + status.shouldBeInstanceOf() + } + } + + @Test + fun shouldInvokeCallbackWithSuccessfulResponse() = scope.runTest { + var lastResult: PollResult? = null + testConnectivity( + configure = { + onPollResult { lastResult = it } + } + ) { connectivity -> + connectivity.status() + lastResult.shouldNotBeNull() + lastResult.shouldBeInstanceOf() + + val response = lastResult as PollResult.Response + response.response.status shouldBe HttpStatusCode.OK + response.isSuccess.shouldBeTrue() + } + } + + @Test + fun shouldInvokeCallbackWithFailureResponse() = scope.runTest { + mockEngine = MockEngine { _ -> + respondError(InternalServerError) + } + httpClient = HttpClient(mockEngine) + + var lastResult: PollResult? = null + testConnectivity( + configure = { + onPollResult { lastResult = it } + } + ) { connectivity -> + connectivity.status() + lastResult.shouldNotBeNull() + lastResult.shouldBeInstanceOf() + + val response = lastResult as PollResult.Response + response.response.status shouldBe InternalServerError + response.isFailure.shouldBeTrue() + } + } + + @Test + fun shouldUseSpecifiedHttpMethod() = scope.runTest { + var lastMethod: HttpMethod? = null + mockEngine = MockEngine { request -> + lastMethod = request.method + respond(content = "OK", status = HttpStatusCode.OK) + } + httpClient = HttpClient(mockEngine) + + testConnectivity(configure = { method = HttpMethod.Post }) { connectivity -> + connectivity.status() + + lastMethod shouldBe HttpMethod.Post + } + } + + private suspend fun testConnectivity( + httpClient: HttpClient = this.httpClient, + configure: HttpConnectivityOptions.Builder.() -> Unit = {}, + test: suspend (Connectivity) -> Unit, + ) { + val scope = CoroutineScope(Dispatchers.Default) + val options = HttpConnectivityOptions.build(configure) + val connectivity = HttpConnectivity(scope, options, httpClient) + test(connectivity) + scope.cancel() + } +} \ No newline at end of file From 59ac73c854d86ad46c0a29d3d2ffb0c3a9db02b2 Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Fri, 3 Jan 2025 16:10:10 -0500 Subject: [PATCH 03/13] add test for HttpConnectivity --- .../HttpConnectivityFactoryTest.kt | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/HttpConnectivityFactoryTest.kt diff --git a/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/HttpConnectivityFactoryTest.kt b/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/HttpConnectivityFactoryTest.kt new file mode 100644 index 0000000..fed2049 --- /dev/null +++ b/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/HttpConnectivityFactoryTest.kt @@ -0,0 +1,105 @@ +package dev.jordond.connectivity + +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respondOk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class HttpConnectivityFactoryTest { + + private lateinit var testScope: TestScope + private lateinit var sutScope: CoroutineScope + private lateinit var mockEngine: MockEngine + private lateinit var httpClient: HttpClient + + @BeforeTest + fun setup() { + testScope = TestScope() + sutScope = CoroutineScope(Dispatchers.Default) + mockEngine = MockEngine { respondOk() } + httpClient = HttpClient(mockEngine) + } + + @AfterTest + fun cleanup() { + httpClient.close() + mockEngine.close() + testScope.cancel() + } + + @Test + fun canCreateConnectivityWithDefaultOptions() = testScope.runTest { + val connectivity = Connectivity( + scope = sutScope, + httpClient = httpClient, + ) + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + connectivity.monitoring.value shouldBe false + sutScope.cancel() + } + + @Test + fun canCreateConnectivityWithCustomOptions() = testScope.runTest { + val options = HttpConnectivityOptions.build { + autoStart = true + url("test.com") + port = 8080 + } + + val connectivity = Connectivity( + options = options, + scope = sutScope, + httpClient = httpClient, + ) + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + connectivity.monitoring.value shouldBe true + sutScope.cancel() + } + + @Test + fun canCreateConnectivityWithBuilderBlock() = testScope.runTest { + val connectivity = Connectivity( + scope = sutScope, + httpClient = httpClient, + ) { + autoStart = true + url("test.com") + port = 8080 + } + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + connectivity.monitoring.value shouldBe true + sutScope.cancel() + } + + @Test + fun canForcePollUsingExtensionFunction() = testScope.runTest { + val connectivity = Connectivity( + scope = sutScope, + httpClient = httpClient, + ) + + connectivity.start() + connectivity.force() + + val status = connectivity.statusUpdates.first() + status.shouldBeInstanceOf() + sutScope.cancel() + } +} \ No newline at end of file From 3f23a63958fccf87ac547cf9693d6fc3fb137a90 Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Fri, 3 Jan 2025 16:33:46 -0500 Subject: [PATCH 04/13] write tests for DefaultConnectivity --- .../connectivity/ConnectivityOptionsTest.kt | 57 +++++++++ .../connectivity/ConnectivityProviderTest.kt | 71 +++++++++++ .../jordond/connectivity/ConnectivityTest.kt | 87 +++++++++++++ .../internal/DefaultConnectivityTest.kt | 115 ++++++++++++++++++ 4 files changed, 330 insertions(+) create mode 100644 connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityOptionsTest.kt create mode 100644 connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityProviderTest.kt create mode 100644 connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityTest.kt create mode 100644 connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/internal/DefaultConnectivityTest.kt diff --git a/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityOptionsTest.kt b/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityOptionsTest.kt new file mode 100644 index 0000000..ae68865 --- /dev/null +++ b/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityOptionsTest.kt @@ -0,0 +1,57 @@ +package dev.jordond.connectivity + +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import kotlin.test.Test + +class ConnectivityOptionsTest { + + @Test + fun verifyDefaultValues() { + val options = ConnectivityOptions.build { } + + options.autoStart.shouldBeFalse() + } + + @Test + fun canSetAutoStart() { + val options = ConnectivityOptions.build { + autoStart = true + } + + options.autoStart.shouldBeTrue() + } + + @Test + fun canSetAutoStartUsingBuilderFunction() { + val options = ConnectivityOptions.build { + autoStart(true) + } + + options.autoStart.shouldBeTrue() + } + + @Test + fun multipleConfigurationsAreAppliedCorrectly() { + val options = ConnectivityOptions.build { + autoStart = true + autoStart(false) + } + + options.autoStart.shouldBeFalse() + } + + @Test + fun defaultConstructorHasCorrectValues() { + val options = ConnectivityOptions() + + options.autoStart.shouldBeFalse() + } + + @Test + fun constructorWithParametersHasCorrectValues() { + val options = ConnectivityOptions(autoStart = true) + + options.autoStart.shouldBeTrue() + } +} \ No newline at end of file diff --git a/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityProviderTest.kt b/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityProviderTest.kt new file mode 100644 index 0000000..b18a978 --- /dev/null +++ b/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityProviderTest.kt @@ -0,0 +1,71 @@ +package dev.jordond.connectivity + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class ConnectivityProviderTest { + + @Test + fun factoryFunctionCreatesProvider() { + val flow = flowOf(Connectivity.Status.Connected(false)) + val provider = ConnectivityProvider(flow) + + provider.shouldBeInstanceOf() + } + + @Test + fun extensionFunctionCreatesProvider() { + val flow = flowOf(Connectivity.Status.Connected(false)) + val provider = flow.asProvider() + + provider.shouldBeInstanceOf() + } + + @Test + fun monitorReturnsCorrectFlow() = runTest { + val status = Connectivity.Status.Connected(false) + val flow = flowOf(status) + val provider = ConnectivityProvider(flow) + + val result = provider.monitor().first() + + result shouldBe status + } + + @Test + fun extensionFunctionMonitorReturnsCorrectFlow() = runTest { + val status = Connectivity.Status.Connected(false) + val flow = flowOf(status) + val provider = flow.asProvider() + + val result = provider.monitor().first() + + result shouldBe status + } + + @Test + fun disconnectedStatusIsEmittedCorrectly() = runTest { + val status = Connectivity.Status.Disconnected + val flow = flowOf(status) + val provider = ConnectivityProvider(flow) + + val result = provider.monitor().first() + + result shouldBe status + } + + @Test + fun connectedStatusWithMeteredIsEmittedCorrectly() = runTest { + val status = Connectivity.Status.Connected(metered = true) + val flow = flowOf(status) + val provider = ConnectivityProvider(flow) + + val result = provider.monitor().first() + + result shouldBe status + } +} \ No newline at end of file diff --git a/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityTest.kt b/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityTest.kt new file mode 100644 index 0000000..094d222 --- /dev/null +++ b/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityTest.kt @@ -0,0 +1,87 @@ +package dev.jordond.connectivity + +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class ConnectivityTest { + + private lateinit var testScope: TestScope + private lateinit var sutScope: CoroutineScope + private lateinit var provider: ConnectivityProvider + + @BeforeTest + fun setup() { + testScope = TestScope() + sutScope = CoroutineScope(Dispatchers.Default) + provider = ConnectivityProvider(flowOf(Connectivity.Status.Connected(false))) + } + + @AfterTest + fun cleanup() { + testScope.cancel() + sutScope.cancel() + } + + @Test + fun shouldCreateConnectivityWithDefaultOptions() = testScope.runTest { + val connectivity = Connectivity( + provider = provider, + scope = sutScope, + ) + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + connectivity.monitoring.value shouldBe false + } + + @Test + fun shouldCreateConnectivityWithCustomOptions() = testScope.runTest { + val options = ConnectivityOptions(autoStart = true) + + val connectivity = Connectivity( + provider = provider, + options = options, + scope = sutScope, + ) + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + connectivity.monitoring.value shouldBe true + } + + @Test + fun shouldCreateConnectivityWithBuilderBlock() = testScope.runTest { + val connectivity = Connectivity( + provider = provider, + scope = sutScope, + ) { + autoStart = true + } + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + testScheduler.advanceUntilIdle() + connectivity.monitoring.value shouldBe true + } + + @Test + fun shouldUseProvidedCoroutineScope() = testScope.runTest { + val connectivity = Connectivity( + provider = provider, + scope = sutScope, + ) + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + } +} \ No newline at end of file diff --git a/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/internal/DefaultConnectivityTest.kt b/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/internal/DefaultConnectivityTest.kt new file mode 100644 index 0000000..2956462 --- /dev/null +++ b/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/internal/DefaultConnectivityTest.kt @@ -0,0 +1,115 @@ +package dev.jordond.connectivity.internal + +import dev.jordond.connectivity.Connectivity +import dev.jordond.connectivity.ConnectivityOptions +import dev.jordond.connectivity.ConnectivityProvider +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.types.shouldBeInstanceOf +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultConnectivityTest { + + private lateinit var testScope: TestScope + private lateinit var sutScope: TestScope + private lateinit var provider: ConnectivityProvider + + @BeforeTest + fun setup() { + testScope = TestScope() + sutScope = TestScope() + provider = ConnectivityProvider(flowOf(Connectivity.Status.Connected(false))) + } + + @AfterTest + fun cleanup() { + testScope.cancel() + sutScope.cancel() + } + + @Test + fun shouldStartMonitoringWhenAutoStartIsTrue() = testScope.runTest { + val connectivity = createConnectivity(autoStart = true) + sutScope.advanceUntilIdle() + connectivity.monitoring.value.shouldBeTrue() + } + + @Test + fun shouldNotStartMonitoringWhenAutoStartIsFalse() = testScope.runTest { + val connectivity = createConnectivity(autoStart = false) + sutScope.advanceUntilIdle() + connectivity.monitoring.value.shouldBeFalse() + } + + @Test + fun shouldEmitConnectedStatusWhenStarted() = testScope.runTest { + val connectivity = createConnectivity() + connectivity.start() + sutScope.advanceUntilIdle() + + val status = connectivity.statusUpdates.first() + status.shouldBeInstanceOf() + status.isConnected.shouldBeTrue() + } + + @Test + fun shouldStopMonitoringWhenStopIsCalled() = testScope.runTest { + val connectivity = createConnectivity(autoStart = true) + sutScope.advanceUntilIdle() + connectivity.monitoring.value.shouldBeTrue() + + connectivity.stop() + sutScope.advanceUntilIdle() + connectivity.monitoring.value.shouldBeFalse() + } + + @Test + fun shouldReturnCurrentStatusWhenRequested() = testScope.runTest { + val connectivity = createConnectivity() + val status = connectivity.status() + sutScope.advanceUntilIdle() + + status.shouldNotBeNull() + status.shouldBeInstanceOf() + status.isConnected.shouldBeTrue() + } + + @Test + fun shouldEmitDisconnectedStatus() = testScope.runTest { + provider = ConnectivityProvider(flowOf(Connectivity.Status.Disconnected)) + val connectivity = createConnectivity(autoStart = true) + sutScope.advanceUntilIdle() + + val status = connectivity.statusUpdates.first() + status.shouldBeInstanceOf() + status.isDisconnected.shouldBeTrue() + } + + @Test + fun shouldEmitMeteredStatus() = testScope.runTest { + provider = ConnectivityProvider(flowOf(Connectivity.Status.Connected(metered = true))) + val connectivity = createConnectivity() + connectivity.start() + sutScope.advanceUntilIdle() + + val status = connectivity.statusUpdates.first() + status.shouldBeInstanceOf() + status.isMetered.shouldBeTrue() + } + + private fun createConnectivity(autoStart: Boolean = false): Connectivity { + val options = ConnectivityOptions(autoStart = autoStart) + return DefaultConnectivity(sutScope, provider, options) + } +} \ No newline at end of file From 9e25079236abcb98c4353c30254c635bc58aed34 Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Fri, 3 Jan 2025 16:51:48 -0500 Subject: [PATCH 05/13] add tests for android tools --- .../connectivity/convention/Multiplatform.kt | 10 ++++ .../connectivity/tools/ContextProvider.kt | 2 +- .../tools/ContextProviderInitializerTest.kt | 30 ++++++++++ .../connectivity/tools/ContextProviderTest.kt | 58 +++++++++++++++++++ gradle/libs.versions.toml | 4 ++ 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderInitializerTest.kt create mode 100644 connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt diff --git a/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt b/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt index 7dac91b..271eb59 100644 --- a/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt +++ b/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt @@ -120,4 +120,14 @@ internal fun KotlinMultiplatformExtension.configurePlatforms( implementation(project.libs.findLibrary("kotlinx-coroutines-test").get()) implementation(project.libs.findLibrary("kotest-assertions").get()) } + + sourceSets.androidUnitTest.dependencies { + implementation(project.libs.findLibrary("mockk-android").get()) + implementation(project.libs.findLibrary("mockk-agent").get()) + } + + sourceSets.androidInstrumentedTest.dependencies { + implementation(project.libs.findLibrary("mockk-android").get()) + implementation(project.libs.findLibrary("mockk-agent").get()) + } } \ No newline at end of file diff --git a/connectivity-tools-android/src/androidMain/kotlin/dev/jordond/connectivity/tools/ContextProvider.kt b/connectivity-tools-android/src/androidMain/kotlin/dev/jordond/connectivity/tools/ContextProvider.kt index 2e77aa1..ffb10bc 100644 --- a/connectivity-tools-android/src/androidMain/kotlin/dev/jordond/connectivity/tools/ContextProvider.kt +++ b/connectivity-tools-android/src/androidMain/kotlin/dev/jordond/connectivity/tools/ContextProvider.kt @@ -14,7 +14,7 @@ public class ContextProvider(public val context: Context) { public companion object { @SuppressLint("StaticFieldLeak") - private var instance: ContextProvider? = null + internal var instance: ContextProvider? = null /** * Create a new instance of [ContextProvider] with the given [context]. diff --git a/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderInitializerTest.kt b/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderInitializerTest.kt new file mode 100644 index 0000000..a287d07 --- /dev/null +++ b/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderInitializerTest.kt @@ -0,0 +1,30 @@ +package dev.jordond.connectivity.tools + +import android.content.Context +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.mockk +import kotlin.test.Test + +class ContextProviderInitializerTest { + + @Test + fun shouldCreateContextProvider() { + val context = mockk() + val initializer = ContextProviderInitializer() + + val result = initializer.create(context) + + result.shouldBeInstanceOf() + result.context shouldBe context + } + + @Test + fun shouldReturnEmptyDependencyList() { + val initializer = ContextProviderInitializer() + + val dependencies = initializer.dependencies() + + dependencies shouldBe emptyList() + } +} \ No newline at end of file diff --git a/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt b/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt new file mode 100644 index 0000000..79e3306 --- /dev/null +++ b/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt @@ -0,0 +1,58 @@ +package dev.jordond.connectivity.tools + +import android.content.Context +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.mockk +import kotlin.test.AfterTest +import kotlin.test.Test + +class ContextProviderTest { + + @AfterTest + fun cleanup() { + ContextProvider.Companion.instance = null + } + + @Test + fun shouldCreateContextProviderInstance() { + val context = mockk() + val provider = ContextProvider.create(context) + + provider.shouldNotBeNull() + provider.shouldBeInstanceOf() + provider.context shouldBe context + } + + @Test + fun shouldReturnExistingInstanceWhenCreatingMultipleTimes() { + val context1 = mockk() + val context2 = mockk() + + val provider1 = ContextProvider.create(context1) + val provider2 = ContextProvider.create(context2) + + provider1 shouldBe provider2 + provider1.context shouldBe context1 + provider2.context shouldBe context1 + } + + @Test + fun shouldGetInstanceAfterCreation() { + val context = mockk() + val created = ContextProvider.create(context) + val instance = ContextProvider.getInstance() + + instance shouldBe created + instance.context shouldBe context + } + + @Test + fun shouldThrowWhenGettingInstanceBeforeCreation() { + shouldThrow { + ContextProvider.getInstance() + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b72e3f2..a5ef922 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ androidx-activityCompose = "1.9.3" kermit = "2.0.5" binaryCompatibility = "0.17.0" dokka = "2.0.0" +mockk = "1.13.14" publish = "0.30.0" poko = "0.18.2" versions = "0.51.0" @@ -47,6 +48,9 @@ compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } +mockk-agent = { module = "io.mockk:mockk-agent", version.ref = "mockk" } stateHolder = { module = "dev.stateholder:core", version.ref = "stateHolder" } stateHolder-compose = { module = "dev.stateholder:extensions-compose", version.ref = "stateHolder" } stateHolder-voyager = { module = "dev.stateholder:extensions-voyager", version.ref = "stateHolder" } From f6c8a3eeb28eaf817bb430bc37b647b76e412718 Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Fri, 3 Jan 2025 19:02:36 -0500 Subject: [PATCH 06/13] add tests for AndroidConnectivityProvider --- .../connectivity/convention/Multiplatform.kt | 1 + .../internal/AndroidConnectivityProvider.kt | 9 +- .../internal/VersionCodeProvider.kt | 14 + .../connectivity/AndroidConnectivityTest.kt | 103 +++++ .../AndroidConnectivityProviderTest.kt | 374 ++++++++++++++++++ gradle/libs.versions.toml | 2 + 6 files changed, 501 insertions(+), 2 deletions(-) create mode 100644 connectivity-android/src/androidMain/kotlin/dev/jordond/connectivity/internal/VersionCodeProvider.kt create mode 100644 connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/AndroidConnectivityTest.kt create mode 100644 connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProviderTest.kt diff --git a/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt b/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt index 271eb59..8867cfa 100644 --- a/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt +++ b/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt @@ -119,6 +119,7 @@ internal fun KotlinMultiplatformExtension.configurePlatforms( implementation(kotlin("test")) implementation(project.libs.findLibrary("kotlinx-coroutines-test").get()) implementation(project.libs.findLibrary("kotest-assertions").get()) + implementation(project.libs.findLibrary("turbine").get()) } sourceSets.androidUnitTest.dependencies { diff --git a/connectivity-android/src/androidMain/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProvider.kt b/connectivity-android/src/androidMain/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProvider.kt index 1cf20d7..fc160e5 100644 --- a/connectivity-android/src/androidMain/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProvider.kt +++ b/connectivity-android/src/androidMain/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProvider.kt @@ -17,8 +17,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flowOf +// This suppress is needed because we now use a wrapper for the version code see VersionCodeProvider +@SuppressLint("NewApi") internal class AndroidConnectivityProvider( private val context: Context, + private val versionCodeProvider: VersionCodeProvider = VersionCodeProvider.Default, ) : ConnectivityProvider { // The permission is in the manifest but the IDE doesn't seem to recognize it @@ -49,10 +52,12 @@ internal class AndroidConnectivityProvider( } try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (versionCodeProvider.code >= Build.VERSION_CODES.N) { + println("Using registerDefaultNetworkCallback") manager.registerDefaultNetworkCallback(networkCallback) } else { val networkRequest = NetworkRequest.Builder().build() + println("Using registerNetworkCallback") manager.registerNetworkCallback(networkRequest, networkCallback) } @@ -67,7 +72,7 @@ internal class AndroidConnectivityProvider( } private fun ConnectivityManager.initialStatus(): Connectivity.Status { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return if (versionCodeProvider.code >= Build.VERSION_CODES.M) { activeNetwork?.let { network -> getNetworkCapabilities(network)?.let { capabilities -> status(capabilities) diff --git a/connectivity-android/src/androidMain/kotlin/dev/jordond/connectivity/internal/VersionCodeProvider.kt b/connectivity-android/src/androidMain/kotlin/dev/jordond/connectivity/internal/VersionCodeProvider.kt new file mode 100644 index 0000000..65a5d1d --- /dev/null +++ b/connectivity-android/src/androidMain/kotlin/dev/jordond/connectivity/internal/VersionCodeProvider.kt @@ -0,0 +1,14 @@ +package dev.jordond.connectivity.internal + +import android.os.Build + +internal interface VersionCodeProvider { + + val code: Int + + companion object { + val Default = object : VersionCodeProvider { + override val code: Int = Build.VERSION.SDK_INT + } + } +} \ No newline at end of file diff --git a/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/AndroidConnectivityTest.kt b/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/AndroidConnectivityTest.kt new file mode 100644 index 0000000..fbbb4b8 --- /dev/null +++ b/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/AndroidConnectivityTest.kt @@ -0,0 +1,103 @@ +package dev.jordond.connectivity + +import dev.jordond.connectivity.tools.ContextProvider +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AndroidConnectivityTest { + + private lateinit var testScope: TestScope + private lateinit var sutScope: TestScope + private lateinit var contextProvider: ContextProvider + + @BeforeTest + fun setup() { + testScope = TestScope() + sutScope = TestScope() + contextProvider = mockk() + mockkObject(ContextProvider.Companion) + every { ContextProvider.getInstance() } returns contextProvider + every { contextProvider.context } returns mockk() + } + + @AfterTest + fun cleanup() { + testScope.cancel() + sutScope.cancel() + unmockkObject(ContextProvider.Companion) + } + + @Test + fun shouldCreateConnectivityWithDefaultOptions() = testScope.runTest { + val connectivity = Connectivity() + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + sutScope.advanceUntilIdle() + connectivity.monitoring.value shouldBe false + } + + @Test + fun shouldCreateConnectivityWithCustomOptions() = testScope.runTest { + val options = ConnectivityOptions(autoStart = true) + + val connectivity = Connectivity( + options = options, + scope = sutScope, + ) + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + sutScope.advanceUntilIdle() + connectivity.monitoring.value shouldBe true + } + + @Test + fun shouldCreateAndroidConnectivityWithDefaultOptions() = testScope.runTest { + val connectivity = AndroidConnectivity() + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + sutScope.advanceUntilIdle() + connectivity.monitoring.value shouldBe false + } + + @Test + fun shouldCreateAndroidConnectivityWithCustomOptions() = testScope.runTest { + val options = ConnectivityOptions(autoStart = true) + + val connectivity = AndroidConnectivity( + options = options, + scope = sutScope, + ) + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + sutScope.advanceUntilIdle() + connectivity.monitoring.value shouldBe true + } + + @Test + fun shouldUseProvidedCoroutineScope() = testScope.runTest { + val connectivity = Connectivity(scope = sutScope) + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + } +} \ No newline at end of file diff --git a/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProviderTest.kt b/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProviderTest.kt new file mode 100644 index 0000000..7496b5d --- /dev/null +++ b/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProviderTest.kt @@ -0,0 +1,374 @@ +package dev.jordond.connectivity.internal + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkInfo +import android.net.NetworkRequest +import android.os.Build +import app.cash.turbine.test +import dev.jordond.connectivity.Connectivity +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.slot +import io.mockk.verify +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class AndroidConnectivityProviderTest { + + private lateinit var context: Context + private lateinit var connectivityManager: ConnectivityManager + private lateinit var provider: AndroidConnectivityProvider + private lateinit var networkCallback: ConnectivityManager.NetworkCallback + private lateinit var versionCodeProvider: VersionCodeProvider + private val callbackSlot = slot() + + @BeforeTest + fun setup() { + // Version to use newer network callback + versionCodeProvider = versionCodeProvider(Build.VERSION_CODES.N) + + mockkConstructor(NetworkRequest.Builder::class) + every { anyConstructed().build() } returns mockk(relaxed = true) + + connectivityManager = mockk(relaxed = true) + context = mockk { + every { getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager + } + provider = AndroidConnectivityProvider(context, versionCodeProvider) + } + + @Test + fun shouldReturnDisconnectedWhenNoConnectivityManager() = runTest { + val contextWithoutManager = mockk { + every { getSystemService(Context.CONNECTIVITY_SERVICE) } returns null + } + val provider = AndroidConnectivityProvider(contextWithoutManager) + val status = provider.monitor().first() + status.shouldBeInstanceOf() + } + + @Test + fun shouldEmitConnectedStatusWhenNetworkIsAvailable() = runTest { + val network = mockk() + val capabilities = mockk { + every { hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns true + every { hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) } returns false + } + every { connectivityManager.getNetworkCapabilities(network) } returns capabilities + every { connectivityManager.isActiveNetworkMetered } returns false + + provider.monitor().test { + triggerNetworkCallback() + + // Initial status + skipItems(1) + + networkCallback.onAvailable(network) + val status = awaitItem() + status.shouldBeInstanceOf() + status.metered shouldBe false + } + } + + @Test + fun shouldEmitDisconnectedStatusWhenNetworkIsLost() = runTest { + val network = mockk() + provider.monitor().test { + triggerNetworkCallback() + + // Initial status + skipItems(1) + + networkCallback.onLost(network) + val status = awaitItem() + status.shouldBeInstanceOf() + } + } + + @Test + fun shouldEmitDisconnectedStatusWhenNetworkIsLostOnOlderAndroid() = runTest { + versionCodeProvider = versionCodeProvider(Build.VERSION_CODES.LOLLIPOP) + provider = AndroidConnectivityProvider(context, versionCodeProvider) + + val network = mockk() + provider.monitor().test { + triggerNetworkCallback() + + // Initial status + skipItems(1) + + networkCallback.onLost(network) + val status = awaitItem() + status.shouldBeInstanceOf() + } + } + + @Test + fun shouldEmitMeteredStatusWhenOnCellularNetwork() = runTest { + val network = mockk() + val capabilities = mockk { + every { hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns false + every { hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) } returns true + } + every { connectivityManager.getNetworkCapabilities(network) } returns capabilities + every { connectivityManager.isActiveNetworkMetered } returns true + + provider.monitor().test { + triggerNetworkCallback() + + // Initial status + skipItems(1) + + networkCallback.onAvailable(network) + val status = awaitItem() + status.shouldBeInstanceOf() + status.metered shouldBe true + } + } + + @Test + fun shouldUpdateStatusWhenCapabilitiesChange() = runTest { + val network = mockk() + val capabilities = mockk { + every { hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns true + every { hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) } returns false + } + every { connectivityManager.getNetworkCapabilities(network) } returns capabilities + every { connectivityManager.isActiveNetworkMetered } returns false + + provider.monitor().test { + triggerNetworkCallback() + + // Initial status + skipItems(1) + + val updatedCapabilities = mockk { + every { hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns false + every { hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) } returns true + } + networkCallback.onCapabilitiesChanged(network, updatedCapabilities) + val status = awaitItem() + status.shouldBeInstanceOf() + status.metered shouldBe true + } + } + + @Test + fun shouldReturnConnectedWithMeteredFalseWhenOnUnmeteredWifi() = runTest { + val (network, _) = setupNetworkCapabilities( + isWifi = true, + isCellular = false, + isMetered = false, + ) + verifyNetworkStatus(network, expectedMetered = false) + } + + @Test + fun shouldReturnConnectedWithMeteredTrueWhenOnCellular() = runTest { + val (network, _) = setupNetworkCapabilities( + isWifi = false, + isCellular = true, + isMetered = true, + ) + verifyNetworkStatus(network, expectedMetered = true) + } + + @Test + fun shouldReturnConnectedWithMeteredTrueWhenOnMeteredWifi() = runTest { + val (network, _) = setupNetworkCapabilities( + isWifi = true, + isCellular = false, + isMetered = true, + ) + verifyNetworkStatus(network, expectedMetered = true) + } + + @Test + fun shouldHandleNullCapabilities() = runTest { + val network = mockk() + every { connectivityManager.getNetworkCapabilities(network) } returns null + every { connectivityManager.isActiveNetworkMetered } returns false + verifyNetworkStatus(network, expectedMetered = true) + } + + @Test + fun shouldReturnMeteredTrueWhenBothWifiAndCellularAreFalse() = runTest { + val (network, _) = setupNetworkCapabilities( + isWifi = false, + isCellular = false, + isMetered = false, + ) + verifyNetworkStatus(network, expectedMetered = true) + } + + @Test + fun shouldReturnConnectedStatusWhenVersionIsNewerAndHasActiveNetwork() = runTest { + versionCodeProvider = versionCodeProvider(Build.VERSION_CODES.M) + provider = AndroidConnectivityProvider(context, versionCodeProvider) + + val network = mockk() + val capabilities = mockk { + every { hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns true + every { hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) } returns false + } + every { connectivityManager.activeNetwork } returns network + every { connectivityManager.getNetworkCapabilities(network) } returns capabilities + every { connectivityManager.isActiveNetworkMetered } returns false + + provider.monitor().test { + triggerNetworkCallback() + val status = awaitItem() + + status.shouldBeInstanceOf() + status.metered shouldBe false + } + } + + @Test + fun shouldReturnDisconnectedStatusWhenVersionIsNewerButNoActiveNetwork() = runTest { + versionCodeProvider = versionCodeProvider(Build.VERSION_CODES.M) + provider = AndroidConnectivityProvider(context, versionCodeProvider) + + every { connectivityManager.activeNetwork } returns null + + provider.monitor().test { + triggerNetworkCallback() + val status = awaitItem() + + status.shouldBeInstanceOf() + } + } + + @Test + fun shouldReturnDisconnectedStatusWhenVersionIsNewerButNoNetworkCapabilities() = runTest { + versionCodeProvider = versionCodeProvider(Build.VERSION_CODES.M) + provider = AndroidConnectivityProvider(context, versionCodeProvider) + + val network = mockk() + every { connectivityManager.activeNetwork } returns network + every { connectivityManager.getNetworkCapabilities(network) } returns null + + provider.monitor().test { + triggerNetworkCallback() + val status = awaitItem() + + status.shouldBeInstanceOf() + } + } + + @Test + fun shouldReturnConnectedStatusWhenVersionIsOlderAndNetworkIsConnected() = runTest { + versionCodeProvider = versionCodeProvider(Build.VERSION_CODES.LOLLIPOP) + provider = AndroidConnectivityProvider(context, versionCodeProvider) + + @Suppress("DEPRECATION") + val networkInfo = mockk { + every { isConnected } returns true + } + + every { + @Suppress("DEPRECATION") + connectivityManager.activeNetworkInfo + } returns networkInfo + every { connectivityManager.isActiveNetworkMetered } returns true + + provider.monitor().test { + triggerNetworkCallback() + val status = awaitItem() + + status.shouldBeInstanceOf() + status.metered shouldBe true + } + } + + @Test + fun shouldReturnDisconnectedStatusWhenVersionIsOlderAndNetworkIsDisconnected() = runTest { + versionCodeProvider = versionCodeProvider(Build.VERSION_CODES.LOLLIPOP) + provider = AndroidConnectivityProvider(context, versionCodeProvider) + + @Suppress("DEPRECATION") + val networkInfo = mockk { + every { isConnected } returns false + } + + every { + @Suppress("DEPRECATION") + connectivityManager.activeNetworkInfo + } returns networkInfo + + provider.monitor().test { + triggerNetworkCallback() + val status = awaitItem() + + status.shouldBeInstanceOf() + } + } + + @Test + fun shouldReturnDisconnectedStatusWhenVersionIsOlderAndNetworkInfoIsNull() = runTest { + versionCodeProvider = versionCodeProvider(Build.VERSION_CODES.LOLLIPOP) + provider = AndroidConnectivityProvider(context, versionCodeProvider) + every { + @Suppress("DEPRECATION") + connectivityManager.activeNetworkInfo + } returns null + + provider.monitor().test { + triggerNetworkCallback() + val status = awaitItem() + + status.shouldBeInstanceOf() + } + } + + private fun triggerNetworkCallback() { + if (versionCodeProvider.code >= Build.VERSION_CODES.N) { + verify { connectivityManager.registerDefaultNetworkCallback(capture(callbackSlot)) } + } else { + verify { connectivityManager.registerNetworkCallback(any(), capture(callbackSlot)) } + } + networkCallback = callbackSlot.captured + } + + private fun setupNetworkCapabilities( + isWifi: Boolean = false, + isCellular: Boolean = false, + isMetered: Boolean = false, + ): Pair { + val network = mockk() + val capabilities = mockk { + every { hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } returns isWifi + every { hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) } returns isCellular + } + every { connectivityManager.getNetworkCapabilities(network) } returns capabilities + every { connectivityManager.isActiveNetworkMetered } returns isMetered + return network to capabilities + } + + private suspend fun verifyNetworkStatus( + network: Network, + expectedMetered: Boolean, + ) { + provider.monitor().test { + triggerNetworkCallback() + skipItems(1) // Initial status + + networkCallback.onAvailable(network) + val status = awaitItem() + status.shouldBeInstanceOf() + status.metered shouldBe expectedMetered + } + } +} + +internal fun versionCodeProvider(code: Int): VersionCodeProvider = object : VersionCodeProvider { + override val code: Int = code +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5ef922..69502c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ poko = "0.18.2" versions = "0.51.0" kotest = "5.9.1" ktor = "3.0.3" +turbine = "1.2.0" voyager = "1.1.0-beta03" stateHolder = "1.2.0" @@ -55,6 +56,7 @@ stateHolder = { module = "dev.stateholder:core", version.ref = "stateHolder" } stateHolder-compose = { module = "dev.stateholder:extensions-compose", version.ref = "stateHolder" } stateHolder-voyager = { module = "dev.stateholder:extensions-voyager", version.ref = "stateHolder" } voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } +turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } voyager-screenModel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } gradlePlugin-android = { module = "com.android.tools.build:gradle", version.ref = "agp" } gradlePlugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } From 42b27e7c6731b7a1955c3f59944e0f50d45614af Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Fri, 3 Jan 2025 19:04:51 -0500 Subject: [PATCH 07/13] add tests for AppleConnectivity --- .../connectivity/AndroidConnectivityTest.kt | 2 - .../connectivity/AppleConnectivityTest.kt | 90 +++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 connectivity-apple/src/appleTest/kotlin/dev/jordond/connectivity/AppleConnectivityTest.kt diff --git a/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/AndroidConnectivityTest.kt b/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/AndroidConnectivityTest.kt index fbbb4b8..ba437d0 100644 --- a/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/AndroidConnectivityTest.kt +++ b/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/AndroidConnectivityTest.kt @@ -8,8 +8,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject import io.mockk.unmockkObject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.test.TestScope diff --git a/connectivity-apple/src/appleTest/kotlin/dev/jordond/connectivity/AppleConnectivityTest.kt b/connectivity-apple/src/appleTest/kotlin/dev/jordond/connectivity/AppleConnectivityTest.kt new file mode 100644 index 0000000..30fa799 --- /dev/null +++ b/connectivity-apple/src/appleTest/kotlin/dev/jordond/connectivity/AppleConnectivityTest.kt @@ -0,0 +1,90 @@ +package dev.jordond.connectivity + +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class AppleConnectivityTest { + + private lateinit var testScope: TestScope + private lateinit var sutScope: TestScope + + @BeforeTest + fun setup() { + testScope = TestScope() + sutScope = TestScope() + } + + @AfterTest + fun cleanup() { + testScope.cancel() + sutScope.cancel() + } + + @Test + fun shouldCreateConnectivityWithDefaultOptions() = testScope.runTest { + val connectivity = Connectivity() + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + sutScope.advanceUntilIdle() + connectivity.monitoring.value shouldBe false + } + + @Test + fun shouldCreateConnectivityWithCustomOptions() = testScope.runTest { + val options = ConnectivityOptions(autoStart = true) + + val connectivity = Connectivity( + options = options, + scope = sutScope, + ) + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + sutScope.advanceUntilIdle() + connectivity.monitoring.value shouldBe true + } + + @Test + fun shouldCreateAppleConnectivityWithDefaultOptions() = testScope.runTest { + val connectivity = AppleConnectivity() + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + sutScope.advanceUntilIdle() + connectivity.monitoring.value shouldBe false + } + + @Test + fun shouldCreateAppleConnectivityWithCustomOptions() = testScope.runTest { + val options = ConnectivityOptions(autoStart = true) + + val connectivity = AppleConnectivity( + options = options, + scope = sutScope, + ) + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + sutScope.advanceUntilIdle() + connectivity.monitoring.value shouldBe true + } + + @Test + fun shouldUseProvidedCoroutineScope() = testScope.runTest { + val connectivity = Connectivity(scope = sutScope) + + connectivity.shouldNotBeNull() + connectivity.shouldBeInstanceOf() + } +} \ No newline at end of file From a74fc04367a2279706cfb55df701acf71614532a Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Fri, 3 Jan 2025 19:22:23 -0500 Subject: [PATCH 08/13] run apiDump --- connectivity-http/api/android/connectivity-http.api | 2 ++ connectivity-http/api/jvm/connectivity-http.api | 2 ++ 2 files changed, 4 insertions(+) diff --git a/connectivity-http/api/android/connectivity-http.api b/connectivity-http/api/android/connectivity-http.api index 7085b5c..d2ff61e 100644 --- a/connectivity-http/api/android/connectivity-http.api +++ b/connectivity-http/api/android/connectivity-http.api @@ -67,6 +67,8 @@ public final class dev/jordond/connectivity/PollResult$Response : dev/jordond/co public fun equals (Ljava/lang/Object;)Z public final fun getResponse ()Lio/ktor/client/statement/HttpResponse; public fun hashCode ()I + public final fun isFailure ()Z + public final fun isSuccess ()Z public fun toString ()Ljava/lang/String; } diff --git a/connectivity-http/api/jvm/connectivity-http.api b/connectivity-http/api/jvm/connectivity-http.api index 7085b5c..d2ff61e 100644 --- a/connectivity-http/api/jvm/connectivity-http.api +++ b/connectivity-http/api/jvm/connectivity-http.api @@ -67,6 +67,8 @@ public final class dev/jordond/connectivity/PollResult$Response : dev/jordond/co public fun equals (Ljava/lang/Object;)Z public final fun getResponse ()Lio/ktor/client/statement/HttpResponse; public fun hashCode ()I + public final fun isFailure ()Z + public final fun isSuccess ()Z public fun toString ()Ljava/lang/String; } From a9b163b37e3ec8145a2222488634f1cebb3e2803 Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Sat, 4 Jan 2025 12:54:12 -0500 Subject: [PATCH 09/13] add tests to CI --- .github/workflows/ci.yml | 19 +++++++++++++------ .../jordond/connectivity/ConnectivityTest.kt | 11 ++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64bb119..ca79754 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,9 +52,11 @@ jobs: strategy: matrix: config: [ - { target: "default", os: "ubuntu-latest", tasks: "test", continueOnError: false }, - { target: "osx", os: "macos-latest", tasks: "test", continueOnError: false }, - { target: "package", os: "ubuntu-latest", tasks: ":demo:composeApp:packageDistributionForCurrentOS", continueOnError: false } + { target: "default", os: "ubuntu-latest", tasks: "test" }, + { target: "osx", os: "macos-latest", tasks: "test" }, + { target: "js-node", os: "ubuntu-latest", tasks: "jsNodeTest" }, + { target: "wasm-node", os: "ubuntu-latest", tasks: "wasmJsNodeTest" }, + { target: "package", os: "ubuntu-latest", tasks: ":demo:composeApp:packageDistributionForCurrentOS" } ] runs-on: ${{ matrix.config.os }} name: Test ${{ matrix.config.target }} @@ -87,12 +89,17 @@ jobs: uses: gradle/actions/wrapper-validation@v4 - name: Run ${{ matrix.config.target }} tests - continue-on-error: ${{ matrix.config.continueOnError }} run: ./gradlew ${{ matrix.config.tasks }} --scan test-browser: + strategy: + matrix: + config: [ + { target: "js", tasks: "jsBrowserTest" }, + { target: "wasm", tasks: "wasmJsBrowserTest" }, + ] runs-on: ubuntu-latest - name: Test JS in browser + name: Test ${{ matrix.config.target }} in browser needs: verify steps: - name: Checkout @@ -114,4 +121,4 @@ jobs: uses: browser-actions/setup-chrome@v1 - name: Run tests in browser - run: ./gradlew jsTest --scan \ No newline at end of file + run: ./gradlew ${{ matrix.config.tasks }} --scan \ No newline at end of file diff --git a/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityTest.kt b/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityTest.kt index 094d222..c9773d0 100644 --- a/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityTest.kt +++ b/connectivity-core/src/commonTest/kotlin/dev/jordond/connectivity/ConnectivityTest.kt @@ -5,24 +5,27 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test +@OptIn(ExperimentalCoroutinesApi::class) class ConnectivityTest { private lateinit var testScope: TestScope - private lateinit var sutScope: CoroutineScope + private lateinit var sutScope: TestScope private lateinit var provider: ConnectivityProvider @BeforeTest fun setup() { testScope = TestScope() - sutScope = CoroutineScope(Dispatchers.Default) + sutScope = TestScope() provider = ConnectivityProvider(flowOf(Connectivity.Status.Connected(false))) } @@ -41,6 +44,7 @@ class ConnectivityTest { connectivity.shouldNotBeNull() connectivity.shouldBeInstanceOf() + sutScope.advanceUntilIdle() connectivity.monitoring.value shouldBe false } @@ -56,6 +60,7 @@ class ConnectivityTest { connectivity.shouldNotBeNull() connectivity.shouldBeInstanceOf() + sutScope.advanceUntilIdle() connectivity.monitoring.value shouldBe true } @@ -70,7 +75,7 @@ class ConnectivityTest { connectivity.shouldNotBeNull() connectivity.shouldBeInstanceOf() - testScheduler.advanceUntilIdle() + sutScope.advanceUntilIdle() connectivity.monitoring.value shouldBe true } From 1d6b86d52b0d4ee440a447b501c8bc1e19a635dc Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Sat, 4 Jan 2025 13:07:42 -0500 Subject: [PATCH 10/13] fix failing tests and warnings --- .../connectivity/convention/KotlinAndroid.kt | 11 ++--------- .../connectivity/convention/Multiplatform.kt | 4 ---- .../AndroidConnectivityProviderTest.kt | 19 +++++-------------- .../connectivity/tools/ContextProviderTest.kt | 3 ++- demo/composeApp/build.gradle.kts | 1 - 5 files changed, 9 insertions(+), 29 deletions(-) diff --git a/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/KotlinAndroid.kt b/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/KotlinAndroid.kt index e6304d8..444be85 100644 --- a/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/KotlinAndroid.kt +++ b/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/KotlinAndroid.kt @@ -6,8 +6,7 @@ import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.get -import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode -import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension internal fun Project.configureAndroid(name: String = this.name) { setNamespace(name) @@ -60,14 +59,8 @@ internal fun Project.setNamespace(name: String) { } } -fun Project.disableExplicitApi() { - extensions.configure { - explicitApi = ExplicitApiMode.Disabled - } -} - internal fun Project.configureKotlin() { - extensions.configure { + extensions.configure { explicitApi() jvmToolchain(jvmTargetVersion) } diff --git a/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt b/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt index 8867cfa..5d0a2ff 100644 --- a/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt +++ b/buildLogic/convention/src/main/kotlin/dev/jordond/connectivity/convention/Multiplatform.kt @@ -1,15 +1,11 @@ -@file:OptIn(ExperimentalWasmDsl::class, ExperimentalKotlinGradlePluginApi::class) - package dev.jordond.connectivity.convention import com.vanniktech.maven.publish.MavenPublishBaseExtension import org.gradle.api.Project import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.get -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl fun Project.configureMultiplatform( platform: Platform, diff --git a/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProviderTest.kt b/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProviderTest.kt index 7496b5d..4e814b1 100644 --- a/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProviderTest.kt +++ b/connectivity-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/internal/AndroidConnectivityProviderTest.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package dev.jordond.connectivity.internal import android.content.Context @@ -269,15 +271,11 @@ class AndroidConnectivityProviderTest { versionCodeProvider = versionCodeProvider(Build.VERSION_CODES.LOLLIPOP) provider = AndroidConnectivityProvider(context, versionCodeProvider) - @Suppress("DEPRECATION") val networkInfo = mockk { every { isConnected } returns true } - every { - @Suppress("DEPRECATION") - connectivityManager.activeNetworkInfo - } returns networkInfo + every { connectivityManager.activeNetworkInfo } returns networkInfo every { connectivityManager.isActiveNetworkMetered } returns true provider.monitor().test { @@ -294,15 +292,11 @@ class AndroidConnectivityProviderTest { versionCodeProvider = versionCodeProvider(Build.VERSION_CODES.LOLLIPOP) provider = AndroidConnectivityProvider(context, versionCodeProvider) - @Suppress("DEPRECATION") val networkInfo = mockk { every { isConnected } returns false } - every { - @Suppress("DEPRECATION") - connectivityManager.activeNetworkInfo - } returns networkInfo + every { connectivityManager.activeNetworkInfo } returns networkInfo provider.monitor().test { triggerNetworkCallback() @@ -316,10 +310,7 @@ class AndroidConnectivityProviderTest { fun shouldReturnDisconnectedStatusWhenVersionIsOlderAndNetworkInfoIsNull() = runTest { versionCodeProvider = versionCodeProvider(Build.VERSION_CODES.LOLLIPOP) provider = AndroidConnectivityProvider(context, versionCodeProvider) - every { - @Suppress("DEPRECATION") - connectivityManager.activeNetworkInfo - } returns null + every { connectivityManager.activeNetworkInfo } returns null provider.monitor().test { triggerNetworkCallback() diff --git a/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt b/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt index 79e3306..e5cbb2f 100644 --- a/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt +++ b/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt @@ -5,6 +5,7 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf +import io.kotest.matchers.types.shouldBeSameInstanceAs import io.mockk.mockk import kotlin.test.AfterTest import kotlin.test.Test @@ -23,7 +24,7 @@ class ContextProviderTest { provider.shouldNotBeNull() provider.shouldBeInstanceOf() - provider.context shouldBe context + provider.context shouldBeSameInstanceAs context } @Test diff --git a/demo/composeApp/build.gradle.kts b/demo/composeApp/build.gradle.kts index 3ab1565..d8edb7a 100644 --- a/demo/composeApp/build.gradle.kts +++ b/demo/composeApp/build.gradle.kts @@ -1,6 +1,5 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi -import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl plugins { alias(libs.plugins.multiplatform) From 2e73242049e1b8e08321e5f17db4515ce22ccf37 Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Sat, 4 Jan 2025 13:52:03 -0500 Subject: [PATCH 11/13] add kover for coverage --- build.gradle.kts | 1 + connectivity-android/build.gradle.kts | 1 + connectivity-apple/build.gradle.kts | 1 + connectivity-compose-device/build.gradle.kts | 1 + connectivity-compose-http/build.gradle.kts | 1 + connectivity-compose/build.gradle.kts | 1 + connectivity-core/build.gradle.kts | 1 + connectivity-device/build.gradle.kts | 1 + connectivity-http/build.gradle.kts | 1 + .../internal/HttpConnectivityTest.kt | 96 ++++++++++++++++++- connectivity-tools-android/build.gradle.kts | 1 + .../connectivity/tools/ContextProviderTest.kt | 11 +-- gradle/libs.versions.toml | 2 + 13 files changed, 108 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3434c98..eb592ef 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,7 @@ plugins { alias(libs.plugins.dokka) alias(libs.plugins.dependencies) alias(libs.plugins.binaryCompatibility) + alias(libs.plugins.kotlinx.kover) } apiValidation { diff --git a/connectivity-android/build.gradle.kts b/connectivity-android/build.gradle.kts index 160159c..3853d5f 100644 --- a/connectivity-android/build.gradle.kts +++ b/connectivity-android/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.poko) alias(libs.plugins.dokka) alias(libs.plugins.publish) + alias(libs.plugins.kotlinx.kover) alias(libs.plugins.convention.multiplatform) } diff --git a/connectivity-apple/build.gradle.kts b/connectivity-apple/build.gradle.kts index 7584c1b..4b17841 100644 --- a/connectivity-apple/build.gradle.kts +++ b/connectivity-apple/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.poko) alias(libs.plugins.dokka) alias(libs.plugins.publish) + alias(libs.plugins.kotlinx.kover) alias(libs.plugins.convention.multiplatform) } diff --git a/connectivity-compose-device/build.gradle.kts b/connectivity-compose-device/build.gradle.kts index 5a495a6..d1e1c71 100644 --- a/connectivity-compose-device/build.gradle.kts +++ b/connectivity-compose-device/build.gradle.kts @@ -9,6 +9,7 @@ plugins { alias(libs.plugins.poko) alias(libs.plugins.dokka) alias(libs.plugins.publish) + alias(libs.plugins.kotlinx.kover) alias(libs.plugins.convention.multiplatform) } diff --git a/connectivity-compose-http/build.gradle.kts b/connectivity-compose-http/build.gradle.kts index 13fcd06..5c725d6 100644 --- a/connectivity-compose-http/build.gradle.kts +++ b/connectivity-compose-http/build.gradle.kts @@ -9,6 +9,7 @@ plugins { alias(libs.plugins.poko) alias(libs.plugins.dokka) alias(libs.plugins.publish) + alias(libs.plugins.kotlinx.kover) alias(libs.plugins.convention.multiplatform) } diff --git a/connectivity-compose/build.gradle.kts b/connectivity-compose/build.gradle.kts index f91819d..978d94c 100644 --- a/connectivity-compose/build.gradle.kts +++ b/connectivity-compose/build.gradle.kts @@ -9,6 +9,7 @@ plugins { alias(libs.plugins.poko) alias(libs.plugins.dokka) alias(libs.plugins.publish) + alias(libs.plugins.kotlinx.kover) alias(libs.plugins.convention.multiplatform) } diff --git a/connectivity-core/build.gradle.kts b/connectivity-core/build.gradle.kts index b26184c..2b8d8dd 100644 --- a/connectivity-core/build.gradle.kts +++ b/connectivity-core/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.poko) alias(libs.plugins.dokka) alias(libs.plugins.publish) + alias(libs.plugins.kotlinx.kover) alias(libs.plugins.convention.multiplatform) } diff --git a/connectivity-device/build.gradle.kts b/connectivity-device/build.gradle.kts index 6375c51..81e1f2d 100644 --- a/connectivity-device/build.gradle.kts +++ b/connectivity-device/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.poko) alias(libs.plugins.dokka) alias(libs.plugins.publish) + alias(libs.plugins.kotlinx.kover) alias(libs.plugins.convention.multiplatform) } diff --git a/connectivity-http/build.gradle.kts b/connectivity-http/build.gradle.kts index 0748b0e..38ee600 100644 --- a/connectivity-http/build.gradle.kts +++ b/connectivity-http/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.poko) alias(libs.plugins.dokka) alias(libs.plugins.publish) + alias(libs.plugins.kotlinx.kover) alias(libs.plugins.convention.multiplatform) } diff --git a/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/internal/HttpConnectivityTest.kt b/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/internal/HttpConnectivityTest.kt index ee8f6d0..8d91e30 100644 --- a/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/internal/HttpConnectivityTest.kt +++ b/connectivity-http/src/commonTest/kotlin/dev/jordond/connectivity/internal/HttpConnectivityTest.kt @@ -16,6 +16,7 @@ import io.ktor.client.engine.mock.respondOk import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode.Companion.InternalServerError +import io.ktor.http.URLProtocol import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -25,6 +26,7 @@ import kotlinx.coroutines.test.runTest import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds class HttpConnectivityTest { @@ -160,6 +162,31 @@ class HttpConnectivityTest { } } + @Test + fun shouldHandleExceptionInMakeRequest() = scope.runTest(timeout = 3.seconds) { + mockEngine = MockEngine { + throw IllegalStateException("Test exception") + } + httpClient = HttpClient(mockEngine) + + var lastResult: PollResult? = null + testConnectivity( + configure = { + onPollResult { lastResult = it } + } + ) { connectivity -> + val status = connectivity.status() + + status.shouldBeInstanceOf() + lastResult.shouldNotBeNull() + lastResult.shouldBeInstanceOf() + + val error = lastResult as PollResult.Error + error.throwable.shouldBeInstanceOf() + error.throwable.message shouldBe "Test exception" + } + } + @Test fun shouldUseSpecifiedHttpMethod() = scope.runTest { var lastMethod: HttpMethod? = null @@ -176,12 +203,79 @@ class HttpConnectivityTest { } } + @Test + fun shouldParseUrlWithHttpPrefix() { + val (protocol, host) = getProtocolAndHost("http://example.com", 80) + + protocol shouldBe URLProtocol.HTTP + host shouldBe "example.com" + } + + @Test + fun shouldParseUrlWithHttpsPrefix() { + val (protocol, host) = getProtocolAndHost("https://example.com", 80) + + protocol shouldBe URLProtocol.HTTPS + host shouldBe "example.com" + } + + @Test + fun shouldDefaultToHttpsWhenPort443() { + val (protocol, host) = getProtocolAndHost("example.com", 443) + + protocol shouldBe URLProtocol.HTTPS + host shouldBe "example.com" + } + + @Test + fun shouldDefaultToHttpWhenNonSecurePort() { + val (protocol, host) = getProtocolAndHost("example.com", 8080) + + protocol shouldBe URLProtocol.HTTP + host shouldBe "example.com" + } + + @Test + fun shouldStopMonitoringWhenStopIsCalled() = scope.runTest { + testConnectivity { connectivity -> + connectivity.start() + connectivity.monitoring.value.shouldBeTrue() + + connectivity.stop() + connectivity.monitoring.value.shouldBeFalse() + } + } + + @Test + fun shouldHandleStopWhenNotMonitoring() = scope.runTest { + testConnectivity { connectivity -> + // Verify initial state + connectivity.monitoring.value.shouldBeFalse() + + // Should not throw or change state + connectivity.stop() + connectivity.monitoring.value.shouldBeFalse() + } + } + + @Test + fun shouldNotStartNewJobWhenAlreadyMonitoring() = scope.runTest { + testConnectivity { connectivity -> + connectivity.start() + connectivity.monitoring.value.shouldBeTrue() + + // Should not affect monitoring state + connectivity.start() + connectivity.monitoring.value.shouldBeTrue() + } + } + private suspend fun testConnectivity( httpClient: HttpClient = this.httpClient, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), configure: HttpConnectivityOptions.Builder.() -> Unit = {}, test: suspend (Connectivity) -> Unit, ) { - val scope = CoroutineScope(Dispatchers.Default) val options = HttpConnectivityOptions.build(configure) val connectivity = HttpConnectivity(scope, options, httpClient) test(connectivity) diff --git a/connectivity-tools-android/build.gradle.kts b/connectivity-tools-android/build.gradle.kts index 1a89a49..7ea7750 100644 --- a/connectivity-tools-android/build.gradle.kts +++ b/connectivity-tools-android/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.multiplatform) alias(libs.plugins.dokka) alias(libs.plugins.publish) + alias(libs.plugins.kotlinx.kover) alias(libs.plugins.convention.multiplatform) } diff --git a/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt b/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt index e5cbb2f..06f9167 100644 --- a/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt +++ b/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt @@ -6,6 +6,7 @@ import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.mockk.every import io.mockk.mockk import kotlin.test.AfterTest import kotlin.test.Test @@ -17,16 +18,6 @@ class ContextProviderTest { ContextProvider.Companion.instance = null } - @Test - fun shouldCreateContextProviderInstance() { - val context = mockk() - val provider = ContextProvider.create(context) - - provider.shouldNotBeNull() - provider.shouldBeInstanceOf() - provider.context shouldBeSameInstanceAs context - } - @Test fun shouldReturnExistingInstanceWhenCreatingMultipleTimes() { val context1 = mockk() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69502c0..9e02a6e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ jvmTarget = "17" agp = "8.7.3" kotlin = "2.1.0" kotlinx-coroutines = "1.10.1" +kotlinx-kover = "0.9.0" compose = "1.7.6" compose-multiplatform = "1.7.3" androidx-activity = "1.9.3" @@ -80,4 +81,5 @@ binaryCompatibility = { id = "org.jetbrains.kotlinx.binary-compatibility-validat dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } publish = { id = "com.vanniktech.maven.publish", version.ref = "publish" } poko = { id = "dev.drewhamilton.poko", version.ref = "poko" } +kotlinx-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kotlinx-kover" } convention-multiplatform = { id = "convention.multiplatform", version = "0" } From 12e771fbf48dbfca767a57c479dbb684a34cd4be Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Sat, 4 Jan 2025 14:12:19 -0500 Subject: [PATCH 12/13] update test suite --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca79754..16c3bd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,9 @@ jobs: matrix: config: [ { target: "default", os: "ubuntu-latest", tasks: "test" }, - { target: "osx", os: "macos-latest", tasks: "test" }, + { target: "ios-simulator", os: "macos-latest", tasks: "iosSimulatorArm64Test" }, + { target: "macos-arm", os: "macos-latest", tasks: "macosArm64Test" }, + { target: "macos-x64", os: "macos-latest", tasks: "macosX64Test" }, { target: "js-node", os: "ubuntu-latest", tasks: "jsNodeTest" }, { target: "wasm-node", os: "ubuntu-latest", tasks: "wasmJsNodeTest" }, { target: "package", os: "ubuntu-latest", tasks: ":demo:composeApp:packageDistributionForCurrentOS" } From 91aba2e5adc93510dced62ada1a53648c4772b04 Mon Sep 17 00:00:00 2001 From: Jordon de Hoog Date: Sat, 4 Jan 2025 15:40:34 -0500 Subject: [PATCH 13/13] fix tests for context --- .../connectivity/tools/ContextProviderTest.kt | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt b/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt index 06f9167..2acb7c0 100644 --- a/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt +++ b/connectivity-tools-android/src/androidUnitTest/kotlin/dev/jordond/connectivity/tools/ContextProviderTest.kt @@ -2,15 +2,14 @@ package dev.jordond.connectivity.tools import android.content.Context import io.kotest.assertions.throwables.shouldThrow -import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.equals.shouldBeEqual import io.kotest.matchers.shouldBe -import io.kotest.matchers.types.shouldBeInstanceOf -import io.kotest.matchers.types.shouldBeSameInstanceAs import io.mockk.every import io.mockk.mockk import kotlin.test.AfterTest import kotlin.test.Test +// The tests seem to fail if we just compare the instances directly, so we need to compare the properties class ContextProviderTest { @AfterTest @@ -20,25 +19,31 @@ class ContextProviderTest { @Test fun shouldReturnExistingInstanceWhenCreatingMultipleTimes() { - val context1 = mockk() - val context2 = mockk() + val context1 = mockk { + every { packageName } returns "com.example" + } + val context2 = mockk { + every { packageName } returns "com.other" + } val provider1 = ContextProvider.create(context1) val provider2 = ContextProvider.create(context2) provider1 shouldBe provider2 - provider1.context shouldBe context1 - provider2.context shouldBe context1 + provider1.context.packageName shouldBeEqual "com.example" + provider2.context.packageName shouldBeEqual "com.example" } @Test fun shouldGetInstanceAfterCreation() { - val context = mockk() + val context = mockk { + every { packageName } returns "com.example" + } val created = ContextProvider.create(context) val instance = ContextProvider.getInstance() instance shouldBe created - instance.context shouldBe context + instance.context.packageName shouldBeEqual "com.example" } @Test