From c6b3367cfa6f34c59d413e0f5afe63ce072dfe0f Mon Sep 17 00:00:00 2001 From: FyloZ Date: Mon, 26 Apr 2021 23:30:46 -0400 Subject: [PATCH] =?UTF-8?q?Ajout=20d'un=20API=20d=C3=A9di=C3=A9=20aux=20fi?= =?UTF-8?q?chiers.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout de la bibliothèque MockK pour simplifier le mocking dans Kotlin. --- build.gradle.kts | 3 +- .../config/properties/CreProperties.kt | 1 + .../model/EmployeePermission.kt | 22 +- .../rest/files/FileController.kt | 55 +++++ .../service/RecipeService.kt | 97 ++++---- .../service/files/FileService.kt | 219 +++++++++++++----- .../service/files/SimdutService.kt | 32 +-- src/main/resources/application.properties | 1 + .../service/RecipeServiceTest.kt | 40 ++-- .../service/files/SimdutServiceTest.kt | 31 +-- 10 files changed, 321 insertions(+), 180 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt diff --git a/build.gradle.kts b/build.gradle.kts index cfa2953..5432c74 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,9 +40,10 @@ dependencies { implementation("org.springframework.boot:spring-boot-devtools:2.3.4.RELEASE") testImplementation("org.springframework:spring-test:5.1.6.RELEASE") - testImplementation("org.mockito:mockito-core:3.6.0") + testImplementation("org.mockito:mockito-inline:3.6.0") 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") testImplementation("org.springframework.boot:spring-boot-starter-test:2.3.4.RELEASE") testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:2.3.4.RELEASE") testImplementation("org.jetbrains.kotlin:kotlin-test:1.4.10") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/CreProperties.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/CreProperties.kt index 4a14568..ee2d3c7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/CreProperties.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/CreProperties.kt @@ -5,4 +5,5 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties(prefix = "cre.server") class CreProperties { var workingDirectory: String = "data" + var deploymentUrl: String = "http://localhost" } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt index e6ef1b7..7f1aecc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt @@ -8,30 +8,34 @@ enum class EmployeePermission( val impliedPermissions: List = listOf(), val deprecated: Boolean = false ) { - VIEW_RECIPES, - VIEW_CATALOG, + READ_FILE, + WRITE_FILE(listOf(READ_FILE)), + REMOVE_FILE(listOf(WRITE_FILE)), + + VIEW_RECIPES(listOf(READ_FILE)), + VIEW_CATALOG(listOf(READ_FILE)), VIEW_USERS, PRINT_MIXES(listOf(VIEW_RECIPES)), EDIT_RECIPES_PUBLIC_DATA(listOf(VIEW_RECIPES)), - EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA)), - EDIT_MATERIALS(listOf(VIEW_CATALOG)), + EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA, WRITE_FILE)), + EDIT_MATERIALS(listOf(VIEW_CATALOG, WRITE_FILE)), EDIT_MATERIAL_TYPES(listOf(VIEW_CATALOG)), EDIT_COMPANIES(listOf(VIEW_CATALOG)), EDIT_USERS(listOf(VIEW_USERS)), EDIT_CATALOG(listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)), - ADD_TO_INVENTORY(listOf(VIEW_CATALOG)), - DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)), - - REMOVE_RECIPES(listOf(EDIT_RECIPES)), - REMOVE_MATERIALS(listOf(EDIT_MATERIALS)), + REMOVE_RECIPES(listOf(EDIT_RECIPES, REMOVE_FILE)), + REMOVE_MATERIALS(listOf(EDIT_MATERIALS, REMOVE_FILE)), REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES)), REMOVE_COMPANIES(listOf(EDIT_COMPANIES)), REMOVE_USERS(listOf(EDIT_USERS)), REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES)), + ADD_TO_INVENTORY(listOf(VIEW_CATALOG)), + DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)), + ADMIN( listOf( EDIT_CATALOG, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt new file mode 100644 index 0000000..2735583 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt @@ -0,0 +1,55 @@ +package dev.fyloz.colorrecipesexplorer.rest.files + +import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties +import dev.fyloz.colorrecipesexplorer.rest.noContent +import dev.fyloz.colorrecipesexplorer.service.files.FileService +import org.springframework.core.io.ByteArrayResource +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile +import java.net.URI + +const val FILE_CONTROLLER_PATH = "/api/file" + +@RestController +@RequestMapping(FILE_CONTROLLER_PATH) +class FileController( + private val fileService: FileService, + private val creProperties: CreProperties +) { + @GetMapping(produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE]) + @PreAuthorize("hasAnyAuthority('READ_FILE')") + fun upload(@RequestParam path: String): ResponseEntity { + val file = fileService.read(path) + return ResponseEntity.ok() + .contentLength(file.contentLength()) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(file) + } + + @PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) + @PreAuthorize("hasAnyAuthority('WRITE_FILE')") + fun download( + file: MultipartFile, + @RequestParam path: String, + @RequestParam(required = false) overwrite: Boolean = false + ): ResponseEntity { + fileService.write(file, path, overwrite) + return created(path) + } + + @DeleteMapping + @PreAuthorize("hasAnyAuthority('REMOVE_FILE')") + fun delete(@RequestParam path: String): ResponseEntity { + return noContent { + fileService.delete(path) + } + } + + private fun created(path: String): ResponseEntity = + ResponseEntity + .created(URI.create("${creProperties.deploymentUrl}$FILE_CONTROLLER_PATH?path=$path")) + .build() +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt index 7df7296..e1313bb 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -31,14 +31,14 @@ interface RecipeService : ExternalModelService(recipeRepository), - RecipeService { + AbstractExternalModelService(recipeRepository), + RecipeService { override fun idNotFoundException(id: Long) = recipeIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = recipeIdAlreadyExistsException(id) @@ -49,14 +49,14 @@ class RecipeServiceImpl( // TODO checks if name is unique in the scope of the [company] return save(with(entity) { recipe( - name = name, - description = description, - color = color, - gloss = gloss, - sample = sample, - approbationDate = approbationDate, - remark = remark ?: "", - company = companyService.getById(companyId) + name = name, + description = description, + color = color, + gloss = gloss, + sample = sample, + approbationDate = approbationDate, + remark = remark ?: "", + company = companyService.getById(companyId) ) }) } @@ -67,17 +67,17 @@ class RecipeServiceImpl( return update(with(entity) { recipe( - id = id, - name = name or persistedRecipe.name, - description = description or persistedRecipe.description, - color = color or persistedRecipe.color, - gloss = gloss ?: persistedRecipe.gloss, - sample = sample ?: persistedRecipe.sample, - approbationDate = approbationDate ?: persistedRecipe.approbationDate, - remark = remark or persistedRecipe.remark, - company = persistedRecipe.company, - mixes = persistedRecipe.mixes, - groupsInformation = updateGroupsInformation(persistedRecipe, entity) + id = id, + name = name or persistedRecipe.name, + description = description or persistedRecipe.description, + color = color or persistedRecipe.color, + gloss = gloss ?: persistedRecipe.gloss, + sample = sample ?: persistedRecipe.sample, + approbationDate = approbationDate ?: persistedRecipe.approbationDate, + remark = remark or persistedRecipe.remark, + company = persistedRecipe.company, + mixes = persistedRecipe.mixes, + groupsInformation = updateGroupsInformation(persistedRecipe, entity) ) }) } @@ -96,8 +96,8 @@ class RecipeServiceImpl( this.steps = it.steps.toMutableSet() } } ?: recipeGroupInformation( - group = groupService.getById(it.groupId), - steps = it.steps.toMutableSet() + group = groupService.getById(it.groupId), + steps = it.steps.toMutableSet() ) updatedGroupsInformation.add(updatedGroupInformation) @@ -114,7 +114,7 @@ class RecipeServiceImpl( val recipe = getById(publicDataDto.recipeId) fun noteForGroup(group: EmployeeGroup) = - publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content + publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content // Notes recipe.groupsInformation.map { @@ -133,15 +133,16 @@ class RecipeServiceImpl( } override fun addMix(recipe: Recipe, mix: Mix) = - update(recipe.apply { mixes.add(mix) }) + update(recipe.apply { mixes.add(mix) }) override fun removeMix(mix: Mix): Recipe = - update(mix.recipe.apply { mixes.remove(mix) }) + update(mix.recipe.apply { mixes.remove(mix) }) } const val RECIPE_IMAGES_DIRECTORY = "images/recipe" interface RecipeImageService { + // TOOD change return type to ByteArrayResource fun getByIdForRecipe(id: Long, recipeId: Long): ByteArray /** Gets the identifier of every images associated to the recipe with the given [recipeId]. */ @@ -157,11 +158,11 @@ interface RecipeImageService { @Service class RecipeImageServiceImpl(val recipeService: RecipeService, val fileService: FileService) : RecipeImageService { override fun getByIdForRecipe(id: Long, recipeId: Long): ByteArray = - try { - fileService.readAsBytes(getPath(id, recipeId)) - } catch (ex: NoSuchFileException) { - throw RecipeImageNotFoundException(id, recipeService.getById(recipeId)) - } + try { + fileService.read(getPath(id, recipeId)).byteArray + } catch (ex: NoSuchFileException) { + throw RecipeImageNotFoundException(id, recipeService.getById(recipeId)) + } override fun getAllIdsForRecipe(recipeId: Long): Collection { val recipe = recipeService.getById(recipeId) @@ -170,31 +171,31 @@ class RecipeImageServiceImpl(val recipeService: RecipeService, val fileService: return listOf() } return recipeDirectory.listFiles()!! // Should never be null because we check if recipeDirectory is a directory and exists before - .filterNotNull() - .map { it.name.toLong() } + .filterNotNull() + .map { it.name.toLong() } } override fun save(image: MultipartFile, recipeId: Long): Long { /** Gets the next id available for a new image for the recipe with the given [recipeId]. */ fun getNextAvailableId(): Long = - with(getAllIdsForRecipe(recipeId)) { - if (isEmpty()) - 0 - else - maxOrNull()!! + 1L // maxOrNull() cannot return null because existingIds cannot be empty at this point - } + with(getAllIdsForRecipe(recipeId)) { + if (isEmpty()) + 0 + else + maxOrNull()!! + 1L // maxOrNull() cannot return null because existingIds cannot be empty at this point + } val nextAvailableId = getNextAvailableId() - fileService.write(image, getPath(nextAvailableId, recipeId)) + fileService.write(image, getPath(nextAvailableId, recipeId), true) return nextAvailableId } override fun delete(id: Long, recipeId: Long) = - fileService.delete(getPath(id, recipeId)) + fileService.delete(getPath(id, recipeId)) /** Gets the images directory of the recipe with the given [recipeId]. */ - fun getRecipeDirectory(recipeId: Long) = File(fileService.getPath("$RECIPE_IMAGES_DIRECTORY/$recipeId")) + fun getRecipeDirectory(recipeId: Long) = File("$RECIPE_IMAGES_DIRECTORY/$recipeId") /** Gets the file of the image with the given [recipeId] and [id]. */ - fun getPath(id: Long, recipeId: Long): String = fileService.getPath("$RECIPE_IMAGES_DIRECTORY/$recipeId/$id") + fun getPath(id: Long, recipeId: Long): String = "$RECIPE_IMAGES_DIRECTORY/$recipeId/$id" } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt index c6c11fd..8fa6d3d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt @@ -1,83 +1,178 @@ package dev.fyloz.colorrecipesexplorer.service.files import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties +import dev.fyloz.colorrecipesexplorer.exception.RestException import org.slf4j.Logger -import org.springframework.core.io.ResourceLoader +import org.springframework.core.io.ByteArrayResource +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile import java.io.File import java.io.IOException -import java.io.InputStream -import java.nio.charset.StandardCharsets import java.nio.file.Files -@Service -class FileService( - private val resourcesLoader: ResourceLoader, - private val creProperties: CreProperties, - private val logger: Logger -) { - /** Reads the resource at the given [path] as a [String]. */ - fun readResource(path: String): String = try { - resourcesLoader.getResource("classpath:$path").inputStream.use { - readInputStreamAsString(it) - } - } catch (ex: IOException) { - logger.error("Could not read resource", ex) - "" - } +interface FileService { + /** Checks if the file at the given [path] exists. */ + fun exists(path: String): Boolean - /** Reads the given [stream] as a [String]. */ - fun readInputStreamAsString(stream: InputStream) = with(stream.readAllBytes()) { - String(this, StandardCharsets.UTF_8) - } + /** Reads the file at the given [path]. */ + fun read(path: String): ByteArrayResource - /** Reads the file at the given [path] as a [ByteArray]. */ - fun readAsBytes(path: String) = - withFileAt(path) { this.readBytes() } + /** Creates a file at the given [path]. */ + fun create(path: String) - /** Writes the given [multipartFile] to the file at the given [path]. */ - fun write(multipartFile: MultipartFile, path: String): Boolean = - if (multipartFile.size <= 0) true - else try { - multipartFile.transferTo(create(path).toPath()) - true - } catch (ex: IOException) { - logger.error("Unable to write multipart file", ex) - false - } - - /** Creates a new file at the given [path]. If the file already exists, nothing will be done. */ - fun create(path: String) = withFileAt(path) { - if (!exists(path)) { - try { - Files.createDirectories(this.parentFile.toPath()) - Files.createFile(this.toPath()) - } catch (ex: IOException) { - logger.error("Unable to create file", ex) - } - } - this - } + /** Writes the given [file] at the given [path]. If the file already exists, it will be overwritten if [overwrite] is true. */ + fun write(file: MultipartFile, path: String, overwrite: Boolean) /** Deletes the file at the given [path]. */ - fun delete(path: String) = withFileAt(path) { - try { - if (exists(path)) Files.delete(this.toPath()) - } catch (ex: IOException) { - logger.error("Unable to delete file", ex) - } - } + fun delete(path: String) - /** Checks if a file with the given [path] exists on the disk. */ - fun exists(path: String): Boolean = withFileAt(path) { + /** Completes the path of the given [String] by adding the working directory. */ + fun String.fullPath(): FilePath +} + +@Service +class FileServiceImpl( + private val creProperties: CreProperties, + private val logger: Logger +) : FileService { + override fun exists(path: String) = withFileAt(path.fullPath()) { this.exists() && this.isFile } - /** Runs the given [block] in the context of a file with the given [path]. */ - fun withFileAt(path: String, block: File.() -> T) = - File(path).block() + override fun read(path: String) = ByteArrayResource( + withFileAt(path.fullPath()) { + if (!exists(path)) throw FileNotFoundException(path) + try { + readBytes() + } catch (ex: IOException) { + FileReadException(path).logAndThrow(ex, logger) + } + } + ) - fun getPath(fileName: String): String = - "${creProperties.workingDirectory}/$fileName" + override fun create(path: String) { + val fullPath = path.fullPath() + if (!exists(path)) { + try { + withFileAt(fullPath) { + this.create() + } + } catch (ex: IOException) { + FileCreateException(path).logAndThrow(ex, logger) + } + } + } + + override fun write(file: MultipartFile, path: String, overwrite: Boolean) { + val fullPath = path.fullPath() + + if (exists(path)) { + if (!overwrite) throw FileExistsException(path) + } else { + create(path) + } + + try { + withFileAt(fullPath) { + file.transferTo(this.toPath()) + } + } catch (ex: IOException) { + FileWriteException(path).logAndThrow(ex, logger) + } + } + + override fun delete(path: String) { + try { + withFileAt(path.fullPath()) { + if (!exists(path)) throw FileNotFoundException(path) + !this.delete() + } + } catch (ex: IOException) { + FileDeleteException(path).logAndThrow(ex, logger) + } + } + + override fun String.fullPath() = + FilePath("${creProperties.workingDirectory}/$this") + + /** Runs the given [block] in the context of a file with the given [fullPath]. */ + private fun withFileAt(fullPath: FilePath, block: File.() -> T) = + fullPath.file.block() +} + +data class FilePath(val path: String) { + val file: File + get() = File(path) +} + +/** Shortcut to create a file and its parent directories. */ +fun File.create() { + Files.createDirectories(this.parentFile.toPath()) + Files.createFile(this.toPath()) +} + +private const val FILE_IO_EXCEPTION_TITLE = "File IO error" + +class FileExistsException(val path: String) : + RestException( + "io-exists", + FILE_IO_EXCEPTION_TITLE, + HttpStatus.BAD_REQUEST, + "Could not write file to '$path' because it already exists. To overwrite the file set the overwrite parameter to true", + pathMap(path) + ) + +class FileNotFoundException(val path: String) : + RestException( + "io-notfound", + FILE_IO_EXCEPTION_TITLE, + HttpStatus.NOT_FOUND, + "Could not access file at '$path' because it does not exists", + pathMap(path) + ) + +sealed class FileIOException(type: String, details: String, val path: String) : + RestException( + "io-$type", + FILE_IO_EXCEPTION_TITLE, + HttpStatus.INTERNAL_SERVER_ERROR, + details, + pathMap(path) + ) + +class FileReadException(path: String) : + FileIOException( + "read", + "Could not read file at '$path'", + path + ) + +class FileWriteException(path: String) : + FileIOException( + "write", + "Could not write file to '$path'", + path + ) + +class FileCreateException(path: String) : + FileIOException( + "create", + "Could not create file at '$path'", + path + ) + +class FileDeleteException(path: String) : + FileIOException( + "delete", + "Could not delete file at '$path'", + path + ) + +private fun pathMap(path: String) = + mapOf("path" to path) + +private fun T.logAndThrow(baseException: Exception, logger: Logger): Nothing { + logger.error(this.details, baseException) + throw this } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutService.kt index e13c501..c781859 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutService.kt @@ -12,20 +12,21 @@ const val SIMDUT_DIRECTORY = "simdut" @Service class SimdutService( - private val fileService: FileService, - private val logger: Logger + private val fileService: FileService, + private val logger: Logger ) { /** Checks if the given [material] has a SIMDUT file. */ fun exists(material: Material) = - fileService.exists(getPath(material)) + fileService.exists(getPath(material)) /** Reads the SIMDUT file of the given [material]. */ + // TODO change return type to ByteArrayResource fun read(material: Material): ByteArray { val path = getPath(material) if (!fileService.exists(path)) return ByteArray(0) return try { - fileService.readAsBytes(path) + fileService.read(path).byteArray } catch (ex: IOException) { logger.error("Could not read SIMDUT file", ex) ByteArray(0) @@ -34,8 +35,11 @@ class SimdutService( /** Writes the given [simdut] file for the given [material] to the disk. */ fun write(material: Material, simdut: MultipartFile) { - if (!fileService.write(simdut, getPath(material))) + try { + fileService.write(simdut, getPath(material), true) + } catch (ex: FileWriteException) { throw SimdutWriteException(material) + } } /** Updates the SIMDUT file of the given [material] with the given [simdut]. */ @@ -46,21 +50,21 @@ class SimdutService( /** Deletes the SIMDUT file of the given [material]. */ fun delete(material: Material) = - fileService.delete(getPath(material)) + fileService.delete(getPath(material)) /** Gets the path of the SIMDUT file of the given [material]. */ fun getPath(material: Material) = - fileService.getPath("$SIMDUT_DIRECTORY/${getSimdutFileName(material)}") + "$SIMDUT_DIRECTORY/${getSimdutFileName(material)}" /** Gets the name of the SIMDUT file of the given [material]. */ fun getSimdutFileName(material: Material) = - material.id.toString() + material.id.toString() } class SimdutWriteException(material: Material) : - RestException( - "simdut-write", - "Could not write SIMDUT file", - HttpStatus.INTERNAL_SERVER_ERROR, - "Could not write the SIMDUT file for the material ${material.name} to the disk" - ) + RestException( + "simdut-write", + "Could not write SIMDUT file", + HttpStatus.INTERNAL_SERVER_ERROR, + "Could not write the SIMDUT file for the material ${material.name} to the disk" + ) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 492010d..3250a34 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,6 +2,7 @@ server.port=9090 # CRE cre.server.working-directory=data +cre.server.deployment-url=http://localhost:9090 cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE cre.security.jwt-duration=18000000 # Root user diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt index e42820e..776275e 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt @@ -7,6 +7,7 @@ import dev.fyloz.colorrecipesexplorer.service.files.FileService import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.springframework.core.io.ByteArrayResource import org.springframework.mock.web.MockMultipartFile import java.io.File import java.nio.file.NoSuchFileException @@ -15,13 +16,14 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class RecipeServiceTest : - AbstractExternalModelServiceTest() { + AbstractExternalModelServiceTest() { override val repository: RecipeRepository = mock() private val companyService: CompanyService = mock() private val mixService: MixService = mock() private val groupService: EmployeeGroupService = mock() private val recipeStepService: RecipeStepService = mock() - override val service: RecipeService = spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService)) + override val service: RecipeService = + spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService)) private val company: Company = company(id = 0L) override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company) @@ -79,22 +81,22 @@ class RecipeServiceTest : @Test override fun `update(dto) calls and returns update() with the created entity`() = - withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) + withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) // updatePublicData() @Test fun `updatePublicData() updates the notes of a recipe groups information according to the RecipePublicDataDto`() { val recipe = recipe( - id = 0L, groupsInformation = setOf( - recipeGroupInformation(id = 0L, group = employeeGroup(id = 1L), note = "Old note"), - recipeGroupInformation(id = 1L, group = employeeGroup(id = 2L), note = "Another note"), - recipeGroupInformation(id = 2L, group = employeeGroup(id = 3L), note = "Up to date note") - ) + id = 0L, groupsInformation = setOf( + recipeGroupInformation(id = 0L, group = employeeGroup(id = 1L), note = "Old note"), + recipeGroupInformation(id = 1L, group = employeeGroup(id = 2L), note = "Another note"), + recipeGroupInformation(id = 2L, group = employeeGroup(id = 3L), note = "Up to date note") + ) ) val notes = setOf( - noteDto(groupId = 1, content = "Note 1"), - noteDto(groupId = 2, content = null) + noteDto(groupId = 1, content = "Note 1"), + noteDto(groupId = 2, content = null) ) val publicData = recipePublicDataDto(recipeId = recipe.id!!, notes = notes) @@ -115,10 +117,10 @@ class RecipeServiceTest : @Test fun `updatePublicData() update the location of a recipe mixes in the mix service according to the RecipePublicDataDto`() { val publicData = recipePublicDataDto( - mixesLocation = setOf( - mixLocationDto(mixId = 0L, location = "Loc 1"), - mixLocationDto(mixId = 1L, location = "Loc 2") - ) + mixesLocation = setOf( + mixLocationDto(mixId = 0L, location = "Loc 1"), + mixLocationDto(mixId = 1L, location = "Loc 2") + ) ) service.updatePublicData(publicData) @@ -186,8 +188,7 @@ class RecipeImageServiceTest { @Test fun `getByIdForRecipe() returns data for the given recipe and image id red by the file service`() { - whenever(fileService.getPath(imagePath)).doReturn(imagePath) - whenever(fileService.readAsBytes(imagePath)).doReturn(imageData) + whenever(fileService.read(imagePath)).doReturn(ByteArrayResource(imageData)) val found = service.getByIdForRecipe(imageId, recipeId) @@ -198,7 +199,7 @@ class RecipeImageServiceTest { fun `getByIdForRecipe() throws RecipeImageNotFoundException when no image with the given recipe and image id exists`() { doReturn(imagePath).whenever(service).getPath(imageId, recipeId) whenever(recipeService.getById(recipeId)).doReturn(recipe) - whenever(fileService.readAsBytes(imagePath)).doAnswer { throw NoSuchFileException(imagePath) } + whenever(fileService.read(imagePath)).doAnswer { throw NoSuchFileException(imagePath) } assertThrows { service.getByIdForRecipe(imageId, recipeId) } } @@ -256,7 +257,7 @@ class RecipeImageServiceTest { service.save(image, recipeId) - verify(fileService).write(image, imagePath) + verify(fileService).write(image, imagePath, true) } // delete() @@ -275,7 +276,6 @@ class RecipeImageServiceTest { @Test fun `getRecipeDirectory() returns a file with the expected path`() { val recipeDirectoryPath = "$RECIPE_IMAGES_DIRECTORY/$recipeId" - whenever(fileService.getPath(recipeDirectoryPath)).doReturn(recipeDirectoryPath) val found = service.getRecipeDirectory(recipeId) @@ -286,8 +286,6 @@ class RecipeImageServiceTest { @Test fun `getPath() returns the expected path`() { - whenever(fileService.getPath(any())).doAnswer { it.arguments[0] as String } - val found = service.getPath(imageId, recipeId) assertEquals(imagePath, found) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutServiceTest.kt index c80391b..22ecfc0 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutServiceTest.kt @@ -6,6 +6,7 @@ import dev.fyloz.colorrecipesexplorer.model.material import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.springframework.core.io.ByteArrayResource import org.springframework.web.multipart.MultipartFile import java.io.IOException import kotlin.test.assertEquals @@ -25,7 +26,7 @@ class SimdutServiceTest { @JvmName("withNullableMaterialPath") private inline fun withMaterialPath(material: Material? = null, exists: Boolean = true, test: (String) -> Unit) = - withMaterialPath(material ?: this.material, exists, test) + withMaterialPath(material ?: this.material, exists, test) private inline fun withMaterialPath(material: Material, exists: Boolean = true, test: (String) -> Unit) { val path = "data/simdut/${material.id}" @@ -58,7 +59,7 @@ class SimdutServiceTest { withMaterialPath { path -> val simdutContent = byteArrayOf(0xf) - whenever(fileService.readAsBytes(path)).doReturn(simdutContent) + whenever(fileService.read(path)).doReturn(ByteArrayResource(simdutContent)) val found = service.read(material) @@ -78,7 +79,7 @@ class SimdutServiceTest { @Test fun `read() returns a empty ByteArray when reading the SIMDUT throws an IOException`() { withMaterialPath { path -> - whenever(fileService.readAsBytes(path)).doAnswer { throw IOException() } + whenever(fileService.read(path)).doAnswer { throw IOException() } val found = service.read(material) @@ -93,11 +94,9 @@ class SimdutServiceTest { withMaterialPath { path -> val simdutMultipart = mock() - whenever(fileService.write(simdutMultipart, path)).doReturn(true) - service.write(material, simdutMultipart) - verify(fileService).write(simdutMultipart, path) + verify(fileService).write(simdutMultipart, path, true) } } @@ -106,7 +105,7 @@ class SimdutServiceTest { withMaterialPath { path -> val simdutMultipart = mock() - whenever(fileService.write(simdutMultipart, path)).doReturn(false) + whenever(fileService.write(simdutMultipart, path, true)).doAnswer { throw FileCreateException(path) } assertThrows { service.write(material, simdutMultipart) } } @@ -138,22 +137,4 @@ class SimdutServiceTest { verify(fileService).delete(path) } } - - // getPath() - - @Test - fun `getPath() returns the appropriate path for the given material`() { - val simdutFileName = material.id.toString() - val workingDirectory = "data" - val expectedPath = "$workingDirectory/$SIMDUT_DIRECTORY/$simdutFileName" - - whenever(fileService.getPath(any())).doAnswer { "$workingDirectory/${it.arguments[0]}" } - doAnswer { simdutFileName }.whenever(service).getSimdutFileName(material) - - val found = service.getPath(material) - - assertEquals(expectedPath, found) - - verify(fileService).getPath("$SIMDUT_DIRECTORY/$simdutFileName") - } }