diff --git a/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSessionEventsIntegrationTests.java b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSessionEventsIntegrationTests.java index f6edc31..0ce25f3 100644 --- a/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSessionEventsIntegrationTests.java +++ b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSessionEventsIntegrationTests.java @@ -54,10 +54,10 @@ import org.springframework.test.context.web.WebAppConfiguration; * of the {@link GemFireOperationsSessionRepository} and GemFire's configuration. * * @author John Blum - * @since 1.1.0 * @see org.junit.Test * @see org.junit.runner.RunWith * @see org.apache.geode.cache.Region + * @see org.springframework.data.gemfire.config.annotation.PeerCacheApplication * @see org.springframework.session.Session * @see org.springframework.session.data.gemfire.AbstractGemFireIntegrationTests * @see org.springframework.session.data.gemfire.GemFireOperationsSessionRepository @@ -68,6 +68,7 @@ import org.springframework.test.context.web.WebAppConfiguration; * @see org.springframework.test.context.ContextConfiguration * @see org.springframework.test.context.junit4.SpringRunner * @see org.springframework.test.context.web.WebAppConfiguration + * @since 1.1.0 */ @RunWith(SpringRunner.class) @ContextConfiguration @@ -164,6 +165,7 @@ public class EnableGemFireHttpSessionEventsIntegrationTests extends AbstractGemF Session expectedSession = save(touch(createSession())); Session savedSession = this.gemfireSessionRepository.findById(expectedSession.getId()); + assertThat(savedSession).isNotNull(); assertThat(savedSession).isEqualTo(expectedSession); assertThat(savedSession.isExpired()).isFalse(); diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java index 89a88da..6e064db 100644 --- a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java @@ -38,15 +38,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.apache.geode.DataSerializable; import org.apache.geode.DataSerializer; import org.apache.geode.Delta; -import org.apache.geode.Instantiator; import org.apache.geode.InvalidDeltaException; -import org.apache.geode.cache.AttributesMutator; import org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.InterestResultPolicy; import org.apache.geode.cache.Operation; import org.apache.geode.cache.Region; import org.apache.geode.cache.util.CacheListenerAdapter; -import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; @@ -62,6 +60,8 @@ import org.springframework.session.SessionRepository; import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration; import org.springframework.session.data.gemfire.support.GemFireUtils; import org.springframework.session.data.gemfire.support.SessionIdHolder; +import org.springframework.session.data.gemfire.support.SessionUtils; +import org.springframework.session.events.AbstractSessionEvent; import org.springframework.session.events.SessionCreatedEvent; import org.springframework.session.events.SessionDeletedEvent; import org.springframework.session.events.SessionDestroyedEvent; @@ -78,35 +78,45 @@ import org.apache.commons.logging.LogFactory; * common to all implementations that support {@link SessionRepository} operations backed by Apache Geode. * * @author John Blum + * @see java.time.Duration + * @see java.time.Instant * @see org.apache.geode.DataSerializable * @see org.apache.geode.DataSerializer * @see org.apache.geode.Delta - * @see org.apache.geode.Instantiator * @see org.apache.geode.cache.EntryEvent * @see org.apache.geode.cache.Operation * @see org.apache.geode.cache.Region - * @see org.springframework.beans.factory.InitializingBean + * @see org.apache.geode.cache.util.CacheListenerAdapter * @see org.springframework.context.ApplicationEvent * @see org.springframework.context.ApplicationEventPublisher * @see org.springframework.context.ApplicationEventPublisherAware * @see org.springframework.data.gemfire.GemfireOperations - * @see org.springframework.expression.Expression * @see org.springframework.session.FindByIndexNameSessionRepository * @see org.springframework.session.Session * @see org.springframework.session.SessionRepository * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration * @see org.springframework.session.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession + * @see org.springframework.session.data.gemfire.support.SessionIdHolder + * @see org.springframework.session.events.AbstractSessionEvent * @see org.springframework.session.events.SessionCreatedEvent * @see org.springframework.session.events.SessionDeletedEvent * @see org.springframework.session.events.SessionDestroyedEvent * @see org.springframework.session.events.SessionExpiredEvent * @since 1.1.0 */ -public abstract class AbstractGemFireOperationsSessionRepository extends CacheListenerAdapter - implements ApplicationEventPublisherAware, FindByIndexNameSessionRepository, InitializingBean { +public abstract class AbstractGemFireOperationsSessionRepository + implements ApplicationEventPublisherAware, FindByIndexNameSessionRepository { + + private static final boolean DEFAULT_REGISTER_INTEREST_ENABLED = false; + private static final boolean DEFAULT_REGISTER_INTEREST_DURABILITY = false; + private static final boolean DEFAULT_REGISTER_INTEREST_RECEIVE_VALUES = false; private static final AtomicBoolean usingDataSerialization = new AtomicBoolean(false); + private static final InterestResultPolicy DEFAULT_REGISTER_INTEREST_RESULT_POLICY = InterestResultPolicy.NONE; + + private boolean registerInterestEnabled = DEFAULT_REGISTER_INTEREST_ENABLED; + private ApplicationEventPublisher applicationEventPublisher = event -> {}; private Duration maxInactiveInterval = @@ -116,32 +126,111 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi private final Log logger = newLogger(); - private final Set cachedSessionIds = new ConcurrentSkipListSet<>(); + private final Region sessions; - private String fullyQualifiedRegionName; + private SessionEventHandlerCacheListenerAdapter sessionEventHandler; + + private final Set interestingSessionIds = new ConcurrentSkipListSet<>(); /** * Protected, default constructor used by extensions of {@link AbstractGemFireOperationsSessionRepository} * in order to affect and assess {@link SessionRepository} configuration and state. */ protected AbstractGemFireOperationsSessionRepository() { + + this.sessions = null; this.template = null; } /** - * Constructs an instance of {@link AbstractGemFireOperationsSessionRepository} - * with a required {@link GemfireOperations} instance used to perform Pivotal GemFire data access operations - * and interactions supporting the SessionRepository operations. + * Constructs a new instance of {@link AbstractGemFireOperationsSessionRepository} initialized with a required + * {@link GemfireOperations} object, which is used to perform Apache Geode or Pivotal GemFire data access operations + * on the cache {@link Region} storing and managing {@link Session} state to support this {@link SessionRepository} + * and its operations. * - * @param template {@link GemfireOperations} instance used to interact with GemFire; must not be {@literal null}. + * @param template {@link GemfireOperations} object used to interact with the Apache Geode or Pivotal GemFire + * cache {@link Region} storing and managing {@link Session} state; must not be {@literal null}. * @throws IllegalArgumentException if {@link GemfireOperations} is {@literal null}. * @see org.springframework.data.gemfire.GemfireOperations + * @see #resolveSessionsRegion(GemfireOperations) + * @see #initializeSessionsRegion(Region) + * @see #newLogger() */ public AbstractGemFireOperationsSessionRepository(GemfireOperations template) { Assert.notNull(template, "GemfireOperations is required"); this.template = template; + this.sessions = initializeSessionsRegion(resolveSessionsRegion(template)); + } + + /** + * Resolves the cache {@link Region} used to store and manage {@link Session} state + * from the given {@link GemfireOperations} object. + * + * @param gemfireOperations {@link GemfireOperations} object used to resolve the {@link Session} {@link Region}. + * @return the resolve cache {@link Region} used to store and manage {@link Session} state. + * @throws IllegalStateException if the {@link Session Sessions} {@link Region} could not be resolved. + * @see org.springframework.data.gemfire.GemfireOperations + * @see org.springframework.session.Session + * @see org.apache.geode.cache.Region + */ + private Region resolveSessionsRegion(@Nullable GemfireOperations gemfireOperations) { + + return Optional.ofNullable(gemfireOperations) + .filter(GemfireAccessor.class::isInstance) + .map(GemfireAccessor.class::cast) + .>map(GemfireAccessor::getRegion) + .orElseThrow(() -> newIllegalStateException("The ClusteredSpringSessions Region could not be resolved")); + } + + /** + * Initializes the cache {@link Region} used to store and manage {@link Session} state and register this + * {@link SessionRepository} as an Apache Geode / Pivotal GemFire {@link org.apache.geode.cache.CacheListener}. + * + * @param sessionsRegion {@link Region} to initialize. + * @return the given {@link Region}. + * @see org.apache.geode.cache.Region + * @see #newSessionEventHandler() + * @see #newSessionIdInterestRegistrar() + */ + private Region initializeSessionsRegion(@Nullable Region sessionsRegion) { + + Optional.ofNullable(sessionsRegion) + .map(Region::getAttributesMutator) + .ifPresent(sessionsRegionAttributesMutator -> { + + this.sessionEventHandler = newSessionEventHandler(); + + sessionsRegionAttributesMutator.addCacheListener(this.sessionEventHandler); + + if (GemFireUtils.isNonLocalClientRegion(sessionsRegion)) { + this.registerInterestEnabled = true; + sessionsRegionAttributesMutator.addCacheListener(newSessionIdInterestRegistrar()); + } + }); + + return sessionsRegion; + } + + /** + * Constructs a new instance of {@link SessionEventHandlerCacheListenerAdapter}. + * + * @return a new instance of {@link SessionEventHandlerCacheListenerAdapter}. + * @see SessionEventHandlerCacheListenerAdapter + */ + protected SessionEventHandlerCacheListenerAdapter newSessionEventHandler() { + return new SessionEventHandlerCacheListenerAdapter(this); + } + + /** + * Constructs a new instance of {@link SessionIdInterestRegisteringCacheListener}. + * + * @return a new instance of {@link SessionIdInterestRegisteringCacheListener}. + * @see SessionIdInterestRegisteringCacheListener + */ + protected SessionIdInterestRegisteringCacheListener newSessionIdInterestRegistrar() { + return new SessionIdInterestRegisteringCacheListener(this); } /** @@ -156,15 +245,15 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi } /** - * Sets the ApplicationEventPublisher used to publish Session events corresponding to - * Pivotal GemFire cache events. + * Sets the configured {@link ApplicationEventPublisher} used to publish {@link Session} + * {@link AbstractSessionEvent events} corresponding to Apache Geode/Pivotal GemFire cache events. * - * @param applicationEventPublisher the Spring ApplicationEventPublisher used to - * publish Session-based events; must not be {@literal null}. + * @param applicationEventPublisher {@link ApplicationEventPublisher} used to publish {@link Session}-based events; + * must not be {@literal null}. * @throws IllegalArgumentException if {@link ApplicationEventPublisher} is {@literal null}. * @see org.springframework.context.ApplicationEventPublisher */ - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + public void setApplicationEventPublisher(@NonNull ApplicationEventPublisher applicationEventPublisher) { Assert.notNull(applicationEventPublisher, "ApplicationEventPublisher is required"); @@ -172,24 +261,37 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi } /** - * Returns a reference to the {@link ApplicationEventPublisher} used to publish {@link Session} events - * corresponding to GemFire/Geode cache events. + * Returns a reference to the configured {@link ApplicationEventPublisher} used to publish {@link Session} + * {@link AbstractSessionEvent events} corresponding to Apache Geode/Pivotal GemFire cache events. * - * @return the Spring {@link ApplicationEventPublisher} used to publish {@link Session} events. + * @return the configured {@link ApplicationEventPublisher} used to publish {@link Session} + * {@link AbstractSessionEvent events}. * @see org.springframework.context.ApplicationEventPublisher */ - protected ApplicationEventPublisher getApplicationEventPublisher() { + protected @NonNull ApplicationEventPublisher getApplicationEventPublisher() { return this.applicationEventPublisher; } /** - * Returns the fully-qualified name of the cache {@link Region} used to store and manage {@link Session} state. + * Returns the {@link String fully-qualified name} of the cache {@link Region} used to store + * and manage {@link Session} state. * * @return a {@link String} containing the fully qualified name of the cache {@link Region} * used to store and manage {@link Session} data. + * @see #getSessionsRegion() */ protected String getFullyQualifiedRegionName() { - return this.fullyQualifiedRegionName; + return getSessionsRegion().getFullPath(); + } + + /** + * Determines whether {@link Region} {@literal register interest} is enabled + * in the current Apache Geode / Pivotal GemFire configuration. + * + * @return a boolean value indicating whether interest registration is enabled. + */ + protected boolean isRegisterInterestEnabled() { + return this.registerInterestEnabled; } /** @@ -254,17 +356,43 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi .orElse(0); } + protected Optional getSessionEventHandler() { + return Optional.ofNullable(this.sessionEventHandler); + } + /** - * Gets a reference to the {@link GemfireOperations template} used to perform data access operations - * and other interactions on the cache {@link Region} backing this {@link SessionRepository}. + * Returns a reference to the configured Apache Geode / Pivotal GemFire cache {@link Region} used to + * store and manage (HTTP) {@link Session} data. * - * @return a reference to the {@link GemfireOperations template} used to interact with GemFire/Geode. + * @return a reference to the configured {@link Session Sessions} {@link Region}. + * @see org.springframework.session.Session + * @see org.apache.geode.cache.Region + */ + protected @NonNull Region getSessionsRegion() { + return this.sessions; + } + + /** + * Returns a reference to the {@link GemfireOperations template} used to perform data access operations + * and other interactions on the cache {@link Region} storing and managing {@link Session} state + * and backing this {@link SessionRepository}. + * + * @return a reference to the {@link GemfireOperations template} used to interact the {@link Region} + * storing and managing {@link Session} state. * @see org.springframework.data.gemfire.GemfireOperations */ - public GemfireOperations getTemplate() { + public @NonNull GemfireOperations getSessionsTemplate() { return this.template; } + /** + * @deprecated use {@link #getSessionsTemplate()}. + */ + @Deprecated + public @NonNull GemfireOperations getTemplate() { + return getSessionsTemplate(); + } + /** * Sets a condition indicating whether the DataSerialization framework has been configured. * @@ -283,175 +411,10 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi return usingDataSerialization.get(); } - /** - * Callback method during Spring bean initialization that will capture the fully-qualified name - * of the cache {@link Region} used to manage {@link Session} state and register this {@link SessionRepository} - * as a GemFire/Geode {@link org.apache.geode.cache.CacheListener}. - * - * Additionally, this method registers GemFire/Geode {@link Instantiator Instantiators} - * for the {@link GemFireSession} and {@link GemFireSessionAttributes} types to optimize GemFire/Geode's - * instantiation logic on deserialization using the data serialization framework when accessing the stored - * {@link Session} state. - * - * @throws Exception if an error occurs during the initialization process. - */ - public void afterPropertiesSet() throws Exception { - - GemfireOperations template = getTemplate(); - - Assert.isInstanceOf(GemfireAccessor.class, template); - - Region region = ((GemfireAccessor) template).getRegion(); - - this.fullyQualifiedRegionName = region.getFullPath(); - - AttributesMutator attributesMutator = region.getAttributesMutator(); - - attributesMutator.addCacheListener(this); - } - - boolean isCreate(EntryEvent event) { - return isCreate(event.getOperation()) && isNotUpdate(event) && isSession(event.getNewValue()); - } - - private boolean isCreate(Operation operation) { - return operation.isCreate() && !Operation.LOCAL_LOAD_CREATE.equals(operation); - } - - private boolean isNotUpdate(EntryEvent event) { - return isNotProxyRegion() || !this.cachedSessionIds.contains(ObjectUtils.nullSafeHashCode(event.getKey())); - } - - private boolean isNotProxyRegion() { - return !isProxyRegion(); - } - - private boolean isProxyRegion() { - return GemFireUtils.isProxy(((GemfireAccessor) getTemplate()).getRegion()); - } - - /** - * Used to determine whether the application developer is storing (HTTP) Sessions with other, arbitrary - * application domain objects in the same Pivotal GemFire cache {@link Region}; crazier things have happened! - * - * @param obj {@link Object} to evaluate. - * @return a boolean value indicating whether the old/new {@link Object} from the {@link Region} - * {@link EntryEvent} is indeed a {@link Session}. - * @see org.springframework.session.Session - */ - private boolean isSession(Object obj) { - return obj instanceof Session; - } - - /** - * Forgets the given {@link Object session ID}. - * - * @param sessionId {@link Object} containing the session ID to forget. - * @return a boolean value indicating whether the given session ID was even being remembered. - * @see #remember(Object) - */ - boolean forget(Object sessionId) { - return this.cachedSessionIds.remove(ObjectUtils.nullSafeHashCode(sessionId)); - } - - /** - * Remembers the given {@link Object session ID}. - * - * @param sessionId {@link Object} containing the session ID to remember. - * @return a boolean value whether Spring Session is interested in and will remember - * this given session ID. - * @see #forget(Object) - */ - @SuppressWarnings("all") - boolean remember(Object sessionId) { - return isProxyRegion() && this.cachedSessionIds.add(ObjectUtils.nullSafeHashCode(sessionId)); - } - - /** - * Casts the given {@link Object} into a {@link Session} iff the {@link Object} is a {@link Session}. - * - * Otherwise, this method attempts to use the supplied {@link String session ID} to create a {@link Session} - * containing only the ID. - * - * @param obj {@link Object} to evaluate as a {@link Session}. - * @param sessionId {@link String} containing the session ID. - * @return a {@link Session} from the given {@link Object} - * or a {@link Session} containing only the supplied {@link String session ID}. - * @throws IllegalStateException if the given {@link Object} is not a {@link Session} - * and {@link String session ID} was not supplied. - */ - Session toSession(Object obj, String sessionId) { - - return obj instanceof Session - ? (Session) obj - : Optional.ofNullable(sessionId) - .filter(StringUtils::hasText) - .map(SessionIdHolder::create) - .orElseThrow(() -> newIllegalStateException( - "Minimally, the session ID [%s] must be known to trigger a Session event", sessionId)); - } - /** - * Callback method triggered when an entry is created in the Pivotal GemFire cache {@link Region}. - * - * @param event {@link EntryEvent} containing the details of the cache operation. - * @see org.apache.geode.cache.EntryEvent - * @see #handleCreated(String, Session) - */ - @Override - public void afterCreate(EntryEvent event) { - - Optional.ofNullable(event) - .filter(this::isCreate) - .ifPresent(it -> { - - String sessionId = it.getKey().toString(); - - handleCreated(sessionId, toSession(it.getNewValue(), sessionId)); - }); - } - - /** - * Callback method triggered when an entry is destroyed in the Pivotal GemFire cache {@link Region}. - * - * @param event {@link EntryEvent} containing the details of the cache operation. - * @see org.apache.geode.cache.EntryEvent - * @see #handleDestroyed(String, Session) - */ - @Override - public void afterDestroy(EntryEvent event) { - - Optional.ofNullable(event) - .ifPresent(it -> { - - String sessionId = event.getKey().toString(); - - handleDestroyed(sessionId, toSession(event.getOldValue(), sessionId)); - }); - } - - /** - * Callback method triggered when an entry is invalidated in the Pivotal GemFire cache {@link Region}. - * - * @param event {@link EntryEvent} containing the details of the cache operation. - * @see org.apache.geode.cache.EntryEvent - * @see #handleExpired(String, Session) - */ - @Override - public void afterInvalidate(EntryEvent event) { - - Optional.ofNullable(event) - .ifPresent(it -> { - - String sessionId = event.getKey().toString(); - - handleExpired(sessionId, toSession(event.getOldValue(), sessionId)); - }); - } - /** * Commits the given {@link Session}. * - * @param session {@link Session} to commit, if committable. + * @param session {@link Session} to commit, iff the {@link Session} is {@literal committable}. * @return the given {@link Session} * @see GemFireSession#commit() */ @@ -468,14 +431,15 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi } /** - * Deletes the given {@link Session} from GemFire. + * Deletes the given {@link Session} from Apache Geode / Pivotal GemFire. * * @param session {@link Session} to delete. * @return {@literal null}. * @see org.springframework.session.Session#getId() + * @see org.springframework.session.Session * @see #deleteById(String) */ - protected Session delete(Session session) { + protected Session delete(@NonNull Session session) { deleteById(session.getId()); @@ -483,88 +447,27 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi } /** - * Causes Session created events to be published to the Spring application context. + * Handles the deletion of the given {@link Session}. * - * @param sessionId a String indicating the ID of the Session. - * @param session a reference to the Session triggering the event. - * @see org.springframework.session.events.SessionCreatedEvent + * @param sessionId {@link String} containing the {@link Session#getId()} of the given {@link Session}. + * @param session deleted {@link Session}. + * @see SessionEventHandlerCacheListenerAdapter#handleDeleted(String, Session) * @see org.springframework.session.Session - * @see #newSessionCreatedEvent(Session) - * @see #publishEvent(ApplicationEvent) - */ - protected void handleCreated(String sessionId, Session session) { - remember(sessionId); - publishEvent(newSessionCreatedEvent(session)); - } - - /** - * Causes Session deleted events to be published to the Spring application context. - * - * @param sessionId a String indicating the ID of the Session. - * @param session a reference to the Session triggering the event. - * @see org.springframework.session.events.SessionDeletedEvent - * @see org.springframework.session.Session - * @see #newSessionDeletedEvent(Session) - * @see #publishEvent(ApplicationEvent) - * @see #forget(Object) + * @see #unregisterInterest(Object) */ protected void handleDeleted(String sessionId, Session session) { - forget(sessionId); - publishEvent(newSessionDeletedEvent(session)); + + getSessionEventHandler() + .ifPresent(it -> it.handleDeleted(sessionId, session)); + + unregisterInterest(sessionId); } /** - * Causes Session destroyed events to be published to the Spring application context. + * Publishes the specified {@link ApplicationEvent} to the Spring container thereby notifying other (potentially) + * interested application components/beans. * - * @param sessionId a String indicating the ID of the Session. - * @param session a reference to the Session triggering the event. - * @see org.springframework.session.events.SessionDestroyedEvent - * @see org.springframework.session.Session - * @see #newSessionDestroyedEvent(Session) - * @see #publishEvent(ApplicationEvent) - * @see #forget(Object) - */ - protected void handleDestroyed(String sessionId, Session session) { - forget(sessionId); - publishEvent(newSessionDestroyedEvent(session)); - } - - /** - * Causes Session expired events to be published to the Spring application context. - * - * @param sessionId a String indicating the ID of the Session. - * @param session a reference to the Session triggering the event. - * @see org.springframework.session.events.SessionExpiredEvent - * @see org.springframework.session.Session - * @see #newSessionExpiredEvent(Session) - * @see #publishEvent(ApplicationEvent) - * @see #forget(Object) - */ - protected void handleExpired(String sessionId, Session session) { - forget(sessionId); - publishEvent(newSessionExpiredEvent(session)); - } - - private SessionCreatedEvent newSessionCreatedEvent(Session session) { - return new SessionCreatedEvent(this, session); - } - - private SessionDeletedEvent newSessionDeletedEvent(Session session) { - return new SessionDeletedEvent(this, session); - } - - private SessionDestroyedEvent newSessionDestroyedEvent(Session session) { - return new SessionDestroyedEvent(this, session); - } - - private SessionExpiredEvent newSessionExpiredEvent(Session session) { - return new SessionExpiredEvent(this, session); - } - - /** - * Publishes the specified ApplicationEvent to the Spring application context. - * - * @param event the ApplicationEvent to publish. + * @param event {@link ApplicationEvent} to publish. * @see org.springframework.context.ApplicationEventPublisher#publishEvent(ApplicationEvent) * @see org.springframework.context.ApplicationEvent */ @@ -579,20 +482,99 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi } /** - * Updates the {@link Session#setLastAccessedTime(Instant)} property of the {@link Session}. + * Registers interest in the given {@link Session} in order to receive notifications and updates. + * + * @param session {@link Session} of interest to this application that will be registered. + * @return the given {@link Session}. + * @see org.springframework.session.Session#getId() + * @see org.springframework.session.Session + * @see #registerInterest(Object) + */ + protected Session registerInterest(@Nullable Session session) { + + Optional.ofNullable(session) + .map(Session::getId) + .ifPresent(this::registerInterest); + + return session; + } + + /** + * Registers interest on the {@link Session#getId()} ID} of a {@link Session}. + * + * And, only registers interest in the given Session ID iff we have not already registered interest + * in this Session ID before. + * + * @param sessionId {@link Session#getId() ID} of the {@link Session} of interest to this application. + * @see org.apache.geode.cache.Region#registerInterest(Object, InterestResultPolicy, boolean, boolean) + * @see #isRegisterInterestEnabled() + */ + protected void registerInterest(Object sessionId) { + + Optional.ofNullable(sessionId) + .filter(it -> this.isRegisterInterestEnabled()) + .filter(SessionUtils::isValidSessionId) + .map(ObjectUtils::nullSafeHashCode) + .filter(this.interestingSessionIds::add) + .ifPresent(it -> + getSessionsRegion().registerInterest(sessionId, DEFAULT_REGISTER_INTEREST_RESULT_POLICY, + DEFAULT_REGISTER_INTEREST_DURABILITY, DEFAULT_REGISTER_INTEREST_RECEIVE_VALUES) + ); + } + + /** + * Updates the {@link Session#setLastAccessedTime(Instant)} property of the {@link Session} + * to the {@link Instant#now() current time}. * * @param session {@link Session} to touch. * @return the {@link Session}. * @see org.springframework.session.Session#setLastAccessedTime(Instant) + * @see org.springframework.session.Session * @see java.time.Instant#now() */ - protected Session touch(Session session) { + protected @NonNull Session touch(@NonNull Session session) { session.setLastAccessedTime(Instant.now()); return session; } + /** + * Unregisters interest in the given {@link Session} in order to stop notifications and updates. + * + * @param session {@link Session} no longer of any interest to this application that will be unregistered. + * @return the given {@link Session}. + * @see org.springframework.session.Session#getId() + * @see org.springframework.session.Session + * @see #unregisterInterest(Object) + */ + @SuppressWarnings("unused") + protected Session unregisterInterest(@Nullable Session session) { + + Optional.ofNullable(session) + .map(Session::getId) + .ifPresent(this::unregisterInterest); + + return session; + } + + /** + * Unregisters interest on the {@link Session#getId()} ID} of a {@link Session}. + * + * @param sessionId {@link Session#getId() ID} of the {@link Session} no longer of any interest + * to this application. + * @see org.apache.geode.cache.Region#unregisterInterest(Object) + * @see #isRegisterInterestEnabled() + */ + protected void unregisterInterest(@Nullable Object sessionId) { + + Optional.ofNullable(sessionId) + .filter(it -> this.isRegisterInterestEnabled()) + .map(ObjectUtils::nullSafeHashCode) + .filter(this.interestingSessionIds::remove) + .ifPresent(it -> getSessionsRegion().unregisterInterest(sessionId)); + } + @SuppressWarnings("unused") public static class DeltaCapableGemFireSession extends GemFireSession implements Delta { @@ -1273,4 +1255,275 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi } } } + + protected static class SessionEventHandlerCacheListenerAdapter extends CacheListenerAdapter { + + private final AbstractGemFireOperationsSessionRepository sessionRepository; + + private final Set cachedSessionIds = new ConcurrentSkipListSet<>(); + + protected SessionEventHandlerCacheListenerAdapter(AbstractGemFireOperationsSessionRepository sessionRepository) { + + Assert.notNull(sessionRepository, "SessionRepository is required"); + + this.sessionRepository = sessionRepository; + } + + protected AbstractGemFireOperationsSessionRepository getSessionRepository() { + return this.sessionRepository; + } + + /** + * Callback method triggered when an entry is created in the Pivotal GemFire cache {@link Region}. + * + * @param event {@link EntryEvent} containing the details of the cache operation. + * @see org.apache.geode.cache.EntryEvent + * @see #handleCreated(String, Session) + */ + @Override + public void afterCreate(EntryEvent event) { + + Optional.ofNullable(event) + .filter(this::isCreate) + .ifPresent(it -> { + + String sessionId = it.getKey().toString(); + + handleCreated(sessionId, toSession(it.getNewValue(), sessionId)); + }); + } + + /** + * Causes Session created events to be published to the Spring application context. + * + * @param sessionId a String indicating the ID of the Session. + * @param session a reference to the Session triggering the event. + * @see org.springframework.session.events.SessionCreatedEvent + * @see org.springframework.session.Session + * @see #newSessionCreatedEvent(Session) + * @see #publishEvent(ApplicationEvent) + */ + protected void handleCreated(String sessionId, Session session) { + + remember(sessionId); + getSessionRepository().publishEvent(newSessionCreatedEvent(session)); + } + + private SessionCreatedEvent newSessionCreatedEvent(Session session) { + return new SessionCreatedEvent(getSessionRepository(), session); + } + + /** + * Callback method triggered when an entry is destroyed in the Pivotal GemFire cache {@link Region}. + * + * @param event {@link EntryEvent} containing the details of the cache operation. + * @see org.apache.geode.cache.EntryEvent + * @see #handleDestroyed(String, Session) + */ + @Override + public void afterDestroy(EntryEvent event) { + + Optional.ofNullable(event) + .ifPresent(it -> { + + String sessionId = event.getKey().toString(); + + handleDestroyed(sessionId, toSession(event.getOldValue(), sessionId)); + }); + } + + /** + * Causes Session destroyed events to be published to the Spring application context. + * + * @param sessionId a String indicating the ID of the Session. + * @param session a reference to the Session triggering the event. + * @see org.springframework.session.events.SessionDestroyedEvent + * @see org.springframework.session.Session + * @see #newSessionDestroyedEvent(Session) + * @see #publishEvent(ApplicationEvent) + * @see #forget(Object) + */ + protected void handleDestroyed(String sessionId, Session session) { + + forget(sessionId); + getSessionRepository().publishEvent(newSessionDestroyedEvent(session)); + } + + private SessionDestroyedEvent newSessionDestroyedEvent(Session session) { + return new SessionDestroyedEvent(getSessionRepository(), session); + } + + /** + * Callback method triggered when an entry is invalidated in the Pivotal GemFire cache {@link Region}. + * + * @param event {@link EntryEvent} containing the details of the cache operation. + * @see org.apache.geode.cache.EntryEvent + * @see #handleExpired(String, Session) + */ + @Override + public void afterInvalidate(EntryEvent event) { + + Optional.ofNullable(event) + .ifPresent(it -> { + + String sessionId = event.getKey().toString(); + + handleExpired(sessionId, toSession(event.getOldValue(), sessionId)); + }); + } + + /** + * Causes Session expired events to be published to the Spring application context. + * + * @param sessionId a String indicating the ID of the Session. + * @param session a reference to the Session triggering the event. + * @see org.springframework.session.events.SessionExpiredEvent + * @see org.springframework.session.Session + * @see #newSessionExpiredEvent(Session) + * @see #publishEvent(ApplicationEvent) + * @see #forget(Object) + */ + protected void handleExpired(String sessionId, Session session) { + + forget(sessionId); + getSessionRepository().publishEvent(newSessionExpiredEvent(session)); + } + + private SessionExpiredEvent newSessionExpiredEvent(Session session) { + return new SessionExpiredEvent(getSessionRepository(), session); + } + + /** + * Causes Session deleted events to be published to the Spring application context. + * + * @param sessionId a String indicating the ID of the Session. + * @param session a reference to the Session triggering the event. + * @see org.springframework.session.events.SessionDeletedEvent + * @see org.springframework.session.Session + * @see #newSessionDeletedEvent(Session) + * @see #publishEvent(ApplicationEvent) + * @see #forget(Object) + */ + protected void handleDeleted(String sessionId, Session session) { + + forget(sessionId); + getSessionRepository().publishEvent(newSessionDeletedEvent(toSession(session, sessionId))); + } + + private SessionDeletedEvent newSessionDeletedEvent(Session session) { + return new SessionDeletedEvent(getSessionRepository(), session); + } + + boolean isCreate(EntryEvent event) { + return isCreate(event.getOperation()) && isNotUpdate(event) && isSession(event.getNewValue()); + } + + private boolean isCreate(Operation operation) { + return operation.isCreate() && !Operation.LOCAL_LOAD_CREATE.equals(operation); + } + + private boolean isNotUpdate(EntryEvent event) { + return isNotProxyRegion() || !this.cachedSessionIds.contains(ObjectUtils.nullSafeHashCode(event.getKey())); + } + + private boolean isNotProxyRegion() { + return !isProxyRegion(); + } + + private boolean isProxyRegion() { + return GemFireUtils.isProxy(getSessionRepository().getSessionsRegion()); + } + + /** + * Used to determine whether the application developer is storing (HTTP) {@link Session Sessions} with other, + * arbitrary application domain objects in the same Apache Geode / Pivotal GemFire cache {@link Region}; + * crazier things have happened! + * + * @param obj {@link Object} to evaluate. + * @return a boolean value indicating whether the old/new {@link Object} from the {@link Region} + * {@link EntryEvent} is indeed a {@link Session}. + * @see org.springframework.session.Session + */ + private boolean isSession(Object obj) { + return obj instanceof Session; + } + + /** + * Forgets the given {@link Object Session ID}. + * + * @param sessionId {@link Object} containing the Session ID to forget. + * @return a boolean value indicating whether the given Session ID was even being remembered. + * @see #remember(Object) + */ + boolean forget(Object sessionId) { + return this.cachedSessionIds.remove(ObjectUtils.nullSafeHashCode(sessionId)); + } + + /** + * Remembers the given {@link Object Session ID}. + * + * @param sessionId {@link Object} containing the Session ID to remember. + * @return a boolean value indicating whether Spring Session is interested in + * and will remember the given Session ID. + * @see #forget(Object) + */ + boolean remember(Object sessionId) { + return isProxyRegion() && this.cachedSessionIds.add(ObjectUtils.nullSafeHashCode(sessionId)); + } + + /** + * Casts the given {@link Object} into a {@link Session} iff the {@link Object} is a {@link Session}. + * + * Otherwise, this method attempts to use the supplied {@link String Session ID} to create a {@link Session} + * representation containing only the ID. + * + * @param obj {@link Object} to evaluate as a {@link Session}. + * @param sessionId {@link String} containing the Session ID. + * @return a {@link Session} from the given {@link Object} or a {@link Session} representation + * containing only the supplied {@link String Session ID}. + * @throws IllegalStateException if the given {@link Object} is not a {@link Session} + * and a {@link String Session ID} was not supplied. + */ + Session toSession(@Nullable Object obj, String sessionId) { + + return obj instanceof Session + ? (Session) obj + : Optional.ofNullable(sessionId) + .filter(StringUtils::hasText) + .map(SessionIdHolder::create) + .orElseThrow(() -> newIllegalStateException( + "Minimally, the Session ID [%s] must be known to trigger a Session event", sessionId)); + } + } + + protected static class SessionIdInterestRegisteringCacheListener extends CacheListenerAdapter { + + private final AbstractGemFireOperationsSessionRepository sessionRepository; + + public SessionIdInterestRegisteringCacheListener(AbstractGemFireOperationsSessionRepository sessionRepository) { + + Assert.notNull(sessionRepository, "SessionRepository is required"); + + this.sessionRepository = sessionRepository; + } + + protected AbstractGemFireOperationsSessionRepository getSessionRepository() { + return this.sessionRepository; + } + + @Override + public void afterCreate(EntryEvent event) { + getSessionRepository().registerInterest(event.getKey()); + } + + @Override + public void afterDestroy(EntryEvent event) { + getSessionRepository().unregisterInterest(event.getKey()); + } + + @Override + public void afterInvalidate(EntryEvent event) { + getSessionRepository().unregisterInterest(event.getKey()); + } + } } diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepository.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepository.java index 929dc04..7a46a19 100644 --- a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepository.java +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepository.java @@ -76,52 +76,58 @@ public class GemFireOperationsSessionRepository extends AbstractGemFireOperation } /** - * Gets a copy of an existing, non-expired {@link Session} by ID. + * Finds an existing, non-expired {@link Session} by ID. * - * If the {@link Session} is expired, then the {@link Session }is deleted. + * If the {@link Session} is expired, then the {@link Session} is deleted and {@literal null} is returned. * - * @param sessionId a String indicating the ID of the Session to get. - * @return an existing {@link Session} by ID or null if no {@link Session} exists. + * @param sessionId {@link String} containing the {@link Session#getId() ID}} of the {@link Session} to get. + * @return an existing {@link Session} by ID or {@literal null} if no {@link Session} exists + * or the {@link Session} expired. * @see AbstractGemFireOperationsSessionRepository.GemFireSession#from(Session) * @see org.springframework.session.Session + * @see #commit(Session) * @see #deleteById(String) + * @see #registerInterest(Session) + * @see #touch(Session) */ @Nullable public Session findById(String sessionId) { - Session storedSession = getTemplate().get(sessionId); + Session storedSession = getSessionsTemplate().get(sessionId); if (storedSession != null) { storedSession = storedSession.isExpired() ? delete(storedSession) - : touch(commit(GemFireSession.from(storedSession))); + : registerInterest(touch(commit(GemFireSession.from(storedSession)))); } return storedSession; } /** - * Looks up all available Sessions with the particular attribute indexed by name - * having the given value. + * Finds all available {@link Session Sessions} with the particular attribute indexed by {@link String name} + * having the given {@link Object value}. * - * @param indexName name of the indexed Session attribute. (e.g. - * {@link org.springframework.session.FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME} - * ). - * @param indexValue value of the indexed Session attribute to search on (e.g. - * username). - * @return a mapping of Session ID to Session instances. + * @param indexName {@link String name} of the indexed {@link Session} attribute. + * (e.g. {@link org.springframework.session.FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME}). + * @param indexValue {@link Object value} of the indexed {@link Session} attribute to search on + * (e.g. {@literal username}). + * @return a mapping of {@link Session#getId()} Session IDs} to {@link Session} objects. * @see org.springframework.session.Session - * @see java.util.Map * @see #prepareQuery(String) + * @see java.util.Map + * @see #commit(Session) + * @see #registerInterest(Session) + * @see #touch(Session) */ @Override public Map findByIndexNameAndIndexValue(String indexName, String indexValue) { - SelectResults results = getTemplate().find(prepareQuery(indexName), indexValue); + SelectResults results = getSessionsTemplate().find(prepareQuery(indexName), indexValue); Map sessions = new HashMap<>(results.size()); - results.asList().forEach(session -> sessions.put(session.getId(), session)); + results.asList().forEach(session -> sessions.put(session.getId(), registerInterest(touch(commit(session))))); return sessions; } @@ -136,9 +142,11 @@ public class GemFireOperationsSessionRepository extends AbstractGemFireOperation */ protected String prepareQuery(String indexName) { + String fullyQualifiedRegionName = getFullyQualifiedRegionName(); + return PRINCIPAL_NAME_INDEX_NAME.equals(indexName) - ? String.format(FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, getFullyQualifiedRegionName()) - : String.format(FIND_SESSIONS_BY_INDEX_NAME_AND_INDEX_VALUE_QUERY, getFullyQualifiedRegionName(), indexName); + ? String.format(FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, fullyQualifiedRegionName) + : String.format(FIND_SESSIONS_BY_INDEX_NAME_AND_INDEX_VALUE_QUERY, fullyQualifiedRegionName, indexName); } /** @@ -170,7 +178,7 @@ public class GemFireOperationsSessionRepository extends AbstractGemFireOperation void doSave(@NonNull Session session) { // Save Session As GemFireSession - getTemplate().put(session.getId(), GemFireSession.from(session)); + getSessionsTemplate().put(session.getId(), GemFireSession.from(session)); // Commit Session commit(session); @@ -185,6 +193,6 @@ public class GemFireOperationsSessionRepository extends AbstractGemFireOperation * @see #handleDeleted(String, Session) */ public void deleteById(String sessionId) { - handleDeleted(sessionId, toSession(getTemplate().remove(sessionId), sessionId)); + handleDeleted(sessionId, getSessionsTemplate().remove(sessionId)); } } diff --git a/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java index 5c8c0de..fed8ad6 100644 --- a/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java +++ b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java @@ -18,11 +18,12 @@ package org.springframework.session.data.gemfire; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; -import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; @@ -40,6 +41,8 @@ import static org.springframework.session.data.gemfire.AbstractGemFireOperations import static org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.DeltaCapableGemFireSessionAttributes; import static org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.GemFireSession; import static org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.GemFireSessionAttributes; +import static org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.SessionEventHandlerCacheListenerAdapter; +import static org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.SessionIdInterestRegisteringCacheListener; import java.io.DataInput; import java.io.DataOutput; @@ -50,6 +53,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -69,18 +73,21 @@ import edu.umd.cs.mtc.TestFramework; import org.apache.geode.cache.AttributesMutator; import org.apache.geode.cache.DataPolicy; import org.apache.geode.cache.EntryEvent; +import org.apache.geode.cache.InterestResultPolicy; import org.apache.geode.cache.Operation; import org.apache.geode.cache.Region; import org.apache.geode.cache.RegionAttributes; +import org.apache.geode.cache.client.ClientCache; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.gemfire.GemfireOperations; import org.springframework.data.gemfire.GemfireTemplate; +import org.springframework.data.gemfire.util.RegionUtils; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.Session; import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration; -import org.springframework.session.data.gemfire.support.GemFireUtils; +import org.springframework.session.data.gemfire.support.GemFireOperationsSessionRepositorySupport; import org.springframework.session.events.AbstractSessionEvent; import org.springframework.session.events.SessionCreatedEvent; import org.springframework.session.events.SessionDeletedEvent; @@ -99,11 +106,14 @@ import org.apache.commons.logging.Log; * @see org.mockito.Mockito * @see org.mockito.junit.MockitoJUnitRunner * @see org.mockito.Spy + * @see org.apache.geode.cache.Region * @see org.springframework.data.gemfire.GemfireOperations + * @see org.springframework.data.gemfire.GemfireTemplate + * @see org.springframework.session.FindByIndexNameSessionRepository * @see org.springframework.session.Session * @see org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration - * @see org.apache.geode.cache.Region + * @see org.springframework.session.events.AbstractSessionEvent * @see edu.umd.cs.mtc.MultithreadedTestCase * @see edu.umd.cs.mtc.TestFramework * @since 1.1.0 @@ -113,6 +123,7 @@ public class AbstractGemFireOperationsSessionRepositoryTests { protected static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 300; + // Subject Under Test (SUT) private AbstractGemFireOperationsSessionRepository sessionRepository; @Mock @@ -128,6 +139,10 @@ public class AbstractGemFireOperationsSessionRepositoryTests { @SuppressWarnings("all") public void setup() { + AttributesMutator mockAttributesMutator = mock(AttributesMutator.class); + + when(mockRegion.getAttributesMutator()).thenReturn(mockAttributesMutator); + GemfireTemplate gemfireTemplate = new GemfireTemplate(this.mockRegion); this.sessionRepository = spy(new TestGemFireOperationsSessionRepository(gemfireTemplate)); @@ -186,13 +201,18 @@ public class AbstractGemFireOperationsSessionRepositoryTests { return mockSession; } + @SuppressWarnings("unused") + private Session mockSession(String sessionId) { + return mockSession(sessionId, Instant.now().toEpochMilli(), MAX_INACTIVE_INTERVAL_IN_SECONDS); + } + private Session mockSession(String sessionId, long creationAndLastAccessedTime, long maxInactiveInterval) { return mockSession(sessionId, creationAndLastAccessedTime, creationAndLastAccessedTime, maxInactiveInterval); } private Session mockSession(String sessionId, long creationTime, long lastAccessedTime, long maxInactiveInterval) { - Session mockSession = mock(Session.class, sessionId); + Session mockSession = mock(Session.class, withSettings().lenient().name(sessionId)); when(mockSession.getId()).thenReturn(sessionId); when(mockSession.getCreationTime()).thenReturn(Instant.ofEpochMilli(creationTime)); @@ -214,49 +234,63 @@ public class AbstractGemFireOperationsSessionRepositoryTests { private AbstractGemFireOperationsSessionRepository withRegion( AbstractGemFireOperationsSessionRepository sessionRepository, Region region) { - ((GemfireTemplate) sessionRepository.getTemplate()).setRegion(region); + doReturn(region).when(sessionRepository).getSessionsRegion(); return sessionRepository; } @Test @SuppressWarnings("unchecked") - public void constructGemFireOperationsSessionRepositoryAndInitialize() throws Exception { + public void constructGemFireOperationsSessionRepository() throws Exception { ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); AttributesMutator mockAttributesMutator = mock(AttributesMutator.class); + ClientCache mockClientCache = mock(ClientCache.class); + Region mockRegion = mock(Region.class); + RegionAttributes mockRegionAttributes = mock(RegionAttributes.class); + + when(mockRegion.getAttributes()).thenReturn(mockRegionAttributes); when(mockRegion.getAttributesMutator()).thenReturn(mockAttributesMutator); - when(mockRegion.getFullPath()).thenReturn(GemFireUtils.toRegionPath("Example")); + when(mockRegion.getFullPath()).thenReturn(RegionUtils.toRegionPath("Example")); + when(mockRegion.getRegionService()).thenReturn(mockClientCache); + when(mockRegionAttributes.getPoolName()).thenReturn("Car"); GemfireTemplate template = new GemfireTemplate(mockRegion); AbstractGemFireOperationsSessionRepository sessionRepository = new TestGemFireOperationsSessionRepository(template); - assertThat(sessionRepository.getApplicationEventPublisher()).isNotNull(); + assertThat(sessionRepository.getApplicationEventPublisher()).isInstanceOf(ApplicationEventPublisher.class); assertThat(sessionRepository.getApplicationEventPublisher()).isNotEqualTo(mockApplicationEventPublisher); - assertThat(sessionRepository.getFullyQualifiedRegionName()).isNull(); + assertThat(sessionRepository.getFullyQualifiedRegionName()) + .isEqualTo(RegionUtils.toRegionPath("Example")); assertThat(sessionRepository.getMaxInactiveIntervalInSeconds()) .isEqualTo(GemFireHttpSessionConfiguration.DEFAULT_MAX_INACTIVE_INTERVAL_IN_SECONDS); - assertThat(sessionRepository.getTemplate()).isSameAs(template); + assertThat(sessionRepository.getSessionEventHandler().orElse(null)) + .isInstanceOf(SessionEventHandlerCacheListenerAdapter.class); + assertThat(sessionRepository.getSessionsRegion()).isSameAs(mockRegion); + assertThat(sessionRepository.getSessionsTemplate()).isSameAs(template); + assertThat(AbstractGemFireOperationsSessionRepository.isUsingDataSerialization()).isFalse(); sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); sessionRepository.setMaxInactiveIntervalInSeconds(300); - sessionRepository.afterPropertiesSet(); assertThat(sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - assertThat(sessionRepository.getFullyQualifiedRegionName()) - .isEqualTo(GemFireUtils.toRegionPath("Example")); assertThat(sessionRepository.getMaxInactiveIntervalInSeconds()).isEqualTo(300); - assertThat(sessionRepository.getTemplate()).isSameAs(template); + verify(mockRegion, times(1)).getAttributes(); verify(mockRegion, times(1)).getAttributesMutator(); verify(mockRegion, times(1)).getFullPath(); - verify(mockAttributesMutator, times(1)).addCacheListener(same(sessionRepository)); + verify(mockRegion, times(1)).getRegionService(); + verify(mockRegionAttributes, times(1)).getPoolName(); + verify(mockAttributesMutator, times(1)) + .addCacheListener(isA(SessionEventHandlerCacheListenerAdapter.class)); + verify(mockAttributesMutator, times(1)) + .addCacheListener(isA(SessionIdInterestRegisteringCacheListener.class)); } @Test(expected = IllegalArgumentException.class) @@ -274,9 +308,38 @@ public class AbstractGemFireOperationsSessionRepositoryTests { } } + @Test(expected = IllegalStateException.class) + public void constructGemFireOperationSessionRepositoryWithUnresolvableRegion() { + + GemfireOperations mockGemfireOperations = mock(GemfireOperations.class); + + try { + new TestGemFireOperationsSessionRepository(mockGemfireOperations); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage("The ClusteredSpringSessions Region could not be resolved"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void setAndGetApplicationEventPublisher() { + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isNotNull(); + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + } + @SuppressWarnings("all") @Test(expected = IllegalArgumentException.class) - public void setApplicationEventListenerToNull() { + public void setApplicationEventPublisherToNull() { try { this.sessionRepository.setApplicationEventPublisher(null); @@ -290,6 +353,27 @@ public class AbstractGemFireOperationsSessionRepositoryTests { } } + @Test + public void setAndGetMaxInactiveInterval() { + + assertThat(this.sessionRepository.getMaxInactiveInterval()) + .isEqualTo(Duration.ofSeconds(GemFireHttpSessionConfiguration.DEFAULT_MAX_INACTIVE_INTERVAL_IN_SECONDS)); + + Duration tenMinutes = Duration.ofMinutes(10); + + this.sessionRepository.setMaxInactiveInterval(tenMinutes); + + assertThat(this.sessionRepository.getMaxInactiveInterval()).isEqualTo(tenMinutes); + + this.sessionRepository.setMaxInactiveIntervalInSeconds(300); + + assertThat(this.sessionRepository.getMaxInactiveInterval()).isEqualTo(Duration.ofMinutes(5)); + + this.sessionRepository.setMaxInactiveInterval(null); + + assertThat(this.sessionRepository.getMaxInactiveInterval()).isNull(); + } + @Test public void maxInactiveIntervalInSecondsAllowsExtremelyLargeAndNegativeValues() { @@ -313,27 +397,6 @@ public class AbstractGemFireOperationsSessionRepositoryTests { assertThat(this.sessionRepository.getMaxInactiveIntervalInSeconds()).isEqualTo(Integer.MAX_VALUE); } - @Test - public void setAndGetMaxInactiveInterval() { - - assertThat(this.sessionRepository.getMaxInactiveInterval()) - .isEqualTo(Duration.ofSeconds(GemFireHttpSessionConfiguration.DEFAULT_MAX_INACTIVE_INTERVAL_IN_SECONDS)); - - Duration tenMinutes = Duration.ofMinutes(10); - - this.sessionRepository.setMaxInactiveInterval(tenMinutes); - - assertThat(this.sessionRepository.getMaxInactiveInterval()).isEqualTo(tenMinutes); - - this.sessionRepository.setMaxInactiveIntervalInSeconds(300); - - assertThat(this.sessionRepository.getMaxInactiveInterval()).isEqualTo(Duration.ofMinutes(5)); - - this.sessionRepository.setMaxInactiveInterval(null); - - assertThat(this.sessionRepository.getMaxInactiveInterval()).isNull(); - } - @Test public void setAndIsUsingDataSerialization() { @@ -348,731 +411,6 @@ public class AbstractGemFireOperationsSessionRepositoryTests { assertThat(GemFireOperationsSessionRepository.isUsingDataSerialization()).isFalse(); } - @Test - public void isCreateWithCreateOperationReturnsTrue() { - - EntryEvent mockEntryEvent = - mockEntryEvent(Operation.CREATE, "12345", null, this.mockSession); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); - - assertThat(this.sessionRepository.isCreate(mockEntryEvent)).isTrue(); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, times(1)).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verifyZeroInteractions(this.mockSession); - } - - @Test - public void isCreateWithCreateOperationAndNonProxyRegionReturnsTrue() { - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.CREATE, "12345", null, this.mockSession); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.NORMAL)); - - this.sessionRepository.remember("12345"); - - assertThat(this.sessionRepository.isCreate(mockEntryEvent)).isTrue(); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, never()).getKey(); - verify(mockEntryEvent, times(1)).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verifyZeroInteractions(this.mockSession); - } - - @Test - public void isCreateWithLocalLoadCreateOperationReturnsFalse() { - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.LOCAL_LOAD_CREATE, "12345", null, this.mockSession); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); - - assertThat(this.sessionRepository.isCreate(mockEntryEvent)).isFalse(); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, never()).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verifyZeroInteractions(this.mockSession); - } - - @Test - public void isCreateWithRememberedSessionIdReturnsFalse() { - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.CREATE, "12345", null, this.mockSession); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); - - this.sessionRepository.remember("12345"); - - assertThat(this.sessionRepository.isCreate(mockEntryEvent)).isFalse(); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verifyZeroInteractions(this.mockSession); - } - - @Test - public void isCreateWithUpdateOperationReturnsFalse() { - - Session mockOldValue = mock(Session.class); - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.UPDATE, "12345", mockOldValue, this.mockSession); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); - - assertThat(this.sessionRepository.isCreate(mockEntryEvent)).isFalse(); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, never()).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verifyZeroInteractions(mockOldValue); - verifyZeroInteractions(this.mockSession); - } - - @Test - public void isCreateWithTombstoneReturnsFalse() { - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.CREATE, "12345", null, new Tombstone()); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); - - assertThat(this.sessionRepository.isCreate(mockEntryEvent)).isFalse(); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, times(1)).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verifyZeroInteractions(this.mockSession); - } - - @Test - public void isCreateWithNullReturnsFalse() { - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.CREATE, "12345", null, null); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); - - assertThat(this.sessionRepository.isCreate(mockEntryEvent)).isFalse(); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, times(1)).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - } - - @Test - public void toSessionWithSession() { - assertThat(this.sessionRepository.toSession(this.mockSession, "12345")).isSameAs(this.mockSession); - } - - @Test - public void toSessionWithTombstoneAndSessionId() { - - Tombstone tombstone = new Tombstone(); - - Session session = this.sessionRepository.toSession(tombstone, "12345"); - - assertThat(session).isNotNull(); - assertThat(session).isNotSameAs(tombstone); - assertThat(session.getId()).isEqualTo("12345"); - } - - @Test(expected = IllegalStateException.class) - public void toSessionWithNullSessionAndEmptySessionId() { - - try { - this.sessionRepository.toSession(null, " "); - } - catch (IllegalStateException expected) { - - assertThat(expected).hasMessage("Minimally, the session ID [ ] must be known to trigger a Session event"); - assertThat(expected).hasNoCause(); - - throw expected; - } - } - - @Test(expected = IllegalStateException.class) - public void toSessionWithNullSessionAndNullSessionId() { - - try { - this.sessionRepository.toSession(null, null); - } - catch (IllegalStateException expected) { - - assertThat(expected).hasMessage("Minimally, the session ID [null] must be known to trigger a Session event"); - assertThat(expected).hasNoCause(); - - throw expected; - } - } - - @Test - public void afterCreateHandlesNullEntryEvent() { - - ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); - - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); - - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - - this.sessionRepository.afterCreate(null); - - verify(this.sessionRepository, never()).handleCreated(anyString(), any()); - verifyZeroInteractions(mockApplicationEventPublisher); - } - - @Test - @SuppressWarnings("unchecked") - public void afterCreateWithNewSessionPublishesSessionCreatedEvent() { - - String sessionId = "12345"; - - when(this.mockSession.getId()).thenReturn(sessionId); - - ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); - - doAnswer(invocation -> { - - ApplicationEvent applicationEvent = invocation.getArgument(0); - - assertThat(applicationEvent).isInstanceOf(SessionCreatedEvent.class); - - AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; - - assertThat(sessionEvent.getSession()).isEqualTo(this.mockSession); - assertThat(sessionEvent.getSessionId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSource()) - .isEqualTo(AbstractGemFireOperationsSessionRepositoryTests.this.sessionRepository); - - return null; - - }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.CREATE, sessionId, null, this.mockSession); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); - - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); - - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - - this.sessionRepository.afterCreate(mockEntryEvent); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, times(2)).getKey(); - verify(mockEntryEvent, times(2)).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); - verify(this.mockSession, times(1)).getId(); - verify(this.sessionRepository, times(1)) - .handleCreated(eq(sessionId), eq(this.mockSession)); - verify(mockApplicationEventPublisher, times(1)) - .publishEvent(isA(SessionCreatedEvent.class)); - } - - @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void afterCreateForCreateOperationDoesNotPublishSessionCreatedEventWhenSessionIdIsRemembered() { - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.CREATE, "12345", null, this.mockSession); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); - - this.sessionRepository.remember("12345"); - this.sessionRepository.afterCreate(mockEntryEvent); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verifyZeroInteractions(this.mockSession); - verify(this.sessionRepository, never()).handleCreated(anyString(), any()); - } - - @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void afterCreateForLocalLoadCreateOperationDoesNotPublishSessionCreatedEvent() { - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.LOCAL_LOAD_CREATE, "12345", null, this.mockSession); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.REPLICATE)); - - this.sessionRepository.afterCreate(mockEntryEvent); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, never()).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verifyZeroInteractions(this.mockSession); - verify(this.sessionRepository, never()).handleCreated(anyString(), any()); - } - - @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void afterCreateForDestroyOperationDoesNotPublishSessionCreatedEvent() { - - EntryEvent mockEntryEvent = - mockEntryEvent(Operation.DESTROY, "12345", null, null); - - this.sessionRepository.afterCreate(mockEntryEvent); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, never()).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verify(this.sessionRepository, never()).handleCreated(anyString(), any()); - } - - @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void afterCreateForInvalidateOperationDoesNotPublishSessionCreatedEvent() { - - EntryEvent mockEntryEvent = - mockEntryEvent(Operation.INVALIDATE, "12345", null, this.mockSession); - - this.sessionRepository.afterCreate(mockEntryEvent); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, never()).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verifyZeroInteractions(this.mockSession); - verify(this.sessionRepository, never()).handleCreated(anyString(), any()); - } - - @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void afterCreateForUpdateOperationDoesNotPublishSessionCreatedEvent() { - - Session mockOldValue = mock(Session.class); - - EntryEvent mockEntryEvent = - mockEntryEvent(Operation.UPDATE, "12345", mockOldValue, this.mockSession); - - this.sessionRepository.afterCreate(mockEntryEvent); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, never()).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verifyZeroInteractions(mockOldValue); - verifyZeroInteractions(this.mockSession); - verify(this.sessionRepository, never()).handleCreated(anyString(), any()); - } - - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void afterCreateWithTombstoneDoesNotPublishSessionCreatedEvent() { - - EntryEvent mockEntryEvent = mockEntryEvent(Operation.CREATE, "12345", null, new Tombstone()); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); - - this.sessionRepository.afterCreate(mockEntryEvent); - - verify(mockEntryEvent, times(1)).getOperation(); - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, times(1)).getNewValue(); - verify(mockEntryEvent, never()).getOldValue(); - verify(this.sessionRepository, never()).handleCreated(anyString(), any()); - } - - @Test - public void afterDestroyHandlesNullEntryEvent() { - - ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); - - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); - - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - - this.sessionRepository.afterDestroy(null); - - verify(this.sessionRepository, never()).handleDestroyed(anyString(), any()); - verifyZeroInteractions(mockApplicationEventPublisher); - } - - @Test - @SuppressWarnings("unchecked") - public void afterDestroyWithSessionPublishesSessionDestroyedEvent() { - - String sessionId = "12345"; - - when(this.mockSession.getId()).thenReturn(sessionId); - - ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); - - doAnswer(invocation -> { - - ApplicationEvent applicationEvent = invocation.getArgument(0); - - assertThat(applicationEvent).isInstanceOf(SessionDestroyedEvent.class); - - AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; - - assertThat(sessionEvent.getSession()).isEqualTo(this.mockSession); - assertThat(sessionEvent.getSessionId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSource()) - .isEqualTo(AbstractGemFireOperationsSessionRepositoryTests.this.sessionRepository); - - return null; - - }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.DESTROY, sessionId, this.mockSession, null); - - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); - - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - - this.sessionRepository.afterDestroy(mockEntryEvent); - - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, times(1)).getOldValue(); - verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); - verify(this.mockSession, times(1)).getId(); - verify(this.sessionRepository, times(1)) - .handleDestroyed(eq(sessionId), isA(Session.class)); - verify(mockApplicationEventPublisher, times(1)) - .publishEvent(isA(SessionDestroyedEvent.class)); - } - - @Test - @SuppressWarnings("unchecked") - public void afterDestroyWithSessionIdPublishesSessionDestroyedEvent() { - - String sessionId = "12345"; - - ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); - - doAnswer(invocation -> { - - ApplicationEvent applicationEvent = invocation.getArgument(0); - - assertThat(applicationEvent).isInstanceOf(SessionDestroyedEvent.class); - - AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; - - Session session = sessionEvent.getSession(); - - assertThat(session).isNotNull(); - assertThat(session.getId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSessionId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSource()) - .isEqualTo(AbstractGemFireOperationsSessionRepositoryTests.this.sessionRepository); - - return null; - - }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.DESTROY, sessionId, null, null); - - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); - - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - - this.sessionRepository.afterDestroy(mockEntryEvent); - - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, times(1)).getOldValue(); - verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); - verify(this.sessionRepository, times(1)) - .handleDestroyed(eq(sessionId), isA(Session.class)); - verify(mockApplicationEventPublisher, times(1)) - .publishEvent(isA(SessionDestroyedEvent.class)); - } - - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void afterDestroyWithTombstonePublishesSessionDestroyedEventWithSessionId() { - - String sessionId = "12345"; - - ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); - - doAnswer(invocation -> { - - ApplicationEvent applicationEvent = invocation.getArgument(0); - - assertThat(applicationEvent).isInstanceOf(SessionDestroyedEvent.class); - - AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; - - Session session = sessionEvent.getSession(); - - assertThat(session).isNotNull(); - assertThat(session.getId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSessionId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSource()) - .isEqualTo(AbstractGemFireOperationsSessionRepositoryTests.this.sessionRepository); - - return null; - - }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); - - EntryEvent mockEntryEvent = mockEntryEvent(Operation.DESTROY, sessionId, new Tombstone(), null); - - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); - - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - - this.sessionRepository.afterDestroy((EntryEvent) mockEntryEvent); - - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, times(1)).getOldValue(); - verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); - verify(this.sessionRepository, times(1)) - .handleDestroyed(eq(sessionId), isA(Session.class)); - verify(mockApplicationEventPublisher, times(1)) - .publishEvent(isA(SessionDestroyedEvent.class)); - } - - @Test - public void afterInvalidateHandlesNullEntryEvent() { - - ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); - - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); - - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - - this.sessionRepository.afterInvalidate(null); - - verify(this.sessionRepository, never()).handleExpired(anyString(), any()); - verifyZeroInteractions(mockApplicationEventPublisher); - } - - @Test - @SuppressWarnings("unchecked") - public void afterInvalidateWithSessionPublishesSessionExpiredEvent() { - - String sessionId = "12345"; - - when(this.mockSession.getId()).thenReturn(sessionId); - - ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); - - doAnswer(invocation -> { - - ApplicationEvent applicationEvent = invocation.getArgument(0); - - assertThat(applicationEvent).isInstanceOf(SessionExpiredEvent.class); - - AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; - - assertThat(sessionEvent.getSession()).isEqualTo(this.mockSession); - assertThat(sessionEvent.getSessionId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSource()) - .isEqualTo(AbstractGemFireOperationsSessionRepositoryTests.this.sessionRepository); - - return null; - - }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.INVALIDATE, sessionId, mockSession, null); - - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); - - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - - this.sessionRepository.afterInvalidate(mockEntryEvent); - - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, times(1)).getOldValue(); - verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); - verify(this.mockSession, times(1)).getId(); - verify(this.sessionRepository, times(1)) - .handleExpired(eq(sessionId), eq(this.mockSession)); - verify(mockApplicationEventPublisher, times(1)) - .publishEvent(isA(SessionExpiredEvent.class)); - } - - @Test - @SuppressWarnings("unchecked") - public void afterInvalidateWithSessionIdPublishesSessionExpiredEvent() { - - String sessionId = "12345"; - - ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); - - doAnswer(invocation -> { - - ApplicationEvent applicationEvent = invocation.getArgument(0); - - assertThat(applicationEvent).isInstanceOf(SessionExpiredEvent.class); - - AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; - - Session session = sessionEvent.getSession(); - - assertThat(session).isNotNull(); - assertThat(session.getId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSessionId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSource()) - .isEqualTo(AbstractGemFireOperationsSessionRepositoryTests.this.sessionRepository); - - return null; - - }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); - - EntryEvent mockEntryEvent = - this.mockEntryEvent(Operation.INVALIDATE, sessionId, null, null); - - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); - - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - - this.sessionRepository.afterInvalidate(mockEntryEvent); - - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, times(1)).getOldValue(); - verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); - verify(this.sessionRepository, times(1)) - .handleExpired(eq(sessionId), isA(Session.class)); - verify(mockApplicationEventPublisher, times(1)) - .publishEvent(isA(SessionExpiredEvent.class)); - } - - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void afterInvalidateWithTombstonePublishesSessionExpiredEventWithSessionId() { - - String sessionId = "12345"; - - ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); - - doAnswer(invocation -> { - - ApplicationEvent applicationEvent = invocation.getArgument(0); - - assertThat(applicationEvent).isInstanceOf(SessionExpiredEvent.class); - - AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; - - Session session = sessionEvent.getSession(); - - assertThat(session).isNotNull(); - assertThat(session.getId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSessionId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSource()) - .isEqualTo(AbstractGemFireOperationsSessionRepositoryTests.this.sessionRepository); - - return null; - - }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); - - EntryEvent mockEntryEvent = mockEntryEvent(Operation.INVALIDATE, sessionId, new Tombstone(), null); - - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); - - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - - this.sessionRepository.afterInvalidate((EntryEvent) mockEntryEvent); - - verify(mockEntryEvent, times(1)).getKey(); - verify(mockEntryEvent, never()).getNewValue(); - verify(mockEntryEvent, times(1)).getOldValue(); - verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); - verify(this.sessionRepository, times(1)) - .handleExpired(eq(sessionId), isA(Session.class)); - verify(mockApplicationEventPublisher, times(1)) - .publishEvent(isA(SessionExpiredEvent.class)); - } - - @Test - public void sessionCreateCreateExpireRecreatePublishesSessionEventsCreateExpireCreate() { - - String sessionId = "123456789"; - - when(this.mockSession.getId()).thenReturn(sessionId); - - ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); - - doAnswer(new Answer() { - - int index = 0; - - Class[] expectedSessionTypes = { - SessionCreatedEvent.class, SessionExpiredEvent.class, SessionCreatedEvent.class - }; - - public Void answer(InvocationOnMock invocation) throws Throwable { - ApplicationEvent applicationEvent = invocation.getArgument(0); - - assertThat(applicationEvent).isInstanceOf(this.expectedSessionTypes[this.index++]); - - AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; - - assertThat(sessionEvent.getSession()).isEqualTo(mockSession); - assertThat(sessionEvent.getSessionId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSource()) - .isEqualTo(AbstractGemFireOperationsSessionRepositoryTests.this.sessionRepository); - - return null; - } - }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); - - EntryEvent mockCreateEvent = - this.mockEntryEvent(Operation.CREATE, sessionId, null, this.mockSession); - - EntryEvent mockExpireEvent = - this.mockEntryEvent(Operation.INVALIDATE, sessionId, this.mockSession, null); - - withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); - - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); - this.sessionRepository.afterCreate(mockCreateEvent); - this.sessionRepository.afterCreate(mockCreateEvent); - this.sessionRepository.afterInvalidate(mockExpireEvent); - this.sessionRepository.afterCreate(mockCreateEvent); - - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); - - verify(mockCreateEvent, times(3)).getOperation(); - verify(mockCreateEvent, times(5)).getKey(); - verify(mockCreateEvent, times(4)).getNewValue(); - verify(mockCreateEvent, never()).getOldValue(); - verify(mockExpireEvent, never()).getOperation(); - verify(mockExpireEvent, times(1)).getKey(); - verify(mockExpireEvent, never()).getNewValue(); - verify(mockExpireEvent, times(1)).getOldValue(); - verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); - verify(this.mockSession, times(3)).getId(); - verify(this.sessionRepository, times(2)) - .handleCreated(eq(sessionId), eq(this.mockSession)); - verify(this.sessionRepository, times(1)) - .handleExpired(eq(sessionId), eq(this.mockSession)); - verify(mockApplicationEventPublisher, times(2)) - .publishEvent(isA(SessionCreatedEvent.class)); - verify(mockApplicationEventPublisher, times(1)) - .publishEvent(isA(SessionExpiredEvent.class)); - } - @Test public void commitGemFireSessionIsCorrect() { @@ -1099,9 +437,10 @@ public class AbstractGemFireOperationsSessionRepositoryTests { } @Test - public void deleteSessionCallsDeleteSessionId() { + public void deleteSessionCallsDeleteSessionById() { doNothing().when(this.sessionRepository).deleteById(anyString()); + when(this.mockSession.getId()).thenReturn("2"); assertThat(this.sessionRepository.delete(this.mockSession)).isNull(); @@ -1111,7 +450,7 @@ public class AbstractGemFireOperationsSessionRepositoryTests { } @Test - public void handleDeletedWithSessionPublishesSessionDeletedEvent() { + public void handleDeletedSessionForgetsSessionIdPublishesSessionDeletedEventAndUnregistersInterest() { String sessionId = "12345"; @@ -1129,25 +468,64 @@ public class AbstractGemFireOperationsSessionRepositoryTests { assertThat(sessionEvent.getSession()).isEqualTo(this.mockSession); assertThat(sessionEvent.getSessionId()).isEqualTo(sessionId); - assertThat(sessionEvent.getSource()) - .isEqualTo(AbstractGemFireOperationsSessionRepositoryTests.this.sessionRepository); + assertThat(sessionEvent.getSource()).isEqualTo(this.sessionRepository); return null; }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); - this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + SessionEventHandlerCacheListenerAdapter mockSessionEventHandler = + mock(SessionEventHandlerCacheListenerAdapter.class); - assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + doReturn(this.sessionRepository).when(mockSessionEventHandler).getSessionRepository(); + doCallRealMethod().when(mockSessionEventHandler).handleDeleted(anyString(), any(Session.class)); + doCallRealMethod().when(mockSessionEventHandler).toSession(any(), anyString()); + doReturn(mockApplicationEventPublisher).when(this.sessionRepository).getApplicationEventPublisher(); + doReturn(Optional.of(mockSessionEventHandler)).when(this.sessionRepository).getSessionEventHandler(); this.sessionRepository.handleDeleted(sessionId, this.mockSession); + verify(mockSessionEventHandler, times(1)).handleDeleted(eq(sessionId), eq(this.mockSession)); + verify(mockSessionEventHandler, times(1)).forget(eq(sessionId)); + verify(this.sessionRepository, times(1)).publishEvent(isA(SessionDeletedEvent.class)); + verify(this.sessionRepository, times(1)).unregisterInterest(eq(sessionId)); verify(this.mockSession, times(1)).getId(); verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); verify(mockApplicationEventPublisher, times(1)) .publishEvent(isA(SessionDeletedEvent.class)); } + @Test + public void handleDeletedSessionWhenNoSessionEventHandlerIsPresentDoesNotPublishEventButStillUnregistersInterest() { + + Session mockSession = mock(Session.class); + + doReturn(Optional.empty()).when(this.sessionRepository).getSessionEventHandler(); + + this.sessionRepository.handleDeleted("1", mockSession); + + verify(this.sessionRepository, times(1)).getSessionEventHandler(); + verify(this.sessionRepository, never()).publishEvent(any(ApplicationEvent.class)); + verify(this.sessionRepository, times(1)).unregisterInterest(eq("1")); + verifyZeroInteractions(mockSession); + } + + @Test + public void publishEventPublishesApplicationEvent() { + + ApplicationEvent mockApplicationEvent = mock(ApplicationEvent.class); + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + this.sessionRepository.publishEvent(mockApplicationEvent); + + verify(mockApplicationEventPublisher, times(1)).publishEvent(eq(mockApplicationEvent)); + } + @Test public void publishEventHandlesThrowable() { @@ -1170,6 +548,76 @@ public class AbstractGemFireOperationsSessionRepositoryTests { isA(IllegalStateException.class)); } + @Test + public void registerInterestIsNullSafe() { + assertThat(testRegisterInterestWithInvalidSession(null)).isNull(); + } + + @Test + public void registerInterestWithSession() { + + when(this.mockSession.getId()).thenReturn("1"); + when(this.sessionRepository.isRegisterInterestEnabled()).thenReturn(true); + + assertThat(this.sessionRepository.registerInterest(this.mockSession)).isSameAs(this.mockSession); + + verify(this.mockSession, times(1)).getId(); + verify(this.mockRegion, times(1)) + .registerInterest(eq("1"), eq(InterestResultPolicy.NONE), eq(false), eq(false)); + } + + private Session testRegisterInterestWithInvalidSession(Session session) { + + Session returnedSession = this.sessionRepository.registerInterest(session); + + verify(this.mockRegion, never()).registerInterest(any()); + verify(this.mockRegion, never()).registerInterest(any(), anyBoolean()); + verify(this.mockRegion, never()).registerInterest(any(), anyBoolean(), anyBoolean()); + verify(this.mockRegion, never()).registerInterest(any(), any(InterestResultPolicy.class)); + verify(this.mockRegion, never()).registerInterest(any(), any(InterestResultPolicy.class), anyBoolean()); + verify(this.mockRegion, never()).registerInterest(any(), any(InterestResultPolicy.class), anyBoolean(), anyBoolean()); + + return returnedSession; + } + + @Test + public void registerInterestWithSessionHavingEmptyId() { + + when(this.mockSession.getId()).thenReturn(""); + + assertThat(testRegisterInterestWithInvalidSession(this.mockSession)).isEqualTo(this.mockSession); + } + + @Test + public void registerInterestWithSessionHavingNullId() { + + when(this.mockSession.getId()).thenReturn(null); + + assertThat(testRegisterInterestWithInvalidSession(this.mockSession)).isEqualTo(this.mockSession); + } + + @Test + public void registerInterestWithSessionHavingUnspecifiedId() { + + when(this.mockSession.getId()).thenReturn(" "); + + assertThat(testRegisterInterestWithInvalidSession(this.mockSession)).isEqualTo(this.mockSession); + } + + @Test + public void registerInterestWithTheSameSessionTwice() { + + when(this.mockSession.getId()).thenReturn("1"); + when(this.sessionRepository.isRegisterInterestEnabled()).thenReturn(true); + + assertThat(this.sessionRepository.registerInterest(this.mockSession)).isEqualTo(this.mockSession); + assertThat(this.sessionRepository.registerInterest(this.mockSession)).isEqualTo(this.mockSession); + + verify(this.sessionRepository, times(2)).registerInterest(eq("1")); + verify(this.mockRegion, times(1)) + .registerInterest(eq("1"), eq(InterestResultPolicy.NONE), eq(false), eq(false)); + } + @Test public void touchSetsLastAccessedTime() { @@ -1178,6 +626,815 @@ public class AbstractGemFireOperationsSessionRepositoryTests { verify(this.mockSession, times(1)).setLastAccessedTime(any(Instant.class)); } + @Test + public void unregisterInterestIsNullSafe() { + assertThat(this.sessionRepository.unregisterInterest(null)).isNull(); + } + + @Test + public void unregisterInterestWithRegisteredSession() { + + when(this.mockSession.getId()).thenReturn("1"); + when(this.sessionRepository.isRegisterInterestEnabled()).thenReturn(true); + + assertThat(this.sessionRepository.registerInterest(this.mockSession)).isSameAs(this.mockSession); + assertThat(this.sessionRepository.unregisterInterest(this.mockSession)).isSameAs(this.mockSession); + + verify(this.mockSession, times(2)).getId(); + verify(this.mockRegion, times(1)).unregisterInterest(eq("1")); + } + + @Test + public void unregisterInterestWithUnknownSession() { + + when(this.mockSession.getId()).thenReturn("1"); + + assertThat(this.sessionRepository.unregisterInterest(this.mockSession)).isSameAs(this.mockSession); + + verify(this.mockSession, times(1)).getId(); + verify(this.sessionRepository, times(1)).unregisterInterest(eq("1")); + verify(this.mockRegion, never()).unregisterInterest(any()); + } + + @Test + public void isCreateWithCreateOperationReturnsTrue() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = this.sessionRepository.newSessionEventHandler(); + + EntryEvent mockEntryEvent = + mockEntryEvent(Operation.CREATE, "12345", null, this.mockSession); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); + + assertThat(sessionEventHandler.isCreate(mockEntryEvent)).isTrue(); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, times(1)).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verifyZeroInteractions(this.mockSession); + } + + @Test + public void isCreateWithCreateOperationAndNonProxyRegionReturnsTrue() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = this.sessionRepository.newSessionEventHandler(); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.CREATE, "12345", null, this.mockSession); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.NORMAL)); + + sessionEventHandler.remember("12345"); + + assertThat(sessionEventHandler.isCreate(mockEntryEvent)).isTrue(); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, never()).getKey(); + verify(mockEntryEvent, times(1)).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verifyZeroInteractions(this.mockSession); + } + + @Test + public void isCreateWithLocalLoadCreateOperationReturnsFalse() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = this.sessionRepository.newSessionEventHandler(); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.LOCAL_LOAD_CREATE, "12345", null, this.mockSession); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); + + assertThat(sessionEventHandler.isCreate(mockEntryEvent)).isFalse(); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, never()).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verifyZeroInteractions(this.mockSession); + } + + @Test + public void isCreateWithRememberedSessionIdReturnsFalse() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = this.sessionRepository.newSessionEventHandler(); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.CREATE, "12345", null, this.mockSession); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); + + sessionEventHandler.remember("12345"); + + assertThat(sessionEventHandler.isCreate(mockEntryEvent)).isFalse(); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verifyZeroInteractions(this.mockSession); + } + + @Test + public void isCreateWithUpdateOperationReturnsFalse() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = this.sessionRepository.newSessionEventHandler(); + + Session mockOldValue = mock(Session.class); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.UPDATE, "12345", mockOldValue, this.mockSession); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); + + assertThat(sessionEventHandler.isCreate(mockEntryEvent)).isFalse(); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, never()).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verifyZeroInteractions(mockOldValue); + verifyZeroInteractions(this.mockSession); + } + + @Test + public void isCreateWithTombstoneReturnsFalse() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = this.sessionRepository.newSessionEventHandler(); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.CREATE, "12345", null, new Tombstone()); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); + + assertThat(sessionEventHandler.isCreate(mockEntryEvent)).isFalse(); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, times(1)).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verifyZeroInteractions(this.mockSession); + } + + @Test + public void isCreateWithNullReturnsFalse() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = this.sessionRepository.newSessionEventHandler(); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.CREATE, "12345", null, null); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); + + assertThat(sessionEventHandler.isCreate(mockEntryEvent)).isFalse(); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, times(1)).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + } + + @Test + public void toSessionWithSession() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = this.sessionRepository.newSessionEventHandler(); + + assertThat(sessionEventHandler.toSession(this.mockSession, "12345")).isSameAs(this.mockSession); + } + + @Test + public void toSessionWithTombstoneAndSessionId() { + + Tombstone tombstone = new Tombstone(); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = this.sessionRepository.newSessionEventHandler(); + + Session session = sessionEventHandler.toSession(tombstone, "12345"); + + assertThat(session).isNotNull(); + assertThat(session).isNotSameAs(tombstone); + assertThat(session.getId()).isEqualTo("12345"); + } + + @Test(expected = IllegalStateException.class) + public void toSessionWithNullSessionAndNullSessionId() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = this.sessionRepository.newSessionEventHandler(); + + try { + sessionEventHandler.toSession(null, null); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage("Minimally, the Session ID [null] must be known to trigger a Session event"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalStateException.class) + public void toSessionWithNullSessionAndUnspecifiedSessionId() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = this.sessionRepository.newSessionEventHandler(); + + try { + sessionEventHandler.toSession(null, " "); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage("Minimally, the Session ID [ ] must be known to trigger a Session event"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void afterCreateHandlesNullEntryEvent() { + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + sessionEventHandler.afterCreate(null); + + verify(sessionEventHandler, never()).handleCreated(anyString(), any()); + verifyZeroInteractions(mockApplicationEventPublisher); + } + + @Test + @SuppressWarnings("unchecked") + public void afterCreateWithNewSessionPublishesSessionCreatedEvent() { + + when(this.mockSession.getId()).thenReturn("12345"); + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + doAnswer(invocation -> { + + ApplicationEvent applicationEvent = invocation.getArgument(0); + + assertThat(applicationEvent).isInstanceOf(SessionCreatedEvent.class); + + AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; + + assertThat(sessionEvent.getSession()).isEqualTo(this.mockSession); + assertThat(sessionEvent.getSessionId()).isEqualTo("12345"); + assertThat(sessionEvent.getSource()).isEqualTo(this.sessionRepository); + + return null; + + }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.CREATE, "12345", null, this.mockSession); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + sessionEventHandler.afterCreate(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, times(2)).getKey(); + verify(mockEntryEvent, times(2)).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); + verify(this.mockSession, times(1)).getId(); + verify(sessionEventHandler, times(1)) + .handleCreated(eq("12345"), eq(this.mockSession)); + verify(mockApplicationEventPublisher, times(1)) + .publishEvent(isA(SessionCreatedEvent.class)); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void afterCreateForCreateOperationDoesNotPublishSessionCreatedEventWhenSessionIdIsRemembered() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.CREATE, "12345", null, this.mockSession); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); + + sessionEventHandler.remember("12345"); + sessionEventHandler.afterCreate(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verify(sessionEventHandler, never()).handleCreated(anyString(), any()); + verifyZeroInteractions(this.mockSession); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void afterCreateForLocalLoadCreateOperationDoesNotPublishSessionCreatedEvent() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.LOCAL_LOAD_CREATE, "12345", null, this.mockSession); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.REPLICATE)); + + sessionEventHandler.afterCreate(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, never()).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verify(sessionEventHandler, never()).handleCreated(anyString(), any()); + verifyZeroInteractions(this.mockSession); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void afterCreateForDestroyOperationDoesNotPublishSessionCreatedEvent() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = + mockEntryEvent(Operation.DESTROY, "12345", null, null); + + sessionEventHandler.afterCreate(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, never()).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verify(sessionEventHandler, never()).handleCreated(anyString(), any()); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void afterCreateForInvalidateOperationDoesNotPublishSessionCreatedEvent() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = + mockEntryEvent(Operation.INVALIDATE, "12345", null, this.mockSession); + + sessionEventHandler.afterCreate(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, never()).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verify(sessionEventHandler, never()).handleCreated(anyString(), any()); + verifyZeroInteractions(this.mockSession); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void afterCreateForUpdateOperationDoesNotPublishSessionCreatedEvent() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + Session mockOldValue = mock(Session.class); + + EntryEvent mockEntryEvent = + mockEntryEvent(Operation.UPDATE, "12345", mockOldValue, this.mockSession); + + sessionEventHandler.afterCreate(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, never()).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verifyZeroInteractions(mockOldValue); + verifyZeroInteractions(this.mockSession); + verify(sessionEventHandler, never()).handleCreated(anyString(), any()); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void afterCreateWithTombstoneDoesNotPublishSessionCreatedEvent() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = mockEntryEvent(Operation.CREATE, "12345", null, new Tombstone()); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); + + sessionEventHandler.afterCreate(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getOperation(); + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, times(1)).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verify(sessionEventHandler, never()).handleCreated(anyString(), any()); + } + + @Test + public void afterDestroyHandlesNullEntryEvent() { + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + sessionEventHandler.afterDestroy(null); + + verify(sessionEventHandler, never()).handleDestroyed(anyString(), any()); + verifyZeroInteractions(mockApplicationEventPublisher); + } + + @Test + @SuppressWarnings("unchecked") + public void afterDestroyWithSessionPublishesSessionDestroyedEvent() { + + when(this.mockSession.getId()).thenReturn("12345"); + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + doAnswer(invocation -> { + + ApplicationEvent applicationEvent = invocation.getArgument(0); + + assertThat(applicationEvent).isInstanceOf(SessionDestroyedEvent.class); + + AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; + + assertThat(sessionEvent.getSession()).isEqualTo(this.mockSession); + assertThat(sessionEvent.getSessionId()).isEqualTo("12345"); + assertThat(sessionEvent.getSource()).isEqualTo(this.sessionRepository); + + return null; + + }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.DESTROY, "12345", this.mockSession, null); + + sessionEventHandler.afterDestroy(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, times(1)).getOldValue(); + verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); + verify(this.mockSession, times(1)).getId(); + verify(sessionEventHandler, times(1)) + .handleDestroyed(eq("12345"), isA(Session.class)); + verify(mockApplicationEventPublisher, times(1)) + .publishEvent(isA(SessionDestroyedEvent.class)); + } + + @Test + @SuppressWarnings("unchecked") + public void afterDestroyWithSessionIdPublishesSessionDestroyedEvent() { + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + doAnswer(invocation -> { + + ApplicationEvent applicationEvent = invocation.getArgument(0); + + assertThat(applicationEvent).isInstanceOf(SessionDestroyedEvent.class); + + AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; + + Session session = sessionEvent.getSession(); + + assertThat(session).isNotNull(); + assertThat(session.getId()).isEqualTo("12345"); + assertThat(sessionEvent.getSessionId()).isEqualTo("12345"); + assertThat(sessionEvent.getSource()).isEqualTo(this.sessionRepository); + + return null; + + }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.DESTROY, "12345", null, null); + + sessionEventHandler.afterDestroy(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, times(1)).getOldValue(); + verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); + verify(sessionEventHandler, times(1)) + .handleDestroyed(eq("12345"), isA(Session.class)); + verify(mockApplicationEventPublisher, times(1)) + .publishEvent(isA(SessionDestroyedEvent.class)); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void afterDestroyWithTombstonePublishesSessionDestroyedEventWithSessionId() { + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + doAnswer(invocation -> { + + ApplicationEvent applicationEvent = invocation.getArgument(0); + + assertThat(applicationEvent).isInstanceOf(SessionDestroyedEvent.class); + + AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; + + Session session = sessionEvent.getSession(); + + assertThat(session).isNotNull(); + assertThat(session.getId()).isEqualTo("12345"); + assertThat(sessionEvent.getSessionId()).isEqualTo("12345"); + assertThat(sessionEvent.getSource()).isEqualTo(this.sessionRepository); + + return null; + + }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = mockEntryEvent(Operation.DESTROY, "12345", new Tombstone(), null); + + sessionEventHandler.afterDestroy((EntryEvent) mockEntryEvent); + + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, times(1)).getOldValue(); + verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); + verify(sessionEventHandler, times(1)) + .handleDestroyed(eq("12345"), isA(Session.class)); + verify(mockApplicationEventPublisher, times(1)) + .publishEvent(isA(SessionDestroyedEvent.class)); + } + + @Test + public void afterInvalidateHandlesNullEntryEvent() { + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + sessionEventHandler.afterInvalidate(null); + + verify(sessionEventHandler, never()).handleExpired(anyString(), any()); + verifyZeroInteractions(mockApplicationEventPublisher); + } + + @Test + @SuppressWarnings("unchecked") + public void afterInvalidateWithSessionPublishesSessionExpiredEvent() { + + when(this.mockSession.getId()).thenReturn("12345"); + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + doAnswer(invocation -> { + + ApplicationEvent applicationEvent = invocation.getArgument(0); + + assertThat(applicationEvent).isInstanceOf(SessionExpiredEvent.class); + + AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; + + assertThat(sessionEvent.getSession()).isEqualTo(this.mockSession); + assertThat(sessionEvent.getSessionId()).isEqualTo("12345"); + assertThat(sessionEvent.getSource()).isEqualTo(this.sessionRepository); + + return null; + + }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.INVALIDATE, "12345", mockSession, null); + + sessionEventHandler.afterInvalidate(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, times(1)).getOldValue(); + verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); + verify(this.mockSession, times(1)).getId(); + verify(sessionEventHandler, times(1)) + .handleExpired(eq("12345"), eq(this.mockSession)); + verify(mockApplicationEventPublisher, times(1)) + .publishEvent(isA(SessionExpiredEvent.class)); + } + + @Test + @SuppressWarnings("unchecked") + public void afterInvalidateWithSessionIdPublishesSessionExpiredEvent() { + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + doAnswer(invocation -> { + + ApplicationEvent applicationEvent = invocation.getArgument(0); + + assertThat(applicationEvent).isInstanceOf(SessionExpiredEvent.class); + + AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; + + Session session = sessionEvent.getSession(); + + assertThat(session).isNotNull(); + assertThat(session.getId()).isEqualTo("12345"); + assertThat(sessionEvent.getSessionId()).isEqualTo("12345"); + assertThat(sessionEvent.getSource()).isEqualTo(this.sessionRepository); + + return null; + + }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = + this.mockEntryEvent(Operation.INVALIDATE, "12345", null, null); + + sessionEventHandler.afterInvalidate(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, times(1)).getOldValue(); + verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); + verify(sessionEventHandler, times(1)) + .handleExpired(eq("12345"), isA(Session.class)); + verify(mockApplicationEventPublisher, times(1)) + .publishEvent(isA(SessionExpiredEvent.class)); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void afterInvalidateWithTombstonePublishesSessionExpiredEventWithSessionId() { + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + doAnswer(invocation -> { + + ApplicationEvent applicationEvent = invocation.getArgument(0); + + assertThat(applicationEvent).isInstanceOf(SessionExpiredEvent.class); + + AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; + + Session session = sessionEvent.getSession(); + + assertThat(session).isNotNull(); + assertThat(session.getId()).isEqualTo("12345"); + assertThat(sessionEvent.getSessionId()).isEqualTo("12345"); + assertThat(sessionEvent.getSource()).isEqualTo(this.sessionRepository); + + return null; + + }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = mockEntryEvent(Operation.INVALIDATE, "12345", new Tombstone(), null); + + sessionEventHandler.afterInvalidate((EntryEvent) mockEntryEvent); + + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, never()).getNewValue(); + verify(mockEntryEvent, times(1)).getOldValue(); + verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); + verify(sessionEventHandler, times(1)) + .handleExpired(eq("12345"), isA(Session.class)); + verify(mockApplicationEventPublisher, times(1)) + .publishEvent(isA(SessionExpiredEvent.class)); + } + + @Test + public void sessionCreateCreateExpireRecreatePublishesSessionEventsCreateExpireCreate() { + + when(this.mockSession.getId()).thenReturn("123456789"); + + ApplicationEventPublisher mockApplicationEventPublisher = mock(ApplicationEventPublisher.class); + + doAnswer(new Answer() { + + int index = 0; + + Class[] expectedSessionTypes = { + SessionCreatedEvent.class, SessionExpiredEvent.class, SessionCreatedEvent.class + }; + + public Void answer(InvocationOnMock invocation) throws Throwable { + ApplicationEvent applicationEvent = invocation.getArgument(0); + + assertThat(applicationEvent).isInstanceOf(this.expectedSessionTypes[this.index++]); + + AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; + + assertThat(sessionEvent.getSession()).isEqualTo(mockSession); + assertThat(sessionEvent.getSessionId()).isEqualTo("123456789"); + assertThat(sessionEvent.getSource()) + .isEqualTo(AbstractGemFireOperationsSessionRepositoryTests.this.sessionRepository); + + return null; + } + }).when(mockApplicationEventPublisher).publishEvent(isA(ApplicationEvent.class)); + + withRegion(this.sessionRepository, mockRegion("Example", DataPolicy.EMPTY)); + + this.sessionRepository.setApplicationEventPublisher(mockApplicationEventPublisher); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockCreateEvent = + this.mockEntryEvent(Operation.CREATE, "123456789", null, this.mockSession); + + EntryEvent mockExpireEvent = + this.mockEntryEvent(Operation.INVALIDATE, "123456789", this.mockSession, null); + + sessionEventHandler.afterCreate(mockCreateEvent); + sessionEventHandler.afterCreate(mockCreateEvent); + sessionEventHandler.afterInvalidate(mockExpireEvent); + sessionEventHandler.afterCreate(mockCreateEvent); + + assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(mockApplicationEventPublisher); + + verify(mockCreateEvent, times(3)).getOperation(); + verify(mockCreateEvent, times(5)).getKey(); + verify(mockCreateEvent, times(4)).getNewValue(); + verify(mockCreateEvent, never()).getOldValue(); + verify(mockExpireEvent, never()).getOperation(); + verify(mockExpireEvent, times(1)).getKey(); + verify(mockExpireEvent, never()).getNewValue(); + verify(mockExpireEvent, times(1)).getOldValue(); + verify(this.mockLog, never()).error(anyString(), any(Throwable.class)); + verify(this.mockSession, times(3)).getId(); + verify(sessionEventHandler, times(2)) + .handleCreated(eq("123456789"), eq(this.mockSession)); + verify(sessionEventHandler, times(1)) + .handleExpired(eq("123456789"), eq(this.mockSession)); + verify(mockApplicationEventPublisher, times(2)) + .publishEvent(isA(SessionCreatedEvent.class)); + verify(mockApplicationEventPublisher, times(1)) + .publishEvent(isA(SessionExpiredEvent.class)); + } + @Test public void constructDefaultGemFireSession() { @@ -2552,31 +2809,11 @@ public class AbstractGemFireOperationsSessionRepositoryTests { } } - static class TestGemFireOperationsSessionRepository extends AbstractGemFireOperationsSessionRepository { + static class TestGemFireOperationsSessionRepository extends GemFireOperationsSessionRepositorySupport { TestGemFireOperationsSessionRepository(GemfireOperations gemfireOperations) { super(gemfireOperations); } - - public Session createSession() { - throw new UnsupportedOperationException("Not Implemented"); - } - - public Session findById(String id) { - throw new UnsupportedOperationException("Not Implemented"); - } - - public Map findByIndexNameAndIndexValue(String indexName, String indexValue) { - throw new UnsupportedOperationException("Not Implemented"); - } - - public void save(Session session) { - throw new UnsupportedOperationException("Not Implemented"); - } - - public void deleteById(String id) { - throw new UnsupportedOperationException("Not Implemented"); - } } static class Tombstone { } diff --git a/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryTests.java b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryTests.java index 881b3c8..1442c11 100644 --- a/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryTests.java +++ b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryTests.java @@ -22,17 +22,16 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.same; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; import static org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; import static org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.GemFireSession; @@ -41,11 +40,9 @@ import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.Map; -import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -61,8 +58,9 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.gemfire.GemfireAccessor; import org.springframework.data.gemfire.GemfireOperations; +import org.springframework.data.gemfire.util.RegionUtils; import org.springframework.session.Session; -import org.springframework.session.data.gemfire.support.GemFireUtils; +import org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.SessionEventHandlerCacheListenerAdapter; import org.springframework.session.events.AbstractSessionEvent; import org.springframework.session.events.SessionDeletedEvent; @@ -97,102 +95,172 @@ public class GemFireOperationsSessionRepositoryTests { @Mock private ApplicationEventPublisher mockApplicationEventPublisher; - @Mock - private AttributesMutator mockAttributesMutator; - @Mock private GemfireOperationsAccessor mockTemplate; // Subject Under Test (SUT) private GemFireOperationsSessionRepository sessionRepository; - @Mock - private Region mockRegion; - @Before + @SuppressWarnings("unchecked") public void setup() throws Exception { - when(this.mockRegion.getAttributesMutator()).thenReturn(this.mockAttributesMutator); - when(this.mockRegion.getFullPath()).thenReturn(GemFireUtils.toRegionPath("Example")); - when(this.mockTemplate.getRegion()).thenReturn(this.mockRegion); + AttributesMutator mockAttributesMutator = mock(AttributesMutator.class); - this.sessionRepository = spy(new GemFireOperationsSessionRepository(this.mockTemplate)); + Region mockRegion = mock(Region.class); + + when(mockRegion.getAttributesMutator()).thenReturn(mockAttributesMutator); + when(mockRegion.getFullPath()).thenReturn(RegionUtils.toRegionPath("Example")); + + doReturn(mockRegion).when(this.mockTemplate).getRegion(); + + this.sessionRepository = new GemFireOperationsSessionRepository(this.mockTemplate); this.sessionRepository.setApplicationEventPublisher(this.mockApplicationEventPublisher); this.sessionRepository.setMaxInactiveIntervalInSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS); this.sessionRepository.setUseDataSerialization(false); - this.sessionRepository.afterPropertiesSet(); assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(this.mockApplicationEventPublisher); - assertThat(this.sessionRepository.getFullyQualifiedRegionName()).isEqualTo(GemFireUtils.toRegionPath("Example")); + assertThat(this.sessionRepository.getFullyQualifiedRegionName()).isEqualTo(RegionUtils.toRegionPath("Example")); assertThat(this.sessionRepository.getMaxInactiveIntervalInSeconds()).isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS); - assertThat(this.sessionRepository.getTemplate()).isSameAs(this.mockTemplate); + assertThat(this.sessionRepository.getSessionEventHandler().orElse(null)).isInstanceOf(SessionEventHandlerCacheListenerAdapter.class); + assertThat(this.sessionRepository.getSessionsRegion()).isSameAs(mockRegion); + assertThat(this.sessionRepository.getSessionsTemplate()).isSameAs(this.mockTemplate); assertThat(GemFireOperationsSessionRepository.isUsingDataSerialization()).isFalse(); - } - private Session mockSession() { - - String sessionId = UUID.randomUUID().toString(); - - Instant now = Instant.now(); - - Duration maxInactiveInterval = Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS); - - Session mockSession = mock(Session.class, withSettings().name(sessionId).lenient()); - - when(mockSession.getId()).thenReturn(sessionId); - when(mockSession.getAttributeNames()).thenReturn(Collections.emptySet()); - when(mockSession.getCreationTime()).thenReturn(now); - when(mockSession.getLastAccessedTime()).thenReturn(now); - when(mockSession.getMaxInactiveInterval()).thenReturn(maxInactiveInterval); - - return mockSession; + verify(mockAttributesMutator).addCacheListener(isA(SessionEventHandlerCacheListenerAdapter.class)); + verify(mockRegion, times(1)).getAttributesMutator(); + verify(this.mockTemplate, times(1)).getRegion(); } private GemFireSession newNonDirtyGemFireSession() { GemFireSession session = GemFireSession.create(); - session.commit();; + session.commit(); return session; } - @After - public void tearDown() { + @Test + @SuppressWarnings("unchecked") + public void constructGemFireOperationSessionRepositoryWithTemplate() { - verify(this.mockAttributesMutator, times(1)).addCacheListener(same(this.sessionRepository)); - verify(this.mockRegion, times(1)).getFullPath(); - verify(this.mockTemplate, times(1)).getRegion(); + AttributesMutator mockAttributesMutator = mock(AttributesMutator.class); + + Region mockRegion = mock(Region.class); + + GemfireOperationsAccessor mockTemplate = mock(GemfireOperationsAccessor.class); + + when(mockRegion.getAttributesMutator()).thenReturn(mockAttributesMutator); + + doReturn(mockRegion).when(mockTemplate).getRegion(); + + GemFireOperationsSessionRepository sessionRepository = + new GemFireOperationsSessionRepository(mockTemplate); + + assertThat(sessionRepository).isNotNull(); + assertThat(sessionRepository.getSessionsRegion()).isSameAs(mockRegion); + assertThat(sessionRepository.getSessionsTemplate()).isSameAs(mockTemplate); + + verify(mockTemplate, times(1)).getRegion(); + verify(mockRegion, times(1)).getAttributesMutator(); + verify(mockAttributesMutator, times(1)) + .addCacheListener(isA(SessionEventHandlerCacheListenerAdapter.class)); + verifyNoMoreInteractions(mockAttributesMutator); } @Test public void createProperlyInitializedSession() { - Instant beforeOrAtCreationTime = Instant.now(); + Instant beforeCreationTime = Instant.now(); Session session = this.sessionRepository.createSession(); assertThat(session).isInstanceOf(AbstractGemFireOperationsSessionRepository.GemFireSession.class); - assertThat(session.getId()).isNotNull(); + assertThat(session.getId()).isNotEmpty(); assertThat(session.getAttributeNames()).isEmpty(); - assertThat(session.getCreationTime().compareTo(beforeOrAtCreationTime)).isGreaterThanOrEqualTo(0); - assertThat(session.getLastAccessedTime().compareTo(beforeOrAtCreationTime)).isGreaterThanOrEqualTo(0); + assertThat(session.getCreationTime()).isAfterOrEqualTo(beforeCreationTime); + assertThat(session.getCreationTime()).isBeforeOrEqualTo(Instant.now()); + assertThat(session.isExpired()).isFalse(); + assertThat(session.getLastAccessedTime()).isEqualTo(session.getCreationTime()); assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); } + @Test + public void createProperlyInitializedDeltaAwareSession() { + + Instant beforeCreationTime = Instant.now(); + + this.sessionRepository.setUseDataSerialization(true); + + Session session = this.sessionRepository.createSession(); + + assertThat(session).isInstanceOf(AbstractGemFireOperationsSessionRepository.DeltaCapableGemFireSession.class); + assertThat(session.getId()).isNotEmpty(); + assertThat(session.getAttributeNames()).isEmpty(); + assertThat(session.getCreationTime()).isAfterOrEqualTo(beforeCreationTime); + assertThat(session.getCreationTime()).isBeforeOrEqualTo(Instant.now()); + assertThat(session.isExpired()).isFalse(); + assertThat(session.getLastAccessedTime()).isEqualTo(session.getCreationTime()); + assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS)); + } + + @Test + public void findByIdReturnsMatchingNonExpiredSession() { + + Instant expectedCreationTime = Instant.now(); + Instant currentLastAccessedTime = expectedCreationTime.plusMillis(TimeUnit.MINUTES.toMillis(5L)); + + Session mockSession = mock(Session.class); + + when(mockSession.isExpired()).thenReturn(false); + when(mockSession.getId()).thenReturn("1"); + when(mockSession.getCreationTime()).thenReturn(expectedCreationTime); + when(mockSession.getLastAccessedTime()).thenReturn(currentLastAccessedTime); + when(mockSession.getAttributeNames()).thenReturn(Collections.singleton("attributeOne")); + when(mockSession.getAttribute(eq("attributeOne"))).thenReturn("test"); + when(this.mockTemplate.get(eq("1"))).thenReturn(mockSession); + + GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository); + + Session actualSession = sessionRepositorySpy.findById("1"); + + assertThat(actualSession).isNotNull(); + assertThat(actualSession).isNotSameAs(mockSession); + assertThat(actualSession.getId()).isEqualTo("1"); + assertThat(actualSession.getCreationTime()).isEqualTo(expectedCreationTime); + assertThat(actualSession.getLastAccessedTime()).isNotEqualTo(currentLastAccessedTime); + assertThat(actualSession.getLastAccessedTime()).isAfterOrEqualTo(expectedCreationTime); + assertThat(actualSession.getLastAccessedTime()).isBeforeOrEqualTo(Instant.now()); + assertThat(actualSession.getAttributeNames()).containsExactly("attributeOne"); + assertThat(actualSession.getAttribute("attributeOne")).isEqualTo("test"); + + verify(this.mockTemplate, times(1)).get(eq("1")); + verify(mockSession, times(1)).isExpired(); + verify(mockSession, times(1)).getId(); + verify(mockSession, times(1)).getCreationTime(); + verify(mockSession, times(1)).getLastAccessedTime(); + verify(mockSession, times(1)).getAttributeNames(); + verify(mockSession, times(1)).getAttribute(eq("attributeOne")); + + InOrder inOrder = inOrder(sessionRepositorySpy); + + inOrder.verify(sessionRepositorySpy, times(1)).commit(eq(actualSession)); + inOrder.verify(sessionRepositorySpy, times(1)).touch(eq(actualSession)); + inOrder.verify(sessionRepositorySpy, times(1)).registerInterest(eq(actualSession)); + } + @Test public void findByIdDeletesMatchingExpiredSessionReturnsNull() { - String expectedSessionId = "1"; - Session mockSession = mock(Session.class); - given(mockSession.isExpired()).willReturn(true); - given(mockSession.getId()).willReturn(expectedSessionId); - given(this.mockTemplate.get(eq(expectedSessionId))).willReturn(mockSession); - given(this.mockTemplate.remove(eq(expectedSessionId))).willReturn(mockSession); + when(mockSession.getId()).thenReturn("1"); + when(mockSession.isExpired()).thenReturn(true); + when(this.mockTemplate.get(eq("1"))).thenReturn(mockSession); + when(this.mockTemplate.remove(eq("1"))).thenReturn(mockSession); - willAnswer(invocation -> { + doAnswer(invocation -> { ApplicationEvent applicationEvent = invocation.getArgument(0); @@ -201,83 +269,49 @@ public class GemFireOperationsSessionRepositoryTests { AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; assertThat(sessionEvent.getSession()).isSameAs(mockSession); - assertThat(sessionEvent.getSessionId()).isEqualTo(expectedSessionId); - assertThat(sessionEvent.getSource()) - .isSameAs(GemFireOperationsSessionRepositoryTests.this.sessionRepository); + assertThat(sessionEvent.getSessionId()).isEqualTo("1"); + assertThat(sessionEvent.getSource()).isSameAs(this.sessionRepository); return null; - }).given(this.mockApplicationEventPublisher).publishEvent(any(ApplicationEvent.class)); + }).when(this.mockApplicationEventPublisher).publishEvent(any(ApplicationEvent.class)); - assertThat(this.sessionRepository.findById(expectedSessionId)).isNull(); + assertThat(this.sessionRepository.findById("1")).isNull(); - verify(this.mockTemplate, times(1)).get(eq(expectedSessionId)); - verify(this.mockTemplate, times(1)).remove(eq(expectedSessionId)); - verify(mockSession, times(1)).isExpired(); + verify(this.mockTemplate, times(1)).get(eq("1")); + verify(this.mockTemplate, times(1)).remove(eq("1")); verify(mockSession, times(2)).getId(); + verify(mockSession, times(1)).isExpired(); verify(this.mockApplicationEventPublisher, times(1)) .publishEvent(isA(SessionDeletedEvent.class)); } - @Test - public void findByIdReturnsMatchingNonExpiredSession() { - - String expectedId = "1"; - - Instant expectedCreationTime = Instant.now(); - Instant currentLastAccessedTime = expectedCreationTime.plusMillis(TimeUnit.MINUTES.toMillis(5)); - - Session mockSession = mock(Session.class); - - given(mockSession.isExpired()).willReturn(false); - given(mockSession.getId()).willReturn(expectedId); - given(mockSession.getCreationTime()).willReturn(expectedCreationTime); - given(mockSession.getLastAccessedTime()).willReturn(currentLastAccessedTime); - given(mockSession.getAttributeNames()).willReturn(Collections.singleton("attrOne")); - given(mockSession.getAttribute(eq("attrOne"))).willReturn("test"); - given(this.mockTemplate.get(eq(expectedId))).willReturn(mockSession); - - Session actualSession = this.sessionRepository.findById(expectedId); - - assertThat(actualSession).isNotNull(); - assertThat(actualSession).isNotSameAs(mockSession); - assertThat(actualSession.getId()).isEqualTo(expectedId); - assertThat(actualSession.getCreationTime()).isEqualTo(expectedCreationTime); - assertThat(actualSession.getLastAccessedTime()).isNotEqualTo(currentLastAccessedTime); - assertThat(actualSession.getLastAccessedTime().compareTo(expectedCreationTime)).isGreaterThanOrEqualTo(0); - assertThat(actualSession.getAttributeNames()).isEqualTo(Collections.singleton("attrOne")); - assertThat(String.valueOf(actualSession.getAttribute("attrOne"))).isEqualTo("test"); - - verify(this.mockTemplate, times(1)).get(eq(expectedId)); - verify(mockSession, times(1)).isExpired(); - verify(mockSession, times(1)).getId(); - verify(mockSession, times(1)).getCreationTime(); - verify(mockSession, times(1)).getLastAccessedTime(); - verify(mockSession, times(1)).getAttributeNames(); - verify(mockSession, times(1)).getAttribute(eq("attrOne")); - } - @Test public void findByIdReturnsNull() { when(this.mockTemplate.get(anyString())).thenReturn(null); - assertThat(this.sessionRepository.findById("1")).isNull(); + GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository); + + assertThat(sessionRepositorySpy.findById("1")).isNull(); verify(this.mockTemplate, times(1)).get(eq("1")); + verify(sessionRepositorySpy, times(1)).findById(eq("1")); + verify(sessionRepositorySpy, never()).delete(any()); + verify(sessionRepositorySpy, never()).commit(any()); } @Test @SuppressWarnings("unchecked") public void findByIndexNameAndIndexValueReturnsMatchingSession() { - Session mockSession = mock(Session.class, "MockSession"); + Session mockSession = mock(Session.class); - given(mockSession.getId()).willReturn("1"); + when(mockSession.getId()).thenReturn("1"); SelectResults mockSelectResults = mock(SelectResults.class); - given(mockSelectResults.asList()).willReturn(Collections.singletonList(mockSession)); + when(mockSelectResults.asList()).thenReturn(Collections.singletonList(mockSession)); String indexName = "vip"; String indexValue = "rwinch"; @@ -286,18 +320,26 @@ public class GemFireOperationsSessionRepositoryTests { String.format(GemFireOperationsSessionRepository.FIND_SESSIONS_BY_INDEX_NAME_AND_INDEX_VALUE_QUERY, this.sessionRepository.getFullyQualifiedRegionName(), indexName); - given(this.mockTemplate.find(eq(expectedQql), eq(indexValue))).willReturn(mockSelectResults); + when(this.mockTemplate.find(eq(expectedQql), eq(indexValue))).thenReturn(mockSelectResults); + + GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository); Map sessions = - this.sessionRepository.findByIndexNameAndIndexValue(indexName, indexValue); + sessionRepositorySpy.findByIndexNameAndIndexValue(indexName, indexValue); assertThat(sessions).isNotNull(); - assertThat(sessions.size()).isEqualTo(1); + assertThat(sessions).hasSize(1); assertThat(sessions.get("1")).isEqualTo(mockSession); verify(this.mockTemplate, times(1)).find(eq(expectedQql), eq(indexValue)); verify(mockSelectResults, times(1)).asList(); - verify(mockSession, times(1)).getId(); + verify(mockSession, times(2)).getId(); + + InOrder inOrder = inOrder(sessionRepositorySpy); + + inOrder.verify(sessionRepositorySpy, times(1)).commit(eq(mockSession)); + inOrder.verify(sessionRepositorySpy, times(1)).touch(eq(mockSession)); + inOrder.verify(sessionRepositorySpy, times(1)).registerInterest(eq(mockSession)); } @Test @@ -308,13 +350,13 @@ public class GemFireOperationsSessionRepositoryTests { Session mockSessionTwo = mock(Session.class, "MockSessionTwo"); Session mockSessionThree = mock(Session.class, "MockSessionThree"); - given(mockSessionOne.getId()).willReturn("1"); - given(mockSessionTwo.getId()).willReturn("2"); - given(mockSessionThree.getId()).willReturn("3"); + when(mockSessionOne.getId()).thenReturn("1"); + when(mockSessionTwo.getId()).thenReturn("2"); + when(mockSessionThree.getId()).thenReturn("3"); SelectResults mockSelectResults = mock(SelectResults.class); - given(mockSelectResults.asList()).willReturn(Arrays.asList(mockSessionOne, mockSessionTwo, mockSessionThree)); + when(mockSelectResults.asList()).thenReturn(Arrays.asList(mockSessionOne, mockSessionTwo, mockSessionThree)); String principalName = "jblum"; @@ -322,22 +364,36 @@ public class GemFireOperationsSessionRepositoryTests { String.format(GemFireOperationsSessionRepository.FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, this.sessionRepository.getFullyQualifiedRegionName()); - given(this.mockTemplate.find(eq(expectedOql), eq(principalName))).willReturn(mockSelectResults); + when(this.mockTemplate.find(eq(expectedOql), eq(principalName))).thenReturn(mockSelectResults); + + GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository); Map sessions = - this.sessionRepository.findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName); + sessionRepositorySpy.findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName); assertThat(sessions).isNotNull(); - assertThat(sessions.size()).isEqualTo(3); + assertThat(sessions).hasSize(3); assertThat(sessions.get("1")).isEqualTo(mockSessionOne); assertThat(sessions.get("2")).isEqualTo(mockSessionTwo); assertThat(sessions.get("3")).isEqualTo(mockSessionThree); verify(this.mockTemplate, times(1)).find(eq(expectedOql), eq(principalName)); verify(mockSelectResults, times(1)).asList(); - verify(mockSessionOne, times(1)).getId(); - verify(mockSessionTwo, times(1)).getId(); - verify(mockSessionThree, times(1)).getId(); + verify(mockSessionOne, times(2)).getId(); + verify(mockSessionTwo, times(2)).getId(); + verify(mockSessionThree, times(2)).getId(); + + InOrder inOrder = inOrder(sessionRepositorySpy); + + inOrder.verify(sessionRepositorySpy, times(1)).commit(eq(mockSessionOne)); + inOrder.verify(sessionRepositorySpy, times(1)).touch(eq(mockSessionOne)); + inOrder.verify(sessionRepositorySpy, times(1)).registerInterest(eq(mockSessionOne)); + inOrder.verify(sessionRepositorySpy, times(1)).commit(eq(mockSessionTwo)); + inOrder.verify(sessionRepositorySpy, times(1)).touch(eq(mockSessionTwo)); + inOrder.verify(sessionRepositorySpy, times(1)).registerInterest(eq(mockSessionTwo)); + inOrder.verify(sessionRepositorySpy, times(1)).commit(eq(mockSessionThree)); + inOrder.verify(sessionRepositorySpy, times(1)).touch(eq(mockSessionThree)); + inOrder.verify(sessionRepositorySpy, times(1)).registerInterest(eq(mockSessionThree)); } @Test @@ -346,7 +402,7 @@ public class GemFireOperationsSessionRepositoryTests { SelectResults mockSelectResults = mock(SelectResults.class); - given(mockSelectResults.asList()).willReturn(Collections.emptyList()); + when(mockSelectResults.asList()).thenReturn(Collections.emptyList()); String principalName = "jblum"; @@ -354,20 +410,25 @@ public class GemFireOperationsSessionRepositoryTests { String.format(GemFireOperationsSessionRepository.FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, this.sessionRepository.getFullyQualifiedRegionName()); - given(this.mockTemplate.find(eq(expectedOql), eq(principalName))).willReturn(mockSelectResults); + when(this.mockTemplate.find(eq(expectedOql), eq(principalName))).thenReturn(mockSelectResults); + + GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository); Map sessions = - this.sessionRepository.findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName); + sessionRepositorySpy.findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName); assertThat(sessions).isNotNull(); - assertThat(sessions.isEmpty()).isTrue(); + assertThat(sessions).isEmpty(); verify(this.mockTemplate, times(1)).find(eq(expectedOql), eq(principalName)); verify(mockSelectResults, times(1)).asList(); + verify(sessionRepositorySpy, times(1)) + .findByIndexNameAndIndexValue(eq(PRINCIPAL_NAME_INDEX_NAME), eq(principalName)); + verify(sessionRepositorySpy, never()).commit(any()); } @Test - public void prepareQueryReturnsIndexNameValueOql() { + public void prepareQueryReturnsIndexNameAndIndexValueOql() { String attributeName = "testAttributeName"; @@ -403,8 +464,6 @@ public class GemFireOperationsSessionRepositoryTests { @Test public void saveStoresSession() { - String expectedSessionId = "1"; - Instant expectedCreationTime = Instant.now(); Instant expectedLastAccessTime = expectedCreationTime.plusMillis(TimeUnit.MINUTES.toMillis(5L)); @@ -412,19 +471,19 @@ public class GemFireOperationsSessionRepositoryTests { Session mockSession = mock(Session.class); - when(mockSession.getId()).thenReturn(expectedSessionId); + when(mockSession.getId()).thenReturn("1"); when(mockSession.getCreationTime()).thenReturn(expectedCreationTime); when(mockSession.getLastAccessedTime()).thenReturn(expectedLastAccessTime); when(mockSession.getMaxInactiveInterval()).thenReturn(expectedMaxInactiveInterval); when(mockSession.getAttributeNames()).thenReturn(Collections.emptySet()); - when(this.mockTemplate.put(eq(expectedSessionId), isA(GemFireSession.class))) + when(this.mockTemplate.put(eq("1"), isA(GemFireSession.class))) .thenAnswer(invocation -> { Session session = invocation.getArgument(1); assertThat(session).isNotNull(); - assertThat(session.getId()).isEqualTo(expectedSessionId); + assertThat(session.getId()).isEqualTo("1"); assertThat(session.getCreationTime()).isEqualTo(expectedCreationTime); assertThat(session.getLastAccessedTime()).isEqualTo(expectedLastAccessTime); assertThat(session.getMaxInactiveInterval()).isEqualTo(expectedMaxInactiveInterval); @@ -433,30 +492,36 @@ public class GemFireOperationsSessionRepositoryTests { return null; }); - this.sessionRepository.save(mockSession); + GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository); + + sessionRepositorySpy.save(mockSession); verify(mockSession, times(2)).getId(); verify(mockSession, times(1)).getCreationTime(); verify(mockSession, times(1)).getLastAccessedTime(); verify(mockSession, times(1)).getMaxInactiveInterval(); verify(mockSession, times(1)).getAttributeNames(); - verify(this.mockTemplate, times(1)).put(eq(expectedSessionId), + verify(this.mockTemplate, times(1)).put(eq("1"), isA(GemFireSession.class)); + verify(sessionRepositorySpy, times(1)).save(eq(mockSession)); + verify(sessionRepositorySpy, times(1)).commit(eq(mockSession)); } @Test public void saveStoresAndCommitsGemFireSession() { - GemFireSession session = spy(GemFireSession.create()); + GemFireSession session = GemFireSession.create(); assertThat(session).isNotNull(); assertThat(session.hasDelta()).isTrue(); + session = spy(session); + this.sessionRepository.save(session); InOrder orderVerifier = inOrder(session); - orderVerifier.verify(session, times(2)).hasDelta(); + orderVerifier.verify(session, times(1)).hasDelta(); orderVerifier.verify(session, times(1)).getId(); orderVerifier.verify(session, times(1)).commit(); @@ -468,14 +533,16 @@ public class GemFireOperationsSessionRepositoryTests { @SuppressWarnings("unchecked") public void saveWillNotStoreNonDirtyGemFireSessions() { - GemFireSession session = spy(newNonDirtyGemFireSession()); + GemFireSession session = newNonDirtyGemFireSession(); assertThat(session).isNotNull(); assertThat(session.hasDelta()).isFalse(); + session = spy(session); + this.sessionRepository.save(session); - verify(session, times(2)).hasDelta(); + verify(session, times(1)).hasDelta(); verify(session, never()).getId(); verify(session, never()).commit(); verify(this.mockTemplate, never()).put(any(), any(GemFireSession.class)); @@ -484,14 +551,14 @@ public class GemFireOperationsSessionRepositoryTests { @Test public void deleteRemovesExistingSessionAndHandlesDelete() { - String expectedSessionId = "1"; + AtomicBoolean methodCalled = new AtomicBoolean(false); Session mockSession = mock(Session.class); - given(mockSession.getId()).willReturn(expectedSessionId); - given(this.mockTemplate.remove(eq(expectedSessionId))).willReturn(mockSession); + when(mockSession.getId()).thenReturn("1"); + when(this.mockTemplate.remove(eq("1"))).thenReturn(mockSession); - willAnswer(invocation -> { + doAnswer(invocation -> { ApplicationEvent applicationEvent = invocation.getArgument(0); @@ -499,18 +566,22 @@ public class GemFireOperationsSessionRepositoryTests { AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent; - assertThat(sessionEvent.getSource()).isSameAs(GemFireOperationsSessionRepositoryTests.this.sessionRepository); assertThat(sessionEvent.getSession()).isSameAs(mockSession); - assertThat(sessionEvent.getSessionId()).isEqualTo(expectedSessionId); + assertThat(sessionEvent.getSessionId()).isEqualTo("1"); + assertThat(sessionEvent.getSource()).isSameAs(this.sessionRepository); + + methodCalled.set(true); return null; - }).given(this.mockApplicationEventPublisher).publishEvent(isA(SessionDeletedEvent.class)); + }).when(this.mockApplicationEventPublisher).publishEvent(isA(SessionDeletedEvent.class)); - this.sessionRepository.deleteById(expectedSessionId); + this.sessionRepository.deleteById("1"); + + assertThat(methodCalled.get()).isTrue(); verify(mockSession, times(1)).getId(); - verify(this.mockTemplate, times(1)).remove(eq(expectedSessionId)); + verify(this.mockTemplate, times(1)).remove(eq("1")); verify(this.mockApplicationEventPublisher, times(1)) .publishEvent(isA(SessionDeletedEvent.class)); } @@ -518,14 +589,9 @@ public class GemFireOperationsSessionRepositoryTests { @Test public void deleteRemovesNonExistingSessionAndHandlesDelete() { - AtomicBoolean called = new AtomicBoolean(false); + AtomicBoolean methodCalled = new AtomicBoolean(false); - Session mockSession = mock(Session.class); - - String expectedSessionId = "1"; - - when(mockSession.getId()).thenReturn(expectedSessionId); - when(this.mockTemplate.remove(anyString())).thenReturn(mockSession); + when(this.mockTemplate.remove(eq("1"))).thenReturn(null); doAnswer(invocation -> { @@ -538,23 +604,21 @@ public class GemFireOperationsSessionRepositoryTests { Session session = sessionEvent.getSession(); assertThat(session).isNotNull(); - assertThat(session.getId()).isEqualTo(expectedSessionId); - assertThat(sessionEvent.getSessionId()).isEqualTo(expectedSessionId); - assertThat(sessionEvent.getSource()) - .isSameAs(GemFireOperationsSessionRepositoryTests.this.sessionRepository); + assertThat(session.getId()).isEqualTo("1"); + assertThat(sessionEvent.getSessionId()).isEqualTo("1"); + assertThat(sessionEvent.getSource()).isEqualTo(this.sessionRepository); - called.set(true); + methodCalled.set(true); return null; }).when(this.mockApplicationEventPublisher).publishEvent(isA(SessionDeletedEvent.class)); - this.sessionRepository.deleteById(expectedSessionId); + this.sessionRepository.deleteById("1"); - assertThat(called.get()).isTrue(); + assertThat(methodCalled.get()).isTrue(); - verify(mockSession, times(2)).getId(); - verify(this.mockTemplate, times(1)).remove(eq(expectedSessionId)); + verify(this.mockTemplate, times(1)).remove(eq("1")); verify(this.mockApplicationEventPublisher, times(1)) .publishEvent(isA(SessionDeletedEvent.class)); } diff --git a/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationTests.java b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationTests.java index de9200d..a6a9909 100644 --- a/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationTests.java +++ b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationTests.java @@ -23,6 +23,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -58,7 +59,6 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.data.gemfire.GemfireOperations; import org.springframework.data.gemfire.GemfireTemplate; import org.springframework.data.gemfire.RegionAttributesFactoryBean; import org.springframework.data.gemfire.util.ArrayUtils; @@ -96,7 +96,7 @@ import org.springframework.util.ReflectionUtils; public class GemFireHttpSessionConfigurationTests { @SuppressWarnings("unchecked") - private T getField(Object obj, String fieldName) { + private static T getField(Object obj, String fieldName) { try { @@ -678,9 +678,14 @@ public class GemFireHttpSessionConfigurationTests { } @Test + @SuppressWarnings("unchecked") public void createsAndInitializesSessionRepositoryBean() { - GemfireOperations mockGemfireOperations = mock(GemfireOperations.class); + Region mockRegion = mock(Region.class); + + GemfireTemplate mockGemfireOperations = mock(GemfireTemplate.class); + + doReturn(mockRegion).when(mockGemfireOperations).getRegion(); this.gemfireConfiguration.setMaxInactiveIntervalInSeconds(120); @@ -688,7 +693,7 @@ public class GemFireHttpSessionConfigurationTests { this.gemfireConfiguration.sessionRepository(mockGemfireOperations); assertThat(sessionRepository).isNotNull(); - assertThat(sessionRepository.getTemplate()).isSameAs(mockGemfireOperations); + assertThat(sessionRepository.getSessionsTemplate()).isSameAs(mockGemfireOperations); assertThat(sessionRepository.getMaxInactiveIntervalInSeconds()).isEqualTo(120); }