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
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '25'
|
||||||
|
|
||||||
- name: Run production build
|
- name: Run production build
|
||||||
env:
|
env:
|
||||||
|
|||||||
4
.github/workflows/docker-preview.yml
vendored
4
.github/workflows/docker-preview.yml
vendored
@ -20,11 +20,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '25'
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
uses: gradle/actions/setup-gradle@v5
|
||||||
|
|||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@ -75,11 +75,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: modified-files
|
name: modified-files
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '25'
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
uses: gradle/actions/setup-gradle@v5
|
||||||
|
|
||||||
@ -162,11 +162,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: modified-files
|
name: modified-files
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '25'
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
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")
|
kotlin("plugin.jpa")
|
||||||
id("com.google.devtools.ksp")
|
id("com.google.devtools.ksp")
|
||||||
application
|
application
|
||||||
|
jacoco
|
||||||
|
id("org.sonarqube")
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@ -33,15 +35,19 @@ dependencies {
|
|||||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
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("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
|
// Kotlin extensions
|
||||||
implementation(kotlin("reflect"))
|
implementation(kotlin("reflect"))
|
||||||
|
|
||||||
// Reactive
|
// 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("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||||
|
|
||||||
@ -49,17 +55,19 @@ dependencies {
|
|||||||
implementation("com.vaadin:vaadin-core") {
|
implementation("com.vaadin:vaadin-core") {
|
||||||
exclude("com.vaadin:flow-react")
|
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
|
// Logging
|
||||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
|
implementation("io.github.oshai:kotlin-logging-jvm:${rootProject.extra["kotlinLoggingVersion"]}")
|
||||||
|
|
||||||
// Persistence & I/O
|
// Persistence & I/O
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.17")
|
implementation("org.springframework.boot:spring-boot-starter-flyway")
|
||||||
implementation("org.flywaydb:flyway-core")
|
implementation("commons-io:commons-io:${rootProject.extra["commonsIoVersion"]}")
|
||||||
implementation("commons-io:commons-io:2.18.0")
|
implementation("com.google.guava:guava:${rootProject.extra["guavaVersion"]}")
|
||||||
implementation("com.google.guava:guava:33.5.0-jre")
|
|
||||||
|
|
||||||
// SSO
|
// SSO
|
||||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
|
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
|
||||||
@ -68,18 +76,19 @@ dependencies {
|
|||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
implementation("org.springframework.boot:spring-boot-starter-mail")
|
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
|
// Plugins
|
||||||
implementation(project(":plugin-api"))
|
implementation(project(":plugin-api"))
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
implementation("org.apache.tika:tika-core:3.2.3")
|
implementation("org.apache.tika:tika-core:${rootProject.extra["tikaVersion"]}")
|
||||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
implementation("me.xdrop:fuzzywuzzy:${rootProject.extra["fuzzywuzzyVersion"]}")
|
||||||
implementation("com.vanniktech:blurhash:0.3.0")
|
implementation("com.vanniktech:blurhash:${rootProject.extra["blurhashVersion"]}")
|
||||||
|
|
||||||
// Development
|
// Development
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||||
|
developmentOnly("com.vaadin:vaadin-dev")
|
||||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||||
runtimeOnly("com.h2database:h2")
|
runtimeOnly("com.h2database:h2")
|
||||||
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
|
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
|
||||||
@ -103,6 +112,30 @@ dependencyManagement {
|
|||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
useJUnitPlatform()
|
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") {
|
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": {
|
"dependencies": {
|
||||||
"@heroui/react": "^2.8.7",
|
"@heroui/react": "^2.8.7",
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@polymer/polymer": "3.5.2",
|
|
||||||
"@react-stately/data": "^3.12.2",
|
"@react-stately/data": "^3.12.2",
|
||||||
"@react-types/shared": "^3.28.0",
|
"@react-types/shared": "^3.28.0",
|
||||||
"@tailwindcss/vite": "4.1.13",
|
"@tailwindcss/vite": "4.1.13",
|
||||||
"@vaadin/bundles": "24.9.4",
|
"@vaadin/aura": "25.0.3",
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
"@vaadin/common-frontend": "0.0.19",
|
||||||
"@vaadin/hilla-file-router": "24.9.4",
|
"@vaadin/hilla-file-router": "25.0.4",
|
||||||
"@vaadin/hilla-frontend": "24.9.4",
|
"@vaadin/hilla-frontend": "25.0.4",
|
||||||
"@vaadin/hilla-lit-form": "24.9.4",
|
"@vaadin/hilla-lit-form": "25.0.4",
|
||||||
"@vaadin/hilla-react-auth": "24.9.4",
|
"@vaadin/hilla-react-auth": "25.0.4",
|
||||||
"@vaadin/hilla-react-crud": "24.9.4",
|
"@vaadin/hilla-react-crud": "25.0.4",
|
||||||
"@vaadin/hilla-react-form": "24.9.4",
|
"@vaadin/hilla-react-form": "25.0.4",
|
||||||
"@vaadin/hilla-react-i18n": "24.9.4",
|
"@vaadin/hilla-react-i18n": "25.0.4",
|
||||||
"@vaadin/hilla-react-signals": "24.9.4",
|
"@vaadin/hilla-react-signals": "25.0.4",
|
||||||
"@vaadin/polymer-legacy-adapter": "24.9.4",
|
"@vaadin/react-components": "25.0.3",
|
||||||
"@vaadin/react-components": "24.9.4",
|
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||||
"@vaadin/vaadin-lumo-styles": "24.9.4",
|
"@vaadin/vaadin-lumo-styles": "25.0.3",
|
||||||
"@vaadin/vaadin-material-styles": "24.9.4",
|
"@vaadin/vaadin-themable-mixin": "25.0.3",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.9.4",
|
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"construct-style-sheets-polyfill": "3.1.0",
|
"date-fns": "4.1.0",
|
||||||
"date-fns": "2.29.3",
|
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"framer-motion": "^12.23.22",
|
"framer-motion": "^12.23.22",
|
||||||
"fzf": "^0.5.2",
|
"fzf": "^0.5.2",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"lit": "3.3.1",
|
"lit": "3.3.2",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.47",
|
"moment-timezone": "^0.5.47",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"postcss-import": "^16.1.1",
|
"postcss-import": "^16.1.1",
|
||||||
"rand-seed": "^2.1.7",
|
"rand-seed": "^2.1.7",
|
||||||
"react": "19.1.1",
|
"react": "19.2.3",
|
||||||
"react-accessible-treeview": "^2.11.1",
|
"react-accessible-treeview": "^2.11.1",
|
||||||
"react-aria-components": "^1.7.1",
|
"react-aria-components": "^1.7.1",
|
||||||
"react-confetti-boom": "^1.0.0",
|
"react-confetti-boom": "^1.0.0",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.2.3",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-player": "^2.16.0",
|
"react-player": "^2.16.0",
|
||||||
"react-realtime-chart": "^0.8.1",
|
"react-realtime-chart": "^0.8.1",
|
||||||
"react-router": "7.6.3",
|
"react-router": "7.12.0",
|
||||||
"react-window": "^2.2.3",
|
"react-window": "^2.2.3",
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"swiper": "^11.2.6",
|
"swiper": "^11.2.6",
|
||||||
@ -57,59 +53,53 @@
|
|||||||
"yup": "^1.6.1"
|
"yup": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-react": "7.27.1",
|
"@babel/preset-react": "7.28.5",
|
||||||
"@lit-labs/react": "^2.1.3",
|
"@lit-labs/react": "^2.1.3",
|
||||||
"@preact/signals-react-transform": "0.6.0",
|
"@preact/signals-react-transform": "0.6.0",
|
||||||
"@rollup/plugin-replace": "6.0.2",
|
"@rollup/plugin-replace": "6.0.3",
|
||||||
"@rollup/pluginutils": "5.3.0",
|
"@rollup/pluginutils": "5.3.0",
|
||||||
"@types/node": "^22.4.0",
|
"@types/node": "25.0.3",
|
||||||
"@types/react": "19.1.17",
|
"@types/react": "19.2.7",
|
||||||
"@types/react-dom": "19.1.11",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@vaadin/hilla-generator-cli": "24.9.4",
|
"@vaadin/hilla-generator-cli": "25.0.4",
|
||||||
"@vaadin/hilla-generator-core": "24.9.4",
|
"@vaadin/hilla-generator-core": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-backbone": "24.9.4",
|
"@vaadin/hilla-generator-plugin-backbone": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-barrel": "24.9.4",
|
"@vaadin/hilla-generator-plugin-barrel": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-client": "24.9.4",
|
"@vaadin/hilla-generator-plugin-client": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-model": "24.9.4",
|
"@vaadin/hilla-generator-plugin-model": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-push": "24.9.4",
|
"@vaadin/hilla-generator-plugin-push": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-signals": "24.9.4",
|
"@vaadin/hilla-generator-plugin-signals": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-subtypes": "24.9.4",
|
"@vaadin/hilla-generator-plugin-subtypes": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.9.4",
|
"@vaadin/hilla-generator-plugin-transfertypes": "25.0.4",
|
||||||
"@vaadin/hilla-generator-utils": "24.9.4",
|
"@vaadin/hilla-generator-utils": "25.0.4",
|
||||||
"@vitejs/plugin-react": "4.7.0",
|
"@vitejs/plugin-react": "5.1.2",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||||
"glob": "11.0.3",
|
"baseline-browser-mapping": "^2.9.19",
|
||||||
"magic-string": "0.30.19",
|
"magic-string": "0.30.21",
|
||||||
"rollup-plugin-brotli": "3.1.0",
|
"rollup-plugin-brotli": "3.1.0",
|
||||||
"rollup-plugin-visualizer": "5.14.0",
|
"rollup-plugin-visualizer": "6.0.5",
|
||||||
"strip-css-comments": "5.0.0",
|
"strip-css-comments": "5.0.0",
|
||||||
"tailwindcss": "4.1.13",
|
"tailwindcss": "4.1.13",
|
||||||
"transform-ast": "2.4.4",
|
"transform-ast": "2.4.4",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "6.4.1",
|
"vite": "7.3.1",
|
||||||
"vite-plugin-checker": "0.10.3",
|
"vite-plugin-checker": "0.12.0",
|
||||||
"workbox-build": "7.3.0",
|
"workbox-build": "7.4.0"
|
||||||
"workbox-core": "7.3.0",
|
|
||||||
"workbox-precaching": "7.3.0"
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@react-aria/utils": "^3.28.1",
|
"@react-aria/utils": "^3.28.1",
|
||||||
"classnames": "$classnames",
|
"classnames": "$classnames",
|
||||||
"react": "$react",
|
"react": "$react",
|
||||||
"react-dom": "$react-dom",
|
"react-dom": "$react-dom",
|
||||||
"@vaadin/bundles": "$@vaadin/bundles",
|
|
||||||
"@vaadin/common-frontend": "$@vaadin/common-frontend",
|
"@vaadin/common-frontend": "$@vaadin/common-frontend",
|
||||||
"construct-style-sheets-polyfill": "$construct-style-sheets-polyfill",
|
|
||||||
"lit": "$lit",
|
"lit": "$lit",
|
||||||
"@polymer/polymer": "$@polymer/polymer",
|
|
||||||
"@phosphor-icons/react": "$@phosphor-icons/react",
|
"@phosphor-icons/react": "$@phosphor-icons/react",
|
||||||
"formik": "$formik",
|
"formik": "$formik",
|
||||||
"yup": "$yup",
|
"yup": "$yup",
|
||||||
"@heroui/react": "$@heroui/react",
|
"@heroui/react": "$@heroui/react",
|
||||||
"framer-motion": "$framer-motion",
|
"framer-motion": "$framer-motion",
|
||||||
"http-status-codes": "$http-status-codes",
|
"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-development-mode-detector": "$@vaadin/vaadin-development-mode-detector",
|
||||||
"@vaadin/vaadin-usage-statistics": "$@vaadin/vaadin-usage-statistics",
|
"@vaadin/vaadin-usage-statistics": "$@vaadin/vaadin-usage-statistics",
|
||||||
"@vaadin/react-components": "$@vaadin/react-components",
|
"@vaadin/react-components": "$@vaadin/react-components",
|
||||||
@ -127,7 +117,6 @@
|
|||||||
"date-fns": "$date-fns",
|
"date-fns": "$date-fns",
|
||||||
"@vaadin/vaadin-themable-mixin": "$@vaadin/vaadin-themable-mixin",
|
"@vaadin/vaadin-themable-mixin": "$@vaadin/vaadin-themable-mixin",
|
||||||
"@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles",
|
"@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles",
|
||||||
"@vaadin/vaadin-material-styles": "$@vaadin/vaadin-material-styles",
|
|
||||||
"@react-types/shared": "$@react-types/shared",
|
"@react-types/shared": "$@react-types/shared",
|
||||||
"@react-stately/data": "$@react-stately/data",
|
"@react-stately/data": "$@react-stately/data",
|
||||||
"react-aria-components": "$react-aria-components",
|
"react-aria-components": "$react-aria-components",
|
||||||
@ -140,133 +129,128 @@
|
|||||||
"remark-breaks": "$remark-breaks",
|
"remark-breaks": "$remark-breaks",
|
||||||
"valtio": "$valtio",
|
"valtio": "$valtio",
|
||||||
"fzf": "$fzf",
|
"fzf": "$fzf",
|
||||||
"@vaadin/router": "2.0.0",
|
|
||||||
"@tailwindcss/vite": "$@tailwindcss/vite",
|
"@tailwindcss/vite": "$@tailwindcss/vite",
|
||||||
"postcss": "$postcss",
|
"postcss": "$postcss",
|
||||||
"postcss-import": "$postcss-import",
|
"postcss-import": "$postcss-import",
|
||||||
"next-themes": "$next-themes",
|
"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-realtime-chart": "$react-realtime-chart",
|
||||||
"react-window": "$react-window",
|
"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": {
|
"vaadin": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@polymer/polymer": "3.5.2",
|
"@vaadin/aura": "25.0.3",
|
||||||
"@vaadin/bundles": "24.9.4",
|
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
"@vaadin/common-frontend": "0.0.19",
|
||||||
"@vaadin/hilla-file-router": "24.9.4",
|
"@vaadin/hilla-file-router": "25.0.4",
|
||||||
"@vaadin/hilla-frontend": "24.9.4",
|
"@vaadin/hilla-frontend": "25.0.4",
|
||||||
"@vaadin/hilla-lit-form": "24.9.4",
|
"@vaadin/hilla-lit-form": "25.0.4",
|
||||||
"@vaadin/hilla-react-auth": "24.9.4",
|
"@vaadin/hilla-react-auth": "25.0.4",
|
||||||
"@vaadin/hilla-react-crud": "24.9.4",
|
"@vaadin/hilla-react-crud": "25.0.4",
|
||||||
"@vaadin/hilla-react-form": "24.9.4",
|
"@vaadin/hilla-react-form": "25.0.4",
|
||||||
"@vaadin/hilla-react-i18n": "24.9.4",
|
"@vaadin/hilla-react-i18n": "25.0.4",
|
||||||
"@vaadin/hilla-react-signals": "24.9.4",
|
"@vaadin/hilla-react-signals": "25.0.4",
|
||||||
"@vaadin/polymer-legacy-adapter": "24.9.4",
|
"@vaadin/react-components": "25.0.3",
|
||||||
"@vaadin/react-components": "24.9.4",
|
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||||
"@vaadin/vaadin-lumo-styles": "24.9.4",
|
"@vaadin/vaadin-lumo-styles": "25.0.3",
|
||||||
"@vaadin/vaadin-material-styles": "24.9.4",
|
"@vaadin/vaadin-themable-mixin": "25.0.3",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.9.4",
|
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||||
"construct-style-sheets-polyfill": "3.1.0",
|
"date-fns": "4.1.0",
|
||||||
"date-fns": "2.29.3",
|
"lit": "3.3.2",
|
||||||
"lit": "3.3.1",
|
"react": "19.2.3",
|
||||||
"react": "19.1.1",
|
"react-dom": "19.2.3",
|
||||||
"react-dom": "19.1.1",
|
"react-router": "7.12.0"
|
||||||
"react-router": "7.6.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-react": "7.27.1",
|
"@babel/preset-react": "7.28.5",
|
||||||
"@preact/signals-react-transform": "0.6.0",
|
"@preact/signals-react-transform": "0.6.0",
|
||||||
"@rollup/plugin-replace": "6.0.2",
|
"@rollup/plugin-replace": "6.0.3",
|
||||||
"@rollup/pluginutils": "5.3.0",
|
"@rollup/pluginutils": "5.3.0",
|
||||||
"@types/react": "19.1.17",
|
"@types/node": "25.0.3",
|
||||||
"@types/react-dom": "19.1.11",
|
"@types/react": "19.2.7",
|
||||||
"@vaadin/hilla-generator-cli": "24.9.4",
|
"@types/react-dom": "19.2.3",
|
||||||
"@vaadin/hilla-generator-core": "24.9.4",
|
"@vaadin/hilla-generator-cli": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-backbone": "24.9.4",
|
"@vaadin/hilla-generator-core": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-barrel": "24.9.4",
|
"@vaadin/hilla-generator-plugin-backbone": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-client": "24.9.4",
|
"@vaadin/hilla-generator-plugin-barrel": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-model": "24.9.4",
|
"@vaadin/hilla-generator-plugin-client": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-push": "24.9.4",
|
"@vaadin/hilla-generator-plugin-model": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-signals": "24.9.4",
|
"@vaadin/hilla-generator-plugin-push": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-subtypes": "24.9.4",
|
"@vaadin/hilla-generator-plugin-signals": "25.0.4",
|
||||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.9.4",
|
"@vaadin/hilla-generator-plugin-subtypes": "25.0.4",
|
||||||
"@vaadin/hilla-generator-utils": "24.9.4",
|
"@vaadin/hilla-generator-plugin-transfertypes": "25.0.4",
|
||||||
"@vitejs/plugin-react": "4.7.0",
|
"@vaadin/hilla-generator-utils": "25.0.4",
|
||||||
"glob": "11.0.3",
|
"@vitejs/plugin-react": "5.1.2",
|
||||||
"magic-string": "0.30.19",
|
"magic-string": "0.30.21",
|
||||||
"rollup-plugin-brotli": "3.1.0",
|
"rollup-plugin-brotli": "3.1.0",
|
||||||
"rollup-plugin-visualizer": "5.14.0",
|
"rollup-plugin-visualizer": "6.0.5",
|
||||||
"strip-css-comments": "5.0.0",
|
"strip-css-comments": "5.0.0",
|
||||||
"transform-ast": "2.4.4",
|
"transform-ast": "2.4.4",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "6.4.1",
|
"vite": "7.3.1",
|
||||||
"vite-plugin-checker": "0.10.3",
|
"vite-plugin-checker": "0.12.0",
|
||||||
"workbox-build": "7.3.0",
|
"workbox-build": "7.4.0"
|
||||||
"workbox-core": "7.3.0",
|
|
||||||
"workbox-precaching": "7.3.0"
|
|
||||||
},
|
},
|
||||||
"disableUsageStatistics": true,
|
"disableUsageStatistics": true,
|
||||||
"hash": "760523c518e07bbe0567ae5d1b281ccf90326b285b5feb3c0f269c52ec774f88"
|
"hash": "d2c583f908a126db3f53ccbc87688b5089107afb58a87159631dc257a3a279ae"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
@ -1,5 +1,6 @@
|
|||||||
package org.gameyfin.app.collections.entities
|
package org.gameyfin.app.collections.entities
|
||||||
|
|
||||||
|
import jakarta.persistence.Column
|
||||||
import jakarta.persistence.ElementCollection
|
import jakarta.persistence.ElementCollection
|
||||||
import jakarta.persistence.Embeddable
|
import jakarta.persistence.Embeddable
|
||||||
import jakarta.persistence.FetchType
|
import jakarta.persistence.FetchType
|
||||||
@ -11,5 +12,6 @@ class CollectionMetadata(
|
|||||||
val displayOrder: Int = -1,
|
val displayOrder: Int = -1,
|
||||||
|
|
||||||
@ElementCollection(fetch = FetchType.EAGER)
|
@ElementCollection(fetch = FetchType.EAGER)
|
||||||
|
@Column(nullable = false)
|
||||||
val gamesAddedAt: MutableMap<Long, Instant> = mutableMapOf()
|
val gamesAddedAt: MutableMap<Long, Instant> = mutableMapOf()
|
||||||
)
|
)
|
||||||
@ -331,6 +331,7 @@ sealed class ConfigProperties<T : Serializable>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("EnumEntryName")
|
||||||
enum class MatchUsersBy {
|
enum class MatchUsersBy {
|
||||||
username, email
|
username, email
|
||||||
}
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
package org.gameyfin.app.config
|
package org.gameyfin.app.config
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.gameyfin.app.config.dto.ConfigEntryDto
|
import org.gameyfin.app.config.dto.ConfigEntryDto
|
||||||
import org.gameyfin.app.config.dto.ConfigUpdateDto
|
import org.gameyfin.app.config.dto.ConfigUpdateDto
|
||||||
@ -11,6 +9,8 @@ import org.springframework.data.repository.findByIdOrNull
|
|||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
|
import tools.jackson.core.JacksonException
|
||||||
|
import tools.jackson.databind.ObjectMapper
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.toJavaDuration
|
import kotlin.time.toJavaDuration
|
||||||
@ -186,7 +186,7 @@ class ConfigService(
|
|||||||
return try {
|
return try {
|
||||||
val typeReference = objectMapper.typeFactory.constructType(configProperty.type.java)
|
val typeReference = objectMapper.typeFactory.constructType(configProperty.type.java)
|
||||||
objectMapper.readValue(value.toString(), typeReference) as T
|
objectMapper.readValue(value.toString(), typeReference) as T
|
||||||
} catch (e: JsonProcessingException) {
|
} catch (e: JacksonException) {
|
||||||
throw IllegalArgumentException(
|
throw IllegalArgumentException(
|
||||||
"Failed to deserialize value '$value' for key '${configProperty.key}' to type ${configProperty.type.simpleName}: ${e.message}",
|
"Failed to deserialize value '$value' for key '${configProperty.key}' to type ${configProperty.type.simpleName}: ${e.message}",
|
||||||
e
|
e
|
||||||
@ -209,7 +209,7 @@ class ConfigService(
|
|||||||
private fun <T : Serializable> serializeValue(value: T, key: String): String {
|
private fun <T : Serializable> serializeValue(value: T, key: String): String {
|
||||||
return try {
|
return try {
|
||||||
objectMapper.writeValueAsString(value)
|
objectMapper.writeValueAsString(value)
|
||||||
} catch (e: JsonProcessingException) {
|
} catch (e: JacksonException) {
|
||||||
throw IllegalArgumentException(
|
throw IllegalArgumentException(
|
||||||
"Failed to serialize value for key '$key': ${e.message}",
|
"Failed to serialize value for key '$key': ${e.message}",
|
||||||
e
|
e
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package org.gameyfin.app.config.dto
|
package org.gameyfin.app.config.dto
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
|
||||||
import org.gameyfin.app.core.serialization.ArrayDeserializer
|
import org.gameyfin.app.core.serialization.ArrayDeserializer
|
||||||
|
import tools.jackson.databind.annotation.JsonDeserialize
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
data class ConfigUpdateDto(
|
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.JsonCreator
|
||||||
import com.fasterxml.jackson.annotation.JsonValue
|
import com.fasterxml.jackson.annotation.JsonValue
|
||||||
import org.gameyfin.app.users.RoleService
|
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) {
|
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 }
|
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)
|
val enumString = type.removePrefix(RoleService.INTERNAL_ROLE_PREFIX)
|
||||||
return try {
|
return try {
|
||||||
Enum.valueOf(Role::class.java, enumString)
|
java.lang.Enum.valueOf(Role::class.java, enumString)
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,11 +33,14 @@ class SetupDataLoader(
|
|||||||
|
|
||||||
val protocol = if (env.getProperty("server.ssl.key-store") != null) "https" else "http"
|
val protocol = if (env.getProperty("server.ssl.key-store") != null) "https" else "http"
|
||||||
val rawAppUrl = env.getProperty("app.url")
|
val rawAppUrl = env.getProperty("app.url")
|
||||||
|
|
||||||
|
@Suppress("HttpUrlsUsage")
|
||||||
val appUrl = when {
|
val appUrl = when {
|
||||||
rawAppUrl.isNullOrBlank() -> null
|
rawAppUrl.isNullOrBlank() -> null
|
||||||
rawAppUrl.startsWith("http://") || rawAppUrl.startsWith("https://") -> rawAppUrl
|
rawAppUrl.startsWith("http://") || rawAppUrl.startsWith("https://") -> rawAppUrl
|
||||||
else -> "$protocol://$rawAppUrl"
|
else -> "$protocol://$rawAppUrl"
|
||||||
}
|
}
|
||||||
|
|
||||||
val setupUrl =
|
val setupUrl =
|
||||||
appUrl ?: "${protocol}://${InetAddress.getLocalHost().hostName}:${env.getProperty("server.port")}/setup"
|
appUrl ?: "${protocol}://${InetAddress.getLocalHost().hostName}:${env.getProperty("server.port")}/setup"
|
||||||
log.info { "Visit $setupUrl to complete the setup" }
|
log.info { "Visit $setupUrl to complete the setup" }
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
package org.gameyfin.app.core.config
|
package org.gameyfin.app.core.config
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
import com.vaadin.hilla.EndpointController.ENDPOINT_MAPPER_FACTORY_BEAN_QUALIFIER
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
import com.vaadin.hilla.parser.jackson.ByteArrayModule
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
import com.vaadin.hilla.parser.jackson.JacksonObjectMapperFactory
|
||||||
import org.gameyfin.app.core.serialization.*
|
import org.gameyfin.app.core.serialization.*
|
||||||
import org.gameyfin.pluginapi.gamemetadata.*
|
import org.gameyfin.pluginapi.gamemetadata.*
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
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.
|
* Jackson configuration for custom serializers and deserializers.
|
||||||
@ -15,14 +18,21 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
|
|||||||
@Configuration
|
@Configuration
|
||||||
class JacksonConfig {
|
class JacksonConfig {
|
||||||
|
|
||||||
|
@Bean(ENDPOINT_MAPPER_FACTORY_BEAN_QUALIFIER)
|
||||||
@Bean
|
fun jsonMapperFactory(): JacksonObjectMapperFactory {
|
||||||
fun objectMapperCustomizer(): Jackson2ObjectMapperBuilder {
|
return JacksonObjectMapperFactory {
|
||||||
return Jackson2ObjectMapperBuilder()
|
JsonMapper.builder()
|
||||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
// Default Hilla options
|
||||||
.modulesToInstall(JavaTimeModule(), displayableEnumModule())
|
.addModule(ByteArrayModule())
|
||||||
|
.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false)
|
||||||
|
.enable(DeserializationFeature.ACCEPT_FLOAT_AS_INT)
|
||||||
|
// Custom modules
|
||||||
|
.addModule(displayableEnumModule())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun displayableEnumModule(): SimpleModule {
|
fun displayableEnumModule(): SimpleModule {
|
||||||
val module = SimpleModule("DisplayableEnumModule")
|
val module = SimpleModule("DisplayableEnumModule")
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package org.gameyfin.app.core.config
|
|||||||
|
|
||||||
import org.gameyfin.app.core.interceptors.EntityUpdateInterceptor
|
import org.gameyfin.app.core.interceptors.EntityUpdateInterceptor
|
||||||
import org.hibernate.cfg.AvailableSettings
|
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.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
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
|
package org.gameyfin.app.core.download.bandwidth
|
||||||
|
|
||||||
import com.vaadin.hilla.Endpoint
|
import com.vaadin.hilla.Endpoint
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
||||||
import jakarta.annotation.security.PermitAll
|
import jakarta.annotation.security.PermitAll
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
import org.gameyfin.app.core.Role
|
import org.gameyfin.app.core.Role
|
||||||
@ -17,11 +16,6 @@ import reactor.core.publisher.Flux
|
|||||||
class BandwidthMonitoringEndpoint(
|
class BandwidthMonitoringEndpoint(
|
||||||
private val bandwidthMonitoringService: BandwidthMonitoringService
|
private val bandwidthMonitoringService: BandwidthMonitoringService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val log = KotlinLogging.logger {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun subscribe(): Flux<List<List<SessionStatsDto>>> {
|
fun subscribe(): Flux<List<List<SessionStatsDto>>> {
|
||||||
return if (isCurrentUserAdmin()) BandwidthMonitoringService.subscribe()
|
return if (isCurrentUserAdmin()) BandwidthMonitoringService.subscribe()
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class FilesystemService(
|
|||||||
* @return A list of FileDto objects representing the files and directories.
|
* @return A list of FileDto objects representing the files and directories.
|
||||||
*/
|
*/
|
||||||
fun listContents(path: String?): List<FileDto> {
|
fun listContents(path: String?): List<FileDto> {
|
||||||
if (path == null || path.isEmpty()) {
|
if (path.isNullOrEmpty()) {
|
||||||
val roots = FileSystems.getDefault().rootDirectories.toList()
|
val roots = FileSystems.getDefault().rootDirectories.toList()
|
||||||
|
|
||||||
if (getHostOperatingSystem() == OperatingSystemType.WINDOWS) return roots.map {
|
if (getHostOperatingSystem() == OperatingSystemType.WINDOWS) return roots.map {
|
||||||
@ -145,7 +145,7 @@ class FilesystemService(
|
|||||||
if (file.isFile) {
|
if (file.isFile) {
|
||||||
file.length()
|
file.length()
|
||||||
} else if (file.isDirectory) {
|
} else if (file.isDirectory) {
|
||||||
File(path).walkTopDown().filter { it.isFile }.map { it.length() }.sum()
|
File(path).walkTopDown().filter { it.isFile }.sumOf { it.length() }
|
||||||
} else {
|
} else {
|
||||||
0L
|
0L
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import org.hibernate.type.Type
|
|||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class EntityUpdateInterceptor() : Interceptor {
|
class EntityUpdateInterceptor : Interceptor {
|
||||||
|
|
||||||
override fun onFlushDirty(
|
override fun onFlushDirty(
|
||||||
entity: Any?,
|
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?> {
|
fun getConfig(pluginWrapper: PluginWrapper): Map<String, String?> {
|
||||||
log.debug { "Getting config for plugin ${pluginWrapper.pluginId}" }
|
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>) {
|
fun updateConfig(pluginId: String, config: Map<String, String>) {
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
package org.gameyfin.app.core.plugins.config
|
package org.gameyfin.app.core.plugins.config
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
|
||||||
interface PluginConfigRepository : JpaRepository<PluginConfigEntry, PluginConfigEntryKey> {
|
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 {
|
) : PluginStatusProvider {
|
||||||
|
|
||||||
override fun isPluginDisabled(pluginId: String): Boolean {
|
override fun isPluginDisabled(pluginId: String): Boolean {
|
||||||
val pluginManagement = pluginManagementRepository.findByIdOrNull(pluginId)
|
val pluginManagement = pluginManagementRepository.findByIdOrNull(pluginId) ?: return true
|
||||||
|
|
||||||
if (pluginManagement == null) return true
|
|
||||||
|
|
||||||
return !pluginManagement.enabled
|
return !pluginManagement.enabled
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,10 @@ package org.gameyfin.app.core.plugins.management
|
|||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.pf4j.ExtensionDescriptor
|
import org.pf4j.ExtensionDescriptor
|
||||||
import org.pf4j.ExtensionWrapper
|
import org.pf4j.ExtensionWrapper
|
||||||
import org.pf4j.LegacyExtensionFinder
|
import org.pf4j.IndexedExtensionFinder
|
||||||
import org.pf4j.PluginManager
|
import org.pf4j.PluginManager
|
||||||
|
|
||||||
class GameyfinExtensionFinder(pluginManager: PluginManager) : LegacyExtensionFinder(pluginManager) {
|
class GameyfinExtensionFinder(pluginManager: PluginManager) : IndexedExtensionFinder(pluginManager) {
|
||||||
companion object {
|
companion object {
|
||||||
private val log = KotlinLogging.logger { }
|
private val log = KotlinLogging.logger { }
|
||||||
}
|
}
|
||||||
@ -27,7 +27,7 @@ class GameyfinExtensionFinder(pluginManager: PluginManager) : LegacyExtensionFin
|
|||||||
}
|
}
|
||||||
|
|
||||||
val classLoader =
|
val classLoader =
|
||||||
if (pluginId != null) pluginManager.getPluginClassLoader(pluginId) else javaClass.getClassLoader()
|
if (pluginId != null) pluginManager.getPluginClassLoader(pluginId) else javaClass.classLoader
|
||||||
|
|
||||||
for (className in classNames) {
|
for (className in classNames) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package org.gameyfin.app.core.plugins.management
|
|||||||
import org.pf4j.ManifestPluginDescriptorFinder
|
import org.pf4j.ManifestPluginDescriptorFinder
|
||||||
import java.util.jar.Manifest
|
import java.util.jar.Manifest
|
||||||
|
|
||||||
class GameyfinManifestPluginDescriptorFinder() : ManifestPluginDescriptorFinder() {
|
class GameyfinManifestPluginDescriptorFinder : ManifestPluginDescriptorFinder() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PLUGIN_NAME: String = "Plugin-Name"
|
const val PLUGIN_NAME: String = "Plugin-Name"
|
||||||
|
|||||||
@ -253,7 +253,7 @@ class GameyfinPluginManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getConfig(pluginId: String): Map<String, String?> {
|
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 {
|
private fun loadPluginSignaturePublicKey(): PublicKey {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class AppKeyValidator : CommandLineRunner {
|
|||||||
val log = KotlinLogging.logger {}
|
val log = KotlinLogging.logger {}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun run(vararg args: String?) {
|
override fun run(vararg args: String) {
|
||||||
val base64Key = System.getenv("APP_KEY")
|
val base64Key = System.getenv("APP_KEY")
|
||||||
if (!hasValidAppKey(base64Key)) exitProcess(1)
|
if (!hasValidAppKey(base64Key)) exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ class CustomAuthenticationEntryPoint : AuthenticationEntryPoint {
|
|||||||
override fun commence(
|
override fun commence(
|
||||||
request: HttpServletRequest,
|
request: HttpServletRequest,
|
||||||
response: HttpServletResponse,
|
response: HttpServletResponse,
|
||||||
authException: AuthenticationException?
|
authException: AuthenticationException
|
||||||
) {
|
) {
|
||||||
if (request.getParameter("direct") == "1") {
|
if (request.getParameter("direct") == "1") {
|
||||||
response.sendRedirect("/login")
|
response.sendRedirect("/login")
|
||||||
|
|||||||
@ -12,23 +12,13 @@ import java.util.function.Supplier
|
|||||||
class DynamicPublicAccessAuthorizationManager(
|
class DynamicPublicAccessAuthorizationManager(
|
||||||
private val config: ConfigService
|
private val config: ConfigService
|
||||||
) : AuthorizationManager<RequestAuthorizationContext> {
|
) : AuthorizationManager<RequestAuthorizationContext> {
|
||||||
|
override fun authorize(
|
||||||
@Deprecated("Deprecated in superclass")
|
authentication: Supplier<out Authentication>,
|
||||||
override fun check(
|
`object`: RequestAuthorizationContext
|
||||||
authentication: Supplier<Authentication?>?,
|
): AuthorizationResult {
|
||||||
`object`: RequestAuthorizationContext?
|
val auth = authentication.get()
|
||||||
): AuthorizationDecision {
|
val allow = (auth.isAuthenticated && auth.principal != "anonymousUser") ||
|
||||||
val auth = authentication?.get()
|
|
||||||
val allow = (auth?.isAuthenticated == true && auth.principal != "anonymousUser") ||
|
|
||||||
config.get(ConfigProperties.Security.AllowPublicAccess) == true
|
config.get(ConfigProperties.Security.AllowPublicAccess) == true
|
||||||
return AuthorizationDecision(allow)
|
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
|
package org.gameyfin.app.core.security
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import jakarta.persistence.AttributeConverter
|
import jakarta.persistence.AttributeConverter
|
||||||
import jakarta.persistence.Converter
|
import jakarta.persistence.Converter
|
||||||
|
import tools.jackson.databind.ObjectMapper
|
||||||
|
|
||||||
@Converter
|
@Converter
|
||||||
class EncryptionMapConverter : AttributeConverter<Map<String, String>, String> {
|
class EncryptionMapConverter : AttributeConverter<Map<String, String>, String> {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package org.gameyfin.app.core.security
|
package org.gameyfin.app.core.security
|
||||||
|
|
||||||
import com.vaadin.flow.spring.security.VaadinAwareSecurityContextHolderStrategyConfiguration
|
|
||||||
import com.vaadin.flow.spring.security.VaadinSecurityConfigurer
|
import com.vaadin.flow.spring.security.VaadinSecurityConfigurer
|
||||||
import com.vaadin.hilla.route.RouteUtil
|
import com.vaadin.hilla.route.RouteUtil
|
||||||
import org.gameyfin.app.config.ConfigProperties
|
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.Bean
|
||||||
import org.springframework.context.annotation.Conditional
|
import org.springframework.context.annotation.Conditional
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.context.annotation.Import
|
|
||||||
import org.springframework.core.env.Environment
|
import org.springframework.core.env.Environment
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
@ -25,9 +23,6 @@ import org.springframework.security.web.authentication.logout.HttpStatusReturnin
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@Import(
|
|
||||||
VaadinAwareSecurityContextHolderStrategyConfiguration::class
|
|
||||||
)
|
|
||||||
class SecurityConfig(
|
class SecurityConfig(
|
||||||
private val environment: Environment,
|
private val environment: Environment,
|
||||||
private val config: ConfigService,
|
private val config: ConfigService,
|
||||||
@ -41,6 +36,31 @@ class SecurityConfig(
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun filterChain(http: HttpSecurity, routeUtil: RouteUtil): SecurityFilterChain {
|
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 ->
|
http.authorizeHttpRequests { auth ->
|
||||||
// Set default security policy that permits Hilla internal requests and denies all other
|
// Set default security policy that permits Hilla internal requests and denies all other
|
||||||
auth.requestMatchers(routeUtil::isRouteAllowed).permitAll()
|
auth.requestMatchers(routeUtil::isRouteAllowed).permitAll()
|
||||||
@ -56,6 +76,13 @@ class SecurityConfig(
|
|||||||
"/favicon.ico",
|
"/favicon.ico",
|
||||||
"/favicon.svg"
|
"/favicon.svg"
|
||||||
).permitAll()
|
).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
|
// Dynamic public access for certain endpoints
|
||||||
.requestMatchers(
|
.requestMatchers(
|
||||||
"/",
|
"/",
|
||||||
@ -78,30 +105,6 @@ class SecurityConfig(
|
|||||||
http.cors { cors -> cors.disable() }
|
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) {
|
if ("dev" in environment.activeProfiles) {
|
||||||
http.authorizeHttpRequests { auth -> auth.requestMatchers("/h2-console/**").permitAll() }
|
http.authorizeHttpRequests { auth -> auth.requestMatchers("/h2-console/**").permitAll() }
|
||||||
|
|||||||
@ -13,5 +13,5 @@ fun isCurrentUserAdmin(): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun Authentication.isAdmin(): 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 userService: UserService,
|
||||||
private val roleService: RoleService,
|
private val roleService: RoleService,
|
||||||
private val config: ConfigService,
|
private val config: ConfigService,
|
||||||
private val roleHierarchy: RoleHierarchy,
|
roleHierarchy: RoleHierarchy,
|
||||||
) : AuthenticationSuccessHandler {
|
) : AuthenticationSuccessHandler {
|
||||||
|
|
||||||
private val authoritiesMapper = RoleHierarchyAuthoritiesMapper(roleHierarchy)
|
private val authoritiesMapper = RoleHierarchyAuthoritiesMapper(roleHierarchy)
|
||||||
@ -77,9 +77,13 @@ class SsoAuthenticationSuccessHandler(
|
|||||||
// Update SecurityContext with expanded authorities through RoleHierarchy
|
// Update SecurityContext with expanded authorities through RoleHierarchy
|
||||||
val mappedAuthorities = authoritiesMapper.mapAuthorities(grantedAuthorities)
|
val mappedAuthorities = authoritiesMapper.mapAuthorities(grantedAuthorities)
|
||||||
|
|
||||||
val newAuth =
|
val authPrincipal = authentication.principal
|
||||||
UsernamePasswordAuthenticationToken(authentication.principal, authentication.credentials, mappedAuthorities)
|
val authCredentials = authentication.credentials
|
||||||
SecurityContextHolder.getContext().authentication = newAuth
|
|
||||||
|
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
|
// Get the continue parameter from the request to redirect back to the original page
|
||||||
val continueUrl = request.getParameter("continue")
|
val continueUrl = request.getParameter("continue")
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package org.gameyfin.app.core.security
|
package org.gameyfin.app.core.security
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.gameyfin.app.config.ConfigProperties
|
import org.gameyfin.app.config.ConfigProperties
|
||||||
|
import org.springframework.beans.factory.getBean
|
||||||
import org.springframework.context.annotation.Condition
|
import org.springframework.context.annotation.Condition
|
||||||
import org.springframework.context.annotation.ConditionContext
|
import org.springframework.context.annotation.ConditionContext
|
||||||
import org.springframework.core.env.Environment
|
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.
|
* So we are rawdogging the database connection and query execution here.
|
||||||
*/
|
*/
|
||||||
class SsoEnabledCondition : Condition {
|
class SsoEnabledCondition : Condition {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = KotlinLogging.logger { }
|
||||||
|
}
|
||||||
|
|
||||||
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
|
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
|
||||||
try {
|
try {
|
||||||
val environment = context.beanFactory!!.getBean(Environment::class.java);
|
val environment = context.beanFactory?.getBean<Environment>()
|
||||||
val url = environment.getProperty("spring.datasource.url");
|
|
||||||
val user = environment.getProperty("spring.datasource.username");
|
if (environment == null) {
|
||||||
val password = environment.getProperty("spring.datasource.password");
|
log.warn { "Environment hasn't been loaded yet, cannot determine if SSO is enabled." }
|
||||||
val connection = DriverManager.getConnection(url, user, password);
|
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 ->
|
connection.use { c ->
|
||||||
val statement = c.prepareStatement("SELECT \"value\" FROM app_config WHERE \"key\" = ?")
|
val statement = c.prepareStatement("SELECT \"value\" FROM app_config WHERE \"key\" = ?")
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
package org.gameyfin.app.core.serialization
|
package org.gameyfin.app.core.serialization
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
import tools.jackson.core.JsonParser
|
||||||
import com.fasterxml.jackson.databind.DeserializationContext
|
import tools.jackson.databind.DeserializationContext
|
||||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
import tools.jackson.databind.JsonNode
|
||||||
import com.fasterxml.jackson.databind.JsonNode
|
import tools.jackson.databind.ValueDeserializer
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
class ArrayDeserializer : JsonDeserializer<Serializable>() {
|
class ArrayDeserializer : ValueDeserializer<Serializable>() {
|
||||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): 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) {
|
return if (node.isArray) {
|
||||||
node.map { it.asText() }.toTypedArray()
|
node.map { it.asString() }.toTypedArray()
|
||||||
} else {
|
} else {
|
||||||
p.codec.treeToValue(node, Serializable::class.java)
|
ctxt.readTreeAsValue(node, Serializable::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,15 +1,16 @@
|
|||||||
package org.gameyfin.app.core.serialization
|
package org.gameyfin.app.core.serialization
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonGenerator
|
import tools.jackson.core.JsonGenerator
|
||||||
import com.fasterxml.jackson.databind.JsonSerializer
|
import tools.jackson.databind.SerializationContext
|
||||||
import com.fasterxml.jackson.databind.SerializerProvider
|
import tools.jackson.databind.ValueSerializer
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A generic Jackson serializer for enums that have a displayName property.
|
* A generic Jackson serializer for enums that have a displayName property.
|
||||||
* This serializer writes the displayName value instead of the enum constant name.
|
* This serializer writes the displayName value instead of the enum constant name.
|
||||||
*/
|
*/
|
||||||
class DisplayableSerializer : JsonSerializer<Any>() {
|
class DisplayableSerializer : ValueSerializer<Any>() {
|
||||||
override fun serialize(value: Any?, gen: JsonGenerator, serializers: SerializerProvider) {
|
override fun serialize(value: Any?, gen: JsonGenerator, serializers: SerializationContext) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
package org.gameyfin.app.core.serialization
|
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 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.
|
* Jackson deserializer for GameFeature enum.
|
||||||
* Deserializes JSON strings by matching against the GameFeature's displayName property.
|
* 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? {
|
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): GameFeature? {
|
||||||
val displayName = p.text ?: return null
|
val displayName = p.string ?: return null
|
||||||
|
|
||||||
if (displayName.isEmpty()) {
|
if (displayName.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
package org.gameyfin.app.core.serialization
|
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 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.
|
* Jackson deserializer for Genre enum.
|
||||||
* Deserializes JSON strings by matching against the Genre's displayName property.
|
* 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? {
|
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Genre? {
|
||||||
val displayName = p.text ?: return null
|
val displayName = p.string ?: return null
|
||||||
|
|
||||||
if (displayName.isEmpty()) {
|
if (displayName.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
package org.gameyfin.app.core.serialization
|
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 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.
|
* Jackson deserializer for Platform enum.
|
||||||
* Deserializes JSON strings by matching against the Platform's displayName property.
|
* 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? {
|
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Platform? {
|
||||||
val displayName = p.text ?: return null
|
val displayName = p.string ?: return null
|
||||||
|
|
||||||
if (displayName.isEmpty()) {
|
if (displayName.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
package org.gameyfin.app.core.serialization
|
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 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.
|
* Jackson deserializer for PlayerPerspective enum.
|
||||||
* Deserializes JSON strings by matching against the PlayerPerspective's displayName property.
|
* 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? {
|
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): PlayerPerspective? {
|
||||||
val displayName = p.text ?: return null
|
val displayName = p.string ?: return null
|
||||||
|
|
||||||
if (displayName.isEmpty()) {
|
if (displayName.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
package org.gameyfin.app.core.serialization
|
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 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.
|
* Jackson deserializer for Theme enum.
|
||||||
* Deserializes JSON strings by matching against the Theme's displayName property.
|
* 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? {
|
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Theme? {
|
||||||
val displayName = p.text ?: return null
|
val displayName = p.string ?: return null
|
||||||
|
|
||||||
if (displayName.isEmpty()) {
|
if (displayName.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@ -15,6 +15,10 @@ import kotlin.time.toJavaDuration
|
|||||||
@Entity
|
@Entity
|
||||||
class Token<T : TokenType>(
|
class Token<T : TokenType>(
|
||||||
@Id
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
val id: String? = null,
|
||||||
|
|
||||||
|
@Column(unique = true, nullable = false)
|
||||||
@Convert(converter = EncryptionConverter::class)
|
@Convert(converter = EncryptionConverter::class)
|
||||||
val secret: String = UUID.randomUUID().toString(),
|
val secret: String = UUID.randomUUID().toString(),
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.gameyfin.app.core.token
|
package org.gameyfin.app.core.token
|
||||||
|
|
||||||
import org.hibernate.engine.spi.SharedSessionContractImplementor
|
import org.hibernate.type.descriptor.WrapperOptions
|
||||||
import org.hibernate.usertype.UserType
|
import org.hibernate.usertype.UserType
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.sql.PreparedStatement
|
import java.sql.PreparedStatement
|
||||||
@ -25,8 +25,7 @@ class TokenTypeUserType : UserType<TokenType> {
|
|||||||
override fun nullSafeGet(
|
override fun nullSafeGet(
|
||||||
rs: ResultSet,
|
rs: ResultSet,
|
||||||
position: Int,
|
position: Int,
|
||||||
session: SharedSessionContractImplementor,
|
options: WrapperOptions
|
||||||
owner: Any?
|
|
||||||
): TokenType? {
|
): TokenType? {
|
||||||
val key = rs.getString(position) ?: return null
|
val key = rs.getString(position) ?: return null
|
||||||
val tokenTypeClass = TokenType::class
|
val tokenTypeClass = TokenType::class
|
||||||
@ -41,7 +40,7 @@ class TokenTypeUserType : UserType<TokenType> {
|
|||||||
st: PreparedStatement,
|
st: PreparedStatement,
|
||||||
value: TokenType?,
|
value: TokenType?,
|
||||||
index: Int,
|
index: Int,
|
||||||
session: SharedSessionContractImplementor
|
options: WrapperOptions
|
||||||
) {
|
) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
st.setNull(index, Types.VARCHAR)
|
st.setNull(index, Types.VARCHAR)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
package org.gameyfin.app.core.token
|
package org.gameyfin.app.core.token
|
||||||
|
|
||||||
enum class TokenValidationResult() {
|
enum class TokenValidationResult {
|
||||||
VALID, INVALID, EXPIRED
|
VALID, INVALID, EXPIRED
|
||||||
}
|
}
|
||||||
@ -120,7 +120,7 @@ class GameService(
|
|||||||
imageService.downloadIfNew(it)
|
imageService.downloadIfNew(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
game.images.map {
|
game.images.forEach {
|
||||||
imageService.downloadIfNew(it)
|
imageService.downloadIfNew(it)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -647,7 +647,7 @@ class GameService(
|
|||||||
// (Optional) Step 0: Extract title from filename using regex
|
// (Optional) Step 0: Extract title from filename using regex
|
||||||
if (config.get(ConfigProperties.Libraries.Scan.ExtractTitleUsingRegex) == true) {
|
if (config.get(ConfigProperties.Libraries.Scan.ExtractTitleUsingRegex) == true) {
|
||||||
val regexString = config.get(ConfigProperties.Libraries.Scan.TitleExtractionRegex)
|
val regexString = config.get(ConfigProperties.Libraries.Scan.TitleExtractionRegex)
|
||||||
if (regexString != null && regexString.isNotEmpty()) {
|
if (!regexString.isNullOrEmpty()) {
|
||||||
try {
|
try {
|
||||||
val regex = Regex(regexString)
|
val regex = Regex(regexString)
|
||||||
val originalQuery = query
|
val originalQuery = query
|
||||||
|
|||||||
@ -32,6 +32,7 @@ class Game(
|
|||||||
|
|
||||||
@ElementCollection(targetClass = Platform::class, fetch = FetchType.EAGER)
|
@ElementCollection(targetClass = Platform::class, fetch = FetchType.EAGER)
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
var platforms: MutableList<Platform> = mutableListOf(),
|
var platforms: MutableList<Platform> = mutableListOf(),
|
||||||
|
|
||||||
var title: String? = null,
|
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
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
|
|
||||||
@Column(unique = true, nullable = false, length = 1024)
|
@Column(unique = true, nullable = false, length = 1024)
|
||||||
val path: String,
|
val path: String,
|
||||||
|
|
||||||
@OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.EAGER)
|
@OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.EAGER)
|
||||||
|
@JoinColumn(nullable = false)
|
||||||
val source: IgnoredPathSource
|
val source: IgnoredPathSource
|
||||||
) {
|
) {
|
||||||
fun getType(): IgnoredPathSourceType {
|
fun getType(): IgnoredPathSourceType {
|
||||||
|
|||||||
@ -29,6 +29,7 @@ class Library(
|
|||||||
|
|
||||||
@ElementCollection(targetClass = Platform::class, fetch = FetchType.EAGER)
|
@ElementCollection(targetClass = Platform::class, fetch = FetchType.EAGER)
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
var platforms: MutableList<Platform> = ArrayList(),
|
var platforms: MutableList<Platform> = ArrayList(),
|
||||||
|
|
||||||
@OneToMany(mappedBy = "library", fetch = FetchType.EAGER, orphanRemoval = true)
|
@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
|
package org.gameyfin.app.media
|
||||||
|
|
||||||
import jakarta.persistence.*
|
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
|
@Entity
|
||||||
class Image(
|
class Image(
|
||||||
@ -16,13 +13,10 @@ class Image(
|
|||||||
|
|
||||||
val type: ImageType,
|
val type: ImageType,
|
||||||
|
|
||||||
@ContentId
|
|
||||||
var contentId: String? = null,
|
var contentId: String? = null,
|
||||||
|
|
||||||
@ContentLength
|
|
||||||
var contentLength: Long? = null,
|
var contentLength: Long? = null,
|
||||||
|
|
||||||
@MimeType
|
|
||||||
var mimeType: String? = null,
|
var mimeType: String? = null,
|
||||||
|
|
||||||
var blurhash: String? = null
|
var blurhash: String? = null
|
||||||
|
|||||||
@ -28,22 +28,22 @@ class ImageEndpoint(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
@GetMapping("/screenshot/{id}")
|
@GetMapping("/screenshot/{id}")
|
||||||
fun getScreenshot(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? {
|
fun getScreenshot(@PathVariable id: Long): ResponseEntity<InputStreamResource>? {
|
||||||
return getImageContent(id)
|
return getImageContent(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/cover/{id}")
|
@GetMapping("/cover/{id}")
|
||||||
fun getCover(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? {
|
fun getCover(@PathVariable id: Long): ResponseEntity<InputStreamResource>? {
|
||||||
return getImageContent(id)
|
return getImageContent(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/header/{id}")
|
@GetMapping("/header/{id}")
|
||||||
fun getHeader(@PathVariable("id") id: Long): ResponseEntity<InputStreamResource>? {
|
fun getHeader(@PathVariable id: Long): ResponseEntity<InputStreamResource>? {
|
||||||
return getImageContent(id)
|
return getImageContent(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/plugins/{id}/logo")
|
@GetMapping("/plugins/{pluginId}/logo")
|
||||||
fun getPluginLogo(@PathVariable("id") pluginId: String): ResponseEntity<ByteArrayResource>? {
|
fun getPluginLogo(@PathVariable pluginId: String): ResponseEntity<ByteArrayResource>? {
|
||||||
val logo = pluginService.getLogo(pluginId)
|
val logo = pluginService.getLogo(pluginId)
|
||||||
return Utils.inputStreamToResponseEntity(logo)
|
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.UserDeletedEvent
|
||||||
import org.gameyfin.app.core.events.UserUpdatedEvent
|
import org.gameyfin.app.core.events.UserUpdatedEvent
|
||||||
import org.gameyfin.app.games.repositories.GameRepository
|
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.games.repositories.ImageRepository
|
||||||
import org.gameyfin.app.users.persistence.UserRepository
|
import org.gameyfin.app.users.persistence.UserRepository
|
||||||
import org.springframework.dao.DataIntegrityViolationException
|
import org.springframework.dao.DataIntegrityViolationException
|
||||||
@ -28,7 +27,7 @@ import javax.imageio.ImageIO
|
|||||||
@Service
|
@Service
|
||||||
class ImageService(
|
class ImageService(
|
||||||
private val imageRepository: ImageRepository,
|
private val imageRepository: ImageRepository,
|
||||||
private val imageContentStore: ImageContentStore,
|
private val fileStorageService: FileStorageService,
|
||||||
private val gameRepository: GameRepository,
|
private val gameRepository: GameRepository,
|
||||||
private val userRepository: UserRepository
|
private val userRepository: UserRepository
|
||||||
) {
|
) {
|
||||||
@ -39,6 +38,7 @@ class ImageService(
|
|||||||
* Scale down image for faster blurhash calculation.
|
* Scale down image for faster blurhash calculation.
|
||||||
* Blurhash doesn't need full resolution - 100px width is plenty for a good blur.
|
* Blurhash doesn't need full resolution - 100px width is plenty for a good blur.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("DuplicatedCode")
|
||||||
fun scaleImageForBlurhash(original: BufferedImage, maxWidth: Int = 100): BufferedImage {
|
fun scaleImageForBlurhash(original: BufferedImage, maxWidth: Int = 100): BufferedImage {
|
||||||
val originalWidth = original.width
|
val originalWidth = original.width
|
||||||
val originalHeight = original.height
|
val originalHeight = original.height
|
||||||
@ -49,10 +49,9 @@ class ImageService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val scale = maxWidth.toDouble() / originalWidth
|
val scale = maxWidth.toDouble() / originalWidth
|
||||||
val targetWidth = maxWidth
|
|
||||||
val targetHeight = (originalHeight * scale).toInt()
|
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()
|
val g2d = scaled.createGraphics()
|
||||||
|
|
||||||
// Use fast scaling for blurhash - quality doesn't matter much for a blur
|
// 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_RENDERING, RenderingHints.VALUE_RENDER_SPEED)
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF)
|
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()
|
g2d.dispose()
|
||||||
|
|
||||||
return scaled
|
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 the existing image has valid content we can just associate it instead of downloading again
|
||||||
if (existingImageHasValidContent && existingImage.contentId != null) {
|
if (existingImageHasValidContent && existingImage.contentId != null) {
|
||||||
// Associate existing content with the current image entity reference
|
// Associate existing content with the current image entity reference
|
||||||
imageContentStore.associate(image, existingImage.contentId)
|
|
||||||
image.contentId = existingImage.contentId
|
image.contentId = existingImage.contentId
|
||||||
image.contentLength = existingImage.contentLength
|
image.contentLength = existingImage.contentLength
|
||||||
image.mimeType = existingImage.mimeType
|
image.mimeType = existingImage.mimeType
|
||||||
@ -162,19 +160,7 @@ class ImageService(
|
|||||||
// If no existing image or existing image has no valid content, download it
|
// If no existing image or existing image has no valid content, download it
|
||||||
TikaInputStream.get { URI.create(image.originalUrl).toURL().openStream() }.use { input ->
|
TikaInputStream.get { URI.create(image.originalUrl).toURL().openStream() }.use { input ->
|
||||||
image.mimeType = tika.detect(input)
|
image.mimeType = tika.detect(input)
|
||||||
|
processImageContent(image, 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save or update the image to ensure it's persisted
|
// 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 {
|
fun createFromInputStream(type: ImageType, content: InputStream, mimeType: String): Image {
|
||||||
val image = Image(type = type, mimeType = mimeType)
|
val image = Image(type = type, mimeType = mimeType)
|
||||||
|
processImageContent(image, content)
|
||||||
// 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
|
|
||||||
return imageRepository.save(image)
|
return imageRepository.save(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,8 +182,7 @@ class ImageService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFileContent(image: Image): InputStream? {
|
fun getFileContent(image: Image): InputStream? {
|
||||||
return imageContentStore.getContent(image)
|
return fileStorageService.getFile(image.contentId)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteImageIfUnused(image: Image) {
|
fun deleteImageIfUnused(image: Image) {
|
||||||
@ -221,13 +192,30 @@ class ImageService(
|
|||||||
|
|
||||||
if (!isImageStillInUse) {
|
if (!isImageStillInUse) {
|
||||||
imageRepository.delete(image)
|
imageRepository.delete(image)
|
||||||
imageContentStore.unsetContent(image)
|
fileStorageService.deleteFile(image.contentId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFileContent(image: Image, content: InputStream, mimeType: String? = null): Image {
|
fun updateFileContent(image: Image, content: InputStream, mimeType: String? = null): Image {
|
||||||
mimeType?.let { image.mimeType = it }
|
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
|
// Read the input stream into a byte array so we can use it twice
|
||||||
val imageBytes = content.readBytes()
|
val imageBytes = content.readBytes()
|
||||||
|
|
||||||
@ -238,16 +226,9 @@ class ImageService(
|
|||||||
|
|
||||||
// Store content
|
// Store content
|
||||||
ByteArrayInputStream(imageBytes).use { contentStream ->
|
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? {
|
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.MessageTemplateService
|
||||||
import org.gameyfin.app.messages.templates.MessageTemplates
|
import org.gameyfin.app.messages.templates.MessageTemplates
|
||||||
import org.gameyfin.app.users.UserService
|
import org.gameyfin.app.users.UserService
|
||||||
|
import org.springframework.beans.factory.getBeansOfType
|
||||||
import org.springframework.context.ApplicationContext
|
import org.springframework.context.ApplicationContext
|
||||||
import org.springframework.context.event.EventListener
|
import org.springframework.context.event.EventListener
|
||||||
import org.springframework.scheduling.annotation.Async
|
import org.springframework.scheduling.annotation.Async
|
||||||
@ -27,7 +28,7 @@ class MessageService(
|
|||||||
get() = providers.any { it.enabled }
|
get() = providers.any { it.enabled }
|
||||||
|
|
||||||
private val providers: List<AbstractMessageProvider>
|
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 {
|
fun testCredentials(provider: String, credentials: Map<String, Any>): Boolean {
|
||||||
val messageProvider = providers.find { it.providerKey == provider }
|
val messageProvider = providers.find { it.providerKey == provider }
|
||||||
|
|||||||
@ -67,7 +67,7 @@ class EmailMessageProvider(
|
|||||||
|
|
||||||
val transport = session.getTransport("smtp")
|
val transport = session.getTransport("smtp")
|
||||||
|
|
||||||
try {
|
transport.use { transport ->
|
||||||
transport.connect(
|
transport.connect(
|
||||||
credentials["host"] as String,
|
credentials["host"] as String,
|
||||||
credentials["port"] as Int,
|
credentials["port"] as Int,
|
||||||
@ -75,8 +75,6 @@ class EmailMessageProvider(
|
|||||||
credentials["password"] as String
|
credentials["password"] as String
|
||||||
)
|
)
|
||||||
transport.sendMessage(mimeMessage, mimeMessage.allRecipients)
|
transport.sendMessage(mimeMessage, mimeMessage.allRecipients)
|
||||||
} finally {
|
|
||||||
transport.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,18 +7,7 @@ import org.springframework.stereotype.Service
|
|||||||
class SystemService(
|
class SystemService(
|
||||||
private val restartEndpoint: RestartEndpoint,
|
private val restartEndpoint: RestartEndpoint,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var restartRequired = false;
|
|
||||||
|
|
||||||
fun restart() {
|
fun restart() {
|
||||||
restartEndpoint.restart()
|
restartEndpoint.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setRestartRequired() {
|
|
||||||
restartRequired = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isRestartRequired(): Boolean {
|
|
||||||
return restartRequired
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -47,12 +47,12 @@ class RoleService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getRolesBelowAuth(auth: Authentication): List<Role> {
|
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 }
|
return Role.entries.filter { it.powerLevel < highestUserRole.powerLevel }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authoritiesToRoles(authorities: Collection<GrantedAuthority>): List<Role> {
|
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) {
|
class SessionService(private val sessionRegistry: SessionRegistry) {
|
||||||
|
|
||||||
fun logoutAllSessions() {
|
fun logoutAllSessions() {
|
||||||
val auth = getCurrentAuth()
|
val authPrincipal = getCurrentAuth()?.principal ?: return
|
||||||
val sessions: List<SessionInformation> = sessionRegistry.getAllSessions(auth?.principal, false)
|
val sessions: List<SessionInformation> = sessionRegistry.getAllSessions(authPrincipal, false)
|
||||||
for (sessionInfo in sessions) {
|
for (sessionInfo in sessions) {
|
||||||
sessionInfo.expireNow()
|
sessionInfo.expireNow()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,13 +6,11 @@ import jakarta.annotation.security.RolesAllowed
|
|||||||
import org.gameyfin.app.core.Role
|
import org.gameyfin.app.core.Role
|
||||||
import org.gameyfin.app.core.token.TokenDto
|
import org.gameyfin.app.core.token.TokenDto
|
||||||
import org.gameyfin.app.core.token.TokenValidationResult
|
import org.gameyfin.app.core.token.TokenValidationResult
|
||||||
import org.gameyfin.app.users.UserService
|
|
||||||
|
|
||||||
@Endpoint
|
@Endpoint
|
||||||
@AnonymousAllowed
|
@AnonymousAllowed
|
||||||
class PasswordResetEndpoint(
|
class PasswordResetEndpoint(
|
||||||
private val passwordResetService: PasswordResetService,
|
private val passwordResetService: PasswordResetService
|
||||||
private val userService: UserService
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun requestPasswordReset(email: String) {
|
fun requestPasswordReset(email: String) {
|
||||||
|
|||||||
@ -59,7 +59,7 @@ class PasswordResetService(
|
|||||||
*/
|
*/
|
||||||
fun requestPasswordReset(email: String) {
|
fun requestPasswordReset(email: String) {
|
||||||
|
|
||||||
val maskedEmail = Utils.Companion.maskEmail(email)
|
val maskedEmail = Utils.maskEmail(email)
|
||||||
|
|
||||||
log.info { "Initiating password reset request for '${maskedEmail}'" }
|
log.info { "Initiating password reset request for '${maskedEmail}'" }
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ class PasswordResetService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val token = generate(user)
|
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
|
// Simulate a delay to prevent timing attacks
|
||||||
Thread.sleep(secureRandom.nextLong(1024))
|
Thread.sleep(secureRandom.nextLong(1024))
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class UserPreferencesService(
|
|||||||
return if (appConfig != null) {
|
return if (appConfig != null) {
|
||||||
getValue(appConfig.value, userPreference)
|
getValue(appConfig.value, userPreference)
|
||||||
} else {
|
} else {
|
||||||
return null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ class UserPreferencesService(
|
|||||||
return if (appConfig != null) {
|
return if (appConfig != null) {
|
||||||
getValue(appConfig.value, userPreference).toString()
|
getValue(appConfig.value, userPreference).toString()
|
||||||
} else {
|
} else {
|
||||||
return null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,14 +27,14 @@ class InvitationService(
|
|||||||
|
|
||||||
fun createInvitation(email: String): TokenDto {
|
fun createInvitation(email: String): TokenDto {
|
||||||
if (userService.existsByEmail(email))
|
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 auth = getCurrentAuth() ?: throw IllegalStateException("No authentication found")
|
||||||
val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found")
|
val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found")
|
||||||
val payload = mapOf(EMAIL_KEY to email)
|
val payload = mapOf(EMAIL_KEY to email)
|
||||||
val token = super.generateWithPayload(user, payload)
|
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)
|
return TokenDto(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,8 +52,8 @@ class InvitationService(
|
|||||||
try {
|
try {
|
||||||
val user = userService.registerUserFromInvitation(registration, email)
|
val user = userService.registerUserFromInvitation(registration, email)
|
||||||
super.delete(invitationToken)
|
super.delete(invitationToken)
|
||||||
eventPublisher.publishEvent(AccountStatusChangedEvent(this, user, Utils.Companion.getBaseUrl()))
|
eventPublisher.publishEvent(AccountStatusChangedEvent(this, user, Utils.getBaseUrl()))
|
||||||
} catch (e: IllegalStateException) {
|
} catch (_: IllegalStateException) {
|
||||||
return UserInvitationAcceptanceResult.USERNAME_TAKEN
|
return UserInvitationAcceptanceResult.USERNAME_TAKEN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package org.gameyfin.app.util
|
package org.gameyfin.app.util
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager
|
import jakarta.persistence.EntityManager
|
||||||
|
import org.springframework.beans.factory.getBean
|
||||||
import org.springframework.context.ApplicationContext
|
import org.springframework.context.ApplicationContext
|
||||||
import org.springframework.context.ApplicationContextAware
|
import org.springframework.context.ApplicationContextAware
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
@ -10,7 +11,7 @@ object EntityManagerHolder : ApplicationContextAware {
|
|||||||
private var entityManager: EntityManager? = null
|
private var entityManager: EntityManager? = null
|
||||||
|
|
||||||
override fun setApplicationContext(context: ApplicationContext) {
|
override fun setApplicationContext(context: ApplicationContext) {
|
||||||
entityManager = context.getBean(EntityManager::class.java)
|
entityManager = context.getBean<EntityManager>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntityManager(): EntityManager {
|
fun getEntityManager(): EntityManager {
|
||||||
|
|||||||
@ -22,6 +22,7 @@ object BlurhashMigration {
|
|||||||
* Scale down image for faster blurhash calculation.
|
* Scale down image for faster blurhash calculation.
|
||||||
* Blurhash doesn't need full resolution - 100px width is plenty for a good blur.
|
* 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 {
|
private fun scaleImageForBlurhash(original: BufferedImage, maxWidth: Int = 100): BufferedImage {
|
||||||
val originalWidth = original.width
|
val originalWidth = original.width
|
||||||
val originalHeight = original.height
|
val originalHeight = original.height
|
||||||
@ -32,10 +33,9 @@ object BlurhashMigration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val scale = maxWidth.toDouble() / originalWidth
|
val scale = maxWidth.toDouble() / originalWidth
|
||||||
val targetWidth = maxWidth
|
|
||||||
val targetHeight = (originalHeight * scale).toInt()
|
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()
|
val g2d = scaled.createGraphics()
|
||||||
|
|
||||||
// Use fast scaling for blurhash - quality doesn't matter much for a blur
|
// 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_RENDERING, RenderingHints.VALUE_RENDER_SPEED)
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF)
|
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()
|
g2d.dispose()
|
||||||
|
|
||||||
return scaled
|
return scaled
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package org.gameyfin.db.h2
|
package org.gameyfin.db.h2
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import org.gameyfin.app.core.security.EncryptionUtils
|
import org.gameyfin.app.core.security.EncryptionUtils
|
||||||
|
import tools.jackson.databind.ObjectMapper
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.sql.SQLException
|
import java.sql.SQLException
|
||||||
|
|
||||||
|
|||||||
@ -7,8 +7,6 @@ logging.level:
|
|||||||
org.gameyfin.GameyfinApplicationKt: warn
|
org.gameyfin.GameyfinApplicationKt: warn
|
||||||
# Suppress false positive warnings from Spring Security 6
|
# Suppress false positive warnings from Spring Security 6
|
||||||
org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer: error
|
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:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
@ -17,10 +15,10 @@ server:
|
|||||||
tracking-modes: cookie
|
tracking-modes: cookie
|
||||||
timeout: 24h
|
timeout: 24h
|
||||||
forward-headers-strategy: framework
|
forward-headers-strategy: framework
|
||||||
tomcat:
|
jetty:
|
||||||
remoteip:
|
threads:
|
||||||
protocol-header: X-Forwarded-Proto
|
max: 200
|
||||||
remote-ip-header: X-Forwarded-For
|
min: 8
|
||||||
|
|
||||||
management:
|
management:
|
||||||
server:
|
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
|
package org.gameyfin.app.config
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
import org.gameyfin.app.config.entities.ConfigEntry
|
import org.gameyfin.app.config.entities.ConfigEntry
|
||||||
import org.gameyfin.app.config.persistence.ConfigRepository
|
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.junit.jupiter.api.assertThrows
|
||||||
import org.springframework.boot.logging.LogLevel
|
import org.springframework.boot.logging.LogLevel
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import tools.jackson.databind.ObjectMapper
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package org.gameyfin.app.core.download.bandwidth
|
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.Assertions.assertDoesNotThrow
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -104,7 +103,7 @@ class SessionBandwidthManagerTest {
|
|||||||
|
|
||||||
val stat1 = stats["session-1"]
|
val stat1 = stats["session-1"]
|
||||||
assertNotNull(stat1)
|
assertNotNull(stat1)
|
||||||
assertEquals("session-1", stat1!!.sessionId)
|
assertEquals("session-1", stat1.sessionId)
|
||||||
assertEquals(1, stat1.activeDownloads)
|
assertEquals(1, stat1.activeDownloads)
|
||||||
assertEquals("user1", stat1.username)
|
assertEquals("user1", stat1.username)
|
||||||
assertEquals("192.168.1.1", stat1.remoteIp)
|
assertEquals("192.168.1.1", stat1.remoteIp)
|
||||||
|
|||||||
@ -425,5 +425,122 @@ class SessionBandwidthTrackerTest {
|
|||||||
val afterRecordTime = tracker.lastActivityTime
|
val afterRecordTime = tracker.lastActivityTime
|
||||||
assertTrue(afterRecordTime > initialTime)
|
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.
|
* Helper method to wait for DeferredResult to complete and get the result.
|
||||||
* Handles async processing with timeout.
|
* 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)
|
val latch = CountDownLatch(1)
|
||||||
var result: T? = null
|
var result: T? = null
|
||||||
var error: Throwable? = null
|
var error: Throwable? = null
|
||||||
@ -108,7 +108,7 @@ class DownloadEndpointTest {
|
|||||||
|
|
||||||
assertEquals(HttpStatus.OK, response.statusCode)
|
assertEquals(HttpStatus.OK, response.statusCode)
|
||||||
assertNotNull(response.body)
|
assertNotNull(response.body)
|
||||||
assertTrue(response.headers.containsKey("Content-Disposition"))
|
assertTrue(response.headers.containsHeader("Content-Disposition"))
|
||||||
assertTrue(response.headers["Content-Disposition"]!![0].contains("Test Game.zip"))
|
assertTrue(response.headers["Content-Disposition"]!![0].contains("Test Game.zip"))
|
||||||
|
|
||||||
verify(exactly = 1) { gameService.getById(gameId) }
|
verify(exactly = 1) { gameService.getById(gameId) }
|
||||||
@ -142,9 +142,9 @@ class DownloadEndpointTest {
|
|||||||
val response = awaitDeferredResult(deferredResult)
|
val response = awaitDeferredResult(deferredResult)
|
||||||
|
|
||||||
assertEquals(HttpStatus.OK, response.statusCode)
|
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
|
// Content-Length should not be present for directories
|
||||||
assertFalse(response.headers.containsKey("Content-Length"))
|
assertFalse(response.headers.containsHeader("Content-Length"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -171,7 +171,7 @@ class DownloadEndpointTest {
|
|||||||
val response = awaitDeferredResult(deferredResult)
|
val response = awaitDeferredResult(deferredResult)
|
||||||
|
|
||||||
assertEquals(HttpStatus.OK, response.statusCode)
|
assertEquals(HttpStatus.OK, response.statusCode)
|
||||||
assertFalse(response.headers.containsKey("Content-Length"))
|
assertFalse(response.headers.containsHeader("Content-Length"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -198,7 +198,7 @@ class DownloadEndpointTest {
|
|||||||
val response = awaitDeferredResult(deferredResult)
|
val response = awaitDeferredResult(deferredResult)
|
||||||
|
|
||||||
assertEquals(HttpStatus.OK, response.statusCode)
|
assertEquals(HttpStatus.OK, response.statusCode)
|
||||||
assertFalse(response.headers.containsKey("Content-Length"))
|
assertFalse(response.headers.containsHeader("Content-Length"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -131,7 +131,8 @@ class LogEndpointTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getApplicationLogs should return empty flux when authentication is null`() {
|
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)
|
SecurityContextHolder.setContext(securityContext)
|
||||||
|
|
||||||
val result = logEndpoint.getApplicationLogs()
|
val result = logEndpoint.getApplicationLogs()
|
||||||
|
|||||||
@ -42,7 +42,7 @@ class GameyfinPluginManagerTest {
|
|||||||
pluginManagementRepository = mockk(relaxed = true)
|
pluginManagementRepository = mockk(relaxed = true)
|
||||||
|
|
||||||
// Set up default mocks
|
// 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.findByIdOrNull(any()) } returns null
|
||||||
every { pluginManagementRepository.save(any()) } returnsArgument 0
|
every { pluginManagementRepository.save(any()) } returnsArgument 0
|
||||||
every { pluginManagementRepository.findMaxPriority() } returns null
|
every { pluginManagementRepository.findMaxPriority() } returns null
|
||||||
@ -233,7 +233,7 @@ class GameyfinPluginManagerTest {
|
|||||||
every { pluginManager.getPlugin("test-plugin") } returns pluginWrapper
|
every { pluginManager.getPlugin("test-plugin") } returns pluginWrapper
|
||||||
every { pluginManager.stopPlugin("test-plugin") } returns PluginState.STOPPED
|
every { pluginManager.stopPlugin("test-plugin") } returns PluginState.STOPPED
|
||||||
every { pluginManager.startPlugin("test-plugin") } returns PluginState.STARTED
|
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")
|
pluginManager.restart("test-plugin")
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse
|
|||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException
|
||||||
import org.springframework.security.authentication.BadCredentialsException
|
import org.springframework.security.authentication.BadCredentialsException
|
||||||
import org.springframework.security.core.AuthenticationException
|
import org.springframework.security.core.AuthenticationException
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
@ -96,7 +97,7 @@ class CustomAuthenticationEntryPointTest {
|
|||||||
every { request.getParameter("direct") } returns "1"
|
every { request.getParameter("direct") } returns "1"
|
||||||
every { response.sendRedirect(any()) } just Runs
|
every { response.sendRedirect(any()) } just Runs
|
||||||
|
|
||||||
entryPoint.commence(request, response, null)
|
entryPoint.commence(request, response, AuthenticationCredentialsNotFoundException("Test"))
|
||||||
|
|
||||||
verify(exactly = 1) { response.sendRedirect("/login") }
|
verify(exactly = 1) { response.sendRedirect("/login") }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
|||||||
"password",
|
"password",
|
||||||
listOf(SimpleGrantedAuthority("ROLE_USER"))
|
listOf(SimpleGrantedAuthority("ROLE_USER"))
|
||||||
)
|
)
|
||||||
val authSupplier = Supplier<Authentication?> { authentication }
|
val authSupplier = Supplier<Authentication> { authentication }
|
||||||
|
|
||||||
val decision = manager.authorize(authSupplier, context)
|
val decision = manager.authorize(authSupplier, context)
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
|||||||
"password",
|
"password",
|
||||||
listOf(SimpleGrantedAuthority("ROLE_USER"))
|
listOf(SimpleGrantedAuthority("ROLE_USER"))
|
||||||
)
|
)
|
||||||
val authSupplier = Supplier<Authentication?> { authentication }
|
val authSupplier = Supplier<Authentication> { authentication }
|
||||||
|
|
||||||
val decision = manager.authorize(authSupplier, context)
|
val decision = manager.authorize(authSupplier, context)
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
|||||||
every { authentication.isAuthenticated } returns false
|
every { authentication.isAuthenticated } returns false
|
||||||
every { authentication.principal } returns "anonymousUser"
|
every { authentication.principal } returns "anonymousUser"
|
||||||
|
|
||||||
val authSupplier = Supplier<Authentication?> { authentication }
|
val authSupplier = Supplier<Authentication> { authentication }
|
||||||
|
|
||||||
val decision = manager.authorize(authSupplier, context)
|
val decision = manager.authorize(authSupplier, context)
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
|||||||
every { authentication.isAuthenticated } returns false
|
every { authentication.isAuthenticated } returns false
|
||||||
every { authentication.principal } returns "anonymousUser"
|
every { authentication.principal } returns "anonymousUser"
|
||||||
|
|
||||||
val authSupplier = Supplier<Authentication?> { authentication }
|
val authSupplier = Supplier<Authentication> { authentication }
|
||||||
|
|
||||||
val decision = manager.authorize(authSupplier, context)
|
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`() {
|
fun `check should deny access when authentication is null and public access is disabled`() {
|
||||||
every { configService.get(ConfigProperties.Security.AllowPublicAccess) } returns false
|
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)
|
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`() {
|
fun `check should allow access when authentication is null and public access is enabled`() {
|
||||||
every { configService.get(ConfigProperties.Security.AllowPublicAccess) } returns true
|
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)
|
val decision = manager.authorize(authSupplier, context)
|
||||||
|
|
||||||
@ -135,7 +137,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
|||||||
every { authentication.isAuthenticated } returns true
|
every { authentication.isAuthenticated } returns true
|
||||||
every { authentication.principal } returns "anonymousUser"
|
every { authentication.principal } returns "anonymousUser"
|
||||||
|
|
||||||
val authSupplier = Supplier<Authentication?> { authentication }
|
val authSupplier = Supplier<Authentication> { authentication }
|
||||||
|
|
||||||
val decision = manager.authorize(authSupplier, context)
|
val decision = manager.authorize(authSupplier, context)
|
||||||
|
|
||||||
@ -151,7 +153,7 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
|||||||
every { authentication.isAuthenticated } returns true
|
every { authentication.isAuthenticated } returns true
|
||||||
every { authentication.principal } returns "john.doe"
|
every { authentication.principal } returns "john.doe"
|
||||||
|
|
||||||
val authSupplier = Supplier<Authentication?> { authentication }
|
val authSupplier = Supplier<Authentication> { authentication }
|
||||||
|
|
||||||
val decision = manager.authorize(authSupplier, context)
|
val decision = manager.authorize(authSupplier, context)
|
||||||
|
|
||||||
@ -167,38 +169,11 @@ class DynamicPublicAccessAuthorizationManagerTest {
|
|||||||
every { authentication.isAuthenticated } returns false
|
every { authentication.isAuthenticated } returns false
|
||||||
every { authentication.principal } returns "anonymousUser"
|
every { authentication.principal } returns "anonymousUser"
|
||||||
|
|
||||||
val authSupplier = Supplier<Authentication?> { authentication }
|
val authSupplier = Supplier<Authentication> { authentication }
|
||||||
|
|
||||||
val decision = manager.authorize(authSupplier, context)
|
val decision = manager.authorize(authSupplier, context)
|
||||||
|
|
||||||
assertNotNull(decision)
|
assertNotNull(decision)
|
||||||
assertFalse(decision.isGranted)
|
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))
|
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
|
@Test
|
||||||
fun `passwordEncoder should handle long password`() {
|
fun `passwordEncoder should handle long password`() {
|
||||||
val encoder = config.passwordEncoder()
|
val encoder = config.passwordEncoder()
|
||||||
|
|||||||
@ -116,17 +116,6 @@ class SecurityUtilsTest {
|
|||||||
assertFalse(result)
|
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
|
@Test
|
||||||
fun `isCurrentUserAdmin should return true when user has both ADMIN and USER roles`() {
|
fun `isCurrentUserAdmin should return true when user has both ADMIN and USER roles`() {
|
||||||
val authentication = UsernamePasswordAuthenticationToken(
|
val authentication = UsernamePasswordAuthenticationToken(
|
||||||
@ -188,14 +177,6 @@ class SecurityUtilsTest {
|
|||||||
assertFalse(authentication.isAdmin())
|
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
|
@Test
|
||||||
fun `Authentication isAdmin should return true when user has SUPERADMIN among multiple roles`() {
|
fun `Authentication isAdmin should return true when user has SUPERADMIN among multiple roles`() {
|
||||||
val authentication = UsernamePasswordAuthenticationToken(
|
val authentication = UsernamePasswordAuthenticationToken(
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import org.gameyfin.app.config.ConfigProperties
|
|||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.getBean
|
||||||
import org.springframework.context.annotation.ConditionContext
|
import org.springframework.context.annotation.ConditionContext
|
||||||
import org.springframework.core.env.Environment
|
import org.springframework.core.env.Environment
|
||||||
import org.springframework.core.type.AnnotatedTypeMetadata
|
import org.springframework.core.type.AnnotatedTypeMetadata
|
||||||
@ -30,7 +31,7 @@ class SsoEnabledConditionTest {
|
|||||||
environment = mockk<Environment>()
|
environment = mockk<Environment>()
|
||||||
|
|
||||||
every { context.beanFactory } returns mockk {
|
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.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertDoesNotThrow
|
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.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -169,6 +171,7 @@ class LibraryWatcherServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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`() {
|
fun `file create event should trigger quick scan`() {
|
||||||
val libraryDir = tempDir.resolve("watch-create")
|
val libraryDir = tempDir.resolve("watch-create")
|
||||||
libraryDir.createDirectories()
|
libraryDir.createDirectories()
|
||||||
@ -197,6 +200,7 @@ class LibraryWatcherServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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`() {
|
fun `file delete event should trigger quick scan`() {
|
||||||
val libraryDir = tempDir.resolve("watch-delete")
|
val libraryDir = tempDir.resolve("watch-delete")
|
||||||
libraryDir.createDirectories()
|
libraryDir.createDirectories()
|
||||||
@ -227,6 +231,7 @@ class LibraryWatcherServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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`() {
|
fun `directory delete event should trigger quick scan`() {
|
||||||
val libraryDir = tempDir.resolve("watch-delete-dir")
|
val libraryDir = tempDir.resolve("watch-delete-dir")
|
||||||
libraryDir.createDirectories()
|
libraryDir.createDirectories()
|
||||||
@ -256,6 +261,7 @@ class LibraryWatcherServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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`() {
|
fun `file modify event should update game file size`() {
|
||||||
val libraryDir = tempDir.resolve("watch-modify")
|
val libraryDir = tempDir.resolve("watch-modify")
|
||||||
libraryDir.createDirectories()
|
libraryDir.createDirectories()
|
||||||
@ -290,6 +296,7 @@ class LibraryWatcherServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@DisabledOnOs(OS.MAC, disabledReason = "File system watcher events are unreliable on macOS due to FSEvents latency")
|
||||||
fun `should handle multiple rapid file changes`() {
|
fun `should handle multiple rapid file changes`() {
|
||||||
val libraryDir = tempDir.resolve("watch-rapid")
|
val libraryDir = tempDir.resolve("watch-rapid")
|
||||||
libraryDir.createDirectories()
|
libraryDir.createDirectories()
|
||||||
@ -320,6 +327,7 @@ class LibraryWatcherServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@DisabledOnOs(OS.MAC, disabledReason = "File system watcher events are unreliable on macOS due to FSEvents latency")
|
||||||
fun `should handle library with multiple directories`() {
|
fun `should handle library with multiple directories`() {
|
||||||
val dir1 = tempDir.resolve("lib-dir1")
|
val dir1 = tempDir.resolve("lib-dir1")
|
||||||
val dir2 = tempDir.resolve("lib-dir2")
|
val dir2 = tempDir.resolve("lib-dir2")
|
||||||
@ -354,6 +362,7 @@ class LibraryWatcherServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@DisabledOnOs(OS.MAC, disabledReason = "File system watcher events are unreliable on macOS due to FSEvents latency")
|
||||||
fun `should handle directory creation`() {
|
fun `should handle directory creation`() {
|
||||||
val libraryDir = tempDir.resolve("watch-dir")
|
val libraryDir = tempDir.resolve("watch-dir")
|
||||||
libraryDir.createDirectories()
|
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.core.events.UserUpdatedEvent
|
||||||
import org.gameyfin.app.games.entities.Game
|
import org.gameyfin.app.games.entities.Game
|
||||||
import org.gameyfin.app.games.repositories.GameRepository
|
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.games.repositories.ImageRepository
|
||||||
import org.gameyfin.app.users.entities.User
|
import org.gameyfin.app.users.entities.User
|
||||||
import org.gameyfin.app.users.persistence.UserRepository
|
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.dao.DataIntegrityViolationException
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
import kotlin.test.assertNull
|
import kotlin.test.assertNull
|
||||||
@ -27,7 +25,7 @@ import kotlin.test.assertNull
|
|||||||
class ImageServiceTest {
|
class ImageServiceTest {
|
||||||
|
|
||||||
private lateinit var imageRepository: ImageRepository
|
private lateinit var imageRepository: ImageRepository
|
||||||
private lateinit var imageContentStore: ImageContentStore
|
private lateinit var fileStorageService: FileStorageService
|
||||||
private lateinit var gameRepository: GameRepository
|
private lateinit var gameRepository: GameRepository
|
||||||
private lateinit var userRepository: UserRepository
|
private lateinit var userRepository: UserRepository
|
||||||
private lateinit var imageService: ImageService
|
private lateinit var imageService: ImageService
|
||||||
@ -35,10 +33,10 @@ class ImageServiceTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
imageRepository = mockk()
|
imageRepository = mockk()
|
||||||
imageContentStore = mockk()
|
fileStorageService = mockk()
|
||||||
gameRepository = mockk()
|
gameRepository = mockk()
|
||||||
userRepository = mockk()
|
userRepository = mockk()
|
||||||
imageService = ImageService(imageRepository, imageContentStore, gameRepository, userRepository)
|
imageService = ImageService(imageRepository, fileStorageService, gameRepository, userRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@ -181,19 +179,16 @@ class ImageServiceTest {
|
|||||||
contentLength = 1024L,
|
contentLength = 1024L,
|
||||||
mimeType = "image/jpeg"
|
mimeType = "image/jpeg"
|
||||||
)
|
)
|
||||||
val inputStream = ByteArrayInputStream("image data".toByteArray())
|
|
||||||
|
|
||||||
every { imageRepository.findAllByOriginalUrl(url) } returns listOf(existingImage)
|
every { imageRepository.findAllByOriginalUrl(url) } returns listOf(existingImage)
|
||||||
every { imageContentStore.getContent(existingImage) } returns inputStream
|
every { fileStorageService.fileExists("existing-content-id") } returns true
|
||||||
every { imageContentStore.associate(image, "existing-content-id") } just Runs
|
|
||||||
|
|
||||||
imageService.downloadIfNew(image)
|
imageService.downloadIfNew(image)
|
||||||
|
|
||||||
assertEquals("existing-content-id", image.contentId)
|
assertEquals("existing-content-id", image.contentId)
|
||||||
assertEquals(1024L, image.contentLength)
|
assertEquals(1024L, image.contentLength)
|
||||||
assertEquals("image/jpeg", image.mimeType)
|
assertEquals("image/jpeg", image.mimeType)
|
||||||
verify(exactly = 1) { imageContentStore.associate(image, "existing-content-id") }
|
verify(exactly = 0) { fileStorageService.saveFile(any()) }
|
||||||
verify(exactly = 0) { imageContentStore.setContent(any(), any<InputStream>()) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -216,14 +211,13 @@ class ImageServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
every { imageRepository.findAllByOriginalUrl(url) } returns listOf(existingImage)
|
every { imageRepository.findAllByOriginalUrl(url) } returns listOf(existingImage)
|
||||||
every { imageContentStore.getContent(existingImage) } returns null
|
every { fileStorageService.fileExists(null) } returns false
|
||||||
every { imageContentStore.setContent(any<Image>(), any<InputStream>()) } returnsArgument 0
|
every { fileStorageService.saveFile(any()) } returns "new-content-id"
|
||||||
every { imageRepository.save(image) } returns image
|
every { imageRepository.save(image) } returns image
|
||||||
|
|
||||||
imageService.downloadIfNew(image)
|
imageService.downloadIfNew(image)
|
||||||
|
|
||||||
verify(exactly = 0) { imageContentStore.associate(any(), any()) }
|
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
|
||||||
verify(exactly = 1) { imageRepository.save(image) }
|
verify(exactly = 1) { imageRepository.save(image) }
|
||||||
unmockkStatic(TikaInputStream::class)
|
unmockkStatic(TikaInputStream::class)
|
||||||
}
|
}
|
||||||
@ -240,7 +234,6 @@ class ImageServiceTest {
|
|||||||
contentLength = 0L,
|
contentLength = 0L,
|
||||||
mimeType = "image/jpeg"
|
mimeType = "image/jpeg"
|
||||||
)
|
)
|
||||||
val inputStream = ByteArrayInputStream("image data".toByteArray())
|
|
||||||
|
|
||||||
mockkStatic(TikaInputStream::class)
|
mockkStatic(TikaInputStream::class)
|
||||||
val testData = "test image data".toByteArray()
|
val testData = "test image data".toByteArray()
|
||||||
@ -249,14 +242,13 @@ class ImageServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
every { imageRepository.findAllByOriginalUrl(url) } returns listOf(existingImage)
|
every { imageRepository.findAllByOriginalUrl(url) } returns listOf(existingImage)
|
||||||
every { imageContentStore.getContent(existingImage) } returns inputStream
|
every { fileStorageService.fileExists("existing-content-id") } returns true
|
||||||
every { imageContentStore.setContent(any<Image>(), any<InputStream>()) } returnsArgument 0
|
every { fileStorageService.saveFile(any()) } returns "new-content-id"
|
||||||
every { imageRepository.save(image) } returns image
|
every { imageRepository.save(image) } returns image
|
||||||
|
|
||||||
imageService.downloadIfNew(image)
|
imageService.downloadIfNew(image)
|
||||||
|
|
||||||
verify(exactly = 0) { imageContentStore.associate(any(), any()) }
|
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
|
||||||
verify(exactly = 1) { imageRepository.save(image) }
|
verify(exactly = 1) { imageRepository.save(image) }
|
||||||
unmockkStatic(TikaInputStream::class)
|
unmockkStatic(TikaInputStream::class)
|
||||||
}
|
}
|
||||||
@ -273,12 +265,12 @@ class ImageServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
every { imageRepository.findAllByOriginalUrl(url) } returns emptyList()
|
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
|
every { imageRepository.save(image) } returns image
|
||||||
|
|
||||||
imageService.downloadIfNew(image)
|
imageService.downloadIfNew(image)
|
||||||
|
|
||||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||||
verify(exactly = 1) { imageRepository.save(image) }
|
verify(exactly = 1) { imageRepository.save(image) }
|
||||||
unmockkStatic(TikaInputStream::class)
|
unmockkStatic(TikaInputStream::class)
|
||||||
}
|
}
|
||||||
@ -295,12 +287,12 @@ class ImageServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
every { imageRepository.findAllByOriginalUrl(url) } returns emptyList()
|
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")
|
every { imageRepository.save(image) } throws DataIntegrityViolationException("Duplicate")
|
||||||
|
|
||||||
imageService.downloadIfNew(image)
|
imageService.downloadIfNew(image)
|
||||||
|
|
||||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||||
verify(exactly = 1) { imageRepository.save(image) }
|
verify(exactly = 1) { imageRepository.save(image) }
|
||||||
unmockkStatic(TikaInputStream::class)
|
unmockkStatic(TikaInputStream::class)
|
||||||
}
|
}
|
||||||
@ -311,13 +303,13 @@ class ImageServiceTest {
|
|||||||
val savedImage = Image(id = 1L, type = ImageType.AVATAR, mimeType = "image/png")
|
val savedImage = Image(id = 1L, type = ImageType.AVATAR, mimeType = "image/png")
|
||||||
|
|
||||||
every { imageRepository.save(any<Image>()) } returns savedImage
|
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")
|
val result = imageService.createFromInputStream(ImageType.AVATAR, inputStream, "image/png")
|
||||||
|
|
||||||
assertNotNull(result)
|
assertNotNull(result)
|
||||||
verify(exactly = 1) { imageRepository.save(any<Image>()) }
|
verify(exactly = 1) { imageRepository.save(any<Image>()) }
|
||||||
verify(exactly = 1) { imageContentStore.setContent(any<Image>(), any<InputStream>()) }
|
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -347,24 +339,24 @@ class ImageServiceTest {
|
|||||||
val image = Image(id = 1L, type = ImageType.COVER, contentId = "content-id")
|
val image = Image(id = 1L, type = ImageType.COVER, contentId = "content-id")
|
||||||
val inputStream = ByteArrayInputStream("image data".toByteArray())
|
val inputStream = ByteArrayInputStream("image data".toByteArray())
|
||||||
|
|
||||||
every { imageContentStore.getContent(image) } returns inputStream
|
every { fileStorageService.getFile("content-id") } returns inputStream
|
||||||
|
|
||||||
val result = imageService.getFileContent(image)
|
val result = imageService.getFileContent(image)
|
||||||
|
|
||||||
assertEquals(inputStream, result)
|
assertEquals(inputStream, result)
|
||||||
verify(exactly = 1) { imageContentStore.getContent(image) }
|
verify(exactly = 1) { fileStorageService.getFile("content-id") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getFileContent should return null when content store returns null`() {
|
fun `getFileContent should return null when content store returns null`() {
|
||||||
val image = Image(id = 1L, type = ImageType.COVER, contentId = "content-id")
|
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)
|
val result = imageService.getFileContent(image)
|
||||||
|
|
||||||
assertNull(result)
|
assertNull(result)
|
||||||
verify(exactly = 1) { imageContentStore.getContent(image) }
|
verify(exactly = 1) { fileStorageService.getFile("content-id") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -374,14 +366,14 @@ class ImageServiceTest {
|
|||||||
every { gameRepository.existsByImage(1L) } returns false
|
every { gameRepository.existsByImage(1L) } returns false
|
||||||
every { userRepository.existsByAvatar(1L) } returns false
|
every { userRepository.existsByAvatar(1L) } returns false
|
||||||
every { imageRepository.delete(image) } just Runs
|
every { imageRepository.delete(image) } just Runs
|
||||||
every { imageContentStore.unsetContent(image) } returnsArgument 0
|
every { fileStorageService.deleteFile(any()) } just Runs
|
||||||
|
|
||||||
imageService.deleteImageIfUnused(image)
|
imageService.deleteImageIfUnused(image)
|
||||||
|
|
||||||
verify(exactly = 1) { gameRepository.existsByImage(1L) }
|
verify(exactly = 1) { gameRepository.existsByImage(1L) }
|
||||||
verify(exactly = 1) { userRepository.existsByAvatar(1L) }
|
verify(exactly = 1) { userRepository.existsByAvatar(1L) }
|
||||||
verify(exactly = 1) { imageRepository.delete(image) }
|
verify(exactly = 1) { imageRepository.delete(image) }
|
||||||
verify(exactly = 1) { imageContentStore.unsetContent(image) }
|
verify(exactly = 1) { fileStorageService.deleteFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -395,7 +387,7 @@ class ImageServiceTest {
|
|||||||
verify(exactly = 1) { gameRepository.existsByImage(1L) }
|
verify(exactly = 1) { gameRepository.existsByImage(1L) }
|
||||||
verify(exactly = 0) { userRepository.existsByAvatar(any()) }
|
verify(exactly = 0) { userRepository.existsByAvatar(any()) }
|
||||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -410,7 +402,7 @@ class ImageServiceTest {
|
|||||||
verify(exactly = 1) { gameRepository.existsByImage(1L) }
|
verify(exactly = 1) { gameRepository.existsByImage(1L) }
|
||||||
verify(exactly = 1) { userRepository.existsByAvatar(1L) }
|
verify(exactly = 1) { userRepository.existsByAvatar(1L) }
|
||||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -422,7 +414,7 @@ class ImageServiceTest {
|
|||||||
verify(exactly = 0) { gameRepository.existsByImage(any()) }
|
verify(exactly = 0) { gameRepository.existsByImage(any()) }
|
||||||
verify(exactly = 0) { userRepository.existsByAvatar(any()) }
|
verify(exactly = 0) { userRepository.existsByAvatar(any()) }
|
||||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -431,13 +423,14 @@ class ImageServiceTest {
|
|||||||
val inputStream = ByteArrayInputStream("new image data".toByteArray())
|
val inputStream = ByteArrayInputStream("new image data".toByteArray())
|
||||||
|
|
||||||
every { imageRepository.save(image) } returns image
|
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")
|
imageService.updateFileContent(image, inputStream, "image/jpeg")
|
||||||
|
|
||||||
assertEquals("image/jpeg", image.mimeType)
|
assertEquals("image/jpeg", image.mimeType)
|
||||||
verify(exactly = 1) { imageRepository.save(image) }
|
verify(exactly = 1) { imageRepository.save(image) }
|
||||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -446,13 +439,14 @@ class ImageServiceTest {
|
|||||||
val inputStream = ByteArrayInputStream("new image data".toByteArray())
|
val inputStream = ByteArrayInputStream("new image data".toByteArray())
|
||||||
|
|
||||||
every { imageRepository.save(image) } returns image
|
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)
|
imageService.updateFileContent(image, inputStream)
|
||||||
|
|
||||||
assertEquals("image/png", image.mimeType)
|
assertEquals("image/png", image.mimeType)
|
||||||
verify(exactly = 1) { imageRepository.save(image) }
|
verify(exactly = 1) { imageRepository.save(image) }
|
||||||
verify(exactly = 1) { imageContentStore.setContent(image, any<InputStream>()) }
|
verify(exactly = 1) { fileStorageService.saveFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -474,7 +468,7 @@ class ImageServiceTest {
|
|||||||
every { gameRepository.existsByImage(3L) } returns false
|
every { gameRepository.existsByImage(3L) } returns false
|
||||||
every { userRepository.existsByAvatar(3L) } returns false
|
every { userRepository.existsByAvatar(3L) } returns false
|
||||||
every { imageRepository.delete(any()) } just Runs
|
every { imageRepository.delete(any()) } just Runs
|
||||||
every { imageContentStore.unsetContent(any()) } returnsArgument 0
|
every { fileStorageService.deleteFile(any()) } just Runs
|
||||||
|
|
||||||
imageService.onGameDeleted(event)
|
imageService.onGameDeleted(event)
|
||||||
|
|
||||||
@ -496,12 +490,12 @@ class ImageServiceTest {
|
|||||||
every { gameRepository.existsByImage(1L) } returns false
|
every { gameRepository.existsByImage(1L) } returns false
|
||||||
every { userRepository.existsByAvatar(1L) } returns false
|
every { userRepository.existsByAvatar(1L) } returns false
|
||||||
every { imageRepository.delete(screenshot) } just Runs
|
every { imageRepository.delete(screenshot) } just Runs
|
||||||
every { imageContentStore.unsetContent(screenshot) } returnsArgument 0
|
every { fileStorageService.deleteFile(any()) } just Runs
|
||||||
|
|
||||||
imageService.onGameDeleted(event)
|
imageService.onGameDeleted(event)
|
||||||
|
|
||||||
verify(exactly = 1) { imageRepository.delete(screenshot) }
|
verify(exactly = 1) { imageRepository.delete(screenshot) }
|
||||||
verify(exactly = 1) { imageContentStore.unsetContent(screenshot) }
|
verify(exactly = 1) { fileStorageService.deleteFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -528,7 +522,7 @@ class ImageServiceTest {
|
|||||||
every { gameRepository.existsByImage(3L) } returns false
|
every { gameRepository.existsByImage(3L) } returns false
|
||||||
every { userRepository.existsByAvatar(3L) } returns false
|
every { userRepository.existsByAvatar(3L) } returns false
|
||||||
every { imageRepository.delete(any()) } just Runs
|
every { imageRepository.delete(any()) } just Runs
|
||||||
every { imageContentStore.unsetContent(any()) } returnsArgument 0
|
every { fileStorageService.deleteFile(any()) } just Runs
|
||||||
|
|
||||||
imageService.onGameUpdated(event)
|
imageService.onGameUpdated(event)
|
||||||
|
|
||||||
@ -558,7 +552,7 @@ class ImageServiceTest {
|
|||||||
imageService.onGameUpdated(event)
|
imageService.onGameUpdated(event)
|
||||||
|
|
||||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -572,12 +566,12 @@ class ImageServiceTest {
|
|||||||
every { gameRepository.existsByImage(1L) } returns false
|
every { gameRepository.existsByImage(1L) } returns false
|
||||||
every { userRepository.existsByAvatar(1L) } returns false
|
every { userRepository.existsByAvatar(1L) } returns false
|
||||||
every { imageRepository.delete(avatar) } just Runs
|
every { imageRepository.delete(avatar) } just Runs
|
||||||
every { imageContentStore.unsetContent(avatar) } returnsArgument 0
|
every { fileStorageService.deleteFile(any()) } just Runs
|
||||||
|
|
||||||
imageService.onAccountDeleted(event)
|
imageService.onAccountDeleted(event)
|
||||||
|
|
||||||
verify(exactly = 1) { imageRepository.delete(avatar) }
|
verify(exactly = 1) { imageRepository.delete(avatar) }
|
||||||
verify(exactly = 1) { imageContentStore.unsetContent(avatar) }
|
verify(exactly = 1) { fileStorageService.deleteFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -590,7 +584,7 @@ class ImageServiceTest {
|
|||||||
imageService.onAccountDeleted(event)
|
imageService.onAccountDeleted(event)
|
||||||
|
|
||||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -608,12 +602,12 @@ class ImageServiceTest {
|
|||||||
every { gameRepository.existsByImage(1L) } returns false
|
every { gameRepository.existsByImage(1L) } returns false
|
||||||
every { userRepository.existsByAvatar(1L) } returns false
|
every { userRepository.existsByAvatar(1L) } returns false
|
||||||
every { imageRepository.delete(oldAvatar) } just Runs
|
every { imageRepository.delete(oldAvatar) } just Runs
|
||||||
every { imageContentStore.unsetContent(oldAvatar) } returnsArgument 0
|
every { fileStorageService.deleteFile(any()) } just Runs
|
||||||
|
|
||||||
imageService.onUserUpdated(event)
|
imageService.onUserUpdated(event)
|
||||||
|
|
||||||
verify(exactly = 1) { imageRepository.delete(oldAvatar) }
|
verify(exactly = 1) { imageRepository.delete(oldAvatar) }
|
||||||
verify(exactly = 1) { imageContentStore.unsetContent(oldAvatar) }
|
verify(exactly = 1) { fileStorageService.deleteFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -630,7 +624,7 @@ class ImageServiceTest {
|
|||||||
imageService.onUserUpdated(event)
|
imageService.onUserUpdated(event)
|
||||||
|
|
||||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -647,7 +641,7 @@ class ImageServiceTest {
|
|||||||
imageService.onUserUpdated(event)
|
imageService.onUserUpdated(event)
|
||||||
|
|
||||||
verify(exactly = 0) { imageRepository.delete(any()) }
|
verify(exactly = 0) { imageRepository.delete(any()) }
|
||||||
verify(exactly = 0) { imageContentStore.unsetContent(any()) }
|
verify(exactly = 0) { fileStorageService.deleteFile(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -664,11 +658,11 @@ class ImageServiceTest {
|
|||||||
every { gameRepository.existsByImage(1L) } returns false
|
every { gameRepository.existsByImage(1L) } returns false
|
||||||
every { userRepository.existsByAvatar(1L) } returns false
|
every { userRepository.existsByAvatar(1L) } returns false
|
||||||
every { imageRepository.delete(oldAvatar) } just Runs
|
every { imageRepository.delete(oldAvatar) } just Runs
|
||||||
every { imageContentStore.unsetContent(oldAvatar) } returnsArgument 0
|
every { fileStorageService.deleteFile(any()) } just Runs
|
||||||
|
|
||||||
imageService.onUserUpdated(event)
|
imageService.onUserUpdated(event)
|
||||||
|
|
||||||
verify(exactly = 1) { imageRepository.delete(oldAvatar) }
|
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.Assertions.assertThrows
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.getBeansOfType
|
||||||
import org.springframework.context.ApplicationContext
|
import org.springframework.context.ApplicationContext
|
||||||
import org.springframework.security.core.Authentication
|
import org.springframework.security.core.Authentication
|
||||||
import org.springframework.security.core.context.SecurityContext
|
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`() {
|
fun `enabled should return true when at least one provider is enabled`() {
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { mockProvider2.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,
|
"provider1" to mockProvider1,
|
||||||
"provider2" to mockProvider2
|
"provider2" to mockProvider2
|
||||||
)
|
)
|
||||||
@ -69,7 +70,7 @@ class MessageServiceTest {
|
|||||||
fun `enabled should return false when no providers are enabled`() {
|
fun `enabled should return false when no providers are enabled`() {
|
||||||
every { mockProvider1.enabled } returns false
|
every { mockProvider1.enabled } returns false
|
||||||
every { mockProvider2.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,
|
"provider1" to mockProvider1,
|
||||||
"provider2" to mockProvider2
|
"provider2" to mockProvider2
|
||||||
)
|
)
|
||||||
@ -81,7 +82,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `enabled should return false when no providers exist`() {
|
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
|
val result = messageService.enabled
|
||||||
|
|
||||||
@ -95,7 +96,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.providerKey } returns providerKey
|
every { mockProvider1.providerKey } returns providerKey
|
||||||
every { mockProvider1.testCredentials(any()) } returns true
|
every { mockProvider1.testCredentials(any()) } returns true
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -112,7 +113,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.providerKey } returns providerKey
|
every { mockProvider1.providerKey } returns providerKey
|
||||||
every { mockProvider1.testCredentials(any()) } returns false
|
every { mockProvider1.testCredentials(any()) } returns false
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -127,7 +128,7 @@ class MessageServiceTest {
|
|||||||
val credentials = mapOf("host" to "smtp.example.com")
|
val credentials = mapOf("host" to "smtp.example.com")
|
||||||
|
|
||||||
every { mockProvider1.providerKey } returns "email"
|
every { mockProvider1.providerKey } returns "email"
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -145,7 +146,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.providerKey } returns providerKey
|
every { mockProvider1.providerKey } returns providerKey
|
||||||
every { mockProvider1.testCredentials(any()) } returns true
|
every { mockProvider1.testCredentials(any()) } returns true
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -167,7 +168,7 @@ class MessageServiceTest {
|
|||||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||||
every { mockProvider2.enabled } returns true
|
every { mockProvider2.enabled } returns true
|
||||||
every { mockProvider2.supportedTemplateType } returns TemplateType.TEXT
|
every { mockProvider2.supportedTemplateType } returns TemplateType.TEXT
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1,
|
"provider1" to mockProvider1,
|
||||||
"provider2" to mockProvider2
|
"provider2" to mockProvider2
|
||||||
)
|
)
|
||||||
@ -197,7 +198,7 @@ class MessageServiceTest {
|
|||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||||
every { mockProvider2.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,
|
"provider1" to mockProvider1,
|
||||||
"provider2" to mockProvider2
|
"provider2" to mockProvider2
|
||||||
)
|
)
|
||||||
@ -218,7 +219,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.enabled } returns false
|
every { mockProvider1.enabled } returns false
|
||||||
every { mockProvider2.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,
|
"provider1" to mockProvider1,
|
||||||
"provider2" to mockProvider2
|
"provider2" to mockProvider2
|
||||||
)
|
)
|
||||||
@ -231,7 +232,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sendTestNotification should return false when messaging is disabled`() {
|
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())
|
val result = messageService.sendTestNotification("password-reset-request", emptyMap())
|
||||||
|
|
||||||
@ -249,7 +250,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
every { userService.getByUsername("testuser") } returns user
|
every { userService.getByUsername("testuser") } returns user
|
||||||
@ -274,7 +275,7 @@ class MessageServiceTest {
|
|||||||
val placeholders = mapOf("username" to "testuser", "resetLink" to "http://example.com/reset")
|
val placeholders = mapOf("username" to "testuser", "resetLink" to "http://example.com/reset")
|
||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -291,7 +292,7 @@ class MessageServiceTest {
|
|||||||
setupSecurityContext("testuser")
|
setupSecurityContext("testuser")
|
||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
every { userService.getByUsername("testuser") } returns null
|
every { userService.getByUsername("testuser") } returns null
|
||||||
@ -310,7 +311,7 @@ class MessageServiceTest {
|
|||||||
setupSecurityContext("testuser")
|
setupSecurityContext("testuser")
|
||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
every { userService.getByUsername("testuser") } returns user
|
every { userService.getByUsername("testuser") } returns user
|
||||||
@ -330,7 +331,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
every {
|
every {
|
||||||
@ -358,7 +359,7 @@ class MessageServiceTest {
|
|||||||
val token = Token(creator = user, secret = "secret123", type = TokenType.PasswordReset)
|
val token = Token(creator = user, secret = "secret123", type = TokenType.PasswordReset)
|
||||||
val event = PasswordResetRequestEvent(this, token, "http://example.com")
|
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)
|
messageService.onPasswordResetRequest(event)
|
||||||
|
|
||||||
@ -373,7 +374,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
every {
|
every {
|
||||||
@ -394,7 +395,7 @@ class MessageServiceTest {
|
|||||||
val user = User(username = "newuser", email = "new@example.com", password = "hash")
|
val user = User(username = "newuser", email = "new@example.com", password = "hash")
|
||||||
val event = UserRegistrationWaitingForApprovalEvent(this, user)
|
val event = UserRegistrationWaitingForApprovalEvent(this, user)
|
||||||
|
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns emptyMap()
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns emptyMap()
|
||||||
|
|
||||||
messageService.onUserRegistrationWaitingForApproval(event)
|
messageService.onUserRegistrationWaitingForApproval(event)
|
||||||
|
|
||||||
@ -409,7 +410,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
every {
|
every {
|
||||||
@ -439,7 +440,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
every {
|
every {
|
||||||
@ -466,7 +467,7 @@ class MessageServiceTest {
|
|||||||
val user = User(username = "testuser", email = "user@example.com", password = "hash")
|
val user = User(username = "testuser", email = "user@example.com", password = "hash")
|
||||||
val event = AccountStatusChangedEvent(this, user, "http://example.com")
|
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)
|
messageService.onAccountStatusChanged(event)
|
||||||
|
|
||||||
@ -481,7 +482,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
every {
|
every {
|
||||||
@ -502,7 +503,7 @@ class MessageServiceTest {
|
|||||||
val user = User(username = "existinguser", email = "existing@example.com", password = "hash")
|
val user = User(username = "existinguser", email = "existing@example.com", password = "hash")
|
||||||
val event = RegistrationAttemptWithExistingEmailEvent(this, user, "http://example.com")
|
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)
|
messageService.onRegistrationAttemptWithExistingEmail(event)
|
||||||
|
|
||||||
@ -518,7 +519,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
every {
|
every {
|
||||||
@ -543,7 +544,7 @@ class MessageServiceTest {
|
|||||||
val token = Token(creator = user, secret = "confirm123", type = TokenType.EmailConfirmation)
|
val token = Token(creator = user, secret = "confirm123", type = TokenType.EmailConfirmation)
|
||||||
val event = EmailNeedsConfirmationEvent(this, token, "http://example.com")
|
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)
|
messageService.onEmailNeedsConfirmation(event)
|
||||||
|
|
||||||
@ -559,7 +560,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
every {
|
every {
|
||||||
@ -587,7 +588,7 @@ class MessageServiceTest {
|
|||||||
val token = Token(creator = user, secret = "invite123", type = TokenType.Invitation)
|
val token = Token(creator = user, secret = "invite123", type = TokenType.Invitation)
|
||||||
val event = UserInvitationEvent(this, token, "http://example.com", "invited@example.com")
|
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)
|
messageService.onUserInvitation(event)
|
||||||
|
|
||||||
@ -602,7 +603,7 @@ class MessageServiceTest {
|
|||||||
|
|
||||||
every { mockProvider1.enabled } returns true
|
every { mockProvider1.enabled } returns true
|
||||||
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
every { mockProvider1.supportedTemplateType } returns TemplateType.MJML
|
||||||
every { applicationContext.getBeansOfType(AbstractMessageProvider::class.java) } returns mapOf(
|
every { applicationContext.getBeansOfType<AbstractMessageProvider>() } returns mapOf(
|
||||||
"provider1" to mockProvider1
|
"provider1" to mockProvider1
|
||||||
)
|
)
|
||||||
every {
|
every {
|
||||||
@ -629,7 +630,7 @@ class MessageServiceTest {
|
|||||||
val user = User(username = "deleteduser", email = "deleted@example.com", password = "hash")
|
val user = User(username = "deleteduser", email = "deleted@example.com", password = "hash")
|
||||||
val event = UserDeletedEvent(this, user, "http://example.com")
|
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)
|
messageService.onAccountDeletion(event)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
package org.gameyfin.app.platforms.serialization
|
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.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.unmockkAll
|
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.AfterEach
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import tools.jackson.core.JsonParser
|
||||||
|
import tools.jackson.databind.DeserializationContext
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNull
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should return correct platform for valid displayName`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should return null for unknown displayName`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should return null for empty string`() {
|
fun `deserialize should return null for empty string`() {
|
||||||
every { jsonParser.text } returns ""
|
every { jsonParser.string } returns ""
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should return correct platform for PlayStation 5`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should return correct platform for Xbox Series X S`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should return correct platform for Nintendo Switch`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should be case-sensitive`() {
|
fun `deserialize should be case-sensitive`() {
|
||||||
every { jsonParser.text } returns "playstation 5"
|
every { jsonParser.string } returns "playstation 5"
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle platforms with special characters`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle platforms with numbers at start`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle platforms with hyphens`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle platforms with apostrophes`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should return null for whitespace-only string`() {
|
fun `deserialize should return null for whitespace-only string`() {
|
||||||
every { jsonParser.text } returns " "
|
every { jsonParser.string } returns " "
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should not trim whitespace from displayName`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle Arcade platform`() {
|
fun `deserialize should handle Arcade platform`() {
|
||||||
every { jsonParser.text } returns "Arcade"
|
every { jsonParser.string } returns "Arcade"
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -159,7 +159,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle Web browser platform`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle Android platform`() {
|
fun `deserialize should handle Android platform`() {
|
||||||
every { jsonParser.text } returns "Android"
|
every { jsonParser.string } returns "Android"
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle iOS platform`() {
|
fun `deserialize should handle iOS platform`() {
|
||||||
every { jsonParser.text } returns "iOS"
|
every { jsonParser.string } returns "iOS"
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -186,7 +186,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle Linux platform`() {
|
fun `deserialize should handle Linux platform`() {
|
||||||
every { jsonParser.text } returns "Linux"
|
every { jsonParser.string } returns "Linux"
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -195,7 +195,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle Mac platform`() {
|
fun `deserialize should handle Mac platform`() {
|
||||||
every { jsonParser.text } returns "Mac"
|
every { jsonParser.string } returns "Mac"
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -204,7 +204,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle DOS platform`() {
|
fun `deserialize should handle DOS platform`() {
|
||||||
every { jsonParser.text } returns "DOS"
|
every { jsonParser.string } returns "DOS"
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -213,7 +213,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle Dreamcast platform`() {
|
fun `deserialize should handle Dreamcast platform`() {
|
||||||
every { jsonParser.text } returns "Dreamcast"
|
every { jsonParser.string } returns "Dreamcast"
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -222,7 +222,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle Virtual Boy platform`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -231,7 +231,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle ZX Spectrum platform`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -240,7 +240,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle Game Boy platform`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -249,7 +249,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle PlayStation VR2 platform`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -258,7 +258,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle Nintendo Entertainment System platform`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -267,7 +267,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle Super Nintendo Entertainment System platform`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -276,7 +276,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle Sega Mega Drive Genesis platform`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -285,7 +285,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle platforms with long names`() {
|
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)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -294,7 +294,7 @@ class PlatformDeserializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deserialize should return null for partial match`() {
|
fun `deserialize should return null for partial match`() {
|
||||||
every { jsonParser.text } returns "PlayStation"
|
every { jsonParser.string } returns "PlayStation"
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
@ -304,7 +304,7 @@ class PlatformDeserializerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `deserialize should handle all valid platform displayNames correctly`() {
|
fun `deserialize should handle all valid platform displayNames correctly`() {
|
||||||
Platform.entries.forEach { platform ->
|
Platform.entries.forEach { platform ->
|
||||||
every { jsonParser.text } returns platform.displayName
|
every { jsonParser.string } returns platform.displayName
|
||||||
|
|
||||||
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
val result = deserializer.deserialize(jsonParser, deserializationContext)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
package org.gameyfin.app.platforms.serialization
|
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.mockk
|
||||||
import io.mockk.unmockkAll
|
import io.mockk.unmockkAll
|
||||||
import io.mockk.verify
|
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.AfterEach
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import tools.jackson.core.JsonGenerator
|
||||||
|
import tools.jackson.databind.SerializationContext
|
||||||
|
|
||||||
class PlatformSerializerTest {
|
class PlatformSerializerTest {
|
||||||
|
|
||||||
private lateinit var serializer: DisplayableSerializer
|
private lateinit var serializer: DisplayableSerializer
|
||||||
private lateinit var jsonGenerator: JsonGenerator
|
private lateinit var jsonGenerator: JsonGenerator
|
||||||
private lateinit var serializerProvider: SerializerProvider
|
private lateinit var serializationContext: SerializationContext
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
serializer = DisplayableSerializer()
|
serializer = DisplayableSerializer()
|
||||||
jsonGenerator = mockk(relaxed = true)
|
jsonGenerator = mockk(relaxed = true)
|
||||||
serializerProvider = mockk()
|
serializationContext = mockk()
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@ -33,14 +33,14 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should write displayName for valid platform`() {
|
fun `serialize should write displayName for valid platform`() {
|
||||||
val platform = Platform.PC_MICROSOFT_WINDOWS
|
val platform = Platform.PC_MICROSOFT_WINDOWS
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("PC (Microsoft Windows)") }
|
verify(exactly = 1) { jsonGenerator.writeString("PC (Microsoft Windows)") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `serialize should handle null platform value`() {
|
fun `serialize should handle null platform value`() {
|
||||||
serializer.serialize(null, jsonGenerator, serializerProvider)
|
serializer.serialize(null, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 0) { jsonGenerator.writeString(any<String>()) }
|
verify(exactly = 0) { jsonGenerator.writeString(any<String>()) }
|
||||||
}
|
}
|
||||||
@ -49,7 +49,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should write correct displayName for PlayStation 5`() {
|
fun `serialize should write correct displayName for PlayStation 5`() {
|
||||||
val platform = Platform.PLAYSTATION_5
|
val platform = Platform.PLAYSTATION_5
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("PlayStation 5") }
|
verify(exactly = 1) { jsonGenerator.writeString("PlayStation 5") }
|
||||||
}
|
}
|
||||||
@ -58,7 +58,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should write correct displayName for Xbox Series X S`() {
|
fun `serialize should write correct displayName for Xbox Series X S`() {
|
||||||
val platform = Platform.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") }
|
verify(exactly = 1) { jsonGenerator.writeString("Xbox Series X|S") }
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should write correct displayName for Nintendo Switch`() {
|
fun `serialize should write correct displayName for Nintendo Switch`() {
|
||||||
val platform = Platform.NINTENDO_SWITCH
|
val platform = Platform.NINTENDO_SWITCH
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("Nintendo Switch") }
|
verify(exactly = 1) { jsonGenerator.writeString("Nintendo Switch") }
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle platforms with special characters in name`() {
|
fun `serialize should handle platforms with special characters in name`() {
|
||||||
val platform = Platform.ODYSSEY_2_VIDEOPAC_G7000
|
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") }
|
verify(exactly = 1) { jsonGenerator.writeString("Odyssey 2 / Videopac G7000") }
|
||||||
}
|
}
|
||||||
@ -85,7 +85,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle platforms with numbers in name`() {
|
fun `serialize should handle platforms with numbers in name`() {
|
||||||
val platform = Platform._3DO_INTERACTIVE_MULTIPLAYER
|
val platform = Platform._3DO_INTERACTIVE_MULTIPLAYER
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("3DO Interactive Multiplayer") }
|
verify(exactly = 1) { jsonGenerator.writeString("3DO Interactive Multiplayer") }
|
||||||
}
|
}
|
||||||
@ -94,7 +94,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle platforms with hyphens in name`() {
|
fun `serialize should handle platforms with hyphens in name`() {
|
||||||
val platform = Platform.ATARI_8_BIT
|
val platform = Platform.ATARI_8_BIT
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("Atari 8-bit") }
|
verify(exactly = 1) { jsonGenerator.writeString("Atari 8-bit") }
|
||||||
}
|
}
|
||||||
@ -103,7 +103,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle platforms with apostrophes in name`() {
|
fun `serialize should handle platforms with apostrophes in name`() {
|
||||||
val platform = Platform.SUPER_ACAN
|
val platform = Platform.SUPER_ACAN
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("Super A'Can") }
|
verify(exactly = 1) { jsonGenerator.writeString("Super A'Can") }
|
||||||
}
|
}
|
||||||
@ -112,7 +112,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle arcade platform`() {
|
fun `serialize should handle arcade platform`() {
|
||||||
val platform = Platform.ARCADE
|
val platform = Platform.ARCADE
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("Arcade") }
|
verify(exactly = 1) { jsonGenerator.writeString("Arcade") }
|
||||||
}
|
}
|
||||||
@ -121,7 +121,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle web browser platform`() {
|
fun `serialize should handle web browser platform`() {
|
||||||
val platform = Platform.WEB_BROWSER
|
val platform = Platform.WEB_BROWSER
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("Web browser") }
|
verify(exactly = 1) { jsonGenerator.writeString("Web browser") }
|
||||||
}
|
}
|
||||||
@ -130,7 +130,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle Android platform`() {
|
fun `serialize should handle Android platform`() {
|
||||||
val platform = Platform.ANDROID
|
val platform = Platform.ANDROID
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("Android") }
|
verify(exactly = 1) { jsonGenerator.writeString("Android") }
|
||||||
}
|
}
|
||||||
@ -139,7 +139,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle iOS platform`() {
|
fun `serialize should handle iOS platform`() {
|
||||||
val platform = Platform.IOS
|
val platform = Platform.IOS
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("iOS") }
|
verify(exactly = 1) { jsonGenerator.writeString("iOS") }
|
||||||
}
|
}
|
||||||
@ -148,7 +148,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle Linux platform`() {
|
fun `serialize should handle Linux platform`() {
|
||||||
val platform = Platform.LINUX
|
val platform = Platform.LINUX
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("Linux") }
|
verify(exactly = 1) { jsonGenerator.writeString("Linux") }
|
||||||
}
|
}
|
||||||
@ -157,7 +157,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle Mac platform`() {
|
fun `serialize should handle Mac platform`() {
|
||||||
val platform = Platform.MAC
|
val platform = Platform.MAC
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("Mac") }
|
verify(exactly = 1) { jsonGenerator.writeString("Mac") }
|
||||||
}
|
}
|
||||||
@ -166,7 +166,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle DOS platform`() {
|
fun `serialize should handle DOS platform`() {
|
||||||
val platform = Platform.DOS
|
val platform = Platform.DOS
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("DOS") }
|
verify(exactly = 1) { jsonGenerator.writeString("DOS") }
|
||||||
}
|
}
|
||||||
@ -175,7 +175,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle Dreamcast platform`() {
|
fun `serialize should handle Dreamcast platform`() {
|
||||||
val platform = Platform.DREAMCAST
|
val platform = Platform.DREAMCAST
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("Dreamcast") }
|
verify(exactly = 1) { jsonGenerator.writeString("Dreamcast") }
|
||||||
}
|
}
|
||||||
@ -184,7 +184,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle Virtual Boy platform`() {
|
fun `serialize should handle Virtual Boy platform`() {
|
||||||
val platform = Platform.VIRTUAL_BOY
|
val platform = Platform.VIRTUAL_BOY
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("Virtual Boy") }
|
verify(exactly = 1) { jsonGenerator.writeString("Virtual Boy") }
|
||||||
}
|
}
|
||||||
@ -193,7 +193,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle ZX Spectrum platform`() {
|
fun `serialize should handle ZX Spectrum platform`() {
|
||||||
val platform = Platform.ZX_SPECTRUM
|
val platform = Platform.ZX_SPECTRUM
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("ZX Spectrum") }
|
verify(exactly = 1) { jsonGenerator.writeString("ZX Spectrum") }
|
||||||
}
|
}
|
||||||
@ -202,7 +202,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle Game Boy platform`() {
|
fun `serialize should handle Game Boy platform`() {
|
||||||
val platform = Platform.GAME_BOY
|
val platform = Platform.GAME_BOY
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("Game Boy") }
|
verify(exactly = 1) { jsonGenerator.writeString("Game Boy") }
|
||||||
}
|
}
|
||||||
@ -211,7 +211,7 @@ class PlatformSerializerTest {
|
|||||||
fun `serialize should handle PlayStation VR2 platform`() {
|
fun `serialize should handle PlayStation VR2 platform`() {
|
||||||
val platform = Platform.PLAYSTATION_VR2
|
val platform = Platform.PLAYSTATION_VR2
|
||||||
|
|
||||||
serializer.serialize(platform, jsonGenerator, serializerProvider)
|
serializer.serialize(platform, jsonGenerator, serializationContext)
|
||||||
|
|
||||||
verify(exactly = 1) { jsonGenerator.writeString("PlayStation VR2") }
|
verify(exactly = 1) { jsonGenerator.writeString("PlayStation VR2") }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class PasswordResetEndpointTest {
|
|||||||
fun setup() {
|
fun setup() {
|
||||||
passwordResetService = mockk()
|
passwordResetService = mockk()
|
||||||
userService = mockk()
|
userService = mockk()
|
||||||
passwordResetEndpoint = PasswordResetEndpoint(passwordResetService, userService)
|
passwordResetEndpoint = PasswordResetEndpoint(passwordResetService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"inlineSources": true,
|
"inlineSources": true,
|
||||||
"module": "esNext",
|
"module": "esNext",
|
||||||
"target": "es2022",
|
"target": "es2023",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": 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 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 vaadinI18n from './build/plugins/rollup-plugin-vaadin-i18n/rollup-plugin-vaadin-i18n.js';
|
||||||
import serviceWorkerPlugin from './build/plugins/vite-plugin-service-worker';
|
import serviceWorkerPlugin from './build/plugins/vite-plugin-service-worker';
|
||||||
|
import vaadinBundlesPlugin from './build/plugins/vite-plugin-vaadin-bundles';
|
||||||
import { createRequire } from 'module';
|
|
||||||
|
|
||||||
import { visualizer } from 'rollup-plugin-visualizer';
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
import reactPlugin from '@vitejs/plugin-react';
|
import reactPlugin from '@vitejs/plugin-react';
|
||||||
|
|
||||||
import vitePluginFileSystemRouter from '@vaadin/hilla-file-router/vite-plugin.js';
|
|
||||||
|
|
||||||
// Make `require` compatible with ES modules
|
import vitePluginFileSystemRouter from '@vaadin/hilla-file-router/vite-plugin.js';
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
|
|
||||||
const frontendFolder = path.resolve(__dirname, settings.frontendFolder);
|
const frontendFolder = path.resolve(__dirname, settings.frontendFolder);
|
||||||
const themeFolder = path.resolve(frontendFolder, settings.themeFolder);
|
const themeFolder = path.resolve(frontendFolder, settings.themeFolder);
|
||||||
@ -78,14 +75,16 @@ const themeOptions = {
|
|||||||
projectStaticAssetsOutputFolder: devBundle
|
projectStaticAssetsOutputFolder: devBundle
|
||||||
? path.resolve(devBundleFolder, '../assets')
|
? path.resolve(devBundleFolder, '../assets')
|
||||||
: path.resolve(__dirname, settings.staticOutput),
|
: 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 hasExportedWebComponents = existsSync(path.resolve(frontendFolder, 'web-component.html'));
|
||||||
const commercialBannerComponent = path.resolve(frontendFolder, settings.generatedFolder, 'commercial-banner.js');
|
const commercialBannerComponent = path.resolve(frontendFolder, settings.generatedFolder, 'commercial-banner.js');
|
||||||
const hasCommercialBanner = existsSync(commercialBannerComponent);
|
const hasCommercialBanner = existsSync(commercialBannerComponent);
|
||||||
|
|
||||||
const target = ['safari15', 'es2022'];
|
const target = ['es2023'];
|
||||||
|
|
||||||
// Block debug and trace logs.
|
// Block debug and trace logs.
|
||||||
console.trace = () => {};
|
console.trace = () => {};
|
||||||
@ -183,6 +182,10 @@ function statsExtracterPlugin(): PluginOption {
|
|||||||
path.resolve(themeOptions.frontendGeneratedFolder, 'flow', 'generated-flow-imports.js'),
|
path.resolve(themeOptions.frontendGeneratedFolder, 'flow', 'generated-flow-imports.js'),
|
||||||
generatedImportsSet
|
generatedImportsSet
|
||||||
);
|
);
|
||||||
|
parseImports(
|
||||||
|
path.resolve(themeOptions.frontendGeneratedFolder, 'app-shell-imports.js'),
|
||||||
|
generatedImportsSet
|
||||||
|
);
|
||||||
const generatedImports = Array.from(generatedImportsSet).sort();
|
const generatedImports = Array.from(generatedImportsSet).sort();
|
||||||
|
|
||||||
const frontendFiles: Record<string, string> = {};
|
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 {
|
function themePlugin(opts: { devMode: boolean }): PluginOption {
|
||||||
const fullThemeOptions = { ...themeOptions, devMode: opts.devMode };
|
const fullThemeOptions = { ...themeOptions, devMode: opts.devMode };
|
||||||
@ -563,9 +413,12 @@ function preserveUsageStats() {
|
|||||||
transform(src: string, id: string) {
|
transform(src: string, id: string) {
|
||||||
if (id.includes('vaadin-usage-statistics')) {
|
if (id.includes('vaadin-usage-statistics')) {
|
||||||
if (src.includes('vaadin-dev-mode:start')) {
|
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) {
|
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)) {
|
} else if (!newSrc.match(DEV_MODE_CODE_REGEXP)) {
|
||||||
console.error('New comment fails to match original regexp');
|
console.error('New comment fails to match original regexp');
|
||||||
} else {
|
} else {
|
||||||
@ -612,6 +465,9 @@ export const vaadinConfig: UserConfigFn = (env) => {
|
|||||||
allow: allowedFrontendFolders
|
allow: allowedFrontendFolders
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
esbuild: {
|
||||||
|
legalComments: 'inline',
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
minify: productionMode,
|
minify: productionMode,
|
||||||
outDir: buildOutputFolder,
|
outDir: buildOutputFolder,
|
||||||
@ -668,7 +524,9 @@ export const vaadinConfig: UserConfigFn = (env) => {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
productionMode && brotli(),
|
productionMode && brotli(),
|
||||||
devMode && vaadinBundlesPlugin(),
|
devMode && vaadinBundlesPlugin({
|
||||||
|
nodeModulesFolder
|
||||||
|
}),
|
||||||
devMode && showRecompileReason(),
|
devMode && showRecompileReason(),
|
||||||
settings.offlineEnabled && serviceWorkerPlugin({
|
settings.offlineEnabled && serviceWorkerPlugin({
|
||||||
srcPath: settings.clientServiceWorkerSource,
|
srcPath: settings.clientServiceWorkerSource,
|
||||||
@ -714,6 +572,7 @@ export const vaadinConfig: UserConfigFn = (env) => {
|
|||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
productionMode && vaadinI18n({
|
productionMode && vaadinI18n({
|
||||||
cwd: __dirname,
|
cwd: __dirname,
|
||||||
meta: {
|
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