diff --git a/module-domain/src/main/java/com/depromeet/notification/domain/FollowLog.java b/module-domain/src/main/java/com/depromeet/notification/domain/FollowLog.java index 6c512579..2d14ee3c 100644 --- a/module-domain/src/main/java/com/depromeet/notification/domain/FollowLog.java +++ b/module-domain/src/main/java/com/depromeet/notification/domain/FollowLog.java @@ -29,4 +29,8 @@ public FollowLog( this.createdAt = createdAt; this.hasRead = hasRead; } + + public void read() { + this.hasRead = true; + } } diff --git a/module-domain/src/main/java/com/depromeet/notification/domain/ReactionLog.java b/module-domain/src/main/java/com/depromeet/notification/domain/ReactionLog.java index 8e4dac96..a242b275 100644 --- a/module-domain/src/main/java/com/depromeet/notification/domain/ReactionLog.java +++ b/module-domain/src/main/java/com/depromeet/notification/domain/ReactionLog.java @@ -23,4 +23,8 @@ public ReactionLog( this.createdAt = createdAt; this.hasRead = hasRead; } + + public void read() { + this.hasRead = true; + } } diff --git a/module-domain/src/test/java/com/depromeet/mock/notification/FakeFollowLogRepository.java b/module-domain/src/test/java/com/depromeet/mock/notification/FakeFollowLogRepository.java new file mode 100644 index 00000000..3edd1173 --- /dev/null +++ b/module-domain/src/test/java/com/depromeet/mock/notification/FakeFollowLogRepository.java @@ -0,0 +1,104 @@ +package com.depromeet.mock.notification; + +import com.depromeet.friend.domain.Friend; +import com.depromeet.member.domain.Member; +import com.depromeet.notification.domain.FollowLog; +import com.depromeet.notification.port.out.FollowLogPersistencePort; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; + +public class FakeFollowLogRepository implements FollowLogPersistencePort { + private Long followLogAutoGeneratedId = 0L; + private Map followLogDatabase = new HashMap<>(); + private Map friendDatabase = new HashMap<>(); + + @BeforeEach + void setUp() { + Member member1 = Member.builder().id(100L).build(); + Member member2 = Member.builder().id(101L).build(); + Friend friend = + Friend.builder() + .id(1L) + .member(member1) + .following(member2) + .createdAt(LocalDateTime.now()) + .build(); + + friendDatabase.put(friend.getId(), friend); + } + + @Override + public FollowLog save(FollowLog followLog) { + if (followLog.getId() == null) { + FollowLog newFollowLog = + FollowLog.builder() + .id(++followLogAutoGeneratedId) + .follower(followLog.getFollower()) + .receiver(followLog.getReceiver()) + .type(followLog.getType()) + .hasRead(followLog.isHasRead()) + .createdAt(LocalDateTime.now()) + .build(); + followLogDatabase.put(followLogAutoGeneratedId, newFollowLog); + return newFollowLog; + } + followLogDatabase.replace(followLog.getId(), followLog); + return followLog; + } + + @Override + public List findByMemberIdAndCursorCreatedAt( + Long memberId, LocalDateTime cursorCreatedAt) { + return followLogDatabase.values().stream() + .filter(followLog -> followLog.getReceiver().getId().equals(memberId)) + .filter( + followLog -> + cursorCreatedAt == null + || followLog.getCreatedAt().isAfter(cursorCreatedAt)) + .collect(Collectors.toList()); + } + + @Override + public void updateAllAsRead(Long memberId) { + followLogDatabase.values().stream() + .filter(followLog -> followLog.getReceiver().getId().equals(memberId)) + .forEach(FollowLog::read); + } + + @Override + public Long countUnread(Long memberId) { + return followLogDatabase.values().stream() + .filter(followLog -> followLog.getReceiver().getId().equals(memberId)) + .filter(followLog -> !followLog.isHasRead()) + .count(); + } + + @Override + public void deleteAllByMemberId(Long memberId) { + followLogDatabase + .values() + .removeIf(followLog -> followLog.getReceiver().getId().equals(memberId)); + } + + @Override + public boolean existsByReceiverIdAndFollowerId(Long receiverId, Long followerId) { + return followLogDatabase.values().stream() + .anyMatch( + followLog -> + followLog.getReceiver().getId().equals(receiverId) + && followLog.getFollower().getId().equals(followerId)); + } + + @Override + public List getFriendList(Long memberId, List followerIds) { + return friendDatabase.values().stream() + .filter(friend -> friend.getMember().getId().equals(memberId)) + .filter(friend -> followerIds.contains(friend.getFollowing().getId())) + .map(friend -> friend.getFollowing().getId()) + .toList(); + } +} diff --git a/module-domain/src/test/java/com/depromeet/mock/notification/FakeReactionLogRepository.java b/module-domain/src/test/java/com/depromeet/mock/notification/FakeReactionLogRepository.java new file mode 100644 index 00000000..6ff09140 --- /dev/null +++ b/module-domain/src/test/java/com/depromeet/mock/notification/FakeReactionLogRepository.java @@ -0,0 +1,66 @@ +package com.depromeet.mock.notification; + +import com.depromeet.notification.domain.ReactionLog; +import com.depromeet.notification.port.out.ReactionLogPersistencePort; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class FakeReactionLogRepository implements ReactionLogPersistencePort { + private Long reactionLogAutoGeneratedId = 0L; + private Map reactionLogDatabase = new HashMap<>(); + + @Override + public ReactionLog save(ReactionLog reactionLog) { + if (reactionLog.getId() == null) { + ReactionLog newReactionLog = + ReactionLog.builder() + .id(++reactionLogAutoGeneratedId) + .receiver(reactionLog.getReceiver()) + .reaction(reactionLog.getReaction()) + .hasRead(reactionLog.isHasRead()) + .createdAt(LocalDateTime.now()) + .build(); + reactionLogDatabase.put(reactionLogAutoGeneratedId, newReactionLog); + return newReactionLog; + } + reactionLogDatabase.replace(reactionLog.getId(), reactionLog); + return reactionLog; + } + + @Override + public List findByMemberIdAndCursorCreatedAt( + Long memberId, LocalDateTime cursorCreatedAt) { + return reactionLogDatabase.values().stream() + .filter(reactionLog -> reactionLog.getReceiver().getId().equals(memberId)) + .filter( + reactionLog -> + cursorCreatedAt == null + || reactionLog.getCreatedAt().isBefore(cursorCreatedAt)) + .limit(11) + .sorted((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt())) + .collect(Collectors.toList()); + } + + @Override + public void updateAllAsRead(Long memberId) { + reactionLogDatabase.values().stream() + .filter(reactionLog -> reactionLog.getReceiver().getId().equals(memberId)) + .forEach(ReactionLog::read); + } + + @Override + public Long countUnread(Long memberId) { + return reactionLogDatabase.values().stream() + .filter(reactionLog -> reactionLog.getReceiver().getId().equals(memberId)) + .filter(reactionLog -> !reactionLog.isHasRead()) + .count(); + } + + @Override + public void deleteAllByReactionId(List reactionIds) { + reactionIds.forEach(reactionLogDatabase.keySet()::remove); + } +} diff --git a/module-domain/src/test/java/com/depromeet/service/notification/FollowLogServiceTest.java b/module-domain/src/test/java/com/depromeet/service/notification/FollowLogServiceTest.java new file mode 100644 index 00000000..d91a90ce --- /dev/null +++ b/module-domain/src/test/java/com/depromeet/service/notification/FollowLogServiceTest.java @@ -0,0 +1,100 @@ +package com.depromeet.service.notification; + +import static org.assertj.core.api.Assertions.*; + +import com.depromeet.fixture.domain.member.MemberFixture; +import com.depromeet.friend.port.out.persistence.FriendPersistencePort; +import com.depromeet.member.domain.Member; +import com.depromeet.mock.friend.FakeFriendRepository; +import com.depromeet.mock.notification.FakeFollowLogRepository; +import com.depromeet.notification.domain.FollowLog; +import com.depromeet.notification.domain.FollowType; +import com.depromeet.notification.event.FollowLogEvent; +import com.depromeet.notification.port.out.FollowLogPersistencePort; +import com.depromeet.notification.service.FollowLogService; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class FollowLogServiceTest { + private FollowLogPersistencePort followLogPersistencePort; + private FriendPersistencePort friendPersistencePort; + private FollowLogService followLogService; + private Member member1; + private Member member2; + + @BeforeEach + void init() { + followLogPersistencePort = new FakeFollowLogRepository(); + friendPersistencePort = new FakeFriendRepository(); + + followLogService = new FollowLogService(followLogPersistencePort, friendPersistencePort); + + member1 = MemberFixture.make(1L, "USER"); + member2 = MemberFixture.make(2L, "USER"); + + FollowLogEvent event = FollowLogEvent.of(member1, member2); + followLogService.save(event); + } + + @Test + void 팔로우_로그를_저장합니다() throws Exception { + // given + Member member3 = MemberFixture.make(3L, "USER"); + FollowLogEvent event = FollowLogEvent.of(member1, member3); + + // when + followLogService.save(event); + + // then + List followLogs = followLogService.getFollowLogs(member1.getId(), null); + assertThat(followLogs.size()).isEqualTo(2); + assertThat(followLogs.getLast().getReceiver().getId()).isEqualTo(1L); + assertThat(followLogs.getLast().getFollower().getId()).isEqualTo(3L); + } + + @Test + public void 팔로우_로그를_조회합니다() throws Exception { + // when + List followLogs = followLogService.getFollowLogs(member1.getId(), null); + + // then + assertThat(followLogs.size()).isEqualTo(1); + assertThat(followLogs.getFirst().getReceiver().getId()).isEqualTo(1L); + assertThat(followLogs.getFirst().getFollower().getId()).isEqualTo(2L); + assertThat(followLogs.getFirst().getType()).isEqualTo(FollowType.FOLLOW); + } + + @Test + public void 미확인_팔로우_로그카운트를_조회합니다() throws Exception { + // when + Long unreadFollowLogCount = followLogService.getUnreadFollowLogCount(1L); + + // then + assertThat(unreadFollowLogCount).isEqualTo(1L); + } + + @Test + public void 팔로우_확인_및_로그카운트_변경을_확인합니다() throws Exception { + // given + followLogService.markAsReadFollowLogs(1L); + + // when + Long unreadFollowLogCount = followLogService.getUnreadFollowLogCount(1L); + + // then + assertThat(unreadFollowLogCount).isEqualTo(0L); + } + + @Test + public void 팔로우_로그_삭제를_수행합니다() throws Exception { + // given + followLogService.deleteAllByMemberId(1L); + + // when + List followLogs = followLogService.getFollowLogs(1L, null); + + // then + assertThat(followLogs.size()).isEqualTo(0); + } +} diff --git a/module-domain/src/test/java/com/depromeet/service/notification/ReactionLogServiceTest.java b/module-domain/src/test/java/com/depromeet/service/notification/ReactionLogServiceTest.java new file mode 100644 index 00000000..6ddf73f8 --- /dev/null +++ b/module-domain/src/test/java/com/depromeet/service/notification/ReactionLogServiceTest.java @@ -0,0 +1,98 @@ +package com.depromeet.service.notification; + +import static org.assertj.core.api.Assertions.*; + +import com.depromeet.fixture.domain.member.MemberFixture; +import com.depromeet.fixture.domain.memory.MemoryFixture; +import com.depromeet.fixture.domain.reaction.ReactionFixture; +import com.depromeet.member.domain.Member; +import com.depromeet.memory.domain.Memory; +import com.depromeet.mock.notification.FakeReactionLogRepository; +import com.depromeet.notification.domain.ReactionLog; +import com.depromeet.notification.event.ReactionLogEvent; +import com.depromeet.notification.port.out.ReactionLogPersistencePort; +import com.depromeet.notification.service.ReactionLogService; +import com.depromeet.reaction.domain.Reaction; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ReactionLogServiceTest { + private ReactionLogPersistencePort reactionLogPersistencePort; + private ReactionLogService reactionLogService; + private Reaction reaction; + private Member member1; + private Member member2; + + @BeforeEach + void init() { + reactionLogPersistencePort = new FakeReactionLogRepository(); + reactionLogService = new ReactionLogService(reactionLogPersistencePort); + member1 = MemberFixture.make(1L, "USER"); + member2 = MemberFixture.make(2L, "USER"); + Memory memory = MemoryFixture.make(member2, null, null, null); + reaction = ReactionFixture.make(member1, memory); + + var event = ReactionLogEvent.of(member2, reaction); + reactionLogService.save(event); + } + + @Test + public void 응원_로그를_저장합니다() throws Exception { + // given + var reactionLogEvent = ReactionLogEvent.of(member2, reaction); + + // when + reactionLogService.save(reactionLogEvent); + + // then + List reactionLogs = reactionLogService.getReactionsLogs(member2.getId(), null); + assertThat(reactionLogs.size()).isEqualTo(2); + assertThat(reactionLogs.getFirst().getReceiver().getId()).isEqualTo(2L); + assertThat(reactionLogs.getFirst().getReaction().getMember().getId()).isEqualTo(1L); + } + + @Test + public void 응원_로그를_조회합니다() throws Exception { + // when + List reactionLogs = reactionLogService.getReactionsLogs(member2.getId(), null); + + // then + assertThat(reactionLogs.size()).isEqualTo(1); + assertThat(reactionLogs.getFirst().getReceiver().getId()).isEqualTo(2L); + assertThat(reactionLogs.getFirst().getReaction().getMember().getId()).isEqualTo(1L); + } + + @Test + public void 미확인_로그_수를_확인합니다() throws Exception { + // when + Long unreadReactionLogCount = reactionLogService.getUnreadReactionLogCount(2L); + + // then + assertThat(unreadReactionLogCount).isEqualTo(1L); + } + + @Test + public void 로그_조회_및_미확인_로그카운트_변경을_확인합니다() throws Exception { + // given + reactionLogService.markAsReadReactionLogs(2L); + + // when + Long unreadReactionLogCount = reactionLogService.getUnreadReactionLogCount(2L); + + // then + assertThat(unreadReactionLogCount).isEqualTo(0L); + } + + @Test + public void 응원_로그_삭제를_수행합니다() throws Exception { + // given + reactionLogService.deleteAllByReactionId(List.of(1L)); + + // when + List reactionLogs = reactionLogService.getReactionsLogs(member2.getId(), null); + + // then + assertThat(reactionLogs.size()).isEqualTo(0); + } +} diff --git a/module-domain/src/testFixtures/java/com/depromeet/fixture/domain/reaction/ReactionFixture.java b/module-domain/src/testFixtures/java/com/depromeet/fixture/domain/reaction/ReactionFixture.java new file mode 100644 index 00000000..d75b0fe4 --- /dev/null +++ b/module-domain/src/testFixtures/java/com/depromeet/fixture/domain/reaction/ReactionFixture.java @@ -0,0 +1,18 @@ +package com.depromeet.fixture.domain.reaction; + +import com.depromeet.member.domain.Member; +import com.depromeet.memory.domain.Memory; +import com.depromeet.reaction.domain.Reaction; +import java.time.LocalDateTime; + +public class ReactionFixture { + public static Reaction make(Member member, Memory memory) { + return Reaction.builder() + .member(member) + .memory(memory) + .emoji("🦭") + .comment("물개세요?") + .createdAt(LocalDateTime.now()) + .build(); + } +} diff --git a/module-presentation/src/test/java/com/depromeet/notification/api/NotificationControllerTest.java b/module-presentation/src/test/java/com/depromeet/notification/api/NotificationControllerTest.java new file mode 100644 index 00000000..b8d196e3 --- /dev/null +++ b/module-presentation/src/test/java/com/depromeet/notification/api/NotificationControllerTest.java @@ -0,0 +1,47 @@ +package com.depromeet.notification.api; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import com.depromeet.config.ControllerTestConfig; +import com.depromeet.config.mock.WithCustomMockMember; +import com.depromeet.notification.facade.NotificationFacade; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +@WebMvcTest(NotificationController.class) +public class NotificationControllerTest extends ControllerTestConfig { + @MockBean NotificationFacade notificationFacade; + + @Test + @WithCustomMockMember + public void 알림을_조회합니다() throws Exception { + mockMvc.perform(get("/notification")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("NOTIFICATION_1")) + .andExpect(jsonPath("$.message").value("알림 조회에 성공하였습니다")) + .andDo(print()); + } + + @Test + @WithCustomMockMember + public void 알림을_확인처리합니다() throws Exception { + mockMvc.perform(patch("/notification/read")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("NOTIFICATION_2")) + .andExpect(jsonPath("$.message").value("알림을 읽음 처리하였습니다")) + .andDo(print()); + } + + @Test + @WithCustomMockMember + public void 미확인_알림_개수를_조회합니다() throws Exception { + mockMvc.perform(get("/notification/count")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("NOTIFICATION_3")) + .andExpect(jsonPath("$.message").value("읽지 않은 알림 개수 조회에 성공하였습니다")) + .andDo(print()); + } +}