#18 Add FileCache
continuous-integration/drone/push Build is passing Details

This commit is contained in:
FyloZ 2022-01-03 13:44:23 -05:00
parent 26d696d66b
commit e6b1ba3b45
Signed by: william
GPG Key ID: 835378AE9AF4AE97
10 changed files with 276 additions and 118 deletions

View File

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

View File

@ -0,0 +1,150 @@
package dev.fyloz.colorrecipesexplorer.service.files
import dev.fyloz.colorrecipesexplorer.JavaFile
import dev.fyloz.colorrecipesexplorer.utils.File
import dev.fyloz.colorrecipesexplorer.utils.FilePath
import mu.KotlinLogging
object FileCache {
private val logger = KotlinLogging.logger {}
private val cache = hashMapOf<String, CachedFileSystemItem>()
operator fun contains(filePath: FilePath) =
filePath.path in cache
operator fun get(filePath: FilePath) =
cache[filePath.path]
fun getDirectory(filePath: FilePath) =
if (directoryExists(filePath)) {
this[filePath] as CachedDirectory
} else {
null
}
fun getFile(filePath: FilePath) =
if (fileExists(filePath)) {
this[filePath] as CachedFile
} else {
null
}
private operator fun set(filePath: FilePath, item: CachedFileSystemItem) {
cache[filePath.path] = item
}
fun exists(filePath: FilePath) =
filePath in this && cache[filePath.path]!!.exists
fun directoryExists(filePath: FilePath) =
exists(filePath) && this[filePath] is CachedDirectory
fun fileExists(filePath: FilePath) =
exists(filePath) && this[filePath] is CachedFile
fun setExists(filePath: FilePath, exists: Boolean = true) {
if (filePath !in this) {
load(filePath)
}
this[filePath] = this[filePath]!!.clone(exists)
logger.debug("Updated FileCache state: ${filePath.path} exists -> $exists")
}
fun setDoesNotExists(filePath: FilePath) =
setExists(filePath, false)
fun load(filePath: FilePath) =
with(JavaFile(filePath.path).toFileSystemItem()) {
cache[filePath.path] = this
logger.debug("Loaded file at ${filePath.path} into FileCache")
}
fun addContent(filePath: FilePath, childFilePath: FilePath) {
val directory = prepareDirectory(filePath) ?: return
val updatedContent = setOf(
*directory.content.toTypedArray(),
JavaFile(childFilePath.path).toFileSystemItem()
)
this[filePath] = directory.copy(content = updatedContent)
logger.debug("Added child ${childFilePath.path} to ${filePath.path} in FileCache")
}
fun removeContent(filePath: FilePath, childFilePath: FilePath) {
val directory = prepareDirectory(filePath) ?: return
val updatedContent = directory.content
.filter { it.path.path != childFilePath.path }
.toSet()
this[filePath] = directory.copy(content = updatedContent)
logger.debug("Removed child ${childFilePath.path} from ${filePath.path} in FileCache")
}
private fun prepareDirectory(filePath: FilePath): CachedDirectory? {
if (!directoryExists(filePath)) {
logger.warn("Cannot add child to ${filePath.path} because it is not in the cache")
return null
}
val directory = getDirectory(filePath)
if (directory == null) {
logger.warn("Cannot add child to ${filePath.path} 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))
}

View File

@ -1,32 +0,0 @@
package dev.fyloz.colorrecipesexplorer.service.files
import dev.fyloz.colorrecipesexplorer.utils.FilePath
import mu.KotlinLogging
object FileExistCache {
private val logger = KotlinLogging.logger {}
private val map = hashMapOf<String, Boolean>()
/** Checks if the given [path] is in the cache. */
operator fun contains(path: FilePath) =
path.path in map
/** Checks if the file at the given [path] exists. */
fun exists(path: FilePath) =
map[path.path] ?: false
/** Sets the file at the given [path] as existing. */
fun setExists(path: FilePath) =
set(path, true)
/** Sets the file at the given [path] as not existing. */
fun setDoesNotExists(path: FilePath) =
set(path, false)
/** Sets if the file at the given [path] [exists]. */
fun set(path: FilePath, exists: Boolean) {
map[path.path] = exists
logger.debug("Updated FileExistCache state: ${path.path} -> $exists")
}
}

View File

@ -28,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 {
@ -42,8 +45,14 @@ 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
@ -53,20 +62,20 @@ class FileServiceImpl(
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
override fun exists(path: String): Boolean { override fun exists(path: String): Boolean {
val fullPath = path.fullPath() val fullPath = fullPath(path)
return if (fullPath in FileExistCache) { return if (fullPath in FileCache) {
FileExistCache.exists(fullPath) FileCache.exists(fullPath)
} else { } else {
withFileAt(fullPath) { withFileAt(fullPath) {
(this.exists() && this.isFile).also { (this.exists() && this.isFile).also {
FileExistCache.set(fullPath, it) 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()
@ -76,13 +85,23 @@ 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()
FileExistCache.setExists(fullPath) FileCache.setExists(fullPath)
logger.info("Created file at '${fullPath.path}'") logger.info("Created file at '${fullPath.path}'")
} }
@ -104,14 +123,19 @@ class FileServiceImpl(
this.writeBytes(data.byteArray) this.writeBytes(data.byteArray)
} }
override fun writeToDirectory(data: MultipartFile, path: String, parentPath: String, overwrite: Boolean) {
FileCache.addContent(fullPath(parentPath), fullPath(path))
write(data, path, overwrite)
}
override fun delete(path: String) { override fun delete(path: String) {
try { try {
val fullPath = path.fullPath() val fullPath = fullPath(path)
withFileAt(fullPath) { withFileAt(fullPath) {
if (!exists(path)) throw FileNotFoundException(path) if (!exists(path)) throw FileNotFoundException(path)
this.delete() this.delete()
FileExistCache.setDoesNotExists(fullPath) FileCache.setDoesNotExists(fullPath)
logger.info("Deleted file at '${fullPath.path}'") logger.info("Deleted file at '${fullPath.path}'")
} }
@ -120,16 +144,21 @@ class FileServiceImpl(
} }
} }
override fun String.fullPath(): FilePath { override fun deleteFromDirectory(path: String, parentPath: String) {
BANNED_FILE_PATH_SHARDS FileCache.removeContent(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)

View File

@ -10,17 +10,26 @@ 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.path)

View File

@ -6,12 +6,21 @@ import java.nio.file.Path
/** Mockable file wrapper, to prevent issues when mocking [java.io.File]. */ /** Mockable file wrapper, to prevent issues when mocking [java.io.File]. */
class File(val file: JavaFile) { class File(val file: JavaFile) {
val name: String
get() = file.name
val isFile: Boolean val isFile: Boolean
get() = file.isFile get() = file.isFile
val isDirectory: Boolean
get() = file.isDirectory
fun toPath(): Path = fun toPath(): Path =
file.toPath() file.toPath()
fun toFilePath(): FilePath =
FilePath(file.path)
fun exists() = fun exists() =
file.exists() file.exists()

View File

@ -8,9 +8,9 @@
%green(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{36}): %msg%n%throwable %green(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{36}): %msg%n%throwable
</Pattern> </Pattern>
</layout> </layout>
<!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter">--> <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- <level>INFO</level>--> <level>INFO</level>
<!-- </filter>--> </filter>
</appender> </appender>
<appender name="LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">

View File

@ -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())
} }
} }
} }

View File

@ -43,8 +43,8 @@ class FileServiceTest {
} }
private fun whenFileCached(cached: Boolean = true, test: () -> Unit) { private fun whenFileCached(cached: Boolean = true, test: () -> Unit) {
mockkObject(FileExistCache) { mockkObject(FileCache) {
every { FileExistCache.contains(any()) } returns cached every { FileCache.contains(any()) } returns cached
test() test()
} }
@ -106,34 +106,34 @@ class FileServiceTest {
@Test @Test
fun `exists() returns true when the file at the given path is cached as existing`() { fun `exists() returns true when the file at the given path is cached as existing`() {
whenFileCached { whenFileCached {
every { FileExistCache.exists(any()) } returns true every { FileCache.exists(any()) } returns true
assertTrue { fileService.exists(mockFilePath) } assertTrue { fileService.exists(mockFilePath) }
verify { verify {
FileExistCache.contains(any()) FileCache.contains(any())
FileExistCache.exists(any()) FileCache.exists(any())
mockFile wasNot called mockFile wasNot called
} }
confirmVerified(FileExistCache, mockFile) confirmVerified(FileCache, mockFile)
} }
} }
@Test @Test
fun `exists() returns false when the file at the given path is cached as not existing`() { fun `exists() returns false when the file at the given path is cached as not existing`() {
whenFileCached { whenFileCached {
every { FileExistCache.exists(any()) } returns false every { FileCache.exists(any()) } returns false
assertFalse { fileService.exists(mockFilePath) } assertFalse { fileService.exists(mockFilePath) }
verify { verify {
FileExistCache.contains(any()) FileCache.contains(any())
FileExistCache.exists(any()) FileCache.exists(any())
mockFile wasNot called mockFile wasNot called
} }
confirmVerified(FileExistCache, mockFile) confirmVerified(FileCache, mockFile)
} }
} }
@ -180,16 +180,16 @@ class FileServiceTest {
whenFileNotCached { whenFileNotCached {
mockkStatic(File::create) { mockkStatic(File::create) {
every { mockFile.create() } just Runs every { mockFile.create() } just Runs
every { FileExistCache.setExists(any()) } just Runs every { FileCache.setExists(any()) } just Runs
fileService.create(mockFilePath) fileService.create(mockFilePath)
verify { verify {
mockFile.create() mockFile.create()
FileExistCache.setExists(any()) FileCache.setExists(any())
} }
confirmVerified(mockFile, FileExistCache) confirmVerified(mockFile, FileCache)
} }
} }
} }
@ -278,16 +278,16 @@ class FileServiceTest {
whenMockFilePathExists { whenMockFilePathExists {
whenFileCached { whenFileCached {
every { mockFile.delete() } returns true every { mockFile.delete() } returns true
every { FileExistCache.setDoesNotExists(any()) } just Runs every { FileCache.setDoesNotExists(any()) } just Runs
fileService.delete(mockFilePath) fileService.delete(mockFilePath)
verify { verify {
mockFile.delete() mockFile.delete()
FileExistCache.setDoesNotExists(any()) FileCache.setDoesNotExists(any())
} }
confirmVerified(mockFile, FileExistCache) confirmVerified(mockFile, FileCache)
} }
} }
} }
@ -317,7 +317,7 @@ 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`() {
with(fileService) { with(fileService) {
val fullFilePath = mockFilePath.fullPath() val fullFilePath = fullPath(mockFilePath)
assertEquals("${creProperties.dataDirectory}/$mockFilePath", fullFilePath.path) assertEquals("${creProperties.dataDirectory}/$mockFilePath", fullFilePath.path)
} }
@ -329,7 +329,7 @@ class FileServiceTest {
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)
} }

View File

@ -27,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
} }
@ -61,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
} }
@ -92,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.path)
}
} }
@Test @Test