Merge pull request #8 from usbharu/feature/post

Feature/post
This commit is contained in:
usbharu 2023-04-21 22:23:22 +09:00 committed by GitHub
commit 2f04aef82c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 610 additions and 36 deletions

View File

@ -5,15 +5,16 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.config.ConfigData
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.plugins.*
import dev.usbharu.hideout.repository.IUserAuthRepository
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.repository.UserAuthRepository
import dev.usbharu.hideout.repository.UserRepository
import dev.usbharu.hideout.repository.*
import dev.usbharu.hideout.routing.register
import dev.usbharu.hideout.service.IPostService
import dev.usbharu.hideout.service.IUserAuthService
import dev.usbharu.hideout.service.TwitterSnowflakeIdGenerateService
import dev.usbharu.hideout.service.activitypub.*
import dev.usbharu.hideout.service.impl.PostService
import dev.usbharu.hideout.service.impl.UserAuthService
import dev.usbharu.hideout.service.impl.UserService
import dev.usbharu.hideout.service.job.JobQueueParentService
@ -25,11 +26,6 @@ import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.logging.*
import io.ktor.server.application.*
import kjob.core.Job
import kjob.core.KJob
import kjob.core.dsl.JobContextWithProps
import kjob.core.dsl.JobRegisterContext
import kjob.core.dsl.KJobFunctions
import kjob.core.kjob
import org.jetbrains.exposed.sql.Database
import org.koin.ktor.ext.inject
@ -76,15 +72,18 @@ fun Application.parent() {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
install(httpSignaturePlugin){
install(httpSignaturePlugin) {
keyMap = KtorKeyMap(get())
}
}
}
single<ActivityPubFollowService> { ActivityPubFollowServiceImpl(get(), get(), get(),get()) }
single<ActivityPubService> { ActivityPubServiceImpl(get()) }
single<ActivityPubFollowService> { ActivityPubFollowServiceImpl(get(), get(), get(), get()) }
single<ActivityPubService> { ActivityPubServiceImpl(get(), get()) }
single<UserService> { UserService(get()) }
single<ActivityPubUserService> { ActivityPubUserServiceImpl(get(), get(), get()) }
single<ActivityPubNoteService> { ActivityPubNoteServiceImpl(get(), get(), get()) }
single<IPostService> { PostService(get(), get()) }
single<IPostRepository> { PostRepositoryImpl(get(), TwitterSnowflakeIdGenerateService) }
}
@ -98,9 +97,11 @@ fun Application.parent() {
inject<HttpSignatureVerifyService>().value,
inject<ActivityPubService>().value,
inject<UserService>().value,
inject<ActivityPubUserService>().value
inject<ActivityPubUserService>().value,
inject<IPostService>().value
)
}
@Suppress("unused")
fun Application.worker() {
val kJob = kjob(ExposedKJob) {
@ -109,9 +110,14 @@ fun Application.worker() {
val activityPubService = inject<ActivityPubService>().value
kJob.register(ReceiveFollowJob){
kJob.register(ReceiveFollowJob) {
execute {
activityPubService.processActivity(this,it)
activityPubService.processActivity(this, it)
}
}
kJob.register(DeliverPostJob){
execute {
activityPubService.processActivity(this, it)
}
}
}

View File

@ -0,0 +1,53 @@
package dev.usbharu.hideout.domain.model
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.Table
object Posts : Table() {
val id = long("id")
val userId = long("userId").references(Users.id)
val overview = varchar("overview", 100).nullable()
val text = varchar("text", 3000)
val createdAt = long("createdAt")
val visibility = integer("visibility").default(0)
val url = varchar("url", 500)
val repostId = long("repostId").references(id).nullable()
val replyId = long("replyId").references(id).nullable()
override val primaryKey: PrimaryKey = PrimaryKey(id)
}
data class Post(
val userId: Long,
val overview: String? = null,
val text: String,
val createdAt: Long,
val visibility: Int,
val repostId: Long? = null,
val replyId: Long? = null
)
data class PostEntity(
val id: Long,
val userId:Long,
val overview: String? = null,
val text: String,
val createdAt: Long,
val visibility: Int,
val url: String,
val repostId: Long? = null,
val replyId: Long? = null
)
fun ResultRow.toPost():PostEntity{
return PostEntity(
id = this[Posts.id],
userId = this[Posts.userId],
overview = this[Posts.overview],
text = this[Posts.text],
createdAt = this[Posts.createdAt],
visibility = this[Posts.visibility],
url = this[Posts.url],
repostId = this[Posts.repostId],
replyId = this[Posts.replyId]
)
}

View File

@ -1,6 +1,7 @@
package dev.usbharu.hideout.domain.model
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.ResultRow
data class User(
val name: String,
@ -47,3 +48,16 @@ object Users : LongIdTable("users") {
uniqueIndex(name, domain)
}
}
fun ResultRow.toUser(): User {
return User(
this[Users.name],
this[Users.domain],
this[Users.screenName],
this[Users.description],
this[Users.inbox],
this[Users.outbox],
this[Users.url]
)
}

View File

@ -0,0 +1,30 @@
package dev.usbharu.hideout.domain.model.ap
open class Create : Object {
var `object` : Object? = null
protected constructor() : super()
constructor(type: List<String> = emptyList(), name: String, `object`: Object?) : super(add(type,"Create"), name) {
this.`object` = `object`
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Create) return false
if (!super.equals(other)) return false
return `object` == other.`object`
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "Create(`object`=$`object`) ${super.toString()}"
}
}

View File

@ -0,0 +1,53 @@
package dev.usbharu.hideout.domain.model.ap
open class Note : Object {
var id:String? = null
var attributedTo:String? = null
var content:String? = null
var published:String? = null
var to:List<String> = emptyList()
protected constructor() : super()
constructor(
type: List<String> = emptyList(),
name: String,
id: String?,
attributedTo: String?,
content: String?,
published: String?,
to: List<String> = emptyList()
) : super(add(type,"Note"), name) {
this.id = id
this.attributedTo = attributedTo
this.content = content
this.published = published
this.to = to
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Note) return false
if (!super.equals(other)) return false
if (id != other.id) return false
if (attributedTo != other.attributedTo) return false
if (content != other.content) return false
if (published != other.published) return false
return to == other.to
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (id?.hashCode() ?: 0)
result = 31 * result + (attributedTo?.hashCode() ?: 0)
result = 31 * result + (content?.hashCode() ?: 0)
result = 31 * result + (published?.hashCode() ?: 0)
result = 31 * result + to.hashCode()
return result
}
override fun toString(): String {
return "Note(id=$id, attributedTo=$attributedTo, content=$content, published=$published, to=$to) ${super.toString()}"
}
}

View File

@ -0,0 +1,6 @@
package dev.usbharu.hideout.domain.model.api
data class StatusForPost(
val status:String,
val userId:Long
)

View File

@ -9,3 +9,9 @@ object ReceiveFollowJob : HideoutJob("ReceiveFollowJob"){
val follow = string("follow")
val targetActor = string("targetActor")
}
object DeliverPostJob : HideoutJob("DeliverPostJob"){
val post = string("post")
val actor = string("actor")
val inbox = string("inbox")
}

View File

@ -3,7 +3,9 @@ package dev.usbharu.hideout.plugins
import dev.usbharu.hideout.routing.activitypub.inbox
import dev.usbharu.hideout.routing.activitypub.outbox
import dev.usbharu.hideout.routing.activitypub.usersAP
import dev.usbharu.hideout.routing.api.v1.statuses
import dev.usbharu.hideout.routing.wellknown.webfinger
import dev.usbharu.hideout.service.IPostService
import dev.usbharu.hideout.service.activitypub.ActivityPubService
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
import dev.usbharu.hideout.service.impl.UserService
@ -15,14 +17,20 @@ import io.ktor.server.routing.*
fun Application.configureRouting(
httpSignatureVerifyService: HttpSignatureVerifyService,
activityPubService: ActivityPubService,
userService:UserService,
activityPubUserService: ActivityPubUserService
userService: UserService,
activityPubUserService: ActivityPubUserService,
postService: IPostService
) {
install(AutoHeadResponse)
routing {
inbox(httpSignatureVerifyService, activityPubService)
outbox()
usersAP(activityPubUserService)
usersAP(activityPubUserService,userService)
webfinger(userService)
route("/api/v1") {
statuses(postService)
}
}
}

View File

@ -0,0 +1,10 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.Post
import dev.usbharu.hideout.domain.model.PostEntity
interface IPostRepository {
suspend fun insert(post:Post):PostEntity
suspend fun findOneById(id:Long):PostEntity
suspend fun delete(id:Long)
}

View File

@ -1,3 +0,0 @@
package dev.usbharu.hideout.repository
interface IUserKeyRepository

View File

@ -0,0 +1,65 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.*
import dev.usbharu.hideout.service.IdGenerateService
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction
class PostRepositoryImpl(database: Database, private val idGenerateService: IdGenerateService) : IPostRepository {
init {
transaction(database) {
SchemaUtils.create(Posts)
}
}
suspend fun <T> query(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
override suspend fun insert(post: Post): PostEntity {
return query {
val generateId = idGenerateService.generateId()
val name = Users.select { Users.id eq post.userId }.single().toUser().name
val postUrl = Config.configData.url + "/users/$name/posts/$generateId"
Posts.insert {
it[id] = generateId
it[userId] = post.userId
it[overview] = post.overview
it[text] = post.text
it[createdAt] = post.createdAt
it[visibility] = post.visibility
it[url] = postUrl
it[repostId] = post.repostId
it[replyId] = post.replyId
}
return@query PostEntity(
generateId,
post.userId,
post.overview,
post.text,
post.createdAt,
post.visibility,
postUrl,
post.repostId,
post.replyId
)
}
}
override suspend fun findOneById(id: Long): PostEntity {
return query {
Posts.select { Posts.id eq id }.single().toPost()
}
}
override suspend fun delete(id: Long) {
return query {
Posts.deleteWhere { Posts.id eq id }
}
}
}

View File

@ -1,4 +0,0 @@
package dev.usbharu.hideout.repository
class UserKeyRepository {
}

View File

@ -112,7 +112,9 @@ class UserRepository(private val database: Database) : IUserRepository {
}
override suspend fun findByUrls(urls: List<String>): List<UserEntity> {
TODO("Not yet implemented")
return query {
Users.select { Users.url inList urls }.map { it.toUserEntity() }
}
}
override suspend fun findFollowersById(id: Long): List<UserEntity> {

View File

@ -3,14 +3,16 @@ package dev.usbharu.hideout.routing.activitypub
import dev.usbharu.hideout.exception.ParameterNotExistException
import dev.usbharu.hideout.plugins.respondAp
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
import dev.usbharu.hideout.service.impl.UserService
import dev.usbharu.hideout.util.HttpUtil.Activity
import dev.usbharu.hideout.util.HttpUtil.JsonLd
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Routing.usersAP(activityPubUserService: ActivityPubUserService) {
fun Routing.usersAP(activityPubUserService: ActivityPubUserService, userService: UserService) {
route("/users/{name}") {
createChild(ContentTypeRouteSelector(ContentType.Application.Activity, ContentType.Application.JsonLd)).handle {
val name =
@ -21,6 +23,10 @@ fun Routing.usersAP(activityPubUserService: ActivityPubUserService) {
HttpStatusCode.OK
)
}
get {
val userEntity = userService.findByName(call.parameters["name"]!!)
call.respondText(userEntity.toString() + "\n" + userService.findFollowersById(userEntity.id))
}
}
}

View File

@ -0,0 +1,26 @@
package dev.usbharu.hideout.routing.api.v1
import dev.usbharu.hideout.domain.model.Post
import dev.usbharu.hideout.domain.model.api.StatusForPost
import dev.usbharu.hideout.service.IPostService
import dev.usbharu.hideout.service.impl.PostService
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.statuses(postService: IPostService) {
route("/statuses") {
post {
val status: StatusForPost = call.receive()
val post = Post(
userId = status.userId,
createdAt = System.currentTimeMillis(),
text = status.status,
visibility = 1
)
postService.create(post)
call.respond(status)
}
}
}

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.service
import dev.usbharu.hideout.domain.model.Post
interface IPostService {
suspend fun create(post:Post)
}

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.service
interface IdGenerateService {
suspend fun generateId():Long
}

View File

@ -0,0 +1,47 @@
package dev.usbharu.hideout.service
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.time.Instant
open class SnowflakeIdGenerateService(private val baseTime:Long) : IdGenerateService {
var lastTimeStamp: Long = -1
var sequenceId: Int = 0
val mutex = Mutex()
@Throws(IllegalStateException::class)
override suspend fun generateId(): Long {
return mutex.withLock {
var timestamp = getTime()
if (timestamp < lastTimeStamp) {
while (timestamp <= lastTimeStamp) {
delay(1L)
timestamp = getTime()
}
// throw IllegalStateException(" $lastTimeStamp $timestamp ${lastTimeStamp-timestamp} ")
}
if (timestamp == lastTimeStamp) {
sequenceId++
if (sequenceId >= 4096) {
while (timestamp <= lastTimeStamp) {
delay(1L)
timestamp = getTime()
}
sequenceId = 0
}
} else {
sequenceId = 0
}
lastTimeStamp = timestamp
return@withLock (timestamp - baseTime).shl(22).or(1L.shl(12)).or(sequenceId.toLong())
}
}
private fun getTime(): Long {
return Instant.now().toEpochMilli()
}
}

View File

@ -0,0 +1,4 @@
package dev.usbharu.hideout.service
// 2010-11-04T01:42:54.657
object TwitterSnowflakeIdGenerateService : SnowflakeIdGenerateService(1288834974657L)

View File

@ -0,0 +1,11 @@
package dev.usbharu.hideout.service.activitypub
import dev.usbharu.hideout.domain.model.PostEntity
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import kjob.core.job.JobProps
interface ActivityPubNoteService {
suspend fun createNote(post:PostEntity)
suspend fun createNoteJob(props:JobProps<DeliverPostJob>)
}

View File

@ -0,0 +1,61 @@
package dev.usbharu.hideout.service.activitypub
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.PostEntity
import dev.usbharu.hideout.domain.model.ap.Create
import dev.usbharu.hideout.domain.model.ap.Note
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.plugins.postAp
import dev.usbharu.hideout.service.impl.UserService
import dev.usbharu.hideout.service.job.JobQueueParentService
import io.ktor.client.*
import kjob.core.job.JobProps
import org.slf4j.LoggerFactory
import java.time.Instant
class ActivityPubNoteServiceImpl(
private val httpClient: HttpClient,
private val jobQueueParentService: JobQueueParentService,
private val userService: UserService
) : ActivityPubNoteService {
private val logger = LoggerFactory.getLogger(this::class.java)
override suspend fun createNote(post: PostEntity) {
val followers = userService.findFollowersById(post.userId)
val userEntity = userService.findById(post.userId)
val note = Config.configData.objectMapper.writeValueAsString(post)
followers.forEach { followerEntity ->
jobQueueParentService.schedule(DeliverPostJob) {
props[it.actor] = userEntity.url
props[it.post] = note
props[it.inbox] = followerEntity.inbox
}
}
}
override suspend fun createNoteJob(props: JobProps<DeliverPostJob>) {
val actor = props[DeliverPostJob.actor]
val postEntity = Config.configData.objectMapper.readValue<PostEntity>(props[DeliverPostJob.post])
val note = Note(
name = "Note",
id = postEntity.url,
attributedTo = actor,
content = postEntity.text,
published = Instant.ofEpochMilli(postEntity.createdAt).toString(),
to = listOf("https://www.w3.org/ns/activitystreams#Public", actor + "/followers")
)
val inbox = props[DeliverPostJob.inbox]
logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox)
httpClient.postAp(
urlString = inbox,
username = "$actor#pubkey",
jsonLd = Create(
name = "Create Note",
`object` = note
)
)
}
}

View File

@ -1,9 +1,10 @@
package dev.usbharu.hideout.service.activitypub
import com.fasterxml.jackson.databind.JsonNode
import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.ActivityPubResponse
import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.domain.model.job.HideoutJob
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.exception.JsonParseException
@ -11,7 +12,10 @@ import kjob.core.dsl.JobContextWithProps
import kjob.core.job.JobProps
import org.slf4j.LoggerFactory
class ActivityPubServiceImpl(private val activityPubFollowService: ActivityPubFollowService) : ActivityPubService {
class ActivityPubServiceImpl(
private val activityPubFollowService: ActivityPubFollowService,
private val activityPubNoteService: ActivityPubNoteService
) : ActivityPubService {
val logger = LoggerFactory.getLogger(this::class.java)
override fun parseActivity(json: String): ActivityType {
@ -70,8 +74,10 @@ class ActivityPubServiceImpl(private val activityPubFollowService: ActivityPubFo
}
override suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) {
logger.debug("processActivity: ${hideoutJob.name}")
when (hideoutJob) {
ReceiveFollowJob -> activityPubFollowService.receiveFollowJob(job.props as JobProps<ReceiveFollowJob>)
DeliverPostJob -> activityPubNoteService.createNoteJob(job.props as JobProps<DeliverPostJob>)
}
}

View File

@ -16,6 +16,7 @@ import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import org.slf4j.LoggerFactory
class ActivityPubUserServiceImpl(
private val userService: UserService,
@ -23,6 +24,8 @@ class ActivityPubUserServiceImpl(
private val httpClient: HttpClient
) :
ActivityPubUserService {
private val logger = LoggerFactory.getLogger(this::class.java)
override suspend fun getPersonByName(name: String): Person {
// TODO: JOINで書き直し
val userEntity = userService.findByName(name)
@ -91,8 +94,8 @@ class ActivityPubUserServiceImpl(
name = person.preferredUsername
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
domain = url.substringAfter(":").substringBeforeLast("/"),
screenName = person.name ?: throw IllegalActivityPubObjectException("name is null"),
description = person.summary ?: throw IllegalActivityPubObjectException("summary is null"),
screenName = (person.name ?: person.preferredUsername) ?: throw IllegalActivityPubObjectException("preferredUsername is null"),
description = person.summary ?: "",
inbox = person.inbox ?: throw IllegalActivityPubObjectException("inbox is null"),
outbox = person.outbox ?: throw IllegalActivityPubObjectException("outbox is null"),
url = url

View File

@ -0,0 +1,18 @@
package dev.usbharu.hideout.service.impl
import dev.usbharu.hideout.domain.model.Post
import dev.usbharu.hideout.repository.IPostRepository
import dev.usbharu.hideout.service.IPostService
import dev.usbharu.hideout.service.activitypub.ActivityPubNoteService
import dev.usbharu.hideout.service.job.JobQueueParentService
import org.slf4j.LoggerFactory
class PostService(private val postRepository:IPostRepository,private val activityPubNoteService: ActivityPubNoteService) : IPostService {
private val logger = LoggerFactory.getLogger(this::class.java)
override suspend fun create(post: Post) {
logger.debug("create post={}",post)
val postEntity = postRepository.insert(post)
activityPubNoteService.createNote(postEntity)
}
}

View File

@ -6,9 +6,12 @@ import kjob.core.KJob
import kjob.core.dsl.ScheduleContext
import kjob.core.kjob
import org.jetbrains.exposed.sql.Database
import org.slf4j.LoggerFactory
class KJobJobQueueParentService(private val database: Database) : JobQueueParentService {
private val logger = LoggerFactory.getLogger(this::class.java)
val kjob: KJob = kjob(ExposedKJob) {
connectionDatabase = database
isWorker = false
@ -19,6 +22,7 @@ class KJobJobQueueParentService(private val database: Database) : JobQueueParent
}
override suspend fun <J : Job> schedule(job: J,block:ScheduleContext<J>.(J)->Unit) {
logger.debug("schedule job={}",job.name)
kjob.schedule(job,block)
}
}

View File

@ -9,4 +9,7 @@
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
<logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/>
<logger name="Exposed" level="INFO"/>
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
</configuration>

View File

@ -27,7 +27,7 @@ class InboxRoutingKtTest {
}
application {
configureSerialization()
configureRouting(mock(), mock(), mock(), mock())
configureRouting(mock(), mock(), mock(), mock(),mock())
}
client.get("/inbox").let {
Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status)
@ -50,7 +50,7 @@ class InboxRoutingKtTest {
application {
configureStatusPages()
configureSerialization()
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService)
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService,mock())
}
client.post("/inbox").let {
Assertions.assertEquals(HttpStatusCode.BadRequest, it.status)
@ -64,7 +64,7 @@ class InboxRoutingKtTest {
}
application {
configureSerialization()
configureRouting(mock(), mock(), mock(), mock())
configureRouting(mock(), mock(), mock(), mock(),mock())
}
client.get("/users/test/inbox").let {
Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status)
@ -87,7 +87,7 @@ class InboxRoutingKtTest {
application {
configureStatusPages()
configureSerialization()
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService)
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService,mock())
}
client.post("/users/test/inbox").let {
Assertions.assertEquals(HttpStatusCode.BadRequest, it.status)

View File

@ -70,7 +70,7 @@ class UsersAPTest {
application {
configureSerialization()
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService)
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService,mock())
}
client.get("/users/test") {
accept(ContentType.Application.Activity)

View File

@ -0,0 +1,35 @@
package dev.usbharu.hideout.service
//import kotlinx.coroutines.NonCancellable.message
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class TwitterSnowflakeIdGenerateServiceTest {
@Test
fun noDuplicateTest() = runBlocking {
val mutex = Mutex()
val mutableListOf = mutableListOf<Long>()
coroutineScope {
repeat(500000) {
launch(Dispatchers.IO) {
val id = TwitterSnowflakeIdGenerateService.generateId()
mutex.withLock {
mutableListOf.add(id)
}
}
}
}
assertEquals(0, mutableListOf.size - mutableListOf.toSet().size)
}
}

View File

@ -0,0 +1,92 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
package dev.usbharu.hideout.service.activitypub
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.config.ConfigData
import dev.usbharu.hideout.domain.model.PostEntity
import dev.usbharu.hideout.domain.model.UserEntity
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
import dev.usbharu.hideout.service.impl.UserService
import dev.usbharu.hideout.service.job.JobQueueParentService
import io.ktor.client.*
import io.ktor.client.engine.mock.*
import kjob.core.job.JobProps
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Test
import org.mockito.Mockito.eq
import org.mockito.kotlin.*
import utils.JsonObjectMapper
import kotlin.test.assertEquals
class ActivityPubNoteServiceImplTest {
@Test
fun `createPost 新しい投稿`() = runTest {
val followers = listOf<UserEntity>(
UserEntity(
2L,
"follower",
"follower.example.com",
"followerUser",
"test follower user",
"https://follower.example.com/inbox",
"https://follower.example.com/outbox",
"https://follower.example.com"
),
UserEntity(
3L,
"follower2",
"follower2.example.com",
"follower2User",
"test follower2 user",
"https://follower2.example.com/inbox",
"https://follower2.example.com/outbox",
"https:.//follower2.example.com"
)
)
val userService = mock<UserService> {
onBlocking { findById(eq(1L)) } doReturn UserEntity(
1L,
"test",
"example.com",
"testUser",
"test user",
"https://example.com/inbox",
"https://example.com/outbox",
"https:.//example.com"
)
onBlocking { findFollowersById(eq(1L)) } doReturn followers
}
val jobQueueParentService = mock<JobQueueParentService>()
val activityPubNoteService = ActivityPubNoteServiceImpl(mock(), jobQueueParentService, userService)
val postEntity = PostEntity(
1L, 1L, null, "test text", 1L, 1, "https://example.com"
)
activityPubNoteService.createNote(postEntity)
verify(jobQueueParentService, times(2)).schedule(eq(DeliverPostJob), any())
}
@Test
fun `createPostJob 新しい投稿のJob`() = runTest {
Config.configData = ConfigData(objectMapper = JsonObjectMapper.objectMapper)
val httpClient = HttpClient(MockEngine { httpRequestData ->
assertEquals("https://follower.example.com/inbox", httpRequestData.url.toString())
respondOk()
})
val activityPubNoteService = ActivityPubNoteServiceImpl(httpClient, mock(), mock())
activityPubNoteService.createNoteJob(
JobProps(
data = mapOf<String, Any>(
DeliverPostJob.actor.name to "https://follower.example.com",
DeliverPostJob.post.name to "{\"id\":1,\"userId\":1,\"inReplyToId\":null,\"text\":\"test text\"," +
"\"createdAt\":1,\"updatedAt\":1,\"url\":\"https://example.com\"}",
DeliverPostJob.inbox.name to "https://follower.example.com/inbox"
),
json = Json
)
)
}
}