Java tutorial
/******************************************************************************* * Copyright (c) 2016, 2018 Stichting Yona Foundation This Source Code Form is subject to the terms of the Mozilla Public License, * v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. *******************************************************************************/ package nu.yona.server.analysis.service; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anySetOf; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.text.MessageFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.slf4j.LoggerFactory; import org.springframework.data.repository.Repository; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import nu.yona.server.Translator; import nu.yona.server.analysis.entities.Activity; import nu.yona.server.analysis.entities.ActivityRepository; import nu.yona.server.analysis.entities.DayActivity; import nu.yona.server.analysis.entities.DayActivityRepository; import nu.yona.server.analysis.entities.WeekActivity; import nu.yona.server.analysis.entities.WeekActivityRepository; import nu.yona.server.crypto.pubkey.PublicKeyUtil; import nu.yona.server.device.entities.DeviceAnonymized; import nu.yona.server.device.entities.DeviceAnonymized.OperatingSystem; import nu.yona.server.device.entities.DeviceAnonymizedRepository; import nu.yona.server.device.service.DeviceAnonymizedDto; import nu.yona.server.device.service.DeviceService; import nu.yona.server.goals.entities.ActivityCategory; import nu.yona.server.goals.entities.BudgetGoal; import nu.yona.server.goals.entities.Goal; import nu.yona.server.goals.entities.GoalRepository; import nu.yona.server.goals.entities.TimeZoneGoal; import nu.yona.server.goals.service.ActivityCategoryDto; import nu.yona.server.goals.service.ActivityCategoryService; import nu.yona.server.goals.service.GoalDto; import nu.yona.server.goals.service.GoalService; import nu.yona.server.messaging.entities.Message; import nu.yona.server.messaging.entities.MessageDestination; import nu.yona.server.messaging.entities.MessageRepository; import nu.yona.server.messaging.service.MessageService; import nu.yona.server.properties.AnalysisServiceProperties; import nu.yona.server.properties.YonaProperties; import nu.yona.server.subscriptions.entities.UserAnonymized; import nu.yona.server.subscriptions.service.UserAnonymizedDto; import nu.yona.server.subscriptions.service.UserAnonymizedService; import nu.yona.server.test.util.JUnitUtil; import nu.yona.server.util.LockPool; import nu.yona.server.util.TimeUtil; import nu.yona.server.util.TransactionHelper; @RunWith(MockitoJUnitRunner.class) public class AnalysisEngineServiceTest { private final Map<String, Goal> goalMap = new HashMap<>(); @Mock private ActivityCategoryService mockActivityCategoryService; @Mock private ActivityCategoryService.FilterService mockActivityCategoryFilterService; @Mock private UserAnonymizedService mockUserAnonymizedService; @Mock private GoalService mockGoalService; @Mock private MessageService mockMessageService; @Mock private YonaProperties mockYonaProperties; @Mock private ActivityCacheService mockAnalysisEngineCacheService; @Mock private GoalRepository mockGoalRepository; @Mock private MessageRepository mockMessageRepository; @Mock private ActivityRepository mockActivityRepository; @Mock private DayActivityRepository mockDayActivityRepository; @Mock private WeekActivityRepository mockWeekActivityRepository; @Mock private LockPool<UUID> userAnonymizedSynchronizer; @Mock private TransactionHelper transactionHelper; @Mock private DeviceService mockDeviceService; @Mock private DeviceAnonymizedRepository mockDeviceAnonymizedRepository; @Mock private Appender<ILoggingEvent> mockLogAppender; @Mock private ActivityUpdateService mockActivityUpdater; @InjectMocks private final AnalysisEngineService service = new AnalysisEngineService(); private Goal gamblingGoal; private Goal newsGoal; private Goal gamingGoal; private Goal socialGoal; private Goal shoppingGoal; private UUID userAnonId; private UUID deviceAnonId; private DeviceAnonymized deviceAnonEntity; private UserAnonymized userAnonEntity; private UserAnonymizedDto userAnonDto; private ZoneId userAnonZoneId; private DeviceAnonymizedDto deviceAnonDto; @Before public void setUp() { Logger logger = (Logger) LoggerFactory.getLogger(AnalysisEngineService.class); logger.addAppender(mockLogAppender); setUpRepositoryMocks(); LocalDateTime yesterday = TimeUtil.utcNow().minusDays(1).withHour(0).withMinute(1).withSecond(0); gamblingGoal = BudgetGoal.createNoGoInstance(yesterday, ActivityCategory.createInstance(UUID.randomUUID(), usString("gambling"), false, new HashSet<>(Arrays.asList("poker", "lotto")), new HashSet<>(Arrays.asList("Poker App", "Lotto App")), usString("Descr"))); newsGoal = BudgetGoal.createNoGoInstance(yesterday, ActivityCategory.createInstance(UUID.randomUUID(), usString("news"), false, new HashSet<>(Arrays.asList("refdag", "bbc")), Collections.emptySet(), usString("Descr"))); gamingGoal = BudgetGoal.createNoGoInstance(yesterday, ActivityCategory.createInstance(UUID.randomUUID(), usString("gaming"), false, new HashSet<>(Arrays.asList("games")), Collections.emptySet(), usString("Descr"))); socialGoal = TimeZoneGoal.createInstance(yesterday, ActivityCategory.createInstance(UUID.randomUUID(), usString("social"), false, new HashSet<>(Arrays.asList("social")), Collections.emptySet(), usString("Descr")), Collections.emptyList()); shoppingGoal = BudgetGoal .createInstance(yesterday, ActivityCategory.createInstance(UUID.randomUUID(), usString("shopping"), false, new HashSet<>(Arrays.asList("webshop")), Collections.emptySet(), usString("Descr")), 1); goalMap.put("gambling", gamblingGoal); goalMap.put("news", newsGoal); goalMap.put("gaming", gamingGoal); goalMap.put("social", socialGoal); goalMap.put("shopping", shoppingGoal); when(mockYonaProperties.getAnalysisService()).thenReturn(new AnalysisServiceProperties()); when(mockActivityCategoryService.getAllActivityCategories()).thenReturn(getAllActivityCategories()); when(mockActivityCategoryFilterService.getMatchingCategoriesForSmoothwallCategories(anySetOf(String.class))) .thenAnswer(new Answer<Set<ActivityCategoryDto>>() { @Override public Set<ActivityCategoryDto> answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); @SuppressWarnings("unchecked") Set<String> smoothwallCategories = (Set<String>) args[0]; return getAllActivityCategories().stream() .filter(ac -> ac.getSmoothwallCategories().stream() .filter(smoothwallCategories::contains).findAny().isPresent()) .collect(Collectors.toSet()); } }); when(mockActivityCategoryFilterService.getMatchingCategoriesForApp(any(String.class))) .thenAnswer(new Answer<Set<ActivityCategoryDto>>() { @Override public Set<ActivityCategoryDto> answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); String application = (String) args[0]; return getAllActivityCategories().stream() .filter(ac -> ac.getApplications().contains(application)) .collect(Collectors.toSet()); } }); // Set up UserAnonymized instance. MessageDestination anonMessageDestinationEntity = MessageDestination .createInstance(PublicKeyUtil.generateKeyPair().getPublic()); Set<Goal> goals = new HashSet<>(Arrays.asList(gamblingGoal, gamingGoal, socialGoal, shoppingGoal)); deviceAnonEntity = DeviceAnonymized.createInstance(0, OperatingSystem.IOS, "Unknown", 0, Optional.empty()); deviceAnonId = deviceAnonEntity.getId(); userAnonEntity = UserAnonymized.createInstance(anonMessageDestinationEntity, goals); userAnonEntity.addDeviceAnonymized(deviceAnonEntity); userAnonDto = UserAnonymizedDto.createInstance(userAnonEntity); deviceAnonDto = DeviceAnonymizedDto.createInstance(deviceAnonEntity); userAnonId = userAnonDto.getId(); userAnonZoneId = userAnonDto.getTimeZone(); // Stub the UserAnonymizedService to return our user. when(mockUserAnonymizedService.getUserAnonymized(userAnonId)).thenReturn(userAnonDto); when(mockUserAnonymizedService.getUserAnonymizedEntity(userAnonId)).thenReturn(userAnonEntity); // Stub the GoalService to return our goals. when(mockGoalService.getGoalEntityForUserAnonymizedId(userAnonId, gamblingGoal.getId())) .thenReturn(gamblingGoal); when(mockGoalService.getGoalEntityForUserAnonymizedId(userAnonId, gamingGoal.getId())) .thenReturn(gamingGoal); when(mockGoalService.getGoalEntityForUserAnonymizedId(userAnonId, socialGoal.getId())) .thenReturn(socialGoal); when(mockGoalService.getGoalEntityForUserAnonymizedId(userAnonId, shoppingGoal.getId())) .thenReturn(shoppingGoal); // Mock the transaction helper doAnswer(new Answer<Void>() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { invocation.getArgumentAt(0, Runnable.class).run(); return null; } }).when(transactionHelper).executeInNewTransaction(any(Runnable.class)); // Mock the week activity repository when(mockWeekActivityRepository.findOne(any(UUID.class), any(UUID.class), any(LocalDate.class))) .thenAnswer(new Answer<WeekActivity>() { @Override public WeekActivity answer(InvocationOnMock invocation) throws Throwable { Optional<Goal> goal = goalMap.values().stream() .filter(g -> g.getId() == invocation.getArgumentAt(1, UUID.class)).findAny(); if (!goal.isPresent()) { return null; } return goal.get().getWeekActivities().stream().filter( wa -> wa.getStartDate().equals(invocation.getArgumentAt(2, LocalDate.class))) .findAny().orElse(null); } }); // Mock device service and repo when(mockDeviceService.getDeviceAnonymized(userAnonDto, -1)).thenReturn(deviceAnonDto); when(mockDeviceService.getDeviceAnonymized(userAnonDto, deviceAnonId)).thenReturn(deviceAnonDto); when(mockDeviceAnonymizedRepository.getOne(deviceAnonId)).thenReturn(deviceAnonEntity); } private void setUpRepositoryMocks() { JUnitUtil.setUpRepositoryMock(mockGoalRepository); JUnitUtil.setUpRepositoryMock(mockMessageRepository); JUnitUtil.setUpRepositoryMock(mockActivityRepository); JUnitUtil.setUpRepositoryMock(mockDayActivityRepository); Map<Class<?>, Repository<?, ?>> repositoriesMap = new HashMap<>(); repositoriesMap.put(Goal.class, mockGoalRepository); repositoriesMap.put(Message.class, mockMessageRepository); repositoriesMap.put(Activity.class, mockActivityRepository); repositoriesMap.put(DayActivity.class, mockDayActivityRepository); JUnitUtil.setUpRepositoryProviderMock(repositoriesMap); } private Map<Locale, String> usString(String string) { return Collections.singletonMap(Translator.EN_US_LOCALE, string); } private Set<ActivityCategoryDto> getAllActivityCategories() { return goalMap.values().stream().map(goal -> ActivityCategoryDto.createInstance(goal.getActivityCategory())) .collect(Collectors.toSet()); } @Test public void getRelevantSmoothwallCategories_default_containsExpectedItems() { Set<String> result = service.getRelevantSmoothwallCategories(); assertThat(result, containsInAnyOrder("poker", "lotto", "refdag", "bbc", "games", "social", "webshop")); } @Test public void analyze_secondConflictAfterConflictInterval_addActivity() { // Normally there is one conflict message sent. // Set a short conflict interval such that the conflict messages are not aggregated. AnalysisServiceProperties p = new AnalysisServiceProperties(); p.setUpdateSkipWindow("PT0.001S"); p.setConflictInterval("PT0.01S"); when(mockYonaProperties.getAnalysisService()).thenReturn(p); mockExistingActivity(gamblingGoal, now()); // Execute the analysis engine service after a period of inactivity longer than the conflict interval. try { Thread.sleep(11L); } catch (InterruptedException e) { } service.analyze(userAnonId, createNetworkActivityForCategories("lotto")); verifyAddActivity(gamblingGoal); } @Test public void analyze_secondConflictWithinConflictInterval_noUpdate() { mockExistingActivity(gamblingGoal, now()); service.analyze(userAnonId, createNetworkActivityForCategories("lotto")); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), eq(GoalDto.createInstance(gamblingGoal)), any()); } @Test public void analyze_matchingCategory_addActivity() { service.analyze(userAnonId, createNetworkActivityForCategories("lotto")); verifyAddActivity(gamblingGoal); } @Test public void analyze_matchOneCategoryOfMultiple_oneAddActivity() { service.analyze(userAnonId, createNetworkActivityForCategories("refdag", "lotto")); verifyAddActivity(gamblingGoal); } @Test public void analyze_multipleMatchingCategories_multipleAddActivity() { service.analyze(userAnonId, createNetworkActivityForCategories("lotto", "games")); verifyAddActivity(gamblingGoal, gamingGoal); } @Test public void analyze_multipleFollowingCallsWithinConflictInterval_updateTimeLastActivity() { // Normally there is one conflict message sent. // Set update skip window to 0 such that the conflict messages are aggregated immediately. AnalysisServiceProperties p = new AnalysisServiceProperties(); p.setUpdateSkipWindow("PT0S"); when(mockYonaProperties.getAnalysisService()).thenReturn(p); ZonedDateTime timeOfMockedExistingActivity = now(); mockExistingActivity(gamblingGoal, timeOfMockedExistingActivity); service.analyze(userAnonId, createNetworkActivityForCategories("refdag")); // not matching category service.analyze(userAnonId, createNetworkActivityForCategories("lotto")); // matching category service.analyze(userAnonId, createNetworkActivityForCategories("poker")); // matching category service.analyze(userAnonId, createNetworkActivityForCategories("refdag")); // not matching category service.analyze(userAnonId, createNetworkActivityForCategories("poker")); // matching category // Verify that the cache is used to check existing activity verify(mockAnalysisEngineCacheService, times(3)).fetchLastActivityForUser(userAnonId, deviceAnonId, gamblingGoal.getId()); verify(mockActivityUpdater, times(3)).updateTimeLastActivity(any(), eq(GoalDto.createInstance(gamblingGoal)), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).addActivity(any(), any(), any(), any()); } @Test public void analyze_noMatchingCategory_noAddOrUpdateActivity() { service.analyze(userAnonId, createNetworkActivityForCategories("refdag")); verify(mockAnalysisEngineCacheService, never()).fetchLastActivityForUser(eq(userAnonId), eq(deviceAnonId), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); verify(mockActivityUpdater, never()).addActivity(any(), any(), any(), any()); } @Test public void analyze_appActivityOnNewDay_addActivity() { JUnitUtil.skipBefore("Skip shortly after midnight", now(), 0, 15); ZonedDateTime today = now().truncatedTo(ChronoUnit.DAYS); // mock earlier activity at yesterday 23:59:58, // add new activity at today 00:00:01 ZonedDateTime existingActivityTime = today.minusDays(1).withHour(23).withMinute(59).withSecond(58); mockExistingActivity(gamblingGoal, existingActivityTime); ZonedDateTime startTime = today.withHour(0).withMinute(0).withSecond(1); ZonedDateTime endTime = today.withHour(0).withMinute(10); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Poker App", startTime, endTime)); verify(mockActivityUpdater).addActivity(any(), any(), eq(GoalDto.createInstance(gamblingGoal)), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); } @Test public void analyze_unorderedAppActivity_addedOrdered() { JUnitUtil.skipBefore("Skip shortly after midnignt", now(), 0, 10); String app = "Poker App"; ZonedDateTime today = now().truncatedTo(ChronoUnit.DAYS); ZonedDateTime t1 = today.withHour(0).withMinute(0).withSecond(1); ZonedDateTime t2 = t1.plusSeconds(15); ZonedDateTime t3 = t2.plusSeconds(1); ZonedDateTime t4 = t3.plusMinutes(5); service.analyze(userAnonId, deviceAnonId, new AppActivitiesDto(now(), new AppActivitiesDto.Activity[] { new AppActivitiesDto.Activity(app, t3, t4), new AppActivitiesDto.Activity(app, t1, t2) })); ArgumentCaptor<ActivityPayload> activityPayloadCaptor = ArgumentCaptor.forClass(ActivityPayload.class); verify(mockActivityUpdater, times(2)).addActivity(any(), activityPayloadCaptor.capture(), eq(GoalDto.createInstance(gamblingGoal)), any()); List<ActivityPayload> payloads = activityPayloadCaptor.getAllValues(); assertThat(payloads.size(), equalTo(2)); assertThat(payloads.get(0).startTime, equalTo(t1)); assertThat(payloads.get(0).endTime, equalTo(t2)); assertThat(payloads.get(1).startTime, equalTo(t3)); assertThat(payloads.get(1).endTime, equalTo(t4)); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); } @Test public void analyze_appActivityCompletelyPrecedingLastCachedActivity_addActivity() { ZonedDateTime now = now(); JUnitUtil.skipBefore("Skip shortly after midnight", now, 0, 11); ZonedDateTime existingActivityStartTime = now.minusMinutes(4); ZonedDateTime existingActivityEndTime = existingActivityStartTime.plusMinutes(2); mockExistingActivity(gamblingGoal, existingActivityStartTime, existingActivityEndTime, "Poker App"); ZonedDateTime startTime = now.minusMinutes(10); ZonedDateTime endTime = startTime.plusMinutes(5); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Poker App", startTime, endTime)); verify(mockActivityUpdater).addActivity(any(), any(), eq(GoalDto.createInstance(gamblingGoal)), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); } @Test public void analyze_appActivityCompletelyPrecedingLastCachedActivityOverlappingExistingActivity_updateTimeExistingActivity() { ZonedDateTime now = now(); JUnitUtil.skipBefore("Skip shortly after midnight", now, 0, 30); ZonedDateTime existingActivityTimeStartTime = now.minusMinutes(20); ZonedDateTime existingActivityTimeEndTime = existingActivityTimeStartTime.plusMinutes(10); Activity existingActivityOne = createActivity(existingActivityTimeStartTime, existingActivityTimeEndTime, "Poker App"); Activity existingActivityTwo = createActivity(now, now, "Poker App"); mockExistingActivities(gamblingGoal, existingActivityOne, existingActivityTwo); when(mockActivityRepository.findOverlappingOfSameApp(any(DayActivity.class), any(UUID.class), any(UUID.class), any(String.class), any(LocalDateTime.class), any(LocalDateTime.class))) .thenAnswer(new Answer<List<Activity>>() { @Override public List<Activity> answer(InvocationOnMock invocation) throws Throwable { return Arrays.asList(existingActivityOne); } }); // Test an activity ZonedDateTime startTime = existingActivityTimeStartTime.plusMinutes(5); ZonedDateTime endTime = startTime.plusMinutes(7); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Poker App", startTime, endTime)); // Verify that a database lookup was done finding the existing DayActivity to update verify(mockDayActivityRepository).findOne(userAnonId, now.toLocalDate(), gamblingGoal.getId()); verify(mockActivityUpdater).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).addActivity(any(), any(), any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); } @Test public void analyze_networkActivityCompletelyPrecedingLastCachedActivityOverlappingMultipleExistingActivities_updateTimeExistingActivityOnFirstActivityAndLogsWarning() { ZonedDateTime now = now(); JUnitUtil.skipBefore("Skip shortly after midnight", now, 0, 11); DayActivity existingDayActivity = mockExistingActivities(gamblingGoal, createActivity(now.minusMinutes(10), now.minusMinutes(8)), createActivity(now.minusMinutes(1), now)); when(mockActivityRepository.findOverlappingOfSameApp(any(DayActivity.class), any(UUID.class), any(UUID.class), any(String.class), any(LocalDateTime.class), any(LocalDateTime.class))) .thenAnswer(new Answer<List<Activity>>() { @Override public List<Activity> answer(InvocationOnMock invocation) throws Throwable { return existingDayActivity.getActivities().stream().collect(Collectors.toList()); } }); String expectedWarnMessage = MessageFormat.format( "Multiple overlapping network activities found. The payload has start time {0} and end time {1}. The day activity ID is {2} and the activity category ID is {3}. The overlapping activities are: {4}, {5}.", now.minusMinutes(9).toLocalDateTime(), now.minusMinutes(9).toLocalDateTime(), existingDayActivity.getId(), gamblingGoal.getActivityCategory().getId(), existingDayActivity.getActivities().get(0), existingDayActivity.getActivities().get(1)); service.analyze(userAnonId, createNetworkActivityForCategories(now.minusMinutes(9), "poker")); verify(mockActivityUpdater).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).addActivity(any(), any(), any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); List<Activity> activities = existingDayActivity.getActivities(); assertThat(activities.size(), equalTo(2)); assertThat(activities.get(0).getApp(), equalTo(Optional.empty())); assertThat(activities.get(0).getStartTimeAsZonedDateTime(), equalTo(now.minusMinutes(10))); assertThat(activities.get(0).getEndTimeAsZonedDateTime(), equalTo(now.minusMinutes(8))); assertThat(activities.get(1).getApp(), equalTo(Optional.empty())); assertThat(activities.get(1).getStartTimeAsZonedDateTime(), equalTo(now.minusMinutes(1))); assertThat(activities.get(1).getEndTimeAsZonedDateTime(), equalTo(now)); ArgumentCaptor<ILoggingEvent> logEventCaptor = ArgumentCaptor.forClass(ILoggingEvent.class); verify(mockLogAppender).doAppend(logEventCaptor.capture()); assertThat(logEventCaptor.getValue().getLevel(), equalTo(Level.WARN)); assertThat(logEventCaptor.getValue().getFormattedMessage(), equalTo(expectedWarnMessage)); } @Test public void analyze_appActivityCompletelyPrecedingLastCachedActivityOverlappingMultipleExistingActivities_updateTimeExistingActivityOnFirstActivityAndLogsWarning() { ZonedDateTime now = now(); JUnitUtil.skipBefore("Skip shortly after midnight", now, 0, 10); DayActivity existingDayActivity = mockExistingActivities(gamblingGoal, createActivity(now.minusMinutes(10), now.minusMinutes(8), "Lotto App"), createActivity(now.minusMinutes(7), now.minusMinutes(5), "Lotto App"), createActivity(now, now, "Lotto App")); when(mockActivityRepository.findOverlappingOfSameApp(any(DayActivity.class), any(UUID.class), any(UUID.class), any(String.class), any(LocalDateTime.class), any(LocalDateTime.class))) .thenAnswer(new Answer<List<Activity>>() { @Override public List<Activity> answer(InvocationOnMock invocation) throws Throwable { return existingDayActivity.getActivities().stream().collect(Collectors.toList()); } }); String expectedWarnMessage = MessageFormat.format( "Multiple overlapping app activities of ''Lotto App'' found. The payload has start time {0} and end time {1}. The day activity ID is {2} and the activity category ID is {3}. The overlapping activities are: {4}, {5}, {6}.", now.minusMinutes(9).toLocalDateTime(), now.minusMinutes(2).toLocalDateTime(), existingDayActivity.getId(), gamblingGoal.getActivityCategory().getId(), existingDayActivity.getActivities().get(0), existingDayActivity.getActivities().get(1), existingDayActivity.getActivities().get(2)); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Lotto App", now.minusMinutes(9), now.minusMinutes(2))); verify(mockActivityUpdater).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).addActivity(any(), any(), any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); ArgumentCaptor<ILoggingEvent> logEventCaptor = ArgumentCaptor.forClass(ILoggingEvent.class); verify(mockLogAppender).doAppend(logEventCaptor.capture()); assertThat(logEventCaptor.getValue().getLevel(), equalTo(Level.WARN)); assertThat(logEventCaptor.getValue().getFormattedMessage(), equalTo(expectedWarnMessage)); } @Test public void analyze_appActivityOverlappingLastCachedActivityBeginAndEnd_updateTimeLastActivity() { ZonedDateTime now = now(); JUnitUtil.skipBefore("Skip shortly after midnight", now, 0, 20); ZonedDateTime existingActivityStartTime = now.minusMinutes(5); ZonedDateTime existingActivityEndTime = now.minusSeconds(15); mockExistingActivity(gamblingGoal, existingActivityStartTime, existingActivityEndTime, "Poker App"); ZonedDateTime startTime = now.minusMinutes(10); ZonedDateTime endTime = now; service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Poker App", startTime, endTime)); verify(mockActivityUpdater).updateTimeLastActivity(any(), eq(GoalDto.createInstance(gamblingGoal)), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).addActivity(any(), any(), any(), any()); } @Test public void analyze_appActivityOverlappingLastCachedActivityBeginOnly_updateTimeLastActivity() { ZonedDateTime now = now(); JUnitUtil.skipBefore("Skip shortly after midnight", now, 0, 11); ZonedDateTime existingActivityStartTime = now.minusMinutes(5); ZonedDateTime existingActivityEndTime = now.minusSeconds(15); mockExistingActivity(gamblingGoal, existingActivityStartTime, existingActivityEndTime, "Poker App"); ZonedDateTime startTime = now.minusMinutes(10); ZonedDateTime endTime = existingActivityEndTime.minusMinutes(2); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Poker App", startTime, endTime)); verify(mockActivityUpdater).updateTimeLastActivity(any(), eq(GoalDto.createInstance(gamblingGoal)), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).addActivity(any(), any(), any(), any()); } @Test public void analyze_appActivityPreviousDayPrecedingCachedDayActivity_addActivity() { ZonedDateTime now = now(); ZonedDateTime yesterdayNoon = now.minusDays(1).withHour(12).withMinute(0).withSecond(0); mockExistingActivity(gamblingGoal, now); ZonedDateTime startTime = yesterdayNoon; ZonedDateTime endTime = yesterdayNoon.plusMinutes(10); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Poker App", startTime, endTime)); // Verify that a database lookup was done for yesterday verify(mockDayActivityRepository).findOne(userAnonId, yesterdayNoon.toLocalDate(), gamblingGoal.getId()); verify(mockActivityUpdater).addActivity(any(), any(), eq(GoalDto.createInstance(gamblingGoal)), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); } @Test public void analyze_crossDayAppActivity_twoDayActivitiesCreated() { ZonedDateTime endTime = now(); JUnitUtil.skipBefore("Skip shortly after midnight", endTime, 0, 5); ZonedDateTime startTime = endTime.minusDays(1); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Poker App", startTime, endTime)); ArgumentCaptor<ActivityPayload> activityPayloadCaptor = ArgumentCaptor.forClass(ActivityPayload.class); verify(mockActivityUpdater, times(2)).addActivity(any(), activityPayloadCaptor.capture(), eq(GoalDto.createInstance(gamblingGoal)), any()); List<ActivityPayload> payloads = activityPayloadCaptor.getAllValues(); assertThat(payloads.size(), equalTo(2)); assertThat(payloads.get(0).startTime, equalTo(startTime)); assertThat(payloads.get(0).endTime, equalTo(endTime.truncatedTo(ChronoUnit.DAYS))); assertThat(payloads.get(1).startTime, equalTo(endTime.truncatedTo(ChronoUnit.DAYS))); assertThat(payloads.get(1).endTime, equalTo(endTime)); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); } @Test public void analyze_appActivityAfterNetworkActivityWithinConflictInterval_addActivity() { ZonedDateTime now = now(); JUnitUtil.skipBefore("Skip shortly after midnight", now, 0, 5); mockExistingActivity(gamblingGoal, now); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Lotto App", now.minusMinutes(4), now.minusMinutes(2))); verify(mockActivityUpdater).addActivity(any(), any(), eq(GoalDto.createInstance(gamblingGoal)), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); } @Test public void analyze_networkActivityAfterAppActivityWithinConflictInterval_addActivity() { ZonedDateTime now = now(); mockExistingActivity(gamblingGoal, now.minusMinutes(10), now.minusMinutes(5), "Lotto App"); service.analyze(userAnonId, createNetworkActivityForCategories("lotto")); verify(mockActivityUpdater).addActivity(any(), any(), eq(GoalDto.createInstance(gamblingGoal)), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); } @Test public void analyze_appActivityDifferentAppWithinConflictInterval_addActivity() { ZonedDateTime now = now(); JUnitUtil.skipBefore("Skip shortly after midnight", now, 0, 11); mockExistingActivity(gamblingGoal, now.minusMinutes(10), now.minusMinutes(5), "Poker App"); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Lotto App", now.minusMinutes(4), now.minusMinutes(2))); verify(mockActivityUpdater).addActivity(any(), any(), eq(GoalDto.createInstance(gamblingGoal)), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).updateTimeLastActivity(any(), any(), any()); } @Test public void analyze_appActivitySameAppWithinConflictIntervalContinuous_updateTimeLastActivity() { ZonedDateTime now = now(); JUnitUtil.skipBefore("Skip shortly after midnight", now, 0, 11); ZonedDateTime existingActivityEndTime = now.minusMinutes(5); mockExistingActivity(gamblingGoal, now.minusMinutes(10), existingActivityEndTime, "Lotto App"); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Lotto App", existingActivityEndTime, now.minusMinutes(2))); verify(mockActivityUpdater).updateTimeLastActivity(any(), eq(GoalDto.createInstance(gamblingGoal)), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).addActivity(any(), any(), any(), any()); } @Test public void analyze_appActivitySameAppOverlappingLastCachedActivityEndTime_updateTimeLastActivity() { ZonedDateTime now = now(); JUnitUtil.skipBefore("Skip shortly after midnight", now, 0, 11); mockExistingActivity(gamblingGoal, now.minusMinutes(10), now.minusMinutes(5), "Lotto App"); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Lotto App", now.minusMinutes(5).minusSeconds(30), now.minusMinutes(2))); verify(mockActivityUpdater).updateTimeLastActivity(any(), eq(GoalDto.createInstance(gamblingGoal)), any()); verify(mockActivityUpdater, never()).updateTimeExistingActivity(any(), any()); verify(mockActivityUpdater, never()).addActivity(any(), any(), any(), any()); } @Test public void analyze_appActivitySameAppWithinConflictIntervalButNotContinuous_addActivity() { ZonedDateTime now = now(); JUnitUtil.skipBefore("Skip shortly after midnight", now, 0, 11); ZonedDateTime existingActivityEndTime = now.minusMinutes(5); mockExistingActivity(gamblingGoal, now.minusMinutes(10), now.minusMinutes(5), "Lotto App"); service.analyze(userAnonId, deviceAnonId, createSingleAppActivity("Lotto App", existingActivityEndTime.plusSeconds(1), now.minusMinutes(2))); verify(mockActivityUpdater).addActivity(any(), any(), eq(GoalDto.createInstance(gamblingGoal)), any()); } private NetworkActivityDto createNetworkActivityForCategories(String... conflictCategories) { return new NetworkActivityDto(-1, new HashSet<>(Arrays.asList(conflictCategories)), "http://localhost/test" + new Random().nextInt(), Optional.empty()); } private NetworkActivityDto createNetworkActivityForCategories(ZonedDateTime time, String... conflictCategories) { return new NetworkActivityDto(-1, new HashSet<>(Arrays.asList(conflictCategories)), "http://localhost/test" + new Random().nextInt(), Optional.of(time)); } private void verifyAddActivity(Goal... forGoals) { for (Goal forGoal : forGoals) { verify(mockActivityUpdater).addActivity(any(), any(), eq(GoalDto.createInstance(forGoal)), any()); } } private DayActivity mockExistingActivity(Goal forGoal, ZonedDateTime activityTime) { return mockExistingActivities(forGoal, createActivity(activityTime, activityTime)); } private DayActivity mockExistingActivity(Goal forGoal, ZonedDateTime startTime, ZonedDateTime endTime, String app) { return mockExistingActivities(forGoal, createActivity(startTime, endTime, app)); } private DayActivity mockExistingActivities(Goal forGoal, Activity... activities) { LocalDateTime startTime = activities[0].getStartTime(); DayActivity dayActivity = DayActivity.createInstance(userAnonEntity, forGoal, userAnonZoneId, startTime.truncatedTo(ChronoUnit.DAYS).toLocalDate()); Arrays.asList(activities).forEach(a -> dayActivity.addActivity(a)); ActivityDto existingActivity = ActivityDto.createInstance(activities[activities.length - 1]); when(mockDayActivityRepository.findOne(userAnonId, dayActivity.getStartDate(), forGoal.getId())) .thenReturn(dayActivity); when(mockAnalysisEngineCacheService.fetchLastActivityForUser(userAnonId, deviceAnonId, forGoal.getId())) .thenReturn(existingActivity); WeekActivity weekActivity = WeekActivity.createInstance(userAnonEntity, forGoal, userAnonZoneId, TimeUtil.getStartOfWeek(startTime.toLocalDate())); weekActivity.addDayActivity(dayActivity); forGoal.addWeekActivity(weekActivity); return dayActivity; } private Activity createActivity(ZonedDateTime startTime, ZonedDateTime endTime) { return Activity.createInstance(deviceAnonEntity, userAnonZoneId, startTime.toLocalDateTime(), endTime.toLocalDateTime(), Optional.empty()); } private Activity createActivity(ZonedDateTime startTime, ZonedDateTime endTime, String app) { return Activity.createInstance(deviceAnonEntity, userAnonZoneId, startTime.toLocalDateTime(), endTime.toLocalDateTime(), Optional.of(app)); } private AppActivitiesDto createSingleAppActivity(String app, ZonedDateTime startTime, ZonedDateTime endTime) { AppActivitiesDto.Activity[] activities = { new AppActivitiesDto.Activity(app, startTime, endTime) }; return new AppActivitiesDto(now(), activities); } private ZonedDateTime now() { return ZonedDateTime.now().withZoneSameInstant(userAnonZoneId); } }