mirror of https://github.com/usbharu/Hideout.git
commit
89e1675611
|
@ -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()}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -45,6 +45,7 @@ class APUndoServiceImpl(
|
|||
val target = userQueryService.findByUrl(follow.`object`!!)
|
||||
userService.unfollow(target.id, follower.id)
|
||||
}
|
||||
return ActivityPubStringResponse(HttpStatusCode.OK, "Accept")
|
||||
}
|
||||
|
||||
else -> {}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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"""")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue