Merge branch 'develop' into usbharu-patch-3

This commit is contained in:
usbharu 2023-11-09 15:37:10 +09:00 committed by GitHub
commit 60bd0951fd
36 changed files with 2406 additions and 47 deletions

View File

@ -13,6 +13,8 @@ on:
permissions:
contents: read
checks: write
id-token: write
jobs:
test:
@ -50,7 +52,12 @@ jobs:
with:
java-version: '17'
distribution: 'temurin'
- name: Gradle Build Action
- name: Run JUnit
uses: gradle/gradle-build-action@v2.8.1
with:
arguments: test
- name: Publish Test Report
uses: mikepenz/action-junit-report@v2
if: always()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'

View File

@ -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<Test> {
val cpus = Runtime.getRuntime().availableProcessors()
maxParallelForks = max(1, cpus - 1)
setForkEvery(4)
doFirst {
jvmArgs = arrayOf(
"--add-opens", "java.base/java.lang=ALL-UNNAMED"
).toMutableList()
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {
@ -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")
}
}
}

View File

@ -11,7 +11,7 @@ open class Delete : Object {
constructor(
type: List<String> = emptyList(),
name: String = "Delete",
name: String? = "Delete",
actor: String,
id: String,
`object`: Object,

View File

@ -19,10 +19,17 @@ open class JsonLd {
@JsonSerialize(include = JsonSerialize.Inclusion.NON_EMPTY, using = ContextSerializer::class)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
var context: List<String> = emptyList()
set(value) {
field = value.filterNotNull().filter { it.isNotBlank() }
}
@JsonCreator
constructor(context: List<String>) {
this.context = context
constructor(context: List<String?>?) {
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<String>() {
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<List<String>>() {
override fun isEmpty(value: List<String>?): Boolean = value.isNullOrEmpty()
override fun isEmpty(value: List<String>?): Boolean {
return value.isNullOrEmpty()
}
override fun isEmpty(provider: SerializerProvider?, value: List<String>?): Boolean {
return value.isNullOrEmpty()
}
override fun serialize(value: List<String>?, gen: JsonGenerator?, serializers: SerializerProvider) {
override fun serialize(value: List<String>?, gen: JsonGenerator?, serializers: SerializerProvider?) {
if (value.isNullOrEmpty()) {
gen?.writeNull()
serializers.defaultSerializeNull(gen)
return
}
if (value.size == 1) {

View File

@ -9,18 +9,22 @@ import dev.usbharu.hideout.activitypub.domain.model.JsonLd
open class Object : JsonLd {
@JsonSerialize(using = TypeSerializer::class)
var type: List<String> = 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<String>, 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

View File

@ -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<Person> {
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)
}

View File

@ -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<Unit> = ResponseEntity(HttpStatus.ACCEPTED)
suspend fun inbox(@RequestBody string: String): ResponseEntity<Unit>
}

View File

@ -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<Note>
}

View File

@ -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<Note> {
val context = SecurityContextHolder.getContext()
val userId =
if (context.authentication is PreAuthenticatedAuthenticationToken &&
context.authentication.details is HttpSignatureUser

View File

@ -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<Unit> = ResponseEntity(HttpStatus.ACCEPTED)
suspend fun outbox(): ResponseEntity<Unit>
}

View File

@ -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<Unit> =
override suspend fun outbox(): ResponseEntity<Unit> =
ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
}

View File

@ -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(

View File

@ -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
}

View File

@ -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 {

View File

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

View File

@ -0,0 +1,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<Delete>(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)
}
}

View File

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

View File

@ -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": "<p><a href=\"https://calckey.jp/@trapezial\" class=\"u-url mention\">@trapezial@calckey.jp</a><span> いやそういうことじゃなくて、連合先と自インスタンスで状態が狂うことが多いのでどっちに合わせるべきかと…</span></p>",
"_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<Note>(json)
val note = Note(
name = "",
id = "https://misskey.usbharu.dev/notes/9f2i9cm88e",
type = listOf("Note"),
attributedTo = "https://misskey.usbharu.dev/users/97ws8y3rj6",
content = "<p><a href=\"https://calckey.jp/@trapezial\" class=\"u-url mention\">@trapezial@calckey.jp</a><span> いやそういうことじゃなくて、連合先と自インスタンスで状態が狂うことが多いのでどっちに合わせるべきかと…</span></p>",
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)
}
}

View File

@ -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<Object>(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<Object>(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<Object>(json)
val expected = Object(
emptyList(),
null,
null,
null
)
assertEquals(expected, readValue)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)

View File

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

View File

@ -0,0 +1,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)))
}
}

View File

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

View File

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

View File

@ -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<HttpMessageConverter<*>>(
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)) } }
}
}

View File

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

View File

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

View File

@ -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<HttpMessageConverter<*>>(
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)) } }
}
}

View File

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

View File

@ -20,15 +20,15 @@ object UserBuilder {
screenName: String = name,
description: String = "This user is test user.",
password: String = "password-$id",
inbox: String = "https://$domain/$id/inbox",
outbox: String = "https://$domain/$id/outbox",
url: String = "https://$domain/$id/",
inbox: String = "https://$domain/users/$id/inbox",
outbox: String = "https://$domain/users/$id/outbox",
url: String = "https://$domain/users/$id",
publicKey: String = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
privateKey: String = "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----",
createdAt: Instant = Instant.now(),
keyId: String = "https://$domain/$id#pubkey",
followers: String = "https://$domain/$id/followers",
following: String = "https://$domain/$id/following"
keyId: String = "https://$domain/users/$id#pubkey",
followers: String = "https://$domain/users/$id/followers",
following: String = "https://$domain/users/$id/following"
): User {
return userBuilder.of(
id = id,