From 8ccafdafa650ae40ff2a675ff7b4b12bcdec6c09 Mon Sep 17 00:00:00 2001 From: Davin Kevin Date: Sun, 14 Jul 2024 19:03:20 +0200 Subject: [PATCH] refactor(backend): move FileStorageService to non Reactive API Related to #231 --- .../podcastserver/cover/CoverService.kt | 4 +- .../podcastserver/item/ItemHandler.kt | 2 +- .../podcastserver/item/ItemService.kt | 12 +- .../manager/downloader/AbstractDownloader.kt | 9 +- .../manager/downloader/FfmpegDownloader.kt | 15 +- .../podcastserver/podcast/PodcastHandler.kt | 2 +- .../podcastserver/podcast/PodcastService.kt | 7 +- .../service/storage/FileStorageConfig.kt | 15 +- .../service/storage/FileStorageService.kt | 206 ++++++++----- .../podcastserver/update/UpdateService.kt | 7 +- .../podcastserver/cover/CoverServiceTest.kt | 6 +- .../youtubedl/YoutubeDlDownloaderTest.kt | 7 +- .../podcastserver/item/ItemHandlerTest.kt | 4 +- .../podcastserver/item/ItemServiceTest.kt | 14 +- .../manager/downloader/DownloaderTest.kt | 6 +- .../downloader/FfmpegDownloaderTest.kt | 8 +- .../manager/downloader/RTMPDownloaderTest.kt | 8 +- .../podcast/PodcastHandlerTest.kt | 4 +- .../podcast/PodcastServiceTest.kt | 13 +- .../service/storage/FileStorageConfigTest.kt | 9 +- .../service/storage/FileStorageServiceTest.kt | 289 ++++++++++-------- .../podcastserver/update/UpdateServiceTest.kt | 12 +- 22 files changed, 362 insertions(+), 297 deletions(-) diff --git a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/cover/CoverService.kt b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/cover/CoverService.kt index 2fd65d9be..2e19b3f33 100644 --- a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/cover/CoverService.kt +++ b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/cover/CoverService.kt @@ -11,8 +11,8 @@ class CoverService( cover .findCoverOlderThan(date) .asSequence() - .filter { file.coverExists(it.podcast.title, it.item.id, it.extension).hasElement().block()!! } - .forEach { file.deleteCover(it).block() } + .filter { file.coverExists(it.podcast.title, it.item.id, it.extension) != null } + .forEach { file.deleteCover(it) } } } diff --git a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/item/ItemHandler.kt b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/item/ItemHandler.kt index 6454c4a63..feaa9547b 100644 --- a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/item/ItemHandler.kt +++ b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/item/ItemHandler.kt @@ -90,7 +90,7 @@ class ItemHandler( } private fun findCoverURIOf(item: Item, host: URI): URI { - val coverPath = fileService.coverExists(item).block() + val coverPath = fileService.coverExists(item) ?: return item.cover.url val fileDescriptor = FileDescriptor(item.podcast.title, coverPath) diff --git a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/item/ItemService.kt b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/item/ItemService.kt index 00e66934b..43382b8a7 100644 --- a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/item/ItemService.kt +++ b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/item/ItemService.kt @@ -36,7 +36,7 @@ class ItemService( log.info("Deletion of items older than {}", date) val items = repository.findAllToDelete(date) - items.forEach { file.deleteItem(it).block() } + items.forEach { file.deleteItem(it) } repository.updateAsDeleted(items.map { it.id }) } @@ -58,7 +58,7 @@ class ItemService( ?: return null if (item.isDownloaded() && item.fileName != Path("")) { - file.deleteItem(DeleteItemRequest(item.id, item.fileName!!, item.podcast.title)).block() + file.deleteItem(DeleteItemRequest(item.id, item.fileName!!, item.podcast.title)) } return repository.resetById(id)!! @@ -77,11 +77,11 @@ class ItemService( fun upload(podcastId: UUID, filePart: FilePart): Item { val filename = Paths.get(filePart.filename().replace("[^a-zA-Z0-9.-]".toRegex(), "_")) - val path = file.cache(filePart, filename).block()!! + val path = file.cache(filePart, filename) val podcast = podcastRepository.findById(podcastId)!! - file.upload(podcast.title, path).block() - val metadata = file.metadata(podcast.title, path).block()!! + file.upload(podcast.title, path) + val metadata = file.metadata(podcast.title, path)!! val (_, p2, p3) = filePart.filename().split(" - ") val title = p3.substringBeforeLast(".") @@ -122,7 +122,7 @@ class ItemService( val item = repository.deleteById(itemId) if (item !== null) { - file.deleteItem(item).block() + file.deleteItem(item) } } } diff --git a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/manager/downloader/AbstractDownloader.kt b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/manager/downloader/AbstractDownloader.kt index e4d3cf12c..522594de6 100644 --- a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/manager/downloader/AbstractDownloader.kt +++ b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/manager/downloader/AbstractDownloader.kt @@ -7,6 +7,8 @@ import com.github.davinkevin.podcastserver.entity.Status import com.github.davinkevin.podcastserver.messaging.MessagingTemplate import com.github.davinkevin.podcastserver.service.storage.FileStorageService import org.slf4j.LoggerFactory +import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.toMono import java.nio.file.Files import java.nio.file.Path import java.time.Clock @@ -73,8 +75,11 @@ abstract class AbstractDownloader( override fun finishDownload() { itemDownloadManager.removeACurrentDownload(downloadingInformation.item.id) - file.upload(downloadingInformation.item.podcast.title, target) - .then(file.metadata(downloadingInformation.item.podcast.title, target)) + val upload = Mono.defer { file.upload(downloadingInformation.item.podcast.title, target).toMono() } + val metadata = Mono.defer { file.metadata(downloadingInformation.item.podcast.title, target).toMono() } + + upload + .then(metadata) .flatMap { (mimeType, size) -> downloadRepository.finishDownload( id = downloadingInformation.item.id, diff --git a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/manager/downloader/FfmpegDownloader.kt b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/manager/downloader/FfmpegDownloader.kt index a14caeb44..07f06063a 100755 --- a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/manager/downloader/FfmpegDownloader.kt +++ b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/manager/downloader/FfmpegDownloader.kt @@ -45,16 +45,13 @@ class FfmpegDownloader( val multiDownloads = downloadingInformation.urls.map { download(it.toASCIIString()) } - Result.runCatching { - if (multiDownloads.any { it.isFailure }) { - val cause = multiDownloads.first { it.isFailure }.exceptionOrNull() - throw RuntimeException("Error during download of a part", cause) - } + if (multiDownloads.any { it.isFailure }) { + val cause = multiDownloads.first { it.isFailure }.exceptionOrNull() + throw RuntimeException("Error during download of a part", cause) + } - ffmpegService.concat( - target, - *multiDownloads.map { it.getOrNull()!! }.toTypedArray() - ) + runCatching { + ffmpegService.concat(target, *multiDownloads.map { it.getOrNull()!! }.toTypedArray()) } multiDownloads diff --git a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastHandler.kt b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastHandler.kt index 8d04d8d42..4c8abe793 100644 --- a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastHandler.kt +++ b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastHandler.kt @@ -83,7 +83,7 @@ class PodcastHandler( log.debug("the url of the podcast cover is {}", podcast.cover.url) - val uri = when(val coverPath = fileService.coverExists(podcast).block()) { + val uri = when(val coverPath = fileService.coverExists(podcast)) { is Path -> fileService.toExternalUrl(FileDescriptor(podcast.title, coverPath), host) else -> podcast.cover.url } diff --git a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastService.kt b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastService.kt index d3b21c246..beea07ad3 100644 --- a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastService.kt +++ b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastService.kt @@ -48,7 +48,7 @@ class PodcastService( cover = cover ) - fileService.downloadPodcastCover(podcast).block() + fileService.downloadPodcastCover(podcast) return podcast } @@ -70,7 +70,6 @@ class PodcastService( coverRepository.save(newCover).block()!!.also { fileService .downloadPodcastCover(p.copy(cover = Cover(it.id, it.url, it.height, it.width))) - .block() } else Cover(oldCover.id, oldCover.url, oldCover.height, oldCover.width) @@ -90,7 +89,7 @@ class PodcastService( from = p.title, to = updatePodcast.title ) - fileService.movePodcast(movePodcastDetails).block() + fileService.movePodcast(movePodcastDetails) } return podcast @@ -99,7 +98,7 @@ class PodcastService( fun deleteById(id: UUID) { val request = repository.deleteById(id) ?: return - fileService.deletePodcast(request).block() + fileService.deletePodcast(request) } } diff --git a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageConfig.kt b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageConfig.kt index 7e594fb00..7c2597729 100644 --- a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageConfig.kt +++ b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageConfig.kt @@ -5,9 +5,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.http.client.reactive.ReactorClientHttpConnector -import org.springframework.web.reactive.function.client.WebClient -import reactor.netty.http.client.HttpClient +import org.springframework.web.client.RestClient import software.amazon.awssdk.auth.credentials.AwsBasicCredentials import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.s3.S3AsyncClient @@ -24,7 +22,7 @@ class FileStorageConfig { @Bean fun fileStorageService( - webClientBuilder: WebClient.Builder, + rcb: RestClient.Builder, properties: StorageProperties, ): FileStorageService { val s3conf = S3Configuration.builder() @@ -38,6 +36,7 @@ class FileStorageConfig { .serviceConfiguration(s3conf) .endpointOverride(properties.url) .region(Region.AWS_GLOBAL) + .asyncConfiguration { } .build() val preSignerBuilder = S3Presigner.builder() @@ -51,12 +50,8 @@ class FileStorageConfig { val preSigner = if (properties.isInternal) requestSpecificPreSigner else externalPreSigner - val wcb = webClientBuilder - .clone() - .clientConnector(ReactorClientHttpConnector(HttpClient.create().followRedirect(true))) - return FileStorageService( - wcb = wcb, + rcb = rcb.clone(), bucket = bucketClient, preSignerBuilder = preSigner, properties = properties, @@ -65,7 +60,7 @@ class FileStorageConfig { @Bean fun createBucket(file: FileStorageService) = CommandLineRunner { - file.initBucket().blockOptional() + file.initBucket() } } diff --git a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageService.kt b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageService.kt index d3e96e20d..139125846 100644 --- a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageService.kt +++ b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageService.kt @@ -10,11 +10,8 @@ import org.slf4j.LoggerFactory import org.springframework.core.io.ByteArrayResource import org.springframework.http.MediaType import org.springframework.http.codec.multipart.FilePart -import org.springframework.web.reactive.function.client.WebClient -import reactor.core.publisher.Mono -import reactor.core.scheduler.Schedulers -import reactor.kotlin.core.publisher.toMono -import reactor.util.retry.Retry +import org.springframework.web.client.RestClient +import org.springframework.web.client.body import software.amazon.awssdk.core.async.AsyncRequestBody import software.amazon.awssdk.services.s3.S3AsyncClient import software.amazon.awssdk.services.s3.model.* @@ -26,12 +23,13 @@ import java.time.Duration import java.util.* import kotlin.io.path.Path import kotlin.io.path.extension +import kotlin.random.Random /** * Created by kevin on 2019-02-09 */ class FileStorageService( - private val wcb: WebClient.Builder, + private val rcb: RestClient.Builder, private val bucket: S3AsyncClient, private val preSignerBuilder: (URI) -> S3Presigner, private val properties: StorageProperties, @@ -39,126 +37,171 @@ class FileStorageService( private val log = LoggerFactory.getLogger(FileStorageService::class.java) - fun deletePodcast(podcast: DeletePodcastRequest) = Mono.defer { + fun deletePodcast(podcast: DeletePodcastRequest): Boolean { log.info("Deletion of podcast {}", podcast.title) - bucket.listObjects { it.bucket(properties.bucket).prefix(podcast.title) }.toMono() - .flatMapIterable { it.contents() } - .flatMap { bucket.deleteObject(it.toDeleteRequest()).toMono() } - .then(true.toMono()) - .onErrorReturn(false) + val result = bucket.listObjects { it.bucket(properties.bucket).prefix(podcast.title) } + .runCatching { join() } + .getOrNull() ?: return false + + return result + .contents() + .map { bucket.deleteObject(it.toDeleteRequest()) } + .map { it.runCatching { join() } } + .all { it.isSuccess } } - fun deleteItem(item: DeleteItemRequest): Mono = Mono.defer { + fun deleteItem(item: DeleteItemRequest): Boolean { val path = "${item.podcastTitle}/${item.fileName}" log.info("Deletion of file {}", path) - bucket.deleteObject { it.bucket(properties.bucket).key(path) }.toMono() - .map { true } - .onErrorReturn(false) + return bucket.deleteObject { it.bucket(properties.bucket).key(path) } + .runCatching { join() } + .isSuccess } - fun deleteCover(cover: DeleteCoverRequest): Mono = Mono.defer { + fun deleteCover(cover: DeleteCoverRequest): Boolean { val path = "${cover.podcast.title}/${cover.item.id}.${cover.extension}" log.info("Deletion of file {}", path) - bucket.deleteObject { it.bucket(properties.bucket).key(path) }.toMono() - .map { true } - .onErrorReturn(false) + return bucket.deleteObject { it.bucket(properties.bucket).key(path) } + .runCatching { join() } + .isSuccess } - fun coverExists(p: Podcast): Mono = coverExists(p.title, p.id, p.cover.extension()) - fun coverExists(i: Item): Mono = coverExists(i.podcast.title, i.id, i.cover.extension()) - fun coverExists(podcastTitle: String, itemId: UUID, extension: String): Mono { + fun coverExists(p: Podcast) = coverExists(p.title, p.id, p.cover.extension()) + fun coverExists(i: Item) = coverExists(i.podcast.title, i.id, i.cover.extension()) + fun coverExists(podcastTitle: String, itemId: UUID, extension: String): Path? { val path = "$podcastTitle/$itemId.$extension" - return bucket.headObject { it.bucket(properties.bucket).key(path) }.toMono() - .map { true } - .onErrorReturn(false) - .filter { it } - .map { path.substringAfterLast("/") } - .map { Path(it) } + + val result = bucket.headObject { it.bucket(properties.bucket).key(path) } + .runCatching { join() } + + if (result.isFailure) { + return null + } + + return path.substringAfterLast("/") + .let(::Path) } - private fun download(url: URI) = wcb.clone() + private fun download(url: URI): ByteArrayResource? = rcb.clone() .baseUrl(url.toASCIIString()) .build() .get() .accept(MediaType.APPLICATION_OCTET_STREAM) .retrieve() - .bodyToMono(ByteArrayResource::class.java) + .body() - private fun upload(key: String, resource: ByteArrayResource): Mono { + private fun upload(key: String, resource: ByteArrayResource): PutObjectResponse? { val request = PutObjectRequest.builder() .bucket(properties.bucket) .acl(ObjectCannedACL.PUBLIC_READ) .key(key) .build() - return bucket.putObject(request, AsyncRequestBody.fromBytes(resource.byteArray)).toMono() + return bucket.putObject(request, AsyncRequestBody.fromBytes(resource.byteArray)) + .runCatching { join() } + .getOrNull() } - fun downloadPodcastCover(podcast: Podcast): Mono = - download(podcast.cover.url) - .flatMap { upload("""${podcast.title}/${podcast.id}.${podcast.cover.extension()}""", it) } - .then() + fun downloadPodcastCover(podcast: Podcast) { + val response = download(podcast.cover.url) + ?: return - fun downloadItemCover(item: Item): Mono = - download(item.cover.url) - .flatMap { upload("""${item.podcast.title}/${item.id}.${item.cover.extension()}""", it) } - .then() + upload("""${podcast.title}/${podcast.id}.${podcast.cover.extension()}""", response) + } + + fun downloadItemCover(item: Item) { + val response = download(item.cover.url) + ?: return + + upload("""${item.podcast.title}/${item.id}.${item.cover.extension()}""", response) + } - fun movePodcast(request: MovePodcastRequest): Mono = Mono.defer { + fun movePodcast(request: MovePodcastRequest) { val listRequest = ListObjectsRequest.builder() .bucket(properties.bucket) .prefix(request.from) .build() - bucket.listObjects(listRequest).toMono() - .flatMapIterable { it.contents() } - .flatMap { bucket - .copyObject(it.toCopyRequest(request.to)).toMono() - .then(Mono.defer { bucket.deleteObject(it.toDeleteRequest()).toMono() }) - } - .then() + val result = bucket.listObjects(listRequest) + .runCatching { join() } + .getOrNull() ?: return + + result.contents().forEach { + val copy = it.toCopyRequest(request.to) + val delete = it.toDeleteRequest() + + bucket.copyObject(copy).runCatching { join() } + .onFailure { t -> log.error("Error during copy of ${it.key()}", t) } + .getOrNull() ?: return@forEach + + bucket.deleteObject(delete).runCatching { join() } + .onFailure { t -> log.error("Error during deletion of ${it.key()}", t) } + .getOrNull() ?: return@forEach + } } - fun cache(filePart: FilePart, destination: Path): Mono = Mono.defer { - Files.createTempDirectory("upload-temp") - .toMono() - .map { it.resolve(destination.fileName) } - .subscribeOn(Schedulers.boundedElastic()) - .publishOn(Schedulers.parallel()) - .flatMap { - filePart.transferTo(it) - .then(it.toMono()) - } + fun cache(filePart: FilePart, destination: Path): Path { + val tmpDirectory = Files.createTempDirectory("upload-temp") + val dest = tmpDirectory.resolve(destination.fileName) + + filePart.transferTo(dest).block() + + return dest } - .subscribeOn(Schedulers.boundedElastic()) - .publishOn(Schedulers.parallel()) - fun upload(podcastTitle: String, file: Path): Mono { + fun upload(podcastTitle: String, file: Path): PutObjectResponse? { val request = PutObjectRequest.builder() .bucket(properties.bucket) .acl(ObjectCannedACL.PUBLIC_READ) .key("$podcastTitle/${file.fileName}") .build() - return bucket.putObject(request, file).toMono() - .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) - .delayUntil { - Files.deleteIfExists(file).toMono() - .subscribeOn(Schedulers.boundedElastic()) - .publishOn(Schedulers.parallel()) + val response = retry { bucket.putObject(request, file).join() } + .onFailure { log.error("Error during upload of file ${file.fileName} to ${request.key()}", it) } + .getOrNull() + + Files.deleteIfExists(file) + + return response + } + + private fun retry(block: () -> T): Result { + val retries = 3 + val delay = Duration.ofSeconds(1) + + val all = (0 .. retries) + .asSequence() + .onEach { + if (it == 0) return@onEach + + val waitTime = delay.plusSeconds(Random.nextDouble(0.0, 0.5).toLong()) + + Thread.sleep(waitTime) } + .map { runCatching { block() } } + .toList() + + val firstSuccess = all.firstOrNull { it.isSuccess } + if (firstSuccess != null) { + return firstSuccess + } + + return all.first { it.isFailure } } - fun metadata(title: String, file: Path): Mono { + fun metadata(title: String, file: Path): FileMetaData? { val key = "$title/${file.fileName}" - return Mono.defer { bucket.headObject { it.bucket(properties.bucket).key(key) }.toMono() } - .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) - .map { FileMetaData(contentType = it.contentType(), size = it.contentLength()) } + + val result = retry { bucket.headObject { it.bucket(properties.bucket).key(key) }.join() } + .getOrNull() + ?: return null + + return FileMetaData(contentType = result.contentType(), size = result.contentLength()) } private fun S3Object.toDeleteRequest(bucket: String = properties.bucket): DeleteObjectRequest = @@ -175,14 +218,17 @@ class FileStorageService( .destinationKey("$key/" + this.key().substringAfterLast("/")) .build() - fun initBucket(): Mono { - return bucket.headBucket { it.bucket(properties.bucket) }.toMono() - .doOnSuccess { log.info("🗂 Bucket already present") } - .then() - .onErrorResume { bucket.createBucket { it.bucket(properties.bucket) }.toMono() - .doOnSuccess { log.info("✅ Bucket creation done") } - .then() - } + fun initBucket() { + val result = bucket.headBucket { it.bucket(properties.bucket) } + .runCatching { join() } + + if (result.isSuccess) { + log.info("🗂 Bucket already present") + return + } + + bucket.createBucket { it.bucket(properties.bucket) }.join() + log.info("✅ Bucket creation done") } fun toExternalUrl(file: FileDescriptor, requestedHost: URI): URI { diff --git a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/UpdateService.kt b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/UpdateService.kt index 94106ea51..20b7aa5bf 100644 --- a/backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/UpdateService.kt +++ b/backend/src/main/kotlin/com/github/davinkevin/podcastserver/update/UpdateService.kt @@ -114,11 +114,8 @@ class UpdateService( } createdItems.forEach { item -> - fileService.downloadItemCover(item).onErrorResume { - log.error("Error during download of cover ${item.cover.url}") - Mono.empty() - } - .block() + runCatching { fileService.downloadItemCover(item) } + .onFailure { log.error("Error during download of cover ${item.cover.url}") } } podcastRepository.updateLastUpdate(podcast.id) diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/cover/CoverServiceTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/cover/CoverServiceTest.kt index 1fbbc0720..04171680e 100644 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/cover/CoverServiceTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/cover/CoverServiceTest.kt @@ -61,8 +61,8 @@ class CoverServiceTest ( randomCover("item3", "podcast3") ) whenever(cover.findCoverOlderThan(date)).thenReturn(covers) - whenever(file.coverExists(any(), any(), any())).thenReturn(Mono.just(Path("file.mp3"))) - whenever(file.deleteCover(any())).thenReturn(Mono.empty()) + whenever(file.coverExists(any(), any(), any())).thenReturn(Path("file.mp3")) + whenever(file.deleteCover(any())).thenReturn(true) /* When */ service.deleteCoversInFileSystemOlderThan(date) @@ -77,7 +77,7 @@ class CoverServiceTest ( val covers = listOf(randomCover("item1", "podcast1")) whenever(cover.findCoverOlderThan(date)).thenReturn(covers) - whenever(file.coverExists(any(), any(), any())).thenReturn(Mono.empty()) + whenever(file.coverExists(any(), any(), any())).thenReturn(null) /* When */ service.deleteCoversInFileSystemOlderThan(date) diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/download/downloaders/youtubedl/YoutubeDlDownloaderTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/download/downloaders/youtubedl/YoutubeDlDownloaderTest.kt index 3c47eeac9..d71c95ee5 100644 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/download/downloaders/youtubedl/YoutubeDlDownloaderTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/download/downloaders/youtubedl/YoutubeDlDownloaderTest.kt @@ -28,7 +28,6 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import import org.springframework.test.context.junit.jupiter.SpringExtension import reactor.core.publisher.Mono -import reactor.kotlin.core.publisher.toMono import software.amazon.awssdk.services.s3.model.PutObjectResponse import java.net.URI import java.nio.file.Files @@ -97,8 +96,8 @@ class YoutubeDlDownloaderTest( downloader.itemDownloadManager = idm whenever(file.upload(eq(dItem.item.podcast.title), any())) - .thenReturn(PutObjectResponse.builder().build().toMono()) - whenever(file.metadata(eq(dItem.item.podcast.title), any())).thenReturn(FileMetaData("foo/bar", 123L).toMono()) + .thenReturn(PutObjectResponse.builder().build()) + whenever(file.metadata(eq(dItem.item.podcast.title), any())).thenReturn(FileMetaData("foo/bar", 123L)) whenever(downloadRepository.updateDownloadItem(any())).thenReturn(Mono.empty()) whenever(downloadRepository.finishDownload(any(), any(), anyOrNull(), any(), any())) .thenReturn(Mono.empty()) @@ -129,7 +128,7 @@ class YoutubeDlDownloaderTest( @DisplayName("but fails due to error") inner class ButFailsDueToError { - val url = URI.create("https://foo.bar.com/one.mp3") + val url: URI = URI.create("https://foo.bar.com/one.mp3") @Nested @DisplayName("DuringDownload") diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/item/ItemHandlerTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/item/ItemHandlerTest.kt index 445bf3cc9..ab9a5231a 100644 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/item/ItemHandlerTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/item/ItemHandlerTest.kt @@ -179,7 +179,7 @@ class ItemHandlerTest( /* Given */ val host = URI.create("https://localhost:8080/") whenever(itemService.findById(item.id)).thenReturn(item) - whenever(fileService.coverExists(any())).thenReturn(Path("${item.id}.png").toMono()) + whenever(fileService.coverExists(any())).thenReturn(Path("${item.id}.png")) whenever(fileService.toExternalUrl(FileDescriptor(item.podcast.title, Path("${item.id}.png")), host)) .thenReturn(URI.create("https://localhost:8080/data/Podcast%20Bar/27184b1a-7642-4ffd-ac7e-14fb36f7f15c.png")) @@ -198,7 +198,7 @@ class ItemHandlerTest( fun `by redirecting to external file if cover does not exist locally`() { /* Given */ whenever(itemService.findById(item.id)).thenReturn(item) - whenever(fileService.coverExists(item)).thenReturn(Mono.empty()) + whenever(fileService.coverExists(item)).thenReturn(null) /* When */ rest diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/item/ItemServiceTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/item/ItemServiceTest.kt index 20fff3cc7..c7cd9fc3f 100644 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/item/ItemServiceTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/item/ItemServiceTest.kt @@ -85,7 +85,7 @@ class ItemServiceTest( DeleteItemRequest(UUID.fromString("40430ce3-b421-4c82-b34d-2deb4c46b1cd"), Path("itemT"), "podcastT") ) whenever(repository.findAllToDelete(limit)).thenReturn(items) - whenever(fileService.deleteItem(any())).thenReturn(Mono.empty()) + whenever(fileService.deleteItem(any())).thenReturn(true) doNothing().whenever(repository).updateAsDeleted(any()) /* When */ @@ -206,7 +206,7 @@ class ItemServiceTest( whenever(idm.isInDownloadingQueueById(item.id)).thenReturn(false.toMono()) whenever(repository.hasToBeDeleted(item.id)).thenReturn(true) whenever(repository.findById(item.id)).thenReturn(currentItem) - whenever(fileService.deleteItem(deleteItemInformation)).thenReturn(Mono.empty()) + whenever(fileService.deleteItem(deleteItemInformation)).thenReturn(true) /* When */ val resetItem = itemService.reset(item.id)!! @@ -328,14 +328,14 @@ class ItemServiceTest( val id = UUID.randomUUID() val deleteItem = DeleteItemRequest(id, Path("foo"), "bar") whenever(repository.deleteById(id)).thenReturn(deleteItem) - whenever(fileService.deleteItem(deleteItem)).thenReturn(Mono.empty()) + whenever(fileService.deleteItem(deleteItem)).thenReturn(true) whenever(idm.removeItemFromQueueAndDownload(id)).thenReturn(Mono.empty()) /* When */ itemService.deleteById(id) /* Then */ - verify(fileService, times(1)).deleteItem(deleteItem) + verify(fileService).deleteItem(deleteItem) } } @@ -408,10 +408,10 @@ class ItemServiceTest( val normalizedFileName = Paths.get(fileName.replace("[^a-zA-Z0-9.-]".toRegex(), "_")) whenever(file.filename()).thenReturn(fileName) whenever(podcastRepository.findById(podcast.id)).thenReturn(podcast) - whenever(fileService.cache(file, normalizedFileName)).thenReturn(normalizedFileName.toMono()) - whenever(fileService.upload(podcast.title, normalizedFileName)).thenReturn(Mono.empty()) + whenever(fileService.cache(file, normalizedFileName)).thenReturn(normalizedFileName) + whenever(fileService.upload(podcast.title, normalizedFileName)).thenReturn(null) whenever(fileService.metadata(podcast.title, normalizedFileName)).thenReturn( - FileMetaData("audio/mp3", 1234L).toMono() + FileMetaData("audio/mp3", 1234L) ) whenever(repository.create(itemToCreate)).thenReturn(itemCreated) doNothing().whenever(podcastRepository).updateLastUpdate(podcast.id) diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/DownloaderTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/DownloaderTest.kt index cf3495306..f125a05ce 100644 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/DownloaderTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/DownloaderTest.kt @@ -98,13 +98,13 @@ class DownloaderTest { @Test @Suppress("UnassignedFluxMonoInstance") - fun `should failed if error occurs during finish method`() { + fun `should fail if error occurs during finish method`() { /* Given */ val information = DownloadingInformation(item, listOf(), Path("file.mp4"), null) downloader.with(information, itemDownloadManager) whenever(downloadRepository.updateDownloadItem(any())).thenReturn(Mono.just(1)) - whenever(file.upload(any(), any())).thenReturn(RuntimeException("not expected error").toMono()) - whenever(file.metadata(any(), any())).thenReturn(RuntimeException("not expected error").toMono()) + whenever(file.upload(any(), any())).thenThrow(RuntimeException("not expected error")) + whenever(file.metadata(any(), any())).thenThrow(RuntimeException("not expected error")) /* When */ downloader.apply { diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/FfmpegDownloaderTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/FfmpegDownloaderTest.kt index 03fa39a1a..bb6f9b269 100755 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/FfmpegDownloaderTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/FfmpegDownloaderTest.kt @@ -112,9 +112,9 @@ class FfmpegDownloaderTest { doAnswer { writeEmptyFileTo(it.getArgument(0).toString()); null }.whenever(ffmpegService).concat(any(), anyVararg()) whenever(file.upload(eq(item.podcast.title), any())) - .thenReturn(PutObjectResponse.builder().build().toMono()) + .thenReturn(PutObjectResponse.builder().build()) whenever(file.metadata(eq(item.podcast.title), any())) - .thenReturn(FileMetaData("video/mp4", 123L).toMono()) + .thenReturn(FileMetaData("video/mp4", 123L)) whenever(downloadRepository.finishDownload( id = item.id, length = 123L, @@ -132,7 +132,7 @@ class FfmpegDownloaderTest { } @Test - fun `should ends on FAILED if one of download failed`() { + fun `should end on FAILED if a download has failed`() { /* Given */ whenever(ffmpegService.getDurationOf(any(), any())).thenReturn(500.0) @@ -142,7 +142,7 @@ class FfmpegDownloaderTest { }.whenever(ffmpegService).download(eq(item.url.toASCIIString()), any(), any()) doAnswer { throw RuntimeException("Error during download of other url") } - .whenever(ffmpegService).download(eq("http://foo.bar.com/end.mp4"), any(), any()) + .whenever(ffmpegService).download(eq("http://foo.bar.com/end.mp4"), any(), any()) whenever(processService.waitFor(any())).thenReturn(Result.success(1)) diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/RTMPDownloaderTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/RTMPDownloaderTest.kt index 98749b883..bab909824 100644 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/RTMPDownloaderTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/manager/downloader/RTMPDownloaderTest.kt @@ -191,9 +191,9 @@ class RTMPDownloaderTest { fun `and save file to disk`() { /* Given */ whenever(file.upload(eq(item.podcast.title), any())) - .thenReturn(PutObjectResponse.builder().build().toMono()) + .thenReturn(PutObjectResponse.builder().build()) whenever(file.metadata(eq(item.podcast.title), any())) - .thenReturn(FileMetaData("video/mp4", 123L).toMono()) + .thenReturn(FileMetaData("video/mp4", 123L)) whenever(downloadRepository.updateDownloadItem(any())).thenReturn(Mono.empty()) whenever(downloadRepository.finishDownload( id = item.id, @@ -226,9 +226,9 @@ class RTMPDownloaderTest { fun stop() { /* GIVEN */ whenever(file.upload(eq(item.podcast.title), any())) - .thenReturn(PutObjectResponse.builder().build().toMono()) + .thenReturn(PutObjectResponse.builder().build()) whenever(file.metadata(eq(item.podcast.title), any())) - .thenReturn(FileMetaData("video/mp4", 123L).toMono()) + .thenReturn(FileMetaData("video/mp4", 123L)) whenever(downloadRepository.finishDownload( id = item.id, length = 123L, diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastHandlerTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastHandlerTest.kt index 044785f66..914d3b508 100644 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastHandlerTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastHandlerTest.kt @@ -631,7 +631,7 @@ class PodcastHandlerTest( val host = URI.create("https://localhost:8080/") whenever(podcastService.findById(podcast.id)).thenReturn(podcast) whenever(fileService.coverExists(podcast)).thenReturn( - Path(podcast.cover.url.toASCIIString().substringAfterLast("/")).toMono() + Path(podcast.cover.url.toASCIIString().substringAfterLast("/")) ) whenever(fileService.toExternalUrl(FileDescriptor(podcast.title, Path("cover.png")), host)) .thenReturn(URI.create("https://localhost:8080/data/Podcast%20title/cover.png")) @@ -650,7 +650,7 @@ class PodcastHandlerTest( fun `by redirecting to external file if cover does not exist locally`() { /* Given */ whenever(podcastService.findById(podcast.id)).thenReturn(podcast) - whenever(fileService.coverExists(podcast)).thenReturn(Mono.empty()) + whenever(fileService.coverExists(podcast)).thenReturn(null) /* When */ rest diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastServiceTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastServiceTest.kt index 8426261c3..898cb5664 100644 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastServiceTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/podcast/PodcastServiceTest.kt @@ -289,7 +289,7 @@ class PodcastServiceTest( @BeforeEach fun beforeEach() { - whenever(fileService.downloadPodcastCover(podcast)).thenReturn(Mono.empty()) + doNothing().whenever(fileService).downloadPodcastCover(podcast) } @AfterEach @@ -639,8 +639,7 @@ class PodcastServiceTest( whenever(repository.findById(p.id)).thenReturn(podcast) whenever(coverRepository.save(newCover)).thenReturn(coverInDb.toMono()) - whenever(fileService.downloadPodcastCover(argThat { title == p.title && cover.url == newCover.url })) - .thenReturn(Mono.empty()) + doNothing().whenever(fileService).downloadPodcastCover(argThat { title == p.title && cover.url == newCover.url }) whenever(repository.update( id = eq(p.id), title = eq(p.title), @@ -656,7 +655,7 @@ class PodcastServiceTest( /* Then */ assertThat(podcastAfterUpdate).isEqualTo(p) verify(coverRepository, times(1)).save(any()) - verify(fileService, times(1)).downloadPodcastCover(any()) + verify(fileService).downloadPodcastCover(any()) } @@ -692,7 +691,7 @@ class PodcastServiceTest( from = p.title, to = pToUpdate.title ) - whenever(fileService.movePodcast(moveOperation)).thenReturn(Mono.empty()) + doNothing().whenever(fileService).movePodcast(moveOperation) /* When */ val podcastAfterUpdate = service.update(pToUpdate) @@ -702,7 +701,7 @@ class PodcastServiceTest( verify(tagRepository, never()).save(any()) verify(coverRepository, never()).save(any()) verify(fileService, never()).downloadPodcastCover(any()) - verify(fileService, times(1)).movePodcast(moveOperation) + verify(fileService).movePodcast(moveOperation) } } @@ -721,7 +720,7 @@ class PodcastServiceTest( val id = UUID.randomUUID() val information = DeletePodcastRequest(id, "foo") whenever(repository.deleteById(id)).thenReturn(information) - whenever(fileService.deletePodcast(information)).thenReturn(Mono.empty()) + whenever(fileService.deletePodcast(information)).thenReturn(true) /* When */ service.deleteById(id) diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageConfigTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageConfigTest.kt index ddbd1b3de..681df555c 100644 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageConfigTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageConfigTest.kt @@ -2,10 +2,7 @@ package com.github.davinkevin.podcastserver.service.storage import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever +import org.mockito.kotlin.* import org.springframework.boot.CommandLineRunner import org.springframework.boot.test.context.runner.ApplicationContextRunner import reactor.core.publisher.Mono @@ -25,7 +22,7 @@ class FileStorageConfigTest { fun `should init bucket`() { /* Given */ val service: FileStorageService = mock() - whenever(service.initBucket()).thenReturn(Mono.empty()) + doNothing().whenever(service).initBucket() /* When */ context.withPropertyValues( @@ -43,7 +40,7 @@ class FileStorageConfigTest { it.getBean(CommandLineRunner::class.java) .run() - verify(service, times(1)).initBucket() + verify(service).initBucket() } } } diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageServiceTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageServiceTest.kt index b8b3acd68..2ec9e464b 100644 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageServiceTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/service/storage/FileStorageServiceTest.kt @@ -11,8 +11,6 @@ import com.github.davinkevin.podcastserver.podcast.DeletePodcastRequest import com.github.davinkevin.podcastserver.podcast.Podcast import com.github.davinkevin.podcastserver.tag.Tag import com.github.tomakehurst.wiremock.client.WireMock.* -import com.github.tomakehurst.wiremock.common.ConsoleNotifier -import com.github.tomakehurst.wiremock.common.Slf4jNotifier import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig import com.github.tomakehurst.wiremock.http.RequestMethod import com.github.tomakehurst.wiremock.junit5.WireMockExtension @@ -28,14 +26,13 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.ImportAutoConfiguration -import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration import org.springframework.context.annotation.Import import org.springframework.http.codec.multipart.FilePart import org.springframework.test.context.TestPropertySource import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.util.DigestUtils -import org.springframework.web.reactive.function.client.WebClient -import reactor.core.publisher.Hooks +import org.springframework.web.client.RestClient import reactor.kotlin.core.publisher.toMono import reactor.test.StepVerifier import java.net.URI @@ -45,8 +42,8 @@ import java.nio.file.Paths import java.time.OffsetDateTime import java.time.ZoneOffset import java.util.* -import kotlin.io.path.writeText import kotlin.io.path.Path +import kotlin.io.path.writeText /** * Created by kevin on 2019-02-12 @@ -62,20 +59,11 @@ const val s3MockBackendPort = 1234 "podcastserver.storage.url=http://localhost:$s3MockBackendPort/", ]) @ExtendWith(SpringExtension::class) -@ImportAutoConfiguration(WebClientAutoConfiguration::class) +@ImportAutoConfiguration(RestClientAutoConfiguration::class) class FileStorageServiceTest( @Autowired val fileService: FileStorageService ) { - @JvmField - @RegisterExtension - val s3Backend: WireMockExtension = WireMockExtension.newInstance() - .options(wireMockConfig() - .port(s3MockBackendPort) -// .notifier(ConsoleNotifier(true)) - ) - .build() - @Nested @DisplayName("should delete podcast") inner class ShouldDeletePodcast { @@ -102,11 +90,10 @@ class FileStorageServiceTest( s3Backend.stubFor(delete("/data/podcast-title/second.mp3").willReturn(ok())) /* When */ - StepVerifier.create(fileService.deletePodcast(request)) - /* Then */ - .expectSubscription() - .expectNext(true) - .verifyComplete() + val result = fileService.deletePodcast(request) + + /* Then */ + assertThat(result).isTrue() } @Test @@ -129,11 +116,10 @@ class FileStorageServiceTest( s3Backend.stubFor(delete("/data/podcast-title/second.mp3").willReturn(notFound())) /* When */ - StepVerifier.create(fileService.deletePodcast(request)) - /* Then */ - .expectSubscription() - .expectNext(false) - .verifyComplete() + val result = fileService.deletePodcast(request) + + /* Then */ + assertThat(result).isFalse() } @Test @@ -142,12 +128,10 @@ class FileStorageServiceTest( s3Backend.stubFor(get("/data?prefix=podcast-title").willReturn(notFound())) /* When */ - StepVerifier.create(fileService.deletePodcast(request)) - /* Then */ - .expectSubscription() - .expectNext(false) - .verifyComplete() + val result = fileService.deletePodcast(request) + /* Then */ + assertThat(result).isFalse() } } @@ -164,11 +148,10 @@ class FileStorageServiceTest( s3Backend.stubFor(delete("/data/podcast-title/foo.txt").willReturn(ok())) /* When */ - StepVerifier.create(fileService.deleteItem(request)) - .expectSubscription() - /* Then */ - .expectNext(true) - .verifyComplete() + val isDeleted = fileService.deleteItem(request) + + /* Then */ + assertThat(isDeleted).isTrue() } @Test @@ -177,11 +160,10 @@ class FileStorageServiceTest( s3Backend.stubFor(delete("/data/podcast-title/foo.txt").willReturn(notFound())) /* When */ - StepVerifier.create(fileService.deleteItem(request)) - .expectSubscription() - /* Then */ - .expectNext(false) - .verifyComplete() + val isDeleted = fileService.deleteItem(request) + + /* Then */ + assertThat(isDeleted).isFalse() } } @@ -202,11 +184,10 @@ class FileStorageServiceTest( .willReturn(ok())) /* When */ - StepVerifier.create(fileService.deleteCover(request)) - .expectSubscription() - /* Then */ - .expectNext(true) - .verifyComplete() + val isDeleted = fileService.deleteCover(request) + + /* Then */ + assertThat(isDeleted).isTrue() } @Test @@ -216,11 +197,10 @@ class FileStorageServiceTest( .willReturn(notFound())) /* When */ - StepVerifier.create(fileService.deleteCover(request)) - .expectSubscription() - /* Then */ - .expectNext(false) - .verifyComplete() + val isDeleted = fileService.deleteCover(request) + + /* Then */ + assertThat(isDeleted).isFalse() } } @@ -257,11 +237,10 @@ class FileStorageServiceTest( .willReturn(ok())) /* When */ - StepVerifier.create(fileService.coverExists(podcast)) - .expectSubscription() - /* Then */ - .expectNext(Path("dd16b2eb-657e-4064-b470-5b99397ce729.png")) - .verifyComplete() + val coverPath = fileService.coverExists(podcast) + + /* Then */ + assertThat(coverPath).isEqualTo(Path("dd16b2eb-657e-4064-b470-5b99397ce729.png")) } @Test @@ -274,11 +253,10 @@ class FileStorageServiceTest( .willReturn(ok())) /* When */ - StepVerifier.create(fileService.coverExists(specificPodcast)) - .expectSubscription() - /* Then */ - .expectNext(Path("dd16b2eb-657e-4064-b470-5b99397ce729.jpg")) - .verifyComplete() + val coverPath = fileService.coverExists(specificPodcast) + + /* Then */ + assertThat(coverPath).isEqualTo(Path("dd16b2eb-657e-4064-b470-5b99397ce729.jpg")) } @Test @@ -286,11 +264,12 @@ class FileStorageServiceTest( /* Given */ s3Backend.stubFor(head(urlEqualTo("/data/podcast-title/dd16b2eb-657e-4064-b470-5b99397ce729.png")) .willReturn(notFound())) + /* When */ - StepVerifier.create(fileService.coverExists(podcast)) - .expectSubscription() - /* Then */ - .verifyComplete() + val coverPath = fileService.coverExists(podcast) + + /* Then */ + assertThat(coverPath).isNull() } } @@ -334,11 +313,10 @@ class FileStorageServiceTest( .willReturn(ok())) /* When */ - StepVerifier.create(fileService.coverExists(item)) - .expectSubscription() - /* Then */ - .expectNext(Path("27184b1a-7642-4ffd-ac7e-14fb36f7f15c.png")) - .verifyComplete() + val coverPath = fileService.coverExists(item) + + /* Then */ + assertThat(coverPath).isEqualTo(Path("27184b1a-7642-4ffd-ac7e-14fb36f7f15c.png")) } @Test @@ -353,11 +331,10 @@ class FileStorageServiceTest( .willReturn(ok())) /* When */ - StepVerifier.create(fileService.coverExists(specificItem)) - .expectSubscription() - /* Then */ - .expectNext(Path("27184b1a-7642-4ffd-ac7e-14fb36f7f15c.jpg")) - .verifyComplete() + val coverPath = fileService.coverExists(specificItem) + + /* Then */ + assertThat(coverPath).isEqualTo(Path("27184b1a-7642-4ffd-ac7e-14fb36f7f15c.jpg")) } @Test @@ -365,11 +342,12 @@ class FileStorageServiceTest( /* Given */ s3Backend.stubFor(head(urlEqualTo("/data/podcast-title/27184b1a-7642-4ffd-ac7e-14fb36f7f15c.png")) .willReturn(notFound())) + /* When */ - StepVerifier.create(fileService.coverExists(item)) - .expectSubscription() - /* Then */ - .verifyComplete() + val coverPath = fileService.coverExists(item) + + /* Then */ + assertThat(coverPath).isEqualTo(null) } } } @@ -411,12 +389,11 @@ class FileStorageServiceTest( /* Given */ externalBackend.stubFor(get("/img/image.png").willReturn(ok().withBody(fileAsByteArray("/__files/img/image.png")))) s3Backend.stubFor(put("/data/podcast-title/dd16b2eb-657e-4064-b470-5b99397ce729.png").willReturn(ok())) + /* When */ - StepVerifier.create(fileService.downloadPodcastCover(podcast)) - /* Then */ - .expectSubscription() - .verifyComplete() + fileService.downloadPodcastCover(podcast) + /* Then */ val bodyDigest = s3Backend.findAll(newRequestPattern(RequestMethod.PUT, urlEqualTo("/data/podcast-title/dd16b2eb-657e-4064-b470-5b99397ce729.png"))) .first().body .let(DigestUtils::md5DigestAsHex) @@ -466,18 +443,9 @@ class FileStorageServiceTest( s3Backend.stubFor(put(urlInBucket).willReturn(ok())) /* When */ - StepVerifier.create(fileService.downloadItemCover(item)) - /* Then */ - .expectSubscription() - .verifyComplete() - -// val resultingFile = dir -// .resolve(item.podcast.title) -// .resolve("${item.id}.png") -// -// assertThat(resultingFile) -// .exists() -// .hasDigest("MD5", "1cc21d3dce8bfedbda2d867a3238e8db") + fileService.downloadItemCover(item) + + /* Then */ val bodyDigest = s3Backend.findAll(newRequestPattern(RequestMethod.PUT, urlEqualTo(urlInBucket))) .first().body .let(DigestUtils::md5DigestAsHex) @@ -527,10 +495,18 @@ class FileStorageServiceTest( } /* When */ - StepVerifier.create(fileService.movePodcast(moveOperation)) - /* Then */ - .expectSubscription() - .verifyComplete() + fileService.movePodcast(moveOperation) + + /* Then */ + s3Backend.apply { + verify(putRequestedFor(urlEqualTo("/data/destination/first.mp3")) + .withHeader("x-amz-copy-source", equalTo("data/origin/first.mp3"))) + verify(putRequestedFor(urlEqualTo("/data/destination/second.mp3")) + .withHeader("x-amz-copy-source", equalTo("data/origin/second.mp3"))) + verify(deleteRequestedFor(urlEqualTo("/data/origin/first.mp3"))) + verify(deleteRequestedFor(urlEqualTo("/data/origin/second.mp3"))) + + } } } @@ -546,11 +522,10 @@ class FileStorageServiceTest( .then { Files.createFile(it.getArgument(0)).toMono().then() } /* When */ - StepVerifier.create(fileService.cache(file, Paths.get("foo.mp3"))) - /* Then */ - .expectSubscription() - .assertNext { assertThat(it).exists() } - .verifyComplete() + val path = fileService.cache(file, Paths.get("foo.mp3")) + + /* Then */ + assertThat(path).exists() } } @@ -564,12 +539,12 @@ class FileStorageServiceTest( /* Given */ val file = dir.resolve("toUpload.txt").apply { writeText("text is here !") } s3Backend.stubFor(put("/data/podcast-title/toUpload.txt").willReturn(ok())) + /* When */ - StepVerifier.create(fileService.upload("podcast-title", file)) - /* Then */ - .expectSubscription() - .expectNextCount(1) - .verifyComplete() + val result = fileService.upload("podcast-title", file) + + /* Then */ + assertThat(result).isNotNull() val textContent = s3Backend.findAll(newRequestPattern(RequestMethod.PUT, urlEqualTo("/data/podcast-title/toUpload.txt"))) .first().body @@ -578,6 +553,46 @@ class FileStorageServiceTest( assertThat(textContent).isEqualTo("text is here !") } + @Test + fun `with success after second retry`(@TempDir dir: Path) { + /* Given */ + val file = dir.resolve("toUpload.txt").apply { writeText("text is here !") } + s3Backend.apply { + stubFor(put("/data/podcast-title/toUpload.txt").willReturn(badRequest())) + stubFor(put("/data/podcast-title/toUpload.txt").willReturn(ok())) + } + + /* When */ + val result = fileService.upload("podcast-title", file) + + /* Then */ + assertThat(result).isNotNull() + + val textContent = s3Backend.findAll(newRequestPattern(RequestMethod.PUT, urlEqualTo("/data/podcast-title/toUpload.txt"))) + .first().body + .decodeToString() + + assertThat(textContent).isEqualTo("text is here !") + } + + @Test + fun `with failure after all retries`(@TempDir dir: Path) { + /* Given */ + val file = dir.resolve("toUpload.txt").apply { writeText("text is here !") } + s3Backend.apply { + stubFor(put("/data/podcast-title/toUpload.txt").willReturn(badRequest())) + stubFor(put("/data/podcast-title/toUpload.txt").willReturn(badRequest())) + stubFor(put("/data/podcast-title/toUpload.txt").willReturn(badRequest())) + stubFor(put("/data/podcast-title/toUpload.txt").willReturn(badRequest())) + } + + /* When */ + val result = fileService.upload("podcast-title", file) + + /* Then */ + assertThat(result).isNull() + } + } @Nested @@ -593,16 +608,13 @@ class FileStorageServiceTest( .withHeader("Content-Length", "123") )) /* When */ - StepVerifier.create(fileService.metadata("podcast-title", Paths.get("dd16b2eb-657e-4064-b470-5b99397ce729.png"))) - /* Then */ - .expectSubscription() - .expectNext( - FileMetaData( - contentType = "image/png", - size = 123L - ) - ) - .verifyComplete() + val result = fileService.metadata("podcast-title", Paths.get("dd16b2eb-657e-4064-b470-5b99397ce729.png")) + + /* Then */ + assertThat(result).isEqualTo(FileMetaData( + contentType = "image/png", + size = 123L + )) } } @@ -618,22 +630,30 @@ class FileStorageServiceTest( stubFor(head(urlEqualTo("/data")).willReturn(notFound())) stubFor(put("/data").willReturn(ok())) } + /* When */ - StepVerifier.create(fileService.initBucket()) - /* Then */ - .expectSubscription() - .verifyComplete() + fileService.initBucket() + + /* Then */ + s3Backend.apply { + verify(headRequestedFor(urlEqualTo("/data"))) + verify(putRequestedFor(urlEqualTo("/data"))) + } } @Test fun `with an already existing bucket`() { /* Given */ s3Backend.stubFor(head(urlEqualTo("/data")).willReturn(ok())) + /* When */ - StepVerifier.create(fileService.initBucket()) - /* Then */ - .expectSubscription() - .verifyComplete() + fileService.initBucket() + + /* Then */ + s3Backend.apply { + verify(headRequestedFor(urlEqualTo("/data"))) + verify(0, putRequestedFor(urlEqualTo("/data"))) + } } } @@ -654,7 +674,7 @@ class FileStorageServiceTest( fun `with domain from user request`() { /* Given */ val onDemandFileStorageService = FileStorageConfig().fileStorageService( - WebClient.builder(), + RestClient.builder(), storageProperties.copy(isInternal = true) ) @@ -672,7 +692,7 @@ class FileStorageServiceTest( fun `with domain from external storage system`() { /* Given */ val onDemandFileStorageService = FileStorageConfig().fileStorageService( - WebClient.builder(), + RestClient.builder(), storageProperties.copy(isInternal = false) ) @@ -687,4 +707,15 @@ class FileStorageServiceTest( } } + + companion object { + @JvmField + @RegisterExtension + val s3Backend: WireMockExtension = WireMockExtension.newInstance() + .options(wireMockConfig() + .port(s3MockBackendPort) + // .notifier(ConsoleNotifier(true)) + ) + .build() + } } diff --git a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/update/UpdateServiceTest.kt b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/update/UpdateServiceTest.kt index 8a96259a6..e601b0a72 100644 --- a/backend/src/test/kotlin/com/github/davinkevin/podcastserver/update/UpdateServiceTest.kt +++ b/backend/src/test/kotlin/com/github/davinkevin/podcastserver/update/UpdateServiceTest.kt @@ -196,7 +196,7 @@ class UpdateServiceTest( args.getArgument>(0) .map { it.toItem(podcast) } } - whenever(fileService.downloadItemCover(any())).thenReturn(Mono.empty()) + doNothing().whenever(fileService).downloadItemCover(any()) doNothing().whenever(podcastRepository).updateLastUpdate(eq(podcast.id)) /* When */ @@ -249,7 +249,7 @@ class UpdateServiceTest( args.getArgument>(0) .map { it.toItem(podcast) } } - whenever(fileService.downloadItemCover(any())).thenReturn(Mono.error(RuntimeException("error during cover download"))) + doNothing().whenever(fileService).downloadItemCover(any()) doNothing().whenever(podcastRepository).updateLastUpdate(eq(podcast.id)) /* When */ @@ -454,7 +454,7 @@ class UpdateServiceTest( args.getArgument>(0) .map { it.toItem(podcast1) } } - whenever(fileService.downloadItemCover(any())).thenReturn(Mono.empty()) + doNothing().whenever(fileService).downloadItemCover(any()) doNothing().whenever(podcastRepository).updateLastUpdate(podcast1.id) /* When */ @@ -507,7 +507,7 @@ class UpdateServiceTest( args.getArgument>(0) .map { it.toItem(podcast1) } } - whenever(fileService.downloadItemCover(any())).thenReturn(Mono.empty()) + doNothing().whenever(fileService).downloadItemCover(any()) doNothing().whenever(podcastRepository).updateLastUpdate(podcast1.id) whenever(idm.launchDownload()).thenReturn(Mono.empty()) @@ -562,7 +562,7 @@ class UpdateServiceTest( args.getArgument>(0) .map { it.toItem(podcast1) } } - whenever(fileService.downloadItemCover(any())).thenReturn(Mono.empty()) + doNothing().whenever(fileService).downloadItemCover(any()) doNothing().whenever(podcastRepository).updateLastUpdate(podcast1.id) /* When */ @@ -616,7 +616,7 @@ class UpdateServiceTest( args.getArgument>(0) .map { it.toItem(p) } } - whenever(fileService.downloadItemCover(any())).thenReturn(Mono.empty()) + doNothing().whenever(fileService).downloadItemCover(any()) doNothing().whenever(podcastRepository).updateLastUpdate(p.id) /* When */