Merge pull request #615 from usbharu/controller-test

Controller test
This commit is contained in:
usbharu 2024-09-18 11:56:40 +09:00 committed by GitHub
commit d8d903437e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
161 changed files with 4180 additions and 553 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@v2
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.0
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

@ -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 {
@ -19,3 +24,92 @@ 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

@ -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

@ -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

@ -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

@ -0,0 +1,22 @@
/*
* 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 org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
abstract class SpringApplicationTestBase

View File

@ -0,0 +1,26 @@
/*
* 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.application.shared.Transaction
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:

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

@ -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,13 +41,43 @@ 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 {
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() useJUnitPlatform()
} }
}
publishing { publishing {
repositories { repositories {
@ -70,3 +106,62 @@ 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"))
} }

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,

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,15 +171,15 @@ 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) {

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(
tasks.map {
ReplaceOneModel( ReplaceOneModel(
Filters.eq(it.id.toString()), Filters.eq(it.id.toString()),
TaskMongodb.of(propertySerializerFactory, it), TaskMongodb.of(propertySerializerFactory, it),
ReplaceOptions().upsert(true) 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,14 +119,14 @@ 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())
} }

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 { }
}
} }

View File

@ -38,7 +38,7 @@ class OwlBrokerApplication(
private lateinit var server: Server private lateinit var server: Server
fun start(port: Int,coroutineScope: CoroutineScope = GlobalScope):Job { fun start(port: Int, coroutineScope: CoroutineScope = GlobalScope): Job {
server = ServerBuilder.forPort(port) server = ServerBuilder.forPort(port)
.addService(assignmentTaskService) .addService(assignmentTaskService)
.addService(definitionTaskService) .addService(definitionTaskService)
@ -64,5 +64,4 @@ class OwlBrokerApplication(
fun stop() { fun stop() {
server.shutdown() server.shutdown()
} }
} }

View File

@ -19,7 +19,7 @@ package dev.usbharu.owl.broker.domain.model.consumer
import java.util.* import java.util.*
interface ConsumerRepository { interface ConsumerRepository {
suspend fun save(consumer: Consumer):Consumer suspend fun save(consumer: Consumer): Consumer
suspend fun findById(id:UUID):Consumer? suspend fun findById(id: UUID): Consumer?
} }

View File

@ -20,9 +20,9 @@ import java.time.Instant
import java.util.* import java.util.*
data class Producer( data class Producer(
val id:UUID, val id: UUID,
val name:String, val name: String,
val hostname:String, val hostname: String,
val registeredTask:List<String>, val registeredTask: List<String>,
val createdAt: Instant val createdAt: Instant
) )

View File

@ -17,5 +17,5 @@
package dev.usbharu.owl.broker.domain.model.producer package dev.usbharu.owl.broker.domain.model.producer
interface ProducerRepository { interface ProducerRepository {
suspend fun save(producer: Producer):Producer suspend fun save(producer: Producer): Producer
} }

View File

@ -21,12 +21,12 @@ import java.time.Instant
import java.util.* import java.util.*
interface QueuedTaskRepository { interface QueuedTaskRepository {
suspend fun save(queuedTask: QueuedTask):QueuedTask suspend fun save(queuedTask: QueuedTask): QueuedTask
/** /**
* トランザクションの代わり * トランザクションの代わり
*/ */
suspend fun findByTaskIdAndAssignedConsumerIsNullAndUpdate(id:UUID,update:QueuedTask):QueuedTask suspend fun findByTaskIdAndAssignedConsumerIsNullAndUpdate(id: UUID, update: QueuedTask): QueuedTask
fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(tasks: List<String>, limit: Int): Flow<QueuedTask> fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(tasks: List<String>, limit: Int): Flow<QueuedTask>

View File

@ -24,11 +24,11 @@ import java.util.*
* @param attempt 失敗を含めて試行した回数 * @param attempt 失敗を含めて試行した回数
*/ */
data class Task( data class Task(
val name:String, val name: String,
val id: UUID, val id: UUID,
val publishProducerId:UUID, val publishProducerId: UUID,
val publishedAt: Instant, val publishedAt: Instant,
val nextRetry:Instant, val nextRetry: Instant,
val completedAt: Instant? = null, val completedAt: Instant? = null,
val attempt: Int, val attempt: Int,
val properties: Map<String, PropertyValue<*>> val properties: Map<String, PropertyValue<*>>

View File

@ -21,15 +21,15 @@ import java.time.Instant
import java.util.* import java.util.*
interface TaskRepository { interface TaskRepository {
suspend fun save(task: Task):Task suspend fun save(task: Task): Task
suspend fun saveAll(tasks:List<Task>) suspend fun saveAll(tasks: List<Task>)
fun findByNextRetryBeforeAndCompletedAtIsNull(timestamp:Instant): Flow<Task> fun findByNextRetryBeforeAndCompletedAtIsNull(timestamp: Instant): Flow<Task>
suspend fun findById(uuid: UUID): Task? suspend fun findById(uuid: UUID): Task?
suspend fun findByIdAndUpdate(id:UUID,task: Task) suspend fun findByIdAndUpdate(id: UUID, task: Task)
suspend fun findByPublishProducerIdAndCompletedAtIsNotNull(publishProducerId:UUID):Flow<Task> fun findByPublishProducerIdAndCompletedAtIsNotNull(publishProducerId: UUID): Flow<Task>
} }

View File

@ -22,5 +22,5 @@ data class TaskDefinition(
val maxRetry: Int, val maxRetry: Int,
val timeoutMilli: Long, val timeoutMilli: Long,
val propertyDefinitionHash: Long, val propertyDefinitionHash: Long,
val retryPolicy:String val retryPolicy: String
) )

View File

@ -18,7 +18,7 @@ package dev.usbharu.owl.broker.domain.model.taskdefinition
interface TaskDefinitionRepository { interface TaskDefinitionRepository {
suspend fun save(taskDefinition: TaskDefinition): TaskDefinition suspend fun save(taskDefinition: TaskDefinition): TaskDefinition
suspend fun deleteByName(name:String) suspend fun deleteByName(name: String)
suspend fun findByName(name:String):TaskDefinition? suspend fun findByName(name: String): TaskDefinition?
} }

View File

@ -21,7 +21,7 @@ import java.util.*
data class TaskResult( data class TaskResult(
val id: UUID, val id: UUID,
val taskId:UUID, val taskId: UUID,
val success: Boolean, val success: Boolean,
val attempt: Int, val attempt: Int,
val result: Map<String, PropertyValue<*>>, val result: Map<String, PropertyValue<*>>,

View File

@ -20,6 +20,6 @@ import kotlinx.coroutines.flow.Flow
import java.util.* import java.util.*
interface TaskResultRepository { interface TaskResultRepository {
suspend fun save(taskResult: TaskResult):TaskResult suspend fun save(taskResult: TaskResult): TaskResult
fun findByTaskId(id:UUID): Flow<TaskResult> fun findByTaskId(id: UUID): Flow<TaskResult>
} }

View File

@ -17,7 +17,7 @@
package dev.usbharu.owl.broker.external package dev.usbharu.owl.broker.external
import com.google.protobuf.Timestamp import com.google.protobuf.Timestamp
import dev.usbharu.owl.Uuid import dev.usbharu.owl.generated.Uuid
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@ -32,4 +32,4 @@ fun UUID.toUUID(): Uuid.UUID = Uuid
fun Timestamp.toInstant(): Instant = Instant.ofEpochSecond(seconds, nanos.toLong()) fun Timestamp.toInstant(): Instant = Instant.ofEpochSecond(seconds, nanos.toLong())
fun Instant.toTimestamp():Timestamp = Timestamp.newBuilder().setSeconds(this.epochSecond).setNanos(this.nano).build() fun Instant.toTimestamp(): Timestamp = Timestamp.newBuilder().setSeconds(this.epochSecond).setNanos(this.nano).build()

View File

@ -16,14 +16,13 @@
package dev.usbharu.owl.broker.interfaces.grpc package dev.usbharu.owl.broker.interfaces.grpc
import dev.usbharu.owl.AssignmentTaskServiceGrpcKt
import dev.usbharu.owl.Task
import dev.usbharu.owl.broker.external.toTimestamp import dev.usbharu.owl.broker.external.toTimestamp
import dev.usbharu.owl.broker.external.toUUID import dev.usbharu.owl.broker.external.toUUID
import dev.usbharu.owl.broker.service.QueuedTaskAssigner import dev.usbharu.owl.broker.service.QueuedTaskAssigner
import dev.usbharu.owl.common.property.PropertySerializeUtils import dev.usbharu.owl.common.property.PropertySerializeUtils
import dev.usbharu.owl.common.property.PropertySerializerFactory import dev.usbharu.owl.common.property.PropertySerializerFactory
import dev.usbharu.owl.generated.AssignmentTaskServiceGrpcKt
import dev.usbharu.owl.generated.Task
import io.grpc.Status import io.grpc.Status
import io.grpc.StatusException import io.grpc.StatusException
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -33,7 +32,6 @@ import org.slf4j.LoggerFactory
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
class AssignmentTaskService( class AssignmentTaskService(
coroutineContext: CoroutineContext = EmptyCoroutineContext, coroutineContext: CoroutineContext = EmptyCoroutineContext,
private val queuedTaskAssigner: QueuedTaskAssigner, private val queuedTaskAssigner: QueuedTaskAssigner,
@ -42,7 +40,6 @@ class AssignmentTaskService(
AssignmentTaskServiceGrpcKt.AssignmentTaskServiceCoroutineImplBase(coroutineContext) { AssignmentTaskServiceGrpcKt.AssignmentTaskServiceCoroutineImplBase(coroutineContext) {
override fun ready(requests: Flow<Task.ReadyRequest>): Flow<Task.TaskRequest> { override fun ready(requests: Flow<Task.ReadyRequest>): Flow<Task.TaskRequest> {
return try { return try {
requests requests
.flatMapMerge { .flatMapMerge {

View File

@ -17,25 +17,28 @@
package dev.usbharu.owl.broker.interfaces.grpc package dev.usbharu.owl.broker.interfaces.grpc
import com.google.protobuf.Empty import com.google.protobuf.Empty
import dev.usbharu.owl.DefinitionTask
import dev.usbharu.owl.DefinitionTask.TaskDefined
import dev.usbharu.owl.DefinitionTaskServiceGrpcKt.DefinitionTaskServiceCoroutineImplBase
import dev.usbharu.owl.broker.domain.model.taskdefinition.TaskDefinition import dev.usbharu.owl.broker.domain.model.taskdefinition.TaskDefinition
import dev.usbharu.owl.broker.service.RegisterTaskService import dev.usbharu.owl.broker.service.RegisterTaskService
import dev.usbharu.owl.generated.DefinitionTask
import dev.usbharu.owl.generated.DefinitionTask.TaskDefined
import dev.usbharu.owl.generated.DefinitionTaskServiceGrpcKt.DefinitionTaskServiceCoroutineImplBase
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
class DefinitionTaskService(coroutineContext: CoroutineContext = EmptyCoroutineContext,private val registerTaskService: RegisterTaskService) : class DefinitionTaskService(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
private val registerTaskService: RegisterTaskService
) :
DefinitionTaskServiceCoroutineImplBase(coroutineContext) { DefinitionTaskServiceCoroutineImplBase(coroutineContext) {
override suspend fun register(request: DefinitionTask.TaskDefinition): TaskDefined { override suspend fun register(request: DefinitionTask.TaskDefinition): TaskDefined {
registerTaskService.registerTask( registerTaskService.registerTask(
TaskDefinition( TaskDefinition(
request.name, name = request.name,
request.priority, priority = request.priority,
request.maxRetry, maxRetry = request.maxRetry,
request.timeoutMilli, timeoutMilli = request.timeoutMilli,
request.propertyDefinitionHash, propertyDefinitionHash = request.propertyDefinitionHash,
request.retryPolicy retryPolicy = request.retryPolicy
) )
) )
return TaskDefined return TaskDefined

View File

@ -16,24 +16,26 @@
package dev.usbharu.owl.broker.interfaces.grpc package dev.usbharu.owl.broker.interfaces.grpc
import dev.usbharu.owl.ProducerOuterClass
import dev.usbharu.owl.ProducerServiceGrpcKt.ProducerServiceCoroutineImplBase
import dev.usbharu.owl.broker.external.toUUID import dev.usbharu.owl.broker.external.toUUID
import dev.usbharu.owl.broker.service.ProducerService import dev.usbharu.owl.broker.service.ProducerService
import dev.usbharu.owl.broker.service.RegisterProducerRequest import dev.usbharu.owl.broker.service.RegisterProducerRequest
import dev.usbharu.owl.generated.ProducerOuterClass
import dev.usbharu.owl.generated.ProducerServiceGrpcKt.ProducerServiceCoroutineImplBase
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
class ProducerService( class ProducerService(
coroutineContext: CoroutineContext = EmptyCoroutineContext, coroutineContext: CoroutineContext = EmptyCoroutineContext,
private val producerService: ProducerService private val producerService: ProducerService
) : ) :
ProducerServiceCoroutineImplBase(coroutineContext) { ProducerServiceCoroutineImplBase(coroutineContext) {
override suspend fun registerProducer(request: ProducerOuterClass.Producer): ProducerOuterClass.RegisterProducerResponse { override suspend fun registerProducer(
request: ProducerOuterClass.Producer
): ProducerOuterClass.RegisterProducerResponse {
val registerProducer = producerService.registerProducer( val registerProducer = producerService.registerProducer(
RegisterProducerRequest( RegisterProducerRequest(
request.name, request.hostname request.name,
request.hostname
) )
) )
return ProducerOuterClass.RegisterProducerResponse.newBuilder().setId(registerProducer.toUUID()).build() return ProducerOuterClass.RegisterProducerResponse.newBuilder().setId(registerProducer.toUUID()).build()

View File

@ -16,11 +16,11 @@
package dev.usbharu.owl.broker.interfaces.grpc package dev.usbharu.owl.broker.interfaces.grpc
import dev.usbharu.owl.Consumer
import dev.usbharu.owl.SubscribeTaskServiceGrpcKt.SubscribeTaskServiceCoroutineImplBase
import dev.usbharu.owl.broker.external.toUUID import dev.usbharu.owl.broker.external.toUUID
import dev.usbharu.owl.broker.service.ConsumerService import dev.usbharu.owl.broker.service.ConsumerService
import dev.usbharu.owl.broker.service.RegisterConsumerRequest import dev.usbharu.owl.broker.service.RegisterConsumerRequest
import dev.usbharu.owl.generated.Consumer
import dev.usbharu.owl.generated.SubscribeTaskServiceGrpcKt.SubscribeTaskServiceCoroutineImplBase
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext

View File

@ -16,15 +16,15 @@
package dev.usbharu.owl.broker.interfaces.grpc package dev.usbharu.owl.broker.interfaces.grpc
import dev.usbharu.owl.PublishTaskOuterClass
import dev.usbharu.owl.PublishTaskOuterClass.PublishedTask
import dev.usbharu.owl.PublishTaskOuterClass.PublishedTasks
import dev.usbharu.owl.TaskPublishServiceGrpcKt.TaskPublishServiceCoroutineImplBase
import dev.usbharu.owl.broker.external.toUUID import dev.usbharu.owl.broker.external.toUUID
import dev.usbharu.owl.broker.service.PublishTask import dev.usbharu.owl.broker.service.PublishTask
import dev.usbharu.owl.broker.service.TaskPublishService import dev.usbharu.owl.broker.service.TaskPublishService
import dev.usbharu.owl.common.property.PropertySerializeUtils import dev.usbharu.owl.common.property.PropertySerializeUtils
import dev.usbharu.owl.common.property.PropertySerializerFactory import dev.usbharu.owl.common.property.PropertySerializerFactory
import dev.usbharu.owl.generated.PublishTaskOuterClass
import dev.usbharu.owl.generated.PublishTaskOuterClass.PublishedTask
import dev.usbharu.owl.generated.PublishTaskOuterClass.PublishedTasks
import dev.usbharu.owl.generated.TaskPublishServiceGrpcKt.TaskPublishServiceCoroutineImplBase
import io.grpc.Status import io.grpc.Status
import io.grpc.StatusException import io.grpc.StatusException
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -39,13 +39,9 @@ class TaskPublishService(
TaskPublishServiceCoroutineImplBase(coroutineContext) { TaskPublishServiceCoroutineImplBase(coroutineContext) {
override suspend fun publishTask(request: PublishTaskOuterClass.PublishTask): PublishedTask { override suspend fun publishTask(request: PublishTaskOuterClass.PublishTask): PublishedTask {
logger.warn("aaaaaaaaaaa") logger.warn("aaaaaaaaaaa")
return try { return try {
val publishedTask = taskPublishService.publishTask( val publishedTask = taskPublishService.publishTask(
PublishTask( PublishTask(
request.name, request.name,
@ -61,7 +57,6 @@ class TaskPublishService(
} }
override suspend fun publishTasks(request: PublishTaskOuterClass.PublishTasks): PublishedTasks { override suspend fun publishTasks(request: PublishTaskOuterClass.PublishTasks): PublishedTasks {
val tasks = request.propertiesArrayList.map { val tasks = request.propertiesArrayList.map {
PublishTask( PublishTask(
request.name, request.name,

View File

@ -17,13 +17,13 @@
package dev.usbharu.owl.broker.interfaces.grpc package dev.usbharu.owl.broker.interfaces.grpc
import com.google.protobuf.Empty import com.google.protobuf.Empty
import dev.usbharu.owl.TaskResultOuterClass
import dev.usbharu.owl.TaskResultServiceGrpcKt
import dev.usbharu.owl.broker.domain.model.taskresult.TaskResult import dev.usbharu.owl.broker.domain.model.taskresult.TaskResult
import dev.usbharu.owl.broker.external.toUUID import dev.usbharu.owl.broker.external.toUUID
import dev.usbharu.owl.broker.service.TaskManagementService import dev.usbharu.owl.broker.service.TaskManagementService
import dev.usbharu.owl.common.property.PropertySerializeUtils import dev.usbharu.owl.common.property.PropertySerializeUtils
import dev.usbharu.owl.common.property.PropertySerializerFactory import dev.usbharu.owl.common.property.PropertySerializerFactory
import dev.usbharu.owl.generated.TaskResultOuterClass
import dev.usbharu.owl.generated.TaskResultServiceGrpcKt
import io.grpc.Status import io.grpc.Status
import io.grpc.StatusException import io.grpc.StatusException
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException

View File

@ -16,11 +16,11 @@
package dev.usbharu.owl.broker.interfaces.grpc package dev.usbharu.owl.broker.interfaces.grpc
import dev.usbharu.owl.*
import dev.usbharu.owl.broker.external.toUUID import dev.usbharu.owl.broker.external.toUUID
import dev.usbharu.owl.broker.service.TaskManagementService import dev.usbharu.owl.broker.service.TaskManagementService
import dev.usbharu.owl.common.property.PropertySerializeUtils import dev.usbharu.owl.common.property.PropertySerializeUtils
import dev.usbharu.owl.common.property.PropertySerializerFactory import dev.usbharu.owl.common.property.PropertySerializerFactory
import dev.usbharu.owl.generated.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -41,7 +41,8 @@ class TaskResultSubscribeService(
name = it.name name = it.name
attempt = it.attempt attempt = it.attempt
success = it.success success = it.success
results.addAll(it.results.map { results.addAll(
it.results.map {
taskResult { taskResult {
id = it.taskId.toUUID() id = it.taskId.toUUID()
success = it.success success = it.success
@ -49,7 +50,8 @@ class TaskResultSubscribeService(
result.putAll(PropertySerializeUtils.serialize(propertySerializerFactory, it.result)) result.putAll(PropertySerializeUtils.serialize(propertySerializerFactory, it.result))
message = it.message message = it.message
} }
}) }
)
} }
} }
} }

View File

@ -42,7 +42,5 @@ class AssignQueuedTaskDeciderImpl(
).take(numberOfConcurrent) ).take(numberOfConcurrent)
) )
} }
} }
} }

View File

@ -26,10 +26,8 @@ interface ProducerService {
suspend fun registerProducer(producer: RegisterProducerRequest): UUID suspend fun registerProducer(producer: RegisterProducerRequest): UUID
} }
class ProducerServiceImpl(private val producerRepository: ProducerRepository) : ProducerService { class ProducerServiceImpl(private val producerRepository: ProducerRepository) : ProducerService {
override suspend fun registerProducer(producer: RegisterProducerRequest): UUID { override suspend fun registerProducer(producer: RegisterProducerRequest): UUID {
val id = UUID.randomUUID() val id = UUID.randomUUID()
val saveProducer = Producer( val saveProducer = Producer(

View File

@ -29,19 +29,14 @@ interface QueueScanner {
fun startScan(): Flow<QueuedTask> fun startScan(): Flow<QueuedTask>
} }
class QueueScannerImpl(private val queueStore: QueueStore) : QueueScanner { class QueueScannerImpl(private val queueStore: QueueStore) : QueueScanner {
override fun startScan(): Flow<QueuedTask> { override fun startScan(): Flow<QueuedTask> = flow {
return flow {
while (currentCoroutineContext().isActive) { while (currentCoroutineContext().isActive) {
emitAll(scanQueue()) emitAll(scanQueue())
delay(1000) delay(1000)
} }
} }
}
private fun scanQueue(): Flow<QueuedTask> {
return queueStore.findByQueuedAtBeforeAndIsActiveIsTrue(Instant.now().minusSeconds(10))
}
private fun scanQueue(): Flow<QueuedTask> =
queueStore.findByQueuedAtBeforeAndIsActiveIsTrue(Instant.now().minusSeconds(10))
} }

View File

@ -32,33 +32,24 @@ interface QueueStore {
fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow<QueuedTask> fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow<QueuedTask>
} }
class QueueStoreImpl(private val queuedTaskRepository: QueuedTaskRepository) : QueueStore { class QueueStoreImpl(private val queuedTaskRepository: QueuedTaskRepository) : QueueStore {
override suspend fun enqueue(queuedTask: QueuedTask) { override suspend fun enqueue(queuedTask: QueuedTask) {
queuedTaskRepository.save(queuedTask) queuedTaskRepository.save(queuedTask)
} }
override suspend fun enqueueAll(queuedTaskList: List<QueuedTask>) { override suspend fun enqueueAll(queuedTaskList: List<QueuedTask>) = queuedTaskList.forEach { enqueue(it) }
queuedTaskList.forEach { enqueue(it) }
}
override suspend fun dequeue(queuedTask: QueuedTask) { override suspend fun dequeue(queuedTask: QueuedTask) {
queuedTaskRepository.findByTaskIdAndAssignedConsumerIsNullAndUpdate(queuedTask.task.id, queuedTask) queuedTaskRepository.findByTaskIdAndAssignedConsumerIsNullAndUpdate(queuedTask.task.id, queuedTask)
} }
override suspend fun dequeueAll(queuedTaskList: List<QueuedTask>) { override suspend fun dequeueAll(queuedTaskList: List<QueuedTask>) = queuedTaskList.forEach { dequeue(it) }
return queuedTaskList.forEach { dequeue(it) }
}
override fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority( override fun findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(
tasks: List<String>, tasks: List<String>,
limit: Int limit: Int
): Flow<QueuedTask> { ): Flow<QueuedTask> = queuedTaskRepository.findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(tasks, limit)
return queuedTaskRepository.findByTaskNameInAndIsActiveIsTrueAndOrderByPriority(tasks, limit)
}
override fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow<QueuedTask> {
return queuedTaskRepository.findByQueuedAtBeforeAndIsActiveIsTrue(instant)
}
override fun findByQueuedAtBeforeAndIsActiveIsTrue(instant: Instant): Flow<QueuedTask> =
queuedTaskRepository.findByQueuedAtBeforeAndIsActiveIsTrue(instant)
} }

View File

@ -27,7 +27,6 @@ interface QueuedTaskAssigner {
fun ready(consumerId: UUID, numberOfConcurrent: Int): Flow<QueuedTask> fun ready(consumerId: UUID, numberOfConcurrent: Int): Flow<QueuedTask>
} }
class QueuedTaskAssignerImpl( class QueuedTaskAssignerImpl(
private val taskManagementService: TaskManagementService, private val taskManagementService: TaskManagementService,
private val queueStore: QueueStore private val queueStore: QueueStore
@ -49,7 +48,6 @@ class QueuedTaskAssignerImpl(
private suspend fun assignTask(queuedTask: QueuedTask, consumerId: UUID): QueuedTask? { private suspend fun assignTask(queuedTask: QueuedTask, consumerId: UUID): QueuedTask? {
return try { return try {
val assignedTaskQueue = val assignedTaskQueue =
queuedTask.copy(assignedConsumer = consumerId, assignedAt = Instant.now(), isActive = false) queuedTask.copy(assignedConsumer = consumerId, assignedAt = Instant.now(), isActive = false)
logger.trace( logger.trace(

View File

@ -24,33 +24,34 @@ import org.slf4j.LoggerFactory
interface RegisterTaskService { interface RegisterTaskService {
suspend fun registerTask(taskDefinition: TaskDefinition) suspend fun registerTask(taskDefinition: TaskDefinition)
suspend fun unregisterTask(name:String) suspend fun unregisterTask(name: String)
} }
class RegisterTaskServiceImpl(private val taskDefinitionRepository: TaskDefinitionRepository) : RegisterTaskService { class RegisterTaskServiceImpl(private val taskDefinitionRepository: TaskDefinitionRepository) : RegisterTaskService {
override suspend fun registerTask(taskDefinition: TaskDefinition) { override suspend fun registerTask(taskDefinition: TaskDefinition) {
val definedTask = taskDefinitionRepository.findByName(taskDefinition.name) val definedTask = taskDefinitionRepository.findByName(taskDefinition.name)
if (definedTask != null) { if (definedTask != null) {
logger.debug("Task already defined. name: ${taskDefinition.name}") logger.debug("Task already defined. name: ${taskDefinition.name}")
if (taskDefinition.propertyDefinitionHash != definedTask.propertyDefinitionHash) { if (taskDefinition.propertyDefinitionHash != definedTask.propertyDefinitionHash) {
throw IncompatibleTaskException("Task ${taskDefinition.name} has already been defined, and the parameters are incompatible.") throw IncompatibleTaskException(
"Task ${taskDefinition.name} has already been defined, and the parameters are incompatible."
)
} }
return return
} }
taskDefinitionRepository.save(taskDefinition) taskDefinitionRepository.save(taskDefinition)
logger.info("Register a new task. name: {}",taskDefinition.name) logger.info("Register a new task. name: {}", taskDefinition.name)
} }
// todo すでにpublish済みのタスクをどうするか決めさせる // todo すでにpublish済みのタスクをどうするか決めさせる
override suspend fun unregisterTask(name: String) { override suspend fun unregisterTask(name: String) {
taskDefinitionRepository.deleteByName(name) taskDefinitionRepository.deleteByName(name)
logger.info("Unregister a task. name: {}",name) logger.info("Unregister a task. name: {}", name)
} }
companion object{ companion object {
private val logger = LoggerFactory.getLogger(RegisterTaskServiceImpl::class.java) private val logger = LoggerFactory.getLogger(RegisterTaskServiceImpl::class.java)
} }
} }

View File

@ -31,7 +31,6 @@ import org.slf4j.LoggerFactory
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
interface TaskManagementService { interface TaskManagementService {
suspend fun startManagement(coroutineScope: CoroutineScope) suspend fun startManagement(coroutineScope: CoroutineScope)
@ -75,13 +74,11 @@ class TaskManagementServiceImpl(
} }
} }
override fun findAssignableTask(consumerId: UUID, numberOfConcurrent: Int): Flow<QueuedTask> { override fun findAssignableTask(consumerId: UUID, numberOfConcurrent: Int): Flow<QueuedTask> {
return assignQueuedTaskDecider.findAssignableQueue(consumerId, numberOfConcurrent) return assignQueuedTaskDecider.findAssignableQueue(consumerId, numberOfConcurrent)
} }
private suspend fun enqueueTask(task: Task): QueuedTask { private suspend fun enqueueTask(task: Task): QueuedTask {
val definedTask = taskDefinitionRepository.findByName(task.name) val definedTask = taskDefinitionRepository.findByName(task.name)
?: throw TaskNotRegisterException("Task ${task.name} not definition.") ?: throw TaskNotRegisterException("Task ${task.name} not definition.")
@ -113,7 +110,6 @@ class TaskManagementServiceImpl(
queueStore.dequeue(timeoutQueue) queueStore.dequeue(timeoutQueue)
val task = taskRepository.findById(timeoutQueue.task.id) val task = taskRepository.findById(timeoutQueue.task.id)
?: throw RecordNotFoundException("Task not found. id: ${timeoutQueue.task.id}") ?: throw RecordNotFoundException("Task not found. id: ${timeoutQueue.task.id}")
val copy = task.copy(attempt = timeoutQueue.attempt) val copy = task.copy(attempt = timeoutQueue.attempt)
@ -148,12 +144,10 @@ class TaskManagementServiceImpl(
taskResult.taskId, taskResult.taskId,
task.copy(completedAt = completedAt, attempt = taskResult.attempt) task.copy(completedAt = completedAt, attempt = taskResult.attempt)
) )
} }
override fun subscribeResult(producerId: UUID): Flow<TaskResults> { override fun subscribeResult(producerId: UUID): Flow<TaskResults> {
return flow { return flow {
while (currentCoroutineContext().isActive) { while (currentCoroutineContext().isActive) {
taskRepository taskRepository
.findByPublishProducerIdAndCompletedAtIsNotNull(producerId) .findByPublishProducerIdAndCompletedAtIsNotNull(producerId)
@ -163,7 +157,7 @@ class TaskManagementServiceImpl(
TaskResults( TaskResults(
it.name, it.name,
it.id, it.id,
results.any { it.success }, results.any { taskResult -> taskResult.success },
it.attempt, it.attempt,
results results
) )
@ -171,9 +165,7 @@ class TaskManagementServiceImpl(
} }
delay(500) delay(500)
} }
} }
} }
companion object { companion object {

View File

@ -78,7 +78,6 @@ class TaskPublishServiceImpl(
} }
override suspend fun publishTasks(list: List<PublishTask>): List<PublishedTask> { override suspend fun publishTasks(list: List<PublishTask>): List<PublishedTask> {
val first = list.first() val first = list.first()
val definition = taskDefinitionRepository.findByName(first.name) val definition = taskDefinitionRepository.findByName(first.name)
@ -90,14 +89,14 @@ class TaskPublishServiceImpl(
val tasks = list.map { val tasks = list.map {
Task( Task(
it.name, name = it.name,
UUID.randomUUID(), id = UUID.randomUUID(),
first.producerId, publishProducerId = first.producerId,
published, publishedAt = published,
nextRetry, nextRetry = nextRetry,
null, completedAt = null,
0, attempt = 0,
it.properties properties = it.properties
) )
} }

View File

@ -20,9 +20,9 @@ import dev.usbharu.owl.broker.domain.model.taskresult.TaskResult
import java.util.* import java.util.*
data class TaskResults( data class TaskResults(
val name:String, val name: String,
val id:UUID, val id: UUID,
val success:Boolean, val success: Boolean,
val attempt:Int, val attempt: Int,
val results: List<TaskResult> val results: List<TaskResult>
) )

View File

@ -1,7 +1,7 @@
syntax = "proto3"; syntax = "proto3";
import "uuid.proto"; import "uuid.proto";
option java_package = "dev.usbharu.owl"; option java_package = "dev.usbharu.owl.generated";
message SubscribeTaskRequest { message SubscribeTaskRequest {
string name = 1; string name = 1;

View File

@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
option java_package = "dev.usbharu.owl"; option java_package = "dev.usbharu.owl.generated";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "uuid.proto"; import "uuid.proto";

View File

@ -2,7 +2,7 @@ syntax = "proto3";
import "uuid.proto"; import "uuid.proto";
option java_package = "dev.usbharu.owl"; option java_package = "dev.usbharu.owl.generated";
message Producer { message Producer {
string name = 1; string name = 1;

View File

@ -2,7 +2,7 @@ syntax = "proto3";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
option java_package = "dev.usbharu.owl"; option java_package = "dev.usbharu.owl.generated";
message Property{ message Property{
oneof value { oneof value {

View File

@ -4,7 +4,7 @@ import "google/protobuf/timestamp.proto";
import "uuid.proto"; import "uuid.proto";
option java_package = "dev.usbharu.owl"; option java_package = "dev.usbharu.owl.generated";
message PublishTask { message PublishTask {

View File

@ -3,7 +3,7 @@ import "uuid.proto";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
import "property.proto"; import "property.proto";
option java_package = "dev.usbharu.owl"; option java_package = "dev.usbharu.owl.generated";
message ReadyRequest { message ReadyRequest {
int32 number_of_concurrent = 1; int32 number_of_concurrent = 1;

View File

@ -3,7 +3,7 @@ import "uuid.proto";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "property.proto"; import "property.proto";
option java_package = "dev.usbharu.owl"; option java_package = "dev.usbharu.owl.generated";
message TaskResult { message TaskResult {
UUID id = 1; UUID id = 1;

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