diff --git a/build.gradle.kts b/build.gradle.kts index ec03a89e..78197b53 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -128,6 +128,7 @@ dependencies { 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") diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApController.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApController.kt index 5e284d88..be127c44 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApController.kt @@ -2,15 +2,12 @@ package dev.usbharu.hideout.activitypub.interfaces.api.note import dev.usbharu.hideout.activitypub.domain.model.Note import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.CurrentSecurityContext -import org.springframework.security.core.context.SecurityContext import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable interface NoteApController { @GetMapping("/users/*/posts/{postId}") suspend fun postsAp( - @PathVariable("postId") postId: Long, - @CurrentSecurityContext context: SecurityContext + @PathVariable("postId") postId: Long ): ResponseEntity } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImpl.kt index d2c55e41..307cf16c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImpl.kt @@ -4,8 +4,7 @@ import dev.usbharu.hideout.activitypub.domain.model.Note import dev.usbharu.hideout.activitypub.service.objects.note.NoteApApiService import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUser import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.CurrentSecurityContext -import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RestController @@ -14,8 +13,8 @@ import org.springframework.web.bind.annotation.RestController class NoteApControllerImpl(private val noteApApiService: NoteApApiService) : NoteApController { override suspend fun postsAp( @PathVariable(value = "postId") postId: Long, - @CurrentSecurityContext context: SecurityContext ): ResponseEntity { + val context = SecurityContextHolder.getContext() val userId = if (context.authentication is PreAuthenticatedAuthenticationToken && context.authentication.details is HttpSignatureUser 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 new file mode 100644 index 00000000..9537be64 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImplTest.kt @@ -0,0 +1,129 @@ +package dev.usbharu.hideout.activitypub.interfaces.api.note + +import dev.usbharu.hideout.activitypub.domain.model.Note +import dev.usbharu.hideout.activitypub.service.objects.note.NoteApApiService +import dev.usbharu.hideout.application.config.ActivityPubConfig +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 kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +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) +class NoteApControllerImplTest { + + private lateinit var mockMvc: MockMvc + + @Mock + private lateinit var noteApApiService: NoteApApiService + + @InjectMocks + private lateinit var noteApControllerImpl: NoteApControllerImpl + + @BeforeEach + fun setUp() { + + mockMvc = MockMvcBuilders.standaloneSetup(noteApControllerImpl) + .apply( + springSecurity( + FilterChainProxy( + DefaultSecurityFilterChain( + AnyRequestMatcher.INSTANCE + ) + ) + ) + ) + .build() + } + + @Test + fun `postAP 匿名で取得できる`() = runTest { + + val note = Note( + name = "Note", + id = "https://example.com/users/hoge/posts/1234", + attributedTo = "https://example.com/users/hoge", + content = "Hello", + published = "2023-11-02T15:30:34.160Z" + ) + whenever(noteApApiService.getNote(eq(1234), isNull())).doReturn( + note + ) + + val objectMapper = ActivityPubConfig().objectMapper() + + mockMvc + .get("/users/hoge/posts/1234") { + with(anonymous()) + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(note)) } } + } + + @Test + fun `postAP 存在しない場合は404`() = runTest { + whenever(noteApApiService.getNote(eq(123), isNull())).doReturn(null) + + mockMvc + .get("/users/hoge/posts/123") { + with(anonymous()) + } + .asyncDispatch() + .andExpect { status { isNotFound() } } + } + + @Test + fun `postAP 認証に成功している場合userIdがnullでない`() = runTest { + val note = Note( + name = "Note", + id = "https://example.com/users/hoge/posts/1234", + attributedTo = "https://example.com/users/hoge", + content = "Hello", + published = "2023-11-02T15:30:34.160Z" + ) + whenever(noteApApiService.getNote(eq(1234), isNotNull())).doReturn(note) + + val objectMapper = ActivityPubConfig().objectMapper() + + val preAuthenticatedAuthenticationToken = PreAuthenticatedAuthenticationToken( + "", HttpRequest( + URL("https://follower.example.com"), + HttpHeaders( + mapOf() + ), HttpMethod.GET + ) + ).apply { details = HttpSignatureUser("fuga", "follower.example.com", 123, true, true, mutableListOf()) } + SecurityContextHolder.getContext().authentication = preAuthenticatedAuthenticationToken + + mockMvc.get("/users/hoge/posts/1234") { + with( + authentication( + preAuthenticatedAuthenticationToken + ) + ) + }.asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(note)) } } + } +}