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 |
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,32 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**General info (please complete the following information):**
|
||||
- Browser (if bug is in the Web UI) [e.g. chrome, safari]
|
||||
- Gameyfin version [e.g. v2.0.0.beta3]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
Screenshots can also be attached here.
|
||||
128
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
128
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@ -0,0 +1,128 @@
|
||||
name: Bug report
|
||||
description: Create a report to help us improve
|
||||
type: bug
|
||||
title: '[Bug] '
|
||||
labels:
|
||||
- Bug
|
||||
assignees:
|
||||
- grimsi
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
> [!IMPORTANT]
|
||||
> **Before submitting your bug report**
|
||||
>
|
||||
> To help us resolve your issue efficiently, please ensure you have reviewed our [FAQs](https://gameyfin.org/faq/) and [Getting started guide](https://gameyfin.org/installation/getting-started/).
|
||||
>
|
||||
> Issues that could have been resolved by following these resources may be closed to allow us to focus on genuine bugs.
|
||||
|
||||
- type: checkboxes
|
||||
id: prerequisites
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
description: Please confirm you have read and understood the following resources
|
||||
options:
|
||||
- label: I have read and understood the [FAQs](https://gameyfin.org/faq/)
|
||||
required: true
|
||||
- label: I have read and understood the [Getting started guide](https://gameyfin.org/installation/getting-started/)
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: A clear and concise description of what the bug is
|
||||
placeholder: Describe the bug...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Gameyfin Version
|
||||
description: What version of Gameyfin are you running?
|
||||
placeholder: e.g. v2.0.0.beta3
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: installation-type
|
||||
attributes:
|
||||
label: Installation Type
|
||||
description: How did you install Gameyfin?
|
||||
options:
|
||||
- Docker
|
||||
- Unraid
|
||||
- Other (please specify in Additional Context)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser with Version
|
||||
description: Which browser are you using?
|
||||
placeholder: e.g. Chrome 120.0.6099.129, Firefox 121.0, Safari 17.2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: How to Reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen
|
||||
placeholder: What should have happened?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: A clear and concise description of what actually happened
|
||||
placeholder: What actually happened?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Application Logs
|
||||
description: Please provide relevant logs from the application. You can usually find these in the logs directory or container logs
|
||||
placeholder: Paste your logs here
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem
|
||||
placeholder: Drag and drop images here or paste them
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here
|
||||
placeholder: Any additional information that might be helpful
|
||||
validations:
|
||||
required: false
|
||||
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,10 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Request a feature
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Please use the [discussions](https://github.com/gameyfin/gameyfin/discussions/categories/feature-requests) for feature requests!
|
||||
24
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: Feature request
|
||||
description: Request a feature
|
||||
title: 'Feature request'
|
||||
labels: []
|
||||
assignees: []
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
> [!CAUTION]
|
||||
> **Feature requests should not be submitted as issues!**
|
||||
>
|
||||
> Please use the [discussions](https://github.com/gameyfin/gameyfin/discussions/categories/feature-requests) for feature requests instead.
|
||||
|
||||
- type: input
|
||||
id: acknowledgment
|
||||
attributes:
|
||||
label: I understand
|
||||
description: Type "I will use discussions" to acknowledge
|
||||
placeholder: I will use discussions
|
||||
validations:
|
||||
required: true
|
||||
|
||||
4
.github/workflows/docker-fix.yml
vendored
4
.github/workflows/docker-fix.yml
vendored
@ -16,11 +16,11 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK 21
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
|
||||
- name: Run production build
|
||||
env:
|
||||
|
||||
4
.github/workflows/docker-preview.yml
vendored
4
.github/workflows/docker-preview.yml
vendored
@ -20,11 +20,11 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 21
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@ -75,11 +75,11 @@ jobs:
|
||||
with:
|
||||
name: modified-files
|
||||
|
||||
- name: Set up JDK 21
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
@ -162,11 +162,11 @@ jobs:
|
||||
with:
|
||||
name: modified-files
|
||||
|
||||
- name: Set up JDK 21
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
44
.github/workflows/sonar.yml
vendored
Normal file
44
.github/workflows/sonar.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: Sonar Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened ]
|
||||
|
||||
jobs:
|
||||
sonar:
|
||||
name: Sonar Analysis
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.sonar/cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
- name: Run tests and generate JaCoCo report
|
||||
run: ./gradlew :app:test :app:jacocoTestReport
|
||||
|
||||
- name: SonarCloud Scan
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: ./gradlew :app:sonar
|
||||
@ -12,6 +12,8 @@ plugins {
|
||||
kotlin("plugin.jpa")
|
||||
id("com.google.devtools.ksp")
|
||||
application
|
||||
jacoco
|
||||
id("org.sonarqube")
|
||||
}
|
||||
|
||||
application {
|
||||
@ -33,15 +35,19 @@ dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-aop")
|
||||
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:3.1.0")
|
||||
implementation("jakarta.validation:jakarta.validation-api:${rootProject.extra["jakartaValidationVersion"]}")
|
||||
|
||||
// Kotlin extensions
|
||||
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")
|
||||
|
||||
@ -49,17 +55,19 @@ 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
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:${rootProject.extra["kotlinLoggingVersion"]}")
|
||||
|
||||
// Persistence & I/O
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.17")
|
||||
implementation("org.flywaydb:flyway-core")
|
||||
implementation("commons-io:commons-io:2.18.0")
|
||||
implementation("com.google.guava:guava:33.5.0-jre")
|
||||
implementation("org.springframework.boot:spring-boot-starter-flyway")
|
||||
implementation("commons-io:commons-io:${rootProject.extra["commonsIoVersion"]}")
|
||||
implementation("com.google.guava:guava:${rootProject.extra["guavaVersion"]}")
|
||||
|
||||
// SSO
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
|
||||
@ -68,18 +76,19 @@ dependencies {
|
||||
|
||||
// Notifications
|
||||
implementation("org.springframework.boot:spring-boot-starter-mail")
|
||||
implementation("ch.digitalfondue.mjml4j:mjml4j:1.1.4")
|
||||
implementation("ch.digitalfondue.mjml4j:mjml4j:${rootProject.extra["mjml4jVersion"]}")
|
||||
|
||||
// Plugins
|
||||
implementation(project(":plugin-api"))
|
||||
|
||||
// Utils
|
||||
implementation("org.apache.tika:tika-core:3.2.3")
|
||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||
implementation("com.vanniktech:blurhash:0.3.0")
|
||||
implementation("org.apache.tika:tika-core:${rootProject.extra["tikaVersion"]}")
|
||||
implementation("me.xdrop:fuzzywuzzy:${rootProject.extra["fuzzywuzzyVersion"]}")
|
||||
implementation("com.vanniktech:blurhash:${rootProject.extra["blurhashVersion"]}")
|
||||
|
||||
// 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")
|
||||
@ -103,6 +112,30 @@ dependencyManagement {
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
finalizedBy(tasks.jacocoTestReport)
|
||||
}
|
||||
|
||||
tasks.jacocoTestReport {
|
||||
dependsOn(tasks.test)
|
||||
reports {
|
||||
xml.required = true
|
||||
xml.outputLocation = layout.buildDirectory.file("reports/jacoco/test/jacocoTestReport.xml")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("sonar") {
|
||||
dependsOn(tasks.jacocoTestReport)
|
||||
}
|
||||
|
||||
sonar {
|
||||
properties {
|
||||
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/**")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<ProcessResources>("processResources") {
|
||||
|
||||
3459
app/package-lock.json
generated
3459
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
304
app/package.json
304
app/package.json
@ -5,51 +5,47 @@
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.7",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@react-stately/data": "^3.12.2",
|
||||
"@react-types/shared": "^3.28.0",
|
||||
"@tailwindcss/vite": "4.1.13",
|
||||
"@vaadin/bundles": "24.9.4",
|
||||
"@vaadin/aura": "25.0.3",
|
||||
"@vaadin/common-frontend": "0.0.19",
|
||||
"@vaadin/hilla-file-router": "24.9.4",
|
||||
"@vaadin/hilla-frontend": "24.9.4",
|
||||
"@vaadin/hilla-lit-form": "24.9.4",
|
||||
"@vaadin/hilla-react-auth": "24.9.4",
|
||||
"@vaadin/hilla-react-crud": "24.9.4",
|
||||
"@vaadin/hilla-react-form": "24.9.4",
|
||||
"@vaadin/hilla-react-i18n": "24.9.4",
|
||||
"@vaadin/hilla-react-signals": "24.9.4",
|
||||
"@vaadin/polymer-legacy-adapter": "24.9.4",
|
||||
"@vaadin/react-components": "24.9.4",
|
||||
"@vaadin/hilla-file-router": "25.0.4",
|
||||
"@vaadin/hilla-frontend": "25.0.4",
|
||||
"@vaadin/hilla-lit-form": "25.0.4",
|
||||
"@vaadin/hilla-react-auth": "25.0.4",
|
||||
"@vaadin/hilla-react-crud": "25.0.4",
|
||||
"@vaadin/hilla-react-form": "25.0.4",
|
||||
"@vaadin/hilla-react-i18n": "25.0.4",
|
||||
"@vaadin/hilla-react-signals": "25.0.4",
|
||||
"@vaadin/react-components": "25.0.3",
|
||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||
"@vaadin/vaadin-lumo-styles": "24.9.4",
|
||||
"@vaadin/vaadin-material-styles": "24.9.4",
|
||||
"@vaadin/vaadin-themable-mixin": "24.9.4",
|
||||
"@vaadin/vaadin-lumo-styles": "25.0.3",
|
||||
"@vaadin/vaadin-themable-mixin": "25.0.3",
|
||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||
"blurhash": "^2.0.5",
|
||||
"classnames": "^2.5.1",
|
||||
"construct-style-sheets-polyfill": "3.1.0",
|
||||
"date-fns": "2.29.3",
|
||||
"date-fns": "4.1.0",
|
||||
"formik": "^2.4.6",
|
||||
"framer-motion": "^12.23.22",
|
||||
"fzf": "^0.5.2",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"lit": "3.3.1",
|
||||
"lit": "3.3.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"next-themes": "^0.4.6",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-import": "^16.1.1",
|
||||
"rand-seed": "^2.1.7",
|
||||
"react": "19.1.1",
|
||||
"react": "19.2.3",
|
||||
"react-accessible-treeview": "^2.11.1",
|
||||
"react-aria-components": "^1.7.1",
|
||||
"react-confetti-boom": "^1.0.0",
|
||||
"react-dom": "19.1.1",
|
||||
"react-dom": "19.2.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-player": "^2.16.0",
|
||||
"react-realtime-chart": "^0.8.1",
|
||||
"react-router": "7.6.3",
|
||||
"react-router": "7.12.0",
|
||||
"react-window": "^2.2.3",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"swiper": "^11.2.6",
|
||||
@ -57,59 +53,53 @@
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@babel/preset-react": "7.28.5",
|
||||
"@lit-labs/react": "^2.1.3",
|
||||
"@preact/signals-react-transform": "0.6.0",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/plugin-replace": "6.0.3",
|
||||
"@rollup/pluginutils": "5.3.0",
|
||||
"@types/node": "^22.4.0",
|
||||
"@types/react": "19.1.17",
|
||||
"@types/react-dom": "19.1.11",
|
||||
"@types/node": "25.0.3",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@vaadin/hilla-generator-cli": "24.9.4",
|
||||
"@vaadin/hilla-generator-core": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-backbone": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-barrel": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-client": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-model": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-push": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-signals": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-subtypes": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.9.4",
|
||||
"@vaadin/hilla-generator-utils": "24.9.4",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"@vaadin/hilla-generator-cli": "25.0.4",
|
||||
"@vaadin/hilla-generator-core": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-backbone": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-barrel": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-client": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-model": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-push": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-signals": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-subtypes": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-transfertypes": "25.0.4",
|
||||
"@vaadin/hilla-generator-utils": "25.0.4",
|
||||
"@vitejs/plugin-react": "5.1.2",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"glob": "11.0.3",
|
||||
"magic-string": "0.30.19",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"magic-string": "0.30.21",
|
||||
"rollup-plugin-brotli": "3.1.0",
|
||||
"rollup-plugin-visualizer": "5.14.0",
|
||||
"rollup-plugin-visualizer": "6.0.5",
|
||||
"strip-css-comments": "5.0.0",
|
||||
"tailwindcss": "4.1.13",
|
||||
"transform-ast": "2.4.4",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "6.4.1",
|
||||
"vite-plugin-checker": "0.10.3",
|
||||
"workbox-build": "7.3.0",
|
||||
"workbox-core": "7.3.0",
|
||||
"workbox-precaching": "7.3.0"
|
||||
"typescript": "5.9.3",
|
||||
"vite": "7.3.1",
|
||||
"vite-plugin-checker": "0.12.0",
|
||||
"workbox-build": "7.4.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@react-aria/utils": "^3.28.1",
|
||||
"classnames": "$classnames",
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom",
|
||||
"@vaadin/bundles": "$@vaadin/bundles",
|
||||
"@vaadin/common-frontend": "$@vaadin/common-frontend",
|
||||
"construct-style-sheets-polyfill": "$construct-style-sheets-polyfill",
|
||||
"lit": "$lit",
|
||||
"@polymer/polymer": "$@polymer/polymer",
|
||||
"@phosphor-icons/react": "$@phosphor-icons/react",
|
||||
"formik": "$formik",
|
||||
"yup": "$yup",
|
||||
"@heroui/react": "$@heroui/react",
|
||||
"framer-motion": "$framer-motion",
|
||||
"http-status-codes": "$http-status-codes",
|
||||
"@vaadin/polymer-legacy-adapter": "$@vaadin/polymer-legacy-adapter",
|
||||
"@vaadin/vaadin-development-mode-detector": "$@vaadin/vaadin-development-mode-detector",
|
||||
"@vaadin/vaadin-usage-statistics": "$@vaadin/vaadin-usage-statistics",
|
||||
"@vaadin/react-components": "$@vaadin/react-components",
|
||||
@ -127,7 +117,6 @@
|
||||
"date-fns": "$date-fns",
|
||||
"@vaadin/vaadin-themable-mixin": "$@vaadin/vaadin-themable-mixin",
|
||||
"@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles",
|
||||
"@vaadin/vaadin-material-styles": "$@vaadin/vaadin-material-styles",
|
||||
"@react-types/shared": "$@react-types/shared",
|
||||
"@react-stately/data": "$@react-stately/data",
|
||||
"react-aria-components": "$react-aria-components",
|
||||
@ -140,133 +129,128 @@
|
||||
"remark-breaks": "$remark-breaks",
|
||||
"valtio": "$valtio",
|
||||
"fzf": "$fzf",
|
||||
"@vaadin/router": "2.0.0",
|
||||
"@tailwindcss/vite": "$@tailwindcss/vite",
|
||||
"postcss": "$postcss",
|
||||
"postcss-import": "$postcss-import",
|
||||
"next-themes": "$next-themes",
|
||||
"@vaadin/a11y-base": "24.9.4",
|
||||
"@vaadin/accordion": "24.9.4",
|
||||
"@vaadin/app-layout": "24.9.4",
|
||||
"@vaadin/avatar": "24.9.4",
|
||||
"@vaadin/avatar-group": "24.9.4",
|
||||
"@vaadin/button": "24.9.4",
|
||||
"@vaadin/card": "24.9.4",
|
||||
"@vaadin/checkbox": "24.9.4",
|
||||
"@vaadin/checkbox-group": "24.9.4",
|
||||
"@vaadin/combo-box": "24.9.4",
|
||||
"@vaadin/component-base": "24.9.4",
|
||||
"@vaadin/confirm-dialog": "24.9.4",
|
||||
"@vaadin/context-menu": "24.9.4",
|
||||
"@vaadin/custom-field": "24.9.4",
|
||||
"@vaadin/date-picker": "24.9.4",
|
||||
"@vaadin/date-time-picker": "24.9.4",
|
||||
"@vaadin/details": "24.9.4",
|
||||
"@vaadin/dialog": "24.9.4",
|
||||
"@vaadin/email-field": "24.9.4",
|
||||
"@vaadin/field-base": "24.9.4",
|
||||
"@vaadin/field-highlighter": "24.9.4",
|
||||
"@vaadin/form-layout": "24.9.4",
|
||||
"@vaadin/grid": "24.9.4",
|
||||
"@vaadin/horizontal-layout": "24.9.4",
|
||||
"@vaadin/icon": "24.9.4",
|
||||
"@vaadin/icons": "24.9.4",
|
||||
"@vaadin/input-container": "24.9.4",
|
||||
"@vaadin/integer-field": "24.9.4",
|
||||
"@vaadin/item": "24.9.4",
|
||||
"@vaadin/list-box": "24.9.4",
|
||||
"@vaadin/lit-renderer": "24.9.4",
|
||||
"@vaadin/login": "24.9.4",
|
||||
"@vaadin/markdown": "24.9.4",
|
||||
"@vaadin/master-detail-layout": "24.9.4",
|
||||
"@vaadin/menu-bar": "24.9.4",
|
||||
"@vaadin/message-input": "24.9.4",
|
||||
"@vaadin/message-list": "24.9.4",
|
||||
"@vaadin/multi-select-combo-box": "24.9.4",
|
||||
"@vaadin/notification": "24.9.4",
|
||||
"@vaadin/number-field": "24.9.4",
|
||||
"@vaadin/overlay": "24.9.4",
|
||||
"@vaadin/password-field": "24.9.4",
|
||||
"@vaadin/popover": "24.9.4",
|
||||
"@vaadin/progress-bar": "24.9.4",
|
||||
"@vaadin/radio-group": "24.9.4",
|
||||
"@vaadin/scroller": "24.9.4",
|
||||
"@vaadin/select": "24.9.4",
|
||||
"@vaadin/side-nav": "24.9.4",
|
||||
"@vaadin/split-layout": "24.9.4",
|
||||
"@vaadin/tabs": "24.9.4",
|
||||
"@vaadin/tabsheet": "24.9.4",
|
||||
"@vaadin/text-area": "24.9.4",
|
||||
"@vaadin/text-field": "24.9.4",
|
||||
"@vaadin/time-picker": "24.9.4",
|
||||
"@vaadin/tooltip": "24.9.4",
|
||||
"@vaadin/upload": "24.9.4",
|
||||
"@vaadin/vertical-layout": "24.9.4",
|
||||
"@vaadin/virtual-list": "24.9.4",
|
||||
"react-realtime-chart": "$react-realtime-chart",
|
||||
"react-window": "$react-window",
|
||||
"blurhash": "$blurhash"
|
||||
"blurhash": "$blurhash",
|
||||
"@vaadin/aura": "$@vaadin/aura",
|
||||
"@vaadin/a11y-base": "25.0.3",
|
||||
"@vaadin/accordion": "25.0.3",
|
||||
"@vaadin/app-layout": "25.0.3",
|
||||
"@vaadin/avatar": "25.0.3",
|
||||
"@vaadin/avatar-group": "25.0.3",
|
||||
"@vaadin/button": "25.0.3",
|
||||
"@vaadin/card": "25.0.3",
|
||||
"@vaadin/checkbox": "25.0.3",
|
||||
"@vaadin/checkbox-group": "25.0.3",
|
||||
"@vaadin/combo-box": "25.0.3",
|
||||
"@vaadin/component-base": "25.0.3",
|
||||
"@vaadin/confirm-dialog": "25.0.3",
|
||||
"@vaadin/context-menu": "25.0.3",
|
||||
"@vaadin/custom-field": "25.0.3",
|
||||
"@vaadin/date-picker": "25.0.3",
|
||||
"@vaadin/date-time-picker": "25.0.3",
|
||||
"@vaadin/details": "25.0.3",
|
||||
"@vaadin/dialog": "25.0.3",
|
||||
"@vaadin/email-field": "25.0.3",
|
||||
"@vaadin/field-base": "25.0.3",
|
||||
"@vaadin/field-highlighter": "25.0.3",
|
||||
"@vaadin/form-layout": "25.0.3",
|
||||
"@vaadin/grid": "25.0.3",
|
||||
"@vaadin/horizontal-layout": "25.0.3",
|
||||
"@vaadin/icon": "25.0.3",
|
||||
"@vaadin/icons": "25.0.3",
|
||||
"@vaadin/input-container": "25.0.3",
|
||||
"@vaadin/integer-field": "25.0.3",
|
||||
"@vaadin/item": "25.0.3",
|
||||
"@vaadin/list-box": "25.0.3",
|
||||
"@vaadin/lit-renderer": "25.0.3",
|
||||
"@vaadin/login": "25.0.3",
|
||||
"@vaadin/markdown": "25.0.3",
|
||||
"@vaadin/master-detail-layout": "25.0.3",
|
||||
"@vaadin/menu-bar": "25.0.3",
|
||||
"@vaadin/message-input": "25.0.3",
|
||||
"@vaadin/message-list": "25.0.3",
|
||||
"@vaadin/multi-select-combo-box": "25.0.3",
|
||||
"@vaadin/notification": "25.0.3",
|
||||
"@vaadin/number-field": "25.0.3",
|
||||
"@vaadin/overlay": "25.0.3",
|
||||
"@vaadin/password-field": "25.0.3",
|
||||
"@vaadin/popover": "25.0.3",
|
||||
"@vaadin/progress-bar": "25.0.3",
|
||||
"@vaadin/radio-group": "25.0.3",
|
||||
"@vaadin/scroller": "25.0.3",
|
||||
"@vaadin/select": "25.0.3",
|
||||
"@vaadin/side-nav": "25.0.3",
|
||||
"@vaadin/split-layout": "25.0.3",
|
||||
"@vaadin/tabs": "25.0.3",
|
||||
"@vaadin/tabsheet": "25.0.3",
|
||||
"@vaadin/text-area": "25.0.3",
|
||||
"@vaadin/text-field": "25.0.3",
|
||||
"@vaadin/time-picker": "25.0.3",
|
||||
"@vaadin/tooltip": "25.0.3",
|
||||
"@vaadin/upload": "25.0.3",
|
||||
"@vaadin/router": "2.0.1",
|
||||
"@vaadin/vertical-layout": "25.0.3",
|
||||
"@vaadin/virtual-list": "25.0.3"
|
||||
},
|
||||
"vaadin": {
|
||||
"dependencies": {
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@vaadin/bundles": "24.9.4",
|
||||
"@vaadin/aura": "25.0.3",
|
||||
"@vaadin/common-frontend": "0.0.19",
|
||||
"@vaadin/hilla-file-router": "24.9.4",
|
||||
"@vaadin/hilla-frontend": "24.9.4",
|
||||
"@vaadin/hilla-lit-form": "24.9.4",
|
||||
"@vaadin/hilla-react-auth": "24.9.4",
|
||||
"@vaadin/hilla-react-crud": "24.9.4",
|
||||
"@vaadin/hilla-react-form": "24.9.4",
|
||||
"@vaadin/hilla-react-i18n": "24.9.4",
|
||||
"@vaadin/hilla-react-signals": "24.9.4",
|
||||
"@vaadin/polymer-legacy-adapter": "24.9.4",
|
||||
"@vaadin/react-components": "24.9.4",
|
||||
"@vaadin/hilla-file-router": "25.0.4",
|
||||
"@vaadin/hilla-frontend": "25.0.4",
|
||||
"@vaadin/hilla-lit-form": "25.0.4",
|
||||
"@vaadin/hilla-react-auth": "25.0.4",
|
||||
"@vaadin/hilla-react-crud": "25.0.4",
|
||||
"@vaadin/hilla-react-form": "25.0.4",
|
||||
"@vaadin/hilla-react-i18n": "25.0.4",
|
||||
"@vaadin/hilla-react-signals": "25.0.4",
|
||||
"@vaadin/react-components": "25.0.3",
|
||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||
"@vaadin/vaadin-lumo-styles": "24.9.4",
|
||||
"@vaadin/vaadin-material-styles": "24.9.4",
|
||||
"@vaadin/vaadin-themable-mixin": "24.9.4",
|
||||
"@vaadin/vaadin-lumo-styles": "25.0.3",
|
||||
"@vaadin/vaadin-themable-mixin": "25.0.3",
|
||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||
"construct-style-sheets-polyfill": "3.1.0",
|
||||
"date-fns": "2.29.3",
|
||||
"lit": "3.3.1",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-router": "7.6.3"
|
||||
"date-fns": "4.1.0",
|
||||
"lit": "3.3.2",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-router": "7.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@babel/preset-react": "7.28.5",
|
||||
"@preact/signals-react-transform": "0.6.0",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/plugin-replace": "6.0.3",
|
||||
"@rollup/pluginutils": "5.3.0",
|
||||
"@types/react": "19.1.17",
|
||||
"@types/react-dom": "19.1.11",
|
||||
"@vaadin/hilla-generator-cli": "24.9.4",
|
||||
"@vaadin/hilla-generator-core": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-backbone": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-barrel": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-client": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-model": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-push": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-signals": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-subtypes": "24.9.4",
|
||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.9.4",
|
||||
"@vaadin/hilla-generator-utils": "24.9.4",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"glob": "11.0.3",
|
||||
"magic-string": "0.30.19",
|
||||
"@types/node": "25.0.3",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vaadin/hilla-generator-cli": "25.0.4",
|
||||
"@vaadin/hilla-generator-core": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-backbone": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-barrel": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-client": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-model": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-push": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-signals": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-subtypes": "25.0.4",
|
||||
"@vaadin/hilla-generator-plugin-transfertypes": "25.0.4",
|
||||
"@vaadin/hilla-generator-utils": "25.0.4",
|
||||
"@vitejs/plugin-react": "5.1.2",
|
||||
"magic-string": "0.30.21",
|
||||
"rollup-plugin-brotli": "3.1.0",
|
||||
"rollup-plugin-visualizer": "5.14.0",
|
||||
"rollup-plugin-visualizer": "6.0.5",
|
||||
"strip-css-comments": "5.0.0",
|
||||
"transform-ast": "2.4.4",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "6.4.1",
|
||||
"vite-plugin-checker": "0.10.3",
|
||||
"workbox-build": "7.3.0",
|
||||
"workbox-core": "7.3.0",
|
||||
"workbox-precaching": "7.3.0"
|
||||
"typescript": "5.9.3",
|
||||
"vite": "7.3.1",
|
||||
"vite-plugin-checker": "0.12.0",
|
||||
"workbox-build": "7.4.0"
|
||||
},
|
||||
"disableUsageStatistics": true,
|
||||
"hash": "760523c518e07bbe0567ae5d1b281ccf90326b285b5feb3c0f269c52ec774f88"
|
||||
"hash": "d2c583f908a126db3f53ccbc87688b5089107afb58a87159631dc257a3a279ae"
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
||||
@ -28,10 +24,11 @@ enum class Role(val roleName: String, val powerLevel: Int) {
|
||||
return entries.find { it.roleName == enumString }
|
||||
}
|
||||
|
||||
fun safeValueOf(type: String): Role? {
|
||||
fun safeValueOf(type: String?): Role? {
|
||||
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")
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ package org.gameyfin.app.core.config
|
||||
|
||||
import org.gameyfin.app.core.interceptors.EntityUpdateInterceptor
|
||||
import org.hibernate.cfg.AvailableSettings
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer
|
||||
import org.springframework.boot.hibernate.autoconfigure.HibernatePropertiesCustomizer
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
|
||||
@ -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.web.embedded.tomcat.TomcatProtocolHandlerCustomizer
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
/**
|
||||
* Tomcat configuration to optimize for concurrent connections
|
||||
* and prevent download operations from blocking the server.
|
||||
*/
|
||||
@Configuration
|
||||
class TomcatConfig {
|
||||
|
||||
companion object {
|
||||
private val log = KotlinLogging.logger { }
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun protocolHandlerCustomizer(): TomcatProtocolHandlerCustomizer<*> {
|
||||
return TomcatProtocolHandlerCustomizer { protocolHandler: ProtocolHandler ->
|
||||
if (protocolHandler is AbstractHttp11Protocol<*>) {
|
||||
// Increase max connections to handle more concurrent users
|
||||
protocolHandler.maxConnections = 10000
|
||||
|
||||
// Increase max threads to handle more concurrent requests
|
||||
protocolHandler.maxThreads = 200
|
||||
|
||||
// Set minimum spare threads
|
||||
protocolHandler.minSpareThreads = 10
|
||||
|
||||
// Set connection timeout (20 seconds)
|
||||
protocolHandler.connectionTimeout = 20000
|
||||
|
||||
// Keep alive settings to reuse connections
|
||||
protocolHandler.keepAliveTimeout = 60000
|
||||
protocolHandler.maxKeepAliveRequests = 100
|
||||
|
||||
log.debug {
|
||||
"Configured Tomcat connector: maxConnections=${protocolHandler.maxConnections}, " +
|
||||
"maxThreads=${protocolHandler.maxThreads}, " +
|
||||
"minSpareThreads=${protocolHandler.minSpareThreads}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -12,7 +12,7 @@ class AppKeyValidator : CommandLineRunner {
|
||||
val log = KotlinLogging.logger {}
|
||||
}
|
||||
|
||||
override fun run(vararg args: String?) {
|
||||
override fun run(vararg args: String) {
|
||||
val base64Key = System.getenv("APP_KEY")
|
||||
if (!hasValidAppKey(base64Key)) exitProcess(1)
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ class CustomAuthenticationEntryPoint : AuthenticationEntryPoint {
|
||||
override fun commence(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
authException: AuthenticationException?
|
||||
authException: AuthenticationException
|
||||
) {
|
||||
if (request.getParameter("direct") == "1") {
|
||||
response.sendRedirect("/login")
|
||||
|
||||
@ -12,23 +12,13 @@ import java.util.function.Supplier
|
||||
class DynamicPublicAccessAuthorizationManager(
|
||||
private val config: ConfigService
|
||||
) : AuthorizationManager<RequestAuthorizationContext> {
|
||||
|
||||
@Deprecated("Deprecated in superclass")
|
||||
override fun check(
|
||||
authentication: Supplier<Authentication?>?,
|
||||
`object`: RequestAuthorizationContext?
|
||||
): AuthorizationDecision {
|
||||
val auth = authentication?.get()
|
||||
val allow = (auth?.isAuthenticated == true && auth.principal != "anonymousUser") ||
|
||||
override fun authorize(
|
||||
authentication: Supplier<out Authentication>,
|
||||
`object`: RequestAuthorizationContext
|
||||
): AuthorizationResult {
|
||||
val auth = authentication.get()
|
||||
val allow = (auth.isAuthenticated && auth.principal != "anonymousUser") ||
|
||||
config.get(ConfigProperties.Security.AllowPublicAccess) == true
|
||||
return AuthorizationDecision(allow)
|
||||
}
|
||||
|
||||
override fun authorize(
|
||||
authentication: Supplier<Authentication?>?,
|
||||
`object`: RequestAuthorizationContext?
|
||||
): AuthorizationResult {
|
||||
@Suppress("DEPRECATION")
|
||||
return check(authentication, `object`)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
@ -77,9 +77,13 @@ class SsoAuthenticationSuccessHandler(
|
||||
// Update SecurityContext with expanded authorities through RoleHierarchy
|
||||
val mappedAuthorities = authoritiesMapper.mapAuthorities(grantedAuthorities)
|
||||
|
||||
val newAuth =
|
||||
UsernamePasswordAuthenticationToken(authentication.principal, authentication.credentials, mappedAuthorities)
|
||||
SecurityContextHolder.getContext().authentication = newAuth
|
||||
val authPrincipal = authentication.principal
|
||||
val authCredentials = authentication.credentials
|
||||
|
||||
if (authPrincipal != null && authCredentials != null) {
|
||||
val newAuth = UsernamePasswordAuthenticationToken(authPrincipal, authCredentials, mappedAuthorities)
|
||||
SecurityContextHolder.getContext().authentication = newAuth
|
||||
}
|
||||
|
||||
// Get the continue parameter from the request to redirect back to the original page
|
||||
val continueUrl = request.getParameter("continue")
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -47,12 +47,12 @@ class RoleService(
|
||||
}
|
||||
|
||||
fun getRolesBelowAuth(auth: Authentication): List<Role> {
|
||||
val highestUserRole = getHighestRole(auth.authorities.mapNotNull { Role.Companion.safeValueOf(it.authority) })
|
||||
val highestUserRole = getHighestRole(auth.authorities.mapNotNull { Role.safeValueOf(it.authority) })
|
||||
return Role.entries.filter { it.powerLevel < highestUserRole.powerLevel }
|
||||
}
|
||||
|
||||
fun authoritiesToRoles(authorities: Collection<GrantedAuthority>): List<Role> {
|
||||
return authorities.mapNotNull { Role.Companion.safeValueOf(it.authority) }
|
||||
return authorities.mapNotNull { Role.safeValueOf(it.authority) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -11,8 +11,8 @@ import org.springframework.stereotype.Service
|
||||
class SessionService(private val sessionRegistry: SessionRegistry) {
|
||||
|
||||
fun logoutAllSessions() {
|
||||
val auth = getCurrentAuth()
|
||||
val sessions: List<SessionInformation> = sessionRegistry.getAllSessions(auth?.principal, false)
|
||||
val authPrincipal = getCurrentAuth()?.principal ?: return
|
||||
val sessions: List<SessionInformation> = sessionRegistry.getAllSessions(authPrincipal, false)
|
||||
for (sessionInfo in sessions) {
|
||||
sessionInfo.expireNow()
|
||||
}
|
||||
|
||||
@ -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 +0,0 @@
|
||||
com.vaadin.experimental.react19=true
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package org.gameyfin.app.core.download.bandwidth
|
||||
|
||||
import com.helger.commons.mock.CommonsAssert.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
@ -104,7 +103,7 @@ class SessionBandwidthManagerTest {
|
||||
|
||||
val stat1 = stats["session-1"]
|
||||
assertNotNull(stat1)
|
||||
assertEquals("session-1", stat1!!.sessionId)
|
||||
assertEquals("session-1", stat1.sessionId)
|
||||
assertEquals(1, stat1.activeDownloads)
|
||||
assertEquals("user1", stat1.username)
|
||||
assertEquals("192.168.1.1", stat1.remoteIp)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@ class DownloadEndpointTest {
|
||||
* Helper method to wait for DeferredResult to complete and get the result.
|
||||
* Handles async processing with timeout.
|
||||
*/
|
||||
private fun <T> awaitDeferredResult(deferredResult: DeferredResult<T>, timeoutSeconds: Long = 5): T {
|
||||
private fun <T : Any> awaitDeferredResult(deferredResult: DeferredResult<T>, timeoutSeconds: Long = 5): T {
|
||||
val latch = CountDownLatch(1)
|
||||
var result: T? = null
|
||||
var error: Throwable? = null
|
||||
@ -108,7 +108,7 @@ class DownloadEndpointTest {
|
||||
|
||||
assertEquals(HttpStatus.OK, response.statusCode)
|
||||
assertNotNull(response.body)
|
||||
assertTrue(response.headers.containsKey("Content-Disposition"))
|
||||
assertTrue(response.headers.containsHeader("Content-Disposition"))
|
||||
assertTrue(response.headers["Content-Disposition"]!![0].contains("Test Game.zip"))
|
||||
|
||||
verify(exactly = 1) { gameService.getById(gameId) }
|
||||
@ -142,9 +142,9 @@ class DownloadEndpointTest {
|
||||
val response = awaitDeferredResult(deferredResult)
|
||||
|
||||
assertEquals(HttpStatus.OK, response.statusCode)
|
||||
assertTrue(response.headers.containsKey("Content-Disposition"))
|
||||
assertTrue(response.headers.containsHeader("Content-Disposition"))
|
||||
// Content-Length should not be present for directories
|
||||
assertFalse(response.headers.containsKey("Content-Length"))
|
||||
assertFalse(response.headers.containsHeader("Content-Length"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -171,7 +171,7 @@ class DownloadEndpointTest {
|
||||
val response = awaitDeferredResult(deferredResult)
|
||||
|
||||
assertEquals(HttpStatus.OK, response.statusCode)
|
||||
assertFalse(response.headers.containsKey("Content-Length"))
|
||||
assertFalse(response.headers.containsHeader("Content-Length"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -198,7 +198,7 @@ class DownloadEndpointTest {
|
||||
val response = awaitDeferredResult(deferredResult)
|
||||
|
||||
assertEquals(HttpStatus.OK, response.statusCode)
|
||||
assertFalse(response.headers.containsKey("Content-Length"))
|
||||
assertFalse(response.headers.containsHeader("Content-Length"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -131,7 +131,8 @@ class LogEndpointTest {
|
||||
|
||||
@Test
|
||||
fun `getApplicationLogs should return empty flux when authentication is null`() {
|
||||
val securityContext = SecurityContextImpl(null)
|
||||
val mockAuthentication = mockk<Authentication>(relaxed = true)
|
||||
val securityContext = SecurityContextImpl(mockAuthentication)
|
||||
SecurityContextHolder.setContext(securityContext)
|
||||
|
||||
val result = logEndpoint.getApplicationLogs()
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException
|
||||
import org.springframework.security.authentication.BadCredentialsException
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import kotlin.test.assertEquals
|
||||
@ -96,7 +97,7 @@ class CustomAuthenticationEntryPointTest {
|
||||
every { request.getParameter("direct") } returns "1"
|
||||
every { response.sendRedirect(any()) } just Runs
|
||||
|
||||
entryPoint.commence(request, response, null)
|
||||
entryPoint.commence(request, response, AuthenticationCredentialsNotFoundException("Test"))
|
||||
|
||||
verify(exactly = 1) { response.sendRedirect("/login") }
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
||||
"password",
|
||||
listOf(SimpleGrantedAuthority("ROLE_USER"))
|
||||
)
|
||||
val authSupplier = Supplier<Authentication?> { authentication }
|
||||
val authSupplier = Supplier<Authentication> { authentication }
|
||||
|
||||
val decision = manager.authorize(authSupplier, context)
|
||||
|
||||
@ -63,7 +63,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
||||
"password",
|
||||
listOf(SimpleGrantedAuthority("ROLE_USER"))
|
||||
)
|
||||
val authSupplier = Supplier<Authentication?> { authentication }
|
||||
val authSupplier = Supplier<Authentication> { authentication }
|
||||
|
||||
val decision = manager.authorize(authSupplier, context)
|
||||
|
||||
@ -79,7 +79,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
||||
every { authentication.isAuthenticated } returns false
|
||||
every { authentication.principal } returns "anonymousUser"
|
||||
|
||||
val authSupplier = Supplier<Authentication?> { authentication }
|
||||
val authSupplier = Supplier<Authentication> { authentication }
|
||||
|
||||
val decision = manager.authorize(authSupplier, context)
|
||||
|
||||
@ -95,7 +95,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
||||
every { authentication.isAuthenticated } returns false
|
||||
every { authentication.principal } returns "anonymousUser"
|
||||
|
||||
val authSupplier = Supplier<Authentication?> { authentication }
|
||||
val authSupplier = Supplier<Authentication> { authentication }
|
||||
|
||||
val decision = manager.authorize(authSupplier, context)
|
||||
|
||||
@ -107,7 +107,8 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
||||
fun `check should deny access when authentication is null and public access is disabled`() {
|
||||
every { configService.get(ConfigProperties.Security.AllowPublicAccess) } returns false
|
||||
|
||||
val authSupplier = Supplier<Authentication?> { null }
|
||||
val mockAuthentication = mockk<Authentication>(relaxed = true)
|
||||
val authSupplier = Supplier<Authentication> { mockAuthentication }
|
||||
|
||||
val decision = manager.authorize(authSupplier, context)
|
||||
|
||||
@ -119,7 +120,8 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
||||
fun `check should allow access when authentication is null and public access is enabled`() {
|
||||
every { configService.get(ConfigProperties.Security.AllowPublicAccess) } returns true
|
||||
|
||||
val authSupplier = Supplier<Authentication?> { null }
|
||||
val mockAuthentication = mockk<Authentication>(relaxed = true)
|
||||
val authSupplier = Supplier<Authentication> { mockAuthentication }
|
||||
|
||||
val decision = manager.authorize(authSupplier, context)
|
||||
|
||||
@ -135,7 +137,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
||||
every { authentication.isAuthenticated } returns true
|
||||
every { authentication.principal } returns "anonymousUser"
|
||||
|
||||
val authSupplier = Supplier<Authentication?> { authentication }
|
||||
val authSupplier = Supplier<Authentication> { authentication }
|
||||
|
||||
val decision = manager.authorize(authSupplier, context)
|
||||
|
||||
@ -151,7 +153,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
||||
every { authentication.isAuthenticated } returns true
|
||||
every { authentication.principal } returns "john.doe"
|
||||
|
||||
val authSupplier = Supplier<Authentication?> { authentication }
|
||||
val authSupplier = Supplier<Authentication> { authentication }
|
||||
|
||||
val decision = manager.authorize(authSupplier, context)
|
||||
|
||||
@ -167,38 +169,11 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
||||
every { authentication.isAuthenticated } returns false
|
||||
every { authentication.principal } returns "anonymousUser"
|
||||
|
||||
val authSupplier = Supplier<Authentication?> { authentication }
|
||||
val authSupplier = Supplier<Authentication> { authentication }
|
||||
|
||||
val decision = manager.authorize(authSupplier, context)
|
||||
|
||||
assertNotNull(decision)
|
||||
assertFalse(decision.isGranted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check should work when supplier is null`() {
|
||||
every { configService.get(ConfigProperties.Security.AllowPublicAccess) } returns false
|
||||
|
||||
val decision = manager.authorize(null, context)
|
||||
|
||||
assertNotNull(decision)
|
||||
assertFalse(decision.isGranted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check should work when context is null`() {
|
||||
every { configService.get(ConfigProperties.Security.AllowPublicAccess) } returns false
|
||||
|
||||
val authentication = UsernamePasswordAuthenticationToken(
|
||||
"user",
|
||||
"password",
|
||||
listOf(SimpleGrantedAuthority("ROLE_USER"))
|
||||
)
|
||||
val authSupplier = Supplier<Authentication?> { authentication }
|
||||
|
||||
val decision = manager.authorize(authSupplier, null)
|
||||
|
||||
assertNotNull(decision)
|
||||
assertTrue(decision.isGranted)
|
||||
}
|
||||
}
|
||||
@ -42,17 +42,6 @@ class PasswordEncoderConfigTest {
|
||||
assertTrue(encoder.matches(password, encoded2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passwordEncoder should handle empty password`() {
|
||||
val encoder = config.passwordEncoder()
|
||||
val password = ""
|
||||
|
||||
val encoded = encoder.encode(password)
|
||||
|
||||
assertNotNull(encoded)
|
||||
assertTrue(encoder.matches(password, encoded))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passwordEncoder should handle long password`() {
|
||||
val encoder = config.passwordEncoder()
|
||||
|
||||
@ -116,17 +116,6 @@ class SecurityUtilsTest {
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isCurrentUserAdmin should return false when authorities is null`() {
|
||||
val authentication = mockk<Authentication>()
|
||||
every { authentication.authorities } returns null
|
||||
every { securityContext.authentication } returns authentication
|
||||
|
||||
val result = isCurrentUserAdmin()
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isCurrentUserAdmin should return true when user has both ADMIN and USER roles`() {
|
||||
val authentication = UsernamePasswordAuthenticationToken(
|
||||
@ -188,14 +177,6 @@ class SecurityUtilsTest {
|
||||
assertFalse(authentication.isAdmin())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Authentication isAdmin should return false when authorities is null`() {
|
||||
val authentication = mockk<Authentication>()
|
||||
every { authentication.authorities } returns null
|
||||
|
||||
assertFalse(authentication.isAdmin())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Authentication isAdmin should return true when user has SUPERADMIN among multiple roles`() {
|
||||
val authentication = UsernamePasswordAuthenticationToken(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"jsx": "react-jsx",
|
||||
"inlineSources": true,
|
||||
"module": "esNext",
|
||||
"target": "es2022",
|
||||
"target": "es2023",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
@ -29,16 +29,13 @@ import checker from 'vite-plugin-checker';
|
||||
import postcssLit from './build/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js';
|
||||
import vaadinI18n from './build/plugins/rollup-plugin-vaadin-i18n/rollup-plugin-vaadin-i18n.js';
|
||||
import serviceWorkerPlugin from './build/plugins/vite-plugin-service-worker';
|
||||
|
||||
import { createRequire } from 'module';
|
||||
import vaadinBundlesPlugin from './build/plugins/vite-plugin-vaadin-bundles';
|
||||
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import reactPlugin from '@vitejs/plugin-react';
|
||||
|
||||
import vitePluginFileSystemRouter from '@vaadin/hilla-file-router/vite-plugin.js';
|
||||
|
||||
// Make `require` compatible with ES modules
|
||||
const require = createRequire(import.meta.url);
|
||||
import vitePluginFileSystemRouter from '@vaadin/hilla-file-router/vite-plugin.js';
|
||||
|
||||
const frontendFolder = path.resolve(__dirname, settings.frontendFolder);
|
||||
const themeFolder = path.resolve(frontendFolder, settings.themeFolder);
|
||||
@ -78,14 +75,16 @@ const themeOptions = {
|
||||
projectStaticAssetsOutputFolder: devBundle
|
||||
? path.resolve(devBundleFolder, '../assets')
|
||||
: path.resolve(__dirname, settings.staticOutput),
|
||||
frontendGeneratedFolder: path.resolve(frontendFolder, settings.generatedFolder)
|
||||
frontendGeneratedFolder: path.resolve(frontendFolder, settings.generatedFolder),
|
||||
projectStaticOutput: path.resolve(__dirname, settings.staticOutput),
|
||||
javaResourceFolder: settings.javaResourceFolder ? path.resolve(__dirname, settings.javaResourceFolder) : ''
|
||||
};
|
||||
|
||||
const hasExportedWebComponents = existsSync(path.resolve(frontendFolder, 'web-component.html'));
|
||||
const commercialBannerComponent = path.resolve(frontendFolder, settings.generatedFolder, 'commercial-banner.js');
|
||||
const hasCommercialBanner = existsSync(commercialBannerComponent);
|
||||
|
||||
const target = ['safari15', 'es2022'];
|
||||
const target = ['es2023'];
|
||||
|
||||
// Block debug and trace logs.
|
||||
console.trace = () => {};
|
||||
@ -183,6 +182,10 @@ function statsExtracterPlugin(): PluginOption {
|
||||
path.resolve(themeOptions.frontendGeneratedFolder, 'flow', 'generated-flow-imports.js'),
|
||||
generatedImportsSet
|
||||
);
|
||||
parseImports(
|
||||
path.resolve(themeOptions.frontendGeneratedFolder, 'app-shell-imports.js'),
|
||||
generatedImportsSet
|
||||
);
|
||||
const generatedImports = Array.from(generatedImportsSet).sort();
|
||||
|
||||
const frontendFiles: Record<string, string> = {};
|
||||
@ -303,159 +306,6 @@ function statsExtracterPlugin(): PluginOption {
|
||||
}
|
||||
};
|
||||
}
|
||||
function vaadinBundlesPlugin(): PluginOption {
|
||||
type ExportInfo =
|
||||
| string
|
||||
| {
|
||||
namespace?: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
type ExposeInfo = {
|
||||
exports: ExportInfo[];
|
||||
};
|
||||
|
||||
type PackageInfo = {
|
||||
version: string;
|
||||
exposes: Record<string, ExposeInfo>;
|
||||
};
|
||||
|
||||
type BundleJson = {
|
||||
packages: Record<string, PackageInfo>;
|
||||
};
|
||||
|
||||
const disabledMessage = 'Vaadin component dependency bundles are disabled.';
|
||||
|
||||
const modulesDirectory = nodeModulesFolder.replace(/\\/g, '/');
|
||||
|
||||
let vaadinBundleJson: BundleJson;
|
||||
|
||||
function parseModuleId(id: string): { packageName: string; modulePath: string } {
|
||||
const [scope, scopedPackageName] = id.split('/', 3);
|
||||
const packageName = scope.startsWith('@') ? `${scope}/${scopedPackageName}` : scope;
|
||||
const modulePath = `.${id.substring(packageName.length)}`;
|
||||
return {
|
||||
packageName,
|
||||
modulePath
|
||||
};
|
||||
}
|
||||
|
||||
function getExports(id: string): string[] | undefined {
|
||||
const { packageName, modulePath } = parseModuleId(id);
|
||||
const packageInfo = vaadinBundleJson.packages[packageName];
|
||||
|
||||
if (!packageInfo) return;
|
||||
|
||||
const exposeInfo: ExposeInfo = packageInfo.exposes[modulePath];
|
||||
if (!exposeInfo) return;
|
||||
|
||||
const exportsSet = new Set<string>();
|
||||
for (const e of exposeInfo.exports) {
|
||||
if (typeof e === 'string') {
|
||||
exportsSet.add(e);
|
||||
} else {
|
||||
const { namespace, source } = e;
|
||||
if (namespace) {
|
||||
exportsSet.add(namespace);
|
||||
} else {
|
||||
const sourceExports = getExports(source);
|
||||
if (sourceExports) {
|
||||
sourceExports.forEach((e) => exportsSet.add(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(exportsSet);
|
||||
}
|
||||
|
||||
function getExportBinding(binding: string) {
|
||||
return binding === 'default' ? '_default as default' : binding;
|
||||
}
|
||||
|
||||
function getImportAssigment(binding: string) {
|
||||
return binding === 'default' ? 'default: _default' : binding;
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vaadin:bundles',
|
||||
enforce: 'pre',
|
||||
apply(config, { command }) {
|
||||
if (command !== 'serve') return false;
|
||||
|
||||
try {
|
||||
const vaadinBundleJsonPath = require.resolve('@vaadin/bundles/vaadin-bundle.json');
|
||||
vaadinBundleJson = JSON.parse(readFileSync(vaadinBundleJsonPath, { encoding: 'utf8' }));
|
||||
} catch (e: unknown) {
|
||||
if (typeof e === 'object' && (e as { code: string }).code === 'MODULE_NOT_FOUND') {
|
||||
vaadinBundleJson = { packages: {} };
|
||||
console.info(`@vaadin/bundles npm package is not found, ${disabledMessage}`);
|
||||
return false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const versionMismatches: Array<{ name: string; bundledVersion: string; installedVersion: string }> = [];
|
||||
for (const [name, packageInfo] of Object.entries(vaadinBundleJson.packages)) {
|
||||
let installedVersion: string | undefined = undefined;
|
||||
try {
|
||||
const { version: bundledVersion } = packageInfo;
|
||||
const installedPackageJsonFile = path.resolve(modulesDirectory, name, 'package.json');
|
||||
const packageJson = JSON.parse(readFileSync(installedPackageJsonFile, { encoding: 'utf8' }));
|
||||
installedVersion = packageJson.version;
|
||||
if (installedVersion && installedVersion !== bundledVersion) {
|
||||
versionMismatches.push({
|
||||
name,
|
||||
bundledVersion,
|
||||
installedVersion
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore package not found
|
||||
}
|
||||
}
|
||||
if (versionMismatches.length) {
|
||||
console.info(`@vaadin/bundles has version mismatches with installed packages, ${disabledMessage}`);
|
||||
console.info(`Packages with version mismatches: ${JSON.stringify(versionMismatches, undefined, 2)}`);
|
||||
vaadinBundleJson = { packages: {} };
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async config(config) {
|
||||
return mergeConfig(
|
||||
{
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
// Vaadin bundle
|
||||
'@vaadin/bundles',
|
||||
...Object.keys(vaadinBundleJson.packages),
|
||||
'@vaadin/vaadin-material-styles'
|
||||
]
|
||||
}
|
||||
},
|
||||
config
|
||||
);
|
||||
},
|
||||
load(rawId) {
|
||||
const [path, params] = rawId.split('?');
|
||||
if (!path.startsWith(modulesDirectory)) return;
|
||||
|
||||
const id = path.substring(modulesDirectory.length + 1);
|
||||
const bindings = getExports(id);
|
||||
if (bindings === undefined) return;
|
||||
|
||||
const cacheSuffix = params ? `?${params}` : '';
|
||||
const bundlePath = `@vaadin/bundles/vaadin.js${cacheSuffix}`;
|
||||
|
||||
return `import { init as VaadinBundleInit, get as VaadinBundleGet } from '${bundlePath}';
|
||||
await VaadinBundleInit('default');
|
||||
const { ${bindings.map(getImportAssigment).join(', ')} } = (await VaadinBundleGet('./node_modules/${id}'))();
|
||||
export { ${bindings.map(getExportBinding).join(', ')} };`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function themePlugin(opts: { devMode: boolean }): PluginOption {
|
||||
const fullThemeOptions = { ...themeOptions, devMode: opts.devMode };
|
||||
@ -563,9 +413,12 @@ function preserveUsageStats() {
|
||||
transform(src: string, id: string) {
|
||||
if (id.includes('vaadin-usage-statistics')) {
|
||||
if (src.includes('vaadin-dev-mode:start')) {
|
||||
const newSrc = src.replace(DEV_MODE_START_REGEXP, '/*! vaadin-dev-mode:start');
|
||||
const expectedComment = '/*! vaadin-dev-mode:start';
|
||||
const newSrc = src.replace(DEV_MODE_START_REGEXP, expectedComment);
|
||||
if (newSrc === src) {
|
||||
console.error('Comment replacement failed to change anything');
|
||||
if (!src.includes(expectedComment)) {
|
||||
console.error('vaadin-dev-mode:start tag not found');
|
||||
}
|
||||
} else if (!newSrc.match(DEV_MODE_CODE_REGEXP)) {
|
||||
console.error('New comment fails to match original regexp');
|
||||
} else {
|
||||
@ -612,6 +465,9 @@ export const vaadinConfig: UserConfigFn = (env) => {
|
||||
allow: allowedFrontendFolders
|
||||
}
|
||||
},
|
||||
esbuild: {
|
||||
legalComments: 'inline',
|
||||
},
|
||||
build: {
|
||||
minify: productionMode,
|
||||
outDir: buildOutputFolder,
|
||||
@ -668,7 +524,9 @@ export const vaadinConfig: UserConfigFn = (env) => {
|
||||
},
|
||||
plugins: [
|
||||
productionMode && brotli(),
|
||||
devMode && vaadinBundlesPlugin(),
|
||||
devMode && vaadinBundlesPlugin({
|
||||
nodeModulesFolder
|
||||
}),
|
||||
devMode && showRecompileReason(),
|
||||
settings.offlineEnabled && serviceWorkerPlugin({
|
||||
srcPath: settings.clientServiceWorkerSource,
|
||||
@ -714,6 +572,7 @@ export const vaadinConfig: UserConfigFn = (env) => {
|
||||
].filter(Boolean)
|
||||
}
|
||||
}),
|
||||
|
||||
productionMode && vaadinI18n({
|
||||
cwd: __dirname,
|
||||
meta: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user