mirror of https://github.com/usbharu/Hideout.git
commit
34e48cb429
11
detekt.yml
11
detekt.yml
|
@ -1,5 +1,7 @@
|
|||
build:
|
||||
maxIssues: 20
|
||||
weights:
|
||||
Indentation: 0
|
||||
|
||||
style:
|
||||
ClassOrdering:
|
||||
|
@ -32,6 +34,15 @@ style:
|
|||
ForbiddenComment:
|
||||
active: false
|
||||
|
||||
ThrowsCount:
|
||||
active: false
|
||||
|
||||
UseCheckOrError:
|
||||
active: false
|
||||
|
||||
UseRequire:
|
||||
active: false
|
||||
|
||||
complexity:
|
||||
CognitiveComplexMethod:
|
||||
active: true
|
||||
|
|
|
@ -100,11 +100,12 @@ fun Application.parent() {
|
|||
inject<JwkProvider>().value,
|
||||
)
|
||||
configureRouting(
|
||||
inject<HttpSignatureVerifyService>().value,
|
||||
inject<ActivityPubService>().value,
|
||||
inject<IUserService>().value,
|
||||
inject<ActivityPubUserService>().value,
|
||||
inject<IPostService>().value
|
||||
httpSignatureVerifyService = inject<HttpSignatureVerifyService>().value,
|
||||
activityPubService = inject<ActivityPubService>().value,
|
||||
userService = inject<IUserService>().value,
|
||||
activityPubUserService = inject<ActivityPubUserService>().value,
|
||||
postService = inject<IPostService>().value,
|
||||
userApiService = inject<IUserApiService>().value,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package dev.usbharu.hideout.domain.model
|
||||
|
||||
data class Acct(val username: String, val domain: String? = null, val isRemote: Boolean = domain == null)
|
|
@ -1,54 +0,0 @@
|
|||
package dev.usbharu.hideout.domain.model
|
||||
|
||||
import dev.usbharu.hideout.repository.Users
|
||||
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]
|
||||
)
|
||||
}
|
|
@ -9,7 +9,11 @@ open class Follow : Object {
|
|||
name: String,
|
||||
`object`: String?,
|
||||
actor: String?
|
||||
) : super(add(type, "Follow"), name, actor) {
|
||||
) : super(
|
||||
type = add(type, "Follow"),
|
||||
name = name,
|
||||
actor = actor
|
||||
) {
|
||||
this.`object` = `object`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,10 +8,14 @@ open class Key : Object {
|
|||
constructor(
|
||||
type: List<String>,
|
||||
name: String,
|
||||
id: String?,
|
||||
id: String,
|
||||
owner: String?,
|
||||
publicKeyPem: String?
|
||||
) : super(add(type, "Key"), name, id) {
|
||||
) : super(
|
||||
type = add(list = type, type = "Key"),
|
||||
name = name,
|
||||
id = id
|
||||
) {
|
||||
this.owner = owner
|
||||
this.publicKeyPem = publicKeyPem
|
||||
}
|
||||
|
@ -21,16 +25,16 @@ open class Key : Object {
|
|||
if (other !is Key) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (owner != other.owner) return false
|
||||
return publicKeyPem == other.publicKeyPem
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + (id?.hashCode() ?: 0)
|
||||
result = 31 * result + (owner?.hashCode() ?: 0)
|
||||
result = 31 * result + (publicKeyPem?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String = "Key(owner=$owner, publicKeyPem=$publicKeyPem) ${super.toString()}"
|
||||
}
|
||||
|
|
|
@ -27,7 +27,8 @@ open class Object : JsonLd {
|
|||
|
||||
if (type != other.type) return false
|
||||
if (name != other.name) return false
|
||||
return actor == other.actor
|
||||
if (actor != other.actor) return false
|
||||
return id == other.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
@ -35,10 +36,11 @@ open class Object : JsonLd {
|
|||
result = 31 * result + type.hashCode()
|
||||
result = 31 * result + (name?.hashCode() ?: 0)
|
||||
result = 31 * result + (actor?.hashCode() ?: 0)
|
||||
result = 31 * result + (id?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String = "Object(type=$type, name=$name, actor=$actor) ${super.toString()}"
|
||||
override fun toString(): String = "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}"
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package dev.usbharu.hideout.domain.model.api
|
||||
package dev.usbharu.hideout.domain.model.api.mastodon
|
||||
|
||||
data class StatusForPost(
|
||||
val status: String,
|
|
@ -0,0 +1,12 @@
|
|||
package dev.usbharu.hideout.domain.model.hideout.dto
|
||||
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
|
||||
|
||||
data class PostCreateDto(
|
||||
val text: String,
|
||||
val overview: String? = null,
|
||||
val visibility: Visibility = Visibility.PUBLIC,
|
||||
val repostId: Long? = null,
|
||||
val repolyId: Long? = null,
|
||||
val userId: Long
|
||||
)
|
|
@ -0,0 +1,27 @@
|
|||
package dev.usbharu.hideout.domain.model.hideout.dto
|
||||
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
||||
|
||||
data class UserResponse(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val domain: String,
|
||||
val screenName: String,
|
||||
val description: String = "",
|
||||
val url: String,
|
||||
val createdAt: Long
|
||||
) {
|
||||
companion object {
|
||||
fun from(user: User): UserResponse {
|
||||
return UserResponse(
|
||||
id = user.id,
|
||||
name = user.name,
|
||||
domain = user.domain,
|
||||
screenName = user.screenName,
|
||||
description = user.description,
|
||||
url = user.url,
|
||||
createdAt = user.createdAt.toEpochMilli()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package dev.usbharu.hideout.domain.model.hideout.entity
|
||||
|
||||
data class Post(
|
||||
val id: Long,
|
||||
val userId: Long,
|
||||
val overview: String? = null,
|
||||
val text: String,
|
||||
val createdAt: Long,
|
||||
val visibility: Visibility,
|
||||
val url: String,
|
||||
val repostId: Long? = null,
|
||||
val replyId: Long? = null
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package dev.usbharu.hideout.domain.model.hideout.entity
|
||||
|
||||
enum class Visibility {
|
||||
PUBLIC,
|
||||
UNLISTED,
|
||||
FOLLOWERS,
|
||||
DIRECT
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package dev.usbharu.hideout.domain.model.hideout.form
|
||||
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
|
||||
|
||||
data class Post(
|
||||
val text: String,
|
||||
val overview: String? = null,
|
||||
val visibility: Visibility = Visibility.PUBLIC,
|
||||
val repostId: Long? = null,
|
||||
val replyId: Long? = null
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
package dev.usbharu.hideout.domain.model.hideout.form
|
||||
|
||||
data class UserCreate(val username: String, val password: String)
|
|
@ -0,0 +1,8 @@
|
|||
package dev.usbharu.hideout.exception
|
||||
|
||||
class PostNotFoundException : IllegalArgumentException {
|
||||
constructor() : super()
|
||||
constructor(s: String?) : super(s)
|
||||
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||
constructor(cause: Throwable?) : super(cause)
|
||||
}
|
|
@ -3,9 +3,12 @@ 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.api.internal.v1.posts
|
||||
import dev.usbharu.hideout.routing.api.internal.v1.users
|
||||
import dev.usbharu.hideout.routing.api.mastodon.v1.statuses
|
||||
import dev.usbharu.hideout.routing.wellknown.webfinger
|
||||
import dev.usbharu.hideout.service.IPostService
|
||||
import dev.usbharu.hideout.service.IUserApiService
|
||||
import dev.usbharu.hideout.service.activitypub.ActivityPubService
|
||||
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
|
||||
import dev.usbharu.hideout.service.impl.IUserService
|
||||
|
@ -14,12 +17,14 @@ import io.ktor.server.application.*
|
|||
import io.ktor.server.plugins.autohead.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun Application.configureRouting(
|
||||
httpSignatureVerifyService: HttpSignatureVerifyService,
|
||||
activityPubService: ActivityPubService,
|
||||
userService: IUserService,
|
||||
activityPubUserService: ActivityPubUserService,
|
||||
postService: IPostService
|
||||
postService: IPostService,
|
||||
userApiService: IUserApiService
|
||||
) {
|
||||
install(AutoHeadResponse)
|
||||
routing {
|
||||
|
@ -31,5 +36,9 @@ fun Application.configureRouting(
|
|||
route("/api/v1") {
|
||||
statuses(postService)
|
||||
}
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
users(userService, userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,11 +35,14 @@ fun Application.configureSecurity(
|
|||
acceptLeeway(3)
|
||||
}
|
||||
validate { jwtCredential ->
|
||||
if (jwtCredential.payload.getClaim("username")?.asString().isNullOrBlank().not()) {
|
||||
JWTPrincipal(jwtCredential.payload)
|
||||
} else {
|
||||
null
|
||||
val uid = jwtCredential.payload.getClaim("uid")
|
||||
if (uid.isMissing) {
|
||||
return@validate null
|
||||
}
|
||||
if (uid.asLong() == null) {
|
||||
return@validate null
|
||||
}
|
||||
return@validate JWTPrincipal(jwtCredential.payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,8 +76,8 @@ fun Application.configureSecurity(
|
|||
}
|
||||
authenticate(TOKEN_AUTH) {
|
||||
get("/auth-check") {
|
||||
val principal = call.principal<JWTPrincipal>()
|
||||
val username = principal!!.payload.getClaim("username")
|
||||
val principal = call.principal<JWTPrincipal>() ?: throw IllegalStateException("no principal")
|
||||
val username = principal.payload.getClaim("uid")
|
||||
call.respondText("Hello $username")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package dev.usbharu.hideout.repository
|
||||
|
||||
import dev.usbharu.hideout.domain.model.Post
|
||||
import dev.usbharu.hideout.domain.model.PostEntity
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
||||
|
||||
interface IPostRepository {
|
||||
suspend fun insert(post: Post): PostEntity
|
||||
suspend fun findOneById(id: Long): PostEntity
|
||||
suspend fun generateId(): Long
|
||||
suspend fun save(post: Post): Post
|
||||
suspend fun findOneById(id: Long): Post
|
||||
suspend fun delete(id: Long)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package dev.usbharu.hideout.repository
|
||||
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.domain.model.Post
|
||||
import dev.usbharu.hideout.domain.model.PostEntity
|
||||
import dev.usbharu.hideout.domain.model.Posts
|
||||
import dev.usbharu.hideout.domain.model.toPost
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
|
||||
import dev.usbharu.hideout.service.IdGenerateService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.jetbrains.exposed.sql.*
|
||||
|
@ -22,41 +19,30 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun generateId(): Long = idGenerateService.generateId()
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
suspend fun <T> query(block: suspend () -> T): T =
|
||||
newSuspendedTransaction(Dispatchers.IO) { block() }
|
||||
|
||||
override suspend fun insert(post: Post): PostEntity {
|
||||
override suspend fun save(post: Post): Post {
|
||||
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[id] = post.id
|
||||
it[userId] = post.userId
|
||||
it[overview] = post.overview
|
||||
it[text] = post.text
|
||||
it[createdAt] = post.createdAt
|
||||
it[visibility] = post.visibility
|
||||
it[url] = postUrl
|
||||
it[visibility] = post.visibility.ordinal
|
||||
it[url] = post.url
|
||||
it[repostId] = post.repostId
|
||||
it[replyId] = post.replyId
|
||||
}
|
||||
return@query PostEntity(
|
||||
id = generateId,
|
||||
userId = post.userId,
|
||||
overview = post.overview,
|
||||
text = post.text,
|
||||
createdAt = post.createdAt,
|
||||
visibility = post.visibility,
|
||||
url = postUrl,
|
||||
repostId = post.repostId,
|
||||
replyId = post.replyId
|
||||
)
|
||||
return@query post
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findOneById(id: Long): PostEntity {
|
||||
override suspend fun findOneById(id: Long): Post {
|
||||
return query {
|
||||
Posts.select { Posts.id eq id }.single().toPost()
|
||||
}
|
||||
|
@ -68,3 +54,30 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun ResultRow.toPost(): Post {
|
||||
return Post(
|
||||
id = this[Posts.id],
|
||||
userId = this[Posts.userId],
|
||||
overview = this[Posts.overview],
|
||||
text = this[Posts.text],
|
||||
createdAt = this[Posts.createdAt],
|
||||
visibility = Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] },
|
||||
url = this[Posts.url],
|
||||
repostId = this[Posts.repostId],
|
||||
replyId = this[Posts.replyId]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,9 @@ fun Routing.usersAP(activityPubUserService: ActivityPubUserService, userService:
|
|||
)
|
||||
}
|
||||
get {
|
||||
val userEntity = userService.findByNameLocalUser(call.parameters["name"]!!)
|
||||
val userEntity = userService.findByNameLocalUser(
|
||||
call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='name') does not exist.")
|
||||
)
|
||||
call.respondText(userEntity.toString() + "\n" + userService.findFollowersById(userEntity.id))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package dev.usbharu.hideout.routing.api.internal.v1
|
||||
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
|
||||
import dev.usbharu.hideout.domain.model.hideout.form.Post
|
||||
import dev.usbharu.hideout.exception.ParameterNotExistException
|
||||
import dev.usbharu.hideout.exception.PostNotFoundException
|
||||
import dev.usbharu.hideout.plugins.TOKEN_AUTH
|
||||
import dev.usbharu.hideout.service.IPostService
|
||||
import dev.usbharu.hideout.util.AcctUtil
|
||||
import dev.usbharu.hideout.util.InstantParseUtil
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.jwt.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
@Suppress("LongMethod")
|
||||
fun Route.posts(postService: IPostService) {
|
||||
route("/posts") {
|
||||
authenticate(TOKEN_AUTH) {
|
||||
post {
|
||||
val principal = call.principal<JWTPrincipal>() ?: throw IllegalStateException("no principal")
|
||||
val userId = principal.payload.getClaim("uid").asLong()
|
||||
|
||||
val receive = call.receive<Post>()
|
||||
val postCreateDto = PostCreateDto(
|
||||
text = receive.text,
|
||||
overview = receive.overview,
|
||||
visibility = receive.visibility,
|
||||
repostId = receive.repostId,
|
||||
repolyId = receive.replyId,
|
||||
userId = userId
|
||||
)
|
||||
val create = postService.create(postCreateDto)
|
||||
call.response.header("Location", create.url)
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
authenticate(TOKEN_AUTH, optional = true) {
|
||||
get {
|
||||
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
|
||||
val since = InstantParseUtil.parse(call.request.queryParameters["since"])
|
||||
val until = InstantParseUtil.parse(call.request.queryParameters["until"])
|
||||
val minId = call.request.queryParameters["minId"]?.toLong()
|
||||
val maxId = call.request.queryParameters["maxId"]?.toLong()
|
||||
val limit = call.request.queryParameters["limit"]?.toInt()
|
||||
call.respond(HttpStatusCode.OK, postService.findAll(since, until, minId, maxId, limit, userId))
|
||||
}
|
||||
get("/{id}") {
|
||||
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
|
||||
val id = call.parameters["id"]?.toLong()
|
||||
?: throw ParameterNotExistException("Parameter(id='postsId') does not exist.")
|
||||
val post = (
|
||||
postService.findByIdForUser(id, userId)
|
||||
?: throw PostNotFoundException("$id was not found or is not authorized.")
|
||||
)
|
||||
call.respond(post)
|
||||
}
|
||||
}
|
||||
}
|
||||
route("/users/{name}/posts") {
|
||||
authenticate(TOKEN_AUTH, optional = true) {
|
||||
get {
|
||||
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
|
||||
val targetUserName = call.parameters["name"]
|
||||
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
|
||||
val targetUserId = targetUserName.toLongOrNull()
|
||||
val posts = if (targetUserId == null) {
|
||||
val acct = AcctUtil.parse(targetUserName)
|
||||
postService.findByUserNameAndDomainForUser(
|
||||
acct.username,
|
||||
acct.domain ?: Config.configData.domain,
|
||||
forUserId = userId
|
||||
)
|
||||
} else {
|
||||
postService.findByUserIdForUser(targetUserId, forUserId = userId)
|
||||
}
|
||||
call.respond(posts)
|
||||
}
|
||||
get("/{id}") {
|
||||
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
|
||||
val id = call.parameters["id"]?.toLong()
|
||||
?: throw ParameterNotExistException("Parameter(name='postsId' does not exist.")
|
||||
val post = (
|
||||
postService.findByIdForUser(id, userId)
|
||||
?: throw PostNotFoundException("$id was not found or is not authorized.")
|
||||
)
|
||||
call.respond(post)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package dev.usbharu.hideout.routing.api.internal.v1
|
||||
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
|
||||
import dev.usbharu.hideout.domain.model.hideout.form.UserCreate
|
||||
import dev.usbharu.hideout.exception.ParameterNotExistException
|
||||
import dev.usbharu.hideout.plugins.TOKEN_AUTH
|
||||
import dev.usbharu.hideout.service.IUserApiService
|
||||
import dev.usbharu.hideout.service.impl.IUserService
|
||||
import dev.usbharu.hideout.util.AcctUtil
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.jwt.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
@Suppress("LongMethod")
|
||||
fun Route.users(userService: IUserService, userApiService: IUserApiService) {
|
||||
route("/users") {
|
||||
get {
|
||||
call.respond(userApiService.findAll())
|
||||
}
|
||||
post {
|
||||
val userCreate = call.receive<UserCreate>()
|
||||
if (userService.usernameAlreadyUse(userCreate.username)) {
|
||||
return@post call.respond(HttpStatusCode.BadRequest)
|
||||
}
|
||||
val user = userService.createLocalUser(
|
||||
UserCreateDto(
|
||||
userCreate.username,
|
||||
userCreate.username,
|
||||
"",
|
||||
userCreate.password
|
||||
)
|
||||
)
|
||||
call.response.header("Location", "${Config.configData.url}/api/internal/v1/users/${user.name}")
|
||||
call.respond(HttpStatusCode.Created)
|
||||
}
|
||||
route("/{name}") {
|
||||
authenticate(TOKEN_AUTH, optional = true) {
|
||||
get {
|
||||
val userParameter = (
|
||||
call.parameters["name"]
|
||||
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
|
||||
)
|
||||
if (userParameter.toLongOrNull() != null) {
|
||||
return@get call.respond(userApiService.findById(userParameter.toLong()))
|
||||
} else {
|
||||
val acct = AcctUtil.parse(userParameter)
|
||||
return@get call.respond(userApiService.findByAcct(acct))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
route("/followers") {
|
||||
get {
|
||||
val userParameter = call.parameters["name"]
|
||||
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
|
||||
if (userParameter.toLongOrNull() != null) {
|
||||
return@get call.respond(userApiService.findFollowers(userParameter.toLong()))
|
||||
}
|
||||
val acct = AcctUtil.parse(userParameter)
|
||||
return@get call.respond(userApiService.findFollowersByAcct(acct))
|
||||
}
|
||||
authenticate(TOKEN_AUTH) {
|
||||
post {
|
||||
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
|
||||
?: throw IllegalStateException("no principal")
|
||||
val userParameter = call.parameters["name"]
|
||||
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
|
||||
if (userParameter.toLongOrNull() != null) {
|
||||
if (userService.addFollowers(userParameter.toLong(), userId)) {
|
||||
return@post call.respond(HttpStatusCode.OK)
|
||||
} else {
|
||||
return@post call.respond(HttpStatusCode.Accepted)
|
||||
}
|
||||
}
|
||||
val acct = AcctUtil.parse(userParameter)
|
||||
val targetUser = userApiService.findByAcct(acct)
|
||||
if (userService.addFollowers(targetUser.id, userId)) {
|
||||
return@post call.respond(HttpStatusCode.OK)
|
||||
} else {
|
||||
return@post call.respond(HttpStatusCode.Accepted)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
route("/following") {
|
||||
get {
|
||||
val userParameter = (
|
||||
call.parameters["name"]
|
||||
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
|
||||
)
|
||||
if (userParameter.toLongOrNull() != null) {
|
||||
return@get call.respond(userApiService.findFollowings(userParameter.toLong()))
|
||||
}
|
||||
val acct = AcctUtil.parse(userParameter)
|
||||
return@get call.respond(userApiService.findFollowingsByAcct(acct))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package dev.usbharu.hideout.routing.api.mastodon.v1
|
||||
|
||||
import dev.usbharu.hideout.service.IPostService
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
fun Route.statuses(postService: IPostService) {
|
||||
// route("/statuses") {
|
||||
// post {
|
||||
// val status: StatusForPost = call.receive()
|
||||
// val post = dev.usbharu.hideout.domain.model.hideout.form.Post(
|
||||
// userId = status.userId,
|
||||
// createdAt = System.currentTimeMillis(),
|
||||
// text = status.status,
|
||||
// visibility = 1
|
||||
// )
|
||||
// postService.create(post)
|
||||
// call.respond(status)
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
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 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,61 @@
|
|||
package dev.usbharu.hideout.service
|
||||
|
||||
import dev.usbharu.hideout.domain.model.Post
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
||||
import java.time.Instant
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
interface IPostService {
|
||||
suspend fun create(post: Post)
|
||||
suspend fun create(post: Post): Post
|
||||
suspend fun create(post: PostCreateDto): Post
|
||||
suspend fun findAll(
|
||||
since: Instant? = null,
|
||||
until: Instant? = null,
|
||||
minId: Long? = null,
|
||||
maxId: Long? = null,
|
||||
limit: Int? = 10,
|
||||
userId: Long? = null
|
||||
): List<Post>
|
||||
|
||||
suspend fun findById(id: String): Post
|
||||
|
||||
/**
|
||||
* 権限を考慮して投稿を取得します。
|
||||
*
|
||||
* @param id
|
||||
* @param userId
|
||||
* @return
|
||||
*/
|
||||
suspend fun findByIdForUser(id: Long, userId: Long?): Post?
|
||||
|
||||
/**
|
||||
* 権限を考慮してユーザーの投稿を取得します。
|
||||
*
|
||||
* @param userId
|
||||
* @param forUserId
|
||||
* @return
|
||||
*/
|
||||
suspend fun findByUserIdForUser(
|
||||
userId: Long,
|
||||
since: Instant? = null,
|
||||
until: Instant? = null,
|
||||
minId: Long? = null,
|
||||
maxId: Long? = null,
|
||||
limit: Int? = null,
|
||||
forUserId: Long? = null
|
||||
): List<Post>
|
||||
|
||||
suspend fun findByUserNameAndDomainForUser(
|
||||
userName: String,
|
||||
domain: String = Config.configData.domain,
|
||||
since: Instant? = null,
|
||||
until: Instant? = null,
|
||||
minId: Long? = null,
|
||||
maxId: Long? = null,
|
||||
limit: Int? = null,
|
||||
forUserId: Long? = null
|
||||
): List<Post>
|
||||
|
||||
suspend fun delete(id: String)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package dev.usbharu.hideout.service
|
||||
|
||||
import dev.usbharu.hideout.domain.model.Acct
|
||||
import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse
|
||||
|
||||
interface IUserApiService {
|
||||
suspend fun findAll(limit: Int? = 100, offset: Long = 0): List<UserResponse>
|
||||
|
||||
suspend fun findById(id: Long): UserResponse
|
||||
|
||||
suspend fun findByIds(ids: List<Long>): List<UserResponse>
|
||||
|
||||
suspend fun findByAcct(acct: Acct): UserResponse
|
||||
|
||||
suspend fun findByAccts(accts: List<Acct>): List<UserResponse>
|
||||
|
||||
suspend fun findFollowers(userId: Long): List<UserResponse>
|
||||
|
||||
suspend fun findFollowings(userId: Long): List<UserResponse>
|
||||
|
||||
suspend fun findFollowersByAcct(acct: Acct): List<UserResponse>
|
||||
|
||||
suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse>
|
||||
}
|
|
@ -52,7 +52,7 @@ class JwtServiceImpl(
|
|||
.withAudience("${Config.configData.url}/users/${user.name}")
|
||||
.withIssuer(Config.configData.url)
|
||||
.withKeyId(keyId.await().toString())
|
||||
.withClaim("username", user.name)
|
||||
.withClaim("uid", user.id)
|
||||
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
|
||||
.sign(Algorithm.RSA256(publicKey.await(), privateKey.await()))
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ class ServerInitialiseServiceImpl(private val metaRepository: IMetaRepository) :
|
|||
return
|
||||
}
|
||||
|
||||
if (isVersionChanged(savedMeta!!)) {
|
||||
if (isVersionChanged(requireNotNull(savedMeta))) {
|
||||
logger.info("Version changed!! (${savedMeta.version} -> $implementationVersion)")
|
||||
updateVersion(savedMeta, implementationVersion)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package dev.usbharu.hideout.service
|
||||
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.domain.model.Acct
|
||||
import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse
|
||||
import dev.usbharu.hideout.service.impl.IUserService
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
@Single
|
||||
class UserApiServiceImpl(private val userService: IUserService) : IUserApiService {
|
||||
override suspend fun findAll(limit: Int?, offset: Long): List<UserResponse> =
|
||||
userService.findAll(limit, offset).map { UserResponse.from(it) }
|
||||
|
||||
override suspend fun findById(id: Long): UserResponse = UserResponse.from(userService.findById(id))
|
||||
|
||||
override suspend fun findByIds(ids: List<Long>): List<UserResponse> =
|
||||
userService.findByIds(ids).map { UserResponse.from(it) }
|
||||
|
||||
override suspend fun findByAcct(acct: Acct): UserResponse =
|
||||
UserResponse.from(userService.findByNameAndDomain(acct.username, acct.domain))
|
||||
|
||||
override suspend fun findByAccts(accts: List<Acct>): List<UserResponse> {
|
||||
return userService.findByNameAndDomains(accts.map { it.username to (it.domain ?: Config.configData.domain) })
|
||||
.map { UserResponse.from(it) }
|
||||
}
|
||||
|
||||
override suspend fun findFollowers(userId: Long): List<UserResponse> =
|
||||
userService.findFollowersById(userId).map { UserResponse.from(it) }
|
||||
|
||||
override suspend fun findFollowings(userId: Long): List<UserResponse> =
|
||||
userService.findFollowingById(userId).map { UserResponse.from(it) }
|
||||
|
||||
override suspend fun findFollowersByAcct(acct: Acct): List<UserResponse> =
|
||||
userService.findFollowersByNameAndDomain(acct.username, acct.domain).map { UserResponse.from(it) }
|
||||
|
||||
override suspend fun findFollowingsByAcct(acct: Acct): List<UserResponse> =
|
||||
userService.findFollowingByNameAndDomain(acct.username, acct.domain).map { UserResponse.from(it) }
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
package dev.usbharu.hideout.service.activitypub
|
||||
|
||||
import dev.usbharu.hideout.domain.model.PostEntity
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
||||
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
|
||||
import kjob.core.job.JobProps
|
||||
|
||||
interface ActivityPubNoteService {
|
||||
|
||||
suspend fun createNote(post: PostEntity)
|
||||
suspend fun createNote(post: Post)
|
||||
suspend fun createNoteJob(props: JobProps<DeliverPostJob>)
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ 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.hideout.entity.Post
|
||||
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
|
||||
import dev.usbharu.hideout.plugins.postAp
|
||||
import dev.usbharu.hideout.service.impl.IUserService
|
||||
|
@ -24,7 +24,7 @@ class ActivityPubNoteServiceImpl(
|
|||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
override suspend fun createNote(post: PostEntity) {
|
||||
override suspend fun createNote(post: Post) {
|
||||
val followers = userService.findFollowersById(post.userId)
|
||||
val userEntity = userService.findById(post.userId)
|
||||
val note = Config.configData.objectMapper.writeValueAsString(post)
|
||||
|
@ -39,7 +39,7 @@ class ActivityPubNoteServiceImpl(
|
|||
|
||||
override suspend fun createNoteJob(props: JobProps<DeliverPostJob>) {
|
||||
val actor = props[DeliverPostJob.actor]
|
||||
val postEntity = Config.configData.objectMapper.readValue<PostEntity>(props[DeliverPostJob.post])
|
||||
val postEntity = Config.configData.objectMapper.readValue<Post>(props[DeliverPostJob.post])
|
||||
val note = Note(
|
||||
name = "Note",
|
||||
id = postEntity.url,
|
||||
|
|
|
@ -16,6 +16,8 @@ interface IUserService {
|
|||
|
||||
suspend fun findByNameLocalUser(name: String): User
|
||||
|
||||
suspend fun findByNameAndDomain(name: String, domain: String? = null): User
|
||||
|
||||
suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User>
|
||||
|
||||
suspend fun findByUrl(url: String): User
|
||||
|
@ -30,5 +32,18 @@ interface IUserService {
|
|||
|
||||
suspend fun findFollowersById(id: Long): List<User>
|
||||
|
||||
suspend fun addFollowers(id: Long, follower: Long)
|
||||
suspend fun findFollowersByNameAndDomain(name: String, domain: String?): List<User>
|
||||
|
||||
suspend fun findFollowingById(id: Long): List<User>
|
||||
|
||||
suspend fun findFollowingByNameAndDomain(name: String, domain: String?): List<User>
|
||||
|
||||
/**
|
||||
* フォロワーを追加する
|
||||
*
|
||||
* @param id
|
||||
* @param follower
|
||||
* @return リクエストが成功したか
|
||||
*/
|
||||
suspend fun addFollowers(id: Long, follower: Long): Boolean
|
||||
}
|
||||
|
|
|
@ -1,23 +1,130 @@
|
|||
package dev.usbharu.hideout.service.impl
|
||||
|
||||
import dev.usbharu.hideout.domain.model.Post
|
||||
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
|
||||
import dev.usbharu.hideout.repository.IPostRepository
|
||||
import dev.usbharu.hideout.repository.Posts
|
||||
import dev.usbharu.hideout.repository.UsersFollowers
|
||||
import dev.usbharu.hideout.repository.toPost
|
||||
import dev.usbharu.hideout.service.IPostService
|
||||
import dev.usbharu.hideout.service.activitypub.ActivityPubNoteService
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inSubQuery
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.orIfNotNull
|
||||
import org.jetbrains.exposed.sql.orWhere
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.koin.core.annotation.Single
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Instant
|
||||
|
||||
@Single
|
||||
class PostService(
|
||||
private val postRepository: IPostRepository,
|
||||
private val activityPubNoteService: ActivityPubNoteService
|
||||
private val activityPubNoteService: ActivityPubNoteService,
|
||||
private val userService: IUserService
|
||||
) : IPostService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
override suspend fun create(post: Post) {
|
||||
override suspend fun create(post: Post): Post {
|
||||
logger.debug("create post={}", post)
|
||||
val postEntity = postRepository.insert(post)
|
||||
val postEntity = postRepository.save(post)
|
||||
activityPubNoteService.createNote(postEntity)
|
||||
return post
|
||||
}
|
||||
|
||||
override suspend fun create(post: PostCreateDto): Post {
|
||||
logger.debug("create post={}", post)
|
||||
val user = userService.findById(post.userId)
|
||||
val id = postRepository.generateId()
|
||||
val postEntity = Post(
|
||||
id = id,
|
||||
userId = user.id,
|
||||
overview = null,
|
||||
text = post.text,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
visibility = Visibility.PUBLIC,
|
||||
url = "${user.url}/posts/$id",
|
||||
repostId = null,
|
||||
replyId = null
|
||||
)
|
||||
postRepository.save(postEntity)
|
||||
return postEntity
|
||||
}
|
||||
|
||||
override suspend fun findAll(
|
||||
since: Instant?,
|
||||
until: Instant?,
|
||||
minId: Long?,
|
||||
maxId: Long?,
|
||||
limit: Int?,
|
||||
userId: Long?
|
||||
): List<Post> {
|
||||
return transaction {
|
||||
val select = Posts.select {
|
||||
Posts.visibility.eq(Visibility.PUBLIC.ordinal)
|
||||
}
|
||||
if (userId != null) {
|
||||
select.orWhere {
|
||||
Posts.userId.inSubQuery(
|
||||
UsersFollowers.slice(UsersFollowers.userId).select(UsersFollowers.followerId eq userId)
|
||||
)
|
||||
}
|
||||
}
|
||||
select.map { it.toPost() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findById(id: String): Post {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun findByIdForUser(id: Long, userId: Long?): Post? {
|
||||
return transaction {
|
||||
val select = Posts.select(
|
||||
Posts.id.eq(id).and(
|
||||
Posts.visibility.eq(Visibility.PUBLIC.ordinal).orIfNotNull(
|
||||
userId?.let {
|
||||
Posts.userId.inSubQuery(
|
||||
UsersFollowers.slice(UsersFollowers.userId).select(UsersFollowers.followerId.eq(userId))
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
select.singleOrNull()?.toPost()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findByUserIdForUser(
|
||||
userId: Long,
|
||||
since: Instant?,
|
||||
until: Instant?,
|
||||
minId: Long?,
|
||||
maxId: Long?,
|
||||
limit: Int?,
|
||||
forUserId: Long?
|
||||
): List<Post> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun findByUserNameAndDomainForUser(
|
||||
userName: String,
|
||||
domain: String,
|
||||
since: Instant?,
|
||||
until: Instant?,
|
||||
minId: Long?,
|
||||
maxId: Long?,
|
||||
limit: Int?,
|
||||
forUserId: Long?
|
||||
): List<Post> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun delete(id: String) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,11 @@ class UserService(private val userRepository: IUserRepository, private val userA
|
|||
?: throw UserNotFoundException("$name was not found.")
|
||||
}
|
||||
|
||||
override suspend fun findByNameAndDomain(name: String, domain: String?): User {
|
||||
return userRepository.findByNameAndDomain(name, domain ?: Config.configData.domain)
|
||||
?: throw UserNotFoundException("$name was not found.")
|
||||
}
|
||||
|
||||
override suspend fun findByNameAndDomains(names: List<Pair<String, String>>): List<User> =
|
||||
userRepository.findByNameAndDomains(names)
|
||||
|
||||
|
@ -87,6 +92,21 @@ class UserService(private val userRepository: IUserRepository, private val userA
|
|||
}
|
||||
|
||||
override suspend fun findFollowersById(id: Long): List<User> = userRepository.findFollowersById(id)
|
||||
override suspend fun findFollowersByNameAndDomain(name: String, domain: String?): List<User> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun addFollowers(id: Long, follower: Long) = userRepository.createFollower(id, follower)
|
||||
override suspend fun findFollowingById(id: Long): List<User> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun findFollowingByNameAndDomain(name: String, domain: String?): List<User> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
// TODO APのフォロー処理を作る
|
||||
override suspend fun addFollowers(id: Long, follower: Long): Boolean {
|
||||
userRepository.createFollower(id, follower)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package dev.usbharu.hideout.util
|
||||
|
||||
import dev.usbharu.hideout.domain.model.Acct
|
||||
|
||||
object AcctUtil {
|
||||
fun parse(string: String): Acct {
|
||||
if (string.isBlank()) {
|
||||
throw IllegalArgumentException("Invalid acct.(Blank)")
|
||||
}
|
||||
return when (string.count { c -> c == '@' }) {
|
||||
0 -> {
|
||||
Acct(string)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
if (string.startsWith("@")) {
|
||||
Acct(string.substring(1 until string.length))
|
||||
} else {
|
||||
Acct(string.substringBefore("@"), string.substringAfter("@"))
|
||||
}
|
||||
}
|
||||
|
||||
2 -> {
|
||||
if (string.startsWith("@")) {
|
||||
val substring = string.substring(1 until string.length)
|
||||
val userName = substring.substringBefore("@")
|
||||
val domain = substring.substringAfter("@")
|
||||
Acct(
|
||||
userName,
|
||||
domain.ifBlank { null }
|
||||
)
|
||||
} else {
|
||||
throw IllegalArgumentException("Invalid acct.(@ are in the wrong position)")
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw IllegalArgumentException("Invalid acct.(Too many @)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package dev.usbharu.hideout.util
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeParseException
|
||||
|
||||
object InstantParseUtil {
|
||||
fun parse(str: String?): Instant? {
|
||||
return try {
|
||||
Instant.ofEpochMilli(str?.toLong() ?: return null)
|
||||
} catch (e: NumberFormatException) {
|
||||
try {
|
||||
Instant.parse(str)
|
||||
} catch (e: DateTimeParseException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,572 @@
|
|||
openapi: "3.0.3"
|
||||
info:
|
||||
title: "hideout API"
|
||||
description: "hideout API"
|
||||
version: "1.0.0"
|
||||
servers:
|
||||
- url: "https://hideout"
|
||||
paths:
|
||||
/.well-known/jwks.json:
|
||||
get:
|
||||
description: ""
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: "string"
|
||||
/auth-check:
|
||||
get:
|
||||
description: ""
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: "string"
|
||||
examples:
|
||||
Example#1:
|
||||
value: ""
|
||||
/login:
|
||||
post:
|
||||
description: ""
|
||||
requestBody:
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
$ref: "#/components/schemas/UserLogin"
|
||||
required: true
|
||||
responses:
|
||||
"401":
|
||||
description: "Unauthorized"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
$ref: "#/components/schemas/JwtToken"
|
||||
/refresh-token:
|
||||
post:
|
||||
description: ""
|
||||
requestBody:
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
$ref: "#/components/schemas/RefreshToken"
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
$ref: "#/components/schemas/JwtToken"
|
||||
/.well-known/webfinger:
|
||||
get:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "resource"
|
||||
in: "query"
|
||||
required: false
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
$ref: "#/components/schemas/WebFinger"
|
||||
/api/internal/v1/posts:
|
||||
get:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "since"
|
||||
in: "query"
|
||||
required: false
|
||||
schema:
|
||||
type: "string"
|
||||
- name: "until"
|
||||
in: "query"
|
||||
required: false
|
||||
schema:
|
||||
type: "string"
|
||||
- name: "minId"
|
||||
in: "query"
|
||||
required: false
|
||||
schema:
|
||||
type: "number"
|
||||
- name: "maxId"
|
||||
in: "query"
|
||||
required: false
|
||||
schema:
|
||||
type: "number"
|
||||
- name: "limit"
|
||||
in: "query"
|
||||
required: false
|
||||
schema:
|
||||
type: "integer"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/components/schemas/Post"
|
||||
post:
|
||||
description: ""
|
||||
requestBody:
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
$ref: "#/components/schemas/Post"
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
/api/internal/v1/posts/{id}:
|
||||
get:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "number"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
$ref: "#/components/schemas/Post"
|
||||
/api/internal/v1/users:
|
||||
get:
|
||||
description: ""
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/components/schemas/UserResponse"
|
||||
post:
|
||||
description: ""
|
||||
requestBody:
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
$ref: "#/components/schemas/UserCreate"
|
||||
required: true
|
||||
responses:
|
||||
"400":
|
||||
description: "Bad Request"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
"201":
|
||||
description: "Created"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
/api/internal/v1/users/{name}:
|
||||
get:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "name"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
/api/internal/v1/users/{name}/followers:
|
||||
get:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "name"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "array"
|
||||
items:
|
||||
type: "object"
|
||||
post:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "name"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
"202":
|
||||
description: "Accepted"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
/api/internal/v1/users/{name}/following:
|
||||
get:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "name"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "array"
|
||||
items:
|
||||
type: "object"
|
||||
/api/internal/v1/users/{name}/posts:
|
||||
get:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "name"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/components/schemas/Post"
|
||||
/api/internal/v1/users/{name}/posts/{id}:
|
||||
get:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "number"
|
||||
- name: "name"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
$ref: "#/components/schemas/Post"
|
||||
/inbox:
|
||||
get:
|
||||
description: ""
|
||||
responses:
|
||||
"405":
|
||||
description: "Method Not Allowed"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
post:
|
||||
description: ""
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "string"
|
||||
"501":
|
||||
description: "Not Implemented"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
/outbox:
|
||||
get:
|
||||
description: ""
|
||||
responses:
|
||||
"501":
|
||||
description: "Not Implemented"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
post:
|
||||
description: ""
|
||||
responses:
|
||||
"501":
|
||||
description: "Not Implemented"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
/users/{name}:
|
||||
get:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "name"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: "string"
|
||||
/users/{name}/inbox:
|
||||
get:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "name"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"405":
|
||||
description: "Method Not Allowed"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
post:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "name"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "string"
|
||||
"501":
|
||||
description: "Not Implemented"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
/users/{name}/outbox:
|
||||
get:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "name"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"501":
|
||||
description: "Not Implemented"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
post:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "name"
|
||||
in: "path"
|
||||
required: true
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"501":
|
||||
description: "Not Implemented"
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: "object"
|
||||
/:
|
||||
get:
|
||||
description: ""
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: "string"
|
||||
/register:
|
||||
get:
|
||||
description: ""
|
||||
responses:
|
||||
"200":
|
||||
description: "OK"
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: "string"
|
||||
post:
|
||||
description: ""
|
||||
parameters:
|
||||
- name: "password"
|
||||
in: "query"
|
||||
required: false
|
||||
schema:
|
||||
type: "string"
|
||||
- name: "username"
|
||||
in: "query"
|
||||
required: false
|
||||
schema:
|
||||
type: "string"
|
||||
responses:
|
||||
"200":
|
||||
description: "OK <br> Redirect"
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: "string"
|
||||
examples:
|
||||
Example#1:
|
||||
value: ""
|
||||
Example#2:
|
||||
value: "/register"
|
||||
Example#3:
|
||||
value: "/register"
|
||||
Example#4:
|
||||
value: "/register"
|
||||
components:
|
||||
schemas:
|
||||
UserLogin:
|
||||
type: "object"
|
||||
properties:
|
||||
username:
|
||||
type: "string"
|
||||
password:
|
||||
type: "string"
|
||||
JwtToken:
|
||||
type: "object"
|
||||
properties:
|
||||
token:
|
||||
type: "string"
|
||||
refreshToken:
|
||||
type: "string"
|
||||
RefreshToken:
|
||||
type: "object"
|
||||
properties:
|
||||
refreshToken:
|
||||
type: "string"
|
||||
Link:
|
||||
type: "object"
|
||||
properties:
|
||||
rel:
|
||||
type: "string"
|
||||
type:
|
||||
type: "string"
|
||||
href:
|
||||
type: "string"
|
||||
WebFinger:
|
||||
type: "object"
|
||||
properties:
|
||||
subject:
|
||||
type: "string"
|
||||
links:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/components/schemas/Link"
|
||||
Post:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
userId:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
overview:
|
||||
type: "string"
|
||||
text:
|
||||
type: "string"
|
||||
createdAt:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
visibility:
|
||||
type: "string"
|
||||
enum:
|
||||
- "PUBLIC"
|
||||
- "UNLISTED"
|
||||
- "FOLLOWERS"
|
||||
- "DIRECT"
|
||||
url:
|
||||
type: "string"
|
||||
repostId:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
replyId:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
UserResponse:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
name:
|
||||
type: "string"
|
||||
domain:
|
||||
type: "string"
|
||||
screenName:
|
||||
type: "string"
|
||||
description:
|
||||
type: "string"
|
||||
url:
|
||||
type: "string"
|
||||
createdAt:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
UserCreate:
|
||||
type: "object"
|
||||
properties:
|
||||
username:
|
||||
type: "string"
|
||||
password:
|
||||
type: "string"
|
|
@ -217,7 +217,7 @@ class SecurityKtTest {
|
|||
.withAudience("${Config.configData.url}/users/test")
|
||||
.withIssuer(Config.configData.url)
|
||||
.withKeyId(kid.toString())
|
||||
.withClaim("username", "test")
|
||||
.withClaim("uid", 123456L)
|
||||
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
|
||||
.sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey))
|
||||
val metaService = mock<IMetaService> {
|
||||
|
@ -255,7 +255,7 @@ class SecurityKtTest {
|
|||
header("Authorization", "Bearer $token")
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.OK, call.response.status)
|
||||
assertEquals("Hello \"test\"", call.response.bodyAsText())
|
||||
assertEquals("Hello 123456", call.response.bodyAsText())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,7 +277,7 @@ class SecurityKtTest {
|
|||
.withAudience("${Config.configData.url}/users/test")
|
||||
.withIssuer(Config.configData.url)
|
||||
.withKeyId(kid.toString())
|
||||
.withClaim("username", "test")
|
||||
.withClaim("uid", 123345L)
|
||||
.withExpiresAt(now.minus(30, ChronoUnit.MINUTES))
|
||||
.sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey))
|
||||
val metaService = mock<IMetaService> {
|
||||
|
@ -335,7 +335,7 @@ class SecurityKtTest {
|
|||
.withAudience("${Config.configData.url}/users/test")
|
||||
.withIssuer("https://example.com")
|
||||
.withKeyId(kid.toString())
|
||||
.withClaim("username", "test")
|
||||
.withClaim("uid", 12345L)
|
||||
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
|
||||
.sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey))
|
||||
val metaService = mock<IMetaService> {
|
||||
|
@ -393,7 +393,7 @@ class SecurityKtTest {
|
|||
.withAudience("${Config.configData.url}/users/test")
|
||||
.withIssuer(Config.configData.url)
|
||||
.withKeyId(kid.toString())
|
||||
.withClaim("username", "")
|
||||
.withClaim("uid", null as Long?)
|
||||
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
|
||||
.sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey))
|
||||
val metaService = mock<IMetaService> {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package dev.usbharu.hideout.routing.activitypub
|
||||
|
||||
import dev.usbharu.hideout.exception.JsonParseException
|
||||
import dev.usbharu.hideout.plugins.configureRouting
|
||||
import dev.usbharu.hideout.plugins.configureSerialization
|
||||
import dev.usbharu.hideout.plugins.configureStatusPages
|
||||
import dev.usbharu.hideout.service.activitypub.ActivityPubService
|
||||
|
@ -11,6 +10,7 @@ import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
|
|||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.config.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.testing.*
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -27,7 +27,9 @@ class InboxRoutingKtTest {
|
|||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureRouting(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
inbox(mock(), mock())
|
||||
}
|
||||
}
|
||||
client.get("/inbox").let {
|
||||
Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status)
|
||||
|
@ -45,18 +47,14 @@ class InboxRoutingKtTest {
|
|||
val activityPubService = mock<ActivityPubService> {
|
||||
on { parseActivity(any()) } doThrow JsonParseException()
|
||||
}
|
||||
val userService = mock<IUserService>()
|
||||
val activityPubUserService = mock<ActivityPubUserService>()
|
||||
mock<IUserService>()
|
||||
mock<ActivityPubUserService>()
|
||||
application {
|
||||
configureStatusPages()
|
||||
configureSerialization()
|
||||
configureRouting(
|
||||
httpSignatureVerifyService,
|
||||
activityPubService,
|
||||
userService,
|
||||
activityPubUserService,
|
||||
mock()
|
||||
)
|
||||
routing {
|
||||
inbox(httpSignatureVerifyService, activityPubService)
|
||||
}
|
||||
}
|
||||
client.post("/inbox").let {
|
||||
Assertions.assertEquals(HttpStatusCode.BadRequest, it.status)
|
||||
|
@ -70,7 +68,9 @@ class InboxRoutingKtTest {
|
|||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureRouting(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
inbox(mock(), mock())
|
||||
}
|
||||
}
|
||||
client.get("/users/test/inbox").let {
|
||||
Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status)
|
||||
|
@ -88,18 +88,14 @@ class InboxRoutingKtTest {
|
|||
val activityPubService = mock<ActivityPubService> {
|
||||
on { parseActivity(any()) } doThrow JsonParseException()
|
||||
}
|
||||
val userService = mock<IUserService>()
|
||||
val activityPubUserService = mock<ActivityPubUserService>()
|
||||
mock<IUserService>()
|
||||
mock<ActivityPubUserService>()
|
||||
application {
|
||||
configureStatusPages()
|
||||
configureSerialization()
|
||||
configureRouting(
|
||||
httpSignatureVerifyService,
|
||||
activityPubService,
|
||||
userService,
|
||||
activityPubUserService,
|
||||
mock()
|
||||
)
|
||||
routing {
|
||||
inbox(httpSignatureVerifyService, activityPubService)
|
||||
}
|
||||
}
|
||||
client.post("/users/test/inbox").let {
|
||||
Assertions.assertEquals(HttpStatusCode.BadRequest, it.status)
|
||||
|
|
|
@ -10,18 +10,16 @@ import dev.usbharu.hideout.domain.model.ap.Image
|
|||
import dev.usbharu.hideout.domain.model.ap.Key
|
||||
import dev.usbharu.hideout.domain.model.ap.Person
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
||||
import dev.usbharu.hideout.plugins.configureRouting
|
||||
import dev.usbharu.hideout.plugins.configureSerialization
|
||||
import dev.usbharu.hideout.service.activitypub.ActivityPubService
|
||||
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
|
||||
import dev.usbharu.hideout.service.impl.IUserService
|
||||
import dev.usbharu.hideout.service.signature.HttpSignatureVerifyService
|
||||
import dev.usbharu.hideout.util.HttpUtil.Activity
|
||||
import dev.usbharu.hideout.util.HttpUtil.JsonLd
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.config.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.testing.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
|
@ -64,8 +62,6 @@ class UsersAPTest {
|
|||
)
|
||||
person.context = listOf("https://www.w3.org/ns/activitystreams")
|
||||
|
||||
val httpSignatureVerifyService = mock<HttpSignatureVerifyService> {}
|
||||
val activityPubService = mock<ActivityPubService> {}
|
||||
val userService = mock<IUserService> {}
|
||||
|
||||
val activityPubUserService = mock<ActivityPubUserService> {
|
||||
|
@ -74,13 +70,9 @@ class UsersAPTest {
|
|||
|
||||
application {
|
||||
configureSerialization()
|
||||
configureRouting(
|
||||
httpSignatureVerifyService,
|
||||
activityPubService,
|
||||
userService,
|
||||
activityPubUserService,
|
||||
mock()
|
||||
)
|
||||
routing {
|
||||
usersAP(activityPubUserService, userService)
|
||||
}
|
||||
}
|
||||
client.get("/users/test") {
|
||||
accept(ContentType.Application.Activity)
|
||||
|
@ -130,8 +122,6 @@ class UsersAPTest {
|
|||
)
|
||||
person.context = listOf("https://www.w3.org/ns/activitystreams")
|
||||
|
||||
val httpSignatureVerifyService = mock<HttpSignatureVerifyService> {}
|
||||
val activityPubService = mock<ActivityPubService> {}
|
||||
val userService = mock<IUserService> {}
|
||||
|
||||
val activityPubUserService = mock<ActivityPubUserService> {
|
||||
|
@ -140,13 +130,9 @@ class UsersAPTest {
|
|||
|
||||
application {
|
||||
configureSerialization()
|
||||
configureRouting(
|
||||
httpSignatureVerifyService,
|
||||
activityPubService,
|
||||
userService,
|
||||
activityPubUserService,
|
||||
mock()
|
||||
)
|
||||
routing {
|
||||
usersAP(activityPubUserService, userService)
|
||||
}
|
||||
}
|
||||
client.get("/users/test") {
|
||||
accept(ContentType.Application.JsonLd)
|
||||
|
@ -205,13 +191,9 @@ class UsersAPTest {
|
|||
)
|
||||
}
|
||||
application {
|
||||
configureRouting(
|
||||
mock(),
|
||||
mock(),
|
||||
userService,
|
||||
mock(),
|
||||
mock()
|
||||
)
|
||||
routing {
|
||||
usersAP(mock(), userService)
|
||||
}
|
||||
}
|
||||
client.get("/users/test") {
|
||||
accept(ContentType.Text.Html)
|
||||
|
|
|
@ -0,0 +1,619 @@
|
|||
package dev.usbharu.hideout.routing.api.internal.v1
|
||||
|
||||
import com.auth0.jwt.interfaces.Claim
|
||||
import com.auth0.jwt.interfaces.Payload
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
|
||||
import dev.usbharu.hideout.plugins.TOKEN_AUTH
|
||||
import dev.usbharu.hideout.plugins.configureSecurity
|
||||
import dev.usbharu.hideout.plugins.configureSerialization
|
||||
import dev.usbharu.hideout.service.IPostService
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.jwt.*
|
||||
import io.ktor.server.config.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.testing.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.kotlin.*
|
||||
import utils.JsonObjectMapper
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class PostsTest {
|
||||
|
||||
@Test
|
||||
fun 認証情報無しでpostsにGETしたらPUBLICな投稿一覧が返ってくる() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val posts = listOf(
|
||||
Post(
|
||||
id = 12345,
|
||||
userId = 4321,
|
||||
text = "test1",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/1"
|
||||
),
|
||||
Post(
|
||||
id = 123456,
|
||||
userId = 4322,
|
||||
text = "test2",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/2"
|
||||
)
|
||||
)
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking {
|
||||
findAll(
|
||||
since = anyOrNull(),
|
||||
until = anyOrNull(),
|
||||
minId = anyOrNull(),
|
||||
maxId = anyOrNull(),
|
||||
limit = anyOrNull(),
|
||||
userId = isNull()
|
||||
)
|
||||
} doReturn posts
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/posts").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertContentEquals(posts, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun 認証情報ありでpostsにGETすると権限のある投稿が返ってくる() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val claim = mock<Claim> {
|
||||
on { asLong() } doReturn 1234
|
||||
}
|
||||
val payload = mock<Payload> {
|
||||
on { getClaim(eq("uid")) } doReturn claim
|
||||
}
|
||||
|
||||
val posts = listOf(
|
||||
Post(
|
||||
id = 12345,
|
||||
userId = 4321,
|
||||
text = "test1",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/1"
|
||||
),
|
||||
Post(
|
||||
id = 123456,
|
||||
userId = 4322,
|
||||
text = "test2",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/2"
|
||||
),
|
||||
Post(
|
||||
id = 1234567,
|
||||
userId = 4333,
|
||||
text = "Followers only",
|
||||
visibility = Visibility.FOLLOWERS,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/3"
|
||||
)
|
||||
)
|
||||
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking {
|
||||
findAll(
|
||||
since = anyOrNull(),
|
||||
until = anyOrNull(),
|
||||
minId = anyOrNull(),
|
||||
maxId = anyOrNull(),
|
||||
limit = anyOrNull(),
|
||||
userId = isNotNull()
|
||||
)
|
||||
} doReturn posts
|
||||
}
|
||||
application {
|
||||
authentication {
|
||||
bearer(TOKEN_AUTH) {
|
||||
authenticate {
|
||||
JWTPrincipal(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
configureSerialization()
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
client.get("/api/internal/v1/posts") {
|
||||
header("Authorization", "Bearer asdkaf")
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `posts id にGETしたらPUBLICな投稿を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val post = Post(
|
||||
12345, 1234, text = "aaa", visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(), url = "https://example.com/posts/1"
|
||||
)
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking { findByIdForUser(any(), anyOrNull()) } doReturn post
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
client.get("/api/internal/v1/posts/1").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `認証情報ありでposts id にGETしたら権限のある投稿を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val post = Post(
|
||||
12345, 1234, text = "aaa", visibility = Visibility.FOLLOWERS,
|
||||
createdAt = Instant.now().toEpochMilli(), url = "https://example.com/posts/1"
|
||||
)
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking { findByIdForUser(any(), isNotNull()) } doReturn post
|
||||
}
|
||||
val claim = mock<Claim> {
|
||||
on { asLong() } doReturn 1234
|
||||
}
|
||||
val payload = mock<Payload> {
|
||||
on { getClaim(eq("uid")) } doReturn claim
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
authentication {
|
||||
bearer(TOKEN_AUTH) {
|
||||
authenticate {
|
||||
JWTPrincipal(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
client.get("/api/internal/v1/posts/1") {
|
||||
header("Authorization", "Bearer asdkaf")
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `posts-post postsにpostしたら投稿できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val claim = mock<Claim> {
|
||||
on { asLong() } doReturn 1234
|
||||
}
|
||||
val payload = mock<Payload> {
|
||||
on { getClaim(eq("uid")) } doReturn claim
|
||||
}
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking { create(any<PostCreateDto>()) } doAnswer {
|
||||
val argument = it.getArgument<PostCreateDto>(0)
|
||||
Post(
|
||||
123L,
|
||||
argument.userId,
|
||||
null,
|
||||
argument.text,
|
||||
Instant.now().toEpochMilli(),
|
||||
Visibility.PUBLIC,
|
||||
"https://example.com"
|
||||
)
|
||||
}
|
||||
}
|
||||
application {
|
||||
authentication {
|
||||
|
||||
bearer(TOKEN_AUTH) {
|
||||
authenticate {
|
||||
println("aaaaaaaaaaaa")
|
||||
JWTPrincipal(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
configureSerialization()
|
||||
}
|
||||
|
||||
val post = dev.usbharu.hideout.domain.model.hideout.form.Post("test")
|
||||
client.post("/api/internal/v1/posts") {
|
||||
header("Authorization", "Bearer asdkaf")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(Config.configData.objectMapper.writeValueAsString(post))
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals("https://example.com", headers["Location"])
|
||||
}
|
||||
argumentCaptor<PostCreateDto> {
|
||||
verify(postService).create(capture())
|
||||
assertEquals(PostCreateDto("test", userId = 1234), firstValue)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users userId postsにGETしたらユーザーのPUBLICな投稿一覧を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val posts = listOf(
|
||||
Post(
|
||||
id = 12345,
|
||||
userId = 1,
|
||||
text = "test1",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/1"
|
||||
),
|
||||
Post(
|
||||
id = 123456,
|
||||
userId = 1,
|
||||
text = "test2",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/2"
|
||||
)
|
||||
)
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking {
|
||||
findByUserIdForUser(
|
||||
userId = any(),
|
||||
since = anyOrNull(),
|
||||
until = anyOrNull(),
|
||||
minId = anyOrNull(),
|
||||
maxId = anyOrNull(),
|
||||
limit = anyOrNull(),
|
||||
forUserId = anyOrNull()
|
||||
)
|
||||
} doReturn posts
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/1/posts").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(posts, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users username postsにGETしたらユーザーのPUBLICな投稿一覧を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val posts = listOf(
|
||||
Post(
|
||||
id = 12345,
|
||||
userId = 1,
|
||||
text = "test1",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/1"
|
||||
),
|
||||
Post(
|
||||
id = 123456,
|
||||
userId = 1,
|
||||
text = "test2",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/2"
|
||||
)
|
||||
)
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking {
|
||||
findByUserNameAndDomainForUser(
|
||||
userName = eq("test1"),
|
||||
domain = eq(Config.configData.domain),
|
||||
since = anyOrNull(),
|
||||
until = anyOrNull(),
|
||||
minId = anyOrNull(),
|
||||
maxId = anyOrNull(),
|
||||
limit = anyOrNull(),
|
||||
forUserId = anyOrNull()
|
||||
)
|
||||
} doReturn posts
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/test1/posts").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(posts, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users username@domain postsにGETしたらユーザーのPUBLICな投稿一覧を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val posts = listOf(
|
||||
Post(
|
||||
id = 12345,
|
||||
userId = 1,
|
||||
text = "test1",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/1"
|
||||
),
|
||||
Post(
|
||||
id = 123456,
|
||||
userId = 1,
|
||||
text = "test2",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/2"
|
||||
)
|
||||
)
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking {
|
||||
findByUserNameAndDomainForUser(
|
||||
userName = eq("test1"),
|
||||
domain = eq("example.com"),
|
||||
since = anyOrNull(),
|
||||
until = anyOrNull(),
|
||||
minId = anyOrNull(),
|
||||
maxId = anyOrNull(),
|
||||
limit = anyOrNull(),
|
||||
forUserId = anyOrNull()
|
||||
)
|
||||
} doReturn posts
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/test1@example.com/posts").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(posts, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users @username@domain postsにGETしたらユーザーのPUBLICな投稿一覧を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val posts = listOf(
|
||||
Post(
|
||||
id = 12345,
|
||||
userId = 1,
|
||||
text = "test1",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/1"
|
||||
),
|
||||
Post(
|
||||
id = 123456,
|
||||
userId = 1,
|
||||
text = "test2",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/2"
|
||||
)
|
||||
)
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking {
|
||||
findByUserNameAndDomainForUser(
|
||||
userName = eq("test1"),
|
||||
domain = eq("example.com"),
|
||||
since = anyOrNull(),
|
||||
until = anyOrNull(),
|
||||
minId = anyOrNull(),
|
||||
maxId = anyOrNull(),
|
||||
limit = anyOrNull(),
|
||||
forUserId = anyOrNull()
|
||||
)
|
||||
} doReturn posts
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/@test1@example.com/posts").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(posts, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users name posts id にGETしたらPUBLICな投稿を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val post = Post(
|
||||
id = 123456,
|
||||
userId = 1,
|
||||
text = "test2",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/2"
|
||||
)
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking { findByIdForUser(eq(12345L), anyOrNull()) } doReturn post
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/test/posts/12345").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users id posts id にGETしたらPUBLICな投稿を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val post = Post(
|
||||
id = 123456,
|
||||
userId = 1,
|
||||
text = "test2",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/2"
|
||||
)
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking { findByIdForUser(eq(12345L), anyOrNull()) } doReturn post
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/1/posts/12345").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users name posts id にGETしたらUserIdが間違っててもPUBLICな投稿を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val post = Post(
|
||||
id = 123456,
|
||||
userId = 1,
|
||||
text = "test2",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/2"
|
||||
)
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking { findByIdForUser(eq(12345L), anyOrNull()) } doReturn post
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/423827849732847/posts/12345").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users name posts id にGETしたらuserNameが間違っててもPUBLICな投稿を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val post = Post(
|
||||
id = 123456,
|
||||
userId = 1,
|
||||
text = "test2",
|
||||
visibility = Visibility.PUBLIC,
|
||||
createdAt = Instant.now().toEpochMilli(),
|
||||
url = "https://example.com/posts/2"
|
||||
)
|
||||
val postService = mock<IPostService> {
|
||||
onBlocking { findByIdForUser(eq(12345L), anyOrNull()) } doReturn post
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
posts(postService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/invalidUserName/posts/12345").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(post, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,691 @@
|
|||
package dev.usbharu.hideout.routing.api.internal.v1
|
||||
|
||||
import com.auth0.jwt.interfaces.Claim
|
||||
import com.auth0.jwt.interfaces.Payload
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
||||
import dev.usbharu.hideout.domain.model.hideout.form.UserCreate
|
||||
import dev.usbharu.hideout.plugins.TOKEN_AUTH
|
||||
import dev.usbharu.hideout.plugins.configureSecurity
|
||||
import dev.usbharu.hideout.plugins.configureSerialization
|
||||
import dev.usbharu.hideout.service.IUserApiService
|
||||
import dev.usbharu.hideout.service.impl.IUserService
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.jwt.*
|
||||
import io.ktor.server.config.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.testing.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.kotlin.*
|
||||
import utils.JsonObjectMapper
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class UsersTest {
|
||||
@Test
|
||||
fun `users にGETするとユーザー一覧を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
|
||||
val users = listOf(
|
||||
UserResponse(
|
||||
12345,
|
||||
"test1",
|
||||
"example.com",
|
||||
"test",
|
||||
"",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
),
|
||||
UserResponse(
|
||||
12343,
|
||||
"tes2",
|
||||
"example.com",
|
||||
"test",
|
||||
"",
|
||||
"https://example.com/tes2",
|
||||
Instant.now().toEpochMilli()
|
||||
),
|
||||
)
|
||||
val userService = mock<IUserApiService> {
|
||||
onBlocking { findAll(anyOrNull(), anyOrNull()) } doReturn users
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(mock(), userService)
|
||||
}
|
||||
}
|
||||
}
|
||||
client.get("/api/internal/v1/users").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(users, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users にPOSTすると新規ユーザー作成ができる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val userCreateDto = UserCreate("test", "XXXXXXX")
|
||||
val userService = mock<IUserService> {
|
||||
onBlocking { usernameAlreadyUse(any()) } doReturn false
|
||||
onBlocking { createLocalUser(any()) } doReturn User(
|
||||
id = 12345,
|
||||
name = "test",
|
||||
domain = "example.com",
|
||||
screenName = "testUser",
|
||||
description = "test user",
|
||||
password = "XXXXXXX",
|
||||
inbox = "https://example.com/inbox",
|
||||
outbox = "https://example.com/outbox",
|
||||
url = "https://example.com",
|
||||
publicKey = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
|
||||
privateKey = "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----",
|
||||
createdAt = Instant.now()
|
||||
)
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(userService, mock())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.post("/api/internal/v1/users") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(JsonObjectMapper.objectMapper.writeValueAsString(userCreateDto))
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.Created, status)
|
||||
assertEquals(
|
||||
"${Config.configData.url}/api/internal/v1/users/${userCreateDto.username}",
|
||||
headers["Location"]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users 既にユーザー名が使用されているときはBadRequestが帰ってくる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val userCreateDto = UserCreate("test", "XXXXXXX")
|
||||
val userService = mock<IUserService> {
|
||||
onBlocking { usernameAlreadyUse(any()) } doReturn true
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(userService, mock())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.post("/api/internal/v1/users") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(JsonObjectMapper.objectMapper.writeValueAsString(userCreateDto))
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.BadRequest, status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users name にGETしたらユーザーを取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val userResponse = UserResponse(
|
||||
1234,
|
||||
"test1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findByAcct(any()) } doReturn userResponse
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(mock(), userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/test1").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(userResponse, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users id にGETしたらユーザーを取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val userResponse = UserResponse(
|
||||
1234,
|
||||
"test1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findById(any()) } doReturn userResponse
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(mock(), userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/1234").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(userResponse, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users name@domain にGETしたらユーザーを取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val userResponse = UserResponse(
|
||||
1234,
|
||||
"test1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findByAcct(any()) } doReturn userResponse
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(mock(), userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/test1").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(userResponse, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users @name@domain にGETしたらユーザーを取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val userResponse = UserResponse(
|
||||
1234,
|
||||
"test1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findByAcct(any()) } doReturn userResponse
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(mock(), userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/test1").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(userResponse, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users name followers にGETしたらフォロワー一覧を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
|
||||
val followers = listOf(
|
||||
UserResponse(
|
||||
1235,
|
||||
"follower1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
), UserResponse(
|
||||
1236,
|
||||
"follower2",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
)
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findFollowersByAcct(any()) } doReturn followers
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(mock(), userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/test1/followers").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users name@domain followers にGETしたらフォロワー一覧を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
|
||||
val followers = listOf(
|
||||
UserResponse(
|
||||
1235,
|
||||
"follower1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
), UserResponse(
|
||||
1236,
|
||||
"follower2",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
)
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findFollowersByAcct(any()) } doReturn followers
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(mock(), userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/@test1@example.com/followers").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users id followers にGETしたらフォロワー一覧を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
|
||||
val followers = listOf(
|
||||
UserResponse(
|
||||
1235,
|
||||
"follower1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
), UserResponse(
|
||||
1236,
|
||||
"follower2",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
)
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findFollowers(any()) } doReturn followers
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(mock(), userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/1234/followers").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users name followers に認証情報ありでGETしたらフォローできる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
|
||||
val claim = mock<Claim> {
|
||||
on { asLong() } doReturn 1234
|
||||
}
|
||||
val payload = mock<Payload> {
|
||||
on { getClaim(eq("uid")) } doReturn claim
|
||||
}
|
||||
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findByAcct(any()) } doReturn UserResponse(
|
||||
1235,
|
||||
"follower1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
}
|
||||
val userService = mock<IUserService> {
|
||||
onBlocking { addFollowers(eq(1235), eq(1234)) } doReturn true
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
authentication {
|
||||
bearer(TOKEN_AUTH) {
|
||||
authenticate {
|
||||
JWTPrincipal(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(userService, userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.post("/api/internal/v1/users/test1/followers") {
|
||||
header(HttpHeaders.Authorization, "Bearer test")
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users name followers に認証情報ありでGETしたらフォロー処理受付になることもある`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
|
||||
val claim = mock<Claim> {
|
||||
on { asLong() } doReturn 1234
|
||||
}
|
||||
val payload = mock<Payload> {
|
||||
on { getClaim(eq("uid")) } doReturn claim
|
||||
}
|
||||
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findByAcct(any()) } doReturn UserResponse(
|
||||
1235,
|
||||
"follower1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
}
|
||||
val userService = mock<IUserService> {
|
||||
onBlocking { addFollowers(eq(1235), eq(1234)) } doReturn false
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
authentication {
|
||||
bearer(TOKEN_AUTH) {
|
||||
authenticate {
|
||||
JWTPrincipal(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(userService, userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.post("/api/internal/v1/users/test1/followers") {
|
||||
header(HttpHeaders.Authorization, "Bearer test")
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.Accepted, status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users id followers に認証情報ありでGETしたらフォロー処理受付になることもある`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
|
||||
val claim = mock<Claim> {
|
||||
on { asLong() } doReturn 1234
|
||||
}
|
||||
val payload = mock<Payload> {
|
||||
on { getClaim(eq("uid")) } doReturn claim
|
||||
}
|
||||
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findById(any()) } doReturn UserResponse(
|
||||
1235,
|
||||
"follower1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
}
|
||||
val userService = mock<IUserService> {
|
||||
onBlocking { addFollowers(eq(1235), eq(1234)) } doReturn false
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
authentication {
|
||||
bearer(TOKEN_AUTH) {
|
||||
authenticate {
|
||||
JWTPrincipal(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(userService, userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.post("/api/internal/v1/users/1235/followers") {
|
||||
header(HttpHeaders.Authorization, "Bearer test")
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.Accepted, status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users name following にGETしたらフォロイー一覧を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
|
||||
val followers = listOf(
|
||||
UserResponse(
|
||||
1235,
|
||||
"follower1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
), UserResponse(
|
||||
1236,
|
||||
"follower2",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
)
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findFollowingsByAcct(any()) } doReturn followers
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(mock(), userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/test1/following").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users name@domain following にGETしたらフォロイー一覧を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
|
||||
val followers = listOf(
|
||||
UserResponse(
|
||||
1235,
|
||||
"follower1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
), UserResponse(
|
||||
1236,
|
||||
"follower2",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
)
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findFollowingsByAcct(any()) } doReturn followers
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(mock(), userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/test1@domain/following").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `users id following にGETしたらフォロイー一覧を取得できる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
|
||||
val followers = listOf(
|
||||
UserResponse(
|
||||
1235,
|
||||
"follower1",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
), UserResponse(
|
||||
1236,
|
||||
"follower2",
|
||||
"example.com",
|
||||
"test",
|
||||
"test User",
|
||||
"https://example.com/test",
|
||||
Instant.now().toEpochMilli()
|
||||
)
|
||||
)
|
||||
val userApiService = mock<IUserApiService> {
|
||||
onBlocking { findFollowings(any()) } doReturn followers
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
routing {
|
||||
route("/api/internal/v1") {
|
||||
users(mock(), userApiService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.get("/api/internal/v1/users/1234/following").apply {
|
||||
assertEquals(HttpStatusCode.OK, status)
|
||||
assertEquals(followers, JsonObjectMapper.objectMapper.readValue(bodyAsText()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -115,7 +115,7 @@ class ActivityPubFollowServiceImplTest {
|
|||
createdAt = Instant.now()
|
||||
)
|
||||
)
|
||||
onBlocking { addFollowers(any(), any()) } doReturn Unit
|
||||
onBlocking { addFollowers(any(), any()) } doReturn false
|
||||
}
|
||||
val activityPubFollowService =
|
||||
ActivityPubFollowServiceImpl(
|
||||
|
|
|
@ -5,8 +5,9 @@ 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.hideout.entity.Post
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
|
||||
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
|
||||
import dev.usbharu.hideout.service.impl.IUserService
|
||||
import dev.usbharu.hideout.service.job.JobQueueParentService
|
||||
|
@ -72,13 +73,13 @@ class ActivityPubNoteServiceImplTest {
|
|||
}
|
||||
val jobQueueParentService = mock<JobQueueParentService>()
|
||||
val activityPubNoteService = ActivityPubNoteServiceImpl(mock(), jobQueueParentService, userService)
|
||||
val postEntity = PostEntity(
|
||||
val postEntity = Post(
|
||||
1L,
|
||||
1L,
|
||||
null,
|
||||
"test text",
|
||||
1L,
|
||||
1,
|
||||
Visibility.PUBLIC,
|
||||
"https://example.com"
|
||||
)
|
||||
activityPubNoteService.createNote(postEntity)
|
||||
|
@ -99,8 +100,14 @@ class ActivityPubNoteServiceImplTest {
|
|||
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.post.name to """{
|
||||
"id": 1,
|
||||
"userId": 1,
|
||||
"text": "test text",
|
||||
"createdAt": 132525324,
|
||||
"visibility": 0,
|
||||
"url": "https://example.com"
|
||||
}""",
|
||||
DeliverPostJob.inbox.name to "https://follower.example.com/inbox"
|
||||
),
|
||||
json = Json
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package dev.usbharu.hideout.service.impl
|
||||
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
|
||||
import dev.usbharu.hideout.repository.Posts
|
||||
import dev.usbharu.hideout.repository.UsersFollowers
|
||||
import dev.usbharu.hideout.service.TwitterSnowflakeIdGenerateService
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.batchInsert
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.kotlin.mock
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertContentEquals
|
||||
|
||||
class PostServiceTest {
|
||||
|
||||
lateinit var db: Database
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
|
||||
transaction(db) {
|
||||
SchemaUtils.create(Posts)
|
||||
connection.prepareStatement("SET REFERENTIAL_INTEGRITY FALSE", false).executeUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
transaction(db) {
|
||||
SchemaUtils.drop(Posts)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `findAll 公開投稿を取得できる`() = runTest {
|
||||
val postService = PostService(mock(), mock(), mock())
|
||||
|
||||
suspend fun createPost(userId: Long, text: String, visibility: Visibility = Visibility.PUBLIC): Post {
|
||||
return Post(
|
||||
TwitterSnowflakeIdGenerateService.generateId(),
|
||||
userId,
|
||||
null,
|
||||
text,
|
||||
Instant.now().toEpochMilli(),
|
||||
visibility,
|
||||
"https://example.com"
|
||||
)
|
||||
}
|
||||
|
||||
val userA: Long = 1
|
||||
val userB: Long = 2
|
||||
|
||||
val posts = listOf(
|
||||
createPost(userA, "hello"),
|
||||
createPost(userA, "hello1"),
|
||||
createPost(userA, "hello2"),
|
||||
createPost(userA, "hello3"),
|
||||
createPost(userA, "hello4"),
|
||||
createPost(userA, "hello5"),
|
||||
createPost(userA, "hello6"),
|
||||
createPost(userB, "good bay ", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay1", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay2", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay3", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay4", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay5", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay6", Visibility.FOLLOWERS),
|
||||
)
|
||||
|
||||
transaction {
|
||||
Posts.batchInsert(posts) {
|
||||
this[Posts.id] = it.id
|
||||
this[Posts.userId] = it.userId
|
||||
this[Posts.overview] = it.overview
|
||||
this[Posts.text] = it.text
|
||||
this[Posts.createdAt] = it.createdAt
|
||||
this[Posts.visibility] = it.visibility.ordinal
|
||||
this[Posts.url] = it.url
|
||||
this[Posts.replyId] = it.replyId
|
||||
this[Posts.repostId] = it.repostId
|
||||
}
|
||||
}
|
||||
|
||||
val expect = posts.filter { it.visibility == Visibility.PUBLIC }
|
||||
|
||||
val actual = postService.findAll()
|
||||
assertContentEquals(expect, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `findAll フォロー限定投稿を見れる`() = runTest {
|
||||
val postService = PostService(mock(), mock(), mock())
|
||||
|
||||
suspend fun createPost(userId: Long, text: String, visibility: Visibility = Visibility.PUBLIC): Post {
|
||||
return Post(
|
||||
TwitterSnowflakeIdGenerateService.generateId(),
|
||||
userId,
|
||||
null,
|
||||
text,
|
||||
Instant.now().toEpochMilli(),
|
||||
visibility,
|
||||
"https://example.com"
|
||||
)
|
||||
}
|
||||
|
||||
val userA: Long = 1
|
||||
val userB: Long = 2
|
||||
|
||||
val posts = listOf(
|
||||
createPost(userA, "hello"),
|
||||
createPost(userA, "hello1"),
|
||||
createPost(userA, "hello2"),
|
||||
createPost(userA, "hello3"),
|
||||
createPost(userA, "hello4"),
|
||||
createPost(userA, "hello5"),
|
||||
createPost(userA, "hello6"),
|
||||
createPost(userB, "good bay ", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay1", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay2", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay3", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay4", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay5", Visibility.FOLLOWERS),
|
||||
createPost(userB, "good bay6", Visibility.FOLLOWERS),
|
||||
)
|
||||
|
||||
transaction(db) {
|
||||
SchemaUtils.create(UsersFollowers)
|
||||
}
|
||||
|
||||
transaction {
|
||||
Posts.batchInsert(posts) {
|
||||
this[Posts.id] = it.id
|
||||
this[Posts.userId] = it.userId
|
||||
this[Posts.overview] = it.overview
|
||||
this[Posts.text] = it.text
|
||||
this[Posts.createdAt] = it.createdAt
|
||||
this[Posts.visibility] = it.visibility.ordinal
|
||||
this[Posts.url] = it.url
|
||||
this[Posts.replyId] = it.replyId
|
||||
this[Posts.repostId] = it.repostId
|
||||
}
|
||||
UsersFollowers.insert {
|
||||
it[id] = 100L
|
||||
it[userId] = userB
|
||||
it[followerId] = userA
|
||||
}
|
||||
}
|
||||
|
||||
val actual = postService.findAll(userId = userA)
|
||||
assertContentEquals(posts, actual)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue