Add More Tests
diff --git a/modules/spring-sessions/pom.xml b/modules/spring-sessions/pom.xml
index 0c96f6c..4fca7b6 100644
--- a/modules/spring-sessions/pom.xml
+++ b/modules/spring-sessions/pom.xml
@@ -23,13 +23,13 @@
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-core</artifactId>
-            <version>${spring.version}</version>
+            <version>5.3.8</version>
         </dependency>
 
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-expression</artifactId>
-            <version>${spring.version}</version>
+            <version>5.3.8</version>
         </dependency>
 
         <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
@@ -98,6 +98,36 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>3.0.1</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+            <version>5.3.8</version>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/org.apache.ignite/ignite-indexing -->
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-indexing</artifactId>
+            <version>2.10.0</version>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>2.22.0</version>
+            <scope>test</scope>
+        </dependency>
+
+
     </dependencies>
 
 
diff --git a/modules/spring-sessions/src/test/java/org/apache/ignite/spring/sessions/EmbeddedIgniteIndexedSessionRepositoryITests.java b/modules/spring-sessions/src/test/java/org/apache/ignite/spring/sessions/EmbeddedIgniteIndexedSessionRepositoryITests.java
new file mode 100644
index 0000000..cf3db98
--- /dev/null
+++ b/modules/spring-sessions/src/test/java/org/apache/ignite/spring/sessions/EmbeddedIgniteIndexedSessionRepositoryITests.java
@@ -0,0 +1,44 @@
+package org.apache.ignite.spring.sessions;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.context.web.WebAppConfiguration;
+
+/**
+ * Integration tests for {@link IgniteIndexedSessionRepository} using embedded topology.
+ */
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration
+@WebAppConfiguration
+class EmbeddedIgniteIndexedSessionRepositoryITests extends AbstractIgniteIndexedSessionRepositoryITests {
+
+    @BeforeAll
+    static void setUpClass() {
+        Ignition.stopAll(true);
+    }
+
+    @AfterAll
+    static void tearDownClass() {
+        Ignition.stopAll(true);
+    }
+
+    @EnableIgniteHttpSession
+    @Configuration
+    static class IgniteSessionConfig {
+
+        @Bean
+        Ignite ignite() {
+            return IgniteITestUtils.embeddedIgniteServer();
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/modules/spring-sessions/src/test/java/org/apache/ignite/spring/sessions/IgniteITestUtils.java b/modules/spring-sessions/src/test/java/org/apache/ignite/spring/sessions/IgniteITestUtils.java
new file mode 100644
index 0000000..e55ebb1
--- /dev/null
+++ b/modules/spring-sessions/src/test/java/org/apache/ignite/spring/sessions/IgniteITestUtils.java
@@ -0,0 +1,22 @@
+package org.apache.ignite.spring.sessions;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+
+/**
+ * Utility class for Ignite integration tests.
+ */
+final class IgniteITestUtils {
+
+    private IgniteITestUtils() {
+    }
+
+    /**
+     * Creates {@link Ignite} for use in integration tests.
+     * @return the Ignite instance
+     */
+    static Ignite embeddedIgniteServer() {
+        return Ignition.start();
+    }
+
+}
diff --git a/modules/spring-sessions/src/test/java/org/apache/ignite/spring/sessions/IgniteIndexedSessionRepositoryTests.java b/modules/spring-sessions/src/test/java/org/apache/ignite/spring/sessions/IgniteIndexedSessionRepositoryTests.java
new file mode 100644
index 0000000..cdb1617
--- /dev/null
+++ b/modules/spring-sessions/src/test/java/org/apache/ignite/spring/sessions/IgniteIndexedSessionRepositoryTests.java
@@ -0,0 +1,433 @@
+package org.apache.ignite.spring.sessions;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.cache.expiry.TouchedExpiryPolicy;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.query.FieldsQueryCursor;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.session.FindByIndexNameSessionRepository;
+import org.springframework.session.FlushMode;
+import org.springframework.session.MapSession;
+import org.apache.ignite.spring.sessions.IgniteIndexedSessionRepository.IgniteSession;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+/**
+ * Tests for {@link IgniteIndexedSessionRepository}.
+ */
+class IgniteIndexedSessionRepositoryTests {
+
+    private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
+
+    private final Ignite ignite = mock(Ignite.class);
+
+    @SuppressWarnings("unchecked")
+    private final IgniteCache<String, IgniteSession> sessions = mock(IgniteCache.class);
+
+    private IgniteIndexedSessionRepository repository;
+
+    @BeforeEach
+    void setUp() {
+        given(this.ignite.<String, IgniteSession>getOrCreateCache(
+                ArgumentMatchers.<CacheConfiguration<String, IgniteSession>>any())).willReturn(this.sessions);
+        given(this.sessions.withExpiryPolicy(ArgumentMatchers.any())).willReturn(this.sessions);
+        this.repository = new IgniteIndexedSessionRepository(this.ignite);
+        this.repository.init();
+    }
+
+    @Test
+    void constructorNullIgnite() {
+        assertThatIllegalArgumentException().isThrownBy(() -> new IgniteIndexedSessionRepository(null))
+                .withMessage("Ignite must not be null");
+    }
+
+    @Test
+    void setSaveModeNull() {
+        assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setSaveMode(null))
+                .withMessage("saveMode must not be null");
+    }
+
+    @Test
+    void createSessionDefaultMaxInactiveInterval() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        IgniteSession session = this.repository.createSession();
+
+        assertThat(session.getMaxInactiveInterval()).isEqualTo(new MapSession().getMaxInactiveInterval());
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void createSessionCustomMaxInactiveInterval() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        int interval = 1;
+        this.repository.setDefaultMaxInactiveInterval(interval);
+
+        IgniteSession session = this.repository.createSession();
+
+        assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofSeconds(interval));
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void saveNewFlushModeOnSave() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        IgniteSession session = this.repository.createSession();
+        verifyNoMoreInteractions(this.sessions);
+
+        this.repository.save(session);
+        verify(this.sessions, times(1)).put(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void saveNewFlushModeImmediate() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        this.repository.setFlushMode(FlushMode.IMMEDIATE);
+
+        IgniteSession session = this.repository.createSession();
+        verify(this.sessions, times(1)).put(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void saveUpdatedAttributeFlushModeOnSave() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        IgniteSession session = this.repository.createSession();
+        session.setAttribute("testName", "testValue");
+        verifyNoMoreInteractions(this.sessions);
+
+        this.repository.save(session);
+        verify(this.sessions, times(1)).put(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void saveUpdatedAttributeFlushModeImmediate() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        this.repository.setFlushMode(FlushMode.IMMEDIATE);
+
+        IgniteSession session = this.repository.createSession();
+        session.setAttribute("testName", "testValue");
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+        verify(this.sessions, times(1)).put(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).replace(eq(session.getId()), eq(session));
+
+        this.repository.save(session);
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void removeAttributeFlushModeOnSave() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        IgniteSession session = this.repository.createSession();
+        session.removeAttribute("testName");
+        verifyNoMoreInteractions(this.sessions);
+
+        this.repository.save(session);
+        verify(this.sessions, times(1)).put(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void removeAttributeFlushModeImmediate() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        this.repository.setFlushMode(FlushMode.IMMEDIATE);
+
+        IgniteSession session = this.repository.createSession();
+        session.removeAttribute("testName");
+        verify(this.sessions, times(1)).put(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).replace(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+
+        this.repository.save(session);
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void saveUpdatedLastAccessedTimeFlushModeOnSave() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        IgniteSession session = this.repository.createSession();
+        session.setLastAccessedTime(Instant.now());
+        verifyNoMoreInteractions(this.sessions);
+
+        this.repository.save(session);
+        verify(this.sessions, times(1)).put(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void saveUpdatedLastAccessedTimeFlushModeImmediate() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        this.repository.setFlushMode(FlushMode.IMMEDIATE);
+
+        IgniteSession session = this.repository.createSession();
+        session.setLastAccessedTime(Instant.now());
+        verify(this.sessions, times(1)).put(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).replace(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+
+        this.repository.save(session);
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void saveUpdatedMaxInactiveIntervalInSecondsFlushModeOnSave() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        IgniteSession session = this.repository.createSession();
+        session.setMaxInactiveInterval(Duration.ofSeconds(1));
+        verifyNoMoreInteractions(this.sessions);
+
+        this.repository.save(session);
+        verify(this.sessions, times(1)).put(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void saveUpdatedMaxInactiveIntervalInSecondsFlushModeImmediate() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        this.repository.setFlushMode(FlushMode.IMMEDIATE);
+
+        IgniteSession session = this.repository.createSession();
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+        String sessionId = session.getId();
+        session.setMaxInactiveInterval(Duration.ofSeconds(1));
+        verify(this.sessions, times(1)).put(eq(sessionId), eq(session));
+        verify(this.sessions, times(1)).replace(eq(sessionId), eq(session));
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+
+        this.repository.save(session);
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void saveUnchangedFlushModeOnSave() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        IgniteSession session = this.repository.createSession();
+        this.repository.save(session);
+        verify(this.sessions, times(1)).put(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+
+        this.repository.save(session);
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void saveUnchangedFlushModeImmediate() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        this.repository.setFlushMode(FlushMode.IMMEDIATE);
+
+        IgniteSession session = this.repository.createSession();
+        verify(this.sessions, times(1)).put(eq(session.getId()), eq(session));
+        verify(this.sessions, times(1)).withExpiryPolicy(eq(createExpiryPolicy(session)));
+
+        this.repository.save(session);
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void getSessionNotFound() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        String sessionId = "testSessionId";
+
+        IgniteSession session = this.repository.findById(sessionId);
+
+        assertThat(session).isNull();
+        verify(this.sessions, times(1)).get(eq(sessionId));
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void getSessionExpired() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        IgniteSession expired = this.repository.new IgniteSession(new MapSession(), true);
+
+        expired.setLastAccessedTime(Instant.now().minusSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS + 1));
+        given(this.sessions.get(eq(expired.getId()))).willReturn(expired);
+
+        IgniteSession session = this.repository.findById(expired.getId());
+
+        assertThat(session).isNull();
+        verify(this.sessions, times(1)).get(eq(expired.getId()));
+        verify(this.sessions, times(1)).remove(eq(expired.getId()));
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void getSessionFound() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        IgniteSession saved = this.repository.new IgniteSession(new MapSession(), true);
+        saved.setAttribute("savedName", "savedValue");
+        given(this.sessions.get(eq(saved.getId()))).willReturn(saved);
+
+        IgniteSession session = this.repository.findById(saved.getId());
+
+        assertThat(session.getId()).isEqualTo(saved.getId());
+        assertThat(session.<String>getAttribute("savedName")).isEqualTo("savedValue");
+        verify(this.sessions, times(1)).get(eq(saved.getId()));
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void delete() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        String sessionId = "testSessionId";
+
+        this.repository.deleteById(sessionId);
+
+        verify(this.sessions, times(1)).remove(eq(sessionId));
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void findByIndexNameAndIndexValueUnknownIndexName() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        String indexValue = "testIndexValue";
+
+        Map<String, IgniteSession> sessions = this.repository.findByIndexNameAndIndexValue("testIndexName", indexValue);
+
+        assertThat(sessions).isEmpty();
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void findByIndexNameAndIndexValuePrincipalIndexNameNotFound() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        String principal = "username";
+
+        Map<String, IgniteSession> sessions = this.repository
+                .findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
+
+        verify(this.sessions, times(1)).query(ArgumentMatchers
+                .argThat((argument) -> ("SELECT * FROM IgniteSession WHERE principal='" + principal + "'")
+                        .equals(argument.getSql())));
+
+        assertThat(sessions).isEmpty();
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void findByIndexNameAndIndexValuePrincipalIndexNameFound() {
+        verify(this.sessions, times(1)).registerCacheEntryListener(ArgumentMatchers.any());
+
+        String principal = "username";
+        Authentication authentication = new UsernamePasswordAuthenticationToken(principal, "notused",
+                AuthorityUtils.createAuthorityList("ROLE_USER"));
+
+        List<Object> saved = new ArrayList<>(2);
+
+        final MapSession ses1 = new MapSession();
+        ses1.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
+        IgniteSession saved1 = this.repository.new IgniteSession(ses1, true);
+        saved.add(Arrays.asList(ses1, authentication.getPrincipal()));
+        final MapSession ses2 = new MapSession();
+        ses2.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
+        IgniteSession saved2 = this.repository.new IgniteSession(ses2, true);
+        saved.add(Arrays.asList(ses2, authentication.getPrincipal()));
+
+        given(this.sessions.query(ArgumentMatchers.any())).willReturn(new FieldsQueryCursor<List<?>>() {
+            @Override
+            public String getFieldName(int idx) {
+                return null;
+            }
+
+            @Override
+            public int getColumnsCount() {
+                return 2;
+            }
+
+            @Override
+            public List<List<?>> getAll() {
+                return (List) saved;
+            }
+
+            @Override
+            public void close() {
+
+            }
+
+            @NotNull
+            @Override
+            public Iterator<List<?>> iterator() {
+                return (Iterator) saved.iterator();
+            }
+        });
+
+        Map<String, IgniteSession> sessions = this.repository
+                .findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
+
+        assertThat(sessions).hasSize(2);
+        verify(this.sessions, times(1)).query(any());
+        verifyNoMoreInteractions(this.sessions);
+    }
+
+    @Test
+    void getAttributeNamesAndRemove() {
+        IgniteSession session = this.repository.createSession();
+        session.setAttribute("attribute1", "value1");
+        session.setAttribute("attribute2", "value2");
+
+        for (String attributeName : session.getAttributeNames()) {
+            session.removeAttribute(attributeName);
+        }
+
+        assertThat(session.getAttributeNames()).isEmpty();
+    }
+
+    private static TouchedExpiryPolicy createExpiryPolicy(IgniteSession session) {
+        return new TouchedExpiryPolicy(
+                new javax.cache.expiry.Duration(TimeUnit.SECONDS, session.getMaxInactiveInterval().getSeconds()));
+    }
+
+}