mirror of https://github.com/usbharu/Hideout.git
feat: 投稿のAPIを作成
This commit is contained in:
parent
e1f10ab064
commit
7fad9fcfa6
|
@ -1,7 +1,6 @@
|
||||||
package dev.usbharu.hideout.repository
|
package dev.usbharu.hideout.repository
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
||||||
import dev.usbharu.hideout.repository.toPost
|
|
||||||
import dev.usbharu.hideout.service.IdGenerateService
|
import dev.usbharu.hideout.service.IdGenerateService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
|
|
@ -2,7 +2,9 @@ package dev.usbharu.hideout.routing.api.internal.v1
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
|
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
|
||||||
import dev.usbharu.hideout.domain.model.hideout.form.Post
|
import dev.usbharu.hideout.domain.model.hideout.form.Post
|
||||||
|
import dev.usbharu.hideout.plugins.TOKEN_AUTH
|
||||||
import dev.usbharu.hideout.service.IPostService
|
import dev.usbharu.hideout.service.IPostService
|
||||||
|
import dev.usbharu.hideout.util.InstantParseUtil
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.auth.*
|
import io.ktor.server.auth.*
|
||||||
import io.ktor.server.auth.jwt.*
|
import io.ktor.server.auth.jwt.*
|
||||||
|
@ -11,15 +13,26 @@ import io.ktor.server.routing.*
|
||||||
|
|
||||||
fun Route.posts(postService: IPostService) {
|
fun Route.posts(postService: IPostService) {
|
||||||
route("/posts") {
|
route("/posts") {
|
||||||
authenticate(){
|
authenticate(TOKEN_AUTH) {
|
||||||
post {
|
post {
|
||||||
val principal = call.principal<JWTPrincipal>() ?: throw RuntimeException("no principal")
|
val principal = call.principal<JWTPrincipal>() ?: throw RuntimeException("no principal")
|
||||||
val username = principal.payload.getClaim("username").asString()
|
val username = principal.payload.getClaim("uid").asString()
|
||||||
|
|
||||||
val receive = call.receive<Post>()
|
val receive = call.receive<Post>()
|
||||||
val postCreateDto = PostCreateDto(receive.text, username)
|
val postCreateDto = PostCreateDto(receive.text, username)
|
||||||
postService.create(postCreateDto)
|
postService.create(postCreateDto)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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()
|
||||||
|
postService.findAll(since, until, minId, maxId, limit, userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,20 @@ package dev.usbharu.hideout.service
|
||||||
|
|
||||||
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
|
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.Post
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
interface IPostService {
|
interface IPostService {
|
||||||
suspend fun create(post: Post)
|
suspend fun create(post: Post)
|
||||||
suspend fun create(post: PostCreateDto)
|
suspend fun create(post: PostCreateDto)
|
||||||
|
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
|
||||||
|
suspend fun delete(id: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ class JwtServiceImpl(
|
||||||
.withAudience("${Config.configData.url}/users/${user.name}")
|
.withAudience("${Config.configData.url}/users/${user.name}")
|
||||||
.withIssuer(Config.configData.url)
|
.withIssuer(Config.configData.url)
|
||||||
.withKeyId(keyId.await().toString())
|
.withKeyId(keyId.await().toString())
|
||||||
.withClaim("username", user.name)
|
.withClaim("uid", user.id)
|
||||||
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
|
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
|
||||||
.sign(Algorithm.RSA256(publicKey.await(), privateKey.await()))
|
.sign(Algorithm.RSA256(publicKey.await(), privateKey.await()))
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,14 @@ package dev.usbharu.hideout.service.impl
|
||||||
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
|
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.Post
|
||||||
import dev.usbharu.hideout.repository.IPostRepository
|
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.IPostService
|
||||||
import dev.usbharu.hideout.service.activitypub.ActivityPubNoteService
|
import dev.usbharu.hideout.service.activitypub.ActivityPubNoteService
|
||||||
|
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.koin.core.annotation.Single
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -34,4 +40,35 @@ class PostService(
|
||||||
)
|
)
|
||||||
postRepository.save(postEntity)
|
postRepository.save(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(0)
|
||||||
|
}
|
||||||
|
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 delete(id: String) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,161 @@
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package dev.usbharu.hideout.service.impl
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.model.hideout.entity.Post
|
||||||
|
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: Int = 0): 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", 1),
|
||||||
|
createPost(userB, "good bay1", 1),
|
||||||
|
createPost(userB, "good bay2", 1),
|
||||||
|
createPost(userB, "good bay3", 1),
|
||||||
|
createPost(userB, "good bay4", 1),
|
||||||
|
createPost(userB, "good bay5", 1),
|
||||||
|
createPost(userB, "good bay6", 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
this[Posts.url] = it.url
|
||||||
|
this[Posts.replyId] = it.replyId
|
||||||
|
this[Posts.repostId] = it.repostId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val expect = posts.filter { it.visibility == 0 }
|
||||||
|
|
||||||
|
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: Int = 0): 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", 1),
|
||||||
|
createPost(userB, "good bay1", 1),
|
||||||
|
createPost(userB, "good bay2", 1),
|
||||||
|
createPost(userB, "good bay3", 1),
|
||||||
|
createPost(userB, "good bay4", 1),
|
||||||
|
createPost(userB, "good bay5", 1),
|
||||||
|
createPost(userB, "good bay6", 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
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