diff --git a/src/main/java/io/spring/api/ArticleFavoriteApi.java b/src/main/java/io/spring/api/ArticleFavoriteApi.java new file mode 100644 index 0000000..147db07 --- /dev/null +++ b/src/main/java/io/spring/api/ArticleFavoriteApi.java @@ -0,0 +1,56 @@ +package io.spring.api; + +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.article.ArticleData; +import io.spring.application.article.ArticleQueryService; +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import io.spring.core.favorite.ArticleFavorite; +import io.spring.core.favorite.ArticleFavoriteRepository; +import io.spring.core.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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.RestController; + +import java.util.HashMap; + +@RestController +@RequestMapping(path = "articles/{slug}/favorite") +public class ArticleFavoriteApi { + private ArticleFavoriteRepository articleFavoriteRepository; + private ArticleRepository articleRepository; + private ArticleQueryService articleQueryService; + + @Autowired + public ArticleFavoriteApi(ArticleFavoriteRepository articleFavoriteRepository, + ArticleRepository articleRepository, + ArticleQueryService articleQueryService) { + this.articleFavoriteRepository = articleFavoriteRepository; + this.articleRepository = articleRepository; + this.articleQueryService = articleQueryService; + } + + @PostMapping + public ResponseEntity favoriteArticle(@PathVariable("slug") String slug, + @AuthenticationPrincipal User user) { + Article article = getArticle(slug); + ArticleFavorite articleFavorite = new ArticleFavorite(article.getId(), user.getId()); + articleFavoriteRepository.save(articleFavorite); + return responseArticleData(articleQueryService.findBySlug(slug, user).get()); + } + + private ResponseEntity> responseArticleData(final ArticleData articleData) { + return ResponseEntity.status(201).body(new HashMap() {{ + put("article", articleData); + }}); + } + + private Article getArticle(String slug) { + return articleRepository.findBySlug(slug).map(article -> article) + .orElseThrow(ResourceNotFoundException::new); + } +} diff --git a/src/main/java/io/spring/core/favorite/ArticleFavorite.java b/src/main/java/io/spring/core/favorite/ArticleFavorite.java new file mode 100644 index 0000000..e0e8cb8 --- /dev/null +++ b/src/main/java/io/spring/core/favorite/ArticleFavorite.java @@ -0,0 +1,18 @@ +package io.spring.core.favorite; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +@EqualsAndHashCode +public class ArticleFavorite { + private String articleId; + private String userId; + + public ArticleFavorite(String articleId, String userId) { + this.articleId = articleId; + this.userId = userId; + } +} diff --git a/src/main/java/io/spring/core/favorite/ArticleFavoriteRepository.java b/src/main/java/io/spring/core/favorite/ArticleFavoriteRepository.java new file mode 100644 index 0000000..6ab0e6b --- /dev/null +++ b/src/main/java/io/spring/core/favorite/ArticleFavoriteRepository.java @@ -0,0 +1,5 @@ +package io.spring.core.favorite; + +public interface ArticleFavoriteRepository { + void save(ArticleFavorite articleFavorite); +} diff --git a/src/main/java/io/spring/infrastructure/favorite/ArticleFavoriteMapper.java b/src/main/java/io/spring/infrastructure/favorite/ArticleFavoriteMapper.java new file mode 100644 index 0000000..153ad3b --- /dev/null +++ b/src/main/java/io/spring/infrastructure/favorite/ArticleFavoriteMapper.java @@ -0,0 +1,14 @@ +package io.spring.infrastructure.favorite; + +import io.spring.core.favorite.ArticleFavorite; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +@Mapper +@Component +public interface ArticleFavoriteMapper { + boolean find(@Param("articleFavorite") ArticleFavorite articleFavorite); + + void insert(@Param("articleFavorite") ArticleFavorite articleFavorite); +} diff --git a/src/main/java/io/spring/infrastructure/favorite/MyBatisArticleFavoriteRepository.java b/src/main/java/io/spring/infrastructure/favorite/MyBatisArticleFavoriteRepository.java new file mode 100644 index 0000000..1550b48 --- /dev/null +++ b/src/main/java/io/spring/infrastructure/favorite/MyBatisArticleFavoriteRepository.java @@ -0,0 +1,23 @@ +package io.spring.infrastructure.favorite; + +import io.spring.core.favorite.ArticleFavorite; +import io.spring.core.favorite.ArticleFavoriteRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class MyBatisArticleFavoriteRepository implements ArticleFavoriteRepository { + private ArticleFavoriteMapper mapper; + + @Autowired + public MyBatisArticleFavoriteRepository(ArticleFavoriteMapper mapper) { + this.mapper = mapper; + } + + @Override + public void save(ArticleFavorite articleFavorite) { + if (!mapper.find(articleFavorite)) { + mapper.insert(articleFavorite); + } + } +} diff --git a/src/main/resources/db/migration/V1__create_tables.sql b/src/main/resources/db/migration/V1__create_tables.sql index 210d424..1ba8fa2 100644 --- a/src/main/resources/db/migration/V1__create_tables.sql +++ b/src/main/resources/db/migration/V1__create_tables.sql @@ -20,7 +20,8 @@ create table articles ( create table article_favorites ( article_id varchar(255) not null, - user_id varchar(255) not null + user_id varchar(255) not null, + primary key(article_id, user_id) ); create table follows ( diff --git a/src/main/resources/mapper/ArticleFavoriteMapper.xml b/src/main/resources/mapper/ArticleFavoriteMapper.xml new file mode 100644 index 0000000..4dce7da --- /dev/null +++ b/src/main/resources/mapper/ArticleFavoriteMapper.xml @@ -0,0 +1,10 @@ + + + + + insert into article_favorites (article_id, user_id) values (#{articleFavorite.articleId}, #{articleFavorite.userId}) + + + \ No newline at end of file diff --git a/src/test/java/io/spring/api/ArticleFavoriteApiTest.java b/src/test/java/io/spring/api/ArticleFavoriteApiTest.java new file mode 100644 index 0000000..735a00c --- /dev/null +++ b/src/test/java/io/spring/api/ArticleFavoriteApiTest.java @@ -0,0 +1,92 @@ +package io.spring.api; + +import io.restassured.RestAssured; +import io.spring.application.article.ArticleData; +import io.spring.application.article.ArticleQueryService; +import io.spring.application.article.ArticleReadService; +import io.spring.application.profile.ProfileData; +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import io.spring.core.article.Tag; +import io.spring.core.favorite.ArticleFavoriteRepository; +import io.spring.core.user.User; +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.Optional; +import java.util.stream.Collectors; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.when; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@RunWith(SpringRunner.class) +public class ArticleFavoriteApiTest extends TestWithCurrentUser { + @MockBean + private ArticleFavoriteRepository articleFavoriteRepository; + + @MockBean + private ArticleRepository articleRepository; + + @MockBean + private ArticleQueryService articleQueryService; + + protected String email; + protected String username; + protected String defaultAvatar; + + @LocalServerPort + private int port; + private Article article; + private User anotherUser; + + @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); + anotherUser = new User("other@test.com", "other", "123", "", ""); + article = new Article("title", "desc", "body", new String[]{"java"}, anotherUser.getId()); + when(articleRepository.findBySlug(eq(article.getSlug()))).thenReturn(Optional.of(article)); + ArticleData articleData = new ArticleData( + article.getId(), + article.getSlug(), + article.getTitle(), + article.getDescription(), + article.getBody(), + true, + 1, + article.getCreatedAt(), + article.getUpdatedAt(), + article.getTags().stream().map(Tag::getName).collect(Collectors.toList()), + new ProfileData( + anotherUser.getId(), + anotherUser.getUsername(), + anotherUser.getBio(), + anotherUser.getImage(), + false + )); + when(articleQueryService.findBySlug(eq(articleData.getSlug()), eq(user))).thenReturn(Optional.of(articleData)); + } + + @Test + public void should_favorite_an_article_success() throws Exception { + given() + .header("Authorization", "Token " + token) + .when() + .post("/articles/{slug}/favorite", article.getSlug()) + .prettyPeek() + .then() + .statusCode(201) + .body("article.id", equalTo(article.getId())); + } +} diff --git a/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java b/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java index ca42db8..66f15e4 100644 --- a/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java +++ b/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java @@ -2,10 +2,14 @@ package io.spring.application.article; import io.spring.core.article.Article; import io.spring.core.article.ArticleRepository; +import io.spring.core.favorite.ArticleFavorite; +import io.spring.core.favorite.ArticleFavoriteRepository; import io.spring.core.user.User; import io.spring.core.user.UserRepository; import io.spring.infrastructure.article.MyBatisArticleRepository; +import io.spring.infrastructure.favorite.MyBatisArticleFavoriteRepository; 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; @@ -15,13 +19,14 @@ import org.springframework.test.context.junit4.SpringRunner; import java.util.Optional; +import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @MybatisTest -@Import({ArticleQueryService.class, MyBatisUserRepository.class, MyBatisArticleRepository.class}) +@Import({ArticleQueryService.class, MyBatisUserRepository.class, MyBatisArticleRepository.class, MyBatisArticleFavoriteRepository.class}) public class ArticleQueryServiceTest { @Autowired private ArticleQueryService queryService; @@ -32,13 +37,22 @@ public class ArticleQueryServiceTest { @Autowired private UserRepository userRepository; + @Autowired + private ArticleFavoriteRepository articleFavoriteRepository; + + private User user; + private Article article; + + @Before + public void setUp() throws Exception { + user = new User("aisensiy@gmail.com", "aisensiy", "123", "", ""); + userRepository.save(user); + article = new Article("test", "desc", "body", new String[]{"java", "spring"}, user.getId()); + articleRepository.save(article); + } + @Test public void should_fetch_article_success() throws Exception { - User user = new User("aisensiy@gmail.com", "aisensiy", "123", "", ""); - userRepository.save(user); - - Article article = new Article("test", "desc", "body", new String[]{"java", "spring"}, user.getId()); - articleRepository.save(article); Optional optional = queryService.findById(article.getId(), user); assertThat(optional.isPresent(), is(true)); @@ -48,4 +62,15 @@ public class ArticleQueryServiceTest { assertThat(fetched.getCreatedAt(), notNullValue()); assertThat(fetched.getUpdatedAt(), notNullValue()); } + + @Test + public void should_get_article_with_right_favorite_and_favorite_count() throws Exception { + User anotherUser = new User("other@test.com", "other", "123", "", ""); + userRepository.save(anotherUser); + articleFavoriteRepository.save(new ArticleFavorite(article.getId(), anotherUser.getId())); + + ArticleData articleData = queryService.findById(article.getId(), anotherUser).get(); + assertThat(articleData.getFavoritesCount(), is(1)); + assertThat(articleData.isFavorited(), is(true)); + } } \ No newline at end of file diff --git a/src/test/java/io/spring/infrastructure/favorite/MyBatisArticleFavoriteRepositoryTest.java b/src/test/java/io/spring/infrastructure/favorite/MyBatisArticleFavoriteRepositoryTest.java new file mode 100644 index 0000000..99853d5 --- /dev/null +++ b/src/test/java/io/spring/infrastructure/favorite/MyBatisArticleFavoriteRepositoryTest.java @@ -0,0 +1,31 @@ +package io.spring.infrastructure.favorite; + +import io.spring.core.favorite.ArticleFavorite; +import io.spring.core.favorite.ArticleFavoriteRepository; +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 static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@MybatisTest +@Import({MyBatisArticleFavoriteRepository.class}) +public class MyBatisArticleFavoriteRepositoryTest { + @Autowired + private ArticleFavoriteRepository articleFavoriteRepository; + + @Autowired + private ArticleFavoriteMapper articleFavoriteMapper; + + @Test + public void should_save_and_fetch_articleFavorite_success() throws Exception { + ArticleFavorite articleFavorite = new ArticleFavorite("123", "456"); + articleFavoriteRepository.save(articleFavorite); + assertThat(articleFavoriteMapper.find(articleFavorite), is(true)); + } +} \ No newline at end of file