Add Smoke Test testing the persistent access of a (HTTP) Session stored and managed by Apache Geode using Mock Objects with Spring Session auto-configured with Spring Boot.

This commit is contained in:
John Blum
2020-11-05 16:10:26 -08:00
parent c1f23c3f9d
commit 58fdd5b2ca
10 changed files with 717 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
# This file is generated by the 'io.freefair.lombok' Gradle plugin
config.stopBubbling = true

View File

@@ -0,0 +1,22 @@
plugins {
id "io.freefair.lombok" version "5.2.1"
}
apply plugin: 'io.spring.convention.spring-test'
description = "Smoke Tests to assert (Spring) Session state caching using Apache Geode with Mock Objects auto-configured by Spring Boot."
dependencies {
implementation "org.assertj:assertj-core"
implementation "org.springframework.boot:spring-boot-starter-web"
implementation project(':spring-geode-starter-session')
implementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation project(":spring-geode-starter-test")
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2020 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.hamcrest;
import java.util.regex.Pattern;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
/**
* A Hamcrest {@link Matcher} using a Java {@link Pattern} with a {@link String Regular Expression}
* to match a {@link String} argument.
*
* @author John Blum
* @see java.util.regex.Pattern
* @see org.hamcrest.BaseMatcher
* @see org.hamcrest.Matcher
* @since 1.4.0
*/
public class RegexMatcher extends BaseMatcher<String> {
private final Pattern pattern;
public static RegexMatcher from(@NonNull String regex) {
return new RegexMatcher(regex);
}
public RegexMatcher(@NonNull String regex) {
Assert.hasText(regex, () -> String.format("Regular Expression [%s] is required", regex));
this.pattern = Pattern.compile(regex);
}
@Override
public void describeTo(Description description) {
description.appendText(String.format("Matches text to the Regular Expression (Pattern) [%s]",
this.pattern.toString()));
}
@Override
public boolean matches(Object actual) {
return this.pattern.matcher(String.valueOf(actual)).matches();
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2020 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.security;
/**
* {@link RuntimeException} implementation indicating a Security Authentication error.
*
* @author John Blum
* @see java.lang.Runtime
* @since 1.4.0
*/
@SuppressWarnings("unused")
public class AuthenticationException extends RuntimeException {
public AuthenticationException() { }
public AuthenticationException(String message) {
super(message);
}
public AuthenticationException(Throwable cause) {
super(cause);
}
public AuthenticationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2020 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.session.web.servlet.http;
import java.time.Duration;
import java.util.Collections;
import java.util.Enumeration;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
import org.springframework.lang.NonNull;
import org.springframework.session.Session;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.http.AbstractHttpSession;
/**
* {@link HttpSession} implementation adapting the Spring Session {@link Session} interface.
*
* @author John Blum
* @see java.time.Duration
* @see javax.servlet.ServletContext
* @see javax.servlet.http.HttpSession
* @see org.springframework.session.Session
* @see org.springframework.web.servlet.http.AbstractHttpSession
* @since 1.4.0
*/
@SuppressWarnings("unused")
public class HttpSessionAdapter extends AbstractHttpSession {
private final ServletContext servletContext;
private final Session session;
public HttpSessionAdapter(@NonNull ServletContext servletContext, @NonNull Session session) {
Assert.notNull(servletContext, "ServletContext must not be null");
Assert.notNull(session, "Session must not be null");
this.session = session;
this.servletContext = servletContext;
}
protected @NonNull Session getSession() {
return this.session;
}
@Override
public String getId() {
return getSession().getId();
}
@Override
public long getCreationTime() {
return getSession().getCreationTime().toEpochMilli();
}
@Override
public long getLastAccessedTime() {
return getSession().getLastAccessedTime().toEpochMilli();
}
@Override
public void setMaxInactiveInterval(int interval) {
getSession().setMaxInactiveInterval(Duration.ofSeconds(interval));
}
@Override
public int getMaxInactiveInterval() {
return Long.valueOf(getSession().getMaxInactiveInterval().getSeconds()).intValue();
}
@Override
public @NonNull ServletContext getServletContext() {
return this.servletContext;
}
@Override
@SuppressWarnings("deprecation")
public HttpSessionContext getSessionContext() {
throw new UnsupportedOperationException("Not Implemented");
}
@Override
public void setAttribute(String name, Object value) {
getSession().setAttribute(name, value);
}
@Override
public void removeAttribute(String name) {
getSession().removeAttribute(name);
}
@Override
public Object getAttribute(String name) {
return getSession().getAttribute(name);
}
@Override
public Enumeration<String> getAttributeNames() {
return Collections.enumeration(getSession().getAttributeNames());
}
@Override
public void invalidate() {
throw new UnsupportedOperationException("Not Implemented");
}
@Override
public boolean isNew() {
return !StringUtils.hasText(getId());
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2020 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.session.web.servlet.http;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.geode.core.util.ObjectUtils;
import org.springframework.lang.Nullable;
import org.springframework.session.SessionRepository;
/**
* Abstract utility class used to work with {@link HttpSession} objects.
*
* @author John Blum
* @see javax.servlet.http.HttpServletRequest
* @see javax.servlet.http.HttpSession
* @since 1.4.0
*/
public abstract class SessionUtils {
protected static final String CURRENT_SESSION_REQUEST_ATTRIBUTE =
SessionRepository.class.getName().concat(".CURRENT_SESSION");
protected static final String GET_CURRENT_SESSION_METHOD_NAME = "getCurrentSession";
public static @Nullable HttpSession resolveSession(HttpServletRequest servletRequest) {
try {
return ObjectUtils.invoke(servletRequest, GET_CURRENT_SESSION_METHOD_NAME);
}
catch (IllegalArgumentException ignore) {
return (HttpSession) servletRequest.getAttribute(CURRENT_SESSION_REQUEST_ATTRIBUTE);
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2020 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.session.web.servlet.http;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.http.HttpSession;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.session.Session;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.web.servlet.http.HttpSessionProxy;
/**
* Spring Test Mock Web MVC framework {@link RequestPostProcessor} that substitutes the Spring Session {@link Session}
* for the {@link MockHttpSession}.
*
* @author John Blum
* @see javax.servlet.http.HttpSession
* @see org.springframework.mock.web.MockHttpServletRequest
* @see org.springframework.mock.web.MockHttpSession
* @see org.springframework.session.Session
* @see org.springframework.test.web.servlet.request.RequestPostProcessor
* @see org.springframework.web.servlet.http.HttpSessionProxy
* @since 1.4.0
*/
public class SpringSessionSubstitutingSpyRequestPostProcessor implements RequestPostProcessor {
private static final AtomicReference<SpringSessionSubstitutingSpyRequestPostProcessor> instance = new AtomicReference<>(null);
public static SpringSessionSubstitutingSpyRequestPostProcessor create() {
return instance.updateAndGet(instance -> instance != null ? instance
: new SpringSessionSubstitutingSpyRequestPostProcessor());
}
/**
* @inheritDoc
*/
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
MockHttpServletRequest requestSpy = spy(request);
doAnswer(invocation -> {
HttpSession session = SessionUtils.resolveSession(request);
return session != null
? HttpSessionProxy.from(session)
: request.getSession();
}).when(requestSpy).getSession();
return requestSpy;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2020 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.web.servlet.http;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
/**
* Abstract base class supporting implementations of the {@link HttpSession} interface.
*
* @author John Blum
* @see javax.servlet.http.HttpSession
* @since 1.4.0
*/
@SuppressWarnings("deprecation")
public abstract class AbstractHttpSession implements HttpSession {
@Override
public HttpSessionContext getSessionContext() {
throw new UnsupportedOperationException("Not Implemented");
}
@Override
public @Nullable Object getValue(String name) {
return getAttribute(name);
}
@Override
public @NonNull String[] getValueNames() {
List<String> valueNames = new ArrayList<>();
Enumeration<String> attributeNames = getAttributeNames();
if (Objects.nonNull(attributeNames)) {
while (attributeNames.hasMoreElements()) {
valueNames.add(attributeNames.nextElement());
}
}
return valueNames.toArray(new String[0]);
}
@Override
public void putValue(String name, Object value) {
setAttribute(name, value);
}
@Override
public void removeValue(String name) {
removeAttribute(name);
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2020 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.web.servlet.http;
import java.util.Enumeration;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@link HttpSession} implementation wrapping and proxying for an existing {@link HttpSession} instance.
*
* @author John Blum
* @see javax.servlet.ServletContext
* @see javax.servlet.http.HttpSession
* @see org.springframework.web.servlet.http.AbstractHttpSession
* @since 1.4.0
*/
public class HttpSessionProxy extends AbstractHttpSession {
public static HttpSessionProxy from(HttpSession session) {
return new HttpSessionProxy(session);
}
private final HttpSession session;
private HttpSessionProxy(@NonNull HttpSession session) {
Assert.notNull(session, "HttpSession must not be null");
this.session = session;
}
protected @NonNull HttpSession getSession() {
return this.session;
}
@Override
public String getId() {
return getSession().getId();
}
@Override
public long getCreationTime() {
return getSession().getCreationTime();
}
@Override
public long getLastAccessedTime() {
return getSession().getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(int interval) {
getSession().setMaxInactiveInterval(interval);
}
@Override
public int getMaxInactiveInterval() {
return getSession().getMaxInactiveInterval();
}
@Override
public @NonNull ServletContext getServletContext() {
return getSession().getServletContext();
}
@Override
@SuppressWarnings("deprecation")
public HttpSessionContext getSessionContext() {
return getSession().getSessionContext();
}
@Override
public void setAttribute(String name, Object value) {
getSession().setAttribute(name, value);
}
@Override
public @Nullable Object getAttribute(String name) {
return getSession().getAttribute(name);
}
@Override
public Enumeration<String> getAttributeNames() {
return getSession().getAttributeNames();
}
@Override
public void removeAttribute(String name) {
getSession().removeAttribute(name);
}
@Override
public void invalidate() {
getSession().invalidate();
}
@Override
public boolean isNew() {
return getSession().isNew();
}
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2020 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 example.app.geode.caching.session;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.servlet.http.HttpSession;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects;
import org.springframework.data.gemfire.tests.support.MapBuilder;
import org.springframework.hamcrest.RegexMatcher;
import org.springframework.http.MediaType;
import org.springframework.security.AuthenticationException;
import org.springframework.session.web.servlet.http.SpringSessionSubstitutingSpyRequestPostProcessor;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* Smoke Tests testing the persistent access of a (HTTP) Session stored and managed by Apache Geode using Mock Objects
* with Spring Session auto-configured with Spring Boot.
*
* @author John Blum
* @see javax.servlet.http.HttpSession
* @see org.junit.Test
* @see org.springframework.boot.autoconfigure.SpringBootApplication
* @see org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
* @see org.springframework.boot.test.context.SpringBootTest
* @see org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects
* @see org.springframework.test.context.junit4.SpringRunner
* @see org.springframework.test.web.servlet.MockMvc
* @see org.springframework.web.bind.annotation.RestController
* @since 1.4.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest(
classes = {
MockPersistentSessionAccessSmokeTests.TestSessionGeodeConfiguration.class,
MockPersistentSessionAccessSmokeTests.TestUserAccessController.class
},
webEnvironment = SpringBootTest.WebEnvironment.MOCK
)
@AutoConfigureMockMvc
@SuppressWarnings("unused")
public class MockPersistentSessionAccessSmokeTests {
@Autowired
private MockMvc mvc;
@Test
public void persistentSessionAccessIsSuccessful() throws Exception {
String username = "jonDoe";
this.mvc.perform(get("/users/{username}", username)
.param("password", "p@5$w0rd")
.with(SpringSessionSubstitutingSpyRequestPostProcessor.create()))
.andExpect(status().isOk())
.andExpect(request().sessionAttribute(username, User.newUser(username)))
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(content().string(RegexMatcher.from("\\{\"id\":\".*\",\"name\":\"jonDoe\"\\}")));
}
@SpringBootApplication
@EnableGemFireMockObjects
static class TestSessionGeodeConfiguration { }
@RestController
static class TestUserAccessController {
static final Map<String, String> userAccessControlMap = MapBuilder.<String, String>newMapBuilder()
.put("jonDoe", "p@5$w0rd")
.put("janeDoe", "s3cr3t!")
.build();
@GetMapping("/users/{username}")
public User login(HttpSession session, @PathVariable String username,
@RequestParam(required = false) String password) {
return Optional.ofNullable(password)
.filter(StringUtils::hasText)
.filter(it -> String.valueOf(userAccessControlMap.get(username)).equals(password))
.map(it -> User.newUser(username).identifiedBy(UUID.randomUUID().toString()))
.map(user -> {
session.setAttribute(user.getName(), user);
return user;
})
.orElseThrow(() -> new AuthenticationException(String.format("User [%s] is not authorized", username)));
}
}
@Getter
@ToString(of = "name")
@EqualsAndHashCode(of = "name")
@RequiredArgsConstructor(staticName = "newUser")
static class User {
private String id;
@NonNull
private final String name;
public User identifiedBy(String id) {
this.id = id;
return this;
}
}
}