Ajout d'un API dédié aux fichiers.

Ajout de la bibliothèque MockK pour simplifier le mocking dans Kotlin.
This commit is contained in:
FyloZ 2021-04-26 23:30:46 -04:00
parent 37b5a09479
commit c6b3367cfa
10 changed files with 321 additions and 180 deletions

View File

@ -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")

View File

@ -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"
}

View File

@ -8,30 +8,34 @@ enum class EmployeePermission(
val impliedPermissions: List<EmployeePermission> = 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,

View File

@ -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<ByteArrayResource> {
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<Void> {
fileService.write(file, path, overwrite)
return created(path)
}
@DeleteMapping
@PreAuthorize("hasAnyAuthority('REMOVE_FILE')")
fun delete(@RequestParam path: String): ResponseEntity<Void> {
return noContent {
fileService.delete(path)
}
}
private fun created(path: String): ResponseEntity<Void> =
ResponseEntity
.created(URI.create("${creProperties.deploymentUrl}$FILE_CONTROLLER_PATH?path=$path"))
.build()
}

View File

@ -31,14 +31,14 @@ interface RecipeService : ExternalModelService<Recipe, RecipeSaveDto, RecipeUpda
@Service
class RecipeServiceImpl(
recipeRepository: RecipeRepository,
val companyService: CompanyService,
val mixService: MixService,
val recipeStepService: RecipeStepService,
@Lazy val groupService: EmployeeGroupService
recipeRepository: RecipeRepository,
val companyService: CompanyService,
val mixService: MixService,
val recipeStepService: RecipeStepService,
@Lazy val groupService: EmployeeGroupService
) :
AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeRepository>(recipeRepository),
RecipeService {
AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeRepository>(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<Long> {
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"
}

View File

@ -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 <T> 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 <T> 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 : FileIOException> T.logAndThrow(baseException: Exception, logger: Logger): Nothing {
logger.error(this.details, baseException)
throw this
}

View File

@ -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"
)

View File

@ -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

View File

@ -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<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeService, RecipeRepository>() {
AbstractExternalModelServiceTest<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeService, RecipeRepository>() {
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<RecipeImageNotFoundException> { 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)

View File

@ -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<MultipartFile>()
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<MultipartFile>()
whenever(fileService.write(simdutMultipart, path)).doReturn(false)
whenever(fileService.write(simdutMultipart, path, true)).doAnswer { throw FileCreateException(path) }
assertThrows<SimdutWriteException> { 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")
}
}