package com.mosquito.project.job; import com.mosquito.project.domain.Activity; import com.mosquito.project.domain.DailyActivityStats; import com.mosquito.project.persistence.entity.DailyActivityStatsEntity; import com.mosquito.project.persistence.repository.DailyActivityStatsRepository; import com.mosquito.project.service.ActivityService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDate; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class StatisticsAggregationJobCompleteTest { @Mock private ActivityService activityService; @Mock private DailyActivityStatsRepository dailyStatsRepository; @InjectMocks private StatisticsAggregationJob job; private LocalDate testDate; @BeforeEach void setUp() { testDate = LocalDate.of(2024, 6, 15); } @Test void shouldAggregateDailyStats_whenActivitiesExist() { Activity activity1 = createActivity(1L, "Activity 1"); Activity activity2 = createActivity(2L, "Activity 2"); List activities = List.of(activity1, activity2); when(activityService.getAllActivities()).thenReturn(activities); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); job.aggregateDailyStats(); verify(activityService, times(1)).getAllActivities(); verify(dailyStatsRepository, times(4)).save(any(DailyActivityStatsEntity.class)); } @Test void shouldHandleEmptyActivityList_whenNoActivities() { when(activityService.getAllActivities()).thenReturn(Collections.emptyList()); job.aggregateDailyStats(); verify(activityService, times(1)).getAllActivities(); verify(dailyStatsRepository, never()).save(any()); } @Test void shouldCreateStatsInValidRange_whenAggregateStatsForActivityCalled() { Activity activity = createActivity(1L, "Test Activity"); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate); assertThat(stats).isNotNull(); assertThat(stats.getActivityId()).isEqualTo(1L); assertThat(stats.getStatDate()).isEqualTo(testDate); assertThat(stats.getViews()).isBetween(1000, 1499); assertThat(stats.getShares()).isBetween(200, 299); assertThat(stats.getNewRegistrations()).isBetween(50, 99); assertThat(stats.getConversions()).isBetween(10, 29); } @Test void shouldSetCorrectActivityId_whenDifferentActivitiesProcessed() { Activity activity1 = createActivity(100L, "Activity 100"); Activity activity2 = createActivity(200L, "Activity 200"); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); DailyActivityStats stats1 = job.aggregateStatsForActivity(activity1, testDate); DailyActivityStats stats2 = job.aggregateStatsForActivity(activity2, testDate); assertThat(stats1.getActivityId()).isEqualTo(100L); assertThat(stats2.getActivityId()).isEqualTo(200L); } @Test void shouldSetCorrectDate_whenDifferentDatesProcessed() { Activity activity = createActivity(1L, "Test"); LocalDate date1 = LocalDate.of(2024, 1, 1); LocalDate date2 = LocalDate.of(2024, 12, 31); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); DailyActivityStats stats1 = job.aggregateStatsForActivity(activity, date1); DailyActivityStats stats2 = job.aggregateStatsForActivity(activity, date2); assertThat(stats1.getStatDate()).isEqualTo(date1); assertThat(stats2.getStatDate()).isEqualTo(date2); } @Test void shouldUpdateExistingEntity_whenStatsAlreadyExist() { Activity activity = createActivity(1L, "Test"); DailyActivityStatsEntity existingEntity = new DailyActivityStatsEntity(); existingEntity.setId(100L); existingEntity.setActivityId(1L); existingEntity.setStatDate(testDate); existingEntity.setViews(500); existingEntity.setShares(100); existingEntity.setNewRegistrations(30); existingEntity.setConversions(5); when(dailyStatsRepository.findByActivityIdAndStatDate(1L, testDate)) .thenReturn(Optional.of(existingEntity)); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); job.aggregateStatsForActivity(activity, testDate); ArgumentCaptor captor = ArgumentCaptor.forClass(DailyActivityStatsEntity.class); verify(dailyStatsRepository, atLeastOnce()).save(captor.capture()); DailyActivityStatsEntity savedEntity = captor.getValue(); assertThat(savedEntity.getId()).isEqualTo(100L); assertThat(savedEntity.getViews()).isBetween(1000, 1499); } @Test void shouldCreateNewEntity_whenStatsDoNotExist() { Activity activity = createActivity(1L, "Test"); when(dailyStatsRepository.findByActivityIdAndStatDate(1L, testDate)) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); job.aggregateStatsForActivity(activity, testDate); ArgumentCaptor captor = ArgumentCaptor.forClass(DailyActivityStatsEntity.class); verify(dailyStatsRepository, atLeastOnce()).save(captor.capture()); DailyActivityStatsEntity savedEntity = captor.getValue(); assertThat(savedEntity.getId()).isNull(); assertThat(savedEntity.getActivityId()).isEqualTo(1L); } @Test void shouldHandleSingleActivity_whenOnlyOneActivityExists() { Activity activity = createActivity(1L, "Solo Activity"); when(activityService.getAllActivities()).thenReturn(List.of(activity)); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); job.aggregateDailyStats(); verify(dailyStatsRepository, times(2)).save(any(DailyActivityStatsEntity.class)); } @Test void shouldHandleManyActivities_whenLargeActivityList() { List activities = new ArrayList<>(); for (long i = 1; i <= 100; i++) { activities.add(createActivity(i, "Activity " + i)); } when(activityService.getAllActivities()).thenReturn(activities); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); job.aggregateDailyStats(); verify(activityService, times(1)).getAllActivities(); verify(dailyStatsRepository, times(200)).save(any(DailyActivityStatsEntity.class)); } @Test void shouldGenerateNonNegativeStats_whenRandomValuesGenerated() { Activity activity = createActivity(1L, "Test"); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); for (int i = 0; i < 50; i++) { DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate); assertThat(stats.getViews()).isGreaterThanOrEqualTo(1000); assertThat(stats.getShares()).isGreaterThanOrEqualTo(200); assertThat(stats.getNewRegistrations()).isGreaterThanOrEqualTo(50); assertThat(stats.getConversions()).isGreaterThanOrEqualTo(10); } } @Test void shouldStoreStatsInConcurrentMap_whenAggregated() { Activity activity = createActivity(1L, "Test"); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate); assertThat(stats).isNotNull(); } @Test void shouldCallUpsertDailyStats_whenAggregateStatsForActivity() { Activity activity = createActivity(1L, "Test"); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate); assertThat(stats.getActivityId()).isEqualTo(1L); verify(dailyStatsRepository, atLeastOnce()).save(any(DailyActivityStatsEntity.class)); } @Test void shouldUseYesterdayDate_whenAggregateDailyStatsCalled() { when(activityService.getAllActivities()).thenReturn(Collections.emptyList()); job.aggregateDailyStats(); LocalDate yesterday = LocalDate.now().minusDays(1); verify(activityService, times(1)).getAllActivities(); } @Test void shouldHandleActivityWithNullName_whenAggregated() { Activity activity = new Activity(); activity.setId(1L); activity.setName(null); activity.setStartTime(ZonedDateTime.now()); activity.setEndTime(ZonedDateTime.now().plusDays(1)); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate); assertThat(stats.getActivityId()).isEqualTo(1L); assertThat(stats.getStatDate()).isEqualTo(testDate); } @Test void shouldPreserveAllStatFields_whenSavingToRepository() { Activity activity = createActivity(1L, "Test"); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); job.aggregateStatsForActivity(activity, testDate); ArgumentCaptor captor = ArgumentCaptor.forClass(DailyActivityStatsEntity.class); verify(dailyStatsRepository, atLeastOnce()).save(captor.capture()); DailyActivityStatsEntity saved = captor.getValue(); assertThat(saved.getActivityId()).isNotNull(); assertThat(saved.getStatDate()).isNotNull(); assertThat(saved.getViews()).isNotNull(); assertThat(saved.getShares()).isNotNull(); assertThat(saved.getNewRegistrations()).isNotNull(); assertThat(saved.getConversions()).isNotNull(); } @Test void shouldHandleActivityWithZeroId_whenAggregated() { Activity activity = createActivity(0L, "Zero ID Activity"); when(dailyStatsRepository.findByActivityIdAndStatDate(0L, testDate)) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); DailyActivityStats stats = job.aggregateStatsForActivity(activity, testDate); assertThat(stats.getActivityId()).isEqualTo(0L); } @Test void shouldGenerateStatsWithinExpectedRanges_whenMultipleCalls() { Activity activity = createActivity(1L, "Test"); when(dailyStatsRepository.findByActivityIdAndStatDate(any(), any())) .thenReturn(Optional.empty()); when(dailyStatsRepository.save(any(DailyActivityStatsEntity.class))) .thenAnswer(invocation -> invocation.getArgument(0)); List allStats = new ArrayList<>(); for (int i = 0; i < 20; i++) { allStats.add(job.aggregateStatsForActivity(activity, testDate)); } assertThat(allStats) .allMatch(s -> s.getViews() >= 1000 && s.getViews() < 1500) .allMatch(s -> s.getShares() >= 200 && s.getShares() < 300) .allMatch(s -> s.getNewRegistrations() >= 50 && s.getNewRegistrations() < 100) .allMatch(s -> s.getConversions() >= 10 && s.getConversions() < 30); } private Activity createActivity(Long id, String name) { Activity activity = new Activity(); activity.setId(id); activity.setName(name); activity.setStartTime(ZonedDateTime.now()); activity.setEndTime(ZonedDateTime.now().plusDays(1)); return activity; } }