Compare commits

...

13 Commits

Author SHA1 Message Date
Brian Clozel 99a366baf6 Next development version (v6.1.22-SNAPSHOT) 2025-06-12 10:56:22 +02:00
Brian Clozel 498ccda8fc Upgrade to Gradle 8.14.2 2025-06-12 10:00:55 +02:00
Brian Clozel fd68ea6fcb Encode non-printable character in Content-Disposition parameter
Prior to this commit, the "filename" parameter value for the
"Content-Disposition" header would contain non-printable characters,
causing parsing issues for HTTP clients.
This commit ensures that all non-printable characters are encoded.

Fixes gh-35035
2025-06-12 08:39:41 +02:00
Juergen Hoeller 28caa39020 Upgrade to Reactor 2023.0.19
Includes SLF4J 2.0.17, Groovy 4.0.27, FreeMarker 2.3.34

Closes gh-35022
2025-06-11 12:54:02 +02:00
Sam Brannen 8ecc553696 Polish contribution
See gh-35013
See gh-35018

(cherry picked from commit 4d2cc4ae97)
2025-06-10 12:39:48 +02:00
Mohammad Saeed Nouri cd44efaf68 Allow update of existing WebSession after max sessions limit is reached
Previously, when saving a WebSession, the system did not check whether
the session ID already existed. As a result, even if the session being
saved was an update to an existing one, it was incorrectly treated as a
new session, and a "maximum sessions exceeded" error was triggered.

This fix ensures that if a WebSession with the same ID already exists,
it will be updated rather than counted as a new session, thereby
preventing unnecessary session limit violations.

See gh-35013
Closes gh-35018

Signed-off-by: Mohammad Saeed Nouri <msnsaeed71@gmail.com>
(cherry picked from commit c04902fefb)
2025-06-10 12:38:58 +02:00
Sam Brannen 59d2895c82 Fix InMemoryWebSessionStoreTests.startsSessionImplicitly() test
(cherry picked from commit 3c265e1044)
2025-06-10 12:37:45 +02:00
Sam Brannen a876bb41af Polish WebSession support and tests
(cherry picked from commit 222702f750)
2025-06-10 12:37:19 +02:00
Juergen Hoeller 3b6becac01 Check for package-visible constructor in case of ClassLoader mismatch
Closes gh-34950

(cherry picked from commit 15d1455acb)
2025-05-27 10:30:07 +02:00
Sam Brannen 59ffbd7a59 Test conversion support in PropertySourcesPlaceholderConfigurer
This commit introduces a "regression test" which demonstrates that
PropertySourcesPlaceholderConfigurer uses the ConversionService from
the Environment.

See gh-34936
2025-05-26 18:09:51 +02:00
Sam Brannen c0a9da65f4 Support only Object property values in MockEnvironment test fixture
The setProperty() and withProperty() methods in MockEnvironment were
originally introduced with (String, String) signatures; however, they
should have always had (String, Object) signatures in order to comply
with the MockPropertySource and PropertySource APIs.

To address that, this commit changes the signatures of these methods so
that they only accept Object values for properties.

NOTE: this commit only affects the internal MockEnvironment used as a
test fixture. This commit does not affect the official, public
MockEnvironment implementation in spring-test.

See gh-34947
See gh-34948

(cherry picked from commit d78264756e)
2025-05-26 17:58:15 +02:00
Sam Brannen 30bee4db0f Polish Javadoc for MockPropertySource
(cherry picked from commit 6a6abac003)
2025-05-26 17:54:52 +02:00
Brian Clozel 9a89654a64 Next development version (v6.1.21-SNAPSHOT) 2025-05-15 10:32:10 +02:00
16 changed files with 202 additions and 75 deletions
+4 -4
View File
@@ -11,9 +11,9 @@ dependencies {
api(platform("io.micrometer:micrometer-bom:1.12.12"))
api(platform("io.netty:netty-bom:4.1.121.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2023.0.18"))
api(platform("io.projectreactor:reactor-bom:2023.0.19"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.26"))
api(platform("org.apache.groovy:groovy-bom:4.0.27"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.21"))
@@ -121,7 +121,7 @@ dependencies {
api("org.eclipse:yasson:2.0.4")
api("org.ehcache:ehcache:3.10.8")
api("org.ehcache:jcache:1.0.1")
api("org.freemarker:freemarker:2.3.33")
api("org.freemarker:freemarker:2.3.34")
api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea")
api("org.glassfish:jakarta.el:4.0.2")
api("org.glassfish.tyrus:tyrus-container-servlet:2.1.3")
@@ -140,7 +140,7 @@ dependencies {
api("org.seleniumhq.selenium:htmlunit-driver:2.70.0")
api("org.seleniumhq.selenium:selenium-java:3.141.59")
api("org.skyscreamer:jsonassert:1.5.3")
api("org.slf4j:slf4j-api:2.0.16")
api("org.slf4j:slf4j-api:2.0.17")
api("org.testng:testng:7.9.0")
api("org.webjars:underscorejs:1.8.3")
api("org.webjars:webjars-locator-core:0.55")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.1.20-SNAPSHOT
version=6.1.22-SNAPSHOT
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -16,6 +16,7 @@
package org.springframework.context.annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@@ -139,14 +140,20 @@ class ConfigurationClassEnhancer {
}
/**
* Checks whether the given config class relies on package visibility,
* either for the class itself or for any of its {@code @Bean} methods.
* Checks whether the given config class relies on package visibility, either for
* the class and any of its constructors or for any of its {@code @Bean} methods.
*/
private boolean reliesOnPackageVisibility(Class<?> configSuperClass) {
int mod = configSuperClass.getModifiers();
if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) {
return true;
}
for (Constructor<?> ctor : configSuperClass.getDeclaredConstructors()) {
mod = ctor.getModifiers();
if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) {
return true;
}
}
for (Method method : ReflectionUtils.getDeclaredMethods(configSuperClass)) {
if (BeanAnnotationHelper.isBeanAnnotated(method)) {
mod = method.getModifiers();
@@ -104,6 +104,31 @@ class ConfigurationClassEnhancerTests {
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Test
void withNonPublicConstructor() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new OverridingClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Test
void withNonPublicMethod() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
@@ -160,6 +185,19 @@ class ConfigurationClassEnhancerTests {
}
@Configuration
public static class MyConfigWithNonPublicConstructor {
MyConfigWithNonPublicConstructor() {
}
@Bean
public String myBean() {
return "bean";
}
}
@Configuration
public static class MyConfigWithNonPublicMethod {
@@ -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.
@@ -29,6 +29,7 @@ import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
@@ -72,6 +73,33 @@ class PropertySourcesPlaceholderConfigurerTests {
assertThat(ppc.getAppliedPropertySources()).isNotNull();
}
@Test // gh-34936
void replacementFromEnvironmentPropertiesWithConversion() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerBeanDefinition("testBean",
genericBeanDefinition(TestBean.class)
.addPropertyValue("name", "${my.name}")
.getBeanDefinition());
record Point(int x, int y) {
}
Converter<Point, String> pointToStringConverter =
point -> "(%d,%d)".formatted(point.x, point.y);
DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(Point.class, String.class, pointToStringConverter);
MockEnvironment env = new MockEnvironment();
env.setConversionService(conversionService);
env.setProperty("my.name", new Point(4,5));
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
ppc.setEnvironment(env);
ppc.postProcessBeanFactory(bf);
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("(4,5)");
}
@Test
void localPropertiesViaResource() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
@@ -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.
@@ -27,7 +27,7 @@ import org.springframework.core.testfixture.env.MockPropertySource;
* @author Chris Beams
* @author Sam Brannen
* @since 3.2
* @see org.springframework.core.testfixture.env.MockPropertySource
* @see MockPropertySource
*/
public class MockEnvironment extends AbstractEnvironment {
@@ -44,19 +44,21 @@ public class MockEnvironment extends AbstractEnvironment {
/**
* Set a property on the underlying {@link MockPropertySource} for this environment.
* @see MockPropertySource#setProperty(String, Object)
*/
public void setProperty(String key, String value) {
this.propertySource.setProperty(key, value);
public void setProperty(String name, Object value) {
this.propertySource.setProperty(name, value);
}
/**
* Convenient synonym for {@link #setProperty} that returns the current instance.
* Useful for method chaining and fluent-style use.
* Convenient synonym for {@link #setProperty(String, Object)} that returns
* the current instance.
* <p>Useful for method chaining and fluent-style use.
* @return this {@link MockEnvironment} instance
* @see MockPropertySource#withProperty
* @see MockPropertySource#withProperty(String, Object)
*/
public MockEnvironment withProperty(String key, String value) {
setProperty(key, value);
public MockEnvironment withProperty(String name, Object value) {
setProperty(name, value);
return this;
}
@@ -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.
@@ -26,7 +26,7 @@ import org.springframework.core.env.PropertySource;
* a user-provided {@link Properties} object, or if omitted during construction,
* the implementation will initialize its own.
*
* The {@link #setProperty} and {@link #withProperty} methods are exposed for
* <p>The {@link #setProperty} and {@link #withProperty} methods are exposed for
* convenience, for example:
* <pre class="code">
* {@code
@@ -95,7 +95,7 @@ public class MockPropertySource extends PropertiesPropertySource {
/**
* Convenient synonym for {@link #setProperty} that returns the current instance.
* Useful for method chaining and fluent-style use.
* <p>Useful for method chaining and fluent-style use.
* @return this {@link MockPropertySource} instance
*/
public MockPropertySource withProperty(String name, Object value) {
@@ -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.
@@ -26,7 +26,7 @@ import org.springframework.core.env.PropertySource;
* a user-provided {@link Properties} object, or if omitted during construction,
* the implementation will initialize its own.
*
* The {@link #setProperty} and {@link #withProperty} methods are exposed for
* <p>The {@link #setProperty} and {@link #withProperty} methods are exposed for
* convenience, for example:
* <pre class="code">
* {@code
@@ -36,7 +36,7 @@ import org.springframework.core.env.PropertySource;
*
* @author Chris Beams
* @since 3.1
* @see org.springframework.mock.env.MockEnvironment
* @see MockEnvironment
*/
public class MockPropertySource extends PropertiesPropertySource {
@@ -95,7 +95,7 @@ public class MockPropertySource extends PropertiesPropertySource {
/**
* Convenient synonym for {@link #setProperty} that returns the current instance.
* Useful for method chaining and fluent-style use.
* <p>Useful for method chaining and fluent-style use.
* @return this {@link MockPropertySource} instance
*/
public MockPropertySource withProperty(String name, Object value) {
@@ -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.
@@ -67,6 +67,7 @@ public final class ContentDisposition {
for (int i=33; i<= 126; i++) {
PRINTABLE.set(i);
}
PRINTABLE.set(34, false); // "
PRINTABLE.set(61, false); // =
PRINTABLE.set(63, false); // ?
PRINTABLE.set(95, false); // _
@@ -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();
@@ -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.
@@ -79,10 +79,10 @@ 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
* back in a test, e.g. {@code Clock.offset(clock, Duration.ofMinutes(-31))}
* 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"))}.
* @param clock the clock to use
@@ -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() {
@@ -278,7 +280,7 @@ public class InMemoryWebSessionStore implements WebSessionStore {
private void checkMaxSessionsLimit() {
if (sessions.size() >= maxSessions) {
expiredSessionChecker.removeExpiredSessions(clock.instant());
if (sessions.size() >= maxSessions) {
if (sessions.size() >= maxSessions && !sessions.containsKey(this.id.get())) {
throw new IllegalStateException("Max sessions limit reached: " + sessions.size());
}
}
@@ -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.
@@ -305,6 +305,13 @@ class ContentDispositionTests {
tester.accept("foo.txt\\\\\\", "foo.txt\\\\\\\\\\\\");
}
@Test
void formatWithUtf8FilenameWithQuotes() {
String filename = "\"中文.txt";
assertThat(ContentDisposition.formData().filename(filename, StandardCharsets.UTF_8).build().toString())
.isEqualTo("form-data; filename=\"=?UTF-8?Q?=22=E4=B8=AD=E6=96=87.txt?=\"; filename*=UTF-8''%22%E4%B8%AD%E6%96%87.txt");
}
@Test
void formatWithEncodedFilenameUsingInvalidCharset() {
assertThatIllegalArgumentException().isThrownBy(() ->
@@ -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,11 +19,11 @@ 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;
import reactor.core.scheduler.Schedulers;
import reactor.test.StepVerifier;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.web.server.WebSession;
@@ -35,10 +35,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
@@ -53,13 +54,14 @@ class InMemoryWebSessionStoreTests {
void startsSessionImplicitly() {
WebSession session = this.store.createWebSession().block();
assertThat(session).isNotNull();
session.start();
// We intentionally do not invoke start().
// session.start();
session.getAttributes().put("foo", "bar");
assertThat(session.isStarted()).isTrue();
}
@Test // gh-24027, gh-26958
public void createSessionDoesNotBlock() {
void createSessionDoesNotBlock() {
this.store.createWebSession()
.doOnNext(session -> assertThat(Schedulers.isInNonBlockingThread()).isTrue())
.block();
@@ -103,7 +105,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 +134,69 @@ 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");
}
@Test
void updateSession() {
WebSession session = insertSession();
StepVerifier.create(session.save())
.expectComplete()
.verify();
}
@Test // gh-35013
void updateSessionAfterMaxSessionLimitIsExceeded() {
this.store.setMaxSessions(10);
WebSession session = insertSession();
assertNumSessions(1);
IntStream.rangeClosed(1, 9).forEach(i -> insertSession());
assertNumSessions(10);
// Updating an existing session should succeed.
StepVerifier.create(session.save())
.expectComplete()
.verify();
assertNumSessions(10);
// Saving an additional new session should fail.
assertThatIllegalStateException()
.isThrownBy(this::insertSession)
.withMessage("Max sessions limit reached: 10");
assertNumSessions(10);
// Updating an existing session again should still succeed.
StepVerifier.create(session.save())
.expectComplete()
.verify();
assertNumSessions(10);
}
private WebSession insertSession() {
WebSession session = this.store.createWebSession().block();
assertThat(session).isNotNull();
@@ -167,4 +205,8 @@ class InMemoryWebSessionStoreTests {
return session;
}
private void assertNumSessions(int numSessions) {
assertThat(store.getSessions()).hasSize(numSessions);
}
}