Compare commits

..

4 Commits

6 changed files with 436 additions and 7 deletions

View File

@ -32,6 +32,11 @@ tasks.withType<Test> {
val cpus = Runtime.getRuntime().availableProcessors()
maxParallelForks = max(1, cpus - 1)
setForkEvery(4)
doFirst {
jvmArgs = arrayOf(
"--add-opens", "java.base/java.lang=ALL-UNNAMED"
).toMutableList()
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {
@ -160,7 +165,8 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
testImplementation("org.mockito:mockito-inline:5.2.0")
testImplementation("nl.jqno.equalsverifier:equalsverifier:3.15.3")
testImplementation("com.jparams:to-string-verifier:1.4.8")
implementation("org.drewcarlson:kjob-core:0.6.0")
implementation("org.drewcarlson:kjob-mongo:0.6.0")

View File

@ -0,0 +1,42 @@
package dev.usbharu.hideout
import com.jparams.verifier.tostring.ToStringVerifier
import nl.jqno.equalsverifier.EqualsVerifier
import nl.jqno.equalsverifier.Warning
import nl.jqno.equalsverifier.internal.reflection.PackageScanner
import org.junit.jupiter.api.Test
import java.lang.reflect.Modifier
import kotlin.test.assertFails
class EqualsAndToStringTest {
@Test
fun equalsTest() {
assertFails {
EqualsVerifier
.simple()
.suppress(Warning.INHERITED_DIRECTLY_FROM_OBJECT)
.forPackage("dev.usbharu.hideout", true)
.verify()
}
}
@Test
fun toStringTest() {
PackageScanner.getClassesIn("dev.usbharu.hideout", null, true)
.filter {
it != null && !it.isEnum && !it.isInterface && !Modifier.isAbstract(it.modifiers)
}
.forEach {
try {
ToStringVerifier.forClass(it).verify()
} catch (e: AssertionError) {
println(it.name)
e.printStackTrace()
} catch (e: Exception) {
println(it.name)
e.printStackTrace()
}
}
}
}

View File

@ -0,0 +1,144 @@
package dev.usbharu.hideout.core.service.post
import dev.usbharu.hideout.activitypub.service.activity.create.ApSendCreateService
import dev.usbharu.hideout.application.config.CharacterLimit
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.user.UserRepository
import dev.usbharu.hideout.core.query.PostQueryService
import dev.usbharu.hideout.core.service.timeline.TimelineService
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.mockStatic
import org.mockito.Spy
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.*
import org.springframework.dao.DuplicateKeyException
import utils.PostBuilder
import utils.UserBuilder
import java.time.Instant
@ExtendWith(MockitoExtension::class)
class PostServiceImplTest {
@Mock
private lateinit var postRepository: PostRepository
@Mock
private lateinit var userRepository: UserRepository
@Mock
private lateinit var timelineService: TimelineService
@Mock
private lateinit var postQueryService: PostQueryService
@Spy
private var postBuilder: Post.PostBuilder = Post.PostBuilder(CharacterLimit())
@Mock
private lateinit var apSendCreateService: ApSendCreateService
@InjectMocks
private lateinit var postServiceImpl: PostServiceImpl
@Test
fun `createLocal 正常にpostを作成できる`() = runTest {
val now = Instant.now()
val post = PostBuilder.of(createdAt = now.toEpochMilli())
whenever(postRepository.save(eq(post))).doReturn(true)
whenever(postRepository.generateId()).doReturn(post.id)
whenever(userRepository.findById(eq(post.userId))).doReturn(UserBuilder.localUserOf(id = post.userId))
whenever(timelineService.publishTimeline(eq(post), eq(true))).doReturn(Unit)
mockStatic(Instant::class.java, Mockito.CALLS_REAL_METHODS).use {
it.`when`<Instant>(Instant::now).doReturn(now)
val createLocal = postServiceImpl.createLocal(
PostCreateDto(
post.text,
post.overview,
post.visibility,
post.repostId,
post.replyId,
post.userId,
post.mediaIds
)
)
assertThat(createLocal).isEqualTo(post)
}
verify(postRepository, times(1)).save(eq(post))
verify(timelineService, times(1)).publishTimeline(eq(post), eq(true))
verify(apSendCreateService, times(1)).createNote(eq(post))
}
@Test
fun `createRemote 正常にリモートのpostを作成できる`() = runTest {
val post = PostBuilder.of()
whenever(postRepository.save(eq(post))).doReturn(true)
whenever(timelineService.publishTimeline(eq(post), eq(false))).doReturn(Unit)
val createLocal = postServiceImpl.createRemote(post)
assertThat(createLocal).isEqualTo(post)
verify(postRepository, times(1)).save(eq(post))
verify(timelineService, times(1)).publishTimeline(eq(post), eq(false))
}
@Test
fun `createRemote 既に作成されていた場合はそのまま帰す`() = runTest {
val post = PostBuilder.of()
whenever(postRepository.save(eq(post))).doReturn(false)
val createLocal = postServiceImpl.createRemote(post)
assertThat(createLocal).isEqualTo(post)
verify(postRepository, times(1)).save(eq(post))
verify(timelineService, times(0)).publishTimeline(any(), any())
}
@Test
fun `createRemote 既に作成されていることを検知できず例外が発生した場合はDBから取得して返す`() = runTest {
val post = PostBuilder.of()
whenever(postRepository.save(eq(post))).doAnswer { throw ExposedSQLException(null, emptyList(), mock()) }
whenever(postQueryService.findByApId(eq(post.apId))).doReturn(post)
val createLocal = postServiceImpl.createRemote(post)
assertThat(createLocal).isEqualTo(post)
verify(postRepository, times(1)).save(eq(post))
verify(timelineService, times(0)).publishTimeline(any(), any())
}
@Test
fun `createRemote 既に作成されていることを検知出来ずタイムラインにpush出来なかった場合何もしない`() = runTest {
val post = PostBuilder.of()
whenever(postRepository.save(eq(post))).doReturn(true)
whenever(timelineService.publishTimeline(eq(post), eq(false))).doThrow(DuplicateKeyException::class)
val createLocal = postServiceImpl.createRemote(post)
assertThat(createLocal).isEqualTo(post)
verify(postRepository, times(1)).save(eq(post))
verify(timelineService, times(1)).publishTimeline(eq(post), eq(false))
}
}

View File

@ -0,0 +1,127 @@
package dev.usbharu.hideout.core.service.reaction
import dev.usbharu.hideout.activitypub.service.activity.like.APReactionService
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import dev.usbharu.hideout.core.query.ReactionQueryService
import kotlinx.coroutines.test.runTest
import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.*
import utils.PostBuilder
@ExtendWith(MockitoExtension::class)
class ReactionServiceImplTest {
@Mock
private lateinit var reactionRepository: ReactionRepository
@Mock
private lateinit var apReactionService: APReactionService
@Mock
private lateinit var reactionQueryService: ReactionQueryService
@InjectMocks
private lateinit var reactionServiceImpl: ReactionServiceImpl
@Test
fun `receiveReaction リアクションが存在しないとき保存する`() = runTest {
val post = PostBuilder.of()
whenever(reactionQueryService.reactionAlreadyExist(eq(post.id), eq(post.userId), eq(0))).doReturn(false)
val generateId = TwitterSnowflakeIdGenerateService.generateId()
whenever(reactionRepository.generateId()).doReturn(generateId)
reactionServiceImpl.receiveReaction("", "example.com", post.userId, post.id)
verify(reactionRepository, times(1)).save(eq(Reaction(generateId, 0, post.id, post.userId)))
}
@Test
fun `receiveReaction リアクションが既に作成されていることを検知出来ずに例外が発生した場合は何もしない`() = runTest {
val post = PostBuilder.of()
whenever(reactionQueryService.reactionAlreadyExist(eq(post.id), eq(post.userId), eq(0))).doReturn(false)
val generateId = TwitterSnowflakeIdGenerateService.generateId()
whenever(
reactionRepository.save(
eq(
Reaction(
id = generateId,
emojiId = 0,
postId = post.id,
userId = post.userId
)
)
)
).doAnswer {
throw ExposedSQLException(
null,
emptyList(), mock()
)
}
whenever(reactionRepository.generateId()).doReturn(generateId)
reactionServiceImpl.receiveReaction("", "example.com", post.userId, post.id)
verify(reactionRepository, times(1)).save(eq(Reaction(generateId, 0, post.id, post.userId)))
}
@Test
fun `receiveReaction リアクションが既に作成されている場合は何もしない`() = runTest() {
val post = PostBuilder.of()
whenever(reactionQueryService.reactionAlreadyExist(eq(post.id), eq(post.userId), eq(0))).doReturn(true)
reactionServiceImpl.receiveReaction("", "example.com", post.userId, post.id)
verify(reactionRepository, never()).save(any())
}
@Test
fun `sendReaction リアクションが存在しないとき保存して配送する`() = runTest {
val post = PostBuilder.of()
whenever(reactionQueryService.reactionAlreadyExist(eq(post.id), eq(post.userId), eq(0))).doReturn(false)
val generateId = TwitterSnowflakeIdGenerateService.generateId()
whenever(reactionRepository.generateId()).doReturn(generateId)
reactionServiceImpl.sendReaction("", post.userId, post.id)
verify(reactionRepository, times(1)).save(eq(Reaction(generateId, 0, post.id, post.userId)))
verify(apReactionService, times(1)).reaction(eq(Reaction(generateId, 0, post.id, post.userId)))
}
@Test
fun `sendReaction リアクションが存在するときは削除して保存して配送する`() = runTest {
val post = PostBuilder.of()
whenever(reactionQueryService.reactionAlreadyExist(eq(post.id), eq(post.userId), eq(0))).doReturn(true)
val generateId = TwitterSnowflakeIdGenerateService.generateId()
whenever(reactionRepository.generateId()).doReturn(generateId)
reactionServiceImpl.sendReaction("", post.userId, post.id)
verify(reactionRepository, times(1)).delete(eq(Reaction(generateId, 0, post.id, post.userId)))
verify(reactionRepository, times(1)).save(eq(Reaction(generateId, 0, post.id, post.userId)))
verify(apReactionService, times(1)).removeReaction(eq(Reaction(generateId, 0, post.id, post.userId)))
verify(apReactionService, times(1)).reaction(eq(Reaction(generateId, 0, post.id, post.userId)))
}
@Test
fun `removeReaction リアクションが存在する場合削除して配送`() = runTest {
val post = PostBuilder.of()
whenever(reactionQueryService.reactionAlreadyExist(eq(post.id), eq(post.userId), eq(0))).doReturn(true)
reactionServiceImpl.removeReaction(post.userId, post.id)
verify(reactionRepository, times(1)).delete(eq(Reaction(0, 0, post.id, post.userId)))
verify(apReactionService, times(1)).removeReaction(eq(Reaction(0, 0, post.id, post.userId)))
}
}

View File

@ -0,0 +1,110 @@
package dev.usbharu.hideout.core.service.timeline
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.domain.model.timeline.Timeline
import dev.usbharu.hideout.core.domain.model.timeline.TimelineRepository
import dev.usbharu.hideout.core.domain.model.user.User
import dev.usbharu.hideout.core.query.FollowerQueryService
import dev.usbharu.hideout.core.query.UserQueryService
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.*
import utils.PostBuilder
import utils.UserBuilder
@ExtendWith(MockitoExtension::class)
class TimelineServiceTest {
@Mock
private lateinit var followerQueryService: FollowerQueryService
@Mock
private lateinit var userQueryService: UserQueryService
@Mock
private lateinit var timelineRepository: TimelineRepository
@InjectMocks
private lateinit var timelineService: TimelineService
@Captor
private lateinit var captor: ArgumentCaptor<List<Timeline>>
@Test
fun `publishTimeline ローカルの投稿はローカルのフォロワーと投稿者のタイムラインに追加される`() = runTest {
val post = PostBuilder.of()
val listOf = listOf<User>(UserBuilder.localUserOf(), UserBuilder.localUserOf())
val localUserOf = UserBuilder.localUserOf(id = post.userId)
whenever(followerQueryService.findFollowersById(eq(post.userId))).doReturn(listOf)
whenever(userQueryService.findById(eq(post.userId))).doReturn(localUserOf)
whenever(timelineRepository.generateId()).doReturn(TwitterSnowflakeIdGenerateService.generateId())
timelineService.publishTimeline(post, true)
verify(timelineRepository).saveAll(capture(captor))
val timelineList = captor.value
assertThat(timelineList).hasSize(4).anyMatch { it.userId == post.userId }
}
@Test
fun `publishTimeline リモートの投稿はローカルのフォロワーのタイムラインに追加される`() = runTest {
val post = PostBuilder.of()
val listOf = listOf<User>(UserBuilder.localUserOf(), UserBuilder.localUserOf())
whenever(followerQueryService.findFollowersById(eq(post.userId))).doReturn(listOf)
whenever(timelineRepository.generateId()).doReturn(TwitterSnowflakeIdGenerateService.generateId())
timelineService.publishTimeline(post, false)
verify(timelineRepository).saveAll(capture(captor))
val timelineList = captor.value
assertThat(timelineList).hasSize(3)
}
@Test
fun `publishTimeline パブリック投稿はパブリックタイムラインにも追加される`() = runTest {
val post = PostBuilder.of()
val listOf = listOf<User>(UserBuilder.localUserOf(), UserBuilder.localUserOf())
whenever(followerQueryService.findFollowersById(eq(post.userId))).doReturn(listOf)
whenever(timelineRepository.generateId()).doReturn(TwitterSnowflakeIdGenerateService.generateId())
timelineService.publishTimeline(post, false)
verify(timelineRepository).saveAll(capture(captor))
val timelineList = captor.value
assertThat(timelineList).hasSize(3).anyMatch { it.userId == 0L }
}
@Test
fun `publishTimeline パブリック投稿ではない場合はローカルのフォロワーのみに追加される`() = runTest {
val post = PostBuilder.of(visibility = Visibility.UNLISTED)
val listOf = listOf<User>(UserBuilder.localUserOf(), UserBuilder.localUserOf())
whenever(followerQueryService.findFollowersById(eq(post.userId))).doReturn(listOf)
whenever(timelineRepository.generateId()).doReturn(TwitterSnowflakeIdGenerateService.generateId())
timelineService.publishTimeline(post, false)
verify(timelineRepository).saveAll(capture(captor))
val timelineList = captor.value
assertThat(timelineList).hasSize(2).noneMatch { it.userId == 0L }
}
}

View File

@ -20,15 +20,15 @@ object UserBuilder {
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/",
inbox: String = "https://$domain/users/$id/inbox",
outbox: String = "https://$domain/users/$id/outbox",
url: String = "https://$domain/users/$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"
keyId: String = "https://$domain/users/$id#pubkey",
followers: String = "https://$domain/users/$id/followers",
following: String = "https://$domain/users/$id/following"
): User {
return userBuilder.of(
id = id,