mirror of https://github.com/usbharu/Hideout.git
commit
4943b8e94e
|
@ -51,7 +51,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
build
|
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
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
|
@ -116,6 +116,7 @@ jobs:
|
||||||
arguments: test
|
arguments: test
|
||||||
|
|
||||||
- name: Save Test Report
|
- name: Save Test Report
|
||||||
|
if: always()
|
||||||
uses: actions/cache/save@v3
|
uses: actions/cache/save@v3
|
||||||
with:
|
with:
|
||||||
path: build/test-results
|
path: build/test-results
|
||||||
|
@ -179,6 +180,7 @@ jobs:
|
||||||
arguments: integrationTest
|
arguments: integrationTest
|
||||||
|
|
||||||
- name: Save Test Report
|
- name: Save Test Report
|
||||||
|
if: always()
|
||||||
uses: actions/cache/save@v3
|
uses: actions/cache/save@v3
|
||||||
with:
|
with:
|
||||||
path: build/test-results
|
path: build/test-results
|
||||||
|
@ -234,7 +236,7 @@ jobs:
|
||||||
- name: Run Kover
|
- name: Run Kover
|
||||||
uses: gradle/gradle-build-action@v2.8.1
|
uses: gradle/gradle-build-action@v2.8.1
|
||||||
with:
|
with:
|
||||||
arguments: koverXmlReport -x integrationTest
|
arguments: koverXmlReport -x integrationTest -x e2eTest
|
||||||
|
|
||||||
- name: Add coverage report to PR
|
- name: Add coverage report to PR
|
||||||
if: always()
|
if: always()
|
||||||
|
@ -253,7 +255,7 @@ jobs:
|
||||||
report-tests:
|
report-tests:
|
||||||
name: Report Tests
|
name: Report Tests
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
needs: [ unit-test,integration-test ]
|
needs: [ unit-test,integration-test,e2e-test ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Restore Test Report
|
- name: Restore Test Report
|
||||||
|
@ -268,6 +270,12 @@ jobs:
|
||||||
path: build/test-results
|
path: build/test-results
|
||||||
key: integration-test-report-${{ github.sha }}
|
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
|
- name: JUnit Test Report
|
||||||
uses: mikepenz/action-junit-report@v2
|
uses: mikepenz/action-junit-report@v2
|
||||||
with:
|
with:
|
||||||
|
@ -330,3 +338,75 @@ jobs:
|
||||||
uses: reviewdog/action-suggester@v1
|
uses: reviewdog/action-suggester@v1
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
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/
|
/src/main/web/generated/
|
||||||
/stats.html
|
/stats.html
|
||||||
/tomcat/
|
/tomcat/
|
||||||
|
/tomcat-e2e/
|
||||||
|
/e2eTest.log
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
|
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
|
||||||
|
|
||||||
|
@ -32,6 +31,10 @@ sourceSets {
|
||||||
compileClasspath += sourceSets.main.get().output
|
compileClasspath += sourceSets.main.get().output
|
||||||
runtimeClasspath += 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 {
|
val intTestImplementation by configurations.getting {
|
||||||
|
@ -41,6 +44,14 @@ val intTestRuntimeOnly by configurations.getting {
|
||||||
extendsFrom(configurations.runtimeOnly.get())
|
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") {
|
val integrationTest = task<Test>("integrationTest") {
|
||||||
description = "Runs integration tests."
|
description = "Runs integration tests."
|
||||||
group = "verification"
|
group = "verification"
|
||||||
|
@ -52,13 +63,24 @@ val integrationTest = task<Test>("integrationTest") {
|
||||||
useJUnitPlatform()
|
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> {
|
tasks.withType<Test> {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
val cpus = Runtime.getRuntime().availableProcessors()
|
|
||||||
// maxParallelForks = max(1, cpus - 1)
|
|
||||||
// setForkEvery(4)
|
|
||||||
doFirst {
|
doFirst {
|
||||||
jvmArgs = arrayOf(
|
jvmArgs = arrayOf(
|
||||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED"
|
"--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.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
|
||||||
intTestImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
|
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 {
|
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
|
mockMvc
|
||||||
.post("/api/v1/accounts") {
|
.post("/api/v1/accounts") {
|
||||||
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
||||||
param("username", "api-test-user-3")
|
param("username", "api-test-user-4")
|
||||||
with(SecurityMockMvcRequestPostProcessors.csrf())
|
with(SecurityMockMvcRequestPostProcessors.csrf())
|
||||||
}
|
}
|
||||||
.andExpect { status { isBadRequest() } }
|
.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 {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@AfterAll
|
@AfterAll
|
||||||
|
|
|
@ -3,12 +3,11 @@ package dev.usbharu.hideout.activitypub.domain.model
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
open class Key(
|
open class Key(
|
||||||
type: List<String>,
|
|
||||||
override val id: String,
|
override val id: String,
|
||||||
val owner: String,
|
val owner: String,
|
||||||
val publicKeyPem: String
|
val publicKeyPem: String
|
||||||
) : Object(
|
) : Object(
|
||||||
type = add(list = type, type = "Key")
|
type = add(list = emptyList(), type = "Key")
|
||||||
),
|
),
|
||||||
HasId {
|
HasId {
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ constructor(
|
||||||
var outbox: String,
|
var outbox: String,
|
||||||
var url: String,
|
var url: String,
|
||||||
private var icon: Image?,
|
private var icon: Image?,
|
||||||
var publicKey: Key?,
|
var publicKey: Key,
|
||||||
var endpoints: Map<String, String> = emptyMap(),
|
var endpoints: Map<String, String> = emptyMap(),
|
||||||
var followers: String?,
|
var followers: String?,
|
||||||
var following: String?
|
var following: String?
|
||||||
|
@ -22,9 +22,13 @@ constructor(
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is Person) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
if (!super.equals(other)) 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 (preferredUsername != other.preferredUsername) return false
|
||||||
if (summary != other.summary) return false
|
if (summary != other.summary) return false
|
||||||
if (inbox != other.inbox) return false
|
if (inbox != other.inbox) return false
|
||||||
|
@ -33,20 +37,26 @@ constructor(
|
||||||
if (icon != other.icon) return false
|
if (icon != other.icon) return false
|
||||||
if (publicKey != other.publicKey) return false
|
if (publicKey != other.publicKey) return false
|
||||||
if (endpoints != other.endpoints) return false
|
if (endpoints != other.endpoints) return false
|
||||||
|
if (followers != other.followers) return false
|
||||||
|
if (following != other.following) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + name.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
result = 31 * result + (preferredUsername?.hashCode() ?: 0)
|
result = 31 * result + (preferredUsername?.hashCode() ?: 0)
|
||||||
result = 31 * result + (summary?.hashCode() ?: 0)
|
result = 31 * result + (summary?.hashCode() ?: 0)
|
||||||
result = 31 * result + inbox.hashCode()
|
result = 31 * result + inbox.hashCode()
|
||||||
result = 31 * result + outbox.hashCode()
|
result = 31 * result + outbox.hashCode()
|
||||||
result = 31 * result + url.hashCode()
|
result = 31 * result + url.hashCode()
|
||||||
result = 31 * result + (icon?.hashCode() ?: 0)
|
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 + endpoints.hashCode()
|
||||||
|
result = 31 * result + (followers?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (following?.hashCode() ?: 0)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ interface InboxController {
|
||||||
"application/activity+json",
|
"application/activity+json",
|
||||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||||
],
|
],
|
||||||
|
consumes = ["application/json", "application/*+json"],
|
||||||
method = [RequestMethod.POST]
|
method = [RequestMethod.POST]
|
||||||
)
|
)
|
||||||
suspend fun inbox(@RequestBody string: String): ResponseEntity<Unit>
|
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 request = (requireNotNull(RequestContextHolder.getRequestAttributes()) as ServletRequestAttributes).request
|
||||||
|
|
||||||
val headersList = request.headerNames?.toList().orEmpty()
|
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)
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
.header(
|
.header(
|
||||||
WWW_AUTHENTICATE,
|
WWW_AUTHENTICATE,
|
||||||
|
|
|
@ -34,25 +34,38 @@ class InboxJobProcessor(
|
||||||
private val transaction: Transaction
|
private val transaction: Transaction
|
||||||
) : JobProcessor<InboxJobParam, InboxJob> {
|
) : 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) {
|
val requiredHeaders = when (httpRequest.method) {
|
||||||
HttpMethod.GET -> getRequiredHeaders
|
HttpMethod.GET -> getRequiredHeaders
|
||||||
HttpMethod.POST -> postRequiredHeaders
|
HttpMethod.POST -> postRequiredHeaders
|
||||||
}
|
}
|
||||||
if (signature.headers.containsAll(requiredHeaders).not()) {
|
if (signature.headers.containsAll(requiredHeaders).not()) {
|
||||||
|
logger.warn("FAILED Invalid signature. require: {}", requiredHeaders)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val user = try {
|
val user = transaction.transaction {
|
||||||
userQueryService.findByKeyId(signature.keyId)
|
try {
|
||||||
} catch (_: FailedToGetResourcesException) {
|
userQueryService.findByKeyId(signature.keyId)
|
||||||
apUserService.fetchPersonWithEntity(signature.keyId).second
|
} catch (_: FailedToGetResourcesException) {
|
||||||
|
apUserService.fetchPersonWithEntity(signature.keyId).second
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val verify = signatureVerifier.verify(
|
@Suppress("TooGenericExceptionCaught")
|
||||||
httpRequest,
|
val verify = try {
|
||||||
PublicKey(RsaUtil.decodeRsaPublicKeyPem(user.publicKey), signature.keyId)
|
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
|
return verify.success
|
||||||
}
|
}
|
||||||
|
@ -60,6 +73,7 @@ class InboxJobProcessor(
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
private fun parseSignatureHeader(httpHeaders: HttpHeaders): Signature? {
|
private fun parseSignatureHeader(httpHeaders: HttpHeaders): Signature? {
|
||||||
return try {
|
return try {
|
||||||
|
println("Signature Header =" + httpHeaders.get("Signature").single())
|
||||||
signatureHeaderParser.parse(httpHeaders)
|
signatureHeaderParser.parse(httpHeaders)
|
||||||
} catch (e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
logger.trace("FAILED parse signature header", e)
|
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)
|
val jsonNode = objectMapper.readTree(param.json)
|
||||||
|
|
||||||
logger.info("START Process inbox. type: {}", param.type)
|
logger.info("START Process inbox. type: {}", param.type)
|
||||||
|
@ -83,22 +97,24 @@ class InboxJobProcessor(
|
||||||
|
|
||||||
logger.debug("Has signature? {}", signature != null)
|
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 =
|
val activityPubProcessor =
|
||||||
activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as ActivityPubProcessor<Object>?
|
activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as ActivityPubProcessor<Object>?
|
||||||
|
|
||||||
if (activityPubProcessor == null) {
|
if (activityPubProcessor == null) {
|
||||||
logger.warn("ActivityType {} is not support.", param.type)
|
logger.warn("ActivityType {} is not support.", param.type)
|
||||||
throw IllegalStateException("ActivityPubProcessor not found.")
|
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
|
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.RemoteUserCreateDto
|
||||||
import dev.usbharu.hideout.core.service.user.UserService
|
import dev.usbharu.hideout.core.service.user.UserService
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
interface APUserService {
|
interface APUserService {
|
||||||
suspend fun getPersonByName(name: String): Person
|
suspend fun getPersonByName(name: String): Person
|
||||||
|
@ -61,7 +62,6 @@ class APUserServiceImpl(
|
||||||
url = "$userUrl/icon.png"
|
url = "$userUrl/icon.png"
|
||||||
),
|
),
|
||||||
publicKey = Key(
|
publicKey = Key(
|
||||||
type = emptyList(),
|
|
||||||
id = userEntity.keyId,
|
id = userEntity.keyId,
|
||||||
owner = userUrl,
|
owner = userUrl,
|
||||||
publicKeyPem = userEntity.publicKey
|
publicKeyPem = userEntity.publicKey
|
||||||
|
@ -75,6 +75,7 @@ class APUserServiceImpl(
|
||||||
override suspend fun fetchPerson(url: String, targetActor: String?): Person =
|
override suspend fun fetchPerson(url: String, targetActor: String?): Person =
|
||||||
fetchPersonWithEntity(url, targetActor).first
|
fetchPersonWithEntity(url, targetActor).first
|
||||||
|
|
||||||
|
@Transactional
|
||||||
override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair<Person, User> {
|
override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair<Person, User> {
|
||||||
return try {
|
return try {
|
||||||
val userEntity = userQueryService.findByUrl(url)
|
val userEntity = userQueryService.findByUrl(url)
|
||||||
|
@ -94,15 +95,13 @@ class APUserServiceImpl(
|
||||||
name = person.preferredUsername
|
name = person.preferredUsername
|
||||||
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
|
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
|
||||||
domain = id.substringAfter("://").substringBefore("/"),
|
domain = id.substringAfter("://").substringBefore("/"),
|
||||||
screenName = person.name
|
screenName = person.name,
|
||||||
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
|
|
||||||
description = person.summary.orEmpty(),
|
description = person.summary.orEmpty(),
|
||||||
inbox = person.inbox,
|
inbox = person.inbox,
|
||||||
outbox = person.outbox,
|
outbox = person.outbox,
|
||||||
url = id,
|
url = id,
|
||||||
publicKey = person.publicKey?.publicKeyPem
|
publicKey = person.publicKey.publicKeyPem,
|
||||||
?: throw IllegalActivityPubObjectException("publicKey is null"),
|
keyId = person.publicKey.id,
|
||||||
keyId = person.publicKey?.id ?: throw IllegalActivityPubObjectException("publicKey keyId is null"),
|
|
||||||
following = person.following,
|
following = person.following,
|
||||||
followers = person.followers,
|
followers = person.followers,
|
||||||
sharedInbox = person.endpoints["sharedInbox"]
|
sharedInbox = person.endpoints["sharedInbox"]
|
||||||
|
@ -129,7 +128,6 @@ class APUserServiceImpl(
|
||||||
url = "$id/icon.png"
|
url = "$id/icon.png"
|
||||||
),
|
),
|
||||||
publicKey = Key(
|
publicKey = Key(
|
||||||
type = emptyList(),
|
|
||||||
id = userEntity.keyId,
|
id = userEntity.keyId,
|
||||||
owner = id,
|
owner = id,
|
||||||
publicKeyPem = userEntity.publicKey
|
publicKeyPem = userEntity.publicKey
|
||||||
|
|
|
@ -7,6 +7,8 @@ import com.fasterxml.jackson.core.JsonParser
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
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.HttpSignatureSigner
|
||||||
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
|
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
@ -24,12 +26,13 @@ class ActivityPubConfig {
|
||||||
val objectMapper = jacksonObjectMapper()
|
val objectMapper = jacksonObjectMapper()
|
||||||
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||||
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||||
.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY))
|
.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP))
|
||||||
.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY))
|
.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SKIP))
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
|
.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
|
||||||
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
|
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
|
||||||
.configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true)
|
.configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true)
|
||||||
|
.addMixIn(HttpRequest::class.java, HttpRequestMixIn::class.java)
|
||||||
return objectMapper
|
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.HttpSignatureUserDetailsService
|
||||||
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureVerifierComposite
|
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.UserDetailsImpl
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsServiceImpl
|
||||||
import dev.usbharu.hideout.core.query.UserQueryService
|
import dev.usbharu.hideout.core.query.UserQueryService
|
||||||
import dev.usbharu.hideout.util.RsaUtil
|
import dev.usbharu.hideout.util.RsaUtil
|
||||||
import dev.usbharu.hideout.util.hasAnyScope
|
import dev.usbharu.hideout.util.hasAnyScope
|
||||||
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
|
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
|
||||||
import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser
|
import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser
|
||||||
import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier
|
import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier
|
||||||
|
import jakarta.annotation.PostConstruct
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer
|
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.http.converter.json.MappingJackson2HttpMessageConverter
|
||||||
import org.springframework.security.authentication.AccountStatusUserDetailsChecker
|
import org.springframework.security.authentication.AccountStatusUserDetailsChecker
|
||||||
import org.springframework.security.authentication.AuthenticationManager
|
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.authentication.configuration.AuthenticationConfiguration
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
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.security.interfaces.RSAPublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@EnableWebSecurity(debug = true)
|
@EnableWebSecurity(debug = false)
|
||||||
@Configuration
|
@Configuration
|
||||||
@Suppress("FunctionMaxLength", "TooManyFunctions")
|
@Suppress("FunctionMaxLength", "TooManyFunctions")
|
||||||
class SecurityConfig {
|
class SecurityConfig {
|
||||||
|
@ -80,7 +84,9 @@ class SecurityConfig {
|
||||||
http {
|
http {
|
||||||
securityMatcher("/users/*/posts/*")
|
securityMatcher("/users/*/posts/*")
|
||||||
addFilterAt<RequestCacheAwareFilter>(httpSignatureFilter)
|
addFilterAt<RequestCacheAwareFilter>(httpSignatureFilter)
|
||||||
addFilterBefore<HttpSignatureFilter>(ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
|
addFilterBefore<HttpSignatureFilter>(
|
||||||
|
ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
|
||||||
|
)
|
||||||
authorizeHttpRequests {
|
authorizeHttpRequests {
|
||||||
authorize(anyRequest, permitAll)
|
authorize(anyRequest, permitAll)
|
||||||
}
|
}
|
||||||
|
@ -114,6 +120,16 @@ class SecurityConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@Order(2)
|
||||||
|
fun daoAuthenticationProvider(userDetailsServiceImpl: UserDetailsServiceImpl): DaoAuthenticationProvider {
|
||||||
|
val daoAuthenticationProvider = DaoAuthenticationProvider()
|
||||||
|
daoAuthenticationProvider.setUserDetailsService(userDetailsServiceImpl)
|
||||||
|
|
||||||
|
return daoAuthenticationProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(1)
|
||||||
fun httpSignatureAuthenticationProvider(transaction: Transaction): PreAuthenticatedAuthenticationProvider {
|
fun httpSignatureAuthenticationProvider(transaction: Transaction): PreAuthenticatedAuthenticationProvider {
|
||||||
val provider = PreAuthenticatedAuthenticationProvider()
|
val provider = PreAuthenticatedAuthenticationProvider()
|
||||||
val signatureHeaderParser = DefaultSignatureHeaderParser()
|
val signatureHeaderParser = DefaultSignatureHeaderParser()
|
||||||
|
@ -146,7 +162,6 @@ class SecurityConfig {
|
||||||
}
|
}
|
||||||
oauth2ResourceServer {
|
oauth2ResourceServer {
|
||||||
jwt {
|
jwt {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,7 +173,6 @@ class SecurityConfig {
|
||||||
fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
http {
|
http {
|
||||||
authorizeHttpRequests {
|
authorizeHttpRequests {
|
||||||
|
|
||||||
authorize("/error", permitAll)
|
authorize("/error", permitAll)
|
||||||
authorize("/login", permitAll)
|
authorize("/login", permitAll)
|
||||||
authorize(GET, "/.well-known/**", permitAll)
|
authorize(GET, "/.well-known/**", permitAll)
|
||||||
|
@ -186,11 +200,10 @@ class SecurityConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
formLogin {
|
formLogin {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
csrf {
|
csrf {
|
||||||
ignoringRequestMatchers("/users/*/inbox", "/inbox", "/api/v1/apps", "/api/v1/accounts")
|
ignoringRequestMatchers("/users/*/inbox", "/inbox", "/api/v1/apps")
|
||||||
}
|
}
|
||||||
|
|
||||||
headers {
|
headers {
|
||||||
|
@ -269,3 +282,17 @@ data class JwkConfig(
|
||||||
val publicKey: String,
|
val publicKey: String,
|
||||||
val privateKey: 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) {
|
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)
|
val convert: ScheduleContext<J>.(J) -> Unit = job.convert(jobProps)
|
||||||
kjob.schedule(job, convert)
|
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 com.mongodb.reactivestreams.client.MongoClient
|
||||||
import dev.usbharu.hideout.core.external.job.HideoutJob
|
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 dev.usbharu.hideout.core.service.job.JobQueueWorkerService
|
||||||
import kjob.core.dsl.JobContextWithProps
|
import kjob.core.dsl.JobContextWithProps
|
||||||
import kjob.core.dsl.JobRegisterContext
|
import kjob.core.dsl.JobRegisterContext
|
||||||
|
@ -13,7 +14,10 @@ import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ConditionalOnProperty(name = ["hideout.use-mongodb"], havingValue = "true", matchIfMissing = false)
|
@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 {
|
val kjob by lazy {
|
||||||
kjob(Mongo) {
|
kjob(Mongo) {
|
||||||
client = mongoClient
|
client = mongoClient
|
||||||
|
@ -30,6 +34,14 @@ class KJobMongoJobQueueWorkerService(private val mongoClient: MongoClient) : Job
|
||||||
defines.forEach { job ->
|
defines.forEach { job ->
|
||||||
kjob.register(job.first, job.second)
|
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() {
|
override fun close() {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import kjob.core.Job
|
||||||
import kjob.core.dsl.ScheduleContext
|
import kjob.core.dsl.ScheduleContext
|
||||||
import kjob.core.kjob
|
import kjob.core.kjob
|
||||||
import kjob.mongo.Mongo
|
import kjob.mongo.Mongo
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@ -26,15 +27,23 @@ class KjobMongoJobQueueParentService(private val mongoClient: MongoClient) : Job
|
||||||
|
|
||||||
@Deprecated("use type safe → scheduleTypeSafe")
|
@Deprecated("use type safe → scheduleTypeSafe")
|
||||||
override suspend fun <J : Job> schedule(job: J, block: ScheduleContext<J>.(J) -> Unit) {
|
override suspend fun <J : Job> schedule(job: J, block: ScheduleContext<J>.(J) -> Unit) {
|
||||||
|
logger.debug("SCHEDULE Job: {}", job.name)
|
||||||
kjob.schedule(job, block)
|
kjob.schedule(job, block)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun <T, J : HideoutJob<T, J>> scheduleTypeSafe(job: J, jobProps: T) {
|
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)
|
val convert = job.convert(jobProps)
|
||||||
kjob.schedule(job, convert)
|
kjob.schedule(job, convert)
|
||||||
|
logger.debug("SUCCESS Job: {}", job.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
kjob.shutdown()
|
kjob.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(KjobMongoJobQueueParentService::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,13 +27,10 @@ class HttpSignatureUserDetailsService(
|
||||||
) :
|
) :
|
||||||
AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
|
AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
|
||||||
override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking {
|
override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking {
|
||||||
if (token.principal !is String) {
|
check(token.principal is String) { "Token is not String" }
|
||||||
throw IllegalStateException("Token is not String")
|
|
||||||
}
|
|
||||||
val credentials = token.credentials
|
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 keyId = token.principal as String
|
||||||
val findByKeyId = transaction.transaction {
|
val findByKeyId = transaction.transaction {
|
||||||
|
|
|
@ -81,7 +81,7 @@ class InstanceServiceImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
TODO()
|
throw IllegalStateException("Unknown nodeinfo versions: $key url: $value")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,13 @@ class InMemoryCacheManager : CacheManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (needRunBlock) {
|
if (needRunBlock) {
|
||||||
val processed = block()
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
val processed = try {
|
||||||
|
block()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
cacheKey.remove(key)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
if (cacheKey.containsKey(key)) {
|
if (cacheKey.containsKey(key)) {
|
||||||
valueStore[key] = processed
|
valueStore[key] = processed
|
||||||
|
|
|
@ -12,6 +12,7 @@ import dev.usbharu.hideout.core.service.instance.InstanceService
|
||||||
import org.jetbrains.exposed.exceptions.ExposedSQLException
|
import org.jetbrains.exposed.exceptions.ExposedSQLException
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -57,7 +58,9 @@ class UserServiceImpl(
|
||||||
return userRepository.save(userEntity)
|
return userRepository.save(userEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
override suspend fun createRemoteUser(user: RemoteUserCreateDto): User {
|
override suspend fun createRemoteUser(user: RemoteUserCreateDto): User {
|
||||||
|
logger.info("START Create New remote user. name: {} url: {}", user.name, user.url)
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
val instance = try {
|
val instance = try {
|
||||||
instanceService.fetchInstance(user.url, user.sharedInbox)
|
instanceService.fetchInstance(user.url, user.sharedInbox)
|
||||||
|
@ -84,8 +87,11 @@ class UserServiceImpl(
|
||||||
instance = instance?.id
|
instance = instance?.id
|
||||||
)
|
)
|
||||||
return try {
|
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) {
|
} catch (_: ExposedSQLException) {
|
||||||
|
logger.warn("FAILED User already exists. name: {} url: {}", user.name, user.url)
|
||||||
userQueryService.findByUrl(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> =
|
fun AuthorizeHttpRequestsDsl.hasScope(scope: String): AuthorizationManager<RequestAuthorizationContext> =
|
||||||
hasAuthority("SCOPE_$scope")
|
hasAuthority("SCOPE_$scope")
|
||||||
|
|
||||||
|
@Suppress("SpreadOperator")
|
||||||
fun AuthorizeHttpRequestsDsl.hasAnyScope(vararg scopes: String): AuthorizationManager<RequestAuthorizationContext> =
|
fun AuthorizeHttpRequestsDsl.hasAnyScope(vararg scopes: String): AuthorizationManager<RequestAuthorizationContext> =
|
||||||
hasAnyAuthority(
|
hasAnyAuthority(*scopes.map { "SCOPE_$it" }.toTypedArray())
|
||||||
*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(
|
publicKey = Key(
|
||||||
id = "https://example.com/users/hoge#pubkey",
|
id = "https://example.com/users/hoge#pubkey",
|
||||||
owner = "https://example.com/users/hoge",
|
owner = "https://example.com/users/hoge",
|
||||||
publicKeyPem = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
|
publicKeyPem = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----"
|
||||||
type = emptyList()
|
|
||||||
),
|
),
|
||||||
endpoints = mapOf("sharedInbox" to "https://example.com/inbox"),
|
endpoints = mapOf("sharedInbox" to "https://example.com/inbox"),
|
||||||
followers = "https://example.com/users/hoge/followers",
|
followers = "https://example.com/users/hoge/followers",
|
||||||
|
|
|
@ -126,7 +126,6 @@ class APNoteServiceImplTest {
|
||||||
url = user.url + "/icon.png"
|
url = user.url + "/icon.png"
|
||||||
),
|
),
|
||||||
publicKey = Key(
|
publicKey = Key(
|
||||||
type = emptyList(),
|
|
||||||
id = user.keyId,
|
id = user.keyId,
|
||||||
owner = user.url,
|
owner = user.url,
|
||||||
publicKeyPem = user.publicKey
|
publicKeyPem = user.publicKey
|
||||||
|
@ -245,7 +244,6 @@ class APNoteServiceImplTest {
|
||||||
url = user.url + "/icon.png"
|
url = user.url + "/icon.png"
|
||||||
),
|
),
|
||||||
publicKey = Key(
|
publicKey = Key(
|
||||||
type = emptyList(),
|
|
||||||
id = user.keyId,
|
id = user.keyId,
|
||||||
owner = user.url,
|
owner = user.url,
|
||||||
publicKeyPem = user.publicKey
|
publicKeyPem = user.publicKey
|
||||||
|
|
Loading…
Reference in New Issue