test(kotlin-sdk): add JUnit5 + MockWebServer tests for PlatformConfig, PlatformClient, FeatureFlag, KillSwitch, License (35 tests)

This commit is contained in:
saravanakumardb1 2026-03-01 20:36:58 -08:00
parent 91c48a7bc7
commit 70703a5009
5 changed files with 621 additions and 0 deletions

View File

@ -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
}
}

View File

@ -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"))
}
}

View File

@ -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)
}
}

View File

@ -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"))
}
}

View File

@ -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)
}
}