package fr.openwide.core.test.jpa.more.business.history.test;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.when;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.apache.commons.lang3.time.DateUtils;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Answers;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import com.google.common.base.Supplier;
import com.google.common.collect.Lists;
import fr.openwide.core.commons.util.fieldpath.FieldPath;
import fr.openwide.core.jpa.business.generic.model.GenericEntityReference;
import fr.openwide.core.jpa.business.generic.service.IEntityService;
import fr.openwide.core.jpa.exception.SecurityServiceException;
import fr.openwide.core.jpa.exception.ServiceException;
import fr.openwide.core.jpa.more.business.difference.model.Difference;
import fr.openwide.core.jpa.more.business.difference.util.IDifferenceFromReferenceGenerator;
import fr.openwide.core.jpa.more.business.difference.util.IHistoryDifferenceGenerator;
import fr.openwide.core.jpa.more.business.history.model.atomic.HistoryDifferenceEventType;
import fr.openwide.core.jpa.more.business.history.model.embeddable.HistoryDifferencePath;
import fr.openwide.core.jpa.more.business.history.model.embeddable.HistoryValue;
import fr.openwide.core.jpa.more.junit.difference.TestHistoryDifferenceCollectionMatcher;
import fr.openwide.core.jpa.more.junit.difference.TestHistoryDifferenceDescription;
import fr.openwide.core.jpa.more.util.transaction.service.ITransactionSynchronizationTaskManagerService;
import fr.openwide.core.test.jpa.more.business.AbstractJpaMoreTestCase;
import fr.openwide.core.test.jpa.more.business.entity.model.TestEntity;
import fr.openwide.core.test.jpa.more.business.history.model.TestHistoryDifference;
import fr.openwide.core.test.jpa.more.business.history.model.TestHistoryLog;
import fr.openwide.core.test.jpa.more.business.history.model.atomic.TestHistoryEventType;
import fr.openwide.core.test.jpa.more.business.history.model.bean.TestHistoryLogAdditionalInformationBean;
import fr.openwide.core.test.jpa.more.business.history.service.ITestHistoryLogService;
public class TestHistoryLogService extends AbstractJpaMoreTestCase {
private static final Date DATE = new Date();
/*
* Only here to mock some parameters passed to the log() method.
* The Spring context is still used for most beans.
*/
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock(answer = Answers.RETURNS_MOCKS)
private IDifferenceFromReferenceGenerator<TestEntity> differenceGeneratorMock;
@Mock
private IHistoryDifferenceGenerator<TestEntity> historyDifferenceGeneratorMock;
@Autowired
private ITestHistoryLogService historyLogService;
@Autowired
private IEntityService entityService;
@Autowired
private ITransactionSynchronizationTaskManagerService transactionSynchronizationService;
private TransactionTemplate writeTransactionTemplate;
private HistoryValue entityHistoryValueBefore;
private HistoryValue entityHistoryValueAfter;
private HistoryValue stringHistoryValueAfter = new HistoryValue("after");
private HistoryValue createExpectedHistoryValue(TestEntity entity) {
return new HistoryValue(entity.toString(), GenericEntityReference.of(entity));
}
@Autowired
private void setTransactionTemplate(PlatformTransactionManager transactionManager) {
DefaultTransactionAttribute writeTransactionAttribute =
new DefaultTransactionAttribute(TransactionAttribute.PROPAGATION_REQUIRED);
writeTransactionAttribute.setReadOnly(false);
writeTransactionTemplate = new TransactionTemplate(transactionManager, writeTransactionAttribute);
}
@Before
public void initValues() throws ServiceException, SecurityServiceException {
TestEntity before = new TestEntity("beforeEntity");
TestEntity after = new TestEntity("afterEntity");
testEntityService.create(before);
testEntityService.create(after);
entityHistoryValueBefore = createExpectedHistoryValue(before);
entityHistoryValueAfter = createExpectedHistoryValue(after);
}
@Before
public void initMocks() {
// Make the difference generation fail if the modified object is not attached to the session
AssertionError error = new AssertionError("Attempt to compute differences on an object that was not attached to the session");
when(differenceGeneratorMock.diff(
argThat(not(this.<TestEntity>isAttachedToSession())), Matchers.<TestEntity>anyObject()
)).thenThrow(error);
when(differenceGeneratorMock.diffFromReference(
argThat(not(this.<TestEntity>isAttachedToSession()))
)).thenThrow(error);
}
@Override
protected void cleanAll() throws ServiceException, SecurityServiceException {
cleanEntities(historyLogService);
super.cleanAll();
}
private List<TestHistoryDifference> createExpectedDifferences() {
TestHistoryDifference difference1 = new TestHistoryDifference(
new HistoryDifferencePath(FieldPath.fromString(".somePropertyIJustInvented")),
HistoryDifferenceEventType.ADDED,
entityHistoryValueBefore, entityHistoryValueAfter
);
TestHistoryDifference difference2 = new TestHistoryDifference(
new HistoryDifferencePath(FieldPath.fromString(".somePropertyIJustInvented2").item(), new HistoryValue("1")),
HistoryDifferenceEventType.REMOVED,
null, stringHistoryValueAfter
);
return Lists.newArrayList(
difference1,
difference2
);
}
private Matcher<Collection<TestHistoryDifference>> matchesExpectedDifferences() {
return new TestHistoryDifferenceCollectionMatcher<>(
TestHistoryDifferenceDescription.builder()
.put(FieldPath.fromString(".somePropertyIJustInvented"), HistoryDifferenceEventType.ADDED)
.putItem(FieldPath.fromString(".somePropertyIJustInvented2"), 1, HistoryDifferenceEventType.REMOVED)
.build()
);
}
@Test
public void logNow() throws ServiceException, SecurityServiceException {
TestEntity object = new TestEntity("object");
testEntityService.create(object);
TestEntity secondaryObject = new TestEntity("secondaryObject");
testEntityService.create(secondaryObject);
List<TestHistoryDifference> differences = createExpectedDifferences();
historyLogService.logNow(DATE, TestHistoryEventType.EVENT1, differences, object,
TestHistoryLogAdditionalInformationBean.of(secondaryObject));
HistoryValue expectedObjectHistoryValue = createExpectedHistoryValue(object);
HistoryValue expectedSecondaryObjectHistoryValue = createExpectedHistoryValue(secondaryObject);
entityService.flush();
entityService.clear();
List<TestHistoryLog> logs = historyLogService.list();
assertEquals(1, logs.size());
TestHistoryLog log = logs.iterator().next();
assertNotNull(log.getId());
assertEquals(DATE, log.getDate());
assertEquals(TestHistoryEventType.EVENT1, log.getEventType());
assertEquals(expectedObjectHistoryValue, log.getMainObject());
assertEquals(expectedSecondaryObjectHistoryValue, log.getObject1());
assertThat(log.getDifferences(), matchesExpectedDifferences());
}
@Test
public void logBeforeCommit() throws ServiceException, SecurityServiceException {
final TestEntity object = new TestEntity("object");
testEntityService.create(object);
final TestEntity secondaryObject = new TestEntity("secondaryObject");
testEntityService.create(secondaryObject);
HistoryValue expectedObjectHistoryValue = createExpectedHistoryValue(object);
HistoryValue expectedSecondaryObjectHistoryValue = createExpectedHistoryValue(secondaryObject);
entityService.flush();
entityService.clear();
Mockito.when(historyDifferenceGeneratorMock.toHistoryDifferences(
Matchers.<Supplier<TestHistoryDifference>>anyObject(), Matchers.<Difference<TestEntity>>anyObject()
))
.then(new Answer<List<TestHistoryDifference>>() {
@Override
public List<TestHistoryDifference> answer(InvocationOnMock invocation) throws Throwable {
return createExpectedDifferences();
}
});
// The value must be truncated because timestamps do not have the same precision as java.util.Date
final Date before = DateUtils.truncate(new Date(), Calendar.SECOND);
writeTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
@SuppressWarnings("unchecked")
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
TestEntity objectReloaded = entityService.getEntity(object);
TestEntity secondaryObjectReloaded = entityService.getEntity(secondaryObject);
try {
historyLogService.logWithDifferences(TestHistoryEventType.EVENT1, objectReloaded,
TestHistoryLogAdditionalInformationBean.of(secondaryObjectReloaded),
differenceGeneratorMock, historyDifferenceGeneratorMock);
} catch (ServiceException|SecurityServiceException e) {
throw new IllegalStateException(e);
}
}
});
final Date after = new Date();
List<TestHistoryLog> logs = historyLogService.list();
assertEquals(1, logs.size());
TestHistoryLog log = logs.iterator().next();
assertNotNull(log.getId());
assertThat(log.getDate(), new TypeSafeMatcher<Date>() {
@Override
public void describeTo(Description description) {
description.appendText("a date between ").appendValue(before).appendText(" and ").appendValue(after);
}
@Override
protected boolean matchesSafely(Date item) {
return !item.before(before) && !item.after(after);
}
});
assertEquals(TestHistoryEventType.EVENT1, log.getEventType());
assertEquals(expectedObjectHistoryValue, log.getMainObject());
assertEquals(expectedSecondaryObjectHistoryValue, log.getObject1());
assertThat(log.getDifferences(), matchesExpectedDifferences());
}
@Test
public void logBeforeClear() throws ServiceException, SecurityServiceException {
final TestEntity object = new TestEntity("object");
testEntityService.create(object);
final TestEntity secondaryObject = new TestEntity("secondaryObject");
testEntityService.create(secondaryObject);
HistoryValue expectedObjectHistoryValue = createExpectedHistoryValue(object);
HistoryValue expectedSecondaryObjectHistoryValue = createExpectedHistoryValue(secondaryObject);
entityService.flush();
entityService.clear();
Mockito.when(historyDifferenceGeneratorMock.toHistoryDifferences(
Matchers.<Supplier<TestHistoryDifference>>anyObject(), Matchers.<Difference<TestEntity>>anyObject()
))
.then(new Answer<List<TestHistoryDifference>>() {
@Override
public List<TestHistoryDifference> answer(InvocationOnMock invocation) throws Throwable {
return createExpectedDifferences();
}
});
// The value must be truncated because timestamps do not have the same precision as java.util.Date
final Date before = DateUtils.truncate(new Date(), Calendar.SECOND);
writeTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
@SuppressWarnings("unchecked")
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
TestEntity objectReloaded = entityService.getEntity(object);
TestEntity secondaryObjectReloaded = entityService.getEntity(secondaryObject);
try {
historyLogService.logWithDifferences(TestHistoryEventType.EVENT1, objectReloaded,
TestHistoryLogAdditionalInformationBean.of(secondaryObjectReloaded),
differenceGeneratorMock, historyDifferenceGeneratorMock);
// Simulate a batch treatment, that flushes and clears the session repeatedly
entityService.flush();
transactionSynchronizationService.beforeClear();
entityService.clear();
} catch (ServiceException|SecurityServiceException e) {
throw new IllegalStateException(e);
}
}
});
final Date after = new Date();
List<TestHistoryLog> logs = historyLogService.list();
assertEquals(1, logs.size());
TestHistoryLog log = logs.iterator().next();
assertNotNull(log.getId());
assertThat(log.getDate(), new TypeSafeMatcher<Date>() {
@Override
public void describeTo(Description description) {
description.appendText("a date between ").appendValue(before).appendText(" and ").appendValue(after);
}
@Override
protected boolean matchesSafely(Date item) {
return !item.before(before) && !item.after(after);
}
});
assertEquals(TestHistoryEventType.EVENT1, log.getEventType());
assertEquals(expectedObjectHistoryValue, log.getMainObject());
assertEquals(expectedSecondaryObjectHistoryValue, log.getObject1());
assertThat(log.getDifferences(), matchesExpectedDifferences());
}
}