From 0d3ce8ceae83e3d48cca615e3b959c410cf1827e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 04:39:03 +0000 Subject: [PATCH] Add JUnit 5 tests for 5 least-covered service/API classes - ArticleCommandServiceTest: tests create/update article flows - CommentQueryServiceCursorTest: tests cursor pagination, following status - CustomizeExceptionHandlerTest: tests all exception handler methods - UserServiceTest: tests user creation and update operations - CursorPagerTest: tests pagination direction, cursors, edge cases Coverage improved from 33% to 37% instruction coverage. New tests: 42 test methods covering happy path, error, and edge cases. --- .../CustomizeExceptionHandlerTest.java | 186 +++++++++++++++++ .../spring/application/CursorPagerTest.java | 148 +++++++++++++ .../article/ArticleCommandServiceTest.java | 139 +++++++++++++ .../CommentQueryServiceCursorTest.java | 195 ++++++++++++++++++ .../application/user/UserServiceTest.java | 163 +++++++++++++++ 5 files changed, 831 insertions(+) create mode 100644 src/test/java/io/spring/api/exception/CustomizeExceptionHandlerTest.java create mode 100644 src/test/java/io/spring/application/CursorPagerTest.java create mode 100644 src/test/java/io/spring/application/article/ArticleCommandServiceTest.java create mode 100644 src/test/java/io/spring/application/comment/CommentQueryServiceCursorTest.java create mode 100644 src/test/java/io/spring/application/user/UserServiceTest.java diff --git a/src/test/java/io/spring/api/exception/CustomizeExceptionHandlerTest.java b/src/test/java/io/spring/api/exception/CustomizeExceptionHandlerTest.java new file mode 100644 index 000000000..93efa2bfb --- /dev/null +++ b/src/test/java/io/spring/api/exception/CustomizeExceptionHandlerTest.java @@ -0,0 +1,186 @@ +package io.spring.api.exception; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Path; +import javax.validation.metadata.ConstraintDescriptor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.web.context.request.WebRequest; + +public class CustomizeExceptionHandlerTest { + + private CustomizeExceptionHandler handler; + private WebRequest webRequest; + + @BeforeEach + public void setUp() { + handler = new CustomizeExceptionHandler(); + webRequest = mock(WebRequest.class); + } + + @Test + public void should_handle_invalid_request_exception() { + Errors errors = new BeanPropertyBindingResult(new Object(), "testObject"); + ((BeanPropertyBindingResult) errors) + .addError(new FieldError("testObject", "fieldName", "must not be blank")); + + InvalidRequestException exception = new InvalidRequestException(errors); + + ResponseEntity response = handler.handleInvalidRequest(exception, webRequest); + + Assertions.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, response.getStatusCode()); + Assertions.assertNotNull(response.getBody()); + } + + @Test + public void should_handle_invalid_request_with_multiple_field_errors() { + Errors errors = new BeanPropertyBindingResult(new Object(), "testObject"); + ((BeanPropertyBindingResult) errors) + .addError(new FieldError("testObject", "email", "must be valid email")); + ((BeanPropertyBindingResult) errors) + .addError(new FieldError("testObject", "username", "must not be blank")); + + InvalidRequestException exception = new InvalidRequestException(errors); + + ResponseEntity response = handler.handleInvalidRequest(exception, webRequest); + + Assertions.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, response.getStatusCode()); + } + + @Test + @SuppressWarnings("unchecked") + public void should_handle_invalid_authentication_exception() { + InvalidAuthenticationException exception = new InvalidAuthenticationException(); + + ResponseEntity response = handler.handleInvalidAuthentication(exception, webRequest); + + Assertions.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, response.getStatusCode()); + Assertions.assertNotNull(response.getBody()); + Map body = (Map) response.getBody(); + Assertions.assertEquals("invalid email or password", body.get("message")); + } + + @Test + @SuppressWarnings("unchecked") + public void should_handle_constraint_violation_exception_simple_path() { + ConstraintViolation violation = mock(ConstraintViolation.class); + Path path = mock(Path.class); + when(path.toString()).thenReturn("fieldName"); + when(violation.getPropertyPath()).thenReturn(path); + when(violation.getMessage()).thenReturn("must not be blank"); + when(violation.getRootBeanClass()).thenReturn((Class) Object.class); + + ConstraintDescriptor descriptor = mock(ConstraintDescriptor.class); + Annotation annotation = mock(Annotation.class); + when(annotation.annotationType()).thenReturn((Class) Override.class); + when(descriptor.getAnnotation()).thenReturn(annotation); + doReturn(descriptor).when(violation).getConstraintDescriptor(); + + Set> violations = new HashSet<>(); + violations.add(violation); + ConstraintViolationException ex = new ConstraintViolationException(violations); + + ErrorResource result = handler.handleConstraintViolation(ex, webRequest); + + Assertions.assertNotNull(result); + List fieldErrors = result.getFieldErrors(); + Assertions.assertEquals(1, fieldErrors.size()); + Assertions.assertEquals("fieldName", fieldErrors.get(0).getField()); + Assertions.assertEquals("must not be blank", fieldErrors.get(0).getMessage()); + } + + @Test + @SuppressWarnings("unchecked") + public void should_handle_constraint_violation_exception_nested_path() { + ConstraintViolation violation = mock(ConstraintViolation.class); + Path path = mock(Path.class); + when(path.toString()).thenReturn("createArticle.newArticleParam.title"); + when(violation.getPropertyPath()).thenReturn(path); + when(violation.getMessage()).thenReturn("can't be empty"); + when(violation.getRootBeanClass()).thenReturn((Class) Object.class); + + ConstraintDescriptor descriptor = mock(ConstraintDescriptor.class); + Annotation annotation = mock(Annotation.class); + when(annotation.annotationType()).thenReturn((Class) Override.class); + when(descriptor.getAnnotation()).thenReturn(annotation); + doReturn(descriptor).when(violation).getConstraintDescriptor(); + + Set> violations = new HashSet<>(); + violations.add(violation); + ConstraintViolationException ex = new ConstraintViolationException(violations); + + ErrorResource result = handler.handleConstraintViolation(ex, webRequest); + + Assertions.assertNotNull(result); + List fieldErrors = result.getFieldErrors(); + Assertions.assertEquals(1, fieldErrors.size()); + Assertions.assertEquals("title", fieldErrors.get(0).getField()); + } + + @Test + @SuppressWarnings("unchecked") + public void should_handle_constraint_violation_with_multiple_violations() { + ConstraintViolation violation1 = mock(ConstraintViolation.class); + Path path1 = mock(Path.class); + when(path1.toString()).thenReturn("method.param.email"); + when(violation1.getPropertyPath()).thenReturn(path1); + when(violation1.getMessage()).thenReturn("invalid email"); + when(violation1.getRootBeanClass()).thenReturn((Class) Object.class); + + ConstraintDescriptor descriptor1 = mock(ConstraintDescriptor.class); + Annotation annotation1 = mock(Annotation.class); + when(annotation1.annotationType()).thenReturn((Class) Override.class); + when(descriptor1.getAnnotation()).thenReturn(annotation1); + doReturn(descriptor1).when(violation1).getConstraintDescriptor(); + + ConstraintViolation violation2 = mock(ConstraintViolation.class); + Path path2 = mock(Path.class); + when(path2.toString()).thenReturn("method.param.username"); + when(violation2.getPropertyPath()).thenReturn(path2); + when(violation2.getMessage()).thenReturn("already taken"); + when(violation2.getRootBeanClass()).thenReturn((Class) Object.class); + + ConstraintDescriptor descriptor2 = mock(ConstraintDescriptor.class); + Annotation annotation2 = mock(Annotation.class); + when(annotation2.annotationType()).thenReturn((Class) Override.class); + when(descriptor2.getAnnotation()).thenReturn(annotation2); + doReturn(descriptor2).when(violation2).getConstraintDescriptor(); + + Set> violations = new HashSet<>(); + violations.add(violation1); + violations.add(violation2); + ConstraintViolationException ex = new ConstraintViolationException(violations); + + ErrorResource result = handler.handleConstraintViolation(ex, webRequest); + + Assertions.assertNotNull(result); + Assertions.assertEquals(2, result.getFieldErrors().size()); + } + + @Test + public void should_handle_invalid_request_with_no_field_errors() { + Errors errors = new BeanPropertyBindingResult(new Object(), "testObject"); + + InvalidRequestException exception = new InvalidRequestException(errors); + + ResponseEntity response = handler.handleInvalidRequest(exception, webRequest); + + Assertions.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, response.getStatusCode()); + } +} diff --git a/src/test/java/io/spring/application/CursorPagerTest.java b/src/test/java/io/spring/application/CursorPagerTest.java new file mode 100644 index 000000000..839f1acc8 --- /dev/null +++ b/src/test/java/io/spring/application/CursorPagerTest.java @@ -0,0 +1,148 @@ +package io.spring.application; + +import io.spring.application.CursorPager.Direction; +import io.spring.application.data.CommentData; +import io.spring.application.data.ProfileData; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import org.joda.time.DateTime; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class CursorPagerTest { + + private ProfileData profileData = new ProfileData("id", "user", "bio", "image", false); + + private CommentData createComment(String id, DateTime createdAt) { + return new CommentData(id, "body", "articleId", createdAt, createdAt, profileData); + } + + @Test + public void should_set_next_true_when_has_extra_in_next_direction() { + DateTime now = new DateTime(); + CommentData comment1 = createComment("c1", now); + CommentData comment2 = createComment("c2", now.plusMinutes(1)); + + CursorPager pager = + new CursorPager<>(Arrays.asList(comment1, comment2), Direction.NEXT, true); + + Assertions.assertTrue(pager.hasNext()); + Assertions.assertFalse(pager.hasPrevious()); + } + + @Test + public void should_set_next_false_when_no_extra_in_next_direction() { + DateTime now = new DateTime(); + CommentData comment1 = createComment("c1", now); + + CursorPager pager = + new CursorPager<>(Arrays.asList(comment1), Direction.NEXT, false); + + Assertions.assertFalse(pager.hasNext()); + Assertions.assertFalse(pager.hasPrevious()); + } + + @Test + public void should_set_previous_true_when_has_extra_in_prev_direction() { + DateTime now = new DateTime(); + CommentData comment1 = createComment("c1", now); + + CursorPager pager = + new CursorPager<>(Arrays.asList(comment1), Direction.PREV, true); + + Assertions.assertFalse(pager.hasNext()); + Assertions.assertTrue(pager.hasPrevious()); + } + + @Test + public void should_set_previous_false_when_no_extra_in_prev_direction() { + DateTime now = new DateTime(); + CommentData comment1 = createComment("c1", now); + + CursorPager pager = + new CursorPager<>(Arrays.asList(comment1), Direction.PREV, false); + + Assertions.assertFalse(pager.hasNext()); + Assertions.assertFalse(pager.hasPrevious()); + } + + @Test + public void should_return_null_start_cursor_when_data_empty() { + CursorPager pager = + new CursorPager<>(Collections.emptyList(), Direction.NEXT, false); + + Assertions.assertNull(pager.getStartCursor()); + } + + @Test + public void should_return_null_end_cursor_when_data_empty() { + CursorPager pager = + new CursorPager<>(Collections.emptyList(), Direction.NEXT, false); + + Assertions.assertNull(pager.getEndCursor()); + } + + @Test + public void should_return_start_cursor_from_first_element() { + DateTime now = new DateTime(); + CommentData comment1 = createComment("c1", now); + CommentData comment2 = createComment("c2", now.plusMinutes(1)); + + CursorPager pager = + new CursorPager<>(Arrays.asList(comment1, comment2), Direction.NEXT, false); + + Assertions.assertNotNull(pager.getStartCursor()); + Assertions.assertEquals(String.valueOf(now.getMillis()), pager.getStartCursor().toString()); + } + + @Test + public void should_return_end_cursor_from_last_element() { + DateTime now = new DateTime(); + DateTime later = now.plusMinutes(5); + CommentData comment1 = createComment("c1", now); + CommentData comment2 = createComment("c2", later); + + CursorPager pager = + new CursorPager<>(Arrays.asList(comment1, comment2), Direction.NEXT, false); + + Assertions.assertNotNull(pager.getEndCursor()); + Assertions.assertEquals(String.valueOf(later.getMillis()), pager.getEndCursor().toString()); + } + + @Test + public void should_return_same_cursor_for_single_element() { + DateTime now = new DateTime(); + CommentData comment1 = createComment("c1", now); + + CursorPager pager = + new CursorPager<>(Arrays.asList(comment1), Direction.NEXT, false); + + Assertions.assertEquals(pager.getStartCursor().toString(), pager.getEndCursor().toString()); + } + + @Test + public void should_get_data_list() { + DateTime now = new DateTime(); + CommentData comment1 = createComment("c1", now); + CommentData comment2 = createComment("c2", now.plusMinutes(1)); + + CursorPager pager = + new CursorPager<>(Arrays.asList(comment1, comment2), Direction.NEXT, false); + + Assertions.assertEquals(2, pager.getData().size()); + Assertions.assertEquals("c1", pager.getData().get(0).getId()); + Assertions.assertEquals("c2", pager.getData().get(1).getId()); + } + + @Test + public void should_handle_empty_list_with_prev_direction() { + CursorPager pager = new CursorPager<>(new ArrayList<>(), Direction.PREV, false); + + Assertions.assertTrue(pager.getData().isEmpty()); + Assertions.assertFalse(pager.hasNext()); + Assertions.assertFalse(pager.hasPrevious()); + Assertions.assertNull(pager.getStartCursor()); + Assertions.assertNull(pager.getEndCursor()); + } +} diff --git a/src/test/java/io/spring/application/article/ArticleCommandServiceTest.java b/src/test/java/io/spring/application/article/ArticleCommandServiceTest.java new file mode 100644 index 000000000..d17d424b5 --- /dev/null +++ b/src/test/java/io/spring/application/article/ArticleCommandServiceTest.java @@ -0,0 +1,139 @@ +package io.spring.application.article; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import io.spring.core.user.User; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ArticleCommandServiceTest { + + @Mock private ArticleRepository articleRepository; + + @InjectMocks private ArticleCommandService articleCommandService; + + private User user; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + user = new User("test@test.com", "testuser", "password", "bio", "image"); + } + + @Test + public void should_create_article_success() { + NewArticleParam param = + NewArticleParam.builder() + .title("Test Title") + .description("Test Description") + .body("Test Body") + .tagList(Arrays.asList("java", "spring")) + .build(); + + Article article = articleCommandService.createArticle(param, user); + + Assertions.assertNotNull(article); + Assertions.assertEquals("Test Title", article.getTitle()); + Assertions.assertEquals("Test Description", article.getDescription()); + Assertions.assertEquals("Test Body", article.getBody()); + Assertions.assertEquals(user.getId(), article.getUserId()); + Assertions.assertEquals("test-title", article.getSlug()); + verify(articleRepository).save(any(Article.class)); + } + + @Test + public void should_create_article_with_empty_tag_list() { + NewArticleParam param = + NewArticleParam.builder() + .title("No Tags Article") + .description("Description") + .body("Body") + .tagList(Collections.emptyList()) + .build(); + + Article article = articleCommandService.createArticle(param, user); + + Assertions.assertNotNull(article); + Assertions.assertTrue(article.getTags().isEmpty()); + verify(articleRepository).save(any(Article.class)); + } + + @Test + public void should_create_article_with_null_tag_list() { + NewArticleParam param = + NewArticleParam.builder() + .title("Null Tags Article") + .description("Description") + .body("Body") + .tagList(null) + .build(); + + Assertions.assertThrows( + NullPointerException.class, () -> articleCommandService.createArticle(param, user)); + } + + @Test + public void should_update_article_title() { + Article article = + new Article("Old Title", "Old Desc", "Old Body", Arrays.asList("java"), user.getId()); + + UpdateArticleParam updateParam = new UpdateArticleParam("New Title", "", ""); + + Article updated = articleCommandService.updateArticle(article, updateParam); + + Assertions.assertEquals("New Title", updated.getTitle()); + Assertions.assertEquals("new-title", updated.getSlug()); + verify(articleRepository).save(any(Article.class)); + } + + @Test + public void should_update_article_body_and_description() { + Article article = + new Article("Title", "Old Desc", "Old Body", Arrays.asList("java"), user.getId()); + + UpdateArticleParam updateParam = new UpdateArticleParam("", "New Body", "New Desc"); + + Article updated = articleCommandService.updateArticle(article, updateParam); + + Assertions.assertEquals("New Body", updated.getBody()); + Assertions.assertEquals("New Desc", updated.getDescription()); + verify(articleRepository).save(any(Article.class)); + } + + @Test + public void should_update_article_all_fields() { + Article article = + new Article("Old Title", "Old Desc", "Old Body", Arrays.asList("java"), user.getId()); + + UpdateArticleParam updateParam = + new UpdateArticleParam("New Title", "New Body", "New Description"); + + Article updated = articleCommandService.updateArticle(article, updateParam); + + Assertions.assertEquals("New Title", updated.getTitle()); + Assertions.assertEquals("New Body", updated.getBody()); + Assertions.assertEquals("New Description", updated.getDescription()); + verify(articleRepository).save(any(Article.class)); + } + + @Test + public void should_preserve_article_id_after_update() { + Article article = new Article("Title", "Desc", "Body", Arrays.asList("java"), user.getId()); + String originalId = article.getId(); + + UpdateArticleParam updateParam = new UpdateArticleParam("Updated Title", "", ""); + + Article updated = articleCommandService.updateArticle(article, updateParam); + + Assertions.assertEquals(originalId, updated.getId()); + } +} diff --git a/src/test/java/io/spring/application/comment/CommentQueryServiceCursorTest.java b/src/test/java/io/spring/application/comment/CommentQueryServiceCursorTest.java new file mode 100644 index 000000000..0ed7759ce --- /dev/null +++ b/src/test/java/io/spring/application/comment/CommentQueryServiceCursorTest.java @@ -0,0 +1,195 @@ +package io.spring.application.comment; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import io.spring.application.CommentQueryService; +import io.spring.application.CursorPageParameter; +import io.spring.application.CursorPager; +import io.spring.application.CursorPager.Direction; +import io.spring.application.data.CommentData; +import io.spring.application.data.ProfileData; +import io.spring.core.user.User; +import io.spring.infrastructure.mybatis.readservice.CommentReadService; +import io.spring.infrastructure.mybatis.readservice.UserRelationshipQueryService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class CommentQueryServiceCursorTest { + + @Mock private CommentReadService commentReadService; + + @Mock private UserRelationshipQueryService userRelationshipQueryService; + + @InjectMocks private CommentQueryService commentQueryService; + + private User user; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + user = new User("test@test.com", "testuser", "password", "bio", "image"); + } + + @Test + public void should_return_empty_cursor_pager_when_no_comments() { + CursorPageParameter page = new CursorPageParameter<>(null, 20, Direction.NEXT); + when(commentReadService.findByArticleIdWithCursor(eq("article1"), any())) + .thenReturn(Collections.emptyList()); + + CursorPager result = + commentQueryService.findByArticleIdWithCursor("article1", user, page); + + Assertions.assertTrue(result.getData().isEmpty()); + Assertions.assertFalse(result.hasNext()); + } + + @Test + public void should_return_comments_with_cursor_for_next_direction() { + CursorPageParameter page = new CursorPageParameter<>(null, 20, Direction.NEXT); + ProfileData profileData = new ProfileData(user.getId(), user.getUsername(), "", "", false); + DateTime now = new DateTime(); + + CommentData comment1 = new CommentData("c1", "body1", "article1", now, now, profileData); + + when(commentReadService.findByArticleIdWithCursor(eq("article1"), any())) + .thenReturn(new ArrayList<>(Arrays.asList(comment1))); + when(userRelationshipQueryService.followingAuthors(eq(user.getId()), anyList())) + .thenReturn(Collections.emptySet()); + + CursorPager result = + commentQueryService.findByArticleIdWithCursor("article1", user, page); + + Assertions.assertEquals(1, result.getData().size()); + Assertions.assertFalse(result.hasNext()); + } + + @Test + public void should_set_has_next_when_extra_results() { + CursorPageParameter page = new CursorPageParameter<>(null, 1, Direction.NEXT); + ProfileData profileData = new ProfileData(user.getId(), user.getUsername(), "", "", false); + DateTime now = new DateTime(); + + CommentData comment1 = new CommentData("c1", "body1", "article1", now, now, profileData); + CommentData comment2 = + new CommentData( + "c2", "body2", "article1", now.plusMinutes(1), now.plusMinutes(1), profileData); + + when(commentReadService.findByArticleIdWithCursor(eq("article1"), any())) + .thenReturn(new ArrayList<>(Arrays.asList(comment1, comment2))); + when(userRelationshipQueryService.followingAuthors(eq(user.getId()), anyList())) + .thenReturn(Collections.emptySet()); + + CursorPager result = + commentQueryService.findByArticleIdWithCursor("article1", user, page); + + Assertions.assertEquals(1, result.getData().size()); + Assertions.assertTrue(result.hasNext()); + } + + @Test + public void should_reverse_comments_for_prev_direction() { + CursorPageParameter page = new CursorPageParameter<>(null, 20, Direction.PREV); + ProfileData profileData = new ProfileData(user.getId(), user.getUsername(), "", "", false); + DateTime now = new DateTime(); + + CommentData comment1 = new CommentData("c1", "body1", "article1", now, now, profileData); + CommentData comment2 = + new CommentData( + "c2", "body2", "article1", now.plusMinutes(1), now.plusMinutes(1), profileData); + + when(commentReadService.findByArticleIdWithCursor(eq("article1"), any())) + .thenReturn(new ArrayList<>(Arrays.asList(comment1, comment2))); + when(userRelationshipQueryService.followingAuthors(eq(user.getId()), anyList())) + .thenReturn(Collections.emptySet()); + + CursorPager result = + commentQueryService.findByArticleIdWithCursor("article1", user, page); + + Assertions.assertEquals(2, result.getData().size()); + Assertions.assertEquals("c2", result.getData().get(0).getId()); + Assertions.assertEquals("c1", result.getData().get(1).getId()); + } + + @Test + public void should_set_following_for_followed_authors_with_cursor() { + CursorPageParameter page = new CursorPageParameter<>(null, 20, Direction.NEXT); + ProfileData profileData = new ProfileData("author1", "authoruser", "", "", false); + DateTime now = new DateTime(); + + CommentData comment1 = new CommentData("c1", "body1", "article1", now, now, profileData); + + when(commentReadService.findByArticleIdWithCursor(eq("article1"), any())) + .thenReturn(new ArrayList<>(Arrays.asList(comment1))); + when(userRelationshipQueryService.followingAuthors(eq(user.getId()), anyList())) + .thenReturn(new HashSet<>(Arrays.asList("author1"))); + + CursorPager result = + commentQueryService.findByArticleIdWithCursor("article1", user, page); + + Assertions.assertTrue(result.getData().get(0).getProfileData().isFollowing()); + } + + @Test + public void should_not_query_following_when_user_is_null_with_cursor() { + CursorPageParameter page = new CursorPageParameter<>(null, 20, Direction.NEXT); + ProfileData profileData = new ProfileData("author1", "authoruser", "", "", false); + DateTime now = new DateTime(); + + CommentData comment1 = new CommentData("c1", "body1", "article1", now, now, profileData); + + when(commentReadService.findByArticleIdWithCursor(eq("article1"), any())) + .thenReturn(new ArrayList<>(Arrays.asList(comment1))); + + CursorPager result = + commentQueryService.findByArticleIdWithCursor("article1", null, page); + + Assertions.assertFalse(result.getData().get(0).getProfileData().isFollowing()); + } + + @Test + public void should_find_comment_by_id_returns_empty_when_not_found() { + when(commentReadService.findById("nonexistent")).thenReturn(null); + + Optional result = commentQueryService.findById("nonexistent", user); + + Assertions.assertFalse(result.isPresent()); + } + + @Test + public void should_find_by_article_id_returns_empty_when_no_comments() { + when(commentReadService.findByArticleId("article1")).thenReturn(Collections.emptyList()); + + List result = commentQueryService.findByArticleId("article1", user); + + Assertions.assertTrue(result.isEmpty()); + } + + @Test + public void should_find_by_article_id_sets_following_for_followed_authors() { + ProfileData profileData = new ProfileData("author1", "authoruser", "", "", false); + DateTime now = new DateTime(); + CommentData comment1 = new CommentData("c1", "body1", "article1", now, now, profileData); + + when(commentReadService.findByArticleId("article1")).thenReturn(Arrays.asList(comment1)); + when(userRelationshipQueryService.followingAuthors(eq(user.getId()), anyList())) + .thenReturn(new HashSet<>(Arrays.asList("author1"))); + + List result = commentQueryService.findByArticleId("article1", user); + + Assertions.assertTrue(result.get(0).getProfileData().isFollowing()); + } +} diff --git a/src/test/java/io/spring/application/user/UserServiceTest.java b/src/test/java/io/spring/application/user/UserServiceTest.java new file mode 100644 index 000000000..d59938238 --- /dev/null +++ b/src/test/java/io/spring/application/user/UserServiceTest.java @@ -0,0 +1,163 @@ +package io.spring.application.user; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.security.crypto.password.PasswordEncoder; + +public class UserServiceTest { + + @Mock private UserRepository userRepository; + + @Mock private PasswordEncoder passwordEncoder; + + private UserService userService; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + when(passwordEncoder.encode(any(CharSequence.class))) + .thenAnswer(i -> "encoded_" + i.getArgument(0)); + userService = new UserService(userRepository, "https://default-image.png", passwordEncoder); + } + + @Test + public void should_create_user_success() { + RegisterParam registerParam = new RegisterParam("test@test.com", "testuser", "password"); + + User user = userService.createUser(registerParam); + + Assertions.assertNotNull(user); + Assertions.assertEquals("test@test.com", user.getEmail()); + Assertions.assertEquals("testuser", user.getUsername()); + Assertions.assertEquals("encoded_password", user.getPassword()); + Assertions.assertEquals("https://default-image.png", user.getImage()); + verify(userRepository).save(any(User.class)); + } + + @Test + public void should_create_user_with_empty_bio() { + RegisterParam registerParam = new RegisterParam("user@email.com", "newuser", "pass123"); + + User user = userService.createUser(registerParam); + + Assertions.assertEquals("", user.getBio()); + verify(userRepository).save(any(User.class)); + } + + @Test + public void should_update_user_email() { + User existingUser = new User("old@test.com", "testuser", "password", "bio", "image"); + UpdateUserParam updateParam = + UpdateUserParam.builder() + .email("new@test.com") + .username("") + .password("") + .bio("") + .image("") + .build(); + UpdateUserCommand command = new UpdateUserCommand(existingUser, updateParam); + + userService.updateUser(command); + + Assertions.assertEquals("new@test.com", existingUser.getEmail()); + verify(userRepository).save(existingUser); + } + + @Test + public void should_update_user_username() { + User existingUser = new User("test@test.com", "olduser", "password", "bio", "image"); + UpdateUserParam updateParam = + UpdateUserParam.builder() + .email("") + .username("newuser") + .password("") + .bio("") + .image("") + .build(); + UpdateUserCommand command = new UpdateUserCommand(existingUser, updateParam); + + userService.updateUser(command); + + Assertions.assertEquals("newuser", existingUser.getUsername()); + verify(userRepository).save(existingUser); + } + + @Test + public void should_update_user_bio_and_image() { + User existingUser = new User("test@test.com", "testuser", "password", "", ""); + UpdateUserParam updateParam = + UpdateUserParam.builder() + .email("") + .username("") + .password("") + .bio("new bio") + .image("new-image.png") + .build(); + UpdateUserCommand command = new UpdateUserCommand(existingUser, updateParam); + + userService.updateUser(command); + + Assertions.assertEquals("new bio", existingUser.getBio()); + Assertions.assertEquals("new-image.png", existingUser.getImage()); + verify(userRepository).save(existingUser); + } + + @Test + public void should_update_user_all_fields() { + User existingUser = new User("old@test.com", "olduser", "oldpass", "old bio", "old-image.png"); + UpdateUserParam updateParam = + UpdateUserParam.builder() + .email("new@test.com") + .username("newuser") + .password("newpass") + .bio("new bio") + .image("new-image.png") + .build(); + UpdateUserCommand command = new UpdateUserCommand(existingUser, updateParam); + + userService.updateUser(command); + + Assertions.assertEquals("new@test.com", existingUser.getEmail()); + Assertions.assertEquals("newuser", existingUser.getUsername()); + Assertions.assertEquals("newpass", existingUser.getPassword()); + Assertions.assertEquals("new bio", existingUser.getBio()); + Assertions.assertEquals("new-image.png", existingUser.getImage()); + verify(userRepository).save(existingUser); + } + + @Test + public void should_not_update_user_fields_when_empty() { + User existingUser = + new User("keep@test.com", "keepuser", "keeppass", "keep bio", "keep-image.png"); + UpdateUserParam updateParam = + UpdateUserParam.builder().email("").username("").password("").bio("").image("").build(); + UpdateUserCommand command = new UpdateUserCommand(existingUser, updateParam); + + userService.updateUser(command); + + Assertions.assertEquals("keep@test.com", existingUser.getEmail()); + Assertions.assertEquals("keepuser", existingUser.getUsername()); + Assertions.assertEquals("keeppass", existingUser.getPassword()); + Assertions.assertEquals("keep bio", existingUser.getBio()); + Assertions.assertEquals("keep-image.png", existingUser.getImage()); + verify(userRepository).save(existingUser); + } + + @Test + public void should_encode_password_when_creating_user() { + RegisterParam registerParam = new RegisterParam("test@test.com", "testuser", "rawpassword"); + + User user = userService.createUser(registerParam); + + Assertions.assertEquals("encoded_rawpassword", user.getPassword()); + } +}