From 486abaaaf352a10dc87f531e55064ea32630f048 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 29 Oct 2023 16:36:24 +0900 Subject: [PATCH 01/14] =?UTF-8?q?test:=20APAcceptServiceImpl=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 --- .../domain/model/ActivityPubStringResponse.kt | 70 ++++++++++- .../service/ap/APAcceptServiceImplTest.kt | 110 ++++++++++++++++++ src/test/kotlin/utils/UserBuilder.kt | 55 +++++++++ 3 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 src/test/kotlin/dev/usbharu/hideout/service/ap/APAcceptServiceImplTest.kt create mode 100644 src/test/kotlin/utils/UserBuilder.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt index 402079b0..26b04f51 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt @@ -7,18 +7,78 @@ import io.ktor.http.* sealed class ActivityPubResponse( val httpStatusCode: HttpStatusCode, val contentType: ContentType = ContentType.Application.Activity -) +) { + + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ActivityPubResponse) return false + + if (httpStatusCode != other.httpStatusCode) return false + if (contentType != other.contentType) return false + + return true + } + + override fun hashCode(): Int { + var result = httpStatusCode.hashCode() + result = 31 * result + contentType.hashCode() + return result + } + + override fun toString(): String { + return "ActivityPubResponse(httpStatusCode=$httpStatusCode, contentType=$contentType)" + } +} class ActivityPubStringResponse( httpStatusCode: HttpStatusCode = HttpStatusCode.OK, val message: String, contentType: ContentType = ContentType.Application.Activity -) : - ActivityPubResponse(httpStatusCode, contentType) +) : ActivityPubResponse(httpStatusCode, contentType) { + + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ActivityPubStringResponse) return false + + if (message != other.message) return false + + return true + } + + override fun hashCode(): Int { + return message.hashCode() + } + + override fun toString(): String { + return "ActivityPubStringResponse(message='$message') ${super.toString()}" + } + + +} class ActivityPubObjectResponse( httpStatusCode: HttpStatusCode = HttpStatusCode.OK, val message: JsonLd, contentType: ContentType = ContentType.Application.Activity -) : - ActivityPubResponse(httpStatusCode, contentType) +) : ActivityPubResponse(httpStatusCode, contentType) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ActivityPubObjectResponse) return false + + if (message != other.message) return false + + return true + } + + override fun hashCode(): Int { + return message.hashCode() + } + + override fun toString(): String { + return "ActivityPubObjectResponse(message=$message) ${super.toString()}" + } + + +} diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APAcceptServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APAcceptServiceImplTest.kt new file mode 100644 index 00000000..e4f0a851 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APAcceptServiceImplTest.kt @@ -0,0 +1,110 @@ +package dev.usbharu.hideout.service.ap + +import dev.usbharu.hideout.domain.model.ActivityPubStringResponse +import dev.usbharu.hideout.domain.model.ap.Accept +import dev.usbharu.hideout.domain.model.ap.Follow +import dev.usbharu.hideout.domain.model.ap.Like +import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException +import dev.usbharu.hideout.query.FollowerQueryService +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.service.user.UserService +import io.ktor.http.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.* +import utils.TestTransaction +import utils.UserBuilder + +class APAcceptServiceImplTest { + + @Test + fun `receiveAccept 正常なAcceptを処理できる`() = runTest { + val actor = "https://example.com" + val follower = "https://follower.example.com" + val targetUser = UserBuilder.localUserOf() + val followerUser = UserBuilder.localUserOf() + val userQueryService = mock { + onBlocking { findByUrl(eq(actor)) } doReturn targetUser + onBlocking { findByUrl(eq(follower)) } doReturn followerUser + } + val followerQueryService = mock { + onBlocking { alreadyFollow(eq(targetUser.id), eq(followerUser.id)) } doReturn false + } + val userService = mock() + val apAcceptServiceImpl = + APAcceptServiceImpl(userService, userQueryService, followerQueryService, TestTransaction) + + val accept = Accept( + name = "Accept", + `object` = Follow( + name = "", + `object` = actor, + actor = follower + ), + actor = actor + ) + + + val actual = apAcceptServiceImpl.receiveAccept(accept) + assertEquals(ActivityPubStringResponse(HttpStatusCode.OK, "accepted"), actual) + verify(userService, times(1)).follow(eq(targetUser.id), eq(followerUser.id)) + } + + @Test + fun `receiveAccept 既にフォローしている場合は無視する`() = runTest { + + val actor = "https://example.com" + val follower = "https://follower.example.com" + val targetUser = UserBuilder.localUserOf() + val followerUser = UserBuilder.localUserOf() + val userQueryService = mock { + onBlocking { findByUrl(eq(actor)) } doReturn targetUser + onBlocking { findByUrl(eq(follower)) } doReturn followerUser + } + val followerQueryService = mock { + onBlocking { alreadyFollow(eq(targetUser.id), eq(followerUser.id)) } doReturn true + } + val userService = mock() + val apAcceptServiceImpl = + APAcceptServiceImpl(userService, userQueryService, followerQueryService, TestTransaction) + + val accept = Accept( + name = "Accept", + `object` = Follow( + name = "", + `object` = actor, + actor = follower + ), + actor = actor + ) + + + val actual = apAcceptServiceImpl.receiveAccept(accept) + assertEquals(ActivityPubStringResponse(HttpStatusCode.OK, "accepted"), actual) + verify(userService, times(0)).follow(eq(targetUser.id), eq(followerUser.id)) + } + + @Test + fun `revieveAccept AcceptのobjectのtypeがFollow以外の場合IllegalActivityPubObjectExceptionがthrowされる`() = + runTest { + val accept = Accept( + name = "Accept", + `object` = Like( + name = "Like", + actor = "actor", + id = "https://example.com", + `object` = "https://example.com", + content = "aaaa" + ), + actor = "https://example.com" + ) + + val apAcceptServiceImpl = APAcceptServiceImpl(mock(), mock(), mock(), TestTransaction) + + assertThrows { + apAcceptServiceImpl.receiveAccept(accept) + } + } +} diff --git a/src/test/kotlin/utils/UserBuilder.kt b/src/test/kotlin/utils/UserBuilder.kt new file mode 100644 index 00000000..9d8116cd --- /dev/null +++ b/src/test/kotlin/utils/UserBuilder.kt @@ -0,0 +1,55 @@ +package utils + +import dev.usbharu.hideout.config.ApplicationConfig +import dev.usbharu.hideout.config.CharacterLimit +import dev.usbharu.hideout.domain.model.hideout.entity.User +import dev.usbharu.hideout.service.core.TwitterSnowflakeIdGenerateService +import kotlinx.coroutines.runBlocking +import java.net.URL +import java.time.Instant + +object UserBuilder { + private val userBuilder = User.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com"))) + + private val idGenerator = TwitterSnowflakeIdGenerateService + + suspend fun localUserOf( + id: Long = generateId(), + name: String = "test-user-$id", + domain: String = "example.com", + screenName: String = name, + description: String = "This user is test user.", + password: String = "password-$id", + inbox: String = "https://$domain/$id/inbox", + outbox: String = "https://$domain/$id/outbox", + url: String = "https://$domain/$id/", + publicKey: String = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", + privateKey: String = "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----", + createdAt: Instant = Instant.now(), + keyId: String = "https://$domain/$id#pubkey", + followers: String = "https://$domain/$id/followers", + following: String = "https://$domain/$id/following" + ): User { + return userBuilder.of( + id = id, + name = name, + domain = domain, + screenName = screenName, + description = description, + password = password, + inbox = inbox, + outbox = outbox, + url = url, + publicKey = publicKey, + privateKey = privateKey, + createdAt = createdAt, + keyId = keyId, + followers = following, + following = followers + ) + } + + private fun generateId(): Long = runBlocking { + idGenerator.generateId() + } +} From e2e564d71b01d6fa1217fc84851d09763910f44c Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 29 Oct 2023 16:58:42 +0900 Subject: [PATCH 02/14] =?UTF-8?q?test:=20APCreateServiceimpl=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 --- .../service/ap/APCreateServiceImplTest.kt | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/test/kotlin/dev/usbharu/hideout/service/ap/APCreateServiceImplTest.kt diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APCreateServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APCreateServiceImplTest.kt new file mode 100644 index 00000000..0d3acf72 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APCreateServiceImplTest.kt @@ -0,0 +1,63 @@ +package dev.usbharu.hideout.service.ap + +import dev.usbharu.hideout.domain.model.ActivityPubStringResponse +import dev.usbharu.hideout.domain.model.ap.Create +import dev.usbharu.hideout.domain.model.ap.Like +import dev.usbharu.hideout.domain.model.ap.Note +import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException +import io.ktor.http.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.* +import utils.TestTransaction + +class APCreateServiceImplTest { + + @Test + fun `receiveCreate 正常なCreateを処理できる`() = runTest { + val create = Create( + name = "Create", + `object` = Note( + name = "Note", + id = "https://example.com/note", + attributedTo = "https://example.com/actor", + content = "Hello World", + published = "Date: Wed, 21 Oct 2015 07:28:00 GMT" + ), + actor = "https://example.com/actor", + id = "https://example.com/create", + ) + + val apNoteService = mock() + val apCreateServiceImpl = APCreateServiceImpl(apNoteService, TestTransaction) + + val actual = ActivityPubStringResponse(HttpStatusCode.OK, "Created") + + val receiveCreate = apCreateServiceImpl.receiveCreate(create) + verify(apNoteService, times(1)).fetchNote(any(), anyOrNull()) + assertEquals(actual, receiveCreate) + } + + @Test + fun `reveiveCreate CreateのobjectのtypeがNote以外の場合IllegalActivityPubObjectExceptionがthrowされる`() = runTest { + val create = Create( + name = "Create", + `object` = Like( + name = "Like", + id = "https://example.com/note", + actor = "https://example.com/actor", + `object` = "https://example.com/create", + content = "aaa" + ), + actor = "https://example.com/actor", + id = "https://example.com/create", + ) + + val apCreateServiceImpl = APCreateServiceImpl(mock(), TestTransaction) + assertThrows { + apCreateServiceImpl.receiveCreate(create) + } + } +} From dfd6655e91f8c45a6a43e1edcbe4e584ee4b4192 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 29 Oct 2023 17:32:04 +0900 Subject: [PATCH 03/14] =?UTF-8?q?test:=20APLikeServiceImpl=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 --- .../service/ap/APLikeServiceImplTest.kt | 110 ++++++++++++++++++ src/test/kotlin/utils/PostBuilder.kt | 39 +++++++ 2 files changed, 149 insertions(+) create mode 100644 src/test/kotlin/dev/usbharu/hideout/service/ap/APLikeServiceImplTest.kt create mode 100644 src/test/kotlin/utils/PostBuilder.kt diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APLikeServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APLikeServiceImplTest.kt new file mode 100644 index 00000000..88140695 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APLikeServiceImplTest.kt @@ -0,0 +1,110 @@ +package dev.usbharu.hideout.service.ap + +import dev.usbharu.hideout.domain.model.ActivityPubStringResponse +import dev.usbharu.hideout.domain.model.ap.Like +import dev.usbharu.hideout.domain.model.ap.Note +import dev.usbharu.hideout.domain.model.ap.Person +import dev.usbharu.hideout.exception.ap.FailedToGetActivityPubResourceException +import dev.usbharu.hideout.query.PostQueryService +import dev.usbharu.hideout.service.reaction.ReactionService +import io.ktor.http.* +import kotlinx.coroutines.async +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.mockito.kotlin.* +import utils.PostBuilder +import utils.TestTransaction +import utils.UserBuilder + + +class APLikeServiceImplTest { + @Test + fun `receiveLike 正常なLikeを処理できる`() = runTest { + val actor = "https://example.com/actor" + val note = "https://example.com/note" + val like = Like( + name = "Like", actor = actor, id = "htps://example.com", `object` = note, content = "aaa" + ) + + val user = UserBuilder.localUserOf() + val apUserService = mock { + onBlocking { fetchPersonWithEntity(eq(actor), anyOrNull()) } doReturn (Person( + name = "TestUser", + id = "https://example.com", + preferredUsername = "Test user", + summary = "test user", + inbox = "https://example.com/inbox", + outbox = "https://example.com/outbox", + url = "https://example.com/", + icon = null, + publicKey = null, + followers = null, + following = null + ) to user) + } + val apNoteService = mock { + on { fetchNoteAsync(eq(note), anyOrNull()) } doReturn async { + Note( + name = "Note", + id = "https://example.com/note", + attributedTo = "https://example.com/actor", + content = "Hello World", + published = "Date: Wed, 21 Oct 2015 07:28:00 GMT", + ) + } + } + val post = PostBuilder.of() + val postQueryService = mock { + onBlocking { findByUrl(eq(note)) } doReturn post + } + val reactionService = mock() + val apLikeServiceImpl = APLikeServiceImpl( + reactionService, apUserService, apNoteService, postQueryService, TestTransaction + ) + + val actual = apLikeServiceImpl.receiveLike(like) + + verify(reactionService, times(1)).receiveReaction(eq("aaa"), eq("example.com"), eq(user.id), eq(post.id)) + assertEquals(ActivityPubStringResponse(HttpStatusCode.OK, ""), actual) + } + + @Test + fun `recieveLike Likeのobjectのurlが取得できないとき何もしない`() = runTest { + val actor = "https://example.com/actor" + val note = "https://example.com/note" + val like = Like( + name = "Like", actor = actor, id = "htps://example.com", `object` = note, content = "aaa" + ) + + val user = UserBuilder.localUserOf() + val apUserService = mock { + onBlocking { fetchPersonWithEntity(eq(actor), anyOrNull()) } doReturn (Person( + name = "TestUser", + id = "https://example.com", + preferredUsername = "Test user", + summary = "test user", + inbox = "https://example.com/inbox", + outbox = "https://example.com/outbox", + url = "https://example.com/", + icon = null, + publicKey = null, + followers = null, + following = null + ) to user) + } + val apNoteService = mock { + on { fetchNoteAsync(eq(note), anyOrNull()) } doThrow FailedToGetActivityPubResourceException() + } + + val reactionService = mock() + val apLikeServiceImpl = APLikeServiceImpl( + reactionService, apUserService, apNoteService, mock(), TestTransaction + ) + + val actual = apLikeServiceImpl.receiveLike(like) + + verify(reactionService, times(0)).receiveReaction(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + assertEquals(ActivityPubStringResponse(HttpStatusCode.OK, ""), actual) + } +} diff --git a/src/test/kotlin/utils/PostBuilder.kt b/src/test/kotlin/utils/PostBuilder.kt new file mode 100644 index 00000000..630d006e --- /dev/null +++ b/src/test/kotlin/utils/PostBuilder.kt @@ -0,0 +1,39 @@ +package utils + +import dev.usbharu.hideout.config.CharacterLimit +import dev.usbharu.hideout.domain.model.hideout.entity.Post +import dev.usbharu.hideout.domain.model.hideout.entity.Visibility +import dev.usbharu.hideout.service.core.TwitterSnowflakeIdGenerateService +import kotlinx.coroutines.runBlocking +import java.time.Instant + +object PostBuilder { + + private val postBuilder = Post.PostBuilder(CharacterLimit()) + + private val idGenerator = TwitterSnowflakeIdGenerateService + + fun of( + id: Long = generateId(), + userId: Long = generateId(), + overview: String? = null, + text: String = "Hello World", + createdAt: Long = Instant.now().toEpochMilli(), + visibility: Visibility = Visibility.PUBLIC, + url: String = "https://example.com/users/$userId/posts/$id" + ): Post { + return postBuilder.of( + id = id, + userId = userId, + overview = overview, + text = text, + createdAt = createdAt, + visibility = visibility, + url = url, + ) + } + + private fun generateId(): Long = runBlocking { + idGenerator.generateId() + } +} 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 04/14] =?UTF-8?q?test:=20APNoteServiceImpl=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 --- .../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 ) ) } From b9d568c9c37e6e8267ac4cf369832cadb653fb10 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:46:30 +0900 Subject: [PATCH 05/14] =?UTF-8?q?test:=20APReactionServiceImpl=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 --- .../service/ap/APReactionServiceImplTest.kt | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/test/kotlin/dev/usbharu/hideout/service/ap/APReactionServiceImplTest.kt diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APReactionServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APReactionServiceImplTest.kt new file mode 100644 index 00000000..d0a89b23 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APReactionServiceImplTest.kt @@ -0,0 +1,92 @@ +package dev.usbharu.hideout.service.ap + + +import dev.usbharu.hideout.domain.model.hideout.entity.Reaction +import dev.usbharu.hideout.domain.model.job.DeliverReactionJob +import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob +import dev.usbharu.hideout.query.FollowerQueryService +import dev.usbharu.hideout.query.PostQueryService +import dev.usbharu.hideout.service.core.TwitterSnowflakeIdGenerateService +import dev.usbharu.hideout.service.job.JobQueueParentService +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.mockito.kotlin.* +import utils.JsonObjectMapper.objectMapper +import utils.PostBuilder +import utils.UserBuilder + +class APReactionServiceImplTest { + @Test + fun `reaction リアクションするとフォロワーの数だけ配送ジョブが作成される`() = runTest { + + val user = UserBuilder.localUserOf() + val post = PostBuilder.of() + + val postQueryService = mock { + onBlocking { findById(eq(post.id)) } doReturn post + } + val followerQueryService = mock { + onBlocking { findFollowersById(eq(user.id)) } doReturn listOf( + UserBuilder.localUserOf(), + UserBuilder.localUserOf(), + UserBuilder.localUserOf() + ) + } + val jobQueueParentService = mock() + val apReactionServiceImpl = APReactionServiceImpl( + jobQueueParentService = jobQueueParentService, + userQueryService = mock(), + followerQueryService = followerQueryService, + postQueryService = postQueryService, + objectMapper = objectMapper + ) + + apReactionServiceImpl.reaction( + Reaction( + id = TwitterSnowflakeIdGenerateService.generateId(), + emojiId = 0, + postId = post.id, + userId = user.id + ) + ) + + verify(jobQueueParentService, times(3)).schedule(eq(DeliverReactionJob), any()) + } + + @Test + fun `removeReaction リアクションを削除するとフォロワーの数だけ配送ジョブが作成される`() = runTest { + + val user = UserBuilder.localUserOf() + val post = PostBuilder.of() + + val postQueryService = mock { + onBlocking { findById(eq(post.id)) } doReturn post + } + val followerQueryService = mock { + onBlocking { findFollowersById(eq(user.id)) } doReturn listOf( + UserBuilder.localUserOf(), + UserBuilder.localUserOf(), + UserBuilder.localUserOf() + ) + } + val jobQueueParentService = mock() + val apReactionServiceImpl = APReactionServiceImpl( + jobQueueParentService = jobQueueParentService, + userQueryService = mock(), + followerQueryService = followerQueryService, + postQueryService = postQueryService, + objectMapper = objectMapper + ) + + apReactionServiceImpl.removeReaction( + Reaction( + id = TwitterSnowflakeIdGenerateService.generateId(), + emojiId = 0, + postId = post.id, + userId = user.id + ) + ) + + verify(jobQueueParentService, times(3)).schedule(eq(DeliverRemoveReactionJob), any()) + } +} From 6251e02266821d778f5f2024859114632b370a6c Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:19:53 +0900 Subject: [PATCH 06/14] =?UTF-8?q?test:=20APRequestServiceImpl=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 --- .../service/ap/APRequestServiceImplTest.kt | 348 ++++++++++++++++++ src/test/kotlin/utils/UserBuilder.kt | 36 +- 2 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImplTest.kt diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImplTest.kt new file mode 100644 index 00000000..3d77a924 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImplTest.kt @@ -0,0 +1,348 @@ +package dev.usbharu.hideout.service.ap + +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.domain.model.ap.Follow +import dev.usbharu.hideout.util.Base64Util +import dev.usbharu.httpsignature.common.HttpHeaders +import dev.usbharu.httpsignature.common.HttpMethod +import dev.usbharu.httpsignature.common.HttpRequest +import dev.usbharu.httpsignature.sign.HttpSignatureSigner +import dev.usbharu.httpsignature.sign.Signature +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.util.* +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import utils.JsonObjectMapper.objectMapper +import utils.UserBuilder +import java.net.URL +import java.security.MessageDigest +import java.time.format.DateTimeFormatter +import java.util.* + + +class APRequestServiceImplTest { + @Test + fun `apGet signerがnullのとき署名なしリクエストをする`() = runTest { + val dateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + val apRequestServiceImpl = APRequestServiceImpl( + HttpClient(MockEngine { + assertTrue(it.headers.contains("Date")) + assertTrue(it.headers.contains("Accept")) + assertFalse(it.headers.contains("Signature")) + assertDoesNotThrow { + dateTimeFormatter.parse(it.headers["Date"]) + } + respond("{}") + }), + objectMapper, + mock(), + dateTimeFormatter + ) + + val responseClass = Follow( + name = "Follow", + `object` = "https://example.com", + actor = "https://example.com" + ) + apRequestServiceImpl.apGet("https://example.com", responseClass = responseClass::class.java) + } + + @Test + fun `apGet signerがnullではないがprivateKeyがnullのとき署名なしリクエストをする`() = runTest { + val dateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + val apRequestServiceImpl = APRequestServiceImpl( + HttpClient(MockEngine { + assertTrue(it.headers.contains("Date")) + assertTrue(it.headers.contains("Accept")) + assertFalse(it.headers.contains("Signature")) + assertDoesNotThrow { + dateTimeFormatter.parse(it.headers["Date"]) + } + respond("{}") + }), + objectMapper, + mock(), + dateTimeFormatter + ) + + val responseClass = Follow( + name = "Follow", + `object` = "https://example.com", + actor = "https://example.com" + ) + apRequestServiceImpl.apGet( + "https://example.com", + UserBuilder.remoteUserOf(), + responseClass = responseClass::class.java + ) + } + + @Test + fun `apGet signerとprivatekeyがnullではないとき署名付きリクエストをする`() = runTest { + val dateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + val httpSignatureSigner = mock { + onBlocking { + sign( + any(), + any(), + eq(listOf("(request-target)", "date", "host", "accept")) + ) + } doReturn Signature( + HttpRequest(URL("https://example.com"), HttpHeaders(mapOf()), HttpMethod.GET), "", "" + ) + } + val apRequestServiceImpl = APRequestServiceImpl( + HttpClient(MockEngine { + assertTrue(it.headers.contains("Date")) + assertTrue(it.headers.contains("Accept")) + assertTrue(it.headers.contains("Signature")) + assertDoesNotThrow { + dateTimeFormatter.parse(it.headers["Date"]) + } + respond("{}") + }), + objectMapper, + httpSignatureSigner, + dateTimeFormatter + ) + + val responseClass = Follow( + name = "Follow", + `object` = "https://example.com", + actor = "https://example.com" + ) + apRequestServiceImpl.apGet( + "https://example.com", + UserBuilder.localUserOf( + privateKey = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJhNETcFVoZW36\n" + + "pDiaaUDa1FsWGqULUa6jDWYbMXFirbbceJEfvaasac+E8VUQ3krrEhYBArntB1do\n" + + "1Zq/MpI97WaQefwrBmjJwjYglB8AHF1RRqFlJ0aABMBvuHiIzuTPv4dLS4+pJQWl\n" + + "iE9TKsxXgUrEdWLmpSukZpyiWnrgFtJ8322LXRuL9+O4ivns1JfozbrHTprI4ohe\n" + + "6taZJX1mhGBXQT+U/UrEILk+z70P2rrwxwerdO7s6nkkC3ieJWdi924/AopDlg12\n" + + "8udubLPbpWVVrHbSKviUr3VKBKGe4xmvO7hqpGwKmctaXRVPjh/ue2mCIzv3qyxQ\n" + + "3n2Xyhb3AgMBAAECggEAGddiSC/bg+ud0spER+i/XFBm7cq052KuFlKdiVcpxxGn\n" + + "pVYApiVXvjxDVDTuR5950/MZxz9mQDL0zoi1s1b00eQjhttdrta/kT/KWRslboo0\n" + + "nTuFbsc+jyQM2Ua6jjCZvto8qzchUPtiYfu80Floor/9qnuzFwiPNCHEbD1WDG4m\n" + + "fLuH+INnGY6eRF+pgly1dykGs18DaR3vC9CWOqR9PWH+p/myksVymR5adKauMc+l\n" + + "gjLaeB1YjnzXnHYLqwtCgh053kedPG/xZZwq48YNP5npSBIHsd9g8JIPVNOOc6+s\n" + + "bbFqD9aQQxG/WaA5hxHRupLkKGjE6lw4SnVYzKMZIQKBgQDryFa3qzJIBrCQQa0r\n" + + "6YlmZeeCQ8mQL8d0gY0Ixo9Gm2/9J71m/oBkhOqnS6Z5e5UHS5iVaqM7sIOZ2Ony\n" + + "kPADAtxUsk71Il+z+JgyN3OQ+DROLREi2TIWS523hbtN7e/fRFs7KoN6cH7IeF13\n" + + "3pphg9+WWRGX7y1zMd1puY/gSwKBgQDazFrAt/oZbnDhkX350OdIybz62OHNyuZv\n" + + "UX9fFl9i93SF+UhOpJ8YvDJtfLEJUkwO+V3TB+we1OlOYMTqir5M8GFn6YDotwxB\n" + + "r6eT886UpJgtJwswwwW2yaXo7zXaeg3ovRE8RJ4y++Mhuqeq3ajIo7xlhQjzBDEf\n" + + "ZAqasSWwhQKBgQC0VbUlo1XAywUOQH0/oc4KOJS6CDjJBBIsZM3G0X9SBJ7B5Dwz\n" + + "4yG2QAbtT6oTLldMjiA036vbgmUVLVe5w+sekniMexhy2wiRsOhPOCQ20+/Ffyil\n" + + "G7P4Y3tMm4cn0n1tqW2RsjF/Wz1M/OqYPPSc8uz2pEcVisSbX582Nsv5QwKBgEuy\n" + + "vAtFG6BE14UTIzSVFA/YzCs1choTAtqspZauVN4WoxffASdESU7zfbbnlxCUin/7\n" + + "wnxKl2SrYPSfAkHrMp/H4stivBjHi9QGA8JqbaR7tbKZeYOrVYTCC0alzEoERF+r\n" + + "WhUx4FHfV9vJikzRV53jGEE/X7NEVgJ4SDrw4wtJAoGAAMJ2kOIL3HSQPd8csXeU\n" + + "nkxLNzBsFpF76LVmLdzJttlr8HWBjLP/EJFQZFzuf5Hd38cLUOWWD3FRZVw0dUcN\n" + + "RSqfIYT4yDc/9GSRb6rOkdmBUWpTsrZjXBo0MC3p1QE6sNO8JfvmxHTSAe8apBh/\n" + + "gaYuQGh0lNa23HwwFoJxuoc=\n" + + "-----END PRIVATE KEY-----" + ), + responseClass = responseClass::class.java + ) + } + + @Test + fun `apPost bodyがnullでないときcontextにactivitystreamのURLを追加する`() = runTest { + val dateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + val apRequestServiceImpl = APRequestServiceImpl(HttpClient(MockEngine { + val readValue = objectMapper.readValue(it.body.toByteArray()) + + assertThat(readValue.context).contains("https://www.w3.org/ns/activitystreams") + + respondOk("{}") + }), objectMapper, mock(), dateTimeFormatter) + + val body = Follow( + name = "Follow", + `object` = "https://example.com", + actor = "https://example.com" + ) + apRequestServiceImpl.apPost("https://example.com", body, null) + } + + @Test + fun `apPost bodyがnullのときリクエストボディは空`() = runTest { + val dateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + val apRequestServiceImpl = APRequestServiceImpl(HttpClient(MockEngine { + + assertEquals(0, it.body.toByteArray().size) + + respondOk("{}") + }), objectMapper, mock(), dateTimeFormatter) + + apRequestServiceImpl.apPost("https://example.com", null, null) + } + + @Test + fun `apPost signerがnullのとき署名なしリクエストをする`() = runTest { + val dateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + val apRequestServiceImpl = APRequestServiceImpl(HttpClient(MockEngine { + val src = it.body.toByteArray() + val readValue = objectMapper.readValue(src) + + assertThat(readValue.context).contains("https://www.w3.org/ns/activitystreams") + + val map = it.headers.toMap() + assertThat(map).containsKey("Date") + .containsKey("Digest") + .containsKey("Accept") + .doesNotContainKey("Signature") + + assertDoesNotThrow { + dateTimeFormatter.parse(it.headers["Date"]) + } + val messageDigest = MessageDigest.getInstance("SHA-256") + val digest = Base64Util.encode(messageDigest.digest(src)) + + assertEquals(digest, it.headers["Digest"].orEmpty().split("256=").last()) + + respondOk("{}") + }), objectMapper, mock(), dateTimeFormatter) + + val body = Follow( + name = "Follow", + `object` = "https://example.com", + actor = "https://example.com" + ) + apRequestServiceImpl.apPost("https://example.com", body, null) + } + + @Test + fun `apPost signerがnullではないがprivatekeyがnullのとき署名なしリクエストをする`() = runTest { + val dateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + val apRequestServiceImpl = APRequestServiceImpl(HttpClient(MockEngine { + val src = it.body.toByteArray() + val readValue = objectMapper.readValue(src) + + assertThat(readValue.context).contains("https://www.w3.org/ns/activitystreams") + + val map = it.headers.toMap() + assertThat(map).containsKey("Date") + .containsKey("Digest") + .containsKey("Accept") + .doesNotContainKey("Signature") + + val messageDigest = MessageDigest.getInstance("SHA-256") + val digest = Base64Util.encode(messageDigest.digest(src)) + + assertEquals(digest, it.headers["Digest"].orEmpty().split("256=").last()) + + respondOk("{}") + }), objectMapper, mock(), dateTimeFormatter) + + val body = Follow( + name = "Follow", + `object` = "https://example.com", + actor = "https://example.com" + ) + apRequestServiceImpl.apPost("https://example.com", body, UserBuilder.remoteUserOf()) + } + + @Test + fun `apPost signerがnullではないとき署名付きリクエストをする`() = runTest { + val dateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + val httpSignatureSigner = mock { + onBlocking { + sign( + any(), + any(), + eq(listOf("(request-target)", "date", "host", "digest")) + ) + } doReturn Signature( + HttpRequest(URL("https://example.com"), HttpHeaders(mapOf()), HttpMethod.POST), "", "" + ) + } + val apRequestServiceImpl = APRequestServiceImpl(HttpClient(MockEngine { + val src = it.body.toByteArray() + val readValue = objectMapper.readValue(src) + + assertThat(readValue.context).contains("https://www.w3.org/ns/activitystreams") + + val map = it.headers.toMap() + assertThat(map).containsKey("Date") + .containsKey("Digest") + .containsKey("Accept") + .containsKey("Signature") + + val messageDigest = MessageDigest.getInstance("SHA-256") + val digest = Base64Util.encode(messageDigest.digest(src)) + + assertEquals(digest, it.headers["Digest"].orEmpty().split("256=").last()) + + respondOk("{}") + }), objectMapper, httpSignatureSigner, dateTimeFormatter) + + val body = Follow( + name = "Follow", + `object` = "https://example.com", + actor = "https://example.com" + ) + apRequestServiceImpl.apPost( + "https://example.com", body, UserBuilder.localUserOf( + privateKey = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1+pj+/t5WwU6P\n" + + "OiaAKfOHCUVMdOR5e2Jp0BUYfAFpim27pLsHRXVjdzs+D4gvDnQWC0FMltPyBldk\n" + + "gjisNMtTKgTTsYhlLlSi+yRDZvIQyH4b7xSX0hCeflTrTkt18ZldBRPfMHE0KSho\n" + + "mm3Lc7ubF32YzGoo3A3qEVDAR9dVQOnt/GXLiN4RHoStX+y5UiP6B4s49nyEwuLm\n" + + "+HE4ph3Loqn0dTEL4cEuI8ZX51J3mTKT3rmMo0wCXXOm8gD2Fu7hYEdr9ulWF8GO\n" + + "yVe7Miu9prbBlY/r4skdXc5o6uE8tsPT88Ly9lSr3xqbmn1/EhyqBRdcyoj28C65\n" + + "cThO38jvAgMBAAECggEAFbOaXkJ3smHgI/17zOnz1EU7QehovMIFlPfPJDnZk0QC\n" + + "XQ/CjBXw71kvM/H3PCFdn6lc8qzD/sdZ0a8j4glzu+m1ZKd1zBcv2bXYd79Fm9HF\n" + + "FEC5NHfFKpmHN/6AykJzFyA9Y+7reRx1aLAN6ubU1ySAgmHSQSgo8qJ4/k0y9UQS\n" + + "EbjxQL5ziXuxRBMn7InLUGLl5UfCC0V1R8MZQAe+fApKDXMQ0LHSJUg1A365PyhV\n" + + "seotqvhurHH3UVHf5n0/sFeqp2hI4ymR3cs4kd8IuNIXE7afh+89IyuVKMvJh+iQ\n" + + "ZGO1RL0v0mNtUpI81agSrrQ4LRBjSkP+5s5PdXTrSQKBgQD2lwMXLylhQzhRyhLx\n" + + "sSPRf9mKDUcretwA5Fh9GuAurKOz7SvIdzrUPFYUTUKSTwk8mVRRamkFtJ8IOB7Z\n" + + "MLenlFqxs4XrNGBcZxut5cPv68xn2F00Y4HwX9xmEi+vniNVrDpdVLxEoVfm1pBk\n" + + "02ZHCcfYVN0t8dnvXvlL+eJSqQKBgQC87GMoMvFnWgT23wdXtQH+F+gQAMUrkMWz\n" + + "Ld2uRwuSVQArgp+YgnwWMlYlFp/QIW90t7UVmf6bHIplO5bL2OwayIO1r/WxD1eN\n" + + "RLrFIeDbtCZWQTHUypnWtl+9lrh/RrCjZo/sZFl07OSIKgGM37j9taG6Nv6fV7gv\n" + + "T0q6eDCV1wKBgGh3CUQlIq6lv5JGvUfO95GlTA+EGIZ/Af0Ov74gSKD9Wky7STUf\n" + + "7bhD52OqZ218NjmJ64KiReO45TaiL89rKCLCYrmtiCpgggIjXEKLeDqH9ox3yOSM\n" + + "01t2APTs926629VLpV4sq6WXhJmyhHFybX3i0tr++MSiFOWnoo1hS1QhAoGAfVY6\n" + + "ppW9kDqppnrqrSZ6Lu//VnacWL3QW4JnWtLpe2iHF1auuQiAeF1mx25OEk/MWNvz\n" + + "+GPVBWUW7/hrn8vHQDGdJ/GYB6LNC/z4CAbk3f2TnY/dFnZfP5J4zBftSQtF7vIB\n" + + "M+yTaL4tE6UCqEpYuYFBzX/kxyP0Hvb09eb9HLsCgYEArFSgWpaLbADcWd+ygWls\n" + + "LNfch1Yl2bnqXKz1Dnw3J4l2gbVNcABXQLrB6upjtkytxj4ae66Sio7nf+dB5yJ6\n" + + "NVY7i4C0JrniY2OvLnuz2bKpaTgMPJxyZqGQ6Vu2b3x9WhcpiI83SCuCUgBKxjh/\n" + + "qEGv2ZqFfnNVrz5RXLHBoG4=\n" + + "-----END PRIVATE KEY-----" + ) + ) + } + + @Test + fun `apPost responseClassを指定した場合はjsonでシリアライズされる`() = runTest { + val dateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) + val apRequestServiceImpl = APRequestServiceImpl(HttpClient(MockEngine { + val src = it.body.toByteArray() + val readValue = objectMapper.readValue(src) + + assertThat(readValue.context).contains("https://www.w3.org/ns/activitystreams") + + respondOk(src.decodeToString()) + }), objectMapper, mock(), dateTimeFormatter) + + val body = Follow( + name = "Follow", + `object` = "https://example.com", + actor = "https://example.com" + ) + val actual = apRequestServiceImpl.apPost("https://example.com", body, null, body::class.java) + + assertThat(body).isEqualTo(actual) + } +} diff --git a/src/test/kotlin/utils/UserBuilder.kt b/src/test/kotlin/utils/UserBuilder.kt index 9d8116cd..d3294786 100644 --- a/src/test/kotlin/utils/UserBuilder.kt +++ b/src/test/kotlin/utils/UserBuilder.kt @@ -13,7 +13,7 @@ object UserBuilder { private val idGenerator = TwitterSnowflakeIdGenerateService - suspend fun localUserOf( + fun localUserOf( id: Long = generateId(), name: String = "test-user-$id", domain: String = "example.com", @@ -49,6 +49,40 @@ object UserBuilder { ) } + fun remoteUserOf( + id: Long = generateId(), + name: String = "test-user-$id", + domain: String = "remote.example.com", + screenName: String = name, + description: String = "This user is test user.", + inbox: String = "https://$domain/$id/inbox", + outbox: String = "https://$domain/$id/outbox", + url: String = "https://$domain/$id/", + publicKey: String = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", + createdAt: Instant = Instant.now(), + keyId: String = "https://$domain/$id#pubkey", + followers: String = "https://$domain/$id/followers", + following: String = "https://$domain/$id/following" + ): User { + return userBuilder.of( + id = id, + name = name, + domain = domain, + screenName = screenName, + description = description, + password = null, + inbox = inbox, + outbox = outbox, + url = url, + publicKey = publicKey, + privateKey = null, + createdAt = createdAt, + keyId = keyId, + followers = following, + following = followers + ) + } + private fun generateId(): Long = runBlocking { idGenerator.generateId() } From 93fd726ffbc84d58e5cd20305a1e31bdb3c544c6 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:25:52 +0900 Subject: [PATCH 07/14] =?UTF-8?q?test:=20APSendFollowServiceImpl=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 --- .../service/ap/APSendFollowServiceImplTest.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/test/kotlin/dev/usbharu/hideout/service/ap/APSendFollowServiceImplTest.kt diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APSendFollowServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APSendFollowServiceImplTest.kt new file mode 100644 index 00000000..30d9b362 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APSendFollowServiceImplTest.kt @@ -0,0 +1,36 @@ +package dev.usbharu.hideout.service.ap + +import dev.usbharu.hideout.domain.model.ap.Follow +import dev.usbharu.hideout.domain.model.hideout.dto.SendFollowDto +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import utils.UserBuilder + +class APSendFollowServiceImplTest { + @Test + fun `sendFollow フォローするユーザーのinboxにFollowオブジェクトが送られる`() = runTest { + val apRequestService = mock() + val apSendFollowServiceImpl = APSendFollowServiceImpl(apRequestService) + + val sendFollowDto = SendFollowDto( + UserBuilder.localUserOf(), + UserBuilder.remoteUserOf() + ) + apSendFollowServiceImpl.sendFollow(sendFollowDto) + + val value = Follow( + name = "Follow", + `object` = sendFollowDto.followTargetUserId.url, + actor = sendFollowDto.userId.url + ) + verify(apRequestService, times(1)).apPost( + eq(sendFollowDto.followTargetUserId.inbox), + eq(value), + eq(sendFollowDto.userId) + ) + } +} From 031678f805c9033153c9faa2b73ef968ab64194e Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:48:03 +0900 Subject: [PATCH 08/14] =?UTF-8?q?test:=20APServiceImpl=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 --- .../hideout/service/ap/APServiceImplTest.kt | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 src/test/kotlin/dev/usbharu/hideout/service/ap/APServiceImplTest.kt diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APServiceImplTest.kt new file mode 100644 index 00000000..c3140427 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APServiceImplTest.kt @@ -0,0 +1,270 @@ +package dev.usbharu.hideout.service.ap + +import dev.usbharu.hideout.exception.JsonParseException +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.mock +import utils.JsonObjectMapper.objectMapper +import kotlin.test.assertEquals + +class APServiceImplTest { + @Test + fun `parseActivity 正常なActivityをパースできる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + val activityType = apServiceImpl.parseActivity("""{"type": "Follow"}""") + + assertEquals(ActivityType.Follow, activityType) + } + + @Test + fun `parseActivity Typeが配列のActivityをパースできる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + val activityType = apServiceImpl.parseActivity("""{"type": ["Follow"]}""") + + assertEquals(ActivityType.Follow, activityType) + } + + @Test + fun `parseActivity Typeが配列で関係ない物が入っていてもパースできる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + val activityType = apServiceImpl.parseActivity("""{"type": ["Hello","Follow"]}""") + + assertEquals(ActivityType.Follow, activityType) + } + + @Test + fun `parseActivity jsonとして解釈できない場合JsonParseExceptionがthrowされる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + assertThrows { + apServiceImpl.parseActivity("""hoge""") + } + } + + @Test + fun `parseActivity 空の場合JsonParseExceptionがthrowされる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + assertThrows { + apServiceImpl.parseActivity("") + } + } + + @Test + fun `parseActivity jsonにtypeプロパティがない場合JsonParseExceptionがthrowされる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + assertThrows { + apServiceImpl.parseActivity("""{"actor": "https://example.com"}""") + } + } + + @Test + fun `parseActivity typeが配列でないときtypeが未定義の場合IllegalArgumentExceptionがthrowされる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + assertThrows { + apServiceImpl.parseActivity("""{"type": "Hoge"}""") + } + } + + @Test + fun `parseActivity typeが配列のとき定義済みのtypeを見つけられなかった場合IllegalArgumentExceptionがthrowされる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + assertThrows { + apServiceImpl.parseActivity("""{"type": ["Hoge","Fuga"]}""") + } + } + + @Test + fun `parseActivity typeが空の場合IllegalArgumentExceptionがthrowされる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + assertThrows { + apServiceImpl.parseActivity("""{"type": ""}""") + } + } + + @Test + fun `parseActivity typeに指定されている文字の判定がcase-insensitiveで行われる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + val activityType = apServiceImpl.parseActivity("""{"type": "FoLlOw"}""") + + assertEquals(ActivityType.Follow, activityType) + } + + @Test + fun `parseActivity typeが配列のとき指定されている文字の判定がcase-insensitiveで行われる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + val activityType = apServiceImpl.parseActivity("""{"type": ["HoGE","fOllOw"]}""") + + assertEquals(ActivityType.Follow, activityType) + } + + @Test + fun `parseActivity activityがarrayのときJsonParseExceptionがthrowされる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + assertThrows { + apServiceImpl.parseActivity("""[{"type": "Follow"},{"type": "Accept"}]""") + } + } + + @Test + fun `parseActivity activityがvalueのときJsonParseExceptionがthrowされる`() { + val apServiceImpl = APServiceImpl( + apReceiveFollowService = mock(), + apUndoService = mock(), + apAcceptService = mock(), + apCreateService = mock(), + apLikeService = mock(), + objectMapper = objectMapper, + apReceiveFollowJobService = mock(), + apNoteJobService = mock(), + apReactionJobService = mock() + ) + + //language=JSON + assertThrows { + apServiceImpl.parseActivity(""""hoge"""") + } + } +} From d884387bcf1254c141a1c1020cf04c0c77920b2e Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:15:49 +0900 Subject: [PATCH 09/14] =?UTF-8?q?test:=20APUndoServiceImpl=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 --- .../service/ap/APUndoServiceImplTest.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/test/kotlin/dev/usbharu/hideout/service/ap/APUndoServiceImplTest.kt diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APUndoServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APUndoServiceImplTest.kt new file mode 100644 index 00000000..3eb01c14 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APUndoServiceImplTest.kt @@ -0,0 +1,48 @@ +package dev.usbharu.hideout.service.ap + +import dev.usbharu.hideout.domain.model.ActivityPubStringResponse +import dev.usbharu.hideout.domain.model.ap.Follow +import dev.usbharu.hideout.domain.model.ap.Undo +import dev.usbharu.hideout.query.UserQueryService +import io.ktor.http.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import utils.TestTransaction +import utils.UserBuilder +import java.time.Instant + +class APUndoServiceImplTest { + @Test + fun `receiveUndo FollowのUndoを処理できる`() = runTest { + + val userQueryService = mock { + onBlocking { findByUrl(eq("https://follower.example.com/actor")) } doReturn UserBuilder.remoteUserOf() + onBlocking { findByUrl(eq("https://example.com/actor")) } doReturn UserBuilder.localUserOf() + } + val apUndoServiceImpl = APUndoServiceImpl( + userService = mock(), + apUserService = mock(), + userQueryService = userQueryService, + transaction = TestTransaction + ) + + val undo = Undo( + name = "Undo", + actor = "https://follower.example.com/actor", + id = "https://follower.example.com/undo/follow", + `object` = Follow( + name = "Follow", + `object` = "https://example.com/actor", + actor = "https://follower.example.com/actor" + ), + published = Instant.now() + ) + val activityPubResponse = apUndoServiceImpl.receiveUndo(undo) + assertEquals(ActivityPubStringResponse(HttpStatusCode.OK, "Accept"), activityPubResponse) + } + +} From 51a477d5b9607212907cc2a73a1ee0b8c2c83825 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:24:15 +0900 Subject: [PATCH 10/14] =?UTF-8?q?fix:=20Activity=E3=81=AEType=E3=81=AE?= =?UTF-8?q?=E3=83=91=E3=83=BC=E3=82=B9=E6=99=82=E3=81=AE=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usbharu/hideout/service/ap/APService.kt | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt index 9cd1cedf..a1483b00 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt @@ -186,7 +186,11 @@ class APServiceImpl( val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java) override fun parseActivity(json: String): ActivityType { - val readTree = objectMapper.readTree(json) + val readTree = try { + objectMapper.readTree(json) + } catch (e: com.fasterxml.jackson.core.JsonParseException) { + throw JsonParseException("Failed to parse json", e) + } logger.trace( """ | @@ -204,11 +208,19 @@ class APServiceImpl( } val type = readTree["type"] ?: throw JsonParseException("Type is null") if (type.isArray) { - return type.firstNotNullOf { jsonNode: JsonNode -> - ActivityType.values().firstOrNull { it.name.equals(jsonNode.asText(), true) } + try { + return type.firstNotNullOf { jsonNode: JsonNode -> + ActivityType.values().firstOrNull { it.name.equals(jsonNode.asText(), true) } + } + } catch (e: NoSuchElementException) { + throw IllegalArgumentException("No valid TYPE", e) } } - return ActivityType.values().first { it.name.equals(type.asText(), true) } + try { + return ActivityType.values().first { it.name.equals(type.asText(), true) } + } catch (e: NoSuchElementException) { + throw IllegalArgumentException("No valid TYPE", e) + } } @Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration") @@ -219,6 +231,7 @@ class APServiceImpl( ActivityType.Follow -> apReceiveFollowService .receiveFollow(objectMapper.readValue(json, Follow::class.java)) + ActivityType.Create -> apCreateService.receiveCreate(objectMapper.readValue(json)) ActivityType.Like -> apLikeService.receiveLike(objectMapper.readValue(json)) ActivityType.Undo -> apUndoService.receiveUndo(objectMapper.readValue(json)) From f84dc530fa81bb284cf82f4a87c0796399764fe8 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:26:46 +0900 Subject: [PATCH 11/14] =?UTF-8?q?fix:=20Follow=E3=81=AEUndo=E3=81=AE?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=81=AB=E6=88=90=E5=8A=9F=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=82=8B=E3=81=AE=E3=81=AB=E6=9C=AA=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=8C=E5=87=BA=E3=82=8B=E3=81=AE?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoService.kt index db4fe99c..a9e8f176 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APUndoService.kt @@ -45,6 +45,7 @@ class APUndoServiceImpl( val target = userQueryService.findByUrl(follow.`object`!!) userService.unfollow(target.id, follower.id) } + return ActivityPubStringResponse(HttpStatusCode.OK, "Accept") } else -> {} From b514af026f47414b7de54549006a5f74fbe2e219 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:27:17 +0900 Subject: [PATCH 12/14] =?UTF-8?q?fix:=20apPost=E6=99=82=E3=81=ABbody?= =?UTF-8?q?=E3=81=8Cnull=E3=81=AE=E3=81=A8=E3=81=8D=E3=83=AA=E3=82=AF?= =?UTF-8?q?=E3=82=A8=E3=82=B9=E3=83=88=E3=83=9C=E3=83=87=E3=82=A3=E3=81=AB?= =?UTF-8?q?null=E3=81=A8=E5=87=BA=E5=8A=9B=E3=81=95=E3=82=8C=E3=81=A6?= =?UTF-8?q?=E3=81=97=E3=81=BE=E3=81=86=E5=95=8F=E9=A1=8C=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 --- .../hideout/service/ap/APRequestServiceImpl.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt index 37a76333..f264d4cc 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt @@ -98,14 +98,17 @@ class APRequestServiceImpl( override suspend fun apPost(url: String, body: T?, signer: User?): String { logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url) - if (body != null) { + val requestBody = if (body != null) { val mutableListOf = mutableListOf() mutableListOf.add("https://www.w3.org/ns/activitystreams") mutableListOf.addAll(body.context) body.context = mutableListOf + objectMapper.writeValueAsString(body) + } else { + null } - val requestBody = objectMapper.writeValueAsString(body) + logger.trace( """ @@ -123,17 +126,19 @@ class APRequestServiceImpl( val sha256 = MessageDigest.getInstance("SHA-256") - val digest = Base64Util.encode(sha256.digest(requestBody.toByteArray())) + val digest = Base64Util.encode(sha256.digest(requestBody.orEmpty().toByteArray())) val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT"))) val u = URL(url) if (signer?.privateKey == null) { val bodyAsText = httpClient.post(url) { - header("Accept", ContentType.Application.Activity) + accept(ContentType.Application.Activity) header("Date", date) header("Digest", "sha-256=$digest") - setBody(requestBody) - contentType(ContentType.Application.Activity) + if (requestBody != null) { + setBody(requestBody) + contentType(ContentType.Application.Activity) + } }.bodyAsText() logBody(bodyAsText, url) return bodyAsText From 617a8c5a6f89473ab3f9969e70d11d2ab43a2d9e Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:35:05 +0900 Subject: [PATCH 13/14] =?UTF-8?q?refactor:=20=E4=B8=8D=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E4=BE=9D=E5=AD=98=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usbharu/hideout/service/ap/APService.kt | 8 +-- .../hideout/service/ap/APServiceImplTest.kt | 65 ++++--------------- 2 files changed, 14 insertions(+), 59 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt index a1483b00..8ea18249 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APService.kt @@ -6,9 +6,6 @@ import com.fasterxml.jackson.module.kotlin.readValue import dev.usbharu.hideout.domain.model.ActivityPubResponse import dev.usbharu.hideout.domain.model.ap.Follow import dev.usbharu.hideout.exception.JsonParseException -import dev.usbharu.hideout.service.ap.job.APReceiveFollowJobService -import dev.usbharu.hideout.service.ap.job.ApNoteJobService -import dev.usbharu.hideout.service.ap.job.ApReactionJobService import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Qualifier @@ -178,10 +175,7 @@ class APServiceImpl( private val apAcceptService: APAcceptService, private val apCreateService: APCreateService, private val apLikeService: APLikeService, - @Qualifier("activitypub") private val objectMapper: ObjectMapper, - private val apReceiveFollowJobService: APReceiveFollowJobService, - private val apNoteJobService: ApNoteJobService, - private val apReactionJobService: ApReactionJobService + @Qualifier("activitypub") private val objectMapper: ObjectMapper ) : APService { val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java) diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APServiceImplTest.kt index c3140427..e96cc491 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/APServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APServiceImplTest.kt @@ -16,10 +16,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -36,10 +33,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -56,10 +50,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -76,10 +67,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -96,10 +84,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -116,10 +101,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -136,10 +118,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -156,10 +135,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -176,10 +152,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -196,10 +169,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -216,10 +186,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -236,10 +203,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON @@ -256,10 +220,7 @@ class APServiceImplTest { apAcceptService = mock(), apCreateService = mock(), apLikeService = mock(), - objectMapper = objectMapper, - apReceiveFollowJobService = mock(), - apNoteJobService = mock(), - apReactionJobService = mock() + objectMapper = objectMapper ) //language=JSON From 5de5c6cb9b07747a2a5caa322af354a4bba993f7 Mon Sep 17 00:00:00 2001 From: usbharu Date: Tue, 31 Oct 2023 16:40:31 +0900 Subject: [PATCH 14/14] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../hideout/domain/model/ActivityPubStringResponse.kt | 6 ------ .../dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt | 2 -- 2 files changed, 8 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt index 26b04f51..b0a546d9 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt @@ -9,7 +9,6 @@ sealed class ActivityPubResponse( val contentType: ContentType = ContentType.Application.Activity ) { - override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is ActivityPubResponse) return false @@ -37,7 +36,6 @@ class ActivityPubStringResponse( contentType: ContentType = ContentType.Application.Activity ) : ActivityPubResponse(httpStatusCode, contentType) { - override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is ActivityPubStringResponse) return false @@ -54,8 +52,6 @@ class ActivityPubStringResponse( override fun toString(): String { return "ActivityPubStringResponse(message='$message') ${super.toString()}" } - - } class ActivityPubObjectResponse( @@ -79,6 +75,4 @@ class ActivityPubObjectResponse( override fun toString(): String { return "ActivityPubObjectResponse(message=$message) ${super.toString()}" } - - } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt index f264d4cc..c4f78ec7 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt @@ -108,8 +108,6 @@ class APRequestServiceImpl( null } - - logger.trace( """ |