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

View File

@ -98,15 +98,16 @@ class APRequestServiceImpl(
override suspend fun <T : Object> 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<String>()
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 +124,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

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.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,15 +175,16 @@ 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)
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 +202,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 +225,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))

View File

@ -45,6 +45,7 @@ class APUndoServiceImpl(
val target = userQueryService.findByUrl(follow.`object`!!)
userService.unfollow(target.id, follower.id)
}
return ActivityPubStringResponse(HttpStatusCode.OK, "Accept")
}
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: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<JobQueueParentService>()
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<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
fun `createPostJob 新しい投稿のJob`() {
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(
userQueryService = mock(),
@ -147,19 +426,15 @@ class APNoteServiceImplTest {
activityPubNoteService.createNoteJob(
JobProps(
data = mapOf<String, Any>(
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
)
)
}

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