Add actuator endpoint for finding and deleting sessions

See gh-8342
This commit is contained in:
Vedran Pavic 2016-11-27 23:18:37 +01:00 committed by Stephane Nicoll
parent 714c533509
commit cf151b1717
18 changed files with 615 additions and 19 deletions

View File

@ -311,6 +311,11 @@
<artifactId>spring-security-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<optional>true</optional>
</dependency>
<!-- Annotation processing -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -0,0 +1,62 @@
/*
* Copyright 2012-2017 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
*
* http://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.autoconfigure.session;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
import org.springframework.boot.actuate.session.SessionsEndpoint;
import org.springframework.boot.actuate.session.SessionsWebEndpointExtension;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
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.session.SessionAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link SessionsEndpoint}.
*
* @author Vedran Pavic
* @since 2.0.0
*/
@Configuration
@ConditionalOnClass(FindByIndexNameSessionRepository.class)
@AutoConfigureAfter(SessionAutoConfiguration.class)
public class SessionsEndpointAutoConfiguration {
@Bean
@ConditionalOnBean(FindByIndexNameSessionRepository.class)
@ConditionalOnMissingBean
@ConditionalOnEnabledEndpoint
public SessionsEndpoint sessionEndpoint(
FindByIndexNameSessionRepository<? extends Session> sessionRepository) {
return new SessionsEndpoint(sessionRepository);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledEndpoint
@ConditionalOnBean(SessionsEndpoint.class)
public SessionsWebEndpointExtension sessionsWebEndpointExtension(
SessionsEndpoint sessionsEndpoint) {
return new SessionsWebEndpointExtension(sessionsEndpoint);
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Auto-configuration for actuator Spring Sessions concerns.
*/
package org.springframework.boot.actuate.autoconfigure.session;

View File

@ -29,6 +29,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration,
org.springframework.boot.actuate.autoconfigure.mongo.MongoHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.redis.RedisHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.solr.SolrHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.trace.TraceEndpointAutoConfiguration,\

View File

@ -0,0 +1,73 @@
/*
* Copyright 2012-2017 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
*
* http://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.autoconfigure.session;
import org.junit.Test;
import org.springframework.boot.actuate.session.SessionsEndpoint;
import org.springframework.boot.actuate.session.SessionsWebEndpointExtension;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.FindByIndexNameSessionRepository;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SessionsEndpointAutoConfiguration}.
*
* @author Vedran Pavic
*/
public class SessionsEndpointAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(SessionsEndpointAutoConfiguration.class))
.withUserConfiguration(SessionConfiguration.class);
@Test
public void runShouldHaveEndpointBean() {
this.contextRunner.run(
(context) -> assertThat(context).hasSingleBean(SessionsEndpoint.class));
}
@Test
public void runShouldHaveWebExtensionBean() {
this.contextRunner.run((context) -> assertThat(context)
.hasSingleBean(SessionsWebEndpointExtension.class));
}
@Test
public void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointOrExtensionBean()
throws Exception {
this.contextRunner.withPropertyValues("endpoints.sessions.enabled:false").run(
(context) -> assertThat(context).doesNotHaveBean(SessionsEndpoint.class));
}
@Configuration
static class SessionConfiguration {
@Bean
public FindByIndexNameSessionRepository sessionRepository() {
return mock(FindByIndexNameSessionRepository.class);
}
}
}

View File

@ -199,6 +199,11 @@
<artifactId>spring-security-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<optional>true</optional>
</dependency>
<!-- Annotation processing -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -0,0 +1,144 @@
/*
* Copyright 2012-2017 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
*
* http://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.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
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.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
/**
* {@link Endpoint} to expose a user's {@link Session}s.
*
* @author Vedran Pavic
* @since 2.0.0
*/
@Endpoint(id = "sessions")
public class SessionsEndpoint {
private final FindByIndexNameSessionRepository<? extends Session> sessionRepository;
/**
* Create a new {@link SessionsEndpoint} instance.
* @param sessionRepository the session repository
*/
public SessionsEndpoint(
FindByIndexNameSessionRepository<? extends Session> sessionRepository) {
this.sessionRepository = sessionRepository;
}
@ReadOperation
public SessionsReport sessionsForUsername(String username) {
Map<String, ? extends Session> sessions = this.sessionRepository
.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
username);
return new SessionsReport(sessions);
}
@ReadOperation
public SessionDescriptor getSession(@Selector String sessionId) {
Session session = this.sessionRepository.findById(sessionId);
return new SessionDescriptor(session);
}
@DeleteOperation
public void deleteSession(@Selector String sessionId) {
this.sessionRepository.deleteById(sessionId);
}
/**
* A report of user's {@link Session sessions}. Primarily intended for serialization
* to JSON.
*/
public static final class SessionsReport {
private final List<SessionDescriptor> sessions;
public SessionsReport(Map<String, ? extends Session> sessions) {
this.sessions = sessions.entrySet().stream()
.map(s -> new SessionDescriptor(s.getValue()))
.collect(Collectors.toList());
}
public List<SessionDescriptor> getSessions() {
return this.sessions;
}
}
/**
* A description of user's {@link Session session}. Primarily intended for
* serialization to JSON.
*/
public static final class SessionDescriptor {
private final String id;
private final Set<String> attributeNames;
private final long creationTime;
private final long 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().toEpochMilli();
this.lastAccessedTime = session.getLastAccessedTime().toEpochMilli();
this.maxInactiveInterval = session.getMaxInactiveInterval().getSeconds();
this.expired = session.isExpired();
}
public String getId() {
return this.id;
}
public Set<String> getAttributeNames() {
return this.attributeNames;
}
public long getCreationTime() {
return this.creationTime;
}
public long getLastAccessedTime() {
return this.lastAccessedTime;
}
public long getMaxInactiveInterval() {
return this.maxInactiveInterval;
}
public boolean isExpired() {
return this.expired;
}
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2012-2017 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
*
* http://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.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointExtension;
import org.springframework.boot.actuate.session.SessionsEndpoint.SessionsReport;
/**
* {@link WebEndpointExtension} for the {@link SessionsEndpoint}.
*
* @author Vedran Pavic
* @since 2.0.0
*/
@WebEndpointExtension(endpoint = SessionsEndpoint.class)
public class SessionsWebEndpointExtension {
private final SessionsEndpoint delegate;
public SessionsWebEndpointExtension(SessionsEndpoint delegate) {
this.delegate = delegate;
}
@ReadOperation
public WebEndpointResponse<SessionsReport> sessionsForUsername(String username) {
if (username == null) {
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST);
}
SessionsReport sessions = this.delegate.sessionsForUsername(username);
return new WebEndpointResponse<>(sessions);
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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
*
* http://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.
*/
/**
* Actuator support for Spring Session.
*/
package org.springframework.boot.actuate.session;

View File

@ -0,0 +1,90 @@
/*
* Copyright 2012-2017 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
*
* http://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 java.util.List;
import org.junit.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 static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link SessionsEndpoint}.
*
* @author Vedran Pavic
*/
public class SessionsEndpointTests {
private static final Session session = new MapSession();
private final FindByIndexNameSessionRepository repository = mock(
FindByIndexNameSessionRepository.class);
@SuppressWarnings("unchecked")
private final SessionsEndpoint endpoint = new SessionsEndpoint(this.repository);
@Test
public void sessionsForUsername() {
given(this.repository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "user"))
.willReturn(Collections.singletonMap(session.getId(), session));
List<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());
assertThat(result.get(0).getCreationTime())
.isEqualTo(session.getCreationTime().toEpochMilli());
assertThat(result.get(0).getLastAccessedTime())
.isEqualTo(session.getLastAccessedTime().toEpochMilli());
assertThat(result.get(0).getMaxInactiveInterval())
.isEqualTo(session.getMaxInactiveInterval().getSeconds());
assertThat(result.get(0).isExpired()).isEqualTo(session.isExpired());
}
@Test
public void getSession() {
given(this.repository.findById(session.getId())).willReturn(session);
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().toEpochMilli());
assertThat(result.getLastAccessedTime())
.isEqualTo(session.getLastAccessedTime().toEpochMilli());
assertThat(result.getMaxInactiveInterval())
.isEqualTo(session.getMaxInactiveInterval().getSeconds());
assertThat(result.isExpired()).isEqualTo(session.isExpired());
}
@Test
public void deleteSession() {
this.endpoint.deleteSession(session.getId());
verify(this.repository).deleteById(session.getId());
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointRunners;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
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 SessionsEndpoint} exposed by Jersey, Spring MVC, and
* WebFlux.
*
* @author Vedran Pavic
*/
@RunWith(WebEndpointRunners.class)
public class SessionsEndpointWebIntegrationTests {
private static final Session session = new MapSession();
private static final FindByIndexNameSessionRepository repository = mock(
FindByIndexNameSessionRepository.class);
private static WebTestClient client;
@Test
public void sessionsForUsernameWithoutUsernameParam() throws Exception {
client.get().uri((builder) -> builder.path("/application/sessions").build())
.exchange().expectStatus().isBadRequest();
}
@Test
public void sessionsForUsernameNoResults() throws Exception {
given(repository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "user"))
.willReturn(Collections.emptyMap());
client.get()
.uri((builder) -> builder.path("/application/sessions")
.queryParam("username", "user").build())
.exchange().expectStatus().isOk().expectBody().jsonPath("sessions")
.isEmpty();
}
@Test
public void sessionsForUsernameFound() throws Exception {
given(repository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, "user"))
.willReturn(Collections.singletonMap(session.getId(), session));
client.get()
.uri((builder) -> builder.path("/application/sessions")
.queryParam("username", "user").build())
.exchange().expectStatus().isOk().expectBody().jsonPath("sessions.[*].id")
.isEqualTo(new JSONArray().appendElement(session.getId()));
}
@Configuration
protected static class TestConfiguration {
@Bean
@SuppressWarnings("unchecked")
public SessionsEndpoint sessionsEndpoint() {
return new SessionsEndpoint(repository);
}
@Bean
public SessionsWebEndpointExtension sessionsWebEndpointExtension(
SessionsEndpoint delegate) {
return new SessionsWebEndpointExtension(delegate);
}
}
}

View File

@ -1168,6 +1168,12 @@ content into your application; rather pick only the properties that you need.
endpoints.prometheus.enabled= # Enable the metrics endpoint.
endpoints.prometheus.web.enabled= # Expose the metrics endpoint as a Web endpoint.
# SESSIONS ENDPOINT ({sc-spring-boot-actuator}/session/SessionsEndpoint.{sc-ext}[SessionsEndpoint])
endpoints.sessions.cache.time-to-live=0 # Maximum time in milliseconds that a response can be cached.
endpoints.sessions.enabled= # Enable the sessions endpoint.
endpoints.sessions.jmx.enabled= # Expose the sessions endpoint as a JMX MBean.
endpoints.sessions.web.enabled= # Expose the sessions endpoint as a Web endpoint.
# SHUTDOWN ENDPOINT ({sc-spring-boot-actuator}/context/ShutdownEndpoint.{sc-ext}[ShutdownEndpoint])
endpoints.shutdown.cache.time-to-live=0 # Maximum time in milliseconds that a response can be cached.
endpoints.shutdown.enabled=false # Enable the shutdown endpoint.

View File

@ -106,6 +106,10 @@ The following technology agnostic endpoints are available:
|`mappings`
|Displays a collated list of all `@RequestMapping` paths.
|`sessions`
|Allows retrieval and deletion of user's sessions from Spring Session backed session
store.
|`shutdown`
|Allows the application to be gracefully shutdown (not enabled by default).

View File

@ -24,6 +24,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -16,8 +16,6 @@
package sample.session;
import java.util.UUID;
import javax.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.GetMapping;
@ -28,12 +26,7 @@ public class HelloRestController {
@GetMapping("/")
String uid(HttpSession session) {
UUID uid = (UUID) session.getAttribute("uid");
if (uid == null) {
uid = UUID.randomUUID();
}
session.setAttribute("uid", uid);
return uid.toString();
return session.getId();
}
}

View File

@ -18,10 +18,22 @@ package sample.session;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@SpringBootApplication
public class SampleSessionApplication {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(
User.withUsername("user").password("password").roles("USER").build());
return manager;
}
public static void main(String[] args) throws Exception {
SpringApplication.run(SampleSessionApplication.class);
}

View File

@ -1 +1 @@
server.session.timeout=5
endpoints.sessions.web.enabled=true

View File

@ -17,6 +17,7 @@
package sample.session;
import java.net.URI;
import java.util.Base64;
import org.junit.Test;
@ -35,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link SampleSessionApplication}.
*
* @author Andy Wilkinson
* @author Vedran Pavic
*/
public class SampleSessionApplicationTests {
@ -42,29 +44,33 @@ public class SampleSessionApplicationTests {
public void sessionExpiry() throws Exception {
ConfigurableApplicationContext context = new SpringApplicationBuilder()
.sources(SampleSessionApplication.class)
.properties("server.port:0")
.initializers(new ServerPortInfoApplicationContextInitializer())
.run();
.properties("server.port:0", "server.session.timeout:1")
.initializers(new ServerPortInfoApplicationContextInitializer()).run();
String port = context.getEnvironment().getProperty("local.server.port");
URI uri = URI.create("http://localhost:" + port + "/");
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class);
String uuid1 = response.getBody();
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("Authorization", "Basic "
+ Base64.getEncoder().encodeToString("user:password".getBytes()));
ResponseEntity<String> response = restTemplate.exchange(
new RequestEntity<>(requestHeaders, HttpMethod.GET, uri), String.class);
String sessionId1 = response.getBody();
requestHeaders.clear();
requestHeaders.set("Cookie", response.getHeaders().getFirst("Set-Cookie"));
RequestEntity<Void> request = new RequestEntity<>(requestHeaders, HttpMethod.GET,
uri);
String uuid2 = restTemplate.exchange(request, String.class).getBody();
assertThat(uuid1).isEqualTo(uuid2);
String sessionId2 = restTemplate.exchange(request, String.class).getBody();
assertThat(sessionId1).isEqualTo(sessionId2);
Thread.sleep(5000);
Thread.sleep(1000);
String uuid3 = restTemplate.exchange(request, String.class).getBody();
assertThat(uuid2).isNotEqualTo(uuid3);
String loginPage = restTemplate.exchange(request, String.class).getBody();
assertThat(loginPage).containsIgnoringCase("login");
}
}