nu.yona.server.device.service.DeviceServiceTestConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for nu.yona.server.device.service.DeviceServiceTestConfiguration.java

Source

/*******************************************************************************
 * Copyright (c) 2017, 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.device.service;

import static com.spencerwi.hamcrestJDK8Time.matchers.IsBetween.between;
import static nu.yona.server.test.util.Matchers.hasMessageId;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.isEmptyString;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.hamcrest.core.StringContains.containsString;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.MethodRule;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.data.repository.Repository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import nu.yona.server.CoreConfiguration;
import nu.yona.server.analysis.entities.Activity;
import nu.yona.server.analysis.entities.ActivityRepository;
import nu.yona.server.crypto.seckey.CryptoSession;
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.entities.UserDevice;
import nu.yona.server.device.entities.UserDeviceRepository;
import nu.yona.server.entities.ActivityRepositoryMock;
import nu.yona.server.entities.DeviceAnonymizedRepositoryMock;
import nu.yona.server.entities.UserDeviceRepositoryMock;
import nu.yona.server.entities.UserRepositoriesConfiguration;
import nu.yona.server.goals.entities.Goal;
import nu.yona.server.goals.entities.GoalRepository;
import nu.yona.server.messaging.entities.Message;
import nu.yona.server.messaging.service.MessageService;
import nu.yona.server.subscriptions.entities.BuddyDeviceChangeMessage;
import nu.yona.server.subscriptions.entities.User;
import nu.yona.server.subscriptions.service.LDAPUserService;
import nu.yona.server.subscriptions.service.UserAnonymizedDto;
import nu.yona.server.subscriptions.service.UserDto;
import nu.yona.server.test.util.BaseSpringIntegrationTest;
import nu.yona.server.test.util.CryptoSessionRule;
import nu.yona.server.test.util.JUnitUtil;
import nu.yona.server.util.LockPool;
import nu.yona.server.util.TimeUtil;

@Configuration
@ComponentScan(useDefaultFilters = false, basePackages = { "nu.yona.server.device.service",
        "nu.yona.server.subscriptions.service", "nu.yona.server.properties", "nu.yona.server" }, includeFilters = {
                @ComponentScan.Filter(pattern = "nu.yona.server.subscriptions.service.UserService", type = FilterType.REGEX),
                @ComponentScan.Filter(pattern = "nu.yona.server.subscriptions.service.UserAnonymizedService", type = FilterType.REGEX),
                @ComponentScan.Filter(pattern = "nu.yona.server.device.service.DeviceService", type = FilterType.REGEX),
                @ComponentScan.Filter(pattern = "nu.yona.server.properties.YonaProperties", type = FilterType.REGEX),
                @ComponentScan.Filter(pattern = "nu.yona.server.Translator", type = FilterType.REGEX) })
class DeviceServiceTestConfiguration extends UserRepositoriesConfiguration {
    @Bean(name = "messageSource")
    public ReloadableResourceBundleMessageSource messageSource() {
        return new CoreConfiguration().messageSource();
    }

    @Bean
    UserDeviceRepository getMockDeviceRepository() {
        return Mockito.spy(new UserDeviceRepositoryMock());
    }

    @Bean
    DeviceAnonymizedRepository getMockDeviceAnonymizedRepository() {
        return Mockito.spy(new DeviceAnonymizedRepositoryMock());
    }

    @Bean
    ActivityRepository getActivityRepository() {
        return new ActivityRepositoryMock();
    }
}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { DeviceServiceTestConfiguration.class })
public class DeviceServiceTest extends BaseSpringIntegrationTest {
    private static final String SOME_APP_VERSION = "9.9.9";
    private static final int SUPPORTED_APP_VERSION_CODE = 999;

    @Autowired
    private UserDeviceRepository userDeviceRepository;

    @Autowired
    private ActivityRepository activityRepository;

    @Autowired
    private DeviceService service;

    @Autowired
    private DeviceAnonymizedRepository deviceAnonymizedRepository;

    @Mock
    private GoalRepository mockGoalRepository;

    @MockBean
    private MessageService mockMessageService;

    @MockBean
    private LDAPUserService mockLdapUserService;

    @MockBean
    private LockPool<UUID> mockUserSynchronizer;

    @Captor
    private ArgumentCaptor<Supplier<Message>> messageSupplierCaptor;

    private static final String PASSWORD = "password";
    private User richard;

    @Rule
    public MethodRule cryptoSession = new CryptoSessionRule(PASSWORD);

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Before
    public void setUpPerTest() {
        try (CryptoSession cryptoSession = CryptoSession.start(PASSWORD)) {
            richard = JUnitUtil.createRichard();
        }
        reset(userRepository);
        reset(userDeviceRepository);
    }

    @Override
    protected Map<Class<?>, Repository<?, ?>> getRepositories() {
        Map<Class<?>, Repository<?, ?>> repositoriesMap = new HashMap<>();
        repositoriesMap.put(Goal.class, mockGoalRepository);
        repositoriesMap.put(DeviceAnonymized.class, deviceAnonymizedRepository);
        repositoriesMap.put(UserDevice.class, userDeviceRepository);
        repositoriesMap.put(Activity.class, activityRepository);
        return repositoriesMap;
    }

    @Test
    public void getDevice_tryGetNonExistingDevice_exception() throws Exception {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.not.found.id"));
        service.getDevice(UUID.randomUUID());
    }

    @Test
    public void addDeviceToUser_addFirstDevice_userHasOneDevice() {
        String deviceName = "Testing";
        OperatingSystem operatingSystem = OperatingSystem.ANDROID;
        LocalDateTime startTime = TimeUtil.utcNow();
        UserDeviceDto deviceDto = new UserDeviceDto(deviceName, operatingSystem, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);
        service.addDeviceToUser(richard, deviceDto);

        verify(userDeviceRepository, times(1)).save(any(UserDevice.class));
        assertThat(deviceAnonymizedRepository.count(), equalTo(1L));

        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.size(), equalTo(1));

        UserDevice device = devices.iterator().next();
        assertDevice(device, startTime, deviceName, operatingSystem, 0);

        assertVpnAccountCreated(device);
    }

    @Test
    public void addDeviceToUser_addSecondDevice_userHasTwoDevices() {
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        LocalDateTime startTime = TimeUtil.utcNow();
        richard.addDevice(
                createDevice(0, deviceName1, operatingSystem1, SOME_APP_VERSION, SUPPORTED_APP_VERSION_CODE));

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        UserDeviceDto deviceDto2 = new UserDeviceDto(deviceName2, operatingSystem2, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);
        service.addDeviceToUser(richard, deviceDto2);

        verify(userDeviceRepository, times(2)).save(any(UserDevice.class));
        assertThat(deviceAnonymizedRepository.count(), equalTo(2L));

        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.size(), equalTo(2));

        Optional<UserDevice> device1Optional = devices.stream().filter(d -> d.getName().equals(deviceName1))
                .findAny();
        assertThat(device1Optional.isPresent(), equalTo(true));
        UserDevice device1 = device1Optional.get();
        assertDevice(device1, startTime, deviceName1, operatingSystem1, 0);

        Optional<UserDevice> device2Optional = devices.stream().filter(d -> d.getName().equals(deviceName2))
                .findAny();
        assertThat(device2Optional.isPresent(), equalTo(true));
        UserDevice device2 = device2Optional.get();
        assertDevice(device2, startTime, deviceName2, operatingSystem2, 1);

        assertVpnAccountCreated(device2);
    }

    @Test
    public void addDeviceToUser_tryAddDuplicateName_exception() {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.name.already.exists"));

        String deviceName = "First";
        richard.addDevice(
                createDevice(0, deviceName, OperatingSystem.ANDROID, SOME_APP_VERSION, SUPPORTED_APP_VERSION_CODE));

        UserDeviceDto deviceDto2 = new UserDeviceDto(deviceName, OperatingSystem.IOS, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);
        service.addDeviceToUser(richard, deviceDto2);
    }

    @Test
    public void deleteDevice_deleteOneOfTwo_userHasOneDeviceBuddyInformed() {
        LocalDateTime startTime = TimeUtil.utcNow();
        String deviceName = "First";
        UserDevice device1 = createDevice(0, deviceName, OperatingSystem.ANDROID, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);
        richard.addDevice(device1);

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        richard.addDevice(
                createDevice(1, deviceName2, operatingSystem2, SOME_APP_VERSION, SUPPORTED_APP_VERSION_CODE));

        assertThat(richard.getDevices().size(), equalTo(2));

        service.deleteDevice(richard.getId(), device1.getId());
        assertThat(deviceAnonymizedRepository.count(), equalTo(1L));

        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.size(), equalTo(1));

        UserDevice device2 = devices.iterator().next();

        assertDevice(device2, startTime, deviceName2, operatingSystem2, 1);

        verify(mockMessageService, times(1)).broadcastMessageToBuddies(Matchers.<UserAnonymizedDto>any(),
                messageSupplierCaptor.capture());
        Message message = messageSupplierCaptor.getValue().get();
        assertThat(message, instanceOf(BuddyDeviceChangeMessage.class));
        BuddyDeviceChangeMessage buddyDeviceChangeMessage = (BuddyDeviceChangeMessage) message;
        assertThat(buddyDeviceChangeMessage.getChange(), equalTo(DeviceChange.DELETE));
        assertThat(buddyDeviceChangeMessage.getOldName().get(), equalTo(deviceName));
        assertThat(buddyDeviceChangeMessage.getMessage(), containsString("User deleted device"));
    }

    @Test
    public void addDeviceToUser_addAfterDelete_deviceIdReused() {
        LocalDateTime startTime = TimeUtil.utcNow();
        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        richard.addDevice(
                createDevice(1, deviceName2, operatingSystem2, SOME_APP_VERSION, SUPPORTED_APP_VERSION_CODE));

        assertThat(richard.getDevices().size(), equalTo(1));

        String deviceName3 = "Third";
        OperatingSystem operatingSystem3 = OperatingSystem.IOS;
        UserDeviceDto deviceDto3 = new UserDeviceDto(deviceName3, operatingSystem3, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);
        service.addDeviceToUser(richard, deviceDto3);

        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.size(), equalTo(2));

        Optional<UserDevice> device2Optional = devices.stream().filter(d -> d.getName().equals(deviceName2))
                .findAny();
        assertThat(device2Optional.isPresent(), equalTo(true));
        UserDevice device2 = device2Optional.get();
        assertDevice(device2, startTime, deviceName2, operatingSystem2, 1);

        Optional<UserDevice> device3Optional = devices.stream().filter(d -> d.getName().equals(deviceName3))
                .findAny();
        assertThat(device3Optional.isPresent(), equalTo(true));
        UserDevice device3 = device3Optional.get();
        assertDevice(device3, startTime, deviceName3, operatingSystem3, 0);
    }

    @Test
    public void addDeviceToUser_byId_addFirstDevice_userHasOneDevice() {
        String deviceName = "Testing";
        OperatingSystem operatingSystem = OperatingSystem.ANDROID;
        LocalDateTime startTime = TimeUtil.utcNow();
        UserDeviceDto deviceDto = new UserDeviceDto(deviceName, operatingSystem, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);
        service.addDeviceToUser(richard.getId(), deviceDto);

        verify(userDeviceRepository, times(1)).save(any(UserDevice.class));
        assertThat(deviceAnonymizedRepository.count(), equalTo(1L));

        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.size(), equalTo(1));

        UserDevice device = devices.iterator().next();
        assertDevice(device, startTime, deviceName, operatingSystem, 0);
    }

    @Test
    public void deleteDevice_tryDeleteOneAndOnlyDevice_exception() throws Exception {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.cannot.delete.last.one"));

        richard.addDevice(
                createDevice(0, "Testing", OperatingSystem.ANDROID, SOME_APP_VERSION, SUPPORTED_APP_VERSION_CODE));

        assertThat(richard.getDevices().size(), equalTo(1));

        UserDevice device = richard.getDevices().iterator().next();

        service.deleteDevice(richard.getId(), device.getId());
    }

    @Test
    public void deleteDevice_tryDeleteNonExistingDevice_exception() throws Exception {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.not.found.id"));

        richard.addDevice(
                createDevice(0, "First", OperatingSystem.ANDROID, SOME_APP_VERSION, SUPPORTED_APP_VERSION_CODE));
        UserDevice notAddedDevice = createDevice(1, "NotAddedDevice", OperatingSystem.IOS, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);

        assertThat(richard.getDevices().size(), equalTo(1));

        service.deleteDevice(richard.getId(), notAddedDevice.getId());
    }

    private void assertDevice(UserDevice device, LocalDateTime startTime, String expectedDeviceName,
            OperatingSystem expectedOperatingSystem, int expectedDeviceIndex) {
        assertThat(device.getName(), equalTo(expectedDeviceName));
        assertThat(device.getAppLastOpenedDate(), equalTo(TimeUtil.utcNow().toLocalDate()));
        assertThat(device.getRegistrationTime(), is(between(startTime, TimeUtil.utcNow())));
        assertThat(device.isVpnConnected(), equalTo(false));

        DeviceAnonymized deviceAnonymized = device.getDeviceAnonymized();
        assertThat(deviceAnonymized.getOperatingSystem(), equalTo(expectedOperatingSystem));
        assertThat(deviceAnonymized.getLastMonitoredActivityDate().isPresent(), equalTo(false));
        assertThat(deviceAnonymized.getDeviceIndex(), equalTo(expectedDeviceIndex));
        assertThat(deviceAnonymized.getUserAnonymized().getId(), equalTo(richard.getAnonymized().getId()));
    }

    @Test
    public void getDefaultDeviceId_oneDevice_deviceReturned() {
        UserDevice device = createDevice(0, "First", OperatingSystem.ANDROID, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);
        richard.addDevice(device);

        UUID defaultDeviceId = service.getDefaultDeviceId(createRichardUserDto());

        assertThat(defaultDeviceId, equalTo(device.getId()));
    }

    @Test
    public void updateDevice_rename_renamedBuddyInformed() {
        LocalDateTime startTime = TimeUtil.utcNow();
        String oldName = "Original name";
        OperatingSystem operatingSystem = OperatingSystem.ANDROID;
        UserDevice device = createDevice(0, oldName, operatingSystem, "1.0.0", 2);
        richard.addDevice(device);

        assertThat(richard.getDevices().size(), equalTo(1));

        String newName = "New name";
        DeviceUpdateRequestDto changeRequest = new DeviceUpdateRequestDto(newName, Optional.empty());
        service.updateDevice(richard.getId(), device.getId(), changeRequest);
        assertThat(deviceAnonymizedRepository.count(), equalTo(1L));

        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.size(), equalTo(1));

        assertDevice(device, startTime, newName, operatingSystem, 0);

        verify(mockMessageService, times(1)).broadcastMessageToBuddies(Matchers.<UserAnonymizedDto>any(),
                messageSupplierCaptor.capture());
        Message message = messageSupplierCaptor.getValue().get();
        assertThat(message, instanceOf(BuddyDeviceChangeMessage.class));
        BuddyDeviceChangeMessage buddyDeviceChangeMessage = (BuddyDeviceChangeMessage) message;
        assertThat(buddyDeviceChangeMessage.getChange(), equalTo(DeviceChange.RENAME));
        assertThat(buddyDeviceChangeMessage.getOldName().get(), equalTo(oldName));
        assertThat(buddyDeviceChangeMessage.getNewName().get(), equalTo(newName));
        assertThat(buddyDeviceChangeMessage.getMessage(), containsString("User renamed device"));
    }

    @Test
    public void updateDevice_unchangedName_noAction() {
        LocalDateTime startTime = TimeUtil.utcNow();
        String oldName = "original";
        OperatingSystem operatingSystem = OperatingSystem.ANDROID;
        UserDevice device = createDevice(0, oldName, operatingSystem, "1.0.0", 2);
        richard.addDevice(device);

        assertThat(richard.getDevices().size(), equalTo(1));

        String newName = "ORIGINAL".toLowerCase();
        assertThat(oldName, not(sameInstance(newName)));
        DeviceUpdateRequestDto changeRequest = new DeviceUpdateRequestDto(newName, Optional.empty());
        service.updateDevice(richard.getId(), device.getId(), changeRequest);
        assertThat(deviceAnonymizedRepository.count(), equalTo(1L));

        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.size(), equalTo(1));

        assertDevice(device, startTime, newName, operatingSystem, 0);

        verify(mockMessageService, never()).broadcastMessageToBuddies(Matchers.<UserAnonymizedDto>any(), any());
        assertThat(device.getName(), sameInstance(oldName));
    }

    @Test
    public void updateDevice_tryDuplicateName_exception() {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.name.already.exists"));

        String firstDeviceName = "First";
        UserDevice device1 = createDevice(0, firstDeviceName, OperatingSystem.ANDROID, "Unknown", 0);
        richard.addDevice(device1);

        OperatingSystem operatingSystem = OperatingSystem.ANDROID;
        UserDevice device = createDevice(0, "Original name", operatingSystem, "1.0.0", 2);
        richard.addDevice(device);

        assertThat(richard.getDevices().size(), equalTo(2));

        DeviceUpdateRequestDto changeRequest = new DeviceUpdateRequestDto(firstDeviceName, Optional.empty());
        service.updateDevice(richard.getId(), device.getId(), changeRequest);
    }

    @Test
    public void getDefaultDeviceId_twoDevices_firstDeviceReturned() {
        UserDevice device1 = createDevice(0, "First", OperatingSystem.ANDROID, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);
        richard.addDevice(device1);
        UserDevice device2 = createDevice(1, "Second", OperatingSystem.IOS, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);
        richard.addDevice(device2);

        UUID defaultDeviceId = service.getDefaultDeviceId(createRichardUserDto());

        assertThat(defaultDeviceId, equalTo(device1.getId()));
    }

    @Test
    public void getDefaultDeviceId_tryNoDevices_exception() throws Exception {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.collection.empty"));

        service.getDefaultDeviceId(createRichardUserDto());
    }

    @Test
    public void getDeviceAnonymized_byIndex_firstDevice_correctDevice() {
        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        UserDevice device1 = addDeviceToRichard(0, deviceName1, operatingSystem1);

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        addDeviceToRichard(1, deviceName2, operatingSystem2);

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1, deviceName2));

        // Get the anonymized device for the first index
        DeviceAnonymizedDto deviceAnonymized = service.getDeviceAnonymized(createRichardAnonymizedDto(), 0);

        // Assert success
        assertThat(deviceAnonymized.getId(),
                equalTo(userDeviceRepository.getOne(device1.getId()).getDeviceAnonymizedId()));
    }

    @Test
    public void getDeviceAnonymized_byIndex_secondDevice_correctDevice() {
        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        addDeviceToRichard(0, deviceName1, operatingSystem1);

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        UserDevice device2 = addDeviceToRichard(1, deviceName2, operatingSystem2);

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1, deviceName2));

        // Get the anonymized device for the second index
        DeviceAnonymizedDto deviceAnonymized = service.getDeviceAnonymized(createRichardAnonymizedDto(), 1);

        // Assert success
        assertThat(deviceAnonymized.getId(),
                equalTo(userDeviceRepository.getOne(device2.getId()).getDeviceAnonymizedId()));
    }

    @Test
    public void getDeviceAnonymized_byIndex_defaultDevice_correctDevice() {
        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        UserDevice device1 = addDeviceToRichard(0, deviceName1, operatingSystem1);

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        addDeviceToRichard(1, deviceName2, operatingSystem2);

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1, deviceName2));

        // Get the anonymized device for a negative index, implies fall back to default device
        DeviceAnonymizedDto deviceAnonymized = service.getDeviceAnonymized(createRichardAnonymizedDto(), -1);

        // Assert success
        assertThat(deviceAnonymized.getId(),
                equalTo(userDeviceRepository.getOne(device1.getId()).getDeviceAnonymizedId()));
    }

    @Test
    public void getDeviceAnonymized_byIndex_tryGetNonExistingIndex_exception() {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.not.found.index"));

        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        addDeviceToRichard(0, deviceName1, operatingSystem1);

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        addDeviceToRichard(1, deviceName2, operatingSystem2);

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1, deviceName2));

        // Try to get the anonymized ID for a nonexisting index
        service.getDeviceAnonymized(createRichardAnonymizedDto(), 2);
    }

    @Test
    public void getDeviceAnonymized_byId_firstDevice_correctDevice() {
        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        UserDevice device1 = addDeviceToRichard(0, deviceName1, operatingSystem1);
        UUID deviceAnonymizedId1 = device1.getDeviceAnonymizedId();

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        addDeviceToRichard(1, deviceName2, operatingSystem2);

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1, deviceName2));

        // Get the anonymized device for the first ID
        DeviceAnonymizedDto deviceAnonymized = service.getDeviceAnonymized(createRichardAnonymizedDto(),
                deviceAnonymizedId1);

        // Assert success
        assertThat(deviceAnonymized.getId(),
                equalTo(userDeviceRepository.getOne(device1.getId()).getDeviceAnonymizedId()));
    }

    @Test
    public void getDeviceAnonymized_byId_secondDevice_correctDevice() {
        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        addDeviceToRichard(0, deviceName1, operatingSystem1);

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        UserDevice device2 = addDeviceToRichard(1, deviceName2, operatingSystem2);
        UUID deviceAnonymizedId2 = device2.getDeviceAnonymizedId();

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1, deviceName2));

        // Get the anonymized device for the second ID
        DeviceAnonymizedDto deviceAnonymized = service.getDeviceAnonymized(createRichardAnonymizedDto(),
                deviceAnonymizedId2);

        // Assert success
        assertThat(deviceAnonymized.getId(),
                equalTo(userDeviceRepository.getOne(device2.getId()).getDeviceAnonymizedId()));
    }

    @Test
    public void getDeviceAnonymized_byId_tryGetNonExistingId_exception() {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.not.found.anonymized.id"));

        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        addDeviceToRichard(0, deviceName1, operatingSystem1);

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        addDeviceToRichard(1, deviceName2, operatingSystem2);

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1, deviceName2));

        // Try to get the anonymized ID for a nonexisting ID
        service.getDeviceAnonymized(createRichardAnonymizedDto(), UUID.randomUUID());
    }

    @Test
    public void getDeviceAnonymizedId_byId_firstDevice_correctDevice() {
        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        UserDevice device1 = addDeviceToRichard(0, deviceName1, operatingSystem1);

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        addDeviceToRichard(1, deviceName2, operatingSystem2);

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1, deviceName2));

        // Get the anonymized ID for the first device ID
        UUID deviceAnonymizedId = service.getDeviceAnonymizedId(createRichardUserDto(), device1.getId());

        // Assert success
        assertThat(deviceAnonymizedId,
                equalTo(userDeviceRepository.getOne(device1.getId()).getDeviceAnonymizedId()));
    }

    @Test
    public void getDeviceAnonymizedId_byId_secondDevice_correctDevice() {
        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        addDeviceToRichard(0, deviceName1, operatingSystem1);

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        UserDevice device2 = addDeviceToRichard(1, deviceName2, operatingSystem2);

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1, deviceName2));

        // Get the anonymized ID for the second device ID
        UUID deviceAnonymizedId = service.getDeviceAnonymizedId(createRichardUserDto(), device2.getId());

        // Assert success
        assertThat(deviceAnonymizedId,
                equalTo(userDeviceRepository.getOne(device2.getId()).getDeviceAnonymizedId()));
    }

    @Test
    public void getDeviceAnonymizedId_byId_tryGetNonExistingId_exception() {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.not.found.id"));

        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        addDeviceToRichard(0, deviceName1, operatingSystem1);

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        addDeviceToRichard(1, deviceName2, operatingSystem2);

        UserDevice notAddedDevice = createDevice(2, "NotAddedDevice", OperatingSystem.IOS, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1, deviceName2));

        // Try to get the anonymized ID for a nonexisting device ID
        service.getDeviceAnonymizedId(createRichardUserDto(), notAddedDevice.getId());
    }

    @Test
    public void postOpenAppEvent_appLastOpenedDateOnEarlierDay_appLastOpenedDateUpdated() {
        UserDevice device = addDeviceToRichard(0, "First", OperatingSystem.ANDROID);
        LocalDate originalDate = TimeUtil.utcNow().toLocalDate().minusDays(1);
        richard.setAppLastOpenedDate(originalDate);
        device.setAppLastOpenedDate(originalDate);

        service.postOpenAppEvent(richard.getId(), device.getId(), Optional.empty(), Optional.of(SOME_APP_VERSION),
                SUPPORTED_APP_VERSION_CODE);

        assertThat(richard.getAppLastOpenedDate().get(), equalTo(TimeUtil.utcNow().toLocalDate()));
        assertThat(device.getAppLastOpenedDate(), equalTo(TimeUtil.utcNow().toLocalDate()));
        verify(userRepository, times(1)).save(any(User.class));
    }

    @Test
    public void postOpenAppEvent_appLastOpenedDateOnSameDay_notUpdated() {
        UserDevice device = addDeviceToRichard(0, "First", OperatingSystem.ANDROID);
        LocalDate originalDate = TimeUtil.utcNow().toLocalDate();
        richard.setAppLastOpenedDate(originalDate);
        device.setAppLastOpenedDate(originalDate);

        service.postOpenAppEvent(richard.getId(), device.getId(), Optional.empty(), Optional.of(SOME_APP_VERSION),
                SUPPORTED_APP_VERSION_CODE);

        assertThat(richard.getAppLastOpenedDate().get(), sameInstance(originalDate));
        assertThat(device.getAppLastOpenedDate(), sameInstance(originalDate));
    }

    @Test
    public void postOpenAppEvent_unsupportedVersionCode_exception() {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.app.version.not.supported"));

        OperatingSystem operatingSystem = OperatingSystem.ANDROID;
        UserDevice device = addDeviceToRichard(0, "First", operatingSystem);
        LocalDate originalDate = TimeUtil.utcNow().toLocalDate().minusDays(1);
        richard.setAppLastOpenedDate(originalDate);
        device.setAppLastOpenedDate(originalDate);

        service.postOpenAppEvent(richard.getId(), device.getId(), Optional.of(operatingSystem),
                Optional.of("0.0.1"), 1);
    }

    @Test
    public void postOpenAppEvent_invalidVersionCode_exception() {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.invalid.version.code"));

        OperatingSystem operatingSystem = OperatingSystem.ANDROID;
        UserDevice device = addDeviceToRichard(0, "First", operatingSystem);
        LocalDate originalDate = TimeUtil.utcNow().toLocalDate().minusDays(1);
        richard.setAppLastOpenedDate(originalDate);
        device.setAppLastOpenedDate(originalDate);

        service.postOpenAppEvent(richard.getId(), device.getId(), Optional.of(operatingSystem), Optional.of("1.0"),
                -1);
    }

    @Test
    public void postOpenAppEvent_differentOperatingSystem_exception() {
        expectedException.expect(DeviceServiceException.class);
        expectedException.expect(hasMessageId("error.device.cannot.switch.operating.system"));

        UserDevice device = addDeviceToRichard(0, "First", OperatingSystem.ANDROID);
        LocalDate originalDate = TimeUtil.utcNow().toLocalDate().minusDays(1);
        richard.setAppLastOpenedDate(originalDate);
        device.setAppLastOpenedDate(originalDate);

        service.postOpenAppEvent(richard.getId(), device.getId(), Optional.of(OperatingSystem.IOS),
                Optional.of(SOME_APP_VERSION), SUPPORTED_APP_VERSION_CODE);
    }

    @Test
    public void removeDuplicateDefaultDevices_singleDevice_noChange() {
        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        UserDevice device1 = addDeviceToRichard(0, deviceName1, operatingSystem1);

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1));

        service.removeDuplicateDefaultDevices(createRichardUserDto(), device1.getId());

        // Assert success
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1));
    }

    @Test
    public void removeDuplicateDefaultDevices_twoDevicesUseFirst_duplicateRemovedActivitiesMoved() {
        removeDuplicateDefaultDevicesFirstOrSecond(0);
    }

    @Test
    public void removeDuplicateDefaultDevices_twoDevicesUseSecond_duplicateRemovedActivitiesMoved() {
        removeDuplicateDefaultDevicesFirstOrSecond(1);
    }

    private void assertVpnAccountCreated(UserDevice device) {
        ArgumentCaptor<String> vpnLoginId = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> vpnPassword = ArgumentCaptor.forClass(String.class);
        verify(mockLdapUserService).createVpnAccount(vpnLoginId.capture(), vpnPassword.capture());
        assertThat(vpnLoginId.getValue(), equalTo(
                richard.getAnonymized().getId().toString() + "$" + device.getDeviceAnonymized().getDeviceIndex()));
        assertThat(vpnPassword.getValue(), is(not(isEmptyString())));
    }

    private void removeDuplicateDefaultDevicesFirstOrSecond(int indexToRetain) {
        // Add devices
        String deviceName1 = "First";
        OperatingSystem operatingSystem1 = OperatingSystem.ANDROID;
        LocalDateTime startTime = TimeUtil.utcNow();
        UserDevice device1 = addDeviceToRichard(0, deviceName1, operatingSystem1);
        ActivityData activity1 = new ActivityData("WhatsApp", 10, 2);
        ActivityData activity2 = new ActivityData("WhatsApp", 5, 1);
        Set<Activity> device1Activities = addActivities(device1, startTime, activity1, activity2);

        String deviceName2 = "Second";
        OperatingSystem operatingSystem2 = OperatingSystem.IOS;
        ActivityData activity3 = new ActivityData("WhatsApp", 9, 2);
        ActivityData activity4 = new ActivityData("WhatsApp", 3, 1);
        UserDevice device2 = addDeviceToRichard(0, deviceName2, operatingSystem2);
        Set<Activity> device2Activities = addActivities(device2, startTime, activity3, activity4);

        List<UserDevice> createdDevices = Arrays.asList(device1, device2);

        // Verify two devices are present
        Set<UserDevice> devices = richard.getDevices();
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(deviceName1, deviceName2));

        service.removeDuplicateDefaultDevices(createRichardUserDto(), createdDevices.get(indexToRetain).getId());

        // Assert success
        assertThat(devices.stream().map(UserDevice::getName).collect(Collectors.toSet()),
                containsInAnyOrder(createdDevices.get(indexToRetain).getName()));
        device1Activities.forEach(a -> assertThat(a.getDeviceAnonymized().get(),
                sameInstance(createdDevices.get(indexToRetain).getDeviceAnonymized())));
        device2Activities.forEach(a -> assertThat(a.getDeviceAnonymized().get(),
                sameInstance(createdDevices.get(indexToRetain).getDeviceAnonymized())));
    }

    private Set<Activity> addActivities(UserDevice deviceEntity, LocalDateTime startTime,
            ActivityData... activityData) {
        return Arrays.asList(activityData).stream().map(ad -> makeActivity(startTime, ad, deviceEntity))
                .collect(Collectors.toSet());
    }

    private UserDevice createDevice(int deviceIndex, String deviceName, OperatingSystem operatingSystem,
            String appVersion, int appVersionCode) {
        DeviceAnonymized deviceAnonymized = DeviceAnonymized.createInstance(deviceIndex, operatingSystem,
                appVersion, appVersionCode, Optional.empty());
        UserDevice device = UserDevice.createInstance(deviceName, deviceAnonymized.getId(), "topSecret");
        deviceAnonymizedRepository.save(deviceAnonymized);
        userDeviceRepository.save(device);
        return device;
    }

    private UserDto createRichardUserDto() {
        return UserDto.createInstanceWithPrivateData(richard, Collections.emptySet());
    }

    private UserAnonymizedDto createRichardAnonymizedDto() {
        return UserAnonymizedDto.createInstance(richard.getAnonymized());
    }

    private UserDevice addDeviceToRichard(int deviceIndex, String deviceName, OperatingSystem operatingSystem) {
        UserDevice deviceEntity = createDevice(deviceIndex, deviceName, operatingSystem, SOME_APP_VERSION,
                SUPPORTED_APP_VERSION_CODE);
        richard.addDevice(deviceEntity);
        return deviceEntity;
    }

    private Activity makeActivity(LocalDateTime startTime, ActivityData activityData, UserDevice device) {
        LocalDateTime activityStartTime = startTime.minusMinutes(activityData.minutesAgo);
        return activityRepository.save(Activity.createInstance(device.getDeviceAnonymized(),
                ZoneId.of("Europe/Amsterdam"), activityStartTime,
                activityStartTime.plusMinutes(activityData.durationMinutes), Optional.of(activityData.app)));
    }

    private static class ActivityData {
        final String app;
        final int minutesAgo;
        final int durationMinutes;

        public ActivityData(String app, int minutesAgo, int durationMinutes) {
            this.app = app;
            this.minutesAgo = minutesAgo;
            this.durationMinutes = durationMinutes;
        }
    }
}