Skip to content
Open
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 @@ -13,6 +13,7 @@ import com.lightswitch.presentation.model.flag.variantPairs
import jakarta.persistence.EntityNotFoundException
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.Instant

@Service
class FeatureFlagService(
Expand Down Expand Up @@ -72,9 +73,11 @@ class FeatureFlagService(
flag.description = request.description
flag.updatedBy = user

flag.conditions.forEach { it.deletedAt = Instant.now() }
conditionRepository.saveAllAndFlush(flag.conditions)

flag.defaultCondition = null
flag.conditions.clear()
conditionRepository.deleteByFlag(flag)

request.defaultValueAsPair()
.let { (key, value) -> flag.addDefaultCondition(key = key, value = value) }
Expand All @@ -95,4 +98,20 @@ class FeatureFlagService(
flag.updatedBy = user
featureFlagRepository.save(flag)
}

@Transactional
fun delete(
user: User,
key: String,
deletedAt: Instant = Instant.now(),
) {
val flag = getFlagOrThrow(key)

flag.updatedBy = user
flag.deletedAt = deletedAt
flag.conditions.forEach { it.deletedAt = deletedAt }

featureFlagRepository.save(flag)
conditionRepository.saveAll(flag.conditions)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import org.hibernate.annotations.SQLRestriction

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

import com.lightswitch.infrastructure.database.entity.Condition
import com.lightswitch.infrastructure.database.entity.FeatureFlag
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query

interface ConditionRepository : JpaRepository<Condition, Long> {
@Modifying(clearAutomatically = true)
@Query("DELETE FROM Condition c where c.flag = :flag")
fun deleteByFlag(flag: FeatureFlag)
}
interface ConditionRepository : JpaRepository<Condition, Long>
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,13 @@ class FeatureFlagController(
fun deleteFlag(
@PathVariable key: String,
): StatusResponse {
return StatusResponse(
status = "status",
message = "message",
)
// TODO: Improve the way finding the authenticated user.
val authentication = SecurityContextHolder.getContext().authentication
val userId = authentication.name
val user = userRepository.findByIdOrNull(userId.toLong()) ?: throw BusinessException("User not found")

featureFlagService.delete(user, key)

return StatusResponse.success("Successfully deleted the feature flag")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset

class FeatureFlagServiceTest : BaseRepositoryTest() {
@Autowired
Expand Down Expand Up @@ -315,16 +316,17 @@ class FeatureFlagServiceTest : BaseRepositoryTest() {

@Test
fun `update feature flag can update feature flag`() {
val user = saveTestUser()
val user1 = saveTestUser("user1")
val user2 = saveTestUser("user2")
val flag =
featureFlagRepository.save(
FeatureFlag(
name = "user-limit",
description = "description",
type = Type.BOOLEAN,
enabled = true,
createdBy = user,
updatedBy = user,
createdBy = user1,
updatedBy = user1,
),
).apply {
this.defaultCondition = Condition(flag = this, key = "boolean", value = true)
Expand All @@ -346,9 +348,10 @@ class FeatureFlagServiceTest : BaseRepositoryTest() {
),
)

val updated = featureFlagService.update(user, "user-limit", request)
val updated = featureFlagService.update(user2, "user-limit", request)

assertThat(flag.id).isEqualTo(updated.id)
assertThat(conditionRepository.findAll()).hasSize(3)
assertThat(updated.id).isEqualTo(flag.id)
assertThat(updated.name).isEqualTo("user-limit-updated")
assertThat(updated.description).isEqualTo("Updated description")
assertThat(updated.type).isEqualTo(Type.NUMBER)
Expand All @@ -363,7 +366,7 @@ class FeatureFlagServiceTest : BaseRepositoryTest() {
tuple("free", 10),
tuple("pro", 100),
)
assertThat(updated.updatedBy.id).isEqualTo(user.id)
assertThat(updated.updatedBy).isEqualTo(user2)
}

@Test
Expand Down Expand Up @@ -456,25 +459,26 @@ class FeatureFlagServiceTest : BaseRepositoryTest() {

@Test
fun `update should update feature flag status`() {
val user = saveTestUser()
val user1 = saveTestUser("user1")
val user2 = saveTestUser("user2")
val flag =
featureFlagRepository.save(
FeatureFlag(
name = "test-flag",
description = "description",
type = Type.BOOLEAN,
enabled = false,
createdBy = user,
updatedBy = user,
createdBy = user1,
updatedBy = user1,
),
)

featureFlagService.update(user, "test-flag", true)
featureFlagService.update(user2, "test-flag", true)

val updatedFlag = featureFlagRepository.findById(flag.id!!.toInt()).orElseThrow()

assertThat(updatedFlag.enabled).isTrue()
assertThat(updatedFlag.updatedBy).isEqualTo(user)
assertThat(updatedFlag.updatedBy).isEqualTo(user2)
}

@Test
Expand All @@ -488,10 +492,52 @@ class FeatureFlagServiceTest : BaseRepositoryTest() {
.hasMessageContaining("Feature flag flag-one does not exist")
}

private fun saveTestUser() =
@Test
fun `delete should remove feature flag successfully`() {
val deletedAt = LocalDate.of(2025, 1, 1).atStartOfDay().toInstant(ZoneOffset.UTC)
val user1 = saveTestUser("user1")
val user2 = saveTestUser("user2")
val flag =
featureFlagRepository.save(
FeatureFlag(
name = "test-flag",
description = "Test flag for deletion",
type = Type.BOOLEAN,
enabled = true,
createdBy = user1,
updatedBy = user1,
),
).apply {
this.defaultCondition = Condition(flag = this, key = "boolean", value = true)
this.conditions.add(this.defaultCondition!!)
this.conditions.add(Condition(flag = this, key = "free", value = true))
}.let {
featureFlagRepository.save(it)
}

featureFlagService.delete(user2, "test-flag", deletedAt)

val deleted = featureFlagRepository.findById(flag.id!!.toInt()).get()
assertThat(deleted.updatedBy).isEqualTo(user2)
assertThat(deleted.deletedAt).isEqualTo(deletedAt)
assertThat(deleted.conditions).hasSize(2).allMatch { it.deletedAt == deletedAt }
assertThat(featureFlagRepository.findAll()).isEmpty()
assertThat(conditionRepository.findAll()).isEmpty()
}

@Test
fun `delete should throw EntityNotFoundException when flag does not exist`() {
val user = saveTestUser()

assertThatThrownBy { featureFlagService.delete(user, "non-existent-flag") }
.isInstanceOf(EntityNotFoundException::class.java)
.hasMessageContaining("Feature flag non-existent-flag does not exist")
}

private fun saveTestUser(username: String = "test-user") =
userRepository.save(
User(
username = "test-user",
username = username,
passwordHash = "test-pass",
lastLoginAt = LocalDate.of(2025, 1, 1).atStartOfDay(),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.`when`
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.security.test.context.support.WithMockUser
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
import org.springframework.test.context.bean.override.mockito.MockitoBean
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
Expand Down Expand Up @@ -488,4 +490,37 @@ class FeatureFlagControllerTest {
.andExpect(jsonPath("$.status").value("BAD_REQUEST"))
.andExpect(jsonPath("$.message").value("Entity not found"))
}

@Test
@WithMockUser(username = "1")
fun `DELETE flags should delete a feature flag successfully`() {
`when`(userRepository.findById(1L)).thenReturn(Optional.of(user))
doNothing().`when`(featureFlagService).delete(user, "test-flag")

mockMvc.perform(
delete("/api/v1/flags/test-flag")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON),
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.status").value("Success"))
.andExpect(jsonPath("$.message").value("Successfully deleted the feature flag"))
.andDo(print())
}

@Test
@WithMockUser(username = "1")
fun `DELETE flags return 400 when flag does not exist`() {
`when`(userRepository.findById(1L)).thenReturn(Optional.of(user))
`when`(featureFlagService.delete(any(), any(), any())).thenThrow(EntityNotFoundException())

mockMvc.perform(
delete("/api/v1/flags/test-flag")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON),
)
.andExpect(status().isBadRequest)
.andExpect(jsonPath("$.status").value("BAD_REQUEST"))
.andExpect(jsonPath("$.message").value("Entity not found"))
}
}