From 736f83e3e1ac4ba417914d391542fe9090e796c6 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 5 May 2023 16:04:48 +0900 Subject: [PATCH 01/21] =?UTF-8?q?refactor:=20Post=E3=82=92=E3=83=AA?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/usbharu/hideout/domain/model/Posts.kt | 54 ----------------- .../model/api/{ => mastodon}/StatusForPost.kt | 2 +- .../domain/model/hideout/dto/PostCreateDto.kt | 3 + .../domain/model/hideout/entity/Post.kt | 13 ++++ .../hideout/domain/model/hideout/form/Post.kt | 3 + .../dev/usbharu/hideout/plugins/Routing.kt | 2 +- .../hideout/repository/IPostRepository.kt | 8 +-- .../hideout/repository/PostRepositoryImpl.kt | 60 ++++++++++++------- .../hideout/routing/api/internal/v1/Posts.kt | 25 ++++++++ .../routing/api/mastodon/v1/Statuses.kt | 20 +++++++ .../hideout/routing/api/v1/Statuses.kt | 25 -------- .../usbharu/hideout/service/IPostService.kt | 4 +- .../activitypub/ActivityPubNoteService.kt | 4 +- .../activitypub/ActivityPubNoteServiceImpl.kt | 6 +- .../hideout/service/impl/PostService.kt | 20 ++++++- .../ActivityPubNoteServiceImplTest.kt | 4 +- 16 files changed, 134 insertions(+), 119 deletions(-) delete mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/Posts.kt rename src/main/kotlin/dev/usbharu/hideout/domain/model/api/{ => mastodon}/StatusForPost.kt (57%) create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/routing/api/mastodon/v1/Statuses.kt delete mode 100644 src/main/kotlin/dev/usbharu/hideout/routing/api/v1/Statuses.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/Posts.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/Posts.kt deleted file mode 100644 index 56ae54a3..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/Posts.kt +++ /dev/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] - ) -} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/api/StatusForPost.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/api/mastodon/StatusForPost.kt similarity index 57% rename from src/main/kotlin/dev/usbharu/hideout/domain/model/api/StatusForPost.kt rename to src/main/kotlin/dev/usbharu/hideout/domain/model/api/mastodon/StatusForPost.kt index b89a0516..bc1f7dfc 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/api/StatusForPost.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/api/mastodon/StatusForPost.kt @@ -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, diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt new file mode 100644 index 00000000..f23c1dc8 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.domain.model.hideout.dto + +data class PostCreateDto(val text:String,val username:String) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt new file mode 100644 index 00000000..3b997549 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt @@ -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: Int, + val url:String, + val repostId:Long? = null, + val replyId:Long? = null +) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt new file mode 100644 index 00000000..99014e88 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.domain.model.hideout.form + +data class Post(val text:String) diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt index fed45736..24f94a6a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt @@ -3,7 +3,7 @@ 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.mastodon.v1.statuses import dev.usbharu.hideout.routing.wellknown.webfinger import dev.usbharu.hideout.service.IPostService import dev.usbharu.hideout.service.activitypub.ActivityPubService diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IPostRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IPostRepository.kt index 9fee2d02..38223a97 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/IPostRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/IPostRepository.kt @@ -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) } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt index 9a57f4e7..86d01ad5 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt @@ -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.repository.toPost import dev.usbharu.hideout.service.IdGenerateService import kotlinx.coroutines.Dispatchers import org.jetbrains.exposed.sql.* @@ -22,41 +19,31 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe } } + override suspend fun generateId(): Long = idGenerateService.generateId() + @Suppress("InjectDispatcher") suspend fun 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[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 +55,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 = this[Posts.visibility], + url = this[Posts.url], + repostId = this[Posts.repostId], + replyId = this[Posts.replyId] + ) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt new file mode 100644 index 00000000..357eb131 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt @@ -0,0 +1,25 @@ +package dev.usbharu.hideout.routing.api.internal.v1 + +import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto +import dev.usbharu.hideout.domain.model.hideout.form.Post +import dev.usbharu.hideout.service.IPostService +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.routing.* + +fun Route.posts(postService: IPostService){ + route("/posts"){ + authenticate(){ + post{ + val principal = call.principal() ?: throw RuntimeException("no principal") + val username = principal.payload.getClaim("username").asString() + + val receive = call.receive() + val postCreateDto = PostCreateDto(receive.text,username) + postService.create(postCreateDto) + } + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/mastodon/v1/Statuses.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/mastodon/v1/Statuses.kt new file mode 100644 index 00000000..9b5c5fa8 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/mastodon/v1/Statuses.kt @@ -0,0 +1,20 @@ +package dev.usbharu.hideout.routing.api.mastodon.v1 + +import dev.usbharu.hideout.service.IPostService +import io.ktor.server.routing.* + +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) +// } +// } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/v1/Statuses.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/v1/Statuses.kt deleted file mode 100644 index e6c08942..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/v1/Statuses.kt +++ /dev/null @@ -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) - } - } -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt index b523cb1e..2e16bdae 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt @@ -1,7 +1,9 @@ package dev.usbharu.hideout.service -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 interface IPostService { suspend fun create(post: Post) + suspend fun create(post: PostCreateDto) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteService.kt index 33ba42cc..5c6ccf96 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteService.kt @@ -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) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImpl.kt index 401367d7..42f0df20 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImpl.kt @@ -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) { val actor = props[DeliverPostJob.actor] - val postEntity = Config.configData.objectMapper.readValue(props[DeliverPostJob.post]) + val postEntity = Config.configData.objectMapper.readValue(props[DeliverPostJob.post]) val note = Note( name = "Note", id = postEntity.url, diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt index 71dd6be3..fdb10359 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt @@ -1,23 +1,37 @@ 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.repository.IPostRepository import dev.usbharu.hideout.service.IPostService import dev.usbharu.hideout.service.activitypub.ActivityPubNoteService 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) { logger.debug("create post={}", post) - val postEntity = postRepository.insert(post) + val postEntity = postRepository.save(post) activityPubNoteService.createNote(postEntity) } + + override suspend fun create(post: PostCreateDto) { + logger.debug("create post={}", post) + val user = userService.findByNameLocalUser(post.username) + val id = postRepository.generateId() + val postEntity = Post( + id, user.id, null, post.text, + Instant.now().toEpochMilli(), 0, "${user.url}/posts/$id", null, null + ) + postRepository.save(postEntity) + } } diff --git a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt index af5274de..9fad103e 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt @@ -5,7 +5,7 @@ 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.job.DeliverPostJob import dev.usbharu.hideout.service.impl.IUserService @@ -72,7 +72,7 @@ class ActivityPubNoteServiceImplTest { } val jobQueueParentService = mock() val activityPubNoteService = ActivityPubNoteServiceImpl(mock(), jobQueueParentService, userService) - val postEntity = PostEntity( + val postEntity = Post( 1L, 1L, null, From 69c0a8692f2b389348cdff53af142a3fac9e7f31 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 8 May 2023 11:11:52 +0900 Subject: [PATCH 02/21] =?UTF-8?q?feat:=20username=E3=81=8B=E3=82=89uid?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/plugins/Security.kt | 13 ++++++++----- .../dev/usbharu/hideout/plugins/SecurityKtTest.kt | 10 +++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index 939fe5ba..afb89c1e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -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) } } } @@ -74,7 +77,7 @@ fun Application.configureSecurity( authenticate(TOKEN_AUTH) { get("/auth-check") { val principal = call.principal() - val username = principal!!.payload.getClaim("username") + val username = principal!!.payload.getClaim("uid") call.respondText("Hello $username") } } diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt index 9c558f36..9346fa27 100644 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt @@ -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 { @@ -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 { @@ -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 { @@ -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 { From aaaaf6aff640fed796bdf7d640e65f7383cc7851 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 8 May 2023 11:13:01 +0900 Subject: [PATCH 03/21] =?UTF-8?q?feat:=20=E6=8A=95=E7=A8=BF=E3=81=AEAPI?= =?UTF-8?q?=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hideout/repository/PostRepositoryImpl.kt | 1 - .../hideout/routing/api/internal/v1/Posts.kt | 25 ++- .../usbharu/hideout/service/IPostService.kt | 12 ++ .../usbharu/hideout/service/JwtServiceImpl.kt | 2 +- .../hideout/service/impl/PostService.kt | 37 ++++ .../usbharu/hideout/util/InstantParseUtil.kt | 18 ++ .../hideout/service/impl/PostServiceTest.kt | 161 ++++++++++++++++++ 7 files changed, 248 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/util/InstantParseUtil.kt create mode 100644 src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt index 86d01ad5..a2661b6b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt @@ -1,7 +1,6 @@ package dev.usbharu.hideout.repository import dev.usbharu.hideout.domain.model.hideout.entity.Post -import dev.usbharu.hideout.repository.toPost import dev.usbharu.hideout.service.IdGenerateService import kotlinx.coroutines.Dispatchers import org.jetbrains.exposed.sql.* diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt index 357eb131..ab5fb85b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt @@ -2,24 +2,37 @@ package dev.usbharu.hideout.routing.api.internal.v1 import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto 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.util.InstantParseUtil 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.routing.* -fun Route.posts(postService: IPostService){ - route("/posts"){ - authenticate(){ - post{ +fun Route.posts(postService: IPostService) { + route("/posts") { + authenticate(TOKEN_AUTH) { + post { val principal = call.principal() ?: throw RuntimeException("no principal") - val username = principal.payload.getClaim("username").asString() + val username = principal.payload.getClaim("uid").asString() val receive = call.receive() - val postCreateDto = PostCreateDto(receive.text,username) + val postCreateDto = PostCreateDto(receive.text, username) postService.create(postCreateDto) } } + authenticate(TOKEN_AUTH, optional = true) { + get { + val userId = call.principal()?.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) + } + } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt index 2e16bdae..bf64ac10 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt @@ -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.entity.Post +import java.time.Instant interface IPostService { suspend fun create(post: Post) 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 + + suspend fun findById(id: String): Post + suspend fun delete(id: String) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt index e8519f2c..4dea9bb1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt @@ -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())) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt index fdb10359..8de2594d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt @@ -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.entity.Post 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.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 @@ -34,4 +40,35 @@ class PostService( ) postRepository.save(postEntity) } + + override suspend fun findAll( + since: Instant?, + until: Instant?, + minId: Long?, + maxId: Long?, + limit: Int?, + userId: Long? + ): List { + 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") + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/util/InstantParseUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/InstantParseUtil.kt new file mode 100644 index 00000000..66da1b60 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/InstantParseUtil.kt @@ -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 + } + } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt new file mode 100644 index 00000000..8758bd3f --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt @@ -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) + } +} From dfa3bb74fb6e3a5cb818da836b86f0378d881563 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 8 May 2023 11:42:55 +0900 Subject: [PATCH 04/21] =?UTF-8?q?refactor:=20=E5=85=AC=E9=96=8B=E7=AF=84?= =?UTF-8?q?=E5=9B=B2=E3=82=92=E5=88=97=E6=8C=99=E5=9E=8B=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/hideout/entity/Post.kt | 2 +- .../domain/model/hideout/entity/Visibility.kt | 8 ++++ .../hideout/repository/PostRepositoryImpl.kt | 5 ++- .../hideout/service/impl/PostService.kt | 5 ++- .../ActivityPubNoteServiceImplTest.kt | 13 +++++-- .../hideout/service/impl/PostServiceTest.kt | 39 ++++++++++--------- 6 files changed, 45 insertions(+), 27 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Visibility.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt index 3b997549..423009cf 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt @@ -6,7 +6,7 @@ data class Post( val overview:String? = null, val text:String, val createdAt:Long, - val visibility: Int, + val visibility: Visibility, val url:String, val repostId:Long? = null, val replyId:Long? = null diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Visibility.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Visibility.kt new file mode 100644 index 00000000..9acf8c16 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Visibility.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.domain.model.hideout.entity + +enum class Visibility { + PUBLIC, + UNLISTED, + FOLLOWERS, + DIRECT +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt index a2661b6b..9f679901 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt @@ -1,6 +1,7 @@ package dev.usbharu.hideout.repository 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.* @@ -33,7 +34,7 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe it[overview] = post.overview it[text] = post.text it[createdAt] = post.createdAt - it[visibility] = post.visibility + it[visibility] = post.visibility.ordinal it[url] = post.url it[repostId] = post.repostId it[replyId] = post.replyId @@ -75,7 +76,7 @@ fun ResultRow.toPost(): Post { overview = this[Posts.overview], text = this[Posts.text], createdAt = this[Posts.createdAt], - visibility = this[Posts.visibility], + visibility = Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] }, url = this[Posts.url], repostId = this[Posts.repostId], replyId = this[Posts.replyId] diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt index 8de2594d..d8ec4f5f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt @@ -2,6 +2,7 @@ package dev.usbharu.hideout.service.impl 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 @@ -36,7 +37,7 @@ class PostService( val id = postRepository.generateId() val postEntity = Post( id, user.id, null, post.text, - Instant.now().toEpochMilli(), 0, "${user.url}/posts/$id", null, null + Instant.now().toEpochMilli(), Visibility.PUBLIC, "${user.url}/posts/$id", null, null ) postRepository.save(postEntity) } @@ -51,7 +52,7 @@ class PostService( ): List { return transaction { val select = Posts.select { - Posts.visibility.eq(0) + Posts.visibility.eq(Visibility.PUBLIC.ordinal) } if (userId != null) { select.orWhere { diff --git a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt index 9fad103e..0372f88e 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt @@ -7,6 +7,7 @@ import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.ConfigData 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 @@ -78,7 +79,7 @@ class ActivityPubNoteServiceImplTest { null, "test text", 1L, - 1, + Visibility.PUBLIC, "https://example.com" ) activityPubNoteService.createNote(postEntity) @@ -99,8 +100,14 @@ class ActivityPubNoteServiceImplTest { JobProps( data = mapOf( 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 diff --git a/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt index 8758bd3f..46e41fd2 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt @@ -3,6 +3,7 @@ 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 @@ -44,7 +45,7 @@ class PostServiceTest { fun `findAll 公開投稿を取得できる`() = runTest { val postService = PostService(mock(), mock(), mock()) - suspend fun createPost(userId: Long, text: String, visibility: Int = 0): Post { + suspend fun createPost(userId: Long, text: String, visibility: Visibility = Visibility.PUBLIC): Post { return Post( TwitterSnowflakeIdGenerateService.generateId(), userId, @@ -67,13 +68,13 @@ class PostServiceTest { 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), + 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 { @@ -83,14 +84,14 @@ class PostServiceTest { this[Posts.overview] = it.overview this[Posts.text] = it.text this[Posts.createdAt] = it.createdAt - this[Posts.visibility] = it.visibility + 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 == 0 } + val expect = posts.filter { it.visibility == Visibility.PUBLIC } val actual = postService.findAll() assertContentEquals(expect, actual) @@ -100,7 +101,7 @@ class PostServiceTest { fun `findAll フォロー限定投稿を見れる`() = runTest { val postService = PostService(mock(), mock(), mock()) - suspend fun createPost(userId: Long, text: String, visibility: Int = 0): Post { + suspend fun createPost(userId: Long, text: String, visibility: Visibility = Visibility.PUBLIC): Post { return Post( TwitterSnowflakeIdGenerateService.generateId(), userId, @@ -123,13 +124,13 @@ class PostServiceTest { 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), + 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) { @@ -143,7 +144,7 @@ class PostServiceTest { this[Posts.overview] = it.overview this[Posts.text] = it.text this[Posts.createdAt] = it.createdAt - this[Posts.visibility] = it.visibility + this[Posts.visibility] = it.visibility.ordinal this[Posts.url] = it.url this[Posts.replyId] = it.replyId this[Posts.repostId] = it.repostId From a99f7036fa25a8445175de54c9e69162fcbadda4 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 8 May 2023 11:48:27 +0900 Subject: [PATCH 05/21] =?UTF-8?q?style:=20=E3=82=B9=E3=82=BF=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/hideout/dto/PostCreateDto.kt | 2 +- .../hideout/domain/model/hideout/entity/Post.kt | 14 +++++++------- .../hideout/domain/model/hideout/form/Post.kt | 2 +- .../kotlin/dev/usbharu/hideout/plugins/Security.kt | 2 +- .../hideout/repository/PostRepositoryImpl.kt | 1 - 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt index f23c1dc8..d863489a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt @@ -1,3 +1,3 @@ package dev.usbharu.hideout.domain.model.hideout.dto -data class PostCreateDto(val text:String,val username:String) +data class PostCreateDto(val text: String, val username: String) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt index 423009cf..2cfb45be 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt @@ -1,13 +1,13 @@ package dev.usbharu.hideout.domain.model.hideout.entity data class Post( - val id:Long, + val id: Long, val userId: Long, - val overview:String? = null, - val text:String, - val createdAt: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 + val url: String, + val repostId: Long? = null, + val replyId: Long? = null ) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt index 99014e88..7e654ab2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt @@ -1,3 +1,3 @@ package dev.usbharu.hideout.domain.model.hideout.form -data class Post(val text:String) +data class Post(val text: String) diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index afb89c1e..bf5c95f2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -39,7 +39,7 @@ fun Application.configureSecurity( if (uid.isMissing) { return@validate null } - if (uid.asLong() == null) { + if (uid.asLong() == null) { return@validate null } return@validate JWTPrincipal(jwtCredential.payload) diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt index 9f679901..45101190 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt @@ -25,7 +25,6 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe suspend fun query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } - override suspend fun save(post: Post): Post { return query { Posts.insert { From c4f8101729d16b804ee06b80c4e8384f1f2236d4 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 8 May 2023 16:07:19 +0900 Subject: [PATCH 06/21] =?UTF-8?q?feat:=20=E6=8A=95=E7=A8=BF=E3=81=AE?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/hideout/dto/PostCreateDto.kt | 2 +- .../hideout/domain/model/hideout/form/Post.kt | 10 ++- .../hideout/routing/api/internal/v1/Posts.kt | 10 ++- .../usbharu/hideout/service/IPostService.kt | 4 +- .../hideout/service/impl/PostService.kt | 8 +- .../routing/api/internal/v1/PostsKtTest.kt | 84 +++++++++++++++++++ 6 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt index d863489a..9d699fb7 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt @@ -1,3 +1,3 @@ package dev.usbharu.hideout.domain.model.hideout.dto -data class PostCreateDto(val text: String, val username: String) +data class PostCreateDto(val text: String, val userId: Long) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt index 7e654ab2..bc768d32 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/Post.kt @@ -1,3 +1,11 @@ package dev.usbharu.hideout.domain.model.hideout.form -data class Post(val text: String) +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 +) diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt index ab5fb85b..7477cf6c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt @@ -5,10 +5,12 @@ 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.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.* fun Route.posts(postService: IPostService) { @@ -16,11 +18,13 @@ fun Route.posts(postService: IPostService) { authenticate(TOKEN_AUTH) { post { val principal = call.principal() ?: throw RuntimeException("no principal") - val username = principal.payload.getClaim("uid").asString() + val userId = principal.payload.getClaim("uid").asLong() val receive = call.receive() - val postCreateDto = PostCreateDto(receive.text, username) - postService.create(postCreateDto) + val postCreateDto = PostCreateDto(receive.text, userId) + val create = postService.create(postCreateDto) + call.response.header("Location", create.url) + call.respond(HttpStatusCode.OK) } } authenticate(TOKEN_AUTH, optional = true) { diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt index bf64ac10..0b4b4d59 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt @@ -5,8 +5,8 @@ import dev.usbharu.hideout.domain.model.hideout.entity.Post import java.time.Instant interface IPostService { - suspend fun create(post: Post) - suspend fun create(post: PostCreateDto) + suspend fun create(post: Post): Post + suspend fun create(post: PostCreateDto): Post suspend fun findAll( since: Instant? = null, until: Instant? = null, diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt index d8ec4f5f..137075b1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt @@ -25,21 +25,23 @@ class PostService( 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.save(post) activityPubNoteService.createNote(postEntity) + return post } - override suspend fun create(post: PostCreateDto) { + override suspend fun create(post: PostCreateDto): Post { logger.debug("create post={}", post) - val user = userService.findByNameLocalUser(post.username) + val user = userService.findById(post.userId) val id = postRepository.generateId() val postEntity = Post( id, user.id, null, post.text, Instant.now().toEpochMilli(), Visibility.PUBLIC, "${user.url}/posts/$id", null, null ) postRepository.save(postEntity) + return postEntity } override suspend fun findAll( diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt new file mode 100644 index 00000000..de9e768a --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt @@ -0,0 +1,84 @@ +package dev.usbharu.hideout.routing.api.internal.v1 + +import com.auth0.jwt.interfaces.Claim +import com.auth0.jwt.interfaces.Payload +import dev.usbharu.hideout.config.Config +import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto +import dev.usbharu.hideout.domain.model.hideout.entity.Visibility +import dev.usbharu.hideout.domain.model.hideout.form.Post +import dev.usbharu.hideout.plugins.TOKEN_AUTH +import dev.usbharu.hideout.plugins.configureSerialization +import dev.usbharu.hideout.service.IPostService +import io.ktor.client.request.* +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 java.time.Instant +import kotlin.test.assertEquals + +class PostsKtTest { + + + @Test + fun `posts-post postsにpostしたら投稿できる`() = testApplication { + environment { + config = ApplicationConfig("empty.conf") + } + val claim = mock { + on { asLong() } doReturn 1234 + } + val payload = mock { + on { getClaim(eq("uid")) } doReturn claim + } + val postService = mock { + onBlocking { create(any()) } doAnswer { + val argument = it.getArgument(0) + dev.usbharu.hideout.domain.model.hideout.entity.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 = 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 { + verify(postService).create(capture()) + assertEquals(PostCreateDto("test", 1234), firstValue) + } + } +} From 9de04f944f0df8a20b890db62474f0cdabf3f575 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 17 May 2023 21:56:06 +0900 Subject: [PATCH 07/21] =?UTF-8?q?feat:=20=E6=8A=95=E7=A8=BF=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E3=81=AEAPI=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/usbharu/hideout/domain/model/Acct.kt | 3 ++ .../domain/model/hideout/dto/PostCreateDto.kt | 11 +++- .../exception/PostNotFoundException.kt | 8 +++ .../dev/usbharu/hideout/plugins/Routing.kt | 4 ++ .../hideout/routing/api/internal/v1/Posts.kt | 54 ++++++++++++++++++- .../usbharu/hideout/service/IPostService.kt | 39 ++++++++++++++ .../hideout/service/impl/PostService.kt | 21 ++++++++ .../dev/usbharu/hideout/util/AcctUtil.kt | 41 ++++++++++++++ 8 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/Acct.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/exception/PostNotFoundException.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/Acct.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/Acct.kt new file mode 100644 index 00000000..af771e1b --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/Acct.kt @@ -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) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt index 9d699fb7..272215a2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt @@ -1,3 +1,12 @@ package dev.usbharu.hideout.domain.model.hideout.dto -data class PostCreateDto(val text: String, val userId: Long) +import dev.usbharu.hideout.domain.model.hideout.entity.Visibility + +data class PostCreateDto( + val text: String, + val overview: String? = null, + val visibility: Visibility, + val repostId: Long? = null, + val repolyId: Long? = null, + val userId: Long +) diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/PostNotFoundException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/PostNotFoundException.kt new file mode 100644 index 00000000..9fe3cf78 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/exception/PostNotFoundException.kt @@ -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) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt index 24f94a6a..1d7cce98 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt @@ -3,6 +3,7 @@ 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.internal.v1.posts import dev.usbharu.hideout.routing.api.mastodon.v1.statuses import dev.usbharu.hideout.routing.wellknown.webfinger import dev.usbharu.hideout.service.IPostService @@ -31,5 +32,8 @@ fun Application.configureRouting( route("/api/v1") { statuses(postService) } + route("/api/internal/v1") { + posts(postService) + } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt index 7477cf6c..db1b3f70 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt @@ -1,9 +1,13 @@ 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.* @@ -21,7 +25,14 @@ fun Route.posts(postService: IPostService) { val userId = principal.payload.getClaim("uid").asLong() val receive = call.receive() - val postCreateDto = PostCreateDto(receive.text, userId) + val postCreateDto = PostCreateDto( + receive.text, + receive.overview, + receive.visibility, + receive.repostId, + receive.replyId, + userId + ) val create = postService.create(postCreateDto) call.response.header("Location", create.url) call.respond(HttpStatusCode.OK) @@ -35,7 +46,46 @@ fun Route.posts(postService: IPostService) { 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) + call.respond(postService.findAll(since, until, minId, maxId, limit, userId)) + } + get("/{id}") { + val userId = call.principal()?.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()?.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()?.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.response.header("Content-Location", post.url) + call.respond(post) } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt index 0b4b4d59..f2da7a53 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt @@ -1,5 +1,6 @@ package dev.usbharu.hideout.service +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 @@ -17,5 +18,43 @@ interface IPostService { ): List 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 + + 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 + suspend fun delete(id: String) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt index 137075b1..e017610d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt @@ -9,6 +9,10 @@ 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 @@ -71,6 +75,23 @@ class PostService( 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 delete(id: String) { TODO("Not yet implemented") } diff --git a/src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt new file mode 100644 index 00000000..70ce1ff2 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt @@ -0,0 +1,41 @@ +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 @)") + } + } + } +} From cd8e8d530bc98b48aa80a4e4506790cd71340411 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 18 May 2023 15:11:07 +0900 Subject: [PATCH 08/21] =?UTF-8?q?feat:=20users=E3=81=AEAPI=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/hideout/form/UserCreate.kt | 3 + .../hideout/routing/api/internal/v1/Users.kt | 89 +++++++++++++++++++ .../hideout/service/impl/IUserService.kt | 17 +++- .../hideout/service/impl/PostService.kt | 25 ++++++ .../hideout/service/impl/UserService.kt | 21 ++++- 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/UserCreate.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/UserCreate.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/UserCreate.kt new file mode 100644 index 00000000..40d96a92 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/UserCreate.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.domain.model.hideout.form + +data class UserCreate(val username: String, val password: String) diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt new file mode 100644 index 00000000..a16f28b7 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt @@ -0,0 +1,89 @@ +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.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.* + +fun Route.users(userService: IUserService) { + route("/users") { + get { + call.respond(userService.findAll()) + } + post { + val userCreate = call.receive() + 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.OK) + } + route("/{name}") { + + 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(userService.findFollowersById(userParameter.toLong())) + } + val acct = AcctUtil.parse(userParameter) + return@get call.respond(userService.findFollowersByNameAndDomain(acct.username, acct.domain)) + } + authenticate(TOKEN_AUTH) { + + post { + val userId = call.principal()?.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 = userService.findByNameAndDomain(acct.username, acct.domain) + 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(userService.findFollowingById(userParameter.toLong())) + } + val acct = AcctUtil.parse(userParameter) + return@get call.respond(userService.findFollowingByNameAndDomain(acct.username, acct.domain)) + } + } + } + + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt index d2d61b1b..ab726ac1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt @@ -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>): List suspend fun findByUrl(url: String): User @@ -30,5 +32,18 @@ interface IUserService { suspend fun findFollowersById(id: Long): List - suspend fun addFollowers(id: Long, follower: Long) + suspend fun findFollowersByNameAndDomain(name: String, domain: String?): List + + suspend fun findFollowingById(id: Long): List + + suspend fun findFollowingByNameAndDomain(name: String, domain: String?): List + + /** + * フォロワーを追加する + * + * @param id + * @param follower + * @return リクエストが成功したか + */ + suspend fun addFollowers(id: Long, follower: Long): Boolean } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt index e017610d..28fa7c9c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt @@ -92,6 +92,31 @@ class PostService( } } + override suspend fun findByUserIdForUser( + userId: Long, + since: Instant?, + until: Instant?, + minId: Long?, + maxId: Long?, + limit: Int?, + forUserId: Long? + ): List { + 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 { + TODO("Not yet implemented") + } + override suspend fun delete(id: String) { TODO("Not yet implemented") } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt index 599e274f..490c3859 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt @@ -35,6 +35,10 @@ class UserService(private val userRepository: IUserRepository, private val userA ?: throw UserNotFoundException("$name was not found.") } + override suspend fun findByNameAndDomain(name: String, domain: String?): User { + TODO("Not yet implemented") + } + override suspend fun findByNameAndDomains(names: List>): List = userRepository.findByNameAndDomains(names) @@ -87,6 +91,21 @@ class UserService(private val userRepository: IUserRepository, private val userA } override suspend fun findFollowersById(id: Long): List = userRepository.findFollowersById(id) + override suspend fun findFollowersByNameAndDomain(name: String, domain: String?): List { + TODO("Not yet implemented") + } - override suspend fun addFollowers(id: Long, follower: Long) = userRepository.createFollower(id, follower) + override suspend fun findFollowingById(id: Long): List { + TODO("Not yet implemented") + } + + override suspend fun findFollowingByNameAndDomain(name: String, domain: String?): List { + TODO("Not yet implemented") + } + + //TODO APのフォロー処理を作る + override suspend fun addFollowers(id: Long, follower: Long): Boolean { + userRepository.createFollower(id, follower) + return false + } } From 7c55f5c8c332b15ec300139b72a80addecaad270 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 18 May 2023 15:33:00 +0900 Subject: [PATCH 09/21] =?UTF-8?q?test:=20=E8=AA=8D=E8=A8=BC=E9=96=A2?= =?UTF-8?q?=E4=BF=82=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/hideout/dto/PostCreateDto.kt | 2 +- .../hideout/routing/api/internal/v1/Users.kt | 13 + src/main/resources/openapi/documentation.yaml | 365 ++++++++++++++++++ .../routing/activitypub/UsersAPTest.kt | 38 +- .../routing/api/internal/v1/PostsKtTest.kt | 2 +- .../ActivityPubFollowServiceImplTest.kt | 2 +- 6 files changed, 391 insertions(+), 31 deletions(-) create mode 100644 src/main/resources/openapi/documentation.yaml diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt index 272215a2..1869da21 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt @@ -5,7 +5,7 @@ import dev.usbharu.hideout.domain.model.hideout.entity.Visibility data class PostCreateDto( val text: String, val overview: String? = null, - val visibility: Visibility, + val visibility: Visibility = Visibility.PUBLIC, val repostId: Long? = null, val repolyId: Long? = null, val userId: Long diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt index a16f28b7..13917837 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt @@ -38,6 +38,19 @@ fun Route.users(userService: IUserService) { } 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(userService.findById(userParameter.toLong())) + } else { + val acct = AcctUtil.parse(userParameter) + return@get call.respond(userService.findByNameAndDomain(acct.username, acct.domain)) + } + } + } + route("/followers") { get { val userParameter = call.parameters["name"] diff --git a/src/main/resources/openapi/documentation.yaml b/src/main/resources/openapi/documentation.yaml new file mode 100644 index 00000000..d4e6d624 --- /dev/null +++ b/src/main/resources/openapi/documentation.yaml @@ -0,0 +1,365 @@ +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" + post: + description: "" + requestBody: + content: + '*/*': + schema: + $ref: "#/components/schemas/Post" + required: true + responses: + "200": + description: "OK" + content: + '*/*': + schema: + type: "object" + /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
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: + text: + type: "string" + overview: + type: "string" + visibility: + type: "string" + enum: + - "PUBLIC" + - "UNLISTED" + - "FOLLOWERS" + - "DIRECT" + repostId: + type: "integer" + format: "int64" + replyId: + type: "integer" + format: "int64" diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt index 03bf53a4..736ea065 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt @@ -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 {} - val activityPubService = mock {} val userService = mock {} val activityPubUserService = mock { @@ -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 {} - val activityPubService = mock {} val userService = mock {} val activityPubUserService = mock { @@ -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) diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt index de9e768a..a5143505 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt @@ -78,7 +78,7 @@ class PostsKtTest { } argumentCaptor { verify(postService).create(capture()) - assertEquals(PostCreateDto("test", 1234), firstValue) + assertEquals(PostCreateDto("test", userId = 1234), firstValue) } } } diff --git a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt index 93dfec61..12e54893 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt @@ -115,7 +115,7 @@ class ActivityPubFollowServiceImplTest { createdAt = Instant.now() ) ) - onBlocking { addFollowers(any(), any()) } doReturn Unit + onBlocking { addFollowers(any(), any()) } doReturn false } val activityPubFollowService = ActivityPubFollowServiceImpl( From da183caca0684f7f075189cc0df751561737d0b8 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 18 May 2023 15:45:37 +0900 Subject: [PATCH 10/21] =?UTF-8?q?test:=20=E8=AA=8D=E8=A8=BC=E9=96=A2?= =?UTF-8?q?=E4=BF=82=E3=82=92=E4=BF=AE=E6=AD=A32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routing/activitypub/InboxRoutingKtTest.kt | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRoutingKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRoutingKtTest.kt index 8e93e159..d1096731 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRoutingKtTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRoutingKtTest.kt @@ -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 { on { parseActivity(any()) } doThrow JsonParseException() } - val userService = mock() - val activityPubUserService = mock() + mock() + mock() 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 { on { parseActivity(any()) } doThrow JsonParseException() } - val userService = mock() - val activityPubUserService = mock() + mock() + mock() 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) From befd69b9f22d0de3a5377a15dfaac5739e647f09 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 19 May 2023 22:54:22 +0900 Subject: [PATCH 11/21] =?UTF-8?q?test:=20Posts=E3=81=AE=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/hideout/dto/UserResponse.kt | 11 + .../hideout/routing/api/internal/v1/Posts.kt | 3 +- .../hideout/routing/api/internal/v1/Users.kt | 4 +- .../hideout/service/impl/IUserService.kt | 3 + .../hideout/service/impl/UserService.kt | 5 + .../routing/api/internal/v1/PostsKtTest.kt | 84 --- .../routing/api/internal/v1/PostsTest.kt | 619 ++++++++++++++++++ .../routing/api/internal/v1/UsersTest.kt | 142 ++++ 8 files changed, 783 insertions(+), 88 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt delete mode 100644 src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt create mode 100644 src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsTest.kt create mode 100644 src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt new file mode 100644 index 00000000..7e73a81f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt @@ -0,0 +1,11 @@ +package dev.usbharu.hideout.domain.model.hideout.dto + +data class UserResponse( + val id: Long, + val name: String, + val domain: String, + val screenName: String, + val description: String = "", + val url: String, + val createdAt: Long +) diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt index db1b3f70..772ad8cf 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt @@ -46,7 +46,7 @@ fun Route.posts(postService: IPostService) { val minId = call.request.queryParameters["minId"]?.toLong() val maxId = call.request.queryParameters["maxId"]?.toLong() val limit = call.request.queryParameters["limit"]?.toInt() - call.respond(postService.findAll(since, until, minId, maxId, limit, userId)) + call.respond(HttpStatusCode.OK, postService.findAll(since, until, minId, maxId, limit, userId)) } get("/{id}") { val userId = call.principal()?.payload?.getClaim("uid")?.asLong() @@ -84,7 +84,6 @@ fun Route.posts(postService: IPostService) { ?: 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.response.header("Content-Location", post.url) call.respond(post) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt index 13917837..a4bea389 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt @@ -18,7 +18,7 @@ import io.ktor.server.routing.* fun Route.users(userService: IUserService) { route("/users") { get { - call.respond(userService.findAll()) + call.respond(userService.findAllForUser()) } post { val userCreate = call.receive() @@ -34,7 +34,7 @@ fun Route.users(userService: IUserService) { ) ) call.response.header("Location", "${Config.configData.url}/api/internal/v1/users/${user.name}") - call.respond(HttpStatusCode.OK) + call.respond(HttpStatusCode.Created) } route("/{name}") { diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt index ab726ac1..1fc6922e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt @@ -2,12 +2,15 @@ package dev.usbharu.hideout.service.impl import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto +import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse import dev.usbharu.hideout.domain.model.hideout.entity.User @Suppress("TooManyFunctions") interface IUserService { suspend fun findAll(limit: Int? = 100, offset: Long? = 0): List + suspend fun findAllForUser(limit: Int? = 100, offset: Long? = 0): List + suspend fun findById(id: Long): User suspend fun findByIds(ids: List): List diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt index 490c3859..b4f3157c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt @@ -3,6 +3,7 @@ package dev.usbharu.hideout.service.impl import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto +import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.repository.IUserRepository @@ -23,6 +24,10 @@ class UserService(private val userRepository: IUserRepository, private val userA ) } + override suspend fun findAllForUser(limit: Int?, offset: Long?): List { + TODO("Not yet implemented") + } + override suspend fun findById(id: Long): User = userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.") diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt deleted file mode 100644 index a5143505..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsKtTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -package dev.usbharu.hideout.routing.api.internal.v1 - -import com.auth0.jwt.interfaces.Claim -import com.auth0.jwt.interfaces.Payload -import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto -import dev.usbharu.hideout.domain.model.hideout.entity.Visibility -import dev.usbharu.hideout.domain.model.hideout.form.Post -import dev.usbharu.hideout.plugins.TOKEN_AUTH -import dev.usbharu.hideout.plugins.configureSerialization -import dev.usbharu.hideout.service.IPostService -import io.ktor.client.request.* -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 java.time.Instant -import kotlin.test.assertEquals - -class PostsKtTest { - - - @Test - fun `posts-post postsにpostしたら投稿できる`() = testApplication { - environment { - config = ApplicationConfig("empty.conf") - } - val claim = mock { - on { asLong() } doReturn 1234 - } - val payload = mock { - on { getClaim(eq("uid")) } doReturn claim - } - val postService = mock { - onBlocking { create(any()) } doAnswer { - val argument = it.getArgument(0) - dev.usbharu.hideout.domain.model.hideout.entity.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 = 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 { - verify(postService).create(capture()) - assertEquals(PostCreateDto("test", userId = 1234), firstValue) - } - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsTest.kt new file mode 100644 index 00000000..901acc89 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/PostsTest.kt @@ -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 { + 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 { + on { asLong() } doReturn 1234 + } + val payload = mock { + 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 { + 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 { + 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 { + onBlocking { findByIdForUser(any(), isNotNull()) } doReturn post + } + val claim = mock { + on { asLong() } doReturn 1234 + } + val payload = mock { + 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 { + on { asLong() } doReturn 1234 + } + val payload = mock { + on { getClaim(eq("uid")) } doReturn claim + } + val postService = mock { + onBlocking { create(any()) } doAnswer { + val argument = it.getArgument(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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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())) + } + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt new file mode 100644 index 00000000..7e411c49 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt @@ -0,0 +1,142 @@ +package dev.usbharu.hideout.routing.api.internal.v1 + +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.configureSecurity +import dev.usbharu.hideout.plugins.configureSerialization +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.config.* +import io.ktor.server.routing.* +import io.ktor.server.testing.* +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +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 { + onBlocking { findAllForUser(anyOrNull(), anyOrNull()) } doReturn users + } + application { + configureSerialization() + configureSecurity(mock(), mock(), mock(), mock(), mock()) + routing { + route("/api/internal/v1") { + users(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 { + 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) + } + } + } + + 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 { + onBlocking { usernameAlreadyUse(any()) } doReturn true + } + application { + configureSerialization() + configureSecurity(mock(), mock(), mock(), mock(), mock()) + routing { + route("/api/internal/v1") { + users(userService) + } + } + } + + client.post("/api/internal/v1/users") { + contentType(ContentType.Application.Json) + setBody(JsonObjectMapper.objectMapper.writeValueAsString(userCreateDto)) + }.apply { + assertEquals(HttpStatusCode.BadRequest, status) + } + } +} From 30e63fd2c9059c4191941b90001112a16f6a0a15 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 20 May 2023 15:04:56 +0900 Subject: [PATCH 12/21] =?UTF-8?q?test:=20Users=E3=81=AE=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routing/api/internal/v1/UsersTest.kt | 567 +++++++++++++++++- 1 file changed, 558 insertions(+), 9 deletions(-) diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt index 7e411c49..2fe03d92 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt @@ -1,24 +1,27 @@ 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.any -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock +import org.mockito.kotlin.* import utils.JsonObjectMapper import java.time.Instant import kotlin.test.assertEquals @@ -50,15 +53,15 @@ class UsersTest { Instant.now().toEpochMilli() ), ) - val userService = mock { - onBlocking { findAllForUser(anyOrNull(), anyOrNull()) } doReturn users + val userService = mock { + onBlocking { findAll(anyOrNull(), anyOrNull()) } doReturn users } application { configureSerialization() configureSecurity(mock(), mock(), mock(), mock(), mock()) routing { route("/api/internal/v1") { - users(userService) + users(mock(), userService) } } } @@ -96,7 +99,7 @@ class UsersTest { configureSecurity(mock(), mock(), mock(), mock(), mock()) routing { route("/api/internal/v1") { - users(userService) + users(userService, mock()) } } } @@ -127,7 +130,7 @@ class UsersTest { configureSecurity(mock(), mock(), mock(), mock(), mock()) routing { route("/api/internal/v1") { - users(userService) + users(userService, mock()) } } } @@ -139,4 +142,550 @@ class UsersTest { 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + on { asLong() } doReturn 1234 + } + val payload = mock { + on { getClaim(eq("uid")) } doReturn claim + } + + val userApiService = mock { + onBlocking { findByAcct(any()) } doReturn UserResponse( + 1235, + "follower1", + "example.com", + "test", + "test User", + "https://example.com/test", + Instant.now().toEpochMilli() + ) + } + val userService = mock { + 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 { + on { asLong() } doReturn 1234 + } + val payload = mock { + on { getClaim(eq("uid")) } doReturn claim + } + + val userApiService = mock { + onBlocking { findByAcct(any()) } doReturn UserResponse( + 1235, + "follower1", + "example.com", + "test", + "test User", + "https://example.com/test", + Instant.now().toEpochMilli() + ) + } + val userService = mock { + 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 { + on { asLong() } doReturn 1234 + } + val payload = mock { + on { getClaim(eq("uid")) } doReturn claim + } + + val userApiService = mock { + onBlocking { findById(any()) } doReturn UserResponse( + 1235, + "follower1", + "example.com", + "test", + "test User", + "https://example.com/test", + Instant.now().toEpochMilli() + ) + } + val userService = mock { + 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 { + 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 { + 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 { + 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())) + } + } } From 934a4317ca4eb8694ce67dee84c53ed46df2c365 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 20 May 2023 15:05:27 +0900 Subject: [PATCH 13/21] =?UTF-8?q?feat:=20Users=E3=81=AEAPI=E3=81=AE?= =?UTF-8?q?=E8=BF=94=E3=82=8A=E5=80=A4=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/hideout/dto/UserResponse.kt | 18 ++++- .../hideout/routing/api/internal/v1/Users.kt | 19 ++--- .../hideout/service/IUserApiService.kt | 24 ++++++ .../hideout/service/UserApiServiceImpl.kt | 38 +++++++++ .../hideout/service/impl/IUserService.kt | 3 - .../hideout/service/impl/UserService.kt | 2 +- src/main/resources/openapi/documentation.yaml | 79 ++++++++++++++++++- 7 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/IUserApiService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/UserApiServiceImpl.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt index 7e73a81f..e74a4a72 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt @@ -1,5 +1,7 @@ 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, @@ -8,4 +10,18 @@ data class UserResponse( val description: String = "", val url: String, val createdAt: Long -) +) { + companion object { + fun from(user: User): UserResponse { + return UserResponse( + user.id, + user.name, + user.domain, + user.screenName, + user.description, + user.url, + user.createdAt.toEpochMilli() + ) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt index a4bea389..92297e11 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt @@ -5,6 +5,7 @@ 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.* @@ -15,10 +16,10 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -fun Route.users(userService: IUserService) { +fun Route.users(userService: IUserService, userApiService: IUserApiService) { route("/users") { get { - call.respond(userService.findAllForUser()) + call.respond(userApiService.findAll()) } post { val userCreate = call.receive() @@ -43,10 +44,10 @@ fun Route.users(userService: IUserService) { val userParameter = (call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")) if (userParameter.toLongOrNull() != null) { - return@get call.respond(userService.findById(userParameter.toLong())) + return@get call.respond(userApiService.findById(userParameter.toLong())) } else { val acct = AcctUtil.parse(userParameter) - return@get call.respond(userService.findByNameAndDomain(acct.username, acct.domain)) + return@get call.respond(userApiService.findByAcct(acct)) } } } @@ -56,10 +57,10 @@ fun Route.users(userService: IUserService) { val userParameter = call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.") if (userParameter.toLongOrNull() != null) { - return@get call.respond(userService.findFollowersById(userParameter.toLong())) + return@get call.respond(userApiService.findFollowers(userParameter.toLong())) } val acct = AcctUtil.parse(userParameter) - return@get call.respond(userService.findFollowersByNameAndDomain(acct.username, acct.domain)) + return@get call.respond(userApiService.findFollowersByAcct(acct)) } authenticate(TOKEN_AUTH) { @@ -76,7 +77,7 @@ fun Route.users(userService: IUserService) { } } val acct = AcctUtil.parse(userParameter) - val targetUser = userService.findByNameAndDomain(acct.username, acct.domain) + val targetUser = userApiService.findByAcct(acct) if (userService.addFollowers(targetUser.id, userId)) { return@post call.respond(HttpStatusCode.OK) } else { @@ -90,10 +91,10 @@ fun Route.users(userService: IUserService) { val userParameter = (call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")) if (userParameter.toLongOrNull() != null) { - return@get call.respond(userService.findFollowingById(userParameter.toLong())) + return@get call.respond(userApiService.findFollowings(userParameter.toLong())) } val acct = AcctUtil.parse(userParameter) - return@get call.respond(userService.findFollowingByNameAndDomain(acct.username, acct.domain)) + return@get call.respond(userApiService.findFollowingsByAcct(acct)) } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IUserApiService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IUserApiService.kt new file mode 100644 index 00000000..dd79e471 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/IUserApiService.kt @@ -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 + + suspend fun findById(id: Long): UserResponse + + suspend fun findByIds(ids: List): List + + suspend fun findByAcct(acct: Acct): UserResponse + + suspend fun findByAccts(accts: List): List + + suspend fun findFollowers(userId: Long): List + + suspend fun findFollowings(userId: Long): List + + suspend fun findFollowersByAcct(acct: Acct): List + + suspend fun findFollowingsByAcct(acct: Acct): List +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/UserApiServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/UserApiServiceImpl.kt new file mode 100644 index 00000000..f3e70e10 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/UserApiServiceImpl.kt @@ -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 = + 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): List = + 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): List { + return userService.findByNameAndDomains(accts.map { it.username to (it.domain ?: Config.configData.domain) }) + .map { UserResponse.from(it) } + } + + override suspend fun findFollowers(userId: Long): List = + userService.findFollowersById(userId).map { UserResponse.from(it) } + + override suspend fun findFollowings(userId: Long): List = + userService.findFollowingById(userId).map { UserResponse.from(it) } + + override suspend fun findFollowersByAcct(acct: Acct): List = + userService.findFollowersByNameAndDomain(acct.username, acct.domain).map { UserResponse.from(it) } + + override suspend fun findFollowingsByAcct(acct: Acct): List = + userService.findFollowingByNameAndDomain(acct.username, acct.domain).map { UserResponse.from(it) } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt index 1fc6922e..ab726ac1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt @@ -2,15 +2,12 @@ package dev.usbharu.hideout.service.impl import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto -import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse import dev.usbharu.hideout.domain.model.hideout.entity.User @Suppress("TooManyFunctions") interface IUserService { suspend fun findAll(limit: Int? = 100, offset: Long? = 0): List - suspend fun findAllForUser(limit: Int? = 100, offset: Long? = 0): List - suspend fun findById(id: Long): User suspend fun findByIds(ids: List): List diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt index b4f3157c..1500d145 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt @@ -24,7 +24,7 @@ class UserService(private val userRepository: IUserRepository, private val userA ) } - override suspend fun findAllForUser(limit: Int?, offset: Long?): List { + suspend fun findAllForUser(limit: Int?, offset: Long?): List { TODO("Not yet implemented") } diff --git a/src/main/resources/openapi/documentation.yaml b/src/main/resources/openapi/documentation.yaml index d4e6d624..6c90a035 100644 --- a/src/main/resources/openapi/documentation.yaml +++ b/src/main/resources/openapi/documentation.yaml @@ -112,6 +112,15 @@ paths: required: false schema: type: "integer" + responses: + "200": + description: "OK" + content: + '*/*': + schema: + type: "array" + items: + $ref: "#/components/schemas/Post" post: description: "" requestBody: @@ -127,6 +136,61 @@ paths: '*/*': 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/{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: "" @@ -346,10 +410,19 @@ components: Post: type: "object" properties: - text: - type: "string" + 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: @@ -357,6 +430,8 @@ components: - "UNLISTED" - "FOLLOWERS" - "DIRECT" + url: + type: "string" repostId: type: "integer" format: "int64" From cbddc11e675640b2e7d39ec0c23848db9416c18a Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 20 May 2023 15:29:44 +0900 Subject: [PATCH 14/21] =?UTF-8?q?feat:=20Users=E3=81=AE=E3=83=AB=E3=83=BC?= =?UTF-8?q?=E3=83=86=E3=82=A3=E3=83=B3=E3=82=B0=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/dev/usbharu/hideout/Application.kt | 3 ++- src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 6a6dbf15..58f587df 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -104,7 +104,8 @@ fun Application.parent() { inject().value, inject().value, inject().value, - inject().value + inject().value, + inject().value, ) } diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt index 1d7cce98..3661fcb9 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt @@ -4,9 +4,11 @@ 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.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 @@ -20,7 +22,8 @@ fun Application.configureRouting( activityPubService: ActivityPubService, userService: IUserService, activityPubUserService: ActivityPubUserService, - postService: IPostService + postService: IPostService, + userApiService: IUserApiService ) { install(AutoHeadResponse) routing { @@ -34,6 +37,7 @@ fun Application.configureRouting( } route("/api/internal/v1") { posts(postService) + users(userService, userApiService) } } } From a41af72a8ff22dd9f530031603695f7dee4050a6 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 20 May 2023 16:45:36 +0900 Subject: [PATCH 15/21] =?UTF-8?q?chore:=20OpenAPI=E5=AE=9A=E7=BE=A9?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/openapi/documentation.yaml | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/src/main/resources/openapi/documentation.yaml b/src/main/resources/openapi/documentation.yaml index 6c90a035..df2efd03 100644 --- a/src/main/resources/openapi/documentation.yaml +++ b/src/main/resources/openapi/documentation.yaml @@ -152,6 +152,112 @@ paths: '*/*': 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: "" @@ -438,3 +544,29 @@ components: 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" From f76e904a820589343c48993bdaf19a746c1add27 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 20 May 2023 17:19:38 +0900 Subject: [PATCH 16/21] =?UTF-8?q?feat:=20=E5=AE=9F=E8=A3=85=E3=81=97?= =?UTF-8?q?=E5=BF=98=E3=82=8C=E3=81=A6=E3=81=84=E3=81=9F=E9=83=A8=E5=88=86?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/service/impl/UserService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt index 1500d145..748dc49f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt @@ -41,7 +41,8 @@ class UserService(private val userRepository: IUserRepository, private val userA } override suspend fun findByNameAndDomain(name: String, domain: String?): User { - TODO("Not yet implemented") + return userRepository.findByNameAndDomain(name, domain ?: Config.configData.domain) + ?: throw UserNotFoundException("$name was not found.") } override suspend fun findByNameAndDomains(names: List>): List = From 3061277c57727e38b204391531cc24ea70da2fcd Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 20 May 2023 17:56:11 +0900 Subject: [PATCH 17/21] =?UTF-8?q?fix:=20AP=E3=81=AEKey=E3=81=ABid=E3=81=8C?= =?UTF-8?q?=E3=81=AA=E3=81=8F=E3=81=AA=E3=81=A3=E3=81=A6=E3=81=84=E3=81=9F?= =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/usbharu/hideout/domain/model/ap/Key.kt | 15 +++++++++++---- .../dev/usbharu/hideout/domain/model/ap/Object.kt | 9 +++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt index 850bacb8..63986e4c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt @@ -8,29 +8,36 @@ open class Key : Object { constructor( type: List, 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 } + override fun equals(other: Any?): Boolean { if (this === other) return true 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 { + return "Key(owner=$owner, publicKeyPem=$publicKeyPem) ${super.toString()}" + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt index ae52c978..1adcb211 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt @@ -20,6 +20,7 @@ open class Object : JsonLd { this.id = id } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Object) return false @@ -27,7 +28,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 +37,13 @@ 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 { + return "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}" + } companion object { @JvmStatic From e2150c3a07c0b73cd97f7ea19eca1de74968ace1 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 20 May 2023 18:02:25 +0900 Subject: [PATCH 18/21] =?UTF-8?q?style:=20=E5=90=8D=E5=89=8D=E4=BB=98?= =?UTF-8?q?=E3=81=8D=E5=BC=95=E6=95=B0=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/domain/model/ap/Follow.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Follow.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Follow.kt index 7977721d..8e29fbb5 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Follow.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Follow.kt @@ -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` } } From 5f01ccab634e43f586d404f3ad389770d05b2bbf Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 20 May 2023 18:10:09 +0900 Subject: [PATCH 19/21] =?UTF-8?q?style:=20=E3=82=B9=E3=82=BF=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/usbharu/hideout/domain/model/ap/Key.kt | 1 - .../dev/usbharu/hideout/domain/model/ap/Object.kt | 1 - .../hideout/routing/api/internal/v1/Posts.kt | 13 ++++++++----- .../hideout/routing/api/internal/v1/Users.kt | 15 ++++++++------- .../usbharu/hideout/service/impl/UserService.kt | 2 +- .../kotlin/dev/usbharu/hideout/util/AcctUtil.kt | 3 ++- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt index 63986e4c..f9b772ff 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt @@ -20,7 +20,6 @@ open class Key : Object { this.publicKeyPem = publicKeyPem } - override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Key) return false diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt index 1adcb211..ef51aa7f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt @@ -20,7 +20,6 @@ open class Object : JsonLd { this.id = id } - override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Object) return false diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt index 772ad8cf..b858a41b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt @@ -52,8 +52,10 @@ fun Route.posts(postService: IPostService) { val userId = call.principal()?.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.")) + val post = ( + postService.findByIdForUser(id, userId) + ?: throw PostNotFoundException("$id was not found or is not authorized.") + ) call.respond(post) } } @@ -76,14 +78,15 @@ fun Route.posts(postService: IPostService) { postService.findByUserIdForUser(targetUserId, forUserId = userId) } call.respond(posts) - } get("/{id}") { val userId = call.principal()?.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.")) + val post = ( + postService.findByIdForUser(id, userId) + ?: throw PostNotFoundException("$id was not found or is not authorized.") + ) call.respond(post) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt index 92297e11..38efa48a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt @@ -38,11 +38,12 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) { 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.")) + 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 { @@ -63,7 +64,6 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) { return@get call.respond(userApiService.findFollowersByAcct(acct)) } authenticate(TOKEN_AUTH) { - post { val userId = call.principal()?.payload?.getClaim("uid")?.asLong() ?: throw IllegalStateException("no principal") @@ -88,8 +88,10 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) { } route("/following") { get { - val userParameter = (call.parameters["name"] - ?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")) + 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())) } @@ -98,6 +100,5 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) { } } } - } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt index 748dc49f..7e336fd9 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt @@ -109,7 +109,7 @@ class UserService(private val userRepository: IUserRepository, private val userA TODO("Not yet implemented") } - //TODO APのフォロー処理を作る + // TODO APのフォロー処理を作る override suspend fun addFollowers(id: Long, follower: Long): Boolean { userRepository.createFollower(id, follower) return false diff --git a/src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt index 70ce1ff2..34ab88ba 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/AcctUtil.kt @@ -26,7 +26,8 @@ object AcctUtil { val userName = substring.substringBefore("@") val domain = substring.substringAfter("@") Acct( - userName, domain.ifBlank { null } + userName, + domain.ifBlank { null } ) } else { throw IllegalArgumentException("Invalid acct.(@ are in the wrong position)") From 7211a6643d5a47291d180ad9877cd3905c38cbae Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 20 May 2023 19:11:14 +0900 Subject: [PATCH 20/21] =?UTF-8?q?style:=20=E3=82=B9=E3=82=BF=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- detekt.yml | 9 +++++++++ .../kotlin/dev/usbharu/hideout/Application.kt | 12 ++++++------ .../dev/usbharu/hideout/domain/model/ap/Key.kt | 4 +--- .../dev/usbharu/hideout/domain/model/ap/Object.kt | 4 +--- .../domain/model/hideout/dto/UserResponse.kt | 14 +++++++------- .../kotlin/dev/usbharu/hideout/plugins/Routing.kt | 1 + .../dev/usbharu/hideout/plugins/Security.kt | 4 ++-- .../hideout/routing/activitypub/UserRouting.kt | 4 +++- .../hideout/routing/api/internal/v1/Posts.kt | 15 ++++++++------- .../hideout/routing/api/internal/v1/Users.kt | 1 + .../hideout/routing/api/mastodon/v1/Statuses.kt | 1 + .../dev/usbharu/hideout/service/IPostService.kt | 1 + .../service/ServerInitialiseServiceImpl.kt | 2 +- .../usbharu/hideout/service/impl/PostService.kt | 11 +++++++++-- .../usbharu/hideout/service/impl/UserService.kt | 5 ----- 15 files changed, 51 insertions(+), 37 deletions(-) diff --git a/detekt.yml b/detekt.yml index 73a9d00a..436ffa18 100644 --- a/detekt.yml +++ b/detekt.yml @@ -32,6 +32,15 @@ style: ForbiddenComment: active: false + ThrowsCount: + active: false + + UseCheckOrError: + active: false + + UseRequire: + active: false + complexity: CognitiveComplexMethod: active: true diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 58f587df..6b0b8c90 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -100,12 +100,12 @@ fun Application.parent() { inject().value, ) configureRouting( - inject().value, - inject().value, - inject().value, - inject().value, - inject().value, - inject().value, + httpSignatureVerifyService = inject().value, + activityPubService = inject().value, + userService = inject().value, + activityPubUserService = inject().value, + postService = inject().value, + userApiService = inject().value, ) } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt index f9b772ff..b5fd3529 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Key.kt @@ -36,7 +36,5 @@ open class Key : Object { return result } - override fun toString(): String { - return "Key(owner=$owner, publicKeyPem=$publicKeyPem) ${super.toString()}" - } + override fun toString(): String = "Key(owner=$owner, publicKeyPem=$publicKeyPem) ${super.toString()}" } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt index ef51aa7f..5d673244 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt @@ -40,9 +40,7 @@ open class Object : JsonLd { return result } - override fun toString(): String { - return "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}" - } + override fun toString(): String = "Object(type=$type, name=$name, actor=$actor, id=$id) ${super.toString()}" companion object { @JvmStatic diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt index e74a4a72..ab8c1829 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/UserResponse.kt @@ -14,13 +14,13 @@ data class UserResponse( companion object { fun from(user: User): UserResponse { return UserResponse( - user.id, - user.name, - user.domain, - user.screenName, - user.description, - user.url, - user.createdAt.toEpochMilli() + id = user.id, + name = user.name, + domain = user.domain, + screenName = user.screenName, + description = user.description, + url = user.url, + createdAt = user.createdAt.toEpochMilli() ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt index 3661fcb9..2e0c6b1a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt @@ -17,6 +17,7 @@ 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, diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index bf5c95f2..b6170270 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -76,8 +76,8 @@ fun Application.configureSecurity( } authenticate(TOKEN_AUTH) { get("/auth-check") { - val principal = call.principal() - val username = principal!!.payload.getClaim("uid") + val principal = call.principal() ?: throw IllegalStateException("no principal") + val username = principal.payload.getClaim("uid") call.respondText("Hello $username") } } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt index 1814c62e..a22dc3cc 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt @@ -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)) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt index b858a41b..e91c5b93 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Posts.kt @@ -17,21 +17,22 @@ 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() ?: throw RuntimeException("no principal") + val principal = call.principal() ?: throw IllegalStateException("no principal") val userId = principal.payload.getClaim("uid").asLong() val receive = call.receive() val postCreateDto = PostCreateDto( - receive.text, - receive.overview, - receive.visibility, - receive.repostId, - receive.replyId, - userId + 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) diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt index 38efa48a..5a51c4c2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt @@ -16,6 +16,7 @@ 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 { diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/mastodon/v1/Statuses.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/mastodon/v1/Statuses.kt index 9b5c5fa8..2bd31725 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/mastodon/v1/Statuses.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/mastodon/v1/Statuses.kt @@ -3,6 +3,7 @@ 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 { diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt index f2da7a53..e4a33550 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt @@ -5,6 +5,7 @@ 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): Post suspend fun create(post: PostCreateDto): Post diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt index 78bc0192..13fabeaf 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt @@ -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) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt index 28fa7c9c..cbb7a3bb 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt @@ -41,8 +41,15 @@ class PostService( val user = userService.findById(post.userId) val id = postRepository.generateId() val postEntity = Post( - id, user.id, null, post.text, - Instant.now().toEpochMilli(), Visibility.PUBLIC, "${user.url}/posts/$id", null, null + 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 diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt index 7e336fd9..14f7b04b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt @@ -3,7 +3,6 @@ package dev.usbharu.hideout.service.impl import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto -import dev.usbharu.hideout.domain.model.hideout.dto.UserResponse import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.repository.IUserRepository @@ -24,10 +23,6 @@ class UserService(private val userRepository: IUserRepository, private val userA ) } - suspend fun findAllForUser(limit: Int?, offset: Long?): List { - TODO("Not yet implemented") - } - override suspend fun findById(id: Long): User = userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.") From 7a20527238cec6375e99ed0a1b1a915d602c6e72 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 20 May 2023 19:19:51 +0900 Subject: [PATCH 21/21] =?UTF-8?q?chore:=20=E3=82=A4=E3=83=B3=E3=83=87?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=81=AE=E9=87=8D=E8=A6=81=E5=BA=A6=E3=82=92?= =?UTF-8?q?0=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- detekt.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/detekt.yml b/detekt.yml index 436ffa18..d0f97bdc 100644 --- a/detekt.yml +++ b/detekt.yml @@ -1,5 +1,7 @@ build: maxIssues: 20 + weights: + Indentation: 0 style: ClassOrdering: