diff --git a/src/main/java/io/gaia_app/stacks/controller/TerraformStateController.java b/src/main/java/io/gaia_app/stacks/controller/TerraformStateController.java index e4aa4d72f..6ad986fae 100644 --- a/src/main/java/io/gaia_app/stacks/controller/TerraformStateController.java +++ b/src/main/java/io/gaia_app/stacks/controller/TerraformStateController.java @@ -1,7 +1,7 @@ package io.gaia_app.stacks.controller; import io.gaia_app.stacks.bo.TerraformState; -import io.gaia_app.stacks.repository.TerraformStateRepository; +import io.gaia_app.stacks.service.StateService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -12,16 +12,16 @@ @RestController public class TerraformStateController { - private TerraformStateRepository repository; + private StateService stateService; @Autowired - public TerraformStateController(TerraformStateRepository repository) { - this.repository = repository; + public TerraformStateController(StateService stateService) { + this.stateService = stateService; } @GetMapping("/api/state/{id}") public Map getState(@PathVariable String id){ - return repository.findById(id) + return stateService.findById(id) .orElseThrow( () -> new ResponseStatusException(HttpStatus.NOT_FOUND)) .getValue(); @@ -32,7 +32,7 @@ public void postState(@PathVariable String id, @RequestBody Map var terraformState = new TerraformState(); terraformState.setId(id); terraformState.setValue(body); - repository.save(terraformState); + stateService.save(terraformState); } } diff --git a/src/main/java/io/gaia_app/stacks/service/StateService.kt b/src/main/java/io/gaia_app/stacks/service/StateService.kt new file mode 100644 index 000000000..04005fafb --- /dev/null +++ b/src/main/java/io/gaia_app/stacks/service/StateService.kt @@ -0,0 +1,62 @@ +package io.gaia_app.stacks.service + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import io.gaia_app.encryption.EncryptionService +import io.gaia_app.stacks.bo.TerraformState +import io.gaia_app.stacks.repository.TerraformStateRepository +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.stereotype.Service +import java.util.* + +/** + * Service that manages state + */ +interface StateService { + + fun findById(id: String): Optional + + fun save(state: TerraformState): TerraformState +} + +/** + * Pass-through implementation to the repository + */ +@Service +@ConditionalOnMissingBean(EncryptionService::class) +class StateServiceImpl(val terraformStateRepository: TerraformStateRepository): StateService { + + override fun findById(id: String): Optional = terraformStateRepository.findById(id) + + override fun save(state: TerraformState): TerraformState = terraformStateRepository.save(state) + +} + +/** + * Implementation that encrypts / decrypts the content + */ +@Service +@ConditionalOnBean(EncryptionService::class) +class EncryptedStateServiceImpl( + val terraformStateRepository: TerraformStateRepository, + val encryptionService: EncryptionService, + val objectMapper: ObjectMapper): StateService { + + override fun findById(id: String): Optional { + val state = this.terraformStateRepository.findById(id) + return state.map { + val decrypted = encryptionService.decrypt(it.value["encrypted"] as String); + it.value = objectMapper.readValue(decrypted) + it + } + } + + override fun save(state: TerraformState): TerraformState { + val stateString = objectMapper.writeValueAsString(state.value) + val encrypted = encryptionService.encrypt(stateString) + state.value = mapOf("encrypted" to encrypted) + return terraformStateRepository.save(state) + } + +} diff --git a/src/test/java/io/gaia_app/stacks/service/EncryptedStateServiceImplTest.kt b/src/test/java/io/gaia_app/stacks/service/EncryptedStateServiceImplTest.kt new file mode 100644 index 000000000..246c0546b --- /dev/null +++ b/src/test/java/io/gaia_app/stacks/service/EncryptedStateServiceImplTest.kt @@ -0,0 +1,92 @@ +package io.gaia_app.stacks.service + +import com.fasterxml.jackson.databind.ObjectMapper +import io.gaia_app.encryption.EncryptionService +import io.gaia_app.stacks.bo.TerraformState +import io.gaia_app.stacks.repository.TerraformStateRepository +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.any +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.stubbing.Answer +import java.util.* + +@ExtendWith(MockitoExtension::class) +internal class EncryptedStateServiceImplTest { + + @Mock + lateinit var terraformStateRepository: TerraformStateRepository + + @Mock + lateinit var encryptionService: EncryptionService + + @Spy + var objectMapper = ObjectMapper() + + @InjectMocks + lateinit var stateServiceImpl: EncryptedStateServiceImpl + + @Test + fun `findById() should decrypt state`() { + // given + val encryptedState = TerraformState(); + encryptedState.value = mapOf("encrypted" to "encryptedMapValue") + + `when`(encryptionService.decrypt("encryptedMapValue")) + .thenReturn("""{ + "state": "content", + "nestedContent": { + "key": "value", + "integer": 1, + "boolean": true + } + }""".trimMargin()) + `when`(terraformStateRepository.findById("12")).thenReturn(Optional.of(encryptedState)) + + // when + val decryptedState = stateServiceImpl.findById("12") + + // then + assertThat(decryptedState).isNotEmpty + assertThat(decryptedState.get().value).containsEntry("state","content") + assertThat(decryptedState.get().value["nestedContent"] as Map).containsEntry("key", "value") + } + + @Test + fun `save() should encrypt state`() { + // given + val plainState = TerraformState(); + plainState.value = mapOf( + "state" to "content", + "nestedContent" to mapOf( + "key" to "value", + "integer" to 1, + "boolean" to true + ) + ) + val valueAsString = objectMapper.writeValueAsString(plainState.value) + + `when`(encryptionService.encrypt(valueAsString)).thenReturn("encryptedMapValue") + + `when`(terraformStateRepository.save(any(TerraformState::class.java))).thenAnswer(firstArg()) + + // when + val encryptedState = stateServiceImpl.save(plainState) + + // then + assertThat(encryptedState.value).isEqualTo(mapOf("encrypted" to "encryptedMapValue")) + } + +} + +/** + * Mockito answer which returns the first argument of the mock invocation. + */ +fun firstArg(): Answer<*>? = Answer { it.arguments[0] } diff --git a/src/test/java/io/gaia_app/stacks/service/StateServiceImplTest.kt b/src/test/java/io/gaia_app/stacks/service/StateServiceImplTest.kt new file mode 100644 index 000000000..28163e8d0 --- /dev/null +++ b/src/test/java/io/gaia_app/stacks/service/StateServiceImplTest.kt @@ -0,0 +1,41 @@ +package io.gaia_app.stacks.service + +import io.gaia_app.stacks.bo.TerraformState +import io.gaia_app.stacks.repository.TerraformStateRepository +import io.gaia_app.test.any +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.runner.RunWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.jupiter.MockitoExtension + +@ExtendWith(MockitoExtension::class) +internal class StateServiceImplTest { + + @Mock + lateinit var terraformStateRepository: TerraformStateRepository + + @InjectMocks + lateinit var stateServiceImpl: StateServiceImpl + + @Test + fun `findById() should call the repository`() { + stateServiceImpl.findById("12") + + verify(terraformStateRepository).findById("12") + } + + @Test + fun `save() should call the repository`() { + val state = TerraformState() + `when`(terraformStateRepository.save(state)).thenReturn(state) + + stateServiceImpl.save(state) + + verify(terraformStateRepository).save(state) + } +}