From 50415688d52c2ec72091ec40e9bdb988b6b3a12e Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 30 Nov 2023 23:01:18 +0900 Subject: [PATCH 01/33] =?UTF-8?q?test:=20e2e=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 39 +++++++++-- src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt | 69 ++++++++++++++++++++ src/e2eTest/resources/application.yml | 42 ++++++++++++ src/e2eTest/resources/logback.xml | 11 ++++ 4 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt create mode 100644 src/e2eTest/resources/application.yml create mode 100644 src/e2eTest/resources/logback.xml diff --git a/build.gradle.kts b/build.gradle.kts index 7a5f4242..002d9613 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,13 @@ 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") + + + } detekt { diff --git a/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt new file mode 100644 index 00000000..e3c29202 --- /dev/null +++ b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt @@ -0,0 +1,69 @@ +package oauth2 + +import dev.usbharu.hideout.SpringApplication +import org.jsoup.Jsoup +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.function.BodyInserters + +@SpringBootTest( + classes = [SpringApplication::class], + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, +) +@TestMethodOrder(OrderAnnotation::class) +class OAuth2LoginTest { + + @Autowired + private lateinit var webTestClient: WebTestClient + + @Test + @Order(2) + fun アカウント作成() { + val returnResult = webTestClient.get() + .uri("/auth/sign_up") + .exchange() + .expectStatus() + .isOk + .returnResult(String::class.java) + + val html = returnResult + .responseBody + .toStream() + .toList() + .toList() + .joinToString("") + + val session = returnResult.responseCookies["JSESSIONID"]?.first()?.value!! + + val attr = Jsoup.parse(html).selectXpath("//input[@name=\"_csrf\"]").attr("value") + + println("CSRF TOKEN = $attr") + + val csrfToken = attr + + webTestClient + .post() + .uri("/api/v1/accounts") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body( + BodyInserters.fromFormData("username", "oatuh-login-test") + .with("password", "very-secure-password").with("_csrf", csrfToken) + ) + .cookie("JSESSIONID", session) + .exchange() + .expectStatus().isFound + .expectCookie() + + } + +// @Test +// fun `OAuth2で権限read writeを持ったトークンでのログインができる`() { +//// webTestClient.post().uri("/api/v1/apps") +// } +} diff --git a/src/e2eTest/resources/application.yml b/src/e2eTest/resources/application.yml new file mode 100644 index 00000000..b40fcd91 --- /dev/null +++ b/src/e2eTest/resources/application.yml @@ -0,0 +1,42 @@ +hideout: + url: "https://localhost:8080" + use-mongodb: true + security: + jwt: + generate: true + key-id: a + private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ==" + public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB" + storage: + 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:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4;" + username: "" + password: + data: + mongodb: + auto-index-creation: true + host: localhost + port: 27017 + database: hideout + h2: + console: + enabled: true + +# exposed: +# generate-ddl: true +# excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed +server: + port: 8080 diff --git a/src/e2eTest/resources/logback.xml b/src/e2eTest/resources/logback.xml new file mode 100644 index 00000000..a8bb21c4 --- /dev/null +++ b/src/e2eTest/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n + + + + + + + From 6727a1c8da4bf9156b4ba3ee57f8de06a2a38760 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 30 Nov 2023 23:11:09 +0900 Subject: [PATCH 02/33] =?UTF-8?q?fix:=20POST:=20/api/v1/accounts=E3=81=AB?= =?UTF-8?q?=E5=AF=BE=E3=81=99=E3=82=8B=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=81=ABCSRF=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3?= =?UTF-8?q?=E3=81=8C=E5=BF=85=E9=A0=88=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/mastodon/account/AccountApiTest.kt | 26 ++++++++++++++++++- .../application/config/SecurityConfig.kt | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) 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/application/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt index fb2e86ec..be0d6919 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -190,7 +190,7 @@ class SecurityConfig { } csrf { - ignoringRequestMatchers("/users/*/inbox", "/inbox", "/api/v1/apps", "/api/v1/accounts") + ignoringRequestMatchers("/users/*/inbox", "/inbox", "/api/v1/apps") } headers { From 1ad9eb8e88ffa5b24a4c888fbb5fe67462e7f099 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 1 Dec 2023 10:50:52 +0900 Subject: [PATCH 03/33] =?UTF-8?q?test:=20=E4=B8=8D=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E5=A4=89=E6=95=B0=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt index e3c29202..723f6f05 100644 --- a/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt +++ b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt @@ -45,15 +45,13 @@ class OAuth2LoginTest { println("CSRF TOKEN = $attr") - val csrfToken = attr - webTestClient .post() .uri("/api/v1/accounts") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body( BodyInserters.fromFormData("username", "oatuh-login-test") - .with("password", "very-secure-password").with("_csrf", csrfToken) + .with("password", "very-secure-password").with("_csrf", attr) ) .cookie("JSESSIONID", session) .exchange() From ef40b94b5ed6b9e2cd2cfc89efdaa8f8bcbe4c0d Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 1 Dec 2023 17:32:10 +0900 Subject: [PATCH 04/33] =?UTF-8?q?test:=20e2e=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 + src/e2eTest/kotlin/KarateUtil.kt | 12 +++ src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt | 82 ++++++++----------- src/e2eTest/resources/karate-config.js | 25 ++++++ .../resources/oauth2/Oauth2LoginTest.feature | 32 ++++++++ src/e2eTest/resources/oauth2/test.feature | 9 ++ src/e2eTest/resources/oauth2/user.sql | 46 +++++++++++ 7 files changed, 158 insertions(+), 50 deletions(-) create mode 100644 src/e2eTest/kotlin/KarateUtil.kt create mode 100644 src/e2eTest/resources/karate-config.js create mode 100644 src/e2eTest/resources/oauth2/Oauth2LoginTest.feature create mode 100644 src/e2eTest/resources/oauth2/test.feature create mode 100644 src/e2eTest/resources/oauth2/user.sql diff --git a/build.gradle.kts b/build.gradle.kts index 002d9613..c8d31ca7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -233,6 +233,8 @@ dependencies { 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") + diff --git a/src/e2eTest/kotlin/KarateUtil.kt b/src/e2eTest/kotlin/KarateUtil.kt new file mode 100644 index 00000000..e78e8510 --- /dev/null +++ b/src/e2eTest/kotlin/KarateUtil.kt @@ -0,0 +1,12 @@ +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") + } + } +} diff --git a/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt index 723f6f05..ba524c0d 100644 --- a/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt +++ b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt @@ -1,67 +1,49 @@ package oauth2 +import KarateUtil +import com.intuit.karate.junit5.Karate import dev.usbharu.hideout.SpringApplication -import org.jsoup.Jsoup -import org.junit.jupiter.api.MethodOrderer.OrderAnnotation -import org.junit.jupiter.api.Order -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestMethodOrder +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.http.MediaType -import org.springframework.test.web.reactive.server.WebTestClient -import org.springframework.web.reactive.function.BodyInserters +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.test.context.jdbc.Sql @SpringBootTest( classes = [SpringApplication::class], webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, ) -@TestMethodOrder(OrderAnnotation::class) +@Sql("/oauth2/user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) class OAuth2LoginTest { - @Autowired - private lateinit var webTestClient: WebTestClient + @LocalServerPort + private var port = "" - @Test - @Order(2) - fun アカウント作成() { - val returnResult = webTestClient.get() - .uri("/auth/sign_up") - .exchange() - .expectStatus() - .isOk - .returnResult(String::class.java) - - val html = returnResult - .responseBody - .toStream() - .toList() - .toList() - .joinToString("") - - val session = returnResult.responseCookies["JSESSIONID"]?.first()?.value!! - - val attr = Jsoup.parse(html).selectXpath("//input[@name=\"_csrf\"]").attr("value") - - println("CSRF TOKEN = $attr") - - webTestClient - .post() - .uri("/api/v1/accounts") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body( - BodyInserters.fromFormData("username", "oatuh-login-test") - .with("password", "very-secure-password").with("_csrf", attr) - ) - .cookie("JSESSIONID", session) - .exchange() - .expectStatus().isFound - .expectCookie() + @Karate.Test + @TestFactory + fun test(): Karate = + Karate.run("test").scenarioName("invalid").relativeTo(javaClass).systemProperty("karate.port", port) + .karateEnv("dev") + @Karate.Test + @TestFactory + fun `スコープwrite readを持ったトークンの作成`(): Karate { + return KarateUtil.springBootKarateTest( + "Oauth2LoginTest", + "スコープwrite readを持ったトークンの作成", + javaClass, + port + ) } -// @Test -// fun `OAuth2で権限read writeを持ったトークンでのログインができる`() { -//// webTestClient.post().uri("/api/v1/apps") -// } + companion object { + @JvmStatic + @AfterAll + fun dropDatabase(@Autowired flyway: Flyway) { + flyway.clean() + flyway.migrate() + } + } } diff --git a/src/e2eTest/resources/karate-config.js b/src/e2eTest/resources/karate-config.js new file mode 100644 index 00000000..9fd5dd1e --- /dev/null +++ b/src/e2eTest/resources/karate-config.js @@ -0,0 +1,25 @@ +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') { + config = { + baseUrl: 'https://test-hideout.usbharu.dev' + } + } else if (env === 'dev') { + let port = karate.properties['karate.port'] || '8080' + config = { + baseUrl: 'http://localhost:' + port + } + } 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/oauth2/Oauth2LoginTest.feature b/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature new file mode 100644 index 00000000..29ae760c --- /dev/null +++ b/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature @@ -0,0 +1,32 @@ +Feature: OAuth2 Login Test + + Background: + * url baseUrl + * configure driver = { type: 'chrome' } + + Scenario: スコープwrite readを持ったトークンの作成 + + * def apps = + """ + { + "client_name": "oauth2-test-client-1", + "redirect_uris": "https://example.com", + "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://example.com&client_id=' + client_id + '&scope=write read' + + Given driver authorizeEndpoint + And driver.input('#username','test-user') + And driver.input('#password','password') + When driver.submit().click('body > div > form > button') + Then match driver.title == 'test' diff --git a/src/e2eTest/resources/oauth2/test.feature b/src/e2eTest/resources/oauth2/test.feature new file mode 100644 index 00000000..7a801de8 --- /dev/null +++ b/src/e2eTest/resources/oauth2/test.feature @@ -0,0 +1,9 @@ +Feature: test + + Background: + * url baseUrl + + Scenario: test + Given path '/api/v1/apps' + When method get + Then status 401 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); From aff9b68480953226961f6873fbfcd1d9c4a94df7 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 1 Dec 2023 17:32:36 +0900 Subject: [PATCH 05/33] =?UTF-8?q?fix:=20OAuth2=E3=81=A7=E3=83=AD=E3=82=B0?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=8F=E3=81=AA?= =?UTF-8?q?=E3=81=A3=E3=81=A6=E3=81=84=E3=81=9F=E3=81=AE=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/config/SecurityConfig.kt | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) 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 be0d6919..aa60fa16 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,8 @@ 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 { @@ -114,6 +119,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() @@ -269,3 +284,18 @@ 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) + } +} From c1c2e1e8437bc2572e966f99d33745018fbccf05 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 1 Dec 2023 22:59:39 +0900 Subject: [PATCH 06/33] =?UTF-8?q?test:=20OAuth2=E3=81=A7=E3=81=AE=E3=83=88?= =?UTF-8?q?=E3=83=BC=E3=82=AF=E3=83=B3=E4=BD=9C=E6=88=90=E3=81=AE=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/e2eTest/resources/application.yml | 4 ++++ src/e2eTest/resources/logback.xml | 8 +++++++ .../resources/oauth2/Oauth2LoginTest.feature | 24 ++++++++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/e2eTest/resources/application.yml b/src/e2eTest/resources/application.yml index b40fcd91..69e0ebcd 100644 --- a/src/e2eTest/resources/application.yml +++ b/src/e2eTest/resources/application.yml @@ -40,3 +40,7 @@ spring: # excluded-packages: dev.usbharu.hideout.core.infrastructure.kjobexposed server: port: 8080 + tomcat: + basedir: tomcat-e2e + accesslog: + enabled: true diff --git a/src/e2eTest/resources/logback.xml b/src/e2eTest/resources/logback.xml index a8bb21c4..7f19e2eb 100644 --- a/src/e2eTest/resources/logback.xml +++ b/src/e2eTest/resources/logback.xml @@ -1,4 +1,11 @@ + + ./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 @@ -6,6 +13,7 @@ + diff --git a/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature b/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature index 29ae760c..582af835 100644 --- a/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature +++ b/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature @@ -10,7 +10,7 @@ Feature: OAuth2 Login Test """ { "client_name": "oauth2-test-client-1", - "redirect_uris": "https://example.com", + "redirect_uris": "https://usbharu.dev", "scopes": "write read" } """ @@ -23,10 +23,28 @@ Feature: OAuth2 Login Test * def client_id = response.client_id * def client_secret = response.client_secret - * def authorizeEndpoint = baseUrl + '/oauth/authorize?response_type=code&redirect_uri=https://example.com&client_id=' + client_id + '&scope=write read' + * 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 match driver.title == 'test' + 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 From d94099bacfe6f624c439749061352ff7085992b5 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:00:27 +0900 Subject: [PATCH 07/33] =?UTF-8?q?chore:=20gitignore=E3=82=92=E7=B7=A8?= =?UTF-8?q?=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) 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 From 58dbcb810e0012ccb9142299da8a9b3bfe50dd72 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:10:47 +0900 Subject: [PATCH 08/33] =?UTF-8?q?chore:=20=E9=81=8E=E5=89=B0=E3=81=AB?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E3=82=92=E5=87=BA=E5=8A=9B=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=9F=E3=82=82=E3=81=AE=E3=82=92=E5=81=9C=E6=AD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/e2eTest/resources/application.yml | 2 +- src/e2eTest/resources/logback.xml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/e2eTest/resources/application.yml b/src/e2eTest/resources/application.yml index 69e0ebcd..ad806350 100644 --- a/src/e2eTest/resources/application.yml +++ b/src/e2eTest/resources/application.yml @@ -22,7 +22,7 @@ spring: clean-disabled: false datasource: driver-class-name: org.h2.Driver - url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4;" + url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true" username: "" password: data: diff --git a/src/e2eTest/resources/logback.xml b/src/e2eTest/resources/logback.xml index 7f19e2eb..1eacbcd5 100644 --- a/src/e2eTest/resources/logback.xml +++ b/src/e2eTest/resources/logback.xml @@ -16,4 +16,6 @@ + + From 5ff06b88f713d5bae6a8beb7ae51140e595d0af6 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:53:22 +0900 Subject: [PATCH 09/33] =?UTF-8?q?test:=20=E5=88=A5=E3=81=AE=E3=82=B9?= =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=97=E3=82=92=E8=A6=81=E6=B1=82=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3=E3=81=AE=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt | 11 +++++ src/e2eTest/resources/logback.xml | 2 +- .../resources/oauth2/Oauth2LoginTest.feature | 45 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt index ba524c0d..3dfece24 100644 --- a/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt +++ b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt @@ -38,6 +38,17 @@ class OAuth2LoginTest { ) } + @Karate.Test + @TestFactory + fun `スコープread_statuses write_statusesを持ったトークンの作成`(): Karate { + return KarateUtil.springBootKarateTest( + "Oauth2LoginTest", + "スコープread:statuses write:statusesを持ったトークンの作成", + javaClass, + port + ) + } + companion object { @JvmStatic @AfterAll diff --git a/src/e2eTest/resources/logback.xml b/src/e2eTest/resources/logback.xml index 1eacbcd5..c21752ee 100644 --- a/src/e2eTest/resources/logback.xml +++ b/src/e2eTest/resources/logback.xml @@ -16,6 +16,6 @@ - + diff --git a/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature b/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature index 582af835..f330c369 100644 --- a/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature +++ b/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature @@ -48,3 +48,48 @@ Feature: OAuth2 Login Test 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 From e52064c330110f26ea06b91fc9d1d3ae4d08a28e Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:54:44 +0900 Subject: [PATCH 10/33] =?UTF-8?q?test:=20=E4=B8=8D=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt | 6 ------ src/e2eTest/resources/oauth2/test.feature | 9 --------- 2 files changed, 15 deletions(-) delete mode 100644 src/e2eTest/resources/oauth2/test.feature diff --git a/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt index 3dfece24..c13bd810 100644 --- a/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt +++ b/src/e2eTest/kotlin/oauth2/OAuth2LoginTest.kt @@ -21,12 +21,6 @@ class OAuth2LoginTest { @LocalServerPort private var port = "" - @Karate.Test - @TestFactory - fun test(): Karate = - Karate.run("test").scenarioName("invalid").relativeTo(javaClass).systemProperty("karate.port", port) - .karateEnv("dev") - @Karate.Test @TestFactory fun `スコープwrite readを持ったトークンの作成`(): Karate { diff --git a/src/e2eTest/resources/oauth2/test.feature b/src/e2eTest/resources/oauth2/test.feature deleted file mode 100644 index 7a801de8..00000000 --- a/src/e2eTest/resources/oauth2/test.feature +++ /dev/null @@ -1,9 +0,0 @@ -Feature: test - - Background: - * url baseUrl - - Scenario: test - Given path '/api/v1/apps' - When method get - Then status 401 From faff15f5590105926ee6a8839d27d5f311ad0469 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 10:18:27 +0900 Subject: [PATCH 11/33] =?UTF-8?q?test:=20inbox=E3=81=AB=E3=82=A2=E3=82=AF?= =?UTF-8?q?=E3=82=BB=E3=82=B9=E3=81=97=E3=81=A6=E3=81=8D=E3=81=9F=E3=83=A6?= =?UTF-8?q?=E3=83=BC=E3=82=B6=E3=83=BC=E3=82=92=E5=8F=96=E5=BE=97=E3=81=99?= =?UTF-8?q?=E3=82=8Be2e=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/e2eTest/kotlin/AssertionUtil.kt | 18 +++ src/e2eTest/kotlin/KarateUtil.kt | 16 +++ .../kotlin/federation/InboxCommonTest.kt | 91 +++++++++++++ src/e2eTest/resources/application.yml | 4 +- .../federation/InboxCommonTest.feature | 22 +++ .../InboxxCommonMockServerTest.feature | 126 ++++++++++++++++++ src/e2eTest/resources/karate-config.js | 9 +- 7 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 src/e2eTest/kotlin/AssertionUtil.kt create mode 100644 src/e2eTest/kotlin/federation/InboxCommonTest.kt create mode 100644 src/e2eTest/resources/federation/InboxCommonTest.feature create mode 100644 src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature diff --git a/src/e2eTest/kotlin/AssertionUtil.kt b/src/e2eTest/kotlin/AssertionUtil.kt new file mode 100644 index 00000000..4d559624 --- /dev/null +++ b/src/e2eTest/kotlin/AssertionUtil.kt @@ -0,0 +1,18 @@ +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 + +object AssertionUtil { + + fun assertUserExist(username: String, domain: String): Boolean { + 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 domain) }.empty().not() + } +} diff --git a/src/e2eTest/kotlin/KarateUtil.kt b/src/e2eTest/kotlin/KarateUtil.kt index e78e8510..c71cd44b 100644 --- a/src/e2eTest/kotlin/KarateUtil.kt +++ b/src/e2eTest/kotlin/KarateUtil.kt @@ -9,4 +9,20 @@ object KarateUtil { .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/InboxCommonTest.kt b/src/e2eTest/kotlin/federation/InboxCommonTest.kt new file mode 100644 index 00000000..59bc4e14 --- /dev/null +++ b/src/e2eTest/kotlin/federation/InboxCommonTest.kt @@ -0,0 +1,91 @@ +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 +import java.net.MalformedURLException +import java.net.URL + +@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 + ) + } + + 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 + } + } + + assertTrue(check, "User @$username@$s 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/resources/application.yml b/src/e2eTest/resources/application.yml index ad806350..1013de15 100644 --- a/src/e2eTest/resources/application.yml +++ b/src/e2eTest/resources/application.yml @@ -1,6 +1,6 @@ hideout: url: "https://localhost:8080" - use-mongodb: true + use-mongodb: false security: jwt: generate: true @@ -22,7 +22,7 @@ spring: clean-disabled: false datasource: driver-class-name: org.h2.Driver - url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true" + 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: diff --git a/src/e2eTest/resources/federation/InboxCommonTest.feature b/src/e2eTest/resources/federation/InboxCommonTest.feature new file mode 100644 index 00000000..7d102607 --- /dev/null +++ b/src/e2eTest/resources/federation/InboxCommonTest.feature @@ -0,0 +1,22 @@ +Feature: Inbox Common Test + + Background: + * url baseUrl + + Scenario: inboxにHTTP Signature付きのリクエストがあったらリモートに取得しに行く + + * 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) diff --git a/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature new file mode 100644 index 00000000..601dcf83 --- /dev/null +++ b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature @@ -0,0 +1,126 @@ +Feature: InboxCommonMockServer + + Background: + * def assertInbox = Java.type(`federation.InboxCommonTest`) + + Scenario: pathMatches('/users/test-user') && methodIs('get') + * def remoteUrl = 'http://localhost:' + assertInbox.getRemotePort() + * def userUrl = remoteUrl + '/users/test-user' + + * 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": "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": "test-user", + "name": "test-user", + "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" + } +} + + """ + + * def response = person diff --git a/src/e2eTest/resources/karate-config.js b/src/e2eTest/resources/karate-config.js index 9fd5dd1e..a83c2bb4 100644 --- a/src/e2eTest/resources/karate-config.js +++ b/src/e2eTest/resources/karate-config.js @@ -7,18 +7,23 @@ function fn() { } let config; if (env === 'test') { + let remotePort = karate.properties['karate.remotePort'] || '8081' config = { - baseUrl: 'https://test-hideout.usbharu.dev' + 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 + 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; From bfc5c9e1106bb677c3886c85d00213633e27489a Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 10:26:48 +0900 Subject: [PATCH 12/33] =?UTF-8?q?fix:=20=E4=B8=80=E9=83=A8=E3=81=AEAP=20Ob?= =?UTF-8?q?ject=E3=81=8C=E3=83=87=E3=82=B7=E3=83=AA=E3=82=A2=E3=83=A9?= =?UTF-8?q?=E3=82=A4=E3=82=BA=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=8F=E3=81=AA?= =?UTF-8?q?=E3=81=A3=E3=81=A6=E3=81=84=E3=81=9F=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hideout/activitypub/domain/model/Key.kt | 3 +- .../activitypub/domain/model/Person.kt | 2 +- .../service/objects/user/APUserService.kt | 2 - .../application/config/ActivityPubConfig.kt | 7 +- .../httpsignature/HttpRequestMixIn.kt | 33 ++++++++ .../domain/model/KeySerializeTest.kt | 24 ++++++ .../domain/model/PersonSerializeTest.kt | 75 +++++++++++++++++++ .../objects/note/APNoteServiceImplTest.kt | 2 - 8 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/core/infrastructure/httpsignature/HttpRequestMixIn.kt create mode 100644 src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/KeySerializeTest.kt create mode 100644 src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/PersonSerializeTest.kt 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..4791fa68 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? 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..9556590f 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 @@ -61,7 +61,6 @@ class APUserServiceImpl( url = "$userUrl/icon.png" ), publicKey = Key( - type = emptyList(), id = userEntity.keyId, owner = userUrl, publicKeyPem = userEntity.publicKey @@ -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/core/infrastructure/httpsignature/HttpRequestMixIn.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/httpsignature/HttpRequestMixIn.kt new file mode 100644 index 00000000..3686d3c9 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/httpsignature/HttpRequestMixIn.kt @@ -0,0 +1,33 @@ +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/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/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 From be7bd590eb76d03c52f3d07a29e0e70e5a7114f3 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 10:27:46 +0900 Subject: [PATCH 13/33] =?UTF-8?q?fix:=20HTTP=20Signature=E3=81=AE=E3=83=98?= =?UTF-8?q?=E3=83=83=E3=83=80=E3=83=BC=E5=8F=96=E5=BE=97=E6=99=82=E3=81=AB?= =?UTF-8?q?=E5=A4=A7=E6=96=87=E5=AD=97=E5=B0=8F=E6=96=87=E5=AD=97=E3=81=AE?= =?UTF-8?q?=E5=B7=AE=E3=82=92=E7=84=A1=E8=A6=96=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activitypub/interfaces/api/inbox/InboxControllerImpl.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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, From 158cd3a6dfea55f9c9484fc2f32b5a1577d59b1c Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 10:28:19 +0900 Subject: [PATCH 14/33] =?UTF-8?q?fix:=20Inbox=E5=87=A6=E7=90=86=E3=81=AE?= =?UTF-8?q?=E3=83=88=E3=83=A9=E3=83=B3=E3=82=B6=E3=82=AF=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/inbox/InboxJobProcessor.kt | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) 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..91c3f4ed 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,37 @@ 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) - ) + 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 +72,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 +80,8 @@ 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 From 0d5fecbd4d8d852f32b7ee4ef55e3923071c20f4 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 10:28:44 +0900 Subject: [PATCH 15/33] =?UTF-8?q?fix:=20=E3=82=B8=E3=83=A7=E3=83=96?= =?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=BC=E3=81=AE=E4=BF=AE=E6=AD=A3=20?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kjobexposed/KJobJobQueueParentService.kt | 3 +++ .../kjobmongodb/KJobMongoJobQueueWorkerService.kt | 14 +++++++++++++- .../kjobmongodb/KjobMongoJobQueueParentService.kt | 9 +++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) 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) + } } From 51aeff6015062bc5884683f4fa5b98b5c0e2cdae Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 10:29:22 +0900 Subject: [PATCH 16/33] =?UTF-8?q?fix:=20=E3=83=88=E3=83=A9=E3=83=B3?= =?UTF-8?q?=E3=82=B6=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AE=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hideout/activitypub/service/objects/user/APUserService.kt | 2 ++ .../dev/usbharu/hideout/core/service/user/UserServiceImpl.kt | 2 ++ .../interfaces/api/actor/UserAPControllerImplTest.kt | 3 +-- 3 files changed, 5 insertions(+), 2 deletions(-) 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 9556590f..47568b8d 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 @@ -74,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) 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..94346400 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,6 +58,7 @@ class UserServiceImpl( return userRepository.save(userEntity) } + @Transactional override suspend fun createRemoteUser(user: RemoteUserCreateDto): User { @Suppress("TooGenericExceptionCaught") val instance = try { 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", From 14034cd1b9d3359ad46e1adae320e027c791afa3 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 11:10:59 +0900 Subject: [PATCH 17/33] =?UTF-8?q?test:=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E7=94=A8=E3=81=AE=E3=83=80=E3=83=9F=E3=83=BC=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InboxxCommonMockServerTest.feature | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature index 601dcf83..9457b36e 100644 --- a/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature +++ b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature @@ -72,52 +72,52 @@ Feature: InboxCommonMockServer "id": #(userUrl), "type": "Person", "following": #(userUrl + '/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", + "followers": #(userUrl + '/followers'), + "inbox": #(userUrl + '/inbox'), + "outbox": #(userUrl + '/outbox'), + "featured": #(userUrl + '/collections/featured'), + "featuredTags": #(userUrl + '/collections/tags'), "preferredUsername": "test-user", "name": "test-user", - "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", + "summary": "E2E Test User Jaga/Cotlin/Winter Boot/Ktol\nYonTude: https://example.com\nY(Tvvitter): https://example.com\n", + "url": #(userUrl + '/@test-user'), "manuallyApprovesFollowers": false, "discoverable": true, "published": "2016-03-16T00:00:00Z", - "devices": "https://mastodon.social/users/Gargron/collections/devices", + "devices": #(userUrl + '/collections/devices'), "alsoKnownAs": [ - "https://tooting.ai/users/Gargron" + "https://example.com/users/test-users" ], "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" + "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": "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" + "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/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" + "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": "https://mastodon.social/inbox" + "sharedInbox": #(userUrl + 'inbox') }, "icon": { "type": "Image", "mediaType": "image/jpeg", - "url": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg" + "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://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.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" } } From 5a77b9e6691b5d67eba85169465f757eeb834890 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 13:02:54 +0900 Subject: [PATCH 18/33] =?UTF-8?q?test:=20user-inbox=E3=81=AE=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/federation/InboxCommonTest.kt | 29 +++++++- .../federation/InboxCommonTest.feature | 71 +++++++++++++++++++ .../InboxxCommonMockServerTest.feature | 24 +++++-- .../core/service/instance/InstanceService.kt | 2 +- .../core/service/user/UserServiceImpl.kt | 6 +- 5 files changed, 121 insertions(+), 11 deletions(-) diff --git a/src/e2eTest/kotlin/federation/InboxCommonTest.kt b/src/e2eTest/kotlin/federation/InboxCommonTest.kt index 59bc4e14..dff26b42 100644 --- a/src/e2eTest/kotlin/federation/InboxCommonTest.kt +++ b/src/e2eTest/kotlin/federation/InboxCommonTest.kt @@ -42,9 +42,35 @@ class InboxCommonTest { ) } - companion object { + @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 + ) + } + + + companion object { lateinit var server: MockServer + lateinit var _remotePort: String @JvmStatic @@ -81,7 +107,6 @@ class InboxCommonTest { flyway.clean() flyway.migrate() } - @AfterAll @JvmStatic fun afterAll() { diff --git a/src/e2eTest/resources/federation/InboxCommonTest.feature b/src/e2eTest/resources/federation/InboxCommonTest.feature index 7d102607..142f6746 100644 --- a/src/e2eTest/resources/federation/InboxCommonTest.feature +++ b/src/e2eTest/resources/federation/InboxCommonTest.feature @@ -5,6 +5,14 @@ Feature: Inbox Common Test 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" } @@ -20,3 +28,66 @@ Feature: Inbox Common Test * 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'] diff --git a/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature index 9457b36e..519b7d05 100644 --- a/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature +++ b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature @@ -2,10 +2,13 @@ Feature: InboxCommonMockServer Background: * def assertInbox = Java.type(`federation.InboxCommonTest`) + * def req = {req: []} - Scenario: pathMatches('/users/test-user') && methodIs('get') + Scenario: pathMatches('/users/{username}') && methodIs('get') * def remoteUrl = 'http://localhost:' + assertInbox.getRemotePort() - * def userUrl = remoteUrl + '/users/test-user' + * def username = pathParams.username + * def userUrl = remoteUrl + '/users/' + username + * def person = """ @@ -77,16 +80,16 @@ Feature: InboxCommonMockServer "outbox": #(userUrl + '/outbox'), "featured": #(userUrl + '/collections/featured'), "featuredTags": #(userUrl + '/collections/tags'), - "preferredUsername": "test-user", - "name": "test-user", + "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 + '/@test-user'), + "url": #(userUrl + '/@' + username), "manuallyApprovesFollowers": false, "discoverable": true, "published": "2016-03-16T00:00:00Z", "devices": #(userUrl + '/collections/devices'), "alsoKnownAs": [ - "https://example.com/users/test-users" + #( 'https://example.com/users/' + username) ], "publicKey": { "id": #(userUrl + '#main-key'), @@ -122,5 +125,12 @@ Feature: InboxCommonMockServer } """ - + * 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/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/user/UserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt index 94346400..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 @@ -60,6 +60,7 @@ class UserServiceImpl( @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) @@ -86,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) } } From e1d99b7ea9dc4b0ed28b4dafc6468f43f417d584 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 13:03:45 +0900 Subject: [PATCH 19/33] =?UTF-8?q?fix:=20#184=20ResourceResolver=E3=81=A7?= =?UTF-8?q?=E5=88=A9=E7=94=A8=E3=81=97=E3=81=A6=E3=81=84=E3=82=8BCacheMana?= =?UTF-8?q?ger=E3=81=A7=E4=BE=8B=E5=A4=96=E3=81=8C=E7=99=BA=E7=94=9F?= =?UTF-8?q?=E3=81=97=E3=81=9F=E5=A0=B4=E5=90=88=E3=81=AB=E3=83=AD=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=81=8C=E8=A7=A3=E6=94=BE=E3=81=95=E3=82=8C=E3=81=AA?= =?UTF-8?q?=E3=81=84=E3=83=90=E3=82=B0=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hideout/core/service/resource/InMemoryCacheManager.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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..d5ca028d 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,12 @@ class InMemoryCacheManager : CacheManager { } } if (needRunBlock) { - val processed = block() + val processed = try { + block() + } catch (e: Exception) { + cacheKey.remove(key) + throw e + } if (cacheKey.containsKey(key)) { valueStore[key] = processed From beeb27350ec9102be27b60c853b62c748ca21e58 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 13:09:21 +0900 Subject: [PATCH 20/33] =?UTF-8?q?test:=20user-inbox=E3=81=AE=E8=AA=8D?= =?UTF-8?q?=E8=A8=BC=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/e2eTest/kotlin/federation/InboxCommonTest.kt | 10 ++++++++++ .../resources/federation/InboxCommonTest.feature | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/e2eTest/kotlin/federation/InboxCommonTest.kt b/src/e2eTest/kotlin/federation/InboxCommonTest.kt index dff26b42..43c204e8 100644 --- a/src/e2eTest/kotlin/federation/InboxCommonTest.kt +++ b/src/e2eTest/kotlin/federation/InboxCommonTest.kt @@ -67,6 +67,16 @@ class InboxCommonTest { ) } + @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 + ) + } companion object { lateinit var server: MockServer diff --git a/src/e2eTest/resources/federation/InboxCommonTest.feature b/src/e2eTest/resources/federation/InboxCommonTest.feature index 142f6746..eec903d3 100644 --- a/src/e2eTest/resources/federation/InboxCommonTest.feature +++ b/src/e2eTest/resources/federation/InboxCommonTest.feature @@ -91,3 +91,15 @@ Feature: Inbox Common Test * 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 From 8d4288247a82a602e03a7556a3abe974465b1dcf Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 14:01:11 +0900 Subject: [PATCH 21/33] =?UTF-8?q?test:=20inbox=E3=81=AEConsumes=E3=81=AE?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/federation/InboxCommonTest.kt | 11 ++++ .../federation/InboxCommonTest.feature | 53 +++++++++++++++++++ .../interfaces/api/inbox/InboxController.kt | 1 + 3 files changed, 65 insertions(+) diff --git a/src/e2eTest/kotlin/federation/InboxCommonTest.kt b/src/e2eTest/kotlin/federation/InboxCommonTest.kt index 43c204e8..3f869b2f 100644 --- a/src/e2eTest/kotlin/federation/InboxCommonTest.kt +++ b/src/e2eTest/kotlin/federation/InboxCommonTest.kt @@ -78,6 +78,17 @@ class InboxCommonTest { ) } + @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 diff --git a/src/e2eTest/resources/federation/InboxCommonTest.feature b/src/e2eTest/resources/federation/InboxCommonTest.feature index eec903d3..848e5630 100644 --- a/src/e2eTest/resources/federation/InboxCommonTest.feature +++ b/src/e2eTest/resources/federation/InboxCommonTest.feature @@ -103,3 +103,56 @@ Feature: Inbox Common Test 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/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 From aaa8bcdad89fb3ccf74ae7aa029e5a122e32c75e Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 15:50:23 +0900 Subject: [PATCH 22/33] =?UTF-8?q?test:=20FollowAcceptTest=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/e2eTest/kotlin/AssertionUtil.kt | 12 +- .../kotlin/federation/FollowAcceptTest.kt | 87 +++++++++++ .../kotlin/federation/InboxCommonTest.kt | 14 +- .../federation/FollowAcceptMockServer.feature | 140 ++++++++++++++++++ .../federation/FollowAcceptTest.feature | 29 ++++ .../InboxxCommonMockServerTest.feature | 2 +- 6 files changed, 271 insertions(+), 13 deletions(-) create mode 100644 src/e2eTest/kotlin/federation/FollowAcceptTest.kt create mode 100644 src/e2eTest/resources/federation/FollowAcceptMockServer.feature create mode 100644 src/e2eTest/resources/federation/FollowAcceptTest.feature diff --git a/src/e2eTest/kotlin/AssertionUtil.kt b/src/e2eTest/kotlin/AssertionUtil.kt index 4d559624..8083b825 100644 --- a/src/e2eTest/kotlin/AssertionUtil.kt +++ b/src/e2eTest/kotlin/AssertionUtil.kt @@ -2,10 +2,20 @@ 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) @@ -13,6 +23,6 @@ object AssertionUtil { selectAll.map { "@${it[Users.name]}@${it[Users.domain]}" }.forEach { println(it) } - return Users.select { Users.name eq username and (Users.domain eq domain) }.empty().not() + return Users.select { Users.name eq username and (Users.domain eq s) }.empty().not() } } diff --git a/src/e2eTest/kotlin/federation/FollowAcceptTest.kt b/src/e2eTest/kotlin/federation/FollowAcceptTest.kt new file mode 100644 index 00000000..2f7cdf4d --- /dev/null +++ b/src/e2eTest/kotlin/federation/FollowAcceptTest.kt @@ -0,0 +1,87 @@ +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 +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 +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 + 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 index 3f869b2f..33d595af 100644 --- a/src/e2eTest/kotlin/federation/InboxCommonTest.kt +++ b/src/e2eTest/kotlin/federation/InboxCommonTest.kt @@ -16,8 +16,6 @@ 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], @@ -96,24 +94,17 @@ class InboxCommonTest { @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 + check = AssertionUtil.assertUserExist(username, domain) or check if (check) { return@repeat } } - assertTrue(check, "User @$username@$s not exist.") + assertTrue(check, "User @$username@$domain not exist.") } @JvmStatic @@ -128,6 +119,7 @@ class InboxCommonTest { flyway.clean() flyway.migrate() } + @AfterAll @JvmStatic fun afterAll() { 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/InboxxCommonMockServerTest.feature b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature index 519b7d05..6d114c04 100644 --- a/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature +++ b/src/e2eTest/resources/federation/InboxxCommonMockServerTest.feature @@ -110,7 +110,7 @@ Feature: InboxCommonMockServer } ], "endpoints": { - "sharedInbox": #(userUrl + 'inbox') + "sharedInbox": #(remoteUrl + '/inbox') }, "icon": { "type": "Image", From 2b3b2b48d387dc9986bcc179aeaf417adebc9108 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 15:52:01 +0900 Subject: [PATCH 23/33] =?UTF-8?q?test:=20FollowAcceptTest=E3=82=92?= =?UTF-8?q?=E7=84=A1=E5=8A=B9=E5=8C=96=20https://github.com/usbharu/Hideou?= =?UTF-8?q?t/issues/179#issuecomment-1837388337?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/e2eTest/kotlin/federation/FollowAcceptTest.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/e2eTest/kotlin/federation/FollowAcceptTest.kt b/src/e2eTest/kotlin/federation/FollowAcceptTest.kt index 2f7cdf4d..3c7cd02d 100644 --- a/src/e2eTest/kotlin/federation/FollowAcceptTest.kt +++ b/src/e2eTest/kotlin/federation/FollowAcceptTest.kt @@ -8,10 +8,7 @@ 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 -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.TestFactory +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 @@ -30,6 +27,7 @@ class FollowAcceptTest { @Karate.Test @TestFactory + @Disabled fun `FollowAcceptTest`(): Karate { return KarateUtil.e2eTest( "FollowAcceptTest", "Follow Accept Test", From b5872e5c765d4b7aea90d29e04c60af58cafb97a Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 16:00:05 +0900 Subject: [PATCH 24/33] =?UTF-8?q?chore:=20e2e=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=AE=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/pull-request-merge-check.yml | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-merge-check.yml b/.github/workflows/pull-request-merge-check.yml index 72f62868..8503a645 100644 --- a/.github/workflows/pull-request-merge-check.yml +++ b/.github/workflows/pull-request-merge-check.yml @@ -253,7 +253,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 @@ -330,3 +330,66 @@ 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: E2E Test + uses: gradle/gradle-build-action@v2.8.1 + with: + arguments: e2eTest + + - name: Save Test Report + uses: actions/cache/save@v3 + with: + path: build/test-results + key: e2e-test-report-${{ github.sha }} From 96196cf7ba0626ef951dd7da655671801845c143 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 16:14:17 +0900 Subject: [PATCH 25/33] =?UTF-8?q?chore:=20e2e=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=AE=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pull-request-merge-check.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-merge-check.yml b/.github/workflows/pull-request-merge-check.yml index 8503a645..0e6c9b10 100644 --- a/.github/workflows/pull-request-merge-check.yml +++ b/.github/workflows/pull-request-merge-check.yml @@ -234,7 +234,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() @@ -268,6 +268,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: From 96402acf1cd4c1a6fd69b852edc0a4ece138965c Mon Sep 17 00:00:00 2001 From: usbharu Date: Sun, 3 Dec 2023 16:15:55 +0900 Subject: [PATCH 26/33] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../hideout/activitypub/service/inbox/InboxJobProcessor.kt | 1 - .../dev/usbharu/hideout/application/config/SecurityConfig.kt | 2 -- .../core/infrastructure/httpsignature/HttpRequestMixIn.kt | 5 ----- 3 files changed, 8 deletions(-) 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 91c3f4ed..b707e5e6 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 @@ -81,7 +81,6 @@ class InboxJobProcessor( } override suspend fun process(param: InboxJobParam) { - val jsonNode = objectMapper.readTree(param.json) logger.info("START Process inbox. type: {}", param.type) 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 aa60fa16..c0f0bbff 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -63,7 +63,6 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* - @EnableWebSecurity(debug = false) @Configuration @Suppress("FunctionMaxLength", "TooManyFunctions") @@ -285,7 +284,6 @@ data class JwkConfig( val privateKey: String ) - @Configuration class PostSecurityConfig( val auth: AuthenticationManagerBuilder, 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 index 3686d3c9..4f998d91 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/httpsignature/HttpRequestMixIn.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/httpsignature/HttpRequestMixIn.kt @@ -11,23 +11,18 @@ 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()) ) } - } From 3676961a40bc63125f518f68792e15db52e5b5bf Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 16:22:06 +0900 Subject: [PATCH 27/33] =?UTF-8?q?chore:=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=83=AC=E3=83=9D=E3=83=BC=E3=83=88=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pull-request-merge-check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pull-request-merge-check.yml b/.github/workflows/pull-request-merge-check.yml index 0e6c9b10..4c8f5763 100644 --- a/.github/workflows/pull-request-merge-check.yml +++ b/.github/workflows/pull-request-merge-check.yml @@ -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 @@ -395,6 +397,7 @@ jobs: arguments: e2eTest - name: Save Test Report + if: always() uses: actions/cache/save@v3 with: path: build/test-results From 488659e767494e2f9e96a0f78e7cc2ab3fb10158 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 16:39:52 +0900 Subject: [PATCH 28/33] =?UTF-8?q?chore:=20e2e=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E7=94=A8=E3=81=ABChrome=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pull-request-merge-check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pull-request-merge-check.yml b/.github/workflows/pull-request-merge-check.yml index 4c8f5763..99d76f1d 100644 --- a/.github/workflows/pull-request-merge-check.yml +++ b/.github/workflows/pull-request-merge-check.yml @@ -391,6 +391,9 @@ jobs: with: mongodb-version: latest + - name: Setup Chrome + uses: browser-actions/setup-chrome@v1.4.0 + - name: E2E Test uses: gradle/gradle-build-action@v2.8.1 with: From 32bb44c8a1c721883adb63e149b5302cd40bb6b6 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 17:14:22 +0900 Subject: [PATCH 29/33] =?UTF-8?q?chore:=20e2e=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E7=94=A8=E3=81=ABChrome=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pull-request-merge-check.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-merge-check.yml b/.github/workflows/pull-request-merge-check.yml index 99d76f1d..0e5d0973 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 @@ -394,11 +394,15 @@ jobs: - name: 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 From 78622b5827794ed2f961a2d81d15ceeafb107a85 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 17:20:13 +0900 Subject: [PATCH 30/33] style: fix lint --- .../hideout/activitypub/domain/model/Person.kt | 14 ++++++++++++-- .../activitypub/service/inbox/InboxJobProcessor.kt | 1 + .../service/objects/user/APUserService.kt | 8 +++----- .../hideout/application/config/SecurityConfig.kt | 7 +++---- .../HttpSignatureUserDetailsService.kt | 9 +++------ .../core/service/resource/InMemoryCacheManager.kt | 1 + .../util/SpringSecurityKotlinDslExtension.kt | 5 ++--- 7 files changed, 25 insertions(+), 20 deletions(-) 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 4791fa68..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 @@ -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/service/inbox/InboxJobProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/inbox/InboxJobProcessor.kt index b707e5e6..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 @@ -56,6 +56,7 @@ class InboxJobProcessor( } } + @Suppress("TooGenericExceptionCaught") val verify = try { signatureVerifier.verify( httpRequest, 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 47568b8d..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 @@ -95,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"] 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 c0f0bbff..dead45f0 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -84,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) } @@ -160,7 +162,6 @@ class SecurityConfig { } oauth2ResourceServer { jwt { - } } } @@ -172,7 +173,6 @@ class SecurityConfig { fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { http { authorizeHttpRequests { - authorize("/error", permitAll) authorize("/login", permitAll) authorize(GET, "/.well-known/**", permitAll) @@ -200,7 +200,6 @@ class SecurityConfig { } formLogin { - } csrf { 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/resource/InMemoryCacheManager.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/resource/InMemoryCacheManager.kt index d5ca028d..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,6 +29,7 @@ class InMemoryCacheManager : CacheManager { } } if (needRunBlock) { + @Suppress("TooGenericExceptionCaught") val processed = try { block() } catch (e: Exception) { 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()) From 1fdb5895a70c65b89c6d9a463ccb2624b357f736 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 17:21:23 +0900 Subject: [PATCH 31/33] =?UTF-8?q?chore:=20=E3=82=B9=E3=83=86=E3=83=83?= =?UTF-8?q?=E3=83=97=E5=90=8D=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pull-request-merge-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-merge-check.yml b/.github/workflows/pull-request-merge-check.yml index 0e5d0973..1a1366a4 100644 --- a/.github/workflows/pull-request-merge-check.yml +++ b/.github/workflows/pull-request-merge-check.yml @@ -391,7 +391,7 @@ jobs: with: mongodb-version: latest - - name: Setup Chrome + - name: setup-chrome uses: browser-actions/setup-chrome@v1.4.0 - name: Add Path From bad5a2e268e9dfa39b9cef344d869e4d32756f32 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 3 Dec 2023 17:26:10 +0900 Subject: [PATCH 32/33] =?UTF-8?q?chore:=20=E3=82=B9=E3=83=86=E3=83=83?= =?UTF-8?q?=E3=83=97id=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pull-request-merge-check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull-request-merge-check.yml b/.github/workflows/pull-request-merge-check.yml index 1a1366a4..7d1a26f2 100644 --- a/.github/workflows/pull-request-merge-check.yml +++ b/.github/workflows/pull-request-merge-check.yml @@ -392,6 +392,7 @@ jobs: mongodb-version: latest - name: setup-chrome + id: setup-chrome uses: browser-actions/setup-chrome@v1.4.0 - name: Add Path From 3f5a573fb31e42a670f46d444fa404f7760a0623 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:33:55 +0900 Subject: [PATCH 33/33] =?UTF-8?q?test:=20headless=20chrome=E3=81=AE?= =?UTF-8?q?=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/e2eTest/resources/oauth2/Oauth2LoginTest.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature b/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature index f330c369..af203344 100644 --- a/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature +++ b/src/e2eTest/resources/oauth2/Oauth2LoginTest.feature @@ -2,7 +2,7 @@ Feature: OAuth2 Login Test Background: * url baseUrl - * configure driver = { type: 'chrome' } + * configure driver = { type: 'chrome',start: true, headless: true, showDriverLog: true, addOptions: [ '--headless=new' ] } Scenario: スコープwrite readを持ったトークンの作成