Provide an Actuator endpoint for non-indexed session repositories

At present, Actuator sessions endpoint is supported only on a Servlet stack and also requires an indexed session repository. With Spring Session moving to non-indexed session repositories as a default for some session stores, this means that sessions endpoint won't be available unless users opt into a (non-default) indexed session repository.

This commit updates SessionEndpoint so that it is able to work with a non-indexed session repository. In such setup, it exposes operations for fetching session by id and deleting the session.

Additionally, this also adds support for reactive stack by introducing ReactiveSessionEndpoint and its auto-configuration support.

See gh-32046
This commit is contained in:
Vedran Pavic
2022-08-09 23:36:38 +02:00
committed by Moritz Halbritter
parent a09cc22841
commit 6a9eb7754f
10 changed files with 477 additions and 49 deletions

View File

@@ -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<Session> 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());
}
}

View File

@@ -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<Session> 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);
}
}
}

View File

@@ -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<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();
List<SessionsEndpoint.SessionDescriptor> 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());
}
}

View File

@@ -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);
}
}