diff --git a/build.gradle.kts b/build.gradle.kts index 4dee9ed6..0dd14e61 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -65,6 +65,7 @@ dependencies { testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") + testImplementation("io.ktor:ktor-client-mock:$ktor_version") implementation("io.ktor:ktor-client-core:$ktor_version") implementation("io.ktor:ktor-client-cio:$ktor_version") diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Accept.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Accept.kt index 15014b52..35b822c8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Accept.kt +++ b/src/main/kotlin/dev/usbharu/hideout/ap/Accept.kt @@ -13,4 +13,26 @@ open class Accept : Object { this.`object` = `object` this.actor = actor } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Accept) return false + if (!super.equals(other)) return false + + if (`object` != other.`object`) return false + return actor == other.actor + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (`object`?.hashCode() ?: 0) + result = 31 * result + (actor?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "Accept(`object`=$`object`, actor=$actor) ${super.toString()}" + } + + } diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/JsonLd.kt b/src/main/kotlin/dev/usbharu/hideout/ap/JsonLd.kt index bef45e70..285319bd 100644 --- a/src/main/kotlin/dev/usbharu/hideout/ap/JsonLd.kt +++ b/src/main/kotlin/dev/usbharu/hideout/ap/JsonLd.kt @@ -37,6 +37,10 @@ open class JsonLd { return context.hashCode() } + override fun toString(): String { + return "JsonLd(context=$context)" + } + } diff --git a/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt b/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt index 1a05564d..27362b30 100644 --- a/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt +++ b/src/main/kotlin/dev/usbharu/hideout/ap/Object.kt @@ -39,6 +39,10 @@ open class Object : JsonLd { return result } + override fun toString(): String { + return "Object(type=$type, name=$name) ${super.toString()}" + } + } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt index 294a1513..2e4b8b4d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ActivityPubStringResponse.kt @@ -10,14 +10,14 @@ sealed class ActivityPubResponse( ) class ActivityPubStringResponse( - httpStatusCode: HttpStatusCode, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, val message: String, contentType: ContentType = ContentType.Application.Activity ) : ActivityPubResponse(httpStatusCode, contentType) class ActivityPubObjectResponse( - httpStatusCode: HttpStatusCode, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, val message: JsonLd, contentType: ContentType = ContentType.Application.Activity ) : diff --git a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt new file mode 100644 index 00000000..d447ce3e --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt @@ -0,0 +1,153 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + +package dev.usbharu.hideout.service.activitypub + +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.ap.* +import dev.usbharu.hideout.config.Config +import dev.usbharu.hideout.config.ConfigData +import dev.usbharu.hideout.domain.model.UserEntity +import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob +import dev.usbharu.hideout.service.impl.UserService +import dev.usbharu.hideout.service.job.JobQueueParentService +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import kjob.core.dsl.ScheduleContext +import kjob.core.job.JobProps +import kotlinx.coroutines.ExperimentalCoroutinesApi +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.mockito.ArgumentMatchers.anyString +import org.mockito.kotlin.* +import utils.JsonObjectMapper + +class ActivityPubFollowServiceImplTest { + @Test + fun `receiveFollow フォロー受付処理`() = runTest { + val jobQueueParentService = mock { + onBlocking { schedule(eq(ReceiveFollowJob), any()) } doReturn Unit + } + val activityPubFollowService = ActivityPubFollowServiceImpl(jobQueueParentService, mock(), mock(), mock()) + activityPubFollowService.receiveFollow( + Follow( + emptyList(), + "Follow", + "https://example.com", + "https://follower.example.com" + ) + ) + verify(jobQueueParentService, times(1)).schedule(eq(ReceiveFollowJob), any()) + argumentCaptor.(ReceiveFollowJob) -> Unit> { + verify(jobQueueParentService, times(1)).schedule(eq(ReceiveFollowJob), capture()) + val scheduleContext = ScheduleContext(Json) + firstValue.invoke(scheduleContext, ReceiveFollowJob) + val actor = scheduleContext.props.props[ReceiveFollowJob.actor.name] + val targetActor = scheduleContext.props.props[ReceiveFollowJob.targetActor.name] + val follow = scheduleContext.props.props[ReceiveFollowJob.follow.name] + assertEquals("https://follower.example.com", actor) + assertEquals("https://example.com", targetActor) + assertEquals( + """{"type":"Follow","name":"Follow","object":"https://example.com","actor":"https://follower.example.com","@context":null}""", + follow + ) + } + } + + @Test + fun `receiveFollowJob フォロー受付処理のJob`() = runTest { + Config.configData = ConfigData(objectMapper = JsonObjectMapper.objectMapper) + val person = Person( + type = emptyList(), + name = "follower", + id = "https://follower.example.com", + preferredUsername = "followerUser", + summary = "This user is follower user.", + inbox = "https://follower.example.com/inbox", + outbox = "https://follower.example.com/outbox", + url = "https://follower.example.com", + icon = Image( + type = emptyList(), + name = "https://follower.example.com/image", + mediaType = "image/png", + url = "https://follower.example.com/image" + ), + publicKey = Key( + type = emptyList(), + name = "Public Key", + id = "https://follower.example.com#main-key", + owner = "https://follower.example.com", + publicKeyPem = "BEGIN PUBLIC KEY...END PUBLIC KEY", + ) + + ) + val activityPubUserService = mock { + onBlocking { fetchPerson(anyString()) } doReturn person + } + val userService = mock { + onBlocking { findByUrls(any()) } doReturn listOf( + UserEntity( + id = 1L, + name = "test", + domain = "example.com", + screenName = "testUser", + description = "This user is test user.", + inbox = "https://example.com/inbox", + outbox = "https://example.com/outbox", + url = "https://example.com" + ), + UserEntity( + id = 2L, + name = "follower", + domain = "follower.example.com", + screenName = "followerUser", + description = "This user is test follower user.", + inbox = "https://follower.example.com/inbox", + outbox = "https://follower.example.com/outbox", + url = "https://follower.example.com" + ) + ) + onBlocking { addFollowers(any(), any()) } doReturn Unit + } + val activityPubFollowService = + ActivityPubFollowServiceImpl( + mock(), + activityPubUserService, + userService, + HttpClient(MockEngine { httpRequestData -> + assertEquals(person.inbox, httpRequestData.url.toString()) + val accept = Accept( + type = emptyList(), + name = "Follow", + `object` = Follow( + type = emptyList(), + name = "Follow", + `object` = "https://example.com", + actor = "https://follower.example.com" + ), + actor = "https://example.com" + ) + accept.context += "https://www.w3.org/ns/activitystreams" + assertEquals( + accept, + Config.configData.objectMapper.readValue( + httpRequestData.body.toByteArray().decodeToString() + ) + ) + respondOk() + }) + ) + activityPubFollowService.receiveFollowJob( + JobProps( + data = mapOf( + ReceiveFollowJob.actor.name to "https://follower.example.com", + ReceiveFollowJob.targetActor.name to "https://example.com", + ReceiveFollowJob.follow.name to """{"type":"Follow","name":"Follow","object":"https://example.com","actor":"https://follower.example.com","@context":null}""" + ), + json = Json + ) + ) + } +} diff --git a/src/test/kotlin/utils/JsonObjectMapper.kt b/src/test/kotlin/utils/JsonObjectMapper.kt new file mode 100644 index 00000000..4a595630 --- /dev/null +++ b/src/test/kotlin/utils/JsonObjectMapper.kt @@ -0,0 +1,22 @@ +package utils + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonSetter +import com.fasterxml.jackson.annotation.Nulls +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper + +object JsonObjectMapper { + val objectMapper: com.fasterxml.jackson.databind.ObjectMapper = + jacksonObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + init { + objectMapper.configOverride(List::class.java).setSetterInfo( + JsonSetter.Value.forValueNulls( + Nulls.AS_EMPTY + ) + ) + } +}