mirror of https://github.com/usbharu/Hideout.git
commit
4943b8e94e
|
@ -51,7 +51,7 @@ jobs:
|
|||
with:
|
||||
path: |
|
||||
build
|
||||
key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('src') }}-${{ github.sha }}
|
||||
key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('**/*.kt') }}-${{ github.sha }}
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
|
@ -116,6 +116,7 @@ jobs:
|
|||
arguments: test
|
||||
|
||||
- name: Save Test Report
|
||||
if: always()
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: build/test-results
|
||||
|
@ -179,6 +180,7 @@ jobs:
|
|||
arguments: integrationTest
|
||||
|
||||
- name: Save Test Report
|
||||
if: always()
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: build/test-results
|
||||
|
@ -234,7 +236,7 @@ jobs:
|
|||
- name: Run Kover
|
||||
uses: gradle/gradle-build-action@v2.8.1
|
||||
with:
|
||||
arguments: koverXmlReport -x integrationTest
|
||||
arguments: koverXmlReport -x integrationTest -x e2eTest
|
||||
|
||||
- name: Add coverage report to PR
|
||||
if: always()
|
||||
|
@ -253,7 +255,7 @@ jobs:
|
|||
report-tests:
|
||||
name: Report Tests
|
||||
if: success() || failure()
|
||||
needs: [ unit-test,integration-test ]
|
||||
needs: [ unit-test,integration-test,e2e-test ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Restore Test Report
|
||||
|
@ -268,6 +270,12 @@ jobs:
|
|||
path: build/test-results
|
||||
key: integration-test-report-${{ github.sha }}
|
||||
|
||||
- name: Restore Test Report
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: build/test-results
|
||||
key: e2e-test-report-${{ github.sha }}
|
||||
|
||||
- name: JUnit Test Report
|
||||
uses: mikepenz/action-junit-report@v2
|
||||
with:
|
||||
|
@ -330,3 +338,75 @@ jobs:
|
|||
uses: reviewdog/action-suggester@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
e2e-test:
|
||||
name: E2E Test
|
||||
needs: [ setup ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Gradle Wrapper Cache
|
||||
uses: actions/cache@v3.3.2
|
||||
with:
|
||||
path: ~/.gradle/wrapper
|
||||
key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Dependencies Cache
|
||||
uses: actions/cache@v3.3.2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/cache/jars-*
|
||||
~/.gradle/caches/transforms-*
|
||||
~/.gradle/caches/modules-*
|
||||
key: gradle-dependencies-${{ hashFiles('**/*.gradle.kts') }}
|
||||
restore-keys: gradle-dependencies-
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v3.3.2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches/build-cache-*
|
||||
~/.gradle/caches/[0-9]*.*
|
||||
.gradle
|
||||
key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}-
|
||||
|
||||
- name: Build Cache
|
||||
uses: actions/cache@v3.3.2
|
||||
with:
|
||||
path: |
|
||||
build
|
||||
key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('src') }}-${{ github.sha }}
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: MongoDB in GitHub Actions
|
||||
uses: supercharge/mongodb-github-action@1.10.0
|
||||
with:
|
||||
mongodb-version: latest
|
||||
|
||||
- name: setup-chrome
|
||||
id: setup-chrome
|
||||
uses: browser-actions/setup-chrome@v1.4.0
|
||||
|
||||
- name: Add Path
|
||||
run: echo ${{ steps.setup-chrome.outputs.chrome-path }} >> $GITHUB_PATH
|
||||
|
||||
- name: E2E Test
|
||||
uses: gradle/gradle-build-action@v2.8.1
|
||||
with:
|
||||
arguments: e2eTest
|
||||
|
||||
|
||||
- name: Save Test Report
|
||||
if: always()
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: build/test-results
|
||||
key: e2e-test-report-${{ github.sha }}
|
||||
|
|
|
@ -40,3 +40,5 @@ out/
|
|||
/src/main/web/generated/
|
||||
/stats.html
|
||||
/tomcat/
|
||||
/tomcat-e2e/
|
||||
/e2eTest.log
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
|
||||
|
||||
|
@ -32,6 +31,10 @@ sourceSets {
|
|||
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 {
|
||||
|
@ -41,6 +44,14 @@ 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"
|
||||
|
@ -52,13 +63,24 @@ val integrationTest = task<Test>("integrationTest") {
|
|||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
tasks.check { dependsOn(integrationTest) }
|
||||
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()
|
||||
val cpus = Runtime.getRuntime().availableProcessors()
|
||||
// maxParallelForks = max(1, cpus - 1)
|
||||
// setForkEvery(4)
|
||||
doFirst {
|
||||
jvmArgs = arrayOf(
|
||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED"
|
||||
|
@ -207,6 +229,15 @@ dependencies {
|
|||
intTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
|
||||
intTestImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
|
||||
|
||||
e2eTestImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
e2eTestImplementation("org.springframework.security:spring-security-test")
|
||||
e2eTestImplementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||
e2eTestImplementation("org.jsoup:jsoup:1.17.1")
|
||||
e2eTestImplementation("com.intuit.karate:karate-junit5:1.4.1")
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
detekt {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Users
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.select
|
||||
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 = Users.selectAll()
|
||||
println(selectAll.fetchSize)
|
||||
|
||||
println(selectAll.toList().size)
|
||||
|
||||
selectAll.map { "@${it[Users.name]}@${it[Users.domain]}" }.forEach { println(it) }
|
||||
|
||||
return Users.select { Users.name eq username and (Users.domain eq s) }.empty().not()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
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")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
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.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
|
||||
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(@Autowired flyway: Flyway) {
|
||||
server = MockServer.feature("classpath:federation/InboxxCommonMockServerTest.feature").http(0).build()
|
||||
_remotePort = server.port.toString()
|
||||
|
||||
flyway.clean()
|
||||
flyway.migrate()
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
@JvmStatic
|
||||
fun afterAll() {
|
||||
server.stop()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
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:
|
||||
use-s3: true
|
||||
endpoint: "http://localhost:8082/test-hideout"
|
||||
public-url: "http://localhost:8082/test-hideout"
|
||||
bucket: "test-hideout"
|
||||
region: "auto"
|
||||
access-key: ""
|
||||
secret-key: ""
|
||||
|
||||
spring:
|
||||
flyway:
|
||||
enabled: true
|
||||
clean-disabled: false
|
||||
datasource:
|
||||
driver-class-name: org.h2.Driver
|
||||
url: "jdbc:h2:./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
|
|
@ -0,0 +1,140 @@
|
|||
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
|
|
@ -0,0 +1,29 @@
|
|||
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
|
|
@ -0,0 +1,158 @@
|
|||
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
|
|
@ -0,0 +1,136 @@
|
|||
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
|
|
@ -0,0 +1,30 @@
|
|||
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;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<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>
|
|
@ -0,0 +1,95 @@
|
|||
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
|
|
@ -0,0 +1,46 @@
|
|||
insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY,
|
||||
CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS, INSTANCE)
|
||||
VALUES (1730415786666758144, 'test-user', 'localhost', 'Im test user.', 'THis account is test user.',
|
||||
'$2a$10$/mWC/n7nC7X3l9qCEOKnredxne2zewoqEsJWTOdlKfg2zXKJ0F9Em', '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', null);
|
|
@ -129,12 +129,36 @@ class AccountApiTest {
|
|||
mockMvc
|
||||
.post("/api/v1/accounts") {
|
||||
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
||||
param("username", "api-test-user-3")
|
||||
param("username", "api-test-user-4")
|
||||
with(SecurityMockMvcRequestPostProcessors.csrf())
|
||||
}
|
||||
.andExpect { status { isBadRequest() } }
|
||||
}
|
||||
|
||||
@Test
|
||||
@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(SecurityMockMvcRequestPostProcessors.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() } }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@AfterAll
|
||||
|
|
|
@ -3,12 +3,11 @@ package dev.usbharu.hideout.activitypub.domain.model
|
|||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||
|
||||
open class Key(
|
||||
type: List<String>,
|
||||
override val id: String,
|
||||
val owner: String,
|
||||
val publicKeyPem: String
|
||||
) : Object(
|
||||
type = add(list = type, type = "Key")
|
||||
type = add(list = emptyList(), type = "Key")
|
||||
),
|
||||
HasId {
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ constructor(
|
|||
var outbox: String,
|
||||
var url: String,
|
||||
private var icon: Image?,
|
||||
var publicKey: Key?,
|
||||
var publicKey: Key,
|
||||
var endpoints: Map<String, String> = emptyMap(),
|
||||
var followers: String?,
|
||||
var following: String?
|
||||
|
@ -22,9 +22,13 @@ constructor(
|
|||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Person) return false
|
||||
if (javaClass != other?.javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as Person
|
||||
|
||||
if (name != other.name) return false
|
||||
if (id != other.id) return false
|
||||
if (preferredUsername != other.preferredUsername) return false
|
||||
if (summary != other.summary) return false
|
||||
if (inbox != other.inbox) return false
|
||||
|
@ -33,20 +37,26 @@ constructor(
|
|||
if (icon != other.icon) return false
|
||||
if (publicKey != other.publicKey) return false
|
||||
if (endpoints != other.endpoints) return false
|
||||
if (followers != other.followers) return false
|
||||
if (following != other.following) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + id.hashCode()
|
||||
result = 31 * result + (preferredUsername?.hashCode() ?: 0)
|
||||
result = 31 * result + (summary?.hashCode() ?: 0)
|
||||
result = 31 * result + inbox.hashCode()
|
||||
result = 31 * result + outbox.hashCode()
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + (icon?.hashCode() ?: 0)
|
||||
result = 31 * result + (publicKey?.hashCode() ?: 0)
|
||||
result = 31 * result + publicKey.hashCode()
|
||||
result = 31 * result + endpoints.hashCode()
|
||||
result = 31 * result + (followers?.hashCode() ?: 0)
|
||||
result = 31 * result + (following?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ interface InboxController {
|
|||
"application/activity+json",
|
||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||
],
|
||||
consumes = ["application/json", "application/*+json"],
|
||||
method = [RequestMethod.POST]
|
||||
)
|
||||
suspend fun inbox(@RequestBody string: String): ResponseEntity<Unit>
|
||||
|
|
|
@ -23,7 +23,9 @@ class InboxControllerImpl(private val apService: APService) : InboxController {
|
|||
val request = (requireNotNull(RequestContextHolder.getRequestAttributes()) as ServletRequestAttributes).request
|
||||
|
||||
val headersList = request.headerNames?.toList().orEmpty()
|
||||
if (headersList.contains("Signature").not()) {
|
||||
LOGGER.trace("Inbox Headers {}", headersList)
|
||||
|
||||
if (headersList.map { it.lowercase() }.contains("signature").not()) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.header(
|
||||
WWW_AUTHENTICATE,
|
||||
|
|
|
@ -34,25 +34,38 @@ class InboxJobProcessor(
|
|||
private val transaction: Transaction
|
||||
) : JobProcessor<InboxJobParam, InboxJob> {
|
||||
|
||||
private suspend fun verifyHttpSignature(httpRequest: HttpRequest, signature: Signature): Boolean {
|
||||
private suspend fun verifyHttpSignature(
|
||||
httpRequest: HttpRequest,
|
||||
signature: Signature,
|
||||
transaction: Transaction
|
||||
): Boolean {
|
||||
val requiredHeaders = when (httpRequest.method) {
|
||||
HttpMethod.GET -> getRequiredHeaders
|
||||
HttpMethod.POST -> postRequiredHeaders
|
||||
}
|
||||
if (signature.headers.containsAll(requiredHeaders).not()) {
|
||||
logger.warn("FAILED Invalid signature. require: {}", requiredHeaders)
|
||||
return false
|
||||
}
|
||||
|
||||
val user = try {
|
||||
userQueryService.findByKeyId(signature.keyId)
|
||||
} catch (_: FailedToGetResourcesException) {
|
||||
apUserService.fetchPersonWithEntity(signature.keyId).second
|
||||
val user = transaction.transaction {
|
||||
try {
|
||||
userQueryService.findByKeyId(signature.keyId)
|
||||
} catch (_: FailedToGetResourcesException) {
|
||||
apUserService.fetchPersonWithEntity(signature.keyId).second
|
||||
}
|
||||
}
|
||||
|
||||
val verify = signatureVerifier.verify(
|
||||
httpRequest,
|
||||
PublicKey(RsaUtil.decodeRsaPublicKeyPem(user.publicKey), signature.keyId)
|
||||
)
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
val verify = try {
|
||||
signatureVerifier.verify(
|
||||
httpRequest,
|
||||
PublicKey(RsaUtil.decodeRsaPublicKeyPem(user.publicKey), signature.keyId)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("FAILED Verify Http Signature", e)
|
||||
return false
|
||||
}
|
||||
|
||||
return verify.success
|
||||
}
|
||||
|
@ -60,6 +73,7 @@ class InboxJobProcessor(
|
|||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun parseSignatureHeader(httpHeaders: HttpHeaders): Signature? {
|
||||
return try {
|
||||
println("Signature Header =" + httpHeaders.get("Signature").single())
|
||||
signatureHeaderParser.parse(httpHeaders)
|
||||
} catch (e: RuntimeException) {
|
||||
logger.trace("FAILED parse signature header", e)
|
||||
|
@ -67,7 +81,7 @@ class InboxJobProcessor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun process(param: InboxJobParam) = transaction.transaction {
|
||||
override suspend fun process(param: InboxJobParam) {
|
||||
val jsonNode = objectMapper.readTree(param.json)
|
||||
|
||||
logger.info("START Process inbox. type: {}", param.type)
|
||||
|
@ -83,22 +97,24 @@ class InboxJobProcessor(
|
|||
|
||||
logger.debug("Has signature? {}", signature != null)
|
||||
|
||||
val verify = signature?.let { verifyHttpSignature(httpRequest, it) } ?: false
|
||||
val verify = signature?.let { verifyHttpSignature(httpRequest, it, transaction) } ?: false
|
||||
|
||||
logger.debug("Is verifying success? {}", verify)
|
||||
transaction.transaction {
|
||||
logger.debug("Is verifying success? {}", verify)
|
||||
|
||||
val activityPubProcessor =
|
||||
activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as ActivityPubProcessor<Object>?
|
||||
val activityPubProcessor =
|
||||
activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as ActivityPubProcessor<Object>?
|
||||
|
||||
if (activityPubProcessor == null) {
|
||||
logger.warn("ActivityType {} is not support.", param.type)
|
||||
throw IllegalStateException("ActivityPubProcessor not found.")
|
||||
if (activityPubProcessor == null) {
|
||||
logger.warn("ActivityType {} is not support.", param.type)
|
||||
throw IllegalStateException("ActivityPubProcessor not found.")
|
||||
}
|
||||
|
||||
val value = objectMapper.treeToValue(jsonNode, activityPubProcessor.type())
|
||||
activityPubProcessor.process(ActivityPubProcessContext(value, jsonNode, httpRequest, signature, verify))
|
||||
|
||||
logger.info("SUCCESS Process inbox. type: {}", param.type)
|
||||
}
|
||||
|
||||
val value = objectMapper.treeToValue(jsonNode, activityPubProcessor.type())
|
||||
activityPubProcessor.process(ActivityPubProcessContext(value, jsonNode, httpRequest, signature, verify))
|
||||
|
||||
logger.info("SUCCESS Process inbox. type: {}", param.type)
|
||||
}
|
||||
|
||||
override fun job(): InboxJob = InboxJob
|
||||
|
|
|
@ -14,6 +14,7 @@ import dev.usbharu.hideout.core.query.UserQueryService
|
|||
import dev.usbharu.hideout.core.service.user.RemoteUserCreateDto
|
||||
import dev.usbharu.hideout.core.service.user.UserService
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
interface APUserService {
|
||||
suspend fun getPersonByName(name: String): Person
|
||||
|
@ -61,7 +62,6 @@ class APUserServiceImpl(
|
|||
url = "$userUrl/icon.png"
|
||||
),
|
||||
publicKey = Key(
|
||||
type = emptyList(),
|
||||
id = userEntity.keyId,
|
||||
owner = userUrl,
|
||||
publicKeyPem = userEntity.publicKey
|
||||
|
@ -75,6 +75,7 @@ class APUserServiceImpl(
|
|||
override suspend fun fetchPerson(url: String, targetActor: String?): Person =
|
||||
fetchPersonWithEntity(url, targetActor).first
|
||||
|
||||
@Transactional
|
||||
override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair<Person, User> {
|
||||
return try {
|
||||
val userEntity = userQueryService.findByUrl(url)
|
||||
|
@ -94,15 +95,13 @@ class APUserServiceImpl(
|
|||
name = person.preferredUsername
|
||||
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
|
||||
domain = id.substringAfter("://").substringBefore("/"),
|
||||
screenName = person.name
|
||||
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
|
||||
screenName = person.name,
|
||||
description = person.summary.orEmpty(),
|
||||
inbox = person.inbox,
|
||||
outbox = person.outbox,
|
||||
url = id,
|
||||
publicKey = person.publicKey?.publicKeyPem
|
||||
?: throw IllegalActivityPubObjectException("publicKey is null"),
|
||||
keyId = person.publicKey?.id ?: throw IllegalActivityPubObjectException("publicKey keyId is null"),
|
||||
publicKey = person.publicKey.publicKeyPem,
|
||||
keyId = person.publicKey.id,
|
||||
following = person.following,
|
||||
followers = person.followers,
|
||||
sharedInbox = person.endpoints["sharedInbox"]
|
||||
|
@ -129,7 +128,6 @@ class APUserServiceImpl(
|
|||
url = "$id/icon.png"
|
||||
),
|
||||
publicKey = Key(
|
||||
type = emptyList(),
|
||||
id = userEntity.keyId,
|
||||
owner = id,
|
||||
publicKeyPem = userEntity.publicKey
|
||||
|
|
|
@ -7,6 +7,8 @@ import com.fasterxml.jackson.core.JsonParser
|
|||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import dev.usbharu.hideout.core.infrastructure.httpsignature.HttpRequestMixIn
|
||||
import dev.usbharu.httpsignature.common.HttpRequest
|
||||
import dev.usbharu.httpsignature.sign.HttpSignatureSigner
|
||||
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
|
@ -24,12 +26,13 @@ class ActivityPubConfig {
|
|||
val objectMapper = jacksonObjectMapper()
|
||||
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||
.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY))
|
||||
.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY))
|
||||
.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP))
|
||||
.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SKIP))
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
|
||||
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
|
||||
.configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true)
|
||||
.addMixIn(HttpRequest::class.java, HttpRequestMixIn::class.java)
|
||||
return objectMapper
|
||||
}
|
||||
|
||||
|
|
|
@ -11,12 +11,14 @@ import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.Htt
|
|||
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUserDetailsService
|
||||
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureVerifierComposite
|
||||
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsImpl
|
||||
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsServiceImpl
|
||||
import dev.usbharu.hideout.core.query.UserQueryService
|
||||
import dev.usbharu.hideout.util.RsaUtil
|
||||
import dev.usbharu.hideout.util.hasAnyScope
|
||||
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
|
||||
import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser
|
||||
import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier
|
||||
import jakarta.annotation.PostConstruct
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer
|
||||
|
@ -32,6 +34,8 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
|
|||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
|
||||
import org.springframework.security.authentication.AccountStatusUserDetailsChecker
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
|
@ -59,7 +63,7 @@ import java.security.interfaces.RSAPrivateKey
|
|||
import java.security.interfaces.RSAPublicKey
|
||||
import java.util.*
|
||||
|
||||
@EnableWebSecurity(debug = true)
|
||||
@EnableWebSecurity(debug = false)
|
||||
@Configuration
|
||||
@Suppress("FunctionMaxLength", "TooManyFunctions")
|
||||
class SecurityConfig {
|
||||
|
@ -80,7 +84,9 @@ class SecurityConfig {
|
|||
http {
|
||||
securityMatcher("/users/*/posts/*")
|
||||
addFilterAt<RequestCacheAwareFilter>(httpSignatureFilter)
|
||||
addFilterBefore<HttpSignatureFilter>(ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
|
||||
addFilterBefore<HttpSignatureFilter>(
|
||||
ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
|
||||
)
|
||||
authorizeHttpRequests {
|
||||
authorize(anyRequest, permitAll)
|
||||
}
|
||||
|
@ -114,6 +120,16 @@ class SecurityConfig {
|
|||
}
|
||||
|
||||
@Bean
|
||||
@Order(2)
|
||||
fun daoAuthenticationProvider(userDetailsServiceImpl: UserDetailsServiceImpl): DaoAuthenticationProvider {
|
||||
val daoAuthenticationProvider = DaoAuthenticationProvider()
|
||||
daoAuthenticationProvider.setUserDetailsService(userDetailsServiceImpl)
|
||||
|
||||
return daoAuthenticationProvider
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
fun httpSignatureAuthenticationProvider(transaction: Transaction): PreAuthenticatedAuthenticationProvider {
|
||||
val provider = PreAuthenticatedAuthenticationProvider()
|
||||
val signatureHeaderParser = DefaultSignatureHeaderParser()
|
||||
|
@ -146,7 +162,6 @@ class SecurityConfig {
|
|||
}
|
||||
oauth2ResourceServer {
|
||||
jwt {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -158,7 +173,6 @@ class SecurityConfig {
|
|||
fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
authorizeHttpRequests {
|
||||
|
||||
authorize("/error", permitAll)
|
||||
authorize("/login", permitAll)
|
||||
authorize(GET, "/.well-known/**", permitAll)
|
||||
|
@ -186,11 +200,10 @@ class SecurityConfig {
|
|||
}
|
||||
|
||||
formLogin {
|
||||
|
||||
}
|
||||
|
||||
csrf {
|
||||
ignoringRequestMatchers("/users/*/inbox", "/inbox", "/api/v1/apps", "/api/v1/accounts")
|
||||
ignoringRequestMatchers("/users/*/inbox", "/inbox", "/api/v1/apps")
|
||||
}
|
||||
|
||||
headers {
|
||||
|
@ -269,3 +282,17 @@ data class JwkConfig(
|
|||
val publicKey: String,
|
||||
val privateKey: String
|
||||
)
|
||||
|
||||
@Configuration
|
||||
class PostSecurityConfig(
|
||||
val auth: AuthenticationManagerBuilder,
|
||||
val daoAuthenticationProvider: DaoAuthenticationProvider,
|
||||
val httpSignatureAuthenticationProvider: PreAuthenticatedAuthenticationProvider
|
||||
) {
|
||||
|
||||
@PostConstruct
|
||||
fun config() {
|
||||
auth.authenticationProvider(daoAuthenticationProvider)
|
||||
auth.authenticationProvider(httpSignatureAuthenticationProvider)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package dev.usbharu.hideout.core.infrastructure.httpsignature
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import dev.usbharu.httpsignature.common.HttpHeaders
|
||||
import dev.usbharu.httpsignature.common.HttpMethod
|
||||
import dev.usbharu.httpsignature.common.HttpRequest
|
||||
import java.net.URL
|
||||
|
||||
@JsonDeserialize(using = HttpRequestDeserializer::class)
|
||||
@JsonSubTypes
|
||||
abstract class HttpRequestMixIn
|
||||
|
||||
class HttpRequestDeserializer : JsonDeserializer<HttpRequest>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): HttpRequest {
|
||||
val readTree: JsonNode = p.codec.readTree(p)
|
||||
|
||||
return HttpRequest(
|
||||
URL(readTree["url"].textValue()),
|
||||
HttpHeaders(emptyMap()),
|
||||
HttpMethod.valueOf(readTree["method"].textValue())
|
||||
)
|
||||
}
|
||||
}
|
|
@ -32,7 +32,10 @@ class KJobJobQueueParentService : JobQueueParentService {
|
|||
}
|
||||
|
||||
override suspend fun <T, J : HideoutJob<T, J>> scheduleTypeSafe(job: J, jobProps: T) {
|
||||
logger.debug("SCHEDULE Job: {}", job.name)
|
||||
logger.trace("Job props: {}", jobProps)
|
||||
val convert: ScheduleContext<J>.(J) -> Unit = job.convert(jobProps)
|
||||
kjob.schedule(job, convert)
|
||||
logger.debug("SUCCESS Schedule Job: {}", job.name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package dev.usbharu.hideout.core.infrastructure.kjobmongodb
|
|||
|
||||
import com.mongodb.reactivestreams.client.MongoClient
|
||||
import dev.usbharu.hideout.core.external.job.HideoutJob
|
||||
import dev.usbharu.hideout.core.service.job.JobProcessor
|
||||
import dev.usbharu.hideout.core.service.job.JobQueueWorkerService
|
||||
import kjob.core.dsl.JobContextWithProps
|
||||
import kjob.core.dsl.JobRegisterContext
|
||||
|
@ -13,7 +14,10 @@ import org.springframework.stereotype.Service
|
|||
|
||||
@Service
|
||||
@ConditionalOnProperty(name = ["hideout.use-mongodb"], havingValue = "true", matchIfMissing = false)
|
||||
class KJobMongoJobQueueWorkerService(private val mongoClient: MongoClient) : JobQueueWorkerService, AutoCloseable {
|
||||
class KJobMongoJobQueueWorkerService(
|
||||
private val mongoClient: MongoClient,
|
||||
private val jobQueueProcessorList: List<JobProcessor<*, *>>
|
||||
) : JobQueueWorkerService, AutoCloseable {
|
||||
val kjob by lazy {
|
||||
kjob(Mongo) {
|
||||
client = mongoClient
|
||||
|
@ -30,6 +34,14 @@ class KJobMongoJobQueueWorkerService(private val mongoClient: MongoClient) : Job
|
|||
defines.forEach { job ->
|
||||
kjob.register(job.first, job.second)
|
||||
}
|
||||
for (jobProcessor in jobQueueProcessorList) {
|
||||
kjob.register(jobProcessor.job()) {
|
||||
execute {
|
||||
val param = it.convertUnsafe(props)
|
||||
jobProcessor.process(param)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
|
|
@ -7,6 +7,7 @@ import kjob.core.Job
|
|||
import kjob.core.dsl.ScheduleContext
|
||||
import kjob.core.kjob
|
||||
import kjob.mongo.Mongo
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
|
@ -26,15 +27,23 @@ class KjobMongoJobQueueParentService(private val mongoClient: MongoClient) : Job
|
|||
|
||||
@Deprecated("use type safe → scheduleTypeSafe")
|
||||
override suspend fun <J : Job> schedule(job: J, block: ScheduleContext<J>.(J) -> Unit) {
|
||||
logger.debug("SCHEDULE Job: {}", job.name)
|
||||
kjob.schedule(job, block)
|
||||
}
|
||||
|
||||
override suspend fun <T, J : HideoutJob<T, J>> scheduleTypeSafe(job: J, jobProps: T) {
|
||||
logger.debug("SCHEDULE Job: {}", job.name)
|
||||
logger.trace("Job props: {}", jobProps)
|
||||
val convert = job.convert(jobProps)
|
||||
kjob.schedule(job, convert)
|
||||
logger.debug("SUCCESS Job: {}", job.name)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
kjob.shutdown()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(KjobMongoJobQueueParentService::class.java)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,13 +27,10 @@ class HttpSignatureUserDetailsService(
|
|||
) :
|
||||
AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
|
||||
override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking {
|
||||
if (token.principal !is String) {
|
||||
throw IllegalStateException("Token is not String")
|
||||
}
|
||||
check(token.principal is String) { "Token is not String" }
|
||||
val credentials = token.credentials
|
||||
if (credentials !is HttpRequest) {
|
||||
throw IllegalStateException("Credentials is not HttpRequest")
|
||||
}
|
||||
|
||||
check(credentials is HttpRequest) { "Credentials is not HttpRequest" }
|
||||
|
||||
val keyId = token.principal as String
|
||||
val findByKeyId = transaction.transaction {
|
||||
|
|
|
@ -81,7 +81,7 @@ class InstanceServiceImpl(
|
|||
}
|
||||
|
||||
else -> {
|
||||
TODO()
|
||||
throw IllegalStateException("Unknown nodeinfo versions: $key url: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,13 @@ class InMemoryCacheManager : CacheManager {
|
|||
}
|
||||
}
|
||||
if (needRunBlock) {
|
||||
val processed = block()
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
val processed = try {
|
||||
block()
|
||||
} catch (e: Exception) {
|
||||
cacheKey.remove(key)
|
||||
throw e
|
||||
}
|
||||
|
||||
if (cacheKey.containsKey(key)) {
|
||||
valueStore[key] = processed
|
||||
|
|
|
@ -12,6 +12,7 @@ import dev.usbharu.hideout.core.service.instance.InstanceService
|
|||
import org.jetbrains.exposed.exceptions.ExposedSQLException
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.Instant
|
||||
|
||||
@Service
|
||||
|
@ -57,7 +58,9 @@ class UserServiceImpl(
|
|||
return userRepository.save(userEntity)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override suspend fun createRemoteUser(user: RemoteUserCreateDto): User {
|
||||
logger.info("START Create New remote user. name: {} url: {}", user.name, user.url)
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
val instance = try {
|
||||
instanceService.fetchInstance(user.url, user.sharedInbox)
|
||||
|
@ -84,8 +87,11 @@ class UserServiceImpl(
|
|||
instance = instance?.id
|
||||
)
|
||||
return try {
|
||||
userRepository.save(userEntity)
|
||||
val save = userRepository.save(userEntity)
|
||||
logger.warn("SUCCESS Create New remote user. id: {} name: {} url: {}", userEntity.id, user.name, user.url)
|
||||
save
|
||||
} catch (_: ExposedSQLException) {
|
||||
logger.warn("FAILED User already exists. name: {} url: {}", user.name, user.url)
|
||||
userQueryService.findByUrl(user.url)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import org.springframework.security.web.access.intercept.RequestAuthorizationCon
|
|||
fun AuthorizeHttpRequestsDsl.hasScope(scope: String): AuthorizationManager<RequestAuthorizationContext> =
|
||||
hasAuthority("SCOPE_$scope")
|
||||
|
||||
@Suppress("SpreadOperator")
|
||||
fun AuthorizeHttpRequestsDsl.hasAnyScope(vararg scopes: String): AuthorizationManager<RequestAuthorizationContext> =
|
||||
hasAnyAuthority(
|
||||
*scopes.map { "SCOPE_$it" }.toTypedArray()
|
||||
)
|
||||
hasAnyAuthority(*scopes.map { "SCOPE_$it" }.toTypedArray())
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package dev.usbharu.hideout.activitypub.domain.model
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.usbharu.hideout.application.config.ActivityPubConfig
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class KeySerializeTest {
|
||||
@Test
|
||||
fun Keyのデシリアライズができる() {
|
||||
//language=JSON
|
||||
val trimIndent = """
|
||||
{
|
||||
"id": "https://mastodon.social/users/Gargron#main-key",
|
||||
"owner": "https://mastodon.social/users/Gargron",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXc4vkECU2/CeuSo1wtn\nFoim94Ne1jBMYxTZ9wm2YTdJq1oiZKif06I2fOqDzY/4q/S9uccrE9Bkajv1dnkO\nVm31QjWlhVpSKynVxEWjVBO5Ienue8gND0xvHIuXf87o61poqjEoepvsQFElA5ym\novljWGSA/jpj7ozygUZhCXtaS2W5AD5tnBQUpcO0lhItYPYTjnmzcc4y2NbJV8hz\n2s2G8qKv8fyimE23gY1XrPJg+cRF+g4PqFXujjlJ7MihD9oqtLGxbu7o1cifTn3x\nBfIdPythWu5b4cujNsB3m3awJjVmx+MHQ9SugkSIYXV0Ina77cTNS0M2PYiH1PFR\nTwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val objectMapper = ActivityPubConfig().objectMapper()
|
||||
|
||||
val readValue = objectMapper.readValue<Key>(trimIndent)
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package dev.usbharu.hideout.activitypub.domain.model
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.usbharu.hideout.application.config.ActivityPubConfig
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PersonSerializeTest {
|
||||
@Test
|
||||
fun MastodonのPersonのデシリアライズができる() {
|
||||
val personString = """
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1"
|
||||
],
|
||||
"id": "https://mastodon.social/users/Gargron",
|
||||
"type": "Person",
|
||||
"following": "https://mastodon.social/users/Gargron/following",
|
||||
"followers": "https://mastodon.social/users/Gargron/followers",
|
||||
"inbox": "https://mastodon.social/users/Gargron/inbox",
|
||||
"outbox": "https://mastodon.social/users/Gargron/outbox",
|
||||
"featured": "https://mastodon.social/users/Gargron/collections/featured",
|
||||
"featuredTags": "https://mastodon.social/users/Gargron/collections/tags",
|
||||
"preferredUsername": "Gargron",
|
||||
"name": "Eugen Rochko",
|
||||
"summary": "\u003cp\u003eFounder, CEO and lead developer \u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.social/@Mastodon\" class=\"u-url mention\"\u003e@\u003cspan\u003eMastodon\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e, Germany.\u003c/p\u003e",
|
||||
"url": "https://mastodon.social/@Gargron",
|
||||
"manuallyApprovesFollowers": false,
|
||||
"discoverable": true,
|
||||
"published": "2016-03-16T00:00:00Z",
|
||||
"devices": "https://mastodon.social/users/Gargron/collections/devices",
|
||||
"alsoKnownAs": [
|
||||
"https://tooting.ai/users/Gargron"
|
||||
],
|
||||
"publicKey": {
|
||||
"id": "https://mastodon.social/users/Gargron#main-key",
|
||||
"owner": "https://mastodon.social/users/Gargron",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXc4vkECU2/CeuSo1wtn\nFoim94Ne1jBMYxTZ9wm2YTdJq1oiZKif06I2fOqDzY/4q/S9uccrE9Bkajv1dnkO\nVm31QjWlhVpSKynVxEWjVBO5Ienue8gND0xvHIuXf87o61poqjEoepvsQFElA5ym\novljWGSA/jpj7ozygUZhCXtaS2W5AD5tnBQUpcO0lhItYPYTjnmzcc4y2NbJV8hz\n2s2G8qKv8fyimE23gY1XrPJg+cRF+g4PqFXujjlJ7MihD9oqtLGxbu7o1cifTn3x\nBfIdPythWu5b4cujNsB3m3awJjVmx+MHQ9SugkSIYXV0Ina77cTNS0M2PYiH1PFR\nTwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"tag": [],
|
||||
"attachment": [
|
||||
{
|
||||
"type": "PropertyValue",
|
||||
"name": "Patreon",
|
||||
"value": "\u003ca href=\"https://www.patreon.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003epatreon.com/mastodon\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
|
||||
},
|
||||
{
|
||||
"type": "PropertyValue",
|
||||
"name": "GitHub",
|
||||
"value": "\u003ca href=\"https://github.com/Gargron\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egithub.com/Gargron\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
|
||||
}
|
||||
],
|
||||
"endpoints": {
|
||||
"sharedInbox": "https://mastodon.social/inbox"
|
||||
},
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/jpeg",
|
||||
"url": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg"
|
||||
},
|
||||
"image": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/jpeg",
|
||||
"url": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg"
|
||||
}
|
||||
}
|
||||
|
||||
""".trimIndent()
|
||||
|
||||
|
||||
val objectMapper = ActivityPubConfig().objectMapper()
|
||||
|
||||
val readValue = objectMapper.readValue<Person>(personString)
|
||||
}
|
||||
}
|
|
@ -55,8 +55,7 @@ class UserAPControllerImplTest {
|
|||
publicKey = Key(
|
||||
id = "https://example.com/users/hoge#pubkey",
|
||||
owner = "https://example.com/users/hoge",
|
||||
publicKeyPem = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
|
||||
type = emptyList()
|
||||
publicKeyPem = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----"
|
||||
),
|
||||
endpoints = mapOf("sharedInbox" to "https://example.com/inbox"),
|
||||
followers = "https://example.com/users/hoge/followers",
|
||||
|
|
|
@ -126,7 +126,6 @@ class APNoteServiceImplTest {
|
|||
url = user.url + "/icon.png"
|
||||
),
|
||||
publicKey = Key(
|
||||
type = emptyList(),
|
||||
id = user.keyId,
|
||||
owner = user.url,
|
||||
publicKeyPem = user.publicKey
|
||||
|
@ -245,7 +244,6 @@ class APNoteServiceImplTest {
|
|||
url = user.url + "/icon.png"
|
||||
),
|
||||
publicKey = Key(
|
||||
type = emptyList(),
|
||||
id = user.keyId,
|
||||
owner = user.url,
|
||||
publicKeyPem = user.publicKey
|
||||
|
|
Loading…
Reference in New Issue