Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Part 1: Convert integration tests into proper unit tests #5

Merged
merged 12 commits into from
Jan 9, 2023
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
build
/captures
.externalNativeBuild
.cxx
Expand Down
1 change: 0 additions & 1 deletion FlagsmithClient/.gitignore

This file was deleted.

3 changes: 2 additions & 1 deletion FlagsmithClient/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ dependencies {
implementation 'com.github.kittinunf.fuel:fuel-gson:2.3.1'

testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
testImplementation 'org.mock-server:mockserver-netty-no-dependencies:5.14.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
}

afterEvaluate {
Expand Down
147 changes: 62 additions & 85 deletions FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.flagsmith.internal.FlagsmithApi
import com.flagsmith.internal.*
import com.flagsmith.entities.*
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.result.Result as FuelResult

/**
* Flagsmith
Expand All @@ -20,21 +21,18 @@ import com.github.kittinunf.fuel.Fuel
*/
class Flagsmith constructor(
private val environmentKey: String,
private val baseUrl: String? = null,
val context: Context? = null,
private val baseUrl: String = "https://edge.api.flagsmith.com/api/v1",
private val context: Context? = null,
private val enableAnalytics: Boolean = DEFAULT_ENABLE_ANALYTICS,
private val analyticsFlushPeriod: Int = DEFAULT_ANALYTICS_FLUSH_PERIOD_SECONDS
) {
private var analytics: FlagsmithAnalytics? = null
private val analytics: FlagsmithAnalytics? =
if (!enableAnalytics) null
else if (context != null) FlagsmithAnalytics(context, analyticsFlushPeriod)
else throw IllegalArgumentException("Flagsmith requires a context to use the analytics feature")

init {
if (enableAnalytics && context != null) {
this.analytics = FlagsmithAnalytics(context, analyticsFlushPeriod)
}
if (enableAnalytics && context == null) {
throw IllegalArgumentException("Flagsmith requires a context to use the analytics feature")
}
FlagsmithApi.baseUrl = baseUrl ?: "https://edge.api.flagsmith.com/api/v1"
FlagsmithApi.baseUrl = baseUrl
FlagsmithApi.environmentKey = environmentKey
}

Expand All @@ -45,97 +43,76 @@ class Flagsmith constructor(

fun getFeatureFlags(identity: String? = null, result: (Result<List<Flag>>) -> Unit) {
if (identity != null) {
Fuel.request(
FlagsmithApi.getIdentityFlagsAndTraits(identity = identity))
.responseObject(IdentityFlagsAndTraitsDeserializer()) { _, _, res ->
res.fold(
success = { value -> result(Result.success(value.flags)) },
failure = { err -> result(Result.failure(err)) }
)
}
getIdentityFlagsAndTraits(identity) { res ->
result(res.map { it.flags })
}
} else {
Fuel.request(FlagsmithApi.getFlags())
Fuel.request(FlagsmithApi.GetFlags)
.responseObject(FlagListDeserializer()) { _, _, res ->
res.fold(
success = { value -> result(Result.success(value)) },
failure = { err -> result(Result.failure(err)) }
)
result(res.convertToResult())
}
}
}

fun hasFeatureFlag(forFeatureId: String, identity: String? = null, result:(Result<Boolean>) -> Unit) {
getFeatureFlags(identity) { res ->
res.fold(
onSuccess = { flags ->
val foundFlag = flags.find { flag -> flag.feature.name == forFeatureId && flag.enabled }
analytics?.trackEvent(forFeatureId)
result(Result.success(foundFlag != null))
},
onFailure = { err -> result(Result.failure(err))}
)
}
fun hasFeatureFlag(
featureId: String,
identity: String? = null,
result: (Result<Boolean>) -> Unit
) = getFeatureFlag(featureId, identity) { res ->
result(res.map { flag -> flag != null })
}

fun getValueForFeature(searchFeatureId: String, identity: String? = null, result: (Result<Any?>) -> Unit) {
getFeatureFlags(identity) { res ->
res.fold(
onSuccess = { flags ->
val foundFlag = flags.find { flag -> flag.feature.name == searchFeatureId && flag.enabled }
analytics?.trackEvent(searchFeatureId)
result(Result.success(foundFlag?.featureStateValue))
},
onFailure = { err -> result(Result.failure(err))}
)
}
fun getValueForFeature(
featureId: String,
identity: String? = null,
result: (Result<Any?>) -> Unit
) = getFeatureFlag(featureId, identity) { res ->
result(res.map { flag -> flag?.featureStateValue })
}

fun getTrait(id: String, identity: String, result: (Result<Trait?>) -> Unit) {
Fuel.request(
FlagsmithApi.getIdentityFlagsAndTraits(identity = identity))
.responseObject(IdentityFlagsAndTraitsDeserializer()) { _, _, res ->
res.fold(
success = { value ->
val trait = value.traits.find { it.key == id }
result(Result.success(trait))
},
failure = { err -> result(Result.failure(err)) }
)
}
}
fun getTrait(id: String, identity: String, result: (Result<Trait?>) -> Unit) =
getIdentityFlagsAndTraits(identity) { res ->
result(res.map { value -> value.traits.find { it.key == id } })
}

fun getTraits(identity: String, result: (Result<List<Trait>>) -> Unit) {
Fuel.request(
FlagsmithApi.getIdentityFlagsAndTraits(identity = identity))
.responseObject(IdentityFlagsAndTraitsDeserializer()) { _, _, res ->
res.fold(
success = { value -> result(Result.success(value.traits)) },
failure = { err -> result(Result.failure(err)) }
)
}
}
fun getTraits(identity: String, result: (Result<List<Trait>>) -> Unit) =
getIdentityFlagsAndTraits(identity) { res ->
result(res.map { it.traits })
}

fun setTrait(trait: Trait, identity: String, result: (Result<TraitWithIdentity>) -> Unit) {
Fuel.request(
FlagsmithApi.setTrait(trait = trait, identity = identity))
Fuel.request(FlagsmithApi.SetTrait(trait = trait, identity = identity))
.responseObject(TraitWithIdentityDeserializer()) { _, _, res ->
res.fold(
success = { value -> result(Result.success(value)) },
failure = { err -> result(Result.failure(err)) }
)
result(res.convertToResult())
}
}

fun getIdentity(identity: String, result: (Result<IdentityFlagsAndTraits>) -> Unit){
Fuel.request(
FlagsmithApi.getIdentityFlagsAndTraits(identity = identity))
.responseObject(IdentityFlagsAndTraitsDeserializer()) { _, _, res ->
res.fold(
success = { value ->
result(Result.success(value))
},
failure = { err -> result(Result.failure(err)) }
)
}
fun getIdentity(identity: String, result: (Result<IdentityFlagsAndTraits>) -> Unit) =
getIdentityFlagsAndTraits(identity, result)

private fun getFeatureFlag(
featureId: String,
identity: String?,
result: (Result<Flag?>) -> Unit
) = getFeatureFlags(identity) { res ->
result(res.map { flags ->
val foundFlag = flags.find { flag -> flag.feature.name == featureId && flag.enabled }
analytics?.trackEvent(featureId)
foundFlag
})
}

private fun getIdentityFlagsAndTraits(
identity: String,
result: (Result<IdentityFlagsAndTraits>) -> Unit
) = Fuel.request(FlagsmithApi.GetIdentityFlagsAndTraits(identity = identity))
.responseObject(IdentityFlagsAndTraitsDeserializer()) { _, _, res ->
result(res.convertToResult())
}

private fun <A, B : Exception> FuelResult<A, B>.convertToResult(): Result<A> =
fold(
success = { value -> Result.success(value) },
failure = { err -> Result.failure(err) }
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.flagsmith.Flagsmith
import com.github.kittinunf.fuel.Fuel
import org.json.JSONException
import org.json.JSONObject
Expand All @@ -21,7 +20,7 @@ class FlagsmithAnalytics constructor(
private val timerRunnable = object : Runnable {
override fun run() {
if (currentEvents.isNotEmpty()) {
Fuel.request(FlagsmithApi.postAnalytics(eventMap = currentEvents))
Fuel.request(FlagsmithApi.PostAnalytics(eventMap = currentEvents))
.response { _, _, res ->
res.fold(
success = { resetMap() },
Expand All @@ -44,7 +43,7 @@ class FlagsmithAnalytics constructor(
/// Counts the instances of a `Flag` being queried.
fun trackEvent(flagName: String) {
val currentFlagCount = currentEvents[flagName] ?: 0
currentEvents[flagName] = currentFlagCount + 1;
currentEvents[flagName] = currentFlagCount + 1

// Update events cache
setMap(currentEvents)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import com.github.kittinunf.fuel.core.Parameters
import com.github.kittinunf.fuel.util.FuelRouting
import com.google.gson.Gson

sealed class FlagsmithApi: FuelRouting {
class getIdentityFlagsAndTraits(val identity: String): FlagsmithApi()
class getFlags(): FlagsmithApi()
class setTrait(val trait: Trait, val identity: String): FlagsmithApi()
class postAnalytics(val eventMap: Map<String, Int?>): FlagsmithApi()
sealed class FlagsmithApi : FuelRouting {
class GetIdentityFlagsAndTraits(val identity: String) : FlagsmithApi()
object GetFlags : FlagsmithApi()
matthewelwell marked this conversation as resolved.
Show resolved Hide resolved
class SetTrait(val trait: Trait, val identity: String) : FlagsmithApi()
class PostAnalytics(val eventMap: Map<String, Int?>) : FlagsmithApi()

companion object {
var environmentKey: String? = null
Expand All @@ -29,52 +29,49 @@ sealed class FlagsmithApi: FuelRouting {
}
}
override val body: String?
get() {
return when(this) {
is setTrait -> Gson().toJson(TraitWithIdentity(key = trait.key, value = trait.value, identity = Identity(identity)))
is postAnalytics -> Gson().toJson(eventMap)
else -> null
}
get() = when (this) {
is SetTrait -> Gson().toJson(
TraitWithIdentity(
key = trait.key,
value = trait.value,
identity = Identity(identity)
)
)
is PostAnalytics -> Gson().toJson(eventMap)
else -> null
}

override val bytes: ByteArray?
get() = null
override val bytes: ByteArray? = null

override val headers: Map<String, HeaderValues>?
get() {
val headers = mutableMapOf<String, HeaderValues>("X-Environment-Key" to listOf(environmentKey ?: ""))
get() = mutableMapOf<String, HeaderValues>(
"X-Environment-Key" to listOf(environmentKey ?: "")
).apply {
if (method == Method.POST) {
headers["Content-Type"] = listOf("application/json")
this += "Content-Type" to listOf("application/json")
}
return headers
}

override val method: Method
get() {
return when(this) {
is getIdentityFlagsAndTraits -> Method.GET
is getFlags -> Method.GET
is setTrait -> Method.POST
is postAnalytics -> Method.POST
}
get() = when (this) {
is GetIdentityFlagsAndTraits -> Method.GET
is GetFlags -> Method.GET
is SetTrait -> Method.POST
is PostAnalytics -> Method.POST
}

override val params: Parameters?
get() {
return when(this) {
is getIdentityFlagsAndTraits -> listOf("identifier" to this.identity)
is setTrait -> listOf("identifier" to this.identity)
else -> null
}
get() = when (this) {
is GetIdentityFlagsAndTraits -> listOf("identifier" to this.identity)
is SetTrait -> listOf("identifier" to this.identity)
matthewelwell marked this conversation as resolved.
Show resolved Hide resolved
else -> null
}

override val path: String
get() {
return when(this) {
is getIdentityFlagsAndTraits -> "/identities"
is getFlags -> "/flags"
is setTrait -> "/traits"
is postAnalytics -> "/analytics/flags"
}
get() = when (this) {
is GetIdentityFlagsAndTraits -> "/identities"
is GetFlags -> "/flags"
is SetTrait -> "/traits"
is PostAnalytics -> "/analytics/flags"
}
}
Loading