diff --git a/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt b/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt index 00895c4..ec2547e 100644 --- a/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt +++ b/src/main/kotlin/io/github/nomisrev/env/Dependencies.kt @@ -32,7 +32,8 @@ suspend fun ResourceScope.dependencies(env: Env): Dependencies { val hikari = hikari(env.dataSource) val sqlDelight = sqlDelight(hikari) val userRepo = userPersistence(sqlDelight.usersQueries, sqlDelight.followingQueries) - val articleRepo = articleRepo(sqlDelight.articlesQueries, sqlDelight.tagsQueries) + val articleRepo = + articleRepo(sqlDelight.articlesQueries, sqlDelight.commentsQueries, sqlDelight.tagsQueries) val tagPersistence = tagPersistence(sqlDelight.tagsQueries) val favouritePersistence = favouritePersistence(sqlDelight.favoritesQueries) val jwtService = jwtService(env.auth, userRepo) diff --git a/src/main/kotlin/io/github/nomisrev/repo/ArticlePersistence.kt b/src/main/kotlin/io/github/nomisrev/repo/ArticlePersistence.kt index 4d7921f..f8e3e40 100644 --- a/src/main/kotlin/io/github/nomisrev/repo/ArticlePersistence.kt +++ b/src/main/kotlin/io/github/nomisrev/repo/ArticlePersistence.kt @@ -11,6 +11,8 @@ import io.github.nomisrev.routes.Profile import io.github.nomisrev.service.Slug import io.github.nomisrev.sqldelight.Articles import io.github.nomisrev.sqldelight.ArticlesQueries +import io.github.nomisrev.sqldelight.CommentsQueries +import io.github.nomisrev.sqldelight.SelectForSlug import io.github.nomisrev.sqldelight.TagsQueries import java.time.OffsetDateTime @@ -37,9 +39,11 @@ interface ArticlePersistence { suspend fun getFeed(userId: UserId, limit: FeedLimit, offset: FeedOffset): List
suspend fun getArticleBySlug(slug: Slug): Either + + suspend fun getCommentsForSlug(slug: Slug): List } -fun articleRepo(articles: ArticlesQueries, tagsQueries: TagsQueries) = +fun articleRepo(articles: ArticlesQueries, comments: CommentsQueries, tagsQueries: TagsQueries) = object : ArticlePersistence { override suspend fun create( authorId: UserId, @@ -108,4 +112,7 @@ fun articleRepo(articles: ArticlesQueries, tagsQueries: TagsQueries) = val article = articles.selectBySlug(slug.value).executeAsOneOrNull() ensureNotNull(article) { ArticleBySlugNotFound(slug.value) } } + + override suspend fun getCommentsForSlug(slug: Slug): List = + comments.selectForSlug(slug.value).executeAsList() } diff --git a/src/main/kotlin/io/github/nomisrev/routes/articles.kt b/src/main/kotlin/io/github/nomisrev/routes/articles.kt index 3d4554f..4c8c29c 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/articles.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/articles.kt @@ -13,6 +13,7 @@ import io.ktor.server.application.call import io.ktor.server.request.receive import io.ktor.server.resources.get import io.ktor.server.resources.post +import io.ktor.server.response.respond import io.ktor.server.routing.Route import java.time.OffsetDateTime import kotlinx.serialization.KSerializer @@ -61,6 +62,8 @@ data class Comment( val author: Profile ) +@Serializable data class MultipleCommentsResponse(val comments: List) + @Serializable data class NewArticle( val title: String, @@ -97,6 +100,9 @@ data class ArticleResource(val parent: RootResource = RootResource) { data class ArticlesResource(val parent: RootResource = RootResource) { @Resource("{slug}") data class Slug(val parent: ArticlesResource = ArticlesResource(), val slug: String) + + @Resource("{slug}/comments") + data class Comments(val parent: ArticlesResource = ArticlesResource(), val slug: String) } fun Route.articleRoutes( @@ -159,6 +165,15 @@ fun Route.articleRoutes( } } +fun Route.commentRoutes(articleService: ArticleService, jwtService: JwtService) { + get { slug -> + jwtAuth(jwtService) { (_, _) -> + val comments = articleService.getCommentsForSlug(Slug(slug.slug)) + call.respond(MultipleCommentsResponse(comments)) + } + } +} + private object OffsetDateTimeIso8601Serializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("OffsetDateTime", PrimitiveKind.STRING) diff --git a/src/main/kotlin/io/github/nomisrev/routes/root.kt b/src/main/kotlin/io/github/nomisrev/routes/root.kt index 4209e71..aacf97f 100644 --- a/src/main/kotlin/io/github/nomisrev/routes/root.kt +++ b/src/main/kotlin/io/github/nomisrev/routes/root.kt @@ -10,6 +10,7 @@ fun Application.routes(deps: Dependencies) = routing { tagRoutes(deps.tagPersistence) articleRoutes(deps.articleService, deps.jwtService) profileRoutes(deps.userPersistence, deps.jwtService) + commentRoutes(deps.articleService, deps.jwtService) } @Resource("/api") data object RootResource diff --git a/src/main/kotlin/io/github/nomisrev/service/ArticleService.kt b/src/main/kotlin/io/github/nomisrev/service/ArticleService.kt index f230b1c..d7a3903 100644 --- a/src/main/kotlin/io/github/nomisrev/service/ArticleService.kt +++ b/src/main/kotlin/io/github/nomisrev/service/ArticleService.kt @@ -9,6 +9,7 @@ import io.github.nomisrev.repo.TagPersistence import io.github.nomisrev.repo.UserId import io.github.nomisrev.repo.UserPersistence import io.github.nomisrev.routes.Article +import io.github.nomisrev.routes.Comment import io.github.nomisrev.routes.FeedLimit import io.github.nomisrev.routes.FeedOffset import io.github.nomisrev.routes.MultipleArticlesResponse @@ -38,6 +39,8 @@ interface ArticleService { /** Get article by Slug */ suspend fun getArticleBySlug(slug: Slug): Either + + suspend fun getCommentsForSlug(slug: Slug): List } fun articleService( @@ -117,4 +120,15 @@ fun articleService( articleTags ) } + + override suspend fun getCommentsForSlug(slug: Slug): List = + articlePersistence.getCommentsForSlug(slug).map { comment -> + Comment( + comment.comment__id, + comment.comment__createdAt, + comment.comment__updatedAt, + comment.comment__body, + Profile(comment.author__username, comment.author__bio, comment.author__image, false) + ) + } } diff --git a/src/main/sqldelight/io/github/nomisrev/sqldelight/Comments.sq b/src/main/sqldelight/io/github/nomisrev/sqldelight/Comments.sq index f44dd37..38c995b 100644 --- a/src/main/sqldelight/io/github/nomisrev/sqldelight/Comments.sq +++ b/src/main/sqldelight/io/github/nomisrev/sqldelight/Comments.sq @@ -21,3 +21,11 @@ WHERE article_id = :articleId; delete: DELETE FROM comments WHERE id = :id; + +selectForSlug: +SELECT comments.id AS comment__id, comments.article_id AS comment__articleId, comments.body AS comment__body, comments.author AS comment__author, comments.createdAt AS comment__createdAt, comments.updatedAt AS comment__updatedAt, +users.username AS author__username, users.bio AS author__bio, users.image AS author__image +FROM comments +INNER JOIN articles ON comments.article_id = articles.id +INNER JOIN users ON comments.author = users.id +WHERE articles.slug = :slug; \ No newline at end of file diff --git a/src/test/kotlin/io/github/nomisrev/routes/ArticlesRouteSpec.kt b/src/test/kotlin/io/github/nomisrev/routes/ArticlesRouteSpec.kt index 9de77e6..c0b30de 100644 --- a/src/test/kotlin/io/github/nomisrev/routes/ArticlesRouteSpec.kt +++ b/src/test/kotlin/io/github/nomisrev/routes/ArticlesRouteSpec.kt @@ -3,8 +3,11 @@ package io.github.nomisrev.routes import arrow.core.flatMap import io.github.nefilim.kjwt.JWSHMAC512Algorithm import io.github.nefilim.kjwt.JWT +import io.github.nomisrev.KotestProject +import io.github.nomisrev.auth.JwtToken import io.github.nomisrev.repo.UserId import io.github.nomisrev.service.CreateArticle +import io.github.nomisrev.service.Login import io.github.nomisrev.service.RegisterUser import io.github.nomisrev.withServer import io.kotest.assertions.arrow.core.shouldBeRight @@ -12,7 +15,9 @@ import io.kotest.assertions.arrow.core.shouldBeSome import io.kotest.core.spec.style.StringSpec import io.ktor.client.call.body import io.ktor.client.plugins.resources.get +import io.ktor.client.request.bearerAuth import io.ktor.http.HttpStatusCode +import kotlin.properties.Delegates class ArticlesRouteSpec : StringSpec({ @@ -20,12 +25,39 @@ class ArticlesRouteSpec : val validUsername = "username2" val validEmail = "valid2@domain.com" val validPw = "123456789" + // User 3 + val validUsername3 = "username3" + val validEmail3 = "valid3@domain.com" + val validPw3 = "123456789" + // Article val validTags = setOf("arrow", "kotlin", "ktor", "sqldelight") val validTitle = "Fake Article Arrow " val validDescription = "This is a fake article description." val validBody = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + var token: JwtToken by Delegates.notNull() + var userId: UserId by Delegates.notNull() + + beforeAny { + KotestProject.dependencies + .get() + .userService + .register(RegisterUser(validUsername3, validEmail3, validPw3)) + .shouldBeRight() + } + + beforeTest { + token = + KotestProject.dependencies + .get() + .userService + .login(Login(validEmail3, validPw3)) + .shouldBeRight() + .first + userId = KotestProject.dependencies.get().jwtService.verifyJwtToken(token).shouldBeRight() + } + "Article by slug not found" { withServer { val response = get(ArticlesResource.Slug(slug = "slug")) @@ -39,7 +71,7 @@ class ArticlesRouteSpec : "Can get an article by slug" { withServer { dependencies -> - val userId = + val user1Id = dependencies.userService .register(RegisterUser(validUsername, validEmail, validPw)) .flatMap { JWT.decodeT(it.value, JWSHMAC512Algorithm) } @@ -49,7 +81,7 @@ class ArticlesRouteSpec : val article = dependencies.articleService .createArticle( - CreateArticle(UserId(userId), validTitle, validDescription, validBody, validTags) + CreateArticle(UserId(user1Id), validTitle, validDescription, validBody, validTags) ) .shouldBeRight() @@ -59,4 +91,36 @@ class ArticlesRouteSpec : assert(response.body().article == article) } } + + "can get comments for an article by slug when authenticated" { + withServer { dependencies -> + val article = + dependencies.articleService + .createArticle( + CreateArticle(userId, validTitle, validDescription, validBody, validTags) + ) + .shouldBeRight() + + val response = + get(ArticlesResource.Comments(slug = article.slug)) { bearerAuth(token.value) } + + assert(response.status == HttpStatusCode.OK) + assert(response.body().comments == emptyList()) + } + } + + "can not get comments for an article when not authenticated" { + withServer { dependencies -> + val article = + dependencies.articleService + .createArticle( + CreateArticle(userId, validTitle, validDescription, validBody, validTags) + ) + .shouldBeRight() + + val response = get(ArticlesResource.Comments(slug = article.slug)) + + assert(response.status == HttpStatusCode.Unauthorized) + } + } })