Polish WebSession support and tests
This commit is contained in:
		
							parent
							
								
									7bb19fcde8
								
							
						
					
					
						commit
						222702f750
					
				| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2002-2023 the original author or authors.
 | 
			
		||||
 * Copyright 2002-2025 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -107,12 +107,12 @@ public interface ServerWebExchange {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the web session for the current request. Always guaranteed  to
 | 
			
		||||
	 * return an instance either matching to the session id requested by the
 | 
			
		||||
	 * client, or with a new session id either because the client did not
 | 
			
		||||
	 * specify one or because the underlying session had expired. Use of this
 | 
			
		||||
	 * method does not automatically create a session. See {@link WebSession}
 | 
			
		||||
	 * for more details.
 | 
			
		||||
	 * Return the web session for the current request.
 | 
			
		||||
	 * <p>Always guaranteed to return either an instance matching the session id
 | 
			
		||||
	 * requested by the client, or a new session either because the client did not
 | 
			
		||||
	 * specify a session id or because the underlying session expired.
 | 
			
		||||
	 * <p>Use of this method does not automatically create a session. See
 | 
			
		||||
	 * {@link WebSession} for more details.
 | 
			
		||||
	 */
 | 
			
		||||
	Mono<WebSession> getSession();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -79,9 +79,9 @@ public class InMemoryWebSessionStore implements WebSessionStore {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Configure the {@link Clock} to use to set lastAccessTime on every created
 | 
			
		||||
	 * session and to calculate if it is expired.
 | 
			
		||||
	 * <p>This may be useful to align to different timezone or to set the clock
 | 
			
		||||
	 * Configure the {@link Clock} to use to set the {@code lastAccessTime} on
 | 
			
		||||
	 * every created session and to calculate if the session has expired.
 | 
			
		||||
	 * <p>This may be useful to align to different time zones or to set the clock
 | 
			
		||||
	 * back in a test, for example, {@code Clock.offset(clock, Duration.ofMinutes(-31))}
 | 
			
		||||
	 * in order to simulate session expiration.
 | 
			
		||||
	 * <p>By default this is {@code Clock.system(ZoneId.of("GMT"))}.
 | 
			
		||||
| 
						 | 
				
			
			@ -94,16 +94,17 @@ public class InMemoryWebSessionStore implements WebSessionStore {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the configured clock for session lastAccessTime calculations.
 | 
			
		||||
	 * Return the configured clock for session {@code lastAccessTime} calculations.
 | 
			
		||||
	 */
 | 
			
		||||
	public Clock getClock() {
 | 
			
		||||
		return this.clock;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the map of sessions with an {@link Collections#unmodifiableMap
 | 
			
		||||
	 * unmodifiable} wrapper. This could be used for management purposes, to
 | 
			
		||||
	 * list active sessions, invalidate expired ones, etc.
 | 
			
		||||
	 * Return an {@linkplain Collections#unmodifiableMap unmodifiable} copy of the
 | 
			
		||||
	 * map of sessions.
 | 
			
		||||
	 * <p>This could be used for management purposes, to list active sessions,
 | 
			
		||||
	 * to invalidate expired sessions, etc.
 | 
			
		||||
	 * @since 5.0.8
 | 
			
		||||
	 */
 | 
			
		||||
	public Map<String, WebSession> getSessions() {
 | 
			
		||||
| 
						 | 
				
			
			@ -157,10 +158,11 @@ public class InMemoryWebSessionStore implements WebSessionStore {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Check for expired sessions and remove them. Typically such checks are
 | 
			
		||||
	 * kicked off lazily during calls to {@link #createWebSession() create} or
 | 
			
		||||
	 * {@link #retrieveSession retrieve}, no less than 60 seconds apart.
 | 
			
		||||
	 * This method can be called to force a check at a specific time.
 | 
			
		||||
	 * Check for expired sessions and remove them.
 | 
			
		||||
	 * <p>Typically such checks are kicked off lazily during calls to
 | 
			
		||||
	 * {@link #createWebSession()} or {@link #retrieveSession}, no less than 60
 | 
			
		||||
	 * seconds apart.
 | 
			
		||||
	 * <p>This method can be called to force a check at a specific time.
 | 
			
		||||
	 * @since 5.0.8
 | 
			
		||||
	 */
 | 
			
		||||
	public void removeExpiredSessions() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,10 +32,10 @@ import org.springframework.web.server.WebSession;
 | 
			
		|||
public interface WebSessionManager {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return the {@link WebSession} for the given exchange. Always guaranteed
 | 
			
		||||
	 * to return an instance either matching to the session id requested by the
 | 
			
		||||
	 * client, or a new session either because the client did not specify one
 | 
			
		||||
	 * or because the underlying session expired.
 | 
			
		||||
	 * Return the {@link WebSession} for the given exchange.
 | 
			
		||||
	 * <p>Always guaranteed to return either an instance matching the session id
 | 
			
		||||
	 * requested by the client, or a new session either because the client did not
 | 
			
		||||
	 * specify a session id or because the underlying session expired.
 | 
			
		||||
	 * @param exchange the current exchange
 | 
			
		||||
	 * @return promise for the WebSession
 | 
			
		||||
	 */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2002-2022 the original author or authors.
 | 
			
		||||
 * Copyright 2002-2025 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ public interface WebSessionStore {
 | 
			
		|||
	 * Return the WebSession for the given id.
 | 
			
		||||
	 * <p><strong>Note:</strong> This method should perform an expiration check,
 | 
			
		||||
	 * and if it has expired remove the session and return empty. This method
 | 
			
		||||
	 * should also update the lastAccessTime of retrieved sessions.
 | 
			
		||||
	 * should also update the {@code lastAccessTime} of retrieved sessions.
 | 
			
		||||
	 * @param sessionId the session to load
 | 
			
		||||
	 * @return the session, or an empty {@code Mono}
 | 
			
		||||
	 */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2002-2024 the original author or authors.
 | 
			
		||||
 * Copyright 2002-2025 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +19,6 @@ package org.springframework.web.server.session;
 | 
			
		|||
import java.time.Clock;
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
import java.time.Instant;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.stream.IntStream;
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
| 
						 | 
				
			
			@ -35,10 +34,11 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
 | 
			
		|||
 * Tests for {@link InMemoryWebSessionStore}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Rob Winch
 | 
			
		||||
 * @author Sam Brannen
 | 
			
		||||
 */
 | 
			
		||||
class InMemoryWebSessionStoreTests {
 | 
			
		||||
 | 
			
		||||
	private InMemoryWebSessionStore store = new InMemoryWebSessionStore();
 | 
			
		||||
	private final InMemoryWebSessionStore store = new InMemoryWebSessionStore();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +59,7 @@ class InMemoryWebSessionStoreTests {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	@Test // gh-24027, gh-26958
 | 
			
		||||
	public void createSessionDoesNotBlock() {
 | 
			
		||||
	void createSessionDoesNotBlock() {
 | 
			
		||||
		this.store.createWebSession()
 | 
			
		||||
				.doOnNext(session -> assertThat(Schedulers.isInNonBlockingThread()).isTrue())
 | 
			
		||||
				.block();
 | 
			
		||||
| 
						 | 
				
			
			@ -103,7 +103,7 @@ class InMemoryWebSessionStoreTests {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	@Test // SPR-17051
 | 
			
		||||
	public void sessionInvalidatedBeforeSave() {
 | 
			
		||||
	void sessionInvalidatedBeforeSave() {
 | 
			
		||||
		// Request 1 creates session
 | 
			
		||||
		WebSession session1 = this.store.createWebSession().block();
 | 
			
		||||
		assertThat(session1).isNotNull();
 | 
			
		||||
| 
						 | 
				
			
			@ -132,33 +132,31 @@ class InMemoryWebSessionStoreTests {
 | 
			
		|||
 | 
			
		||||
	@Test
 | 
			
		||||
	void expirationCheckPeriod() {
 | 
			
		||||
 | 
			
		||||
		DirectFieldAccessor accessor = new DirectFieldAccessor(this.store);
 | 
			
		||||
		Map<?,?> sessions = (Map<?, ?>) accessor.getPropertyValue("sessions");
 | 
			
		||||
		assertThat(sessions).isNotNull();
 | 
			
		||||
 | 
			
		||||
		// Create 100 sessions
 | 
			
		||||
		IntStream.range(0, 100).forEach(i -> insertSession());
 | 
			
		||||
		assertThat(sessions).hasSize(100);
 | 
			
		||||
		IntStream.rangeClosed(1, 100).forEach(i -> insertSession());
 | 
			
		||||
		assertNumSessions(100);
 | 
			
		||||
 | 
			
		||||
		// Force a new clock (31 min later), don't use setter which would clean expired sessions
 | 
			
		||||
		// Force a new clock (31 min later). Don't use setter which would clean expired sessions.
 | 
			
		||||
		DirectFieldAccessor accessor = new DirectFieldAccessor(this.store);
 | 
			
		||||
		accessor.setPropertyValue("clock", Clock.offset(this.store.getClock(), Duration.ofMinutes(31)));
 | 
			
		||||
		assertThat(sessions).hasSize(100);
 | 
			
		||||
		assertNumSessions(100);
 | 
			
		||||
 | 
			
		||||
		// Create 1 more which forces a time-based check (clock moved forward)
 | 
			
		||||
		// Create 1 more which forces a time-based check (clock moved forward).
 | 
			
		||||
		insertSession();
 | 
			
		||||
		assertThat(sessions).hasSize(1);
 | 
			
		||||
		assertNumSessions(1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Test
 | 
			
		||||
	void maxSessions() {
 | 
			
		||||
		this.store.setMaxSessions(10);
 | 
			
		||||
 | 
			
		||||
		IntStream.range(0, 10000).forEach(i -> insertSession());
 | 
			
		||||
		assertThatIllegalStateException().isThrownBy(
 | 
			
		||||
				this::insertSession)
 | 
			
		||||
			.withMessage("Max sessions limit reached: 10000");
 | 
			
		||||
		IntStream.rangeClosed(1, 10).forEach(i -> insertSession());
 | 
			
		||||
		assertThatIllegalStateException()
 | 
			
		||||
				.isThrownBy(this::insertSession)
 | 
			
		||||
				.withMessage("Max sessions limit reached: 10");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	private WebSession insertSession() {
 | 
			
		||||
		WebSession session = this.store.createWebSession().block();
 | 
			
		||||
		assertThat(session).isNotNull();
 | 
			
		||||
| 
						 | 
				
			
			@ -167,4 +165,8 @@ class InMemoryWebSessionStoreTests {
 | 
			
		|||
		return session;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void assertNumSessions(int numSessions) {
 | 
			
		||||
		assertThat(store.getSessions()).hasSize(numSessions);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue