Skip to content

Commit 98ccba5

Browse files
committed
Add basic integration tests
Signed-off-by: Bence Hornák <[email protected]>
1 parent a895a3f commit 98ccba5

File tree

6 files changed

+459
-1
lines changed

6 files changed

+459
-1
lines changed

gradle/libs.versions.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ ktlint = { id="org.jlleitschuh.gradle.ktlint", version="13.1.0" }
2525
nexus-publish = { id="io.github.gradle-nexus.publish-plugin", version="2.0.0" }
2626
binary-compatibility-validator = { id="org.jetbrains.kotlinx.binary-compatibility-validator", version="0.18.1" }
2727
vanniktech-maven-publish = { id="com.vanniktech.maven.publish", version="0.34.0" }
28-
android-library = { id="com.android.library", version.ref="android" }
28+
android-library = { id="com.android.library", version.ref="android" }
29+
docker-compose = { id="com.avast.gradle.docker-compose", version="0.17.12" }

providers/ofrep/build.gradle.kts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinTest
23

34
plugins {
45
id("com.android.library")
56
id("dev.openfeature.provider-conventions")
67
alias(libs.plugins.kotlinx.serialization)
78
// Needed for the JS coroutine support for the tests
89
alias(libs.plugins.kotlinx.atomicfu)
10+
alias(libs.plugins.docker.compose)
911
}
1012

1113
kotlin {
@@ -75,3 +77,17 @@ android {
7577
}
7678
}
7779
}
80+
81+
// Launch test container for IntegrationTest.kt
82+
dockerCompose {
83+
useComposeFiles = listOf("src/integrationTest/docker-compose.yaml")
84+
}
85+
// Note: some Kotlin test targets extend the Test (e.g. JVM), some others the KotlinTest class (e.g. Native, JS)
86+
tasks.withType(Test::class) {
87+
dependsOn(tasks.named("composeUp"))
88+
finalizedBy(tasks.named("composeDown"))
89+
}
90+
tasks.withType(KotlinTest::class) {
91+
dependsOn(tasks.named("composeUp"))
92+
finalizedBy(tasks.named("composeDown"))
93+
}
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
package dev.openfeature.kotlin.contrib.providers.ofrep.integration
2+
3+
import dev.openfeature.kotlin.contrib.providers.ofrep.OfrepProvider
4+
import dev.openfeature.kotlin.contrib.providers.ofrep.bean.OfrepOptions
5+
import dev.openfeature.kotlin.sdk.EvaluationMetadata
6+
import dev.openfeature.kotlin.sdk.ImmutableContext
7+
import dev.openfeature.kotlin.sdk.Value
8+
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError
9+
import kotlinx.coroutines.test.runTest
10+
import kotlin.test.AfterTest
11+
import kotlin.test.BeforeTest
12+
import kotlin.test.Test
13+
import kotlin.test.assertEquals
14+
import kotlin.test.assertFailsWith
15+
import kotlin.test.assertNull
16+
17+
/**
18+
* Uses an actual OFREP backend launched as a Docker container, see the `dockerCompose` configuration
19+
* in `build.gradle.kts`.
20+
*/
21+
class IntegrationTest {
22+
private lateinit var ofrepProvider: OfrepProvider
23+
24+
@BeforeTest
25+
fun setUp() {
26+
ofrepProvider =
27+
OfrepProvider(
28+
OfrepOptions(
29+
endpoint = "http://localhost:1031",
30+
),
31+
)
32+
}
33+
34+
@AfterTest
35+
fun tearDown() {
36+
ofrepProvider.shutdown()
37+
}
38+
39+
@Test
40+
fun `should fetch String flag`() =
41+
runTest {
42+
ofrepProvider.initialize(SIMPLE_CONTEXT)
43+
44+
val evaluation = ofrepProvider.getStringEvaluation(KEY_STATIC_STRING, defaultValue = DEFAULT_STRING, context = null)
45+
46+
assertEquals(SIMPLE_VALUE_STRING, evaluation.value)
47+
assertEquals(DEFAULT_VARIATION, evaluation.variant)
48+
assertEquals(STATIC_REASON, evaluation.reason)
49+
assertNull(evaluation.errorCode)
50+
assertNull(evaluation.errorMessage)
51+
assertEquals(EvaluationMetadata.EMPTY, evaluation.metadata)
52+
}
53+
54+
@Test
55+
fun `should fetch Boolean flag`() =
56+
runTest {
57+
ofrepProvider.initialize(SIMPLE_CONTEXT)
58+
59+
val evaluation = ofrepProvider.getBooleanEvaluation(KEY_STATIC_BOOLEAN, defaultValue = DEFAULT_BOOLEAN, context = null)
60+
61+
assertEquals(SIMPLE_VALUE_BOOLEAN, evaluation.value)
62+
assertEquals(DEFAULT_VARIATION, evaluation.variant)
63+
assertEquals(STATIC_REASON, evaluation.reason)
64+
assertNull(evaluation.errorCode)
65+
assertNull(evaluation.errorMessage)
66+
assertEquals(EvaluationMetadata.EMPTY, evaluation.metadata)
67+
}
68+
69+
@Test
70+
fun `should fetch Integer flag`() =
71+
runTest {
72+
ofrepProvider.initialize(SIMPLE_CONTEXT)
73+
74+
val evaluation = ofrepProvider.getIntegerEvaluation(KEY_STATIC_INTEGER, defaultValue = DEFAULT_INTEGER, context = null)
75+
76+
assertEquals(SIMPLE_VALUE_INTEGER, evaluation.value)
77+
assertEquals(DEFAULT_VARIATION, evaluation.variant)
78+
assertEquals(STATIC_REASON, evaluation.reason)
79+
assertNull(evaluation.errorCode)
80+
assertNull(evaluation.errorMessage)
81+
assertEquals(EvaluationMetadata.EMPTY, evaluation.metadata)
82+
}
83+
84+
@Test
85+
fun `should fetch Double flag`() =
86+
runTest {
87+
ofrepProvider.initialize(SIMPLE_CONTEXT)
88+
89+
val evaluation = ofrepProvider.getDoubleEvaluation(KEY_STATIC_DOUBLE, defaultValue = DEFAULT_DOUBLE, context = null)
90+
91+
assertEquals(SIMPLE_VALUE_DOUBLE, evaluation.value)
92+
assertEquals(DEFAULT_VARIATION, evaluation.variant)
93+
assertEquals(STATIC_REASON, evaluation.reason)
94+
assertNull(evaluation.errorCode)
95+
assertNull(evaluation.errorMessage)
96+
assertEquals(EvaluationMetadata.EMPTY, evaluation.metadata)
97+
}
98+
99+
@Test
100+
fun `should fetch Object flag`() =
101+
runTest {
102+
ofrepProvider.initialize(SIMPLE_CONTEXT)
103+
104+
val evaluation = ofrepProvider.getObjectEvaluation(KEY_STATIC_OBJECT, defaultValue = DEFAULT_OBJECT, context = null)
105+
106+
assertEquals(SIMPLE_VALUE_OBJECT, evaluation.value)
107+
assertEquals(DEFAULT_VARIATION, evaluation.variant)
108+
assertEquals(STATIC_REASON, evaluation.reason)
109+
assertNull(evaluation.errorCode)
110+
assertNull(evaluation.errorMessage)
111+
assertEquals(EvaluationMetadata.EMPTY, evaluation.metadata)
112+
}
113+
114+
@Test
115+
fun `should return not found for non-existent String flag`() =
116+
runTest {
117+
ofrepProvider.initialize(SIMPLE_CONTEXT)
118+
119+
assertFailsWith<OpenFeatureError.FlagNotFoundError> {
120+
ofrepProvider.getStringEvaluation(KEY_NON_EXISTENT, defaultValue = DEFAULT_STRING, context = null)
121+
}
122+
}
123+
124+
@Test
125+
fun `should return not found for non-existent Boolean flag`() =
126+
runTest {
127+
ofrepProvider.initialize(SIMPLE_CONTEXT)
128+
129+
assertFailsWith<OpenFeatureError.FlagNotFoundError> {
130+
ofrepProvider.getBooleanEvaluation(KEY_NON_EXISTENT, defaultValue = DEFAULT_BOOLEAN, context = null)
131+
}
132+
}
133+
134+
@Test
135+
fun `should return not found for non-existent Integer flag`() =
136+
runTest {
137+
ofrepProvider.initialize(SIMPLE_CONTEXT)
138+
139+
assertFailsWith<OpenFeatureError.FlagNotFoundError> {
140+
ofrepProvider.getIntegerEvaluation(KEY_NON_EXISTENT, defaultValue = DEFAULT_INTEGER, context = null)
141+
}
142+
}
143+
144+
@Test
145+
fun `should return not found for non-existent Object flag`() =
146+
runTest {
147+
ofrepProvider.initialize(SIMPLE_CONTEXT)
148+
149+
assertFailsWith<OpenFeatureError.FlagNotFoundError> {
150+
ofrepProvider.getObjectEvaluation(KEY_NON_EXISTENT, defaultValue = DEFAULT_OBJECT, context = null)
151+
}
152+
}
153+
154+
@Test
155+
fun `should return not found for non-existent Double flag`() =
156+
runTest {
157+
ofrepProvider.initialize(SIMPLE_CONTEXT)
158+
159+
assertFailsWith<OpenFeatureError.FlagNotFoundError> {
160+
ofrepProvider.getDoubleEvaluation(KEY_NON_EXISTENT, defaultValue = DEFAULT_DOUBLE, context = null)
161+
}
162+
}
163+
164+
@Test
165+
fun `should fetch targeted String flag`() =
166+
runTest {
167+
ofrepProvider.initialize(TARGETED_CONTEXT)
168+
169+
val evaluation = ofrepProvider.getStringEvaluation(KEY_TARGETED_STRING, defaultValue = DEFAULT_STRING, context = null)
170+
171+
assertEquals(TARGETED_VALUE_STRING, evaluation.value)
172+
assertEquals(TARGETED_VARIATION, evaluation.variant)
173+
assertEquals(TARGETING_REASON, evaluation.reason)
174+
assertNull(evaluation.errorCode)
175+
assertNull(evaluation.errorMessage)
176+
assertEquals(EvaluationMetadata.EMPTY, evaluation.metadata)
177+
}
178+
179+
@Test
180+
fun `should fetch targeted Boolean flag`() =
181+
runTest {
182+
ofrepProvider.initialize(TARGETED_CONTEXT)
183+
184+
val evaluation = ofrepProvider.getBooleanEvaluation(KEY_TARGETED_BOOLEAN, defaultValue = DEFAULT_BOOLEAN, context = null)
185+
186+
assertEquals(TARGETED_VALUE_BOOLEAN, evaluation.value)
187+
assertEquals(TARGETED_VARIATION, evaluation.variant)
188+
assertEquals(TARGETING_REASON, evaluation.reason)
189+
assertNull(evaluation.errorCode)
190+
assertNull(evaluation.errorMessage)
191+
assertEquals(EvaluationMetadata.EMPTY, evaluation.metadata)
192+
}
193+
194+
@Test
195+
fun `should fetch targeted Integer flag`() =
196+
runTest {
197+
ofrepProvider.initialize(TARGETED_CONTEXT)
198+
199+
val evaluation = ofrepProvider.getIntegerEvaluation(KEY_TARGETED_INTEGER, defaultValue = DEFAULT_INTEGER, context = null)
200+
201+
assertEquals(TARGETED_VALUE_INTEGER, evaluation.value)
202+
assertEquals(TARGETED_VARIATION, evaluation.variant)
203+
assertEquals(TARGETING_REASON, evaluation.reason)
204+
assertNull(evaluation.errorCode)
205+
assertNull(evaluation.errorMessage)
206+
assertEquals(EvaluationMetadata.EMPTY, evaluation.metadata)
207+
}
208+
209+
@Test
210+
fun `should fetch targeted Double flag`() =
211+
runTest {
212+
ofrepProvider.initialize(TARGETED_CONTEXT)
213+
214+
val evaluation = ofrepProvider.getDoubleEvaluation(KEY_TARGETED_DOUBLE, defaultValue = DEFAULT_DOUBLE, context = null)
215+
216+
assertEquals(TARGETED_VALUE_DOUBLE, evaluation.value)
217+
assertEquals(TARGETED_VARIATION, evaluation.variant)
218+
assertEquals(TARGETING_REASON, evaluation.reason)
219+
assertNull(evaluation.errorCode)
220+
assertNull(evaluation.errorMessage)
221+
assertEquals(EvaluationMetadata.EMPTY, evaluation.metadata)
222+
}
223+
224+
@Test
225+
fun `should fetch targeted Object flag`() =
226+
runTest {
227+
ofrepProvider.initialize(TARGETED_CONTEXT)
228+
229+
val evaluation = ofrepProvider.getObjectEvaluation(KEY_TARGETED_OBJECT, defaultValue = DEFAULT_OBJECT, context = null)
230+
231+
assertEquals(TARGETED_VALUE_OBJECT, evaluation.value)
232+
assertEquals(TARGETED_VARIATION, evaluation.variant)
233+
assertEquals(TARGETING_REASON, evaluation.reason)
234+
assertNull(evaluation.errorCode)
235+
assertNull(evaluation.errorMessage)
236+
assertEquals(EvaluationMetadata.EMPTY, evaluation.metadata)
237+
}
238+
239+
companion object {
240+
private const val TARGETING_KEY = "123"
241+
private val SIMPLE_CONTEXT = ImmutableContext(TARGETING_KEY)
242+
private val TARGETED_CONTEXT =
243+
ImmutableContext(TARGETING_KEY, mapOf("targeted" to Value.Boolean(true)))
244+
245+
private const val KEY_STATIC_STRING = "static-string-flag"
246+
private const val KEY_STATIC_BOOLEAN = "static-boolean-flag"
247+
private const val KEY_STATIC_INTEGER = "static-integer-flag"
248+
private const val KEY_STATIC_DOUBLE = "static-double-flag"
249+
private const val KEY_STATIC_OBJECT = "static-object-flag"
250+
251+
private const val KEY_NON_EXISTENT = "non-existent-flag"
252+
253+
private const val KEY_TARGETED_STRING = "targeted-string-flag"
254+
private const val KEY_TARGETED_BOOLEAN = "targeted-boolean-flag"
255+
private const val KEY_TARGETED_INTEGER = "targeted-integer-flag"
256+
private const val KEY_TARGETED_DOUBLE = "targeted-double-flag"
257+
private const val KEY_TARGETED_OBJECT = "targeted-object-flag"
258+
259+
private const val SIMPLE_VALUE_STRING = "foo"
260+
private const val SIMPLE_VALUE_BOOLEAN = true
261+
private const val SIMPLE_VALUE_INTEGER = 123
262+
private const val SIMPLE_VALUE_DOUBLE = 3.14
263+
private val SIMPLE_VALUE_OBJECT =
264+
Value.Structure(
265+
mapOf(
266+
"string" to Value.String("foo"),
267+
"boolean" to Value.Boolean(true),
268+
"number" to Value.Double(2.78),
269+
"list" to
270+
Value.List(
271+
listOf(
272+
Value.String("one"),
273+
Value.String("two"),
274+
Value.String("three"),
275+
),
276+
),
277+
),
278+
)
279+
280+
private const val TARGETED_VALUE_STRING = "bar"
281+
private const val TARGETED_VALUE_BOOLEAN = false
282+
private const val TARGETED_VALUE_INTEGER = 999
283+
private const val TARGETED_VALUE_DOUBLE = 999.9
284+
private val TARGETED_VALUE_OBJECT =
285+
Value.Structure(
286+
mapOf(
287+
"special" to Value.Boolean(true),
288+
),
289+
)
290+
291+
private const val DEFAULT_STRING = "default"
292+
private const val DEFAULT_BOOLEAN = false
293+
private const val DEFAULT_INTEGER = -1
294+
private const val DEFAULT_DOUBLE = -1.0
295+
private val DEFAULT_OBJECT = Value.Null
296+
297+
private const val DEFAULT_VARIATION = "default_variation"
298+
private const val TARGETED_VARIATION = "targeted_variation"
299+
300+
private const val STATIC_REASON = "STATIC"
301+
private const val TARGETING_REASON = "TARGETING_MATCH"
302+
}
303+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
goff:
3+
container_name: test-ofrep-backend
4+
image: docker.io/gofeatureflag/go-feature-flag:v1.45
5+
ports:
6+
- "1031:1031"
7+
volumes:
8+
- ./goff/goff-proxy.yaml:/goff/goff-proxy.yaml:ro
9+
- ./goff/flags.goff.yaml:/goff/flags.goff.yaml:ro
10+
restart: "no"

0 commit comments

Comments
 (0)