diff --git a/spring-session-core/spring-session-core.gradle b/spring-session-core/spring-session-core.gradle index e7413ce9..8c78724f 100644 --- a/spring-session-core/spring-session-core.gradle +++ b/spring-session-core/spring-session-core.gradle @@ -7,14 +7,16 @@ dependencies { optional "io.projectreactor:reactor-core" optional "javax.servlet:javax.servlet-api" + optional "org.springframework.security:spring-security-core" + optional "org.springframework.security:spring-security-web" optional "org.springframework:spring-context" optional "org.springframework:spring-jdbc" optional "org.springframework:spring-messaging" optional "org.springframework:spring-web" + optional "org.springframework:spring-webflux" optional "org.springframework:spring-websocket" - optional "org.springframework.security:spring-security-core" - optional "org.springframework.security:spring-security-web" + testCompile "io.projectreactor:reactor-test" testCompile "junit:junit" testCompile "org.mockito:mockito-core" testCompile "edu.umd.cs.mtc:multithreadedtc" diff --git a/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionManager.java b/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionManager.java new file mode 100644 index 00000000..599f7b9f --- /dev/null +++ b/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionManager.java @@ -0,0 +1,156 @@ +/* + * Copyright 2014-2017 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 + * + * http://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.web.server.session; + +import org.springframework.session.ReactorSessionRepository; +import org.springframework.session.Session; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebSession; +import org.springframework.web.server.session.CookieWebSessionIdResolver; +import org.springframework.web.server.session.WebSessionIdResolver; +import org.springframework.web.server.session.WebSessionManager; +import org.springframework.web.server.session.WebSessionStore; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.util.List; + +/** + * @author Rob Winch + * @since 2.0 + */ +public class SpringSessionWebSessionManager implements WebSessionManager { + private final SpringSessionWebSessionStore sessionStore; + + private WebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver(); + + private Clock clock = Clock.system(ZoneId.of("GMT")); + + public SpringSessionWebSessionManager(ReactorSessionRepository sessionRepository) { + sessionStore + = new SpringSessionWebSessionStore<>(sessionRepository); + } + + /** + * Configure the id resolution strategy. + *

By default an instance of {@link CookieWebSessionIdResolver}. + * @param sessionIdResolver the resolver to use + */ + public void setSessionIdResolver(WebSessionIdResolver sessionIdResolver) { + Assert.notNull(sessionIdResolver, "WebSessionIdResolver is required."); + this.sessionIdResolver = sessionIdResolver; + } + + /** + * Return the configured {@link WebSessionIdResolver}. + */ + private WebSessionIdResolver getSessionIdResolver() { + return this.sessionIdResolver; + } + + /** + * Return the configured {@link WebSessionStore}. + */ + private WebSessionStore getSessionStore() { + return this.sessionStore; + } + + /** + * Configure the {@link Clock} to use to set lastAccessTime on every created + * session and to calculate if it is expired. + *

This may be useful to align to different timezone or to set the clock + * back in a test, e.g. {@code Clock.offset(clock, Duration.ofMinutes(-31))} + * in order to simulate session expiration. + *

By default this is {@code Clock.system(ZoneId.of("GMT"))}. + * @param clock the clock to use + */ + public void setClock(Clock clock) { + Assert.notNull(clock, "'clock' is required."); + this.clock = clock; + } + + /** + * Return the configured clock for session lastAccessTime calculations. + */ + private Clock getClock() { + return this.clock; + } + + @Override + public Mono getSession(ServerWebExchange exchange) { + return Mono.defer(() -> + retrieveSession(exchange) + .flatMap(session -> removeSessionIfExpired(exchange, session)) + .flatMap(session -> { + Instant lastAccessTime = Instant.now(getClock()); + return this.sessionStore.setLastAccessedTime(session, lastAccessTime); + }) + .switchIfEmpty(createSession(exchange)) + .doOnNext(session -> exchange.getResponse().beforeCommit(session::save))); + } + + private Mono retrieveSession(ServerWebExchange exchange) { + return Flux.fromIterable(getSessionIdResolver().resolveSessionIds(exchange)) + .concatMap(this.sessionStore::retrieveSession) + .cast(WebSession.class) + .next(); + } + + private Mono removeSessionIfExpired(ServerWebExchange exchange, WebSession session) { + if (session.isExpired()) { + this.sessionIdResolver.expireSession(exchange); + return this.sessionStore.removeSession(session.getId()).then(Mono.empty()); + } + return Mono.just(session); + } + + private Mono saveSession(ServerWebExchange exchange, WebSession session) { + if (session.isExpired()) { + return Mono.error(new IllegalStateException( + "Sessions are checked for expiration and have their " + + "lastAccessTime updated when first accessed during request processing. " + + "However this session is expired meaning that maxIdleTime elapsed " + + "before the call to session.save().")); + } + + if (!session.isStarted()) { + return Mono.empty(); + } + + // Force explicit start + session.start(); + + if (hasNewSessionId(exchange, session)) { + this.sessionIdResolver.setSessionId(exchange, session.getId()); + } + + return this.sessionStore.storeSession(session); + } + + private boolean hasNewSessionId(ServerWebExchange exchange, WebSession session) { + List ids = getSessionIdResolver().resolveSessionIds(exchange); + return ids.isEmpty() || !session.getId().equals(ids.get(0)); + } + + private Mono createSession(ServerWebExchange exchange) { + return this.sessionStore.createSession(); + } +} diff --git a/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionStore.java b/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionStore.java new file mode 100644 index 00000000..427060d5 --- /dev/null +++ b/spring-session-core/src/main/java/org/springframework/session/web/server/session/SpringSessionWebSessionStore.java @@ -0,0 +1,296 @@ +/* + * Copyright 2014-2017 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 + * + * http://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.web.server.session; + +import org.springframework.lang.Nullable; +import org.springframework.session.ReactorSessionRepository; +import org.springframework.session.Session; +import org.springframework.util.Assert; +import org.springframework.web.server.WebSession; +import org.springframework.web.server.session.WebSessionStore; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +/** + * @author Rob Winch + * @since 2.0 + */ +class SpringSessionWebSessionStore implements WebSessionStore { + private final ReactorSessionRepository sessions; + + SpringSessionWebSessionStore(ReactorSessionRepository sessions) { + Assert.notNull(sessions, "sessions cannot be null"); + this.sessions = sessions; + } + + public Mono createSession() { + return this.sessions.createSession().map(this::createSession); + } + + public Mono setLastAccessedTime(WebSession session, + Instant lastAccessedTime) { + SpringSessionWebSession springSessionWebSession = (SpringSessionWebSession) session; + springSessionWebSession.session.setLastAccessedTime(lastAccessedTime); + return Mono.just(session); + } + + @Override + public Mono storeSession(WebSession session) { + @SuppressWarnings("unchecked") + SpringSessionWebSession springWebSession = (SpringSessionWebSession) session; + return this.sessions.save(springWebSession.session); + } + + @Override + public Mono retrieveSession(String sessionId) { + return this.sessions.findById(sessionId).map(this::existingSession); + } + + @Override + public Mono changeSessionId(String s, WebSession webSession) { + return storeSession(webSession); + } + + private SpringSessionWebSession createSession(S session) { + return new SpringSessionWebSession(session, State.NEW); + } + + private SpringSessionWebSession existingSession(S session) { + return new SpringSessionWebSession(session, State.STARTED); + } + + @Override + public Mono removeSession(String sessionId) { + return sessions.delete(sessionId); + } + + private class SpringSessionWebSession implements WebSession { + private final S session; + + private final Map attributes; + + private AtomicReference state = new AtomicReference<>(); + + private volatile transient Supplier> saveOperation = Mono::empty; + + SpringSessionWebSession(S session, State state) { + Assert.notNull(session, "session cannot be null"); + this.session = session; + this.attributes = new SpringSessionMap(session); + this.state.set(state); + } + + @Override + public String getId() { + return session.getId(); + } + + @Override + public Mono changeSessionId() { + return Mono.defer(() -> { + session.changeSessionId(); + return save(); + }); + } + + @Override + public Map getAttributes() { + return this.attributes; + } + + @Override + public void start() { + this.state.compareAndSet(State.NEW, State.STARTED); + } + + @Override + public boolean isStarted() { + State value = this.state.get(); + return (State.STARTED.equals(value) + || (State.NEW.equals(value) && !getAttributes().isEmpty())); + } + + @Override + public Mono save() { + return this.saveOperation.get(); + } + + @Override + public boolean isExpired() { + return this.session.isExpired(); + } + + @Override + public Instant getCreationTime() { + return this.session.getCreationTime(); + } + + @Override + public Instant getLastAccessTime() { + return this.session.getLastAccessedTime(); + } + + @Override + public void setMaxIdleTime(Duration maxIdleTime) { + this.session.setMaxInactiveInterval(maxIdleTime); + } + + @Override + public Duration getMaxIdleTime() { + return this.session.getMaxInactiveInterval(); + } + } + + private enum State { + NEW, STARTED + } + + static class SpringSessionMap implements Map { + private final Session session; + private final Collection values = new SessionValues(); + + SpringSessionMap(Session session) { + this.session = session; + } + + @Override + public int size() { + return this.session.getAttributeNames().size(); + } + + @Override + public boolean isEmpty() { + return this.session.getAttributeNames().isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return key instanceof String + && this.session.getAttributeNames().contains(key); + } + + @Override + public boolean containsValue(Object value) { + return this.session.getAttributeNames().stream() + .anyMatch(attrName -> this.session.getAttribute(attrName) != null); + } + + @Override + @Nullable + public Object get(Object key) { + if (key instanceof String) { + return this.session.getAttribute((String) key); + } + return null; + } + + @Override + public Object put(String key, Object value) { + Object original = this.session.getAttribute(key); + this.session.setAttribute(key, value); + return original; + } + + @Override + @Nullable + public Object remove(Object key) { + if (key instanceof String) { + String attrName = (String) key; + Object original = this.session.getAttribute(attrName); + this.session.removeAttribute(attrName); + return original; + } + return null; + } + + @Override + public void putAll(Map m) { + for (Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void clear() { + for (String attrName : this.session.getAttributeNames()) { + remove(attrName); + } + } + + @Override + public Set keySet() { + return this.session.getAttributeNames(); + } + + @Override + public Collection values() { + return values; + } + + @Override + public Set> entrySet() { + Set attrNames = keySet(); + Set> entries = new HashSet<>(attrNames.size()); + for (String attrName : attrNames) { + Object value = this.session.getAttribute(attrName); + entries.add(new AbstractMap.SimpleEntry<>(attrName, value)); + } + return Collections.unmodifiableSet(entries); + } + + private class SessionValues extends AbstractCollection { + public Iterator iterator() { + return new Iterator() { + private Iterator> i = entrySet().iterator(); + + public boolean hasNext() { + return i.hasNext(); + } + + public Object next() { + return i.next().getValue(); + } + + public void remove() { + i.remove(); + } + }; + } + + public int size() { + return SpringSessionMap.this.size(); + } + + public boolean isEmpty() { + return SpringSessionMap.this.isEmpty(); + } + + public void clear() { + SpringSessionMap.this.clear(); + } + + public boolean contains(Object v) { + return SpringSessionMap.this.containsValue(v); + } + } + } +} diff --git a/spring-session-core/src/main/java/org/springframework/session/web/server/session/package-info.java b/spring-session-core/src/main/java/org/springframework/session/web/server/session/package-info.java new file mode 100644 index 00000000..615951bd --- /dev/null +++ b/spring-session-core/src/main/java/org/springframework/session/web/server/session/package-info.java @@ -0,0 +1,8 @@ +/** + * @author Rob Winch + * @since 5.0 + */ +@NonNullApi +package org.springframework.session.web.server.session; + +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/spring-session-core/src/test/java/org/springframework/session/web/server/session/SpringSessionWebSessionManagerTests.java b/spring-session-core/src/test/java/org/springframework/session/web/server/session/SpringSessionWebSessionManagerTests.java new file mode 100644 index 00000000..0e25ff21 --- /dev/null +++ b/spring-session-core/src/test/java/org/springframework/session/web/server/session/SpringSessionWebSessionManagerTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2014-2017 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 + * + * http://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.web.server.session; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpCookie; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.session.ReactorSessionRepository; +import org.springframework.session.Session; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebSession; +import org.springframework.web.server.session.WebSessionIdResolver; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class SpringSessionWebSessionManagerTests { + @Mock + ReactorSessionRepository sessions; + + @Mock + WebSessionIdResolver resolver; + + @Mock + S createSession; + + @Mock + S findByIdSession; + + Mono createSessionMono; + + ServerWebExchange exchange = MockServerHttpRequest.get("/").toExchange(); + + + SpringSessionWebSessionManager manager; + + @Before + public void setup() { + when(this.createSession.getId()).thenReturn("createSession-id"); + when(this.findByIdSession.getId()).thenReturn("findByIdSession-id"); + this.createSessionMono = Mono.just(this.createSession); + when(this.sessions.createSession()).thenReturn(createSessionMono); + this.manager = new SpringSessionWebSessionManager(this.sessions); + this.manager.setSessionIdResolver(this.resolver); + } + + @Test + public void getSessionWhenDefaultSessionIdResolverFoundSessionUsed() { + String findByIdSessionId = this.findByIdSession.getId(); + this.exchange = MockServerHttpRequest.get("/").cookie(new HttpCookie("SESSION", findByIdSessionId)).toExchange(); + this.manager = new SpringSessionWebSessionManager(this.sessions); + when(this.sessions.findById(findByIdSessionId)).thenReturn(Mono.just(findByIdSession)); + + WebSession webSession = this.manager.getSession(exchange).block(); + + assertThat(webSession.getId()).isEqualTo(findByIdSessionId); + verify(this.sessions).findById(findByIdSessionId); + } + + @Test + public void getSessionWhenNewThenCreateSessionInvoked() { + WebSession webSession = this.manager.getSession(exchange).block(); + + assertThat(webSession.getId()).isEqualTo(this.createSession.getId()); + verify(this.sessions).createSession(); + } + + @Test + public void getSessionWhenNewAndPutThenSetAttributeInvoked() { + String attrName = "attrName"; + String attrValue = "attrValue"; + + WebSession webSession = this.manager.getSession(exchange).block(); + webSession.getAttributes().put(attrName, attrValue); + + verify(this.createSession).setAttribute(attrName, attrValue); + } + + @Test + public void getSessionWhenInvalidIdThenCreateSessionInvoked() { + String invalidId = "invalid"; + String createSessionId = this.createSession.getId(); + when(this.sessions.findById(any())).thenReturn(Mono.empty()); + when(this.resolver.resolveSessionIds(exchange)).thenReturn(Arrays.asList(invalidId)); + + WebSession webSession = this.manager.getSession(exchange).block(); + + assertThat(webSession.getId()).isEqualTo(createSessionId); + verify(this.sessions).findById(invalidId); + + Mono mono = Mono.just("toTest"); + StepVerifier + .create(mono) + .expectNoEvent(Duration.ZERO); + } + + @Test + public void getSessionWhenValidIdThenFoundSessionUsed() { + String findByIdSessionId = this.findByIdSession.getId(); + when(this.sessions.findById(findByIdSessionId)).thenReturn(Mono.just(findByIdSession)); + when(this.resolver.resolveSessionIds(exchange)).thenReturn(Arrays.asList(findByIdSessionId)); + + WebSession webSession = this.manager.getSession(exchange).block(); + + assertThat(webSession.getId()).isEqualTo(findByIdSessionId); + verify(this.sessions).findById(findByIdSessionId); + } +} \ No newline at end of file diff --git a/spring-session-core/src/test/java/org/springframework/session/web/server/session/SpringSessionWebSessionStoreTests.java b/spring-session-core/src/test/java/org/springframework/session/web/server/session/SpringSessionWebSessionStoreTests.java new file mode 100644 index 00000000..2f443698 --- /dev/null +++ b/spring-session-core/src/test/java/org/springframework/session/web/server/session/SpringSessionWebSessionStoreTests.java @@ -0,0 +1,248 @@ +/* + * Copyright 2014-2017 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 + * + * http://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.web.server.session; + + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.session.ReactorSessionRepository; +import org.springframework.session.Session; +import org.springframework.web.server.WebSession; +import reactor.core.publisher.Mono; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class SpringSessionWebSessionStoreTests { + @Mock + ReactorSessionRepository sessionRepository; + @Mock + S createSession; + @Mock + S findByIdSession; + SpringSessionWebSessionStore webSessionStore; + + @Before + public void setup() { + this.webSessionStore = new SpringSessionWebSessionStore<>(sessionRepository); + when(this.sessionRepository.findById(any())).thenReturn(Mono.just(findByIdSession)); + when(this.sessionRepository.createSession()).thenReturn(Mono.just(createSession)); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorWhenNullRepositoryThenThrowsIllegalArgumentException() { + new SpringSessionWebSessionStore((ReactorSessionRepository) null); + } + + @Test + public void createSessionWhenNoAttributesThenNotStarted() { + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + assertThat(createdWebSession.isStarted()).isFalse(); + } + + @Test + public void createSessionWhenAddAttributeThenStarted() { + when(createSession.getAttributeNames()).thenReturn(Collections.singleton("a")); + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + assertThat(createdWebSession.isStarted()).isTrue(); + } + + @Test + public void createSessionWhenGetAttributesAndSizeThenDelegatesToCreateSession() { + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + + assertThat(attributes.size()).isEqualTo(0); + + when(this.createSession.getAttributeNames()).thenReturn(Collections.singleton("a")); + + assertThat(attributes.size()).isEqualTo(1); + } + + @Test + public void createSessionWhenGetAttributesAndIsEmptyThenDelegatesToCreateSession() { + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + + assertThat(attributes.isEmpty()).isTrue(); + + when(this.createSession.getAttributeNames()).thenReturn(Collections.singleton("a")); + + assertThat(attributes.isEmpty()).isFalse(); + } + + @Test + public void createSessionWhenGetAttributesAndContainsKeyAndNotStringThenFalse() { + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + + assertThat(attributes.containsKey(1L)).isFalse(); + } + + @Test + public void createSessionWhenGetAttributesAndContainsKeyAndNotFoundThenFalse() { + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + + assertThat(attributes.containsKey("a")).isFalse(); + } + + @Test + public void createSessionWhenGetAttributesAndContainsKeyAndFoundThenTrue() { + when(this.createSession.getAttributeNames()).thenReturn(Collections.singleton("a")); + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + + assertThat(attributes.containsKey("a")).isTrue(); + } + + @Test + public void createSessionWhenGetAttributesAndPutThenDelegatesToCreateSession() { + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + attributes.put("a", "b"); + + verify(createSession).setAttribute("a", "b"); + } + + @Test + public void createSessionWhenGetAttributesAndPutNullThenDelegatesToCreateSession() { + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + attributes.put("a", null); + + verify(createSession).setAttribute("a", null); + } + + @Test + public void createSessionWhenGetAttributesAndRemoveThenDelegatesToCreateSession() { + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + attributes.remove("a"); + + verify(createSession).removeAttribute("a"); + } + + @Test + public void createSessionWhenGetAttributesAndPutAllThenDelegatesToCreateSession() { + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + attributes.putAll(Collections.singletonMap("a","b")); + + verify(createSession).setAttribute("a", "b"); + } + + @Test + public void createSessionWhenGetAttributesAndClearThenDelegatesToCreateSession() { + when(this.createSession.getAttributeNames()).thenReturn(Collections.singleton("a")); + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + attributes.clear(); + + verify(createSession).removeAttribute("a"); + } + + @Test + public void createSessionWhenGetAttributesAndKeySetThenDelegatesToCreateSession() { + when(this.createSession.getAttributeNames()).thenReturn(Collections.singleton("a")); + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + + assertThat(attributes.keySet()).containsExactly("a"); + } + + @Test + public void createSessionWhenGetAttributesAndValuesThenDelegatesToCreateSession() { + when(this.createSession.getAttributeNames()).thenReturn(Collections.singleton("a")); + when(this.createSession.getAttribute("a")).thenReturn("b"); + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + + assertThat(attributes.values()).containsExactly("b"); + } + + @Test + public void createSessionWhenGetAttributesAndEntrySetThenDelegatesToCreateSession() { + String attrName = "attrName"; + when(createSession.getAttributeNames()).thenReturn(Collections.singleton(attrName)); + String attrValue = "attrValue"; + when(createSession.getAttribute(attrName)).thenReturn(attrValue); + WebSession createdWebSession = this.webSessionStore.createSession().block(); + + Map attributes = createdWebSession.getAttributes(); + Set> entries = attributes.entrySet(); + + assertThat(entries).containsExactly(new AbstractMap.SimpleEntry(attrName, attrValue)); + } + + @Test + public void storeSessionWhenInvokedThenSessionSaved() { + when(this.sessionRepository.save(this.createSession)).thenReturn(Mono.empty()); + WebSession createdSession = this.webSessionStore.createSession().block(); + + this.webSessionStore.storeSession(createdSession).block(); + + verify(this.sessionRepository).save(this.createSession); + } + + @Test + public void retrieveSessionThenStarted() { + String id = "id"; + WebSession retrievedWebSession = this.webSessionStore.retrieveSession(id).block(); + + assertThat(retrievedWebSession.isStarted()).isTrue(); + } + + @Test + public void removeSessionWhenInvokedThenSessionSaved() { + String sessionId = "session-id"; + when(this.sessionRepository.delete(sessionId)).thenReturn(Mono.empty()); + + this.webSessionStore.removeSession(sessionId).block(); + + verify(this.sessionRepository).delete(sessionId); + } +} \ No newline at end of file