From 659dee87a0be2340c168bec5d35f89780c5b5348 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 30 Oct 2023 11:57:18 +0900 Subject: [PATCH] =?UTF-8?q?test:=20APNoteServiceImpl=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 --- .../service/ap/APNoteServiceImplTest.kt | 365 +++++++++++++++--- 1 file changed, 320 insertions(+), 45 deletions(-) diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt index 51afa187..1c8bf1f6 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt @@ -1,34 +1,58 @@ -@file:OptIn(ExperimentalCoroutinesApi::class) -@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +@file:OptIn(ExperimentalCoroutinesApi::class) @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") package dev.usbharu.hideout.service.ap import dev.usbharu.hideout.config.ApplicationConfig import dev.usbharu.hideout.config.CharacterLimit +import dev.usbharu.hideout.domain.model.ap.Image +import dev.usbharu.hideout.domain.model.ap.Key +import dev.usbharu.hideout.domain.model.ap.Note +import dev.usbharu.hideout.domain.model.ap.Person import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.Visibility import dev.usbharu.hideout.domain.model.job.DeliverPostJob +import dev.usbharu.hideout.exception.FailedToGetResourcesException +import dev.usbharu.hideout.exception.ap.FailedToGetActivityPubResourceException import dev.usbharu.hideout.query.FollowerQueryService import dev.usbharu.hideout.query.MediaQueryService +import dev.usbharu.hideout.query.PostQueryService import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.repository.PostRepository +import dev.usbharu.hideout.service.ap.APNoteServiceImpl.Companion.public import dev.usbharu.hideout.service.ap.job.ApNoteJobServiceImpl +import dev.usbharu.hideout.service.ap.resource.APResourceResolveService +import dev.usbharu.hideout.service.core.TwitterSnowflakeIdGenerateService import dev.usbharu.hideout.service.job.JobQueueParentService +import dev.usbharu.hideout.service.post.PostService import io.ktor.client.* -import io.ktor.client.engine.mock.* +import io.ktor.client.call.* +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.client.utils.* +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.util.* +import io.ktor.util.date.* import kjob.core.job.JobProps +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.mockito.Mockito.anyLong -import org.mockito.Mockito.eq import org.mockito.kotlin.* import utils.JsonObjectMapper.objectMapper +import utils.PostBuilder import utils.TestTransaction +import utils.UserBuilder import java.net.URL import java.time.Instant -import kotlin.test.assertEquals + class APNoteServiceImplTest { @@ -58,8 +82,7 @@ class APNoteServiceImplTest { publicKey = "", createdAt = Instant.now(), keyId = "a" - ), - userBuilder.of( + ), userBuilder.of( 3L, "follower2", "follower2.example.com", @@ -95,47 +118,303 @@ class APNoteServiceImplTest { onBlocking { findFollowersById(eq(1L)) } doReturn followers } val jobQueueParentService = mock() - val activityPubNoteService = - APNoteServiceImpl( - jobQueueParentService = jobQueueParentService, - postRepository = mock(), - apUserService = mock(), - userQueryService = userQueryService, - followerQueryService = followerQueryService, - postQueryService = mock(), - mediaQueryService = mediaQueryService, - objectMapper = objectMapper, - postService = mock(), - apResourceResolveService = mock(), - postBuilder = postBuilder - ) + val activityPubNoteService = APNoteServiceImpl( + jobQueueParentService = jobQueueParentService, + postRepository = mock(), + apUserService = mock(), + userQueryService = userQueryService, + followerQueryService = followerQueryService, + postQueryService = mock(), + mediaQueryService = mediaQueryService, + objectMapper = objectMapper, + postService = mock(), + apResourceResolveService = mock(), + postBuilder = postBuilder + ) val postEntity = postBuilder.of( - 1L, - 1L, - null, - "test text", - 1L, - Visibility.PUBLIC, - "https://example.com" + 1L, 1L, null, "test text", 1L, Visibility.PUBLIC, "https://example.com" ) activityPubNoteService.createNote(postEntity) verify(jobQueueParentService, times(2)).schedule(eq(DeliverPostJob), any()) } } + @Test + fun `fetchNote(String,String) ノートが既に存在する場合はDBから取得したものを返す`() = runTest { + val url = "https://example.com/note" + val post = PostBuilder.of() + + val postQueryService = mock { + onBlocking { findByUrl(eq(url)) } doReturn post + } + val user = UserBuilder.localUserOf(id = post.userId) + val userQueryService = mock { + onBlocking { findById(eq(post.userId)) } doReturn user + } + val apNoteServiceImpl = APNoteServiceImpl( + jobQueueParentService = mock(), + postRepository = mock(), + apUserService = mock(), + userQueryService = userQueryService, + followerQueryService = mock(), + postQueryService = postQueryService, + mediaQueryService = mock(), + objectMapper = objectMapper, + postService = mock(), + apResourceResolveService = mock(), + postBuilder = Post.PostBuilder(CharacterLimit()) + ) + + val actual = apNoteServiceImpl.fetchNote(url) + + val expected = Note( + name = "Post", + id = post.apId, + attributedTo = user.url, + content = post.text, + published = Instant.ofEpochMilli(post.createdAt).toString(), + to = listOfNotNull(public, user.followers), + sensitive = post.sensitive, + cc = listOfNotNull(public, user.followers), + inReplyTo = null + ) + assertEquals(expected, actual) + } + + @Test + fun `fetchNote(String,String) ノートがDBに存在しない場合リモートに取得しにいく`() = runTest { + val url = "https://example.com/note" + val post = PostBuilder.of() + + val postQueryService = mock { + onBlocking { findByUrl(eq(url)) } doThrow FailedToGetResourcesException() + onBlocking { findByApId(eq(post.apId)) } doReturn post + } + val user = UserBuilder.localUserOf(id = post.userId) + val userQueryService = mock { + onBlocking { findById(eq(post.userId)) } doReturn user + } + val note = Note( + name = "Post", + id = post.apId, + attributedTo = user.url, + content = post.text, + published = Instant.ofEpochMilli(post.createdAt).toString(), + to = listOfNotNull(public, user.followers), + sensitive = post.sensitive, + cc = listOfNotNull(public, user.followers), + inReplyTo = null + ) + val apResourceResolveService = mock { + onBlocking { resolve(eq(url), any(), isNull()) } doReturn note + } + val apNoteServiceImpl = APNoteServiceImpl( + jobQueueParentService = mock(), + postRepository = mock(), + apUserService = mock(), + userQueryService = userQueryService, + followerQueryService = mock(), + postQueryService = postQueryService, + mediaQueryService = mock(), + objectMapper = objectMapper, + postService = mock(), + apResourceResolveService = apResourceResolveService, + postBuilder = Post.PostBuilder(CharacterLimit()) + ) + + val actual = apNoteServiceImpl.fetchNote(url) + + assertEquals(note, actual) + } + + @OptIn(InternalAPI::class) + @Test + fun `fetchNote(String,String) ノートをリモートから取得した際にエラーが返ってきたらFailedToGetActivityPubResourceExceptionがthrowされる`() = + runTest { + val url = "https://example.com/note" + val post = PostBuilder.of() + + val postQueryService = mock { + onBlocking { findByUrl(eq(url)) } doThrow FailedToGetResourcesException() + onBlocking { findByApId(eq(post.apId)) } doReturn post + } + val user = UserBuilder.localUserOf(id = post.userId) + val userQueryService = mock { + onBlocking { findById(eq(post.userId)) } doReturn user + } + val note = Note( + name = "Post", + id = post.apId, + attributedTo = user.url, + content = post.text, + published = Instant.ofEpochMilli(post.createdAt).toString(), + to = listOfNotNull(public, user.followers), + sensitive = post.sensitive, + cc = listOfNotNull(public, user.followers), + inReplyTo = null + ) + val apResourceResolveService = mock { + val responseData = HttpResponseData( + HttpStatusCode.BadRequest, + GMTDate(), + Headers.Empty, + HttpProtocolVersion.HTTP_1_1, + NullBody, + Dispatchers.IO + ) + onBlocking { resolve(eq(url), any(), isNull()) } doThrow ClientRequestException( + DefaultHttpResponse( + HttpClientCall( + HttpClient(), HttpRequestData( + Url("http://example.com"), + HttpMethod.Get, + Headers.Empty, + EmptyContent, + Job(null), + Attributes() + ), responseData + ), responseData + ), "" + ) + } + val apNoteServiceImpl = APNoteServiceImpl( + jobQueueParentService = mock(), + postRepository = mock(), + apUserService = mock(), + userQueryService = userQueryService, + followerQueryService = mock(), + postQueryService = postQueryService, + mediaQueryService = mock(), + objectMapper = objectMapper, + postService = mock(), + apResourceResolveService = apResourceResolveService, + postBuilder = Post.PostBuilder(CharacterLimit()) + ) + + assertThrows { apNoteServiceImpl.fetchNote(url) } + + } + + @Test + fun `fetchNote(Note,String) DBに無いNoteは保存される`() = runTest { + val user = UserBuilder.localUserOf() + val generateId = TwitterSnowflakeIdGenerateService.generateId() + val post = PostBuilder.of(id = generateId, userId = user.id) + val postQueryService = mock { + onBlocking { findByApId(eq(post.apId)) } doThrow FailedToGetResourcesException() + } + val postRepository = mock { + onBlocking { generateId() } doReturn generateId + } + val person = Person( + name = user.name, + id = user.url, + preferredUsername = user.name, + summary = user.name, + inbox = user.inbox, + outbox = user.outbox, + url = user.url, + icon = Image( + name = user.url + "/icon.png", mediaType = "image/png", url = user.url + "/icon.png" + ), + publicKey = Key( + type = emptyList(), + name = "Public Key", + id = user.keyId, + owner = user.url, + publicKeyPem = user.publicKey + ), + endpoints = mapOf("sharedInbox" to "https://example.com/inbox"), + following = user.following, + followers = user.followers + ) + val apUserService = mock { + onBlocking { fetchPersonWithEntity(eq(user.url), anyOrNull()) } doReturn (person to user) + } + val postService = mock() + val apNoteServiceImpl = APNoteServiceImpl( + jobQueueParentService = mock(), + postRepository = postRepository, + apUserService = apUserService, + userQueryService = mock(), + followerQueryService = mock(), + postQueryService = postQueryService, + mediaQueryService = mock(), + objectMapper = objectMapper, + postService = postService, + apResourceResolveService = mock(), + postBuilder = postBuilder + ) + + val note = Note( + name = "Post", + id = post.apId, + attributedTo = user.url, + content = post.text, + published = Instant.ofEpochMilli(post.createdAt).toString(), + to = listOfNotNull(public, user.followers), + sensitive = post.sensitive, + cc = listOfNotNull(public, user.followers), + inReplyTo = null + ) + + + val fetchNote = apNoteServiceImpl.fetchNote(note, null) + verify(postService, times(1)).createRemote( + eq( + PostBuilder.of( + id = generateId, userId = user.id, createdAt = post.createdAt + ) + ) + ) + assertEquals(note, fetchNote) + } + + @Test + fun `fetchNote DBに存在する場合取得して返す`() = runTest { + + val user = UserBuilder.localUserOf() + val post = PostBuilder.of(userId = user.id) + + val postQueryService = mock { + onBlocking { findByApId(eq(post.apId)) } doReturn post + } + val userQueryService = mock { + onBlocking { findById(eq(user.id)) } doReturn user + } + val apNoteServiceImpl = APNoteServiceImpl( + jobQueueParentService = mock(), + postRepository = mock(), + apUserService = mock(), + userQueryService = userQueryService, + followerQueryService = mock(), + postQueryService = postQueryService, + mediaQueryService = mock(), + objectMapper = objectMapper, + postService = mock(), + apResourceResolveService = mock(), + postBuilder = postBuilder + ) + + val note = Note( + name = "Post", + id = post.apId, + attributedTo = user.url, + content = post.text, + published = Instant.ofEpochMilli(post.createdAt).toString(), + to = listOfNotNull(public, user.followers), + sensitive = post.sensitive, + cc = listOfNotNull(public, user.followers), + inReplyTo = null + ) + + val fetchNote = apNoteServiceImpl.fetchNote(note, null) + assertEquals(note, fetchNote) + } + @Test fun `createPostJob 新しい投稿のJob`() { runTest { - val mediaQueryService = mock { - onBlocking { findByPostId(anyLong()) } doReturn emptyList() - } - - val httpClient = HttpClient( - MockEngine { httpRequestData -> - assertEquals("https://follower.example.com/inbox", httpRequestData.url.toString()) - respondOk() - } - ) val activityPubNoteService = ApNoteJobServiceImpl( userQueryService = mock(), @@ -147,19 +426,15 @@ class APNoteServiceImplTest { activityPubNoteService.createNoteJob( JobProps( data = mapOf( - DeliverPostJob.actor.name to "https://follower.example.com", - DeliverPostJob.post.name to """{ + DeliverPostJob.actor.name to "https://follower.example.com", DeliverPostJob.post.name to """{ "id": 1, "userId": 1, "text": "test text", "createdAt": 132525324, "visibility": 0, "url": "https://example.com" - }""", - DeliverPostJob.inbox.name to "https://follower.example.com/inbox", - DeliverPostJob.media.name to "[]" - ), - json = Json + }""", DeliverPostJob.inbox.name to "https://follower.example.com/inbox", DeliverPostJob.media.name to "[]" + ), json = Json ) ) }