Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ class FeatureFlagService(
private val conditionRepository: ConditionRepository,
private val featureFlagRepository: FeatureFlagRepository,
) {
fun getFlags(): List<FeatureFlag> {
return featureFlagRepository.findAll()
}

fun getFlagOrThrow(key: String): FeatureFlag {
return featureFlagRepository.findByName(key) ?: throw BusinessException("Feature flag $key does not exist")
}

@Transactional
fun create(
user: User,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.OneToMany
import jakarta.persistence.OneToOne
import org.hibernate.annotations.SQLRestriction
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.LastModifiedBy

@Entity
@SQLRestriction("deleted_at is null")
class FeatureFlag(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package com.lightswitch.infrastructure.database.repository

import com.lightswitch.infrastructure.database.entity.FeatureFlag
import org.springframework.data.jpa.repository.EntityGraph
import org.springframework.data.jpa.repository.JpaRepository

interface FeatureFlagRepository : JpaRepository<FeatureFlag, Int> {
@EntityGraph(attributePaths = ["createdBy", "updatedBy", "defaultCondition", "conditions"])
fun findByName(name: String): FeatureFlag?

@EntityGraph(attributePaths = ["createdBy", "updatedBy", "defaultCondition", "conditions"])
override fun findAll(): List<FeatureFlag>
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ class FeatureFlagController(
)
@GetMapping
fun getFlags(): PayloadResponse<List<FeatureFlagResponse>> {
return PayloadResponse<List<FeatureFlagResponse>>(
status = "status",
message = "message",
data = listOf()
val flags = featureFlagService.getFlags()

return PayloadResponse.success(
message = "Fetched all feature flags successfully",
data = flags.map { FeatureFlagResponse.from(it) },
)
}

Expand All @@ -49,10 +50,11 @@ class FeatureFlagController(
fun getFlag(
@PathVariable key: String,
): PayloadResponse<FeatureFlagResponse> {
return PayloadResponse<FeatureFlagResponse>(
status = "status",
message = "message",
data = null
val flag = featureFlagService.getFlagOrThrow(key)

return PayloadResponse.success(
message = "Fetched a flag successfully",
data = FeatureFlagResponse.from(flag)
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.lightswitch.application.service

import com.lightswitch.infrastructue.database.repository.BaseRepositoryTest
import com.lightswitch.infrastructure.database.entity.Condition
import com.lightswitch.infrastructure.database.entity.FeatureFlag
import com.lightswitch.infrastructure.database.entity.User
import com.lightswitch.infrastructure.database.model.Type
import com.lightswitch.infrastructure.database.repository.ConditionRepository
Expand All @@ -14,6 +16,7 @@ import org.assertj.core.api.Assertions.tuple
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import java.time.Instant
import java.time.LocalDate

class FeatureFlagServiceTest : BaseRepositoryTest() {
Expand All @@ -31,9 +34,190 @@ class FeatureFlagServiceTest : BaseRepositoryTest() {
@BeforeEach
fun setUp() {
featureFlagService = FeatureFlagService(conditionRepository, featureFlagRepository)
conditionRepository.deleteAllInBatch()
featureFlagRepository.deleteAllInBatch()
userRepository.deleteAllInBatch()
conditionRepository.deleteAll()
featureFlagRepository.deleteAll()
userRepository.deleteAll()
}

@Test
fun `getFlags should return all feature flags`() {
val user = userRepository.save(
User(
username = "test-user",
passwordHash = "passwordHash",
lastLoginAt = LocalDate.of(2025, 1, 1).atStartOfDay(),
)
)
val flag1 = featureFlagRepository.save(
FeatureFlag(
name = "feature-1",
description = "Feature Flag 1",
type = Type.BOOLEAN,
enabled = true,
createdBy = user,
updatedBy = user
)
)
val flag2 = featureFlagRepository.save(
FeatureFlag(
name = "feature-2",
description = "Feature Flag 2",
type = Type.NUMBER,
enabled = false,
createdBy = user,
updatedBy = user
)
)
val flag3 = featureFlagRepository.save(
FeatureFlag(
name = "feature-3",
description = "Feature Flag 3",
type = Type.STRING,
enabled = true,
createdBy = user,
updatedBy = user
)
)
flag1.defaultCondition = Condition(flag = flag1, key = "boolean", value = true)
flag2.defaultCondition = Condition(flag = flag2, key = "number", value = 10)
flag3.defaultCondition = Condition(flag = flag3, key = "string", value = "value")

val flags = featureFlagService.getFlags()

assertThat(flags).hasSize(3)
assertThat(flags)
.extracting("name", "description", "type", "enabled", "createdBy", "updatedBy")
.containsExactly(
tuple("feature-1", "Feature Flag 1", Type.BOOLEAN, true, user, user),
tuple("feature-2", "Feature Flag 2", Type.NUMBER, false, user, user),
tuple("feature-3", "Feature Flag 3", Type.STRING, true, user, user),
)
assertThat(flags)
.extracting("defaultCondition.key", "defaultCondition.value")
.containsExactlyInAnyOrder(
tuple("boolean", true),
tuple("number", 10),
tuple("string", "value"),
)
}

@Test
fun `getFlags should return empty list when feature flag not exists`() {
assertThat(featureFlagService.getFlags()).isEmpty()
}

@Test
fun `getFlags should return empty list when all feature flags are deleted`() {
val user = userRepository.save(
User(
username = "test-user",
passwordHash = "passwordHash",
lastLoginAt = LocalDate.of(2025, 1, 1).atStartOfDay(),
),
)
val flag = FeatureFlag(
name = "user-limit",
description = "User Limit Flag",
type = Type.NUMBER,
enabled = true,
createdBy = user,
updatedBy = user,
).apply {
this.deletedAt = Instant.now()
}
featureFlagRepository.save(flag)

assertThat(featureFlagService.getFlags()).isEmpty()
}

@Test
fun `getFlagOrThrow should return feature flag when key exists`() {
val user = userRepository.save(
User(
username = "test-user",
passwordHash = "passwordHash",
lastLoginAt = LocalDate.of(2025, 1, 1).atStartOfDay(),
),
)
val savedFlag = featureFlagRepository.save(
FeatureFlag(
name = "user-limit",
description = "User Limit Flag",
type = Type.NUMBER,
enabled = true,
createdBy = user,
updatedBy = user
)
)
val defaultCondition = Condition(flag = savedFlag, key = "number", value = 10)
val conditions = listOf(
Condition(flag = savedFlag, key = "free", value = 10),
Condition(flag = savedFlag, key = "pro", value = 100),
Condition(flag = savedFlag, key = "enterprise", value = 1000),
)
featureFlagRepository.save(savedFlag.apply {
this.defaultCondition = defaultCondition
this.conditions.addAll(conditions)
this.conditions.add(defaultCondition)
})

val flag = featureFlagService.getFlagOrThrow("user-limit")

assertThat(flag.id).isNotNull()
assertThat(flag.name).isEqualTo("user-limit")
assertThat(flag.description).isEqualTo("User Limit Flag")
assertThat(flag.type).isEqualTo(Type.NUMBER)
assertThat(flag.enabled).isTrue()
assertThat(flag.createdBy).isEqualTo(user)
assertThat(flag.updatedBy).isEqualTo(user)
assertThat(flag.defaultCondition)
.extracting("key", "value")
.containsOnly("number", 10)
assertThat(flag.conditions)
.hasSize(4)
.extracting("key", "value")
.containsExactlyInAnyOrder(
tuple("number", 10),
tuple("free", 10),
tuple("pro", 100),
tuple("enterprise", 1000),
)
}

@Test
fun `getFlagOrThrow should throw BusinessException when key does not exist`() {
val nonExistentKey = "non-existent-key"

assertThatThrownBy { featureFlagService.getFlagOrThrow(nonExistentKey) }
.isInstanceOf(BusinessException::class.java)
.hasMessageContaining("Feature flag $nonExistentKey does not exist")
}


@Test
fun `getFlagOrThrow should not return when feature flag is deleted`() {
val user = userRepository.save(
User(
username = "test-user",
passwordHash = "passwordHash",
lastLoginAt = LocalDate.of(2025, 1, 1).atStartOfDay(),
),
)
val flag = FeatureFlag(
name = "user-limit",
description = "User Limit Flag",
type = Type.NUMBER,
enabled = true,
createdBy = user,
updatedBy = user,
).apply {
this.deletedAt = Instant.now()
}
featureFlagRepository.save(flag)

assertThatThrownBy { featureFlagService.getFlagOrThrow("user-limit") }
.isInstanceOf(BusinessException::class.java)
.hasMessageContaining("Feature flag user-limit does not exist")
}

@Test
Expand Down
Loading