package io.dropwizard.hibernate;
import org.glassfish.jersey.server.ExtendedUriInfo;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.hibernate.CacheMode;
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.context.internal.ManagedSessionContext;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import java.lang.reflect.Method;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.hibernate.resource.transaction.spi.TransactionStatus.ACTIVE;
import static org.hibernate.resource.transaction.spi.TransactionStatus.NOT_ACTIVE;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
@SuppressWarnings("HibernateResourceOpenedButNotSafelyClosed")
public class UnitOfWorkApplicationListenerTest {
private final SessionFactory sessionFactory = mock(SessionFactory.class);
private final SessionFactory analyticsSessionFactory = mock(SessionFactory.class);
private final UnitOfWorkApplicationListener listener = new UnitOfWorkApplicationListener();
private final ExtendedUriInfo uriInfo = mock(ExtendedUriInfo.class);
private final RequestEvent requestStartEvent = mock(RequestEvent.class);
private final RequestEvent requestMethodStartEvent = mock(RequestEvent.class);
private final RequestEvent responseFiltersStartEvent = mock(RequestEvent.class);
private final RequestEvent responseFinishedEvent = mock(RequestEvent.class);
private final RequestEvent requestMethodExceptionEvent = mock(RequestEvent.class);
private final Session session = mock(Session.class);
private final Session analyticsSession = mock(Session.class);
private final Transaction transaction = mock(Transaction.class);
private final Transaction analyticsTransaction = mock(Transaction.class);
@SuppressWarnings("unchecked")
@Before
public void setUp() throws Exception {
listener.registerSessionFactory(HibernateBundle.DEFAULT_NAME, sessionFactory);
listener.registerSessionFactory("analytics", analyticsSessionFactory);
when(sessionFactory.openSession()).thenReturn(session);
when(session.getSessionFactory()).thenReturn(sessionFactory);
when(session.beginTransaction()).thenReturn(transaction);
when(session.getTransaction()).thenReturn(transaction);
when(transaction.getStatus()).thenReturn(ACTIVE);
when(analyticsSessionFactory.openSession()).thenReturn(analyticsSession);
when(analyticsSession.getSessionFactory()).thenReturn(analyticsSessionFactory);
when(analyticsSession.beginTransaction()).thenReturn(analyticsTransaction);
when(analyticsSession.getTransaction()).thenReturn(analyticsTransaction);
when(analyticsTransaction.getStatus()).thenReturn(ACTIVE);
when(requestMethodStartEvent.getType()).thenReturn(RequestEvent.Type.RESOURCE_METHOD_START);
when(responseFinishedEvent.getType()).thenReturn(RequestEvent.Type.FINISHED);
when(requestMethodExceptionEvent.getType()).thenReturn(RequestEvent.Type.ON_EXCEPTION);
when(responseFiltersStartEvent.getType()).thenReturn(RequestEvent.Type.RESP_FILTERS_START);
when(requestMethodStartEvent.getUriInfo()).thenReturn(uriInfo);
when(responseFinishedEvent.getUriInfo()).thenReturn(uriInfo);
when(requestMethodExceptionEvent.getUriInfo()).thenReturn(uriInfo);
prepareResourceMethod("methodWithDefaultAnnotation");
}
@Test
public void opensAndClosesASession() throws Exception {
execute();
final InOrder inOrder = inOrder(sessionFactory, session);
inOrder.verify(sessionFactory).openSession();
inOrder.verify(session).close();
}
@Test
public void bindsAndUnbindsTheSessionToTheManagedContext() throws Exception {
doAnswer(invocation -> {
assertThat(ManagedSessionContext.hasBind(sessionFactory))
.isTrue();
return null;
}).when(session).beginTransaction();
execute();
assertThat(ManagedSessionContext.hasBind(sessionFactory)).isFalse();
}
@Test
public void configuresTheSessionsReadOnlyDefault() throws Exception {
prepareResourceMethod("methodWithReadOnlyAnnotation");
execute();
verify(session).setDefaultReadOnly(true);
}
@Test
public void configuresTheSessionsCacheMode() throws Exception {
prepareResourceMethod("methodWithCacheModeIgnoreAnnotation");
execute();
verify(session).setCacheMode(CacheMode.IGNORE);
}
@Test
public void configuresTheSessionsFlushMode() throws Exception {
prepareResourceMethod("methodWithFlushModeAlwaysAnnotation");
execute();
verify(session).setHibernateFlushMode(FlushMode.ALWAYS);
}
@Test
public void doesNotBeginATransactionIfNotTransactional() throws Exception {
final String resourceMethodName = "methodWithTransactionalFalseAnnotation";
prepareResourceMethod(resourceMethodName);
when(session.getTransaction()).thenReturn(null);
execute();
verify(session, never()).beginTransaction();
verifyZeroInteractions(transaction);
}
@Test
public void detectsAnnotationOnHandlingMethod() throws NoSuchMethodException {
final String resourceMethodName = "handlingMethodAnnotated";
prepareResourceMethod(resourceMethodName);
execute();
verify(session).setDefaultReadOnly(true);
}
@Test
public void detectsAnnotationOnDefinitionMethod() throws NoSuchMethodException {
final String resourceMethodName = "definitionMethodAnnotated";
prepareResourceMethod(resourceMethodName);
execute();
verify(session).setDefaultReadOnly(true);
}
@Test
public void annotationOnDefinitionMethodOverridesHandlingMethod() throws NoSuchMethodException {
final String resourceMethodName = "bothMethodsAnnotated";
prepareResourceMethod(resourceMethodName);
execute();
verify(session).setDefaultReadOnly(true);
}
@Test
public void beginsAndCommitsATransactionIfTransactional() throws Exception {
execute();
final InOrder inOrder = inOrder(session, transaction);
inOrder.verify(session).beginTransaction();
inOrder.verify(transaction).commit();
inOrder.verify(session).close();
}
@Test
public void rollsBackTheTransactionOnException() throws Exception {
executeWithException();
final InOrder inOrder = inOrder(session, transaction);
inOrder.verify(session).beginTransaction();
inOrder.verify(transaction).rollback();
inOrder.verify(session).close();
}
@Test
public void doesNotCommitAnInactiveTransaction() throws Exception {
when(transaction.getStatus()).thenReturn(NOT_ACTIVE);
execute();
verify(transaction, never()).commit();
}
@Test
public void doesNotCommitANullTransaction() throws Exception {
when(session.getTransaction()).thenReturn(null);
execute();
verify(transaction, never()).commit();
}
@Test
public void doesNotRollbackAnInactiveTransaction() throws Exception {
when(transaction.getStatus()).thenReturn(NOT_ACTIVE);
executeWithException();
verify(transaction, never()).rollback();
}
@Test
public void doesNotRollbackANullTransaction() throws Exception {
when(session.getTransaction()).thenReturn(null);
executeWithException();
verify(transaction, never()).rollback();
}
@Test
public void beginsAndCommitsATransactionForAnalytics() throws Exception {
prepareResourceMethod("methodWithUnitOfWorkOnAnalyticsDatabase");
execute();
final InOrder inOrder = inOrder(analyticsSession, analyticsTransaction);
inOrder.verify(analyticsSession).beginTransaction();
inOrder.verify(analyticsTransaction).commit();
inOrder.verify(analyticsSession).close();
}
@Test
public void throwsExceptionOnNotRegisteredDatabase() throws Exception {
prepareResourceMethod("methodWithUnitOfWorkOnNotRegisteredDatabase");
assertThatThrownBy(this::execute)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unregistered Hibernate bundle: 'warehouse'");
}
private void prepareResourceMethod(String resourceMethodName) throws NoSuchMethodException {
final Method handlingMethod = MockResource.class.getMethod(resourceMethodName);
Method definitionMethod = handlingMethod;
Class<?> interfaceClass = MockResource.class.getInterfaces()[0];
if (methodDefinedOnInterface(resourceMethodName, interfaceClass.getMethods())) {
definitionMethod = interfaceClass.getMethod(resourceMethodName);
}
when(uriInfo.getMatchedResourceMethod()).thenReturn(Resource.builder()
.addMethod()
.handlingMethod(handlingMethod)
.handledBy(new MockResource(), definitionMethod)
.build());
}
private static boolean methodDefinedOnInterface(String methodName, Method[] methods) {
for (Method method : methods) {
if (method.getName().equals(methodName)) {
return true;
}
}
return false;
}
private void execute() {
RequestEventListener requestListener = listener.onRequest(requestStartEvent);
requestListener.onEvent(requestMethodStartEvent);
requestListener.onEvent(responseFiltersStartEvent);
requestListener.onEvent(responseFinishedEvent);
}
private void executeWithException() {
RequestEventListener requestListener = listener.onRequest(requestStartEvent);
requestListener.onEvent(requestMethodStartEvent);
requestListener.onEvent(responseFiltersStartEvent);
requestListener.onEvent(requestMethodExceptionEvent);
requestListener.onEvent(responseFinishedEvent);
}
public static class MockResource implements MockResourceInterface {
@UnitOfWork(readOnly = false, cacheMode = CacheMode.NORMAL, transactional = true, flushMode = FlushMode.AUTO)
public void methodWithDefaultAnnotation() {
}
@UnitOfWork(readOnly = true, cacheMode = CacheMode.NORMAL, transactional = true, flushMode = FlushMode.AUTO)
public void methodWithReadOnlyAnnotation() {
}
@UnitOfWork(readOnly = false, cacheMode = CacheMode.IGNORE, transactional = true, flushMode = FlushMode.AUTO)
public void methodWithCacheModeIgnoreAnnotation() {
}
@UnitOfWork(readOnly = false, cacheMode = CacheMode.NORMAL, transactional = true, flushMode = FlushMode.ALWAYS)
public void methodWithFlushModeAlwaysAnnotation() {
}
@UnitOfWork(readOnly = false, cacheMode = CacheMode.NORMAL, transactional = false, flushMode = FlushMode.AUTO)
public void methodWithTransactionalFalseAnnotation() {
}
@UnitOfWork(readOnly = true)
@Override
public void handlingMethodAnnotated() {
}
@Override
public void definitionMethodAnnotated() {
}
@UnitOfWork(readOnly = false)
@Override
public void bothMethodsAnnotated() {
}
@UnitOfWork("analytics")
public void methodWithUnitOfWorkOnAnalyticsDatabase() {
}
@UnitOfWork("warehouse")
public void methodWithUnitOfWorkOnNotRegisteredDatabase() {
}
}
public static interface MockResourceInterface {
void handlingMethodAnnotated();
@UnitOfWork(readOnly = true)
void definitionMethodAnnotated();
@UnitOfWork(readOnly = true)
void bothMethodsAnnotated();
}
}