Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get articles comments #206

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/main/kotlin/io/github/nomisrev/env/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -37,9 +39,11 @@ interface ArticlePersistence {
suspend fun getFeed(userId: UserId, limit: FeedLimit, offset: FeedOffset): List<Article>

suspend fun getArticleBySlug(slug: Slug): Either<ArticleBySlugNotFound, Articles>

suspend fun getCommentsForSlug(slug: Slug): List<SelectForSlug>
}

fun articleRepo(articles: ArticlesQueries, tagsQueries: TagsQueries) =
fun articleRepo(articles: ArticlesQueries, comments: CommentsQueries, tagsQueries: TagsQueries) =
object : ArticlePersistence {
override suspend fun create(
authorId: UserId,
Expand Down Expand Up @@ -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<SelectForSlug> =
comments.selectForSlug(slug.value).executeAsList()
}
15 changes: 15 additions & 0 deletions src/main/kotlin/io/github/nomisrev/routes/articles.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -61,6 +62,8 @@ data class Comment(
val author: Profile
)

@Serializable data class MultipleCommentsResponse(val comments: List<Comment>)

@Serializable
data class NewArticle(
val title: String,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -159,6 +165,15 @@ fun Route.articleRoutes(
}
}

fun Route.commentRoutes(articleService: ArticleService, jwtService: JwtService) {
get<ArticlesResource.Comments> { slug ->
jwtAuth(jwtService) { (_, _) ->
val comments = articleService.getCommentsForSlug(Slug(slug.slug))
call.respond(MultipleCommentsResponse(comments))
}
}
}

private object OffsetDateTimeIso8601Serializer : KSerializer<OffsetDateTime> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("OffsetDateTime", PrimitiveKind.STRING)
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/io/github/nomisrev/routes/root.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions src/main/kotlin/io/github/nomisrev/service/ArticleService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -38,6 +39,8 @@ interface ArticleService {

/** Get article by Slug */
suspend fun getArticleBySlug(slug: Slug): Either<DomainError, Article>

suspend fun getCommentsForSlug(slug: Slug): List<Comment>
}

fun articleService(
Expand Down Expand Up @@ -117,4 +120,15 @@ fun articleService(
articleTags
)
}

override suspend fun getCommentsForSlug(slug: Slug): List<Comment> =
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)
)
}
}
8 changes: 8 additions & 0 deletions src/main/sqldelight/io/github/nomisrev/sqldelight/Comments.sq
Original file line number Diff line number Diff line change
Expand Up @@ -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;
68 changes: 66 additions & 2 deletions src/test/kotlin/io/github/nomisrev/routes/ArticlesRouteSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,61 @@ 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
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({
// User
val validUsername = "username2"
val validEmail = "[email protected]"
val validPw = "123456789"
// User 3
val validUsername3 = "username3"
val validEmail3 = "[email protected]"
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"))
Expand All @@ -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) }
Expand All @@ -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()

Expand All @@ -59,4 +91,36 @@ class ArticlesRouteSpec :
assert(response.body<SingleArticleResponse>().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<MultipleCommentsResponse>().comments == emptyList<Comment>())
}
}

"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)
}
}
})