Skip to content

Commit

Permalink
Merge pull request #102 from /issues/90
Browse files Browse the repository at this point in the history
Issue #90
  • Loading branch information
vazarkevych committed May 9, 2024
2 parents 705e48e + 2ac4c8f commit dd4f938
Show file tree
Hide file tree
Showing 13 changed files with 113 additions and 56 deletions.
3 changes: 2 additions & 1 deletion GrowthBook/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ kotlin {
dependencies {
implementation(kotlin("test-junit"))
implementation("junit:junit:4.13.2")
implementation("org.mockito:mockito-core:4.2.0")
implementation("org.mockito:mockito-core:4.8.0")
implementation("io.ktor:ktor-client-mock:$ktorVersion")
implementation("com.soywiz.korlibs.krypto:krypto-android:$kryptoVersion")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

/**
* Android Application Dispatcher
* IO Dispatcher
*/
internal actual val ApplicationDispatcher: CoroutineDispatcher = Dispatchers.Default
internal actual val PlatformDependentIODispatcher: CoroutineDispatcher =
Dispatchers.IO
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.sdk.growthbook

import com.sdk.growthbook.network.DefaultGBNetworkClient
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import org.junit.Test
import io.ktor.client.engine.mock.respond
import io.ktor.http.HttpStatusCode
import io.ktor.http.headersOf
import io.ktor.http.HttpHeaders
import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerializationException
import org.junit.Assert.assertTrue

private const val FEATURES_ENDPOINT = "/api/features/"

class DefaultGBNetworkClientTest {
private val classUnderTest: DefaultGBNetworkClient

init {
val mockEngine = MockEngine {
respond(
content = ByteReadChannel("some content"),
status = HttpStatusCode.OK,
headers = headersOf(HttpHeaders.ContentType, "application/json")
)
}

classUnderTest = DefaultGBNetworkClient(
HttpClient(mockEngine)
)
}

@Test
fun `test successful get request`() {
var wasOnSuccessCalled = false

val job = classUnderTest.consumeGETRequest(
request = FEATURES_ENDPOINT,
onSuccess = { _ ->
wasOnSuccessCalled = true
},
onError = {},
)

runBlocking {
job.join()
}

assertTrue(wasOnSuccessCalled)
}

@Test
fun `test failed get request`() {
var wasOnErrorCalled = false

val job = classUnderTest.consumeGETRequest(
request = FEATURES_ENDPOINT,
onSuccess = {
// typically in onSuccess callback JSON is parsed
throw SerializationException()
},
onError = {
wasOnErrorCalled = true
},
)

runBlocking {
job.join()
}

assertTrue(wasOnErrorCalled)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ package com.sdk.growthbook
import kotlinx.coroutines.CoroutineDispatcher

/**
* Expect Application Dispatcher - from respective iOS & Android counter parts
* Expect IO Dispatcher - from respective iOS & Android counter parts
*/
internal expect val ApplicationDispatcher: CoroutineDispatcher
internal expect val PlatformDependentIODispatcher: CoroutineDispatcher
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.sdk.growthbook.sandbox.CachingImpl
import com.sdk.growthbook.stickybucket.GBStickyBucketService
import com.sdk.growthbook.stickybucket.GBStickyBucketServiceImp
import com.sdk.growthbook.utils.GBCacheRefreshHandler
import kotlinx.coroutines.DelicateCoroutinesApi

/**
* SDKBuilder - Root Class for SDK Initializers for GrowthBook SDK
Expand Down Expand Up @@ -58,7 +57,6 @@ abstract class SDKBuilder(
/**
* This method is open to be overridden by subclasses
*/
@DelicateCoroutinesApi
abstract fun initialize(): GrowthBookSDK
}

Expand Down Expand Up @@ -134,7 +132,6 @@ class GBSDKBuilder(
/**
* Initialize the Kotlin SDK
*/
@DelicateCoroutinesApi
override fun initialize(): GrowthBookSDK {

val gbContext = GBContext(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import com.sdk.growthbook.model.GBContext
import com.sdk.growthbook.model.GBExperiment
import com.sdk.growthbook.model.GBExperimentResult
import com.sdk.growthbook.model.GBFeatureResult
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.Flow

typealias GBTrackingCallback = (GBExperiment, GBExperimentResult) -> Unit
Expand All @@ -43,7 +42,6 @@ class GrowthBookSDK() : FeaturesFlowDelegate {
internal lateinit var gbContext: GBContext
}

@DelicateCoroutinesApi
internal constructor(
context: GBContext,
refreshHandler: GBCacheRefreshHandler?,
Expand Down Expand Up @@ -76,7 +74,6 @@ class GrowthBookSDK() : FeaturesFlowDelegate {
/**
* Manually Refresh Cache
*/
@DelicateCoroutinesApi
fun refreshCache() {
if (gbContext.remoteEval) {
refreshForRemoteEval()
Expand All @@ -95,7 +92,6 @@ class GrowthBookSDK() : FeaturesFlowDelegate {
/**
* receive Features automatically when updated SSE
*/
@OptIn(DelicateCoroutinesApi::class)
fun autoRefreshFeatures(): Flow<Resource<GBFeatures?>> {
return featuresViewModel.autoRefreshFeatures()
}
Expand Down Expand Up @@ -249,7 +245,6 @@ class GrowthBookSDK() : FeaturesFlowDelegate {
/**
* Method for sending request evaluate features remotely
*/
@OptIn(DelicateCoroutinesApi::class)
private fun refreshForRemoteEval() {
if (!gbContext.remoteEval) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.sdk.growthbook.utils.FeatureRefreshStrategy
import com.sdk.growthbook.utils.GBFeatures
import com.sdk.growthbook.utils.GBRemoteEvalParams
import com.sdk.growthbook.utils.Resource
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.transform
import kotlinx.serialization.decodeFromString
Expand Down Expand Up @@ -35,7 +34,6 @@ internal class FeaturesDataSource(private val dispatcher: NetworkDispatcher) {
/**
* Executes API Call to fetch features
*/
@DelicateCoroutinesApi
fun fetchFeatures(
success: (FeaturesDataModel) -> Unit, failure: (Throwable?) -> Unit
) {
Expand All @@ -55,7 +53,6 @@ internal class FeaturesDataSource(private val dispatcher: NetworkDispatcher) {
/**
* Supportive method for automatically refresh features
*/
@DelicateCoroutinesApi
fun autoRefresh(
success: (FeaturesDataModel) -> Unit, failure: (Throwable?) -> Unit
): Flow<Resource<GBFeatures?>> = dispatcher.consumeSSEConnection(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import com.sdk.growthbook.utils.getFeaturesFromEncryptedFeatures
import com.sdk.growthbook.sandbox.CachingImpl
import com.sdk.growthbook.sandbox.getData
import com.sdk.growthbook.sandbox.putData
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.Flow

/**
Expand Down Expand Up @@ -39,7 +38,6 @@ internal class FeaturesViewModel(
/**
* Fetch Features
*/
@DelicateCoroutinesApi
fun fetchFeatures(remoteEval: Boolean = false, payload: GBRemoteEvalParams? = null) {
try {
// Check for cache data
Expand Down Expand Up @@ -101,7 +99,6 @@ internal class FeaturesViewModel(
/**
* Supportive method for automatically refresh features
*/
@DelicateCoroutinesApi
fun autoRefreshFeatures(): Flow<Resource<GBFeatures?>> {
return dataSource.autoRefresh(success = { dataModel ->
prepareFeaturesData(dataModel = dataModel)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.sdk.growthbook.network

import com.sdk.growthbook.ApplicationDispatcher
import com.sdk.growthbook.PlatformDependentIODispatcher
import com.sdk.growthbook.utils.Resource
import com.sdk.growthbook.utils.readSse
import com.sdk.growthbook.utils.toJsonElement
Expand All @@ -18,8 +18,8 @@ import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
Expand All @@ -31,12 +31,11 @@ import kotlinx.serialization.json.Json
* Implement this interface to define specific implementation for Network Calls - to be made by SDK
*/
interface NetworkDispatcher {
@DelicateCoroutinesApi
fun consumeGETRequest(
request: String,
onSuccess: (String) -> Unit,
onError: (Throwable) -> Unit
)
): Job

fun consumeSSEConnection(
url: String
Expand All @@ -50,16 +49,8 @@ interface NetworkDispatcher {
)
}

/**
* Default Ktor Implementation for Network Dispatcher
*/
@Suppress("unused")
class DefaultGBNetworkClient : NetworkDispatcher {

/**
* Ktor http client instance for sending request
*/
private val client = HttpClient {
internal fun createDefaultHttpClient(): HttpClient =
HttpClient {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
Expand All @@ -69,27 +60,34 @@ class DefaultGBNetworkClient : NetworkDispatcher {
}
}

/**
* Default Ktor Implementation for Network Dispatcher
*/
class DefaultGBNetworkClient(

/**
* Ktor http client instance for sending request
*/
private val client: HttpClient = createDefaultHttpClient()

) : NetworkDispatcher {

/**
* Function that execute API Call to fetch features
*/
@DelicateCoroutinesApi
override fun consumeGETRequest(
request: String,
onSuccess: (String) -> Unit,
onError: (Throwable) -> Unit
) {

GlobalScope.launch(ApplicationDispatcher) {

): Job =
CoroutineScope(PlatformDependentIODispatcher).launch {
val result = client.get(request)
try {
val result = client.get(request)
onSuccess(result.body())
} catch (ex: Exception) {
onError(ex)
}

}
}

/**
* Supportive method for preparing GET request for consuming SSE connection
Expand All @@ -109,11 +107,10 @@ class DefaultGBNetworkClient : NetworkDispatcher {
/**
* Method that produce SSE connection
*/
@OptIn(DelicateCoroutinesApi::class)
override fun consumeSSEConnection(
url: String
) = callbackFlow {
GlobalScope.launch(ApplicationDispatcher) {
CoroutineScope(PlatformDependentIODispatcher).launch {
try {
prepareGetRequest(url).execute { response ->
val channel: ByteReadChannel = response.body()
Expand All @@ -135,14 +132,13 @@ class DefaultGBNetworkClient : NetworkDispatcher {
/**
* Method that make POST request to server for remote feature evaluation
*/
@OptIn(DelicateCoroutinesApi::class)
override fun consumePOSTRequest(
url: String,
bodyParams: Map<String, Any>,
onSuccess: (String) -> Unit,
onError: (Throwable) -> Unit
) {
GlobalScope.launch(ApplicationDispatcher) {
CoroutineScope(PlatformDependentIODispatcher).launch {
try {
val response = client.post(url) {
headers {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.sdk.growthbook.integration

import org.intellij.lang.annotations.Language
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class IntegrationTests {
Expand Down Expand Up @@ -40,9 +39,8 @@ class IntegrationTests {

val sdkInstance = buildSDK(json, attrs)

assertEquals(
expected = "true",
actual = sdkInstance.feature("user576-feature").value.toString(),
assertTrue(
sdkInstance.isOn("user576-feature")
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import com.sdk.growthbook.GBSDKBuilder
import com.sdk.growthbook.utils.DefaultCrypto
import com.sdk.growthbook.utils.encryptToFeaturesDataModel
import com.soywiz.krypto.encoding.Base64
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.serialization.json.JsonArray
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -46,7 +45,6 @@ class GBEncryptedFeatures {
private val testHostURL = "https://host.com"
private val testAttributes: HashMap<String, Any> = HashMap()

@OptIn(DelicateCoroutinesApi::class)
@Test
fun testEncrypt() {

Expand Down
Loading

0 comments on commit dd4f938

Please sign in to comment.