From df4a402da50425d4dce09b23dcfebba319bb52b2 Mon Sep 17 00:00:00 2001 From: gunkim Date: Mon, 17 Feb 2025 18:12:19 +0900 Subject: [PATCH] Add article creation feature to API and integration tests --- .../web/api/article/ArticlesController.kt | 30 +++++++++++++++ .../model/request/CreateArticleRequest.kt | 8 ++++ .../article/model/response/ArticleResponse.kt | 9 +++-- .../api/ArticlesControllerIntegrationTest.kt | 37 ++++++++++++++++++- 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/io/github/gunkim/realworld/web/api/article/model/request/CreateArticleRequest.kt diff --git a/src/main/kotlin/io/github/gunkim/realworld/web/api/article/ArticlesController.kt b/src/main/kotlin/io/github/gunkim/realworld/web/api/article/ArticlesController.kt index ef2c2b3..e22ae2a 100644 --- a/src/main/kotlin/io/github/gunkim/realworld/web/api/article/ArticlesController.kt +++ b/src/main/kotlin/io/github/gunkim/realworld/web/api/article/ArticlesController.kt @@ -1,19 +1,25 @@ package io.github.gunkim.realworld.web.api.article +import io.github.gunkim.realworld.config.request.JsonRequest import io.github.gunkim.realworld.domain.article.Article +import io.github.gunkim.realworld.domain.article.service.CreateArticleService import io.github.gunkim.realworld.domain.article.service.FavoriteArticleService import io.github.gunkim.realworld.domain.article.service.GetArticleService import io.github.gunkim.realworld.domain.user.service.FollowUserService import io.github.gunkim.realworld.share.AuthenticatedUser +import io.github.gunkim.realworld.web.api.article.model.request.CreateArticleRequest import io.github.gunkim.realworld.web.api.article.model.request.GetArticlesRequest import io.github.gunkim.realworld.web.api.article.model.response.ArticleResponse import io.github.gunkim.realworld.web.api.article.model.response.ArticleResponseWrapper import io.github.gunkim.realworld.web.api.article.model.response.ArticlesResponse import java.util.UUID +import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RequestMapping("/api/articles") @@ -26,11 +32,19 @@ interface ArticleResource { @GetMapping("/{slug}") fun getArticle(@PathVariable slug: String): ArticleResponseWrapper + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + fun createArticle( + @JsonRequest("article") request: CreateArticleRequest, + @AuthenticationPrincipal authenticatedUser: AuthenticatedUser, + ): ArticleResponseWrapper } @RestController class ArticlesController( private val getArticleService: GetArticleService, + private val createArticleService: CreateArticleService, private val favoriteArticleService: FavoriteArticleService, private val followUserService: FollowUserService, ) : ArticleResource { @@ -62,6 +76,22 @@ class ArticlesController( ).let(::ArticleResponseWrapper) } + override fun createArticle( + request: CreateArticleRequest, + authenticatedUser: AuthenticatedUser, + ): ArticleResponseWrapper { + val article = createArticleService.createArticle( + request.title, + request.description, + request.body, + request.tagList, + authenticatedUser.uuid + ) + + return ArticleResponse.create(article) + .let(::ArticleResponseWrapper) + } + private fun articlesResponse( articles: List
, authenticatedUser: AuthenticatedUser?, diff --git a/src/main/kotlin/io/github/gunkim/realworld/web/api/article/model/request/CreateArticleRequest.kt b/src/main/kotlin/io/github/gunkim/realworld/web/api/article/model/request/CreateArticleRequest.kt new file mode 100644 index 0000000..efad2f5 --- /dev/null +++ b/src/main/kotlin/io/github/gunkim/realworld/web/api/article/model/request/CreateArticleRequest.kt @@ -0,0 +1,8 @@ +package io.github.gunkim.realworld.web.api.article.model.request + +data class CreateArticleRequest( + val title: String, + val description: String, + val body: String, + val tagList: List = listOf(), +) \ No newline at end of file diff --git a/src/main/kotlin/io/github/gunkim/realworld/web/api/article/model/response/ArticleResponse.kt b/src/main/kotlin/io/github/gunkim/realworld/web/api/article/model/response/ArticleResponse.kt index 0234baf..d5c5b3e 100644 --- a/src/main/kotlin/io/github/gunkim/realworld/web/api/article/model/response/ArticleResponse.kt +++ b/src/main/kotlin/io/github/gunkim/realworld/web/api/article/model/response/ArticleResponse.kt @@ -1,9 +1,6 @@ package io.github.gunkim.realworld.web.api.article.model.response import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.annotation.JsonTypeInfo.As.WRAPPER_OBJECT -import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME -import com.fasterxml.jackson.annotation.JsonTypeName import io.github.gunkim.realworld.domain.article.Article import io.github.gunkim.realworld.web.api.user.model.response.ProfileResponse import java.time.Instant @@ -47,5 +44,11 @@ data class ArticleResponse( article = article, favoritesCount = favoritesCount ) + + fun create(article: Article) = from( + article = article, + favoritesCount = 0, + favorited = false + ) } } diff --git a/src/test/kotlin/io/github/gunkim/realworld/web/api/ArticlesControllerIntegrationTest.kt b/src/test/kotlin/io/github/gunkim/realworld/web/api/ArticlesControllerIntegrationTest.kt index 1ebc3fe..a6d567f 100644 --- a/src/test/kotlin/io/github/gunkim/realworld/web/api/ArticlesControllerIntegrationTest.kt +++ b/src/test/kotlin/io/github/gunkim/realworld/web/api/ArticlesControllerIntegrationTest.kt @@ -1,26 +1,31 @@ package io.github.gunkim.realworld.web.api +import com.fasterxml.jackson.databind.ObjectMapper import io.github.gunkim.realworld.domain.article.Article import io.github.gunkim.realworld.domain.article.service.CreateArticleService import io.github.gunkim.realworld.domain.user.model.User import io.github.gunkim.realworld.share.IntegrationTest +import io.github.gunkim.realworld.web.api.article.model.request.CreateArticleRequest import io.kotest.core.annotation.Tags import io.kotest.core.spec.DisplayName import io.kotest.core.test.TestCase import org.springframework.http.HttpHeaders import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post @Tags("Integration Test") @DisplayName("Articles Controller - Integration Test") class ArticlesControllerIntegrationTest( private val createArticleService: CreateArticleService, + private val objectMapper: ObjectMapper, ) : IntegrationTest() { lateinit var token: String lateinit var articles: List
lateinit var author: User + lateinit var authUser: User override suspend fun beforeEach(testCase: TestCase) { - val (_, token) = createUser("gunkim.author@gmail.com", "gunkim", "password") + val (authUser, token) = createUser("gunkim.author@gmail.com", "gunkim", "password") val (author, _) = createUser("gunkim@gmail.com", "author gunkim", "password") val articles = listOf( createArticleService.createArticle( @@ -33,6 +38,7 @@ class ArticlesControllerIntegrationTest( ) this.token = token + this.authUser = authUser this.articles = articles this.author = author } @@ -72,5 +78,34 @@ class ArticlesControllerIntegrationTest( jsonPath("$.article.slug") { value(articles[0].slug) } }.andDo { print() } } + + "POST /api/articles - Create an article" { + val request = CreateArticleRequest( + title = "New Article", + description = "New Article Description", + body = "This is the body of the new article.", + tagList = listOf("tag1", "tag3") + ) + + val requestBody = mapOf("article" to request) + val requestJson = objectMapper.writeValueAsString(requestBody) + + mockMvc.post("/api/articles") { + contentType = org.springframework.http.MediaType.APPLICATION_JSON + header(HttpHeaders.AUTHORIZATION, token) + content = requestJson + }.andExpect { + status { isCreated() } + jsonPath("$.article.title") { value(request.title) } + jsonPath("$.article.description") { value(request.description) } + jsonPath("$.article.body") { value(request.body) } + jsonPath("$.article.tagList[0]") { value(request.tagList[0]) } + jsonPath("$.article.tagList[1]") { value(request.tagList[1]) } + jsonPath("$.article.author.username") { value(authUser.name) } + jsonPath("$.article.slug") { exists() } + jsonPath("$.article.createdAt") { exists() } + jsonPath("$.article.updatedAt") { exists() } + }.andDo { print() } + } } } \ No newline at end of file