Add ReactiveRedisIndexedSessionRepository

Closes gh-2700
This commit is contained in:
Marcus Da Coregio
2023-08-03 11:50:34 -03:00
committed by Marcus Hert Da Coregio
parent 3049318427
commit e65da12d82
34 changed files with 3646 additions and 8 deletions

View File

@@ -0,0 +1,197 @@
/*
* Copyright 2014-2023 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.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.session.DelegatingIndexResolver;
import org.springframework.session.IndexResolver;
import org.springframework.session.PrincipalNameIndexResolver;
import org.springframework.session.ReactiveFindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SingleIndexResolver;
import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository.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.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link ReactiveRedisSessionIndexer}.
*
* @author Marcus da Coregio
*/
class ReactiveRedisSessionIndexerTests {
ReactiveRedisSessionIndexer indexer;
ReactiveRedisOperations<String, Object> sessionRedisOperations = mock(Answers.RETURNS_DEEP_STUBS);
String indexKeyPrefix = "spring:session:sessions:index:";
@BeforeEach
void setup() {
this.indexer = new ReactiveRedisSessionIndexer(this.sessionRedisOperations, "spring:session:");
}
@Test
void getIndexesWhenIndexKeyExistsThenReturnsIndexNameAndValue() {
List<Object> indexKeys = List.of(this.indexKeyPrefix + "principalName:user",
this.indexKeyPrefix + "index_name:index_value");
given(this.sessionRedisOperations.opsForSet().members(anyString())).willReturn(Flux.fromIterable(indexKeys));
Map<String, String> indexes = this.indexer.getIndexes("1234").block();
assertThat(indexes).hasSize(2)
.containsEntry("principalName", "user")
.containsEntry("index_name", "index_value");
}
@Test
void deleteWhenSessionIdHasIndexesThenRemoveSessionIdFromIndexesAndDeleteSessionIndexKey() {
String index1 = this.indexKeyPrefix + "principalName:user";
String index2 = this.indexKeyPrefix + "index_name:index_value";
List<Object> indexKeys = List.of(index1, index2);
given(this.sessionRedisOperations.opsForSet().members(anyString())).willReturn(Flux.fromIterable(indexKeys));
given(this.sessionRedisOperations.delete(anyString())).willReturn(Mono.just(1L));
given(this.sessionRedisOperations.opsForSet().remove(anyString(), anyString())).willReturn(Mono.just(1L));
this.indexer.delete("1234").block();
verify(this.sessionRedisOperations).delete("spring:session:sessions:1234:idx");
verify(this.sessionRedisOperations.opsForSet()).remove(index1, "1234");
verify(this.sessionRedisOperations.opsForSet()).remove(index2, "1234");
}
@Test
void updateWhenSessionHasNoIndexesSavedThenUpdates() {
RedisSession session = mock();
given(session.getAttribute(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME))
.willReturn("user");
given(session.getId()).willReturn("1234");
given(this.sessionRedisOperations.opsForSet().members(anyString())).willReturn(Flux.empty());
given(this.sessionRedisOperations.opsForSet().add(anyString(), anyString())).willReturn(Mono.just(1L));
this.indexer.update(session).block();
verify(this.sessionRedisOperations.opsForSet()).add(this.indexKeyPrefix + "PRINCIPAL_NAME_INDEX_NAME:user",
"1234");
}
@Test
void updateWhenSessionIndexesSavedWithSameValueThenDoesNotUpdate() {
String indexKey = this.indexKeyPrefix + ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME
+ ":user";
RedisSession session = mock();
given(session.getAttribute(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME))
.willReturn("user");
given(session.getId()).willReturn("1234");
given(this.sessionRedisOperations.opsForSet().members(anyString()))
.willReturn(Flux.fromIterable(List.of(indexKey)));
this.indexer.update(session).block();
verify(this.sessionRedisOperations.opsForSet(), never()).add(anyString(), anyString());
verify(this.sessionRedisOperations.opsForSet(), never()).remove(anyString(), anyString());
}
@Test
void updateWhenSessionIndexesSavedWithDifferentValueThenUpdates() {
String indexKey = this.indexKeyPrefix + ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME
+ ":user";
RedisSession session = mock();
given(session.getAttribute(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME))
.willReturn("newuser");
given(session.getId()).willReturn("1234");
given(this.sessionRedisOperations.opsForSet().members(anyString()))
.willReturn(Flux.fromIterable(List.of(indexKey)));
given(this.sessionRedisOperations.opsForSet().add(anyString(), anyString())).willReturn(Mono.just(1L));
given(this.sessionRedisOperations.opsForSet().remove(anyString(), anyString())).willReturn(Mono.just(1L));
this.indexer.update(session).block();
verify(this.sessionRedisOperations.opsForSet()).add(
this.indexKeyPrefix + ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":newuser",
"1234");
verify(this.sessionRedisOperations.opsForSet()).remove(indexKey, "1234");
}
@Test
void updateWhenMultipleIndexResolvedThenUpdated() {
IndexResolver<Session> indexResolver = new DelegatingIndexResolver<>(
new PrincipalNameIndexResolver<>(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME),
new TestIndexResolver<>("test"));
this.indexer.setIndexResolver(indexResolver);
RedisSession session = mock();
given(session.getAttribute(ReactiveFindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME))
.willReturn("user");
given(session.getAttribute("test")).willReturn("testvalue");
given(session.getId()).willReturn("1234");
given(this.sessionRedisOperations.opsForSet().members(anyString())).willReturn(Flux.empty());
given(this.sessionRedisOperations.opsForSet().add(anyString(), anyString())).willReturn(Mono.just(1L));
this.indexer.update(session).block();
verify(this.sessionRedisOperations.opsForSet()).add(this.indexKeyPrefix + "PRINCIPAL_NAME_INDEX_NAME:user",
"1234");
verify(this.sessionRedisOperations.opsForSet()).add(this.indexKeyPrefix + "test:testvalue", "1234");
}
@Test
void setNamespaceShouldUpdateIndexKeyPrefix() {
String originalPrefix = (String) ReflectionTestUtils.getField(this.indexer, "indexKeyPrefix");
this.indexer.setNamespace("my:namespace:");
String updatedPrefix = (String) ReflectionTestUtils.getField(this.indexer, "indexKeyPrefix");
assertThat(originalPrefix).isEqualTo(this.indexKeyPrefix);
assertThat(updatedPrefix).isEqualTo("my:namespace:sessions:index:");
}
@Test
void constructorWhenSessionRedisOperationsNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveRedisSessionIndexer(null, "spring:session:"))
.withMessage("sessionRedisOperations cannot be null");
}
@Test
void constructorWhenNamespaceNullThenException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ReactiveRedisSessionIndexer(this.sessionRedisOperations, null))
.withMessage("namespace cannot be empty");
}
@Test
void constructorWhenNamespaceEmptyThenException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ReactiveRedisSessionIndexer(this.sessionRedisOperations, ""))
.withMessage("namespace cannot be empty");
}
static class TestIndexResolver<S extends Session> extends SingleIndexResolver<S> {
protected TestIndexResolver(String indexName) {
super(indexName);
}
@Override
public String resolveIndexValueFor(S session) {
return session.getAttribute(getIndexName());
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2014-2023 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.Instant;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.data.domain.Range;
import org.springframework.data.redis.connection.Limit;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@SuppressWarnings("unchecked")
class SortedSetReactiveRedisSessionExpirationStoreTests {
SortedSetReactiveRedisSessionExpirationStore store;
ReactiveRedisOperations<String, Object> sessionRedisOperations = mock(Answers.RETURNS_DEEP_STUBS);
String namespace = "spring:session:";
@BeforeEach
void setup() {
this.store = new SortedSetReactiveRedisSessionExpirationStore(this.sessionRedisOperations, this.namespace);
given(this.sessionRedisOperations.opsForZSet().add(anyString(), anyString(), anyDouble()))
.willReturn(Mono.empty());
given(this.sessionRedisOperations.opsForZSet().remove(anyString(), anyString())).willReturn(Mono.empty());
given(this.sessionRedisOperations.opsForZSet()
.reverseRangeByScore(anyString(), any(Range.class), any(Limit.class))).willReturn(Flux.empty());
}
@Test
void addThenStoresSessionIdRankedByExpireAtEpochMilli() {
String sessionId = "1234";
Instant expireAt = Instant.ofEpochMilli(1702314490000L);
StepVerifier.create(this.store.add(sessionId, expireAt)).verifyComplete();
verify(this.sessionRedisOperations.opsForZSet()).add(this.namespace + "sessions:expirations", sessionId,
expireAt.toEpochMilli());
}
@Test
void removeThenRemovesSessionIdFromSortedSet() {
String sessionId = "1234";
StepVerifier.create(this.store.remove(sessionId)).verifyComplete();
verify(this.sessionRedisOperations.opsForZSet()).remove(this.namespace + "sessions:expirations", sessionId);
}
@Test
void retrieveExpiredSessionsThenUsesExpectedRangeAndLimit() {
Instant now = Instant.now();
StepVerifier.create(this.store.retrieveExpiredSessions(now)).verifyComplete();
ArgumentCaptor<Range<Double>> rangeCaptor = ArgumentCaptor.forClass(Range.class);
ArgumentCaptor<Limit> limitCaptor = ArgumentCaptor.forClass(Limit.class);
verify(this.sessionRedisOperations.opsForZSet()).reverseRangeByScore(
eq(this.namespace + "sessions:expirations"), rangeCaptor.capture(), limitCaptor.capture());
assertThat(rangeCaptor.getValue().getLowerBound().getValue()).hasValue(0D);
assertThat(rangeCaptor.getValue().getUpperBound().getValue()).hasValue((double) now.toEpochMilli());
assertThat(limitCaptor.getValue().getCount()).isEqualTo(100);
assertThat(limitCaptor.getValue().getOffset()).isEqualTo(0);
}
}