From 70703a50097b49346539cab4f1362716acbb3a8a Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 1 Mar 2026 20:36:58 -0800 Subject: [PATCH] test(kotlin-sdk): add JUnit5 + MockWebServer tests for PlatformConfig, PlatformClient, FeatureFlag, KillSwitch, License (35 tests) --- .../platform/BLFeatureFlagClientTest.kt | 134 ++++++++++++++ .../platform/BLKillSwitchClientTest.kt | 113 +++++++++++ .../bytelyst/platform/BLLicenseClientTest.kt | 137 ++++++++++++++ .../bytelyst/platform/BLPlatformClientTest.kt | 175 ++++++++++++++++++ .../bytelyst/platform/BLPlatformConfigTest.kt | 62 +++++++ 5 files changed, 621 insertions(+) create mode 100644 packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLFeatureFlagClientTest.kt create mode 100644 packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLKillSwitchClientTest.kt create mode 100644 packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLLicenseClientTest.kt create mode 100644 packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLPlatformClientTest.kt create mode 100644 packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLPlatformConfigTest.kt diff --git a/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLFeatureFlagClientTest.kt b/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLFeatureFlagClientTest.kt new file mode 100644 index 00000000..050a2ff3 --- /dev/null +++ b/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLFeatureFlagClientTest.kt @@ -0,0 +1,134 @@ +package com.bytelyst.platform + +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class BLFeatureFlagClientTest { + + private lateinit var server: MockWebServer + private lateinit var config: BLPlatformConfig + + @BeforeEach + fun setup() { + server = MockWebServer() + server.start() + config = BLPlatformConfig( + productId = "testapp", + baseUrl = server.url("/api").toString().trimEnd('/'), + applicationId = "com.test.app", + ) + } + + @AfterEach + fun teardown() { + server.shutdown() + } + + @Test + fun `isEnabled returns false before init`() { + val client = BLFeatureFlagClient(config) + assertFalse(client.isEnabled("any_flag")) + } + + @Test + fun `getAllFlags returns empty map before init`() { + val client = BLFeatureFlagClient(config) + assertTrue(client.getAllFlags().isEmpty()) + } + + @Test + fun `refresh fetches flags from server`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"flags":{"dark_mode":true,"beta_feature":false}}""") + .setResponseCode(200) + ) + + val client = BLFeatureFlagClient(config) + client.refresh() + + assertTrue(client.isEnabled("dark_mode")) + assertFalse(client.isEnabled("beta_feature")) + assertFalse(client.isEnabled("unknown_flag")) + } + + @Test + fun `getAllFlags returns current flags after refresh`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"flags":{"a":true,"b":false}}""") + .setResponseCode(200) + ) + + val client = BLFeatureFlagClient(config) + client.refresh() + + val flags = client.getAllFlags() + assertEquals(2, flags.size) + assertTrue(flags["a"] == true) + assertTrue(flags["b"] == false) + } + + @Test + fun `keeps existing flags on server error`() = runTest { + // First: successful fetch + server.enqueue( + MockResponse() + .setBody("""{"flags":{"feature_x":true}}""") + .setResponseCode(200) + ) + // Second: server error + server.enqueue(MockResponse().setResponseCode(500)) + + val client = BLFeatureFlagClient(config) + client.refresh() + assertTrue(client.isEnabled("feature_x")) + + client.refresh() + // Should still have the old flags + assertTrue(client.isEnabled("feature_x")) + } + + @Test + fun `sends platform query parameter`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"flags":{}}""") + .setResponseCode(200) + ) + + val client = BLFeatureFlagClient(config) + client.refresh() + + val recorded = server.takeRequest() + assertTrue(recorded.path!!.contains("platform=android")) + } + + @Test + fun `sends X-Product-Id header`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"flags":{}}""") + .setResponseCode(200) + ) + + val client = BLFeatureFlagClient(config) + client.refresh() + + val recorded = server.takeRequest() + assertEquals("testapp", recorded.getHeader("X-Product-Id")) + } + + @Test + fun `stop cancels polling`() { + val client = BLFeatureFlagClient(config, pollIntervalMs = 100_000L) + client.init() + client.stop() + // No assertions needed — verifies stop() doesn't throw + } +} diff --git a/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLKillSwitchClientTest.kt b/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLKillSwitchClientTest.kt new file mode 100644 index 00000000..0f2438d2 --- /dev/null +++ b/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLKillSwitchClientTest.kt @@ -0,0 +1,113 @@ +package com.bytelyst.platform + +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class BLKillSwitchClientTest { + + private lateinit var server: MockWebServer + private lateinit var config: BLPlatformConfig + + @BeforeEach + fun setup() { + server = MockWebServer() + server.start() + config = BLPlatformConfig( + productId = "testapp", + baseUrl = server.url("/api").toString().trimEnd('/'), + applicationId = "com.test.app", + ) + } + + @AfterEach + fun teardown() { + server.shutdown() + } + + @Test + fun `returns ok when not disabled`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"disabled":false}""") + .setResponseCode(200) + ) + + val client = BLKillSwitchClient(config) + val result = client.check() + + assertFalse(result.disabled) + assertNull(result.message) + } + + @Test + fun `returns disabled with message`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"disabled":true,"message":"Maintenance in progress"}""") + .setResponseCode(200) + ) + + val client = BLKillSwitchClient(config) + val result = client.check() + + assertTrue(result.disabled) + assertEquals("Maintenance in progress", result.message) + } + + @Test + fun `fail-open on server error`() = runTest { + server.enqueue(MockResponse().setResponseCode(500)) + + val client = BLKillSwitchClient(config) + val result = client.check() + + assertFalse(result.disabled) + assertNull(result.message) + } + + @Test + fun `fail-open on network error`() = runTest { + server.shutdown() // Force connection failure + + val client = BLKillSwitchClient(config) + val result = client.check() + + assertFalse(result.disabled) + } + + @Test + fun `sends correct query parameters`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"disabled":false}""") + .setResponseCode(200) + ) + + val client = BLKillSwitchClient(config) + client.check() + + val recorded = server.takeRequest() + assertTrue(recorded.path!!.contains("platform=android")) + assertEquals("GET", recorded.method) + } + + @Test + fun `sends X-Product-Id header`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"disabled":false}""") + .setResponseCode(200) + ) + + val client = BLKillSwitchClient(config) + client.check() + + val recorded = server.takeRequest() + assertEquals("testapp", recorded.getHeader("X-Product-Id")) + } +} diff --git a/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLLicenseClientTest.kt b/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLLicenseClientTest.kt new file mode 100644 index 00000000..81088017 --- /dev/null +++ b/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLLicenseClientTest.kt @@ -0,0 +1,137 @@ +package com.bytelyst.platform + +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class BLLicenseClientTest { + + private lateinit var server: MockWebServer + private lateinit var config: BLPlatformConfig + + @BeforeEach + fun setup() { + server = MockWebServer() + server.start() + config = BLPlatformConfig( + productId = "testapp", + baseUrl = server.url("/api").toString().trimEnd('/'), + applicationId = "com.test.app", + ) + } + + @AfterEach + fun teardown() { + server.shutdown() + } + + @Test + fun `activate returns success result`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"success":true,"plan":"pro","message":"Activated"}""") + .setResponseCode(200) + ) + + val client = BLLicenseClient(config) + val result = client.activate("LYSNR-ABCD-1234") + + assertTrue(result.success) + assertEquals("pro", result.plan) + assertEquals("Activated", result.message) + } + + @Test + fun `activate URL-encodes the license key`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"success":true,"plan":"pro"}""") + .setResponseCode(200) + ) + + val client = BLLicenseClient(config) + client.activate("KEY+WITH SPACE") + + val recorded = server.takeRequest() + assertTrue(recorded.path!!.contains("KEY%2BWITH+SPACE") || recorded.path!!.contains("KEY%2BWITH%20SPACE")) + } + + @Test + fun `activate sends productId in body`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"success":true,"plan":"free"}""") + .setResponseCode(200) + ) + + val client = BLLicenseClient(config) + client.activate("TEST-KEY") + + val recorded = server.takeRequest() + assertTrue(recorded.body.readUtf8().contains("testapp")) + } + + @Test + fun `activate returns failure on error`() = runTest { + server.enqueue(MockResponse().setResponseCode(400).setBody("""{"error":"invalid"}""")) + + val client = BLLicenseClient(config) + val result = client.activate("BAD-KEY") + + assertFalse(result.success) + assertNotNull(result.message) + } + + @Test + fun `checkStatus returns valid license`() = runTest { + server.enqueue( + MockResponse() + .setBody("""{"valid":true,"plan":"pro","expiresAt":"2027-01-01"}""") + .setResponseCode(200) + ) + + val client = BLLicenseClient(config) + val result = client.checkStatus("LYSNR-ABCD-1234") + + assertTrue(result.valid) + assertEquals("pro", result.plan) + assertEquals("2027-01-01", result.expiresAt) + } + + @Test + fun `checkStatus returns invalid on error`() = runTest { + server.enqueue(MockResponse().setResponseCode(404).setBody("""{"error":"not found"}""")) + + val client = BLLicenseClient(config) + val result = client.checkStatus("UNKNOWN") + + assertFalse(result.valid) + } + + @Test + fun `deactivate returns true on success`() = runTest { + server.enqueue(MockResponse().setBody("{}").setResponseCode(200)) + + val client = BLLicenseClient(config) + val result = client.deactivate("LYSNR-ABCD-1234") + + assertTrue(result) + val recorded = server.takeRequest() + assertEquals("POST", recorded.method) + assertTrue(recorded.path!!.contains("deactivate")) + } + + @Test + fun `deactivate returns false on error`() = runTest { + server.enqueue(MockResponse().setResponseCode(500)) + + val client = BLLicenseClient(config) + val result = client.deactivate("LYSNR-ABCD-1234") + + assertFalse(result) + } +} diff --git a/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLPlatformClientTest.kt b/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLPlatformClientTest.kt new file mode 100644 index 00000000..c7e801fa --- /dev/null +++ b/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLPlatformClientTest.kt @@ -0,0 +1,175 @@ +package com.bytelyst.platform + +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class BLPlatformClientTest { + + private lateinit var server: MockWebServer + private lateinit var config: BLPlatformConfig + + @BeforeEach + fun setup() { + server = MockWebServer() + server.start() + config = BLPlatformConfig( + productId = "testapp", + baseUrl = server.url("/api").toString().trimEnd('/'), + applicationId = "com.test.app", + timeoutMs = 5_000L, + ) + } + + @AfterEach + fun teardown() { + server.shutdown() + } + + @Test + fun `GET request returns response body`() = runTest { + server.enqueue(MockResponse().setBody("""{"ok":true}""").setResponseCode(200)) + + val client = BLPlatformClient(config) + val result = client.request("GET", "/health") + + assertEquals("""{"ok":true}""", result) + val recorded = server.takeRequest() + assertEquals("GET", recorded.method) + assertTrue(recorded.path!!.endsWith("/api/health")) + } + + @Test + fun `POST request sends body`() = runTest { + server.enqueue(MockResponse().setBody("""{"id":"1"}""").setResponseCode(201)) + + val client = BLPlatformClient(config) + val result = client.request("POST", "/items", body = """{"name":"test"}""") + + assertEquals("""{"id":"1"}""", result) + val recorded = server.takeRequest() + assertEquals("POST", recorded.method) + assertEquals("""{"name":"test"}""", recorded.body.readUtf8()) + } + + @Test + fun `injects X-Product-Id header`() = runTest { + server.enqueue(MockResponse().setBody("{}").setResponseCode(200)) + + val client = BLPlatformClient(config) + client.request("GET", "/test") + + val recorded = server.takeRequest() + assertEquals("testapp", recorded.getHeader("X-Product-Id")) + } + + @Test + fun `injects X-Request-Id header`() = runTest { + server.enqueue(MockResponse().setBody("{}").setResponseCode(200)) + + val client = BLPlatformClient(config) + client.request("GET", "/test") + + val recorded = server.takeRequest() + val requestId = recorded.getHeader("X-Request-Id") + assertNotNull(requestId) + assertTrue(requestId!!.isNotBlank()) + } + + @Test + fun `injects Authorization header when token provided`() = runTest { + server.enqueue(MockResponse().setBody("{}").setResponseCode(200)) + + val client = BLPlatformClient(config) { "my-token-123" } + client.request("GET", "/test") + + val recorded = server.takeRequest() + assertEquals("Bearer my-token-123", recorded.getHeader("Authorization")) + } + + @Test + fun `omits Authorization header when no token`() = runTest { + server.enqueue(MockResponse().setBody("{}").setResponseCode(200)) + + val client = BLPlatformClient(config) { null } + client.request("GET", "/test") + + val recorded = server.takeRequest() + assertNull(recorded.getHeader("Authorization")) + } + + @Test + fun `skips auth when skipAuth is true`() = runTest { + server.enqueue(MockResponse().setBody("{}").setResponseCode(200)) + + val client = BLPlatformClient(config) { "should-not-appear" } + client.request("GET", "/test", skipAuth = true) + + val recorded = server.takeRequest() + assertNull(recorded.getHeader("Authorization")) + } + + @Test + fun `throws BLApiException on non-2xx`() = runTest { + server.enqueue(MockResponse().setBody("""{"error":"not found"}""").setResponseCode(404)) + + val client = BLPlatformClient(config) + val ex = assertThrows(BLApiException::class.java) { + kotlinx.coroutines.runBlocking { + client.request("GET", "/missing") + } + } + assertEquals(404, ex.statusCode) + assertTrue(ex.responseBody.contains("not found")) + } + + @Test + fun `fireAndForget swallows errors`() = runTest { + server.enqueue(MockResponse().setResponseCode(500)) + + val client = BLPlatformClient(config) + // Should not throw + client.fireAndForget("POST", "/telemetry", body = """{"event":"test"}""") + + assertEquals(1, server.requestCount) + } + + @Test + fun `PUT request works`() = runTest { + server.enqueue(MockResponse().setBody("""{"updated":true}""").setResponseCode(200)) + + val client = BLPlatformClient(config) + val result = client.request("PUT", "/items/1", body = """{"name":"updated"}""") + + assertEquals("""{"updated":true}""", result) + val recorded = server.takeRequest() + assertEquals("PUT", recorded.method) + } + + @Test + fun `DELETE request works`() = runTest { + server.enqueue(MockResponse().setBody("").setResponseCode(204)) + + val client = BLPlatformClient(config) + val result = client.request("DELETE", "/items/1") + + assertEquals("", result) + val recorded = server.takeRequest() + assertEquals("DELETE", recorded.method) + } + + @Test + fun `extra headers are sent`() = runTest { + server.enqueue(MockResponse().setBody("{}").setResponseCode(200)) + + val client = BLPlatformClient(config) + client.request("GET", "/test", extraHeaders = mapOf("X-Custom" to "value")) + + val recorded = server.takeRequest() + assertEquals("value", recorded.getHeader("X-Custom")) + } +} diff --git a/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLPlatformConfigTest.kt b/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLPlatformConfigTest.kt new file mode 100644 index 00000000..b40ba6ef --- /dev/null +++ b/packages/kotlin-platform-sdk/src/test/kotlin/com/bytelyst/platform/BLPlatformConfigTest.kt @@ -0,0 +1,62 @@ +package com.bytelyst.platform + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class BLPlatformConfigTest { + + @Test + fun `should create config with required fields`() { + val config = BLPlatformConfig( + productId = "testapp", + baseUrl = "https://api.test.com/api", + applicationId = "com.test.app", + ) + assertEquals("testapp", config.productId) + assertEquals("https://api.test.com/api", config.baseUrl) + assertEquals("com.test.app", config.applicationId) + } + + @Test + fun `should have sensible defaults`() { + val config = BLPlatformConfig( + productId = "testapp", + baseUrl = "https://api.test.com", + applicationId = "com.test.app", + ) + assertEquals("android", config.platform) + assertEquals("native", config.channel) + assertEquals(15_000L, config.timeoutMs) + } + + @Test + fun `should allow overriding defaults`() { + val config = BLPlatformConfig( + productId = "testapp", + baseUrl = "https://api.test.com", + platform = "wear_os", + channel = "keyboard", + applicationId = "com.test.app", + timeoutMs = 5_000L, + ) + assertEquals("wear_os", config.platform) + assertEquals("keyboard", config.channel) + assertEquals(5_000L, config.timeoutMs) + } + + @Test + fun `data class equality should work`() { + val a = BLPlatformConfig(productId = "x", baseUrl = "http://a", applicationId = "com.x") + val b = BLPlatformConfig(productId = "x", baseUrl = "http://a", applicationId = "com.x") + assertEquals(a, b) + assertEquals(a.hashCode(), b.hashCode()) + } + + @Test + fun `copy should create modified config`() { + val original = BLPlatformConfig(productId = "x", baseUrl = "http://a", applicationId = "com.x") + val modified = original.copy(productId = "y") + assertEquals("y", modified.productId) + assertEquals(original.baseUrl, modified.baseUrl) + } +}