mirror of
https://github.com/grimsi/gameyfin.git
synced 2026-02-06 11:27:07 +00:00
Compare commits
3 Commits
ff0f9b6ce0
...
c20364ac6c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c20364ac6c | ||
|
|
ecd369cd30 | ||
|
|
111e164fab |
@ -36,6 +36,7 @@ dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-aspectj")
|
||||
implementation("org.springframework.boot:spring-boot-starter-jackson")
|
||||
implementation("org.springframework.cloud:spring-cloud-starter")
|
||||
implementation("jakarta.validation:jakarta.validation-api:${rootProject.extra["jakartaValidationVersion"]}")
|
||||
|
||||
@ -43,7 +44,10 @@ dependencies {
|
||||
implementation(kotlin("reflect"))
|
||||
|
||||
// Reactive
|
||||
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||
implementation("org.springframework.boot:spring-boot-starter-webflux") {
|
||||
exclude(group = "org.springframework.boot", module = "spring-boot-starter-reactor-netty")
|
||||
}
|
||||
implementation("org.springframework.boot:spring-boot-starter-jetty")
|
||||
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||
|
||||
@ -51,7 +55,9 @@ dependencies {
|
||||
implementation("com.vaadin:vaadin-core") {
|
||||
exclude("com.vaadin:flow-react")
|
||||
}
|
||||
implementation("com.vaadin:vaadin-spring-boot-starter")
|
||||
implementation("com.vaadin:vaadin-spring-boot-starter") {
|
||||
exclude(group = "org.springframework.boot", module = "spring-boot-starter-tomcat")
|
||||
}
|
||||
implementation("com.vaadin:hilla-spring-boot-starter")
|
||||
|
||||
// Logging
|
||||
@ -59,8 +65,7 @@ dependencies {
|
||||
|
||||
// Persistence & I/O
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("com.github.paulcwarren:spring-content-fs-boot-starter:${rootProject.extra["springContentVersion"]}")
|
||||
implementation("org.flywaydb:flyway-core")
|
||||
implementation("org.springframework.boot:spring-boot-starter-flyway")
|
||||
implementation("commons-io:commons-io:${rootProject.extra["commonsIoVersion"]}")
|
||||
implementation("com.google.guava:guava:${rootProject.extra["guavaVersion"]}")
|
||||
|
||||
@ -83,6 +88,7 @@ dependencies {
|
||||
|
||||
// Development
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
developmentOnly("com.vaadin:vaadin-dev")
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
runtimeOnly("com.h2database:h2")
|
||||
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
|
||||
@ -123,10 +129,12 @@ tasks.named("sonar") {
|
||||
|
||||
sonar {
|
||||
properties {
|
||||
property("sonar.projectKey", "gameyfin_gameyfin")
|
||||
property("sonar.organization", "gameyfin")
|
||||
property("sonar.projectKey", "gameyfin_gameyfin")
|
||||
property("sonar.projectName", "gameyfin")
|
||||
property("sonar.host.url", "https://sonarcloud.io")
|
||||
property("sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/test/jacocoTestReport.xml")
|
||||
property("sonar.coverage.exclusions", "**/*Config.kt,**/org/gameyfin/db/h2/**")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@ -1,5 +1,6 @@
|
||||
package org.gameyfin.app.collections.entities
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.ElementCollection
|
||||
import jakarta.persistence.Embeddable
|
||||
import jakarta.persistence.FetchType
|
||||
@ -11,5 +12,6 @@ class CollectionMetadata(
|
||||
val displayOrder: Int = -1,
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@Column(nullable = false)
|
||||
val gamesAddedAt: MutableMap<Long, Instant> = mutableMapOf()
|
||||
)
|
||||
@ -331,6 +331,7 @@ sealed class ConfigProperties<T : Serializable>(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("EnumEntryName")
|
||||
enum class MatchUsersBy {
|
||||
username, email
|
||||
}
|
||||
@ -1,7 +1,5 @@
|
||||
package org.gameyfin.app.config
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.gameyfin.app.config.dto.ConfigEntryDto
|
||||
import org.gameyfin.app.config.dto.ConfigUpdateDto
|
||||
@ -11,6 +9,8 @@ import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Sinks
|
||||
import tools.jackson.core.JacksonException
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.io.Serializable
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.toJavaDuration
|
||||
@ -186,7 +186,7 @@ class ConfigService(
|
||||
return try {
|
||||
val typeReference = objectMapper.typeFactory.constructType(configProperty.type.java)
|
||||
objectMapper.readValue(value.toString(), typeReference) as T
|
||||
} catch (e: JsonProcessingException) {
|
||||
} catch (e: JacksonException) {
|
||||
throw IllegalArgumentException(
|
||||
"Failed to deserialize value '$value' for key '${configProperty.key}' to type ${configProperty.type.simpleName}: ${e.message}",
|
||||
e
|
||||
@ -209,7 +209,7 @@ class ConfigService(
|
||||
private fun <T : Serializable> serializeValue(value: T, key: String): String {
|
||||
return try {
|
||||
objectMapper.writeValueAsString(value)
|
||||
} catch (e: JsonProcessingException) {
|
||||
} catch (e: JacksonException) {
|
||||
throw IllegalArgumentException(
|
||||
"Failed to serialize value for key '$key': ${e.message}",
|
||||
e
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package org.gameyfin.app.config.dto
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import org.gameyfin.app.core.serialization.ArrayDeserializer
|
||||
import tools.jackson.databind.annotation.JsonDeserialize
|
||||
import java.io.Serializable
|
||||
|
||||
data class ConfigUpdateDto(
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
package org.gameyfin.app.config.dto
|
||||
|
||||
data class CronExpressionVerificationResultDto(
|
||||
val valid: Boolean,
|
||||
val errorMessage: String? = null
|
||||
) {
|
||||
companion object {
|
||||
val valid = CronExpressionVerificationResultDto(true)
|
||||
fun invalid(errorMessage: String) = CronExpressionVerificationResultDto(false, errorMessage)
|
||||
}
|
||||
}
|
||||
@ -3,10 +3,6 @@ package org.gameyfin.app.core
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import com.fasterxml.jackson.annotation.JsonValue
|
||||
import org.gameyfin.app.users.RoleService
|
||||
import java.lang.Enum
|
||||
import kotlin.IllegalArgumentException
|
||||
import kotlin.Int
|
||||
import kotlin.String
|
||||
|
||||
enum class Role(val roleName: String, val powerLevel: Int) {
|
||||
|
||||
@ -32,7 +28,7 @@ enum class Role(val roleName: String, val powerLevel: Int) {
|
||||
if (type == null) return null
|
||||
val enumString = type.removePrefix(RoleService.INTERNAL_ROLE_PREFIX)
|
||||
return try {
|
||||
Enum.valueOf(Role::class.java, enumString)
|
||||
java.lang.Enum.valueOf(Role::class.java, enumString)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
@ -33,11 +33,14 @@ class SetupDataLoader(
|
||||
|
||||
val protocol = if (env.getProperty("server.ssl.key-store") != null) "https" else "http"
|
||||
val rawAppUrl = env.getProperty("app.url")
|
||||
|
||||
@Suppress("HttpUrlsUsage")
|
||||
val appUrl = when {
|
||||
rawAppUrl.isNullOrBlank() -> null
|
||||
rawAppUrl.startsWith("http://") || rawAppUrl.startsWith("https://") -> rawAppUrl
|
||||
else -> "$protocol://$rawAppUrl"
|
||||
}
|
||||
|
||||
val setupUrl =
|
||||
appUrl ?: "${protocol}://${InetAddress.getLocalHost().hostName}:${env.getProperty("server.port")}/setup"
|
||||
log.info { "Visit $setupUrl to complete the setup" }
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
package org.gameyfin.app.core.config
|
||||
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import com.vaadin.hilla.EndpointController.ENDPOINT_MAPPER_FACTORY_BEAN_QUALIFIER
|
||||
import com.vaadin.hilla.parser.jackson.ByteArrayModule
|
||||
import com.vaadin.hilla.parser.jackson.JacksonObjectMapperFactory
|
||||
import org.gameyfin.app.core.serialization.*
|
||||
import org.gameyfin.pluginapi.gamemetadata.*
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
|
||||
import tools.jackson.databind.DeserializationFeature
|
||||
import tools.jackson.databind.json.JsonMapper
|
||||
import tools.jackson.databind.module.SimpleModule
|
||||
|
||||
|
||||
/**
|
||||
* Jackson configuration for custom serializers and deserializers.
|
||||
@ -15,14 +18,21 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
|
||||
@Configuration
|
||||
class JacksonConfig {
|
||||
|
||||
|
||||
@Bean
|
||||
fun objectMapperCustomizer(): Jackson2ObjectMapperBuilder {
|
||||
return Jackson2ObjectMapperBuilder()
|
||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
.modulesToInstall(JavaTimeModule(), displayableEnumModule())
|
||||
@Bean(ENDPOINT_MAPPER_FACTORY_BEAN_QUALIFIER)
|
||||
fun jsonMapperFactory(): JacksonObjectMapperFactory {
|
||||
return JacksonObjectMapperFactory {
|
||||
JsonMapper.builder()
|
||||
// Default Hilla options
|
||||
.addModule(ByteArrayModule())
|
||||
.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false)
|
||||
.enable(DeserializationFeature.ACCEPT_FLOAT_AS_INT)
|
||||
// Custom modules
|
||||
.addModule(displayableEnumModule())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun displayableEnumModule(): SimpleModule {
|
||||
val module = SimpleModule("DisplayableEnumModule")
|
||||
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
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.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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package org.gameyfin.app.core.download.bandwidth
|
||||
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import jakarta.annotation.security.PermitAll
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
import org.gameyfin.app.core.Role
|
||||
@ -17,11 +16,6 @@ import reactor.core.publisher.Flux
|
||||
class BandwidthMonitoringEndpoint(
|
||||
private val bandwidthMonitoringService: BandwidthMonitoringService
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger {}
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
fun subscribe(): Flux<List<List<SessionStatsDto>>> {
|
||||
return if (isCurrentUserAdmin()) BandwidthMonitoringService.subscribe()
|
||||
|
||||
@ -29,7 +29,7 @@ class FilesystemService(
|
||||
* @return A list of FileDto objects representing the files and directories.
|
||||
*/
|
||||
fun listContents(path: String?): List<FileDto> {
|
||||
if (path == null || path.isEmpty()) {
|
||||
if (path.isNullOrEmpty()) {
|
||||
val roots = FileSystems.getDefault().rootDirectories.toList()
|
||||
|
||||
if (getHostOperatingSystem() == OperatingSystemType.WINDOWS) return roots.map {
|
||||
@ -145,7 +145,7 @@ class FilesystemService(
|
||||
if (file.isFile) {
|
||||
file.length()
|
||||
} else if (file.isDirectory) {
|
||||
File(path).walkTopDown().filter { it.isFile }.map { it.length() }.sum()
|
||||
File(path).walkTopDown().filter { it.isFile }.sumOf { it.length() }
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import org.hibernate.type.Type
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class EntityUpdateInterceptor() : Interceptor {
|
||||
class EntityUpdateInterceptor : Interceptor {
|
||||
|
||||
override fun onFlushDirty(
|
||||
entity: Any?,
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
package org.gameyfin.app.core.logging.dto
|
||||
|
||||
import org.springframework.boot.logging.LogLevel
|
||||
|
||||
data class LogConfigDto(
|
||||
val logFolder: String,
|
||||
val maxHistoryDays: Int,
|
||||
val logLevel: LogLevel
|
||||
)
|
||||
@ -136,7 +136,7 @@ class PluginService(
|
||||
|
||||
fun getConfig(pluginWrapper: PluginWrapper): Map<String, String?> {
|
||||
log.debug { "Getting config for plugin ${pluginWrapper.pluginId}" }
|
||||
return pluginConfigRepository.findAllById_PluginId(pluginWrapper.pluginId).associate { it.id.key to it.value }
|
||||
return pluginConfigRepository.findAllByPluginId(pluginWrapper.pluginId).associate { it.id.key to it.value }
|
||||
}
|
||||
|
||||
fun updateConfig(pluginId: String, config: Map<String, String>) {
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package org.gameyfin.app.core.plugins.config
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
|
||||
interface PluginConfigRepository : JpaRepository<PluginConfigEntry, PluginConfigEntryKey> {
|
||||
fun findAllById_PluginId(pluginId: String): List<PluginConfigEntry>
|
||||
fun findById_PluginIdAndId_Key(pluginId: String, key: String): PluginConfigEntry?
|
||||
|
||||
@Query("SELECT p FROM PluginConfigEntry p WHERE p.id.pluginId = :pluginId")
|
||||
fun findAllByPluginId(pluginId: String): List<PluginConfigEntry>
|
||||
}
|
||||
@ -10,9 +10,7 @@ class DatabasePluginStatusProvider(
|
||||
) : PluginStatusProvider {
|
||||
|
||||
override fun isPluginDisabled(pluginId: String): Boolean {
|
||||
val pluginManagement = pluginManagementRepository.findByIdOrNull(pluginId)
|
||||
|
||||
if (pluginManagement == null) return true
|
||||
val pluginManagement = pluginManagementRepository.findByIdOrNull(pluginId) ?: return true
|
||||
|
||||
return !pluginManagement.enabled
|
||||
}
|
||||
|
||||
@ -3,10 +3,10 @@ package org.gameyfin.app.core.plugins.management
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.pf4j.ExtensionDescriptor
|
||||
import org.pf4j.ExtensionWrapper
|
||||
import org.pf4j.LegacyExtensionFinder
|
||||
import org.pf4j.IndexedExtensionFinder
|
||||
import org.pf4j.PluginManager
|
||||
|
||||
class GameyfinExtensionFinder(pluginManager: PluginManager) : LegacyExtensionFinder(pluginManager) {
|
||||
class GameyfinExtensionFinder(pluginManager: PluginManager) : IndexedExtensionFinder(pluginManager) {
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger { }
|
||||
}
|
||||
@ -27,7 +27,7 @@ class GameyfinExtensionFinder(pluginManager: PluginManager) : LegacyExtensionFin
|
||||
}
|
||||
|
||||
val classLoader =
|
||||
if (pluginId != null) pluginManager.getPluginClassLoader(pluginId) else javaClass.getClassLoader()
|
||||
if (pluginId != null) pluginManager.getPluginClassLoader(pluginId) else javaClass.classLoader
|
||||
|
||||
for (className in classNames) {
|
||||
try {
|
||||
|
||||
@ -3,7 +3,7 @@ package org.gameyfin.app.core.plugins.management
|
||||
import org.pf4j.ManifestPluginDescriptorFinder
|
||||
import java.util.jar.Manifest
|
||||
|
||||
class GameyfinManifestPluginDescriptorFinder() : ManifestPluginDescriptorFinder() {
|
||||
class GameyfinManifestPluginDescriptorFinder : ManifestPluginDescriptorFinder() {
|
||||
|
||||
companion object {
|
||||
const val PLUGIN_NAME: String = "Plugin-Name"
|
||||
|
||||
@ -253,7 +253,7 @@ class GameyfinPluginManager(
|
||||
}
|
||||
|
||||
private fun getConfig(pluginId: String): Map<String, String?> {
|
||||
return pluginConfigRepository.findAllById_PluginId(pluginId).associate { it.id.key to it.value }
|
||||
return pluginConfigRepository.findAllByPluginId(pluginId).associate { it.id.key to it.value }
|
||||
}
|
||||
|
||||
private fun loadPluginSignaturePublicKey(): PublicKey {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package org.gameyfin.app.core.security
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import jakarta.persistence.AttributeConverter
|
||||
import jakarta.persistence.Converter
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
|
||||
@Converter
|
||||
class EncryptionMapConverter : AttributeConverter<Map<String, String>, String> {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package org.gameyfin.app.core.security
|
||||
|
||||
import com.vaadin.flow.spring.security.VaadinAwareSecurityContextHolderStrategyConfiguration
|
||||
import com.vaadin.flow.spring.security.VaadinSecurityConfigurer
|
||||
import com.vaadin.hilla.route.RouteUtil
|
||||
import org.gameyfin.app.config.ConfigProperties
|
||||
@ -8,7 +7,6 @@ import org.gameyfin.app.config.ConfigService
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Conditional
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
@ -25,9 +23,6 @@ import org.springframework.security.web.authentication.logout.HttpStatusReturnin
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@Import(
|
||||
VaadinAwareSecurityContextHolderStrategyConfiguration::class
|
||||
)
|
||||
class SecurityConfig(
|
||||
private val environment: Environment,
|
||||
private val config: ConfigService,
|
||||
@ -41,6 +36,31 @@ class SecurityConfig(
|
||||
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity, routeUtil: RouteUtil): SecurityFilterChain {
|
||||
// Apply Vaadin configuration first to properly configure CSRF and request matchers
|
||||
if (config.get(ConfigProperties.SSO.OIDC.Enabled) == true) {
|
||||
http.with(VaadinSecurityConfigurer.vaadin()) { configurer ->
|
||||
// Redirect to SSO provider on logout
|
||||
configurer.loginView("/login", config.get(ConfigProperties.SSO.OIDC.LogoutUrl))
|
||||
}
|
||||
|
||||
// Use custom success handler to handle user registration
|
||||
http.oauth2Login { oauth2Login ->
|
||||
oauth2Login.successHandler(ssoAuthenticationSuccessHandler)
|
||||
}
|
||||
// Prevent unnecessary redirects
|
||||
http.logout { logout -> logout.logoutSuccessHandler((HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))) }
|
||||
|
||||
// Custom authentication entry point to support SSO and direct login
|
||||
http.exceptionHandling { exceptionHandling ->
|
||||
exceptionHandling.authenticationEntryPoint(CustomAuthenticationEntryPoint())
|
||||
}
|
||||
} else {
|
||||
// Use default Vaadin login URLs
|
||||
http.with(VaadinSecurityConfigurer.vaadin()) { configurer ->
|
||||
configurer.loginView("/login")
|
||||
}
|
||||
}
|
||||
|
||||
http.authorizeHttpRequests { auth ->
|
||||
// Set default security policy that permits Hilla internal requests and denies all other
|
||||
auth.requestMatchers(routeUtil::isRouteAllowed).permitAll()
|
||||
@ -56,6 +76,13 @@ class SecurityConfig(
|
||||
"/favicon.ico",
|
||||
"/favicon.svg"
|
||||
).permitAll()
|
||||
// Client-side SPA routes - these need to pass through to serve index.html
|
||||
// Authentication will be handled by Hilla on the client side
|
||||
.requestMatchers(
|
||||
"/administration/**",
|
||||
"/settings/**",
|
||||
"/collection/**"
|
||||
).permitAll()
|
||||
// Dynamic public access for certain endpoints
|
||||
.requestMatchers(
|
||||
"/",
|
||||
@ -78,30 +105,6 @@ class SecurityConfig(
|
||||
http.cors { cors -> cors.disable() }
|
||||
|
||||
|
||||
if (config.get(ConfigProperties.SSO.OIDC.Enabled) == true) {
|
||||
|
||||
http.with(VaadinSecurityConfigurer.vaadin()) { configurer ->
|
||||
// Redirect to SSO provider on logout
|
||||
configurer.loginView("/login", config.get(ConfigProperties.SSO.OIDC.LogoutUrl))
|
||||
}
|
||||
|
||||
// Use custom success handler to handle user registration
|
||||
http.oauth2Login { oauth2Login ->
|
||||
oauth2Login.successHandler(ssoAuthenticationSuccessHandler)
|
||||
}
|
||||
// Prevent unnecessary redirects
|
||||
http.logout { logout -> logout.logoutSuccessHandler((HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))) }
|
||||
|
||||
// Custom authentication entry point to support SSO and direct login
|
||||
http.exceptionHandling { exceptionHandling ->
|
||||
exceptionHandling.authenticationEntryPoint(CustomAuthenticationEntryPoint())
|
||||
}
|
||||
} else {
|
||||
// Use default Vaadin login URLs
|
||||
http.with(VaadinSecurityConfigurer.vaadin()) { configurer ->
|
||||
configurer.loginView("/login")
|
||||
}
|
||||
}
|
||||
|
||||
if ("dev" in environment.activeProfiles) {
|
||||
http.authorizeHttpRequests { auth -> auth.requestMatchers("/h2-console/**").permitAll() }
|
||||
|
||||
@ -13,5 +13,5 @@ fun isCurrentUserAdmin(): Boolean {
|
||||
}
|
||||
|
||||
fun Authentication.isAdmin(): Boolean {
|
||||
return this.authorities?.any { it.authority == Role.Names.ADMIN || it.authority == Role.Names.SUPERADMIN } ?: false
|
||||
return this.authorities.any { it.authority == Role.Names.ADMIN || it.authority == Role.Names.SUPERADMIN }
|
||||
}
|
||||
@ -23,7 +23,7 @@ class SsoAuthenticationSuccessHandler(
|
||||
private val userService: UserService,
|
||||
private val roleService: RoleService,
|
||||
private val config: ConfigService,
|
||||
private val roleHierarchy: RoleHierarchy,
|
||||
roleHierarchy: RoleHierarchy,
|
||||
) : AuthenticationSuccessHandler {
|
||||
|
||||
private val authoritiesMapper = RoleHierarchyAuthoritiesMapper(roleHierarchy)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package org.gameyfin.app.core.security
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.gameyfin.app.config.ConfigProperties
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.context.annotation.Condition
|
||||
import org.springframework.context.annotation.ConditionContext
|
||||
import org.springframework.core.env.Environment
|
||||
@ -13,13 +15,24 @@ import java.sql.DriverManager
|
||||
* So we are rawdogging the database connection and query execution here.
|
||||
*/
|
||||
class SsoEnabledCondition : Condition {
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger { }
|
||||
}
|
||||
|
||||
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
|
||||
try {
|
||||
val environment = context.beanFactory!!.getBean(Environment::class.java);
|
||||
val url = environment.getProperty("spring.datasource.url");
|
||||
val user = environment.getProperty("spring.datasource.username");
|
||||
val password = environment.getProperty("spring.datasource.password");
|
||||
val connection = DriverManager.getConnection(url, user, password);
|
||||
val environment = context.beanFactory?.getBean<Environment>()
|
||||
|
||||
if (environment == null) {
|
||||
log.warn { "Environment hasn't been loaded yet, cannot determine if SSO is enabled." }
|
||||
return false
|
||||
}
|
||||
|
||||
val url = environment.getProperty("spring.datasource.url")
|
||||
val user = environment.getProperty("spring.datasource.username")
|
||||
val password = environment.getProperty("spring.datasource.password")
|
||||
val connection = DriverManager.getConnection(url, user, password)
|
||||
|
||||
connection.use { c ->
|
||||
val statement = c.prepareStatement("SELECT \"value\" FROM app_config WHERE \"key\" = ?")
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import tools.jackson.databind.JsonNode
|
||||
import tools.jackson.databind.ValueDeserializer
|
||||
import java.io.Serializable
|
||||
|
||||
class ArrayDeserializer : JsonDeserializer<Serializable>() {
|
||||
class ArrayDeserializer : ValueDeserializer<Serializable>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Serializable {
|
||||
val node = p.codec.readTree<JsonNode>(p)
|
||||
val node = p.objectReadContext().readTree<JsonNode>(p)
|
||||
return if (node.isArray) {
|
||||
node.map { it.asText() }.toTypedArray()
|
||||
node.map { it.asString() }.toTypedArray()
|
||||
} else {
|
||||
p.codec.treeToValue(node, Serializable::class.java)
|
||||
ctxt.readTreeAsValue(node, Serializable::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,16 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.JsonSerializer
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import tools.jackson.core.JsonGenerator
|
||||
import tools.jackson.databind.SerializationContext
|
||||
import tools.jackson.databind.ValueSerializer
|
||||
|
||||
|
||||
/**
|
||||
* A generic Jackson serializer for enums that have a displayName property.
|
||||
* This serializer writes the displayName value instead of the enum constant name.
|
||||
*/
|
||||
class DisplayableSerializer : JsonSerializer<Any>() {
|
||||
override fun serialize(value: Any?, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
class DisplayableSerializer : ValueSerializer<Any>() {
|
||||
override fun serialize(value: Any?, gen: JsonGenerator, serializers: SerializationContext) {
|
||||
if (value == null) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import org.gameyfin.pluginapi.gamemetadata.GameFeature
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import tools.jackson.databind.ValueDeserializer
|
||||
|
||||
/**
|
||||
* Jackson deserializer for GameFeature enum.
|
||||
* Deserializes JSON strings by matching against the GameFeature's displayName property.
|
||||
*/
|
||||
class GameFeatureDeserializer : JsonDeserializer<GameFeature?>() {
|
||||
class GameFeatureDeserializer : ValueDeserializer<GameFeature?>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): GameFeature? {
|
||||
val displayName = p.text ?: return null
|
||||
val displayName = p.string ?: return null
|
||||
|
||||
if (displayName.isEmpty()) {
|
||||
return null
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import org.gameyfin.pluginapi.gamemetadata.Genre
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import tools.jackson.databind.ValueDeserializer
|
||||
|
||||
/**
|
||||
* Jackson deserializer for Genre enum.
|
||||
* Deserializes JSON strings by matching against the Genre's displayName property.
|
||||
*/
|
||||
class GenreDeserializer : JsonDeserializer<Genre?>() {
|
||||
class GenreDeserializer : ValueDeserializer<Genre?>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Genre? {
|
||||
val displayName = p.text ?: return null
|
||||
val displayName = p.string ?: return null
|
||||
|
||||
if (displayName.isEmpty()) {
|
||||
return null
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import org.gameyfin.pluginapi.gamemetadata.Platform
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import tools.jackson.databind.ValueDeserializer
|
||||
|
||||
/**
|
||||
* Jackson deserializer for Platform enum.
|
||||
* Deserializes JSON strings by matching against the Platform's displayName property.
|
||||
*/
|
||||
class PlatformDeserializer : JsonDeserializer<Platform?>() {
|
||||
class PlatformDeserializer : ValueDeserializer<Platform?>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Platform? {
|
||||
val displayName = p.text ?: return null
|
||||
val displayName = p.string ?: return null
|
||||
|
||||
if (displayName.isEmpty()) {
|
||||
return null
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import org.gameyfin.pluginapi.gamemetadata.PlayerPerspective
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import tools.jackson.databind.ValueDeserializer
|
||||
|
||||
/**
|
||||
* Jackson deserializer for PlayerPerspective enum.
|
||||
* Deserializes JSON strings by matching against the PlayerPerspective's displayName property.
|
||||
*/
|
||||
class PlayerPerspectiveDeserializer : JsonDeserializer<PlayerPerspective?>() {
|
||||
class PlayerPerspectiveDeserializer : ValueDeserializer<PlayerPerspective?>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): PlayerPerspective? {
|
||||
val displayName = p.text ?: return null
|
||||
val displayName = p.string ?: return null
|
||||
|
||||
if (displayName.isEmpty()) {
|
||||
return null
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import org.gameyfin.pluginapi.gamemetadata.Theme
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import tools.jackson.databind.ValueDeserializer
|
||||
|
||||
/**
|
||||
* Jackson deserializer for Theme enum.
|
||||
* Deserializes JSON strings by matching against the Theme's displayName property.
|
||||
*/
|
||||
class ThemeDeserializer : JsonDeserializer<Theme?>() {
|
||||
class ThemeDeserializer : ValueDeserializer<Theme?>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Theme? {
|
||||
val displayName = p.text ?: return null
|
||||
val displayName = p.string ?: return null
|
||||
|
||||
if (displayName.isEmpty()) {
|
||||
return null
|
||||
|
||||
@ -15,6 +15,10 @@ import kotlin.time.toJavaDuration
|
||||
@Entity
|
||||
class Token<T : TokenType>(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
val id: String? = null,
|
||||
|
||||
@Column(unique = true, nullable = false)
|
||||
@Convert(converter = EncryptionConverter::class)
|
||||
val secret: String = UUID.randomUUID().toString(),
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package org.gameyfin.app.core.token
|
||||
|
||||
import org.hibernate.engine.spi.SharedSessionContractImplementor
|
||||
import org.hibernate.type.descriptor.WrapperOptions
|
||||
import org.hibernate.usertype.UserType
|
||||
import java.io.Serializable
|
||||
import java.sql.PreparedStatement
|
||||
@ -25,8 +25,7 @@ class TokenTypeUserType : UserType<TokenType> {
|
||||
override fun nullSafeGet(
|
||||
rs: ResultSet,
|
||||
position: Int,
|
||||
session: SharedSessionContractImplementor,
|
||||
owner: Any?
|
||||
options: WrapperOptions
|
||||
): TokenType? {
|
||||
val key = rs.getString(position) ?: return null
|
||||
val tokenTypeClass = TokenType::class
|
||||
@ -41,7 +40,7 @@ class TokenTypeUserType : UserType<TokenType> {
|
||||
st: PreparedStatement,
|
||||
value: TokenType?,
|
||||
index: Int,
|
||||
session: SharedSessionContractImplementor
|
||||
options: WrapperOptions
|
||||
) {
|
||||
if (value == null) {
|
||||
st.setNull(index, Types.VARCHAR)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
package org.gameyfin.app.core.token
|
||||
|
||||
enum class TokenValidationResult() {
|
||||
enum class TokenValidationResult {
|
||||
VALID, INVALID, EXPIRED
|
||||
}
|
||||
@ -120,7 +120,7 @@ class GameService(
|
||||
imageService.downloadIfNew(it)
|
||||
}
|
||||
|
||||
game.images.map {
|
||||
game.images.forEach {
|
||||
imageService.downloadIfNew(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@ -647,7 +647,7 @@ class GameService(
|
||||
// (Optional) Step 0: Extract title from filename using regex
|
||||
if (config.get(ConfigProperties.Libraries.Scan.ExtractTitleUsingRegex) == true) {
|
||||
val regexString = config.get(ConfigProperties.Libraries.Scan.TitleExtractionRegex)
|
||||
if (regexString != null && regexString.isNotEmpty()) {
|
||||
if (!regexString.isNullOrEmpty()) {
|
||||
try {
|
||||
val regex = Regex(regexString)
|
||||
val originalQuery = query
|
||||
|
||||
@ -32,6 +32,7 @@ class Game(
|
||||
|
||||
@ElementCollection(targetClass = Platform::class, fetch = FetchType.EAGER)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
var platforms: MutableList<Platform> = mutableListOf(),
|
||||
|
||||
var title: String? = null,
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
package org.gameyfin.app.games.repositories
|
||||
|
||||
import org.gameyfin.app.media.Image
|
||||
import org.springframework.content.commons.store.ContentStore
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface ImageContentStore : ContentStore<Image, String>
|
||||
@ -9,9 +9,12 @@ class IgnoredPath(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
var id: Long? = null,
|
||||
|
||||
@Column(unique = true, nullable = false, length = 1024)
|
||||
val path: String,
|
||||
|
||||
@OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.EAGER)
|
||||
@JoinColumn(nullable = false)
|
||||
val source: IgnoredPathSource
|
||||
) {
|
||||
fun getType(): IgnoredPathSourceType {
|
||||
|
||||
@ -29,6 +29,7 @@ class Library(
|
||||
|
||||
@ElementCollection(targetClass = Platform::class, fetch = FetchType.EAGER)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
var platforms: MutableList<Platform> = ArrayList(),
|
||||
|
||||
@OneToMany(mappedBy = "library", fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
package org.gameyfin.app.media
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.*
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteExisting
|
||||
import kotlin.io.path.exists
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* Service for handling file storage operations.
|
||||
* Files are stored in the filesystem under the specified root directory.
|
||||
* The content ID is a UUID string used as the filename.
|
||||
* Files are stored without extensions; MIME type is managed separately.
|
||||
*
|
||||
* Note: This is a drop-in replacement for Spring Content's filesystem storage (which has been discontinued).
|
||||
*/
|
||||
@Service
|
||||
class FileStorageService(
|
||||
@param:Value($$"${spring.content.fs.filesystem-root:./data/}") private val storageRoot: String
|
||||
) {
|
||||
private val rootPath: Path = Path.of(storageRoot)
|
||||
|
||||
init {
|
||||
// Ensure storage directory exists
|
||||
if (!rootPath.exists()) {
|
||||
rootPath.createDirectories()
|
||||
logger.info { "Created file storage directory: $rootPath" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a file and returns the generated content ID (UUID).
|
||||
*/
|
||||
fun saveFile(inputStream: InputStream): String {
|
||||
val contentId = UUID.randomUUID().toString()
|
||||
val filePath = rootPath.resolve(contentId)
|
||||
|
||||
inputStream.use { input ->
|
||||
Files.copy(input, filePath, StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
|
||||
logger.debug { "Saved file with contentId: $contentId" }
|
||||
return contentId
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a file by content ID.
|
||||
* Returns null if the file doesn't exist.
|
||||
*/
|
||||
fun getFile(contentId: String?): InputStream? {
|
||||
if (contentId == null) return null
|
||||
|
||||
val filePath = rootPath.resolve(contentId)
|
||||
return if (filePath.exists()) {
|
||||
Files.newInputStream(filePath)
|
||||
} else {
|
||||
logger.warn { "File not found for contentId: $contentId" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file by content ID.
|
||||
*/
|
||||
fun deleteFile(contentId: String?) {
|
||||
if (contentId == null) return
|
||||
|
||||
val filePath = rootPath.resolve(contentId)
|
||||
if (filePath.exists()) {
|
||||
filePath.deleteExisting()
|
||||
logger.debug { "Deleted file with contentId: $contentId" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file exists for the given content ID.
|
||||
*/
|
||||
fun fileExists(contentId: String?): Boolean {
|
||||
if (contentId == null) return false
|
||||
return rootPath.resolve(contentId).exists()
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,6 @@
|
||||
package org.gameyfin.app.media
|
||||
|
||||
import jakarta.persistence.*
|
||||
import org.springframework.content.commons.annotations.ContentId
|
||||
import org.springframework.content.commons.annotations.ContentLength
|
||||
import org.springframework.content.commons.annotations.MimeType
|
||||
|
||||
@Entity
|
||||
class Image(
|
||||
@ -16,13 +13,10 @@ class Image(
|
||||
|
||||
val type: ImageType,
|
||||
|
||||
@ContentId
|
||||
var contentId: String? = null,
|
||||
|
||||
@ContentLength
|
||||
var contentLength: Long? = null,
|
||||
|
||||
@MimeType
|
||||
var mimeType: String? = null,
|
||||
|
||||
var blurhash: String? = null
|
||||
|
||||
@ -28,22 +28,22 @@ class ImageEndpoint(
|
||||
) {
|
||||
|
||||
@GetMapping("/screenshot/{id}")
|
||||
fun getScreenshot(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? {
|
||||
fun getScreenshot(@PathVariable id: Long): ResponseEntity<InputStreamResource>? {
|
||||
return getImageContent(id)
|
||||
}
|
||||
|
||||
@GetMapping("/cover/{id}")
|
||||
fun getCover(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? {
|
||||
fun getCover(@PathVariable id: Long): ResponseEntity<InputStreamResource>? {
|
||||
return getImageContent(id)
|
||||
}
|
||||
|
||||
@GetMapping("/header/{id}")
|
||||
fun getHeader(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? {
|
||||
fun getHeader(@PathVariable id: Long): ResponseEntity<InputStreamResource>? {
|
||||
return getImageContent(id)
|
||||
}
|
||||
|
||||
@GetMapping("/plugins/{id}/logo")
|
||||
fun getPluginLogo(@PathVariable("id") pluginId: String): ResponseEntity<ByteArrayResource>? {
|
||||
@GetMapping("/plugins/{pluginId}/logo")
|
||||
fun getPluginLogo(@PathVariable pluginId: String): ResponseEntity<ByteArrayResource>? {
|
||||
val logo = pluginService.getLogo(pluginId)
|
||||
return Utils.inputStreamToResponseEntity(logo)
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import org.gameyfin.app.core.events.GameUpdatedEvent
|
||||
import org.gameyfin.app.core.events.UserDeletedEvent
|
||||
import org.gameyfin.app.core.events.UserUpdatedEvent
|
||||
import org.gameyfin.app.games.repositories.GameRepository
|
||||
import org.gameyfin.app.games.repositories.ImageContentStore
|
||||
import org.gameyfin.app.games.repositories.ImageRepository
|
||||
import org.gameyfin.app.users.persistence.UserRepository
|
||||
import org.springframework.dao.DataIntegrityViolationException
|
||||
@ -28,7 +27,7 @@ import javax.imageio.ImageIO
|
||||
@Service
|
||||
class ImageService(
|
||||
private val imageRepository: ImageRepository,
|
||||
private val imageContentStore: ImageContentStore,
|
||||
private val fileStorageService: FileStorageService,
|
||||
private val gameRepository: GameRepository,
|
||||
private val userRepository: UserRepository
|
||||
) {
|
||||
@ -39,6 +38,7 @@ class ImageService(
|
||||
* Scale down image for faster blurhash calculation.
|
||||
* Blurhash doesn't need full resolution - 100px width is plenty for a good blur.
|
||||
*/
|
||||
@Suppress("DuplicatedCode")
|
||||
fun scaleImageForBlurhash(original: BufferedImage, maxWidth: Int = 100): BufferedImage {
|
||||
val originalWidth = original.width
|
||||
val originalHeight = original.height
|
||||
@ -49,10 +49,9 @@ class ImageService(
|
||||
}
|
||||
|
||||
val scale = maxWidth.toDouble() / originalWidth
|
||||
val targetWidth = maxWidth
|
||||
val targetHeight = (originalHeight * scale).toInt()
|
||||
|
||||
val scaled = BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB)
|
||||
val scaled = BufferedImage(maxWidth, targetHeight, BufferedImage.TYPE_INT_RGB)
|
||||
val g2d = scaled.createGraphics()
|
||||
|
||||
// Use fast scaling for blurhash - quality doesn't matter much for a blur
|
||||
@ -60,7 +59,7 @@ class ImageService(
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED)
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF)
|
||||
|
||||
g2d.drawImage(original, 0, 0, targetWidth, targetHeight, null)
|
||||
g2d.drawImage(original, 0, 0, maxWidth, targetHeight, null)
|
||||
g2d.dispose()
|
||||
|
||||
return scaled
|
||||
@ -152,7 +151,6 @@ class ImageService(
|
||||
// If the existing image has valid content we can just associate it instead of downloading again
|
||||
if (existingImageHasValidContent && existingImage.contentId != null) {
|
||||
// Associate existing content with the current image entity reference
|
||||
imageContentStore.associate(image, existingImage.contentId)
|
||||
image.contentId = existingImage.contentId
|
||||
image.contentLength = existingImage.contentLength
|
||||
image.mimeType = existingImage.mimeType
|
||||
@ -162,19 +160,7 @@ class ImageService(
|
||||
// If no existing image or existing image has no valid content, download it
|
||||
TikaInputStream.get { URI.create(image.originalUrl).toURL().openStream() }.use { input ->
|
||||
image.mimeType = tika.detect(input)
|
||||
|
||||
// Read the input stream into a byte array so we can use it twice
|
||||
val imageBytes = input.readBytes()
|
||||
|
||||
// Calculate blurhash
|
||||
ByteArrayInputStream(imageBytes).use { blurhashStream ->
|
||||
image.blurhash = calculateBlurhash(blurhashStream)
|
||||
}
|
||||
|
||||
// Store content
|
||||
ByteArrayInputStream(imageBytes).use { contentStream ->
|
||||
imageContentStore.setContent(image, contentStream)
|
||||
}
|
||||
processImageContent(image, input)
|
||||
}
|
||||
|
||||
// Save or update the image to ensure it's persisted
|
||||
@ -187,21 +173,7 @@ class ImageService(
|
||||
|
||||
fun createFromInputStream(type: ImageType, content: InputStream, mimeType: String): Image {
|
||||
val image = Image(type = type, mimeType = mimeType)
|
||||
|
||||
// Read the input stream into a byte array so we can use it twice
|
||||
val imageBytes = content.readBytes()
|
||||
|
||||
// Calculate blurhash
|
||||
ByteArrayInputStream(imageBytes).use { blurhashStream ->
|
||||
image.blurhash = calculateBlurhash(blurhashStream)
|
||||
}
|
||||
|
||||
// Store content
|
||||
ByteArrayInputStream(imageBytes).use { contentStream ->
|
||||
imageContentStore.setContent(image, contentStream)
|
||||
}
|
||||
|
||||
// Save with blurhash
|
||||
processImageContent(image, content)
|
||||
return imageRepository.save(image)
|
||||
}
|
||||
|
||||
@ -210,8 +182,7 @@ class ImageService(
|
||||
}
|
||||
|
||||
fun getFileContent(image: Image): InputStream? {
|
||||
return imageContentStore.getContent(image)
|
||||
|
||||
return fileStorageService.getFile(image.contentId)
|
||||
}
|
||||
|
||||
fun deleteImageIfUnused(image: Image) {
|
||||
@ -221,13 +192,30 @@ class ImageService(
|
||||
|
||||
if (!isImageStillInUse) {
|
||||
imageRepository.delete(image)
|
||||
imageContentStore.unsetContent(image)
|
||||
fileStorageService.deleteFile(image.contentId)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFileContent(image: Image, content: InputStream, mimeType: String? = null): Image {
|
||||
mimeType?.let { image.mimeType = it }
|
||||
|
||||
// Delete old file if it exists
|
||||
image.contentId?.let { fileStorageService.deleteFile(it) }
|
||||
|
||||
// Process and store new content
|
||||
processImageContent(image, content)
|
||||
|
||||
return imageRepository.save(image)
|
||||
}
|
||||
|
||||
private fun imageHasValidContent(image: Image): Boolean {
|
||||
return image.contentId != null
|
||||
&& fileStorageService.fileExists(image.contentId)
|
||||
&& image.contentLength != null
|
||||
&& image.contentLength!! > 0
|
||||
}
|
||||
|
||||
private fun processImageContent(image: Image, content: InputStream) {
|
||||
// Read the input stream into a byte array so we can use it twice
|
||||
val imageBytes = content.readBytes()
|
||||
|
||||
@ -238,16 +226,9 @@ class ImageService(
|
||||
|
||||
// Store content
|
||||
ByteArrayInputStream(imageBytes).use { contentStream ->
|
||||
imageContentStore.setContent(image, contentStream)
|
||||
image.contentId = fileStorageService.saveFile(contentStream)
|
||||
image.contentLength = imageBytes.size.toLong()
|
||||
}
|
||||
|
||||
// Save with blurhash
|
||||
return imageRepository.save(image)
|
||||
}
|
||||
|
||||
private fun imageHasValidContent(image: Image): Boolean {
|
||||
val imageContent = imageContentStore.getContent(image)
|
||||
return imageContent != null && image.contentLength != null && image.contentLength!! > 0
|
||||
}
|
||||
|
||||
private fun calculateBlurhash(inputStream: InputStream): String? {
|
||||
|
||||
@ -8,6 +8,7 @@ import org.gameyfin.app.messages.providers.AbstractMessageProvider
|
||||
import org.gameyfin.app.messages.templates.MessageTemplateService
|
||||
import org.gameyfin.app.messages.templates.MessageTemplates
|
||||
import org.gameyfin.app.users.UserService
|
||||
import org.springframework.beans.factory.getBeansOfType
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.scheduling.annotation.Async
|
||||
@ -27,7 +28,7 @@ class MessageService(
|
||||
get() = providers.any { it.enabled }
|
||||
|
||||
private val providers: List<AbstractMessageProvider>
|
||||
get() = applicationContext.getBeansOfType(AbstractMessageProvider::class.java).values.toList()
|
||||
get() = applicationContext.getBeansOfType<AbstractMessageProvider>().values.toList()
|
||||
|
||||
fun testCredentials(provider: String, credentials: Map<String, Any>): Boolean {
|
||||
val messageProvider = providers.find { it.providerKey == provider }
|
||||
|
||||
@ -67,7 +67,7 @@ class EmailMessageProvider(
|
||||
|
||||
val transport = session.getTransport("smtp")
|
||||
|
||||
try {
|
||||
transport.use { transport ->
|
||||
transport.connect(
|
||||
credentials["host"] as String,
|
||||
credentials["port"] as Int,
|
||||
@ -75,8 +75,6 @@ class EmailMessageProvider(
|
||||
credentials["password"] as String
|
||||
)
|
||||
transport.sendMessage(mimeMessage, mimeMessage.allRecipients)
|
||||
} finally {
|
||||
transport.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,18 +7,7 @@ import org.springframework.stereotype.Service
|
||||
class SystemService(
|
||||
private val restartEndpoint: RestartEndpoint,
|
||||
) {
|
||||
|
||||
private var restartRequired = false;
|
||||
|
||||
fun restart() {
|
||||
restartEndpoint.restart()
|
||||
}
|
||||
|
||||
fun setRestartRequired() {
|
||||
restartRequired = true
|
||||
}
|
||||
|
||||
fun isRestartRequired(): Boolean {
|
||||
return restartRequired
|
||||
}
|
||||
}
|
||||
@ -6,13 +6,11 @@ import jakarta.annotation.security.RolesAllowed
|
||||
import org.gameyfin.app.core.Role
|
||||
import org.gameyfin.app.core.token.TokenDto
|
||||
import org.gameyfin.app.core.token.TokenValidationResult
|
||||
import org.gameyfin.app.users.UserService
|
||||
|
||||
@Endpoint
|
||||
@AnonymousAllowed
|
||||
class PasswordResetEndpoint(
|
||||
private val passwordResetService: PasswordResetService,
|
||||
private val userService: UserService
|
||||
private val passwordResetService: PasswordResetService
|
||||
) {
|
||||
|
||||
fun requestPasswordReset(email: String) {
|
||||
|
||||
@ -59,7 +59,7 @@ class PasswordResetService(
|
||||
*/
|
||||
fun requestPasswordReset(email: String) {
|
||||
|
||||
val maskedEmail = Utils.Companion.maskEmail(email)
|
||||
val maskedEmail = Utils.maskEmail(email)
|
||||
|
||||
log.info { "Initiating password reset request for '${maskedEmail}'" }
|
||||
|
||||
@ -81,7 +81,7 @@ class PasswordResetService(
|
||||
}
|
||||
|
||||
val token = generate(user)
|
||||
eventPublisher.publishEvent(PasswordResetRequestEvent(this, token, Utils.Companion.getBaseUrl()))
|
||||
eventPublisher.publishEvent(PasswordResetRequestEvent(this, token, Utils.getBaseUrl()))
|
||||
|
||||
// Simulate a delay to prevent timing attacks
|
||||
Thread.sleep(secureRandom.nextLong(1024))
|
||||
|
||||
@ -29,7 +29,7 @@ class UserPreferencesService(
|
||||
return if (appConfig != null) {
|
||||
getValue(appConfig.value, userPreference)
|
||||
} else {
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ class UserPreferencesService(
|
||||
return if (appConfig != null) {
|
||||
getValue(appConfig.value, userPreference).toString()
|
||||
} else {
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,14 +27,14 @@ class InvitationService(
|
||||
|
||||
fun createInvitation(email: String): TokenDto {
|
||||
if (userService.existsByEmail(email))
|
||||
throw IllegalStateException("User with email ${Utils.Companion.maskEmail(email)} is already registered")
|
||||
throw IllegalStateException("User with email ${Utils.maskEmail(email)} is already registered")
|
||||
|
||||
val auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||
val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found")
|
||||
val payload = mapOf(EMAIL_KEY to email)
|
||||
val token = super.generateWithPayload(user, payload)
|
||||
|
||||
eventPublisher.publishEvent(UserInvitationEvent(this, token, Utils.Companion.getBaseUrl(), email))
|
||||
eventPublisher.publishEvent(UserInvitationEvent(this, token, Utils.getBaseUrl(), email))
|
||||
return TokenDto(token)
|
||||
}
|
||||
|
||||
@ -52,8 +52,8 @@ class InvitationService(
|
||||
try {
|
||||
val user = userService.registerUserFromInvitation(registration, email)
|
||||
super.delete(invitationToken)
|
||||
eventPublisher.publishEvent(AccountStatusChangedEvent(this, user, Utils.Companion.getBaseUrl()))
|
||||
} catch (e: IllegalStateException) {
|
||||
eventPublisher.publishEvent(AccountStatusChangedEvent(this, user, Utils.getBaseUrl()))
|
||||
} catch (_: IllegalStateException) {
|
||||
return UserInvitationAcceptanceResult.USERNAME_TAKEN
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package org.gameyfin.app.util
|
||||
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.context.ApplicationContextAware
|
||||
import org.springframework.stereotype.Component
|
||||
@ -10,7 +11,7 @@ object EntityManagerHolder : ApplicationContextAware {
|
||||
private var entityManager: EntityManager? = null
|
||||
|
||||
override fun setApplicationContext(context: ApplicationContext) {
|
||||
entityManager = context.getBean(EntityManager::class.java)
|
||||
entityManager = context.getBean<EntityManager>()
|
||||
}
|
||||
|
||||
fun getEntityManager(): EntityManager {
|
||||
|
||||
@ -22,6 +22,7 @@ object BlurhashMigration {
|
||||
* Scale down image for faster blurhash calculation.
|
||||
* Blurhash doesn't need full resolution - 100px width is plenty for a good blur.
|
||||
*/
|
||||
@Suppress("DuplicatedCode")
|
||||
private fun scaleImageForBlurhash(original: BufferedImage, maxWidth: Int = 100): BufferedImage {
|
||||
val originalWidth = original.width
|
||||
val originalHeight = original.height
|
||||
@ -32,10 +33,9 @@ object BlurhashMigration {
|
||||
}
|
||||
|
||||
val scale = maxWidth.toDouble() / originalWidth
|
||||
val targetWidth = maxWidth
|
||||
val targetHeight = (originalHeight * scale).toInt()
|
||||
|
||||
val scaled = BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB)
|
||||
val scaled = BufferedImage(maxWidth, targetHeight, BufferedImage.TYPE_INT_RGB)
|
||||
val g2d = scaled.createGraphics()
|
||||
|
||||
// Use fast scaling for blurhash - quality doesn't matter much for a blur
|
||||
@ -43,7 +43,7 @@ object BlurhashMigration {
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED)
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF)
|
||||
|
||||
g2d.drawImage(original, 0, 0, targetWidth, targetHeight, null)
|
||||
g2d.drawImage(original, 0, 0, maxWidth, targetHeight, null)
|
||||
g2d.dispose()
|
||||
|
||||
return scaled
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package org.gameyfin.db.h2
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.gameyfin.app.core.security.EncryptionUtils
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.sql.Connection
|
||||
import java.sql.SQLException
|
||||
|
||||
|
||||
@ -7,8 +7,6 @@ logging.level:
|
||||
org.gameyfin.GameyfinApplicationKt: warn
|
||||
# Suppress false positive warnings from Spring Security 6
|
||||
org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer: error
|
||||
# Hides an error log on the first aborted download
|
||||
org.apache.catalina.core.ContainerBase: off
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
@ -17,10 +15,10 @@ server:
|
||||
tracking-modes: cookie
|
||||
timeout: 24h
|
||||
forward-headers-strategy: framework
|
||||
tomcat:
|
||||
remoteip:
|
||||
protocol-header: X-Forwarded-Proto
|
||||
remote-ip-header: X-Forwarded-For
|
||||
jetty:
|
||||
threads:
|
||||
max: 200
|
||||
min: 8
|
||||
|
||||
management:
|
||||
server:
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
-- Flyway Migration: V2.4.0
|
||||
-- Purpose: Refactor TOKEN table to support encryption on secret field by separating primary key from secret.
|
||||
-- Context: Hibernate 6.x (Spring Boot 4) does not allow AttributeConverter on @Id fields.
|
||||
-- The secret field contains sensitive token data (password reset tokens, etc.) that needs encryption.
|
||||
-- Strategy:
|
||||
-- Modify the existing TOKEN table in-place by adding a new ID column and restructuring constraints.
|
||||
|
||||
-- Step 1: Add new ID column (nullable initially to allow data population)
|
||||
ALTER TABLE TOKEN ADD COLUMN ID CHARACTER VARYING(255);
|
||||
|
||||
-- Step 2: Populate ID column with new UUIDs for existing rows
|
||||
UPDATE TOKEN SET ID = RANDOM_UUID() WHERE ID IS NULL;
|
||||
|
||||
-- Step 3: Make ID column non-null now that it has values
|
||||
ALTER TABLE TOKEN ALTER COLUMN ID SET NOT NULL;
|
||||
|
||||
-- Step 4: Drop the primary key constraint on SECRET
|
||||
-- H2 uses auto-generated constraint names, so we need to find and drop it
|
||||
-- The primary key constraint is typically named PRIMARY_KEY_XXX or CONSTRAINT_XXX
|
||||
ALTER TABLE TOKEN DROP PRIMARY KEY;
|
||||
|
||||
-- Step 5: Add primary key constraint on ID
|
||||
ALTER TABLE TOKEN ADD PRIMARY KEY (ID);
|
||||
|
||||
-- Step 6: Add unique constraint on SECRET (it was previously the primary key, so it was already unique)
|
||||
-- The SECRET column should remain unique for lookups
|
||||
ALTER TABLE TOKEN ADD CONSTRAINT UK_TOKEN_SECRET UNIQUE (SECRET);
|
||||
|
||||
-- Step 7: Create index on SECRET for fast lookups
|
||||
CREATE INDEX IDX_TOKEN_SECRET ON TOKEN(SECRET);
|
||||
@ -1,6 +1,5 @@
|
||||
package org.gameyfin.app.config
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.mockk.*
|
||||
import org.gameyfin.app.config.entities.ConfigEntry
|
||||
import org.gameyfin.app.config.persistence.ConfigRepository
|
||||
@ -9,6 +8,7 @@ import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.springframework.boot.logging.LogLevel
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.io.Serializable
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
@ -425,5 +425,122 @@ class SessionBandwidthTrackerTest {
|
||||
val afterRecordTime = tracker.lastActivityTime
|
||||
assertTrue(afterRecordTime > initialTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateMonitoringStatistics should handle concurrent window rotation`() {
|
||||
val threadCount = 10
|
||||
val executor = Executors.newFixedThreadPool(threadCount)
|
||||
val latch = CountDownLatch(threadCount)
|
||||
|
||||
// Set window start to 11 seconds ago to trigger rotation
|
||||
val monitoringWindowStartField = tracker.javaClass.getDeclaredField("monitoringWindowStart")
|
||||
monitoringWindowStartField.isAccessible = true
|
||||
val elevenSecondsAgo = System.nanoTime() - 11_000_000_000L
|
||||
monitoringWindowStartField.setLong(tracker, elevenSecondsAgo)
|
||||
|
||||
// Have multiple threads try to record bytes at the same time
|
||||
// This should trigger concurrent window rotation attempts
|
||||
repeat(threadCount) {
|
||||
executor.submit {
|
||||
try {
|
||||
tracker.recordBytes(100)
|
||||
} finally {
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS))
|
||||
executor.shutdown()
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS))
|
||||
|
||||
// All bytes should be recorded despite concurrent rotation
|
||||
assertEquals(1000, tracker.totalBytesTransferred)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateMonitoringStatistics should update totalBytesTransferred atomically`() {
|
||||
tracker.recordBytes(1000)
|
||||
assertEquals(1000, tracker.totalBytesTransferred)
|
||||
|
||||
tracker.recordBytes(2000)
|
||||
assertEquals(3000, tracker.totalBytesTransferred)
|
||||
|
||||
tracker.recordBytes(500)
|
||||
assertEquals(3500, tracker.totalBytesTransferred)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateMonitoringStatistics should update lastActivityTime on each call`() {
|
||||
val time1 = tracker.lastActivityTime
|
||||
Thread.sleep(10)
|
||||
|
||||
tracker.recordBytes(100)
|
||||
val time2 = tracker.lastActivityTime
|
||||
assertTrue(time2 > time1, "Activity time should increase after recordBytes")
|
||||
|
||||
Thread.sleep(10)
|
||||
tracker.throttle(100)
|
||||
val time3 = tracker.lastActivityTime
|
||||
assertTrue(time3 > time2, "Activity time should increase after throttle")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentBytesPerSecond should blend with previous window when current window is young`() {
|
||||
// Record bytes in first window
|
||||
tracker.recordBytes(10_000)
|
||||
Thread.sleep(1100) // Wait over 1 second to ensure first window is mature
|
||||
|
||||
// Force window rotation by setting window start to 11 seconds ago
|
||||
val monitoringWindowStartField = tracker.javaClass.getDeclaredField("monitoringWindowStart")
|
||||
monitoringWindowStartField.isAccessible = true
|
||||
val elevenSecondsAgo = System.nanoTime() - 11_000_000_000L
|
||||
monitoringWindowStartField.setLong(tracker, elevenSecondsAgo)
|
||||
|
||||
// Record bytes to trigger rotation
|
||||
tracker.recordBytes(5_000)
|
||||
|
||||
// Immediately check rate - should blend with previous window since current is young
|
||||
Thread.sleep(100) // Sleep a bit but less than 1 second
|
||||
val rate = tracker.getCurrentBytesPerSecond()
|
||||
|
||||
// The rate should be positive and influenced by both windows
|
||||
assertTrue(rate > 0, "Rate should be positive with blended windows")
|
||||
assertTrue(tracker.totalBytesTransferred == 15_000L, "Total should be 15,000 bytes")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateMonitoringStatistics should handle synchronized block correctly during rotation`() {
|
||||
// Record initial bytes
|
||||
tracker.recordBytes(1000)
|
||||
|
||||
// Set up for window rotation
|
||||
val monitoringWindowStartField = tracker.javaClass.getDeclaredField("monitoringWindowStart")
|
||||
monitoringWindowStartField.isAccessible = true
|
||||
val elevenSecondsAgo = System.nanoTime() - 11_000_000_000L
|
||||
monitoringWindowStartField.setLong(tracker, elevenSecondsAgo)
|
||||
|
||||
// Record more bytes - should trigger synchronized block for rotation
|
||||
tracker.recordBytes(2000)
|
||||
|
||||
// Verify the bytes were recorded correctly
|
||||
assertEquals(3000, tracker.totalBytesTransferred)
|
||||
|
||||
// Record more bytes in the new window
|
||||
tracker.recordBytes(500)
|
||||
assertEquals(3500, tracker.totalBytesTransferred)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throttle should call updateMonitoringStatistics with correct byte count`() {
|
||||
val bytes = 5000L
|
||||
tracker.throttle(bytes)
|
||||
|
||||
// Verify bytes were recorded
|
||||
assertEquals(bytes, tracker.totalBytesTransferred)
|
||||
|
||||
// Verify activity time was updated
|
||||
assertTrue(tracker.lastActivityTime > 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ class GameyfinPluginManagerTest {
|
||||
pluginManagementRepository = mockk(relaxed = true)
|
||||
|
||||
// Set up default mocks
|
||||
every { pluginConfigRepository.findAllById_PluginId(any()) } returns emptyList()
|
||||
every { pluginConfigRepository.findAllByPluginId(any()) } returns emptyList()
|
||||
every { pluginManagementRepository.findByIdOrNull(any()) } returns null
|
||||
every { pluginManagementRepository.save(any()) } returnsArgument 0
|
||||
every { pluginManagementRepository.findMaxPriority() } returns null
|
||||
@ -233,7 +233,7 @@ class GameyfinPluginManagerTest {
|
||||
every { pluginManager.getPlugin("test-plugin") } returns pluginWrapper
|
||||
every { pluginManager.stopPlugin("test-plugin") } returns PluginState.STOPPED
|
||||
every { pluginManager.startPlugin("test-plugin") } returns PluginState.STARTED
|
||||
every { pluginConfigRepository.findAllById_PluginId("test-plugin") } returns configEntries
|
||||
every { pluginConfigRepository.findAllByPluginId("test-plugin") } returns configEntries
|
||||
|
||||
pluginManager.restart("test-plugin")
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import org.gameyfin.app.config.ConfigProperties
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.context.annotation.ConditionContext
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata
|
||||
@ -30,7 +31,7 @@ class SsoEnabledConditionTest {
|
||||
environment = mockk<Environment>()
|
||||
|
||||
every { context.beanFactory } returns mockk {
|
||||
every { getBean(Environment::class.java) } returns environment
|
||||
every { getBean<Environment>() } returns environment
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.unmockkAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.core.ObjectReadContext
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import tools.jackson.databind.JsonNode
|
||||
import java.io.Serializable
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ArrayDeserializerTest {
|
||||
|
||||
private lateinit var deserializer: ArrayDeserializer
|
||||
private lateinit var jsonParser: JsonParser
|
||||
private lateinit var deserializationContext: DeserializationContext
|
||||
private lateinit var objectReadContext: ObjectReadContext
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
deserializer = ArrayDeserializer()
|
||||
jsonParser = mockk()
|
||||
deserializationContext = mockk()
|
||||
objectReadContext = mockk()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should convert JSON array to String array`() {
|
||||
val textNode1 = mockk<JsonNode>()
|
||||
val textNode2 = mockk<JsonNode>()
|
||||
val textNode3 = mockk<JsonNode>()
|
||||
every { textNode1.asString() } returns "item1"
|
||||
every { textNode2.asString() } returns "item2"
|
||||
every { textNode3.asString() } returns "item3"
|
||||
|
||||
val arrayNode = mockk<JsonNode>()
|
||||
every { arrayNode.isArray } returns true
|
||||
every { arrayNode.iterator() } returns mutableListOf(textNode1, textNode2, textNode3).iterator()
|
||||
every { jsonParser.objectReadContext() } returns objectReadContext
|
||||
every { objectReadContext.readTree<JsonNode>(jsonParser) } returns arrayNode
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertTrue(result is Array<*>)
|
||||
assertEquals(3, result.size)
|
||||
assertEquals("item1", result[0])
|
||||
assertEquals("item2", result[1])
|
||||
assertEquals("item3", result[2])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should convert empty JSON array to empty String array`() {
|
||||
val arrayNode = mockk<JsonNode>()
|
||||
every { arrayNode.isArray } returns true
|
||||
every { arrayNode.iterator() } returns mutableListOf<JsonNode>().iterator()
|
||||
every { jsonParser.objectReadContext() } returns objectReadContext
|
||||
every { objectReadContext.readTree<JsonNode>(jsonParser) } returns arrayNode
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertTrue(result is Array<*>)
|
||||
assertEquals(0, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle non-array JSON node`() {
|
||||
val textNode = mockk<JsonNode>()
|
||||
val serializable = "test string" as Serializable
|
||||
every { textNode.isArray } returns false
|
||||
every { jsonParser.objectReadContext() } returns objectReadContext
|
||||
every { objectReadContext.readTree<JsonNode>(jsonParser) } returns textNode
|
||||
every { deserializationContext.readTreeAsValue(textNode, Serializable::class.java) } returns serializable
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(serializable, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle array with single element`() {
|
||||
val textNode = mockk<JsonNode>()
|
||||
every { textNode.asString() } returns "single"
|
||||
|
||||
val arrayNode = mockk<JsonNode>()
|
||||
every { arrayNode.isArray } returns true
|
||||
every { arrayNode.iterator() } returns mutableListOf(textNode).iterator()
|
||||
every { jsonParser.objectReadContext() } returns objectReadContext
|
||||
every { objectReadContext.readTree<JsonNode>(jsonParser) } returns arrayNode
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertTrue(result is Array<*>)
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("single", result[0])
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.unmockkAll
|
||||
import io.mockk.verify
|
||||
import org.gameyfin.pluginapi.gamemetadata.GameFeature
|
||||
import org.gameyfin.pluginapi.gamemetadata.Genre
|
||||
import org.gameyfin.pluginapi.gamemetadata.PlayerPerspective
|
||||
import org.gameyfin.pluginapi.gamemetadata.Theme
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import tools.jackson.core.JsonGenerator
|
||||
import tools.jackson.databind.SerializationContext
|
||||
|
||||
class DisplayableSerializerTest {
|
||||
|
||||
private lateinit var serializer: DisplayableSerializer
|
||||
private lateinit var jsonGenerator: JsonGenerator
|
||||
private lateinit var serializationContext: SerializationContext
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
serializer = DisplayableSerializer()
|
||||
jsonGenerator = mockk(relaxed = true)
|
||||
serializationContext = mockk()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should write displayName for valid theme`() {
|
||||
val theme = Theme.SCIENCE_FICTION
|
||||
|
||||
serializer.serialize(theme, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Science Fiction") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should handle null value`() {
|
||||
serializer.serialize(null, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 0) { jsonGenerator.writeString(any<String>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should write displayName for valid genre`() {
|
||||
val genre = Genre.ROLE_PLAYING
|
||||
|
||||
serializer.serialize(genre, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Role-Playing") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should write displayName for valid game feature`() {
|
||||
val feature = GameFeature.MULTIPLAYER
|
||||
|
||||
serializer.serialize(feature, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Multiplayer") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should write displayName for valid player perspective`() {
|
||||
val perspective = PlayerPerspective.FIRST_PERSON
|
||||
|
||||
serializer.serialize(perspective, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("First-Person") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should handle theme with hyphens`() {
|
||||
val theme = Theme.NON_FICTION
|
||||
|
||||
serializer.serialize(theme, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Non-Fiction") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should handle genre with ampersand`() {
|
||||
val genre = Genre.CARD_AND_BOARD_GAME
|
||||
|
||||
serializer.serialize(genre, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Card & Board Game") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should handle genre with slash and apostrophe`() {
|
||||
val genre = Genre.HACK_AND_SLASH_BEAT_EM_UP
|
||||
|
||||
serializer.serialize(genre, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Hack and Slash/Beat 'em up") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should handle feature with hyphen`() {
|
||||
val feature = GameFeature.CROSS_PLATFORM
|
||||
|
||||
serializer.serialize(feature, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Cross-Platform") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should handle perspective with slash`() {
|
||||
val perspective = PlayerPerspective.BIRD_VIEW_ISOMETRIC
|
||||
|
||||
serializer.serialize(perspective, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Bird View/Isometric") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should handle all theme values correctly`() {
|
||||
Theme.entries.forEach { theme ->
|
||||
serializer.serialize(theme, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString(theme.displayName) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should handle all genre values correctly`() {
|
||||
Genre.entries.forEach { genre ->
|
||||
serializer.serialize(genre, jsonGenerator, serializationContext)
|
||||
|
||||
verify(atLeast = 1) { jsonGenerator.writeString(genre.displayName) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should handle all game feature values correctly`() {
|
||||
GameFeature.entries.forEach { feature ->
|
||||
serializer.serialize(feature, jsonGenerator, serializationContext)
|
||||
|
||||
verify(atLeast = 1) { jsonGenerator.writeString(feature.displayName) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should handle all player perspective values correctly`() {
|
||||
PlayerPerspective.entries.forEach { perspective ->
|
||||
serializer.serialize(perspective, jsonGenerator, serializationContext)
|
||||
|
||||
verify(atLeast = 1) { jsonGenerator.writeString(perspective.displayName) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,196 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.unmockkAll
|
||||
import org.gameyfin.pluginapi.gamemetadata.GameFeature
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class GameFeatureDeserializerTest {
|
||||
|
||||
private lateinit var deserializer: GameFeatureDeserializer
|
||||
private lateinit var jsonParser: JsonParser
|
||||
private lateinit var deserializationContext: DeserializationContext
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
deserializer = GameFeatureDeserializer()
|
||||
jsonParser = mockk()
|
||||
deserializationContext = mockk()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return correct feature for valid displayName`() {
|
||||
every { jsonParser.string } returns "Singleplayer"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.SINGLEPLAYER, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for unknown displayName`() {
|
||||
every { jsonParser.string } returns "Unknown Feature"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for empty string`() {
|
||||
every { jsonParser.string } returns ""
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for null string`() {
|
||||
every { jsonParser.string } returns null
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should be case-sensitive`() {
|
||||
every { jsonParser.string } returns "multiplayer"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Multiplayer feature`() {
|
||||
every { jsonParser.string } returns "Multiplayer"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.MULTIPLAYER, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Co-op feature`() {
|
||||
every { jsonParser.string } returns "Co-op"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.CO_OP, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Cross-Platform feature`() {
|
||||
every { jsonParser.string } returns "Cross-Platform"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.CROSS_PLATFORM, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle VR feature`() {
|
||||
every { jsonParser.string } returns "VR"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.VR, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle AR feature`() {
|
||||
every { jsonParser.string } returns "AR"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.AR, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Cloud Saves feature`() {
|
||||
every { jsonParser.string } returns "Cloud Saves"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.CLOUD_SAVES, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Controller Support feature`() {
|
||||
every { jsonParser.string } returns "Controller Support"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.CONTROLLER_SUPPORT, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Local Multiplayer feature`() {
|
||||
every { jsonParser.string } returns "Local Multiplayer"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.LOCAL_MULTIPLAYER, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Online Co-op feature`() {
|
||||
every { jsonParser.string } returns "Online Co-op"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.ONLINE_CO_OP, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Online PvP feature`() {
|
||||
every { jsonParser.string } returns "Online PvP"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.ONLINE_PVP, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Crossplay feature`() {
|
||||
every { jsonParser.string } returns "Crossplay"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.CROSSPLAY, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Splitscreen feature`() {
|
||||
every { jsonParser.string } returns "Splitscreen"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(GameFeature.SPLITSCREEN, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle all valid feature displayNames correctly`() {
|
||||
GameFeature.entries.forEach { feature ->
|
||||
every { jsonParser.string } returns feature.displayName
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(feature, result, "Failed to deserialize ${feature.displayName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,178 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.unmockkAll
|
||||
import org.gameyfin.pluginapi.gamemetadata.Genre
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class GenreDeserializerTest {
|
||||
|
||||
private lateinit var deserializer: GenreDeserializer
|
||||
private lateinit var jsonParser: JsonParser
|
||||
private lateinit var deserializationContext: DeserializationContext
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
deserializer = GenreDeserializer()
|
||||
jsonParser = mockk()
|
||||
deserializationContext = mockk()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return correct genre for valid displayName`() {
|
||||
every { jsonParser.string } returns "Action"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Genre.ACTION, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for unknown displayName`() {
|
||||
every { jsonParser.string } returns "Unknown Genre"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for empty string`() {
|
||||
every { jsonParser.string } returns ""
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for null string`() {
|
||||
every { jsonParser.string } returns null
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should be case-sensitive`() {
|
||||
every { jsonParser.string } returns "action"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Visual Novel genre`() {
|
||||
every { jsonParser.string } returns "Visual Novel"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Genre.VISUAL_NOVEL, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Card & Board Game genre`() {
|
||||
every { jsonParser.string } returns "Card & Board Game"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Genre.CARD_AND_BOARD_GAME, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Point-and-Click genre`() {
|
||||
every { jsonParser.string } returns "Point-and-Click"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Genre.POINT_AND_CLICK, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Real-Time Strategy genre`() {
|
||||
every { jsonParser.string } returns "Real-Time Strategy"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Genre.REAL_TIME_STRATEGY, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Turn-Based Strategy genre`() {
|
||||
every { jsonParser.string } returns "Turn-Based Strategy"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Genre.TURN_BASED_STRATEGY, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Hack and Slash Beat em up genre`() {
|
||||
every { jsonParser.string } returns "Hack and Slash/Beat 'em up"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Genre.HACK_AND_SLASH_BEAT_EM_UP, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Quiz Trivia genre`() {
|
||||
every { jsonParser.string } returns "Quiz/Trivia"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Genre.QUIZ_TRIVIA, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Role-Playing genre`() {
|
||||
every { jsonParser.string } returns "Role-Playing"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Genre.ROLE_PLAYING, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle MOBA genre`() {
|
||||
every { jsonParser.string } returns "MOBA"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Genre.MOBA, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle MMO genre`() {
|
||||
every { jsonParser.string } returns "MMO"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Genre.MMO, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle all valid genre displayNames correctly`() {
|
||||
Genre.entries.forEach { genre ->
|
||||
every { jsonParser.string } returns genre.displayName
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(genre, result, "Failed to deserialize ${genre.displayName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,151 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.unmockkAll
|
||||
import org.gameyfin.pluginapi.gamemetadata.PlayerPerspective
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class PlayerPerspectiveDeserializerTest {
|
||||
|
||||
private lateinit var deserializer: PlayerPerspectiveDeserializer
|
||||
private lateinit var jsonParser: JsonParser
|
||||
private lateinit var deserializationContext: DeserializationContext
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
deserializer = PlayerPerspectiveDeserializer()
|
||||
jsonParser = mockk()
|
||||
deserializationContext = mockk()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return correct perspective for valid displayName`() {
|
||||
every { jsonParser.string } returns "First-Person"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(PlayerPerspective.FIRST_PERSON, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for unknown displayName`() {
|
||||
every { jsonParser.string } returns "Unknown Perspective"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for empty string`() {
|
||||
every { jsonParser.string } returns ""
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for null string`() {
|
||||
every { jsonParser.string } returns null
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should be case-sensitive`() {
|
||||
every { jsonParser.string } returns "first-person"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Third-Person perspective`() {
|
||||
every { jsonParser.string } returns "Third-Person"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(PlayerPerspective.THIRD_PERSON, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Bird View Isometric perspective`() {
|
||||
every { jsonParser.string } returns "Bird View/Isometric"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(PlayerPerspective.BIRD_VIEW_ISOMETRIC, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Side View perspective`() {
|
||||
every { jsonParser.string } returns "Side View"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(PlayerPerspective.SIDE_VIEW, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Text perspective`() {
|
||||
every { jsonParser.string } returns "Text"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(PlayerPerspective.TEXT, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Auditory perspective`() {
|
||||
every { jsonParser.string } returns "Auditory"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(PlayerPerspective.AUDITORY, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Virtual Reality perspective`() {
|
||||
every { jsonParser.string } returns "Virtual Reality"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(PlayerPerspective.VIRTUAL_REALITY, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Unknown perspective`() {
|
||||
every { jsonParser.string } returns "Unknown"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(PlayerPerspective.UNKNOWN, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle all valid perspective displayNames correctly`() {
|
||||
PlayerPerspective.entries.forEach { perspective ->
|
||||
every { jsonParser.string } returns perspective.displayName
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(perspective, result, "Failed to deserialize ${perspective.displayName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,151 @@
|
||||
package org.gameyfin.app.core.serialization
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.unmockkAll
|
||||
import org.gameyfin.pluginapi.gamemetadata.Theme
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class ThemeDeserializerTest {
|
||||
|
||||
private lateinit var deserializer: ThemeDeserializer
|
||||
private lateinit var jsonParser: JsonParser
|
||||
private lateinit var deserializationContext: DeserializationContext
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
deserializer = ThemeDeserializer()
|
||||
jsonParser = mockk()
|
||||
deserializationContext = mockk()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return correct theme for valid displayName`() {
|
||||
every { jsonParser.string } returns "Action"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Theme.ACTION, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for unknown displayName`() {
|
||||
every { jsonParser.string } returns "Unknown Theme"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for empty string`() {
|
||||
every { jsonParser.string } returns ""
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for null string`() {
|
||||
every { jsonParser.string } returns null
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should be case-sensitive`() {
|
||||
every { jsonParser.string } returns "action"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Science Fiction theme`() {
|
||||
every { jsonParser.string } returns "Science Fiction"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Theme.SCIENCE_FICTION, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Non-Fiction theme`() {
|
||||
every { jsonParser.string } returns "Non-Fiction"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Theme.NON_FICTION, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle 4X theme`() {
|
||||
every { jsonParser.string } returns "4X"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Theme.FOUR_X, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Open World theme`() {
|
||||
every { jsonParser.string } returns "Open World"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Theme.OPEN_WORLD, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Horror theme`() {
|
||||
every { jsonParser.string } returns "Horror"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Theme.HORROR, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Fantasy theme`() {
|
||||
every { jsonParser.string } returns "Fantasy"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Theme.FANTASY, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Survival theme`() {
|
||||
every { jsonParser.string } returns "Survival"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(Theme.SURVIVAL, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle all valid theme displayNames correctly`() {
|
||||
Theme.entries.forEach { theme ->
|
||||
every { jsonParser.string } returns theme.displayName
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
assertEquals(theme, result, "Failed to deserialize ${theme.displayName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,8 @@ import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs
|
||||
import org.junit.jupiter.api.condition.OS
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
@ -169,6 +171,7 @@ class LibraryWatcherServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(OS.MAC, disabledReason = "File system watcher events are unreliable on macOS due to FSEvents latency")
|
||||
fun `file create event should trigger quick scan`() {
|
||||
val libraryDir = tempDir.resolve("watch-create")
|
||||
libraryDir.createDirectories()
|
||||
@ -197,6 +200,7 @@ class LibraryWatcherServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(OS.MAC, disabledReason = "File system watcher events are unreliable on macOS due to FSEvents latency")
|
||||
fun `file delete event should trigger quick scan`() {
|
||||
val libraryDir = tempDir.resolve("watch-delete")
|
||||
libraryDir.createDirectories()
|
||||
@ -227,6 +231,7 @@ class LibraryWatcherServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(OS.MAC, disabledReason = "File system watcher events are unreliable on macOS due to FSEvents latency")
|
||||
fun `directory delete event should trigger quick scan`() {
|
||||
val libraryDir = tempDir.resolve("watch-delete-dir")
|
||||
libraryDir.createDirectories()
|
||||
@ -256,6 +261,7 @@ class LibraryWatcherServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(OS.MAC, disabledReason = "File system watcher events are unreliable on macOS due to FSEvents latency")
|
||||
fun `file modify event should update game file size`() {
|
||||
val libraryDir = tempDir.resolve("watch-modify")
|
||||
libraryDir.createDirectories()
|
||||
@ -290,6 +296,7 @@ class LibraryWatcherServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(OS.MAC, disabledReason = "File system watcher events are unreliable on macOS due to FSEvents latency")
|
||||
fun `should handle multiple rapid file changes`() {
|
||||
val libraryDir = tempDir.resolve("watch-rapid")
|
||||
libraryDir.createDirectories()
|
||||
@ -320,6 +327,7 @@ class LibraryWatcherServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(OS.MAC, disabledReason = "File system watcher events are unreliable on macOS due to FSEvents latency")
|
||||
fun `should handle library with multiple directories`() {
|
||||
val dir1 = tempDir.resolve("lib-dir1")
|
||||
val dir2 = tempDir.resolve("lib-dir2")
|
||||
@ -354,6 +362,7 @@ class LibraryWatcherServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(OS.MAC, disabledReason = "File system watcher events are unreliable on macOS due to FSEvents latency")
|
||||
fun `should handle directory creation`() {
|
||||
val libraryDir = tempDir.resolve("watch-dir")
|
||||
libraryDir.createDirectories()
|
||||
|
||||
@ -0,0 +1,235 @@
|
||||
package org.gameyfin.app.media
|
||||
|
||||
import io.mockk.clearAllMocks
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.test.*
|
||||
|
||||
class FileStorageServiceTest {
|
||||
|
||||
@TempDir
|
||||
lateinit var tempDir: Path
|
||||
|
||||
private lateinit var fileStorageService: FileStorageService
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
fileStorageService = FileStorageService(tempDir.toString())
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `init should create storage directory if it does not exist`() {
|
||||
val newDir = tempDir.resolve("newStorage")
|
||||
assertFalse(Files.exists(newDir))
|
||||
|
||||
FileStorageService(newDir.toString())
|
||||
|
||||
assertTrue(Files.exists(newDir))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveFile should store file and return content ID`() {
|
||||
val testData = "test file content".toByteArray()
|
||||
val inputStream = ByteArrayInputStream(testData)
|
||||
|
||||
val contentId = fileStorageService.saveFile(inputStream)
|
||||
|
||||
assertNotNull(contentId)
|
||||
assertTrue(contentId.isNotEmpty())
|
||||
|
||||
val filePath = tempDir.resolve(contentId)
|
||||
assertTrue(Files.exists(filePath))
|
||||
|
||||
val savedContent = Files.readAllBytes(filePath)
|
||||
assertContentEquals(testData, savedContent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveFile should generate unique content IDs for multiple files`() {
|
||||
val testData1 = "first file".toByteArray()
|
||||
val testData2 = "second file".toByteArray()
|
||||
|
||||
val contentId1 = fileStorageService.saveFile(ByteArrayInputStream(testData1))
|
||||
val contentId2 = fileStorageService.saveFile(ByteArrayInputStream(testData2))
|
||||
|
||||
assertNotEquals(contentId1, contentId2)
|
||||
assertTrue(Files.exists(tempDir.resolve(contentId1)))
|
||||
assertTrue(Files.exists(tempDir.resolve(contentId2)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveFile should replace existing file with same content ID`() {
|
||||
val originalData = "original content".toByteArray()
|
||||
val newData = "new content".toByteArray()
|
||||
|
||||
val contentId = fileStorageService.saveFile(ByteArrayInputStream(originalData))
|
||||
|
||||
// Manually save a file with a predictable name
|
||||
val filePath = tempDir.resolve(contentId)
|
||||
Files.write(filePath, newData)
|
||||
|
||||
val retrievedContent = Files.readAllBytes(filePath)
|
||||
assertContentEquals(newData, retrievedContent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFile should return input stream when file exists`() {
|
||||
val testData = "test file content".toByteArray()
|
||||
val contentId = fileStorageService.saveFile(ByteArrayInputStream(testData))
|
||||
|
||||
val inputStream = fileStorageService.getFile(contentId)
|
||||
|
||||
assertNotNull(inputStream)
|
||||
val retrievedData = inputStream.readAllBytes()
|
||||
assertContentEquals(testData, retrievedData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFile should return null when file does not exist`() {
|
||||
val nonExistentId = "non-existent-file-id"
|
||||
|
||||
val inputStream = fileStorageService.getFile(nonExistentId)
|
||||
|
||||
assertNull(inputStream)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFile should return null when content ID is null`() {
|
||||
val inputStream = fileStorageService.getFile(null)
|
||||
|
||||
assertNull(inputStream)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFile should allow reading file multiple times`() {
|
||||
val testData = "test file content".toByteArray()
|
||||
val contentId = fileStorageService.saveFile(ByteArrayInputStream(testData))
|
||||
|
||||
val inputStream1 = fileStorageService.getFile(contentId)
|
||||
assertNotNull(inputStream1)
|
||||
val retrievedData1 = inputStream1.readAllBytes()
|
||||
assertContentEquals(testData, retrievedData1)
|
||||
|
||||
val inputStream2 = fileStorageService.getFile(contentId)
|
||||
assertNotNull(inputStream2)
|
||||
val retrievedData2 = inputStream2.readAllBytes()
|
||||
assertContentEquals(testData, retrievedData2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteFile should remove file when it exists`() {
|
||||
val testData = "test file content".toByteArray()
|
||||
val contentId = fileStorageService.saveFile(ByteArrayInputStream(testData))
|
||||
|
||||
assertTrue(Files.exists(tempDir.resolve(contentId)))
|
||||
|
||||
fileStorageService.deleteFile(contentId)
|
||||
|
||||
assertFalse(Files.exists(tempDir.resolve(contentId)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteFile should not throw exception when file does not exist`() {
|
||||
val nonExistentId = "non-existent-file-id"
|
||||
|
||||
// Should not throw any exception
|
||||
fileStorageService.deleteFile(nonExistentId)
|
||||
|
||||
assertFalse(Files.exists(tempDir.resolve(nonExistentId)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteFile should do nothing when content ID is null`() {
|
||||
// Should not throw any exception
|
||||
fileStorageService.deleteFile(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fileExists should return true when file exists`() {
|
||||
val testData = "test file content".toByteArray()
|
||||
val contentId = fileStorageService.saveFile(ByteArrayInputStream(testData))
|
||||
|
||||
val exists = fileStorageService.fileExists(contentId)
|
||||
|
||||
assertTrue(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fileExists should return false when file does not exist`() {
|
||||
val nonExistentId = "non-existent-file-id"
|
||||
|
||||
val exists = fileStorageService.fileExists(nonExistentId)
|
||||
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fileExists should return false when content ID is null`() {
|
||||
val exists = fileStorageService.fileExists(null)
|
||||
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveFile should handle large files`() {
|
||||
val largeData = ByteArray(10 * 1024 * 1024) { it.toByte() } // 10 MB
|
||||
val inputStream = ByteArrayInputStream(largeData)
|
||||
|
||||
val contentId = fileStorageService.saveFile(inputStream)
|
||||
|
||||
assertNotNull(contentId)
|
||||
assertTrue(fileStorageService.fileExists(contentId))
|
||||
|
||||
val retrievedStream = fileStorageService.getFile(contentId)
|
||||
assertNotNull(retrievedStream)
|
||||
val retrievedData = retrievedStream.readAllBytes()
|
||||
assertContentEquals(largeData, retrievedData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveFile should handle empty files`() {
|
||||
val emptyData = ByteArray(0)
|
||||
val inputStream = ByteArrayInputStream(emptyData)
|
||||
|
||||
val contentId = fileStorageService.saveFile(inputStream)
|
||||
|
||||
assertNotNull(contentId)
|
||||
assertTrue(fileStorageService.fileExists(contentId))
|
||||
|
||||
val retrievedStream = fileStorageService.getFile(contentId)
|
||||
assertNotNull(retrievedStream)
|
||||
val retrievedData = retrievedStream.readAllBytes()
|
||||
assertEquals(0, retrievedData.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `integration test - save, retrieve, and delete file lifecycle`() {
|
||||
val testData = "lifecycle test content".toByteArray()
|
||||
|
||||
// Save file
|
||||
val contentId = fileStorageService.saveFile(ByteArrayInputStream(testData))
|
||||
assertNotNull(contentId)
|
||||
assertTrue(fileStorageService.fileExists(contentId))
|
||||
|
||||
// Retrieve file
|
||||
val retrievedStream = fileStorageService.getFile(contentId)
|
||||
assertNotNull(retrievedStream)
|
||||
val retrievedData = retrievedStream.readAllBytes()
|
||||
assertContentEquals(testData, retrievedData)
|
||||
|
||||
// Delete file
|
||||
fileStorageService.deleteFile(contentId)
|
||||
assertFalse(fileStorageService.fileExists(contentId))
|
||||
assertNull(fileStorageService.getFile(contentId))
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,6 @@ import org.gameyfin.app.core.events.UserDeletedEvent
|
||||
import org.gameyfin.app.core.events.UserUpdatedEvent
|
||||
import org.gameyfin.app.games.entities.Game
|
||||
import org.gameyfin.app.games.repositories.GameRepository
|
||||
import org.gameyfin.app.games.repositories.ImageContentStore
|
||||
import org.gameyfin.app.games.repositories.ImageRepository
|
||||
import org.gameyfin.app.users.entities.User
|
||||
import org.gameyfin.app.users.persistence.UserRepository
|
||||
@ -19,7 +18,6 @@ import org.junit.jupiter.api.Test
|
||||
import org.springframework.dao.DataIntegrityViolationException
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
@ -27,7 +25,7 @@ import kotlin.test.assertNull
|
||||
class ImageServiceTest {
|
||||
|
||||
private lateinit var imageRepository: ImageRepository
|
||||
private lateinit var imageContentStore: ImageContentStore
|
||||
private lateinit var fileStorageService: FileStorageService
|
||||
private lateinit var gameRepository: GameRepository
|
||||
private lateinit var userRepository: UserRepository
|
||||
private lateinit var imageService: ImageService
|
||||
@ -35,10 +33,10 @@ class ImageServiceTest {
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
imageRepository = mockk()
|
||||
imageContentStore = mockk()
|
||||
fileStorageService = mockk()
|
||||
gameRepository = mockk()
|
||||
userRepository = mockk()
|
||||
imageService = ImageService(imageRepository, imageContentStore, gameRepository, userRepository)
|
||||
imageService = ImageService(imageRepository, fileStorageService, gameRepository, userRepository)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@ -181,19 +179,16 @@ class ImageServiceTest {
|
||||
contentLength = 1024L,
|
||||
mimeType = "image/jpeg"
|
||||
)
|
||||
val inputStream = ByteArrayInputStream("image data".toByteArray())
|
||||
|
||||
every { imageRepository.findAllByOriginalUrl(url) } returns listOf(existingImage)
|
||||
every { imageContentStore.getContent(existingImage) } returns inputStream
|
||||
every { imageContentStore.associate(image, "existing-content-id") } just Runs
|
||||
every { fileStorageService.fileExists("existing-content-id") } returns true
|
||||
|
||||
imageService.downloadIfNew(image)
|
||||
|
||||
assertEquals("existing-content-id", image.contentId)
|
||||
assertEquals(1024L, image.contentLength)
|
||||
assertEquals("image/jpeg", image.mimeType)
|
||||
verify(exactly = 1) { imageContentStore.associate(image, "existing-content-id") }
|
||||
verify(exactly = 0) { imageContentStore.setContent(any(), any<InputStream>()) }
|
||||
verify(exactly = 0) { fileStorageService.saveFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -216,14 +211,13 @@ class ImageServiceTest {
|
||||
}
|
||||
|
||||
every { imageRepository.findAllByOriginalUrl(url) } returns listOf(existingImage)
|
||||
every { imageContentStore.getContent(existingImage) } returns null
|
||||
every { imageContentStore.setContent(any<Image>(), any<InputStream>()) } returnsArgument 0
|
||||
every { fileStorageService.fileExists(null) } returns false
|
||||
every { fileStorageService.saveFile(any()) } returns "new-content-id"
|
||||
every { imageRepository.save(image) } returns image
|
||||
|
||||
imageService.downloadIfNew(image)
|
||||
|
||||
verify(exactly = 0) { imageContentStore.associate(any(), any()) }
|
||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
||||
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||
verify(exactly = 1) { imageRepository.save(image) }
|
||||
unmockkStatic(TikaInputStream::class)
|
||||
}
|
||||
@ -240,7 +234,6 @@ class ImageServiceTest {
|
||||
contentLength = 0L,
|
||||
mimeType = "image/jpeg"
|
||||
)
|
||||
val inputStream = ByteArrayInputStream("image data".toByteArray())
|
||||
|
||||
mockkStatic(TikaInputStream::class)
|
||||
val testData = "test image data".toByteArray()
|
||||
@ -249,14 +242,13 @@ class ImageServiceTest {
|
||||
}
|
||||
|
||||
every { imageRepository.findAllByOriginalUrl(url) } returns listOf(existingImage)
|
||||
every { imageContentStore.getContent(existingImage) } returns inputStream
|
||||
every { imageContentStore.setContent(any<Image>(), any<InputStream>()) } returnsArgument 0
|
||||
every { fileStorageService.fileExists("existing-content-id") } returns true
|
||||
every { fileStorageService.saveFile(any()) } returns "new-content-id"
|
||||
every { imageRepository.save(image) } returns image
|
||||
|
||||
imageService.downloadIfNew(image)
|
||||
|
||||
verify(exactly = 0) { imageContentStore.associate(any(), any()) }
|
||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
||||
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||
verify(exactly = 1) { imageRepository.save(image) }
|
||||
unmockkStatic(TikaInputStream::class)
|
||||
}
|
||||
@ -273,12 +265,12 @@ class ImageServiceTest {
|
||||
}
|
||||
|
||||
every { imageRepository.findAllByOriginalUrl(url) } returns emptyList()
|
||||
every { imageContentStore.setContent(any<Image>(), any<InputStream>()) } returnsArgument 0
|
||||
every { fileStorageService.saveFile(any()) } returns "new-content-id"
|
||||
every { imageRepository.save(image) } returns image
|
||||
|
||||
imageService.downloadIfNew(image)
|
||||
|
||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
||||
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||
verify(exactly = 1) { imageRepository.save(image) }
|
||||
unmockkStatic(TikaInputStream::class)
|
||||
}
|
||||
@ -295,12 +287,12 @@ class ImageServiceTest {
|
||||
}
|
||||
|
||||
every { imageRepository.findAllByOriginalUrl(url) } returns emptyList()
|
||||
every { imageContentStore.setContent(any<Image>(), any<InputStream>()) } returnsArgument 0
|
||||
every { fileStorageService.saveFile(any()) } returns "new-content-id"
|
||||
every { imageRepository.save(image) } throws DataIntegrityViolationException("Duplicate")
|
||||
|
||||
imageService.downloadIfNew(image)
|
||||
|
||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
||||
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||
verify(exactly = 1) { imageRepository.save(image) }
|
||||
unmockkStatic(TikaInputStream::class)
|
||||
}
|
||||
@ -311,13 +303,13 @@ class ImageServiceTest {
|
||||
val savedImage = Image(id = 1L, type = ImageType.AVATAR, mimeType = "image/png")
|
||||
|
||||
every { imageRepository.save(any<Image>()) } returns savedImage
|
||||
every { imageContentStore.setContent(any<Image>(), any<InputStream>()) } returns savedImage
|
||||
every { fileStorageService.saveFile(any()) } returns "content-id"
|
||||
|
||||
val result = imageService.createFromInputStream(ImageType.AVATAR, inputStream, "image/png")
|
||||
|
||||
assertNotNull(result)
|
||||
verify(exactly = 1) { imageRepository.save(any<Image>()) }
|
||||
verify(exactly = 1) { imageContentStore.setContent(any<Image>(), any<InputStream>()) }
|
||||
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -347,24 +339,24 @@ class ImageServiceTest {
|
||||
val image = Image(id = 1L, type = ImageType.COVER, contentId = "content-id")
|
||||
val inputStream = ByteArrayInputStream("image data".toByteArray())
|
||||
|
||||
every { imageContentStore.getContent(image) } returns inputStream
|
||||
every { fileStorageService.getFile("content-id") } returns inputStream
|
||||
|
||||
val result = imageService.getFileContent(image)
|
||||
|
||||
assertEquals(inputStream, result)
|
||||
verify(exactly = 1) { imageContentStore.getContent(image) }
|
||||
verify(exactly = 1) { fileStorageService.getFile("content-id") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFileContent should return null when content store returns null`() {
|
||||
val image = Image(id = 1L, type = ImageType.COVER, contentId = "content-id")
|
||||
|
||||
every { imageContentStore.getContent(image) } returns null
|
||||
every { fileStorageService.getFile("content-id") } returns null
|
||||
|
||||
val result = imageService.getFileContent(image)
|
||||
|
||||
assertNull(result)
|
||||
verify(exactly = 1) { imageContentStore.getContent(image) }
|
||||
verify(exactly = 1) { fileStorageService.getFile("content-id") }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -374,14 +366,14 @@ class ImageServiceTest {
|
||||
every { gameRepository.existsByImage(1L) } returns false
|
||||
every { userRepository.existsByAvatar(1L) } returns false
|
||||
every { imageRepository.delete(image) } just Runs
|
||||
every { imageContentStore.unsetContent(image) } returnsArgument 0
|
||||
every { fileStorageService.deleteFile(any()) } just Runs
|
||||
|
||||
imageService.deleteImageIfUnused(image)
|
||||
|
||||
verify(exactly = 1) { gameRepository.existsByImage(1L) }
|
||||
verify(exactly = 1) { userRepository.existsByAvatar(1L) }
|
||||
verify(exactly = 1) { imageRepository.delete(image) }
|
||||
verify(exactly = 1) { imageContentStore.unsetContent(image) }
|
||||
verify(exactly = 1) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -395,7 +387,7 @@ class ImageServiceTest {
|
||||
verify(exactly = 1) { gameRepository.existsByImage(1L) }
|
||||
verify(exactly = 0) { userRepository.existsByAvatar(any()) }
|
||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
||||
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -410,7 +402,7 @@ class ImageServiceTest {
|
||||
verify(exactly = 1) { gameRepository.existsByImage(1L) }
|
||||
verify(exactly = 1) { userRepository.existsByAvatar(1L) }
|
||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
||||
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -422,7 +414,7 @@ class ImageServiceTest {
|
||||
verify(exactly = 0) { gameRepository.existsByImage(any()) }
|
||||
verify(exactly = 0) { userRepository.existsByAvatar(any()) }
|
||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
||||
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -431,13 +423,14 @@ class ImageServiceTest {
|
||||
val inputStream = ByteArrayInputStream("new image data".toByteArray())
|
||||
|
||||
every { imageRepository.save(image) } returns image
|
||||
every { imageContentStore.setContent(any<Image>(), any<InputStream>()) } returns image
|
||||
every { fileStorageService.deleteFile(any()) } just Runs
|
||||
every { fileStorageService.saveFile(any()) } returns "new-content-id"
|
||||
|
||||
imageService.updateFileContent(image, inputStream, "image/jpeg")
|
||||
|
||||
assertEquals("image/jpeg", image.mimeType)
|
||||
verify(exactly = 1) { imageRepository.save(image) }
|
||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
||||
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -446,13 +439,14 @@ class ImageServiceTest {
|
||||
val inputStream = ByteArrayInputStream("new image data".toByteArray())
|
||||
|
||||
every { imageRepository.save(image) } returns image
|
||||
every { imageContentStore.setContent(any<Image>(), any<InputStream>()) } returns image
|
||||
every { fileStorageService.deleteFile(any()) } just Runs
|
||||
every { fileStorageService.saveFile(any()) } returns "new-content-id"
|
||||
|
||||
imageService.updateFileContent(image, inputStream)
|
||||
|
||||
assertEquals("image/png", image.mimeType)
|
||||
verify(exactly = 1) { imageRepository.save(image) }
|
||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
||||
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -474,7 +468,7 @@ class ImageServiceTest {
|
||||
every { gameRepository.existsByImage(3L) } returns false
|
||||
every { userRepository.existsByAvatar(3L) } returns false
|
||||
every { imageRepository.delete(any()) } just Runs
|
||||
every { imageContentStore.unsetContent(any()) } returnsArgument 0
|
||||
every { fileStorageService.deleteFile(any()) } just Runs
|
||||
|
||||
imageService.onGameDeleted(event)
|
||||
|
||||
@ -496,12 +490,12 @@ class ImageServiceTest {
|
||||
every { gameRepository.existsByImage(1L) } returns false
|
||||
every { userRepository.existsByAvatar(1L) } returns false
|
||||
every { imageRepository.delete(screenshot) } just Runs
|
||||
every { imageContentStore.unsetContent(screenshot) } returnsArgument 0
|
||||
every { fileStorageService.deleteFile(any()) } just Runs
|
||||
|
||||
imageService.onGameDeleted(event)
|
||||
|
||||
verify(exactly = 1) { imageRepository.delete(screenshot) }
|
||||
verify(exactly = 1) { imageContentStore.unsetContent(screenshot) }
|
||||
verify(exactly = 1) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -528,7 +522,7 @@ class ImageServiceTest {
|
||||
every { gameRepository.existsByImage(3L) } returns false
|
||||
every { userRepository.existsByAvatar(3L) } returns false
|
||||
every { imageRepository.delete(any()) } just Runs
|
||||
every { imageContentStore.unsetContent(any()) } returnsArgument 0
|
||||
every { fileStorageService.deleteFile(any()) } just Runs
|
||||
|
||||
imageService.onGameUpdated(event)
|
||||
|
||||
@ -558,7 +552,7 @@ class ImageServiceTest {
|
||||
imageService.onGameUpdated(event)
|
||||
|
||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
||||
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -572,12 +566,12 @@ class ImageServiceTest {
|
||||
every { gameRepository.existsByImage(1L) } returns false
|
||||
every { userRepository.existsByAvatar(1L) } returns false
|
||||
every { imageRepository.delete(avatar) } just Runs
|
||||
every { imageContentStore.unsetContent(avatar) } returnsArgument 0
|
||||
every { fileStorageService.deleteFile(any()) } just Runs
|
||||
|
||||
imageService.onAccountDeleted(event)
|
||||
|
||||
verify(exactly = 1) { imageRepository.delete(avatar) }
|
||||
verify(exactly = 1) { imageContentStore.unsetContent(avatar) }
|
||||
verify(exactly = 1) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -590,7 +584,7 @@ class ImageServiceTest {
|
||||
imageService.onAccountDeleted(event)
|
||||
|
||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
||||
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -608,12 +602,12 @@ class ImageServiceTest {
|
||||
every { gameRepository.existsByImage(1L) } returns false
|
||||
every { userRepository.existsByAvatar(1L) } returns false
|
||||
every { imageRepository.delete(oldAvatar) } just Runs
|
||||
every { imageContentStore.unsetContent(oldAvatar) } returnsArgument 0
|
||||
every { fileStorageService.deleteFile(any()) } just Runs
|
||||
|
||||
imageService.onUserUpdated(event)
|
||||
|
||||
verify(exactly = 1) { imageRepository.delete(oldAvatar) }
|
||||
verify(exactly = 1) { imageContentStore.unsetContent(oldAvatar) }
|
||||
verify(exactly = 1) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -630,7 +624,7 @@ class ImageServiceTest {
|
||||
imageService.onUserUpdated(event)
|
||||
|
||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
||||
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -647,7 +641,7 @@ class ImageServiceTest {
|
||||
imageService.onUserUpdated(event)
|
||||
|
||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
||||
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -664,11 +658,11 @@ class ImageServiceTest {
|
||||
every { gameRepository.existsByImage(1L) } returns false
|
||||
every { userRepository.existsByAvatar(1L) } returns false
|
||||
every { imageRepository.delete(oldAvatar) } just Runs
|
||||
every { imageContentStore.unsetContent(oldAvatar) } returnsArgument 0
|
||||
every { fileStorageService.deleteFile(any()) } just Runs
|
||||
|
||||
imageService.onUserUpdated(event)
|
||||
|
||||
verify(exactly = 1) { imageRepository.delete(oldAvatar) }
|
||||
verify(exactly = 1) { imageContentStore.unsetContent(oldAvatar) }
|
||||
verify(exactly = 1) { fileStorageService.deleteFile(any()) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ 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.beans.factory.getBeansOfType
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.context.SecurityContext
|
||||
@ -55,7 +56,7 @@ class MessageServiceTest {
|
||||
fun `enabled should return true when at least one provider is enabled`() {
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { mockProvider2.enabled } returns false
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1,
|
||||
"provider2" to mockProvider2
|
||||
)
|
||||
@ -69,7 +70,7 @@ class MessageServiceTest {
|
||||
fun `enabled should return false when no providers are enabled`() {
|
||||
every { mockProvider1.enabled } returns false
|
||||
every { mockProvider2.enabled } returns false
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1,
|
||||
"provider2" to mockProvider2
|
||||
)
|
||||
@ -81,7 +82,7 @@ class MessageServiceTest {
|
||||
|
||||
@Test
|
||||
fun `enabled should return false when no providers exist`() {
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns emptyMap()
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns emptyMap()
|
||||
|
||||
val result = messageService.enabled
|
||||
|
||||
@ -95,7 +96,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.providerKey } returns providerKey
|
||||
every { mockProvider1.testCredentials(any()) } returns true
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
|
||||
@ -112,7 +113,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.providerKey } returns providerKey
|
||||
every { mockProvider1.testCredentials(any()) } returns false
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
|
||||
@ -127,7 +128,7 @@ class MessageServiceTest {
|
||||
val credentials = mapOf("host" to "smtp.example.com")
|
||||
|
||||
every { mockProvider1.providerKey } returns "email"
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
|
||||
@ -145,7 +146,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.providerKey } returns providerKey
|
||||
every { mockProvider1.testCredentials(any()) } returns true
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
|
||||
@ -167,7 +168,7 @@ class MessageServiceTest {
|
||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||
every { mockProvider2.enabled } returns true
|
||||
every { mockProvider2.supportedTemplateType } returns TemplateType.TEXT
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1,
|
||||
"provider2" to mockProvider2
|
||||
)
|
||||
@ -197,7 +198,7 @@ class MessageServiceTest {
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||
every { mockProvider2.enabled } returns false
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1,
|
||||
"provider2" to mockProvider2
|
||||
)
|
||||
@ -218,7 +219,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.enabled } returns false
|
||||
every { mockProvider2.enabled } returns false
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1,
|
||||
"provider2" to mockProvider2
|
||||
)
|
||||
@ -231,7 +232,7 @@ class MessageServiceTest {
|
||||
|
||||
@Test
|
||||
fun `sendTestNotification should return false when messaging is disabled`() {
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns emptyMap()
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns emptyMap()
|
||||
|
||||
val result = messageService.sendTestNotification("password-reset-request", emptyMap())
|
||||
|
||||
@ -249,7 +250,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
every { userService.getByUsername("testuser") } returns user
|
||||
@ -274,7 +275,7 @@ class MessageServiceTest {
|
||||
val placeholders = mapOf("username" to "testuser", "resetLink" to "http://example.com/reset")
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
|
||||
@ -291,7 +292,7 @@ class MessageServiceTest {
|
||||
setupSecurityContext("testuser")
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
every { userService.getByUsername("testuser") } returns null
|
||||
@ -310,7 +311,7 @@ class MessageServiceTest {
|
||||
setupSecurityContext("testuser")
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
every { userService.getByUsername("testuser") } returns user
|
||||
@ -330,7 +331,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
every {
|
||||
@ -358,7 +359,7 @@ class MessageServiceTest {
|
||||
val token = Token(creator = user, secret = "secret123", type = TokenType.PasswordReset)
|
||||
val event = PasswordResetRequestEvent(this, token, "http://example.com")
|
||||
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns emptyMap()
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns emptyMap()
|
||||
|
||||
messageService.onPasswordResetRequest(event)
|
||||
|
||||
@ -373,7 +374,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
every {
|
||||
@ -394,7 +395,7 @@ class MessageServiceTest {
|
||||
val user = User(username = "newuser", email = "new@example.com", password = "hash")
|
||||
val event = UserRegistrationWaitingForApprovalEvent(this, user)
|
||||
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns emptyMap()
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns emptyMap()
|
||||
|
||||
messageService.onUserRegistrationWaitingForApproval(event)
|
||||
|
||||
@ -409,7 +410,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
every {
|
||||
@ -439,7 +440,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
every {
|
||||
@ -466,7 +467,7 @@ class MessageServiceTest {
|
||||
val user = User(username = "testuser", email = "user@example.com", password = "hash")
|
||||
val event = AccountStatusChangedEvent(this, user, "http://example.com")
|
||||
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns emptyMap()
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns emptyMap()
|
||||
|
||||
messageService.onAccountStatusChanged(event)
|
||||
|
||||
@ -481,7 +482,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
every {
|
||||
@ -502,7 +503,7 @@ class MessageServiceTest {
|
||||
val user = User(username = "existinguser", email = "existing@example.com", password = "hash")
|
||||
val event = RegistrationAttemptWithExistingEmailEvent(this, user, "http://example.com")
|
||||
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns emptyMap()
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns emptyMap()
|
||||
|
||||
messageService.onRegistrationAttemptWithExistingEmail(event)
|
||||
|
||||
@ -518,7 +519,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
every {
|
||||
@ -543,7 +544,7 @@ class MessageServiceTest {
|
||||
val token = Token(creator = user, secret = "confirm123", type = TokenType.EmailConfirmation)
|
||||
val event = EmailNeedsConfirmationEvent(this, token, "http://example.com")
|
||||
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns emptyMap()
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns emptyMap()
|
||||
|
||||
messageService.onEmailNeedsConfirmation(event)
|
||||
|
||||
@ -559,7 +560,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
every {
|
||||
@ -587,7 +588,7 @@ class MessageServiceTest {
|
||||
val token = Token(creator = user, secret = "invite123", type = TokenType.Invitation)
|
||||
val event = UserInvitationEvent(this, token, "http://example.com", "invited@example.com")
|
||||
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns emptyMap()
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns emptyMap()
|
||||
|
||||
messageService.onUserInvitation(event)
|
||||
|
||||
@ -602,7 +603,7 @@ class MessageServiceTest {
|
||||
|
||||
every { mockProvider1.enabled } returns true
|
||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||
"provider1" to mockProvider1
|
||||
)
|
||||
every {
|
||||
@ -629,7 +630,7 @@ class MessageServiceTest {
|
||||
val user = User(username = "deleteduser", email = "deleted@example.com", password = "hash")
|
||||
val event = UserDeletedEvent(this, user, "http://example.com")
|
||||
|
||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns emptyMap()
|
||||
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns emptyMap()
|
||||
|
||||
messageService.onAccountDeletion(event)
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
package org.gameyfin.app.platforms.serialization
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.unmockkAll
|
||||
@ -10,6 +8,8 @@ import org.gameyfin.pluginapi.gamemetadata.Platform
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import tools.jackson.core.JsonParser
|
||||
import tools.jackson.databind.DeserializationContext
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
@ -33,7 +33,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should return correct platform for valid displayName`() {
|
||||
every { jsonParser.text } returns "PC (Microsoft Windows)"
|
||||
every { jsonParser.string } returns "PC (Microsoft Windows)"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -42,7 +42,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for unknown displayName`() {
|
||||
every { jsonParser.text } returns "Unknown Platform"
|
||||
every { jsonParser.string } returns "Unknown Platform"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -51,7 +51,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for empty string`() {
|
||||
every { jsonParser.text } returns ""
|
||||
every { jsonParser.string } returns ""
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -60,7 +60,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should return correct platform for PlayStation 5`() {
|
||||
every { jsonParser.text } returns "PlayStation 5"
|
||||
every { jsonParser.string } returns "PlayStation 5"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -69,7 +69,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should return correct platform for Xbox Series X S`() {
|
||||
every { jsonParser.text } returns "Xbox Series X|S"
|
||||
every { jsonParser.string } returns "Xbox Series X|S"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -78,7 +78,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should return correct platform for Nintendo Switch`() {
|
||||
every { jsonParser.text } returns "Nintendo Switch"
|
||||
every { jsonParser.string } returns "Nintendo Switch"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -87,7 +87,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should be case-sensitive`() {
|
||||
every { jsonParser.text } returns "playstation 5"
|
||||
every { jsonParser.string } returns "playstation 5"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -96,7 +96,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle platforms with special characters`() {
|
||||
every { jsonParser.text } returns "Odyssey 2 / Videopac G7000"
|
||||
every { jsonParser.string } returns "Odyssey 2 / Videopac G7000"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -105,7 +105,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle platforms with numbers at start`() {
|
||||
every { jsonParser.text } returns "3DO Interactive Multiplayer"
|
||||
every { jsonParser.string } returns "3DO Interactive Multiplayer"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -114,7 +114,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle platforms with hyphens`() {
|
||||
every { jsonParser.text } returns "Atari 8-bit"
|
||||
every { jsonParser.string } returns "Atari 8-bit"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -123,7 +123,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle platforms with apostrophes`() {
|
||||
every { jsonParser.text } returns "Super A'Can"
|
||||
every { jsonParser.string } returns "Super A'Can"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -132,7 +132,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for whitespace-only string`() {
|
||||
every { jsonParser.text } returns " "
|
||||
every { jsonParser.string } returns " "
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -141,7 +141,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should not trim whitespace from displayName`() {
|
||||
every { jsonParser.text } returns " PlayStation 5 "
|
||||
every { jsonParser.string } returns " PlayStation 5 "
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -150,7 +150,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Arcade platform`() {
|
||||
every { jsonParser.text } returns "Arcade"
|
||||
every { jsonParser.string } returns "Arcade"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -159,7 +159,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Web browser platform`() {
|
||||
every { jsonParser.text } returns "Web browser"
|
||||
every { jsonParser.string } returns "Web browser"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -168,7 +168,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Android platform`() {
|
||||
every { jsonParser.text } returns "Android"
|
||||
every { jsonParser.string } returns "Android"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -177,7 +177,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle iOS platform`() {
|
||||
every { jsonParser.text } returns "iOS"
|
||||
every { jsonParser.string } returns "iOS"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -186,7 +186,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Linux platform`() {
|
||||
every { jsonParser.text } returns "Linux"
|
||||
every { jsonParser.string } returns "Linux"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -195,7 +195,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Mac platform`() {
|
||||
every { jsonParser.text } returns "Mac"
|
||||
every { jsonParser.string } returns "Mac"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -204,7 +204,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle DOS platform`() {
|
||||
every { jsonParser.text } returns "DOS"
|
||||
every { jsonParser.string } returns "DOS"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -213,7 +213,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Dreamcast platform`() {
|
||||
every { jsonParser.text } returns "Dreamcast"
|
||||
every { jsonParser.string } returns "Dreamcast"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -222,7 +222,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Virtual Boy platform`() {
|
||||
every { jsonParser.text } returns "Virtual Boy"
|
||||
every { jsonParser.string } returns "Virtual Boy"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -231,7 +231,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle ZX Spectrum platform`() {
|
||||
every { jsonParser.text } returns "ZX Spectrum"
|
||||
every { jsonParser.string } returns "ZX Spectrum"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -240,7 +240,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Game Boy platform`() {
|
||||
every { jsonParser.text } returns "Game Boy"
|
||||
every { jsonParser.string } returns "Game Boy"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -249,7 +249,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle PlayStation VR2 platform`() {
|
||||
every { jsonParser.text } returns "PlayStation VR2"
|
||||
every { jsonParser.string } returns "PlayStation VR2"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -258,7 +258,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Nintendo Entertainment System platform`() {
|
||||
every { jsonParser.text } returns "Nintendo Entertainment System"
|
||||
every { jsonParser.string } returns "Nintendo Entertainment System"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -267,7 +267,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Super Nintendo Entertainment System platform`() {
|
||||
every { jsonParser.text } returns "Super Nintendo Entertainment System"
|
||||
every { jsonParser.string } returns "Super Nintendo Entertainment System"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -276,7 +276,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle Sega Mega Drive Genesis platform`() {
|
||||
every { jsonParser.text } returns "Sega Mega Drive/Genesis"
|
||||
every { jsonParser.string } returns "Sega Mega Drive/Genesis"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -285,7 +285,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should handle platforms with long names`() {
|
||||
every { jsonParser.text } returns "Call-A-Computer time-shared mainframe computer system"
|
||||
every { jsonParser.string } returns "Call-A-Computer time-shared mainframe computer system"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -294,7 +294,7 @@ class PlatformDeserializerTest {
|
||||
|
||||
@Test
|
||||
fun `deserialize should return null for partial match`() {
|
||||
every { jsonParser.text } returns "PlayStation"
|
||||
every { jsonParser.string } returns "PlayStation"
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
@ -304,7 +304,7 @@ class PlatformDeserializerTest {
|
||||
@Test
|
||||
fun `deserialize should handle all valid platform displayNames correctly`() {
|
||||
Platform.entries.forEach { platform ->
|
||||
every { jsonParser.text } returns platform.displayName
|
||||
every { jsonParser.string } returns platform.displayName
|
||||
|
||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
package org.gameyfin.app.platforms.serialization
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import io.mockk.mockk
|
||||
import io.mockk.unmockkAll
|
||||
import io.mockk.verify
|
||||
@ -10,18 +8,20 @@ import org.gameyfin.pluginapi.gamemetadata.Platform
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import tools.jackson.core.JsonGenerator
|
||||
import tools.jackson.databind.SerializationContext
|
||||
|
||||
class PlatformSerializerTest {
|
||||
|
||||
private lateinit var serializer: DisplayableSerializer
|
||||
private lateinit var jsonGenerator: JsonGenerator
|
||||
private lateinit var serializerProvider: SerializerProvider
|
||||
private lateinit var serializationContext: SerializationContext
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
serializer = DisplayableSerializer()
|
||||
jsonGenerator = mockk(relaxed = true)
|
||||
serializerProvider = mockk()
|
||||
serializationContext = mockk()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@ -33,14 +33,14 @@ class PlatformSerializerTest {
|
||||
fun `serialize should write displayName for valid platform`() {
|
||||
val platform = Platform.PC_MICROSOFT_WINDOWS
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("PC (Microsoft Windows)") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize should handle null platform value`() {
|
||||
serializer.serialize(null, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(null, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 0) { jsonGenerator.writeString(any<String>()) }
|
||||
}
|
||||
@ -49,7 +49,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should write correct displayName for PlayStation 5`() {
|
||||
val platform = Platform.PLAYSTATION_5
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("PlayStation 5") }
|
||||
}
|
||||
@ -58,7 +58,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should write correct displayName for Xbox Series X S`() {
|
||||
val platform = Platform.XBOX_SERIES_X_S
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Xbox Series X|S") }
|
||||
}
|
||||
@ -67,7 +67,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should write correct displayName for Nintendo Switch`() {
|
||||
val platform = Platform.NINTENDO_SWITCH
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Nintendo Switch") }
|
||||
}
|
||||
@ -76,7 +76,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle platforms with special characters in name`() {
|
||||
val platform = Platform.ODYSSEY_2_VIDEOPAC_G7000
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Odyssey 2 / Videopac G7000") }
|
||||
}
|
||||
@ -85,7 +85,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle platforms with numbers in name`() {
|
||||
val platform = Platform._3DO_INTERACTIVE_MULTIPLAYER
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("3DO Interactive Multiplayer") }
|
||||
}
|
||||
@ -94,7 +94,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle platforms with hyphens in name`() {
|
||||
val platform = Platform.ATARI_8_BIT
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Atari 8-bit") }
|
||||
}
|
||||
@ -103,7 +103,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle platforms with apostrophes in name`() {
|
||||
val platform = Platform.SUPER_ACAN
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Super A'Can") }
|
||||
}
|
||||
@ -112,7 +112,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle arcade platform`() {
|
||||
val platform = Platform.ARCADE
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Arcade") }
|
||||
}
|
||||
@ -121,7 +121,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle web browser platform`() {
|
||||
val platform = Platform.WEB_BROWSER
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Web browser") }
|
||||
}
|
||||
@ -130,7 +130,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle Android platform`() {
|
||||
val platform = Platform.ANDROID
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Android") }
|
||||
}
|
||||
@ -139,7 +139,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle iOS platform`() {
|
||||
val platform = Platform.IOS
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("iOS") }
|
||||
}
|
||||
@ -148,7 +148,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle Linux platform`() {
|
||||
val platform = Platform.LINUX
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Linux") }
|
||||
}
|
||||
@ -157,7 +157,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle Mac platform`() {
|
||||
val platform = Platform.MAC
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Mac") }
|
||||
}
|
||||
@ -166,7 +166,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle DOS platform`() {
|
||||
val platform = Platform.DOS
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("DOS") }
|
||||
}
|
||||
@ -175,7 +175,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle Dreamcast platform`() {
|
||||
val platform = Platform.DREAMCAST
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Dreamcast") }
|
||||
}
|
||||
@ -184,7 +184,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle Virtual Boy platform`() {
|
||||
val platform = Platform.VIRTUAL_BOY
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Virtual Boy") }
|
||||
}
|
||||
@ -193,7 +193,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle ZX Spectrum platform`() {
|
||||
val platform = Platform.ZX_SPECTRUM
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("ZX Spectrum") }
|
||||
}
|
||||
@ -202,7 +202,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle Game Boy platform`() {
|
||||
val platform = Platform.GAME_BOY
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("Game Boy") }
|
||||
}
|
||||
@ -211,7 +211,7 @@ class PlatformSerializerTest {
|
||||
fun `serialize should handle PlayStation VR2 platform`() {
|
||||
val platform = Platform.PLAYSTATION_VR2
|
||||
|
||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
||||
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||
|
||||
verify(exactly = 1) { jsonGenerator.writeString("PlayStation VR2") }
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ class PasswordResetEndpointTest {
|
||||
fun setup() {
|
||||
passwordResetService = mockk()
|
||||
userService = mockk()
|
||||
passwordResetEndpoint = PasswordResetEndpoint(passwordResetService, userService)
|
||||
passwordResetEndpoint = PasswordResetEndpoint(passwordResetService)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
val jacksonVersion = "2.19.1"
|
||||
val jacksonVersion = "3.0.4"
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
@ -16,8 +16,8 @@ dependencies {
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
|
||||
|
||||
// JSON serialization
|
||||
compileOnly("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
|
||||
compileOnly("tools.jackson.core:jackson-databind:$jacksonVersion")
|
||||
implementation("tools.jackson.module:jackson-module-kotlin:$jacksonVersion")
|
||||
}
|
||||
|
||||
mavenPublishing {
|
||||
|
||||
@ -132,11 +132,11 @@ abstract class ConfigurableGameyfinPlugin(wrapper: PluginWrapper) : GameyfinPlug
|
||||
// Try to convert common types
|
||||
try {
|
||||
return when (expectedType) {
|
||||
Int::class.java, Integer::class.java -> value.toString().toInt()
|
||||
Float::class.java, java.lang.Float::class.java -> value.toString().toFloat()
|
||||
Double::class.java, java.lang.Double::class.java -> value.toString().toDouble()
|
||||
Long::class.java, java.lang.Long::class.java -> value.toString().toLong()
|
||||
Boolean::class.java, java.lang.Boolean::class.java -> value.toString().toBooleanStrict()
|
||||
Int::class.java -> value.toString().toInt()
|
||||
Float::class.java -> value.toString().toFloat()
|
||||
Double::class.java -> value.toString().toDouble()
|
||||
Long::class.java -> value.toString().toLong()
|
||||
Boolean::class.java -> value.toString().toBooleanStrict()
|
||||
String::class.java -> value.toString()
|
||||
else -> {
|
||||
// Try valueOf(String) or parse(String) via reflection
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
package org.gameyfin.pluginapi.core.wrapper
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import org.pf4j.Plugin
|
||||
import org.pf4j.PluginWrapper
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import tools.jackson.module.kotlin.jsonMapper
|
||||
import tools.jackson.module.kotlin.kotlinModule
|
||||
import java.io.IOException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
@ -48,7 +49,9 @@ abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) {
|
||||
/**
|
||||
* JSON serializer for serializing and deserializing plugin state.
|
||||
*/
|
||||
val objectMapper: ObjectMapper = ObjectMapper().registerModule(KotlinModule.Builder().build())
|
||||
val objectMapper: ObjectMapper = jsonMapper {
|
||||
addModule(kotlinModule())
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the plugin contains a logo file in any supported format.
|
||||
|
||||
@ -13,7 +13,7 @@ val keystorePasswordProperty = "gameyfin.keystorePassword"
|
||||
|
||||
val keystorePath: String = rootProject.file("certs/gameyfin.jks").absolutePath
|
||||
val keystoreAlias = "gameyfin-plugins"
|
||||
val keystorePasswordProvider = provider {
|
||||
val keystorePasswordProvider: Provider<String> = provider {
|
||||
(findProperty(keystorePasswordProperty) as String?)
|
||||
?: System.getenv(keystorePasswordEnvironmentVariable)
|
||||
?: ""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user