diff --git a/build.gradle.kts b/build.gradle.kts index ec03a89e..b72caa33 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,7 @@ plugins { id("org.springframework.boot") version "3.1.3" kotlin("plugin.spring") version "1.8.21" id("org.openapi.generator") version "7.0.1" + id("org.jetbrains.kotlinx.kover") version "0.7.4" // id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10" } @@ -31,6 +32,11 @@ tasks.withType { 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>().configureEach { @@ -128,6 +134,7 @@ dependencies { implementation("org.jetbrains.exposed:exposed-java-time:$exposed_version") testImplementation("org.springframework.boot:spring-boot-test-autoconfigure") testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.security:spring-security-oauth2-jose") @@ -158,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") @@ -198,3 +206,34 @@ configurations.matching { it.name == "detekt" }.all { } } } + +project.gradle.taskGraph.whenReady { + println(this.allTasks) + this.allTasks.map { println(it.name) } + if (this.hasTask(":koverGenerateArtifact")) { + println("has task") + val task = this.allTasks.find { it.name == "test" } + val verificationTask = task as VerificationTask + verificationTask.ignoreFailures = true + } +} + +kover { + +excludeSourceSets { + names("aot") + } +} + +koverReport { + filters { + excludes { + packages( + "dev.usbharu.hideout.controller.mastodon.generated", + "dev.usbharu.hideout.domain.mastodon.model.generated" + ) + packages("org.springframework") + packages("org.jetbrains") + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Delete.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Delete.kt index 48409e92..07223628 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Delete.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Delete.kt @@ -11,7 +11,7 @@ open class Delete : Object { constructor( type: List = emptyList(), - name: String = "Delete", + name: String? = "Delete", actor: String, id: String, `object`: Object, diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt index a369e036..da4e2def 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLd.kt @@ -19,10 +19,17 @@ open class JsonLd { @JsonSerialize(include = JsonSerialize.Inclusion.NON_EMPTY, using = ContextSerializer::class) @JsonInclude(JsonInclude.Include.NON_EMPTY) var context: List = emptyList() + set(value) { + field = value.filterNotNull().filter { it.isNotBlank() } + } @JsonCreator - constructor(context: List) { - this.context = context + constructor(context: List?) { + if (context != null) { + this.context = context.filterNotNull().filter { it.isNotBlank() } + } else { + this.context = emptyList() + } } protected constructor() @@ -40,25 +47,34 @@ open class JsonLd { } class ContextDeserializer : JsonDeserializer() { + + override fun deserialize( p0: com.fasterxml.jackson.core.JsonParser?, p1: com.fasterxml.jackson.databind.DeserializationContext? ): String { val readTree: JsonNode = p0?.codec?.readTree(p0) ?: return "" - if (readTree.isObject) { - return "" + if (readTree.isValueNode) { + return readTree.textValue() } - return readTree.asText() + return "" } } class ContextSerializer : JsonSerializer>() { - override fun isEmpty(value: List?): Boolean = value.isNullOrEmpty() + override fun isEmpty(value: List?): Boolean { + return value.isNullOrEmpty() + } + + override fun isEmpty(provider: SerializerProvider?, value: List?): Boolean { + return value.isNullOrEmpty() + } + + override fun serialize(value: List?, gen: JsonGenerator?, serializers: SerializerProvider) { - override fun serialize(value: List?, gen: JsonGenerator?, serializers: SerializerProvider?) { if (value.isNullOrEmpty()) { - gen?.writeNull() + serializers.defaultSerializeNull(gen) return } if (value.size == 1) { diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt index 703401ad..f3befd0f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/Object.kt @@ -9,18 +9,22 @@ import dev.usbharu.hideout.activitypub.domain.model.JsonLd open class Object : JsonLd { @JsonSerialize(using = TypeSerializer::class) var type: List = emptyList() + set(value) { + field = value.filter { it.isNotBlank() } + } var name: String? = null var actor: String? = null var id: String? = null protected constructor() constructor(type: List, name: String? = null, actor: String? = null, id: String? = null) : super() { - this.type = type + this.type = type.filter { it.isNotBlank() } this.name = name this.actor = actor this.id = id } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Object) return false @@ -29,7 +33,9 @@ open class Object : JsonLd { if (type != other.type) return false if (name != other.name) return false if (actor != other.actor) return false - return id == other.id + if (id != other.id) return false + + return true } override fun hashCode(): Int { @@ -41,7 +47,9 @@ open class Object : JsonLd { return result } - override fun toString(): String = "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}" + override fun toString(): String { + return "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}" + } companion object { @JvmStatic diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImpl.kt index 0e52ca36..74ede688 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImpl.kt @@ -2,6 +2,7 @@ package dev.usbharu.hideout.activitypub.interfaces.api.actor import dev.usbharu.hideout.activitypub.domain.model.Person import dev.usbharu.hideout.activitypub.service.objects.user.APUserService +import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.RestController @@ -9,7 +10,11 @@ import org.springframework.web.bind.annotation.RestController @RestController class UserAPControllerImpl(private val apUserService: APUserService) : UserAPController { override suspend fun userAp(username: String): ResponseEntity { - val person = apUserService.getPersonByName(username) + val person = try { + apUserService.getPersonByName(username) + } catch (e: FailedToGetResourcesException) { + return ResponseEntity.notFound().build() + } person.context += listOf("https://www.w3.org/ns/activitystreams") return ResponseEntity(person, HttpStatus.OK) } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxController.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxController.kt index e8f2a764..b2e401cc 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxController.kt @@ -1,6 +1,5 @@ package dev.usbharu.hideout.activitypub.interfaces.api.inbox -import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -16,7 +15,7 @@ interface InboxController { "application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" ], - method = [RequestMethod.GET, RequestMethod.POST] + method = [RequestMethod.POST] ) - suspend fun inbox(@RequestBody string: String): ResponseEntity = ResponseEntity(HttpStatus.ACCEPTED) + suspend fun inbox(@RequestBody string: String): ResponseEntity } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApController.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApController.kt index 5e284d88..be127c44 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApController.kt @@ -2,15 +2,12 @@ package dev.usbharu.hideout.activitypub.interfaces.api.note import dev.usbharu.hideout.activitypub.domain.model.Note import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.CurrentSecurityContext -import org.springframework.security.core.context.SecurityContext import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable interface NoteApController { @GetMapping("/users/*/posts/{postId}") suspend fun postsAp( - @PathVariable("postId") postId: Long, - @CurrentSecurityContext context: SecurityContext + @PathVariable("postId") postId: Long ): ResponseEntity } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImpl.kt index d2c55e41..307cf16c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImpl.kt @@ -4,8 +4,7 @@ import dev.usbharu.hideout.activitypub.domain.model.Note import dev.usbharu.hideout.activitypub.service.objects.note.NoteApApiService import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUser import org.springframework.http.ResponseEntity -import org.springframework.security.core.annotation.CurrentSecurityContext -import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RestController @@ -14,8 +13,8 @@ import org.springframework.web.bind.annotation.RestController class NoteApControllerImpl(private val noteApApiService: NoteApApiService) : NoteApController { override suspend fun postsAp( @PathVariable(value = "postId") postId: Long, - @CurrentSecurityContext context: SecurityContext ): ResponseEntity { + val context = SecurityContextHolder.getContext() val userId = if (context.authentication is PreAuthenticatedAuthenticationToken && context.authentication.details is HttpSignatureUser diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/outbox/OutboxController.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/outbox/OutboxController.kt index 39cd78f1..0f422688 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/outbox/OutboxController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/outbox/OutboxController.kt @@ -1,8 +1,6 @@ package dev.usbharu.hideout.activitypub.interfaces.api.outbox -import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMethod import org.springframework.web.bind.annotation.RestController @@ -10,5 +8,5 @@ import org.springframework.web.bind.annotation.RestController @RestController interface OutboxController { @RequestMapping("/outbox", "/users/{username}/outbox", method = [RequestMethod.POST, RequestMethod.GET]) - suspend fun outbox(@RequestBody string: String): ResponseEntity = ResponseEntity(HttpStatus.ACCEPTED) + suspend fun outbox(): ResponseEntity } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/outbox/OutboxControllerImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/outbox/OutboxControllerImpl.kt index 398c680f..63eb9b50 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/outbox/OutboxControllerImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/outbox/OutboxControllerImpl.kt @@ -2,11 +2,10 @@ package dev.usbharu.hideout.activitypub.interfaces.api.outbox import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController @RestController class OutboxControllerImpl : OutboxController { - override suspend fun outbox(@RequestBody string: String): ResponseEntity = + override suspend fun outbox(): ResponseEntity = ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/webfinger/WebFingerController.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/webfinger/WebFingerController.kt index c365d161..3a7bfdd8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/webfinger/WebFingerController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/webfinger/WebFingerController.kt @@ -3,6 +3,7 @@ package dev.usbharu.hideout.activitypub.interfaces.api.webfinger import dev.usbharu.hideout.activitypub.domain.model.webfinger.WebFinger import dev.usbharu.hideout.activitypub.service.webfinger.WebFingerApiService import dev.usbharu.hideout.application.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException import dev.usbharu.hideout.util.AcctUtil import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory @@ -26,8 +27,11 @@ class WebFingerController( logger.warn("FAILED Parse acct.", e) return@runBlocking ResponseEntity.badRequest().build() } - val user = + val user = try { webFingerApiService.findByNameAndDomain(acct.username, acct.domain ?: applicationConfig.url.host) + } catch (_: FailedToGetResourcesException) { + return@runBlocking ResponseEntity.notFound().build() + } val webFinger = WebFinger( "acct:${user.name}@${user.domain}", listOf( diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/ActivityPubConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/ActivityPubConfig.kt index 70d35829..d6bdf301 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/ActivityPubConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/ActivityPubConfig.kt @@ -1,6 +1,9 @@ package dev.usbharu.hideout.application.config import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonSetter +import com.fasterxml.jackson.annotation.Nulls +import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -21,7 +24,11 @@ class ActivityPubConfig { val objectMapper = jacksonObjectMapper() .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) + .setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY)) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(JsonParser.Feature.ALLOW_COMMENTS, true) + .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true) + .configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true) return objectMapper } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/reaction/ReactionServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/reaction/ReactionServiceImpl.kt index fdd2db74..baad0e56 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/reaction/ReactionServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/reaction/ReactionServiceImpl.kt @@ -1,6 +1,7 @@ package dev.usbharu.hideout.core.service.reaction import dev.usbharu.hideout.activitypub.service.activity.like.APReactionService +import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException 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 @@ -26,18 +27,26 @@ class ReactionServiceImpl( } override suspend fun sendReaction(name: String, userId: Long, postId: Long) { - if (reactionQueryService.reactionAlreadyExist(postId, userId, 0)) { - // delete - reactionQueryService.deleteByPostIdAndUserId(postId, userId) - } else { - val reaction = Reaction(reactionRepository.generateId(), 0, postId, userId) - reactionRepository.save(reaction) - apReactionService.reaction(reaction) + try { + val findByPostIdAndUserIdAndEmojiId = + reactionQueryService.findByPostIdAndUserIdAndEmojiId(postId, userId, 0) + apReactionService.removeReaction(findByPostIdAndUserIdAndEmojiId) + reactionRepository.delete(findByPostIdAndUserIdAndEmojiId) + } catch (_: FailedToGetResourcesException) { } + val reaction = Reaction(reactionRepository.generateId(), 0, postId, userId) + reactionRepository.save(reaction) + apReactionService.reaction(reaction) } override suspend fun removeReaction(userId: Long, postId: Long) { - reactionQueryService.deleteByPostIdAndUserId(postId, userId) + try { + val findByPostIdAndUserIdAndEmojiId = + reactionQueryService.findByPostIdAndUserIdAndEmojiId(postId, userId, 0) + reactionRepository.delete(findByPostIdAndUserIdAndEmojiId) + apReactionService.removeReaction(findByPostIdAndUserIdAndEmojiId) + } catch (e: FailedToGetResourcesException) { + } } companion object { diff --git a/src/test/kotlin/dev/usbharu/hideout/EqualsAndToStringTest.kt b/src/test/kotlin/dev/usbharu/hideout/EqualsAndToStringTest.kt new file mode 100644 index 00000000..62f68093 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/EqualsAndToStringTest.kt @@ -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() + } + } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/DeleteSerializeTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/DeleteSerializeTest.kt new file mode 100644 index 00000000..4f190250 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/DeleteSerializeTest.kt @@ -0,0 +1,81 @@ +package dev.usbharu.hideout.activitypub.domain.model + +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.application.config.ActivityPubConfig +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class DeleteSerializeTest { + @Test + fun Misskeyの発行するJSONをデシリアライズできる() { + @Language("JSON") val json = """{ + "@context" : [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { + "manuallyApprovesFollowers" : "as:manuallyApprovesFollowers", + "sensitive" : "as:sensitive", + "Hashtag" : "as:Hashtag", + "quoteUrl" : "as:quoteUrl", + "toot" : "http://joinmastodon.org/ns#", + "Emoji" : "toot:Emoji", + "featured" : "toot:featured", + "discoverable" : "toot:discoverable", + "schema" : "http://schema.org#", + "PropertyValue" : "schema:PropertyValue", + "value" : "schema:value", + "misskey" : "https://misskey-hub.net/ns#", + "_misskey_content" : "misskey:_misskey_content", + "_misskey_quote" : "misskey:_misskey_quote", + "_misskey_reaction" : "misskey:_misskey_reaction", + "_misskey_votes" : "misskey:_misskey_votes", + "isCat" : "misskey:isCat", + "vcard" : "http://www.w3.org/2006/vcard/ns#" + } ], + "type" : "Delete", + "actor" : "https://misskey.usbharu.dev/users/97ws8y3rj6", + "object" : { + "id" : "https://misskey.usbharu.dev/notes/9lkwqnwqk9", + "type" : "Tombstone" + }, + "published" : "2023-11-02T15:30:34.160Z", + "id" : "https://misskey.usbharu.dev/4b5b6ed5-9269-45f3-8403-cba1e74b4b69" +} +""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + val expected = Delete( + name = null, + actor = "https://misskey.usbharu.dev/users/97ws8y3rj6", + id = "https://misskey.usbharu.dev/4b5b6ed5-9269-45f3-8403-cba1e74b4b69", + `object` = Tombstone( + id = "https://misskey.usbharu.dev/notes/9lkwqnwqk9", + ), + published = "2023-11-02T15:30:34.160Z", + ) + expected.context = listOf("https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", "") + assertEquals(expected, readValue) + } + + @Test + fun シリアライズできる() { + val delete = Delete( + name = null, + actor = "https://misskey.usbharu.dev/users/97ws8y3rj6", + id = "https://misskey.usbharu.dev/4b5b6ed5-9269-45f3-8403-cba1e74b4b69", + `object` = Tombstone( + id = "https://misskey.usbharu.dev/notes/9lkwqnwqk9", + ), + published = "2023-11-02T15:30:34.160Z", + ) + + + val objectMapper = ActivityPubConfig().objectMapper() + + val actual = objectMapper.writeValueAsString(delete) + val expected = + """{"type":"Delete","actor":"https://misskey.usbharu.dev/users/97ws8y3rj6","id":"https://misskey.usbharu.dev/4b5b6ed5-9269-45f3-8403-cba1e74b4b69","object":{"type":"Tombstone","name":"Tombstone","id":"https://misskey.usbharu.dev/notes/9lkwqnwqk9"},"published":"2023-11-02T15:30:34.160Z"}""" + assertEquals(expected, actual) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLdSerializeTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLdSerializeTest.kt new file mode 100644 index 00000000..7672f429 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/JsonLdSerializeTest.kt @@ -0,0 +1,136 @@ +package dev.usbharu.hideout.activitypub.domain.model + +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.application.config.ActivityPubConfig +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class JsonLdSerializeTest { + @Test + fun contextが文字列のときデシリアライズできる() { + //language=JSON + val json = """{"@context":"https://example.com"}""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + assertEquals(JsonLd(listOf("https://example.com")), readValue) + } + + @Test + fun contextが文字列の配列のときデシリアライズできる() { + //language=JSON + val json = """{"@context":["https://example.com","https://www.w3.org/ns/activitystreams"]}""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + assertEquals(JsonLd(listOf("https://example.com", "https://www.w3.org/ns/activitystreams")), readValue) + } + + @Test + fun contextがnullのとき空のlistとして解釈してデシリアライズする() { + //language=JSON + val json = """{"@context":null}""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + assertEquals(JsonLd(emptyList()), readValue) + } + + @Test + fun contextがnullを含む文字列の配列のときnullを無視してデシリアライズできる() { + //language=JSON + val json = """{"@context":["https://example.com",null,"https://www.w3.org/ns/activitystreams"]}""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + assertEquals(JsonLd(listOf("https://example.com", "https://www.w3.org/ns/activitystreams")), readValue) + } + + @Test + fun contextがオブジェクトのとき無視してデシリアライズする() { + //language=JSON + val json = """{"@context":{"hoge": "fuga"}}""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + assertEquals(JsonLd(emptyList()), readValue) + } + + @Test + fun contextがオブジェクトを含む文字列の配列のときオブジェクトを無視してデシリアライズする() { + //language=JSON + val json = """{"@context":["https://example.com",{"hoge": "fuga"},"https://www.w3.org/ns/activitystreams"]}""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + assertEquals(JsonLd(listOf("https://example.com", "https://www.w3.org/ns/activitystreams")), readValue) + } + + @Test + fun contextが配列の配列のとき無視してデシリアライズする() { + //language=JSON + val json = """{"@context":[["a","b"],["c","d"]]}""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + assertEquals(JsonLd(emptyList()), readValue) + } + + @Test + fun contextが空のとき無視してシリアライズする() { + val jsonLd = JsonLd(emptyList()) + + val objectMapper = ActivityPubConfig().objectMapper() + + val actual = objectMapper.writeValueAsString(jsonLd) + + assertEquals("{}", actual) + } + + @Test + fun contextがnullのとき無視してシリアライズする() { + val jsonLd = JsonLd(listOf(null)) + + val objectMapper = ActivityPubConfig().objectMapper() + + val actual = objectMapper.writeValueAsString(jsonLd) + + assertEquals("{}", actual) + } + + @Test + fun contextが文字列のとき文字列としてシリアライズされる() { + val jsonLd = JsonLd(listOf("https://example.com")) + + val objectMapper = ActivityPubConfig().objectMapper() + + val actual = objectMapper.writeValueAsString(jsonLd) + + assertEquals("""{"@context":"https://example.com"}""", actual) + } + + @Test + fun contextが文字列の配列のとき配列としてシリアライズされる() { + val jsonLd = JsonLd(listOf("https://example.com", "https://www.w3.org/ns/activitystreams")) + + val objectMapper = ActivityPubConfig().objectMapper() + + val actual = objectMapper.writeValueAsString(jsonLd) + + assertEquals("""{"@context":["https://example.com","https://www.w3.org/ns/activitystreams"]}""", actual) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/NoteSerializeTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/NoteSerializeTest.kt new file mode 100644 index 00000000..1b05eef1 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/NoteSerializeTest.kt @@ -0,0 +1,83 @@ +package dev.usbharu.hideout.activitypub.domain.model + +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public +import dev.usbharu.hideout.application.config.ActivityPubConfig +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class NoteSerializeTest { + @Test + fun Noteのシリアライズができる() { + val note = Note( + name = "Note", + id = "https://example.com", + attributedTo = "https://example.com/actor", + content = "Hello", + published = "2023-05-20T10:28:17.308Z", + ) + + val objectMapper = ActivityPubConfig().objectMapper() + + val writeValueAsString = objectMapper.writeValueAsString(note) + + assertEquals( + "{\"type\":\"Note\",\"name\":\"Note\",\"id\":\"https://example.com\",\"attributedTo\":\"https://example.com/actor\",\"content\":\"Hello\",\"published\":\"2023-05-20T10:28:17.308Z\",\"sensitive\":false}", + writeValueAsString + ) + } + + @Test + fun Noteのデシリアライズができる() { + //language=JSON + val json = """{ + "id": "https://misskey.usbharu.dev/notes/9f2i9cm88e", + "type": "Note", + "attributedTo": "https://misskey.usbharu.dev/users/97ws8y3rj6", + "content": "

@trapezial@calckey.jp いやそういうことじゃなくて、連合先と自インスタンスで状態が狂うことが多いのでどっちに合わせるべきかと…

", + "_misskey_content": "@trapezial@calckey.jp いやそういうことじゃなくて、連合先と自インスタンスで状態が狂うことが多いのでどっちに合わせるべきかと…", + "source": { + "content": "@trapezial@calckey.jp いやそういうことじゃなくて、連合先と自インスタンスで状態が狂うことが多いのでどっちに合わせるべきかと…", + "mediaType": "text/x.misskeymarkdown" + }, + "published": "2023-05-22T14:26:53.600Z", + "to": [ + "https://misskey.usbharu.dev/users/97ws8y3rj6/followers" + ], + "cc": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://calckey.jp/users/9bu1xzwjyb" + ], + "inReplyTo": "https://calckey.jp/notes/9f2i7ymf1d", + "attachment": [], + "sensitive": false, + "tag": [ + { + "type": "Mention", + "href": "https://calckey.jp/users/9bu1xzwjyb", + "name": "@trapezial@calckey.jp" + } + ] + }""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + val note = Note( + name = "", + id = "https://misskey.usbharu.dev/notes/9f2i9cm88e", + type = listOf("Note"), + attributedTo = "https://misskey.usbharu.dev/users/97ws8y3rj6", + content = "

@trapezial@calckey.jp いやそういうことじゃなくて、連合先と自インスタンスで状態が狂うことが多いのでどっちに合わせるべきかと…

", + published = "2023-05-22T14:26:53.600Z", + to = listOf("https://misskey.usbharu.dev/users/97ws8y3rj6/followers"), + cc = listOf(public, "https://calckey.jp/users/9bu1xzwjyb"), + sensitive = false, + inReplyTo = "https://calckey.jp/notes/9f2i7ymf1d", + attachment = emptyList() + ) + note.name = null + assertEquals(note, readValue) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectSerializeTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectSerializeTest.kt new file mode 100644 index 00000000..77f2579b --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectSerializeTest.kt @@ -0,0 +1,65 @@ +package dev.usbharu.hideout.activitypub.domain.model.objects + +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.application.config.ActivityPubConfig +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class ObjectSerializeTest { + @Test + fun typeが文字列のときデシリアライズできる() { + //language=JSON + val json = """{"type": "Object"}""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + val expected = Object( + listOf("Object"), + null, + null, + null + ) + assertEquals(expected, readValue) + } + + @Test + fun typeが文字列の配列のときデシリアライズできる() { + //language=JSON + val json = """{"type": ["Hoge","Object"]}""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + val expected = Object( + listOf("Hoge", "Object"), + null, + null, + null + ) + + assertEquals(expected, readValue) + } + + @Test + fun typeが空のとき無視してデシリアライズする() { + //language=JSON + val json = """{"type": ""}""" + + val objectMapper = ActivityPubConfig().objectMapper() + + val readValue = objectMapper.readValue(json) + + val expected = Object( + emptyList(), + null, + null, + null + ) + + assertEquals(expected, readValue) + } + +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImplTest.kt new file mode 100644 index 00000000..9520eac2 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/actor/UserAPControllerImplTest.kt @@ -0,0 +1,94 @@ +package dev.usbharu.hideout.activitypub.interfaces.api.actor + +import dev.usbharu.hideout.activitypub.domain.model.Image +import dev.usbharu.hideout.activitypub.domain.model.Key +import dev.usbharu.hideout.activitypub.domain.model.Person +import dev.usbharu.hideout.activitypub.service.objects.user.APUserService +import dev.usbharu.hideout.application.config.ActivityPubConfig +import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +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.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.setup.MockMvcBuilders + +@ExtendWith(MockitoExtension::class) +class UserAPControllerImplTest { + + private lateinit var mockMvc: MockMvc + + @Mock + private lateinit var apUserService: APUserService + + @InjectMocks + private lateinit var userAPControllerImpl: UserAPControllerImpl + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(userAPControllerImpl).build() + } + + @Test + fun `userAp 存在するユーザーにGETするとPersonが返ってくる`(): Unit = runTest { + val person = Person( + name = "Hoge", + id = "https://example.com/users/hoge", + preferredUsername = "hoge", + summary = "fuga", + inbox = "https://example.com/users/hoge/inbox", + outbox = "https://example.com/users/hoge/outbox", + url = "https://example.com/users/hoge", + icon = Image( + name = "icon", + mediaType = "image/jpeg", + url = "https://example.com/users/hoge/icon.jpg" + ), + publicKey = Key( + name = "Public Key", + id = "https://example.com/users/hoge#pubkey", + owner = "https://example.com/users/hoge", + publicKeyPem = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", + type = emptyList() + ), + endpoints = mapOf("sharedInbox" to "https://example.com/inbox"), + followers = "https://example.com/users/hoge/followers", + following = "https://example.com/users/hoge/following" + ) + whenever(apUserService.getPersonByName(eq("hoge"))).doReturn(person) + + val objectMapper = ActivityPubConfig().objectMapper() + + mockMvc + .get("/users/hoge") + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { this.json(objectMapper.writeValueAsString(person)) } } + } + + @Test + fun `userAP 存在しないユーザーにGETすると404が返ってくる`() = runTest { + whenever(apUserService.getPersonByName(eq("fuga"))).doThrow(FailedToGetResourcesException::class) + + mockMvc + .get("/users/fuga") + .asyncDispatch() + .andExpect { status { isNotFound() } } + } + + @Test + fun `userAP POSTすると405が返ってくる`() { + mockMvc + .post("/users/hoge") + .andExpect { status { isMethodNotAllowed() } } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxControllerImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxControllerImplTest.kt new file mode 100644 index 00000000..a91e5b7f --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/inbox/InboxControllerImplTest.kt @@ -0,0 +1,179 @@ +package dev.usbharu.hideout.activitypub.interfaces.api.inbox + +import dev.usbharu.hideout.activitypub.domain.exception.JsonParseException +import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubStringResponse +import dev.usbharu.hideout.activitypub.service.common.APService +import dev.usbharu.hideout.activitypub.service.common.ActivityType +import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException +import io.ktor.http.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +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.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.setup.MockMvcBuilders + +@ExtendWith(MockitoExtension::class) +class InboxControllerImplTest { + + private lateinit var mockMvc: MockMvc + + @Mock + private lateinit var apService: APService + + @InjectMocks + private lateinit var inboxController: InboxControllerImpl + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(inboxController).build() + } + + @Test + fun `inbox 正常なPOSTリクエストをしたときAcceptが返ってくる`() = runTest { + + + val json = """{"type":"Follow"}""" + whenever(apService.parseActivity(eq(json))).doReturn(ActivityType.Follow) + whenever(apService.processActivity(eq(json), eq(ActivityType.Follow))).doReturn( + ActivityPubStringResponse( + HttpStatusCode.Accepted, "" + ) + ) + + mockMvc + .post("/inbox") { + content = json + contentType = MediaType.APPLICATION_JSON + } + .asyncDispatch() + .andExpect { + status { isAccepted() } + } + + } + + @Test + fun `inbox parseActivityに失敗したときAcceptが返ってくる`() = runTest { + val json = """{"type":"Hoge"}""" + whenever(apService.parseActivity(eq(json))).doThrow(JsonParseException::class) + + mockMvc + .post("/inbox") { + content = json + contentType = MediaType.APPLICATION_JSON + } + .asyncDispatch() + .andExpect { + status { isAccepted() } + } + + } + + @Test + fun `inbox processActivityに失敗したときAcceptが返ってくる`() = runTest { + val json = """{"type":"Follow"}""" + whenever(apService.parseActivity(eq(json))).doReturn(ActivityType.Follow) + whenever( + apService.processActivity( + eq(json), + eq(ActivityType.Follow) + ) + ).doThrow(FailedToGetResourcesException::class) + + mockMvc + .post("/inbox") { + content = json + contentType = MediaType.APPLICATION_JSON + } + .asyncDispatch() + .andExpect { + status { isAccepted() } + } + + } + + @Test + fun `inbox GETリクエストには405を返す`() { + mockMvc.get("/inbox").andExpect { status { isMethodNotAllowed() } } + } + + @Test + fun `user-inbox 正常なPOSTリクエストをしたときAcceptが返ってくる`() = runTest { + + + val json = """{"type":"Follow"}""" + whenever(apService.parseActivity(eq(json))).doReturn(ActivityType.Follow) + whenever(apService.processActivity(eq(json), eq(ActivityType.Follow))).doReturn( + ActivityPubStringResponse( + HttpStatusCode.Accepted, "" + ) + ) + + mockMvc + .post("/users/hoge/inbox") { + content = json + contentType = MediaType.APPLICATION_JSON + } + .asyncDispatch() + .andExpect { + status { isAccepted() } + } + + } + + @Test + fun `user-inbox parseActivityに失敗したときAcceptが返ってくる`() = runTest { + val json = """{"type":"Hoge"}""" + whenever(apService.parseActivity(eq(json))).doThrow(JsonParseException::class) + + mockMvc + .post("/users/hoge/inbox") { + content = json + contentType = MediaType.APPLICATION_JSON + } + .asyncDispatch() + .andExpect { + status { isAccepted() } + } + + } + + @Test + fun `user-inbox processActivityに失敗したときAcceptが返ってくる`() = runTest { + val json = """{"type":"Follow"}""" + whenever(apService.parseActivity(eq(json))).doReturn(ActivityType.Follow) + whenever( + apService.processActivity( + eq(json), + eq(ActivityType.Follow) + ) + ).doThrow(FailedToGetResourcesException::class) + + mockMvc + .post("/users/hoge/inbox") { + content = json + contentType = MediaType.APPLICATION_JSON + } + .asyncDispatch() + .andExpect { + status { isAccepted() } + } + + } + + @Test + fun `user-inbox GETリクエストには405を返す`() { + mockMvc.get("/users/hoge/inbox").andExpect { status { isMethodNotAllowed() } } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImplTest.kt new file mode 100644 index 00000000..9537be64 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/note/NoteApControllerImplTest.kt @@ -0,0 +1,129 @@ +package dev.usbharu.hideout.activitypub.interfaces.api.note + +import dev.usbharu.hideout.activitypub.domain.model.Note +import dev.usbharu.hideout.activitypub.service.objects.note.NoteApApiService +import dev.usbharu.hideout.application.config.ActivityPubConfig +import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUser +import dev.usbharu.httpsignature.common.HttpHeaders +import dev.usbharu.httpsignature.common.HttpMethod +import dev.usbharu.httpsignature.common.HttpRequest +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +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 org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity +import org.springframework.security.web.DefaultSecurityFilterChain +import org.springframework.security.web.FilterChainProxy +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken +import org.springframework.security.web.util.matcher.AnyRequestMatcher +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder +import java.net.URL + +@ExtendWith(MockitoExtension::class) +class NoteApControllerImplTest { + + private lateinit var mockMvc: MockMvc + + @Mock + private lateinit var noteApApiService: NoteApApiService + + @InjectMocks + private lateinit var noteApControllerImpl: NoteApControllerImpl + + @BeforeEach + fun setUp() { + + mockMvc = MockMvcBuilders.standaloneSetup(noteApControllerImpl) + .apply( + springSecurity( + FilterChainProxy( + DefaultSecurityFilterChain( + AnyRequestMatcher.INSTANCE + ) + ) + ) + ) + .build() + } + + @Test + fun `postAP 匿名で取得できる`() = runTest { + + val note = Note( + name = "Note", + id = "https://example.com/users/hoge/posts/1234", + attributedTo = "https://example.com/users/hoge", + content = "Hello", + published = "2023-11-02T15:30:34.160Z" + ) + whenever(noteApApiService.getNote(eq(1234), isNull())).doReturn( + note + ) + + val objectMapper = ActivityPubConfig().objectMapper() + + mockMvc + .get("/users/hoge/posts/1234") { + with(anonymous()) + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(note)) } } + } + + @Test + fun `postAP 存在しない場合は404`() = runTest { + whenever(noteApApiService.getNote(eq(123), isNull())).doReturn(null) + + mockMvc + .get("/users/hoge/posts/123") { + with(anonymous()) + } + .asyncDispatch() + .andExpect { status { isNotFound() } } + } + + @Test + fun `postAP 認証に成功している場合userIdがnullでない`() = runTest { + val note = Note( + name = "Note", + id = "https://example.com/users/hoge/posts/1234", + attributedTo = "https://example.com/users/hoge", + content = "Hello", + published = "2023-11-02T15:30:34.160Z" + ) + whenever(noteApApiService.getNote(eq(1234), isNotNull())).doReturn(note) + + val objectMapper = ActivityPubConfig().objectMapper() + + val preAuthenticatedAuthenticationToken = PreAuthenticatedAuthenticationToken( + "", HttpRequest( + URL("https://follower.example.com"), + HttpHeaders( + mapOf() + ), HttpMethod.GET + ) + ).apply { details = HttpSignatureUser("fuga", "follower.example.com", 123, true, true, mutableListOf()) } + SecurityContextHolder.getContext().authentication = preAuthenticatedAuthenticationToken + + mockMvc.get("/users/hoge/posts/1234") { + with( + authentication( + preAuthenticatedAuthenticationToken + ) + ) + }.asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(note)) } } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/outbox/OutboxControllerImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/outbox/OutboxControllerImplTest.kt new file mode 100644 index 00000000..c22ddb3f --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/outbox/OutboxControllerImplTest.kt @@ -0,0 +1,62 @@ +package dev.usbharu.hideout.activitypub.interfaces.api.outbox + +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.junit.jupiter.MockitoExtension +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.setup.MockMvcBuilders + +@ExtendWith(MockitoExtension::class) +class OutboxControllerImplTest { + + private lateinit var mockMvc: MockMvc + + @InjectMocks + private lateinit var outboxController: OutboxControllerImpl + + @BeforeEach + fun setUp() { + mockMvc = + MockMvcBuilders.standaloneSetup(outboxController).build() + } + + @Test + fun `outbox GETに501を返す`() { + mockMvc + .get("/outbox") + .asyncDispatch() + .andDo { print() } + .andExpect { status { isNotImplemented() } } + } + + @Test + fun `user-outbox GETに501を返す`() { + mockMvc + .get("/users/hoge/outbox") + .asyncDispatch() + .andDo { print() } + .andExpect { status { isNotImplemented() } } + } + + @Test + fun `outbox POSTに501を返す`() { + mockMvc + .post("/outbox") + .asyncDispatch() + .andDo { print() } + .andExpect { status { isNotImplemented() } } + } + + @Test + fun `user-outbox POSTに501を返す`() { + mockMvc + .post("/users/hoge/outbox") + .asyncDispatch() + .andDo { print() } + .andExpect { status { isNotImplemented() } } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/webfinger/WebFingerControllerTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/webfinger/WebFingerControllerTest.kt new file mode 100644 index 00000000..41017444 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/interfaces/api/webfinger/WebFingerControllerTest.kt @@ -0,0 +1,100 @@ +package dev.usbharu.hideout.activitypub.interfaces.api.webfinger + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import dev.usbharu.hideout.activitypub.domain.model.webfinger.WebFinger +import dev.usbharu.hideout.activitypub.service.webfinger.WebFingerApiService +import dev.usbharu.hideout.application.config.ApplicationConfig +import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +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.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import utils.UserBuilder + +@ExtendWith(MockitoExtension::class) +class WebFingerControllerTest { + + private lateinit var mockMvc: MockMvc + + @Mock + private lateinit var webFingerApiService: WebFingerApiService + + @Mock + private lateinit var applicationConfig: ApplicationConfig + + @InjectMocks + private lateinit var webFingerController: WebFingerController + + @BeforeEach + fun setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(webFingerController).build() + } + + @Test + fun `webfinger 存在するacctを指定したとき200 OKでWebFingerのレスポンスが返ってくる`() = runTest { + + val user = UserBuilder.localUserOf() + whenever( + webFingerApiService.findByNameAndDomain( + eq("hoge"), + eq("example.com") + ) + ).doReturn(user) + + val contentAsString = mockMvc.perform(get("/.well-known/webfinger?resource=acct:hoge@example.com")) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn() + .response + .contentAsString + + val objectMapper = jacksonObjectMapper() + + val readValue = objectMapper.readValue(contentAsString) + + val expected = WebFinger( + subject = "acct:${user.name}@${user.domain}", + listOf( + WebFinger.Link( + "self", + "application/activity+json", + user.url + ) + ) + ) + + assertThat(readValue).isEqualTo(expected) + } + + @Test + fun `webfinger 存在しないacctを指定したとき404 Not Foundが返ってくる`() = runTest { + whenever(webFingerApiService.findByNameAndDomain(eq("fuga"), eq("example.com"))).doThrow( + FailedToGetResourcesException::class + ) + + mockMvc.perform(get("/.well-known/webfinger?resource=acct:fuga@example.com")) + .andDo(print()) + .andExpect(status().isNotFound) + } + + @Test + fun `webfinger acctとして解釈できない場合は400 Bad Requestが返ってくる`() { + mockMvc.perform(get("/.well-known/webfinger?resource=@hello@aa@aab@aaa")) + .andDo(print()) + .andExpect(status().isBadRequest) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/create/ApSendCreateServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/create/ApSendCreateServiceImplTest.kt new file mode 100644 index 00000000..47b5671e --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/create/ApSendCreateServiceImplTest.kt @@ -0,0 +1,79 @@ +package dev.usbharu.hideout.activitypub.service.activity.create + +import com.fasterxml.jackson.databind.ObjectMapper +import dev.usbharu.hideout.activitypub.domain.model.Note +import dev.usbharu.hideout.activitypub.query.NoteQueryService +import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl +import dev.usbharu.hideout.application.config.ActivityPubConfig +import dev.usbharu.hideout.application.config.ApplicationConfig +import dev.usbharu.hideout.core.external.job.DeliverPostJob +import dev.usbharu.hideout.core.query.FollowerQueryService +import dev.usbharu.hideout.core.query.UserQueryService +import dev.usbharu.hideout.core.service.job.JobQueueParentService +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.* +import utils.PostBuilder +import utils.UserBuilder +import java.net.URL +import java.time.Instant + +@ExtendWith(MockitoExtension::class) +class ApSendCreateServiceImplTest { + + @Mock + private lateinit var followerQueryService: FollowerQueryService + + @Spy + private val objectMapper: ObjectMapper = ActivityPubConfig().objectMapper() + + @Mock + private lateinit var jobQueueParentService: JobQueueParentService + + @Mock + private lateinit var userQueryService: UserQueryService + + @Mock + private lateinit var noteQueryService: NoteQueryService + + @Spy + private val applicationConfig: ApplicationConfig = ApplicationConfig(URL("https://example.com")) + + @InjectMocks + private lateinit var apSendCreateServiceImpl: ApSendCreateServiceImpl + + @Test + fun `createNote 正常なPostでCreateのジョブを発行できる`() = runTest { + val post = PostBuilder.of() + val user = UserBuilder.localUserOf(id = post.userId) + val note = Note( + name = "Post", + id = post.apId, + attributedTo = user.url, + content = post.text, + published = Instant.ofEpochMilli(post.createdAt).toString(), + to = listOfNotNull(APNoteServiceImpl.public, user.followers), + sensitive = post.sensitive, + cc = listOfNotNull(APNoteServiceImpl.public, user.followers), + inReplyTo = null + ) + val followers = listOf( + UserBuilder.remoteUserOf(), + UserBuilder.remoteUserOf(), + UserBuilder.remoteUserOf() + ) + + whenever(followerQueryService.findFollowersById(eq(post.userId))).doReturn(followers) + whenever(userQueryService.findById(eq(post.userId))).doReturn(user) + whenever(noteQueryService.findById(eq(post.id))).doReturn(note to post) + + apSendCreateServiceImpl.createNote(post) + + verify(jobQueueParentService, times(followers.size)).schedule(eq(DeliverPostJob), any()) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/follow/APReceiveFollowServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/follow/APReceiveFollowServiceImplTest.kt index a416d82a..d80183dd 100644 --- a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/follow/APReceiveFollowServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/activity/follow/APReceiveFollowServiceImplTest.kt @@ -70,8 +70,8 @@ class APReceiveFollowServiceImplTest { "type": "Follow", "name": "Follow", "actor": "https://follower.example.com", - "object": "https://example.com", - "@context": null + "object": "https://example.com" + }""" ), Json.parseToJsonElement(follow) diff --git a/src/test/kotlin/dev/usbharu/hideout/core/service/post/PostServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/core/service/post/PostServiceImplTest.kt new file mode 100644 index 00000000..e9d17e68 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/core/service/post/PostServiceImplTest.kt @@ -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::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)) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/core/service/reaction/ReactionServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/core/service/reaction/ReactionServiceImplTest.kt new file mode 100644 index 00000000..e3cf0ddd --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/core/service/reaction/ReactionServiceImplTest.kt @@ -0,0 +1,135 @@ +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.exception.FailedToGetResourcesException +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.findByPostIdAndUserIdAndEmojiId(eq(post.id), eq(post.userId), eq(0))).doThrow( + FailedToGetResourcesException::class + ) + 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() + val id = TwitterSnowflakeIdGenerateService.generateId() + whenever(reactionQueryService.findByPostIdAndUserIdAndEmojiId(eq(post.id), eq(post.userId), eq(0))).doReturn( + Reaction(id, 0, post.id, post.userId) + ) + val generateId = TwitterSnowflakeIdGenerateService.generateId() + whenever(reactionRepository.generateId()).doReturn(generateId) + + reactionServiceImpl.sendReaction("❤", post.userId, post.id) + + + verify(reactionRepository, times(1)).delete(eq(Reaction(id, 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(id, 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.findByPostIdAndUserIdAndEmojiId(eq(post.id), eq(post.userId), eq(0))).doReturn( + Reaction(0, 0, post.id, post.userId) + ) + + 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))) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/core/service/timeline/TimelineServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/core/service/timeline/TimelineServiceTest.kt new file mode 100644 index 00000000..4302a2ad --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/core/service/timeline/TimelineServiceTest.kt @@ -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> + + @Test + fun `publishTimeline ローカルの投稿はローカルのフォロワーと投稿者のタイムラインに追加される`() = runTest { + val post = PostBuilder.of() + val listOf = listOf(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(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(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(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 } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiControllerTest.kt b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiControllerTest.kt new file mode 100644 index 00000000..ff114d93 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiControllerTest.kt @@ -0,0 +1,128 @@ +package dev.usbharu.hideout.mastodon.interfaces.api.account + +import dev.usbharu.hideout.application.config.ActivityPubConfig +import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount +import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccountSource +import dev.usbharu.hideout.domain.mastodon.model.generated.Role +import dev.usbharu.hideout.mastodon.service.account.AccountApiService +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever +import org.springframework.http.MediaType +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import utils.TestTransaction + +@ExtendWith(MockitoExtension::class) +class MastodonAccountApiControllerTest { + + private lateinit var mockMvc: MockMvc + + @Spy + private lateinit var testTransaction: TestTransaction + + @Mock + private lateinit var accountApiService: AccountApiService + + @InjectMocks + private lateinit var mastodonAccountApiController: MastodonAccountApiController + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(mastodonAccountApiController).build() + } + + @Test + fun `apiV1AccountsVerifyCredentialsGet JWTで認証時に200が返ってくる`() = runTest { + + val createEmptyContext = SecurityContextHolder.createEmptyContext() + createEmptyContext.authentication = JwtAuthenticationToken( + Jwt.withTokenValue("a").header("alg", "RS236").claim("uid", "1234").build() + ) + SecurityContextHolder.setContext(createEmptyContext) + val credentialAccount = CredentialAccount( + id = "", + username = "", + acct = "", + url = "", + displayName = "", + note = "", + avatar = "", + avatarStatic = "", + header = "", + headerStatic = "", + locked = false, + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = "", + lastStatusAt = "", + statusesCount = 0, + followersCount = 0, + source = CredentialAccountSource( + note = "", + fields = emptyList(), + privacy = CredentialAccountSource.Privacy.public, + sensitive = false, + followRequestsCount = 0 + ), + noindex = false, + moved = false, + suspendex = false, + limited = false, + followingCount = 0, + role = Role(0, "ADMIN", "", 0, false) + ) + whenever(accountApiService.verifyCredentials(eq(1234))).doReturn(credentialAccount) + + val objectMapper = ActivityPubConfig().objectMapper() + + mockMvc + .get("/api/v1/accounts/verify_credentials") + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(credentialAccount)) } } + } + + @Test + fun `apiV1AccountsVerifyCredentialsGet POSTは405が返ってくる`() { + mockMvc.post("/api/v1/accounts/verify_credentials") + .andExpect { status { isMethodNotAllowed() } } + } + + @Test + fun `apiV1AccountsPost GETは405が返ってくる`() { + mockMvc.get("/api/v1/accounts") + .andExpect { status { isMethodNotAllowed() } } + } + + @Test + fun `apiV1AccountsPost アカウント作成成功時302とアカウントのurlが返ってくる`() { + mockMvc + .post("/api/v1/accounts") { + contentType = MediaType.APPLICATION_FORM_URLENCODED + param("username", "hoge") + param("password", "very_secure_password") + param("email", "email@example.com") + param("agreement", "true") + param("locale", "true") + }.asyncDispatch() + .andExpect { header { string("location", "/users/hoge") } } + .andExpect { status { isFound() } } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/apps/MastodonAppsApiControllerTest.kt b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/apps/MastodonAppsApiControllerTest.kt new file mode 100644 index 00000000..539c24e5 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/apps/MastodonAppsApiControllerTest.kt @@ -0,0 +1,115 @@ +package dev.usbharu.hideout.mastodon.interfaces.api.apps + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.hideout.domain.mastodon.model.generated.Application +import dev.usbharu.hideout.domain.mastodon.model.generated.AppsRequest +import dev.usbharu.hideout.generate.JsonOrFormModelMethodProcessor +import dev.usbharu.hideout.mastodon.service.app.AppApiService +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +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.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever +import org.springframework.http.MediaType +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.method.annotation.ModelAttributeMethodProcessor +import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor + +@ExtendWith(MockitoExtension::class) +class MastodonAppsApiControllerTest { + + @Mock + private lateinit var appApiService: AppApiService + + @InjectMocks + private lateinit var mastodonAppsApiController: MastodonAppsApiController + + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(mastodonAppsApiController).setCustomArgumentResolvers( + JsonOrFormModelMethodProcessor( + ModelAttributeMethodProcessor(false), RequestResponseBodyMethodProcessor( + mutableListOf>( + MappingJackson2HttpMessageConverter() + ) + ) + ) + ).build() + } + + @Test + fun `apiV1AppsPost JSONで作成に成功したら200が返ってくる`() = runTest { + + val appsRequest = AppsRequest( + "test", + "https://example.com", + "write", + null + ) + val application = Application( + "test", + "", + null, + "safdash;", + "aksdhgoa" + ) + + whenever(appApiService.createApp(eq(appsRequest))).doReturn(application) + + val objectMapper = jacksonObjectMapper() + val writeValueAsString = objectMapper.writeValueAsString(appsRequest) + + mockMvc + .post("/api/v1/apps") { + contentType = MediaType.APPLICATION_JSON + content = writeValueAsString + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(application)) } } + } + + @Test + fun `apiV1AppsPost FORMで作成に成功したら200が返ってくる`() = runTest { + + val appsRequest = AppsRequest( + "test", + "https://example.com", + "write", + null + ) + val application = Application( + "test", + "", + null, + "safdash;", + "aksdhgoa" + ) + + whenever(appApiService.createApp(eq(appsRequest))).doReturn(application) + + val objectMapper = jacksonObjectMapper() + + mockMvc + .post("/api/v1/apps") { + contentType = MediaType.APPLICATION_FORM_URLENCODED + param("client_name", "test") + param("redirect_uris", "https://example.com") + param("scopes", "write") + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(application)) } } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/instance/MastodonInstanceApiControllerTest.kt b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/instance/MastodonInstanceApiControllerTest.kt new file mode 100644 index 00000000..014060ef --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/instance/MastodonInstanceApiControllerTest.kt @@ -0,0 +1,107 @@ +package dev.usbharu.hideout.mastodon.interfaces.api.instance + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.hideout.domain.mastodon.model.generated.* +import dev.usbharu.hideout.mastodon.service.instance.InstanceApiService +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +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.doReturn +import org.mockito.kotlin.whenever +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.setup.MockMvcBuilders + +@ExtendWith(MockitoExtension::class) +class MastodonInstanceApiControllerTest { + + @Mock + private lateinit var instanceApiService: InstanceApiService + + @InjectMocks + private lateinit var mastodonInstanceApiController: MastodonInstanceApiController + + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(mastodonInstanceApiController).build() + } + + @Test + fun `apiV1InstanceGet GETしたら200が返ってくる`() = runTest { + + val v1Instance = V1Instance( + uri = "https://example.com", + title = "hideout", + shortDescription = "test", + description = "test instance", + email = "test@example.com", + version = "0.0.1", + urls = V1InstanceUrls(streamingApi = "https://example.com/atreaming"), + stats = V1InstanceStats(userCount = 1, statusCount = 0, domainCount = 0), + thumbnail = "https://example.com", + languages = emptyList(), + registrations = false, + approvalRequired = false, + invitesEnabled = false, + configuration = V1InstanceConfiguration( + accounts = V1InstanceConfigurationAccounts(0), + V1InstanceConfigurationStatuses(100, 4, 23), + V1InstanceConfigurationMediaAttachments(emptyList(), 100, 100, 100, 100, 100), + V1InstanceConfigurationPolls( + 10, 10, 10, 10 + ) + ), + contactAccount = Account( + id = "", + username = "", + acct = "", + url = "", + displayName = "", + note = "", + avatar = "", + avatarStatic = "", + header = "", + headerStatic = "", + locked = false, + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = "", + lastStatusAt = "", + statusesCount = 0, + followersCount = 0, + noindex = false, + moved = false, + suspendex = false, + limited = false, + followingCount = 0 + ), + emptyList() + ) + whenever(instanceApiService.v1Instance()).doReturn(v1Instance) + + val objectMapper = jacksonObjectMapper() + + mockMvc + .get("/api/v1/instance") + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(objectMapper)) } } + } + + @Test + fun `apiV1InstanceGet POSTしたら405が返ってくる`() { + mockMvc + .post("/api/v1/instance") + .andExpect { status { isMethodNotAllowed() } } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/media/MastodonMediaApiControllerTest.kt b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/media/MastodonMediaApiControllerTest.kt new file mode 100644 index 00000000..c9a56fee --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/media/MastodonMediaApiControllerTest.kt @@ -0,0 +1,93 @@ +package dev.usbharu.hideout.mastodon.interfaces.api.media + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment +import dev.usbharu.hideout.mastodon.service.media.MediaApiService +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +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.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import org.springframework.mock.web.MockMultipartFile +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.multipart +import org.springframework.test.web.servlet.setup.MockMvcBuilders + +@ExtendWith(MockitoExtension::class) +class MastodonMediaApiControllerTest { + + @Mock + private lateinit var mediaApiService: MediaApiService + + @InjectMocks + private lateinit var mastodonMediaApiController: MastodonMediaApiController + + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(mastodonMediaApiController).build() + } + + @Test + fun `apiV1MediaPost ファイルとサムネイルをアップロードできる`() = runTest { + + val mediaAttachment = MediaAttachment( + id = "1234", + type = MediaAttachment.Type.image, + url = "https://example.com", + previewUrl = "https://example.com", + remoteUrl = "https://example.com", + description = "pngImageStream", + blurhash = "", + textUrl = "https://example.com" + ) + whenever(mediaApiService.postMedia(any())).doReturn(mediaAttachment) + + val objectMapper = jacksonObjectMapper() + + mockMvc + .multipart("/api/v1/media") { + file(MockMultipartFile("file", "test.png", "image/png", "jpgImageStream".toByteArray())) + file(MockMultipartFile("thumbnail", "thumbnail.png", "image/png", "pngImageStream".toByteArray())) + param("description", "jpgImage") + param("focus", "") + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(mediaAttachment)) } } + } + + @Test + fun `apiV1MediaPost ファイルだけをアップロードできる`() = runTest { + + val mediaAttachment = MediaAttachment( + id = "1234", + type = MediaAttachment.Type.image, + url = "https://example.com", + previewUrl = "https://example.com", + remoteUrl = "https://example.com", + description = "pngImageStream", + blurhash = "", + textUrl = "https://example.com" + ) + whenever(mediaApiService.postMedia(any())).doReturn(mediaAttachment) + + val objectMapper = jacksonObjectMapper() + + mockMvc + .multipart("/api/v1/media") { + file(MockMultipartFile("file", "test.png", "image/png", "jpgImageStream".toByteArray())) + param("description", "jpgImage") + param("focus", "") + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(mediaAttachment)) } } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/status/MastodonStatusesApiControllerTest.kt b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/status/MastodonStatusesApiControllerTest.kt new file mode 100644 index 00000000..31cd3643 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/status/MastodonStatusesApiControllerTest.kt @@ -0,0 +1,129 @@ +package dev.usbharu.hideout.mastodon.interfaces.api.status + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.hideout.domain.mastodon.model.generated.Account +import dev.usbharu.hideout.domain.mastodon.model.generated.Status +import dev.usbharu.hideout.generate.JsonOrFormModelMethodProcessor +import dev.usbharu.hideout.mastodon.service.status.StatusesApiService +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +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.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever +import org.springframework.http.MediaType +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.method.annotation.ModelAttributeMethodProcessor +import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor + +@ExtendWith(MockitoExtension::class) +class MastodonStatusesApiControllerTest { + + @Mock + private lateinit var statusesApiService: StatusesApiService + + @InjectMocks + private lateinit var mastodonStatusesApiController: MastodonStatusesApiContoller + + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(mastodonStatusesApiController).setCustomArgumentResolvers( + JsonOrFormModelMethodProcessor( + ModelAttributeMethodProcessor(false), RequestResponseBodyMethodProcessor( + mutableListOf>( + MappingJackson2HttpMessageConverter() + ) + ) + ) + ).build() + } + + @Test + fun `apiV1StatusesPost JWT認証時POSTすると投稿できる`() = runTest { + val createEmptyContext = SecurityContextHolder.createEmptyContext() + createEmptyContext.authentication = JwtAuthenticationToken( + Jwt.withTokenValue("a").header("alg", "RS236").claim("uid", "1234").build() + ) + SecurityContextHolder.setContext(createEmptyContext) + val status = Status( + id = "", + uri = "", + createdAt = "", + account = Account( + id = "", + username = "", + acct = "", + url = "", + displayName = "", + note = "", + avatar = "", + avatarStatic = "", + header = "", + headerStatic = "", + locked = false, + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = "", + lastStatusAt = "", + statusesCount = 0, + followersCount = 0, + noindex = false, + moved = false, + suspendex = false, + limited = false, + followingCount = 0 + ), + content = "", + visibility = Status.Visibility.public, + sensitive = false, + spoilerText = "", + mediaAttachments = emptyList(), + mentions = emptyList(), + tags = emptyList(), + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + repliesCount = 0, + url = "https://example.com", + inReplyToId = null, + inReplyToAccountId = null, + language = "ja_JP", + text = "Test", + editedAt = null + + ) + + val objectMapper = jacksonObjectMapper() + + val statusesRequest = StatusesRequest() + + statusesRequest.status = "hello" + + whenever(statusesApiService.postStatus(eq(statusesRequest), eq(1234))).doReturn(status) + + mockMvc + .post("/api/v1/statuses") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(statusesRequest) + } + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(status)) } } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/timeline/MastodonTimelineApiControllerTest.kt b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/timeline/MastodonTimelineApiControllerTest.kt new file mode 100644 index 00000000..d3602e76 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/timeline/MastodonTimelineApiControllerTest.kt @@ -0,0 +1,261 @@ +package dev.usbharu.hideout.mastodon.interfaces.api.timeline + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.hideout.domain.mastodon.model.generated.Account +import dev.usbharu.hideout.domain.mastodon.model.generated.Status +import dev.usbharu.hideout.mastodon.service.timeline.TimelineApiService +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +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 org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.setup.MockMvcBuilders + +@ExtendWith(MockitoExtension::class) +class MastodonTimelineApiControllerTest { + + @Mock + private lateinit var timelineApiService: TimelineApiService + + @InjectMocks + private lateinit var mastodonTimelineApiController: MastodonTimelineApiController + + private lateinit var mockMvc: MockMvc + + @BeforeEach + fun setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(mastodonTimelineApiController).build() + } + + val statusList = listOf( + Status( + id = "", + uri = "", + createdAt = "", + account = Account( + id = "", + username = "", + acct = "", + url = "", + displayName = "", + note = "", + avatar = "", + avatarStatic = "", + header = "", + headerStatic = "", + locked = false, + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = "", + lastStatusAt = "", + statusesCount = 0, + followersCount = 0, + noindex = false, + moved = false, + suspendex = false, + limited = false, + followingCount = 0 + ), + content = "", + visibility = Status.Visibility.public, + sensitive = false, + spoilerText = "", + mediaAttachments = emptyList(), + mentions = emptyList(), + tags = emptyList(), + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + repliesCount = 0, + url = "https://example.com", + inReplyToId = null, + inReplyToAccountId = null, + language = "ja_JP", + text = "Test", + editedAt = null + + ), + Status( + id = "", + uri = "", + createdAt = "", + account = Account( + id = "", + username = "", + acct = "", + url = "", + displayName = "", + note = "", + avatar = "", + avatarStatic = "", + header = "", + headerStatic = "", + locked = false, + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = "", + lastStatusAt = "", + statusesCount = 0, + followersCount = 0, + noindex = false, + moved = false, + suspendex = false, + limited = false, + followingCount = 0 + ), + content = "", + visibility = Status.Visibility.public, + sensitive = false, + spoilerText = "", + mediaAttachments = emptyList(), + mentions = emptyList(), + tags = emptyList(), + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + repliesCount = 0, + url = "https://example.com", + inReplyToId = null, + inReplyToAccountId = null, + language = "ja_JP", + text = "Test", + editedAt = null + + ) + ) + + @Test + fun `apiV1TimelineHogeGet JWT認証でログインじ200が返ってくる`() = runTest { + + val createEmptyContext = SecurityContextHolder.createEmptyContext() + createEmptyContext.authentication = JwtAuthenticationToken( + Jwt.withTokenValue("a").header("alg", "RS236").claim("uid", "1234").build() + ) + SecurityContextHolder.setContext(createEmptyContext) + + whenever( + timelineApiService.homeTimeline( + eq(1234), + eq(123456), + eq(54321), + eq(1234567), + eq(20) + ) + ).doReturn(statusList) + + val objectMapper = jacksonObjectMapper() + + mockMvc + .get("/api/v1/timelines/home?max_id=123456&since_id=1234567&min_id=54321&limit=20") + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(statusList)) } } + } + + @Test + fun `apiV1TimelineHomeGet パラメーターがなくても取得できる`() = runTest { + val createEmptyContext = SecurityContextHolder.createEmptyContext() + createEmptyContext.authentication = JwtAuthenticationToken( + Jwt.withTokenValue("a").header("alg", "RS236").claim("uid", "1234").build() + ) + SecurityContextHolder.setContext(createEmptyContext) + + whenever( + timelineApiService.homeTimeline( + eq(1234), + isNull(), + isNull(), + isNull(), + eq(20) + ) + ).doReturn(statusList) + + val objectMapper = jacksonObjectMapper() + + mockMvc + .get("/api/v1/timelines/home") + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(statusList)) } } + } + + @Test + fun `apiV1TimelineHomeGet POSTには405を返す`() { + mockMvc + .post("/api/v1/timelines/home?max_id=123456&since_id=1234567&min_id=54321&limit=20") + .andExpect { status { isMethodNotAllowed() } } + } + + @Test + fun `apiV1TimelinePublicGet GETで200が返ってくる`() = runTest { + whenever( + timelineApiService.publicTimeline( + localOnly = eq(false), + remoteOnly = eq(true), + mediaOnly = eq(false), + maxId = eq(1234), + minId = eq(4321), + sinceId = eq(12345), + limit = eq(20) + ) + ).doAnswer { + println(it.arguments.joinToString()) + statusList + } + + val objectMapper = jacksonObjectMapper() + + mockMvc + .get("/api/v1/timelines/public?local=false&remote=true&only_media=false&max_id=1234&since_id=12345&min_id=4321&limit=20") + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(statusList)) } } + } + + @Test + fun `apiV1TimelinePublicGet POSTで405が返ってくる`() { + mockMvc.post("/api/v1/timelines/public") + .andExpect { status { isMethodNotAllowed() } } + } + + @Test + fun `apiV1TimelinePublicGet パラメーターがなくても取得できる`() = runTest { + whenever( + timelineApiService.publicTimeline( + localOnly = eq(false), + remoteOnly = eq(false), + mediaOnly = eq(false), + maxId = isNull(), + minId = isNull(), + sinceId = isNull(), + limit = eq(20) + ) + ).doAnswer { + println(it.arguments.joinToString()) + statusList + } + + val objectMapper = jacksonObjectMapper() + + mockMvc + .get("/api/v1/timelines/public") + .asyncDispatch() + .andExpect { status { isOk() } } + .andExpect { content { json(objectMapper.writeValueAsString(statusList)) } } + } +} diff --git a/src/test/kotlin/utils/UserBuilder.kt b/src/test/kotlin/utils/UserBuilder.kt index 3674ac7e..b62a3cb5 100644 --- a/src/test/kotlin/utils/UserBuilder.kt +++ b/src/test/kotlin/utils/UserBuilder.kt @@ -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,