diff --git a/.github/workflows/pull-request-merge-check.yml b/.github/workflows/pull-request-merge-check.yml index 72f62868..7d1a26f2 100644 --- a/.github/workflows/pull-request-merge-check.yml +++ b/.github/workflows/pull-request-merge-check.yml @@ -51,7 +51,7 @@ jobs: with: path: | build - key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('src') }}-${{ github.sha }} + key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('**/*.kt') }}-${{ github.sha }} - name: Set up JDK 17 uses: actions/setup-java@v3 @@ -116,6 +116,7 @@ jobs: arguments: test - name: Save Test Report + if: always() uses: actions/cache/save@v3 with: path: build/test-results @@ -179,6 +180,7 @@ jobs: arguments: integrationTest - name: Save Test Report + if: always() uses: actions/cache/save@v3 with: path: build/test-results @@ -234,7 +236,7 @@ jobs: - name: Run Kover uses: gradle/gradle-build-action@v2.8.1 with: - arguments: koverXmlReport -x integrationTest + arguments: koverXmlReport -x integrationTest -x e2eTest - name: Add coverage report to PR if: always() @@ -253,7 +255,7 @@ jobs: report-tests: name: Report Tests if: success() || failure() - needs: [ unit-test,integration-test ] + needs: [ unit-test,integration-test,e2e-test ] runs-on: ubuntu-latest steps: - name: Restore Test Report @@ -268,6 +270,12 @@ jobs: path: build/test-results key: integration-test-report-${{ github.sha }} + - name: Restore Test Report + uses: actions/cache/restore@v3 + with: + path: build/test-results + key: e2e-test-report-${{ github.sha }} + - name: JUnit Test Report uses: mikepenz/action-junit-report@v2 with: @@ -330,3 +338,75 @@ jobs: uses: reviewdog/action-suggester@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} + + e2e-test: + name: E2E Test + needs: [ setup ] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Gradle Wrapper Cache + uses: actions/cache@v3.3.2 + with: + path: ~/.gradle/wrapper + key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }} + + - name: Dependencies Cache + uses: actions/cache@v3.3.2 + with: + path: | + ~/.gradle/cache/jars-* + ~/.gradle/caches/transforms-* + ~/.gradle/caches/modules-* + key: gradle-dependencies-${{ hashFiles('**/*.gradle.kts') }} + restore-keys: gradle-dependencies- + + - name: Cache + uses: actions/cache@v3.3.2 + with: + path: | + ~/.gradle/caches/build-cache-* + ~/.gradle/caches/[0-9]*.* + .gradle + key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ github.sha }} + restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}- + + - name: Build Cache + uses: actions/cache@v3.3.2 + with: + path: | + build + key: gradle-build-${{ hashFiles('**/*.gradle.kts') }}-${{ hashFiles('src') }}-${{ github.sha }} + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: MongoDB in GitHub Actions + uses: supercharge/mongodb-github-action@1.10.0 + with: + mongodb-version: latest + + - name: setup-chrome + id: setup-chrome + uses: browser-actions/setup-chrome@v1.4.0 + + - name: Add Path + run: echo ${{ steps.setup-chrome.outputs.chrome-path }} >> $GITHUB_PATH + + - name: E2E Test + uses: gradle/gradle-build-action@v2.8.1 + with: + arguments: e2eTest + + + - name: Save Test Report + if: always() + uses: actions/cache/save@v3 + with: + path: build/test-results + key: e2e-test-report-${{ github.sha }} diff --git a/.gitignore b/.gitignore index 5f98beae..0d7dbff3 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ out/ /src/main/web/generated/ /stats.html /tomcat/ +/tomcat-e2e/ +/e2eTest.log diff --git a/build.gradle.kts b/build.gradle.kts index 7a5f4242..c8d31ca7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,3 @@ - import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.openapitools.generator.gradle.plugin.tasks.GenerateTask @@ -32,6 +31,10 @@ sourceSets { compileClasspath += sourceSets.main.get().output runtimeClasspath += sourceSets.main.get().output } + create("e2eTest") { + compileClasspath += sourceSets.main.get().output + runtimeClasspath += sourceSets.main.get().output + } } val intTestImplementation by configurations.getting { @@ -41,6 +44,14 @@ val intTestRuntimeOnly by configurations.getting { extendsFrom(configurations.runtimeOnly.get()) } +val e2eTestImplementation by configurations.getting { + extendsFrom(configurations.implementation.get()) +} + +val e2eTestRuntimeOnly by configurations.getting { + extendsFrom(configurations.runtimeOnly.get()) +} + val integrationTest = task("integrationTest") { description = "Runs integration tests." group = "verification" @@ -52,13 +63,24 @@ val integrationTest = task("integrationTest") { useJUnitPlatform() } -tasks.check { dependsOn(integrationTest) } +val e2eTest = task("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 { useJUnitPlatform() - val cpus = Runtime.getRuntime().availableProcessors() -// maxParallelForks = max(1, cpus - 1) -// setForkEvery(4) doFirst { jvmArgs = arrayOf( "--add-opens", "java.base/java.lang=ALL-UNNAMED" @@ -207,6 +229,15 @@ dependencies { intTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") intTestImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") + e2eTestImplementation("org.springframework.boot:spring-boot-starter-test") + e2eTestImplementation("org.springframework.security:spring-security-test") + e2eTestImplementation("org.springframework.boot:spring-boot-starter-webflux") + e2eTestImplementation("org.jsoup:jsoup:1.17.1") + e2eTestImplementation("com.intuit.karate:karate-junit5:1.4.1") + + + + } detekt { diff --git a/src/e2eTest/kotlin/AssertionUtil.kt b/src/e2eTest/kotlin/AssertionUtil.kt new file mode 100644 index 00000000..8083b825 --- /dev/null +++ b/src/e2eTest/kotlin/AssertionUtil.kt @@ -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() + } +} diff --git a/src/e2eTest/kotlin/KarateUtil.kt b/src/e2eTest/kotlin/KarateUtil.kt new file mode 100644 index 00000000..c71cd44b --- /dev/null +++ b/src/e2eTest/kotlin/KarateUtil.kt @@ -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, 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") + } +} diff --git a/src/e2eTest/kotlin/federation/FollowAcceptTest.kt b/src/e2eTest/kotlin/federation/FollowAcceptTest.kt new file mode 100644 index 00000000..3c7cd02d --- /dev/null +++ b/src/e2eTest/kotlin/federation/FollowAcceptTest.kt @@ -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() + } + } +} diff --git a/src/e2eTest/kotlin/federation/InboxCommonTest.kt b/src/e2eTest/kotlin/federation/InboxCommonTest.kt new file mode 100644 index 00000000..33d595af --- /dev/null +++ b/src/e2eTest/kotlin/federation/InboxCommonTest.kt @@ -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() + } + } +} diff --git a/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt new file mode 100644 index 00000000..c13bd810 --- /dev/null +++ b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt @@ -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() + } + } +} diff --git a/src/e2eTest/resources/application.yml b/src/e2eTest/resources/application.yml new file mode 100644 index 00000000..1013de15 --- /dev/null +++ b/src/e2eTest/resources/application.yml @@ -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 diff --git a/src/e2eTest/resources/federation/FollowAcceptMockServer.feature b/src/e2eTest/resources/federation/FollowAcceptMockServer.feature new file mode 100644 index 00000000..60793fde --- /dev/null +++ b/src/e2eTest/resources/federation/FollowAcceptMockServer.feature @@ -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 diff --git a/src/e2eTest/resources/federation/FollowAcceptTest.feature b/src/e2eTest/resources/federation/FollowAcceptTest.feature new file mode 100644 index 00000000..7cdb39e5 --- /dev/null +++ b/src/e2eTest/resources/federation/FollowAcceptTest.feature @@ -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 diff --git a/src/e2eTest/resources/federation/InboxCommonTest.feature b/src/e2eTest/resources/federation/InboxCommonTest.feature new file mode 100644 index 00000000..848e5630 --- /dev/null +++ b/src/e2eTest/resources/federation/InboxCommonTest.feature @@ -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 = + """ + + + +""" + + 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 diff --git a/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature new file mode 100644 index 00000000..6d114c04 --- /dev/null +++ b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature @@ -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 diff --git a/src/e2eTest/resources/karate-config.js b/src/e2eTest/resources/karate-config.js new file mode 100644 index 00000000..a83c2bb4 --- /dev/null +++ b/src/e2eTest/resources/karate-config.js @@ -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; +} diff --git a/src/e2eTest/resources/logback.xml b/src/e2eTest/resources/logback.xml new file mode 100644 index 00000000..c21752ee --- /dev/null +++ b/src/e2eTest/resources/logback.xml @@ -0,0 +1,21 @@ + + + ./e2eTest.log + + UTF-8 + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n + + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n + + + + + + + + + + diff --git a/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature b/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature new file mode 100644 index 00000000..af203344 --- /dev/null +++ b/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature @@ -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 diff --git a/src/e2eTest/resources/oauth2/user.sql b/src/e2eTest/resources/oauth2/user.sql new file mode 100644 index 00000000..15aa977f --- /dev/null +++ b/src/e2eTest/resources/oauth2/user.sql @@ -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); diff --git a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt index b08e2cc5..fb8d66c6 100644 --- a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt +++ b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt @@ -129,12 +129,36 @@ class AccountApiTest { mockMvc .post("/api/v1/accounts") { contentType = MediaType.APPLICATION_FORM_URLENCODED - param("username", "api-test-user-3") + param("username", "api-test-user-4") with(SecurityMockMvcRequestPostProcessors.csrf()) } .andExpect { status { isBadRequest() } } } + @Test + @WithAnonymousUser + fun apiV1AccountsPostでJSONで作ろうとしても400() { + mockMvc + .post("/api/v1/accounts") { + contentType = MediaType.APPLICATION_JSON + content = """{"username":"api-test-user-5","password":"very-very-secure-password"}""" + with(SecurityMockMvcRequestPostProcessors.csrf()) + } + .andExpect { status { isUnsupportedMediaType() } } + } + + @Test + @WithAnonymousUser + fun apiV1AccountsPostにCSRFトークンは必要() { + mockMvc + .post("/api/v1/accounts") { + contentType = MediaType.APPLICATION_FORM_URLENCODED + param("username", "api-test-user-2") + param("password", "very-secure-password") + } + .andExpect { status { isForbidden() } } + } + companion object { @JvmStatic @AfterAll diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Key.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Key.kt index 7e22097c..e821601f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Key.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Key.kt @@ -3,12 +3,11 @@ package dev.usbharu.hideout.activitypub.domain.model import dev.usbharu.hideout.activitypub.domain.model.objects.Object open class Key( - type: List, override val id: String, val owner: String, val publicKeyPem: String ) : Object( - type = add(list = type, type = "Key") + type = add(list = emptyList(), type = "Key") ), HasId { diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Person.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Person.kt index fe1b2d5f..c04ba736 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Person.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Person.kt @@ -14,7 +14,7 @@ constructor( var outbox: String, var url: String, private var icon: Image?, - var publicKey: Key?, + var publicKey: Key, var endpoints: Map = emptyMap(), var followers: String?, var following: String? @@ -22,9 +22,13 @@ constructor( override fun equals(other: Any?): Boolean { if (this === other) return true - if (other !is Person) return false + if (javaClass != other?.javaClass) return false if (!super.equals(other)) return false + other as Person + + if (name != other.name) return false + if (id != other.id) return false if (preferredUsername != other.preferredUsername) return false if (summary != other.summary) return false if (inbox != other.inbox) return false @@ -33,20 +37,26 @@ constructor( if (icon != other.icon) return false if (publicKey != other.publicKey) return false if (endpoints != other.endpoints) return false + if (followers != other.followers) return false + if (following != other.following) return false return true } override fun hashCode(): Int { var result = super.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + id.hashCode() result = 31 * result + (preferredUsername?.hashCode() ?: 0) result = 31 * result + (summary?.hashCode() ?: 0) result = 31 * result + inbox.hashCode() result = 31 * result + outbox.hashCode() result = 31 * result + url.hashCode() result = 31 * result + (icon?.hashCode() ?: 0) - result = 31 * result + (publicKey?.hashCode() ?: 0) + result = 31 * result + publicKey.hashCode() result = 31 * result + endpoints.hashCode() + result = 31 * result + (followers?.hashCode() ?: 0) + result = 31 * result + (following?.hashCode() ?: 0) return result } } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxController.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxController.kt index b2e401cc..7fa3ce18 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxController.kt @@ -15,6 +15,7 @@ interface InboxController { "application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" ], + consumes = ["application/json", "application/*+json"], method = [RequestMethod.POST] ) suspend fun inbox(@RequestBody string: String): ResponseEntity diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxControllerImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxControllerImpl.kt index d87d045a..9fb8d101 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxControllerImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxControllerImpl.kt @@ -23,7 +23,9 @@ class InboxControllerImpl(private val apService: APService) : InboxController { val request = (requireNotNull(RequestContextHolder.getRequestAttributes()) as ServletRequestAttributes).request val headersList = request.headerNames?.toList().orEmpty() - if (headersList.contains("Signature").not()) { + LOGGER.trace("Inbox Headers {}", headersList) + + if (headersList.map { it.lowercase() }.contains("signature").not()) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .header( WWW_AUTHENTICATE, diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt index 1ca31144..65220c41 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt @@ -34,25 +34,38 @@ class InboxJobProcessor( private val transaction: Transaction ) : JobProcessor { - private suspend fun verifyHttpSignature(httpRequest: HttpRequest, signature: Signature): Boolean { + private suspend fun verifyHttpSignature( + httpRequest: HttpRequest, + signature: Signature, + transaction: Transaction + ): Boolean { val requiredHeaders = when (httpRequest.method) { HttpMethod.GET -> getRequiredHeaders HttpMethod.POST -> postRequiredHeaders } if (signature.headers.containsAll(requiredHeaders).not()) { + logger.warn("FAILED Invalid signature. require: {}", requiredHeaders) return false } - val user = try { - userQueryService.findByKeyId(signature.keyId) - } catch (_: FailedToGetResourcesException) { - apUserService.fetchPersonWithEntity(signature.keyId).second + val user = transaction.transaction { + try { + userQueryService.findByKeyId(signature.keyId) + } catch (_: FailedToGetResourcesException) { + apUserService.fetchPersonWithEntity(signature.keyId).second + } } - val verify = signatureVerifier.verify( - httpRequest, - PublicKey(RsaUtil.decodeRsaPublicKeyPem(user.publicKey), signature.keyId) - ) + @Suppress("TooGenericExceptionCaught") + val verify = try { + signatureVerifier.verify( + httpRequest, + PublicKey(RsaUtil.decodeRsaPublicKeyPem(user.publicKey), signature.keyId) + ) + } catch (e: Exception) { + logger.warn("FAILED Verify Http Signature", e) + return false + } return verify.success } @@ -60,6 +73,7 @@ class InboxJobProcessor( @Suppress("TooGenericExceptionCaught") private fun parseSignatureHeader(httpHeaders: HttpHeaders): Signature? { return try { + println("Signature Header =" + httpHeaders.get("Signature").single()) signatureHeaderParser.parse(httpHeaders) } catch (e: RuntimeException) { logger.trace("FAILED parse signature header", e) @@ -67,7 +81,7 @@ class InboxJobProcessor( } } - override suspend fun process(param: InboxJobParam) = transaction.transaction { + override suspend fun process(param: InboxJobParam) { val jsonNode = objectMapper.readTree(param.json) logger.info("START Process inbox. type: {}", param.type) @@ -83,22 +97,24 @@ class InboxJobProcessor( logger.debug("Has signature? {}", signature != null) - val verify = signature?.let { verifyHttpSignature(httpRequest, it) } ?: false + val verify = signature?.let { verifyHttpSignature(httpRequest, it, transaction) } ?: false - logger.debug("Is verifying success? {}", verify) + transaction.transaction { + logger.debug("Is verifying success? {}", verify) - val activityPubProcessor = - activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as ActivityPubProcessor? + val activityPubProcessor = + activityPubProcessorList.firstOrNull { it.isSupported(param.type) } as ActivityPubProcessor? - if (activityPubProcessor == null) { - logger.warn("ActivityType {} is not support.", param.type) - throw IllegalStateException("ActivityPubProcessor not found.") + if (activityPubProcessor == null) { + logger.warn("ActivityType {} is not support.", param.type) + throw IllegalStateException("ActivityPubProcessor not found.") + } + + val value = objectMapper.treeToValue(jsonNode, activityPubProcessor.type()) + activityPubProcessor.process(ActivityPubProcessContext(value, jsonNode, httpRequest, signature, verify)) + + logger.info("SUCCESS Process inbox. type: {}", param.type) } - - val value = objectMapper.treeToValue(jsonNode, activityPubProcessor.type()) - activityPubProcessor.process(ActivityPubProcessContext(value, jsonNode, httpRequest, signature, verify)) - - logger.info("SUCCESS Process inbox. type: {}", param.type) } override fun job(): InboxJob = InboxJob diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt index 143df20b..fff97e01 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt @@ -14,6 +14,7 @@ import dev.usbharu.hideout.core.query.UserQueryService import dev.usbharu.hideout.core.service.user.RemoteUserCreateDto import dev.usbharu.hideout.core.service.user.UserService import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional interface APUserService { suspend fun getPersonByName(name: String): Person @@ -61,7 +62,6 @@ class APUserServiceImpl( url = "$userUrl/icon.png" ), publicKey = Key( - type = emptyList(), id = userEntity.keyId, owner = userUrl, publicKeyPem = userEntity.publicKey @@ -75,6 +75,7 @@ class APUserServiceImpl( override suspend fun fetchPerson(url: String, targetActor: String?): Person = fetchPersonWithEntity(url, targetActor).first + @Transactional override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair { return try { val userEntity = userQueryService.findByUrl(url) @@ -94,15 +95,13 @@ class APUserServiceImpl( name = person.preferredUsername ?: throw IllegalActivityPubObjectException("preferredUsername is null"), domain = id.substringAfter("://").substringBefore("/"), - screenName = person.name - ?: throw IllegalActivityPubObjectException("preferredUsername is null"), + screenName = person.name, description = person.summary.orEmpty(), inbox = person.inbox, outbox = person.outbox, url = id, - publicKey = person.publicKey?.publicKeyPem - ?: throw IllegalActivityPubObjectException("publicKey is null"), - keyId = person.publicKey?.id ?: throw IllegalActivityPubObjectException("publicKey keyId is null"), + publicKey = person.publicKey.publicKeyPem, + keyId = person.publicKey.id, following = person.following, followers = person.followers, sharedInbox = person.endpoints["sharedInbox"] @@ -129,7 +128,6 @@ class APUserServiceImpl( url = "$id/icon.png" ), publicKey = Key( - type = emptyList(), id = userEntity.keyId, owner = id, publicKeyPem = userEntity.publicKey diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/ActivityPubConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/ActivityPubConfig.kt index d661a30a..c99d07b2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/ActivityPubConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/ActivityPubConfig.kt @@ -7,6 +7,8 @@ import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.hideout.core.infrastructure.httpsignature.HttpRequestMixIn +import dev.usbharu.httpsignature.common.HttpRequest import dev.usbharu.httpsignature.sign.HttpSignatureSigner import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner import org.springframework.beans.factory.annotation.Qualifier @@ -24,12 +26,13 @@ class ActivityPubConfig { val objectMapper = jacksonObjectMapper() .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) - .setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY)) - .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)) + .setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP)) + .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SKIP)) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(JsonParser.Feature.ALLOW_COMMENTS, true) .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true) .configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true) + .addMixIn(HttpRequest::class.java, HttpRequestMixIn::class.java) return objectMapper } diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt index fb2e86ec..dead45f0 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -11,12 +11,14 @@ import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.Htt import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUserDetailsService import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureVerifierComposite import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsImpl +import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsServiceImpl import dev.usbharu.hideout.core.query.UserQueryService import dev.usbharu.hideout.util.RsaUtil import dev.usbharu.hideout.util.hasAnyScope import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier +import jakarta.annotation.PostConstruct import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer @@ -32,6 +34,8 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter import org.springframework.security.authentication.AccountStatusUserDetailsChecker import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.dao.DaoAuthenticationProvider +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity @@ -59,7 +63,7 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* -@EnableWebSecurity(debug = true) +@EnableWebSecurity(debug = false) @Configuration @Suppress("FunctionMaxLength", "TooManyFunctions") class SecurityConfig { @@ -80,7 +84,9 @@ class SecurityConfig { http { securityMatcher("/users/*/posts/*") addFilterAt(httpSignatureFilter) - addFilterBefore(ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) + addFilterBefore( + ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + ) authorizeHttpRequests { authorize(anyRequest, permitAll) } @@ -114,6 +120,16 @@ class SecurityConfig { } @Bean + @Order(2) + fun daoAuthenticationProvider(userDetailsServiceImpl: UserDetailsServiceImpl): DaoAuthenticationProvider { + val daoAuthenticationProvider = DaoAuthenticationProvider() + daoAuthenticationProvider.setUserDetailsService(userDetailsServiceImpl) + + return daoAuthenticationProvider + } + + @Bean + @Order(1) fun httpSignatureAuthenticationProvider(transaction: Transaction): PreAuthenticatedAuthenticationProvider { val provider = PreAuthenticatedAuthenticationProvider() val signatureHeaderParser = DefaultSignatureHeaderParser() @@ -146,7 +162,6 @@ class SecurityConfig { } oauth2ResourceServer { jwt { - } } } @@ -158,7 +173,6 @@ class SecurityConfig { fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeHttpRequests { - authorize("/error", permitAll) authorize("/login", permitAll) authorize(GET, "/.well-known/**", permitAll) @@ -186,11 +200,10 @@ class SecurityConfig { } formLogin { - } csrf { - ignoringRequestMatchers("/users/*/inbox", "/inbox", "/api/v1/apps", "/api/v1/accounts") + ignoringRequestMatchers("/users/*/inbox", "/inbox", "/api/v1/apps") } headers { @@ -269,3 +282,17 @@ data class JwkConfig( val publicKey: String, val privateKey: String ) + +@Configuration +class PostSecurityConfig( + val auth: AuthenticationManagerBuilder, + val daoAuthenticationProvider: DaoAuthenticationProvider, + val httpSignatureAuthenticationProvider: PreAuthenticatedAuthenticationProvider +) { + + @PostConstruct + fun config() { + auth.authenticationProvider(daoAuthenticationProvider) + auth.authenticationProvider(httpSignatureAuthenticationProvider) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/httpsignature/HttpRequestMixIn.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/httpsignature/HttpRequestMixIn.kt new file mode 100644 index 00000000..4f998d91 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/httpsignature/HttpRequestMixIn.kt @@ -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() { + 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()) + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueParentService.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueParentService.kt index aadf0f9f..f61949e2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueParentService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobexposed/KJobJobQueueParentService.kt @@ -32,7 +32,10 @@ class KJobJobQueueParentService : JobQueueParentService { } override suspend fun > scheduleTypeSafe(job: J, jobProps: T) { + logger.debug("SCHEDULE Job: {}", job.name) + logger.trace("Job props: {}", jobProps) val convert: ScheduleContext.(J) -> Unit = job.convert(jobProps) kjob.schedule(job, convert) + logger.debug("SUCCESS Schedule Job: {}", job.name) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobmongodb/KJobMongoJobQueueWorkerService.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobmongodb/KJobMongoJobQueueWorkerService.kt index bb48b08b..fd57f210 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobmongodb/KJobMongoJobQueueWorkerService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobmongodb/KJobMongoJobQueueWorkerService.kt @@ -2,6 +2,7 @@ package dev.usbharu.hideout.core.infrastructure.kjobmongodb import com.mongodb.reactivestreams.client.MongoClient import dev.usbharu.hideout.core.external.job.HideoutJob +import dev.usbharu.hideout.core.service.job.JobProcessor import dev.usbharu.hideout.core.service.job.JobQueueWorkerService import kjob.core.dsl.JobContextWithProps import kjob.core.dsl.JobRegisterContext @@ -13,7 +14,10 @@ import org.springframework.stereotype.Service @Service @ConditionalOnProperty(name = ["hideout.use-mongodb"], havingValue = "true", matchIfMissing = false) -class KJobMongoJobQueueWorkerService(private val mongoClient: MongoClient) : JobQueueWorkerService, AutoCloseable { +class KJobMongoJobQueueWorkerService( + private val mongoClient: MongoClient, + private val jobQueueProcessorList: List> +) : JobQueueWorkerService, AutoCloseable { val kjob by lazy { kjob(Mongo) { client = mongoClient @@ -30,6 +34,14 @@ class KJobMongoJobQueueWorkerService(private val mongoClient: MongoClient) : Job defines.forEach { job -> kjob.register(job.first, job.second) } + for (jobProcessor in jobQueueProcessorList) { + kjob.register(jobProcessor.job()) { + execute { + val param = it.convertUnsafe(props) + jobProcessor.process(param) + } + } + } } override fun close() { diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobmongodb/KjobMongoJobQueueParentService.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobmongodb/KjobMongoJobQueueParentService.kt index f66f58ef..37f600dc 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobmongodb/KjobMongoJobQueueParentService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/kjobmongodb/KjobMongoJobQueueParentService.kt @@ -7,6 +7,7 @@ import kjob.core.Job import kjob.core.dsl.ScheduleContext import kjob.core.kjob import kjob.mongo.Mongo +import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.stereotype.Service @@ -26,15 +27,23 @@ class KjobMongoJobQueueParentService(private val mongoClient: MongoClient) : Job @Deprecated("use type safe → scheduleTypeSafe") override suspend fun schedule(job: J, block: ScheduleContext.(J) -> Unit) { + logger.debug("SCHEDULE Job: {}", job.name) kjob.schedule(job, block) } override suspend fun > scheduleTypeSafe(job: J, jobProps: T) { + logger.debug("SCHEDULE Job: {}", job.name) + logger.trace("Job props: {}", jobProps) val convert = job.convert(jobProps) kjob.schedule(job, convert) + logger.debug("SUCCESS Job: {}", job.name) } override fun close() { kjob.shutdown() } + + companion object { + private val logger = LoggerFactory.getLogger(KjobMongoJobQueueParentService::class.java) + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt index 3acc12f6..a75fe934 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/springframework/httpsignature/HttpSignatureUserDetailsService.kt @@ -27,13 +27,10 @@ class HttpSignatureUserDetailsService( ) : AuthenticationUserDetailsService { override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking { - if (token.principal !is String) { - throw IllegalStateException("Token is not String") - } + check(token.principal is String) { "Token is not String" } val credentials = token.credentials - if (credentials !is HttpRequest) { - throw IllegalStateException("Credentials is not HttpRequest") - } + + check(credentials is HttpRequest) { "Credentials is not HttpRequest" } val keyId = token.principal as String val findByKeyId = transaction.transaction { diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceService.kt index 2c773ce0..d06e53ad 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/instance/InstanceService.kt @@ -81,7 +81,7 @@ class InstanceServiceImpl( } else -> { - TODO() + throw IllegalStateException("Unknown nodeinfo versions: $key url: $value") } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/resource/InMemoryCacheManager.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/InMemoryCacheManager.kt index 587829c9..6efbc980 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/resource/InMemoryCacheManager.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/InMemoryCacheManager.kt @@ -29,7 +29,13 @@ class InMemoryCacheManager : CacheManager { } } if (needRunBlock) { - val processed = block() + @Suppress("TooGenericExceptionCaught") + val processed = try { + block() + } catch (e: Exception) { + cacheKey.remove(key) + throw e + } if (cacheKey.containsKey(key)) { valueStore[key] = processed diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt index fa1c0f97..d2a2e45e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt @@ -12,6 +12,7 @@ import dev.usbharu.hideout.core.service.instance.InstanceService import org.jetbrains.exposed.exceptions.ExposedSQLException import org.slf4j.LoggerFactory import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import java.time.Instant @Service @@ -57,7 +58,9 @@ class UserServiceImpl( return userRepository.save(userEntity) } + @Transactional override suspend fun createRemoteUser(user: RemoteUserCreateDto): User { + logger.info("START Create New remote user. name: {} url: {}", user.name, user.url) @Suppress("TooGenericExceptionCaught") val instance = try { instanceService.fetchInstance(user.url, user.sharedInbox) @@ -84,8 +87,11 @@ class UserServiceImpl( instance = instance?.id ) return try { - userRepository.save(userEntity) + val save = userRepository.save(userEntity) + logger.warn("SUCCESS Create New remote user. id: {} name: {} url: {}", userEntity.id, user.name, user.url) + save } catch (_: ExposedSQLException) { + logger.warn("FAILED User already exists. name: {} url: {}", user.name, user.url) userQueryService.findByUrl(user.url) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/util/SpringSecurityKotlinDslExtension.kt b/src/main/kotlin/dev/usbharu/hideout/util/SpringSecurityKotlinDslExtension.kt index 42159643..52a2f486 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/SpringSecurityKotlinDslExtension.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/SpringSecurityKotlinDslExtension.kt @@ -7,7 +7,6 @@ import org.springframework.security.web.access.intercept.RequestAuthorizationCon fun AuthorizeHttpRequestsDsl.hasScope(scope: String): AuthorizationManager = hasAuthority("SCOPE_$scope") +@Suppress("SpreadOperator") fun AuthorizeHttpRequestsDsl.hasAnyScope(vararg scopes: String): AuthorizationManager = - hasAnyAuthority( - *scopes.map { "SCOPE_$it" }.toTypedArray() - ) + hasAnyAuthority(*scopes.map { "SCOPE_$it" }.toTypedArray()) diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/KeySerializeTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/KeySerializeTest.kt new file mode 100644 index 00000000..d6e1be7a --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/KeySerializeTest.kt @@ -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(trimIndent) + + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/PersonSerializeTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/PersonSerializeTest.kt new file mode 100644 index 00000000..9b344337 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/PersonSerializeTest.kt @@ -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(personString) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImplTest.kt index 42f44e27..5d7bab4d 100644 --- a/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImplTest.kt @@ -55,8 +55,7 @@ class UserAPControllerImplTest { publicKey = Key( id = "https://example.com/users/hoge#pubkey", owner = "https://example.com/users/hoge", - publicKeyPem = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", - type = emptyList() + publicKeyPem = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----" ), endpoints = mapOf("sharedInbox" to "https://example.com/inbox"), followers = "https://example.com/users/hoge/followers", diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt index 5ed6597d..b8713816 100644 --- a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt @@ -126,7 +126,6 @@ class APNoteServiceImplTest { url = user.url + "/icon.png" ), publicKey = Key( - type = emptyList(), id = user.keyId, owner = user.url, publicKeyPem = user.publicKey @@ -245,7 +244,6 @@ class APNoteServiceImplTest { url = user.url + "/icon.png" ), publicKey = Key( - type = emptyList(), id = user.keyId, owner = user.url, publicKeyPem = user.publicKey