diff --git a/build.gradle.kts b/build.gradle.kts index b72caa33..9e63dc58 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,6 +27,37 @@ apply { group = "dev.usbharu" version = "0.0.1" +sourceSets { + create("intTest") { + compileClasspath += sourceSets.main.get().output + runtimeClasspath += sourceSets.main.get().output + } +} + +val intTestImplementation by configurations.getting { + extendsFrom(configurations.implementation.get()) +} +val intTestRuntimeOnly by configurations.getting { + extendsFrom(configurations.runtimeOnly.get()) +} + +val integrationTest = task("integrationTest") { + description = "Runs integration tests." + group = "verification" + + testClassesDirs = sourceSets["intTest"].output.classesDirs + classpath = sourceSets["intTest"].runtimeClasspath + shouldRunAfter("test") + + useJUnitPlatform() + + testLogging { + events("passed") + } +} + +tasks.check { dependsOn(integrationTest) } + tasks.withType { useJUnitPlatform() val cpus = Runtime.getRuntime().availableProcessors() @@ -132,9 +163,7 @@ dependencies { compileOnly("io.swagger.core.v3:swagger-annotations:2.2.6") implementation("io.swagger.core.v3:swagger-models:2.2.6") implementation("org.jetbrains.exposed:exposed-java-time:$exposed_version") - testImplementation("org.springframework.boot:spring-boot-test-autoconfigure") testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.springframework.security:spring-security-test") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.security:spring-security-oauth2-jose") @@ -162,7 +191,6 @@ dependencies { implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") testImplementation("io.ktor:ktor-client-mock:$ktor_version") - testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") testImplementation("org.mockito:mockito-inline:5.2.0") testImplementation("nl.jqno.equalsverifier:equalsverifier:3.15.3") @@ -172,6 +200,10 @@ dependencies { implementation("org.drewcarlson:kjob-mongo:0.6.0") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.1") + + intTestImplementation("org.springframework.boot:spring-boot-starter-test") + intTestImplementation("org.springframework.security:spring-security-test") + } detekt { @@ -220,7 +252,7 @@ project.gradle.taskGraph.whenReady { kover { -excludeSourceSets { + excludeSourceSets { names("aot") } } diff --git a/src/intTest/kotlin/activitypub/inbox/InboxTest.kt b/src/intTest/kotlin/activitypub/inbox/InboxTest.kt new file mode 100644 index 00000000..92fe3aa2 --- /dev/null +++ b/src/intTest/kotlin/activitypub/inbox/InboxTest.kt @@ -0,0 +1,91 @@ +package activitypub.inbox + +import dev.usbharu.hideout.SpringApplication +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.http.MediaType +import org.springframework.security.test.context.support.WithAnonymousUser +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.context.WebApplicationContext +import util.TestTransaction +import util.WithMockHttpSignature + +@SpringBootTest(classes = [SpringApplication::class]) +@AutoConfigureMockMvc +@Transactional +class InboxTest { + + @Autowired + private lateinit var context: WebApplicationContext + + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(springSecurity()) + .build() + } + + @Test + @WithAnonymousUser + fun `匿名でinboxにPOSTしたら401`() { + mockMvc + .post("/inbox") { + content = "{}" + contentType = MediaType.APPLICATION_JSON + } + .andExpect { status { isUnauthorized() } } + } + + @Test + @WithMockHttpSignature + fun 有効なHttpSignatureでPOSTしたら202() { + mockMvc + .post("/inbox") { + content = "{}" + contentType = MediaType.APPLICATION_JSON + } + .asyncDispatch() + .andExpect { status { isAccepted() } } + } + + @Test + @WithAnonymousUser + fun `匿名でuser-inboxにPOSTしたら401`() { + mockMvc + .post("/users/hoge/inbox") { + content = "{}" + contentType = MediaType.APPLICATION_JSON + } + .andExpect { status { isUnauthorized() } } + } + + @Test + @WithMockHttpSignature + fun 有効なHttpSignaturesでPOSTしたら202() { + mockMvc + .post("/users/hoge/inbox") { + content = "{}" + contentType = MediaType.APPLICATION_JSON + } + .asyncDispatch() + .andExpect { status { isAccepted() } } + } + + @TestConfiguration + class Configuration { + @Bean + fun testTransaction() = TestTransaction + } +} diff --git a/src/intTest/kotlin/activitypub/note/NoteTest.kt b/src/intTest/kotlin/activitypub/note/NoteTest.kt new file mode 100644 index 00000000..a559ca66 --- /dev/null +++ b/src/intTest/kotlin/activitypub/note/NoteTest.kt @@ -0,0 +1,181 @@ +package activitypub.note + +import dev.usbharu.hideout.SpringApplication +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType +import org.springframework.security.test.context.support.WithAnonymousUser +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity +import org.springframework.test.context.jdbc.Sql +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.context.WebApplicationContext +import util.WithHttpSignature +import util.WithMockHttpSignature + +@SpringBootTest(classes = [SpringApplication::class]) +@AutoConfigureMockMvc +@Transactional +class NoteTest { + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var context: WebApplicationContext + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build() + } + + @Test + @WithAnonymousUser + @Sql("/sql/note/匿名でpublic投稿を取得できる.sql") + fun `匿名でpublic投稿を取得できる`() { + mockMvc + .get("/users/test-user/posts/1234") { + accept(MediaType("application", "activity+json")) + } + .asyncDispatch() + .andDo { print() } + .andExpect { status { isOk() } } + .andExpect { content { contentType("application/activity+json") } } + .andExpect { jsonPath("\$.type") { value("Note") } } + .andExpect { jsonPath("\$.to") { value("https://www.w3.org/ns/activitystreams#Public") } } + .andExpect { jsonPath("\$.cc") { value("https://www.w3.org/ns/activitystreams#Public") } } + } + + @Test + @Sql("/sql/note/匿名でunlisted投稿を取得できる.sql") + @WithAnonymousUser + fun 匿名でunlisted投稿を取得できる() { + mockMvc + .get("/users/test-user2/posts/1235") { + accept(MediaType("application", "activity+json")) + } + .asyncDispatch() + .andDo { print() } + .andExpect { status { isOk() } } + .andExpect { content { contentType("application/activity+json") } } + .andExpect { jsonPath("\$.type") { value("Note") } } + .andExpect { jsonPath("\$.to") { value("https://example.com/users/test-user2/followers") } } + .andExpect { jsonPath("\$.cc") { value("https://www.w3.org/ns/activitystreams#Public") } } + } + + @Test + @Transactional + @WithAnonymousUser + @Sql("/sql/note/匿名でfollowers投稿を取得しようとすると404.sql") + fun 匿名でfollowers投稿を取得しようとすると404() { + mockMvc + .get("/users/test-user2/posts/1236") { + accept(MediaType("application", "activity+json")) + } + .asyncDispatch() + .andExpect { status { isNotFound() } } + } + + @Test + @WithAnonymousUser + fun 匿名でdirect投稿を取得しようとすると404() { + mockMvc + .get("/users/test-user2/posts/1236") { + accept(MediaType("application", "activity+json")) + } + .asyncDispatch() + .andExpect { status { isNotFound() } } + } + + @Test + @Sql("/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql") + @WithHttpSignature(keyId = "https://follower.example.com/users/test-user5#pubkey") + fun HttpSignature認証でフォロワーがpublic投稿を取得できる() { + mockMvc + .get("/users/test-user4/posts/1237") { + accept(MediaType("application", "activity+json")) + } + .asyncDispatch() + .andDo { print() } + .andExpect { status { isOk() } } + .andExpect { content { contentType("application/activity+json") } } + .andExpect { jsonPath("\$.type") { value("Note") } } + .andExpect { jsonPath("\$.to") { value("https://www.w3.org/ns/activitystreams#Public") } } + .andExpect { jsonPath("\$.cc") { value("https://www.w3.org/ns/activitystreams#Public") } } + } + + @Test + @Sql("/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql") + @WithHttpSignature(keyId = "https://follower.example.com/users/test-user7#pubkey") + fun httpSignature認証でフォロワーがunlisted投稿を取得できる() { + mockMvc + .get("/users/test-user6/posts/1238") { + accept(MediaType("application", "activity+json")) + } + .asyncDispatch() + .andDo { print() } + .andExpect { status { isOk() } } + .andExpect { content { contentType("application/activity+json") } } + .andExpect { jsonPath("\$.type") { value("Note") } } + .andExpect { jsonPath("\$.to") { value("https://example.com/users/test-user6/followers") } } + .andExpect { jsonPath("\$.cc") { value("https://www.w3.org/ns/activitystreams#Public") } } + } + + @Test + @Sql("/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql") + @WithHttpSignature(keyId = "https://follower.example.com/users/test-user9#pubkey") + fun httpSignature認証でフォロワーがfollowers投稿を取得できる() { + mockMvc + .get("/users/test-user8/posts/1239") { + accept(MediaType("application", "activity+json")) + } + .asyncDispatch() + .andDo { print() } + .andExpect { status { isOk() } } + .andExpect { content { contentType("application/activity+json") } } + .andExpect { jsonPath("\$.type") { value("Note") } } + .andExpect { jsonPath("\$.to") { value("https://example.com/users/test-user8/followers") } } + .andExpect { jsonPath("\$.cc") { value("https://example.com/users/test-user8/followers") } } + + } + + @Test + @Sql("/sql/note/リプライになっている投稿はinReplyToが存在する.sql") + @WithMockHttpSignature + fun リプライになっている投稿はinReplyToが存在する() { + mockMvc + .get("/users/test-user10/posts/1241") { + accept(MediaType("application", "activity+json")) + } + .asyncDispatch() + .andDo { print() } + .andExpect { status { isOk() } } + .andExpect { content { contentType("application/activity+json") } } + .andExpect { jsonPath("\$.type") { value("Note") } } + .andExpect { jsonPath("\$.inReplyTo") { value("https://example.com/users/test-user10/posts/1240") } } + } + + @Test + @Sql("/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql") + @WithMockHttpSignature + fun メディア付き投稿はattachmentにDocumentとして画像が存在する() { + mockMvc + .get("/users/test-user10/posts/1242") { + accept(MediaType("application", "activity+json")) + } + .asyncDispatch() + .andDo { print() } + .andExpect { status { isOk() } } + .andExpect { content { contentType("application/activity+json") } } + .andExpect { jsonPath("\$.type") { value("Note") } } + .andExpect { jsonPath("\$.attachment") { isArray() } } + .andExpect { jsonPath("\$.attachment[0].type") { value("Document") } } + .andExpect { jsonPath("\$.attachment[0].url") { value("https://example.com/media/test-media.png") } } + .andExpect { jsonPath("\$.attachment[1].type") { value("Document") } } + .andExpect { jsonPath("\$.attachment[1].url") { value("https://example.com/media/test-media2.png") } } + } +} diff --git a/src/intTest/kotlin/activitypub/webfinger/WebFingerTest.kt b/src/intTest/kotlin/activitypub/webfinger/WebFingerTest.kt new file mode 100644 index 00000000..d0faaa7f --- /dev/null +++ b/src/intTest/kotlin/activitypub/webfinger/WebFingerTest.kt @@ -0,0 +1,83 @@ +package activitypub.webfinger + +import dev.usbharu.hideout.SpringApplication +import dev.usbharu.hideout.application.external.Transaction +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.test.context.jdbc.Sql +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.transaction.annotation.Transactional +import util.TestTransaction +import java.net.URL + +@SpringBootTest(classes = [SpringApplication::class]) +@AutoConfigureMockMvc +@Transactional +class WebFingerTest { + @Autowired + private lateinit var mockMvc: MockMvc + + @Test + @Sql("/sql/test-user.sql") + fun `webfinger 存在するユーザーを取得`() { + mockMvc + .get("/.well-known/webfinger?resource=acct:test-user@example.com") + .andExpect { status { isOk() } } + .andExpect { header { string("Content-Type", "application/json") } } + .andExpect { + jsonPath("\$.subject") { + value("acct:test-user@example.com") + } + } + .andExpect { + jsonPath("\$.links[0].rel") { + value("self") + } + } + .andExpect { + jsonPath("\$.links[0].href") { value("https://example.com/users/test-user") } + } + .andExpect { + jsonPath("\$.links[0].type") { + value("application/activity+json") + } + } + } + + @Test + fun `webfinger 存在しないユーザーに404`() { + mockMvc + .get("/.well-known/webfinger?resource=acct:test-user@example.com") + .andExpect { status { isNotFound() } } + } + + @Test + fun `webfinger 不正なリクエストは400`() { + mockMvc + .get("/.well-known/webfinger?res=acct:test") + .andExpect { status { isBadRequest() } } + } + + @Test + fun `webfinger acctのパースが出来なくても400`() { + mockMvc + .get("/.well-known/webfinger?resource=acct:@a@b@c@d") + .andExpect { status { isBadRequest() } } + } + + @TestConfiguration + class Configuration { + @Bean + fun url(): URL { + return URL("https://example.com") + } + + @Bean + fun testTransaction(): Transaction = TestTransaction + } +} diff --git a/src/intTest/kotlin/util/SpringApplicationTestBase.kt b/src/intTest/kotlin/util/SpringApplicationTestBase.kt new file mode 100644 index 00000000..f1f686b2 --- /dev/null +++ b/src/intTest/kotlin/util/SpringApplicationTestBase.kt @@ -0,0 +1,6 @@ +package util + +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +abstract class SpringApplicationTestBase diff --git a/src/intTest/kotlin/util/TestTransaction.kt b/src/intTest/kotlin/util/TestTransaction.kt new file mode 100644 index 00000000..2dc53d5e --- /dev/null +++ b/src/intTest/kotlin/util/TestTransaction.kt @@ -0,0 +1,9 @@ +package util + +import dev.usbharu.hideout.application.external.Transaction + +object TestTransaction : Transaction { + override suspend fun transaction(block: suspend () -> T): T = block() + + override suspend fun transaction(transactionLevel: Int, block: suspend () -> T): T = block() +} diff --git a/src/intTest/kotlin/util/WithHttpSignature.kt b/src/intTest/kotlin/util/WithHttpSignature.kt new file mode 100644 index 00000000..ded96f4e --- /dev/null +++ b/src/intTest/kotlin/util/WithHttpSignature.kt @@ -0,0 +1,20 @@ +package util + +import org.springframework.core.annotation.AliasFor +import org.springframework.security.test.context.support.TestExecutionEvent +import org.springframework.security.test.context.support.WithSecurityContext +import java.lang.annotation.Inherited + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@Inherited +@MustBeDocumented +@WithSecurityContext(factory = WithHttpSignatureSecurityContextFactory::class) +annotation class WithHttpSignature( + @get:AliasFor( + annotation = WithSecurityContext::class + ) val setupBefore: TestExecutionEvent = TestExecutionEvent.TEST_METHOD, + val keyId: String = "https://example.com/users/test-user#pubkey", + val url: String = "https://example.com/inbox", + val method: String = "GET" +) diff --git a/src/intTest/kotlin/util/WithHttpSignatureSecurityContextFactory.kt b/src/intTest/kotlin/util/WithHttpSignatureSecurityContextFactory.kt new file mode 100644 index 00000000..9108e357 --- /dev/null +++ b/src/intTest/kotlin/util/WithHttpSignatureSecurityContextFactory.kt @@ -0,0 +1,48 @@ +package util + +import dev.usbharu.hideout.application.external.Transaction +import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUser +import dev.usbharu.hideout.core.query.UserQueryService +import dev.usbharu.httpsignature.common.HttpHeaders +import dev.usbharu.httpsignature.common.HttpMethod +import dev.usbharu.httpsignature.common.HttpRequest +import kotlinx.coroutines.runBlocking +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.test.context.support.WithSecurityContextFactory +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken +import java.net.URL + +class WithHttpSignatureSecurityContextFactory( + private val userQueryService: UserQueryService, + private val transaction: Transaction +) : WithSecurityContextFactory { + + private val securityContextStrategy = SecurityContextHolder.getContextHolderStrategy() + + override fun createSecurityContext(annotation: WithHttpSignature): SecurityContext = runBlocking { + val preAuthenticatedAuthenticationToken = PreAuthenticatedAuthenticationToken( + annotation.keyId, HttpRequest( + URL("https://example.com/inbox"), + HttpHeaders(mapOf()), HttpMethod.GET + ) + ) + val httpSignatureUser = transaction.transaction { + val findByKeyId = userQueryService.findByKeyId(annotation.keyId) + HttpSignatureUser( + findByKeyId.name, + findByKeyId.domain, + findByKeyId.id, + true, + true, + mutableListOf() + ) + } + preAuthenticatedAuthenticationToken.details = httpSignatureUser + preAuthenticatedAuthenticationToken.isAuthenticated = true + val emptyContext = securityContextStrategy.createEmptyContext() + emptyContext.authentication = preAuthenticatedAuthenticationToken + return@runBlocking emptyContext + } + +} diff --git a/src/intTest/kotlin/util/WithMockHttpSignature.kt b/src/intTest/kotlin/util/WithMockHttpSignature.kt new file mode 100644 index 00000000..e5796f6c --- /dev/null +++ b/src/intTest/kotlin/util/WithMockHttpSignature.kt @@ -0,0 +1,23 @@ +package util + +import org.springframework.core.annotation.AliasFor +import org.springframework.security.test.context.support.TestExecutionEvent +import org.springframework.security.test.context.support.WithSecurityContext +import java.lang.annotation.Inherited + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@Inherited +@MustBeDocumented +@WithSecurityContext(factory = WithMockHttpSignatureSecurityContextFactory::class) +annotation class WithMockHttpSignature( + @get:AliasFor( + annotation = WithSecurityContext::class + ) val setupBefore: TestExecutionEvent = TestExecutionEvent.TEST_METHOD, + val username: String = "test-user", + val domain: String = "example.com", + val keyId: String = "https://example.com/users/test-user#pubkey", + val id: Long = 1234L, + val url: String = "https://example.com/inbox", + val method: String = "GET" +) diff --git a/src/intTest/kotlin/util/WithMockHttpSignatureSecurityContextFactory.kt b/src/intTest/kotlin/util/WithMockHttpSignatureSecurityContextFactory.kt new file mode 100644 index 00000000..29dda972 --- /dev/null +++ b/src/intTest/kotlin/util/WithMockHttpSignatureSecurityContextFactory.kt @@ -0,0 +1,39 @@ +package util + +import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUser +import dev.usbharu.httpsignature.common.HttpHeaders +import dev.usbharu.httpsignature.common.HttpMethod +import dev.usbharu.httpsignature.common.HttpRequest +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.test.context.support.WithSecurityContextFactory +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken +import java.net.URL + +class WithMockHttpSignatureSecurityContextFactory : + WithSecurityContextFactory { + + private val securityContextStrategy = SecurityContextHolder.getContextHolderStrategy() + + override fun createSecurityContext(annotation: WithMockHttpSignature): SecurityContext { + val preAuthenticatedAuthenticationToken = PreAuthenticatedAuthenticationToken( + annotation.keyId, HttpRequest( + URL(annotation.url), + HttpHeaders(mapOf()), HttpMethod.valueOf(annotation.method.uppercase()) + ) + ) + val httpSignatureUser = HttpSignatureUser( + annotation.username, + annotation.domain, + annotation.id, + true, + true, + mutableListOf() + ) + preAuthenticatedAuthenticationToken.details = httpSignatureUser + preAuthenticatedAuthenticationToken.isAuthenticated = true + val emptyContext = securityContextStrategy.createEmptyContext() + emptyContext.authentication = preAuthenticatedAuthenticationToken + return emptyContext + } +} diff --git a/src/intTest/resources/application.yml b/src/intTest/resources/application.yml new file mode 100644 index 00000000..9760e828 --- /dev/null +++ b/src/intTest/resources/application.yml @@ -0,0 +1,39 @@ +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: + datasource: + driver-class-name: org.h2.Driver + url: "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1" + 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/intTest/resources/logback.xml b/src/intTest/resources/logback.xml new file mode 100644 index 00000000..54cfd39a --- /dev/null +++ b/src/intTest/resources/logback.xml @@ -0,0 +1,10 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{x-request-id}] %logger{36} - %msg%n + + + + + + diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql new file mode 100644 index 00000000..f1a91192 --- /dev/null +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがfollowers投稿を取得できる.sql @@ -0,0 +1,29 @@ +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (8, 'test-user8', 'example.com', 'Im test-user8.', 'THis account is test-user8.', + '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', + 'https://example.com/users/test-user8/inbox', + 'https://example.com/users/test-user8/outbox', 'https://example.com/users/test-user8', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user8#pubkey', 'https://example.com/users/test-user8/following', + 'https://example.com/users/test-user8/followers'); + +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (9, 'test-user9', 'follower.example.com', 'Im test-user9.', 'THis account is test-user9.', + null, + 'https://follower.example.com/users/test-user9/inbox', + 'https://follower.example.com/users/test-user9/outbox', 'https://follower.example.com/users/test-user9', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + null, 12345678, + 'https://follower.example.com/users/test-user9#pubkey', + 'https://follower.example.com/users/test-user9/following', + 'https://follower.example.com/users/test-user9/followers'); + +insert into USERS_FOLLOWERS (USER_ID, FOLLOWER_ID) +VALUES (8, 9); + +insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) +VALUES (1239, 8, null, 'test post', 12345680, 2, 'https://example.com/users/test-user8/posts/1239', null, null, false, + 'https://example.com/users/test-user8/posts/1239'); diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql new file mode 100644 index 00000000..20a7e5ba --- /dev/null +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがpublic投稿を取得できる.sql @@ -0,0 +1,29 @@ +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (4, 'test-user4', 'example.com', 'Im test user4.', 'THis account is test user4.', + '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', + 'https://example.com/users/test-user4/inbox', + 'https://example.com/users/test-user4/outbox', 'https://example.com/users/test-user4', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user4#pubkey', 'https://example.com/users/test-user4/following', + 'https://example.com/users/test-user4/followers'); + +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (5, 'test-user5', 'follower.example.com', 'Im test user5.', 'THis account is test user5.', + null, + 'https://follower.example.com/users/test-user5/inbox', + 'https://follower.example.com/users/test-user5/outbox', 'https://follower.example.com/users/test-user5', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + null, 12345678, + 'https://follower.example.com/users/test-user5#pubkey', + 'https://follower.example.com/users/test-user5/following', + 'https://follower.example.com/users/test-user5/followers'); + +insert into USERS_FOLLOWERS (USER_ID, FOLLOWER_ID) +VALUES (4, 5); + +insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) +VALUES (1237, 4, null, 'test post', 12345680, 0, 'https://example.com/users/test-user4/posts/1237', null, null, false, + 'https://example.com/users/test-user4/posts/1237'); diff --git a/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql new file mode 100644 index 00000000..cb7707a9 --- /dev/null +++ b/src/intTest/resources/sql/note/httpSignature認証でフォロワーがunlisted投稿を取得できる.sql @@ -0,0 +1,29 @@ +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (6, 'test-user6', 'example.com', 'Im test-user6.', 'THis account is test-user6.', + '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', + 'https://example.com/users/test-user6/inbox', + 'https://example.com/users/test-user6/outbox', 'https://example.com/users/test-user6', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user6#pubkey', 'https://example.com/users/test-user6/following', + 'https://example.com/users/test-user6/followers'); + +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (7, 'test-user7', 'follower.example.com', 'Im test-user7.', 'THis account is test-user7.', + null, + 'https://follower.example.com/users/test-user7/inbox', + 'https://follower.example.com/users/test-user7/outbox', 'https://follower.example.com/users/test-user7', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + null, 12345678, + 'https://follower.example.com/users/test-user7#pubkey', + 'https://follower.example.com/users/test-user7/following', + 'https://follower.example.com/users/test-user7/followers'); + +insert into USERS_FOLLOWERS (USER_ID, FOLLOWER_ID) +VALUES (6, 7); + +insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) +VALUES (1238, 6, null, 'test post', 12345680, 1, 'https://example.com/users/test-user6/posts/1238', null, null, false, + 'https://example.com/users/test-user6/posts/1238'); diff --git a/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql b/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql new file mode 100644 index 00000000..9da287af --- /dev/null +++ b/src/intTest/resources/sql/note/メディア付き投稿はattachmentにDocumentとして画像が存在する.sql @@ -0,0 +1,22 @@ +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (11, 'test-user11', 'example.com', 'Im test-user11.', 'THis account is test-user11.', + '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', + 'https://example.com/users/test-user11/inbox', + 'https://example.com/users/test-user11/outbox', 'https://example.com/users/test-user11', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user11#pubkey', 'https://example.com/users/test-user11/following', + 'https://example.com/users/test-user11/followers'); + +insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) +VALUES (1242, 11, null, 'test post', 12345680, 0, 'https://example.com/users/test-user11/posts/1242', null, null, false, + 'https://example.com/users/test-user11/posts/1242'); + +insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH) +VALUES (1, 'test-media', 'https://example.com/media/test-media.png', null, null, 0, null), + (2, 'test-media2', 'https://example.com/media/test-media2.png', null, null, 0, null); + +insert into POSTSMEDIA(POST_ID, MEDIA_ID) +VALUES (1242, 1), + (1242, 2); diff --git a/src/intTest/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql b/src/intTest/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql new file mode 100644 index 00000000..cf6f842f --- /dev/null +++ b/src/intTest/resources/sql/note/リプライになっている投稿はinReplyToが存在する.sql @@ -0,0 +1,16 @@ +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (10, 'test-user10', 'example.com', 'Im test-user10.', 'THis account is test-user10.', + '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', + 'https://example.com/users/test-user10/inbox', + 'https://example.com/users/test-user10/outbox', 'https://example.com/users/test-user10', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user10#pubkey', 'https://example.com/users/test-user10/following', + 'https://example.com/users/test-user10/followers'); + +insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) +VALUES (1240, 10, null, 'test post', 12345680, 0, 'https://example.com/users/test-user10/posts/1240', null, null, false, + 'https://example.com/users/test-user10/posts/1240'), + (1241, 10, null, 'test post', 12345680, 0, 'https://example.com/users/test-user10/posts/1241', null, 1240, false, + 'https://example.com/users/test-user10/posts/1241'); diff --git a/src/intTest/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql b/src/intTest/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql new file mode 100644 index 00000000..71ee8f8d --- /dev/null +++ b/src/intTest/resources/sql/note/匿名でfollowers投稿を取得しようとすると404.sql @@ -0,0 +1,14 @@ +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (3, 'test-user3', 'example.com', 'Im test user3.', 'THis account is test user3.', + '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', + 'https://example.com/users/test-user3/inbox', + 'https://example.com/users/test-user3/outbox', 'https://example.com/users/test-user3', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user3#pubkey', 'https://example.com/users/test-user3/following', + 'https://example.com/users/test-user3/followers'); + +insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) +VALUES (1236, 3, null, 'test post', 12345680, 2, 'https://example.com/users/test-user3/posts/1236', null, null, false, + 'https://example.com/users/test-user3/posts/1236') diff --git a/src/intTest/resources/sql/note/匿名でpublic投稿を取得できる.sql b/src/intTest/resources/sql/note/匿名でpublic投稿を取得できる.sql new file mode 100644 index 00000000..23f38afc --- /dev/null +++ b/src/intTest/resources/sql/note/匿名でpublic投稿を取得できる.sql @@ -0,0 +1,13 @@ +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test user.', + '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'https://example.com/users/test-user/inbox', + 'https://example.com/users/test-user/outbox', 'https://example.com/users/test-user', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following', + 'https://example.com/users/test-users/followers'); + +insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) +VALUES (1234, 1, null, 'test post', 12345680, 0, 'https://example.com/users/test-user/posts/1234', null, null, false, + 'https://example.com/users/test-user/posts/1234') diff --git a/src/intTest/resources/sql/note/匿名でunlisted投稿を取得できる.sql b/src/intTest/resources/sql/note/匿名でunlisted投稿を取得できる.sql new file mode 100644 index 00000000..88c8bf9a --- /dev/null +++ b/src/intTest/resources/sql/note/匿名でunlisted投稿を取得できる.sql @@ -0,0 +1,14 @@ +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (2, 'test-user2', 'example.com', 'Im test user2.', 'THis account is test user2.', + '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', + 'https://example.com/users/test-user2/inbox', + 'https://example.com/users/test-user2/outbox', 'https://example.com/users/test-user2', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following', + 'https://example.com/users/test-user2/followers'); + +insert into POSTS (ID, "userId", OVERVIEW, TEXT, "createdAt", VISIBILITY, URL, "repostId", "replyId", SENSITIVE, AP_ID) +VALUES (1235, 2, null, 'test post', 12345680, 1, 'https://example.com/users/test-user2/posts/1235', null, null, false, + 'https://example.com/users/test-user2/posts/1235') diff --git a/src/intTest/resources/sql/test-user.sql b/src/intTest/resources/sql/test-user.sql new file mode 100644 index 00000000..8b6df0d4 --- /dev/null +++ b/src/intTest/resources/sql/test-user.sql @@ -0,0 +1,9 @@ +insert into "USERS" (ID, NAME, DOMAIN, SCREEN_NAME, DESCRIPTION, PASSWORD, INBOX, OUTBOX, URL, PUBLIC_KEY, PRIVATE_KEY, + CREATED_AT, KEY_ID, FOLLOWING, FOLLOWERS) +VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test user.', + '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'https://example.com/users/test-user/inbox', + 'https://example.com/users/test-user/outbox', 'https://example.com/users/test-user', + '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', + '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----', 12345678, + 'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following', + 'https://example.com/users/test-users/followers'); diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt index da4e2def..19b88de3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt @@ -48,7 +48,6 @@ open class JsonLd { class ContextDeserializer : JsonDeserializer() { - override fun deserialize( p0: com.fasterxml.jackson.core.JsonParser?, p1: com.fasterxml.jackson.databind.DeserializationContext? @@ -72,7 +71,6 @@ class ContextSerializer : JsonSerializer>() { } override fun serialize(value: List?, gen: JsonGenerator?, serializers: SerializerProvider) { - if (value.isNullOrEmpty()) { serializers.defaultSerializeNull(gen) return diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt index f3befd0f..62c32928 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt @@ -24,7 +24,6 @@ open class Object : JsonLd { this.id = id } - override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Object) return false diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/infrastructure/exposedquery/NoteQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/infrastructure/exposedquery/NoteQueryServiceImpl.kt index 7e1ff9d2..fd516349 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/infrastructure/exposedquery/NoteQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/infrastructure/exposedquery/NoteQueryServiceImpl.kt @@ -14,6 +14,7 @@ import dev.usbharu.hideout.util.singleOr import org.jetbrains.exposed.sql.Query import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.select +import org.slf4j.LoggerFactory import org.springframework.stereotype.Repository import java.time.Instant @@ -47,7 +48,12 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v private suspend fun ResultRow.toNote(mediaList: List): Note { val replyId = this[Posts.replyId] val replyTo = if (replyId != null) { - postRepository.findById(replyId).url + try { + postRepository.findById(replyId).url + } catch (e: FailedToGetResourcesException) { + logger.warn("Failed to get replyId: $replyId", e) + null + } } else { null } @@ -86,4 +92,8 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v Visibility.DIRECT -> TODO() } } + + companion object { + private val logger = LoggerFactory.getLogger(NoteQueryServiceImpl::class.java) + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/NoteApApiServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/NoteApApiServiceImpl.kt index 84485ba3..547befbf 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/NoteApApiServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/NoteApApiServiceImpl.kt @@ -3,8 +3,10 @@ package dev.usbharu.hideout.activitypub.service.objects.note import dev.usbharu.hideout.activitypub.domain.model.Note import dev.usbharu.hideout.activitypub.query.NoteQueryService import dev.usbharu.hideout.application.external.Transaction +import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException import dev.usbharu.hideout.core.domain.model.post.Visibility import dev.usbharu.hideout.core.query.FollowerQueryService +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @Service @@ -14,7 +16,12 @@ class NoteApApiServiceImpl( private val transaction: Transaction ) : NoteApApiService { override suspend fun getNote(postId: Long, userId: Long?): Note? = transaction.transaction { - val findById = noteQueryService.findById(postId) + val findById = try { + noteQueryService.findById(postId) + } catch (e: FailedToGetResourcesException) { + logger.warn("Note not found.", e) + return@transaction null + } when (findById.second.visibility) { Visibility.PUBLIC, Visibility.UNLISTED -> { return@transaction findById.first @@ -34,4 +41,8 @@ class NoteApApiServiceImpl( Visibility.DIRECT -> return@transaction null } } + + companion object { + private val logger = LoggerFactory.getLogger(NoteApApiServiceImpl::class.java) + } } 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 2ba41dba..eb76234f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -59,7 +59,7 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* -@EnableWebSecurity(debug = false) +@EnableWebSecurity(debug = true) @Configuration @Suppress("FunctionMaxLength", "TooManyFunctions") class SecurityConfig { @@ -82,6 +82,7 @@ class SecurityConfig { HttpSignatureFilter::class.java ) .authorizeHttpRequests { + it.requestMatchers("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox").authenticated() it.anyRequest().permitAll() } .csrf { @@ -110,7 +111,6 @@ class SecurityConfig { AuthenticationEntryPointFailureHandler(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) authenticationEntryPointFailureHandler.setRethrowAuthenticationServiceException(false) httpSignatureFilter.setAuthenticationFailureHandler(authenticationEntryPointFailureHandler) - return httpSignatureFilter } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt index 39ddff21..c219f52c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt @@ -5,6 +5,7 @@ import dev.usbharu.hideout.application.service.id.IdGenerateService import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException import dev.usbharu.hideout.core.domain.model.post.Post import dev.usbharu.hideout.core.domain.model.post.PostRepository +import dev.usbharu.hideout.util.singleOr import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.springframework.stereotype.Repository @@ -67,11 +68,11 @@ class PostRepositoryImpl( } override suspend fun findById(id: Long): Post = - Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { postId }) + Posts.leftJoin(PostsMedia) .select { Posts.id eq id } .let(postQueryMapper::map) - .singleOrNull() - ?: throw FailedToGetResourcesException("id: $id was not found.") + .singleOr { FailedToGetResourcesException("id: $id was not found.", it) } + override suspend fun delete(id: Long) { Posts.deleteWhere { Posts.id eq id } diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImplTest.kt index 9537be64..c337e770 100644 --- a/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImplTest.kt @@ -16,17 +16,10 @@ import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.* import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous -import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication -import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity -import org.springframework.security.web.DefaultSecurityFilterChain -import org.springframework.security.web.FilterChainProxy import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken -import org.springframework.security.web.util.matcher.AnyRequestMatcher import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.setup.MockMvcBuilders -import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder import java.net.URL @ExtendWith(MockitoExtension::class) @@ -44,21 +37,21 @@ class NoteApControllerImplTest { fun setUp() { mockMvc = MockMvcBuilders.standaloneSetup(noteApControllerImpl) - .apply( - springSecurity( - FilterChainProxy( - DefaultSecurityFilterChain( - AnyRequestMatcher.INSTANCE - ) - ) - ) - ) +// .apply( +// springSecurity( +// FilterChainProxy( +// DefaultSecurityFilterChain( +// AnyRequestMatcher.INSTANCE +// ) +// ) +// ) +// ) .build() } @Test fun `postAP 匿名で取得できる`() = runTest { - + SecurityContextHolder.clearContext() val note = Note( name = "Note", id = "https://example.com/users/hoge/posts/1234", @@ -74,7 +67,7 @@ class NoteApControllerImplTest { mockMvc .get("/users/hoge/posts/1234") { - with(anonymous()) +// with(anonymous()) } .asyncDispatch() .andExpect { status { isOk() } } @@ -83,11 +76,12 @@ class NoteApControllerImplTest { @Test fun `postAP 存在しない場合は404`() = runTest { + SecurityContextHolder.clearContext() whenever(noteApApiService.getNote(eq(123), isNull())).doReturn(null) mockMvc .get("/users/hoge/posts/123") { - with(anonymous()) +// with(anonymous()) } .asyncDispatch() .andExpect { status { isNotFound() } } @@ -117,11 +111,11 @@ class NoteApControllerImplTest { SecurityContextHolder.getContext().authentication = preAuthenticatedAuthenticationToken mockMvc.get("/users/hoge/posts/1234") { - with( - authentication( - preAuthenticatedAuthenticationToken - ) - ) +// with( +// authentication( +// preAuthenticatedAuthenticationToken +// ) +// ) }.asyncDispatch() .andExpect { status { isOk() } } .andExpect { content { json(objectMapper.writeValueAsString(note)) } }