Merge pull request 'feature/18-add-existing-files-cache' (#26) from feature/18-add-existing-files-cache into develop
continuous-integration/drone/push Build was killed
Details
continuous-integration/drone/push Build was killed
Details
Reviewed-on: #26
This commit is contained in:
commit
5409bc8861
|
@ -2,14 +2,14 @@
|
||||||
global-variables:
|
global-variables:
|
||||||
release: &release ${DRONE_TAG}
|
release: &release ${DRONE_TAG}
|
||||||
environment: &environment
|
environment: &environment
|
||||||
JAVA_VERSION: 11
|
JAVA_VERSION: 17
|
||||||
GRADLE_VERSION: 7.1
|
GRADLE_VERSION: 7.3
|
||||||
CRE_VERSION: dev-${DRONE_BUILD_NUMBER}
|
CRE_VERSION: dev-${DRONE_BUILD_NUMBER}
|
||||||
CRE_ARTIFACT_NAME: ColorRecipesExplorer
|
CRE_ARTIFACT_NAME: ColorRecipesExplorer
|
||||||
CRE_REGISTRY_IMAGE: registry.fyloz.dev/colorrecipesexplorer/backend
|
CRE_REGISTRY_IMAGE: registry.fyloz.dev/colorrecipesexplorer/backend
|
||||||
CRE_PORT: 9101
|
CRE_PORT: 9101
|
||||||
CRE_RELEASE: *release
|
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
|
alpine-image: &alpine-image alpine:latest
|
||||||
docker-registry: &docker-registry registry.fyloz.dev
|
docker-registry: &docker-registry registry.fyloz.dev
|
||||||
docker-registry-repo: &docker-registry-repo registry.fyloz.dev/colorrecipesexplorer/backend
|
docker-registry-repo: &docker-registry-repo registry.fyloz.dev/colorrecipesexplorer/backend
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
ARG GRADLE_VERSION=7.1
|
ARG GRADLE_VERSION=7.3
|
||||||
ARG JAVA_VERSION=11
|
ARG JAVA_VERSION=17
|
||||||
|
|
||||||
FROM gradle:$GRADLE_VERSION-jdk$JAVA_VERSION AS build
|
FROM gradle:$GRADLE_VERSION-jdk$JAVA_VERSION AS build
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
|
@ -31,6 +31,7 @@ dependencies {
|
||||||
|
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
||||||
implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.2.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.github.microutils:kotlin-logging-jvm:2.1.21")
|
||||||
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
|
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
|
||||||
implementation("io.jsonwebtoken:jjwt-impl:0.11.2")
|
implementation("io.jsonwebtoken:jjwt-impl:0.11.2")
|
||||||
|
@ -50,7 +51,7 @@ dependencies {
|
||||||
implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
|
implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
|
||||||
|
|
||||||
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
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.jetbrains.kotlin:kotlin-test:${kotlinVersion}")
|
||||||
testImplementation("org.mockito:mockito-inline:3.11.2")
|
testImplementation("org.mockito:mockito-inline:3.11.2")
|
||||||
testImplementation("org.springframework:spring-test:5.3.13")
|
testImplementation("org.springframework:spring-test:5.3.13")
|
||||||
|
@ -61,6 +62,8 @@ dependencies {
|
||||||
runtimeOnly("mysql:mysql-connector-java:8.0.22")
|
runtimeOnly("mysql:mysql-connector-java:8.0.22")
|
||||||
runtimeOnly("org.postgresql:postgresql:42.2.16")
|
runtimeOnly("org.postgresql:postgresql:42.2.16")
|
||||||
runtimeOnly("com.microsoft.sqlserver:mssql-jdbc:9.2.1.jre11")
|
runtimeOnly("com.microsoft.sqlserver:mssql-jdbc:9.2.1.jre11")
|
||||||
|
|
||||||
|
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}")
|
||||||
}
|
}
|
||||||
|
|
||||||
springBoot {
|
springBoot {
|
||||||
|
@ -68,8 +71,8 @@ springBoot {
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@ -83,23 +86,26 @@ sourceSets {
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
|
||||||
|
jvmArgs("-XX:+ShowCodeDetailsInExceptionMessages")
|
||||||
|
testLogging {
|
||||||
|
events("skipped", "failed")
|
||||||
|
setExceptionFormat("full")
|
||||||
|
}
|
||||||
|
|
||||||
reports {
|
reports {
|
||||||
junitXml.required.set(true)
|
junitXml.required.set(true)
|
||||||
html.required.set(false)
|
html.required.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
useJUnitPlatform()
|
|
||||||
testLogging {
|
|
||||||
events("skipped", "failed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<JavaCompile>() {
|
tasks.withType<JavaCompile>() {
|
||||||
options.compilerArgs.addAll(arrayOf("--release", "11"))
|
options.compilerArgs.addAll(arrayOf("--release", "17"))
|
||||||
}
|
}
|
||||||
tasks.withType<KotlinCompile>().all {
|
tasks.withType<KotlinCompile>().all {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
freeCompilerArgs = listOf(
|
freeCompilerArgs = listOf(
|
||||||
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
|
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
|
||||||
"-Xinline-classes"
|
"-Xinline-classes"
|
||||||
|
|
|
@ -3,3 +3,5 @@ package dev.fyloz.colorrecipesexplorer
|
||||||
typealias SpringUser = org.springframework.security.core.userdetails.User
|
typealias SpringUser = org.springframework.security.core.userdetails.User
|
||||||
typealias SpringUserDetails = org.springframework.security.core.userdetails.UserDetails
|
typealias SpringUserDetails = org.springframework.security.core.userdetails.UserDetails
|
||||||
typealias SpringUserDetailsService = org.springframework.security.core.userdetails.UserDetailsService
|
typealias SpringUserDetailsService = org.springframework.security.core.userdetails.UserDetailsService
|
||||||
|
|
||||||
|
typealias JavaFile = java.io.File
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
package dev.fyloz.colorrecipesexplorer.config
|
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.CreProperties
|
||||||
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
||||||
import org.slf4j.Logger
|
import dev.fyloz.colorrecipesexplorer.service.files.CachedFileSystemItem
|
||||||
import org.slf4j.LoggerFactory
|
import dev.fyloz.memorycache.ExpiringMemoryCache
|
||||||
|
import dev.fyloz.memorycache.MemoryCache
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties(MaterialTypeProperties::class, CreProperties::class)
|
@EnableConfigurationProperties(MaterialTypeProperties::class, CreProperties::class)
|
||||||
class SpringConfiguration {
|
class CreConfiguration(private val creProperties: CreProperties) {
|
||||||
@Bean
|
@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
|
// Remove old system types
|
||||||
oldSystemTypes.forEach {
|
oldSystemTypes.forEach {
|
||||||
logger.info("Material type '${it.name}' is not a system type anymore")
|
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_DATA_DIRECTORY = "data"
|
||||||
const val DEFAULT_CONFIG_DIRECTORY = "config"
|
const val DEFAULT_CONFIG_DIRECTORY = "config"
|
||||||
|
const val DEFAULT_FILE_CACHE_MAX_ACCESS_COUNT = 10_000L
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "cre.server")
|
@ConfigurationProperties(prefix = "cre.server")
|
||||||
class CreProperties {
|
class CreProperties {
|
||||||
var dataDirectory: String = DEFAULT_DATA_DIRECTORY
|
var dataDirectory: String = DEFAULT_DATA_DIRECTORY
|
||||||
var configDirectory: String = DEFAULT_CONFIG_DIRECTORY
|
var configDirectory: String = DEFAULT_CONFIG_DIRECTORY
|
||||||
|
var fileCacheMaxAccessCount: Long = DEFAULT_FILE_CACHE_MAX_ACCESS_COUNT
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "cre.security")
|
@ConfigurationProperties(prefix = "cre.security")
|
||||||
|
|
|
@ -59,6 +59,17 @@ class AlreadyExistsException(
|
||||||
extensions = extensions.apply { this[identifierName] = identifierValue }.toMap()
|
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(
|
class CannotDeleteException(
|
||||||
errorCode: String,
|
errorCode: String,
|
||||||
title: String,
|
title: String,
|
||||||
|
|
|
@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.model
|
||||||
|
|
||||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||||
|
import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException
|
||||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||||
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
|
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
|
||||||
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
|
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_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_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_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"
|
private const val MATERIAL_TYPE_EXCEPTION_ERROR_CODE = "materialtype"
|
||||||
|
|
||||||
fun materialTypeIdNotFoundException(id: Long) =
|
fun materialTypeIdNotFoundException(id: Long) =
|
||||||
|
@ -150,9 +152,23 @@ fun materialTypePrefixAlreadyExistsException(prefix: String) =
|
||||||
"prefix"
|
"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) =
|
fun cannotDeleteMaterialTypeException(materialType: MaterialType) =
|
||||||
CannotDeleteException(
|
CannotDeleteException(
|
||||||
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
|
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
|
||||||
MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE,
|
MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE,
|
||||||
"Cannot delete material type ${materialType.name} because one or more materials depends on it"
|
"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]. */
|
/** Checks if a material type exists with the given [prefix]. */
|
||||||
fun existsByPrefix(prefix: String): Boolean
|
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. */
|
/** Gets all material types which are not system types. */
|
||||||
fun findAllBySystemTypeIs(value: Boolean): Collection<MaterialType>
|
fun findAllBySystemTypeIs(value: Boolean): Collection<MaterialType>
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import org.springframework.context.annotation.Profile
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
interface MaterialTypeService :
|
interface MaterialTypeService :
|
||||||
ExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialType, MaterialTypeRepository> {
|
ExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialType, MaterialTypeRepository> {
|
||||||
/** Checks if a material type with the given [prefix] exists. */
|
/** Checks if a material type with the given [prefix] exists. */
|
||||||
fun existsByPrefix(prefix: String): Boolean
|
fun existsByPrefix(prefix: String): Boolean
|
||||||
|
|
||||||
|
@ -19,14 +19,17 @@ interface MaterialTypeService :
|
||||||
|
|
||||||
/** Gets all material types who are not a system type. */
|
/** Gets all material types who are not a system type. */
|
||||||
fun getAllNonSystemType(): Collection<MaterialType>
|
fun getAllNonSystemType(): Collection<MaterialType>
|
||||||
|
|
||||||
|
/** Allows to update the given system [materialType], should not be exposed to users. */
|
||||||
|
fun updateSystemType(materialType: MaterialType): MaterialType
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Profile("!emergency")
|
@Profile("!emergency")
|
||||||
class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val materialService: MaterialService) :
|
class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val materialService: MaterialService) :
|
||||||
AbstractExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialType, MaterialTypeRepository>(
|
AbstractExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialType, MaterialTypeRepository>(
|
||||||
repository
|
repository
|
||||||
), MaterialTypeService {
|
), MaterialTypeService {
|
||||||
override fun idNotFoundException(id: Long) = materialTypeIdNotFoundException(id)
|
override fun idNotFoundException(id: Long) = materialTypeIdNotFoundException(id)
|
||||||
override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id)
|
override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id)
|
||||||
override fun nameNotFoundException(name: String) = materialTypeNameNotFoundException(name)
|
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 existsByPrefix(prefix: String): Boolean = repository.existsByPrefix(prefix)
|
||||||
override fun isUsedByMaterial(materialType: MaterialType): Boolean =
|
override fun isUsedByMaterial(materialType: MaterialType): Boolean =
|
||||||
materialService.existsByMaterialType(materialType)
|
materialService.existsByMaterialType(materialType)
|
||||||
|
|
||||||
override fun getAllSystemTypes(): Collection<MaterialType> = repository.findAllBySystemTypeIs(true)
|
override fun getAllSystemTypes(): Collection<MaterialType> = repository.findAllBySystemTypeIs(true)
|
||||||
override fun getAllNonSystemType(): Collection<MaterialType> = repository.findAllBySystemTypeIs(false)
|
override fun getAllNonSystemType(): Collection<MaterialType> = repository.findAllBySystemTypeIs(false)
|
||||||
|
@ -52,15 +55,25 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma
|
||||||
|
|
||||||
return update(with(entity) {
|
return update(with(entity) {
|
||||||
MaterialType(
|
MaterialType(
|
||||||
id = id,
|
id = id,
|
||||||
name = if (isNotNullAndNotBlank(name)) name else persistedMaterialType.name,
|
name = if (isNotNullAndNotBlank(name)) name else persistedMaterialType.name,
|
||||||
prefix = if (isNotNullAndNotBlank(prefix)) prefix else persistedMaterialType.prefix,
|
prefix = if (isNotNullAndNotBlank(prefix)) prefix else persistedMaterialType.prefix,
|
||||||
systemType = false
|
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)) {
|
with(repository.findByPrefix(entity.prefix)) {
|
||||||
if (this != null && id != entity.id)
|
if (this != null && id != entity.id)
|
||||||
throw materialTypePrefixAlreadyExistsException(entity.prefix)
|
throw materialTypePrefixAlreadyExistsException(entity.prefix)
|
||||||
|
@ -70,7 +83,11 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(entity: MaterialType) {
|
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)
|
super.delete(entity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package dev.fyloz.colorrecipesexplorer.service
|
package dev.fyloz.colorrecipesexplorer.service
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||||
import dev.fyloz.colorrecipesexplorer.model.*
|
import dev.fyloz.colorrecipesexplorer.model.*
|
||||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||||
import dev.fyloz.colorrecipesexplorer.model.validation.or
|
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.service.users.GroupService
|
||||||
import dev.fyloz.colorrecipesexplorer.utils.setAll
|
import dev.fyloz.colorrecipesexplorer.utils.setAll
|
||||||
import org.springframework.context.annotation.Lazy
|
import org.springframework.context.annotation.Lazy
|
||||||
import org.springframework.context.annotation.Profile
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.web.multipart.MultipartFile
|
import org.springframework.web.multipart.MultipartFile
|
||||||
import java.io.File
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.Period
|
import java.time.Period
|
||||||
import javax.transaction.Transactional
|
import javax.transaction.Transactional
|
||||||
|
@ -45,7 +44,7 @@ interface RecipeService :
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Profile("!emergency")
|
@RequireDatabase
|
||||||
class RecipeServiceImpl(
|
class RecipeServiceImpl(
|
||||||
recipeRepository: RecipeRepository,
|
recipeRepository: RecipeRepository,
|
||||||
val companyService: CompanyService,
|
val companyService: CompanyService,
|
||||||
|
@ -213,29 +212,20 @@ interface RecipeImageService {
|
||||||
|
|
||||||
/** Deletes the image with the given [name] for the given [recipe]. */
|
/** Deletes the image with the given [name] for the given [recipe]. */
|
||||||
fun delete(recipe: Recipe, name: String)
|
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_ID_DELIMITER = "_"
|
||||||
const val RECIPE_IMAGE_EXTENSION = ".jpg"
|
const val RECIPE_IMAGE_EXTENSION = ".jpg"
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Profile("!emergency")
|
@RequireDatabase
|
||||||
class RecipeImageServiceImpl(
|
class RecipeImageServiceImpl(
|
||||||
val fileService: WriteableFileService
|
val fileService: WriteableFileService
|
||||||
) : RecipeImageService {
|
) : RecipeImageService {
|
||||||
override fun getAllImages(recipe: Recipe): Set<String> {
|
override fun getAllImages(recipe: Recipe) =
|
||||||
val recipeDirectory = recipe.getDirectory()
|
fileService.listDirectoryFiles(recipe.imagesDirectoryPath)
|
||||||
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()
|
|
||||||
.map { it.name }
|
.map { it.name }
|
||||||
.toSet()
|
.toSet()
|
||||||
}
|
|
||||||
|
|
||||||
override fun download(image: MultipartFile, recipe: Recipe): String {
|
override fun download(image: MultipartFile, recipe: Recipe): String {
|
||||||
/** Gets the next id available for a new image for the given [recipe]. */
|
/** Gets the next id available for a new image for the given [recipe]. */
|
||||||
|
@ -252,17 +242,15 @@ class RecipeImageServiceImpl(
|
||||||
} + 1L
|
} + 1L
|
||||||
}
|
}
|
||||||
|
|
||||||
return getImageFileName(recipe, getNextAvailableId()).apply {
|
return getImageFileName(recipe, getNextAvailableId()).also {
|
||||||
fileService.write(image, getImagePath(recipe, this), true)
|
with(getImagePath(recipe, it)) {
|
||||||
|
fileService.writeToDirectory(image, this, recipe.imagesDirectoryPath, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(recipe: Recipe, name: String) =
|
override fun delete(recipe: Recipe, name: String) =
|
||||||
fileService.delete(getImagePath(recipe, name))
|
fileService.deleteFromDirectory(getImagePath(recipe, name), recipe.imagesDirectoryPath)
|
||||||
|
|
||||||
override fun Recipe.getDirectory(): File = File(with(fileService) {
|
|
||||||
this@getDirectory.imagesDirectoryPath.fullPath().path
|
|
||||||
})
|
|
||||||
|
|
||||||
private fun getImageFileName(recipe: Recipe, id: Long) =
|
private fun getImageFileName(recipe: Recipe, id: Long) =
|
||||||
"${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$id"
|
"${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$id"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package dev.fyloz.colorrecipesexplorer.service.config
|
package dev.fyloz.colorrecipesexplorer.service.config
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.JavaFile
|
||||||
import dev.fyloz.colorrecipesexplorer.SUPPORTED_DATABASE_VERSION
|
import dev.fyloz.colorrecipesexplorer.SUPPORTED_DATABASE_VERSION
|
||||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
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.ConfigurationType
|
||||||
import dev.fyloz.colorrecipesexplorer.model.configuration
|
import dev.fyloz.colorrecipesexplorer.model.configuration
|
||||||
import dev.fyloz.colorrecipesexplorer.repository.ConfigurationRepository
|
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 dev.fyloz.colorrecipesexplorer.utils.excludeAll
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.springframework.boot.info.BuildProperties
|
import org.springframework.boot.info.BuildProperties
|
||||||
import org.springframework.context.annotation.Lazy
|
import org.springframework.context.annotation.Lazy
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
@ -96,7 +96,7 @@ private class FileConfigurationSource(
|
||||||
private val configFilePath: String
|
private val configFilePath: String
|
||||||
) : ConfigurationSource {
|
) : ConfigurationSource {
|
||||||
private val properties = Properties().apply {
|
private val properties = Properties().apply {
|
||||||
with(File(configFilePath)) {
|
with(JavaFile(configFilePath)) {
|
||||||
if (!this.exists()) this.create()
|
if (!this.exists()) this.create()
|
||||||
FileInputStream(this).use {
|
FileInputStream(this).use {
|
||||||
this@apply.load(it)
|
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.config.properties.CreProperties
|
||||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
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.slf4j.Logger
|
||||||
import org.springframework.core.io.ByteArrayResource
|
import org.springframework.core.io.ByteArrayResource
|
||||||
import org.springframework.core.io.Resource
|
import org.springframework.core.io.Resource
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.web.multipart.MultipartFile
|
import org.springframework.web.multipart.MultipartFile
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
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. */
|
/** Banned path shards. These are banned because they can allow access to files outside the data directory. */
|
||||||
val BANNED_FILE_PATH_SHARDS = setOf(
|
val BANNED_FILE_PATH_SHARDS = setOf(
|
||||||
|
@ -26,8 +28,11 @@ interface FileService {
|
||||||
/** Reads the file at the given [path]. */
|
/** Reads the file at the given [path]. */
|
||||||
fun read(path: String): Resource
|
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. */
|
/** Completes the path of the given [String] by adding the working directory. */
|
||||||
fun String.fullPath(): FilePath
|
fun fullPath(path: String): FilePath
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WriteableFileService : FileService {
|
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. */
|
/** 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)
|
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]. */
|
/** Deletes the file at the given [path]. */
|
||||||
fun delete(path: String)
|
fun delete(path: String)
|
||||||
|
|
||||||
|
/** Deletes the file at the given [path], and specify the [parentPath]. */
|
||||||
|
fun deleteFromDirectory(path: String, parentPath: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class FileServiceImpl(
|
class FileServiceImpl(
|
||||||
private val creProperties: CreProperties,
|
private val fileCache: FileCache,
|
||||||
private val logger: Logger
|
private val creProperties: CreProperties
|
||||||
) : WriteableFileService {
|
) : WriteableFileService {
|
||||||
override fun exists(path: String) = withFileAt(path.fullPath()) {
|
private val logger = KotlinLogging.logger {}
|
||||||
this.exists() && this.isFile
|
|
||||||
|
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(
|
override fun read(path: String) = ByteArrayResource(
|
||||||
withFileAt(path.fullPath()) {
|
withFileAt(fullPath(path)) {
|
||||||
if (!exists(path)) throw FileNotFoundException(path)
|
if (!exists(path)) throw FileNotFoundException(path)
|
||||||
try {
|
try {
|
||||||
readBytes()
|
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) {
|
override fun create(path: String) {
|
||||||
val fullPath = path.fullPath()
|
val fullPath = fullPath(path)
|
||||||
if (!exists(path)) {
|
if (!exists(path)) {
|
||||||
try {
|
try {
|
||||||
withFileAt(fullPath) {
|
withFileAt(fullPath) {
|
||||||
this.create()
|
this.create()
|
||||||
|
fileCache.setExists(fullPath)
|
||||||
|
|
||||||
|
logger.info("Created file at '${fullPath.value}'")
|
||||||
}
|
}
|
||||||
} catch (ex: IOException) {
|
} catch (ex: IOException) {
|
||||||
FileCreateException(path).logAndThrow(ex, logger)
|
FileCreateException(path).logAndThrow(ex, logger)
|
||||||
|
@ -79,35 +114,52 @@ class FileServiceImpl(
|
||||||
|
|
||||||
override fun write(file: MultipartFile, path: String, overwrite: Boolean) =
|
override fun write(file: MultipartFile, path: String, overwrite: Boolean) =
|
||||||
prepareWrite(path, overwrite) {
|
prepareWrite(path, overwrite) {
|
||||||
|
logWrittenDataSize(file.size)
|
||||||
file.transferTo(this.toPath())
|
file.transferTo(this.toPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun write(data: ByteArrayResource, path: String, overwrite: Boolean) =
|
override fun write(data: ByteArrayResource, path: String, overwrite: Boolean) =
|
||||||
prepareWrite(path, overwrite) {
|
prepareWrite(path, overwrite) {
|
||||||
|
logWrittenDataSize(data.contentLength())
|
||||||
this.writeBytes(data.byteArray)
|
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) {
|
override fun delete(path: String) {
|
||||||
try {
|
try {
|
||||||
withFileAt(path.fullPath()) {
|
val fullPath = fullPath(path)
|
||||||
|
withFileAt(fullPath) {
|
||||||
if (!exists(path)) throw FileNotFoundException(path)
|
if (!exists(path)) throw FileNotFoundException(path)
|
||||||
!this.delete()
|
|
||||||
|
this.delete()
|
||||||
|
fileCache.setExists(fullPath, false)
|
||||||
|
|
||||||
|
logger.info("Deleted file at '${fullPath.value}'")
|
||||||
}
|
}
|
||||||
} catch (ex: IOException) {
|
} catch (ex: IOException) {
|
||||||
FileDeleteException(path).logAndThrow(ex, logger)
|
FileDeleteException(path).logAndThrow(ex, logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun String.fullPath(): FilePath {
|
override fun deleteFromDirectory(path: String, parentPath: String) {
|
||||||
BANNED_FILE_PATH_SHARDS
|
fileCache.removeItemFromDirectory(fullPath(parentPath), fullPath(path))
|
||||||
.firstOrNull { this.contains(it) }
|
delete(path)
|
||||||
?.let { throw InvalidFilePathException(this, it) }
|
}
|
||||||
|
|
||||||
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) {
|
private fun prepareWrite(path: String, overwrite: Boolean, op: File.() -> Unit) {
|
||||||
val fullPath = path.fullPath()
|
val fullPath = fullPath(path)
|
||||||
|
|
||||||
if (exists(path)) {
|
if (exists(path)) {
|
||||||
if (!overwrite) throw FileExistsException(path)
|
if (!overwrite) throw FileExistsException(path)
|
||||||
|
@ -118,26 +170,17 @@ class FileServiceImpl(
|
||||||
try {
|
try {
|
||||||
withFileAt(fullPath) {
|
withFileAt(fullPath) {
|
||||||
this.op()
|
this.op()
|
||||||
|
|
||||||
|
logger.info("Wrote data to file at '${fullPath.value}'")
|
||||||
}
|
}
|
||||||
} catch (ex: IOException) {
|
} catch (ex: IOException) {
|
||||||
FileWriteException(path).logAndThrow(ex, logger)
|
FileWriteException(path).logAndThrow(ex, logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Runs the given [block] in the context of a file with the given [fullPath]. */
|
private fun logWrittenDataSize(size: Long) {
|
||||||
private fun <T> withFileAt(fullPath: FilePath, block: File.() -> T) =
|
logger.debug("Writing $size bytes to file system...")
|
||||||
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"
|
private const val FILE_IO_EXCEPTION_TITLE = "File IO error"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package dev.fyloz.colorrecipesexplorer.service.files
|
package dev.fyloz.colorrecipesexplorer.service.files
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||||
import org.springframework.core.io.Resource
|
import org.springframework.core.io.Resource
|
||||||
import org.springframework.core.io.ResourceLoader
|
import org.springframework.core.io.ResourceLoader
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
@ -9,18 +10,27 @@ class ResourceFileService(
|
||||||
private val resourceLoader: ResourceLoader
|
private val resourceLoader: ResourceLoader
|
||||||
) : FileService {
|
) : FileService {
|
||||||
override fun exists(path: String) =
|
override fun exists(path: String) =
|
||||||
path.fullPath().resource.exists()
|
fullPath(path).resource.exists()
|
||||||
|
|
||||||
override fun read(path: String): Resource =
|
override fun read(path: String): Resource =
|
||||||
path.fullPath().resource.also {
|
fullPath(path).resource.also {
|
||||||
if (!it.exists()) {
|
if (!it.exists()) {
|
||||||
throw FileNotFoundException(path)
|
throw FileNotFoundException(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun String.fullPath() =
|
override fun listDirectoryFiles(path: String): Collection<CachedFile> {
|
||||||
FilePath("classpath:${this}")
|
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
|
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.server.config-directory=config
|
||||||
cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE78WWOgl8InrbwBgQsMy0
|
cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE78WWOgl8InrbwBgQsMy0
|
||||||
cre.security.jwt-duration=18000000
|
cre.security.jwt-duration=18000000
|
||||||
cre.security.aes-secret=blabla
|
|
||||||
# Root user
|
# Root user
|
||||||
cre.security.root.id=9999
|
cre.security.root.id=9999
|
||||||
cre.security.root.password=password
|
cre.security.root.password=password
|
||||||
|
|
|
@ -2,6 +2,8 @@ package dev.fyloz.colorrecipesexplorer.service
|
||||||
|
|
||||||
import com.nhaarman.mockitokotlin2.*
|
import com.nhaarman.mockitokotlin2.*
|
||||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
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.exception.NotFoundException
|
||||||
import dev.fyloz.colorrecipesexplorer.model.*
|
import dev.fyloz.colorrecipesexplorer.model.*
|
||||||
import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository
|
import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository
|
||||||
|
@ -164,8 +166,22 @@ class MaterialTypeServiceTest :
|
||||||
.assertErrorCode("prefix")
|
.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()
|
// 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`() {
|
override fun `delete() deletes in the repository`() {
|
||||||
whenCanBeDeleted {
|
whenCanBeDeleted {
|
||||||
super.`delete() deletes in the repository`()
|
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.model.account.group
|
||||||
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
|
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
|
||||||
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
|
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.files.WriteableFileService
|
||||||
import dev.fyloz.colorrecipesexplorer.service.users.GroupService
|
import dev.fyloz.colorrecipesexplorer.service.users.GroupService
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
@ -15,7 +17,6 @@ import org.junit.jupiter.api.TestInstance
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import org.springframework.mock.web.MockMultipartFile
|
import org.springframework.mock.web.MockMultipartFile
|
||||||
import org.springframework.web.multipart.MultipartFile
|
import org.springframework.web.multipart.MultipartFile
|
||||||
import java.io.File
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.Period
|
import java.time.Period
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
|
@ -30,7 +31,17 @@ class RecipeServiceTest :
|
||||||
private val recipeStepService: RecipeStepService = mock()
|
private val recipeStepService: RecipeStepService = mock()
|
||||||
private val configService: ConfigurationService = mock()
|
private val configService: ConfigurationService = mock()
|
||||||
override val service: RecipeService =
|
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)
|
private val company: Company = company(id = 0L)
|
||||||
override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company)
|
override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company)
|
||||||
|
@ -273,18 +284,7 @@ private class RecipeImageServiceTestContext {
|
||||||
val recipe = spyk(recipe())
|
val recipe = spyk(recipe())
|
||||||
val recipeImagesIds = setOf(1L, 10L, 21L)
|
val recipeImagesIds = setOf(1L, 10L, 21L)
|
||||||
val recipeImagesNames = recipeImagesIds.map { it.imageName }.toSet()
|
val recipeImagesNames = recipeImagesIds.map { it.imageName }.toSet()
|
||||||
val recipeImagesFiles = recipeImagesNames.map { File(it) }.toTypedArray()
|
val recipeImagesFiles = recipeImagesNames.map { CachedFile(it, FilePath(it), true) }
|
||||||
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 Long.imageName
|
val Long.imageName
|
||||||
get() = "${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$this"
|
get() = "${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$this"
|
||||||
|
@ -308,6 +308,8 @@ class RecipeImageServiceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `getAllImages() returns a Set containing the name of every files in the recipe's directory`() {
|
fun `getAllImages() returns a Set containing the name of every files in the recipe's directory`() {
|
||||||
test {
|
test {
|
||||||
|
every { fileService.listDirectoryFiles(any()) } returns recipeImagesFiles
|
||||||
|
|
||||||
val foundImagesNames = recipeImageService.getAllImages(recipe)
|
val foundImagesNames = recipeImageService.getAllImages(recipe)
|
||||||
|
|
||||||
assertEquals(recipeImagesNames, foundImagesNames)
|
assertEquals(recipeImagesNames, foundImagesNames)
|
||||||
|
@ -317,7 +319,7 @@ class RecipeImageServiceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `getAllImages() returns an empty Set when the recipe's directory does not exists`() {
|
fun `getAllImages() returns an empty Set when the recipe's directory does not exists`() {
|
||||||
test {
|
test {
|
||||||
every { recipeDirectory.exists() } returns false
|
every { fileService.listDirectoryFiles(any()) } returns emptySet()
|
||||||
|
|
||||||
assertTrue {
|
assertTrue {
|
||||||
recipeImageService.getAllImages(recipe).isEmpty()
|
recipeImageService.getAllImages(recipe).isEmpty()
|
||||||
|
@ -335,12 +337,15 @@ class RecipeImageServiceTest {
|
||||||
val expectedImageName = expectedImageId.imageName
|
val expectedImageName = expectedImageId.imageName
|
||||||
val expectedImagePath = expectedImageName.imagePath
|
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)
|
val foundImageName = recipeImageService.download(mockImage, recipe)
|
||||||
|
|
||||||
assertEquals(expectedImageName, foundImageName)
|
assertEquals(expectedImageName, foundImageName)
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
fileService.write(mockImage, expectedImagePath, true)
|
fileService.writeToDirectory(mockImage, expectedImagePath, any(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -353,10 +358,12 @@ class RecipeImageServiceTest {
|
||||||
val imageName = recipeImagesIds.first().imageName
|
val imageName = recipeImagesIds.first().imageName
|
||||||
val imagePath = imageName.imagePath
|
val imagePath = imageName.imagePath
|
||||||
|
|
||||||
|
every { fileService.deleteFromDirectory(any(), any()) } just runs
|
||||||
|
|
||||||
recipeImageService.delete(recipe, imageName)
|
recipeImageService.delete(recipe, imageName)
|
||||||
|
|
||||||
verify {
|
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
|
package dev.fyloz.colorrecipesexplorer.service.files
|
||||||
|
|
||||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.File
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import org.springframework.mock.web.MockMultipartFile
|
import org.springframework.mock.web.MockMultipartFile
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
@ -20,56 +21,121 @@ private const val mockFilePath = "existingFile"
|
||||||
private val mockFilePathPath = Path.of(mockFilePath)
|
private val mockFilePathPath = Path.of(mockFilePath)
|
||||||
private val mockFileData = byteArrayOf(0x1, 0x8, 0xa, 0xf)
|
private val mockFileData = byteArrayOf(0x1, 0x8, 0xa, 0xf)
|
||||||
|
|
||||||
private class FileServiceTestContext {
|
class FileServiceTest {
|
||||||
val fileService = spyk(FileServiceImpl(creProperties, mockk {
|
private val fileCacheMock = mockk<FileCache> {
|
||||||
every { error(any(), any<Exception>()) } just Runs
|
every { setExists(any(), any()) } just runs
|
||||||
}))
|
}
|
||||||
val mockFile = mockk<File> {
|
private val fileService = spyk(FileServiceImpl(fileCacheMock, creProperties))
|
||||||
every { path } returns mockFilePath
|
|
||||||
|
private val mockFile = mockk<File> {
|
||||||
|
every { file } returns mockk()
|
||||||
every { exists() } returns true
|
every { exists() } returns true
|
||||||
every { isFile } returns true
|
every { isFile } returns true
|
||||||
every { toPath() } returns mockFilePathPath
|
every { toPath() } returns mockFilePathPath
|
||||||
}
|
}
|
||||||
val mockFileFullPath = spyk(FilePath("${creProperties.dataDirectory}/$mockFilePath")) {
|
private val mockMultipartFile = spyk(MockMultipartFile(mockFilePath, mockFileData))
|
||||||
every { file } returns mockFile
|
|
||||||
|
|
||||||
with(fileService) {
|
@BeforeEach
|
||||||
every { mockFilePath.fullPath() } returns this@spyk
|
internal fun beforeEach() {
|
||||||
}
|
mockkObject(File.Companion)
|
||||||
|
every { File.from(any<String>()) } returns mockFile
|
||||||
}
|
}
|
||||||
val mockMultipartFile = spyk(MockMultipartFile(mockFilePath, mockFileData))
|
|
||||||
}
|
|
||||||
|
|
||||||
class FileServiceTest {
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
internal fun afterEach() {
|
internal fun afterEach() {
|
||||||
clearAllMocks()
|
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()
|
// exists()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `exists() returns true when the file at the given path exists and is a file`() {
|
fun `exists() returns true when the file at the given path exists and is a file`() {
|
||||||
test {
|
whenFileNotCached {
|
||||||
assertTrue { fileService.exists(mockFilePath) }
|
assertTrue { fileService.exists(mockFilePath) }
|
||||||
|
|
||||||
|
verify {
|
||||||
|
mockFile.exists()
|
||||||
|
mockFile.isFile
|
||||||
|
}
|
||||||
|
confirmVerified(mockFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `exists() returns false when the file at the given path does not exist`() {
|
fun `exists() returns false when the file at the given path does not exist`() {
|
||||||
test {
|
whenFileNotCached {
|
||||||
every { mockFile.exists() } returns false
|
every { mockFile.exists() } returns false
|
||||||
|
|
||||||
assertFalse { fileService.exists(mockFilePath) }
|
assertFalse { fileService.exists(mockFilePath) }
|
||||||
|
|
||||||
|
verify {
|
||||||
|
mockFile.exists()
|
||||||
|
}
|
||||||
|
confirmVerified(mockFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `exists() returns false when the file at the given path is not a file`() {
|
fun `exists() returns false when the file at the given path is not a file`() {
|
||||||
test {
|
whenFileNotCached {
|
||||||
every { mockFile.isFile } returns false
|
every { mockFile.isFile } returns false
|
||||||
|
|
||||||
assertFalse { fileService.exists(mockFilePath) }
|
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
|
@Test
|
||||||
fun `read() returns a valid ByteArrayResource`() {
|
fun `read() returns a valid ByteArrayResource`() {
|
||||||
test {
|
whenMockFilePathExists {
|
||||||
whenMockFilePathExists {
|
mockkStatic(File::readBytes)
|
||||||
mockkStatic(File::readBytes)
|
every { mockFile.readBytes() } returns mockFileData
|
||||||
every { mockFile.readBytes() } returns mockFileData
|
|
||||||
|
|
||||||
val redResource = fileService.read(mockFilePath)
|
val redResource = fileService.read(mockFilePath)
|
||||||
|
|
||||||
assertEquals(mockFileData, redResource.byteArray)
|
assertEquals(mockFileData, redResource.byteArray)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `read() throws FileNotFoundException when no file exists at the given path`() {
|
fun `read() throws FileNotFoundException when no file exists at the given path`() {
|
||||||
test {
|
whenMockFilePathExists(false) {
|
||||||
whenMockFilePathExists(false) {
|
with(assertThrows<FileNotFoundException> { fileService.read(mockFilePath) }) {
|
||||||
with(assertThrows<FileNotFoundException> { fileService.read(mockFilePath) }) {
|
assertEquals(mockFilePath, this.path)
|
||||||
assertEquals(mockFilePath, this.path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `read() throws FileReadException when an IOException is thrown`() {
|
fun `read() throws FileReadException when an IOException is thrown`() {
|
||||||
test {
|
whenMockFilePathExists {
|
||||||
whenMockFilePathExists {
|
mockkStatic(File::readBytes)
|
||||||
mockkStatic(File::readBytes)
|
every { mockFile.readBytes() } throws IOException()
|
||||||
every { mockFile.readBytes() } throws IOException()
|
|
||||||
|
|
||||||
with(assertThrows<FileReadException> { fileService.read(mockFilePath) }) {
|
with(assertThrows<FileReadException> { fileService.read(mockFilePath) }) {
|
||||||
assertEquals(mockFilePath, this.path)
|
assertEquals(mockFilePath, this.path)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,15 +178,19 @@ class FileServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create() creates a file at the given path`() {
|
fun `create() creates a file at the given path`() {
|
||||||
test {
|
whenMockFilePathExists(false) {
|
||||||
whenMockFilePathExists(false) {
|
whenFileNotCached {
|
||||||
mockkStatic(File::create)
|
mockkStatic(File::create) {
|
||||||
every { mockFile.create() } just Runs
|
every { mockFile.create() } just runs
|
||||||
|
|
||||||
fileService.create(mockFilePath)
|
fileService.create(mockFilePath)
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
mockFile.create()
|
mockFile.create()
|
||||||
|
|
||||||
|
fileCacheMock.setExists(any())
|
||||||
|
}
|
||||||
|
confirmVerified(mockFile, fileCacheMock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,27 +198,23 @@ class FileServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create() does nothing when a file already exists at the given path`() {
|
fun `create() does nothing when a file already exists at the given path`() {
|
||||||
test {
|
whenMockFilePathExists {
|
||||||
whenMockFilePathExists {
|
fileService.create(mockFilePath)
|
||||||
fileService.create(mockFilePath)
|
|
||||||
|
|
||||||
verify(exactly = 0) {
|
verify(exactly = 0) {
|
||||||
mockFile.create()
|
mockFile.create()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create() throws FileCreateException when the file creation throws an IOException`() {
|
fun `create() throws FileCreateException when the file creation throws an IOException`() {
|
||||||
test {
|
whenMockFilePathExists(false) {
|
||||||
whenMockFilePathExists(false) {
|
mockkStatic(File::create)
|
||||||
mockkStatic(File::create)
|
every { mockFile.create() } throws IOException()
|
||||||
every { mockFile.create() } throws IOException()
|
|
||||||
|
|
||||||
with(assertThrows<FileCreateException> { fileService.create(mockFilePath) }) {
|
with(assertThrows<FileCreateException> { fileService.create(mockFilePath) }) {
|
||||||
assertEquals(mockFilePath, this.path)
|
assertEquals(mockFilePath, this.path)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,59 +223,51 @@ class FileServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `write() creates and writes the given MultipartFile to the file at the given path`() {
|
fun `write() creates and writes the given MultipartFile to the file at the given path`() {
|
||||||
test {
|
whenMockFilePathExists(false) {
|
||||||
whenMockFilePathExists(false) {
|
every { fileService.create(mockFilePath) } just runs
|
||||||
every { fileService.create(mockFilePath) } just Runs
|
every { mockMultipartFile.transferTo(mockFilePathPath) } just runs
|
||||||
every { mockMultipartFile.transferTo(mockFilePathPath) } just Runs
|
|
||||||
|
|
||||||
fileService.write(mockMultipartFile, mockFilePath, false)
|
fileService.write(mockMultipartFile, mockFilePath, false)
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
fileService.create(mockFilePath)
|
fileService.create(mockFilePath)
|
||||||
mockMultipartFile.transferTo(mockFilePathPath)
|
mockMultipartFile.transferTo(mockFilePathPath)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `write() throws FileExistsException when a file at the given path already exists and overwrite is disabled`() {
|
fun `write() throws FileExistsException when a file at the given path already exists and overwrite is disabled`() {
|
||||||
test {
|
whenMockFilePathExists {
|
||||||
whenMockFilePathExists {
|
with(assertThrows<FileExistsException> { fileService.write(mockMultipartFile, mockFilePath, false) }) {
|
||||||
with(assertThrows<FileExistsException> { fileService.write(mockMultipartFile, mockFilePath, false) }) {
|
assertEquals(mockFilePath, this.path)
|
||||||
assertEquals(mockFilePath, this.path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `write() writes the given MultipartFile to an existing file when overwrite is enabled`() {
|
fun `write() writes the given MultipartFile to an existing file when overwrite is enabled`() {
|
||||||
test {
|
whenMockFilePathExists {
|
||||||
whenMockFilePathExists {
|
every { mockMultipartFile.transferTo(mockFilePathPath) } just runs
|
||||||
every { mockMultipartFile.transferTo(mockFilePathPath) } just Runs
|
|
||||||
|
|
||||||
fileService.write(mockMultipartFile, mockFilePath, true)
|
fileService.write(mockMultipartFile, mockFilePath, true)
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
mockMultipartFile.transferTo(mockFilePathPath)
|
mockMultipartFile.transferTo(mockFilePathPath)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `write() throws FileWriteException when writing the given file throws an IOException`() {
|
fun `write() throws FileWriteException when writing the given file throws an IOException`() {
|
||||||
test {
|
whenMockFilePathExists(false) {
|
||||||
whenMockFilePathExists(false) {
|
every { fileService.create(mockFilePath) } just runs
|
||||||
every { fileService.create(mockFilePath) } just Runs
|
every { mockMultipartFile.transferTo(mockFilePathPath) } throws IOException()
|
||||||
every { mockMultipartFile.transferTo(mockFilePathPath) } throws IOException()
|
|
||||||
|
|
||||||
with(assertThrows<FileWriteException> {
|
with(assertThrows<FileWriteException> {
|
||||||
fileService.write(mockMultipartFile, mockFilePath, false)
|
fileService.write(mockMultipartFile, mockFilePath, false)
|
||||||
}) {
|
}) {
|
||||||
assertEquals(mockFilePath, this.path)
|
assertEquals(mockFilePath, this.path)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,35 +276,38 @@ class FileServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `delete() deletes the file at the given path`() {
|
fun `delete() deletes the file at the given path`() {
|
||||||
test {
|
whenMockFilePathExists {
|
||||||
whenMockFilePathExists {
|
whenFileCached {
|
||||||
every { mockFile.delete() } returns true
|
every { mockFile.delete() } returns true
|
||||||
|
|
||||||
fileService.delete(mockFilePath)
|
fileService.delete(mockFilePath)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
mockFile.delete()
|
||||||
|
|
||||||
|
fileCacheMock.setExists(any(), false)
|
||||||
|
}
|
||||||
|
confirmVerified(mockFile, fileCacheMock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `delete() throws FileNotFoundException when no file exists at the given path`() {
|
fun `delete() throws FileNotFoundException when no file exists at the given path`() {
|
||||||
test {
|
whenMockFilePathExists(false) {
|
||||||
whenMockFilePathExists(false) {
|
with(assertThrows<FileNotFoundException> { fileService.delete(mockFilePath) }) {
|
||||||
with(assertThrows<FileNotFoundException> { fileService.delete(mockFilePath) }) {
|
assertEquals(mockFilePath, this.path)
|
||||||
assertEquals(mockFilePath, this.path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `delete() throws FileDeleteException when deleting throw and IOException`() {
|
fun `delete() throws FileDeleteException when deleting throw and IOException`() {
|
||||||
test {
|
whenMockFilePathExists {
|
||||||
whenMockFilePathExists {
|
every { mockFile.delete() } throws IOException()
|
||||||
every { mockFile.delete() } throws IOException()
|
|
||||||
|
|
||||||
with(assertThrows<FileDeleteException> { fileService.delete(mockFilePath) }) {
|
with(assertThrows<FileDeleteException> { fileService.delete(mockFilePath) }) {
|
||||||
assertEquals(mockFilePath, this.path)
|
assertEquals(mockFilePath, this.path)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -261,37 +316,24 @@ class FileServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fullPath() appends the given path to the given working directory`() {
|
fun `fullPath() appends the given path to the given working directory`() {
|
||||||
test {
|
with(fileService) {
|
||||||
with(fileService) {
|
val fullFilePath = fullPath(mockFilePath)
|
||||||
val fullFilePath = mockFilePath.fullPath()
|
|
||||||
|
|
||||||
assertEquals("${creProperties.dataDirectory}/$mockFilePath", fullFilePath.path)
|
assertEquals("${creProperties.dataDirectory}/$mockFilePath", fullFilePath.value)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fullPath() throws InvalidFilePathException when the given path contains invalid fragments`() {
|
fun `fullPath() throws InvalidFilePathException when the given path contains invalid fragments`() {
|
||||||
test {
|
with(fileService) {
|
||||||
with(fileService) {
|
BANNED_FILE_PATH_SHARDS.forEach {
|
||||||
BANNED_FILE_PATH_SHARDS.forEach {
|
val maliciousPath = "$it/$mockFilePath"
|
||||||
val maliciousPath = "$it/$mockFilePath"
|
|
||||||
|
|
||||||
with(assertThrows<InvalidFilePathException> { maliciousPath.fullPath() }) {
|
with(assertThrows<InvalidFilePathException> { fullPath(maliciousPath) }) {
|
||||||
assertEquals(maliciousPath, this.path)
|
assertEquals(maliciousPath, this.path)
|
||||||
assertEquals(it, this.fragment)
|
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
|
package dev.fyloz.colorrecipesexplorer.service.files
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||||
import io.mockk.clearAllMocks
|
import io.mockk.clearAllMocks
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
@ -26,7 +27,7 @@ class ResourceFileServiceTest {
|
||||||
private fun existsTest(shouldExists: Boolean, test: (String) -> Unit) {
|
private fun existsTest(shouldExists: Boolean, test: (String) -> Unit) {
|
||||||
val path = "unit_test_resource"
|
val path = "unit_test_resource"
|
||||||
with(service) {
|
with(service) {
|
||||||
every { path.fullPath() } returns mockk {
|
every { fullPath(path) } returns mockk {
|
||||||
every { resource } returns mockk {
|
every { resource } returns mockk {
|
||||||
every { exists() } returns shouldExists
|
every { exists() } returns shouldExists
|
||||||
}
|
}
|
||||||
|
@ -60,7 +61,7 @@ class ResourceFileServiceTest {
|
||||||
}
|
}
|
||||||
val path = "unit_test_path"
|
val path = "unit_test_path"
|
||||||
with(service) {
|
with(service) {
|
||||||
every { path.fullPath() } returns mockk {
|
every { fullPath(path) } returns mockk {
|
||||||
every { resource } returns mockResource
|
every { resource } returns mockResource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,11 +92,9 @@ class ResourceFileServiceTest {
|
||||||
val path = "unit_test_path"
|
val path = "unit_test_path"
|
||||||
val expectedPath = "classpath:$path"
|
val expectedPath = "classpath:$path"
|
||||||
|
|
||||||
with(service) {
|
val found = service.fullPath(path)
|
||||||
val found = path.fullPath()
|
|
||||||
|
|
||||||
assertEquals(expectedPath, found.path)
|
assertEquals(expectedPath, found.value)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -103,7 +102,7 @@ class ResourceFileServiceTest {
|
||||||
val filePath = FilePath("classpath:unit_test_path")
|
val filePath = FilePath("classpath:unit_test_path")
|
||||||
val resource = mockk<Resource>()
|
val resource = mockk<Resource>()
|
||||||
|
|
||||||
every { resourceLoader.getResource(filePath.path) } returns resource
|
every { resourceLoader.getResource(filePath.value) } returns resource
|
||||||
|
|
||||||
with(service) {
|
with(service) {
|
||||||
val found = filePath.resource
|
val found = filePath.resource
|
||||||
|
|
Loading…
Reference in New Issue