test(kotlin-sdk): add JUnit5 + MockWebServer tests for PlatformConfig, PlatformClient, FeatureFlag, KillSwitch, License (35 tests)
This commit is contained in:
parent
91c48a7bc7
commit
70703a5009
@ -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
|
||||
}
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user