Merge pull request #32046 from vpavic
* pr/32046: Use ReactiveFindByIndexNameSessionRepository Polish "Provide an Actuator endpoint for non-indexed session repositories" Provide an Actuator endpoint for non-indexed session repositories Closes gh-32046
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -16,17 +16,25 @@
|
||||
|
||||
package org.springframework.boot.actuate.autoconfigure.session;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
|
||||
import org.springframework.boot.actuate.session.ReactiveSessionsEndpoint;
|
||||
import org.springframework.boot.actuate.session.SessionsEndpoint;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
|
||||
import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.ReactiveFindByIndexNameSessionRepository;
|
||||
import org.springframework.session.ReactiveSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for {@link SessionsEndpoint}.
|
||||
@@ -35,15 +43,36 @@ import org.springframework.session.Session;
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = SessionAutoConfiguration.class)
|
||||
@ConditionalOnClass(FindByIndexNameSessionRepository.class)
|
||||
@ConditionalOnClass(Session.class)
|
||||
@ConditionalOnAvailableEndpoint(endpoint = SessionsEndpoint.class)
|
||||
public class SessionsEndpointAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(FindByIndexNameSessionRepository.class)
|
||||
@ConditionalOnMissingBean
|
||||
public SessionsEndpoint sessionEndpoint(FindByIndexNameSessionRepository<? extends Session> sessionRepository) {
|
||||
return new SessionsEndpoint(sessionRepository);
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnWebApplication(type = Type.SERVLET)
|
||||
@ConditionalOnBean(SessionRepository.class)
|
||||
static class ServletSessionEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
SessionsEndpoint sessionEndpoint(SessionRepository<? extends Session> sessionRepository,
|
||||
ObjectProvider<FindByIndexNameSessionRepository<? extends Session>> indexedSessionRepository) {
|
||||
return new SessionsEndpoint(sessionRepository, indexedSessionRepository.getIfAvailable());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnWebApplication(type = Type.REACTIVE)
|
||||
@ConditionalOnBean(ReactiveSessionRepository.class)
|
||||
static class ReactiveSessionEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ReactiveSessionsEndpoint sessionsEndpoint(ReactiveSessionRepository<? extends Session> sessionRepository,
|
||||
ObjectProvider<ReactiveFindByIndexNameSessionRepository<? extends Session>> indexedSessionRepository) {
|
||||
return new ReactiveSessionsEndpoint(sessionRepository, indexedSessionRepository.getIfAvailable());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ class SessionsEndpointDocumentationTests extends MockMvcEndpointDocumentationTes
|
||||
|
||||
@Bean
|
||||
SessionsEndpoint endpoint(FindByIndexNameSessionRepository<?> sessionRepository) {
|
||||
return new SessionsEndpoint(sessionRepository);
|
||||
return new SessionsEndpoint(sessionRepository, sessionRepository);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -16,14 +16,20 @@
|
||||
|
||||
package org.springframework.boot.actuate.autoconfigure.session;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.session.ReactiveSessionsEndpoint;
|
||||
import org.springframework.boot.actuate.session.SessionsEndpoint;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.ReactiveFindByIndexNameSessionRepository;
|
||||
import org.springframework.session.ReactiveSessionRepository;
|
||||
import org.springframework.session.SessionRepository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -32,36 +38,117 @@ import static org.mockito.Mockito.mock;
|
||||
* Tests for {@link SessionsEndpointAutoConfiguration}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SessionsEndpointAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(SessionsEndpointAutoConfiguration.class))
|
||||
.withUserConfiguration(SessionConfiguration.class);
|
||||
@Nested
|
||||
class ServletSessionEndpointConfigurationTests {
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(SessionsEndpointAutoConfiguration.class))
|
||||
.withUserConfiguration(IndexedSessionRepositoryConfiguration.class);
|
||||
|
||||
@Test
|
||||
void runShouldHaveEndpointBean() {
|
||||
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=sessions")
|
||||
.run((context) -> assertThat(context).hasSingleBean(SessionsEndpoint.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWhenNoIndexedSessionRepositoryShouldHaveEndpointBean() {
|
||||
new WebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(SessionsEndpointAutoConfiguration.class))
|
||||
.withUserConfiguration(SessionRepositoryConfiguration.class)
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=sessions")
|
||||
.run((context) -> assertThat(context).hasSingleBean(SessionsEndpoint.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWhenNotExposedShouldNotHaveEndpointBean() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(SessionsEndpoint.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() {
|
||||
this.contextRunner.withPropertyValues("management.endpoint.sessions.enabled:false")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(SessionsEndpoint.class));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class IndexedSessionRepositoryConfiguration {
|
||||
|
||||
@Bean
|
||||
FindByIndexNameSessionRepository<?> sessionRepository() {
|
||||
return mock(FindByIndexNameSessionRepository.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class SessionRepositoryConfiguration {
|
||||
|
||||
@Bean
|
||||
SessionRepository<?> sessionRepository() {
|
||||
return mock(SessionRepository.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void runShouldHaveEndpointBean() {
|
||||
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=sessions")
|
||||
.run((context) -> assertThat(context).hasSingleBean(SessionsEndpoint.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWhenNotExposedShouldNotHaveEndpointBean() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(SessionsEndpoint.class));
|
||||
}
|
||||
@Nested
|
||||
class ReactiveSessionEndpointConfigurationTests {
|
||||
|
||||
@Test
|
||||
void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() {
|
||||
this.contextRunner.withPropertyValues("management.endpoint.sessions.enabled:false")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(SessionsEndpoint.class));
|
||||
}
|
||||
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(SessionsEndpointAutoConfiguration.class))
|
||||
.withUserConfiguration(ReactiveSessionRepositoryConfiguration.class,
|
||||
ReactiveIndexedSessionRepositoryConfiguration.class);
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class SessionConfiguration {
|
||||
@Test
|
||||
void runShouldHaveEndpointBean() {
|
||||
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=sessions")
|
||||
.run((context) -> assertThat(context).hasSingleBean(ReactiveSessionsEndpoint.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWhenNoIndexedSessionRepositoryShouldHaveEndpointBean() {
|
||||
new ReactiveWebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(SessionsEndpointAutoConfiguration.class))
|
||||
.withUserConfiguration(ReactiveSessionRepositoryConfiguration.class)
|
||||
.withPropertyValues("management.endpoints.web.exposure.include=sessions")
|
||||
.run((context) -> assertThat(context).hasSingleBean(ReactiveSessionsEndpoint.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWhenNotExposedShouldNotHaveEndpointBean() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ReactiveSessionsEndpoint.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() {
|
||||
this.contextRunner.withPropertyValues("management.endpoint.sessions.enabled:false")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ReactiveSessionsEndpoint.class));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ReactiveIndexedSessionRepositoryConfiguration {
|
||||
|
||||
@Bean
|
||||
ReactiveFindByIndexNameSessionRepository<?> indexedSessionRepository() {
|
||||
return mock(ReactiveFindByIndexNameSessionRepository.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ReactiveSessionRepositoryConfiguration {
|
||||
|
||||
@Bean
|
||||
ReactiveSessionRepository<?> sessionRepository() {
|
||||
return mock(ReactiveSessionRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
FindByIndexNameSessionRepository<?> sessionRepository() {
|
||||
return mock(FindByIndexNameSessionRepository.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.boot.actuate.session;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
||||
import org.springframework.boot.actuate.session.SessionsDescriptor.SessionDescriptor;
|
||||
import org.springframework.session.ReactiveFindByIndexNameSessionRepository;
|
||||
import org.springframework.session.ReactiveSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link Endpoint @Endpoint} to expose information about HTTP {@link Session}s on a
|
||||
* reactive stack.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Moritz Halbritter
|
||||
* @since 3.3.0
|
||||
*/
|
||||
@Endpoint(id = "sessions")
|
||||
public class ReactiveSessionsEndpoint {
|
||||
|
||||
private final ReactiveSessionRepository<? extends Session> sessionRepository;
|
||||
|
||||
private final ReactiveFindByIndexNameSessionRepository<? extends Session> indexedSessionRepository;
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveSessionsEndpoint} instance.
|
||||
* @param sessionRepository the session repository
|
||||
* @param indexedSessionRepository the indexed session repository
|
||||
*/
|
||||
public ReactiveSessionsEndpoint(ReactiveSessionRepository<? extends Session> sessionRepository,
|
||||
ReactiveFindByIndexNameSessionRepository<? extends Session> indexedSessionRepository) {
|
||||
Assert.notNull(sessionRepository, "ReactiveSessionRepository must not be null");
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.indexedSessionRepository = indexedSessionRepository;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Mono<SessionsDescriptor> sessionsForUsername(String username) {
|
||||
if (this.indexedSessionRepository == null) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return this.indexedSessionRepository.findByPrincipalName(username).map(SessionsDescriptor::new);
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Mono<SessionDescriptor> getSession(@Selector String sessionId) {
|
||||
return this.sessionRepository.findById(sessionId).map(SessionDescriptor::new);
|
||||
}
|
||||
|
||||
@DeleteOperation
|
||||
public Mono<Void> deleteSession(@Selector String sessionId) {
|
||||
return this.sessionRepository.deleteById(sessionId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.boot.actuate.session;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
|
||||
import org.springframework.session.Session;
|
||||
|
||||
/**
|
||||
* Description of user's {@link Session sessions}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @since 3.3.0
|
||||
*/
|
||||
public final class SessionsDescriptor implements OperationResponseBody {
|
||||
|
||||
private final List<SessionDescriptor> sessions;
|
||||
|
||||
public SessionsDescriptor(Map<String, ? extends Session> sessions) {
|
||||
this.sessions = sessions.values().stream().map(SessionDescriptor::new).toList();
|
||||
}
|
||||
|
||||
public List<SessionDescriptor> getSessions() {
|
||||
return this.sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* A description of user's {@link Session session} exposed by {@code sessions}
|
||||
* endpoint. Primarily intended for serialization to JSON.
|
||||
*/
|
||||
public static final class SessionDescriptor {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final Set<String> attributeNames;
|
||||
|
||||
private final Instant creationTime;
|
||||
|
||||
private final Instant lastAccessedTime;
|
||||
|
||||
private final long maxInactiveInterval;
|
||||
|
||||
private final boolean expired;
|
||||
|
||||
SessionDescriptor(Session session) {
|
||||
this.id = session.getId();
|
||||
this.attributeNames = session.getAttributeNames();
|
||||
this.creationTime = session.getCreationTime();
|
||||
this.lastAccessedTime = session.getLastAccessedTime();
|
||||
this.maxInactiveInterval = session.getMaxInactiveInterval().getSeconds();
|
||||
this.expired = session.isExpired();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public Set<String> getAttributeNames() {
|
||||
return this.attributeNames;
|
||||
}
|
||||
|
||||
public Instant getCreationTime() {
|
||||
return this.creationTime;
|
||||
}
|
||||
|
||||
public Instant getLastAccessedTime() {
|
||||
return this.lastAccessedTime;
|
||||
}
|
||||
|
||||
public long getMaxInactiveInterval() {
|
||||
return this.maxInactiveInterval;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return this.expired;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -16,21 +16,21 @@
|
||||
|
||||
package org.springframework.boot.actuate.session;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
||||
import org.springframework.boot.actuate.session.SessionsDescriptor.SessionDescriptor;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link Endpoint @Endpoint} to expose a user's {@link Session}s.
|
||||
* {@link Endpoint @Endpoint} to expose information about HTTP {@link Session}s on a
|
||||
* Servlet stack.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @since 2.0.0
|
||||
@@ -38,19 +38,40 @@ import org.springframework.session.Session;
|
||||
@Endpoint(id = "sessions")
|
||||
public class SessionsEndpoint {
|
||||
|
||||
private final FindByIndexNameSessionRepository<? extends Session> sessionRepository;
|
||||
private final SessionRepository<? extends Session> sessionRepository;
|
||||
|
||||
private final FindByIndexNameSessionRepository<? extends Session> indexedSessionRepository;
|
||||
|
||||
/**
|
||||
* Create a new {@link SessionsEndpoint} instance.
|
||||
* @param sessionRepository the session repository
|
||||
* @deprecated since 3.3.0 for removal in 3.5.0 in favor of
|
||||
* {@link #SessionsEndpoint(SessionRepository, FindByIndexNameSessionRepository)}
|
||||
*/
|
||||
@Deprecated(since = "3.3.0", forRemoval = true)
|
||||
public SessionsEndpoint(FindByIndexNameSessionRepository<? extends Session> sessionRepository) {
|
||||
this(sessionRepository, sessionRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link SessionsEndpoint} instance.
|
||||
* @param sessionRepository the session repository
|
||||
* @param indexedSessionRepository the indexed session repository
|
||||
* @since 3.3.0
|
||||
*/
|
||||
public SessionsEndpoint(SessionRepository<? extends Session> sessionRepository,
|
||||
FindByIndexNameSessionRepository<? extends Session> indexedSessionRepository) {
|
||||
Assert.notNull(sessionRepository, "SessionRepository must not be null");
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.indexedSessionRepository = indexedSessionRepository;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public SessionsDescriptor sessionsForUsername(String username) {
|
||||
Map<String, ? extends Session> sessions = this.sessionRepository.findByPrincipalName(username);
|
||||
if (this.indexedSessionRepository == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, ? extends Session> sessions = this.indexedSessionRepository.findByPrincipalName(username);
|
||||
return new SessionsDescriptor(sessions);
|
||||
}
|
||||
|
||||
@@ -68,73 +89,4 @@ public class SessionsEndpoint {
|
||||
this.sessionRepository.deleteById(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of user's {@link Session sessions}.
|
||||
*/
|
||||
public static final class SessionsDescriptor implements OperationResponseBody {
|
||||
|
||||
private final List<SessionDescriptor> sessions;
|
||||
|
||||
public SessionsDescriptor(Map<String, ? extends Session> sessions) {
|
||||
this.sessions = sessions.values().stream().map(SessionDescriptor::new).toList();
|
||||
}
|
||||
|
||||
public List<SessionDescriptor> getSessions() {
|
||||
return this.sessions;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of user's {@link Session session}.
|
||||
*/
|
||||
public static final class SessionDescriptor implements OperationResponseBody {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final Set<String> attributeNames;
|
||||
|
||||
private final Instant creationTime;
|
||||
|
||||
private final Instant lastAccessedTime;
|
||||
|
||||
private final long maxInactiveInterval;
|
||||
|
||||
private final boolean expired;
|
||||
|
||||
public SessionDescriptor(Session session) {
|
||||
this.id = session.getId();
|
||||
this.attributeNames = session.getAttributeNames();
|
||||
this.creationTime = session.getCreationTime();
|
||||
this.lastAccessedTime = session.getLastAccessedTime();
|
||||
this.maxInactiveInterval = session.getMaxInactiveInterval().getSeconds();
|
||||
this.expired = session.isExpired();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public Set<String> getAttributeNames() {
|
||||
return this.attributeNames;
|
||||
}
|
||||
|
||||
public Instant getCreationTime() {
|
||||
return this.creationTime;
|
||||
}
|
||||
|
||||
public Instant getLastAccessedTime() {
|
||||
return this.lastAccessedTime;
|
||||
}
|
||||
|
||||
public long getMaxInactiveInterval() {
|
||||
return this.maxInactiveInterval;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return this.expired;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.boot.actuate.session;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.boot.actuate.session.SessionsDescriptor.SessionDescriptor;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.ReactiveFindByIndexNameSessionRepository;
|
||||
import org.springframework.session.ReactiveSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactiveSessionsEndpoint}.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ReactiveSessionsEndpointTests {
|
||||
|
||||
private static final Session session = new MapSession();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private final ReactiveSessionRepository<Session> sessionRepository = mock(ReactiveSessionRepository.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private final ReactiveFindByIndexNameSessionRepository<Session> indexedSessionRepository = mock(
|
||||
ReactiveFindByIndexNameSessionRepository.class);
|
||||
|
||||
private final ReactiveSessionsEndpoint endpoint = new ReactiveSessionsEndpoint(this.sessionRepository,
|
||||
this.indexedSessionRepository);
|
||||
|
||||
@Test
|
||||
void sessionsForUsername() {
|
||||
given(this.indexedSessionRepository.findByPrincipalName("user"))
|
||||
.willReturn(Mono.just(Collections.singletonMap(session.getId(), session)));
|
||||
StepVerifier.create(this.endpoint.sessionsForUsername("user")).consumeNextWith((sessions) -> {
|
||||
List<SessionDescriptor> result = sessions.getSessions();
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).getId()).isEqualTo(session.getId());
|
||||
assertThat(result.get(0).getAttributeNames()).isEqualTo(session.getAttributeNames());
|
||||
assertThat(result.get(0).getCreationTime()).isEqualTo(session.getCreationTime());
|
||||
assertThat(result.get(0).getLastAccessedTime()).isEqualTo(session.getLastAccessedTime());
|
||||
assertThat(result.get(0).getMaxInactiveInterval()).isEqualTo(session.getMaxInactiveInterval().getSeconds());
|
||||
assertThat(result.get(0).isExpired()).isEqualTo(session.isExpired());
|
||||
|
||||
}).expectComplete().verify(Duration.ofSeconds(1));
|
||||
then(this.indexedSessionRepository).should().findByPrincipalName("user");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionsForUsernameWhenNoIndexedRepository() {
|
||||
ReactiveSessionsEndpoint endpoint = new ReactiveSessionsEndpoint(this.sessionRepository, null);
|
||||
StepVerifier.create(endpoint.sessionsForUsername("user")).expectComplete().verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSession() {
|
||||
given(this.sessionRepository.findById(session.getId())).willReturn(Mono.just(session));
|
||||
StepVerifier.create(this.endpoint.getSession(session.getId())).consumeNextWith((result) -> {
|
||||
assertThat(result.getId()).isEqualTo(session.getId());
|
||||
assertThat(result.getAttributeNames()).isEqualTo(session.getAttributeNames());
|
||||
assertThat(result.getCreationTime()).isEqualTo(session.getCreationTime());
|
||||
assertThat(result.getLastAccessedTime()).isEqualTo(session.getLastAccessedTime());
|
||||
assertThat(result.getMaxInactiveInterval()).isEqualTo(session.getMaxInactiveInterval().getSeconds());
|
||||
assertThat(result.isExpired()).isEqualTo(session.isExpired());
|
||||
}).expectComplete().verify(Duration.ofSeconds(1));
|
||||
then(this.sessionRepository).should().findById(session.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSessionWithIdNotFound() {
|
||||
given(this.sessionRepository.findById("not-found")).willReturn(Mono.empty());
|
||||
StepVerifier.create(this.endpoint.getSession("not-found")).expectComplete().verify(Duration.ofSeconds(1));
|
||||
then(this.sessionRepository).should().findById("not-found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteSession() {
|
||||
given(this.sessionRepository.deleteById(session.getId())).willReturn(Mono.empty());
|
||||
StepVerifier.create(this.endpoint.deleteSession(session.getId()))
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1));
|
||||
then(this.sessionRepository).should().deleteById(session.getId());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.boot.actuate.session;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import net.minidev.json.JSONArray;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
|
||||
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest.Infrastructure;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.ReactiveFindByIndexNameSessionRepository;
|
||||
import org.springframework.session.ReactiveSessionRepository;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ReactiveSessionsEndpoint} exposed by WebFlux.
|
||||
*
|
||||
* @author Vedran Pavic
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class ReactiveSessionsEndpointWebIntegrationTests {
|
||||
|
||||
private static final Session session = new MapSession();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final ReactiveSessionRepository<Session> sessionRepository = mock(ReactiveSessionRepository.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final ReactiveFindByIndexNameSessionRepository<Session> indexedSessionRepository = mock(
|
||||
ReactiveFindByIndexNameSessionRepository.class);
|
||||
|
||||
@WebEndpointTest(infrastructure = Infrastructure.WEBFLUX)
|
||||
void sessionsForUsernameWithoutUsernameParam(WebTestClient client) {
|
||||
client.get()
|
||||
.uri((builder) -> builder.path("/actuator/sessions").build())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.is5xxServerError(); // https://github.com/spring-projects/spring-boot/issues/39236
|
||||
}
|
||||
|
||||
@WebEndpointTest(infrastructure = Infrastructure.WEBFLUX)
|
||||
void sessionsForUsernameNoResults(WebTestClient client) {
|
||||
given(indexedSessionRepository.findByPrincipalName("user")).willReturn(Mono.just(Collections.emptyMap()));
|
||||
client.get()
|
||||
.uri((builder) -> builder.path("/actuator/sessions").queryParam("username", "user").build())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody()
|
||||
.jsonPath("sessions")
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@WebEndpointTest(infrastructure = Infrastructure.WEBFLUX)
|
||||
void sessionsForUsernameFound(WebTestClient client) {
|
||||
given(indexedSessionRepository.findByPrincipalName("user"))
|
||||
.willReturn(Mono.just(Collections.singletonMap(session.getId(), session)));
|
||||
client.get()
|
||||
.uri((builder) -> builder.path("/actuator/sessions").queryParam("username", "user").build())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody()
|
||||
.jsonPath("sessions.[*].id")
|
||||
.isEqualTo(new JSONArray().appendElement(session.getId()));
|
||||
}
|
||||
|
||||
@WebEndpointTest(infrastructure = Infrastructure.WEBFLUX)
|
||||
void sessionForIdFound(WebTestClient client) {
|
||||
given(sessionRepository.findById(session.getId())).willReturn(Mono.just(session));
|
||||
client.get()
|
||||
.uri((builder) -> builder.path("/actuator/sessions/{id}").build(session.getId()))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody()
|
||||
.jsonPath("id")
|
||||
.isEqualTo(session.getId());
|
||||
}
|
||||
|
||||
@WebEndpointTest(infrastructure = Infrastructure.WEBFLUX)
|
||||
void sessionForIdNotFound(WebTestClient client) {
|
||||
given(sessionRepository.findById("not-found")).willReturn(Mono.empty());
|
||||
client.get()
|
||||
.uri((builder) -> builder.path("/actuator/sessions/not-found").build())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isNotFound();
|
||||
}
|
||||
|
||||
@WebEndpointTest(infrastructure = Infrastructure.WEBFLUX)
|
||||
void deleteSession(WebTestClient client) {
|
||||
given(sessionRepository.deleteById(session.getId())).willReturn(Mono.empty());
|
||||
client.delete()
|
||||
.uri((builder) -> builder.path("/actuator/sessions/{id}").build(session.getId()))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isNoContent();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
ReactiveSessionsEndpoint sessionsEndpoint() {
|
||||
return new ReactiveSessionsEndpoint(sessionRepository, indexedSessionRepository);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -21,10 +21,11 @@ import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.session.SessionsEndpoint.SessionDescriptor;
|
||||
import org.springframework.boot.actuate.session.SessionsDescriptor.SessionDescriptor;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
@@ -41,13 +42,18 @@ class SessionsEndpointTests {
|
||||
private static final Session session = new MapSession();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private final FindByIndexNameSessionRepository<Session> repository = mock(FindByIndexNameSessionRepository.class);
|
||||
private final SessionRepository<Session> sessionRepository = mock(SessionRepository.class);
|
||||
|
||||
private final SessionsEndpoint endpoint = new SessionsEndpoint(this.repository);
|
||||
@SuppressWarnings("unchecked")
|
||||
private final FindByIndexNameSessionRepository<Session> indexedSessionRepository = mock(
|
||||
FindByIndexNameSessionRepository.class);
|
||||
|
||||
private final SessionsEndpoint endpoint = new SessionsEndpoint(this.sessionRepository,
|
||||
this.indexedSessionRepository);
|
||||
|
||||
@Test
|
||||
void sessionsForUsername() {
|
||||
given(this.repository.findByPrincipalName("user"))
|
||||
given(this.indexedSessionRepository.findByPrincipalName("user"))
|
||||
.willReturn(Collections.singletonMap(session.getId(), session));
|
||||
List<SessionDescriptor> result = this.endpoint.sessionsForUsername("user").getSessions();
|
||||
assertThat(result).hasSize(1);
|
||||
@@ -57,11 +63,18 @@ class SessionsEndpointTests {
|
||||
assertThat(result.get(0).getLastAccessedTime()).isEqualTo(session.getLastAccessedTime());
|
||||
assertThat(result.get(0).getMaxInactiveInterval()).isEqualTo(session.getMaxInactiveInterval().getSeconds());
|
||||
assertThat(result.get(0).isExpired()).isEqualTo(session.isExpired());
|
||||
then(this.indexedSessionRepository).should().findByPrincipalName("user");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionsForUsernameWhenNoIndexedRepository() {
|
||||
SessionsEndpoint endpoint = new SessionsEndpoint(this.sessionRepository, null);
|
||||
assertThat(endpoint.sessionsForUsername("user")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSession() {
|
||||
given(this.repository.findById(session.getId())).willReturn(session);
|
||||
given(this.sessionRepository.findById(session.getId())).willReturn(session);
|
||||
SessionDescriptor result = this.endpoint.getSession(session.getId());
|
||||
assertThat(result.getId()).isEqualTo(session.getId());
|
||||
assertThat(result.getAttributeNames()).isEqualTo(session.getAttributeNames());
|
||||
@@ -69,18 +82,20 @@ class SessionsEndpointTests {
|
||||
assertThat(result.getLastAccessedTime()).isEqualTo(session.getLastAccessedTime());
|
||||
assertThat(result.getMaxInactiveInterval()).isEqualTo(session.getMaxInactiveInterval().getSeconds());
|
||||
assertThat(result.isExpired()).isEqualTo(session.isExpired());
|
||||
then(this.sessionRepository).should().findById(session.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSessionWithIdNotFound() {
|
||||
given(this.repository.findById("not-found")).willReturn(null);
|
||||
given(this.sessionRepository.findById("not-found")).willReturn(null);
|
||||
assertThat(this.endpoint.getSession("not-found")).isNull();
|
||||
then(this.sessionRepository).should().findById("not-found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteSession() {
|
||||
this.endpoint.deleteSession(session.getId());
|
||||
then(this.repository).should().deleteById(session.getId());
|
||||
then(this.sessionRepository).should().deleteById(session.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -21,6 +21,7 @@ import java.util.Collections;
|
||||
import net.minidev.json.JSONArray;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
|
||||
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest.Infrastructure;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.session.FindByIndexNameSessionRepository;
|
||||
@@ -45,7 +46,7 @@ class SessionsEndpointWebIntegrationTests {
|
||||
private static final FindByIndexNameSessionRepository<Session> repository = mock(
|
||||
FindByIndexNameSessionRepository.class);
|
||||
|
||||
@WebEndpointTest
|
||||
@WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC })
|
||||
void sessionsForUsernameWithoutUsernameParam(WebTestClient client) {
|
||||
client.get()
|
||||
.uri((builder) -> builder.path("/actuator/sessions").build())
|
||||
@@ -54,7 +55,7 @@ class SessionsEndpointWebIntegrationTests {
|
||||
.isBadRequest();
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
@WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC })
|
||||
void sessionsForUsernameNoResults(WebTestClient client) {
|
||||
given(repository.findByPrincipalName("user")).willReturn(Collections.emptyMap());
|
||||
client.get()
|
||||
@@ -67,7 +68,7 @@ class SessionsEndpointWebIntegrationTests {
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
@WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC })
|
||||
void sessionsForUsernameFound(WebTestClient client) {
|
||||
given(repository.findByPrincipalName("user")).willReturn(Collections.singletonMap(session.getId(), session));
|
||||
client.get()
|
||||
@@ -80,7 +81,7 @@ class SessionsEndpointWebIntegrationTests {
|
||||
.isEqualTo(new JSONArray().appendElement(session.getId()));
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
@WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC })
|
||||
void sessionForIdNotFound(WebTestClient client) {
|
||||
client.get()
|
||||
.uri((builder) -> builder.path("/actuator/sessions/session-id-not-found").build())
|
||||
@@ -89,12 +90,21 @@ class SessionsEndpointWebIntegrationTests {
|
||||
.isNotFound();
|
||||
}
|
||||
|
||||
@WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC })
|
||||
void deleteSession(WebTestClient client) {
|
||||
client.delete()
|
||||
.uri((builder) -> builder.path("/actuator/sessions/{id}").build(session.getId()))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isNoContent();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
SessionsEndpoint sessionsEndpoint() {
|
||||
return new SessionsEndpoint(repository);
|
||||
return new SessionsEndpoint(repository, repository);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user