diff --git a/src/main/java/io/spring/application/ArticleQueryService.java b/src/main/java/io/spring/application/ArticleQueryService.java index 959e8c638..7dcd7d0dd 100644 --- a/src/main/java/io/spring/application/ArticleQueryService.java +++ b/src/main/java/io/spring/application/ArticleQueryService.java @@ -6,6 +6,7 @@ import io.spring.application.data.ArticleDataList; import io.spring.application.data.ArticleFavoriteCount; import io.spring.core.user.User; +import io.spring.infrastructure.mybatis.readservice.ArticleBookmarksReadService; import io.spring.infrastructure.mybatis.readservice.ArticleFavoritesReadService; import io.spring.infrastructure.mybatis.readservice.ArticleReadService; import io.spring.infrastructure.mybatis.readservice.UserRelationshipQueryService; @@ -26,6 +27,7 @@ public class ArticleQueryService { private ArticleReadService articleReadService; private UserRelationshipQueryService userRelationshipQueryService; private ArticleFavoritesReadService articleFavoritesReadService; + private ArticleBookmarksReadService articleBookmarksReadService; public Optional findById(String id, User user) { ArticleData articleData = articleReadService.findById(id); @@ -126,6 +128,7 @@ private void fillExtraInfo(List articles, User currentUser) { setFavoriteCount(articles); if (currentUser != null) { setIsFavorite(articles, currentUser); + setIsBookmarked(articles, currentUser); setIsFollowingAuthor(articles, currentUser); } } @@ -172,8 +175,23 @@ private void setIsFavorite(List articles, User currentUser) { }); } + private void setIsBookmarked(List articles, User currentUser) { + Set bookmarkedArticles = + articleBookmarksReadService.userBookmarks( + articles.stream().map(articleData -> articleData.getId()).collect(toList()), + currentUser); + + articles.forEach( + articleData -> { + if (bookmarkedArticles.contains(articleData.getId())) { + articleData.setBookmarked(true); + } + }); + } + private void fillExtraInfo(String id, User user, ArticleData articleData) { articleData.setFavorited(articleFavoritesReadService.isUserFavorite(user.getId(), id)); + articleData.setBookmarked(articleBookmarksReadService.isUserBookmark(user.getId(), id)); articleData.setFavoritesCount(articleFavoritesReadService.articleFavoriteCount(id)); articleData .getProfileData() diff --git a/src/main/java/io/spring/application/data/ArticleData.java b/src/main/java/io/spring/application/data/ArticleData.java index 3d3c947e2..bb2290980 100644 --- a/src/main/java/io/spring/application/data/ArticleData.java +++ b/src/main/java/io/spring/application/data/ArticleData.java @@ -18,6 +18,7 @@ public class ArticleData implements io.spring.application.Node { private String description; private String body; private boolean favorited; + private boolean bookmarked; private int favoritesCount; private DateTime createdAt; private DateTime updatedAt; diff --git a/src/main/java/io/spring/graphql/ArticleDatafetcher.java b/src/main/java/io/spring/graphql/ArticleDatafetcher.java index 37c82939a..a8181be56 100644 --- a/src/main/java/io/spring/graphql/ArticleDatafetcher.java +++ b/src/main/java/io/spring/graphql/ArticleDatafetcher.java @@ -374,6 +374,7 @@ private Article buildArticleResult(ArticleData articleData) { .createdAt(ISODateTimeFormat.dateTime().withZoneUTC().print(articleData.getCreatedAt())) .description(articleData.getDescription()) .favorited(articleData.isFavorited()) + .bookmarked(articleData.isBookmarked()) .favoritesCount(articleData.getFavoritesCount()) .slug(articleData.getSlug()) .tagList(articleData.getTagList()) diff --git a/src/main/resources/schema/schema.graphqls b/src/main/resources/schema/schema.graphqls index a3f6be557..c964497bf 100644 --- a/src/main/resources/schema/schema.graphqls +++ b/src/main/resources/schema/schema.graphqls @@ -51,6 +51,7 @@ type Article { createdAt: String! description: String! favorited: Boolean! + bookmarked: Boolean! favoritesCount: Int! slug: String! tagList: [String], diff --git a/src/test/java/io/spring/TestHelper.java b/src/test/java/io/spring/TestHelper.java index dcd57071c..bd281321c 100644 --- a/src/test/java/io/spring/TestHelper.java +++ b/src/test/java/io/spring/TestHelper.java @@ -18,6 +18,7 @@ public static ArticleData articleDataFixture(String seed, User user) { "desc " + seed, "body " + seed, false, + false, 0, now, now, @@ -33,6 +34,7 @@ public static ArticleData getArticleDataFromArticleAndUser(Article article, User article.getDescription(), article.getBody(), false, + false, 0, article.getCreatedAt(), article.getUpdatedAt(), diff --git a/src/test/java/io/spring/api/ArticleApiTest.java b/src/test/java/io/spring/api/ArticleApiTest.java index df2ebe755..9f0318087 100644 --- a/src/test/java/io/spring/api/ArticleApiTest.java +++ b/src/test/java/io/spring/api/ArticleApiTest.java @@ -74,9 +74,35 @@ public void should_read_article_success() throws Exception { .statusCode(200) .body("article.slug", equalTo(slug)) .body("article.body", equalTo(articleData.getBody())) + .body("article.bookmarked", equalTo(false)) .body("article.createdAt", equalTo(ISODateTimeFormat.dateTime().withZoneUTC().print(time))); } + @Test + public void should_read_article_with_bookmarked_flag() throws Exception { + String slug = "test-new-article"; + Article article = + new Article( + "Test New Article", + "Desc", + "Body", + Arrays.asList("java", "spring", "jpg"), + user.getId(), + new DateTime()); + ArticleData articleData = TestHelper.getArticleDataFromArticleAndUser(article, user); + articleData.setBookmarked(true); + + when(articleQueryService.findBySlug(eq(slug), eq(user))).thenReturn(Optional.of(articleData)); + + given() + .header("Authorization", "Token " + token) + .when() + .get("/articles/{slug}", slug) + .then() + .statusCode(200) + .body("article.bookmarked", equalTo(true)); + } + @Test public void should_404_if_article_not_found() throws Exception { when(articleQueryService.findBySlug(anyString(), any())).thenReturn(Optional.empty()); @@ -140,6 +166,7 @@ public void should_get_403_if_not_author_to_update_article() throws Exception { article.getDescription(), article.getBody(), false, + false, 0, time, time, diff --git a/src/test/java/io/spring/api/ArticleFavoriteApiTest.java b/src/test/java/io/spring/api/ArticleFavoriteApiTest.java index 7a609a255..1ec459ca8 100644 --- a/src/test/java/io/spring/api/ArticleFavoriteApiTest.java +++ b/src/test/java/io/spring/api/ArticleFavoriteApiTest.java @@ -58,6 +58,7 @@ public void setUp() throws Exception { article.getDescription(), article.getBody(), true, + false, 1, article.getCreatedAt(), article.getUpdatedAt(), diff --git a/src/test/java/io/spring/api/ArticlesApiTest.java b/src/test/java/io/spring/api/ArticlesApiTest.java index 18948417d..505ba9aab 100644 --- a/src/test/java/io/spring/api/ArticlesApiTest.java +++ b/src/test/java/io/spring/api/ArticlesApiTest.java @@ -62,6 +62,7 @@ public void should_create_article_success() throws Exception { description, body, false, + false, 0, new DateTime(), new DateTime(), @@ -131,6 +132,7 @@ public void should_get_error_message_with_duplicated_title() { description, body, false, + false, 0, new DateTime(), new DateTime(), diff --git a/src/test/java/io/spring/api/ListArticleApiTest.java b/src/test/java/io/spring/api/ListArticleApiTest.java index 032850bce..7e24f9ef8 100644 --- a/src/test/java/io/spring/api/ListArticleApiTest.java +++ b/src/test/java/io/spring/api/ListArticleApiTest.java @@ -3,6 +3,7 @@ import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; import static io.spring.TestHelper.articleDataFixture; import static java.util.Arrays.asList; +import static org.hamcrest.core.IsEqual.equalTo; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @@ -12,6 +13,7 @@ import io.spring.application.ArticleQueryService; import io.spring.application.Page; import io.spring.application.article.ArticleCommandService; +import io.spring.application.data.ArticleData; import io.spring.application.data.ArticleDataList; import io.spring.core.article.ArticleRepository; import org.junit.jupiter.api.BeforeEach; @@ -42,13 +44,20 @@ public void setUp() throws Exception { @Test public void should_get_default_article_list() throws Exception { - ArticleDataList articleDataList = - new ArticleDataList( - asList(articleDataFixture("1", user), articleDataFixture("2", user)), 2); + ArticleData bookmarked = articleDataFixture("1", user); + bookmarked.setBookmarked(true); + ArticleData notBookmarked = articleDataFixture("2", user); + ArticleDataList articleDataList = new ArticleDataList(asList(bookmarked, notBookmarked), 2); when(articleQueryService.findRecentArticles( eq(null), eq(null), eq(null), eq(new Page(0, 20)), eq(null))) .thenReturn(articleDataList); - RestAssuredMockMvc.when().get("/articles").prettyPeek().then().statusCode(200); + RestAssuredMockMvc.when() + .get("/articles") + .prettyPeek() + .then() + .statusCode(200) + .body("articles[0].bookmarked", equalTo(true)) + .body("articles[1].bookmarked", equalTo(false)); } @Test @@ -58,9 +67,10 @@ public void should_get_feeds_401_without_login() throws Exception { @Test public void should_get_feeds_success() throws Exception { + ArticleData bookmarked = articleDataFixture("1", user); + bookmarked.setBookmarked(true); ArticleDataList articleDataList = - new ArticleDataList( - asList(articleDataFixture("1", user), articleDataFixture("2", user)), 2); + new ArticleDataList(asList(bookmarked, articleDataFixture("2", user)), 2); when(articleQueryService.findUserFeed(eq(user), eq(new Page(0, 20)))) .thenReturn(articleDataList); @@ -70,6 +80,8 @@ public void should_get_feeds_success() throws Exception { .get("/articles/feed") .prettyPeek() .then() - .statusCode(200); + .statusCode(200) + .body("articles[0].bookmarked", equalTo(true)) + .body("articles[1].bookmarked", equalTo(false)); } } diff --git a/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java b/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java index 96229376c..2ade51d89 100644 --- a/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java +++ b/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java @@ -10,29 +10,35 @@ import io.spring.application.data.ArticleDataList; import io.spring.core.article.Article; import io.spring.core.article.ArticleRepository; +import io.spring.core.bookmark.ArticleBookmark; +import io.spring.core.bookmark.ArticleBookmarkRepository; import io.spring.core.favorite.ArticleFavorite; import io.spring.core.favorite.ArticleFavoriteRepository; import io.spring.core.user.FollowRelation; import io.spring.core.user.User; import io.spring.core.user.UserRepository; import io.spring.infrastructure.DbTestBase; +import io.spring.infrastructure.repository.MyBatisArticleBookmarkRepository; import io.spring.infrastructure.repository.MyBatisArticleFavoriteRepository; import io.spring.infrastructure.repository.MyBatisArticleRepository; import io.spring.infrastructure.repository.MyBatisUserRepository; import java.util.Arrays; import java.util.Optional; +import javax.sql.DataSource; import org.joda.time.DateTime; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; @Import({ ArticleQueryService.class, MyBatisUserRepository.class, MyBatisArticleRepository.class, - MyBatisArticleFavoriteRepository.class + MyBatisArticleFavoriteRepository.class, + MyBatisArticleBookmarkRepository.class }) public class ArticleQueryServiceTest extends DbTestBase { @Autowired private ArticleQueryService queryService; @@ -43,11 +49,26 @@ public class ArticleQueryServiceTest extends DbTestBase { @Autowired private ArticleFavoriteRepository articleFavoriteRepository; + @Autowired private ArticleBookmarkRepository articleBookmarkRepository; + + @Autowired private DataSource dataSource; + private User user; private Article article; @BeforeEach public void setUp() { + // The test profile only runs Flyway V1 (spring.flyway.target=1), so the V3 + // article_bookmarks table is not present. Create it here so the bookmarked + // read-model flag can be exercised against the real DB. + new JdbcTemplate(dataSource) + .execute( + "create table if not exists article_bookmarks (" + + "article_id varchar(255) not null," + + "user_id varchar(255) not null," + + "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP," + + "primary key (article_id, user_id))"); + user = new User("aisensiy@gmail.com", "aisensiy", "123", "", ""); userRepository.save(user); article = @@ -83,6 +104,117 @@ public void should_get_article_with_right_favorite_and_favorite_count() { Assertions.assertTrue(articleData.isFavorited()); } + @Test + public void should_default_bookmarked_to_false_when_not_bookmarked() { + Optional optional = queryService.findById(article.getId(), user); + Assertions.assertTrue(optional.isPresent()); + Assertions.assertFalse(optional.get().isBookmarked()); + } + + @Test + public void should_default_bookmarked_to_false_for_anonymous_user() { + articleBookmarkRepository.save(new ArticleBookmark(article.getId(), user.getId())); + + Optional optional = queryService.findById(article.getId(), null); + Assertions.assertTrue(optional.isPresent()); + Assertions.assertFalse(optional.get().isBookmarked()); + } + + @Test + public void should_get_article_with_bookmarked_true_when_user_bookmarked() { + articleBookmarkRepository.save(new ArticleBookmark(article.getId(), user.getId())); + + ArticleData byId = queryService.findById(article.getId(), user).orElseThrow(); + Assertions.assertTrue(byId.isBookmarked()); + + ArticleData bySlug = queryService.findBySlug(article.getSlug(), user).orElseThrow(); + Assertions.assertTrue(bySlug.isBookmarked()); + } + + @Test + public void should_flip_bookmarked_flag_when_bookmark_added_and_removed() { + Assertions.assertFalse( + queryService.findById(article.getId(), user).orElseThrow().isBookmarked()); + + ArticleBookmark bookmark = new ArticleBookmark(article.getId(), user.getId()); + articleBookmarkRepository.save(bookmark); + Assertions.assertTrue( + queryService.findById(article.getId(), user).orElseThrow().isBookmarked()); + + articleBookmarkRepository.remove(bookmark); + Assertions.assertFalse( + queryService.findById(article.getId(), user).orElseThrow().isBookmarked()); + } + + @Test + public void should_keep_bookmarked_only_for_current_user() { + User anotherUser = new User("other@test.com", "other", "123", "", ""); + userRepository.save(anotherUser); + articleBookmarkRepository.save(new ArticleBookmark(article.getId(), anotherUser.getId())); + + Assertions.assertTrue( + queryService.findById(article.getId(), anotherUser).orElseThrow().isBookmarked()); + Assertions.assertFalse( + queryService.findById(article.getId(), user).orElseThrow().isBookmarked()); + } + + @Test + public void should_keep_bookmarked_independent_of_favorited() { + articleBookmarkRepository.save(new ArticleBookmark(article.getId(), user.getId())); + + ArticleData articleData = queryService.findById(article.getId(), user).orElseThrow(); + Assertions.assertTrue(articleData.isBookmarked()); + Assertions.assertFalse(articleData.isFavorited()); + } + + @Test + public void should_set_bookmarked_flag_in_article_list() { + articleBookmarkRepository.save(new ArticleBookmark(article.getId(), user.getId())); + + ArticleDataList recentArticles = + queryService.findRecentArticles(null, null, null, new Page(), user); + ArticleData articleData = + recentArticles.getArticleDatas().stream() + .filter(a -> a.getId().equals(article.getId())) + .findFirst() + .orElseThrow(); + Assertions.assertTrue(articleData.isBookmarked()); + } + + @Test + public void should_set_bookmarked_flag_in_article_list_by_cursor() { + articleBookmarkRepository.save(new ArticleBookmark(article.getId(), user.getId())); + + CursorPager recentArticles = + queryService.findRecentArticlesWithCursor( + null, null, null, new CursorPageParameter<>(null, 20, Direction.NEXT), user); + ArticleData articleData = + recentArticles.getData().stream() + .filter(a -> a.getId().equals(article.getId())) + .findFirst() + .orElseThrow(); + Assertions.assertTrue(articleData.isBookmarked()); + } + + @Test + public void should_set_bookmarked_flag_in_user_feed() { + User author = new User("author@test.com", "author", "123", "", ""); + userRepository.save(author); + Article authoredArticle = + new Article("feed article", "desc", "body", Arrays.asList("test"), author.getId()); + articleRepository.save(authoredArticle); + userRepository.saveRelation(new FollowRelation(user.getId(), author.getId())); + articleBookmarkRepository.save(new ArticleBookmark(authoredArticle.getId(), user.getId())); + + ArticleDataList feed = queryService.findUserFeed(user, new Page()); + ArticleData articleData = + feed.getArticleDatas().stream() + .filter(a -> a.getId().equals(authoredArticle.getId())) + .findFirst() + .orElseThrow(); + Assertions.assertTrue(articleData.isBookmarked()); + } + @Test public void should_get_default_article_list() { Article anotherArticle = diff --git a/src/test/java/io/spring/graphql/ArticleDatafetcherTest.java b/src/test/java/io/spring/graphql/ArticleDatafetcherTest.java new file mode 100644 index 000000000..f8824b6e8 --- /dev/null +++ b/src/test/java/io/spring/graphql/ArticleDatafetcherTest.java @@ -0,0 +1,103 @@ +package io.spring.graphql; + +import static io.spring.TestHelper.articleDataFixture; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import graphql.execution.DataFetcherResult; +import io.spring.application.ArticleQueryService; +import io.spring.application.CursorPager; +import io.spring.application.CursorPager.Direction; +import io.spring.application.data.ArticleData; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.graphql.types.Article; +import io.spring.graphql.types.ArticlesConnection; +import java.util.Arrays; +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class ArticleDatafetcherTest { + private ArticleQueryService articleQueryService; + private UserRepository userRepository; + private ArticleDatafetcher articleDatafetcher; + private MockedStatic securityUtil; + private User user; + + @BeforeEach + public void setUp() { + articleQueryService = Mockito.mock(ArticleQueryService.class); + userRepository = Mockito.mock(UserRepository.class); + articleDatafetcher = new ArticleDatafetcher(articleQueryService, userRepository); + user = new User("user@example.com", "user", "123", "", ""); + securityUtil = mockStatic(SecurityUtil.class); + securityUtil.when(SecurityUtil::getCurrentUser).thenReturn(Optional.of(user)); + } + + @AfterEach + public void tearDown() { + securityUtil.close(); + } + + @Test + public void should_expose_bookmarked_true_on_single_article() { + ArticleData articleData = articleDataFixture("1", user); + articleData.setBookmarked(true); + when(articleQueryService.findBySlug(eq(articleData.getSlug()), eq(user))) + .thenReturn(Optional.of(articleData)); + + DataFetcherResult
result = articleDatafetcher.findArticleBySlug(articleData.getSlug()); + + Assertions.assertTrue(result.getData().getBookmarked()); + } + + @Test + public void should_expose_bookmarked_false_for_anonymous_user() { + securityUtil.when(SecurityUtil::getCurrentUser).thenReturn(Optional.empty()); + ArticleData articleData = articleDataFixture("1", user); + when(articleQueryService.findBySlug(eq(articleData.getSlug()), eq(null))) + .thenReturn(Optional.of(articleData)); + + DataFetcherResult
result = articleDatafetcher.findArticleBySlug(articleData.getSlug()); + + Assertions.assertFalse(result.getData().getBookmarked()); + } + + @Test + public void should_expose_bookmarked_flag_in_article_list() { + ArticleData bookmarked = articleDataFixture("1", user); + bookmarked.setBookmarked(true); + ArticleData notBookmarked = articleDataFixture("2", user); + CursorPager pager = + new CursorPager<>(Arrays.asList(bookmarked, notBookmarked), Direction.NEXT, false); + when(articleQueryService.findRecentArticlesWithCursor(any(), any(), any(), any(), eq(user))) + .thenReturn(pager); + + DataFetcherResult result = + articleDatafetcher.getArticles(10, null, null, null, null, null, null, null); + + Assertions.assertTrue(result.getData().getEdges().get(0).getNode().getBookmarked()); + Assertions.assertFalse(result.getData().getEdges().get(1).getNode().getBookmarked()); + } + + @Test + public void should_expose_bookmarked_flag_in_feed() { + ArticleData bookmarked = articleDataFixture("1", user); + bookmarked.setBookmarked(true); + CursorPager pager = + new CursorPager<>(Arrays.asList(bookmarked), Direction.NEXT, false); + when(articleQueryService.findUserFeedWithCursor(eq(user), any())).thenReturn(pager); + + DataFetcherResult result = + articleDatafetcher.getFeed(10, null, null, null, null); + + Assertions.assertTrue(result.getData().getEdges().get(0).getNode().getBookmarked()); + } +}