diff --git a/build.gradle.kts b/build.gradle.kts index 19ca6dd..9912eaf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,12 +2,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile group = "dev.fyloz.colorrecipesexplorer" -val kotlinVersion = "1.5.0" +val kotlinVersion = "1.5.21" val springBootVersion = "2.3.4.RELEASE" plugins { // Outer scope variables can't be accessed in the plugins section, so we have to redefine them here - val kotlinVersion = "1.5.0" + val kotlinVersion = "1.5.21" val springBootVersion = "2.3.4.RELEASE" id("java") @@ -46,7 +46,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}") testImplementation("org.springframework:spring-test:5.1.6.RELEASE") - testImplementation("org.mockito:mockito-inline:3.6.0") + testImplementation("org.mockito:mockito-inline:3.11.2") testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.2") testImplementation("io.mockk:mockk:1.10.6") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt index eb40a1d..c1c4384 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt @@ -66,10 +66,16 @@ data class ConfigurationImageDto( fun configuration( type: ConfigurationType, - content: String, + content: String = type.defaultContent.toString(), lastUpdated: LocalDateTime? = null ) = Configuration(type, content, lastUpdated ?: LocalDateTime.now()) +fun configuration( + dto: ConfigurationDto +) = with(dto) { + configuration(type = key.toConfigurationType(), content = content) +} + enum class ConfigurationType( val key: String, val defaultContent: Any? = null, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt index f26143e..acd1600 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitService.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.model.ConfigurationType import dev.fyloz.colorrecipesexplorer.model.touchupkit.* import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository @@ -30,12 +29,12 @@ interface TouchUpKitService : /** * Generates and returns a [PdfDocument] for the given [job] as a [ByteArrayResource]. * - * If [CreProperties.cacheGeneratedFiles] is enabled and a file exists for the job, its content will be returned. + * If TOUCH_UP_KIT_CACHE_PDF is enabled and a file exists for the job, its content will be returned. * If caching is enabled but no file exists for the job, the generated ByteArrayResource will be cached on the disk. */ fun generateJobPdfResource(job: String): ByteArrayResource - /** Writes the given [document] to the [FileService] if [CreProperties.cacheGeneratedFiles] is enabled. */ + /** Writes the given [document] to the [FileService] if TOUCH_UP_KIT_CACHE_PDF is enabled. */ fun String.cachePdfDocument(document: PdfDocument) } @@ -48,6 +47,10 @@ class TouchUpKitServiceImpl( ) : AbstractExternalModelService( touchUpKitRepository ), TouchUpKitService { + private val cacheGeneratedFiles by lazy { + configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF).content == true.toString() + } + override fun idNotFoundException(id: Long) = touchUpKitIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = touchUpKitIdAlreadyExistsException(id) @@ -118,7 +121,7 @@ class TouchUpKitServiceImpl( } override fun generateJobPdfResource(job: String): ByteArrayResource { - if (cacheGeneratedFiles()) { + if (cacheGeneratedFiles) { with(job.pdfDocumentPath()) { if (fileService.exists(this)) { return fileService.read(this) @@ -132,7 +135,7 @@ class TouchUpKitServiceImpl( } override fun String.cachePdfDocument(document: PdfDocument) { - if (!cacheGeneratedFiles()) return + if (!cacheGeneratedFiles) return fileService.write(document.toByteArrayResource(), this.pdfDocumentPath(), true) } @@ -142,7 +145,4 @@ class TouchUpKitServiceImpl( private fun TouchUpKit.pdfUrl() = "${configService.get(ConfigurationType.INSTANCE_URL).content}$TOUCH_UP_KIT_CONTROLLER_PATH/pdf?job=$project" - - private fun cacheGeneratedFiles() = - configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF).content == "true" } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Crypto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Crypto.kt new file mode 100644 index 0000000..a8206e6 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Crypto.kt @@ -0,0 +1,17 @@ +package dev.fyloz.colorrecipesexplorer.utils + +import org.springframework.security.crypto.encrypt.Encryptors +import org.springframework.security.crypto.encrypt.TextEncryptor + +fun String.encrypt(password: String, salt: String): String = + withTextEncryptor(password, salt) { + it.encrypt(this) + } + +fun String.decrypt(password: String, salt: String): String = + withTextEncryptor(password, salt) { + it.decrypt(this) + } + +private fun withTextEncryptor(password: String, salt: String, op: (TextEncryptor) -> String) = + op(Encryptors.text(password, salt)) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt index 4d15164..a9979b7 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt @@ -1,7 +1,7 @@ package dev.fyloz.colorrecipesexplorer.service import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName +import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.account.* diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationServiceTest.kt index 5275e97..8c4df85 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationServiceTest.kt @@ -1,22 +1,26 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.config.FileConfiguration +import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.ConfigurationRepository +import dev.fyloz.colorrecipesexplorer.service.config.CONFIGURATION_FORMATTED_LIST_DELIMITER +import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationServiceImpl +import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationSource +import dev.fyloz.colorrecipesexplorer.utils.encrypt import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import java.time.LocalDateTime -import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue class ConfigurationServiceTest { - private val repository = mockk() - private val fileConfiguration = mockk() - private val service = spyk(ConfigurationServiceImpl(repository, mockk(), fileConfiguration, mockk(), mockk(), mockk())) + private val fileService = mockk() + private val configurationSource = mockk() + private val securityProperties = mockk { + every { configSalt } returns "d32270943af7e1cc" + } + private val service = spyk(ConfigurationServiceImpl(fileService, configurationSource, securityProperties, mockk())) @AfterEach fun afterEach() { @@ -126,73 +130,22 @@ class ConfigurationServiceTest { } @Test - fun `get(type) gets in the repository when the given ConfigurationType is not computed or a file property`() { + fun `get(type) gets the configuration in the ConfigurationSource`() { val type = ConfigurationType.INSTANCE_ICON_PATH + val configuration = configuration(type = type) - every { repository.findById(type.key) } returns Optional.of( - ConfigurationEntity(type.key, type.key, LocalDateTime.now()) - ) + every { configurationSource.get(type) } returns configuration - val configuration = service.get(type) + val found = service.get(type) - assertTrue { - configuration.key == type.key - } - - verify { - service.get(type) - repository.findById(type.key) - } - confirmVerified(service, repository) - } - - @Test - fun `get(type) gets in the FileConfiguration when the gien ConfigurationType is a file property`() { - val type = ConfigurationType.DATABASE_URL - - every { fileConfiguration.get(type) } returns configuration(type, type.key) - - val configuration = service.get(type) - - assertTrue { - configuration.key == type.key - } - - verify { - service.get(type) - fileConfiguration.get(type) - } - verify(exactly = 0) { - repository.findById(type.key) - } - confirmVerified(service, fileConfiguration, repository) - } - - @Test - fun `get(type) computes computed properties`() { - val type = ConfigurationType.JAVA_VERSION - - val configuration = service.get(type) - - assertTrue { - configuration.key == type.key - } - - verify { - service.get(type) - } - verify(exactly = 0) { - repository.findById(type.key) - fileConfiguration.get(type) - } - confirmVerified(service, repository, fileConfiguration) + assertEquals(configuration, found) } @Test fun `get(type) throws ConfigurationNotSetException when the given ConfigurationType has no set configuration`() { val type = ConfigurationType.INSTANCE_ICON_PATH - every { repository.findById(type.key) } returns Optional.empty() + every { configurationSource.get(type) } returns null with(assertThrows { service.get(type) }) { assertEquals(type, this.type) @@ -200,56 +153,64 @@ class ConfigurationServiceTest { verify { service.get(type) - repository.findById(type.key) + configurationSource.get(type) } } @Test - fun `set() set the configuration in the FileConfiguration when the given ConfigurationType is a file configuration`() { - val type = ConfigurationType.DATABASE_URL - val content = "url" + fun `get(type) throws InvalidConfigurationKeyException when the given ConfigurationType is encryption salt`() { + val type = ConfigurationType.GENERATED_ENCRYPTION_SALT - every { fileConfiguration.set(type, content) } just runs - - service.set(type, content) - - verify { - service.set(type, content) - fileConfiguration.set(type, content) - } - confirmVerified(service, fileConfiguration) + assertThrows { service.get(type) } } @Test - fun `set() set the configuration in the repository when the given ConfigurationType is not a computed configuration of a file configuration`() { - val type = ConfigurationType.INSTANCE_ICON_PATH - val content = "path" - val configuration = configuration(type, content) - val entity = configuration.toEntity() + fun `get(type) decrypts configuration content when the given ConfigurationType is secure`() { + val type = ConfigurationType.DATABASE_PASSWORD + val content = "securepassword" + val configuration = configuration( + type = type, + content = content.encrypt(type.key, securityProperties.configSalt!!) + ) - every { repository.save(entity) } returns entity + every { configurationSource.get(type) } returns configuration - service.set(type, content) + val found = service.get(type) - verify { - service.set(type, content) - repository.save(entity) - } - confirmVerified(service, repository) + assertEquals(content, found.content) } @Test - fun `set() throws CannotSetComputedConfigurationException when the given ConfigurationType is a computed configuration`() { - val type = ConfigurationType.JAVA_VERSION - val content = "5" + fun `set(configuration) set configuration in ConfigurationSource`() { + val configuration = configuration(type = ConfigurationType.INSTANCE_NAME) - with(assertThrows { service.set(type, content) }) { - assertEquals(type, this.type) - } + every { configurationSource.set(any()) } just runs + + service.set(configuration) verify { - service.set(type, content) + configurationSource.set(configuration) + } + } + + @Test + fun `set(configuration) encrypts secure configurations`() { + val type = ConfigurationType.DATABASE_PASSWORD + val content = "securepassword" + val encryptedContent =content.encrypt(type.key, securityProperties.configSalt!!) + val configuration = configuration(type = type, content = content) + + mockkStatic(String::encrypt) + + every { configurationSource.set(any()) } just runs + every { content.encrypt(any(), any()) } returns encryptedContent + + service.set(configuration) + + verify { + configurationSource.set(match { + it.content == encryptedContent + }) } - confirmVerified(service) } } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/FileServiceTest.kt similarity index 99% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/FileServiceTest.kt index c414c9a..8c4ca7a 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/FileServiceTest.kt @@ -15,7 +15,6 @@ import kotlin.test.assertTrue private val creProperties = CreProperties().apply { dataDirectory = "data" - deploymentUrl = "http://localhost" } private const val mockFilePath = "existingFile" private val mockFilePathPath = Path.of(mockFilePath) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt index 4419e3c..58a1193 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt @@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.account.group import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository +import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test @@ -271,7 +272,7 @@ private class RecipeImageServiceTestContext { val recipeImagesIds = setOf(1L, 10L, 21L) val recipeImagesNames = recipeImagesIds.map { it.imageName }.toSet() val recipeImagesFiles = recipeImagesNames.map { File(it) }.toTypedArray() - val recipeDirectory = spyk(File(recipe.imagesDirectoryPath)) { + val recipeDirectory = mockk { every { exists() } returns true every { isDirectory } returns true every { listFiles() } returns recipeImagesFiles diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitServiceTest.kt similarity index 83% rename from src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt rename to src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitServiceTest.kt index c81244a..c913d45 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/TouchUpKitServiceTest.kt @@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.model.ConfigurationType import dev.fyloz.colorrecipesexplorer.model.configuration import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository import dev.fyloz.colorrecipesexplorer.service.* +import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService import dev.fyloz.colorrecipesexplorer.utils.PdfDocument import dev.fyloz.colorrecipesexplorer.utils.toByteArrayResource import io.mockk.* @@ -18,9 +19,7 @@ private class TouchUpKitServiceTestContext { val fileService = mockk { every { write(any(), any(), any()) } just Runs } - val creProperties = mockk { - every { cacheGeneratedFiles } returns false - } + val creProperties = mockk() val configService = mockk(relaxed = true) val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, configService, touchUpKitRepository)) val pdfDocumentData = mockk() @@ -79,10 +78,13 @@ class TouchUpKitServiceTest { @Test fun `generateJobPdfResource() returns a cached ByteArrayResource from the FileService when caching is enabled and a cached file eixsts for the given job`() { test { - every { creProperties.cacheGeneratedFiles } returns true + enableCachePdf() every { fileService.exists(any()) } returns true every { fileService.read(any()) } returns pdfDocumentData - every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF, "true") + every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration( + ConfigurationType.TOUCH_UP_KIT_CACHE_PDF, + "true" + ) val redResource = touchUpKitService.generateJobPdfResource(job) @@ -95,7 +97,7 @@ class TouchUpKitServiceTest { @Test fun `cachePdfDocument() does nothing when caching is disabled`() { test { - every { creProperties.cacheGeneratedFiles } returns false + disableCachePdf() with(touchUpKitService) { job.cachePdfDocument(pdfDocument) @@ -110,8 +112,7 @@ class TouchUpKitServiceTest { @Test fun `cachePdfDocument() writes the given document to the FileService when cache is enabled`() { test { - every { creProperties.cacheGeneratedFiles } returns true - every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF, "true") + enableCachePdf() with(touchUpKitService) { job.cachePdfDocument(pdfDocument) @@ -123,6 +124,19 @@ class TouchUpKitServiceTest { } } + private fun TouchUpKitServiceTestContext.enableCachePdf() = + this.setCachePdf(true) + + private fun TouchUpKitServiceTestContext.disableCachePdf() = + this.setCachePdf(false) + + private fun TouchUpKitServiceTestContext.setCachePdf(enabled: Boolean) { + every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration( + type = ConfigurationType.TOUCH_UP_KIT_CACHE_PDF, + enabled.toString() + ) + } + private fun test(test: TouchUpKitServiceTestContext.() -> Unit) { TouchUpKitServiceTestContext().test() }