diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java index 3ec9589a29f..10f5e92f05d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java @@ -16,17 +16,24 @@ 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.ReactiveSessionRepository; import org.springframework.session.Session; +import org.springframework.session.SessionRepository; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link SessionsEndpoint}. @@ -35,15 +42,35 @@ 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 sessionRepository) { - return new SessionsEndpoint(sessionRepository); + @Configuration(proxyBeanMethods = false) + @ConditionalOnWebApplication(type = Type.SERVLET) + @ConditionalOnBean(SessionRepository.class) + static class ServletSessionEndpointConfiguration { + + @Bean + @ConditionalOnMissingBean + SessionsEndpoint sessionEndpoint(SessionRepository sessionRepository, + ObjectProvider> 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 sessionRepository) { + return new ReactiveSessionsEndpoint(sessionRepository); + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java index 0d43d8fa122..b18e125846d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java @@ -125,7 +125,7 @@ class SessionsEndpointDocumentationTests extends MockMvcEndpointDocumentationTes @Bean SessionsEndpoint endpoint(FindByIndexNameSessionRepository sessionRepository) { - return new SessionsEndpoint(sessionRepository); + return new SessionsEndpoint(sessionRepository, sessionRepository); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java index 576fe6bbaee..6477fdeb0b1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java @@ -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,19 @@ 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.ReactiveSessionRepository; +import org.springframework.session.SessionRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -35,33 +40,93 @@ import static org.mockito.Mockito.mock; */ 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); - @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 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 ReactiveSessionRepositoryConfiguration { + + @Bean + ReactiveSessionRepository sessionRepository() { + return mock(ReactiveSessionRepository.class); + } - @Bean - FindByIndexNameSessionRepository sessionRepository() { - return mock(FindByIndexNameSessionRepository.class); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpoint.java new file mode 100644 index 00000000000..298764d5b32 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpoint.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2022 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.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 + * @since 3.0.0 + */ +@Endpoint(id = "sessions") +public class ReactiveSessionsEndpoint { + + private final ReactiveSessionRepository sessionRepository; + + /** + * Create a new {@link ReactiveSessionsEndpoint} instance. + * @param sessionRepository the session repository + */ + public ReactiveSessionsEndpoint(ReactiveSessionRepository sessionRepository) { + Assert.notNull(sessionRepository, "ReactiveSessionRepository must not be null"); + this.sessionRepository = sessionRepository; + } + + @ReadOperation + public Mono getSession(@Selector String sessionId) { + return this.sessionRepository.findById(sessionId).map(SessionDescriptor::new); + } + + @DeleteOperation + public Mono deleteSession(@Selector String sessionId) { + return this.sessionRepository.deleteById(sessionId); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionDescriptor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionDescriptor.java new file mode 100644 index 00000000000..71e5cc8aebf --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionDescriptor.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2022 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.Set; + +import org.springframework.session.Session; + +/** + * A description of user's {@link Session session} exposed by {@code sessions} endpoint. + * Primarily intended for serialization to JSON. + * + * @author Vedran Pavic + * @since 3.0.0 + */ +public final class SessionDescriptor { + + private final String id; + + private final Set 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 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; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java index b333d8e23c2..db60d0459e3 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java @@ -28,9 +28,12 @@ import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; 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 +41,28 @@ import org.springframework.session.Session; @Endpoint(id = "sessions") public class SessionsEndpoint { - private final FindByIndexNameSessionRepository sessionRepository; + private final SessionRepository sessionRepository; + + private final FindByIndexNameSessionRepository indexedSessionRepository; /** * Create a new {@link SessionsEndpoint} instance. * @param sessionRepository the session repository + * @param indexedSessionRepository the indexed session repository */ - public SessionsEndpoint(FindByIndexNameSessionRepository sessionRepository) { + public SessionsEndpoint(SessionRepository sessionRepository, + FindByIndexNameSessionRepository indexedSessionRepository) { + Assert.notNull(sessionRepository, "SessionRepository must not be null"); this.sessionRepository = sessionRepository; + this.indexedSessionRepository = indexedSessionRepository; } @ReadOperation public SessionsDescriptor sessionsForUsername(String username) { - Map sessions = this.sessionRepository.findByPrincipalName(username); + if (this.indexedSessionRepository == null) { + return null; + } + Map sessions = this.indexedSessionRepository.findByPrincipalName(username); return new SessionsDescriptor(sessions); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java new file mode 100644 index 00000000000..39895c44234 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2022 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 org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.session.MapSession; +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 + */ +class ReactiveSessionsEndpointTests { + + private static final Session session = new MapSession(); + + @SuppressWarnings("unchecked") + private final ReactiveSessionRepository sessionRepository = mock(ReactiveSessionRepository.class); + + private final ReactiveSessionsEndpoint endpoint = new ReactiveSessionsEndpoint(this.sessionRepository); + + @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()); + }).verifyComplete(); + 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")).verifyComplete(); + 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())).verifyComplete(); + then(this.sessionRepository).should().deleteById(session.getId()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointWebIntegrationTests.java new file mode 100644 index 00000000000..7f401484d72 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointWebIntegrationTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2022 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.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.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 + */ +class ReactiveSessionsEndpointWebIntegrationTests { + + private static final Session session = new MapSession(); + + @SuppressWarnings("unchecked") + private static final ReactiveSessionRepository sessionRepository = mock(ReactiveSessionRepository.class); + + @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); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java index eb1647e3891..62e728216bf 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java @@ -21,10 +21,10 @@ import java.util.List; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.session.SessionsEndpoint.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,15 +41,20 @@ class SessionsEndpointTests { private static final Session session = new MapSession(); @SuppressWarnings("unchecked") - private final FindByIndexNameSessionRepository repository = mock(FindByIndexNameSessionRepository.class); + private final SessionRepository sessionRepository = mock(SessionRepository.class); - private final SessionsEndpoint endpoint = new SessionsEndpoint(this.repository); + @SuppressWarnings("unchecked") + private final FindByIndexNameSessionRepository 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 result = this.endpoint.sessionsForUsername("user").getSessions(); + List result = this.endpoint.sessionsForUsername("user").getSessions(); assertThat(result).hasSize(1); assertThat(result.get(0).getId()).isEqualTo(session.getId()); assertThat(result.get(0).getAttributeNames()).isEqualTo(session.getAttributeNames()); @@ -57,30 +62,39 @@ 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); - SessionDescriptor result = this.endpoint.getSession(session.getId()); + given(this.sessionRepository.findById(session.getId())).willReturn(session); + SessionsEndpoint.SessionDescriptor result = this.endpoint.getSession(session.getId()); 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()); + 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()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointWebIntegrationTests.java index 0a6b28dd83b..fcf8d5e57d8 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointWebIntegrationTests.java @@ -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 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); } }