feature/18-add-existing-files-cache #26
|
@ -2,14 +2,14 @@
|
|||
global-variables:
|
||||
release: &release ${DRONE_TAG}
|
||||
environment: &environment
|
||||
JAVA_VERSION: 11
|
||||
GRADLE_VERSION: 7.1
|
||||
JAVA_VERSION: 17
|
||||
GRADLE_VERSION: 7.3
|
||||
CRE_VERSION: dev-${DRONE_BUILD_NUMBER}
|
||||
CRE_ARTIFACT_NAME: ColorRecipesExplorer
|
||||
CRE_REGISTRY_IMAGE: registry.fyloz.dev/colorrecipesexplorer/backend
|
||||
CRE_PORT: 9101
|
||||
CRE_RELEASE: *release
|
||||
gradle-image: &gradle-image gradle:7.1-jdk11
|
||||
gradle-image: &gradle-image gradle:7.3-jdk17
|
||||
alpine-image: &alpine-image alpine:latest
|
||||
docker-registry: &docker-registry registry.fyloz.dev
|
||||
docker-registry-repo: &docker-registry-repo registry.fyloz.dev/colorrecipesexplorer/backend
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
ARG GRADLE_VERSION=7.1
|
||||
ARG JAVA_VERSION=11
|
||||
ARG GRADLE_VERSION=7.3
|
||||
ARG JAVA_VERSION=17
|
||||
|
||||
FROM gradle:$GRADLE_VERSION-jdk$JAVA_VERSION AS build
|
||||
WORKDIR /usr/src
|
||||
|
|
|
@ -31,6 +31,7 @@ dependencies {
|
|||
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
||||
implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.2.1")
|
||||
implementation("dev.fyloz:memorycache:1.0")
|
||||
implementation("io.github.microutils:kotlin-logging-jvm:2.1.21")
|
||||
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
|
||||
implementation("io.jsonwebtoken:jjwt-impl:0.11.2")
|
||||
|
@ -50,7 +51,7 @@ dependencies {
|
|||
implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
|
||||
|
||||
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||
testImplementation("io.mockk:mockk:1.12.0")
|
||||
testImplementation("io.mockk:mockk:1.12.1")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test:${kotlinVersion}")
|
||||
testImplementation("org.mockito:mockito-inline:3.11.2")
|
||||
testImplementation("org.springframework:spring-test:5.3.13")
|
||||
|
@ -61,6 +62,8 @@ dependencies {
|
|||
runtimeOnly("mysql:mysql-connector-java:8.0.22")
|
||||
runtimeOnly("org.postgresql:postgresql:42.2.16")
|
||||
runtimeOnly("com.microsoft.sqlserver:mssql-jdbc:9.2.1.jre11")
|
||||
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}")
|
||||
}
|
||||
|
||||
springBoot {
|
||||
|
@ -68,8 +71,8 @@ springBoot {
|
|||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
@ -83,23 +86,26 @@ sourceSets {
|
|||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
|
||||
jvmArgs("-XX:+ShowCodeDetailsInExceptionMessages")
|
||||
testLogging {
|
||||
events("skipped", "failed")
|
||||
setExceptionFormat("full")
|
||||
}
|
||||
|
||||
reports {
|
||||
junitXml.required.set(true)
|
||||
html.required.set(false)
|
||||
}
|
||||
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events("skipped", "failed")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>() {
|
||||
options.compilerArgs.addAll(arrayOf("--release", "11"))
|
||||
options.compilerArgs.addAll(arrayOf("--release", "17"))
|
||||
}
|
||||
tasks.withType<KotlinCompile>().all {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-Xinline-classes"
|
||||
|
|
|
@ -3,3 +3,5 @@ package dev.fyloz.colorrecipesexplorer
|
|||
typealias SpringUser = org.springframework.security.core.userdetails.User
|
||||
typealias SpringUserDetails = org.springframework.security.core.userdetails.UserDetails
|
||||
typealias SpringUserDetailsService = org.springframework.security.core.userdetails.UserDetailsService
|
||||
|
||||
typealias JavaFile = java.io.File
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.ColorRecipesExplorerApplication
|
||||
import dev.fyloz.colorrecipesexplorer.DatabaseUpdaterProperties
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.CachedFileSystemItem
|
||||
import dev.fyloz.memorycache.ExpiringMemoryCache
|
||||
import dev.fyloz.memorycache.MemoryCache
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(MaterialTypeProperties::class, CreProperties::class)
|
||||
class SpringConfiguration {
|
||||
class CreConfiguration(private val creProperties: CreProperties) {
|
||||
@Bean
|
||||
fun logger(): Logger = LoggerFactory.getLogger(ColorRecipesExplorerApplication::class.java)
|
||||
fun fileCache(): MemoryCache<String, CachedFileSystemItem> =
|
||||
ExpiringMemoryCache(maxAccessCount = creProperties.fileCacheMaxAccessCount)
|
||||
}
|
|
@ -51,7 +51,7 @@ class MaterialTypeInitializer(
|
|||
// Remove old system types
|
||||
oldSystemTypes.forEach {
|
||||
logger.info("Material type '${it.name}' is not a system type anymore")
|
||||
materialTypeService.update(materialType(it, newSystemType = false))
|
||||
materialTypeService.updateSystemType(it.copy(systemType = false))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,11 +5,13 @@ import kotlin.properties.Delegates.notNull
|
|||
|
||||
const val DEFAULT_DATA_DIRECTORY = "data"
|
||||
const val DEFAULT_CONFIG_DIRECTORY = "config"
|
||||
const val DEFAULT_FILE_CACHE_MAX_ACCESS_COUNT = 10_000L
|
||||
|
||||
@ConfigurationProperties(prefix = "cre.server")
|
||||
class CreProperties {
|
||||
var dataDirectory: String = DEFAULT_DATA_DIRECTORY
|
||||
var configDirectory: String = DEFAULT_CONFIG_DIRECTORY
|
||||
var fileCacheMaxAccessCount: Long = DEFAULT_FILE_CACHE_MAX_ACCESS_COUNT
|
||||
}
|
||||
|
||||
@ConfigurationProperties(prefix = "cre.security")
|
||||
|
|
|
@ -59,6 +59,17 @@ class AlreadyExistsException(
|
|||
extensions = extensions.apply { this[identifierName] = identifierValue }.toMap()
|
||||
)
|
||||
|
||||
class CannotUpdateException(
|
||||
errorCode: String,
|
||||
title: String,
|
||||
details: String
|
||||
) : RestException(
|
||||
errorCode = "cannotupdate-$errorCode",
|
||||
title = title,
|
||||
status = HttpStatus.BAD_REQUEST,
|
||||
details = details
|
||||
)
|
||||
|
||||
class CannotDeleteException(
|
||||
errorCode: String,
|
||||
title: String,
|
||||
|
|
|
@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.model
|
|||
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
|
||||
|
@ -105,6 +106,7 @@ fun materialTypeUpdateDto(
|
|||
private const val MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE = "Material type not found"
|
||||
private const val MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Material type already exists"
|
||||
private const val MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material type"
|
||||
private const val MATERIAL_TYPE_CANNOT_UPDATE_EXCEPTION_TITLE = "Cannot update material type"
|
||||
private const val MATERIAL_TYPE_EXCEPTION_ERROR_CODE = "materialtype"
|
||||
|
||||
fun materialTypeIdNotFoundException(id: Long) =
|
||||
|
@ -150,9 +152,23 @@ fun materialTypePrefixAlreadyExistsException(prefix: String) =
|
|||
"prefix"
|
||||
)
|
||||
|
||||
fun cannotUpdateSystemMaterialTypeException(materialType: MaterialType) =
|
||||
CannotUpdateException(
|
||||
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_TYPE_CANNOT_UPDATE_EXCEPTION_TITLE,
|
||||
"Cannot update material type ${materialType.name} because it is a system material type"
|
||||
)
|
||||
|
||||
fun cannotDeleteMaterialTypeException(materialType: MaterialType) =
|
||||
CannotDeleteException(
|
||||
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE,
|
||||
"Cannot delete material type ${materialType.name} because one or more materials depends on it"
|
||||
)
|
||||
|
||||
fun cannotDeleteSystemMaterialTypeException(materialType: MaterialType) =
|
||||
CannotDeleteException(
|
||||
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE,
|
||||
"Cannot delete material type ${materialType.name} because it is a system material type"
|
||||
)
|
||||
|
|
|
@ -9,6 +9,9 @@ interface MaterialTypeRepository : NamedJpaRepository<MaterialType> {
|
|||
/** Checks if a material type exists with the given [prefix]. */
|
||||
fun existsByPrefix(prefix: String): Boolean
|
||||
|
||||
/** Checks if a system material type with the given [id] exists. */
|
||||
fun existsByIdAndSystemTypeIsTrue(id: Long): Boolean
|
||||
|
||||
/** Gets all material types which are not system types. */
|
||||
fun findAllBySystemTypeIs(value: Boolean): Collection<MaterialType>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import org.springframework.context.annotation.Profile
|
|||
import org.springframework.stereotype.Service
|
||||
|
||||
interface MaterialTypeService :
|
||||
ExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialType, MaterialTypeRepository> {
|
||||
ExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialType, MaterialTypeRepository> {
|
||||
/** Checks if a material type with the given [prefix] exists. */
|
||||
fun existsByPrefix(prefix: String): Boolean
|
||||
|
||||
|
@ -19,14 +19,17 @@ interface MaterialTypeService :
|
|||
|
||||
/** Gets all material types who are not a system type. */
|
||||
fun getAllNonSystemType(): Collection<MaterialType>
|
||||
|
||||
/** Allows to update the given system [materialType], should not be exposed to users. */
|
||||
fun updateSystemType(materialType: MaterialType): MaterialType
|
||||
}
|
||||
|
||||
@Service
|
||||
@Profile("!emergency")
|
||||
class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val materialService: MaterialService) :
|
||||
AbstractExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialType, MaterialTypeRepository>(
|
||||
repository
|
||||
), MaterialTypeService {
|
||||
AbstractExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialType, MaterialTypeRepository>(
|
||||
repository
|
||||
), MaterialTypeService {
|
||||
override fun idNotFoundException(id: Long) = materialTypeIdNotFoundException(id)
|
||||
override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id)
|
||||
override fun nameNotFoundException(name: String) = materialTypeNameNotFoundException(name)
|
||||
|
@ -36,7 +39,7 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma
|
|||
|
||||
override fun existsByPrefix(prefix: String): Boolean = repository.existsByPrefix(prefix)
|
||||
override fun isUsedByMaterial(materialType: MaterialType): Boolean =
|
||||
materialService.existsByMaterialType(materialType)
|
||||
materialService.existsByMaterialType(materialType)
|
||||
|
||||
override fun getAllSystemTypes(): Collection<MaterialType> = repository.findAllBySystemTypeIs(true)
|
||||
override fun getAllNonSystemType(): Collection<MaterialType> = repository.findAllBySystemTypeIs(false)
|
||||
|
@ -52,15 +55,25 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma
|
|||
|
||||
return update(with(entity) {
|
||||
MaterialType(
|
||||
id = id,
|
||||
name = if (isNotNullAndNotBlank(name)) name else persistedMaterialType.name,
|
||||
prefix = if (isNotNullAndNotBlank(prefix)) prefix else persistedMaterialType.prefix,
|
||||
systemType = false
|
||||
id = id,
|
||||
name = if (isNotNullAndNotBlank(name)) name else persistedMaterialType.name,
|
||||
prefix = if (isNotNullAndNotBlank(prefix)) prefix else persistedMaterialType.prefix,
|
||||
systemType = false
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
override fun update(entity: MaterialType): MaterialType {
|
||||
override fun updateSystemType(materialType: MaterialType) =
|
||||
update(materialType, true)
|
||||
|
||||
override fun update(entity: MaterialType) =
|
||||
update(entity, false)
|
||||
|
||||
private fun update(entity: MaterialType, allowSystemTypes: Boolean): MaterialType {
|
||||
if (!allowSystemTypes && repository.existsByIdAndSystemTypeIsTrue(entity.id!!)) {
|
||||
throw cannotUpdateSystemMaterialTypeException(entity)
|
||||
}
|
||||
|
||||
with(repository.findByPrefix(entity.prefix)) {
|
||||
if (this != null && id != entity.id)
|
||||
throw materialTypePrefixAlreadyExistsException(entity.prefix)
|
||||
|
@ -70,7 +83,11 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma
|
|||
}
|
||||
|
||||
override fun delete(entity: MaterialType) {
|
||||
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialTypeException(entity)
|
||||
if (repository.existsByIdAndSystemTypeIsTrue(entity.id!!)) {
|
||||
throw cannotDeleteSystemMaterialTypeException(entity)
|
||||
}
|
||||
|
||||
if (!repository.canBeDeleted(entity.id)) throw cannotDeleteMaterialTypeException(entity)
|
||||
super.delete(entity)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.or
|
||||
|
@ -9,10 +10,8 @@ import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService
|
|||
import dev.fyloz.colorrecipesexplorer.service.users.GroupService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.setAll
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.io.File
|
||||
import java.time.LocalDate
|
||||
import java.time.Period
|
||||
import javax.transaction.Transactional
|
||||
|
@ -45,7 +44,7 @@ interface RecipeService :
|
|||
}
|
||||
|
||||
@Service
|
||||
@Profile("!emergency")
|
||||
@RequireDatabase
|
||||
class RecipeServiceImpl(
|
||||
recipeRepository: RecipeRepository,
|
||||
val companyService: CompanyService,
|
||||
|
@ -213,29 +212,20 @@ interface RecipeImageService {
|
|||
|
||||
/** Deletes the image with the given [name] for the given [recipe]. */
|
||||
fun delete(recipe: Recipe, name: String)
|
||||
|
||||
/** Gets the directory containing all images of the given [Recipe]. */
|
||||
fun Recipe.getDirectory(): File
|
||||
}
|
||||
|
||||
const val RECIPE_IMAGE_ID_DELIMITER = "_"
|
||||
const val RECIPE_IMAGE_EXTENSION = ".jpg"
|
||||
|
||||
@Service
|
||||
@Profile("!emergency")
|
||||
@RequireDatabase
|
||||
class RecipeImageServiceImpl(
|
||||
val fileService: WriteableFileService
|
||||
) : RecipeImageService {
|
||||
override fun getAllImages(recipe: Recipe): Set<String> {
|
||||
val recipeDirectory = recipe.getDirectory()
|
||||
if (!recipeDirectory.exists() || !recipeDirectory.isDirectory) {
|
||||
return setOf()
|
||||
}
|
||||
return recipeDirectory.listFiles()!! // Should never be null because we check if recipeDirectory exists and is a directory before
|
||||
.filterNotNull()
|
||||
override fun getAllImages(recipe: Recipe) =
|
||||
fileService.listDirectoryFiles(recipe.imagesDirectoryPath)
|
||||
.map { it.name }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override fun download(image: MultipartFile, recipe: Recipe): String {
|
||||
/** Gets the next id available for a new image for the given [recipe]. */
|
||||
|
@ -252,17 +242,15 @@ class RecipeImageServiceImpl(
|
|||
} + 1L
|
||||
}
|
||||
|
||||
return getImageFileName(recipe, getNextAvailableId()).apply {
|
||||
fileService.write(image, getImagePath(recipe, this), true)
|
||||
return getImageFileName(recipe, getNextAvailableId()).also {
|
||||
with(getImagePath(recipe, it)) {
|
||||
fileService.writeToDirectory(image, this, recipe.imagesDirectoryPath, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(recipe: Recipe, name: String) =
|
||||
fileService.delete(getImagePath(recipe, name))
|
||||
|
||||
override fun Recipe.getDirectory(): File = File(with(fileService) {
|
||||
this@getDirectory.imagesDirectoryPath.fullPath().path
|
||||
})
|
||||
fileService.deleteFromDirectory(getImagePath(recipe, name), recipe.imagesDirectoryPath)
|
||||
|
||||
private fun getImageFileName(recipe: Recipe, id: Long) =
|
||||
"${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$id"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.JavaFile
|
||||
import dev.fyloz.colorrecipesexplorer.SUPPORTED_DATABASE_VERSION
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||
|
@ -8,14 +9,13 @@ import dev.fyloz.colorrecipesexplorer.model.Configuration
|
|||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
|
||||
import dev.fyloz.colorrecipesexplorer.model.configuration
|
||||
import dev.fyloz.colorrecipesexplorer.repository.ConfigurationRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.create
|
||||
import dev.fyloz.colorrecipesexplorer.utils.create
|
||||
import dev.fyloz.colorrecipesexplorer.utils.excludeAll
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.boot.info.BuildProperties
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Component
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.time.LocalDate
|
||||
|
@ -96,7 +96,7 @@ private class FileConfigurationSource(
|
|||
private val configFilePath: String
|
||||
) : ConfigurationSource {
|
||||
private val properties = Properties().apply {
|
||||
with(File(configFilePath)) {
|
||||
with(JavaFile(configFilePath)) {
|
||||
if (!this.exists()) this.create()
|
||||
FileInputStream(this).use {
|
||||
this@apply.load(it)
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.JavaFile
|
||||
import dev.fyloz.colorrecipesexplorer.utils.File
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import dev.fyloz.memorycache.MemoryCache
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
interface FileCache {
|
||||
/** Checks if the cache contains the given [path]. */
|
||||
operator fun contains(path: FilePath): Boolean
|
||||
|
||||
/** Gets the cached file system item at the given [path]. */
|
||||
operator fun get(path: FilePath): CachedFileSystemItem?
|
||||
|
||||
/** Gets the cached directory at the given [path]. */
|
||||
fun getDirectory(path: FilePath): CachedDirectory?
|
||||
|
||||
/** Gets the cached file at the given [path]. */
|
||||
fun getFile(path: FilePath): CachedFile?
|
||||
|
||||
/** Checks if the cached file system item at the given [path] exists. */
|
||||
fun exists(path: FilePath): Boolean
|
||||
|
||||
/** Checks if the cached directory at the given [path] exists. */
|
||||
fun directoryExists(path: FilePath): Boolean
|
||||
|
||||
/** Checks if the cached file at the given [path] exists. */
|
||||
fun fileExists(path: FilePath): Boolean
|
||||
|
||||
/** Sets the file system item at the given [path] as existing or not. Loads the item in the cache if not already present. */
|
||||
fun setExists(path: FilePath, exists: Boolean = true)
|
||||
|
||||
/** Loads the file system item at the given [path] into the cache. */
|
||||
fun load(path: FilePath)
|
||||
|
||||
/** Adds the file system item at the given [itemPath] to the cached directory at the given [directoryPath]. */
|
||||
fun addItemToDirectory(directoryPath: FilePath, itemPath: FilePath)
|
||||
|
||||
/** Removes the file system item at the given [itemPath] from the cached directory at the given [directoryPath]. */
|
||||
fun removeItemFromDirectory(directoryPath: FilePath, itemPath: FilePath)
|
||||
}
|
||||
|
||||
@Component
|
||||
class DefaultFileCache(private val cache: MemoryCache<String, CachedFileSystemItem>) : FileCache {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
override operator fun contains(path: FilePath) =
|
||||
path.value in cache
|
||||
|
||||
override operator fun get(path: FilePath) =
|
||||
cache[path.value]
|
||||
|
||||
private operator fun set(path: FilePath, item: CachedFileSystemItem) {
|
||||
cache[path.value] = item
|
||||
}
|
||||
|
||||
override fun getDirectory(path: FilePath) =
|
||||
if (directoryExists(path)) {
|
||||
this[path] as CachedDirectory
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getFile(path: FilePath) =
|
||||
if (fileExists(path)) {
|
||||
this[path] as CachedFile
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun exists(path: FilePath) =
|
||||
path in this && this[path]!!.exists
|
||||
|
||||
override fun directoryExists(path: FilePath) =
|
||||
exists(path) && this[path] is CachedDirectory
|
||||
|
||||
override fun fileExists(path: FilePath) =
|
||||
exists(path) && this[path] is CachedFile
|
||||
|
||||
override fun setExists(path: FilePath, exists: Boolean) {
|
||||
if (path !in this) {
|
||||
load(path)
|
||||
}
|
||||
|
||||
this[path] = this[path]!!.clone(exists = exists)
|
||||
logger.debug("Updated FileCache state: ${path.value} exists -> $exists")
|
||||
}
|
||||
|
||||
override fun load(path: FilePath) =
|
||||
with(JavaFile(path.value).toFileSystemItem()) {
|
||||
this@DefaultFileCache[path] = this
|
||||
|
||||
logger.debug("Loaded file at ${path.value} into FileCache")
|
||||
}
|
||||
|
||||
override fun addItemToDirectory(directoryPath: FilePath, itemPath: FilePath) {
|
||||
val directory = prepareDirectory(directoryPath) ?: return
|
||||
|
||||
val updatedContent = setOf(
|
||||
*directory.content.toTypedArray(),
|
||||
JavaFile(itemPath.value).toFileSystemItem()
|
||||
)
|
||||
|
||||
this[directoryPath] = directory.copy(content = updatedContent)
|
||||
logger.debug("Added child ${itemPath.value} to ${directoryPath.value} in FileCache")
|
||||
}
|
||||
|
||||
override fun removeItemFromDirectory(directoryPath: FilePath, itemPath: FilePath) {
|
||||
val directory = prepareDirectory(directoryPath) ?: return
|
||||
|
||||
val updatedContent = directory.content
|
||||
.filter { it.path.value != itemPath.value }
|
||||
.toSet()
|
||||
|
||||
this[directoryPath] = directory.copy(content = updatedContent)
|
||||
logger.debug("Removed child ${itemPath.value} from ${directoryPath.value} in FileCache")
|
||||
}
|
||||
|
||||
private fun prepareDirectory(path: FilePath): CachedDirectory? {
|
||||
if (!directoryExists(path)) {
|
||||
logger.warn("Cannot add child to ${path.value} because it is not in the cache")
|
||||
return null
|
||||
}
|
||||
|
||||
val directory = getDirectory(path)
|
||||
if (directory == null) {
|
||||
logger.warn("Cannot add child to ${path.value} because it is not a directory")
|
||||
return null
|
||||
}
|
||||
|
||||
return directory
|
||||
}
|
||||
}
|
||||
|
||||
interface CachedFileSystemItem {
|
||||
val name: String
|
||||
val path: FilePath
|
||||
val exists: Boolean
|
||||
|
||||
fun clone(exists: Boolean): CachedFileSystemItem
|
||||
}
|
||||
|
||||
data class CachedFile(
|
||||
override val name: String,
|
||||
override val path: FilePath,
|
||||
override val exists: Boolean
|
||||
) : CachedFileSystemItem {
|
||||
constructor(file: File) : this(file.name, file.toFilePath(), file.exists() && file.isFile)
|
||||
|
||||
override fun clone(exists: Boolean) =
|
||||
this.copy(exists = exists)
|
||||
}
|
||||
|
||||
data class CachedDirectory(
|
||||
override val name: String,
|
||||
override val path: FilePath,
|
||||
override val exists: Boolean,
|
||||
val content: Set<CachedFileSystemItem> = setOf()
|
||||
) : CachedFileSystemItem {
|
||||
constructor(file: File) : this(file.name, file.toFilePath(), file.exists() && file.isDirectory, file.fetchContent())
|
||||
|
||||
val contentFiles: Collection<CachedFile>
|
||||
get() = content.filterIsInstance<CachedFile>()
|
||||
|
||||
override fun clone(exists: Boolean) =
|
||||
this.copy(exists = exists)
|
||||
|
||||
companion object {
|
||||
private fun File.fetchContent() =
|
||||
(this.file.listFiles() ?: arrayOf<JavaFile>())
|
||||
.filterNotNull()
|
||||
.map { it.toFileSystemItem() }
|
||||
.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
fun JavaFile.toFileSystemItem() =
|
||||
if (this.isDirectory) {
|
||||
CachedDirectory(File(this))
|
||||
} else {
|
||||
CachedFile(File(this))
|
||||
}
|
|
@ -2,15 +2,17 @@ package dev.fyloz.colorrecipesexplorer.service.files
|
|||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.utils.File
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import dev.fyloz.colorrecipesexplorer.utils.withFileAt
|
||||
import mu.KotlinLogging
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import org.springframework.core.io.Resource
|
||||
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.nio.file.Files
|
||||
|
||||
/** Banned path shards. These are banned because they can allow access to files outside the data directory. */
|
||||
val BANNED_FILE_PATH_SHARDS = setOf(
|
||||
|
@ -26,8 +28,11 @@ interface FileService {
|
|||
/** Reads the file at the given [path]. */
|
||||
fun read(path: String): Resource
|
||||
|
||||
/** List the files contained in the folder at the given [path]. Returns an empty collection if the directory does not exist. */
|
||||
fun listDirectoryFiles(path: String): Collection<CachedFile>
|
||||
|
||||
/** Completes the path of the given [String] by adding the working directory. */
|
||||
fun String.fullPath(): FilePath
|
||||
fun fullPath(path: String): FilePath
|
||||
}
|
||||
|
||||
interface WriteableFileService : FileService {
|
||||
|
@ -40,21 +45,38 @@ interface WriteableFileService : FileService {
|
|||
/** Writes the given [data] to the given [path]. If the file at the path already exists, it will be overwritten if [overwrite] is enabled. */
|
||||
fun write(data: ByteArrayResource, path: String, overwrite: Boolean)
|
||||
|
||||
/** Writes the given [data] to the given [path], and specify the [parentPath]. If the file at the path already exists, it will be overwritten if [overwrite] is enabled. */
|
||||
fun writeToDirectory(data: MultipartFile, path: String, parentPath: String, overwrite: Boolean)
|
||||
|
||||
/** Deletes the file at the given [path]. */
|
||||
fun delete(path: String)
|
||||
|
||||
/** Deletes the file at the given [path], and specify the [parentPath]. */
|
||||
fun deleteFromDirectory(path: String, parentPath: String)
|
||||
}
|
||||
|
||||
@Service
|
||||
class FileServiceImpl(
|
||||
private val creProperties: CreProperties,
|
||||
private val logger: Logger
|
||||
private val fileCache: FileCache,
|
||||
private val creProperties: CreProperties
|
||||
) : WriteableFileService {
|
||||
override fun exists(path: String) = withFileAt(path.fullPath()) {
|
||||
this.exists() && this.isFile
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
override fun exists(path: String): Boolean {
|
||||
val fullPath = fullPath(path)
|
||||
return if (fullPath in fileCache) {
|
||||
fileCache.exists(fullPath)
|
||||
} else {
|
||||
withFileAt(fullPath) {
|
||||
(this.exists() && this.isFile).also {
|
||||
fileCache.setExists(fullPath, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(path: String) = ByteArrayResource(
|
||||
withFileAt(path.fullPath()) {
|
||||
withFileAt(fullPath(path)) {
|
||||
if (!exists(path)) throw FileNotFoundException(path)
|
||||
try {
|
||||
readBytes()
|
||||
|
@ -64,12 +86,25 @@ class FileServiceImpl(
|
|||
}
|
||||
)
|
||||
|
||||
override fun listDirectoryFiles(path: String): Collection<CachedFile> =
|
||||
with(fullPath(path)) {
|
||||
if (this !in fileCache) {
|
||||
fileCache.load(this)
|
||||
}
|
||||
|
||||
(fileCache.getDirectory(this) ?: return setOf())
|
||||
.contentFiles
|
||||
}
|
||||
|
||||
override fun create(path: String) {
|
||||
val fullPath = path.fullPath()
|
||||
val fullPath = fullPath(path)
|
||||
if (!exists(path)) {
|
||||
try {
|
||||
withFileAt(fullPath) {
|
||||
this.create()
|
||||
fileCache.setExists(fullPath)
|
||||
|
||||
logger.info("Created file at '${fullPath.value}'")
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
FileCreateException(path).logAndThrow(ex, logger)
|
||||
|
@ -79,35 +114,52 @@ class FileServiceImpl(
|
|||
|
||||
override fun write(file: MultipartFile, path: String, overwrite: Boolean) =
|
||||
prepareWrite(path, overwrite) {
|
||||
logWrittenDataSize(file.size)
|
||||
file.transferTo(this.toPath())
|
||||
}
|
||||
|
||||
override fun write(data: ByteArrayResource, path: String, overwrite: Boolean) =
|
||||
prepareWrite(path, overwrite) {
|
||||
logWrittenDataSize(data.contentLength())
|
||||
this.writeBytes(data.byteArray)
|
||||
}
|
||||
|
||||
override fun writeToDirectory(data: MultipartFile, path: String, parentPath: String, overwrite: Boolean) {
|
||||
fileCache.addItemToDirectory(fullPath(parentPath), fullPath(path))
|
||||
write(data, path, overwrite)
|
||||
}
|
||||
|
||||
override fun delete(path: String) {
|
||||
try {
|
||||
withFileAt(path.fullPath()) {
|
||||
val fullPath = fullPath(path)
|
||||
withFileAt(fullPath) {
|
||||
if (!exists(path)) throw FileNotFoundException(path)
|
||||
!this.delete()
|
||||
|
||||
this.delete()
|
||||
fileCache.setExists(fullPath, false)
|
||||
|
||||
logger.info("Deleted file at '${fullPath.value}'")
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
FileDeleteException(path).logAndThrow(ex, logger)
|
||||
}
|
||||
}
|
||||
|
||||
override fun String.fullPath(): FilePath {
|
||||
BANNED_FILE_PATH_SHARDS
|
||||
.firstOrNull { this.contains(it) }
|
||||
?.let { throw InvalidFilePathException(this, it) }
|
||||
override fun deleteFromDirectory(path: String, parentPath: String) {
|
||||
fileCache.removeItemFromDirectory(fullPath(parentPath), fullPath(path))
|
||||
delete(path)
|
||||
}
|
||||
|
||||
return FilePath("${creProperties.dataDirectory}/$this")
|
||||
override fun fullPath(path: String): FilePath {
|
||||
BANNED_FILE_PATH_SHARDS
|
||||
.firstOrNull { path.contains(it) }
|
||||
?.let { throw InvalidFilePathException(path, it) }
|
||||
|
||||
return FilePath("${creProperties.dataDirectory}/$path")
|
||||
}
|
||||
|
||||
private fun prepareWrite(path: String, overwrite: Boolean, op: File.() -> Unit) {
|
||||
val fullPath = path.fullPath()
|
||||
val fullPath = fullPath(path)
|
||||
|
||||
if (exists(path)) {
|
||||
if (!overwrite) throw FileExistsException(path)
|
||||
|
@ -118,26 +170,17 @@ class FileServiceImpl(
|
|||
try {
|
||||
withFileAt(fullPath) {
|
||||
this.op()
|
||||
|
||||
logger.info("Wrote data to file at '${fullPath.value}'")
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
FileWriteException(path).logAndThrow(ex, logger)
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 fun logWrittenDataSize(size: Long) {
|
||||
logger.debug("Writing $size bytes to file system...")
|
||||
}
|
||||
}
|
||||
|
||||
private const val FILE_IO_EXCEPTION_TITLE = "File IO error"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import org.springframework.core.io.Resource
|
||||
import org.springframework.core.io.ResourceLoader
|
||||
import org.springframework.stereotype.Service
|
||||
|
@ -9,18 +10,27 @@ class ResourceFileService(
|
|||
private val resourceLoader: ResourceLoader
|
||||
) : FileService {
|
||||
override fun exists(path: String) =
|
||||
path.fullPath().resource.exists()
|
||||
fullPath(path).resource.exists()
|
||||
|
||||
override fun read(path: String): Resource =
|
||||
path.fullPath().resource.also {
|
||||
fullPath(path).resource.also {
|
||||
if (!it.exists()) {
|
||||
throw FileNotFoundException(path)
|
||||
}
|
||||
}
|
||||
|
||||
override fun String.fullPath() =
|
||||
FilePath("classpath:${this}")
|
||||
override fun listDirectoryFiles(path: String): Collection<CachedFile> {
|
||||
val content = fullPath(path).resource.file.listFiles() ?: return setOf()
|
||||
|
||||
return content
|
||||
.filterNotNull()
|
||||
.filter { it.isFile }
|
||||
.map { it.toFileSystemItem() as CachedFile }
|
||||
}
|
||||
|
||||
override fun fullPath(path: String) =
|
||||
FilePath("classpath:${path}")
|
||||
|
||||
val FilePath.resource: Resource
|
||||
get() = resourceLoader.getResource(this.path)
|
||||
get() = resourceLoader.getResource(this.value)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package dev.fyloz.colorrecipesexplorer.utils
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.JavaFile
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
/** Mockable file wrapper, to prevent issues when mocking [java.io.File]. */
|
||||
class File(val file: JavaFile) {
|
||||
val name: String
|
||||
get() = file.name
|
||||
|
||||
val isFile: Boolean
|
||||
get() = file.isFile
|
||||
|
||||
val isDirectory: Boolean
|
||||
get() = file.isDirectory
|
||||
|
||||
fun toPath(): Path =
|
||||
file.toPath()
|
||||
|
||||
fun toFilePath(): FilePath =
|
||||
FilePath(file.path)
|
||||
|
||||
fun exists() =
|
||||
file.exists()
|
||||
|
||||
fun readBytes() =
|
||||
file.readBytes()
|
||||
|
||||
fun writeBytes(array: ByteArray) =
|
||||
file.writeBytes(array)
|
||||
|
||||
fun create() =
|
||||
file.create()
|
||||
|
||||
fun delete(): Boolean =
|
||||
file.delete()
|
||||
|
||||
companion object {
|
||||
fun from(path: String) =
|
||||
File(JavaFile(path))
|
||||
|
||||
fun from(path: FilePath) =
|
||||
from(path.value)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move to value class when mocking them with mockk works
|
||||
data class FilePath(val value: String)
|
||||
|
||||
/** Runs the given [block] in the context of a file with the given [fullPath]. */
|
||||
fun <T> withFileAt(fullPath: FilePath, block: File.() -> T) =
|
||||
File.from(fullPath).block()
|
||||
|
||||
/** Shortcut to create a file and its parent directories. */
|
||||
fun JavaFile.create() {
|
||||
Files.createDirectories(this.parentFile.toPath())
|
||||
Files.createFile(this.toPath())
|
||||
}
|
|
@ -5,7 +5,6 @@ cre.server.data-directory=data
|
|||
cre.server.config-directory=config
|
||||
cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE78WWOgl8InrbwBgQsMy0
|
||||
cre.security.jwt-duration=18000000
|
||||
cre.security.aes-secret=blabla
|
||||
# Root user
|
||||
cre.security.root.id=9999
|
||||
cre.security.root.password=password
|
||||
|
|
|
@ -2,6 +2,8 @@ package dev.fyloz.colorrecipesexplorer.service
|
|||
|
||||
import com.nhaarman.mockitokotlin2.*
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository
|
||||
|
@ -164,8 +166,22 @@ class MaterialTypeServiceTest :
|
|||
.assertErrorCode("prefix")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `update() throws CannotUpdateException when updating a system material type`() {
|
||||
whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true)
|
||||
|
||||
assertThrows<CannotUpdateException> { service.update(systemType) }
|
||||
}
|
||||
|
||||
// delete()
|
||||
|
||||
@Test
|
||||
fun `delete() throws CannotDeleteException when deleting a system material type`() {
|
||||
whenever(repository.existsByIdAndSystemTypeIsTrue(systemType.id!!)).doReturn(true)
|
||||
|
||||
assertThrows<CannotDeleteException> { service.delete(systemType) }
|
||||
}
|
||||
|
||||
override fun `delete() deletes in the repository`() {
|
||||
whenCanBeDeleted {
|
||||
super.`delete() deletes in the repository`()
|
||||
|
|
|
@ -6,8 +6,10 @@ 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 dev.fyloz.colorrecipesexplorer.service.files.CachedFile
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.WriteableFileService
|
||||
import dev.fyloz.colorrecipesexplorer.service.users.GroupService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -15,7 +17,6 @@ import org.junit.jupiter.api.TestInstance
|
|||
import org.junit.jupiter.api.assertThrows
|
||||
import org.springframework.mock.web.MockMultipartFile
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.io.File
|
||||
import java.time.LocalDate
|
||||
import java.time.Period
|
||||
import kotlin.test.*
|
||||
|
@ -30,7 +31,17 @@ class RecipeServiceTest :
|
|||
private val recipeStepService: RecipeStepService = mock()
|
||||
private val configService: ConfigurationService = mock()
|
||||
override val service: RecipeService =
|
||||
spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService, mock(), configService))
|
||||
spy(
|
||||
RecipeServiceImpl(
|
||||
repository,
|
||||
companyService,
|
||||
mixService,
|
||||
recipeStepService,
|
||||
groupService,
|
||||
mock(),
|
||||
configService
|
||||
)
|
||||
)
|
||||
|
||||
private val company: Company = company(id = 0L)
|
||||
override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company)
|
||||
|
@ -273,18 +284,7 @@ private class RecipeImageServiceTestContext {
|
|||
val recipe = spyk(recipe())
|
||||
val recipeImagesIds = setOf(1L, 10L, 21L)
|
||||
val recipeImagesNames = recipeImagesIds.map { it.imageName }.toSet()
|
||||
val recipeImagesFiles = recipeImagesNames.map { File(it) }.toTypedArray()
|
||||
val recipeDirectory = mockk<File> {
|
||||
every { exists() } returns true
|
||||
every { isDirectory } returns true
|
||||
every { listFiles() } returns recipeImagesFiles
|
||||
}
|
||||
|
||||
init {
|
||||
with(recipeImageService) {
|
||||
every { recipe.getDirectory() } returns recipeDirectory
|
||||
}
|
||||
}
|
||||
val recipeImagesFiles = recipeImagesNames.map { CachedFile(it, FilePath(it), true) }
|
||||
|
||||
val Long.imageName
|
||||
get() = "${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$this"
|
||||
|
@ -308,6 +308,8 @@ class RecipeImageServiceTest {
|
|||
@Test
|
||||
fun `getAllImages() returns a Set containing the name of every files in the recipe's directory`() {
|
||||
test {
|
||||
every { fileService.listDirectoryFiles(any()) } returns recipeImagesFiles
|
||||
|
||||
val foundImagesNames = recipeImageService.getAllImages(recipe)
|
||||
|
||||
assertEquals(recipeImagesNames, foundImagesNames)
|
||||
|
@ -317,7 +319,7 @@ class RecipeImageServiceTest {
|
|||
@Test
|
||||
fun `getAllImages() returns an empty Set when the recipe's directory does not exists`() {
|
||||
test {
|
||||
every { recipeDirectory.exists() } returns false
|
||||
every { fileService.listDirectoryFiles(any()) } returns emptySet()
|
||||
|
||||
assertTrue {
|
||||
recipeImageService.getAllImages(recipe).isEmpty()
|
||||
|
@ -335,12 +337,15 @@ class RecipeImageServiceTest {
|
|||
val expectedImageName = expectedImageId.imageName
|
||||
val expectedImagePath = expectedImageName.imagePath
|
||||
|
||||
every { fileService.listDirectoryFiles(any()) } returns recipeImagesFiles
|
||||
every { fileService.writeToDirectory(any(), any(), any(), any()) } just runs
|
||||
|
||||
val foundImageName = recipeImageService.download(mockImage, recipe)
|
||||
|
||||
assertEquals(expectedImageName, foundImageName)
|
||||
|
||||
verify {
|
||||
fileService.write(mockImage, expectedImagePath, true)
|
||||
fileService.writeToDirectory(mockImage, expectedImagePath, any(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -353,10 +358,12 @@ class RecipeImageServiceTest {
|
|||
val imageName = recipeImagesIds.first().imageName
|
||||
val imagePath = imageName.imagePath
|
||||
|
||||
every { fileService.deleteFromDirectory(any(), any()) } just runs
|
||||
|
||||
recipeImageService.delete(recipe, imageName)
|
||||
|
||||
verify {
|
||||
fileService.delete(imagePath)
|
||||
fileService.deleteFromDirectory(imagePath, any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,426 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import dev.fyloz.memorycache.MemoryCache
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class DefaultFileCacheTest {
|
||||
private val memoryCacheMock = mockk<MemoryCache<String, CachedFileSystemItem>>()
|
||||
|
||||
private val fileCache = spyk(DefaultFileCache(memoryCacheMock))
|
||||
|
||||
private val path = FilePath("unit_test_path")
|
||||
private val cachedFile = CachedFile("unit_test_file", path, true)
|
||||
private val cachedDirectory = CachedDirectory("unit_test_dictionary", path, true)
|
||||
|
||||
@AfterEach
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
private fun setup_memoryCacheMock_set() {
|
||||
every { memoryCacheMock[any()] = any() } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun contains_normalBehavior_returnsTrue() {
|
||||
// Arrange
|
||||
every { any() in memoryCacheMock} returns true
|
||||
|
||||
// Act
|
||||
val contains = path in fileCache
|
||||
|
||||
// Assert
|
||||
assertTrue(contains)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun contains_pathNotCached_returnsFalse() {
|
||||
// Arrange
|
||||
every { any() in memoryCacheMock} returns false
|
||||
|
||||
// Act
|
||||
val contains = path in fileCache
|
||||
|
||||
// Assert
|
||||
assertFalse(contains)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun get_normalBehavior_returnsCachedItem() {
|
||||
// Arrange
|
||||
every { memoryCacheMock[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val item = fileCache[path]
|
||||
|
||||
// Assert
|
||||
assertEquals(cachedFile, item)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun get_pathNotCached_returnsNull() {
|
||||
// Arrange
|
||||
every { memoryCacheMock[any()] } returns null
|
||||
|
||||
// Act
|
||||
val item = fileCache[path]
|
||||
|
||||
// Assert
|
||||
assertNull(item)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDirectory_normalBehavior_returnsCachedDirectory() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(any()) } returns true
|
||||
every { fileCache[any()] } returns cachedDirectory
|
||||
|
||||
// Act
|
||||
val directory = fileCache.getDirectory(path)
|
||||
|
||||
// Assert
|
||||
assertEquals(cachedDirectory, directory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getDirectory_directoryDoesNotExists_returnsNull() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(any()) } returns false
|
||||
every { fileCache[any()] } returns cachedDirectory
|
||||
|
||||
// Act
|
||||
val directory = fileCache.getDirectory(path)
|
||||
|
||||
// Assert
|
||||
assertNull(directory)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFile_normalBehavior_returnsCachedFile() {
|
||||
// Arrange
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
every { fileCache.fileExists(any()) } returns true
|
||||
|
||||
// Act
|
||||
val file = fileCache.getFile(path)
|
||||
|
||||
// Assert
|
||||
assertEquals(cachedFile, file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFile_fileDoesNotExists_returnsNull() {
|
||||
// Arrange
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
every { fileCache.fileExists(any()) } returns false
|
||||
|
||||
// Act
|
||||
val file = fileCache.getFile(path)
|
||||
|
||||
// Assert
|
||||
assertNull(file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exists_normalBehavior_returnsTrue() {
|
||||
// Arrange
|
||||
every { any() in fileCache } returns true
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val exists = fileCache.exists(path)
|
||||
|
||||
// Assert
|
||||
assertTrue(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exists_pathNotCached_returnsFalse() {
|
||||
// Arrange
|
||||
every { any() in fileCache } returns false
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val exists = fileCache.exists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exists_itemDoesNotExists_returnsFalse() {
|
||||
// Arrange
|
||||
val file = cachedFile.copy(exists = false)
|
||||
|
||||
every { any() in fileCache } returns true
|
||||
every { fileCache[any()] } returns file
|
||||
|
||||
// Act
|
||||
val exists = fileCache.exists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun directoryExists_normalBehavior_returnsTrue() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns true
|
||||
every { fileCache[any()] } returns cachedDirectory
|
||||
|
||||
// Act
|
||||
val exists = fileCache.directoryExists(path)
|
||||
|
||||
// Assert
|
||||
assertTrue(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun directoryExists_pathNotCached_returnsFalse() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns false
|
||||
every { fileCache[any()] } returns cachedDirectory
|
||||
|
||||
// Act
|
||||
val exists = fileCache.directoryExists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun directoryExists_cachedItemIsNotDirectory_returnsFalse() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns true
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val exists = fileCache.directoryExists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fileExists_normalBehavior_returnsTrue() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns true
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val exists = fileCache.fileExists(path)
|
||||
|
||||
// Assert
|
||||
assertTrue(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fileExists_pathNotCached_returnsFalse() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns false
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
// Act
|
||||
val exists = fileCache.fileExists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fileExists_cachedItemIsNotFile_returnsFalse() {
|
||||
// Arrange
|
||||
every { fileCache.exists(any()) } returns true
|
||||
every { fileCache[any()] } returns cachedDirectory
|
||||
|
||||
// Act
|
||||
val exists = fileCache.fileExists(path)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setExists_normalBehavior_callsSetInCache() {
|
||||
// Arrange
|
||||
every { any() in fileCache } returns true
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val shouldExists = !cachedFile.exists
|
||||
|
||||
// Act
|
||||
fileCache.setExists(path, exists = shouldExists)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
memoryCacheMock[path.value] = match { it.exists == shouldExists }
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setExists_pathNotCached_callsLoadPath() {
|
||||
// Arrange
|
||||
every { any() in fileCache } returns false
|
||||
every { fileCache[any()] } returns cachedFile
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
// Act
|
||||
fileCache.setExists(path, exists = true)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
fileCache.load(path)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun load_normalBehavior_callsSetInCache() {
|
||||
// Arrange
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
// Act
|
||||
fileCache.load(path)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
memoryCacheMock[path.value] = match { it.path == path }
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addItemToDirectory_normalBehavior_addsItemToDirectoryContent() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(path) } returns true
|
||||
every { fileCache.getDirectory(path) } returns cachedDirectory
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
|
||||
// Act
|
||||
fileCache.addItemToDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
memoryCacheMock[path.value] = match<CachedDirectory> {
|
||||
it.content.any { item -> item.path == itemPath }
|
||||
}
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addItemToDirectory_directoryDoesNotExists_doesNothing() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(path) } returns false
|
||||
every { fileCache.getDirectory(path) } returns cachedDirectory
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
|
||||
// Act
|
||||
fileCache.addItemToDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) {
|
||||
memoryCacheMock[path.value] = any()
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addItemToDirectory_notADirectory_doesNothing() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(path) } returns true
|
||||
every { fileCache.getDirectory(path) } returns null
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
|
||||
// Act
|
||||
fileCache.addItemToDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) {
|
||||
memoryCacheMock[path.value] = any()
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeItemFromDirectory_normalBehavior_removesItemFromDirectoryContent() {
|
||||
// Arrange
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
val file = cachedFile.copy(path = itemPath)
|
||||
val directory = cachedDirectory.copy(content = setOf(file))
|
||||
|
||||
every { fileCache.directoryExists(path) } returns true
|
||||
every { fileCache.getDirectory(path) } returns directory
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
// Act
|
||||
fileCache.removeItemFromDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
memoryCacheMock[path.value] = match<CachedDirectory> { it.content.isEmpty() }
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeItemFromDirectory_directoryDoesNotExists_doesNothing() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(path) } returns false
|
||||
every { fileCache.getDirectory(path) } returns cachedDirectory
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
|
||||
// Act
|
||||
fileCache.removeItemFromDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) {
|
||||
memoryCacheMock[path.value] = any()
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeItemFromDirectory_notADirectory_doesNothing() {
|
||||
// Arrange
|
||||
every { fileCache.directoryExists(path) } returns true
|
||||
every { fileCache.getDirectory(path) } returns null
|
||||
|
||||
setup_memoryCacheMock_set()
|
||||
|
||||
val itemPath = FilePath("${path.value}/item")
|
||||
|
||||
// Act
|
||||
fileCache.removeItemFromDirectory(path, itemPath)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) {
|
||||
memoryCacheMock[path.value] = any()
|
||||
}
|
||||
confirmVerified(memoryCacheMock)
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.utils.File
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.springframework.mock.web.MockMultipartFile
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
import kotlin.test.assertEquals
|
||||
|
@ -20,56 +21,121 @@ private const val mockFilePath = "existingFile"
|
|||
private val mockFilePathPath = Path.of(mockFilePath)
|
||||
private val mockFileData = byteArrayOf(0x1, 0x8, 0xa, 0xf)
|
||||
|
||||
private class FileServiceTestContext {
|
||||
val fileService = spyk(FileServiceImpl(creProperties, mockk {
|
||||
every { error(any(), any<Exception>()) } just Runs
|
||||
}))
|
||||
val mockFile = mockk<File> {
|
||||
every { path } returns mockFilePath
|
||||
class FileServiceTest {
|
||||
private val fileCacheMock = mockk<FileCache> {
|
||||
every { setExists(any(), any()) } just runs
|
||||
}
|
||||
private val fileService = spyk(FileServiceImpl(fileCacheMock, creProperties))
|
||||
|
||||
private val mockFile = mockk<File> {
|
||||
every { file } returns mockk()
|
||||
every { exists() } returns true
|
||||
every { isFile } returns true
|
||||
every { toPath() } returns mockFilePathPath
|
||||
}
|
||||
val mockFileFullPath = spyk(FilePath("${creProperties.dataDirectory}/$mockFilePath")) {
|
||||
every { file } returns mockFile
|
||||
private val mockMultipartFile = spyk(MockMultipartFile(mockFilePath, mockFileData))
|
||||
|
||||
with(fileService) {
|
||||
every { mockFilePath.fullPath() } returns this@spyk
|
||||
}
|
||||
@BeforeEach
|
||||
internal fun beforeEach() {
|
||||
mockkObject(File.Companion)
|
||||
every { File.from(any<String>()) } returns mockFile
|
||||
}
|
||||
val mockMultipartFile = spyk(MockMultipartFile(mockFilePath, mockFileData))
|
||||
}
|
||||
|
||||
class FileServiceTest {
|
||||
@AfterEach
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
private fun whenFileCached(cached: Boolean = true, test: () -> Unit) {
|
||||
every { fileCacheMock.contains(any()) } returns cached
|
||||
|
||||
test()
|
||||
}
|
||||
|
||||
private fun whenFileNotCached(test: () -> Unit) {
|
||||
whenFileCached(false, test)
|
||||
}
|
||||
|
||||
private fun whenMockFilePathExists(exists: Boolean = true, test: () -> Unit) {
|
||||
every { fileService.exists(mockFilePath) } returns exists
|
||||
test()
|
||||
}
|
||||
|
||||
// exists()
|
||||
|
||||
@Test
|
||||
fun `exists() returns true when the file at the given path exists and is a file`() {
|
||||
test {
|
||||
whenFileNotCached {
|
||||
assertTrue { fileService.exists(mockFilePath) }
|
||||
|
||||
verify {
|
||||
mockFile.exists()
|
||||
mockFile.isFile
|
||||
}
|
||||
confirmVerified(mockFile)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exists() returns false when the file at the given path does not exist`() {
|
||||
test {
|
||||
whenFileNotCached {
|
||||
every { mockFile.exists() } returns false
|
||||
|
||||
assertFalse { fileService.exists(mockFilePath) }
|
||||
|
||||
verify {
|
||||
mockFile.exists()
|
||||
}
|
||||
confirmVerified(mockFile)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exists() returns false when the file at the given path is not a file`() {
|
||||
test {
|
||||
whenFileNotCached {
|
||||
every { mockFile.isFile } returns false
|
||||
|
||||
assertFalse { fileService.exists(mockFilePath) }
|
||||
|
||||
verify {
|
||||
mockFile.exists()
|
||||
mockFile.isFile
|
||||
}
|
||||
confirmVerified(mockFile)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exists() returns true when the file at the given path is cached as existing`() {
|
||||
whenFileCached {
|
||||
every { fileCacheMock.exists(any()) } returns true
|
||||
|
||||
assertTrue { fileService.exists(mockFilePath) }
|
||||
|
||||
verify {
|
||||
fileCacheMock.contains(any())
|
||||
fileCacheMock.exists(any())
|
||||
|
||||
mockFile wasNot called
|
||||
}
|
||||
confirmVerified(fileCacheMock, mockFile)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exists() returns false when the file at the given path is cached as not existing`() {
|
||||
whenFileCached {
|
||||
every { fileCacheMock.exists(any()) } returns false
|
||||
|
||||
assertFalse { fileService.exists(mockFilePath) }
|
||||
|
||||
verify {
|
||||
fileCacheMock.contains(any())
|
||||
fileCacheMock.exists(any())
|
||||
|
||||
mockFile wasNot called
|
||||
}
|
||||
confirmVerified(fileCacheMock, mockFile)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,39 +143,33 @@ class FileServiceTest {
|
|||
|
||||
@Test
|
||||
fun `read() returns a valid ByteArrayResource`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
mockkStatic(File::readBytes)
|
||||
every { mockFile.readBytes() } returns mockFileData
|
||||
whenMockFilePathExists {
|
||||
mockkStatic(File::readBytes)
|
||||
every { mockFile.readBytes() } returns mockFileData
|
||||
|
||||
val redResource = fileService.read(mockFilePath)
|
||||
val redResource = fileService.read(mockFilePath)
|
||||
|
||||
assertEquals(mockFileData, redResource.byteArray)
|
||||
}
|
||||
assertEquals(mockFileData, redResource.byteArray)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `read() throws FileNotFoundException when no file exists at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
with(assertThrows<FileNotFoundException> { fileService.read(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
whenMockFilePathExists(false) {
|
||||
with(assertThrows<FileNotFoundException> { fileService.read(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `read() throws FileReadException when an IOException is thrown`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
mockkStatic(File::readBytes)
|
||||
every { mockFile.readBytes() } throws IOException()
|
||||
whenMockFilePathExists {
|
||||
mockkStatic(File::readBytes)
|
||||
every { mockFile.readBytes() } throws IOException()
|
||||
|
||||
with(assertThrows<FileReadException> { fileService.read(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
with(assertThrows<FileReadException> { fileService.read(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,15 +178,19 @@ class FileServiceTest {
|
|||
|
||||
@Test
|
||||
fun `create() creates a file at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
mockkStatic(File::create)
|
||||
every { mockFile.create() } just Runs
|
||||
whenMockFilePathExists(false) {
|
||||
whenFileNotCached {
|
||||
mockkStatic(File::create) {
|
||||
every { mockFile.create() } just runs
|
||||
|
||||
fileService.create(mockFilePath)
|
||||
fileService.create(mockFilePath)
|
||||
|
||||
verify {
|
||||
mockFile.create()
|
||||
verify {
|
||||
mockFile.create()
|
||||
|
||||
fileCacheMock.setExists(any())
|
||||
}
|
||||
confirmVerified(mockFile, fileCacheMock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -134,27 +198,23 @@ class FileServiceTest {
|
|||
|
||||
@Test
|
||||
fun `create() does nothing when a file already exists at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
fileService.create(mockFilePath)
|
||||
whenMockFilePathExists {
|
||||
fileService.create(mockFilePath)
|
||||
|
||||
verify(exactly = 0) {
|
||||
mockFile.create()
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
mockFile.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create() throws FileCreateException when the file creation throws an IOException`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
mockkStatic(File::create)
|
||||
every { mockFile.create() } throws IOException()
|
||||
whenMockFilePathExists(false) {
|
||||
mockkStatic(File::create)
|
||||
every { mockFile.create() } throws IOException()
|
||||
|
||||
with(assertThrows<FileCreateException> { fileService.create(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
with(assertThrows<FileCreateException> { fileService.create(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -163,59 +223,51 @@ class FileServiceTest {
|
|||
|
||||
@Test
|
||||
fun `write() creates and writes the given MultipartFile to the file at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
every { fileService.create(mockFilePath) } just Runs
|
||||
every { mockMultipartFile.transferTo(mockFilePathPath) } just Runs
|
||||
whenMockFilePathExists(false) {
|
||||
every { fileService.create(mockFilePath) } just runs
|
||||
every { mockMultipartFile.transferTo(mockFilePathPath) } just runs
|
||||
|
||||
fileService.write(mockMultipartFile, mockFilePath, false)
|
||||
fileService.write(mockMultipartFile, mockFilePath, false)
|
||||
|
||||
verify {
|
||||
fileService.create(mockFilePath)
|
||||
mockMultipartFile.transferTo(mockFilePathPath)
|
||||
}
|
||||
verify {
|
||||
fileService.create(mockFilePath)
|
||||
mockMultipartFile.transferTo(mockFilePathPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `write() throws FileExistsException when a file at the given path already exists and overwrite is disabled`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
with(assertThrows<FileExistsException> { fileService.write(mockMultipartFile, mockFilePath, false) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
whenMockFilePathExists {
|
||||
with(assertThrows<FileExistsException> { fileService.write(mockMultipartFile, mockFilePath, false) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `write() writes the given MultipartFile to an existing file when overwrite is enabled`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
every { mockMultipartFile.transferTo(mockFilePathPath) } just Runs
|
||||
whenMockFilePathExists {
|
||||
every { mockMultipartFile.transferTo(mockFilePathPath) } just runs
|
||||
|
||||
fileService.write(mockMultipartFile, mockFilePath, true)
|
||||
fileService.write(mockMultipartFile, mockFilePath, true)
|
||||
|
||||
verify {
|
||||
mockMultipartFile.transferTo(mockFilePathPath)
|
||||
}
|
||||
verify {
|
||||
mockMultipartFile.transferTo(mockFilePathPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `write() throws FileWriteException when writing the given file throws an IOException`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
every { fileService.create(mockFilePath) } just Runs
|
||||
every { mockMultipartFile.transferTo(mockFilePathPath) } throws IOException()
|
||||
whenMockFilePathExists(false) {
|
||||
every { fileService.create(mockFilePath) } just runs
|
||||
every { mockMultipartFile.transferTo(mockFilePathPath) } throws IOException()
|
||||
|
||||
with(assertThrows<FileWriteException> {
|
||||
fileService.write(mockMultipartFile, mockFilePath, false)
|
||||
}) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
with(assertThrows<FileWriteException> {
|
||||
fileService.write(mockMultipartFile, mockFilePath, false)
|
||||
}) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -224,35 +276,38 @@ class FileServiceTest {
|
|||
|
||||
@Test
|
||||
fun `delete() deletes the file at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
whenMockFilePathExists {
|
||||
whenFileCached {
|
||||
every { mockFile.delete() } returns true
|
||||
|
||||
fileService.delete(mockFilePath)
|
||||
|
||||
verify {
|
||||
mockFile.delete()
|
||||
|
||||
fileCacheMock.setExists(any(), false)
|
||||
}
|
||||
confirmVerified(mockFile, fileCacheMock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete() throws FileNotFoundException when no file exists at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
with(assertThrows<FileNotFoundException> { fileService.delete(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
whenMockFilePathExists(false) {
|
||||
with(assertThrows<FileNotFoundException> { fileService.delete(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete() throws FileDeleteException when deleting throw and IOException`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
every { mockFile.delete() } throws IOException()
|
||||
whenMockFilePathExists {
|
||||
every { mockFile.delete() } throws IOException()
|
||||
|
||||
with(assertThrows<FileDeleteException> { fileService.delete(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
with(assertThrows<FileDeleteException> { fileService.delete(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -261,37 +316,24 @@ class FileServiceTest {
|
|||
|
||||
@Test
|
||||
fun `fullPath() appends the given path to the given working directory`() {
|
||||
test {
|
||||
with(fileService) {
|
||||
val fullFilePath = mockFilePath.fullPath()
|
||||
with(fileService) {
|
||||
val fullFilePath = fullPath(mockFilePath)
|
||||
|
||||
assertEquals("${creProperties.dataDirectory}/$mockFilePath", fullFilePath.path)
|
||||
}
|
||||
assertEquals("${creProperties.dataDirectory}/$mockFilePath", fullFilePath.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fullPath() throws InvalidFilePathException when the given path contains invalid fragments`() {
|
||||
test {
|
||||
with(fileService) {
|
||||
BANNED_FILE_PATH_SHARDS.forEach {
|
||||
val maliciousPath = "$it/$mockFilePath"
|
||||
with(fileService) {
|
||||
BANNED_FILE_PATH_SHARDS.forEach {
|
||||
val maliciousPath = "$it/$mockFilePath"
|
||||
|
||||
with(assertThrows<InvalidFilePathException> { maliciousPath.fullPath() }) {
|
||||
assertEquals(maliciousPath, this.path)
|
||||
assertEquals(it, this.fragment)
|
||||
}
|
||||
with(assertThrows<InvalidFilePathException> { fullPath(maliciousPath) }) {
|
||||
assertEquals(maliciousPath, this.path)
|
||||
assertEquals(it, this.fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun test(test: FileServiceTestContext.() -> Unit) {
|
||||
FileServiceTestContext().test()
|
||||
}
|
||||
|
||||
private fun FileServiceTestContext.whenMockFilePathExists(exists: Boolean = true, test: () -> Unit) {
|
||||
every { fileService.exists(mockFilePath) } returns exists
|
||||
test()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
@ -26,7 +27,7 @@ class ResourceFileServiceTest {
|
|||
private fun existsTest(shouldExists: Boolean, test: (String) -> Unit) {
|
||||
val path = "unit_test_resource"
|
||||
with(service) {
|
||||
every { path.fullPath() } returns mockk {
|
||||
every { fullPath(path) } returns mockk {
|
||||
every { resource } returns mockk {
|
||||
every { exists() } returns shouldExists
|
||||
}
|
||||
|
@ -60,7 +61,7 @@ class ResourceFileServiceTest {
|
|||
}
|
||||
val path = "unit_test_path"
|
||||
with(service) {
|
||||
every { path.fullPath() } returns mockk {
|
||||
every { fullPath(path) } returns mockk {
|
||||
every { resource } returns mockResource
|
||||
}
|
||||
|
||||
|
@ -91,11 +92,9 @@ class ResourceFileServiceTest {
|
|||
val path = "unit_test_path"
|
||||
val expectedPath = "classpath:$path"
|
||||
|
||||
with(service) {
|
||||
val found = path.fullPath()
|
||||
val found = service.fullPath(path)
|
||||
|
||||
assertEquals(expectedPath, found.path)
|
||||
}
|
||||
assertEquals(expectedPath, found.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -103,7 +102,7 @@ class ResourceFileServiceTest {
|
|||
val filePath = FilePath("classpath:unit_test_path")
|
||||
val resource = mockk<Resource>()
|
||||
|
||||
every { resourceLoader.getResource(filePath.path) } returns resource
|
||||
every { resourceLoader.getResource(filePath.value) } returns resource
|
||||
|
||||
with(service) {
|
||||
val found = filePath.resource
|
||||
|
|
Loading…
Reference in New Issue