Skip to content

Commit

Permalink
Add flagd java provider integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
alexcardell committed Sep 20, 2024
1 parent b0a5d08 commit 3006381
Show file tree
Hide file tree
Showing 10 changed files with 707 additions and 16 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ lazy val `openfeature-provider-java-it` = crossProject(JVMPlatform)
)
)
.dependsOn(
`openfeature-sdk-circe`,
`openfeature-provider-java`
)

Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ services:
FLIPT_STORAGE_LOCAL_PATH: "/config"
volumes:
- "./docker/flipt/features.yaml:/config/features.yaml"

flagd:
image: ghcr.io/open-feature/flagd:v0.11.2
command: 'start --uri file:./etc/flagd/flagd-features.json'
ports:
- "8013:8013"
volumes:
- "./docker/flagd/flagd-features.json:/etc/flagd/flagd-features.json"
55 changes: 55 additions & 0 deletions docker/flagd/flagd-features.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"$schema": "https://flagd.dev/schema/v0/flags.json",
"flags": {
"boolean-flag-1": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "on"
},
"string-variant-flag-1": {
"state": "ENABLED",
"defaultVariant": "key-1",
"variants": {
"key-1": "string-value-1",
"key-2": "string-value-1"
},
"targeting": {}
},
"int-variant-flag-1": {
"state": "ENABLED",
"defaultVariant": "key-1",
"variants": {
"key-1": 13,
"key-2": 88
},
"targeting": {}
},
"double-variant-flag-1": {
"state": "ENABLED",
"defaultVariant": "key-2",
"variants": {
"key-1": 88.0,
"key-2": 17.1
},
"targeting": {}
},
"structure-variant-flag-1": {
"state": "ENABLED",
"defaultVariant": "structure-1",
"variants": {
"structure-1": {
"field": "string",
"intField": 33
},
"structure-2": {
"field": "other",
"intField": 10001
}
},
"targeting": {}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,15 @@ final class FliptProvider[F[_]: MonadThrow](

jsonAttachment match {
case Left(parseError) =>
ResolutionDetails.error(defaultValue, parseError)
ResolutionDetails.fromThrowable(defaultValue, parseError)
case Right(None) =>
ResolutionDetails.error(
defaultValue,
new Throwable("did not receive json object")
)
ResolutionDetails.error(defaultValue, "did not receive json object")
case Right(Some(jsonObject)) =>
val structure = JsonStructureConverters.jsonToStructure(jsonObject)
val decodedStructure = StructureDecoder[A].decodeStructure(structure)
decodedStructure match {
case Left(error) =>
ResolutionDetails.error[A](defaultValue, error.cause)
ResolutionDetails.fromThrowable[A](defaultValue, error.cause)
case Right(value) => ResolutionDetails[A](value)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright 2023 Alex Cardell
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.cardell.openfeature.provider.java

import cats.effect.IO
import cats.effect.kernel.Resource
import cats.syntax.all._
import com.dimafeng.testcontainers.ContainerDef
import com.dimafeng.testcontainers.DockerComposeContainer
import com.dimafeng.testcontainers.ExposedService
import com.dimafeng.testcontainers.munit.TestContainerForAll
import dev.openfeature.contrib.providers.flagd.FlagdOptions
import dev.openfeature.contrib.providers.flagd.FlagdProvider
import java.io.File
import munit.CatsEffectSuite
import org.testcontainers.containers.wait.strategy.Wait

import io.cardell.openfeature.EvaluationContext
import io.cardell.openfeature.FeatureClient
import io.cardell.openfeature.OpenFeature

class JavaProviderItTest extends CatsEffectSuite with TestContainerForAll {

override val containerDef: ContainerDef = DockerComposeContainer.Def(
new File("docker-compose.yaml"),
exposedServices = Seq(
ExposedService(
"flagd",
8013,
Wait.forLogMessage("^.*watching filepath.*", 1)
)
),
tailChildContainers = true
)

def flagd(containers: Containers): Resource[IO, FeatureClient[IO]] = {
val container =
containers
.asInstanceOf[DockerComposeContainer]
.getContainerByServiceName("flagd")
.get

val flagdProvder =
new FlagdProvider(
FlagdOptions
.builder()
.host(container.getHost())
.port(container.getMappedPort(8013).toInt)
.build()
)

JavaProvider
.resource[IO](flagdProvder)
.map(OpenFeature[IO])
.evalMap(_.client)
}

val evaluationContext = EvaluationContext.empty

test("can fetch boolean flag") {
val expected = true

withContainers { containers =>
flagd(containers)
.use { provider =>
for {
res <- provider.getBooleanDetails(
"boolean-flag-1",
false,
EvaluationContext.empty
)
} yield assertEquals(res.value, expected)
}
}
}

test("uses default when boolean flag missing") {
val expected = false

withContainers { containers =>
flagd(containers).use { provider =>
for {
res <- provider.getBooleanDetails(
"no-flag",
false,
EvaluationContext.empty
)
} yield assertEquals(res.value, expected)
}
}
}

test("can resolve string value for provider variant flag") {
val expected = "string-value-1"

withContainers { containers =>
flagd(containers).use { provider =>
for {
res <- provider.getStringDetails(
"string-variant-flag-1",
"default-string",
evaluationContext
)
} yield assertEquals(res.value, expected)
}
}
}

test("uses default when string flag missing") {
val expected = "some-string"

withContainers { containers =>
flagd(containers).use { provider =>
for {
res <- provider.getStringDetails(
"no-flag",
expected,
EvaluationContext.empty
)
} yield assertEquals(res.value, expected)
}
}
}

test("can resolve int value for provider variant flag") {
val expected = 13

withContainers { containers =>
flagd(containers).use { provider =>
for {
res <- provider.getIntDetails(
"int-variant-flag-1",
99,
evaluationContext
)
} yield assertEquals(res.value, expected)
}
}
}

test("can resolve double value for provider variant flag") {
val expected = 17.1

withContainers { containers =>
flagd(containers).use { provider =>
for {
res <- provider.getDoubleDetails(
"double-variant-flag-1",
99.9,
evaluationContext
)
} yield assertEquals(res.value, expected)
}
}
}

test("can deserialise variant match") {
val expected = TestVariant("string", 33)

withContainers { containers =>
flagd(containers).use { provider =>
for {
res <- provider.getStructureDetails[TestVariant](
"structure-variant-flag-1",
TestVariant("a", 0),
evaluationContext
)
} yield assertEquals(res.value, expected)
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2023 Alex Cardell
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.cardell.openfeature.provider.java

import io.cardell.openfeature.FlagValue
import io.cardell.openfeature.FlagValue.IntValue
import io.cardell.openfeature.FlagValue.StringValue
import io.cardell.openfeature.Structure
import io.cardell.openfeature.StructureDecoder
import io.cardell.openfeature.StructureDecoderError
import io.cardell.openfeature.StructureEncoder

case class TestVariant(field: String, intField: Int)

object TestVariant {

implicit val sd: StructureDecoder[TestVariant] =
new StructureDecoder[TestVariant] {

def decodeStructure(
s: Structure
): Either[StructureDecoderError, TestVariant] = {
val maybeStruct =
for {
field <- s.values.get("field")
intField <- s.values.get("intField")
variant <-
(field, intField) match {
case (StringValue(s), IntValue(i)) => Some(TestVariant(s, i))
case _ => None
}
} yield variant

maybeStruct match {
case None =>
Left(
StructureDecoderError(
new Throwable(
"some fields missing converting TestVariant to Structure"
)
)
)
case Some(value) => Right(value)
}

}

}

implicit val se: StructureEncoder[TestVariant] =
new StructureEncoder[TestVariant] {

def encodeStructure(
in: TestVariant
): Structure = Structure(
Map(
"field" -> StringValue(in.field),
"intField" -> IntValue(in.intField)
)
)

}

}
Loading

0 comments on commit 3006381

Please sign in to comment.