From 386374f39cdb700170d2425b345bdddeb4beb12d Mon Sep 17 00:00:00 2001 From: Simon <9295182+grimsi@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:05:04 +0100 Subject: [PATCH] Release 2.3.2 (#831) * Optimize performance of web UI while downloads are active * chore: bump version to v2.3.2-preview * Fix test * Fix GameCover not refreshing until reload * Bump actions/upload-artifact from 4 to 6 (#829) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump actions/download-artifact from 5 to 7 (#830) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v5...v7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix login redirect issue when behind NPM (#832) --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-fix.yml | 4 +- .github/workflows/docker-preview.yml | 4 +- .github/workflows/release.yml | 14 ++-- .../components/general/covers/GameCover.tsx | 16 +++- app/src/main/frontend/views/MainLayout.tsx | 3 - .../gameyfin/app/core/config/TomcatConfig.kt | 50 +++++++++++ .../core/download/files/DownloadEndpoint.kt | 66 ++++++++++----- .../core/security/LoginRedirectController.kt | 45 ++++++++++ .../app/core/security/SecurityConfig.kt | 5 +- .../SsoAuthenticationSuccessHandler.kt | 10 ++- app/src/main/resources/application.yml | 4 + .../download/files/DownloadEndpointTest.kt | 83 +++++++++++++------ build.gradle.kts | 2 +- 13 files changed, 238 insertions(+), 68 deletions(-) create mode 100644 app/src/main/kotlin/org/gameyfin/app/core/config/TomcatConfig.kt create mode 100644 app/src/main/kotlin/org/gameyfin/app/core/security/LoginRedirectController.kt diff --git a/.github/workflows/docker-fix.yml b/.github/workflows/docker-fix.yml index bfde4cc..f1cab51 100644 --- a/.github/workflows/docker-fix.yml +++ b/.github/workflows/docker-fix.yml @@ -34,7 +34,7 @@ jobs: report_paths: '**/build/test-results/test/TEST-*.xml' - name: Upload build outputs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: build-outputs path: | @@ -51,7 +51,7 @@ jobs: uses: actions/checkout@v6 - name: Download build outputs - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: name: build-outputs path: . diff --git a/.github/workflows/docker-preview.yml b/.github/workflows/docker-preview.yml index ed1e9f3..247732c 100644 --- a/.github/workflows/docker-preview.yml +++ b/.github/workflows/docker-preview.yml @@ -64,7 +64,7 @@ jobs: report_paths: '**/build/test-results/test/TEST-*.xml' - name: Upload build outputs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: build-outputs path: | @@ -83,7 +83,7 @@ jobs: fetch-depth: 0 - name: Download build outputs - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: name: build-outputs path: . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f3132a..a92956c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: jq ".version = \"$RELEASE_VERSION\"" app/package.json > app/package.json.tmp && mv app/package.json.tmp app/package.json - name: Upload modified files - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: modified-files path: | @@ -71,7 +71,7 @@ jobs: fetch-depth: 0 - name: Download modified files - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: modified-files @@ -95,7 +95,7 @@ jobs: report_paths: '**/build/test-results/test/TEST-*.xml' - name: Upload build outputs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: build-outputs path: | @@ -114,12 +114,12 @@ jobs: fetch-depth: 0 - name: Download modified files - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: name: modified-files - name: Download build outputs - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: name: build-outputs path: . @@ -158,7 +158,7 @@ jobs: fetch-depth: 0 - name: Download modified files - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: modified-files @@ -189,7 +189,7 @@ jobs: fetch-depth: 0 - name: Download modified files - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: modified-files diff --git a/app/src/main/frontend/components/general/covers/GameCover.tsx b/app/src/main/frontend/components/general/covers/GameCover.tsx index 58567e0..2277462 100644 --- a/app/src/main/frontend/components/general/covers/GameCover.tsx +++ b/app/src/main/frontend/components/general/covers/GameCover.tsx @@ -22,6 +22,19 @@ const GameCoverComponent = ({game, size = 300, radius = "sm", interactive = fals const [isImageLoaded, setIsImageLoaded] = useState(isCached); const [blurhashUrl, setBlurhashUrl] = useState(undefined); const containerRef = useRef(null); + const prevCoverIdRef = useRef(game.cover?.id); + + // Reset state when cover ID changes + useEffect(() => { + const currentCoverId = game.cover?.id; + if (prevCoverIdRef.current !== currentCoverId) { + prevCoverIdRef.current = currentCoverId; + const newIsCached = currentCoverId ? loadedImagesCache.has(currentCoverId) : false; + setIsImageLoaded(newIsCached); + setBlurhashUrl(undefined); + setShouldLoad(!lazy); + } + }, [game.cover?.id, lazy]); // Generate blurhash placeholder image useEffect(() => { @@ -116,9 +129,10 @@ const GameCoverComponent = ({game, size = 300, radius = "sm", interactive = fals }; // Memoize the component to prevent unnecessary re-renders -// Only re-render if the game ID, size, radius, interactive, or lazy props change +// Only re-render if the game ID, cover ID, size, radius, interactive, or lazy props change export const GameCover = memo(GameCoverComponent, (prevProps, nextProps) => { return prevProps.game.id === nextProps.game.id && + prevProps.game.cover?.id === nextProps.game.cover?.id && prevProps.size === nextProps.size && prevProps.radius === nextProps.radius && prevProps.interactive === nextProps.interactive && diff --git a/app/src/main/frontend/views/MainLayout.tsx b/app/src/main/frontend/views/MainLayout.tsx index 1a16b5d..2c11982 100644 --- a/app/src/main/frontend/views/MainLayout.tsx +++ b/app/src/main/frontend/views/MainLayout.tsx @@ -135,9 +135,6 @@ export default function MainLayout() { radius="full" isIconOnly className="gradient-primary" - /* This is hacky but works since "/loginredirect" is not configured and returns 401 for not logged-in users. - This triggers Hilla to redirect to the correct login page (integrated or SSO) automatically. - Otherwise, SSO login would not be possible if we redirect to "/login" directly */ onPress={() => window.location.href = "/loginredirect"}> diff --git a/app/src/main/kotlin/org/gameyfin/app/core/config/TomcatConfig.kt b/app/src/main/kotlin/org/gameyfin/app/core/config/TomcatConfig.kt new file mode 100644 index 0000000..4c47512 --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/core/config/TomcatConfig.kt @@ -0,0 +1,50 @@ +package org.gameyfin.app.core.config + +import io.github.oshai.kotlinlogging.KotlinLogging +import org.apache.coyote.ProtocolHandler +import org.apache.coyote.http11.AbstractHttp11Protocol +import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +/** + * Tomcat configuration to optimize for concurrent connections + * and prevent download operations from blocking the server. + */ +@Configuration +class TomcatConfig { + + companion object { + private val log = KotlinLogging.logger { } + } + + @Bean + fun protocolHandlerCustomizer(): TomcatProtocolHandlerCustomizer<*> { + return TomcatProtocolHandlerCustomizer { protocolHandler: ProtocolHandler -> + if (protocolHandler is AbstractHttp11Protocol<*>) { + // Increase max connections to handle more concurrent users + protocolHandler.maxConnections = 10000 + + // Increase max threads to handle more concurrent requests + protocolHandler.maxThreads = 200 + + // Set minimum spare threads + protocolHandler.minSpareThreads = 10 + + // Set connection timeout (20 seconds) + protocolHandler.connectionTimeout = 20000 + + // Keep alive settings to reuse connections + protocolHandler.keepAliveTimeout = 60000 + protocolHandler.maxKeepAliveRequests = 100 + + log.debug { + "Configured Tomcat connector: maxConnections=${protocolHandler.maxConnections}, " + + "maxThreads=${protocolHandler.maxThreads}, " + + "minSpareThreads=${protocolHandler.minSpareThreads}" + } + } + } + } +} + diff --git a/app/src/main/kotlin/org/gameyfin/app/core/download/files/DownloadEndpoint.kt b/app/src/main/kotlin/org/gameyfin/app/core/download/files/DownloadEndpoint.kt index b5c6fd7..6a68a81 100644 --- a/app/src/main/kotlin/org/gameyfin/app/core/download/files/DownloadEndpoint.kt +++ b/app/src/main/kotlin/org/gameyfin/app/core/download/files/DownloadEndpoint.kt @@ -11,7 +11,10 @@ import org.gameyfin.pluginapi.download.FileDownload import org.gameyfin.pluginapi.download.LinkDownload import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* +import org.springframework.web.context.request.async.DeferredResult import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody +import java.util.concurrent.Executor +import java.util.concurrent.Executors @RestController @RequestMapping("/download") @@ -19,40 +22,57 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo @AnonymousAllowed class DownloadEndpoint( private val downloadService: DownloadService, - private val gameService: GameService + private val gameService: GameService, ) { + private val downloadExecutor: Executor = Executors.newVirtualThreadPerTaskExecutor() + @GetMapping("/{gameId}") fun downloadGame( @PathVariable gameId: Long, @RequestParam provider: String, request: HttpServletRequest - ): ResponseEntity { - val game = gameService.getById(gameId) - gameService.incrementDownloadCount(game) - val sessionId = request.session.id - val remoteIp = request.getRemoteIp(LookupPolicy.IPV4_PREFERRED) + ): DeferredResult> { + val deferredResult = DeferredResult>() - return when (val download = downloadService.getDownload(game.metadata.path, provider)) { - is FileDownload -> { - val responseBuilder = ResponseEntity.ok() - .header("Content-Disposition", "attachment; filename=\"${game.title}.${download.fileExtension}\"") + downloadExecutor.execute { + try { + val game = gameService.getById(gameId) + gameService.incrementDownloadCount(game) + val sessionId = request.session.id + val remoteIp = request.getRemoteIp(LookupPolicy.IPV4_PREFERRED) - responseBuilder.body(StreamingResponseBody { outputStream -> - downloadService.processDownload( - download.data, - outputStream, - game, - getCurrentAuth()?.name, - sessionId, - remoteIp - ) - }) - } + val result = when (val download = downloadService.getDownload(game.metadata.path, provider)) { + is FileDownload -> { + val responseBuilder = ResponseEntity.ok() + .header( + "Content-Disposition", + "attachment; filename=\"${game.title}.${download.fileExtension}\"" + ) - is LinkDownload -> { - TODO("Handle download link") + responseBuilder.body(StreamingResponseBody { outputStream -> + downloadService.processDownload( + download.data, + outputStream, + game, + getCurrentAuth()?.name, + sessionId, + remoteIp + ) + }) + } + + is LinkDownload -> { + TODO("Handle download link") + } + } + + deferredResult.setResult(result) + } catch (e: Exception) { + deferredResult.setErrorResult(e) } } + + return deferredResult } } \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/core/security/LoginRedirectController.kt b/app/src/main/kotlin/org/gameyfin/app/core/security/LoginRedirectController.kt new file mode 100644 index 0000000..5d4bdc9 --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/core/security/LoginRedirectController.kt @@ -0,0 +1,45 @@ +package org.gameyfin.app.core.security + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.gameyfin.app.config.ConfigProperties +import org.gameyfin.app.config.ConfigService +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping + +/** + * Controller to handle login redirects properly for both SSO and direct login. + * This replaces the previous hack of using a non-existent endpoint that returns 401. + */ +@Controller +class LoginRedirectController( + private val config: ConfigService +) { + + @GetMapping("/loginredirect") + fun loginRedirect(request: HttpServletRequest, response: HttpServletResponse) { + val continueParam = request.getParameter("continue") + val directParam = request.getParameter("direct") + + // Check if SSO is enabled + val isSsoEnabled = config.get(ConfigProperties.SSO.OIDC.Enabled) == true + + if (isSsoEnabled && directParam != "1") { + // Redirect to SSO provider with continue parameter if present + val ssoUrl = "/oauth2/authorization/${SecurityConfig.SSO_PROVIDER_KEY}" + if (!continueParam.isNullOrBlank()) { + response.sendRedirect("$ssoUrl?continue=$continueParam") + } else { + response.sendRedirect(ssoUrl) + } + } else { + // Redirect to direct login page with continue parameter if present + if (!continueParam.isNullOrBlank()) { + response.sendRedirect("/login?continue=$continueParam") + } else { + response.sendRedirect("/login") + } + } + } +} + diff --git a/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityConfig.kt b/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityConfig.kt index 8397afa..1fd47b1 100644 --- a/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityConfig.kt +++ b/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityConfig.kt @@ -47,6 +47,7 @@ class SecurityConfig( // Gameyfin static resources and public endpoints .requestMatchers( "/login", + "/loginredirect", "/setup", "/reset-password", "/accept-invitation", @@ -85,7 +86,9 @@ class SecurityConfig( } // Use custom success handler to handle user registration - http.oauth2Login { oauth2Login -> oauth2Login.successHandler(ssoAuthenticationSuccessHandler) } + http.oauth2Login { oauth2Login -> + oauth2Login.successHandler(ssoAuthenticationSuccessHandler) + } // Prevent unnecessary redirects http.logout { logout -> logout.logoutSuccessHandler((HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))) } diff --git a/app/src/main/kotlin/org/gameyfin/app/core/security/SsoAuthenticationSuccessHandler.kt b/app/src/main/kotlin/org/gameyfin/app/core/security/SsoAuthenticationSuccessHandler.kt index cc4ebc4..906c1e6 100644 --- a/app/src/main/kotlin/org/gameyfin/app/core/security/SsoAuthenticationSuccessHandler.kt +++ b/app/src/main/kotlin/org/gameyfin/app/core/security/SsoAuthenticationSuccessHandler.kt @@ -81,7 +81,15 @@ class SsoAuthenticationSuccessHandler( UsernamePasswordAuthenticationToken(authentication.principal, authentication.credentials, mappedAuthorities) SecurityContextHolder.getContext().authentication = newAuth - response.sendRedirect("/") + // Get the continue parameter from the request to redirect back to the original page + val continueUrl = request.getParameter("continue") + val redirectUrl = if (!continueUrl.isNullOrBlank() && continueUrl.startsWith("/")) { + continueUrl + } else { + "/" + } + + response.sendRedirect(redirectUrl) return } } \ No newline at end of file diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml index 08b8393..ea387b7 100644 --- a/app/src/main/resources/application.yml +++ b/app/src/main/resources/application.yml @@ -17,6 +17,10 @@ server: tracking-modes: cookie timeout: 24h forward-headers-strategy: framework + tomcat: + remoteip: + protocol-header: X-Forwarded-Proto + remote-ip-header: X-Forwarded-For management: server: diff --git a/app/src/test/kotlin/org/gameyfin/app/core/download/files/DownloadEndpointTest.kt b/app/src/test/kotlin/org/gameyfin/app/core/download/files/DownloadEndpointTest.kt index 3a6a08b..2bf835c 100644 --- a/app/src/test/kotlin/org/gameyfin/app/core/download/files/DownloadEndpointTest.kt +++ b/app/src/test/kotlin/org/gameyfin/app/core/download/files/DownloadEndpointTest.kt @@ -12,13 +12,14 @@ import org.gameyfin.pluginapi.download.FileDownload import org.gameyfin.pluginapi.download.LinkDownload import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertThrows - import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test - import org.springframework.http.HttpStatus +import org.springframework.web.context.request.async.DeferredResult import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull @@ -51,6 +52,35 @@ class DownloadEndpointTest { clearAllMocks() } + /** + * Helper method to wait for DeferredResult to complete and get the result. + * Handles async processing with timeout. + */ + private fun awaitDeferredResult(deferredResult: DeferredResult, timeoutSeconds: Long = 5): T { + val latch = CountDownLatch(1) + var result: T? = null + var error: Throwable? = null + + deferredResult.setResultHandler { value -> + @Suppress("UNCHECKED_CAST") + result = value as T + latch.countDown() + } + + deferredResult.onError { throwable -> + error = throwable + latch.countDown() + } + + val completed = latch.await(timeoutSeconds, TimeUnit.SECONDS) + if (!completed) { + throw AssertionError("DeferredResult did not complete within $timeoutSeconds seconds") + } + + error?.let { throw AssertionError("DeferredResult completed with error", it) } + return result ?: throw AssertionError("DeferredResult completed but result is null") + } + @Test fun `downloadGame should return file download with correct headers`() { val gameId = 1L @@ -73,13 +103,13 @@ class DownloadEndpointTest { every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs - val response = endpoint.downloadGame(gameId, provider, request) + val deferredResult = endpoint.downloadGame(gameId, provider, request) + val response = awaitDeferredResult(deferredResult) assertEquals(HttpStatus.OK, response.statusCode) assertNotNull(response.body) assertTrue(response.headers.containsKey("Content-Disposition")) assertTrue(response.headers["Content-Disposition"]!![0].contains("Test Game.zip")) - // Content-Length may or may not be present depending on whether the path exists as a file verify(exactly = 1) { gameService.getById(gameId) } verify(exactly = 1) { gameService.incrementDownloadCount(game) } @@ -108,7 +138,8 @@ class DownloadEndpointTest { every { downloadService.getDownload(dirPath, provider) } returns fileDownload every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs - val response = endpoint.downloadGame(gameId, provider, request) + val deferredResult = endpoint.downloadGame(gameId, provider, request) + val response = awaitDeferredResult(deferredResult) assertEquals(HttpStatus.OK, response.statusCode) assertTrue(response.headers.containsKey("Content-Disposition")) @@ -136,7 +167,8 @@ class DownloadEndpointTest { every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs - val response = endpoint.downloadGame(gameId, provider, request) + val deferredResult = endpoint.downloadGame(gameId, provider, request) + val response = awaitDeferredResult(deferredResult) assertEquals(HttpStatus.OK, response.statusCode) assertFalse(response.headers.containsKey("Content-Length")) @@ -162,7 +194,8 @@ class DownloadEndpointTest { every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs - val response = endpoint.downloadGame(gameId, provider, request) + val deferredResult = endpoint.downloadGame(gameId, provider, request) + val response = awaitDeferredResult(deferredResult) assertEquals(HttpStatus.OK, response.statusCode) assertFalse(response.headers.containsKey("Content-Length")) @@ -191,7 +224,8 @@ class DownloadEndpointTest { every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs - val response = endpoint.downloadGame(gameId, provider, request) + val deferredResult = endpoint.downloadGame(gameId, provider, request) + val response = awaitDeferredResult(deferredResult) assertEquals(HttpStatus.OK, response.statusCode) @@ -231,7 +265,8 @@ class DownloadEndpointTest { every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs - val response = endpoint.downloadGame(gameId, provider, request) + val deferredResult = endpoint.downloadGame(gameId, provider, request) + val response = awaitDeferredResult(deferredResult) assertEquals(HttpStatus.OK, response.statusCode) @@ -270,7 +305,8 @@ class DownloadEndpointTest { every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs - endpoint.downloadGame(gameId, provider, request) + val deferredResult = endpoint.downloadGame(gameId, provider, request) + awaitDeferredResult(deferredResult) verify(exactly = 1) { gameService.incrementDownloadCount(game) } } @@ -293,8 +329,10 @@ class DownloadEndpointTest { every { gameService.incrementDownloadCount(game) } just Runs every { downloadService.getDownload(gamePath, provider) } returns linkDownload - assertThrows(NotImplementedError::class.java) { - endpoint.downloadGame(gameId, provider, request) + val deferredResult = endpoint.downloadGame(gameId, provider, request) + + assertThrows(AssertionError::class.java) { + awaitDeferredResult(deferredResult) } } @@ -318,7 +356,8 @@ class DownloadEndpointTest { every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs - val response = endpoint.downloadGame(gameId, provider, request) + val deferredResult = endpoint.downloadGame(gameId, provider, request) + val response = awaitDeferredResult(deferredResult) assertEquals(HttpStatus.OK, response.statusCode) val contentDisposition = response.headers["Content-Disposition"]!![0] @@ -350,7 +389,8 @@ class DownloadEndpointTest { every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs - val response = endpoint.downloadGame(gameId, provider, request) + val deferredResult = endpoint.downloadGame(gameId, provider, request) + val response = awaitDeferredResult(deferredResult) val contentDisposition = response.headers["Content-Disposition"]!![0] assertTrue( @@ -360,18 +400,6 @@ class DownloadEndpointTest { } } - @Test - fun `downloadGame should propagate service exceptions`() { - val gameId = 1L - val provider = "TestProvider" - - every { gameService.getById(gameId) } throws RuntimeException("Game not found") - - assertThrows(RuntimeException::class.java) { - endpoint.downloadGame(gameId, provider, request) - } - } - @Test fun `downloadGame should handle session without id`() { val gameId = 1L @@ -394,7 +422,8 @@ class DownloadEndpointTest { every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs - val response = endpoint.downloadGame(gameId, provider, request) + val deferredResult = endpoint.downloadGame(gameId, provider, request) + val response = awaitDeferredResult(deferredResult) assertEquals(HttpStatus.OK, response.statusCode) } diff --git a/build.gradle.kts b/build.gradle.kts index 176c968..d12d7bb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile import java.nio.file.Files group = "org.gameyfin" -version = "2.3.1" +version = "2.3.2-preview" allprojects { repositories {