package de.asideas.crowdsource.service;
import de.asideas.crowdsource.domain.exception.InvalidRequestException;
import de.asideas.crowdsource.domain.exception.ResourceNotFoundException;
import de.asideas.crowdsource.domain.model.FinancingRoundEntity;
import de.asideas.crowdsource.domain.model.PledgeEntity;
import de.asideas.crowdsource.domain.model.ProjectEntity;
import de.asideas.crowdsource.domain.model.UserEntity;
import de.asideas.crowdsource.presentation.FinancingRound;
import de.asideas.crowdsource.presentation.Pledge;
import de.asideas.crowdsource.domain.service.financinground.FinancingRoundPostProcessor;
import de.asideas.crowdsource.domain.shared.ProjectStatus;
import de.asideas.crowdsource.repository.FinancingRoundRepository;
import de.asideas.crowdsource.repository.PledgeRepository;
import de.asideas.crowdsource.repository.ProjectRepository;
import de.asideas.crowdsource.repository.UserRepository;
import org.exparity.hamcrest.date.DateMatchers;
import org.joda.time.DateTime;
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.runners.MockitoJUnitRunner;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.scheduling.TaskScheduler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import static junit.framework.TestCase.fail;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
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 static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class FinancingRoundServiceTest {
private static final String ROUND_ID = "4711";
public static final int POST_ROUND_BUDGET = 400;
public static final int ROUND_BUDGET = 1000;
private DateTime fixedDate;
private List<FinancingRoundEntity> financingRoundEntities;
@InjectMocks
private FinancingRoundService financingRoundService;
@Mock
private FinancingRoundRepository financingRoundRepository;
@Mock
private UserRepository userRepository;
@Mock
private ProjectRepository projectRepository;
@Mock
private FinancingRoundPostProcessor financingRoundPostProcessor;
@Mock
private TaskScheduler crowdScheduler;
@Mock
private PledgeRepository pledgeRepository;
@Before
public void init() {
reset(financingRoundRepository, userRepository, projectRepository);
financingRoundEntities = new ArrayList<>();
fixedDate = DateTime.parse("2015-01-10T10:10:10Z");
financingRoundEntities.add(financingRoundEntity(ROUND_ID, fixedDate.minusDays(100), fixedDate.minusDays(50)));
financingRoundEntities.add(financingRoundEntity(ROUND_ID + "2", fixedDate.minusDays(40), fixedDate.minusDays(30)));
when(financingRoundRepository.findAll()).thenReturn(financingRoundEntities);
when(financingRoundRepository.save(any(FinancingRoundEntity.class))).thenAnswer(invocationOnMock -> {
FinancingRoundEntity round = (FinancingRoundEntity) invocationOnMock.getArguments()[0];
round.setId(ROUND_ID);
return round;
});
when(financingRoundPostProcessor.postProcess(any(FinancingRoundEntity.class))).thenAnswer(i -> i.getArguments()[0]);
}
@Test
public void allFinancingRounds() throws Exception {
final int pledgedAmountAfterTermination = 10;
financingRoundEntities.get(0).setTerminationPostProcessingDone(true);
financingRoundEntities.get(1).setTerminationPostProcessingDone(true);
when(pledgeRepository.findByFinancingRoundAndCreatedDateGreaterThan(financingRoundEntities.get(1), financingRoundEntities.get(1).getEndDate()))
.thenReturn(Collections.singletonList(aPledgeEntity(financingRoundEntities.get(1), pledgedAmountAfterTermination)));
final List<FinancingRound> res = financingRoundService.allFinancingRounds();
verify(financingRoundRepository, times(1)).findAll();
verify(pledgeRepository, times(2)).findByFinancingRoundAndCreatedDateGreaterThan(any(FinancingRoundEntity.class), any(DateTime.class));
assertFinancingRoundDto(financingRoundEntities.get(0), res.get(0), POST_ROUND_BUDGET);
assertFinancingRoundDto(financingRoundEntities.get(1), res.get(1), POST_ROUND_BUDGET - pledgedAmountAfterTermination);
}
@Test
public void currentlyActiveRound() throws Exception {
final FinancingRoundEntity financingRoundEntity = financingRoundEntity("test_roundId", fixedDate.minusDays(100), fixedDate.minusDays(50));
when(financingRoundRepository.findActive(any()))
.thenReturn(financingRoundEntity);
final FinancingRound res = financingRoundService.currentlyActiveRound();
assertFinancingRoundDto(financingRoundEntity, res, 0);
verify(financingRoundRepository, times(1)).findActive(any());
}
@Test(expected = ResourceNotFoundException.class)
public void currentlyActiveRound_ThrowsResournceNotFoundExceptionIfNotFonud() throws Exception {
when(financingRoundRepository.findActive(any(DateTime.class))).thenReturn(null);
financingRoundService.currentlyActiveRound();
}
@Test
public void mostRecentRound() throws Exception {
final FinancingRoundEntity expFinancingRound = financingRoundEntity("test_roundId", DateTime.now().minusDays(2), DateTime.now().minusDays(1));
ArgumentCaptor<PageRequest> pageRequestCaptor = ArgumentCaptor.forClass(PageRequest.class);
Page mockedPageAnswer = mock(Page.class);
when(financingRoundRepository.financingRounds(pageRequestCaptor.capture())).thenReturn(mockedPageAnswer);
when(mockedPageAnswer.getNumberOfElements()).thenReturn(1);
when(mockedPageAnswer.getContent()).thenReturn(Collections.singletonList(expFinancingRound));
final FinancingRound res = financingRoundService.mostRecentRound();
assertThat(res, is(new FinancingRound(expFinancingRound, null)));
assertThat(pageRequestCaptor.getValue().getSort().getOrderFor("createdDate"), is(new Sort.Order(Sort.Direction.DESC, "createdDate")));
assertThat(pageRequestCaptor.getValue().getPageSize(), is(1));
assertThat(pageRequestCaptor.getValue().getPageNumber(), is(0));
}
@Test(expected = ResourceNotFoundException.class)
public void mostRecentRound_throwsExceptionIfNoFinancingRoundsExist() throws Exception {
ArgumentCaptor<PageRequest> pageRequestCaptor = ArgumentCaptor.forClass(PageRequest.class);
Page mockedPageAnswer = mock(Page.class);
when(financingRoundRepository.financingRounds(pageRequestCaptor.capture())).thenReturn(mockedPageAnswer);
when(mockedPageAnswer.getNumberOfElements()).thenReturn(0);
financingRoundService.mostRecentRound();
}
@Test
public void startFinancingRound_succeeds() throws Exception {
givenTwoUsersInDatabase();
final FinancingRound financingRoundCreationCmd = financingRound(new DateTime().plusDays(1), 99);
ProjectEntity proposedProject = project(ProjectStatus.PROPOSED);
ProjectEntity deferredProject = project(ProjectStatus.DEFERRED);
ProjectEntity publishedProject = project(ProjectStatus.PUBLISHED);
ProjectEntity rejectedProject = project(ProjectStatus.REJECTED);
when(projectRepository.findAll()).thenReturn(Arrays.asList(
proposedProject, rejectedProject, deferredProject, project(ProjectStatus.FULLY_PLEDGED), publishedProject, project(ProjectStatus.FULLY_PLEDGED)
));
ArgumentCaptor<UserEntity> userEntityCaptor = ArgumentCaptor.forClass(UserEntity.class);
ArgumentCaptor<ProjectEntity> projectCaptor = ArgumentCaptor.forClass(ProjectEntity.class);
ArgumentCaptor<FinancingRoundEntity> financingRoundCaptor = ArgumentCaptor.forClass(FinancingRoundEntity.class);
final FinancingRound res = financingRoundService.startNewFinancingRound(financingRoundCreationCmd);
assertThat(res.isActive(), is(true));
assertThat(res.getBudget(), is(99));
assertThat(res.getEndDate().getMillis(), is(financingRoundCreationCmd.getEndDate().getMillis()));
assertThat(res.getStartDate().toDate(), DateMatchers.sameMinute(financingRoundCreationCmd.getStartDate().toDate()));
assertThat(res.getPostRoundBudget(), is(nullValue()));
verify(financingRoundRepository, times(1)).save(financingRoundCaptor.capture());
verify(userRepository).findAll();
verify(userRepository, times(2)).save(userEntityCaptor.capture());
userEntityCaptor.getAllValues().stream().forEach(u -> assertThat(u.getBudget(), is(financingRoundCaptor.getValue().getBudgetPerUser())));
verify(projectRepository, times(4)).save(projectCaptor.capture());
List<ProjectEntity> updatedProjects = projectCaptor.getAllValues();
assertTrue("Deferred project has been updated", updatedProjects.contains(deferredProject));
assertTrue("Proposed project has been updated", updatedProjects.contains(proposedProject));
assertTrue("Published project has been updated", updatedProjects.contains(publishedProject));
assertTrue("Rejected project has been updated", updatedProjects.contains(rejectedProject));
updatedProjects.stream().forEach(p -> assertThat(p.getFinancingRound(), is(financingRoundCaptor.getValue())));
verify(crowdScheduler).schedule(any(Runnable.class), eq(res.getEndDate().toDate()));
}
@Test
public void startFinancingRound_withSomeDeletedUsers_ShouldOnlyUseNotDeletedUsers() throws Exception {
final FinancingRound financingRoundCreationCmd = financingRound(new DateTime().plusDays(1), 99);
ProjectEntity publishedProject = project(ProjectStatus.PUBLISHED);
when(projectRepository.findAll()).thenReturn(Arrays.asList(publishedProject));
UserEntity user = createUser("test1@mail.com");
UserEntity deletedUser = createDeletedUser("test2@mail.com");
givenUsersInDatabase(user, deletedUser);
final FinancingRound res = financingRoundService.startNewFinancingRound(financingRoundCreationCmd);
assertEquals(99, user.getBudget());
assertEquals(0, deletedUser.getBudget());
}
@Test
public void stopFinancingRound() throws Exception {
final DateTime futureDate = fixedDate.plusDays(5000);
when(financingRoundRepository.findOne(ROUND_ID)).thenReturn(financingRoundEntity(ROUND_ID, fixedDate.minusDays(100), futureDate));
FinancingRound res = financingRoundService.stopFinancingRound(ROUND_ID);
ArgumentCaptor<FinancingRoundEntity> entityCaptor = ArgumentCaptor.forClass(FinancingRoundEntity.class);
verify(financingRoundRepository, atLeastOnce()).save(entityCaptor.capture());
verify(financingRoundPostProcessor).postProcess(entityCaptor.getValue());
assertThat(entityCaptor.getValue().getEndDate(), not(futureDate));
assertThat(res.isActive(), is(false));
assertThat(res.getBudget(), is(ROUND_BUDGET));
}
@Test(expected = ResourceNotFoundException.class)
public void stopFinancingRound_missingRoundThrowsResourceNotFoundException() throws Exception {
String roundId = "1337";
when(financingRoundRepository.findOne(roundId)).thenReturn(null);
financingRoundService.stopFinancingRound(roundId);
}
@Test
public void stopFinancingRound_alreadyStoppedThrowsInvalidRequestException() throws Exception {
String roundId = "1337";
when(financingRoundRepository.findOne(anyString())).thenReturn(financingRoundEntity(null, fixedDate.minusDays(100), fixedDate.minusDays(50)));
try {
financingRoundService.stopFinancingRound(roundId);
fail("Expected InvalidRequestException did not occur");
} catch (Exception e) {
assertTrue("Expected an InvalidRequestException", e instanceof InvalidRequestException);
assertThat(e.getMessage(), is(InvalidRequestException.financingRoundAlreadyStopped().getMessage()));
verify(financingRoundRepository, never()).save(any(FinancingRoundEntity.class));
}
}
@Test
public void schedulePostProcessing() throws Exception {
final FinancingRoundEntity round = financingRoundEntities.get(0);
prepareSynchronizedCrowdScheduler();
when(financingRoundRepository.findOne(round.getId())).thenReturn(round);
financingRoundService.schedulePostProcessing(round);
verify(financingRoundRepository).findOne(eq(round.getId()));
verify(financingRoundPostProcessor).postProcess(round);
}
@Test
public void schedulePostProcessing_doesNothinOnNotExistingEntity() throws Exception {
final FinancingRoundEntity round = financingRoundEntities.get(0);
prepareSynchronizedCrowdScheduler();
when(financingRoundRepository.findOne(round.getId())).thenReturn(null);
financingRoundService.schedulePostProcessing(round);
verify(financingRoundRepository).findOne(eq(round.getId()));
verify(financingRoundPostProcessor, never()).postProcess(round);
}
@Test
public void reschedulePostProcessingOfFinancingRounds() throws Exception {
ArgumentCaptor<Date> scheduleDateCaptor = ArgumentCaptor.forClass(Date.class);
financingRoundService.reschedulePostProcessingOfFinancingRounds();
verify(crowdScheduler, times(financingRoundEntities.size())).schedule(any(Runnable.class), scheduleDateCaptor.capture());
final List<Date> capturedScheduleDates = scheduleDateCaptor.getAllValues();
assertThat(capturedScheduleDates.size(), is(financingRoundEntities.size()));
for (int i = 0; i < capturedScheduleDates.size(); i++) {
assertTrue("Should have captured actual end dates as scheduler dates", capturedScheduleDates.contains(financingRoundEntities.get(i).getEndDate().toDate()));
}
}
@Test
public void financingRound_ShouldCallRepositoryOnTerminatedPostProcessedRound() throws Exception {
final FinancingRoundEntity financingRoundEntity = financingRoundEntity("test_roundId", DateTime.now().minusDays(3), DateTime.now().minusDays(2));
financingRoundEntity.setTerminationPostProcessingDone(true);
when(pledgeRepository.findByFinancingRoundAndCreatedDateGreaterThan(any(FinancingRoundEntity.class), any(DateTime.class)))
.thenReturn(Collections.singletonList(aPledgeEntity(financingRoundEntity, 100)));
final FinancingRound res = financingRoundService.financingRound(financingRoundEntity);
assertFinancingRoundDto(financingRoundEntity, res, 300);
}
@Test
public void financingRound_ShouldNotCallRepositoryOnNonTerminatedNonPostProcessedRound() throws Exception {
final FinancingRoundEntity financingRoundEntity = financingRoundEntity("test_roundId", DateTime.now().minusDays(2), DateTime.now().plusDays(1));
final FinancingRound res = financingRoundService.financingRound(financingRoundEntity);
assertFinancingRoundDto(financingRoundEntity, res, 0);
verify(pledgeRepository, never()).findByFinancingRoundAndCreatedDateGreaterThan(any(FinancingRoundEntity.class), any(DateTime.class));
}
private void givenUsersInDatabase(UserEntity... users) {
when(userRepository.findAll()).thenReturn(Arrays.asList(users));
}
private void givenTwoUsersInDatabase() {
List<UserEntity> userEntities = new ArrayList<>();
userEntities.add(new UserEntity("test1@mail.com"));
userEntities.add(new UserEntity("test2@mail.com"));
when(userRepository.findAll()).thenReturn(userEntities);
}
private void assertFinancingRoundDto(FinancingRoundEntity financingRoundEntity, FinancingRound res, Integer expPostRoundBudgetRemaining) {
assertThat(res.getBudget(), is(financingRoundEntity.getBudget()));
assertThat(res.getPostRoundBudget(), is(financingRoundEntity.getPostRoundBudget()));
assertThat(res.getPostRoundBudgetRemaining(), is(expPostRoundBudgetRemaining));
assertThat(res.getStartDate(), is(financingRoundEntity.getStartDate()));
assertThat(res.getEndDate(), is(financingRoundEntity.getEndDate()));
assertThat(res.isActive(), is(financingRoundEntity.active()));
assertThat(res.getId(), is(financingRoundEntity.getId()));
}
private void prepareSynchronizedCrowdScheduler() {
when(crowdScheduler.schedule(any(Runnable.class), any(Date.class))).thenAnswer(i -> {
((Runnable) i.getArguments()[0]).run();
return null;
});
}
private FinancingRound financingRound(DateTime end, Integer budget) {
FinancingRound financingRound = new FinancingRound();
financingRound.setEndDate(end);
financingRound.setBudget(budget);
return financingRound;
}
private FinancingRoundEntity financingRoundEntity(String roundId, DateTime start, DateTime end) {
FinancingRoundEntity res = new FinancingRoundEntity();
res.setId(roundId);
res.setStartDate(start);
res.setEndDate(end);
res.setBudget(ROUND_BUDGET);
res.setPostRoundBudget(POST_ROUND_BUDGET);
return res;
}
private ProjectEntity project(ProjectStatus status) {
ProjectEntity projectEntity = new ProjectEntity();
projectEntity.setStatus(status);
projectEntity.setId(UUID.randomUUID().toString());
return projectEntity;
}
private PledgeEntity aPledgeEntity(FinancingRoundEntity financingRoundEntity, int pledgeAmount) {
final PledgeEntity res = new PledgeEntity(new ProjectEntity(), null, new Pledge(pledgeAmount), financingRoundEntity);
res.setCreatedDate(DateTime.now());
return res;
}
private UserEntity createDeletedUser(String s) {
UserEntity user = createUser(s);
user.setDeleted(true);
return user;
}
private UserEntity createUser(String s) {
return new UserEntity(s);
}
}