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] <support@github.com>
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] <support@github.com>
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Simon
2025-12-17 11:05:04 +01:00
committed by GitHub
parent 400c4d1c61
commit 386374f39c
13 changed files with 238 additions and 68 deletions
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
report_paths: '**/build/test-results/test/TEST-*.xml' report_paths: '**/build/test-results/test/TEST-*.xml'
- name: Upload build outputs - name: Upload build outputs
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: build-outputs name: build-outputs
path: | path: |
@@ -51,7 +51,7 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Download build outputs - name: Download build outputs
uses: actions/download-artifact@v5 uses: actions/download-artifact@v7
with: with:
name: build-outputs name: build-outputs
path: . path: .
+2 -2
View File
@@ -64,7 +64,7 @@ jobs:
report_paths: '**/build/test-results/test/TEST-*.xml' report_paths: '**/build/test-results/test/TEST-*.xml'
- name: Upload build outputs - name: Upload build outputs
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: build-outputs name: build-outputs
path: | path: |
@@ -83,7 +83,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Download build outputs - name: Download build outputs
uses: actions/download-artifact@v5 uses: actions/download-artifact@v7
with: with:
name: build-outputs name: build-outputs
path: . path: .
+7 -7
View File
@@ -50,7 +50,7 @@ jobs:
jq ".version = \"$RELEASE_VERSION\"" app/package.json > app/package.json.tmp && mv app/package.json.tmp app/package.json jq ".version = \"$RELEASE_VERSION\"" app/package.json > app/package.json.tmp && mv app/package.json.tmp app/package.json
- name: Upload modified files - name: Upload modified files
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: modified-files name: modified-files
path: | path: |
@@ -71,7 +71,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Download modified files - name: Download modified files
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
name: modified-files name: modified-files
@@ -95,7 +95,7 @@ jobs:
report_paths: '**/build/test-results/test/TEST-*.xml' report_paths: '**/build/test-results/test/TEST-*.xml'
- name: Upload build outputs - name: Upload build outputs
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: build-outputs name: build-outputs
path: | path: |
@@ -114,12 +114,12 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Download modified files - name: Download modified files
uses: actions/download-artifact@v5 uses: actions/download-artifact@v7
with: with:
name: modified-files name: modified-files
- name: Download build outputs - name: Download build outputs
uses: actions/download-artifact@v5 uses: actions/download-artifact@v7
with: with:
name: build-outputs name: build-outputs
path: . path: .
@@ -158,7 +158,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Download modified files - name: Download modified files
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
name: modified-files name: modified-files
@@ -189,7 +189,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Download modified files - name: Download modified files
uses: actions/download-artifact@v6 uses: actions/download-artifact@v7
with: with:
name: modified-files name: modified-files
@@ -22,6 +22,19 @@ const GameCoverComponent = ({game, size = 300, radius = "sm", interactive = fals
const [isImageLoaded, setIsImageLoaded] = useState(isCached); const [isImageLoaded, setIsImageLoaded] = useState(isCached);
const [blurhashUrl, setBlurhashUrl] = useState<string | undefined>(undefined); const [blurhashUrl, setBlurhashUrl] = useState<string | undefined>(undefined);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const prevCoverIdRef = useRef<number | undefined>(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 // Generate blurhash placeholder image
useEffect(() => { useEffect(() => {
@@ -116,9 +129,10 @@ const GameCoverComponent = ({game, size = 300, radius = "sm", interactive = fals
}; };
// Memoize the component to prevent unnecessary re-renders // 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) => { export const GameCover = memo(GameCoverComponent, (prevProps, nextProps) => {
return prevProps.game.id === nextProps.game.id && return prevProps.game.id === nextProps.game.id &&
prevProps.game.cover?.id === nextProps.game.cover?.id &&
prevProps.size === nextProps.size && prevProps.size === nextProps.size &&
prevProps.radius === nextProps.radius && prevProps.radius === nextProps.radius &&
prevProps.interactive === nextProps.interactive && prevProps.interactive === nextProps.interactive &&
@@ -135,9 +135,6 @@ export default function MainLayout() {
radius="full" radius="full"
isIconOnly isIconOnly
className="gradient-primary" 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"}> onPress={() => window.location.href = "/loginredirect"}>
<SignInIcon fill="text-background/80"/> <SignInIcon fill="text-background/80"/>
</Button> </Button>
@@ -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}"
}
}
}
}
}
@@ -11,7 +11,10 @@ import org.gameyfin.pluginapi.download.FileDownload
import org.gameyfin.pluginapi.download.LinkDownload import org.gameyfin.pluginapi.download.LinkDownload
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.context.request.async.DeferredResult
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
import java.util.concurrent.Executor
import java.util.concurrent.Executors
@RestController @RestController
@RequestMapping("/download") @RequestMapping("/download")
@@ -19,40 +22,57 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo
@AnonymousAllowed @AnonymousAllowed
class DownloadEndpoint( class DownloadEndpoint(
private val downloadService: DownloadService, private val downloadService: DownloadService,
private val gameService: GameService private val gameService: GameService,
) { ) {
private val downloadExecutor: Executor = Executors.newVirtualThreadPerTaskExecutor()
@GetMapping("/{gameId}") @GetMapping("/{gameId}")
fun downloadGame( fun downloadGame(
@PathVariable gameId: Long, @PathVariable gameId: Long,
@RequestParam provider: String, @RequestParam provider: String,
request: HttpServletRequest request: HttpServletRequest
): ResponseEntity<StreamingResponseBody> { ): DeferredResult<ResponseEntity<StreamingResponseBody>> {
val game = gameService.getById(gameId) val deferredResult = DeferredResult<ResponseEntity<StreamingResponseBody>>()
gameService.incrementDownloadCount(game)
val sessionId = request.session.id
val remoteIp = request.getRemoteIp(LookupPolicy.IPV4_PREFERRED)
return when (val download = downloadService.getDownload(game.metadata.path, provider)) { downloadExecutor.execute {
is FileDownload -> { try {
val responseBuilder = ResponseEntity.ok() val game = gameService.getById(gameId)
.header("Content-Disposition", "attachment; filename=\"${game.title}.${download.fileExtension}\"") gameService.incrementDownloadCount(game)
val sessionId = request.session.id
val remoteIp = request.getRemoteIp(LookupPolicy.IPV4_PREFERRED)
responseBuilder.body(StreamingResponseBody { outputStream -> val result = when (val download = downloadService.getDownload(game.metadata.path, provider)) {
downloadService.processDownload( is FileDownload -> {
download.data, val responseBuilder = ResponseEntity.ok()
outputStream, .header(
game, "Content-Disposition",
getCurrentAuth()?.name, "attachment; filename=\"${game.title}.${download.fileExtension}\""
sessionId, )
remoteIp
)
})
}
is LinkDownload -> { responseBuilder.body(StreamingResponseBody { outputStream ->
TODO("Handle download link") 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
} }
} }
@@ -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")
}
}
}
}
@@ -47,6 +47,7 @@ class SecurityConfig(
// Gameyfin static resources and public endpoints // Gameyfin static resources and public endpoints
.requestMatchers( .requestMatchers(
"/login", "/login",
"/loginredirect",
"/setup", "/setup",
"/reset-password", "/reset-password",
"/accept-invitation", "/accept-invitation",
@@ -85,7 +86,9 @@ class SecurityConfig(
} }
// Use custom success handler to handle user registration // Use custom success handler to handle user registration
http.oauth2Login { oauth2Login -> oauth2Login.successHandler(ssoAuthenticationSuccessHandler) } http.oauth2Login { oauth2Login ->
oauth2Login.successHandler(ssoAuthenticationSuccessHandler)
}
// Prevent unnecessary redirects // Prevent unnecessary redirects
http.logout { logout -> logout.logoutSuccessHandler((HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))) } http.logout { logout -> logout.logoutSuccessHandler((HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))) }
@@ -81,7 +81,15 @@ class SsoAuthenticationSuccessHandler(
UsernamePasswordAuthenticationToken(authentication.principal, authentication.credentials, mappedAuthorities) UsernamePasswordAuthenticationToken(authentication.principal, authentication.credentials, mappedAuthorities)
SecurityContextHolder.getContext().authentication = newAuth 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 return
} }
} }
+4
View File
@@ -17,6 +17,10 @@ server:
tracking-modes: cookie tracking-modes: cookie
timeout: 24h timeout: 24h
forward-headers-strategy: framework forward-headers-strategy: framework
tomcat:
remoteip:
protocol-header: X-Forwarded-Proto
remote-ip-header: X-Forwarded-For
management: management:
server: server:
@@ -12,13 +12,14 @@ import org.gameyfin.pluginapi.download.FileDownload
import org.gameyfin.pluginapi.download.LinkDownload import org.gameyfin.pluginapi.download.LinkDownload
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.web.context.request.async.DeferredResult
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
@@ -51,6 +52,35 @@ class DownloadEndpointTest {
clearAllMocks() clearAllMocks()
} }
/**
* Helper method to wait for DeferredResult to complete and get the result.
* Handles async processing with timeout.
*/
private fun <T> awaitDeferredResult(deferredResult: DeferredResult<T>, 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 @Test
fun `downloadGame should return file download with correct headers`() { fun `downloadGame should return file download with correct headers`() {
val gameId = 1L val gameId = 1L
@@ -73,13 +103,13 @@ class DownloadEndpointTest {
every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.getDownload(gamePath, provider) } returns fileDownload
every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs 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) assertEquals(HttpStatus.OK, response.statusCode)
assertNotNull(response.body) assertNotNull(response.body)
assertTrue(response.headers.containsKey("Content-Disposition")) assertTrue(response.headers.containsKey("Content-Disposition"))
assertTrue(response.headers["Content-Disposition"]!![0].contains("Test Game.zip")) 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.getById(gameId) }
verify(exactly = 1) { gameService.incrementDownloadCount(game) } verify(exactly = 1) { gameService.incrementDownloadCount(game) }
@@ -108,7 +138,8 @@ class DownloadEndpointTest {
every { downloadService.getDownload(dirPath, provider) } returns fileDownload every { downloadService.getDownload(dirPath, provider) } returns fileDownload
every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs 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) assertEquals(HttpStatus.OK, response.statusCode)
assertTrue(response.headers.containsKey("Content-Disposition")) assertTrue(response.headers.containsKey("Content-Disposition"))
@@ -136,7 +167,8 @@ class DownloadEndpointTest {
every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.getDownload(gamePath, provider) } returns fileDownload
every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs 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) assertEquals(HttpStatus.OK, response.statusCode)
assertFalse(response.headers.containsKey("Content-Length")) assertFalse(response.headers.containsKey("Content-Length"))
@@ -162,7 +194,8 @@ class DownloadEndpointTest {
every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.getDownload(gamePath, provider) } returns fileDownload
every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs 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) assertEquals(HttpStatus.OK, response.statusCode)
assertFalse(response.headers.containsKey("Content-Length")) assertFalse(response.headers.containsKey("Content-Length"))
@@ -191,7 +224,8 @@ class DownloadEndpointTest {
every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.getDownload(gamePath, provider) } returns fileDownload
every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs 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) assertEquals(HttpStatus.OK, response.statusCode)
@@ -231,7 +265,8 @@ class DownloadEndpointTest {
every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.getDownload(gamePath, provider) } returns fileDownload
every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs 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) assertEquals(HttpStatus.OK, response.statusCode)
@@ -270,7 +305,8 @@ class DownloadEndpointTest {
every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.getDownload(gamePath, provider) } returns fileDownload
every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs 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) } verify(exactly = 1) { gameService.incrementDownloadCount(game) }
} }
@@ -293,8 +329,10 @@ class DownloadEndpointTest {
every { gameService.incrementDownloadCount(game) } just Runs every { gameService.incrementDownloadCount(game) } just Runs
every { downloadService.getDownload(gamePath, provider) } returns linkDownload every { downloadService.getDownload(gamePath, provider) } returns linkDownload
assertThrows(NotImplementedError::class.java) { val deferredResult = endpoint.downloadGame(gameId, provider, request)
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.getDownload(gamePath, provider) } returns fileDownload
every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs 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) assertEquals(HttpStatus.OK, response.statusCode)
val contentDisposition = response.headers["Content-Disposition"]!![0] val contentDisposition = response.headers["Content-Disposition"]!![0]
@@ -350,7 +389,8 @@ class DownloadEndpointTest {
every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.getDownload(gamePath, provider) } returns fileDownload
every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs 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] val contentDisposition = response.headers["Content-Disposition"]!![0]
assertTrue( 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 @Test
fun `downloadGame should handle session without id`() { fun `downloadGame should handle session without id`() {
val gameId = 1L val gameId = 1L
@@ -394,7 +422,8 @@ class DownloadEndpointTest {
every { downloadService.getDownload(gamePath, provider) } returns fileDownload every { downloadService.getDownload(gamePath, provider) } returns fileDownload
every { downloadService.processDownload(any(), any(), any(), any(), any(), any()) } just Runs 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) assertEquals(HttpStatus.OK, response.statusCode)
} }
+1 -1
View File
@@ -6,7 +6,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import java.nio.file.Files import java.nio.file.Files
group = "org.gameyfin" group = "org.gameyfin"
version = "2.3.1" version = "2.3.2-preview"
allprojects { allprojects {
repositories { repositories {