Merge branch 'develop' into renovate/io.insert-koin-koin-bom-4.x

This commit is contained in:
usbharu 2024-10-02 10:42:16 +09:00 committed by GitHub
commit dfbb3b0f35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
194 changed files with 4928 additions and 683 deletions

View File

@ -1,16 +1,14 @@
name: PullRequest Merge Check name: pull-request-merge-check.yml
on: on:
pull_request: pull_request:
paths-ignore:
- 'owl/**'
branches: branches:
- "develop" - "develop"
types: types:
- opened # default - opened
- reopened # default - reopened
- synchronize # default - synchronize
- ready_for_review # 必要 - ready_for_review
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -22,10 +20,43 @@ permissions:
pull-requests: write pull-requests: write
jobs: jobs:
setup: change:
name: Setup
if: github.event.pull_request.draft == false if: github.event.pull_request.draft == false
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
core: ${{ steps.filter.outputs.core }}
mastodon: ${{ steps.filter.outputs.mastodon }}
activitypub: ${{ steps.filter.outputs.ap }}
owl: ${{ steps.filter.outputs.owl }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
- name: Check Changes
uses: dorny/paths-filter@v3
id: filter
with:
filters: |
core:
- 'hideout-core/**'
- 'libs.versions.toml'
ap:
- 'hideout-activitypub/**'
- 'libs.versions.toml'
mastodon:
- 'hideout-mastodon/**'
- 'libs.versions.toml'
owl:
- 'owl/**'
- 'libs.versions.toml'
hideout-core-setup:
needs:
- change
if: github.event.pull_request.draft == false && needs.change.outputs.core == 'true'
runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -48,11 +79,101 @@ jobs:
gradle-home-cache-cleanup: true gradle-home-cache-cleanup: true
- name: Build - name: Build
run: ./gradlew classes --no-daemon run: ./gradlew :hideout-core:classes --no-daemon
unit-test: hideout-mastodon-setup:
name: Unit Test needs:
needs: [ setup ] - change
if: github.event.pull_request.draft == false && needs.change.outputs.mastodon == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
- name: Gradle Wrapper Validation
uses: gradle/actions/wrapper-validation@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false
gradle-home-cache-cleanup: true
- name: Build
run: ./gradlew :hideout-mastodon:classes --no-daemon
hideout-activitypub-setup:
needs:
- change
if: github.event.pull_request.draft == false && needs.change.outputs.activitypub == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
- name: Gradle Wrapper Validation
uses: gradle/actions/wrapper-validation@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false
gradle-home-cache-cleanup: true
- name: Build
run: ./gradlew :hideout-activitypub:classes --no-daemon
owl-setup:
needs:
- change
if: github.event.pull_request.draft == false && needs.change.outputs.owl == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
- name: Gradle Wrapper Validation
uses: gradle/actions/wrapper-validation@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false
gradle-home-cache-cleanup: true
- name: Build
working-directory: owl
run: ./gradlew :classes --no-daemon
hideout-core-unit-test:
needs:
- hideout-core-setup
- change
if: github.event.pull_request.draft == false && needs.change.outputs.core == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -75,39 +196,30 @@ jobs:
- name: Unit Test - name: Unit Test
run: ./hideout-core/gradlew :hideout-core:koverXmlReport run: ./hideout-core/gradlew :hideout-core:koverXmlReport
- name: Add coverage report to PR
if: always()
id: kover
uses: madrapps/jacoco-report@v1.7.0
with:
paths: |
${{ github.workspace }}/hideout-core/build/reports/kover/report.xml
token: ${{ secrets.GITHUB_TOKEN }}
title: Code Coverage
update-comment: true
min-coverage-overall: 50
min-coverage-changed-files: 80
- name: JUnit Test Report - name: JUnit Test Report
uses: mikepenz/action-junit-report@v4 uses: mikepenz/action-junit-report@v4
with: with:
report_paths: '**/TEST-*.xml' report_paths: '**/TEST-*.xml'
check_name: 'hideout-core JUnit Test Report'
- name: Verify Coverage - name: Upload Coverage Report
run: ./hideout-core/gradlew :hideout-core:koverVerify uses: actions/upload-artifact@v4
with:
name: 'hideout-core.xml'
path: 'hideout-core/build/reports/kover/hideout-core.xml'
lint:
name: Lint hideout-mastodon-unit-test:
needs: [ setup ] needs:
- hideout-mastodon-setup
- change
if: github.event.pull_request.draft == false && needs.change.outputs.mastodon == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ github.head_ref }} token: ${{ secrets.PAT }}
token: '${{ secrets.PAT }}'
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
@ -121,8 +233,166 @@ jobs:
cache-read-only: false cache-read-only: false
gradle-home-cache-cleanup: true gradle-home-cache-cleanup: true
- name: Build with Gradle - name: Unit Test
run: ./gradlew :hideout-core:detektMain :hideout-mastodon:detektMain run: ./hideout-mastodon/gradlew :hideout-mastodon:koverXmlReport
- name: JUnit Test Report
uses: mikepenz/action-junit-report@v4
with:
report_paths: '**/TEST-*.xml'
check_name: 'hideout-mastodon JUnit Test Report'
- name: Upload Coverage Report
uses: actions/upload-artifact@v4
with:
name: 'hideout-mastodon.xml'
path: 'hideout-mastodon/build/reports/kover/hideout-mastodon.xml'
hideout-activitypub-unit-test:
needs:
- hideout-activitypub-setup
- change
if: github.event.pull_request.draft == false && needs.change.outputs.activitypub == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false
gradle-home-cache-cleanup: true
- name: Unit Test
run: ./hideout-activitypub/gradlew :hideout-activitypub:koverXmlReport
- name: JUnit Test Report
uses: mikepenz/action-junit-report@v4
with:
report_paths: '**/TEST-*.xml'
check_name: 'hideout-activitypub JUnit Test Report'
- name: Upload Coverage Report
uses: actions/upload-artifact@v4
with:
name: 'hideout-activitypub.xml'
path: 'hideout-activitypub/build/reports/kover/hideout-activitypub.xml'
owl-unit-test:
needs:
- owl-setup
- change
if: github.event.pull_request.draft == false && needs.change.outputs.owl == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }}
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false
gradle-home-cache-cleanup: true
- name: Unit Test
working-directory: owl
run: ./gradlew :koverXmlReport --rerun-tasks
- name: JUnit Test Report
uses: mikepenz/action-junit-report@v4
with:
report_paths: '**/TEST-*.xml'
check_name: 'owl JUnit Test Report'
- name: Upload Coverage Report
uses: actions/upload-artifact@v4
with:
name: 'owl.xml'
path: 'owl/build/reports/kover/owl.xml'
coverage:
if: always() && (needs.change.outputs.core == 'true' || needs.change.outputs.activitypub == 'true' || needs.change.outputs.mastodon == 'true' || needs.change.outputs.owl == 'true')
needs:
- change
- hideout-core-unit-test
- hideout-mastodon-unit-test
- hideout-activitypub-unit-test
- owl-unit-test
runs-on: ubuntu-latest
steps:
- name: Download Coverage Report
uses: actions/download-artifact@v4
with:
path: 'hideout-core/build/reports/kover'
- name: Report Coverage
uses: madrapps/jacoco-report@v1.7.1
with:
paths: |
${{ github.workspace }}/hideout-core/build/reports/kover/hideout-core.xml/hideout-core.xml,
${{ github.workspace }}/hideout-core/build/reports/kover/hideout-mastodon.xml/hideout-mastodon.xml,
${{ github.workspace }}/hideout-core/build/reports/kover/hideout-activitypub.xml/hideout-activitypub.xml
${{ github.workspace }}/hideout-core/build/reports/kover/owl.xml/owl.xml
token: ${{ secrets.GITHUB_TOKEN }}
title: Code Coverage
update-comment: true
min-coverage-overall: 50
min-coverage-changed-files: 80
lint:
if: always() && (needs.change.outputs.core == 'true' || needs.change.outputs.activitypub == 'true' || needs.change.outputs.mastodon == 'true' || needs.change.outputs.owl == 'true')
needs:
- change
- hideout-core-setup
- hideout-mastodon-setup
- hideout-activitypub-setup
- owl-setup
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.PAT }}
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: false
gradle-home-cache-cleanup: true
- name: Lint
run: ./gradlew :hideout-core:detektMain :hideout-mastodon:detektMain :hideout-activitypub:detektMain
- name: owl Lint
if: always()
working-directory: owl
run: ./gradlew :detektMain
- name: Auto Commit - name: Auto Commit
if: ${{ always() }} if: ${{ always() }}

2
.gitignore vendored
View File

@ -50,3 +50,5 @@ out/
/hideout-mastodon/.kotlin/sessions/ /hideout-mastodon/.kotlin/sessions/
/http-client.private.env.json /http-client.private.env.json
/logs/ /logs/
/hideout-mastodon/logs/
/hideout-mastodon/files/

View File

@ -1,75 +1,3 @@
# Hideout # Hideout
HideoutはMastodon互換APIを備えたSNSで、ActivityPubに対応し、KotlinとSpring Frameworkを使用して制作されているソフトウェアです。現在は開発中で、SNSとして主要な機能を備えていますが、セキュリティの問題や不安定なテーブル定義などがあるためホストすることはおすすめしません。 ActivityPubで繋がれるSNSを目指している
## 特徴
### ActivityPubでつながるネットワーク
ActivityPubを実装しているためMastodonやMisskey、Pleromaとつながることができます。また、ActivityPub以外の分散型の通信プロトコルを実装することがあるかもしれません。
### Mastodon互換API
OAuth2プロバイダーを含めたほとんどのAPIがMastodonとの互換性を持っているため、既存のMastodon クライアントを使用することができます!また、今後Fedibirdやglitch-soc互換のAPIを実装するかもしれません。
## セルフホスト
> [!CAUTION]
> **免責事項**
>
> 本ソフトウェアを利用して発生したすべての事象に対して開発者は責任を負いません。
> 本ソフトウェアはApache License 2.0を採用しています。
現時点でセルフホストはおすすめしませんが、実験用としてホストすることはできます!
### 使用技術
- **Kotlin** 強力な言語機能でアプリケーションの安全性を高めます。
- **Spring Framework** (Spring Boot/Spring Security/Spring Web/Spring Data) 豊富な機能と堅牢なライブラリでソフトウェアの基幹部分を担います。
- **OpenAPI** スキーマファーストのエンドポイント自動生成はAPIの安定性を高めます。
### 要件
#### 起動
- Java 21
- PostgreSQL 12+
- MongoDB(必須でない) 4.4.x+
#### ビルド
- Java 21
- Gradle 8+
実験用途として、PostgreSQLはH2DB(バンドル)のPostgreSQL互換モードでも問題ありません。
Java 17でもビルド/起動ができますが、サポートしません。
MongoDBを使用しない場合、構成で`hideout.use-mongodb`をfalseにする必要があります。
### ビルド
今後のリリースでビルド済みjarなどを使う場合はスキップしてください。
```bash
gradle bootJar
```
`build/libs/hideout-x.x.x.jar`が生成されます。
### 起動
適切に設定した`application-dev.yml`などをクラスパス上に準備します。
`dev`は`prod`などに置き換えたり、[複数指定すること](https://spring.pleiades.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.core.spring.profiles.active)もできます。
```bash
java -jar build/libs/hideout-x.x.x.jar --spring.profiles.active=dev
```
https://spring.pleiades.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started.first-application.executable-jar.gradle
### 注意事項
本ソフトウェアは開発中です。正常に機能しない場合があります。また、連合先に迷惑をかける事になる可能性があります。DB/設定ファイル/その他リソースなどの利用方法に破壊的な変更が入る可能性があります。

View File

@ -52,6 +52,7 @@ repositories {
dependencies { dependencies {
implementation("dev.usbharu:hideout-core:0.0.1") implementation("dev.usbharu:hideout-core:0.0.1")
implementation("dev.usbharu:hideout-mastodon:1.0-SNAPSHOT") implementation("dev.usbharu:hideout-mastodon:1.0-SNAPSHOT")
implementation("dev.usbharu:hideout-activitypub:1.0-SNAPSHOT")
} }
tasks.register("run") { tasks.register("run") {

View File

@ -1,5 +1,9 @@
import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
plugins { plugins {
kotlin("jvm") version "1.9.25" alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.detekt)
alias(libs.plugins.kover)
} }
group = "dev.usbharu" group = "dev.usbharu"
@ -11,6 +15,7 @@ repositories {
dependencies { dependencies {
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
detektPlugins(libs.detekt.formatting)
} }
tasks.test { tasks.test {
@ -18,4 +23,93 @@ tasks.test {
} }
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(21)
} }
configurations {
matching { it.name == "detekt" }.all {
resolutionStrategy.eachDependency {
if (requested.group == "org.jetbrains.kotlin") {
useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion())
}
}
}
all {
exclude("org.apache.logging.log4j", "log4j-slf4j2-impl")
}
}
tasks {
withType<io.gitlab.arturbosch.detekt.Detekt> {
exclude("**/generated/**")
setSource("src/main/kotlin")
exclude("build/")
configureEach {
exclude("**/org/koin/ksp/generated/**", "**/generated/**")
}
}
withType<io.gitlab.arturbosch.detekt.DetektCreateBaselineTask>() {
configureEach {
exclude("**/org/koin/ksp/generated/**", "**/generated/**")
}
}
withType<Test> {
useJUnitPlatform()
}
}
project.gradle.taskGraph.whenReady {
if (this.hasTask(":koverGenerateArtifact")) {
val task = this.allTasks.find { it.name == "test" }
val verificationTask = task as VerificationTask
verificationTask.ignoreFailures = true
}
}
detekt {
parallel = true
config.setFrom(files("../detekt.yml"))
buildUponDefaultConfig = true
basePath = "${rootDir.absolutePath}/src/main/kotlin"
autoCorrect = true
}
kover {
currentProject {
sources {
}
}
reports {
verify {
rule {
bound {
minValue = 50
coverageUnits = CoverageUnit.INSTRUCTION
}
}
}
total {
xml {
title = "Hideout ActivityPub"
xmlFile = file("$buildDir/reports/kover/hideout-activitypub.xml")
}
}
filters {
excludes {
annotatedBy("org.springframework.context.annotation.Configuration")
annotatedBy("org.springframework.boot.context.properties.ConfigurationProperties")
packages(
"dev.usbharu.hideout.controller.mastodon.generated",
"dev.usbharu.hideout.domain.mastodon.model.generated"
)
packages("org.springframework")
packages("org.jetbrains")
}
}
}
}

0
hideout-activitypub/gradlew vendored Normal file → Executable file
View File

View File

@ -3,3 +3,14 @@ plugins {
} }
rootProject.name = "hideout-activitypub" rootProject.name = "hideout-activitypub"
dependencyResolutionManagement {
repositories {
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../libs.versions.toml"))
}
}
}

View File

@ -2,4 +2,4 @@ package dev.usbharu
fun main() { fun main() {
println("Hello World!") println("Hello World!")
} }

View File

@ -211,12 +211,18 @@ kover {
reports { reports {
verify { verify {
rule { rule {
bound{ bound {
minValue = 50 minValue = 50
coverageUnits = CoverageUnit.INSTRUCTION coverageUnits = CoverageUnit.INSTRUCTION
} }
} }
} }
total {
xml {
title = "Hideout Core"
xmlFile = file("$buildDir/reports/kover/hideout-core.xml")
}
}
filters { filters {
excludes { excludes {
annotatedBy("org.springframework.context.annotation.Configuration") annotatedBy("org.springframework.context.annotation.Configuration")
@ -229,6 +235,7 @@ kover {
packages("org.jetbrains") packages("org.jetbrains")
} }
} }
} }
} }

View File

@ -24,6 +24,7 @@ import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.media.MediaRepository import dev.usbharu.hideout.core.domain.model.media.MediaRepository
import dev.usbharu.hideout.core.domain.model.support.acct.Acct import dev.usbharu.hideout.core.domain.model.support.acct.Acct
import dev.usbharu.hideout.core.domain.model.support.domain.apHost
import dev.usbharu.hideout.core.domain.model.support.principal.Principal import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@ -45,7 +46,7 @@ class GetActorDetailApplicationService(
?: throw IllegalArgumentException("Actor ${command.id} not found.") ?: throw IllegalArgumentException("Actor ${command.id} not found.")
} else if (command.actorName != null) { } else if (command.actorName != null) {
val host = if (command.actorName.host.isEmpty()) { val host = if (command.actorName.host.isEmpty()) {
applicationConfig.url.host applicationConfig.url.apHost
} else { } else {
command.actorName.host command.actorName.host
} }

View File

@ -51,7 +51,7 @@ class RegisterLocalActorApplicationService(
if (actorDomainService.usernameAlreadyUse(command.name)) { if (actorDomainService.usernameAlreadyUse(command.name)) {
throw IllegalArgumentException("Username already exists") throw IllegalArgumentException("Username already exists")
} }
val instance = instanceRepository.findByUrl(applicationConfig.url.toURI()) val instance = instanceRepository.findByUrl(applicationConfig.url)
?: throw InternalServerException("Local instance not found.") ?: throw InternalServerException("Local instance not found.")
val actor = actorFactoryImpl.createLocal( val actor = actorFactoryImpl.createLocal(

View File

@ -16,7 +16,10 @@
package dev.usbharu.hideout.core.application.exception package dev.usbharu.hideout.core.application.exception
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
class PermissionDeniedException : RuntimeException { class PermissionDeniedException : RuntimeException {
constructor(principal: Principal) : super("Permission Denied $principal")
constructor() : super() constructor() : super()
constructor(message: String?) : super(message) constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause) constructor(message: String?, cause: Throwable?) : super(message, cause)

View File

@ -34,7 +34,8 @@ class UserGetFilterApplicationService(private val filterRepository: FilterReposi
) { ) {
override suspend fun internalExecute(command: GetFilter, principal: LocalUser): Filter { override suspend fun internalExecute(command: GetFilter, principal: LocalUser): Filter {
val filter = val filter =
filterRepository.findByFilterId(FilterId(command.filterId)) ?: throw IllegalArgumentException("Not Found") filterRepository.findByFilterId(FilterId(command.filterId))
?: throw IllegalArgumentException("Filter ${command.filterId} not found.")
if (filter.userDetailId != principal.userDetailId) { if (filter.userDetailId != principal.userDetailId) {
throw PermissionDeniedException() throw PermissionDeniedException()
} }

View File

@ -46,7 +46,7 @@ class GetLocalInstanceApplicationService(
} }
val instance = ( val instance = (
instanceRepository.findByUrl(applicationConfig.url.toURI()) instanceRepository.findByUrl(applicationConfig.url)
?: throw InternalServerException("Local instance not found.") ?: throw InternalServerException("Local instance not found.")
) )

View File

@ -19,6 +19,7 @@ package dev.usbharu.hideout.core.application.instance
import dev.usbharu.hideout.core.application.shared.Transaction import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.config.ApplicationConfig import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.instance.* import dev.usbharu.hideout.core.domain.model.instance.*
import dev.usbharu.hideout.core.domain.model.support.domain.apHost
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.boot.info.BuildProperties import org.springframework.boot.info.BuildProperties
@ -36,15 +37,15 @@ class InitLocalInstanceApplicationService(
) { ) {
@EventListener(ApplicationReadyEvent::class) @EventListener(ApplicationReadyEvent::class)
suspend fun init() = transaction.transaction { suspend fun init() = transaction.transaction {
val findByUrl = instanceRepository.findByUrl(applicationConfig.url.toURI()) val findByUrl = instanceRepository.findByUrl(applicationConfig.url)
if (findByUrl == null) { if (findByUrl == null) {
val instance = Instance( val instance = Instance(
id = InstanceId(idGenerateService.generateId()), id = InstanceId(idGenerateService.generateId()),
name = InstanceName(applicationConfig.url.host), name = InstanceName(applicationConfig.url.apHost),
description = InstanceDescription(""), description = InstanceDescription(""),
url = applicationConfig.url.toURI(), url = applicationConfig.url,
iconUrl = applicationConfig.url.toURI(), iconUrl = applicationConfig.url,
sharedInbox = null, sharedInbox = null,
software = InstanceSoftware("hideout"), software = InstanceSoftware("hideout"),
version = InstanceVersion(buildProperties.version), version = InstanceVersion(buildProperties.version),

View File

@ -18,6 +18,7 @@ package dev.usbharu.hideout.core.application.model
import dev.usbharu.hideout.core.domain.model.actor.Actor import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.media.Media import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.support.domain.apHost
import java.net.URI import java.net.URI
data class ActorDetail( data class ActorDetail(
@ -41,7 +42,7 @@ data class ActorDetail(
id = actor.id.id, id = actor.id.id,
name = actor.name.name, name = actor.name.name,
screenName = actor.screenName.screenName, screenName = actor.screenName.screenName,
host = actor.url.host, host = actor.url.apHost,
instanceId = actor.instance.instanceId, instanceId = actor.instance.instanceId,
remoteUrl = actor.url, remoteUrl = actor.url,
locked = actor.locked, locked = actor.locked,

View File

@ -24,6 +24,7 @@ import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.relationship.Relationship import dev.usbharu.hideout.core.domain.model.relationship.Relationship
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser
import dev.usbharu.hideout.core.domain.service.relationship.RelationshipDomainService
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@ -32,6 +33,7 @@ class UserFollowRequestApplicationService(
private val relationshipRepository: RelationshipRepository, private val relationshipRepository: RelationshipRepository,
transaction: Transaction, transaction: Transaction,
private val actorRepository: ActorRepository, private val actorRepository: ActorRepository,
private val relationshipDomainService: RelationshipDomainService
) : LocalUserAbstractApplicationService<FollowRequest, Unit>( ) : LocalUserAbstractApplicationService<FollowRequest, Unit>(
transaction, transaction,
logger logger
@ -43,11 +45,15 @@ class UserFollowRequestApplicationService(
val targetId = ActorId(command.targetActorId) val targetId = ActorId(command.targetActorId)
val relationship = relationshipRepository.findByActorIdAndTargetId(actor.id, targetId) ?: Relationship.default( val relationship = relationshipRepository.findByActorIdAndTargetId(actor.id, targetId) ?: Relationship.default(
actor.id, actor.id, targetId
targetId
) )
relationship.followRequest() val inverseRelationship =
relationshipRepository.findByActorIdAndTargetId(targetId, actor.id) ?: Relationship.default(
targetId, actor.id
)
relationshipDomainService.followRequest(relationship, inverseRelationship)
relationshipRepository.save(relationship) relationshipRepository.save(relationship)
} }

View File

@ -17,11 +17,11 @@
package dev.usbharu.hideout.core.config package dev.usbharu.hideout.core.config
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import java.net.URL import java.net.URI
@ConfigurationProperties("hideout") @ConfigurationProperties("hideout")
data class ApplicationConfig( data class ApplicationConfig(
val url: URL, val url: URI,
val private: Boolean = true, val private: Boolean = true,
val keySize: Int = 2048, val keySize: Int = 2048,
) )

View File

@ -82,6 +82,16 @@ class Filter(
} }
override fun hashCode(): Int = id.hashCode() override fun hashCode(): Int = id.hashCode()
override fun toString(): String {
return "Filter(" +
"id=$id, " +
"userDetailId=$userDetailId, " +
"name=$name, " +
"filterContext=$filterContext, " +
"filterAction=$filterAction, " +
"filterKeywords=$filterKeywords" +
")"
}
companion object { companion object {
fun isAllow(user: UserDetail, action: Action, resource: Filter): Boolean { fun isAllow(user: UserDetail, action: Action, resource: Filter): Boolean {

View File

@ -30,7 +30,13 @@ class FilterKeyword(
return id == other.id return id == other.id
} }
override fun hashCode(): Int { override fun hashCode(): Int = id.hashCode()
return id.hashCode()
override fun toString(): String {
return "FilterKeyword(" +
"id=$id, " +
"keyword=$keyword, " +
"mode=$mode" +
")"
} }
} }

View File

@ -29,8 +29,12 @@ class FilterName(name: String) {
return name == other.name return name == other.name
} }
override fun hashCode(): Int { override fun hashCode(): Int = name.hashCode()
return name.hashCode()
override fun toString(): String {
return "FilterName(" +
"name='$name'" +
")"
} }
companion object { companion object {

View File

@ -16,4 +16,29 @@
package dev.usbharu.hideout.core.domain.model.filter package dev.usbharu.hideout.core.domain.model.filter
class FilterResult(val filter: Filter, val matchedKeyword: String) class FilterResult(val filter: Filter, val matchedKeyword: String) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FilterResult
if (filter != other.filter) return false
if (matchedKeyword != other.matchedKeyword) return false
return true
}
override fun hashCode(): Int {
var result = filter.hashCode()
result = 31 * result + matchedKeyword.hashCode()
return result
}
override fun toString(): String {
return "FilterResult(" +
"filter=$filter, " +
"matchedKeyword='$matchedKeyword'" +
")"
}
}

View File

@ -18,4 +18,29 @@ package dev.usbharu.hideout.core.domain.model.filter
import dev.usbharu.hideout.core.domain.model.post.Post import dev.usbharu.hideout.core.domain.model.post.Post
class FilteredPost(val post: Post, val filterResults: List<FilterResult>) class FilteredPost(val post: Post, val filterResults: List<FilterResult>) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FilteredPost
if (post != other.post) return false
if (filterResults != other.filterResults) return false
return true
}
override fun hashCode(): Int {
var result = post.hashCode()
result = 31 * result + filterResults.hashCode()
return result
}
override fun toString(): String {
return "FilteredPost(" +
"post=$post, " +
"filterResults=$filterResults" +
")"
}
}

View File

@ -52,8 +52,8 @@ class Relationship(
addDomainEvent(relationshipEventFactory.createEvent(RelationshipEvent.UNFOLLOW_REQUEST)) addDomainEvent(relationshipEventFactory.createEvent(RelationshipEvent.UNFOLLOW_REQUEST))
} }
fun block() { private fun block() {
require(following.not()) unfollow()
blocking = true blocking = true
addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.BLOCK)) addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.BLOCK))
} }
@ -81,7 +81,7 @@ class Relationship(
mutingFollowRequest = false mutingFollowRequest = false
} }
fun followRequest() { private fun followRequest() {
require(blocking.not()) require(blocking.not())
followRequesting = true followRequesting = true
addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.FOLLOW_REQUEST)) addDomainEvent(RelationshipEventFactory(this).createEvent(RelationshipEvent.FOLLOW_REQUEST))
@ -123,6 +123,17 @@ class Relationship(
return result return result
} }
@Suppress("UnnecessaryAbstractClass")
abstract class InternalRelationshipDomainService {
protected fun block(relationship: Relationship) {
relationship.block()
}
protected fun followRequest(relationship: Relationship) {
relationship.followRequest()
}
}
companion object { companion object {
fun default(actorId: ActorId, targetActorId: ActorId): Relationship = Relationship( fun default(actorId: ActorId, targetActorId: ActorId): Relationship = Relationship(
actorId = actorId, actorId = actorId,

View File

@ -16,6 +16,8 @@
package dev.usbharu.hideout.core.domain.model.support.domain package dev.usbharu.hideout.core.domain.model.support.domain
import java.net.URI
@JvmInline @JvmInline
value class Domain(val domain: String) { value class Domain(val domain: String) {
init { init {
@ -23,6 +25,15 @@ value class Domain(val domain: String) {
} }
companion object { companion object {
const val LENGTH = 1000 const val LENGTH: Int = 1000
fun of(uri: URI): Domain = Domain(uri.apHost)
} }
} }
val URI.apHost: String
get() = if (port == -1) {
host
} else {
"$host:$port"
}

View File

@ -18,6 +18,7 @@ package dev.usbharu.hideout.core.domain.service.actor
import dev.usbharu.hideout.core.config.ApplicationConfig import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.actor.Actor import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.support.domain.apHost
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
interface IRemoteActorCheckDomainService { interface IRemoteActorCheckDomainService {
@ -26,5 +27,5 @@ interface IRemoteActorCheckDomainService {
@Service @Service
class RemoteActorCheckDomainService(private val applicationConfig: ApplicationConfig) : IRemoteActorCheckDomainService { class RemoteActorCheckDomainService(private val applicationConfig: ApplicationConfig) : IRemoteActorCheckDomainService {
override fun isRemoteActor(actor: Actor): Boolean = actor.domain.domain != applicationConfig.url.host override fun isRemoteActor(actor: Actor): Boolean = actor.domain.domain != applicationConfig.url.apHost
} }

View File

@ -20,6 +20,7 @@ import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.actor.ActorPrivateKey import dev.usbharu.hideout.core.domain.model.actor.ActorPrivateKey
import dev.usbharu.hideout.core.domain.model.actor.ActorPublicKey import dev.usbharu.hideout.core.domain.model.actor.ActorPublicKey
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.support.domain.apHost
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
@ -29,7 +30,7 @@ class LocalActorDomainServiceImpl(
private val applicationConfig: ApplicationConfig, private val applicationConfig: ApplicationConfig,
) : LocalActorDomainService { ) : LocalActorDomainService {
override suspend fun usernameAlreadyUse(name: String): Boolean = override suspend fun usernameAlreadyUse(name: String): Boolean =
actorRepository.findByNameAndDomain(name, applicationConfig.url.host) != null actorRepository.findByNameAndDomain(name, applicationConfig.url.apHost) != null
override suspend fun generateKeyPair(): Pair<ActorPublicKey, ActorPrivateKey> { override suspend fun generateKeyPair(): Pair<ActorPublicKey, ActorPrivateKey> {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA") val keyPairGenerator = KeyPairGenerator.getInstance("RSA")

View File

@ -77,12 +77,12 @@ class DefaultPostReadAccessControl(private val relationshipRepository: Relations
override suspend fun areAllows(postList: List<Post>, principal: Principal): List<Post> { override suspend fun areAllows(postList: List<Post>, principal: Principal): List<Post> {
val actorIds = postList.map { it.actorId } val actorIds = postList.map { it.actorId }
val relationshipList = val blockedByList =
relationshipRepository.findByActorIdsAndTargetIdAndBlocking(actorIds, principal.actorId, true) relationshipRepository.findByActorIdsAndTargetIdAndBlocking(actorIds, principal.actorId, true)
.map { it.actorId } .map { it.actorId }
val inverseRelationshipList = val followingList =
relationshipRepository.findByActorIdAndTargetIdsAndFollowing(principal.actorId, actorIds, true) relationshipRepository.findByActorIdAndTargetIdsAndFollowing(principal.actorId, actorIds, true)
.map { it.actorId } .map { it.targetActorId }
fun internalAllow(post: Post): Boolean { fun internalAllow(post: Post): Boolean {
// ポスト主は無条件で見れる // ポスト主は無条件で見れる
@ -90,7 +90,7 @@ class DefaultPostReadAccessControl(private val relationshipRepository: Relations
return true return true
} }
if (relationshipList.contains(post.actorId)) { if (blockedByList.contains(post.actorId)) {
return false return false
} }
@ -106,7 +106,7 @@ class DefaultPostReadAccessControl(private val relationshipRepository: Relations
return true return true
} }
if (post.visibility == Visibility.FOLLOWERS && inverseRelationshipList.contains(principal.actorId)) { if (post.visibility == Visibility.FOLLOWERS && followingList.contains(post.actorId)) {
return true return true
} }
return false return false

View File

@ -17,17 +17,28 @@
package dev.usbharu.hideout.core.domain.service.relationship package dev.usbharu.hideout.core.domain.service.relationship
import dev.usbharu.hideout.core.domain.model.relationship.Relationship import dev.usbharu.hideout.core.domain.model.relationship.Relationship
import dev.usbharu.hideout.core.domain.model.relationship.Relationship.InternalRelationshipDomainService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
class RelationshipDomainService { class RelationshipDomainService : InternalRelationshipDomainService() {
fun block(relationship: Relationship, inverseRelationship: Relationship) { fun block(relationship: Relationship, inverseRelationship: Relationship) {
require(relationship != inverseRelationship) require(relationship != inverseRelationship)
require(relationship.actorId == inverseRelationship.targetActorId) require(relationship.actorId == inverseRelationship.targetActorId)
require(relationship.targetActorId == inverseRelationship.actorId) require(relationship.targetActorId == inverseRelationship.actorId)
relationship.block() block(relationship)
inverseRelationship.unfollow() inverseRelationship.unfollow()
inverseRelationship.unfollowRequest() inverseRelationship.unfollowRequest()
} }
fun followRequest(relationship: Relationship, inverseRelationship: Relationship) {
require(relationship != inverseRelationship)
require(relationship.actorId == inverseRelationship.targetActorId)
require(relationship.targetActorId == inverseRelationship.actorId)
require(inverseRelationship.blocking.not())
require(relationship.blocking.not())
followRequest(relationship)
}
} }

View File

@ -27,7 +27,7 @@ import org.springframework.beans.factory.annotation.Value
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
import java.sql.SQLException import java.sql.SQLException
@Suppress("VarCouldBeVal") @Suppress("VarCouldBeVal", "UnnecessaryAbstractClass")
abstract class AbstractRepository(protected val logger: Logger) { abstract class AbstractRepository(protected val logger: Logger) {
private val sqlErrorCodeSQLExceptionTranslator = SQLErrorCodeSQLExceptionTranslator() private val sqlErrorCodeSQLExceptionTranslator = SQLErrorCodeSQLExceptionTranslator()
private val springDataAccessExceptionSQLExceptionTranslator = SpringDataAccessExceptionSQLExceptionTranslator() private val springDataAccessExceptionSQLExceptionTranslator = SpringDataAccessExceptionSQLExceptionTranslator()

View File

@ -20,6 +20,7 @@ import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.actor.* import dev.usbharu.hideout.core.domain.model.actor.*
import dev.usbharu.hideout.core.domain.model.instance.InstanceId import dev.usbharu.hideout.core.domain.model.instance.InstanceId
import dev.usbharu.hideout.core.domain.model.support.domain.Domain import dev.usbharu.hideout.core.domain.model.support.domain.Domain
import dev.usbharu.hideout.core.domain.model.support.domain.apHost
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.net.URI import java.net.URI
@ -40,7 +41,7 @@ class ActorFactoryImpl(
return Actor( return Actor(
id = ActorId(idGenerateService.generateId()), id = ActorId(idGenerateService.generateId()),
name = actorName, name = actorName,
domain = Domain(applicationConfig.url.host), domain = Domain(applicationConfig.url.apHost),
screenName = ActorScreenName(name), screenName = ActorScreenName(name),
description = ActorDescription(""), description = ActorDescription(""),
inbox = URI.create("$userUrl/inbox"), inbox = URI.create("$userUrl/inbox"),

View File

@ -19,6 +19,7 @@ package dev.usbharu.hideout.core.infrastructure.springframework.oauth2
import dev.usbharu.hideout.core.application.shared.Transaction import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.config.ApplicationConfig import dev.usbharu.hideout.core.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.support.domain.apHost
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.slf4j.MDCContext import kotlinx.coroutines.slf4j.MDCContext
@ -39,7 +40,7 @@ class UserDetailsServiceImpl(
throw UsernameNotFoundException("Username not found") throw UsernameNotFoundException("Username not found")
} }
transaction.transaction { transaction.transaction {
val actor = actorRepository.findByNameAndDomain(username, applicationConfig.url.host) val actor = actorRepository.findByNameAndDomain(username, applicationConfig.url.apHost)
?: throw UsernameNotFoundException("$username not found") ?: throw UsernameNotFoundException("$username not found")
val userDetail = userDetailRepository.findByActorId(actor.id.id) val userDetail = userDetailRepository.findByActorId(actor.id.id)
?: throw UsernameNotFoundException("${actor.id.id} not found") ?: throw UsernameNotFoundException("${actor.id.id} not found")

View File

@ -22,7 +22,7 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq import org.mockito.kotlin.eq
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import utils.TestTransaction import utils.TestTransaction
import java.net.URL import java.net.URI
@ExtendWith(MockitoExtension::class) @ExtendWith(MockitoExtension::class)
class RegisterLocalActorApplicationServiceTest { class RegisterLocalActorApplicationServiceTest {
@ -51,7 +51,7 @@ class RegisterLocalActorApplicationServiceTest {
val transaction = TestTransaction val transaction = TestTransaction
@Spy @Spy
val applicationConfig = ApplicationConfig(URL("http://example.com")) val applicationConfig = ApplicationConfig(URI.create("http://example.com"))
@Spy @Spy
val idGenerateService = TwitterSnowflakeIdGenerateService val idGenerateService = TwitterSnowflakeIdGenerateService

View File

@ -0,0 +1,152 @@
package dev.usbharu.hideout.core.domain.model.relationship
import dev.usbharu.hideout.core.domain.event.relationship.RelationshipEvent
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import org.junit.jupiter.api.Test
import utils.AssertDomainEvent.assertContainsEvent
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class RelationshipTest {
@Test
fun unfollow_フォローとフォローリクエストが取り消されUNFOLLOWとUNFOLLOW_REQUESTが発生する() {
val relationship = Relationship(
actorId = ActorId(1),
targetActorId = ActorId(2),
following = true,
blocking = false,
muting = false,
followRequesting = false,
mutingFollowRequest = false
)
relationship.unfollow()
assertFalse(relationship.following)
assertFalse(relationship.followRequesting)
assertContainsEvent(relationship, RelationshipEvent.UNFOLLOW.eventName)
assertContainsEvent(relationship, RelationshipEvent.UNFOLLOW_REQUEST.eventName)
}
@Test
fun mute_MUTEが発生する() {
val relationship = Relationship(
actorId = ActorId(1),
targetActorId = ActorId(2),
following = true,
blocking = false,
muting = false,
followRequesting = false,
mutingFollowRequest = false
)
relationship.mute()
assertTrue(relationship.muting)
assertContainsEvent(relationship, RelationshipEvent.MUTE.eventName)
}
@Test
fun unmute_UNMUTEが発生する() {
val relationship = Relationship(
actorId = ActorId(1),
targetActorId = ActorId(2),
following = true,
blocking = false,
muting = true,
followRequesting = false,
mutingFollowRequest = true
)
relationship.unmute()
assertFalse(relationship.muting)
assertContainsEvent(relationship, RelationshipEvent.UNMUTE.eventName)
}
@Test
fun muteFollowRequest_muteFollowiRequestがtrueになる() {
val relationship = Relationship(
actorId = ActorId(1),
targetActorId = ActorId(2),
following = true,
blocking = false,
muting = false,
followRequesting = true,
mutingFollowRequest = false
)
relationship.muteFollowRequest()
assertTrue(relationship.mutingFollowRequest)
}
@Test
fun unmuteFollowRequest_muteFollowiRequestがfalseになる() {
val relationship = Relationship(
actorId = ActorId(1),
targetActorId = ActorId(2),
following = true,
blocking = false,
muting = false,
followRequesting = true,
mutingFollowRequest = true
)
relationship.unmuteFollowRequest()
assertFalse(relationship.mutingFollowRequest)
}
@Test
fun unfollowRequest_followRequestingがfalseになりUNFOLLOW_REQUESTが発生する() {
val relationship = Relationship(
ActorId(1),
targetActorId = ActorId(2),
following = false,
blocking = false,
muting = false,
followRequesting = true,
mutingFollowRequest = false
)
relationship.unfollowRequest()
assertFalse(relationship.followRequesting)
assertContainsEvent(relationship, RelationshipEvent.UNFOLLOW_REQUEST.eventName)
}
@Test
fun acceptFollowRequest_followingがtrueにfollowRequestingがfalseになりaccept_followが発生する() {
val relationship = Relationship(
actorId = ActorId(1),
targetActorId = ActorId(2),
following = false,
blocking = false,
muting = false,
followRequesting = true,
mutingFollowRequest = true
)
relationship.acceptFollowRequest()
assertTrue(relationship.following)
assertContainsEvent(relationship, RelationshipEvent.ACCEPT_FOLLOW.eventName)
}
@Test
fun rejectFollowRequest_followRequestingがfalseになりREJECT_FOLLOWが発生する() {
val relationship = Relationship(
actorId = ActorId(1),
targetActorId = ActorId(2),
following = false,
blocking = false,
muting = false,
followRequesting = true,
mutingFollowRequest = false
)
relationship.rejectFollowRequest()
assertFalse(relationship.followRequesting)
assertContainsEvent(relationship, RelationshipEvent.REJECT_FOLLOW.eventName)
}
}

View File

@ -15,7 +15,7 @@ class RemoteActorCheckDomainServiceTest {
val remoteActor = RemoteActorCheckDomainService( val remoteActor = RemoteActorCheckDomainService(
ApplicationConfig( ApplicationConfig(
URI.create("https://local.example.com").toURL() URI.create("https://local.example.com")
) )
).isRemoteActor( ).isRemoteActor(
actor actor
@ -30,7 +30,7 @@ class RemoteActorCheckDomainServiceTest {
val localActor = RemoteActorCheckDomainService( val localActor = RemoteActorCheckDomainService(
ApplicationConfig( ApplicationConfig(
URI.create("https://local.example.com").toURL() URI.create("https://local.example.com")
) )
).isRemoteActor( ).isRemoteActor(
actor actor

View File

@ -15,7 +15,7 @@ import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq import org.mockito.kotlin.eq
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import java.net.URL import java.net.URI
@ExtendWith(MockitoExtension::class) @ExtendWith(MockitoExtension::class)
class LocalActorDomainServiceImplTest { class LocalActorDomainServiceImplTest {
@ -26,7 +26,7 @@ class LocalActorDomainServiceImplTest {
lateinit var actorRepository: ActorRepository lateinit var actorRepository: ActorRepository
@Spy @Spy
val applicationConfig = ApplicationConfig(URL("http://example.com")) val applicationConfig = ApplicationConfig(URI.create("http://example.com"))
@Test @Test
fun findByNameAndDomainがnullならfalse() = runTest { fun findByNameAndDomainがnullならfalse() = runTest {

View File

@ -0,0 +1,86 @@
package dev.usbharu.hideout.core.domain.service.filter
import dev.usbharu.hideout.core.domain.model.filter.*
import dev.usbharu.hideout.core.domain.model.post.TestPostFactory
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class FilterDomainServiceTest {
@Test
fun apply_filterContextの適用範囲にフィルターが適用される() {
val post = TestPostFactory.create()
val domainService = FilterDomainService()
val filter = Filter(
FilterId(1),
userDetailId = UserDetailId(1),
FilterName("filter"),
setOf(FilterContext.HOME),
filterAction = FilterAction.HIDE,
setOf(FilterKeyword(FilterKeywordId(1), FilterKeywordKeyword("test"), FilterMode.NONE))
)
val apply = domainService.apply(post, FilterContext.HOME, listOf(filter))
assertEquals(1, apply.filterResults.size)
assertEquals("test", apply.filterResults.first().matchedKeyword)
}
@Test
fun apply_filterContextに当てはまらないならfilterResultsが空になる() {
val post = TestPostFactory.create()
val domainService = FilterDomainService()
val filter = Filter(
FilterId(1),
userDetailId = UserDetailId(1),
FilterName("filter"),
setOf(FilterContext.PUBLIC),
filterAction = FilterAction.HIDE,
setOf(FilterKeyword(FilterKeywordId(1), FilterKeywordKeyword("test"), FilterMode.NONE))
)
val apply = domainService.apply(post, FilterContext.HOME, listOf(filter))
assertEquals(0, apply.filterResults.size)
}
@Test
fun overviewにも適用される() {
val post = TestPostFactory.create(overview = "test")
val domainService = FilterDomainService()
val filter = Filter(
FilterId(1),
userDetailId = UserDetailId(1),
FilterName("filter"),
setOf(FilterContext.HOME),
filterAction = FilterAction.HIDE,
setOf(FilterKeyword(FilterKeywordId(1), FilterKeywordKeyword("test"), FilterMode.NONE))
)
val apply = domainService.apply(post, FilterContext.HOME, listOf(filter))
assertEquals(2, apply.filterResults.size)
assertEquals("test", apply.filterResults.first().matchedKeyword)
}
@Test
fun applyAll_filterContextの適用範囲にフィルターが適用される() {
val postList = listOf(
TestPostFactory.create(),
TestPostFactory.create(),
TestPostFactory.create(content = "aaaaaaaaaa"),
TestPostFactory.create(),
TestPostFactory.create()
)
val filter = Filter(
FilterId(1),
userDetailId = UserDetailId(1),
FilterName("filter"),
setOf(FilterContext.HOME),
filterAction = FilterAction.HIDE,
setOf(FilterKeyword(FilterKeywordId(1), FilterKeywordKeyword("test"), FilterMode.NONE))
)
val filteredPosts = FilterDomainService().applyAll(postList, FilterContext.HOME, filters = listOf(filter))
assertEquals(5, filteredPosts.size)
}
}

View File

@ -1,6 +1,7 @@
package dev.usbharu.hideout.core.domain.service.post package dev.usbharu.hideout.core.domain.service.post
import dev.usbharu.hideout.core.domain.model.actor.ActorId import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.TestPostFactory import dev.usbharu.hideout.core.domain.model.post.TestPostFactory
import dev.usbharu.hideout.core.domain.model.post.Visibility import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.domain.model.relationship.Relationship import dev.usbharu.hideout.core.domain.model.relationship.Relationship
@ -13,12 +14,14 @@ import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks import org.mockito.InjectMocks
import org.mockito.Mock import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.doReturn import org.mockito.kotlin.*
import org.mockito.kotlin.whenever import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
@ExtendWith(MockitoExtension::class) @ExtendWith(MockitoExtension::class)
class DefaultPostReadAccessControlTest { class DefaultPostReadAccessControlTest {
@ -155,4 +158,217 @@ class DefaultPostReadAccessControlTest {
assertFalse(actual) assertFalse(actual)
} }
@Test
fun ポスト主は無条件で見れる() = runTest {
val actual = service.isAllow(
TestPostFactory.create(actorId = 1, visibility = Visibility.DIRECT),
LocalUser(ActorId(1), UserDetailId(1), Acct("test", "example.com"))
)
assertTrue(actual)
}
@Test
fun areAllows_ポスト主は無条件で見れる() = runTest {
whenever(
relationshipRepository.findByActorIdsAndTargetIdAndBlocking(
any(),
anyValueClass(),
eq(true)
)
).doReturn(
emptyList()
)
whenever(
relationshipRepository.findByActorIdAndTargetIdsAndFollowing(
anyValueClass(),
any(),
eq(true)
)
).doReturn(
emptyList()
)
val postList = listOf<Post>(
TestPostFactory.create(actorId = 1, visibility = Visibility.DIRECT),
TestPostFactory.create(actorId = 1, visibility = Visibility.FOLLOWERS),
TestPostFactory.create(actorId = 1, visibility = Visibility.UNLISTED),
TestPostFactory.create(actorId = 1, visibility = Visibility.PUBLIC),
)
val actual = service.areAllows(postList, LocalUser(ActorId(1), UserDetailId(1), Acct("test", "example.com")))
assertContentEquals(postList, actual)
}
@Test
fun areFollows_ブロックされていたら見れない() = runTest {
whenever(
relationshipRepository.findByActorIdsAndTargetIdAndBlocking(
any(),
anyValueClass(),
eq(true)
)
).doReturn(
listOf(Relationship.default(actorId = ActorId(2), targetActorId = ActorId(1)))
)
whenever(
relationshipRepository.findByActorIdAndTargetIdsAndFollowing(
anyValueClass(),
any(),
eq(true)
)
).doReturn(
emptyList()
)
val postList = listOf<Post>(
TestPostFactory.create(actorId = 1, visibility = Visibility.DIRECT),
TestPostFactory.create(actorId = 2, visibility = Visibility.FOLLOWERS),
TestPostFactory.create(actorId = 1, visibility = Visibility.UNLISTED),
TestPostFactory.create(actorId = 1, visibility = Visibility.PUBLIC),
)
val actual = service.areAllows(postList, LocalUser(ActorId(1), UserDetailId(1), Acct("test", "example.com")))
assertEquals(3, actual.size)
assertAll(actual.map { { assertEquals(1, it.actorId.id) } })
}
@Test
fun areAllows_PUBLICとUNLISTEDは見れる() = runTest {
whenever(
relationshipRepository.findByActorIdsAndTargetIdAndBlocking(
any(),
anyValueClass(),
eq(true)
)
).doReturn(
emptyList()
)
whenever(
relationshipRepository.findByActorIdAndTargetIdsAndFollowing(
anyValueClass(),
any(),
eq(true)
)
).doReturn(
emptyList()
)
val postList = listOf<Post>(
TestPostFactory.create(actorId = 3, visibility = Visibility.DIRECT),
TestPostFactory.create(actorId = 3, visibility = Visibility.FOLLOWERS),
TestPostFactory.create(actorId = 3, visibility = Visibility.UNLISTED),
TestPostFactory.create(actorId = 3, visibility = Visibility.PUBLIC),
)
val actual = service.areAllows(postList, LocalUser(ActorId(1), UserDetailId(1), Acct("test", "example.com")))
assertEquals(2, actual.size)
kotlin.test.assertTrue(actual.all { it.visibility == Visibility.PUBLIC || it.visibility == Visibility.UNLISTED })
}
@Test
fun areAllows_Anonymousは見れない() = runTest {
whenever(
relationshipRepository.findByActorIdsAndTargetIdAndBlocking(
any(),
anyValueClass(),
eq(true)
)
).doReturn(
emptyList()
)
whenever(
relationshipRepository.findByActorIdAndTargetIdsAndFollowing(
anyValueClass(),
any(),
eq(true)
)
).doReturn(
emptyList()
)
val postList = listOf<Post>(
TestPostFactory.create(actorId = 3, visibility = Visibility.DIRECT),
TestPostFactory.create(actorId = 3, visibility = Visibility.FOLLOWERS),
TestPostFactory.create(actorId = 3, visibility = Visibility.UNLISTED),
TestPostFactory.create(actorId = 3, visibility = Visibility.PUBLIC),
)
val actual = service.areAllows(postList, Anonymous)
assertEquals(2, actual.size)
kotlin.test.assertTrue(actual.all { it.visibility == Visibility.PUBLIC || it.visibility == Visibility.UNLISTED })
}
@Test
fun areAllows_DIRECTはVisibleActorsに入っていたら見れる() = runTest {
whenever(
relationshipRepository.findByActorIdsAndTargetIdAndBlocking(
any(),
anyValueClass(),
eq(true)
)
).doReturn(
emptyList()
)
whenever(
relationshipRepository.findByActorIdAndTargetIdsAndFollowing(
anyValueClass(),
any(),
eq(true)
)
).doReturn(
emptyList()
)
val postList = listOf<Post>(
TestPostFactory.create(id = 1, actorId = 3, visibility = Visibility.DIRECT, visibleActors = listOf(1)),
TestPostFactory.create(id = 2, actorId = 3, visibility = Visibility.DIRECT, visibleActors = listOf(2)),
TestPostFactory.create(id = 3, actorId = 3, visibility = Visibility.DIRECT, visibleActors = listOf(3)),
TestPostFactory.create(
id = 4,
actorId = 3,
visibility = Visibility.DIRECT,
visibleActors = listOf(1, 2, 3, 4)
),
)
val actual = service.areAllows(postList, LocalUser(ActorId(1), UserDetailId(1), Acct("test", "example.com")))
assertEquals(2, actual.size)
kotlin.test.assertTrue(actual.all { it.id.id == 1L || it.id.id == 4L })
}
@Test
fun areAllows_FOLLOWERSはフォローされていたら見れる() = runTest {
whenever(
relationshipRepository.findByActorIdsAndTargetIdAndBlocking(
any(),
anyValueClass(),
eq(true)
)
).doReturn(
emptyList()
)
whenever(
relationshipRepository.findByActorIdAndTargetIdsAndFollowing(
anyValueClass(),
any(),
eq(true)
)
).doReturn(
listOf(Relationship.default(actorId = ActorId(1), targetActorId = ActorId(2)))
)
val postList = listOf<Post>(
TestPostFactory.create(actorId = 3, visibility = Visibility.FOLLOWERS),
TestPostFactory.create(actorId = 2, visibility = Visibility.FOLLOWERS),
TestPostFactory.create(actorId = 3, visibility = Visibility.FOLLOWERS),
TestPostFactory.create(actorId = 3, visibility = Visibility.FOLLOWERS),
)
val actual = service.areAllows(postList, LocalUser(ActorId(1), UserDetailId(1), Acct("test", "example.com")))
assertEquals(1, actual.size)
assertAll(actual.map { { assertEquals(2, it.actorId.id) } })
}
} }

View File

@ -0,0 +1,136 @@
package dev.usbharu.hideout.core.domain.service.relationship
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.relationship.Relationship
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class RelationshipDomainServiceTest {
@Test
fun block_relationshipとinverseRelationshipが同じ場合失敗() {
val relationship = Relationship.default(ActorId(1), ActorId(2))
assertThrows<IllegalArgumentException> {
RelationshipDomainService().block(relationship, relationship)
}
}
@Test
fun block_relationship_actorIdとinverseRelationshio_targetActorIdが同じ場合失敗() {
val relationship = Relationship.default(ActorId(1), ActorId(2))
val inverseRelationship = Relationship.default(ActorId(2), ActorId(2))
assertThrows<IllegalArgumentException> {
RelationshipDomainService().block(relationship, inverseRelationship)
}
}
@Test
fun block_relationship_targetActorIdとinverseRelationship_actorIdが同じ場合失敗() {
val relationship = Relationship.default(ActorId(1), ActorId(2))
val inverseRelationship = Relationship.default(ActorId(1), ActorId(1))
assertThrows<IllegalArgumentException> {
RelationshipDomainService().block(relationship, inverseRelationship)
}
}
@Test
fun block_ブロックされお互いのフォローとフォローリクエストが外れる() {
val relationship = Relationship(
ActorId(1),
ActorId(2),
following = true,
blocking = false,
muting = false,
followRequesting = false,
mutingFollowRequest = false
)
val inverseRelationship = Relationship(
ActorId(2),
ActorId(1),
following = false,
blocking = false,
followRequesting = true,
mutingFollowRequest = false,
muting = false
)
RelationshipDomainService().block(relationship, inverseRelationship)
assertTrue(relationship.blocking)
assertFalse(relationship.following)
assertFalse(relationship.followRequesting)
assertFalse(inverseRelationship.following)
assertFalse(inverseRelationship.followRequesting)
}
@Test
fun followRequest_relationshipとinverseRelationshipが同じ場合失敗() {
val relationship = Relationship.default(ActorId(1), ActorId(2))
assertThrows<IllegalArgumentException> {
RelationshipDomainService().followRequest(relationship, relationship)
}
}
@Test
fun followRequest_relationship_actorIdとinverseRelationshio_targetActorIdが同じ場合失敗() {
val relationship = Relationship.default(ActorId(1), ActorId(2))
val inverseRelationship = Relationship.default(ActorId(2), ActorId(2))
assertThrows<IllegalArgumentException> {
RelationshipDomainService().followRequest(relationship, inverseRelationship)
}
}
@Test
fun followRequest_relationship_targetActorIdとinverseRelationship_actorIdが同じ場合失敗() {
val relationship = Relationship.default(ActorId(1), ActorId(2))
val inverseRelationship = Relationship.default(ActorId(1), ActorId(1))
assertThrows<IllegalArgumentException> {
RelationshipDomainService().followRequest(relationship, inverseRelationship)
}
}
@Test
fun followRequest_ブロックされてる場合失敗() {
val relationship = Relationship.default(ActorId(1), ActorId(2))
val inverseRelationship = Relationship(
ActorId(2),
ActorId(1),
following = false,
blocking = true,
muting = false,
followRequesting = false,
mutingFollowRequest = false
)
assertThrows<IllegalArgumentException> {
RelationshipDomainService().followRequest(relationship, inverseRelationship)
}
}
@Test
fun followRequest_ブロックしてる場合は失敗() {
val relationship = Relationship(
ActorId(1),
ActorId(2),
following = false,
blocking = true,
muting = false,
followRequesting = false,
mutingFollowRequest = false
)
val inverseRelationship = Relationship.default(ActorId(2), ActorId(1))
assertThrows<IllegalArgumentException> {
RelationshipDomainService().followRequest(relationship, inverseRelationship)
}
}
@Test
fun followRequest_followRequestingがtrueになる() {
val relationship = Relationship.default(ActorId(1), ActorId(2))
val inverseRelationship = Relationship.default(ActorId(2), ActorId(1))
RelationshipDomainService().followRequest(relationship, inverseRelationship)
assertTrue(relationship.followRequesting)
}
}

View File

@ -0,0 +1,28 @@
package dev.usbharu.hideout.core.domain.service.userdetail
import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityPasswordEncoder
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Spy
import org.mockito.junit.jupiter.MockitoExtension
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import kotlin.test.assertNotEquals
@ExtendWith(MockitoExtension::class)
class UserDetailDomainServiceTest {
@InjectMocks
lateinit var userDetailDomainService: UserDetailDomainService
@Spy
val passwordEncoder: PasswordEncoder = SpringSecurityPasswordEncoder(BCryptPasswordEncoder())
@Test
fun hash() = runTest {
val hashedPassword = userDetailDomainService.hashPassword("password")
assertNotEquals("password", hashedPassword.password)
}
}

View File

@ -464,7 +464,7 @@ class ExposedRelationshipRepositoryTest : AbstractRepositoryTest(Relationships)
mutingFollowRequest = false, mutingFollowRequest = false,
) )
relationship.block() relationship.mute()
repository.save(relationship) repository.save(relationship)
@ -492,7 +492,7 @@ class ExposedRelationshipRepositoryTest : AbstractRepositoryTest(Relationships)
followRequesting = false, followRequesting = false,
mutingFollowRequest = false, mutingFollowRequest = false,
) )
relationship.block() relationship.mute()
repository.delete(relationship) repository.delete(relationship)

View File

@ -19,7 +19,7 @@ import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.* import org.mockito.kotlin.*
import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.core.userdetails.UsernameNotFoundException
import utils.TestTransaction import utils.TestTransaction
import java.net.URL import java.net.URI
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ExtendWith(MockitoExtension::class) @ExtendWith(MockitoExtension::class)
@ -34,7 +34,7 @@ class UserDetailsServiceImplTest {
lateinit var userDetailRepository: UserDetailRepository lateinit var userDetailRepository: UserDetailRepository
@Spy @Spy
val applicationConfig = ApplicationConfig(URL("http://example.com")) val applicationConfig = ApplicationConfig(URI.create("http://example.com"))
@Spy @Spy
val transaction = TestTransaction val transaction = TestTransaction

View File

@ -1,3 +1,4 @@
import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
plugins { plugins {
@ -6,6 +7,7 @@ plugins {
alias(libs.plugins.spring.boot) alias(libs.plugins.spring.boot)
alias(libs.plugins.kotlin.spring) alias(libs.plugins.kotlin.spring)
alias(libs.plugins.detekt) alias(libs.plugins.detekt)
alias(libs.plugins.kover)
} }
@ -60,9 +62,14 @@ dependencies {
implementation(libs.bundles.coroutines) implementation(libs.bundles.coroutines)
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation(libs.bundles.spring.boot.oauth2)
testImplementation(libs.kotlin.junit) testImplementation(libs.kotlin.junit)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
testImplementation(libs.h2db) testImplementation(libs.h2db)
testImplementation(libs.flyway.core)
testImplementation(libs.http.signature)
testRuntimeOnly(libs.flyway.postgresql)
} }
@ -126,6 +133,53 @@ configurations.matching { it.name == "detekt" }.all {
} }
} }
project.gradle.taskGraph.whenReady {
if (this.hasTask(":koverGenerateArtifact")) {
val task = this.allTasks.find { it.name == "test" }
val verificationTask = task as VerificationTask
verificationTask.ignoreFailures = true
}
}
kover {
currentProject {
sources {
}
}
reports {
verify {
rule {
bound {
minValue = 50
coverageUnits = CoverageUnit.INSTRUCTION
}
}
}
total {
xml {
title = "Hideout Mastodon"
xmlFile = file("$buildDir/reports/kover/hideout-mastodon.xml")
}
}
filters {
excludes {
annotatedBy("org.springframework.context.annotation.Configuration")
annotatedBy("org.springframework.boot.context.properties.ConfigurationProperties")
packages(
"dev.usbharu.hideout.controller.mastodon.generated",
"dev.usbharu.hideout.domain.mastodon.model.generated"
)
packages("org.springframework")
packages("org.jetbrains")
}
}
}
}
tasks.withType<io.gitlab.arturbosch.detekt.Detekt> { tasks.withType<io.gitlab.arturbosch.detekt.Detekt> {
exclude("**/generated/**") exclude("**/generated/**")
doFirst { doFirst {

0
hideout-mastodon/gradlew vendored Normal file → Executable file
View File

View File

@ -33,7 +33,7 @@ class DeleteFilterV1ApplicationService(private val filterRepository: FilterRepos
) { ) {
override suspend fun internalExecute(command: DeleteFilterV1, principal: LocalUser) { override suspend fun internalExecute(command: DeleteFilterV1, principal: LocalUser) {
val filter = filterRepository.findByFilterKeywordId(FilterKeywordId(command.filterKeywordId)) val filter = filterRepository.findByFilterKeywordId(FilterKeywordId(command.filterKeywordId))
?: throw IllegalArgumentException("Filter ${command.filterKeywordId} not found") ?: throw IllegalArgumentException("Filter ${command.filterKeywordId} by KeywordId not found")
if (principal.userDetailId != filter.userDetailId) { if (principal.userDetailId != filter.userDetailId) {
throw PermissionDeniedException() throw PermissionDeniedException()
} }

View File

@ -39,7 +39,7 @@ class GetFilterV1ApplicationService(private val filterRepository: FilterReposito
?: throw IllegalArgumentException("Filter ${command.filterKeywordId} not found") ?: throw IllegalArgumentException("Filter ${command.filterKeywordId} not found")
if (filter.userDetailId != principal.userDetailId) { if (filter.userDetailId != principal.userDetailId) {
throw PermissionDeniedException() throw PermissionDeniedException(principal)
} }
val filterKeyword = filter.filterKeywords.find { it.id.id == command.filterKeywordId } val filterKeyword = filter.filterKeywords.find { it.id.id == command.filterKeywordId }

View File

@ -84,6 +84,7 @@ class SpringFilterApi(
account -> FilterContext.ACCOUNT account -> FilterContext.ACCOUNT
} }
}.toSet() }.toSet()
val principal = principalContextHolder.getPrincipal()
val filter = userRegisterFilterApplicationService.execute( val filter = userRegisterFilterApplicationService.execute(
RegisterFilter( RegisterFilter(
v1FilterPostRequest.phrase, v1FilterPostRequest.phrase,
@ -91,12 +92,12 @@ class SpringFilterApi(
FilterAction.WARN, FilterAction.WARN,
setOf(RegisterFilterKeyword(v1FilterPostRequest.phrase, filterMode)) setOf(RegisterFilterKeyword(v1FilterPostRequest.phrase, filterMode))
), ),
principalContextHolder.getPrincipal() principal
) )
return ResponseEntity.ok( return ResponseEntity.ok(
getFilterV1ApplicationService.execute( getFilterV1ApplicationService.execute(
GetFilterV1(filter.filterKeywords.first().id), GetFilterV1(filter.filterKeywords.first().id),
principalContextHolder.getPrincipal() principal
) )
) )
} }

View File

@ -21,9 +21,12 @@ import dev.usbharu.hideout.core.application.media.UploadMediaApplicationService
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.SpringSecurityOauth2PrincipalContextHolder import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.SpringSecurityOauth2PrincipalContextHolder
import dev.usbharu.hideout.mastodon.interfaces.api.generated.MediaApi import dev.usbharu.hideout.mastodon.interfaces.api.generated.MediaApi
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.MediaAttachment import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.MediaAttachment
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
import org.springframework.web.server.ResponseStatusException
import java.nio.file.Files import java.nio.file.Files
@Controller @Controller
@ -37,6 +40,11 @@ class SpringMediaApi(
description: String?, description: String?,
focus: String?, focus: String?,
): ResponseEntity<MediaAttachment> { ): ResponseEntity<MediaAttachment> {
if (file.size == 0L) {
logger.warn("File is empty.")
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File is empty.")
}
val tempFile = Files.createTempFile("hideout-tmp-file", ".tmp") val tempFile = Files.createTempFile("hideout-tmp-file", ".tmp")
Files.newOutputStream(tempFile).use { outputStream -> Files.newOutputStream(tempFile).use { outputStream ->
@ -73,4 +81,8 @@ class SpringMediaApi(
) )
) )
} }
companion object {
private val logger = LoggerFactory.getLogger(SpringMediaApi::class.java)
}
} }

View File

@ -6,6 +6,8 @@ import dev.usbharu.hideout.core.domain.model.support.acct.Acct
import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@ -30,4 +32,13 @@ class StatusQueryServiceImplTest {
assertNull(status) assertNull(status)
} }
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
} }

View File

@ -0,0 +1,160 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mastodon.account
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Status
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/actors.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/accounts/test-accounts-statuses.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class AccountApiPaginationTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@Test
fun `apiV1AccountsIdStatusesGet 投稿を取得できる`() {
val content = mockMvc
.get("/api/v1/accounts/1/statuses"){
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
.andExpect { header { string("Link","<https://example.com/api/v1/accounts/1/statuses?min_id=100>; rel=\"next\", <https://example.com/api/v1/accounts/1/statuses?max_id=81>; rel=\"prev\"") } }
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Status>>() {})
assertThat(value.first().id).isEqualTo("100")
assertThat(value.last().id).isEqualTo("81")
assertThat(value).size().isEqualTo(20)
}
@Test
fun `apiV1AccountsIdStatusesGet 結果が0件のときはLinkヘッダーがない`() {
val content = mockMvc
.get("/api/v1/accounts/1/statuses?min_id=100"){
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.andDo { print() }
.asyncDispatch()
.andExpect { status { isOk() } }
.andExpect { header { doesNotExist("Link") } }
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Status>>() {})
assertThat(value).isEmpty()
}
@Test
fun `apiV1AccountsIdStatusesGet maxIdを指定して取得`() {
val content = mockMvc
.get("/api/v1/accounts/1/statuses?max_id=100"){
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
.andExpect { header { string("Link","<https://example.com/api/v1/accounts/1/statuses?min_id=99>; rel=\"next\", <https://example.com/api/v1/accounts/1/statuses?max_id=80>; rel=\"prev\"") } }
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Status>>() {})
assertThat(value.first().id).isEqualTo("99")
assertThat(value.last().id).isEqualTo("80")
assertThat(value).size().isEqualTo(20)
}
@Test
fun `apiV1AccountsIdStatusesGet minIdを指定して取得`() {
val content = mockMvc
.get("/api/v1/accounts/1/statuses?min_id=1"){
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
.andExpect { header { string("Link","<https://example.com/api/v1/accounts/1/statuses?min_id=21>; rel=\"next\", <https://example.com/api/v1/accounts/1/statuses?max_id=2>; rel=\"prev\"") } }
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Status>>() {})
assertThat(value.first().id).isEqualTo("21")
assertThat(value.last().id).isEqualTo("2")
assertThat(value).size().isEqualTo(20)
}
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,471 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mastodon.account
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.infrastructure.exposedrepository.ExposedRelationshipRepository
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/actors.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class AccountApiTest {
@Autowired
private lateinit var actorRepository: ActorRepository
@Autowired
lateinit var relationshipRepository: ExposedRelationshipRepository
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
}
@Test
fun `apiV1AccountsVerifyCredentialsGetにreadでアクセスできる`() {
mockMvc
.get("/api/v1/accounts/verify_credentials") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsVerifyCredentialsGetにread_accountsでアクセスできる`() {
mockMvc
.get("/api/v1/accounts/verify_credentials") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:accounts")))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsVerifyCredentialsGetに匿名でアクセスすると401() {
mockMvc
.get("/api/v1/accounts/verify_credentials")
.andExpect { status { isUnauthorized() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostに匿名でPOSTしたらアカウントを作成できる() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-1")
param("password", "very-secure-password")
param("email", "test@example.com")
param("agreement", "true")
param("locale", "")
with(csrf())
}
.asyncDispatch()
.andExpect { status { isFound() } }
actorRepository.findByNameAndDomain("api-test-user-1", "example.com")
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostで必須パラメーター以外を省略しても作成できる() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-2")
param("password", "very-secure-password")
with(csrf())
}
.asyncDispatch()
.andExpect { status { isFound() } }
actorRepository.findByNameAndDomain("api-test-user-2", "example.com")
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostでusernameパラメーターを省略したら400() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("password", "api-test-user-3")
with(csrf())
}
.andDo { print() }
.andExpect { status { isUnprocessableEntity() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostでpasswordパラメーターを省略したら400() = runTest {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-4")
with(csrf())
}
.andExpect { status { isUnprocessableEntity() } }
}
@Test
@Disabled("JSONでも作れるようにするため")
@WithAnonymousUser
fun apiV1AccountsPostでJSONで作ろうとしても400() {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_JSON
content = """{"username":"api-test-user-5","password":"very-very-secure-password"}"""
with(csrf())
}
.andExpect { status { isUnsupportedMediaType() } }
}
@Test
@WithAnonymousUser
fun apiV1AccountsPostにCSRFトークンは必要() {
mockMvc
.post("/api/v1/accounts") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("username", "api-test-user-2")
param("password", "very-secure-password")
}
.andExpect { status { isForbidden() } }
}
@Test
@WithAnonymousUser
fun `apiV1AccountsIdGet 匿名でアカウント情報を取得できる`() {
mockMvc
.get("/api/v1/accounts/1")
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsIdFollowPost write_follows権限でPOSTでフォローできる`() {
mockMvc
.post("/api/v1/accounts/2/follow") {
contentType = MediaType.APPLICATION_JSON
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:follows")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsIdFollowPost write権限でPOSTでフォローできる`() {
mockMvc
.post("/api/v1/accounts/2/follow") {
contentType = MediaType.APPLICATION_JSON
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsIdFollowPost read権限でだと403`() {
mockMvc
.post("/api/v1/accounts/2/follow") {
contentType = MediaType.APPLICATION_JSON
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
}
.andExpect { status { isForbidden() } }
}
@Test
@WithAnonymousUser
fun `apiV1AAccountsIdFollowPost 匿名だと401`() {
mockMvc
.post("/api/v1/accounts/2/follow") {
contentType = MediaType.APPLICATION_JSON
with(csrf())
}
.andExpect { status { isUnauthorized() } }
}
@Test
@WithAnonymousUser
fun `apiV1AAccountsIdFollowPost 匿名の場合通常csrfトークンは持ってないので403`() {
mockMvc
.post("/api/v1/accounts/2/follow") {
contentType = MediaType.APPLICATION_JSON
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV1AccountsRelationshipsGet 匿名だと401`() {
mockMvc
.get("/api/v1/accounts/relationships")
.andExpect { status { isUnauthorized() } }
}
@Test
fun `apiV1AccountsRelationshipsGet read_follows権限を持っていたら取得できる`() {
mockMvc
.get("/api/v1/accounts/relationships") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:follows")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsRelationshipsGet read権限を持っていたら取得できる`() {
mockMvc
.get("/api/v1/accounts/relationships") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsRelationshipsGet write権限だと403`() {
mockMvc
.get("/api/v1/accounts/relationships") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.andExpect { status { isForbidden() } }
}
@Test
@Sql("/sql/accounts/apiV1AccountsIdFollowPost フォローできる.sql")
fun `apiV1AccountsIdFollowPost フォローできる`() = runTest {
mockMvc
.post("/api/v1/accounts/3733363/follow") {
contentType = MediaType.APPLICATION_JSON
with(jwt().jwt { it.claim("uid", "37335363") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
val alreadyFollow = relationshipRepository.findByActorIdAndTargetId(ActorId(3733363),ActorId(37335363))?.following
assertThat(alreadyFollow).isTrue()
}
@Test
fun `apiV1AccountsIdMutePost write権限でミュートできる`() {
mockMvc
.post("/api/v1/accounts/2/mute") {
contentType = MediaType.APPLICATION_JSON
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsIdMutePost write_mutes権限でミュートできる`() {
mockMvc
.post("/api/v1/accounts/2/mute") {
contentType = MediaType.APPLICATION_JSON
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:mutes")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsIdMutePost read権限だと403`() = runTest {
mockMvc
.post("/api/v1/accounts/2/mute") {
contentType = MediaType.APPLICATION_JSON
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
}
.andExpect { status { isForbidden() } }
}
@Test
@WithAnonymousUser
fun `apiV1AccountsIdMutePost 匿名だと401`() = runTest {
mockMvc
.post("/api/v1/accounts/2/mute") {
contentType = MediaType.APPLICATION_JSON
with(csrf())
}
.andExpect { status { isUnauthorized() } }
}
@Test
@WithAnonymousUser
fun `apiV1AccountsIdMutePost csrfトークンがないと403`() = runTest {
mockMvc
.post("/api/v1/accounts/2/mute") {
contentType = MediaType.APPLICATION_JSON
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV1AccountsIdUnmutePost write権限でアンミュートできる`() {
mockMvc
.post("/api/v1/accounts/2/unmute") {
contentType = MediaType.APPLICATION_JSON
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsIdUnmutePost write_mutes権限でアンミュートできる`() {
mockMvc
.post("/api/v1/accounts/2/unmute") {
contentType = MediaType.APPLICATION_JSON
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:mutes")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1AccountsIdUnmutePost read権限だと403`() = runTest {
mockMvc
.post("/api/v1/accounts/2/unmute") {
contentType = MediaType.APPLICATION_JSON
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
}
.andExpect { status { isForbidden() } }
}
@Test
@WithAnonymousUser
fun `apiV1AccountsIdUnmutePost 匿名だと401`() = runTest {
mockMvc
.post("/api/v1/accounts/2/unmute") {
contentType = MediaType.APPLICATION_JSON
with(csrf())
}
.andExpect { status { isUnauthorized() } }
}
@Test
@WithAnonymousUser
fun `apiV1AccountsIdUnmutePost csrfトークンがないと403`() = runTest {
mockMvc
.post("/api/v1/accounts/2/unmute") {
contentType = MediaType.APPLICATION_JSON
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV1MutesGet read権限でミュートしているアカウント一覧を取得できる`() {
mockMvc
.get("/api/v1/mutes") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1MutesGet read_mutes権限でミュートしているアカウント一覧を取得できる`() {
mockMvc
.get("/api/v1/mutes") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:mutes")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1MutesGet write権限だと403`() {
mockMvc
.get("/api/v1/mutes") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.andExpect { status { isForbidden() } }
}
@Test
@WithAnonymousUser
fun `apiV1MutesGet 匿名だと401`() {
mockMvc
.get("/api/v1/mutes")
.andExpect { status { isUnauthorized() } }
}
@Test
fun `apiV1AccountsIdStatusesGet read権限で取得できる`() {
mockMvc
.get("/api/v1/accounts/1/statuses")
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
@WithAnonymousUser
fun `apiV1AccountsIdStatusesGet 匿名でもpublic投稿を取得できる`() {
mockMvc
.get("/api/v1/accounts/1/statuses")
.asyncDispatch()
.andExpect { status { isOk() } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,135 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mastodon.apps
import dev.usbharu.hideout.SpringApplication
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcOperations
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository
import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
import util.objectMapper
import kotlin.test.assertNotNull
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
class AppTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@Autowired
lateinit var jdbcOperations: JdbcOperations
val registeredClientRepository: JdbcRegisteredClientRepository by lazy {
JdbcRegisteredClientRepository(
jdbcOperations
)
}
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
@WithAnonymousUser
fun apiV1AppsPostにformで匿名でappを作成できる() {
val contentAsString = mockMvc
.post("/api/v1/apps") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("client_name", "test-client")
param("redirect_uris", "https://example.com")
param("scopes", "write read")
param("website", "https://example.com")
}
.asyncDispatch()
.andExpect { status { isOk() } }
.andReturn()
.response
.contentAsString
val clientId = objectMapper().readTree(contentAsString)["client_id"].asText()
val registeredClient = registeredClientRepository.findByClientId(clientId)
assertNotNull(registeredClient)
assertThat(registeredClient.clientName).isEqualTo("test-client")
assertThat(registeredClient.redirectUris.joinToString(",")).isEqualTo("https://example.com")
assertThat(registeredClient.scopes.joinToString(",")).isEqualTo("read,write")
}
@Test
@WithAnonymousUser
fun apiV1AppsPostにjsonで匿名でappを作成できる() {
val contentAsString = mockMvc
.post("/api/v1/apps") {
contentType = MediaType.APPLICATION_JSON
content = """{
"client_name": "test-client-2",
"redirect_uris": "https://example.com",
"scopes": "write read",
"website": "https;//example.com"
}"""
}
.asyncDispatch()
.andExpect { status { isOk() } }
.andReturn()
.response
.contentAsString
val clientId = objectMapper().readTree(contentAsString)["client_id"].asText()
val registeredClient = registeredClientRepository.findByClientId(clientId)
assertNotNull(registeredClient)
assertThat(registeredClient.clientName).isEqualTo("test-client-2")
assertThat(registeredClient.redirectUris.joinToString(",")).isEqualTo("https://example.com")
assertThat(registeredClient.scopes.joinToString(",")).isEqualTo("read,write")
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,711 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mastodon.filter
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.FilterKeywordsPostRequest
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.FilterPostRequest
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.FilterPostRequestKeyword
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.V1FilterPostRequest
import kotlinx.coroutines.test.runTest
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.delete
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
import util.objectMapper
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/actors.sql", "/sql/userdetail.sql","/sql/filter/test-filter.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class FilterTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
fun `apiV2FiltersPost write権限で追加できる`() {
mockMvc
.post("/api/v2/filters") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper().writeValueAsString(
FilterPostRequest(
title = "mute test",
context = listOf(FilterPostRequest.Context.home, FilterPostRequest.Context.public),
filterAction = FilterPostRequest.FilterAction.warn,
expiresIn = null,
keywordsAttributes = listOf(
FilterPostRequestKeyword(
keyword = "hoge",
wholeWord = false,
regex = true
)
)
)
)
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
.andExpect {
content {
jsonPath("$.keywords[0].keyword") {
value("hoge")
}
}
}
}
@Test
fun `apiV2FiltersPost write_filters権限で追加できる`() {
mockMvc
.post("/api/v2/filters") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper().writeValueAsString(
FilterPostRequest(
title = "mute test",
context = listOf(FilterPostRequest.Context.home, FilterPostRequest.Context.public),
filterAction = FilterPostRequest.FilterAction.warn,
expiresIn = null,
keywordsAttributes = listOf(
FilterPostRequestKeyword(
keyword = "fuga",
wholeWord = true,
regex = false
)
)
)
)
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
.andExpect {
content {
jsonPath("$.keywords[0].keyword") {
value("fuga")
}
}
}
}
@Test
fun `apiV2FiltersPost read権限で401`() {
mockMvc
.post("/api/v2/filters") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper().writeValueAsString(
FilterPostRequest(
title = "mute test",
context = listOf(FilterPostRequest.Context.home, FilterPostRequest.Context.public),
filterAction = FilterPostRequest.FilterAction.warn,
expiresIn = null,
keywordsAttributes = listOf(
FilterPostRequestKeyword(
keyword = "fuga",
wholeWord = true,
regex = false
)
)
)
)
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV2FiltersGet read権限で取得できる`() {
mockMvc
.get("/api/v2/filters") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersGet read_filters権限で取得できる`() {
mockMvc
.get("/api/v2/filters") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersGet write権限で401`() {
mockMvc
.get("/api/v2/filters") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV2FiltersIdGet read権限で取得できる`() {
mockMvc
.get("/api/v2/filters/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersIdGet read_filters権限で取得できる`() {
mockMvc
.get("/api/v2/filters/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersIdGet write権限で401`() {
mockMvc
.get("/api/v2/filters/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV2FiltersFilterIdKeywordsGet read権限で取得できる`() {
mockMvc
.get("/api/v2/filters/1/keywords") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersFilterIdKeywordsGet read_filters権限で取得できる`() {
mockMvc
.get("/api/v2/filters/1/keywords") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersFilterIdKeywordsGet writeで403`() {
mockMvc
.get("/api/v2/filters/1/keywords") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV2FiltersFilterIdKeywordsPost writeで追加できる`() {
mockMvc
.post("/api/v2/filters/1/keywords") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper().writeValueAsString(
FilterKeywordsPostRequest(
"hage", false, false
)
)
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersFilterIdKeywordsPost write_filtersで追加できる`() {
mockMvc
.post("/api/v2/filters/1/keywords") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper().writeValueAsString(
FilterKeywordsPostRequest(
"hage", false, false
)
)
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersFilterIdKeywordsPost readで403`() {
mockMvc
.post("/api/v2/filters/1/keywords") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper().writeValueAsString(
FilterKeywordsPostRequest(
"hage", false, false
)
)
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV2FiltersKeywordsIdGet readで取得できる`() {
mockMvc
.get("/api/v2/filters/keywords/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersKeywordsIdGet read_filtersで取得できる`() {
mockMvc
.get("/api/v2/filters/keywords/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersKeywordsIdGet writeだと403`() {
mockMvc
.get("/api/v2/filters/keywords/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV2FiltersKeyowrdsIdDelete writeで削除できる`() = runTest {
mockMvc
.delete("/api/v2/filters/keywords/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersKeyowrdsIdDelete write_filtersで削除できる`() = runTest {
mockMvc
.delete("/api/v2/filters/keywords/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV2FiltersKeyowrdsIdDelete readで403`() = runTest {
mockMvc
.delete("/api/v2/filters/keywords/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV2FiltersFilterIdStatuses readで取得できる`() {
mockMvc
.get("/api/v2/filters/1/statuses") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isNotFound() } }
}
@Test
fun `apiV2FiltersFilterIdStatuses read_filtersで取得できる`() {
mockMvc
.get("/api/v2/filters/1/statuses") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters"))
)
}
.asyncDispatch()
.andExpect { status { isNotFound() } }
}
@Test
fun `apiV2FiltersFilterIdStatuses writeで403`() {
mockMvc
.get("/api/v2/filters/1/statuses") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV2FiltersStatusesIdGet readで取得できる`() {
mockMvc
.get("/api/v2/filters/statuses/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isNotFound() } }
}
@Test
fun `apiV2FiltersStatusesIdGet read_filtersで取得できる`() {
mockMvc
.get("/api/v2/filters/statuses/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters"))
)
}
.asyncDispatch()
.andExpect { status { isNotFound() } }
}
@Test
fun `apiV2FiltersStatusesIdGet writeで403`() {
mockMvc
.get("/api/v2/filters/statuses/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV2FiltersStatusesIdDelete writeで削除できる`() {
mockMvc
.delete("/api/v2/filters/statuses/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andExpect { status { isNotFound() } }
}
@Test
fun `apiV2FiltersStatusesIdDelete write_filtersで削除できる`() {
mockMvc
.delete("/api/v2/filters/statuses/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters"))
)
}
.asyncDispatch()
.andExpect { status { isNotFound() } }
}
@Test
fun `apiV2FiltersStatusesIdDelete readで403`() {
mockMvc
.delete("/api/v2/filters/statuses/1") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV1FiltersGet readで取得できる`() {
mockMvc
.get("/api/v1/filters") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1FiltersGet read_filtersで取得できる`() {
mockMvc
.get("/api/v1/filters") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1FiltersGet writeで403`() {
mockMvc
.get("/api/v1/filters") {
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV1FiltersPost writeで新規作成`() {
mockMvc
.post("/api/v1/filters") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper().writeValueAsString(
V1FilterPostRequest(
phrase = "hoge",
context = listOf(V1FilterPostRequest.Context.home),
irreversible = false,
wholeWord = false,
expiresIn = null
)
)
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1FiltersPost write_filtersで新規作成`() {
mockMvc
.post("/api/v1/filters") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper().writeValueAsString(
V1FilterPostRequest(
phrase = "hoge",
context = listOf(V1FilterPostRequest.Context.home),
irreversible = false,
wholeWord = false,
expiresIn = null
)
)
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1FiltersPost readで403`() {
mockMvc
.post("/api/v1/filters") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper().writeValueAsString(
V1FilterPostRequest(
phrase = "hoge",
context = listOf(V1FilterPostRequest.Context.home),
irreversible = false,
wholeWord = false,
expiresIn = null
)
)
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
fun `apiV1FiltersIdGet readで取得できる`() {
mockMvc
.get("/api/v1/filters/1") {
with(
jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1FiltersIdGet read_filtersで取得できる`() {
mockMvc
.get("/api/v1/filters/1") {
with(
jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:filters"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1FiltersIdGet writeで403`() {
mockMvc
.get("/api/v1/filters/1") {
with(
jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
@Sql("/sql/filter/test-filter.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
fun `apiV1FiltersIdDelete writeで削除できる`() {
mockMvc
.delete("/api/v1/filters/1") {
with(
jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
@Sql("/sql/filter/test-filter.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
fun `apiV1FiltersIdDelete write_filtersで削除できる`() {
mockMvc
.delete("/api/v1/filters/1") {
with(
jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:filters"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1FiltersIdDelete readで403`() {
mockMvc
.delete("/api/v1/filters/1") {
with(
jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.andExpect { status { isForbidden() } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mastodon.media
import dev.usbharu.hideout.SpringApplication
import kotlinx.coroutines.test.runTest
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.mock.web.MockMultipartFile
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.multipart
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/actors.sql","/sql/userdetail.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class MediaTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
fun メディアをアップロードできる() = runTest {
mockMvc
.multipart("/api/v1/media") {
file(
MockMultipartFile(
"file",
"400x400.png",
"image/png",
String.javaClass.classLoader.getResourceAsStream("media/400x400.png")
)
)
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun write_mediaスコープでメディアをアップロードできる() = runTest {
mockMvc
.multipart("/api/v1/media") {
file(
MockMultipartFile(
"file",
"400x400.png",
"image/png",
String.javaClass.classLoader.getResourceAsStream("media/400x400.png")
)
)
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:media")))
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun 権限がないと403() = runTest {
mockMvc
.multipart("/api/v1/media") {
file(
MockMultipartFile(
"file",
"400x400.png",
"image/png",
String.javaClass.classLoader.getResourceAsStream("media/400x400.png")
)
)
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read")))
}
.andExpect { status { isForbidden() } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,179 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mastodon.notifications
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Notification
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class], properties = ["hideout.use-mongodb=false"])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/actors.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
//@Sql("/sql/notification/test-notifications.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
//@Sql("/sql/notification/test-mastodon_notifications.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class ExposedNotificationsApiPaginationTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@Test
fun `通知を取得できる`() = runTest {
val content = mockMvc
.get("/api/v1/notifications") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=65>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=26>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
assertThat(value.first().id).isEqualTo("65")
assertThat(value.last().id).isEqualTo("26")
}
@Test
fun maxIdを指定して通知を取得できる() = runTest {
val content = mockMvc
.get("/api/v1/notifications?max_id=26") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=25>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=1>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
assertThat(value.first().id).isEqualTo("25")
assertThat(value.last().id).isEqualTo("1")
}
@Test
fun minIdを指定して通知を取得できる() = runTest {
val content = mockMvc
.get("/api/v1/notifications?min_id=25") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=65>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=26>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
assertThat(value.first().id).isEqualTo("65")
assertThat(value.last().id).isEqualTo("26")
}
@Test
fun 結果が0件のときはページネーションのヘッダーがない() = runTest {
val content = mockMvc
.get("/api/v1/notifications?max_id=1") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
doesNotExist("Link")
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
assertThat(value).size().isZero()
}
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,179 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mastodon.notifications
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.mastodon.interfaces.api.generated.model.Notification
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class], properties = ["hideout.use-mongodb=true"])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/actors.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
//@Sql("/sql/notification/test-notifications.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class MongodbNotificationsApiPaginationTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@Test
fun `通知を取得できる`() = runTest {
val content = mockMvc
.get("/api/v1/notifications") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andDo { print() }
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=65>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=26>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
Assertions.assertThat(value.first().id).isEqualTo("65")
Assertions.assertThat(value.last().id).isEqualTo("26")
}
@Test
fun maxIdを指定して通知を取得できる() = runTest {
val content = mockMvc
.get("/api/v1/notifications?max_id=26") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=25>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=1>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
Assertions.assertThat(value.first().id).isEqualTo("25")
Assertions.assertThat(value.last().id).isEqualTo("1")
}
@Test
fun minIdを指定して通知を取得できる() = runTest {
val content = mockMvc
.get("/api/v1/notifications?min_id=25") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=65>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=26>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
Assertions.assertThat(value.first().id).isEqualTo("65")
Assertions.assertThat(value.last().id).isEqualTo("26")
}
@Test
fun 結果が0件のときはページネーションのヘッダーがない() = runTest {
val content = mockMvc
.get("/api/v1/notifications?max_id=1") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
doesNotExist("Link")
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
Assertions.assertThat(value).size().isZero()
}
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,232 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mastodon.status
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
import dev.usbharu.hideout.core.infrastructure.exposedrepository.CustomEmojis
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Reactions
import dev.usbharu.hideout.core.infrastructure.exposedrepository.toReaction
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.selectAll
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.put
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/actors.sql", "/sql/userdetail.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/posts.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/test-custom-emoji.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class StatusTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
fun 投稿できる() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun write_statusesスコープで投稿できる() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:statuses"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun 権限がないと403() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.andExpect { status { isForbidden() } }
}
@Test
@WithAnonymousUser
fun 匿名だと401() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
with(csrf())
}
.andExpect { status { isUnauthorized() } }
}
@Test
@WithAnonymousUser
fun 匿名の場合通常はcsrfが無いので403() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
content = """{"status":"hello"}"""
}
.andExpect { status { isForbidden() } }
}
@Test
fun formでも投稿できる() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_FORM_URLENCODED
param("status", "hello")
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write:statuses"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun in_reply_to_idを指定したら返信として処理される() {
mockMvc
.post("/api/v1/statuses") {
contentType = MediaType.APPLICATION_JSON
//language=JSON
content = """{
"status": "hello",
"in_reply_to_id": "1"
}"""
with(
jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write"))
)
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { jsonPath("\$.in_reply_to_id") { value("1") } }
}
@Test
fun ユニコード絵文字をリアクションできる() {
mockMvc
.put("/api/v1/statuses/1/emoji_reactions/😭") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.andDo { print() }
.asyncDispatch()
.andExpect { status { isOk() } }
val reaction =
Reactions.selectAll().where { Reactions.postId eq 1 and (Reactions.actorId eq 1) }.single().toReaction()
assertThat(reaction.unicodeEmoji).isEqualTo(UnicodeEmoji("😭"))
assertThat(reaction.postId).isEqualTo(1)
assertThat(reaction.actorId).isEqualTo(1)
}
@Test
fun 存在しない絵文字はフォールバックされる() {
mockMvc
.put("/api/v1/statuses/1/emoji_reactions/hoge") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.andDo { print() }
.asyncDispatch()
.andExpect { status { isOk() } }
val reaction =
Reactions.selectAll().where { Reactions.postId eq 1 and (Reactions.actorId eq 1) }.single().toReaction()
assertThat(reaction.unicodeEmoji).isEqualTo(UnicodeEmoji(""))
assertThat(reaction.postId).isEqualTo(1)
assertThat(reaction.actorId).isEqualTo(1)
}
@Test
fun カスタム絵文字をリアクションできる() {
mockMvc
.put("/api/v1/statuses/1/emoji_reactions/kotlin") {
with(jwt().jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_write")))
}
.andDo { print() }
.asyncDispatch()
.andExpect { status { isOk() } }
val reaction =
Reactions.leftJoin(CustomEmojis).selectAll().where { Reactions.postId eq 1 and (Reactions.actorId eq 1) }
.single()
.toReaction()
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,131 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mastodon.timelines
import dev.usbharu.hideout.SpringApplication
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class])
@Transactional
@Sql("/sql/actors.sql", "/sql/userdetail.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class TimelineApiTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun beforeEach() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
fun `apiV1TimelinesHomeGetにreadでアクセスできる`() {
mockMvc
.get("/api/v1/timelines/home") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1TimelinesHomeGetにread statusesでアクセスできる`() {
mockMvc
.get("/api/v1/timelines/home") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:statuses"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
@WithAnonymousUser
fun apiV1TimelineHomeGetに匿名でアクセスすると401() {
mockMvc
.get("/api/v1/timelines/home")
.andExpect { status { isUnauthorized() } }
}
@Test
fun apiV1TimelinesPublicGetにreadでアクセスできる() {
mockMvc
.get("/api/v1/timelines/public") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
fun `apiV1TimelinesPublicGetにread statusesでアクセスできる`() {
mockMvc
.get("/api/v1/timelines/public") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read:statuses"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
@Test
@WithAnonymousUser
fun apiV1TimeinesPublicGetに匿名でアクセスできる() {
mockMvc
.get("/api/v1/timelines/public")
.asyncDispatch()
.andExpect { status { isOk() } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,8 @@
package util
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
fun objectMapper(): ObjectMapper {
return jacksonObjectMapper()
}

View File

@ -14,9 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.core.domain.model.followtimeline package util
interface FollowTimelineRepository { import org.springframework.boot.test.context.SpringBootTest
suspend fun save(followTimeline: FollowTimeline): FollowTimeline
suspend fun delete(followTimeline: FollowTimeline) @SpringBootTest
} abstract class SpringApplicationTestBase

View File

@ -14,9 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package dev.usbharu.hideout.core.domain.model.followtimeline package util
import dev.usbharu.hideout.core.domain.model.timeline.TimelineId import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailId
class FollowTimeline(val userDetailId: UserDetailId, val timelineId: TimelineId)
object TestTransaction : Transaction {
override suspend fun <T> transaction(block: suspend () -> T): T = block()
override suspend fun <T> transaction(transactionLevel: Int, block: suspend () -> T): T = block()
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package util
//@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
//@Retention(AnnotationRetention.RUNTIME)
//@Inherited
//@MustBeDocumented
//@WithSecurityContext(factory = WithHttpSignatureSecurityContextFactory::class)
//annotation class WithHttpSignature(
// @get:AliasFor(
// annotation = WithSecurityContext::class
// ) val setupBefore: TestExecutionEvent = TestExecutionEvent.TEST_METHOD,
// val keyId: String = "https://example.com/users/test-user#pubkey",
// val url: String = "https://example.com/inbox",
// val method: String = "GET"
//)

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package util
//class WithHttpSignatureSecurityContextFactory(
// private val actorRepository: ActorRepository,
// private val transaction: Transaction
//) : WithSecurityContextFactory<WithHttpSignature> {
//
// private val securityContextStrategy = SecurityContextHolder.getContextHolderStrategy()
//
// override fun createSecurityContext(annotation: WithHttpSignature): SecurityContext = runBlocking {
// val preAuthenticatedAuthenticationToken = PreAuthenticatedAuthenticationToken(
// annotation.keyId, HttpRequest(
// URL("https://example.com/inbox"),
// HttpHeaders(mapOf()), HttpMethod.GET
// )
// )
// val httpSignatureUser = transaction.transaction {
// val findByKeyId =
// actorRepository.findByKeyId(annotation.keyId) ?: throw IllegalArgumentException(annotation.keyId)
// HttpSignatureUser(
// findByKeyId.name,
// findByKeyId.domain,
// findByKeyId.id,
// true,
// true,
// mutableListOf()
// )
// }
// preAuthenticatedAuthenticationToken.details = httpSignatureUser
// preAuthenticatedAuthenticationToken.isAuthenticated = true
// val emptyContext = securityContextStrategy.createEmptyContext()
// emptyContext.authentication = preAuthenticatedAuthenticationToken
// return@runBlocking emptyContext
// }
//
//}

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package util
//
//@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
//@Retention(AnnotationRetention.RUNTIME)
//@Inherited
//@MustBeDocumented
//@WithSecurityContext(factory = WithMockHttpSignatureSecurityContextFactory::class)
//annotation class WithMockHttpSignature(
// @get:AliasFor(
// annotation = WithSecurityContext::class
// ) val setupBefore: TestExecutionEvent = TestExecutionEvent.TEST_METHOD,
// val username: String = "test-user",
// val domain: String = "example.com",
// val keyId: String = "https://example.com/users/test-user#pubkey",
// val id: Long = 1234L,
// val url: String = "https://example.com/inbox",
// val method: String = "GET"
//)

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package util
//
//import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUser
//import dev.usbharu.httpsignature.common.HttpHeaders
//import dev.usbharu.httpsignature.common.HttpMethod
//import dev.usbharu.httpsignature.common.HttpRequest
//import org.springframework.security.core.context.SecurityContext
//import org.springframework.security.core.context.SecurityContextHolder
//import org.springframework.security.test.context.support.WithSecurityContextFactory
//import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
//import java.net.URL
//
//class WithMockHttpSignatureSecurityContextFactory :
// WithSecurityContextFactory<WithMockHttpSignature> {
//
// private val securityContextStrategy = SecurityContextHolder.getContextHolderStrategy()
//
// override fun createSecurityContext(annotation: WithMockHttpSignature): SecurityContext {
// val preAuthenticatedAuthenticationToken = PreAuthenticatedAuthenticationToken(
// annotation.keyId, HttpRequest(
// URL(annotation.url),
// HttpHeaders(mapOf()), HttpMethod.valueOf(annotation.method.uppercase())
// )
// )
// val httpSignatureUser = HttpSignatureUser(
// annotation.username,
// annotation.domain,
// annotation.id,
// true,
// true,
// mutableListOf()
// )
// preAuthenticatedAuthenticationToken.details = httpSignatureUser
// preAuthenticatedAuthenticationToken.isAuthenticated = true
// val emptyContext = securityContextStrategy.createEmptyContext()
// emptyContext.authentication = preAuthenticatedAuthenticationToken
// return emptyContext
// }
//}

View File

@ -2,6 +2,8 @@ spring:
datasource: datasource:
url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4;" url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4;"
driver-class-name: org.h2.Driver driver-class-name: org.h2.Driver
flyway:
clean-disabled: false
hideout: hideout:
url: "https://test-hideout-dev.usbharu.dev" url: "https://test-hideout-dev.usbharu.dev"
security: security:
@ -9,4 +11,4 @@ hideout:
generate: true generate: true
key-id: a key-id: a
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ==" private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB" public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,17 @@
insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key,
created_at, key_id, following, followers, instance, locked, following_count, followers_count,
posts_count, last_post_at)
VALUES (3733363, 'follow-test-user-1', 'example.com', 'follow-test-user-1-name', '',
'https://example.com/users/follow-test-user-1/inbox',
'https://example.com/users/follow-test-user-1/outbox', 'https://example.com/users/follow-test-user-1',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/follow-test-user-1#pubkey', 'https://example.com/users/follow-test-user-1/following',
'https://example.com/users/follow-test-user-1/followers', 0, false, 0, 0, 0, null),
(37335363, 'follow-test-user-2', 'example.com', 'follow-test-user-2-name', '',
'https://example.com/users/follow-test-user-2/inbox',
'https://example.com/users/follow-test-user-2/outbox', 'https://example.com/users/follow-test-user-2',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/follow-test-user-2#pubkey', 'https://example.com/users/follow-test-user-2/following',
'https://example.com/users/follow-test-user-2/followers', 0, false, 0, 0, 0, null);

View File

@ -0,0 +1,201 @@
insert into posts (id, actor_id, instance_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id, deleted, hide, move_to)
VALUES (1, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/1',
null, null, false, 'https://example.com/users/1/posts/1', false,false,null),
(2, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/2',
null, 1, false, 'https://example.com/users/1/posts/2', false,false,null),
(3, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/3',
null, null, false, 'https://example.com/users/1/posts/3', false,false,null),
(4, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/4',
null, 3, false, 'https://example.com/users/1/posts/4', false,false,null),
(5, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/5',
null, null, false, 'https://example.com/users/1/posts/5', false,false,null),
(6, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/6',
null, null, false, 'https://example.com/users/1/posts/6', false,false,null),
(7, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/7',
null, null, false, 'https://example.com/users/1/posts/7', false,false,null),
(8, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/8',
null, 7, false, 'https://example.com/users/1/posts/8', false,false,null),
(9, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/9',
null, null, false, 'https://example.com/users/1/posts/9', false,false,null),
(10, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/10',
null, 9, false, 'https://example.com/users/1/posts/10', false,false,null),
(11, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/11',
null, null, false, 'https://example.com/users/1/posts/11', false,false,null),
(12, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/12',
null, null, false, 'https://example.com/users/1/posts/12', false,false,null),
(13, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/13',
null, null, false, 'https://example.com/users/1/posts/13', false,false,null),
(14, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/14',
null, 13, false, 'https://example.com/users/1/posts/14', false,false,null),
(15, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/15',
null, null, false, 'https://example.com/users/1/posts/15', false,false,null),
(16, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/16',
null, 15, false, 'https://example.com/users/1/posts/16', false,false,null),
(17, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/17',
null, null, false, 'https://example.com/users/1/posts/17', false,false,null),
(18, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/18',
null, null, false, 'https://example.com/users/1/posts/18', false,false,null),
(19, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/19',
null, null, false, 'https://example.com/users/1/posts/19', false,false,null),
(20, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/20',
null, 19, false, 'https://example.com/users/1/posts/20', false,false,null),
(21, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/21',
null, null, false, 'https://example.com/users/1/posts/21', false,false,null),
(22, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/22',
null, 21, false, 'https://example.com/users/1/posts/22', false,false,null),
(23, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/23',
null, null, false, 'https://example.com/users/1/posts/23', false,false,null),
(24, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/24',
null, null, false, 'https://example.com/users/1/posts/24', false,false,null),
(25, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/25',
null, null, false, 'https://example.com/users/1/posts/25', false,false,null),
(26, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/26',
null, 25, false, 'https://example.com/users/1/posts/26', false,false,null),
(27, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/27',
null, null, false, 'https://example.com/users/1/posts/27', false,false,null),
(28, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/28',
null, 27, false, 'https://example.com/users/1/posts/28', false,false,null),
(29, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/29',
null, null, false, 'https://example.com/users/1/posts/29', false,false,null),
(30, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/30',
null, null, false, 'https://example.com/users/1/posts/30', false,false,null),
(31, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/31',
null, null, false, 'https://example.com/users/1/posts/31', false,false,null),
(32, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/32',
null, 31, false, 'https://example.com/users/1/posts/32', false,false,null),
(33, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/33',
null, null, false, 'https://example.com/users/1/posts/33', false,false,null),
(34, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/34',
null, 33, false, 'https://example.com/users/1/posts/34', false,false,null),
(35, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/35',
null, null, false, 'https://example.com/users/1/posts/35', false,false,null),
(36, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/36',
null, null, false, 'https://example.com/users/1/posts/36', false,false,null),
(37, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/37',
null, null, false, 'https://example.com/users/1/posts/37', false,false,null),
(38, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/38',
null, 37, false, 'https://example.com/users/1/posts/38', false,false,null),
(39, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/39',
null, null, false, 'https://example.com/users/1/posts/39', false,false,null),
(40, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/40',
null, 39, false, 'https://example.com/users/1/posts/40', false,false,null),
(41, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/41',
null, null, false, 'https://example.com/users/1/posts/41', false,false,null),
(42, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/42',
null, null, false, 'https://example.com/users/1/posts/42', false,false,null),
(43, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/43',
null, null, false, 'https://example.com/users/1/posts/43', false,false,null),
(44, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/44',
null, 43, false, 'https://example.com/users/1/posts/44', false,false,null),
(45, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/45',
null, null, false, 'https://example.com/users/1/posts/45', false,false,null),
(46, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/46',
null, 45, false, 'https://example.com/users/1/posts/46', false,false,null),
(47, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/47',
null, null, false, 'https://example.com/users/1/posts/47', false,false,null),
(48, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/48',
null, null, false, 'https://example.com/users/1/posts/48', false,false,null),
(49, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/49',
null, null, false, 'https://example.com/users/1/posts/49', false,false,null),
(50, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/50',
null, 49, false, 'https://example.com/users/1/posts/50', false,false,null),
(51, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/51',
null, null, false, 'https://example.com/users/1/posts/51', false,false,null),
(52, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/52',
null, 51, false, 'https://example.com/users/1/posts/52', false,false,null),
(53, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/53',
null, null, false, 'https://example.com/users/1/posts/53', false,false,null),
(54, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/54',
null, null, false, 'https://example.com/users/1/posts/54', false,false,null),
(55, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/55',
null, null, false, 'https://example.com/users/1/posts/55', false,false,null),
(56, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/56',
null, 55, false, 'https://example.com/users/1/posts/56', false,false,null),
(57, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/57',
null, null, false, 'https://example.com/users/1/posts/57', false,false,null),
(58, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/58',
null, 57, false, 'https://example.com/users/1/posts/58', false,false,null),
(59, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/59',
null, null, false, 'https://example.com/users/1/posts/59', false,false,null),
(60, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/60',
null, null, false, 'https://example.com/users/1/posts/60', false,false,null),
(61, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/61',
null, null, false, 'https://example.com/users/1/posts/61', false,false,null),
(62, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/62',
null, 61, false, 'https://example.com/users/1/posts/62', false,false,null),
(63, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/63',
null, null, false, 'https://example.com/users/1/posts/63', false,false,null),
(64, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/64',
null, 63, false, 'https://example.com/users/1/posts/64', false,false,null),
(65, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/65',
null, null, false, 'https://example.com/users/1/posts/65', false,false,null),
(66, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/66',
null, null, false, 'https://example.com/users/1/posts/66', false,false,null),
(67, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/67',
null, null, false, 'https://example.com/users/1/posts/67', false,false,null),
(68, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/68',
null, 67, false, 'https://example.com/users/1/posts/68', false,false,null),
(69, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/69',
null, null, false, 'https://example.com/users/1/posts/69', false,false,null),
(70, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/70',
null, 69, false, 'https://example.com/users/1/posts/70', false,false,null),
(71, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/71',
null, null, false, 'https://example.com/users/1/posts/71', false,false,null),
(72, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/72',
null, null, false, 'https://example.com/users/1/posts/72', false,false,null),
(73, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/73',
null, null, false, 'https://example.com/users/1/posts/73', false,false,null),
(74, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/74',
null, 73, false, 'https://example.com/users/1/posts/74', false,false,null),
(75, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/75',
null, null, false, 'https://example.com/users/1/posts/75', false,false,null),
(76, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/76',
null, 75, false, 'https://example.com/users/1/posts/76', false,false,null),
(77, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/77',
null, null, false, 'https://example.com/users/1/posts/77', false,false,null),
(78, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/78',
null, null, false, 'https://example.com/users/1/posts/78', false,false,null),
(79, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/79',
null, null, false, 'https://example.com/users/1/posts/79', false,false,null),
(80, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/80',
null, 79, false, 'https://example.com/users/1/posts/80', false,false,null),
(81, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/81',
null, null, false, 'https://example.com/users/1/posts/81', false,false,null),
(82, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/82',
null, 81, false, 'https://example.com/users/1/posts/82', false,false,null),
(83, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/83',
null, null, false, 'https://example.com/users/1/posts/83', false,false,null),
(84, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/84',
null, null, false, 'https://example.com/users/1/posts/84', false,false,null),
(85, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/85',
null, null, false, 'https://example.com/users/1/posts/85', false,false,null),
(86, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/86',
null, 85, false, 'https://example.com/users/1/posts/86', false,false,null),
(87, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/87',
null, null, false, 'https://example.com/users/1/posts/87', false,false,null),
(88, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/88',
null, 87, false, 'https://example.com/users/1/posts/88', false,false,null),
(89, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/89',
null, null, false, 'https://example.com/users/1/posts/89', false,false,null),
(90, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/90',
null, null, false, 'https://example.com/users/1/posts/90', false,false,null),
(91, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/91',
null, null, false, 'https://example.com/users/1/posts/91', false,false,null),
(92, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/92',
null, 91, false, 'https://example.com/users/1/posts/92', false,false,null),
(93, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/93',
null, null, false, 'https://example.com/users/1/posts/93', false,false,null),
(94, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/94',
null, 93, false, 'https://example.com/users/1/posts/94', false,false,null),
(95, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/95',
null, null, false, 'https://example.com/users/1/posts/95', false,false,null),
(96, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/96',
null, null, false, 'https://example.com/users/1/posts/96', false,false,null),
(97, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/97',
null, null, false, 'https://example.com/users/1/posts/97', false,false,null),
(98, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/98',
null, 97, false, 'https://example.com/users/1/posts/98', false,false,null),
(99, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'FOLLOWERS', 'https://example.com/users/1/posts/99',
null, null, false, 'https://example.com/users/1/posts/99', false,false,null),
(100, 1,1,null, '<p>this is test</p>', 'this is test', current_timestamp, 'PUBLIC', 'https://example.com/users/1/posts/100',
null, 99, false, 'https://example.com/users/1/posts/100', false,false,null);

View File

@ -0,0 +1,4 @@
insert into filters (id, user_id, name, context, action)
VALUES (1, 1, 'test filter', 'HOME', 'WARN');
insert into filter_keywords(id, filter_id, keyword, mode)
VALUES (1, 1, 'hoge', 'NONE')

View File

@ -0,0 +1,28 @@
insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key,
created_at, key_id, following, followers, instance, locked, following_count, followers_count,
posts_count, last_post_at)
VALUES (8, 'test-user8', 'example.com', 'Im test-user8.', 'THis account is test-user8.',
'https://example.com/users/test-user8/inbox',
'https://example.com/users/test-user8/outbox', 'https://example.com/users/test-user8',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user8#pubkey', 'https://example.com/users/test-user8/following',
'https://example.com/users/test-user8/followers', 0, false, 0, 0, 0, null),
(9, 'test-user9', 'follower.example.com', 'Im test-user9.', 'THis account is test-user9.',
'https://follower.example.com/users/test-user9/inbox',
'https://follower.example.com/users/test-user9/outbox', 'https://follower.example.com/users/test-user9',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
null, 12345678,
'https://follower.example.com/users/test-user9#pubkey',
'https://follower.example.com/users/test-user9/following',
'https://follower.example.com/users/test-user9/followers', 0, false, 0, 0, 0, null);
insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request,
ignore_follow_request)
VALUES (9, 8, true, false, false, false, false);
insert into POSTS (ID, ACTOR_ID, OVERVIEW, CONTENT, TEXT, CREATED_AT, VISIBILITY, URL, REPLY_ID, REPOST_ID, SENSITIVE,
AP_ID)
VALUES (1239, 8, null, '<p>test post</p>', 'test post', 12345680, 2, 'https://example.com/users/test-user8/posts/1239',
null, null, false,
'https://example.com/users/test-user8/posts/1239');

View File

@ -0,0 +1,29 @@
insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key,
created_at, key_id, following, followers, instance, locked, following_count, followers_count,
posts_count, last_post_at)
VALUES (4, 'test-user4', 'example.com', 'Im test user4.', 'THis account is test user4.',
'https://example.com/users/test-user4/inbox',
'https://example.com/users/test-user4/outbox', 'https://example.com/users/test-user4',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user4#pubkey', 'https://example.com/users/test-user4/following',
'https://example.com/users/test-user4/followers', 0, false, 0, 0, 0, null),
(5, 'test-user5', 'follower.example.com', 'Im test user5.', 'THis account is test user5.',
'https://follower.example.com/users/test-user5/inbox',
'https://follower.example.com/users/test-user5/outbox', 'https://follower.example.com/users/test-user5',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
null, 12345678,
'https://follower.example.com/users/test-user5#pubkey',
'https://follower.example.com/users/test-user5/following',
'https://follower.example.com/users/test-user5/followers', 0, false, 0, 0, 0, null);
insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request,
ignore_follow_request)
VALUES (5, 4, true, false, false, false, false);
insert into POSTS (ID, "actor_id", OVERVIEW, CONTENT, TEXT, "created_at", VISIBILITY, URL, "repost_id", "reply_id",
SENSITIVE,
AP_ID)
VALUES (1237, 4, null, '<p>test post</p>', 'test post', 12345680, 0, 'https://example.com/users/test-user4/posts/1237',
null, null, false,
'https://example.com/users/test-user4/posts/1237');

View File

@ -0,0 +1,29 @@
insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key,
created_at, key_id, following, followers, instance, locked, following_count, followers_count,
posts_count, last_post_at)
VALUES (6, 'test-user6', 'example.com', 'Im test-user6.', 'THis account is test-user6.',
'https://example.com/users/test-user6/inbox',
'https://example.com/users/test-user6/outbox', 'https://example.com/users/test-user6',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user6#pubkey', 'https://example.com/users/test-user6/following',
'https://example.com/users/test-user6/followers', 0, false, 0, 0, 0, null),
(7, 'test-user7', 'follower.example.com', 'Im test-user7.', 'THis account is test-user7.',
'https://follower.example.com/users/test-user7/inbox',
'https://follower.example.com/users/test-user7/outbox', 'https://follower.example.com/users/test-user7',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
null, 12345678,
'https://follower.example.com/users/test-user7#pubkey',
'https://follower.example.com/users/test-user7/following',
'https://follower.example.com/users/test-user7/followers', 0, false, 0, 0, 0, null);
insert into relationships (actor_id, target_actor_id, following, blocking, muting, follow_request,
ignore_follow_request)
VALUES (7, 6, true, false, false, false, false);
insert into POSTS (ID, "actor_ID", OVERVIEW, CONTENT, TEXT, "CREATED_AT", VISIBILITY, URL, "REPOST_ID", "REPLY_ID",
SENSITIVE,
AP_ID)
VALUES (1238, 6, null, '<p>test post</p>', 'test post', 12345680, 1, 'https://example.com/users/test-user6/posts/1238',
null, null, false,
'https://example.com/users/test-user6/posts/1238');

View File

@ -0,0 +1,25 @@
insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key,
created_at, key_id, following, followers, instance, locked, following_count, followers_count,
posts_count, last_post_at)
VALUES (11, 'test-user11', 'example.com', 'Im test-user11.', 'THis account is test-user11.',
'https://example.com/users/test-user11/inbox',
'https://example.com/users/test-user11/outbox', 'https://example.com/users/test-user11',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user11#pubkey', 'https://example.com/users/test-user11/following',
'https://example.com/users/test-user11/followers', 0, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id,
deleted)
VALUES (1242, 11, null, '<p>test post</p>', 'test post', 12345680, 0,
'https://example.com/users/test-user11/posts/1242', null, null, false,
'https://example.com/users/test-user11/posts/1242', false);
insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH, MIME_TYPE, DESCRIPTION)
VALUES (1, 'test-media', 'https://example.com/media/test-media.png', null, null, 0, null, 'image/png', null),
(2, 'test-media2', 'https://example.com/media/test-media2.png', null, null, 0, null, 'image/png', null);
insert into POSTS_MEDIA(POST_ID, MEDIA_ID)
VALUES (1242, 1),
(1242, 2);

View File

@ -0,0 +1,20 @@
insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key,
created_at, key_id, following, followers, instance, locked, following_count, followers_count,
posts_count, last_post_at)
VALUES (10, 'test-user10', 'example.com', 'Im test-user10.', 'THis account is test-user10.',
'https://example.com/users/test-user10/inbox',
'https://example.com/users/test-user10/outbox', 'https://example.com/users/test-user10',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user10#pubkey', 'https://example.com/users/test-user10/following',
'https://example.com/users/test-user10/followers', 0, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id,
deleted)
VALUES (1240, 10, null, '<p>test post</p>', 'test post', 12345680, 0,
'https://example.com/users/test-user10/posts/1240', null, null, false,
'https://example.com/users/test-user10/posts/1240', false),
(1241, 10, null, '<p>test post</p>', 'test post', 12345680, 0,
'https://example.com/users/test-user10/posts/1241', null, 1240, false,
'https://example.com/users/test-user10/posts/1241', false);

View File

@ -0,0 +1,17 @@
insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key,
created_at, key_id, following, followers, instance, locked, following_count, followers_count,
posts_count, last_post_at)
VALUES (3, 'test-user3', 'example.com', 'Im test user3.', 'THis account is test user3.',
'https://example.com/users/test-user3/inbox',
'https://example.com/users/test-user3/outbox', 'https://example.com/users/test-user3',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user3#pubkey', 'https://example.com/users/test-user3/following',
'https://example.com/users/test-user3/followers', 0, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id,
deleted)
VALUES (1236, 3, null, '<p>test post</p>', 'test post', 12345680, 2, 'https://example.com/users/test-user3/posts/1236',
null, null, false,
'https://example.com/users/test-user3/posts/1236', false)

View File

@ -0,0 +1,17 @@
insert into actors (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, created_at,
key_id, following, followers, instance, locked, following_count, followers_count, posts_count,
last_post_at)
VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test user.',
'https://example.com/users/test-user/inbox',
'https://example.com/users/test-user/outbox', 'https://example.com/users/test-user',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following',
'https://example.com/users/test-users/followers', 0, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id,
deleted)
VALUES (1234, 1, null, '<p>test post</p>', 'test post', 12345680, 0, 'https://example.com/users/test-user/posts/1234',
null, null, false,
'https://example.com/users/test-user/posts/1234', false)

View File

@ -0,0 +1,17 @@
insert into actors (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, created_at,
key_id, following, followers, instance, locked, following_count, followers_count, posts_count,
last_post_at)
VALUES (2, 'test-user2', 'example.com', 'Im test user2.', 'THis account is test user2.',
'https://example.com/users/test-user2/inbox',
'https://example.com/users/test-user2/outbox', 'https://example.com/users/test-user2',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following',
'https://example.com/users/test-user2/followers', 0, false, 0, 0, 0, null);
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id,
deleted)
VALUES (1235, 2, null, '<p>test post</p>', 'test post', 12345680, 1, 'https://example.com/users/test-user2/posts/1235',
null, null, false,
'https://example.com/users/test-user2/posts/1235', false)

View File

@ -0,0 +1,66 @@
insert into mastodon_notifications (id, user_id, type, created_at, account_id, status_id, report_id, relationship_serverance_event_id)
values (1, 1, 'follow', current_timestamp, 2, null, null, null),
(2, 1, 'follow', current_timestamp, 2, null, null, null),
(3, 1, 'follow', current_timestamp, 2, null, null, null),
(4, 1, 'follow', current_timestamp, 2, null, null, null),
(5, 1, 'follow', current_timestamp, 2, null, null, null),
(6, 1, 'follow', current_timestamp, 2, null, null, null),
(7, 1, 'follow', current_timestamp, 2, null, null, null),
(8, 1, 'follow', current_timestamp, 2, null, null, null),
(9, 1, 'follow', current_timestamp, 2, null, null, null),
(10, 1, 'follow', current_timestamp, 2, null, null, null),
(11, 1, 'follow', current_timestamp, 2, null, null, null),
(12, 1, 'follow', current_timestamp, 2, null, null, null),
(13, 1, 'follow', current_timestamp, 2, null, null, null),
(14, 1, 'follow', current_timestamp, 2, null, null, null),
(15, 1, 'follow', current_timestamp, 2, null, null, null),
(16, 1, 'follow', current_timestamp, 2, null, null, null),
(17, 1, 'follow', current_timestamp, 2, null, null, null),
(18, 1, 'follow', current_timestamp, 2, null, null, null),
(19, 1, 'follow', current_timestamp, 2, null, null, null),
(20, 1, 'follow', current_timestamp, 2, null, null, null),
(21, 1, 'follow', current_timestamp, 2, null, null, null),
(22, 1, 'follow', current_timestamp, 2, null, null, null),
(23, 1, 'follow', current_timestamp, 2, null, null, null),
(24, 1, 'follow', current_timestamp, 2, null, null, null),
(25, 1, 'follow', current_timestamp, 2, null, null, null),
(26, 1, 'follow', current_timestamp, 2, null, null, null),
(27, 1, 'follow', current_timestamp, 2, null, null, null),
(28, 1, 'follow', current_timestamp, 2, null, null, null),
(29, 1, 'follow', current_timestamp, 2, null, null, null),
(30, 1, 'follow', current_timestamp, 2, null, null, null),
(31, 1, 'follow', current_timestamp, 2, null, null, null),
(32, 1, 'follow', current_timestamp, 2, null, null, null),
(33, 1, 'follow', current_timestamp, 2, null, null, null),
(34, 1, 'follow', current_timestamp, 2, null, null, null),
(35, 1, 'follow', current_timestamp, 2, null, null, null),
(36, 1, 'follow', current_timestamp, 2, null, null, null),
(37, 1, 'follow', current_timestamp, 2, null, null, null),
(38, 1, 'follow', current_timestamp, 2, null, null, null),
(39, 1, 'follow', current_timestamp, 2, null, null, null),
(40, 1, 'follow', current_timestamp, 2, null, null, null),
(41, 1, 'follow', current_timestamp, 2, null, null, null),
(42, 1, 'follow', current_timestamp, 2, null, null, null),
(43, 1, 'follow', current_timestamp, 2, null, null, null),
(44, 1, 'follow', current_timestamp, 2, null, null, null),
(45, 1, 'follow', current_timestamp, 2, null, null, null),
(46, 1, 'follow', current_timestamp, 2, null, null, null),
(47, 1, 'follow', current_timestamp, 2, null, null, null),
(48, 1, 'follow', current_timestamp, 2, null, null, null),
(49, 1, 'follow', current_timestamp, 2, null, null, null),
(50, 1, 'follow', current_timestamp, 2, null, null, null),
(51, 1, 'follow', current_timestamp, 2, null, null, null),
(52, 1, 'follow', current_timestamp, 2, null, null, null),
(53, 1, 'follow', current_timestamp, 2, null, null, null),
(54, 1, 'follow', current_timestamp, 2, null, null, null),
(55, 1, 'follow', current_timestamp, 2, null, null, null),
(56, 1, 'follow', current_timestamp, 2, null, null, null),
(57, 1, 'follow', current_timestamp, 2, null, null, null),
(58, 1, 'follow', current_timestamp, 2, null, null, null),
(59, 1, 'follow', current_timestamp, 2, null, null, null),
(60, 1, 'follow', current_timestamp, 2, null, null, null),
(61, 1, 'follow', current_timestamp, 2, null, null, null),
(62, 1, 'follow', current_timestamp, 2, null, null, null),
(63, 1, 'follow', current_timestamp, 2, null, null, null),
(64, 1, 'follow', current_timestamp, 2, null, null, null),
(65, 1, 'follow', current_timestamp, 2, null, null, null);

View File

@ -0,0 +1,66 @@
insert into notifications(id, type, user_id, source_actor_id, post_id, text, reaction_id, created_at)
VALUES (1, 'follow', 1, 2, null, null, null, current_timestamp),
(2, 'follow', 1, 2, null, null, null, current_timestamp),
(3, 'follow', 1, 2, null, null, null, current_timestamp),
(4, 'follow', 1, 2, null, null, null, current_timestamp),
(5, 'follow', 1, 2, null, null, null, current_timestamp),
(6, 'follow', 1, 2, null, null, null, current_timestamp),
(7, 'follow', 1, 2, null, null, null, current_timestamp),
(8, 'follow', 1, 2, null, null, null, current_timestamp),
(9, 'follow', 1, 2, null, null, null, current_timestamp),
(10, 'follow', 1, 2, null, null, null, current_timestamp),
(11, 'follow', 1, 2, null, null, null, current_timestamp),
(12, 'follow', 1, 2, null, null, null, current_timestamp),
(13, 'follow', 1, 2, null, null, null, current_timestamp),
(14, 'follow', 1, 2, null, null, null, current_timestamp),
(15, 'follow', 1, 2, null, null, null, current_timestamp),
(16, 'follow', 1, 2, null, null, null, current_timestamp),
(17, 'follow', 1, 2, null, null, null, current_timestamp),
(18, 'follow', 1, 2, null, null, null, current_timestamp),
(19, 'follow', 1, 2, null, null, null, current_timestamp),
(20, 'follow', 1, 2, null, null, null, current_timestamp),
(21, 'follow', 1, 2, null, null, null, current_timestamp),
(22, 'follow', 1, 2, null, null, null, current_timestamp),
(23, 'follow', 1, 2, null, null, null, current_timestamp),
(24, 'follow', 1, 2, null, null, null, current_timestamp),
(25, 'follow', 1, 2, null, null, null, current_timestamp),
(26, 'follow', 1, 2, null, null, null, current_timestamp),
(27, 'follow', 1, 2, null, null, null, current_timestamp),
(28, 'follow', 1, 2, null, null, null, current_timestamp),
(29, 'follow', 1, 2, null, null, null, current_timestamp),
(30, 'follow', 1, 2, null, null, null, current_timestamp),
(31, 'follow', 1, 2, null, null, null, current_timestamp),
(32, 'follow', 1, 2, null, null, null, current_timestamp),
(33, 'follow', 1, 2, null, null, null, current_timestamp),
(34, 'follow', 1, 2, null, null, null, current_timestamp),
(35, 'follow', 1, 2, null, null, null, current_timestamp),
(36, 'follow', 1, 2, null, null, null, current_timestamp),
(37, 'follow', 1, 2, null, null, null, current_timestamp),
(38, 'follow', 1, 2, null, null, null, current_timestamp),
(39, 'follow', 1, 2, null, null, null, current_timestamp),
(40, 'follow', 1, 2, null, null, null, current_timestamp),
(41, 'follow', 1, 2, null, null, null, current_timestamp),
(42, 'follow', 1, 2, null, null, null, current_timestamp),
(43, 'follow', 1, 2, null, null, null, current_timestamp),
(44, 'follow', 1, 2, null, null, null, current_timestamp),
(45, 'follow', 1, 2, null, null, null, current_timestamp),
(46, 'follow', 1, 2, null, null, null, current_timestamp),
(47, 'follow', 1, 2, null, null, null, current_timestamp),
(48, 'follow', 1, 2, null, null, null, current_timestamp),
(49, 'follow', 1, 2, null, null, null, current_timestamp),
(50, 'follow', 1, 2, null, null, null, current_timestamp),
(51, 'follow', 1, 2, null, null, null, current_timestamp),
(52, 'follow', 1, 2, null, null, null, current_timestamp),
(53, 'follow', 1, 2, null, null, null, current_timestamp),
(54, 'follow', 1, 2, null, null, null, current_timestamp),
(55, 'follow', 1, 2, null, null, null, current_timestamp),
(56, 'follow', 1, 2, null, null, null, current_timestamp),
(57, 'follow', 1, 2, null, null, null, current_timestamp),
(58, 'follow', 1, 2, null, null, null, current_timestamp),
(59, 'follow', 1, 2, null, null, null, current_timestamp),
(60, 'follow', 1, 2, null, null, null, current_timestamp),
(61, 'follow', 1, 2, null, null, null, current_timestamp),
(62, 'follow', 1, 2, null, null, null, current_timestamp),
(63, 'follow', 1, 2, null, null, null, current_timestamp),
(64, 'follow', 1, 2, null, null, null, current_timestamp),
(65, 'follow', 1, 2, null, null, null, current_timestamp);

View File

@ -1,15 +1,15 @@
insert into posts (id, actor_id, instance_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, insert into posts (id, actor_id, instance_id, overview, content, text, created_at, visibility, url, repost_id, reply_id,
sensitive, ap_id, deleted, hide, move_to) sensitive, ap_id, deleted, hide, move_to)
values (1, 1, 1, null, 'content', 'text', current_timestamp, 'PUBLIC', 'https://example.com', null, null, false, values (1, 1, 1, null, 'content', 'text', current_timestamp, 'PUBLIC', 'https://example.com', null, null, false,
'https://example.com', false, false, null), 'https://example.com/1', false, false, null),
(2, 2, 2, null, 'content', 'text', current_timestamp, 'FOLLOWERS', 'https://example.com', null, null, false, (2, 2, 2, null, 'content', 'text', current_timestamp, 'FOLLOWERS', 'https://example.com', null, null, false,
'https://example.com', false, false, null), 'https://example.com/2', false, false, null),
(3, 3, 1, null, 'content', 'text', current_timestamp, 'PUBLIC', 'https://example.com', null, null, false, (3, 3, 1, null, 'content', 'text', current_timestamp, 'PUBLIC', 'https://example.com', null, null, false,
'https://example.com', false, false, null), 'https://example.com/3', false, false, null),
(4, 4, 1, null, 'content', 'text', current_timestamp, 'FOLLOWERS', 'https://example.com', null, null, false, (4, 4, 1, null, 'content', 'text', current_timestamp, 'FOLLOWERS', 'https://example.com', null, null, false,
'https://example.com', false, false, null), 'https://example.com/4', false, false, null),
(5, 4, 1, null, 'content', 'text', current_timestamp, 'DIRECT', 'https://example.com', null, null, false, (5, 4, 1, null, 'content', 'text', current_timestamp, 'DIRECT', 'https://example.com', null, null, false,
'https://example.com', false, false, null); 'https://example.com/5', false, false, null);
insert into posts_visible_actors(post_id, actor_id) insert into posts_visible_actors(post_id, actor_id)
VALUES (5, 2); VALUES (5, 2);

View File

@ -0,0 +1,3 @@
insert into emojis(id, name, domain, instance_id, url, category, created_at)
VALUES (1, 'kotlin', 'example.com', null, 'https://example.com/emojis/kotlin', null,
TIMESTAMP '2024-01-08 07:51:30.036Z');

View File

@ -0,0 +1,4 @@
insert into posts (id, actor_id, instance_id, overview, content, text, created_at, visibility, url, repost_id, reply_id,
sensitive, ap_id, deleted, hide, move_to)
VALUES (1, 1, 0, null,'<p>test post</p>', 'hello', 1234455, 0, 'https://localhost/users/1/posts/1', null, null, false,
'https://users/1/posts/1');

View File

@ -0,0 +1,11 @@
insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key,
created_at, key_id, following, followers, instance, locked, following_count, followers_count,
posts_count, last_post_at)
VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test user.',
'https://example.com/users/test-user/inbox',
'https://example.com/users/test-user/outbox', 'https://example.com/users/test-user',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following',
'https://example.com/users/test-users/followers', 0, false, 0, 0, 0, null);

View File

@ -0,0 +1,10 @@
insert into "actors" (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key,
created_at, key_id, following, followers, instance, locked, following_count, followers_count,
posts_count, last_post_at)
VALUES (2, 'test-user2', 'example.com', 'Im test user.', 'THis account is test user.',
'https://example.com/users/test-user2/inbox',
'https://example.com/users/test-user2/outbox', 'https://example.com/users/test-user2',
'-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----',
'-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678,
'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following',
'https://example.com/users/test-user2s/followers', 0, false, 0, 0, 0, null);

View File

@ -0,0 +1,2 @@
insert into user_details(id, actor_id, password, auto_accept_followee_follow_request, last_migration, home_timeline_id)
VALUES (1, 1, 'veeeeeeeeeeryStrongPassword', false, null, null);

View File

@ -124,7 +124,7 @@ grpc-kotlin = ["grpc-kotlin-stub", "grpc-netty", "grpc-protobuf", "protobuf-kotl
[plugins] [plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
spring-boot = { id = "org.springframework.boot", version = "3.3.3" } spring-boot = { id = "org.springframework.boot", version = "3.3.4" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
kover = { id = "org.jetbrains.kotlinx.kover", version = "0.8.3" } kover = { id = "org.jetbrains.kotlinx.kover", version = "0.8.3" }

View File

@ -1,6 +1,10 @@
import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
plugins { plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
id("maven-publish") id("maven-publish")
alias(libs.plugins.kover)
alias(libs.plugins.detekt)
} }
@ -27,6 +31,8 @@ subprojects {
apply { apply {
plugin("org.jetbrains.kotlin.jvm") plugin("org.jetbrains.kotlin.jvm")
plugin("maven-publish") plugin("maven-publish")
plugin(rootProject.libs.plugins.kover.get().pluginId)
plugin(rootProject.libs.plugins.detekt.get().pluginId)
} }
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(21)
@ -35,12 +41,42 @@ subprojects {
dependencies { dependencies {
implementation("org.slf4j:slf4j-api:2.0.15") implementation("org.slf4j:slf4j-api:2.0.15")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.3") testImplementation("org.junit.jupiter:junit-jupiter:5.10.3")
detektPlugins(rootProject.libs.detekt.formatting)
} }
tasks.test { detekt {
useJUnitPlatform() parallel = true
config.setFrom(files("$rootDir/../detekt.yml"))
buildUponDefaultConfig = true
basePath = "${projectDir}/src/main/kotlin"
autoCorrect = true
}
project.gradle.taskGraph.whenReady {
if (this.hasTask(":koverGenerateArtifact")) {
val task = this.allTasks.find { it.name == "test" }
val verificationTask = task as VerificationTask
verificationTask.ignoreFailures = true
}
}
tasks {
withType<io.gitlab.arturbosch.detekt.Detekt> {
exclude("**/generated/**")
setSource("src/main/kotlin")
exclude("build/")
configureEach {
exclude("**/org/koin/ksp/generated/**", "**/generated/**")
}
}
withType<io.gitlab.arturbosch.detekt.DetektCreateBaselineTask>() {
configureEach {
exclude("**/org/koin/ksp/generated/**", "**/generated/**")
}
}
withType<Test> {
useJUnitPlatform()
}
} }
publishing { publishing {
@ -69,4 +105,63 @@ subprojects {
} }
} }
} }
} }
dependencies {
kover(project(":owl-broker"))
kover(project(":owl-broker:owl-broker-mongodb"))
kover(project(":owl-common"))
kover(project(":owl-common:owl-common-serialize-jackson"))
kover(project(":owl-consumer"))
kover(project(":owl-producer"))
kover(project(":owl-producer:owl-producer-api"))
kover(project(":owl-producer:owl-producer-default"))
kover(project(":owl-producer:owl-producer-embedded"))
}
detekt {
parallel = true
config.setFrom(files("../detekt.yml"))
buildUponDefaultConfig = true
basePath = "${projectDir}/src/main/kotlin"
autoCorrect = true
}
project.gradle.taskGraph.whenReady {
if (this.hasTask(":koverGenerateArtifact")) {
val task = this.allTasks.find { it.name == "test" }
val verificationTask = task as VerificationTask
verificationTask.ignoreFailures = true
}
}
kover {
currentProject {
sources {
excludedSourceSets.addAll("grpc", "grpckt")
}
}
reports {
verify {
rule {
bound {
minValue = 50
coverageUnits = CoverageUnit.INSTRUCTION
}
}
}
total {
xml {
title = "Owl"
xmlFile = file("$buildDir/reports/kover/owl.xml")
}
filters {
excludes {
packages("dev.usbharu.owl.generated")
}
}
}
}
}

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -35,7 +35,7 @@ protobuf {
artifact = libs.protoc.gen.grpc.java.get().toString() artifact = libs.protoc.gen.grpc.java.get().toString()
} }
create("grpckt") { create("grpckt") {
artifact = libs.protoc.gen.grpc.kotlin.get().toString() + "jdk8@jar" artifact = libs.protoc.gen.grpc.kotlin.get().toString() + ":jdk8@jar"
} }
} }
generateProtoTasks { generateProtoTasks {

View File

@ -32,7 +32,6 @@ import org.koin.dsl.module
class MongoModuleContext : ModuleContext { class MongoModuleContext : ModuleContext {
override fun module(): Module { override fun module(): Module {
return module { return module {
single { single {
val clientSettings = val clientSettings =
@ -47,7 +46,6 @@ class MongoModuleContext : ModuleContext {
) )
.uuidRepresentation(UuidRepresentation.STANDARD).build() .uuidRepresentation(UuidRepresentation.STANDARD).build()
MongoClient.create(clientSettings) MongoClient.create(clientSettings)
.getDatabase(System.getProperty("owl.broker.mongo.database", "mongo-test")) .getDatabase(System.getProperty("owl.broker.mongo.database", "mongo-test"))
} }
@ -59,4 +57,4 @@ class MongoModuleContext : ModuleContext {
single<TaskResultRepository> { MongodbTaskResultRepository(get(), get()) } single<TaskResultRepository> { MongodbTaskResultRepository(get(), get()) }
} }
} }
} }

View File

@ -33,7 +33,11 @@ class MongodbConsumerRepository(database: MongoDatabase) : ConsumerRepository {
private val collection = database.getCollection<ConsumerMongodb>("consumers") private val collection = database.getCollection<ConsumerMongodb>("consumers")
override suspend fun save(consumer: Consumer): Consumer = withContext(Dispatchers.IO) { override suspend fun save(consumer: Consumer): Consumer = withContext(Dispatchers.IO) {
collection.replaceOne(Filters.eq("_id", consumer.id.toString()), ConsumerMongodb.of(consumer), ReplaceOptions().upsert(true)) collection.replaceOne(
Filters.eq("_id", consumer.id.toString()),
ConsumerMongodb.of(consumer),
ReplaceOptions().upsert(true)
)
return@withContext consumer return@withContext consumer
} }
@ -49,15 +53,19 @@ data class ConsumerMongodb(
val name: String, val name: String,
val hostname: String, val hostname: String,
val tasks: List<String> val tasks: List<String>
){ ) {
fun toConsumer():Consumer{ fun toConsumer(): Consumer {
return Consumer( return Consumer(
UUID.fromString(id), name, hostname, tasks UUID.fromString(id),
name,
hostname,
tasks
) )
} }
companion object{
fun of(consumer: Consumer):ConsumerMongodb{ companion object {
fun of(consumer: Consumer): ConsumerMongodb {
return ConsumerMongodb( return ConsumerMongodb(
consumer.id.toString(), consumer.id.toString(),
consumer.name, consumer.name,
@ -66,4 +74,4 @@ data class ConsumerMongodb(
) )
} }
} }
} }

View File

@ -68,4 +68,4 @@ data class ProducerMongodb(
) )
} }
} }
} }

View File

@ -48,7 +48,8 @@ class MongodbQueuedTaskRepository(
override suspend fun save(queuedTask: QueuedTask): QueuedTask { override suspend fun save(queuedTask: QueuedTask): QueuedTask {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
collection.replaceOne( collection.replaceOne(
eq("_id", queuedTask.task.id.toString()), QueuedTaskMongodb.of(propertySerializerFactory, queuedTask), eq("_id", queuedTask.task.id.toString()),
QueuedTaskMongodb.of(propertySerializerFactory, queuedTask),
ReplaceOptions().upsert(true) ReplaceOptions().upsert(true)
) )
} }
@ -57,7 +58,6 @@ class MongodbQueuedTaskRepository(
override suspend fun findByTaskIdAndAssignedConsumerIsNullAndUpdate(id: UUID, update: QueuedTask): QueuedTask { override suspend fun findByTaskIdAndAssignedConsumerIsNullAndUpdate(id: UUID, update: QueuedTask): QueuedTask {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val findOneAndUpdate = collection.findOneAndUpdate( val findOneAndUpdate = collection.findOneAndUpdate(
and( and(
eq("_id", id.toString()), eq("_id", id.toString()),
@ -108,7 +108,7 @@ data class QueuedTaskMongodb(
val task: TaskMongodb, val task: TaskMongodb,
val attempt: Int, val attempt: Int,
val queuedAt: Instant, val queuedAt: Instant,
val priority:Int, val priority: Int,
val isActive: Boolean, val isActive: Boolean,
val timeoutAt: Instant?, val timeoutAt: Instant?,
val assignedConsumer: String?, val assignedConsumer: String?,
@ -155,14 +155,14 @@ data class QueuedTaskMongodb(
companion object { companion object {
fun of(propertySerializerFactory: PropertySerializerFactory, task: Task): TaskMongodb { fun of(propertySerializerFactory: PropertySerializerFactory, task: Task): TaskMongodb {
return TaskMongodb( return TaskMongodb(
task.name, name = task.name,
task.id.toString(), id = task.id.toString(),
task.publishProducerId.toString(), publishProducerId = task.publishProducerId.toString(),
task.publishedAt, publishedAt = task.publishedAt,
task.nextRetry, nextRetry = task.nextRetry,
task.completedAt, completedAt = task.completedAt,
task.attempt, attempt = task.attempt,
PropertySerializeUtils.serialize(propertySerializerFactory, task.properties) properties = PropertySerializeUtils.serialize(propertySerializerFactory, task.properties)
) )
} }
} }
@ -171,16 +171,16 @@ data class QueuedTaskMongodb(
companion object { companion object {
fun of(propertySerializerFactory: PropertySerializerFactory, queuedTask: QueuedTask): QueuedTaskMongodb { fun of(propertySerializerFactory: PropertySerializerFactory, queuedTask: QueuedTask): QueuedTaskMongodb {
return QueuedTaskMongodb( return QueuedTaskMongodb(
queuedTask.task.id.toString(), id = queuedTask.task.id.toString(),
TaskMongodb.of(propertySerializerFactory, queuedTask.task), task = TaskMongodb.of(propertySerializerFactory, queuedTask.task),
queuedTask.attempt, attempt = queuedTask.attempt,
queuedTask.queuedAt, queuedAt = queuedTask.queuedAt,
queuedTask.priority, priority = queuedTask.priority,
queuedTask.isActive, isActive = queuedTask.isActive,
queuedTask.timeoutAt, timeoutAt = queuedTask.timeoutAt,
queuedTask.assignedConsumer?.toString(), assignedConsumer = queuedTask.assignedConsumer?.toString(),
queuedTask.assignedAt assignedAt = queuedTask.assignedAt
) )
} }
} }
} }

View File

@ -41,7 +41,7 @@ class MongodbTaskDefinitionRepository(database: MongoDatabase) : TaskDefinitionR
} }
override suspend fun deleteByName(name: String): Unit = withContext(Dispatchers.IO) { override suspend fun deleteByName(name: String): Unit = withContext(Dispatchers.IO) {
collection.deleteOne(Filters.eq("_id",name)) collection.deleteOne(Filters.eq("_id", name))
} }
override suspend fun findByName(name: String): TaskDefinition? = withContext(Dispatchers.IO) { override suspend fun findByName(name: String): TaskDefinition? = withContext(Dispatchers.IO) {
@ -82,4 +82,4 @@ data class TaskDefinitionMongodb(
) )
} }
} }
} }

View File

@ -36,27 +36,29 @@ import org.bson.codecs.pojo.annotations.BsonRepresentation
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
class MongodbTaskRepository(database: MongoDatabase, private val propertySerializerFactory: PropertySerializerFactory) : class MongodbTaskRepository(database: MongoDatabase, private val propertySerializerFactory: PropertySerializerFactory) :
TaskRepository { TaskRepository {
private val collection = database.getCollection<TaskMongodb>("tasks") private val collection = database.getCollection<TaskMongodb>("tasks")
override suspend fun save(task: Task): Task = withContext(Dispatchers.IO) { override suspend fun save(task: Task): Task = withContext(Dispatchers.IO) {
collection.replaceOne( collection.replaceOne(
Filters.eq("_id", task.id.toString()), TaskMongodb.of(propertySerializerFactory, task), Filters.eq("_id", task.id.toString()),
TaskMongodb.of(propertySerializerFactory, task),
ReplaceOptions().upsert(true) ReplaceOptions().upsert(true)
) )
return@withContext task return@withContext task
} }
override suspend fun saveAll(tasks: List<Task>): Unit = withContext(Dispatchers.IO) { override suspend fun saveAll(tasks: List<Task>): Unit = withContext(Dispatchers.IO) {
collection.bulkWrite(tasks.map { collection.bulkWrite(
ReplaceOneModel( tasks.map {
Filters.eq(it.id.toString()), ReplaceOneModel(
TaskMongodb.of(propertySerializerFactory, it), Filters.eq(it.id.toString()),
ReplaceOptions().upsert(true) TaskMongodb.of(propertySerializerFactory, it),
) ReplaceOptions().upsert(true)
}) )
}
)
} }
override fun findByNextRetryBeforeAndCompletedAtIsNull(timestamp: Instant): Flow<Task> { override fun findByNextRetryBeforeAndCompletedAtIsNull(timestamp: Instant): Flow<Task> {
@ -75,12 +77,13 @@ class MongodbTaskRepository(database: MongoDatabase, private val propertySeriali
override suspend fun findByIdAndUpdate(id: UUID, task: Task) { override suspend fun findByIdAndUpdate(id: UUID, task: Task) {
collection.replaceOne( collection.replaceOne(
Filters.eq("_id", task.id.toString()), TaskMongodb.of(propertySerializerFactory, task), Filters.eq("_id", task.id.toString()),
TaskMongodb.of(propertySerializerFactory, task),
ReplaceOptions().upsert(false) ReplaceOptions().upsert(false)
) )
} }
override suspend fun findByPublishProducerIdAndCompletedAtIsNotNull(publishProducerId: UUID): Flow<Task> { override fun findByPublishProducerIdAndCompletedAtIsNotNull(publishProducerId: UUID): Flow<Task> {
return collection return collection
.find(Filters.eq(TaskMongodb::publishProducerId.name, publishProducerId.toString())) .find(Filters.eq(TaskMongodb::publishProducerId.name, publishProducerId.toString()))
.map { it.toTask(propertySerializerFactory) } .map { it.toTask(propertySerializerFactory) }
@ -116,15 +119,15 @@ data class TaskMongodb(
companion object { companion object {
fun of(propertySerializerFactory: PropertySerializerFactory, task: Task): TaskMongodb { fun of(propertySerializerFactory: PropertySerializerFactory, task: Task): TaskMongodb {
return TaskMongodb( return TaskMongodb(
task.name, name = task.name,
task.id.toString(), id = task.id.toString(),
task.publishProducerId.toString(), publishProducerId = task.publishProducerId.toString(),
task.publishedAt, publishedAt = task.publishedAt,
task.nextRetry, nextRetry = task.nextRetry,
task.completedAt, completedAt = task.completedAt,
task.attempt, attempt = task.attempt,
PropertySerializeUtils.serialize(propertySerializerFactory, task.properties) properties = PropertySerializeUtils.serialize(propertySerializerFactory, task.properties)
) )
} }
} }
} }

View File

@ -41,14 +41,17 @@ class MongodbTaskResultRepository(
private val collection = database.getCollection<TaskResultMongodb>("task_results") private val collection = database.getCollection<TaskResultMongodb>("task_results")
override suspend fun save(taskResult: TaskResult): TaskResult = withContext(Dispatchers.IO) { override suspend fun save(taskResult: TaskResult): TaskResult = withContext(Dispatchers.IO) {
collection.replaceOne( collection.replaceOne(
Filters.eq(taskResult.id.toString()), TaskResultMongodb.of(propertySerializerFactory, taskResult), Filters.eq(taskResult.id.toString()),
TaskResultMongodb.of(propertySerializerFactory, taskResult),
ReplaceOptions().upsert(true) ReplaceOptions().upsert(true)
) )
return@withContext taskResult return@withContext taskResult
} }
override fun findByTaskId(id: UUID): Flow<TaskResult> { override fun findByTaskId(id: UUID): Flow<TaskResult> {
return collection.find(Filters.eq(id.toString())).map { it.toTaskResult(propertySerializerFactory) }.flowOn(Dispatchers.IO) return collection.find(
Filters.eq(id.toString())
).map { it.toTaskResult(propertySerializerFactory) }.flowOn(Dispatchers.IO)
} }
} }
@ -65,27 +68,25 @@ data class TaskResultMongodb(
fun toTaskResult(propertySerializerFactory: PropertySerializerFactory): TaskResult { fun toTaskResult(propertySerializerFactory: PropertySerializerFactory): TaskResult {
return TaskResult( return TaskResult(
UUID.fromString(id), id = UUID.fromString(id),
UUID.fromString(taskId), taskId = UUID.fromString(taskId),
success, success = success,
attempt, attempt = attempt,
PropertySerializeUtils.deserialize(propertySerializerFactory, result), result = PropertySerializeUtils.deserialize(propertySerializerFactory, result),
message message = message
) )
} }
companion object { companion object {
fun of(propertySerializerFactory: PropertySerializerFactory, taskResult: TaskResult): TaskResultMongodb { fun of(propertySerializerFactory: PropertySerializerFactory, taskResult: TaskResult): TaskResultMongodb {
return TaskResultMongodb( return TaskResultMongodb(
taskResult.id.toString(), id = taskResult.id.toString(),
taskResult.taskId.toString(), taskId = taskResult.taskId.toString(),
taskResult.success, success = taskResult.success,
taskResult.attempt, attempt = taskResult.attempt,
PropertySerializeUtils.serialize(propertySerializerFactory, taskResult.result), result = PropertySerializeUtils.serialize(propertySerializerFactory, taskResult.result),
taskResult.message message = taskResult.message
) )
} }
} }
} }

View File

@ -7,12 +7,14 @@ import dev.usbharu.owl.broker.domain.model.consumer.Consumer
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.bson.UuidRepresentation import org.bson.UuidRepresentation
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.util.* import java.util.*
class MongodbConsumerRepositoryTest { class MongodbConsumerRepositoryTest {
@Test @Test
@Disabled
fun name() { fun name() {
val clientSettings = val clientSettings =

View File

@ -90,7 +90,6 @@ fun main() {
logger.info("Use module name: {}", moduleContext) logger.info("Use module name: {}", moduleContext)
val koin = startKoin { val koin = startKoin {
printLogger() printLogger()
@ -98,7 +97,6 @@ fun main() {
single<RetryPolicyFactory> { single<RetryPolicyFactory> {
DefaultRetryPolicyFactory(mapOf("" to ExponentialRetryPolicy())) DefaultRetryPolicyFactory(mapOf("" to ExponentialRetryPolicy()))
} }
} }
modules(mainModule, module, moduleContext.module()) modules(mainModule, module, moduleContext.module())
} }
@ -108,4 +106,4 @@ fun main() {
runBlocking { runBlocking {
application.start(50051).join() application.start(50051).join()
} }
} }

View File

@ -19,11 +19,9 @@ package dev.usbharu.owl.broker
import org.koin.core.module.Module import org.koin.core.module.Module
interface ModuleContext { interface ModuleContext {
fun module():Module fun module(): Module
} }
data object EmptyModuleContext : ModuleContext { data object EmptyModuleContext : ModuleContext {
override fun module(): Module { override fun module(): Module = org.koin.dsl.module { }
return org.koin.dsl.module { } }
}
}

Some files were not shown because too many files have changed in this diff Show More