Merge pull request #102 from usbharu/feature/test

Feature/test
This commit is contained in:
usbharu 2023-10-31 16:47:02 +09:00 committed by GitHub
commit 89e1675611
15 changed files with 1574 additions and 68 deletions

View File

@ -7,18 +7,72 @@ import io.ktor.http.*
sealed class ActivityPubResponse( sealed class ActivityPubResponse(
val httpStatusCode: HttpStatusCode, val httpStatusCode: HttpStatusCode,
val contentType: ContentType = ContentType.Application.Activity 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( class ActivityPubStringResponse(
httpStatusCode: HttpStatusCode = HttpStatusCode.OK, httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
val message: String, val message: String,
contentType: ContentType = ContentType.Application.Activity 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( class ActivityPubObjectResponse(
httpStatusCode: HttpStatusCode = HttpStatusCode.OK, httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
val message: JsonLd, val message: JsonLd,
contentType: ContentType = ContentType.Application.Activity 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()}"
}
}

View File

@ -98,15 +98,16 @@ class APRequestServiceImpl(
override suspend fun <T : Object> apPost(url: String, body: T?, signer: User?): String { override suspend fun <T : Object> apPost(url: String, body: T?, signer: User?): String {
logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url) logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url)
if (body != null) { val requestBody = if (body != null) {
val mutableListOf = mutableListOf<String>() val mutableListOf = mutableListOf<String>()
mutableListOf.add("https://www.w3.org/ns/activitystreams") mutableListOf.add("https://www.w3.org/ns/activitystreams")
mutableListOf.addAll(body.context) mutableListOf.addAll(body.context)
body.context = mutableListOf body.context = mutableListOf
objectMapper.writeValueAsString(body)
} else {
null
} }
val requestBody = objectMapper.writeValueAsString(body)
logger.trace( logger.trace(
""" """
| |
@ -123,17 +124,19 @@ class APRequestServiceImpl(
val sha256 = MessageDigest.getInstance("SHA-256") 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 date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
val u = URL(url) val u = URL(url)
if (signer?.privateKey == null) { if (signer?.privateKey == null) {
val bodyAsText = httpClient.post(url) { val bodyAsText = httpClient.post(url) {
header("Accept", ContentType.Application.Activity) accept(ContentType.Application.Activity)
header("Date", date) header("Date", date)
header("Digest", "sha-256=$digest") header("Digest", "sha-256=$digest")
setBody(requestBody) if (requestBody != null) {
contentType(ContentType.Application.Activity) setBody(requestBody)
contentType(ContentType.Application.Activity)
}
}.bodyAsText() }.bodyAsText()
logBody(bodyAsText, url) logBody(bodyAsText, url)
return bodyAsText return bodyAsText

View File

@ -6,9 +6,6 @@ import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.domain.model.ActivityPubResponse import dev.usbharu.hideout.domain.model.ActivityPubResponse
import dev.usbharu.hideout.domain.model.ap.Follow import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.exception.JsonParseException 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.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
@ -178,15 +175,16 @@ class APServiceImpl(
private val apAcceptService: APAcceptService, private val apAcceptService: APAcceptService,
private val apCreateService: APCreateService, private val apCreateService: APCreateService,
private val apLikeService: APLikeService, private val apLikeService: APLikeService,
@Qualifier("activitypub") private val objectMapper: ObjectMapper, @Qualifier("activitypub") private val objectMapper: ObjectMapper
private val apReceiveFollowJobService: APReceiveFollowJobService,
private val apNoteJobService: ApNoteJobService,
private val apReactionJobService: ApReactionJobService
) : APService { ) : APService {
val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java) val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java)
override fun parseActivity(json: String): ActivityType { 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( logger.trace(
""" """
| |
@ -204,11 +202,19 @@ class APServiceImpl(
} }
val type = readTree["type"] ?: throw JsonParseException("Type is null") val type = readTree["type"] ?: throw JsonParseException("Type is null")
if (type.isArray) { if (type.isArray) {
return type.firstNotNullOf { jsonNode: JsonNode -> try {
ActivityType.values().firstOrNull { it.name.equals(jsonNode.asText(), true) } 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") @Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration")
@ -219,6 +225,7 @@ class APServiceImpl(
ActivityType.Follow -> ActivityType.Follow ->
apReceiveFollowService apReceiveFollowService
.receiveFollow(objectMapper.readValue(json, Follow::class.java)) .receiveFollow(objectMapper.readValue(json, Follow::class.java))
ActivityType.Create -> apCreateService.receiveCreate(objectMapper.readValue(json)) ActivityType.Create -> apCreateService.receiveCreate(objectMapper.readValue(json))
ActivityType.Like -> apLikeService.receiveLike(objectMapper.readValue(json)) ActivityType.Like -> apLikeService.receiveLike(objectMapper.readValue(json))
ActivityType.Undo -> apUndoService.receiveUndo(objectMapper.readValue(json)) ActivityType.Undo -> apUndoService.receiveUndo(objectMapper.readValue(json))

View File

@ -45,6 +45,7 @@ class APUndoServiceImpl(
val target = userQueryService.findByUrl(follow.`object`!!) val target = userQueryService.findByUrl(follow.`object`!!)
userService.unfollow(target.id, follower.id) userService.unfollow(target.id, follower.id)
} }
return ActivityPubStringResponse(HttpStatusCode.OK, "Accept")
} }
else -> {} else -> {}

View File

@ -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<UserQueryService> {
onBlocking { findByUrl(eq(actor)) } doReturn targetUser
onBlocking { findByUrl(eq(follower)) } doReturn followerUser
}
val followerQueryService = mock<FollowerQueryService> {
onBlocking { alreadyFollow(eq(targetUser.id), eq(followerUser.id)) } doReturn false
}
val userService = mock<UserService>()
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<UserQueryService> {
onBlocking { findByUrl(eq(actor)) } doReturn targetUser
onBlocking { findByUrl(eq(follower)) } doReturn followerUser
}
val followerQueryService = mock<FollowerQueryService> {
onBlocking { alreadyFollow(eq(targetUser.id), eq(followerUser.id)) } doReturn true
}
val userService = mock<UserService>()
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<IllegalActivityPubObjectException> {
apAcceptServiceImpl.receiveAccept(accept)
}
}
}

View File

@ -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<APNoteService>()
val apCreateServiceImpl = APCreateServiceImpl(apNoteService, TestTransaction)
val actual = ActivityPubStringResponse(HttpStatusCode.OK, "Created")
val receiveCreate = apCreateServiceImpl.receiveCreate(create)
verify(apNoteService, times(1)).fetchNote(any<Note>(), 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<IllegalActivityPubObjectException> {
apCreateServiceImpl.receiveCreate(create)
}
}
}

View File

@ -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<APUserService> {
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<APNoteService> {
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<PostQueryService> {
onBlocking { findByUrl(eq(note)) } doReturn post
}
val reactionService = mock<ReactionService>()
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<APUserService> {
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<APNoteService> {
on { fetchNoteAsync(eq(note), anyOrNull()) } doThrow FailedToGetActivityPubResourceException()
}
val reactionService = mock<ReactionService>()
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)
}
}

View File

@ -1,34 +1,58 @@
@file:OptIn(ExperimentalCoroutinesApi::class) @file:OptIn(ExperimentalCoroutinesApi::class) @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
package dev.usbharu.hideout.service.ap package dev.usbharu.hideout.service.ap
import dev.usbharu.hideout.config.ApplicationConfig import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.config.CharacterLimit 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.Post
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.domain.model.job.DeliverPostJob 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.FollowerQueryService
import dev.usbharu.hideout.query.MediaQueryService import dev.usbharu.hideout.query.MediaQueryService
import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService 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.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.job.JobQueueParentService
import dev.usbharu.hideout.service.post.PostService
import io.ktor.client.* 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 kjob.core.job.JobProps
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.mockito.Mockito.anyLong import org.mockito.Mockito.anyLong
import org.mockito.Mockito.eq
import org.mockito.kotlin.* import org.mockito.kotlin.*
import utils.JsonObjectMapper.objectMapper import utils.JsonObjectMapper.objectMapper
import utils.PostBuilder
import utils.TestTransaction import utils.TestTransaction
import utils.UserBuilder
import java.net.URL import java.net.URL
import java.time.Instant import java.time.Instant
import kotlin.test.assertEquals
class APNoteServiceImplTest { class APNoteServiceImplTest {
@ -58,8 +82,7 @@ class APNoteServiceImplTest {
publicKey = "", publicKey = "",
createdAt = Instant.now(), createdAt = Instant.now(),
keyId = "a" keyId = "a"
), ), userBuilder.of(
userBuilder.of(
3L, 3L,
"follower2", "follower2",
"follower2.example.com", "follower2.example.com",
@ -95,47 +118,303 @@ class APNoteServiceImplTest {
onBlocking { findFollowersById(eq(1L)) } doReturn followers onBlocking { findFollowersById(eq(1L)) } doReturn followers
} }
val jobQueueParentService = mock<JobQueueParentService>() val jobQueueParentService = mock<JobQueueParentService>()
val activityPubNoteService = val activityPubNoteService = APNoteServiceImpl(
APNoteServiceImpl( jobQueueParentService = jobQueueParentService,
jobQueueParentService = jobQueueParentService, postRepository = mock(),
postRepository = mock(), apUserService = mock(),
apUserService = mock(), userQueryService = userQueryService,
userQueryService = userQueryService, followerQueryService = followerQueryService,
followerQueryService = followerQueryService, postQueryService = mock(),
postQueryService = mock(), mediaQueryService = mediaQueryService,
mediaQueryService = mediaQueryService, objectMapper = objectMapper,
objectMapper = objectMapper, postService = mock(),
postService = mock(), apResourceResolveService = mock(),
apResourceResolveService = mock(), postBuilder = postBuilder
postBuilder = postBuilder )
)
val postEntity = postBuilder.of( val postEntity = postBuilder.of(
1L, 1L, 1L, null, "test text", 1L, Visibility.PUBLIC, "https://example.com"
1L,
null,
"test text",
1L,
Visibility.PUBLIC,
"https://example.com"
) )
activityPubNoteService.createNote(postEntity) activityPubNoteService.createNote(postEntity)
verify(jobQueueParentService, times(2)).schedule(eq(DeliverPostJob), any()) 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<PostQueryService> {
onBlocking { findByUrl(eq(url)) } doReturn post
}
val user = UserBuilder.localUserOf(id = post.userId)
val userQueryService = mock<UserQueryService> {
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<PostQueryService> {
onBlocking { findByUrl(eq(url)) } doThrow FailedToGetResourcesException()
onBlocking { findByApId(eq(post.apId)) } doReturn post
}
val user = UserBuilder.localUserOf(id = post.userId)
val userQueryService = mock<UserQueryService> {
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<APResourceResolveService> {
onBlocking { resolve<Note>(eq(url), any(), isNull<Long>()) } 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<PostQueryService> {
onBlocking { findByUrl(eq(url)) } doThrow FailedToGetResourcesException()
onBlocking { findByApId(eq(post.apId)) } doReturn post
}
val user = UserBuilder.localUserOf(id = post.userId)
val userQueryService = mock<UserQueryService> {
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<APResourceResolveService> {
val responseData = HttpResponseData(
HttpStatusCode.BadRequest,
GMTDate(),
Headers.Empty,
HttpProtocolVersion.HTTP_1_1,
NullBody,
Dispatchers.IO
)
onBlocking { resolve<Note>(eq(url), any(), isNull<Long>()) } 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<FailedToGetActivityPubResourceException> { 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<PostQueryService> {
onBlocking { findByApId(eq(post.apId)) } doThrow FailedToGetResourcesException()
}
val postRepository = mock<PostRepository> {
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<APUserService> {
onBlocking { fetchPersonWithEntity(eq(user.url), anyOrNull()) } doReturn (person to user)
}
val postService = mock<PostService>()
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<PostQueryService> {
onBlocking { findByApId(eq(post.apId)) } doReturn post
}
val userQueryService = mock<UserQueryService> {
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 @Test
fun `createPostJob 新しい投稿のJob`() { fun `createPostJob 新しい投稿のJob`() {
runTest { runTest {
val mediaQueryService = mock<MediaQueryService> {
onBlocking { findByPostId(anyLong()) } doReturn emptyList()
}
val httpClient = HttpClient(
MockEngine { httpRequestData ->
assertEquals("https://follower.example.com/inbox", httpRequestData.url.toString())
respondOk()
}
)
val activityPubNoteService = ApNoteJobServiceImpl( val activityPubNoteService = ApNoteJobServiceImpl(
userQueryService = mock(), userQueryService = mock(),
@ -147,19 +426,15 @@ class APNoteServiceImplTest {
activityPubNoteService.createNoteJob( activityPubNoteService.createNoteJob(
JobProps( JobProps(
data = mapOf<String, Any>( data = mapOf<String, Any>(
DeliverPostJob.actor.name to "https://follower.example.com", DeliverPostJob.actor.name to "https://follower.example.com", DeliverPostJob.post.name to """{
DeliverPostJob.post.name to """{
"id": 1, "id": 1,
"userId": 1, "userId": 1,
"text": "test text", "text": "test text",
"createdAt": 132525324, "createdAt": 132525324,
"visibility": 0, "visibility": 0,
"url": "https://example.com" "url": "https://example.com"
}""", }""", DeliverPostJob.inbox.name to "https://follower.example.com/inbox", DeliverPostJob.media.name to "[]"
DeliverPostJob.inbox.name to "https://follower.example.com/inbox", ), json = Json
DeliverPostJob.media.name to "[]"
),
json = Json
) )
) )
} }

View File

@ -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<PostQueryService> {
onBlocking { findById(eq(post.id)) } doReturn post
}
val followerQueryService = mock<FollowerQueryService> {
onBlocking { findFollowersById(eq(user.id)) } doReturn listOf(
UserBuilder.localUserOf(),
UserBuilder.localUserOf(),
UserBuilder.localUserOf()
)
}
val jobQueueParentService = mock<JobQueueParentService>()
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<PostQueryService> {
onBlocking { findById(eq(post.id)) } doReturn post
}
val followerQueryService = mock<FollowerQueryService> {
onBlocking { findFollowersById(eq(user.id)) } doReturn listOf(
UserBuilder.localUserOf(),
UserBuilder.localUserOf(),
UserBuilder.localUserOf()
)
}
val jobQueueParentService = mock<JobQueueParentService>()
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())
}
}

View File

@ -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<HttpSignatureSigner> {
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<Follow>(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<Follow>(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<Follow>(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<HttpSignatureSigner> {
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<Follow>(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<Follow>(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)
}
}

View File

@ -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<APRequestService>()
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)
)
}
}

View File

@ -0,0 +1,231 @@
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
)
//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
)
//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
)
//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
)
//language=JSON
assertThrows<JsonParseException> {
apServiceImpl.parseActivity("""hoge""")
}
}
@Test
fun `parseActivity 空の場合JsonParseExceptionがthrowされる`() {
val apServiceImpl = APServiceImpl(
apReceiveFollowService = mock(),
apUndoService = mock(),
apAcceptService = mock(),
apCreateService = mock(),
apLikeService = mock(),
objectMapper = objectMapper
)
//language=JSON
assertThrows<JsonParseException> {
apServiceImpl.parseActivity("")
}
}
@Test
fun `parseActivity jsonにtypeプロパティがない場合JsonParseExceptionがthrowされる`() {
val apServiceImpl = APServiceImpl(
apReceiveFollowService = mock(),
apUndoService = mock(),
apAcceptService = mock(),
apCreateService = mock(),
apLikeService = mock(),
objectMapper = objectMapper
)
//language=JSON
assertThrows<JsonParseException> {
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
)
//language=JSON
assertThrows<IllegalArgumentException> {
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
)
//language=JSON
assertThrows<IllegalArgumentException> {
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
)
//language=JSON
assertThrows<IllegalArgumentException> {
apServiceImpl.parseActivity("""{"type": ""}""")
}
}
@Test
fun `parseActivity typeに指定されている文字の判定がcase-insensitiveで行われる`() {
val apServiceImpl = APServiceImpl(
apReceiveFollowService = mock(),
apUndoService = mock(),
apAcceptService = mock(),
apCreateService = mock(),
apLikeService = mock(),
objectMapper = objectMapper
)
//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
)
//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
)
//language=JSON
assertThrows<JsonParseException> {
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
)
//language=JSON
assertThrows<IllegalArgumentException> {
apServiceImpl.parseActivity(""""hoge"""")
}
}
}

View File

@ -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<UserQueryService> {
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)
}
}

View File

@ -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()
}
}

View File

@ -0,0 +1,89 @@
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
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
)
}
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()
}
}