diff --git a/backend/src/main/kotlin/com/lightswitch/application/service/FeatureFlagService.kt b/backend/src/main/kotlin/com/lightswitch/application/service/FeatureFlagService.kt index 2051076..eafb848 100644 --- a/backend/src/main/kotlin/com/lightswitch/application/service/FeatureFlagService.kt +++ b/backend/src/main/kotlin/com/lightswitch/application/service/FeatureFlagService.kt @@ -16,6 +16,14 @@ class FeatureFlagService( private val conditionRepository: ConditionRepository, private val featureFlagRepository: FeatureFlagRepository, ) { + fun getFlags(): List { + 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, diff --git a/backend/src/main/kotlin/com/lightswitch/infrastructure/database/entity/FeatureFlag.kt b/backend/src/main/kotlin/com/lightswitch/infrastructure/database/entity/FeatureFlag.kt index df3bf79..c5f9d94 100644 --- a/backend/src/main/kotlin/com/lightswitch/infrastructure/database/entity/FeatureFlag.kt +++ b/backend/src/main/kotlin/com/lightswitch/infrastructure/database/entity/FeatureFlag.kt @@ -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) diff --git a/backend/src/main/kotlin/com/lightswitch/infrastructure/database/repository/FeatureFlagRepository.kt b/backend/src/main/kotlin/com/lightswitch/infrastructure/database/repository/FeatureFlagRepository.kt index 79e2578..35192ec 100644 --- a/backend/src/main/kotlin/com/lightswitch/infrastructure/database/repository/FeatureFlagRepository.kt +++ b/backend/src/main/kotlin/com/lightswitch/infrastructure/database/repository/FeatureFlagRepository.kt @@ -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 { + @EntityGraph(attributePaths = ["createdBy", "updatedBy", "defaultCondition", "conditions"]) fun findByName(name: String): FeatureFlag? + + @EntityGraph(attributePaths = ["createdBy", "updatedBy", "defaultCondition", "conditions"]) + override fun findAll(): List } diff --git a/backend/src/main/kotlin/com/lightswitch/presentation/controller/FeatureFlagController.kt b/backend/src/main/kotlin/com/lightswitch/presentation/controller/FeatureFlagController.kt index e84904e..26f7f29 100644 --- a/backend/src/main/kotlin/com/lightswitch/presentation/controller/FeatureFlagController.kt +++ b/backend/src/main/kotlin/com/lightswitch/presentation/controller/FeatureFlagController.kt @@ -35,10 +35,11 @@ class FeatureFlagController( ) @GetMapping fun getFlags(): PayloadResponse> { - return PayloadResponse>( - 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) }, ) } @@ -49,10 +50,11 @@ class FeatureFlagController( fun getFlag( @PathVariable key: String, ): PayloadResponse { - return PayloadResponse( - status = "status", - message = "message", - data = null + val flag = featureFlagService.getFlagOrThrow(key) + + return PayloadResponse.success( + message = "Fetched a flag successfully", + data = FeatureFlagResponse.from(flag) ) } diff --git a/backend/src/test/kotlin/com/lightswitch/application/service/FeatureFlagServiceTest.kt b/backend/src/test/kotlin/com/lightswitch/application/service/FeatureFlagServiceTest.kt index 74980e4..6e89ff2 100644 --- a/backend/src/test/kotlin/com/lightswitch/application/service/FeatureFlagServiceTest.kt +++ b/backend/src/test/kotlin/com/lightswitch/application/service/FeatureFlagServiceTest.kt @@ -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 @@ -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() { @@ -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