From 445311ee1b8eaf46d0356b2ea228a1d702c94886 Mon Sep 17 00:00:00 2001 From: aisensiy Date: Tue, 15 Aug 2017 16:36:07 +0800 Subject: [PATCH] create comment --- src/main/java/io/spring/api/CommentsApi.java | 70 +++++++++++ .../application/comment/CommentData.java | 23 ++++ .../comment/CommentQueryService.java | 31 +++++ .../comment/CommentReadService.java | 10 ++ .../java/io/spring/core/comment/Comment.java | 27 +++++ .../core/comment/CommentRepository.java | 9 ++ .../infrastructure/comment/CommentMapper.java | 14 +++ .../comment/MyBatisCommentRepository.java | 28 +++++ .../db/migration/V1__create_tables.sql | 9 ++ .../resources/mapper/ArticleReadService.xml | 11 +- src/main/resources/mapper/CommentMapper.xml | 32 +++++ .../resources/mapper/CommentReadService.xml | 23 ++++ .../java/io/spring/api/CommentsApiTest.java | 111 ++++++++++++++++++ .../comment/CommentQueryServiceTest.java | 52 ++++++++ .../comment/MyBatisCommentRepositoryTest.java | 33 ++++++ 15 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/spring/api/CommentsApi.java create mode 100644 src/main/java/io/spring/application/comment/CommentData.java create mode 100644 src/main/java/io/spring/application/comment/CommentQueryService.java create mode 100644 src/main/java/io/spring/application/comment/CommentReadService.java create mode 100644 src/main/java/io/spring/core/comment/Comment.java create mode 100644 src/main/java/io/spring/core/comment/CommentRepository.java create mode 100644 src/main/java/io/spring/infrastructure/comment/CommentMapper.java create mode 100644 src/main/java/io/spring/infrastructure/comment/MyBatisCommentRepository.java create mode 100644 src/main/resources/mapper/CommentMapper.xml create mode 100644 src/main/resources/mapper/CommentReadService.xml create mode 100644 src/test/java/io/spring/api/CommentsApiTest.java create mode 100644 src/test/java/io/spring/application/comment/CommentQueryServiceTest.java create mode 100644 src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java diff --git a/src/main/java/io/spring/api/CommentsApi.java b/src/main/java/io/spring/api/CommentsApi.java new file mode 100644 index 0000000..7d814fd --- /dev/null +++ b/src/main/java/io/spring/api/CommentsApi.java @@ -0,0 +1,70 @@ +package io.spring.api; + +import com.fasterxml.jackson.annotation.JsonRootName; +import io.spring.api.exception.InvalidRequestException; +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.comment.CommentData; +import io.spring.application.comment.CommentQueryService; +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import io.spring.core.comment.Comment; +import io.spring.core.comment.CommentRepository; +import io.spring.core.user.User; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.NotBlank; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import javax.xml.ws.Response; + +@RestController +@RequestMapping(path = "/articles/{slug}/comments") +public class CommentsApi { + private ArticleRepository articleRepository; + private CommentRepository commentRepository; + private CommentQueryService commentQueryService; + + @Autowired + public CommentsApi(ArticleRepository articleRepository, + CommentRepository commentRepository, + CommentQueryService commentQueryService) { + this.articleRepository = articleRepository; + this.commentRepository = commentRepository; + this.commentQueryService = commentQueryService; + } + + @PostMapping + public ResponseEntity createComment(@PathVariable("slug") String slug, + @AuthenticationPrincipal User user, + @Valid @RequestBody NewCommentParam newCommentParam, + BindingResult bindingResult) { + Article article = findArticle(slug); + if (bindingResult.hasErrors()) { + throw new InvalidRequestException(bindingResult); + } + Comment comment = new Comment(newCommentParam.getBody(), user.getId(), article.getId()); + commentRepository.save(comment); + return ResponseEntity.status(201).body(commentQueryService.findById(comment.getId(), user).get()); + } + + private Article findArticle(String slug) { + return articleRepository.findBySlug(slug).map(article -> article).orElseThrow(ResourceNotFoundException::new); + } +} + +@Getter +@NoArgsConstructor +@JsonRootName("comment") +class NewCommentParam { + @NotBlank(message = "can't be empty") + private String body; +} diff --git a/src/main/java/io/spring/application/comment/CommentData.java b/src/main/java/io/spring/application/comment/CommentData.java new file mode 100644 index 0000000..6d52d4e --- /dev/null +++ b/src/main/java/io/spring/application/comment/CommentData.java @@ -0,0 +1,23 @@ +package io.spring.application.comment; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import io.spring.application.profile.ProfileData; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@JsonRootName("comment") +public class CommentData { + private String id; + private String body; + private String articleId; + private DateTime createdAt; + private DateTime updatedAt; + @JsonProperty("author") + private ProfileData profileData; +} diff --git a/src/main/java/io/spring/application/comment/CommentQueryService.java b/src/main/java/io/spring/application/comment/CommentQueryService.java new file mode 100644 index 0000000..522dddf --- /dev/null +++ b/src/main/java/io/spring/application/comment/CommentQueryService.java @@ -0,0 +1,31 @@ +package io.spring.application.comment; + +import io.spring.application.profile.UserRelationshipQueryService; +import io.spring.core.user.User; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class CommentQueryService { + private CommentReadService commentReadService; + private UserRelationshipQueryService userRelationshipQueryService; + + public CommentQueryService(CommentReadService commentReadService, UserRelationshipQueryService userRelationshipQueryService) { + this.commentReadService = commentReadService; + this.userRelationshipQueryService = userRelationshipQueryService; + } + + public Optional findById(String id, User user) { + CommentData commentData = commentReadService.findById(id); + if (commentData == null) { + return Optional.empty(); + } else { + commentData.getProfileData().setFollowing( + userRelationshipQueryService.isUserFollowing( + user.getId(), + commentData.getProfileData().getId())); + } + return Optional.ofNullable(commentData); + } +} diff --git a/src/main/java/io/spring/application/comment/CommentReadService.java b/src/main/java/io/spring/application/comment/CommentReadService.java new file mode 100644 index 0000000..5b2a4b8 --- /dev/null +++ b/src/main/java/io/spring/application/comment/CommentReadService.java @@ -0,0 +1,10 @@ +package io.spring.application.comment; + +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Component; + +@Component +@Mapper +public interface CommentReadService { + CommentData findById(String id); +} diff --git a/src/main/java/io/spring/core/comment/Comment.java b/src/main/java/io/spring/core/comment/Comment.java new file mode 100644 index 0000000..ed4f8cd --- /dev/null +++ b/src/main/java/io/spring/core/comment/Comment.java @@ -0,0 +1,27 @@ +package io.spring.core.comment; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; + +import java.util.UUID; + +@Getter +@NoArgsConstructor +@EqualsAndHashCode(of = "id") +public class Comment { + private String id; + private String body; + private String userId; + private String articleId; + private DateTime createdAt; + + public Comment(String body, String userId, String articleId) { + this.id = UUID.randomUUID().toString(); + this.body = body; + this.userId = userId; + this.articleId = articleId; + this.createdAt = new DateTime(); + } +} diff --git a/src/main/java/io/spring/core/comment/CommentRepository.java b/src/main/java/io/spring/core/comment/CommentRepository.java new file mode 100644 index 0000000..0741f76 --- /dev/null +++ b/src/main/java/io/spring/core/comment/CommentRepository.java @@ -0,0 +1,9 @@ +package io.spring.core.comment; + +import java.util.Optional; + +public interface CommentRepository { + void save(Comment comment); + + Optional findById(String id); +} diff --git a/src/main/java/io/spring/infrastructure/comment/CommentMapper.java b/src/main/java/io/spring/infrastructure/comment/CommentMapper.java new file mode 100644 index 0000000..09a957e --- /dev/null +++ b/src/main/java/io/spring/infrastructure/comment/CommentMapper.java @@ -0,0 +1,14 @@ +package io.spring.infrastructure.comment; + +import io.spring.core.comment.Comment; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +@Component +@Mapper +public interface CommentMapper { + void insert(@Param("comment") Comment comment); + + Comment findById(@Param("id") String id); +} diff --git a/src/main/java/io/spring/infrastructure/comment/MyBatisCommentRepository.java b/src/main/java/io/spring/infrastructure/comment/MyBatisCommentRepository.java new file mode 100644 index 0000000..dc40537 --- /dev/null +++ b/src/main/java/io/spring/infrastructure/comment/MyBatisCommentRepository.java @@ -0,0 +1,28 @@ +package io.spring.infrastructure.comment; + +import io.spring.core.comment.Comment; +import io.spring.core.comment.CommentRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +public class MyBatisCommentRepository implements CommentRepository { + private CommentMapper commentMapper; + + @Autowired + public MyBatisCommentRepository(CommentMapper commentMapper) { + this.commentMapper = commentMapper; + } + + @Override + public void save(Comment comment) { + commentMapper.insert(comment); + } + + @Override + public Optional findById(String id) { + return Optional.ofNullable(commentMapper.findById(id)); + } +} diff --git a/src/main/resources/db/migration/V1__create_tables.sql b/src/main/resources/db/migration/V1__create_tables.sql index f84aa9a..210d424 100644 --- a/src/main/resources/db/migration/V1__create_tables.sql +++ b/src/main/resources/db/migration/V1__create_tables.sql @@ -37,3 +37,12 @@ create table article_tags ( article_id varchar(255) not null, tag_id varchar(255) not null ); + +create table comments ( + id varchar(255) primary key, + body text, + article_id varchar(255), + user_id varchar(255), + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/src/main/resources/mapper/ArticleReadService.xml b/src/main/resources/mapper/ArticleReadService.xml index 95c2ab6..98ed172 100644 --- a/src/main/resources/mapper/ArticleReadService.xml +++ b/src/main/resources/mapper/ArticleReadService.xml @@ -1,6 +1,12 @@ + + U.id userId, + U.username userUsername, + U.bio userBio, + U.image userImage + select A.id articleId, @@ -11,10 +17,7 @@ A.created_at articleCreatedAt, A.updated_at articleUpdatedAt, T.name as tagName, - U.id userId, - U.username userUsername, - U.bio userBio, - U.image userImage + from articles A left join article_tags AT on A.id = AT.article_id diff --git a/src/main/resources/mapper/CommentMapper.xml b/src/main/resources/mapper/CommentMapper.xml new file mode 100644 index 0000000..4386399 --- /dev/null +++ b/src/main/resources/mapper/CommentMapper.xml @@ -0,0 +1,32 @@ + + + + + insert into comments(id, body, user_id, article_id, created_at, updated_at) + values ( + #{comment.id}, + #{comment.body}, + #{comment.userId}, + #{comment.articleId}, + #{comment.createdAt}, + #{comment.createdAt} + ) + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/CommentReadService.xml b/src/main/resources/mapper/CommentReadService.xml new file mode 100644 index 0000000..abf1330 --- /dev/null +++ b/src/main/resources/mapper/CommentReadService.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/io/spring/api/CommentsApiTest.java b/src/test/java/io/spring/api/CommentsApiTest.java new file mode 100644 index 0000000..f7ee5a3 --- /dev/null +++ b/src/test/java/io/spring/api/CommentsApiTest.java @@ -0,0 +1,111 @@ +package io.spring.api; + +import io.restassured.RestAssured; +import io.spring.application.comment.CommentData; +import io.spring.application.comment.CommentQueryService; +import io.spring.application.profile.ProfileData; +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import io.spring.core.comment.CommentRepository; +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.any; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +@RunWith(SpringRunner.class) +public class CommentsApiTest extends TestWithCurrentUser { + @LocalServerPort + private int port; + + protected String email; + protected String username; + protected String defaultAvatar; + + @MockBean + private ArticleRepository articleRepository; + + @MockBean + private CommentRepository commentRepository; + @MockBean + private CommentQueryService commentQueryService; + + private Article article; + + @Before + public void setUp() throws Exception { + RestAssured.port = port; + email = "john@jacob.com"; + username = "johnjacob"; + defaultAvatar = "https://static.productionready.io/images/smiley-cyrus.jpg"; + userFixture(email, username, defaultAvatar); + + article = new Article("title", "desc", "body", new String[]{"test", "java"}, user.getId()); + when(articleRepository.findBySlug(eq(article.getSlug()))).thenReturn(Optional.of(article)); + } + + @Test + public void should_create_comment_success() throws Exception { + Map param = new HashMap() {{ + put("comment", new HashMap() {{ + put("body", "comment content"); + }}); + }}; + + CommentData commentData = new CommentData( + "123", + "comment", + article.getId(), + new DateTime(), + new DateTime(), + new ProfileData(user.getId(), user.getUsername(), user.getBio(), user.getImage(), false)); + + when(commentQueryService.findById(anyString(), eq(user))).thenReturn(Optional.of(commentData)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/articles/{slug}/comments", article.getSlug()) + .then() + .statusCode(201) + .body("comment.body", equalTo(commentData.getBody())); + } + + @Test + public void should_get_422_with_empty_body() throws Exception { + Map param = new HashMap() {{ + put("comment", new HashMap() {{ + put("body", ""); + }}); + }}; + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/articles/{slug}/comments", article.getSlug()) + .then() + .statusCode(422) + .body("errors.body[0]", equalTo("can't be empty")); + + } +} diff --git a/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java b/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java new file mode 100644 index 0000000..07e9c14 --- /dev/null +++ b/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java @@ -0,0 +1,52 @@ +package io.spring.application.comment; + +import io.spring.core.comment.Comment; +import io.spring.core.comment.CommentRepository; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.comment.MyBatisCommentRepository; +import io.spring.infrastructure.user.MyBatisUserRepository; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mybatis.spring.boot.test.autoconfigure.MybatisTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +@MybatisTest +@RunWith(SpringRunner.class) +@Import({MyBatisCommentRepository.class, MyBatisUserRepository.class, CommentQueryService.class}) +public class CommentQueryServiceTest { + @Autowired + private CommentRepository commentRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private CommentQueryService commentQueryService; + private User user; + + @Before + public void setUp() throws Exception { + user = new User("aisensiy@test.com", "aisensiy", "123", "", ""); + userRepository.save(user); + } + + @Test + public void should_read_comment_success() throws Exception { + Comment comment = new Comment("content", user.getId(), "123"); + commentRepository.save(comment); + + Optional optional = commentQueryService.findById(comment.getId(), user); + assertThat(optional.isPresent(), is(true)); + CommentData commentData = optional.get(); + assertThat(commentData.getProfileData().getUsername(), is(user.getUsername())); + } +} \ No newline at end of file diff --git a/src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java b/src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java new file mode 100644 index 0000000..c044da7 --- /dev/null +++ b/src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java @@ -0,0 +1,33 @@ +package io.spring.infrastructure.comment; + +import io.spring.core.comment.Comment; +import io.spring.core.comment.CommentRepository; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mybatis.spring.boot.test.autoconfigure.MybatisTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +@MybatisTest +@RunWith(SpringRunner.class) +@Import({MyBatisCommentRepository.class}) +public class MyBatisCommentRepositoryTest { + @Autowired + private CommentRepository commentRepository; + + @Test + public void should_create_and_fetch_comment_success() throws Exception { + Comment comment = new Comment("content", "123", "456"); + commentRepository.save(comment); + + Optional optional = commentRepository.findById(comment.getId()); + assertThat(optional.isPresent(), is(true)); + assertThat(optional.get(), is(comment)); + } +} \ No newline at end of file