@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright 2014-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.session.data.redis;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
|
||||
import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link SimpleRedisOperationsSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
@WebAppConfiguration
|
||||
class SimpleRedisOperationsSessionRepositoryITests extends AbstractRedisITests {
|
||||
|
||||
@Autowired
|
||||
private SimpleRedisOperationsSessionRepository sessionRepository;
|
||||
|
||||
@Test
|
||||
void save_NewSession_ShouldSaveSession() {
|
||||
RedisSession session = createAndSaveSession(Instant.now());
|
||||
assertThat(session.getMaxInactiveInterval()).isEqualTo(
|
||||
Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS));
|
||||
assertThat(session.getAttributeNames())
|
||||
.isEqualTo(Collections.singleton("attribute1"));
|
||||
assertThat(session.<String>getAttribute("attribute1")).isEqualTo("value1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_LastAccessedTimeInPast_ShouldExpireSession() {
|
||||
assertThat(createAndSaveSession(Instant.EPOCH)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_DeletedSession_ShouldThrowException() {
|
||||
RedisSession session = createAndSaveSession(Instant.now());
|
||||
this.sessionRepository.deleteById(session.getId());
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> this.sessionRepository.save(session))
|
||||
.withMessage("Session was invalidated");
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_ConcurrentUpdates_ShouldSaveSession() {
|
||||
RedisSession copy1 = createAndSaveSession(Instant.now());
|
||||
String sessionId = copy1.getId();
|
||||
RedisSession copy2 = this.sessionRepository.findById(sessionId);
|
||||
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
|
||||
updateSession(copy1, now.plusSeconds(1L), "attribute2", "value2");
|
||||
this.sessionRepository.save(copy1);
|
||||
updateSession(copy2, now.plusSeconds(2L), "attribute3", "value3");
|
||||
this.sessionRepository.save(copy2);
|
||||
RedisSession session = this.sessionRepository.findById(sessionId);
|
||||
assertThat(session.getLastAccessedTime()).isEqualTo(now.plusSeconds(2L));
|
||||
assertThat(session.getAttributeNames()).hasSize(3);
|
||||
assertThat(session.<String>getAttribute("attribute1")).isEqualTo("value1");
|
||||
assertThat(session.<String>getAttribute("attribute2")).isEqualTo("value2");
|
||||
assertThat(session.<String>getAttribute("attribute3")).isEqualTo("value3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_ChangeSessionIdAndUpdateAttribute_ShouldChangeSessionId() {
|
||||
RedisSession session = createAndSaveSession(Instant.now());
|
||||
String originalSessionId = session.getId();
|
||||
updateSession(session, Instant.now(), "attribute1", "value2");
|
||||
String newSessionId = session.changeSessionId();
|
||||
this.sessionRepository.save(session);
|
||||
RedisSession loaded = this.sessionRepository.findById(newSessionId);
|
||||
assertThat(loaded).isNotNull();
|
||||
assertThat(loaded.getAttributeNames()).hasSize(1);
|
||||
assertThat(loaded.<String>getAttribute("attribute1")).isEqualTo("value2");
|
||||
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_OnlyChangeSessionId_ShouldChangeSessionId() {
|
||||
RedisSession session = createAndSaveSession(Instant.now());
|
||||
String originalSessionId = session.getId();
|
||||
String newSessionId = session.changeSessionId();
|
||||
this.sessionRepository.save(session);
|
||||
assertThat(this.sessionRepository.findById(newSessionId)).isNotNull();
|
||||
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_ChangeSessionIdTwice_ShouldChangeSessionId() {
|
||||
RedisSession session = createAndSaveSession(Instant.now());
|
||||
String originalSessionId = session.getId();
|
||||
updateSession(session, Instant.now(), "attribute1", "value2");
|
||||
String newSessionId1 = session.changeSessionId();
|
||||
updateSession(session, Instant.now(), "attribute1", "value3");
|
||||
String newSessionId2 = session.changeSessionId();
|
||||
this.sessionRepository.save(session);
|
||||
assertThat(this.sessionRepository.findById(newSessionId1)).isNull();
|
||||
assertThat(this.sessionRepository.findById(newSessionId2)).isNotNull();
|
||||
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_ChangeSessionIdOnNewSession_ShouldChangeSessionId() {
|
||||
RedisSession session = this.sessionRepository.createSession();
|
||||
String originalSessionId = session.getId();
|
||||
updateSession(session, Instant.now(), "attribute1", "value1");
|
||||
String newSessionId = session.changeSessionId();
|
||||
this.sessionRepository.save(session);
|
||||
assertThat(this.sessionRepository.findById(newSessionId)).isNotNull();
|
||||
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_ChangeSessionIdSaveTwice_ShouldChangeSessionId() {
|
||||
RedisSession session = createAndSaveSession(Instant.now());
|
||||
String originalSessionId;
|
||||
originalSessionId = session.getId();
|
||||
updateSession(session, Instant.now(), "attribute1", "value1");
|
||||
String newSessionId = session.changeSessionId();
|
||||
this.sessionRepository.save(session);
|
||||
this.sessionRepository.save(session);
|
||||
assertThat(this.sessionRepository.findById(newSessionId)).isNotNull();
|
||||
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_ChangeSessionIdOnDeletedSession_ShouldThrowException() {
|
||||
RedisSession session = createAndSaveSession(Instant.now());
|
||||
String originalSessionId = session.getId();
|
||||
this.sessionRepository.deleteById(originalSessionId);
|
||||
updateSession(session, Instant.now(), "attribute1", "value1");
|
||||
String newSessionId = session.changeSessionId();
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> this.sessionRepository.save(session))
|
||||
.withMessage("Session was invalidated");
|
||||
assertThat(this.sessionRepository.findById(newSessionId)).isNull();
|
||||
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_ChangeSessionIdConcurrent_ShouldThrowException() {
|
||||
RedisSession copy1 = createAndSaveSession(Instant.now());
|
||||
String originalSessionId = copy1.getId();
|
||||
RedisSession copy2 = this.sessionRepository.findById(originalSessionId);
|
||||
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
|
||||
updateSession(copy1, now.plusSeconds(1L), "attribute2", "value2");
|
||||
String newSessionId1 = copy1.changeSessionId();
|
||||
this.sessionRepository.save(copy1);
|
||||
updateSession(copy2, now.plusSeconds(2L), "attribute3", "value3");
|
||||
String newSessionId2 = copy2.changeSessionId();
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> this.sessionRepository.save(copy2))
|
||||
.withMessage("Session was invalidated");
|
||||
assertThat(this.sessionRepository.findById(newSessionId1)).isNotNull();
|
||||
assertThat(this.sessionRepository.findById(newSessionId2)).isNull();
|
||||
assertThat(this.sessionRepository.findById(originalSessionId)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteById_ValidSession_ShouldDeleteSession() {
|
||||
RedisSession session = createAndSaveSession(Instant.now());
|
||||
this.sessionRepository.deleteById(session.getId());
|
||||
assertThat(this.sessionRepository.findById(session.getId())).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteById_DeletedSession_ShouldDoNothing() {
|
||||
RedisSession session = createAndSaveSession(Instant.now());
|
||||
this.sessionRepository.deleteById(session.getId());
|
||||
this.sessionRepository.deleteById(session.getId());
|
||||
assertThat(this.sessionRepository.findById(session.getId())).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteById_NonexistentSession_ShouldDoNothing() {
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
this.sessionRepository.deleteById(sessionId);
|
||||
assertThat(this.sessionRepository.findById(sessionId)).isNull();
|
||||
}
|
||||
|
||||
private RedisSession createAndSaveSession(Instant lastAccessedTime) {
|
||||
RedisSession session = this.sessionRepository.createSession();
|
||||
session.setLastAccessedTime(lastAccessedTime);
|
||||
session.setAttribute("attribute1", "value1");
|
||||
this.sessionRepository.save(session);
|
||||
return this.sessionRepository.findById(session.getId());
|
||||
}
|
||||
|
||||
private static void updateSession(RedisSession session, Instant lastAccessedTime,
|
||||
String attributeName, Object attributeValue) {
|
||||
session.setLastAccessedTime(lastAccessedTime);
|
||||
session.setAttribute(attributeName, attributeValue);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableSpringHttpSession
|
||||
static class Config extends BaseConfig {
|
||||
|
||||
@Bean
|
||||
public SimpleRedisOperationsSessionRepository sessionRepository(
|
||||
RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
redisTemplate.afterPropertiesSet();
|
||||
return new SimpleRedisOperationsSessionRepository(redisTemplate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright 2014-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.session.data.redis;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link SessionRepository} implementation that uses Spring Data's
|
||||
* {@link RedisOperations} to store sessions is Redis.
|
||||
* <p>
|
||||
* This implementation does not support publishing of session events.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public class SimpleRedisOperationsSessionRepository implements
|
||||
SessionRepository<SimpleRedisOperationsSessionRepository.RedisSession> {
|
||||
|
||||
private static final String DEFAULT_KEY_NAMESPACE = "spring:session:";
|
||||
|
||||
private final RedisOperations<String, Object> sessionRedisOperations;
|
||||
|
||||
private Duration defaultMaxInactiveInterval = Duration
|
||||
.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
|
||||
|
||||
private String keyNamespace = DEFAULT_KEY_NAMESPACE;
|
||||
|
||||
private RedisFlushMode flushMode = RedisFlushMode.ON_SAVE;
|
||||
|
||||
/**
|
||||
* Create a new {@link SimpleRedisOperationsSessionRepository} instance.
|
||||
* @param sessionRedisOperations the {@link RedisOperations} to use for managing
|
||||
* sessions
|
||||
*/
|
||||
public SimpleRedisOperationsSessionRepository(
|
||||
RedisOperations<String, Object> sessionRedisOperations) {
|
||||
Assert.notNull(sessionRedisOperations, "sessionRedisOperations mut not be null");
|
||||
this.sessionRedisOperations = sessionRedisOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default maxInactiveInterval.
|
||||
* @param defaultMaxInactiveInterval the default maxInactiveInterval
|
||||
*/
|
||||
public void setDefaultMaxInactiveInterval(Duration defaultMaxInactiveInterval) {
|
||||
Assert.notNull(defaultMaxInactiveInterval,
|
||||
"defaultMaxInactiveInterval must not be null");
|
||||
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the key namespace.
|
||||
* @param keyNamespace the key namespace
|
||||
*/
|
||||
public void setKeyNamespace(String keyNamespace) {
|
||||
Assert.hasText(keyNamespace, "keyNamespace must not be empty");
|
||||
this.keyNamespace = keyNamespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flush mode.
|
||||
* @param flushMode the flush mode
|
||||
*/
|
||||
public void setFlushMode(RedisFlushMode flushMode) {
|
||||
Assert.notNull(flushMode, "flushMode must not be null");
|
||||
this.flushMode = flushMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedisSession createSession() {
|
||||
RedisSession session = new RedisSession(this.defaultMaxInactiveInterval);
|
||||
session.flushIfRequired();
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(RedisSession session) {
|
||||
if (!session.isNew) {
|
||||
String key = getSessionKey(
|
||||
session.hasChangedSessionId() ? session.originalSessionId
|
||||
: session.getId());
|
||||
Boolean sessionExists = this.sessionRedisOperations.hasKey(key);
|
||||
if (sessionExists == null || !sessionExists) {
|
||||
throw new IllegalStateException("Session was invalidated");
|
||||
}
|
||||
}
|
||||
session.save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedisSession findById(String sessionId) {
|
||||
String key = getSessionKey(sessionId);
|
||||
Map<String, Object> entries = this.sessionRedisOperations
|
||||
.<String, Object>opsForHash().entries(key);
|
||||
if (entries.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
MapSession session = new RedisSessionMapper(sessionId).apply(entries);
|
||||
if (session.isExpired()) {
|
||||
deleteById(sessionId);
|
||||
return null;
|
||||
}
|
||||
return new RedisSession(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(String sessionId) {
|
||||
String key = getSessionKey(sessionId);
|
||||
this.sessionRedisOperations.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link RedisOperations} used for sessions.
|
||||
* @return the {@link RedisOperations} used for sessions
|
||||
*/
|
||||
public RedisOperations<String, Object> getSessionRedisOperations() {
|
||||
return this.sessionRedisOperations;
|
||||
}
|
||||
|
||||
private String getSessionKey(String sessionId) {
|
||||
return this.keyNamespace + "sessions:" + sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* An internal {@link Session} implementation used by this {@link SessionRepository}.
|
||||
*/
|
||||
final class RedisSession implements Session {
|
||||
|
||||
private final MapSession cached;
|
||||
|
||||
private final Map<String, Object> delta = new HashMap<>();
|
||||
|
||||
private boolean isNew;
|
||||
|
||||
private String originalSessionId;
|
||||
|
||||
RedisSession(Duration maxInactiveInterval) {
|
||||
this(new MapSession());
|
||||
this.cached.setMaxInactiveInterval(maxInactiveInterval);
|
||||
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY,
|
||||
getCreationTime().toEpochMilli());
|
||||
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
|
||||
(int) getMaxInactiveInterval().getSeconds());
|
||||
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
|
||||
getLastAccessedTime().toEpochMilli());
|
||||
this.isNew = true;
|
||||
}
|
||||
|
||||
RedisSession(MapSession cached) {
|
||||
this.cached = cached;
|
||||
this.originalSessionId = cached.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.cached.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String changeSessionId() {
|
||||
return this.cached.changeSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getAttribute(String attributeName) {
|
||||
return this.cached.getAttribute(attributeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAttributeNames() {
|
||||
return this.cached.getAttributeNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String attributeName, Object attributeValue) {
|
||||
this.cached.setAttribute(attributeName, attributeValue);
|
||||
putAttribute(RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName,
|
||||
attributeValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String attributeName) {
|
||||
setAttribute(attributeName, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return this.cached.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastAccessedTime(Instant lastAccessedTime) {
|
||||
this.cached.setLastAccessedTime(lastAccessedTime);
|
||||
putAttribute(RedisSessionMapper.LAST_ACCESSED_TIME_KEY,
|
||||
getLastAccessedTime().toEpochMilli());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getLastAccessedTime() {
|
||||
return this.cached.getLastAccessedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(Duration interval) {
|
||||
this.cached.setMaxInactiveInterval(interval);
|
||||
putAttribute(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
|
||||
(int) getMaxInactiveInterval().getSeconds());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getMaxInactiveInterval() {
|
||||
return this.cached.getMaxInactiveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return this.cached.isExpired();
|
||||
}
|
||||
|
||||
private void flushIfRequired() {
|
||||
if (SimpleRedisOperationsSessionRepository.this.flushMode == RedisFlushMode.IMMEDIATE) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasChangedSessionId() {
|
||||
return !getId().equals(this.originalSessionId);
|
||||
}
|
||||
|
||||
private void save() {
|
||||
saveChangeSessionId();
|
||||
saveDelta();
|
||||
if (this.isNew) {
|
||||
this.isNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveChangeSessionId() {
|
||||
if (hasChangedSessionId()) {
|
||||
if (!this.isNew) {
|
||||
String originalSessionIdKey = getSessionKey(this.originalSessionId);
|
||||
String sessionIdKey = getSessionKey(getId());
|
||||
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations
|
||||
.rename(originalSessionIdKey, sessionIdKey);
|
||||
}
|
||||
this.originalSessionId = getId();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDelta() {
|
||||
if (this.delta.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String key = getSessionKey(getId());
|
||||
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations
|
||||
.opsForHash().putAll(key, new HashMap<>(this.delta));
|
||||
SimpleRedisOperationsSessionRepository.this.sessionRedisOperations
|
||||
.expireAt(key,
|
||||
Date.from(Instant
|
||||
.ofEpochMilli(getLastAccessedTime().toEpochMilli())
|
||||
.plusSeconds(getMaxInactiveInterval().getSeconds())));
|
||||
this.delta.clear();
|
||||
}
|
||||
|
||||
private void putAttribute(String name, Object value) {
|
||||
this.delta.put(name, value);
|
||||
flushIfRequired();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
* Copyright 2014-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.session.data.redis;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.data.redis.core.HashOperations;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.data.redis.SimpleRedisOperationsSessionRepository.RedisSession;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link SimpleRedisOperationsSessionRepository}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
*/
|
||||
class SimpleRedisOperationsSessionRepositoryTests {
|
||||
|
||||
private static final String TEST_SESSION_ID = "session-id";
|
||||
|
||||
private static final String TEST_SESSION_KEY = getSessionKey(TEST_SESSION_ID);
|
||||
|
||||
@Mock
|
||||
private RedisOperations<String, Object> sessionRedisOperations;
|
||||
|
||||
@Mock
|
||||
private HashOperations<String, String, Object> sessionHashOperations;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Map<String, Object>> delta;
|
||||
|
||||
private SimpleRedisOperationsSessionRepository sessionRepository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
given(this.sessionRedisOperations.<String, Object>opsForHash())
|
||||
.willReturn(this.sessionHashOperations);
|
||||
this.sessionRepository = new SimpleRedisOperationsSessionRepository(
|
||||
this.sessionRedisOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_NullRedisOperations_ShouldThrowException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new ReactiveRedisOperationsSessionRepository(null))
|
||||
.withMessageContaining("sessionRedisOperations cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setDefaultMaxInactiveInterval_ValidInterval_ShouldSetInterval() {
|
||||
this.sessionRepository.setDefaultMaxInactiveInterval(Duration.ofMinutes(10));
|
||||
assertThat(ReflectionTestUtils.getField(this.sessionRepository,
|
||||
"defaultMaxInactiveInterval")).isEqualTo(Duration.ofMinutes(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setDefaultMaxInactiveInterval_NullInterval_ShouldThrowException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(
|
||||
() -> this.sessionRepository.setDefaultMaxInactiveInterval(null))
|
||||
.withMessage("defaultMaxInactiveInterval must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setKeyNamespace_ValidNamespace_ShouldSetNamespace() {
|
||||
this.sessionRepository.setKeyNamespace("test:");
|
||||
assertThat(ReflectionTestUtils.getField(this.sessionRepository, "keyNamespace"))
|
||||
.isEqualTo("test:");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setKeyNamespace_NullNamespace_ShouldThrowException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.sessionRepository.setKeyNamespace(null))
|
||||
.withMessage("keyNamespace must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setKeyNamespace_EmptyNamespace_ShouldThrowException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.sessionRepository.setKeyNamespace(" "))
|
||||
.withMessage("keyNamespace must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setFlushMode_ValidFlushMode_ShouldSetFlushMode() {
|
||||
this.sessionRepository.setFlushMode(RedisFlushMode.IMMEDIATE);
|
||||
assertThat(ReflectionTestUtils.getField(this.sessionRepository, "flushMode"))
|
||||
.isEqualTo(RedisFlushMode.IMMEDIATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setFlushMode_NullFlushMode_ShouldThrowException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.sessionRepository.setFlushMode(null))
|
||||
.withMessage("flushMode must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSession_DefaultMaxInactiveInterval_ShouldCreateSession() {
|
||||
RedisSession redisSession = this.sessionRepository.createSession();
|
||||
assertThat(redisSession.getMaxInactiveInterval()).isEqualTo(
|
||||
Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS));
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSession_CustomMaxInactiveInterval_ShouldCreateSession() {
|
||||
this.sessionRepository.setDefaultMaxInactiveInterval(Duration.ofMinutes(10));
|
||||
RedisSession redisSession = this.sessionRepository.createSession();
|
||||
assertThat(redisSession.getMaxInactiveInterval())
|
||||
.isEqualTo(Duration.ofMinutes(10));
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSession_ImmediateFlushMode_ShouldCreateSession() {
|
||||
this.sessionRepository.setFlushMode(RedisFlushMode.IMMEDIATE);
|
||||
RedisSession session = this.sessionRepository.createSession();
|
||||
String key = getSessionKey(session.getId());
|
||||
verify(this.sessionRedisOperations).opsForHash();
|
||||
verify(this.sessionRedisOperations).expireAt(eq(key), eq(getExpiry(session)));
|
||||
verify(this.sessionHashOperations).putAll(eq(key), this.delta.capture());
|
||||
assertThat(this.delta.getValue()).hasSize(3);
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_NewSession_ShouldSaveSession() {
|
||||
RedisSession session = this.sessionRepository.createSession();
|
||||
this.sessionRepository.save(session);
|
||||
String key = getSessionKey(session.getId());
|
||||
verify(this.sessionRedisOperations).opsForHash();
|
||||
verify(this.sessionRedisOperations).expireAt(eq(key), eq(getExpiry(session)));
|
||||
verify(this.sessionHashOperations).putAll(eq(key), this.delta.capture());
|
||||
assertThat(this.delta.getValue()).hasSize(3);
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_NewSessionAndCustomKeyNamespace_ShouldSaveSession() {
|
||||
this.sessionRepository.setKeyNamespace("custom:");
|
||||
RedisSession session = this.sessionRepository.createSession();
|
||||
this.sessionRepository.save(session);
|
||||
String key = "custom:sessions:" + session.getId();
|
||||
verify(this.sessionRedisOperations).opsForHash();
|
||||
verify(this.sessionRedisOperations).expireAt(eq(key), eq(getExpiry(session)));
|
||||
verify(this.sessionHashOperations).putAll(eq(key), this.delta.capture());
|
||||
assertThat(this.delta.getValue()).hasSize(3);
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_NewSessionAndChangedSessionId_ShouldSaveSession() {
|
||||
RedisSession session = this.sessionRepository.createSession();
|
||||
session.changeSessionId();
|
||||
this.sessionRepository.save(session);
|
||||
String key = getSessionKey(session.getId());
|
||||
verify(this.sessionRedisOperations).opsForHash();
|
||||
verify(this.sessionRedisOperations).expireAt(eq(key), eq(getExpiry(session)));
|
||||
verify(this.sessionHashOperations).putAll(eq(key), this.delta.capture());
|
||||
assertThat(this.delta.getValue()).hasSize(3);
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_SessionExistsAndHasChanges_ShouldSaveSession() {
|
||||
given(this.sessionRedisOperations.hasKey(eq(TEST_SESSION_KEY))).willReturn(true);
|
||||
RedisSession session = createTestSession();
|
||||
session.setLastAccessedTime(Instant.now());
|
||||
session.setAttribute("attribute2", "value2");
|
||||
this.sessionRepository.save(session);
|
||||
verify(this.sessionRedisOperations).hasKey(eq(TEST_SESSION_KEY));
|
||||
verify(this.sessionRedisOperations).opsForHash();
|
||||
verify(this.sessionRedisOperations).expireAt(eq(TEST_SESSION_KEY),
|
||||
eq(getExpiry(session)));
|
||||
verify(this.sessionHashOperations).putAll(eq(TEST_SESSION_KEY),
|
||||
this.delta.capture());
|
||||
assertThat(this.delta.getValue()).hasSize(2);
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_SessionExistsAndNoChanges_ShouldSaveSession() {
|
||||
given(this.sessionRedisOperations.hasKey(eq(TEST_SESSION_KEY))).willReturn(true);
|
||||
RedisSession session = createTestSession();
|
||||
this.sessionRepository.save(session);
|
||||
verify(this.sessionRedisOperations).hasKey(eq(TEST_SESSION_KEY));
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void save_SessionNotExists_ShouldThrowException() {
|
||||
RedisSession session = createTestSession();
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> this.sessionRepository.save(session))
|
||||
.withMessage("Session was invalidated");
|
||||
verify(this.sessionRedisOperations).hasKey(eq(TEST_SESSION_KEY));
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void findById_SessionExists_ShouldReturnSession() {
|
||||
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
|
||||
given(this.sessionHashOperations.entries(eq(TEST_SESSION_KEY))).willReturn(
|
||||
mapOf(RedisSessionMapper.CREATION_TIME_KEY, Instant.EPOCH.toEpochMilli(),
|
||||
RedisSessionMapper.LAST_ACCESSED_TIME_KEY, now.toEpochMilli(),
|
||||
RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
|
||||
MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS,
|
||||
RedisSessionMapper.ATTRIBUTE_PREFIX + "attribute1", "value1"));
|
||||
RedisSession session = this.sessionRepository.findById(TEST_SESSION_ID);
|
||||
assertThat(session.getId()).isEqualTo(TEST_SESSION_ID);
|
||||
assertThat(session.getCreationTime()).isEqualTo(Instant.EPOCH);
|
||||
assertThat(session.getLastAccessedTime()).isEqualTo(now);
|
||||
assertThat(session.getMaxInactiveInterval()).isEqualTo(
|
||||
Duration.ofSeconds(MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS));
|
||||
assertThat(session.getAttributeNames())
|
||||
.isEqualTo(Collections.singleton("attribute1"));
|
||||
assertThat(session.<String>getAttribute("attribute1")).isEqualTo("value1");
|
||||
verify(this.sessionRedisOperations).opsForHash();
|
||||
verify(this.sessionHashOperations).entries(eq(TEST_SESSION_KEY));
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void findById_SessionExistsAndIsExpired_ShouldReturnNull() {
|
||||
given(this.sessionHashOperations.entries(eq(TEST_SESSION_KEY))).willReturn(mapOf(
|
||||
RedisSessionMapper.CREATION_TIME_KEY, Instant.EPOCH.toEpochMilli(),
|
||||
RedisSessionMapper.LAST_ACCESSED_TIME_KEY, Instant.EPOCH.toEpochMilli(),
|
||||
RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
|
||||
MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS,
|
||||
RedisSessionMapper.ATTRIBUTE_PREFIX + "attribute1", "value1"));
|
||||
assertThat(this.sessionRepository.findById(TEST_SESSION_ID)).isNull();
|
||||
verify(this.sessionRedisOperations).opsForHash();
|
||||
verify(this.sessionHashOperations).entries(eq(TEST_SESSION_KEY));
|
||||
verify(this.sessionRedisOperations).delete(eq(TEST_SESSION_KEY));
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findById_SessionNotExists_ShouldReturnNull() {
|
||||
assertThat(this.sessionRepository.findById(TEST_SESSION_ID)).isNull();
|
||||
verify(this.sessionRedisOperations).opsForHash();
|
||||
verify(this.sessionHashOperations).entries(eq(TEST_SESSION_KEY));
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteById__ShouldDeleteSession() {
|
||||
this.sessionRepository.deleteById(TEST_SESSION_ID);
|
||||
verify(this.sessionRedisOperations).delete(TEST_SESSION_KEY);
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSessionRedisOperations__ShouldReturnRedisOperations() {
|
||||
assertThat(this.sessionRepository.getSessionRedisOperations())
|
||||
.isEqualTo(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionRedisOperations);
|
||||
verifyNoMoreInteractions(this.sessionHashOperations);
|
||||
}
|
||||
|
||||
private static String getSessionKey(String sessionId) {
|
||||
return "spring:session:sessions:" + sessionId;
|
||||
}
|
||||
|
||||
private static Date getExpiry(RedisSession session) {
|
||||
return Date
|
||||
.from(Instant.ofEpochMilli(session.getLastAccessedTime().toEpochMilli())
|
||||
.plusSeconds(session.getMaxInactiveInterval().getSeconds()));
|
||||
}
|
||||
|
||||
private static Map mapOf(Object... objects) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
if (objects != null) {
|
||||
for (int i = 0; i < objects.length; i += 2) {
|
||||
result.put((String) objects[i], objects[i + 1]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private RedisSession createTestSession() {
|
||||
MapSession cached = new MapSession(TEST_SESSION_ID);
|
||||
cached.setCreationTime(Instant.EPOCH);
|
||||
cached.setLastAccessedTime(Instant.EPOCH);
|
||||
cached.setAttribute("attribute1", "value1");
|
||||
return this.sessionRepository.new RedisSession(cached);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user