This commit is contained in:
usbharu 2024-06-15 01:56:06 +09:00
parent 21ab0e0dfd
commit bf8c643d82
61 changed files with 111 additions and 4964 deletions

View File

@ -1,4 +1,6 @@
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC
org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC --add-opens=java.base/java.util.concurrent.locks=ALL-UNNAMED
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn

View File

@ -3,7 +3,7 @@ import com.github.jk1.license.filter.LicenseBundleNormalizer
import com.github.jk1.license.importer.DependencyDataImporter
import com.github.jk1.license.importer.XmlReportImporter
import com.github.jk1.license.render.*
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlin.jvm)
@ -22,58 +22,6 @@ apply {
group = "dev.usbharu"
version = "0.0.1"
sourceSets {
create("intTest") {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
}
create("e2eTest") {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
}
}
val intTestImplementation by configurations.getting {
extendsFrom(configurations.implementation.get())
}
val intTestRuntimeOnly by configurations.getting {
extendsFrom(configurations.runtimeOnly.get())
}
val e2eTestImplementation by configurations.getting {
extendsFrom(configurations.implementation.get())
}
val e2eTestRuntimeOnly by configurations.getting {
extendsFrom(configurations.runtimeOnly.get())
}
val integrationTest = task<Test>("integrationTest") {
description = "Runs integration tests."
group = "verification"
testClassesDirs = sourceSets["intTest"].output.classesDirs
classpath = sourceSets["intTest"].runtimeClasspath
shouldRunAfter("test")
useJUnitPlatform()
}
val e2eTest = task<Test>("e2eTest") {
description = "Runs e2e tests."
group = "verification"
testClassesDirs = sourceSets["e2eTest"].output.classesDirs
classpath = sourceSets["e2eTest"].runtimeClasspath
shouldRunAfter("test")
useJUnitPlatform()
}
tasks.check {
dependsOn(integrationTest)
dependsOn(e2eTest)
}
tasks.withType<Test> {
useJUnitPlatform()
@ -82,24 +30,20 @@ tasks.withType<Test> {
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.base/java.util=ALL-UNNAMED",
"--add-opens", "java.naming/javax.naming=ALL-UNNAMED",
"--add-opens", "java.base/java.util.concurrent.locks=ALL-UNNAMED"
).toMutableList()
}
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
kotlin {
jvmToolchain(21)
compilerOptions {
freeCompilerArgs.add("-Xjsr305=strict")
jvmTarget = JvmTarget.JVM_21
}
// dependsOn("openApiGenerateMastodonCompatibleApi")
// mustRunAfter("openApiGenerateMastodonCompatibleApi")
}
tasks.clean {
delete += listOf("$rootDir/src/main/resources/static")
}
repositories {
mavenCentral()
maven {
@ -125,21 +69,6 @@ repositories {
}
}
kotlin {
target {
compilations.all {
kotlinOptions.jvmTarget = JavaVersion.VERSION_21.toString()
}
}
}
sourceSets.main {
kotlin.srcDirs(
"$buildDir/generated/ksp/main",
"$buildDir/generated/sources/openapi/src/main/kotlin",
"$buildDir/generated/sources/mastodon/src/main/kotlin"
)
}
val os = org.gradle.nativeplatform.platform.internal
.DefaultNativePlatform.getCurrentOperatingSystem()
@ -201,30 +130,15 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
implementation(libs.kotlin.junit)
implementation(libs.coroutines.test)
testImplementation(libs.kotlin.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.ktor.client.mock)
testImplementation(libs.h2db)
testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1")
testImplementation("org.mockito:mockito-inline:5.2.0")
testImplementation("nl.jqno.equalsverifier:equalsverifier:3.16.1")
testImplementation("com.jparams:to-string-verifier:1.4.8")
intTestImplementation("org.springframework.boot:spring-boot-starter-test")
intTestImplementation("org.springframework.security:spring-security-test")
intTestImplementation(libs.kotlin.junit)
intTestImplementation(libs.coroutines.test)
intTestImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1")
intTestImplementation(libs.h2db)
e2eTestImplementation("org.springframework.boot:spring-boot-starter-test")
e2eTestImplementation("org.springframework.security:spring-security-test")
e2eTestImplementation("org.springframework.boot:spring-boot-starter-webflux")
e2eTestImplementation("com.intuit.karate:karate-junit5:1.4.1")
e2eTestImplementation(libs.h2db)
}
detekt {
@ -312,7 +226,9 @@ kover {
}
springBoot {
buildInfo()
buildInfo {
}
}
licenseReport {

View File

@ -18,3 +18,5 @@ org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn

View File

@ -1,42 +0,0 @@
/*
* 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.
*/
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.selectAll
import java.net.MalformedURLException
import java.net.URL
object AssertionUtil {
@JvmStatic
fun assertUserExist(username: String, domain: String): Boolean {
val s = try {
val url = URL(domain)
url.host + ":" + url.port.toString().takeIf { it != "-1" }.orEmpty()
} catch (e: MalformedURLException) {
domain
}
val selectAll = Actors.selectAll()
println(selectAll.fetchSize)
println(selectAll.toList().size)
selectAll.map { "@${it[Actors.name]}@${it[Actors.domain]}" }.forEach { println(it) }
return Actors.selectAll().where { Actors.name eq username and (Actors.domain eq s) }.empty().not()
}
}

View File

@ -1,44 +0,0 @@
/*
* 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.
*/
import com.intuit.karate.junit5.Karate
object KarateUtil {
fun springBootKarateTest(path: String, scenario: String, clazz: Class<*>, port: String): Karate {
if (scenario.isEmpty()) {
return Karate.run(path).relativeTo(clazz).systemProperty("karate.port", port).karateEnv("dev")
} else {
return Karate.run(path).scenarioName(scenario).relativeTo(clazz).systemProperty("karate.port", port)
.karateEnv("dev")
}
}
fun e2eTest(path: String, scenario: String = "", properties: Map<String, String>, clazz: Class<*>): Karate {
val run = Karate.run(path)
val karate = if (scenario.isEmpty()) {
run
} else {
run.scenarioName(scenario)
}
var relativeTo = karate.relativeTo(clazz)
properties.map { relativeTo = relativeTo.systemProperty(it.key, it.value) }
return relativeTo.karateEnv("dev")
}
}

View File

@ -1,101 +0,0 @@
/*
* 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 federation
import AssertionUtil
import KarateUtil
import com.intuit.karate.core.MockServer
import com.intuit.karate.junit5.Karate
import dev.usbharu.hideout.SpringApplication
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.transaction.annotation.Transactional
import java.net.MalformedURLException
import java.net.URL
@SpringBootTest(
classes = [SpringApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@Transactional
class FollowAcceptTest {
@LocalServerPort
private var port = ""
@Karate.Test
@TestFactory
@Disabled
fun `FollowAcceptTest`(): Karate {
return KarateUtil.e2eTest(
"FollowAcceptTest", "Follow Accept Test",
mapOf("karate.port" to port),
javaClass
)
}
companion object {
lateinit var server: MockServer
lateinit var _remotePort: String
@JvmStatic
fun assertUserExist(username: String, domain: String) = runBlocking {
val s = try {
val url = URL(domain)
url.host + ":" + url.port.toString().takeIf { it != "-1" }.orEmpty()
} catch (e: MalformedURLException) {
domain
}
var check = false
repeat(10) {
delay(1000)
check = AssertionUtil.assertUserExist(username, s) or check
if (check) {
return@repeat
}
}
Assertions.assertTrue(check, "User @$username@$s not exist.")
}
@JvmStatic
fun getRemotePort(): String = _remotePort
@BeforeAll
@JvmStatic
fun beforeAll(@Autowired flyway: Flyway) {
server = MockServer.feature("classpath:federation/FollowAcceptMockServer.feature").http(0).build()
_remotePort = server.port.toString()
flyway.clean()
flyway.migrate()
}
@AfterAll
@JvmStatic
fun afterAll() {
server.stop()
}
}
}

View File

@ -1,146 +0,0 @@
/*
* 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 federation
import AssertionUtil
import KarateUtil
import com.intuit.karate.core.MockServer
import com.intuit.karate.junit5.Karate
import dev.usbharu.hideout.SpringApplication
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.TestFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.transaction.annotation.Transactional
@SpringBootTest(
classes = [SpringApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@Transactional
@Disabled
class InboxCommonTest {
@LocalServerPort
private var port = ""
@Karate.Test
@TestFactory
fun `inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く`(): Karate {
return KarateUtil.e2eTest(
"InboxCommonTest",
"inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く",
mapOf(
"karate.port" to port,
"karate.remotePort" to _remotePort
),
javaClass
)
}
@Karate.Test
@TestFactory
fun `user-inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く`(): Karate {
return KarateUtil.e2eTest(
"InboxCommonTest",
"user-inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く",
mapOf(
"karate.port" to port,
"karate.remotePort" to _remotePort
),
javaClass
)
}
@Karate.Test
@TestFactory
fun `inboxにHTTP Signatureがないリクエストがきたら401を返す`(): Karate {
return KarateUtil.e2eTest(
"InboxCommonTest",
"inboxにHTTP Signatureがないリクエストがきたら401を返す",
mapOf("karate.port" to port),
javaClass
)
}
@Karate.Test
@TestFactory
fun `user-inboxにHTTP Signatureがないリクエストがきたら401を返す`(): Karate {
return KarateUtil.e2eTest(
"InboxCommonTest",
"user-inboxにHTTP Signatureがないリクエストがきたら401を返す",
mapOf("karate.port" to port),
javaClass
)
}
@Karate.Test
@TestFactory
fun `inboxにConetnt-Type application *+json以外が来たら415を返す`(): Karate {
return KarateUtil.e2eTest(
"InboxCommonTest",
"inboxにContent-Type application/json以外が来たら415を返す",
mapOf("karate.port" to port),
javaClass
)
}
companion object {
lateinit var server: MockServer
lateinit var _remotePort: String
@JvmStatic
fun assertUserExist(username: String, domain: String) = runBlocking {
var check = false
repeat(10) {
delay(1000)
check = AssertionUtil.assertUserExist(username, domain) or check
if (check) {
return@repeat
}
}
assertTrue(check, "User @$username@$domain not exist.")
}
@JvmStatic
fun getRemotePort(): String = _remotePort
@BeforeAll
@JvmStatic
fun beforeAll() {
server = MockServer.feature("classpath:federation/InboxxCommonMockServerTest.feature").http(0).build()
_remotePort = server.port.toString()
}
@AfterAll
@JvmStatic
fun afterAll(@Autowired flyway: Flyway) {
server.stop()
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -1,70 +0,0 @@
/*
* 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 oauth2
import KarateUtil
import com.intuit.karate.junit5.Karate
import dev.usbharu.hideout.SpringApplication
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.TestFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.context.jdbc.Sql
@SpringBootTest(
classes = [SpringApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
)
@Sql("/oauth2/user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class OAuth2LoginTest {
@LocalServerPort
private var port = ""
@Karate.Test
@TestFactory
fun `スコープwrite readを持ったトークンの作成`(): Karate {
return KarateUtil.springBootKarateTest(
"Oauth2LoginTest",
"スコープwrite readを持ったトークンの作成",
javaClass,
port
)
}
@Karate.Test
@TestFactory
fun `スコープread_statuses write_statusesを持ったトークンの作成`(): Karate {
return KarateUtil.springBootKarateTest(
"Oauth2LoginTest",
"スコープread:statuses write:statusesを持ったトークンの作成",
javaClass,
port
)
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -1,44 +0,0 @@
hideout:
url: "https://localhost:8080"
use-mongodb: false
security:
jwt:
generate: true
key-id: a
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
storage:
type: local
debug:
trace-query-exception: true
trace-query-call: true
private: false
spring:
flyway:
enabled: true
clean-disabled: false
datasource:
driver-class-name: org.h2.Driver
url: "jdbc:h2:mem:e2e-test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4"
username: ""
password:
data:
mongodb:
auto-index-creation: true
host: localhost
port: 27017
database: hideout
h2:
console:
enabled: true
# exposed:
# generate-ddl: true
# excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed
server:
port: 8080
tomcat:
basedir: tomcat-e2e
accesslog:
enabled: true

View File

@ -1,140 +0,0 @@
Feature: Follow Accept Mock Server
Background:
* def assertInbox = Java.type(`federation.FollowAcceptTest`)
* def req = {req: []}
Scenario: pathMatches('/users/test-follower') && methodIs('get')
* def remoteUrl = 'http://localhost:' + assertInbox.getRemotePort()
* def username = 'test-follower'
* def userUrl = remoteUrl + '/users/' + username
* def person =
"""
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"toot": "http://joinmastodon.org/ns#",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"featuredTags": {
"@id": "toot:featuredTags",
"@type": "@id"
},
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"discoverable": "toot:discoverable",
"Device": "toot:Device",
"Ed25519Signature": "toot:Ed25519Signature",
"Ed25519Key": "toot:Ed25519Key",
"Curve25519Key": "toot:Curve25519Key",
"EncryptedMessage": "toot:EncryptedMessage",
"publicKeyBase64": "toot:publicKeyBase64",
"deviceId": "toot:deviceId",
"claim": {
"@type": "@id",
"@id": "toot:claim"
},
"fingerprintKey": {
"@type": "@id",
"@id": "toot:fingerprintKey"
},
"identityKey": {
"@type": "@id",
"@id": "toot:identityKey"
},
"devices": {
"@type": "@id",
"@id": "toot:devices"
},
"messageFranking": "toot:messageFranking",
"messageType": "toot:messageType",
"cipherText": "toot:cipherText",
"suspended": "toot:suspended",
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
}
}
],
"id": #(userUrl),
"type": "Person",
"following": #(userUrl + '/following'),
"followers": #(userUrl + '/followers'),
"inbox": #(userUrl + '/inbox'),
"outbox": #(userUrl + '/outbox'),
"featured": #(userUrl + '/collections/featured'),
"featuredTags": #(userUrl + '/collections/tags'),
"preferredUsername": #(username),
"name": #(username),
"summary": "E2E Test User Jaga/Cotlin/Winter Boot/Ktol\nYonTude: https://example.com\nY(Tvvitter): https://example.com\n",
"url": #(userUrl + '/@' + username),
"manuallyApprovesFollowers": false,
"discoverable": true,
"published": "2016-03-16T00:00:00Z",
"devices": #(userUrl + '/collections/devices'),
"alsoKnownAs": [
#( 'https://example.com/users/' + username)
],
"publicKey": {
"id": #(userUrl + '#main-key'),
"owner": #(userUrl),
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvmtKo0xYGXR4M0LQQhK4\nBkKpRvvUxrqGV6Ew4CBHSyzdnbFsiBqHUWz4JvRiQvAPqQ4jQFpxVZCPr9xx6lJp\nx7EAAKIdVTnBBV4CYfu7QPsRqtjbB5408q+mo5oUXNs8xg2tcC42p2SJ5CRJX/fr\nOgCZwo3LC9pOBdCQZ+tiiPmWNBTNby99JZn4D/xNcwuhV04qcPoHYD9OPuxxGyzc\naVJ2mqJmvi/lewQuR8qnUIbz+Gik+xvyG6LmyuDoa1H2LDQfQXYb62G70HsYdu7a\ndObvZovytp+kkjP/cUaIYkhhOAYqAA4zCwVRY4NHK0MAMq9sMoUfNJa8U+zR9NvD\noQIDAQAB\n-----END PUBLIC KEY-----\n"
},
"tag": [],
"attachment": [
{
"type": "PropertyValue",
"name": "Pixib Fan-Bridge",
"value": "\u003ca href=\"https://example.com/hideout\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003eexample.com/hideout\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
},
{
"type": "PropertyValue",
"name": "GitHub",
"value": "\u003ca href=\"https://github.com/usbharu/hideout\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egithub.com/usbharu/hideout\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
}
],
"endpoints": {
"sharedInbox": #(remoteUrl + '/inbox')
},
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/Destroyer_Vozbuzhdenyy.jpg/320px-Destroyer_Vozbuzhdenyy.jpg"
},
"image": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/63/Views_of_Mount_Fuji_from_%C5%8Cwakudani_20211202.jpg/320px-Views_of_Mount_Fuji_from_%C5%8Cwakudani_20211202.jpg"
}
}
"""
* set req.req[] = '/users/' + username
* def response = person
Scenario: pathMatches('/inbox') && methodIs('post')
* set req.req[] = '/inbox'
* def responseStatus = 202
Scenario: pathMatches('/internal-assertion-api/requests') && methodIs('get')
* def response = req
Scenario: pathMatches('/internal-assertion-api/requests/deleteAll') && methodIs('post')
* set req.req = []
* def responseStatus = 200

View File

@ -1,29 +0,0 @@
Feature: Follow Accept Test
Background:
* url baseUrl
* def assertionUtil = Java.type('AssertionUtil')
Scenario: Follow Accept Test
* def follow =
"""
{"type": "Follow","actor": #(remoteUrl + '/users/test-follower'),"object": #(baseUrl + '/users/test-user')}
"""
Given path '/inbox'
And header Signature = 'keyId="https://test-hideout.usbharu.dev/users/c#pubkey", algorithm="rsa-sha256", headers="x-request-id tpp-redirect-uri digest psu-id", signature="e/91pFiI5bRffP33EMrqoI5A0xjkg3Ar0kzRGHC/1RsLrDW0zV50dHly/qJJ5xrYHRlss3+vd0mznTLBs1X0hx0uXjpfvCvwclpSi8u+sqn+Y2bcQKzf7ah0vAONQd6zeTYW7e/1kDJreP43PsJyz29KZD16Yop8nM++YeEs6C5eWiyYXKoQozXnfmTOX3y6bhxfKKQWVcxA5aLOTmTZRYTsBsTy9zn8NjDQaRI/0gcyYPqpq+2g8j2DbyJu3Z6zP6VmwbGGlQU/s9Pa7G5LqUPH/sBMSlIeqh+Hvm2pL7B3/BMFvGtTD+e2mR60BFnLIxMYx+oX4o33J2XkFIODLQ=="'
And request follow
When method post
Then status 202
And retry until assertionUtil.assertUserExist('test-follower',remoteUrl)
* url remoteUrl
Given path '/internal-assertion-api/requests'
When method get
Then status 200
And match response.req contains ['/users/test-follower']
* url baseUrl

View File

@ -1,158 +0,0 @@
Feature: Inbox Common Test
Background:
* url baseUrl
Scenario: inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く
* url remoteUrl
Given path '/internal-assertion-api/requests/deleteAll'
When method post
Then status 200
* url baseUrl
* def inbox =
"""
{ "type": "Follow" }
"""
Given path `/inbox`
And request inbox
# And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target)", signature="a"'
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
When method post
Then status 202
* def assertInbox = Java.type(`federation.InboxCommonTest`)
And assertInbox.assertUserExist('test-user',remoteUrl)
* url remoteUrl
Given path '/internal-assertion-api/requests'
When method get
Then status 200
* url baseUrl
* print response
Then match response.req == ['/users/test-user']
Scenario: inboxにHTTP Signatureがないリクエストがきたら401を返す
* def inbox =
"""
{"type": "Follow"}
"""
Given path '/inbox'
And request inbox
When method post
Then status 401
Scenario: user-inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く
* url remoteUrl
Given path '/internal-assertion-api/requests/deleteAll'
When method post
Then status 200
* url baseUrl
* def inbox =
"""
{ "type": "Follow" }
"""
Given path `/inbox`
And request inbox
# And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target)", signature="a"'
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user2#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
When method post
Then status 202
* def assertInbox = Java.type(`federation.InboxCommonTest`)
And assertInbox.assertUserExist('test-user2',remoteUrl)
* url remoteUrl
Given path '/internal-assertion-api/requests'
When method get
Then status 200
* url baseUrl
* print response
Then match response.req == ['/users/test-user2']
Scenario: user-inboxにHTTP Signatureがないリクエストがきたら401を返す
* def inbox =
"""
{"type": "Follow"}
"""
Given path '/inbox'
And request inbox
When method post
Then status 401
Scenario: inboxにContent-Type application/json以外が来たら415を返す
* def inbox =
"""
{"type": "Follow"}
"""
Given path '/inbox'
And request inbox
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
And header Accept = 'application/activity+json'
And header Content-Type = 'application/json'
When method post
Then status 202
Given path '/inbox'
And request inbox
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
And header Accept = 'application/activity+json'
And header Content-Type = 'application/activity+json'
When method post
Then status 202
Given path '/inbox'
And request inbox
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
And header Accept = 'application/activity+json'
And header Content-Type = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
When method post
Then status 202
Given path '/inbox'
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
And header Accept = 'application/activity+json'
When method post
Then status 415
* def html =
"""
<html>
</html>
"""
Given path '/inbox'
And header Signature = 'keyId="'+ remoteUrl +'/users/test-user#pubkey", algorithm="rsa-sha256", headers="(request-target) date host digest", signature="FfpkmBogW70FMo94yovGpl15L/m4bDjVIFb9mSZUstPE3H00nHiqNsjAq671qFMJsGOO1uWfLEExcdvzwTiC3wuHShzingvxQUbTgcgRTRZcHbtrOZxT8hYHGndpCXGv/NOLkfXDtZO9v5u0fnA2yJFokzyPHOPJ1cJliWlXP38Bl/pO4H5rBLQBZKpM2jYIjMyI78G2rDXNHEeGrGiyfB5SKb3H6zFQL+X9QpXUI4n0f07VsnwaDyp63oUopmzNUyBEuSqB+8va/lbfcWwrxpZnKGzQRZ+VBcV7jDoKGNOP9/O1xEI2CwB8sh+h6KVHdX3EQEvO1slaaLzcwRRqrQ=="'
And header Accept = 'application/activity+json'
And header Content-Type = 'text/html'
And request html
When method post
Then status 415

View File

@ -1,136 +0,0 @@
Feature: InboxCommonMockServer
Background:
* def assertInbox = Java.type(`federation.InboxCommonTest`)
* def req = {req: []}
Scenario: pathMatches('/users/{username}') && methodIs('get')
* def remoteUrl = 'http://localhost:' + assertInbox.getRemotePort()
* def username = pathParams.username
* def userUrl = remoteUrl + '/users/' + username
* def person =
"""
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"toot": "http://joinmastodon.org/ns#",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"featuredTags": {
"@id": "toot:featuredTags",
"@type": "@id"
},
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"discoverable": "toot:discoverable",
"Device": "toot:Device",
"Ed25519Signature": "toot:Ed25519Signature",
"Ed25519Key": "toot:Ed25519Key",
"Curve25519Key": "toot:Curve25519Key",
"EncryptedMessage": "toot:EncryptedMessage",
"publicKeyBase64": "toot:publicKeyBase64",
"deviceId": "toot:deviceId",
"claim": {
"@type": "@id",
"@id": "toot:claim"
},
"fingerprintKey": {
"@type": "@id",
"@id": "toot:fingerprintKey"
},
"identityKey": {
"@type": "@id",
"@id": "toot:identityKey"
},
"devices": {
"@type": "@id",
"@id": "toot:devices"
},
"messageFranking": "toot:messageFranking",
"messageType": "toot:messageType",
"cipherText": "toot:cipherText",
"suspended": "toot:suspended",
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
}
}
],
"id": #(userUrl),
"type": "Person",
"following": #(userUrl + '/following'),
"followers": #(userUrl + '/followers'),
"inbox": #(userUrl + '/inbox'),
"outbox": #(userUrl + '/outbox'),
"featured": #(userUrl + '/collections/featured'),
"featuredTags": #(userUrl + '/collections/tags'),
"preferredUsername": #(username),
"name": #(username),
"summary": "E2E Test User Jaga/Cotlin/Winter Boot/Ktol\nYonTude: https://example.com\nY(Tvvitter): https://example.com\n",
"url": #(userUrl + '/@' + username),
"manuallyApprovesFollowers": false,
"discoverable": true,
"published": "2016-03-16T00:00:00Z",
"devices": #(userUrl + '/collections/devices'),
"alsoKnownAs": [
#( 'https://example.com/users/' + username)
],
"publicKey": {
"id": #(userUrl + '#main-key'),
"owner": #(userUrl),
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvmtKo0xYGXR4M0LQQhK4\nBkKpRvvUxrqGV6Ew4CBHSyzdnbFsiBqHUWz4JvRiQvAPqQ4jQFpxVZCPr9xx6lJp\nx7EAAKIdVTnBBV4CYfu7QPsRqtjbB5408q+mo5oUXNs8xg2tcC42p2SJ5CRJX/fr\nOgCZwo3LC9pOBdCQZ+tiiPmWNBTNby99JZn4D/xNcwuhV04qcPoHYD9OPuxxGyzc\naVJ2mqJmvi/lewQuR8qnUIbz+Gik+xvyG6LmyuDoa1H2LDQfQXYb62G70HsYdu7a\ndObvZovytp+kkjP/cUaIYkhhOAYqAA4zCwVRY4NHK0MAMq9sMoUfNJa8U+zR9NvD\noQIDAQAB\n-----END PUBLIC KEY-----\n"
},
"tag": [],
"attachment": [
{
"type": "PropertyValue",
"name": "Pixib Fan-Bridge",
"value": "\u003ca href=\"https://example.com/hideout\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003eexample.com/hideout\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
},
{
"type": "PropertyValue",
"name": "GitHub",
"value": "\u003ca href=\"https://github.com/usbharu/hideout\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egithub.com/usbharu/hideout\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
}
],
"endpoints": {
"sharedInbox": #(remoteUrl + '/inbox')
},
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/Destroyer_Vozbuzhdenyy.jpg/320px-Destroyer_Vozbuzhdenyy.jpg"
},
"image": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/63/Views_of_Mount_Fuji_from_%C5%8Cwakudani_20211202.jpg/320px-Views_of_Mount_Fuji_from_%C5%8Cwakudani_20211202.jpg"
}
}
"""
* set req.req[] = '/users/' + username
* def response = person
Scenario: pathMatches('/internal-assertion-api/requests') && methodIs('get')
* def response = req
Scenario: pathMatches('/internal-assertion-api/requests/deleteAll') && methodIs('post')
* set req.req = []
* def responseStatus = 200

View File

@ -1,30 +0,0 @@
function fn() {
var env = karate.env; // get java system property 'karate.env'
karate.log('karate.env system property was:', env);
if (!env) {
env = 'dev'; // a custom 'intelligent' default
karate.log('karate.env set to "dev" as default.');
}
let config;
if (env === 'test') {
let remotePort = karate.properties['karate.remotePort'] || '8081'
config = {
baseUrl: 'https://test-hideout.usbharu.dev',
remoteUrl: 'http://localhost:' + remotePort
}
} else if (env === 'dev') {
let port = karate.properties['karate.port'] || '8080'
let remotePort = karate.properties['karate.remotePort'] || '8081'
config = {
baseUrl: 'http://localhost:' + port,
remoteUrl: 'http://localhost:' + remotePort
}
} else {
throw 'Unknown environment [' + env + '].'
}
// don't waste time waiting for a connection or if servers don't respond within 0,3 seconds
karate.configure('connectTimeout', 1000);
karate.configure('readTimeout', 1000);
return config;
}

View File

@ -1,21 +0,0 @@
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>./e2eTest.log</file>
<encoder>
<charset>UTF-8</charset>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
<logger name="org.springframework.security" level="TRACE"/>
<logger name="com.intuit.karate.driver" level="INFO"/>
<logger name="org.thymeleaf.TemplateEngine.CONFIG" level="INFO"/>
</configuration>

View File

@ -1,95 +0,0 @@
Feature: OAuth2 Login Test
Background:
* url baseUrl
* configure driver = { type: 'chrome',start: true, headless: true, showDriverLog: true, addOptions: [ '--headless=new' ] }
Scenario: スコープwrite readを持ったトークンの作成
* def apps =
"""
{
"client_name": "oauth2-test-client-1",
"redirect_uris": "https://usbharu.dev",
"scopes": "write read"
}
"""
Given path '/api/v1/apps'
And request apps
When method post
Then status 200
* def client_id = response.client_id
* def client_secret = response.client_secret
* def authorizeEndpoint = baseUrl + '/oauth/authorize?response_type=code&redirect_uri=https://usbharu.dev&client_id=' + client_id + '&scope=write%20read'
Given driver authorizeEndpoint
And driver.input('#username','test-user')
And driver.input('#password','password')
When driver.submit().click('body > div > form > button')
Then driver.waitForUrl(authorizeEndpoint + "&continue")
And driver.click('#read')
And driver.click('#write')
When driver.submit().click('#submit-consent')
Then driver.waitUntil("location.host == 'usbharu.dev'")
* def code = script("new URLSearchParams(document.location.search).get('code')")
Given path '/oauth/token'
And form field client_id = client_id
And form field client_secret = client_secret
And form field redirect_uri = 'https://usbharu.dev'
And form field grant_type = 'authorization_code'
And form field code = code
And form field scope = 'write read'
When method post
Then status 200
Scenario: スコープread:statuses write:statusesを持ったトークンの作成
* def apps =
"""
{
"client_name": "oauth2-test-client-2",
"redirect_uris": "https://usbharu.dev",
"scopes": "read:statuses write:statuses"
}
"""
Given path '/api/v1/apps'
And request apps
When method post
Then status 200
* def client_id = response.client_id
* def client_secret = response.client_secret
* def authorizeEndpoint = baseUrl + '/oauth/authorize?response_type=code&redirect_uri=https://usbharu.dev&client_id=' + client_id + '&scope=read:statuses+write:statuses'
Given driver authorizeEndpoint
And driver.input('#username','test-user')
And driver.input('#password','password')
When driver.submit().click('body > div > form > button')
Then driver.waitForUrl(authorizeEndpoint + "&continue")
And driver.click('/html/body/div/div[4]/div/form/div[1]/input')
And driver.click('/html/body/div/div[4]/div/form/div[2]/input')
When driver.submit().click('#submit-consent')
Then driver.waitUntil("location.host == 'usbharu.dev'")
* def code = script("new URLSearchParams(document.location.search).get('code')")
Given path '/oauth/token'
And form field client_id = client_id
And form field client_secret = client_secret
And form field redirect_uri = 'https://usbharu.dev'
And form field grant_type = 'authorization_code'
And form field code = code
And form field scope = 'write read'
When method post
Then status 200

View File

@ -1,51 +0,0 @@
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 (1730415786666758144, 'test-user', 'localhost', 'Im test user.', 'THis account is test user.',
'http://localhost/users/test-user/inbox',
'http://localhost/users/test-user/outbox', 'http://localhost/users/test-user',
'-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi4mifRg6huAIn6DXk3Vn
5tkRC0AO32ZJvczwXr9xDj4HJvrSUHBAxIwwIeuCceAYtiuZk4JmEKydeB6SRkoO
Nty93XZXS1SMmiHCvWOY5YlpnfFU1kLqW3fkXcLNls4XmzujLt1i2sT8mYkENAsP
h6K4SRtmktOVYZOWcVEcfLGKbJvaDD/+lKikNC1XCouylfGV/bA/FPY5vuI+7cdM
Mjana28JdiWlPWSdzcxtCSgN+nGWPjk2WWm8K+wK2zXqMxA0U0p4odyyILBGALxX
zMqObIQvpwPh/t+b6ohem4eq70/0/SwDhd+IzHkT3x4UzG1oxSQS/juPkO7uuS8p
uwIDAQAB
-----END PUBLIC KEY-----
',
'-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCLiaJ9GDqG4Aif
oNeTdWfm2RELQA7fZkm9zPBev3EOPgcm+tJQcEDEjDAh64Jx4Bi2K5mTgmYQrJ14
HpJGSg423L3ddldLVIyaIcK9Y5jliWmd8VTWQupbd+Rdws2WzhebO6Mu3WLaxPyZ
iQQ0Cw+HorhJG2aS05Vhk5ZxURx8sYpsm9oMP/6UqKQ0LVcKi7KV8ZX9sD8U9jm+
4j7tx0wyNqdrbwl2JaU9ZJ3NzG0JKA36cZY+OTZZabwr7ArbNeozEDRTSnih3LIg
sEYAvFfMyo5shC+nA+H+35vqiF6bh6rvT/T9LAOF34jMeRPfHhTMbWjFJBL+O4+Q
7u65Lym7AgMBAAECggEADJLa7v3LbFLsxAGY22NFeRJPTF252VycwXshn9ANbnSd
bWBFqlTrKSrevXe82ekRIP09ygKCkvcS+3t5v9a1gDEU9MtQo2ubfdoT87/xS6G9
wCs6c1I1Twe3LtG6d9/bVbQiiLsPSNpeTrF/jPcAL780bvYGoK1rNQ85C7383Kl6
1nwZCD0itjkmzbO0nGMRCduW46OdQKiOMuEC7z0zwynH3cK3wGvdlKyLG4L3pPZm
1/Uz7AZTieqSCjSgcgmaut7dmS49e3j8ujfb3wcKscfHoofyqNWsW1xyU1WytO9a
QLh9wlqfvGlfwQWkY6z6uFmc4XfRVZSC8nic4cAW3QKBgQC4PYbR5AuylDcfc6Am
jpL5mcF6qEMnEPgnL9z5VvuLs1f/JEyx5VgzQreDOKc1KOxDX7Xhok4gpvIJv1fi
zimviszEmIpHdPvgS7mP2hu42bSIjwVaXpny5aEEZbB6HQ9pGDW/MSsgmb6x31Kx
o+sslpqf9cpalI35UPtkNaEJNwKBgQDB4tVUQ5gGPKllEfCN64B/B7wodWr5cUNU
UpUXdFPCu+HXnRen6GKLo+25wmCUGtcIuvCY1Xm+tL0Z7jrI+oOD4CL9ob7BJrPF
XCq0jUhaEzWFGp1SOa6n+35fWPkCfG4EwfsK8+PWoZsZc1eykMxIJmBln3vufuHz
qybfhy0VnQKBgD2tAxvyXmQar9VMjLk7k0IRUa6w80H5sUjVAgFKOA0NLZEQ4sfO
wdbvJ6W66mamW2k2ehmdjs/pcy8GKfKYF2ZXbbMGaYwAQm1UjDr2xb78yi3Iyv70
mk6wxlVFgW1vmwAQhbWKTSitryO2YeVrvUeA5yRTULk/78Mdc/qY5V7DAoGAAu3I
RzOWMlHsRSiWN66dDE4zm3DaotYBLF7q/aW2NjTcXoNy/ghWpMFfL/UtvE8DfJBG
XiirZCQazy94F90g63cRUD+HQCezg4G2629O7n1ny5DxW3Kfns3/xLT1XgI/Lzc2
8Z1pja53R1Ukt//T9isOPbrBBoNIKoQlXC8QkUkCgYEAsib3uOMAIOJab5jc8FSj
VG+Cg2H63J5DgUUwx2Y0DPENugdGyYzCDMVPBNaB0Ru1SpqbUjgqh+YHynunSVeu
hDXMOteeyeVHUGw8mvcCEt53uRYVNW/rzXTMqfLVxbsJZHCsJBtFpwcgD2w4NjS2
Ja15+ZWbOA4vJA9pOh3x4XM=
-----END PRIVATE KEY-----
', 1701398248417,
'http://localhost/users/test-user#pubkey', 'http://localhost/users/test-user/following',
'http://localhost/users/test-users/followers', 0, false, 0, 0, 0, null);
insert into user_details (actor_id, password, auto_accept_followee_follow_request)
values ( 1730415786666758144
, '$2a$10$/mWC/n7nC7X3l9qCEOKnredxne2zewoqEsJWTOdlKfg2zXKJ0F9Em', true)

View File

@ -1,163 +0,0 @@
/*
* 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 activitypub.inbox
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.util.Base64Util
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
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.beans.factory.annotation.Qualifier
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.http.MediaType
import org.springframework.security.test.context.support.WithAnonymousUser
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
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.TestTransaction
import util.WithMockHttpSignature
import java.security.MessageDigest
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
class InboxTest {
@Autowired
@Qualifier("http")
private lateinit var dateTimeFormatter: DateTimeFormatter
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
}
@Test
@WithAnonymousUser
fun `匿名でinboxにPOSTしたら401`() {
mockMvc
.post("/inbox") {
content = "{}"
contentType = MediaType.APPLICATION_JSON
header("Host", "example.com")
header("Date", ZonedDateTime.now().format(dateTimeFormatter))
header(
"Digest",
"SHA-256=" + Base64Util.encode(MessageDigest.getInstance("SHA-256").digest("{}".toByteArray()))
)
}
.asyncDispatch()
.andExpect { status { isUnauthorized() } }
}
@Test
@WithMockHttpSignature
fun 有効なHttpSignatureでPOSTしたら202() {
mockMvc
.post("/inbox") {
content = "{}"
contentType = MediaType.APPLICATION_JSON
header("Signature", "a")
header("Host", "example.com")
header("Date", ZonedDateTime.now().format(dateTimeFormatter))
header(
"Digest",
"SHA-256=" + Base64Util.encode(MessageDigest.getInstance("SHA-256").digest("{}".toByteArray()))
)
}
.asyncDispatch()
.andExpect { status { isAccepted() } }
}
@Test
@WithAnonymousUser
fun `匿名でuser-inboxにPOSTしたら401`() {
mockMvc
.post("/users/hoge/inbox") {
content = "{}"
contentType = MediaType.APPLICATION_JSON
header("Host", "example.com")
header("Date", ZonedDateTime.now().format(dateTimeFormatter))
header(
"Digest",
"SHA-256=" + Base64Util.encode(MessageDigest.getInstance("SHA-256").digest("{}".toByteArray()))
)
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isUnauthorized() } }
}
@Test
@WithMockHttpSignature
fun 有効なHttpSignaturesでPOSTしたら202() {
mockMvc
.post("/users/hoge/inbox") {
content = "{}"
contentType = MediaType.APPLICATION_JSON
header("Signature", "a")
header("Host", "example.com")
header("Date", ZonedDateTime.now().format(dateTimeFormatter))
header(
"Digest",
"SHA-256=" + Base64Util.encode(MessageDigest.getInstance("SHA-256").digest("{}".toByteArray()))
)
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isAccepted() } }
}
@TestConfiguration
class Configuration {
@Bean
fun testTransaction() = TestTransaction
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway, @Autowired owlProducer: OwlProducer) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
}
}
}

View File

@ -1,243 +0,0 @@
/*
* 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 activitypub.note
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
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.beans.factory.annotation.Qualifier
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.test.context.support.WithAnonymousUser
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.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
import util.WithHttpSignature
import util.WithMockHttpSignature
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
class NoteTest {
private lateinit var mockMvc: MockMvc
@Autowired
private lateinit var context: WebApplicationContext
@Autowired
@Qualifier("http")
private lateinit var dateTimeFormatter: DateTimeFormatter
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).apply<DefaultMockMvcBuilder>(springSecurity()).build()
}
@Test
@WithAnonymousUser
@Sql("/sql/note/匿名でpublic投稿を取得できる.sql")
fun `匿名でpublic投稿を取得できる`() {
mockMvc
.get("/users/test-user/posts/1234") {
accept(MediaType("application", "activity+json"))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { content { contentType("application/activity+json") } }
.andExpect { jsonPath("\$.type") { value("Note") } }
.andExpect { jsonPath("\$.to") { value("https://www.w3.org/ns/activitystreams#Public") } }
.andExpect { jsonPath("\$.cc") { value("https://www.w3.org/ns/activitystreams#Public") } }
}
@Test
@Sql("/sql/note/匿名でunlisted投稿を取得できる.sql")
@WithAnonymousUser
fun 匿名でunlisted投稿を取得できる() {
mockMvc
.get("/users/test-user2/posts/1235") {
accept(MediaType("application", "activity+json"))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { content { contentType("application/activity+json") } }
.andExpect { jsonPath("\$.type") { value("Note") } }
.andExpect { jsonPath("\$.to") { value("https://example.com/users/test-user2/followers") } }
.andExpect { jsonPath("\$.cc") { value("https://www.w3.org/ns/activitystreams#Public") } }
}
@Test
@Transactional
@WithAnonymousUser
@Sql("/sql/note/匿名でfollowers投稿を取得しようとすると404.sql")
fun 匿名でfollowers投稿を取得しようとすると404() {
mockMvc
.get("/users/test-user2/posts/1236") {
accept(MediaType("application", "activity+json"))
}
.asyncDispatch()
.andExpect { status { isNotFound() } }
}
@Test
@WithAnonymousUser
fun 匿名でdirect投稿を取得しようとすると404() {
mockMvc
.get("/users/test-user2/posts/1236") {
accept(MediaType("application", "activity+json"))
}
.asyncDispatch()
.andExpect { status { isNotFound() } }
}
@Test
@Sql("/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql")
@WithHttpSignature(keyId = "https://follower.example.com/users/test-user5#pubkey")
fun HttpSignature認証でフォロワーがpublic投稿を取得できる() {
mockMvc
.get("/users/test-user4/posts/1237") {
accept(MediaType("application", "activity+json"))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { content { contentType("application/activity+json") } }
.andExpect { jsonPath("\$.type") { value("Note") } }
.andExpect { jsonPath("\$.to") { value("https://www.w3.org/ns/activitystreams#Public") } }
.andExpect { jsonPath("\$.cc") { value("https://www.w3.org/ns/activitystreams#Public") } }
}
@Test
@Sql("/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql")
@WithHttpSignature(keyId = "https://follower.example.com/users/test-user7#pubkey")
fun httpSignature認証でフォロワーがunlisted投稿を取得できる() {
mockMvc
.get("/users/test-user6/posts/1238") {
accept(MediaType("application", "activity+json"))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { content { contentType("application/activity+json") } }
.andExpect { jsonPath("\$.type") { value("Note") } }
.andExpect { jsonPath("\$.to") { value("https://example.com/users/test-user6/followers") } }
.andExpect { jsonPath("\$.cc") { value("https://www.w3.org/ns/activitystreams#Public") } }
}
@Test
@Sql("/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql")
@WithHttpSignature(keyId = "https://follower.example.com/users/test-user9#pubkey")
fun httpSignature認証でフォロワーがfollowers投稿を取得できる() {
mockMvc
.get("/users/test-user8/posts/1239") {
accept(MediaType("application", "activity+json"))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { content { contentType("application/activity+json") } }
.andExpect { jsonPath("\$.type") { value("Note") } }
.andExpect { jsonPath("\$.to") { value("https://example.com/users/test-user8/followers") } }
.andExpect { jsonPath("\$.cc") { value("https://example.com/users/test-user8/followers") } }
}
@Test
@Sql("/sql/note/リプライになっている投稿はinReplyToが存在する.sql")
@WithMockHttpSignature
fun リプライになっている投稿はinReplyToが存在する() {
mockMvc
.get("/users/test-user10/posts/1241") {
accept(MediaType("application", "activity+json"))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { content { contentType("application/activity+json") } }
.andExpect { jsonPath("\$.type") { value("Note") } }
.andExpect { jsonPath("\$.inReplyTo") { value("https://example.com/users/test-user10/posts/1240") } }
}
@Test
@Sql("/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql")
@WithMockHttpSignature
fun メディア付き投稿はattachmentにDocumentとして画像が存在する() {
mockMvc
.get("/users/test-user10/posts/1242") {
accept(MediaType("application", "activity+json"))
}
.asyncDispatch()
.andDo { print() }
.andExpect { status { isOk() } }
.andExpect { content { contentType("application/activity+json") } }
.andExpect { jsonPath("\$.type") { value("Note") } }
.andExpect { jsonPath("\$.attachment") { isArray() } }
.andExpect { jsonPath("\$.attachment[0].type") { value("Document") } }
.andExpect { jsonPath("\$.attachment[0].url") { value("https://example.com/media/test-media.png") } }
.andExpect { jsonPath("\$.attachment[1].type") { value("Document") } }
.andExpect { jsonPath("\$.attachment[1].url") { value("https://example.com/media/test-media2.png") } }
}
@Test
fun signatureヘッダーがあるのにhostヘッダーがないと401() {
mockMvc
.get("/users/test-user10/posts/9999") {
accept(MediaType("application", "activity+json"))
header("Signature", "a")
header("Date", ZonedDateTime.now().format(dateTimeFormatter))
}
.andExpect { status { isUnauthorized() } }
}
@Test
fun signatureヘッダーがあるのにdateヘッダーがないと401() {
mockMvc
.get("/users/test-user10/posts/9999") {
accept(MediaType("application", "activity+json"))
header("Signature", "a")
header("Host", "example.com")
}
.andExpect { status { isUnauthorized() } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway, @Autowired owlProducer: OwlProducer) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
}
}
}

View File

@ -1,116 +0,0 @@
/*
* 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 activitypub.webfinger
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
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.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.transaction.annotation.Transactional
import util.TestTransaction
import java.net.URL
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
class WebFingerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Test
@Sql("/sql/test-user.sql")
fun `webfinger 存在するユーザーを取得`() {
mockMvc
.get("/.well-known/webfinger?resource=acct:test-user@example.com")
.andExpect { status { isOk() } }
.andExpect { header { string("Content-Type", "application/json") } }
.andExpect {
jsonPath("\$.subject") {
value("acct:test-user@example.com")
}
}
.andExpect {
jsonPath("\$.links[0].rel") {
value("self")
}
}
.andExpect {
jsonPath("\$.links[0].href") { value("https://example.com/users/test-user") }
}
.andExpect {
jsonPath("\$.links[0].type") {
value("application/activity+json")
}
}
}
@Test
fun `webfinger 存在しないユーザーに404`() {
mockMvc
.get("/.well-known/webfinger?resource=acct:invalid-user-notfound-afdjashfal@example.com")
.andExpect { status { isNotFound() } }
}
@Test
fun `webfinger 不正なリクエストは400`() {
mockMvc
.get("/.well-known/webfinger?res=acct:test")
.andExpect { status { isBadRequest() } }
}
@Test
fun `webfinger acctのパースが出来なくても400`() {
mockMvc
.get("/.well-known/webfinger?resource=acct:@a@b@c@d")
.andExpect { status { isBadRequest() } }
}
@TestConfiguration
class Configuration {
@Bean
fun url(): URL {
return URL("https://example.com")
}
@Bean
fun testTransaction(): Transaction = TestTransaction
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway, @Autowired owlProducer: OwlProducer) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
}
}
}

View File

@ -1,170 +0,0 @@
/*
* 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.
*/
@file:Suppress("SpringJavaInjectionPointsAutowiringInspection")
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.domain.mastodon.model.generated.Status
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
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
@Suppress("NonAsciiCharacters")
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/test-user2.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/accounts/test-accounts-statuses.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class AccountApiPaginationTest {
@Suppress("SpringJavaInjectionPointsAutowiringInspection")
@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"))
)
}
.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, @Autowired owlProducer: OwlProducer) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
}
}
}

View File

@ -1,477 +0,0 @@
/*
* 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.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
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/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/test-user2.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class AccountApiTest {
@Autowired
private lateinit var followerQueryServiceImpl: FollowerQueryServiceImpl
@Autowired
private lateinit var actorRepository: ActorRepository
@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(jwt())
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(jwt())
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())
with(jwt())
}
.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())
with(jwt())
}
.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 = followerQueryServiceImpl.alreadyFollow(3733363, 37335363)
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, @Autowired owlProducer: OwlProducer) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
}
}
}

View File

@ -1,120 +0,0 @@
/*
* 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 dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
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.oauth2.server.authorization.client.RegisteredClient
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
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
class AppTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
@WithAnonymousUser
fun apiV1AppsPostにformで匿名でappを作成できる() {
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() } }
val app = RegisteredClient
.selectAll().where { RegisteredClient.clientName eq "test-client" }
.single()
assertThat(app[RegisteredClient.clientName]).isEqualTo("test-client")
assertThat(app[RegisteredClient.redirectUris]).isEqualTo("https://example.com")
assertThat(app[RegisteredClient.scopes]).isEqualTo("read,write")
}
@Test
@WithAnonymousUser
fun apiV1AppsPostにjsonで匿名でappを作成できる() {
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() } }
val app = RegisteredClient
.selectAll().where { RegisteredClient.clientName eq "test-client-2" }
.single()
assertThat(app[RegisteredClient.clientName]).isEqualTo("test-client-2")
assertThat(app[RegisteredClient.redirectUris]).isEqualTo("https://example.com")
assertThat(app[RegisteredClient.scopes]).isEqualTo("read,write")
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway, @Autowired owlProducer: OwlProducer) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
}
}
}

View File

@ -1,714 +0,0 @@
/*
* 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.domain.mastodon.model.generated.FilterKeywordsPostRequest
import dev.usbharu.hideout.domain.mastodon.model.generated.FilterPostRequest
import dev.usbharu.hideout.domain.mastodon.model.generated.FilterPostRequestKeyword
import dev.usbharu.hideout.domain.mastodon.model.generated.V1FilterPostRequest
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
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
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.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 = ActivityPubConfig().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 = ActivityPubConfig().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 = ActivityPubConfig().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 = ActivityPubConfig().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 = ActivityPubConfig().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 = ActivityPubConfig().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 { isOk() } }
}
@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 { isOk() } }
}
@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 { isOk() } }
}
@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 { isOk() } }
}
@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 { isOk() } }
}
@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 { isOk() } }
}
@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 = ActivityPubConfig().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 = ActivityPubConfig().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 = ActivityPubConfig().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
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
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_write"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway, @Autowired owlProducer: OwlProducer) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
}
}
}

View File

@ -1,145 +0,0 @@
/*
* 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 dev.usbharu.hideout.core.service.media.MediaDataStore
import dev.usbharu.hideout.core.service.media.MediaSaveRequest
import dev.usbharu.hideout.core.service.media.SuccessSavedMedia
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
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.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.whenever
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.boot.test.mock.mockito.MockBean
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/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class MediaTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var mediaDataStore: MediaDataStore
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
@Test
fun メディアをアップロードできる() = runTest {
whenever(mediaDataStore.save(any<MediaSaveRequest>())).doReturn(SuccessSavedMedia("", "a", "a"))
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 {
whenever(mediaDataStore.save(any<MediaSaveRequest>())).doReturn(SuccessSavedMedia("", "b", "b"))
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 {
whenever(mediaDataStore.save(any<MediaSaveRequest>())).doReturn(SuccessSavedMedia("", "", ""))
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, @Autowired owlProducer: OwlProducer) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
}
}
}

View File

@ -1,185 +0,0 @@
/*
* 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.domain.mastodon.model.generated.Notification
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
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/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/test-user2.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, @Autowired owlProducer: OwlProducer) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
}
}
}

View File

@ -1,219 +0,0 @@
/*
* 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.domain.mastodon.model.generated.Notification
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotification
import dev.usbharu.hideout.mastodon.domain.model.NotificationType
import dev.usbharu.hideout.mastodon.infrastructure.mongorepository.MongoMastodonNotificationRepository
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
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.BeforeAll
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
import java.time.Instant
@SpringBootTest(classes = [SpringApplication::class], properties = ["hideout.use-mongodb=true"])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/test-user2.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
@BeforeAll
fun setupMongodb(
@Autowired mongoMastodonNotificationRepository: MongoMastodonNotificationRepository,
) {
mongoMastodonNotificationRepository.deleteAll()
val notifications = (1..65).map {
MastodonNotification(
it.toLong(),
1,
NotificationType.follow,
Instant.now(),
2,
null,
null,
null
)
}
mongoMastodonNotificationRepository.saveAll(notifications)
}
@JvmStatic
@AfterAll
fun dropDatabase(
@Autowired flyway: Flyway,
@Autowired mongodbMastodonNotificationRepository: MongoMastodonNotificationRepository,
@Autowired owlProducer: OwlProducer,
) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
mongodbMastodonNotificationRepository.deleteAll()
}
}
}

View File

@ -1,247 +0,0 @@
/*
* 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.CustomEmoji
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
import dev.usbharu.hideout.core.infrastructure.exposedrepository.CustomEmojis
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
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
import java.time.Instant
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/test-post.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.emoji).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.emoji).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()
assertThat(reaction.emoji).isEqualTo(
CustomEmoji(
1,
"kotlin",
"example.com",
null,
"https://example.com/emojis/kotlin",
null,
Instant.ofEpochMilli(1704700290036)
)
)
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway, @Autowired owlProducer: OwlProducer) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
}
}
}

View File

@ -1,136 +0,0 @@
/*
* 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 dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
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/test-user.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, @Autowired owlProducer: OwlProducer) {
flyway.clean()
flyway.migrate()
runBlocking {
owlProducer.stop()
}
}
}
}

View File

@ -1,36 +0,0 @@
/*
* 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.core.annotation.AliasFor
import org.springframework.security.test.context.support.TestExecutionEvent
import org.springframework.security.test.context.support.WithSecurityContext
import java.lang.annotation.Inherited
@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

@ -1,65 +0,0 @@
/*
* 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
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
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 kotlinx.coroutines.runBlocking
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 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 UserNotFoundException.withKeyId(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

@ -1,39 +0,0 @@
/*
* 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.core.annotation.AliasFor
import org.springframework.security.test.context.support.TestExecutionEvent
import org.springframework.security.test.context.support.WithSecurityContext
import java.lang.annotation.Inherited
@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

@ -1,55 +0,0 @@
/*
* 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

@ -1,40 +0,0 @@
hideout:
debug:
trace-query-exception: true
trace-query-call: true
url: "https://example.com"
use-mongodb: true
security:
jwt:
generate: true
key-id: a
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
storage:
type: local
private: false
spring:
flyway:
enabled: true
clean-disabled: false
datasource:
driver-class-name: org.h2.Driver
url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4;"
username: ""
password:
data:
mongodb:
auto-index-creation: true
host: localhost
port: 27017
database: hideout-integration-test
h2:
console:
enabled: true
# exposed:
# generate-ddl: true
# excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed
server:
port: 8080

View File

@ -1,2 +0,0 @@
junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$Random
junit.jupiter.testmethod.order.default=org.junit.jupiter.api.MethodOrderer$Random

View File

@ -1,11 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.springframework.security" level="TRACE"/>
</configuration>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -1,17 +0,0 @@
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

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

View File

@ -1,4 +0,0 @@
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

@ -1,28 +0,0 @@
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

@ -1,29 +0,0 @@
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

@ -1,29 +0,0 @@
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

@ -1,25 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,66 +0,0 @@
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

@ -1,66 +0,0 @@
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,3 +0,0 @@
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

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

View File

@ -1,10 +0,0 @@
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

@ -1,10 +0,0 @@
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,19 @@
/*
* 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 dev.usbharu.hideout.core.application.media
class Media

View File

@ -14,9 +14,8 @@
* limitations under the License.
*/
package util
package dev.usbharu.hideout.core.application.media
import org.springframework.boot.test.context.SpringBootTest
import java.nio.file.Path
@SpringBootTest
abstract class SpringApplicationTestBase
data class UploadMedia(val path: Path)

View File

@ -0,0 +1,34 @@
/*
* 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 dev.usbharu.hideout.core.application.media
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.CommandExecutor
import dev.usbharu.hideout.core.application.shared.Transaction
import org.slf4j.LoggerFactory
class UploadMediaApplicationService(transaction: Transaction) : AbstractApplicationService<UploadMedia, Media>(
transaction, logger
) {
companion object {
private val logger = LoggerFactory.getLogger(UploadMediaApplicationService::class.java)
}
override suspend fun internalExecute(command: UploadMedia, executor: CommandExecutor): Media {
TODO()
}
}

View File

@ -14,12 +14,10 @@
* limitations under the License.
*/
package util
package dev.usbharu.hideout.core.external.media
import dev.usbharu.hideout.core.application.shared.Transaction
import java.nio.file.Path
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()
}
interface MediaProcessor {
suspend fun process(path: Path): ProcessedMedia
}

View File

@ -0,0 +1,29 @@
/*
* 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 dev.usbharu.hideout.core.external.media
import dev.usbharu.hideout.core.domain.model.media.FileType
import dev.usbharu.hideout.core.domain.model.media.MimeType
import java.nio.file.Path
data class ProcessedMedia(
val path: Path,
val thumbnailPath: Path?,
val fileType: FileType,
val mimeType: MimeType,
val blurHash: String,
)

View File

@ -2,4 +2,6 @@ kotlin.code.style=official
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true
#ksp.useKSP2=true
#ksp.useKSP2=true
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn

View File

@ -19,5 +19,5 @@ tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(21)
jvmToolchain(17)
}