Browse Source

Allow WithSecurityContextTestExecutionListener to execute after @Before

Fixes: gh-2935
Rob Winch 7 years ago
parent
commit
abae2f3e87

+ 38 - 0
docs/manual/src/docs/asciidoc/_includes/test.adoc

@@ -144,6 +144,15 @@ For example, the following would run every test with a user with the username "a
 public class WithMockUserTests {
 ----
 
+By default the `SecurityContext` is set during the `TestExecutionListener.beforeTestMethod` event.
+This is the equivalent of happening before JUnit's `@Before`.
+You can change this to happen during the `TestExecutionListener.beforeTestExecution` event which is after JUnit's `@Before` but before the test method is invoked.
+
+[source,java]
+----
+@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
+----
+
 
 [[test-method-withanonymoususer]]
 === @WithAnonymousUser
@@ -174,6 +183,15 @@ public class WithUserClassLevelAuthenticationTests {
 }
 ----
 
+By default the `SecurityContext` is set during the `TestExecutionListener.beforeTestMethod` event.
+This is the equivalent of happening before JUnit's `@Before`.
+You can change this to happen during the `TestExecutionListener.beforeTestExecution` event which is after JUnit's `@Before` but before the test method is invoked.
+
+[source,java]
+----
+@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
+----
+
 
 [[test-method-withuserdetails]]
 === @WithUserDetails
@@ -227,6 +245,16 @@ public void getMessageWithUserDetailsServiceBeanName() {
 Like `@WithMockUser` we can also place our annotation at the class level so that every test uses the same user.
 However unlike `@WithMockUser`, `@WithUserDetails` requires the user to exist.
 
+By default the `SecurityContext` is set during the `TestExecutionListener.beforeTestMethod` event.
+This is the equivalent of happening before JUnit's `@Before`.
+You can change this to happen during the `TestExecutionListener.beforeTestExecution` event which is after JUnit's `@Before` but before the test method is invoked.
+
+[source,java]
+----
+@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)
+----
+
+
 [[test-method-withsecuritycontext]]
 === @WithSecurityContext
 
@@ -301,6 +329,16 @@ final class WithUserDetailsSecurityContextFactory
 }
 ----
 
+By default the `SecurityContext` is set during the `TestExecutionListener.beforeTestMethod` event.
+This is the equivalent of happening before JUnit's `@Before`.
+You can change this to happen during the `TestExecutionListener.beforeTestExecution` event which is after JUnit's `@Before` but before the test method is invoked.
+
+[source,java]
+----
+@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)
+----
+
+
 [[test-method-meta-annotations]]
 === Test Meta Annotations
 

+ 7 - 19
docs/manual/src/docs/asciidoc/index.adoc

@@ -400,28 +400,16 @@ git clone https://github.com/spring-projects/spring-security.git
 This will give you access to the entire project history (including all releases and branches) on your local machine.
 
 [[new]]
-== What's New in Spring Security 5.0
-
-Spring Security 5.0 provides a number of new features as well as support for Spring Framework 5.
-In total there were 400+ enhancements and bugs resolved.
-You can find the change log at
-https://github.com/spring-projects/spring-security/milestone/90?closed=1[5.0.0.M1]
-https://github.com/spring-projects/spring-security/milestone/97?closed=1[5.0.0.M2]
-https://github.com/spring-projects/spring-security/milestone/100?closed=1[5.0.0.M3]
-https://github.com/spring-projects/spring-security/milestone/101?closed=1[5.0.0.M4]
-https://github.com/spring-projects/spring-security/milestone/102?closed=1[5.0.0.M5]
-https://github.com/spring-projects/spring-security/milestone/103?closed=1[5.0.0.RC1]
-https://github.com/spring-projects/spring-security/milestone/98?closed=1[5.0.0.RELEASE].
-Below are the highlights of this milestone release.
+== What's New in Spring Security 5.1
+
+Spring Security 5.1 provides a number of new features.
+Below are the highlights of the release.
 
 === New Features
 
-* <<jc-oauth2login,OAuth 2.0 Login>>
-* Reactive Support
-** <<jc-webflux,@EnableWebFluxSecurity>>
-** <<jc-erms,@EnableReactiveMethodSecurity>>
-** <<test-webflux,WebFlux Testing Support>>
-* Modernized <<core-services-password-encoding,Password Encoding>>
+* <<test-method>> - Support for customizing when the `SecurityContext` is setup in the test.
+For example, `@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)` will setup a user after JUnit's `@Before` and before the test executes.
+
 
 [[samples]]
 == Samples and Guides (Start Here)

+ 38 - 0
test/src/main/java/org/springframework/security/test/context/support/TestExecutionEvent.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2002-2018 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.security.test.context.support;
+
+import org.springframework.test.context.TestContext;
+
+/**
+ * Represents the events on the methods of {@link org.springframework.test.context.TestExecutionListener}
+ *
+ * @author Rob Winch
+ * @since 5.1
+ */
+public enum TestExecutionEvent {
+	/**
+	 * Associated to {@link org.springframework.test.context.TestExecutionListener#beforeTestMethod(TestContext)}
+	 * event.
+	 */
+	TEST_METHOD,
+	/**
+	 * Associated to {@link org.springframework.test.context.TestExecutionListener#beforeTestExecution(TestContext)}
+	 * event.
+	 */
+	TEST_EXECUTION
+}

+ 11 - 0
test/src/main/java/org/springframework/security/test/context/support/WithAnonymousUser.java

@@ -22,8 +22,10 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+import org.springframework.core.annotation.AliasFor;
 import org.springframework.security.authentication.AnonymousAuthenticationToken;
 import org.springframework.security.core.context.SecurityContext;
+import org.springframework.test.context.TestContext;
 
 /**
  * When used with {@link WithSecurityContextTestExecutionListener} this
@@ -58,4 +60,13 @@ import org.springframework.security.core.context.SecurityContext;
 @WithSecurityContext(factory = WithAnonymousUserSecurityContextFactory.class)
 public @interface WithAnonymousUser {
 
+	/**
+	 * Determines when the {@link SecurityContext} is setup. The default is before
+	 * {@link TestExecutionEvent#TEST_METHOD} which occurs during
+	 * {@link org.springframework.test.context.TestExecutionListener#beforeTestMethod(TestContext)}
+	 * @return the {@link TestExecutionEvent} to initialize before
+	 * @since 5.1
+	 */
+	@AliasFor(annotation = WithSecurityContext.class)
+	TestExecutionEvent setupBefore() default TestExecutionEvent.TEST_METHOD;
 }

+ 13 - 1
test/src/main/java/org/springframework/security/test/context/support/WithMockUser.java

@@ -22,10 +22,12 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+import org.springframework.core.annotation.AliasFor;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.context.TestContext;
 import org.springframework.test.web.servlet.MockMvc;
 
 /**
@@ -102,4 +104,14 @@ public @interface WithMockUser {
 	 * @return
 	 */
 	String password() default "password";
-}
+
+	/**
+	 * Determines when the {@link SecurityContext} is setup. The default is before
+	 * {@link TestExecutionEvent#TEST_METHOD} which occurs during
+	 * {@link org.springframework.test.context.TestExecutionListener#beforeTestMethod(TestContext)}
+	 * @return the {@link TestExecutionEvent} to initialize before
+	 * @since 5.1
+	 */
+	@AliasFor(annotation = WithSecurityContext.class)
+	TestExecutionEvent setupBefore() default TestExecutionEvent.TEST_METHOD;
+}

+ 12 - 1
test/src/main/java/org/springframework/security/test/context/support/WithSecurityContext.java

@@ -25,6 +25,7 @@ import java.lang.annotation.Target;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.core.context.SecurityContext;
+import org.springframework.test.context.TestContext;
 
 /**
  * <p>
@@ -61,4 +62,14 @@ public @interface WithSecurityContext {
 	 * @return
 	 */
 	Class<? extends WithSecurityContextFactory<? extends Annotation>> factory();
-}
+
+	/**
+	 * Determines when the {@link SecurityContext} is setup. The default is before
+	 * {@link TestExecutionEvent#TEST_METHOD} which occurs during
+	 * {@link org.springframework.test.context.TestExecutionListener#beforeTestMethod(TestContext)}
+	 * @return the {@link TestExecutionEvent} to initialize before
+	 * @since 5.1
+	 */
+	TestExecutionEvent setupBefore() default TestExecutionEvent.TEST_METHOD;
+
+}

+ 55 - 12
test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java

@@ -20,6 +20,7 @@ import java.lang.reflect.AnnotatedElement;
 
 import org.springframework.beans.BeanUtils;
 import org.springframework.core.GenericTypeResolver;
+import org.springframework.core.annotation.AnnotatedElementUtils;
 import org.springframework.core.annotation.AnnotationUtils;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -46,6 +47,8 @@ import org.springframework.test.web.servlet.MockMvc;
 public class WithSecurityContextTestExecutionListener
 		extends AbstractTestExecutionListener {
 
+	static final String SECURITY_CONTEXT_ATTR_NAME = WithSecurityContextTestExecutionListener.class.getName().concat(".SECURITY_CONTEXT");
+
 	/**
 	 * Sets up the {@link SecurityContext} for each test method. First the specific method
 	 * is inspected for a {@link WithSecurityContext} or {@link Annotation} that has
@@ -54,46 +57,68 @@ public class WithSecurityContextTestExecutionListener
 	 */
 	@Override
 	public void beforeTestMethod(TestContext testContext) throws Exception {
-		SecurityContext securityContext = createSecurityContext(
+		TestSecurityContext testSecurityContext = createTestSecurityContext(
 				testContext.getTestMethod(), testContext);
-		if (securityContext == null) {
-			securityContext = createSecurityContext(testContext.getTestClass(),
+		if (testSecurityContext == null) {
+			testSecurityContext = createTestSecurityContext(testContext.getTestClass(),
 					testContext);
 		}
-		if (securityContext != null) {
+		if (testSecurityContext == null) {
+			return;
+		}
+
+		SecurityContext securityContext = testSecurityContext.securityContext;
+		if (testSecurityContext.getTestExecutionEvent() == TestExecutionEvent.TEST_METHOD) {
+			TestSecurityContextHolder.setContext(securityContext);
+		} else {
+			testContext.setAttribute(SECURITY_CONTEXT_ATTR_NAME, securityContext);
+		}
+	}
+
+	/**
+	 * If configured before test execution sets the SecurityContext
+	 * @since 5.1
+	 */
+	@Override
+	public void beforeTestExecution(TestContext testContext) {
+		SecurityContext securityContext = (SecurityContext) testContext.removeAttribute(SECURITY_CONTEXT_ATTR_NAME);
+		if(securityContext != null) {
 			TestSecurityContextHolder.setContext(securityContext);
 		}
 	}
 
-	private SecurityContext createSecurityContext(AnnotatedElement annotated,
+	private TestSecurityContext createTestSecurityContext(AnnotatedElement annotated,
 			TestContext context) {
-		WithSecurityContext withSecurityContext = AnnotationUtils
-				.findAnnotation(annotated, WithSecurityContext.class);
-		return createSecurityContext(annotated, withSecurityContext, context);
+		WithSecurityContext withSecurityContext = AnnotatedElementUtils
+				.findMergedAnnotation(annotated, WithSecurityContext.class);
+		return createTestSecurityContext(annotated, withSecurityContext, context);
 	}
 
-	private SecurityContext createSecurityContext(Class<?> annotated,
+	private TestSecurityContext createTestSecurityContext(Class<?> annotated,
 			TestContext context) {
 		MetaAnnotationUtils.AnnotationDescriptor<WithSecurityContext> withSecurityContextDescriptor = MetaAnnotationUtils
 				.findAnnotationDescriptor(annotated, WithSecurityContext.class);
 		WithSecurityContext withSecurityContext = withSecurityContextDescriptor == null
 				? null : withSecurityContextDescriptor.getAnnotation();
-		return createSecurityContext(annotated, withSecurityContext, context);
+		return createTestSecurityContext(annotated, withSecurityContext, context);
 	}
 
 	@SuppressWarnings({ "rawtypes", "unchecked" })
-	private SecurityContext createSecurityContext(AnnotatedElement annotated,
+	private TestSecurityContext createTestSecurityContext(AnnotatedElement annotated,
 			WithSecurityContext withSecurityContext, TestContext context) {
 		if (withSecurityContext == null) {
 			return null;
 		}
+		withSecurityContext = AnnotationUtils
+			.synthesizeAnnotation(withSecurityContext, annotated);
 		WithSecurityContextFactory factory = createFactory(withSecurityContext, context);
 		Class<? extends Annotation> type = (Class<? extends Annotation>) GenericTypeResolver
 				.resolveTypeArgument(factory.getClass(),
 						WithSecurityContextFactory.class);
 		Annotation annotation = findAnnotation(annotated, type);
+		TestExecutionEvent initialize = withSecurityContext.setupBefore();
 		try {
-			return factory.createSecurityContext(annotation);
+			return new TestSecurityContext(factory.createSecurityContext(annotation), initialize);
 		}
 		catch (RuntimeException e) {
 			throw new IllegalStateException(
@@ -150,4 +175,22 @@ public class WithSecurityContextTestExecutionListener
 	public int getOrder() {
 		return 10000;
 	}
+
+	static class TestSecurityContext {
+		private final SecurityContext securityContext;
+		private final TestExecutionEvent testExecutionEvent;
+
+		TestSecurityContext(SecurityContext securityContext, TestExecutionEvent testExecutionEvent) {
+			this.securityContext = securityContext;
+			this.testExecutionEvent = testExecutionEvent;
+		}
+
+		public SecurityContext getSecurityContext() {
+			return this.securityContext;
+		}
+
+		public TestExecutionEvent getTestExecutionEvent() {
+			return this.testExecutionEvent;
+		}
+	}
 }

+ 13 - 1
test/src/main/java/org/springframework/security/test/context/support/WithUserDetails.java

@@ -22,11 +22,13 @@ import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+import org.springframework.core.annotation.AliasFor;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.test.context.TestContext;
 import org.springframework.test.web.servlet.MockMvc;
 
 /**
@@ -69,4 +71,14 @@ public @interface WithUserDetails {
 	 * @since 4.1
 	 */
 	String userDetailsServiceBeanName() default "";
-}
+
+	/**
+	 * Determines when the {@link SecurityContext} is setup. The default is before
+	 * {@link TestExecutionEvent#TEST_METHOD} which occurs during
+	 * {@link org.springframework.test.context.TestExecutionListener#beforeTestMethod(TestContext)}
+	 * @return the {@link TestExecutionEvent} to initialize before
+	 * @since 5.1
+	 */
+	@AliasFor(annotation = WithSecurityContext.class)
+	TestExecutionEvent setupBefore() default TestExecutionEvent.TEST_METHOD;
+}

+ 65 - 0
test/src/test/java/org/springframework/security/test/context/support/WithAnonymousUserTests.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2018 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.security.test.context.support;
+
+import org.junit.Test;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class WithAnonymousUserTests {
+	@Test
+	public void defaults() {
+		WithSecurityContext context = AnnotatedElementUtils.findMergedAnnotation(Annotated.class,
+			WithSecurityContext.class);
+
+		assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_METHOD);
+	}
+
+	@WithAnonymousUser
+	private class Annotated {
+	}
+
+	@Test
+	public void findMergedAnnotationWhenSetupExplicitThenOverridden() {
+		WithSecurityContext context = AnnotatedElementUtils
+			.findMergedAnnotation(SetupExplicit.class,
+				WithSecurityContext.class);
+
+		assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_METHOD);
+	}
+
+	@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_METHOD)
+	private class SetupExplicit {
+	}
+
+	@Test
+	public void findMergedAnnotationWhenSetupOverriddenThenOverridden() {
+		WithSecurityContext context = AnnotatedElementUtils.findMergedAnnotation(SetupOverridden.class,
+			WithSecurityContext.class);
+
+		assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_EXECUTION);
+	}
+
+	@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
+	private class SetupOverridden {
+	}
+}

+ 36 - 4
test/src/test/java/org/springframework/security/test/context/support/WithMockUserTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2018 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.
@@ -13,26 +13,58 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.springframework.security.test.context.support;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
 import org.junit.Test;
-import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.AnnotatedElementUtils;
 
 public class WithMockUserTests {
 
 	@Test
 	public void defaults() {
-		WithMockUser mockUser = AnnotationUtils.findAnnotation(Annotated.class,
+		WithMockUser mockUser = AnnotatedElementUtils.findMergedAnnotation(Annotated.class,
 				WithMockUser.class);
 		assertThat(mockUser.value()).isEqualTo("user");
 		assertThat(mockUser.username()).isEmpty();
 		assertThat(mockUser.password()).isEqualTo("password");
 		assertThat(mockUser.roles()).containsOnly("USER");
+		assertThat(mockUser.setupBefore()).isEqualByComparingTo(TestExecutionEvent.TEST_METHOD);
+
+		WithSecurityContext context = AnnotatedElementUtils.findMergedAnnotation(Annotated.class,
+			WithSecurityContext.class);
+
+		assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_METHOD);
 	}
 
 	@WithMockUser
 	private class Annotated {
 	}
-}
+
+	@Test
+	public void findMergedAnnotationWhenSetupExplicitThenOverridden() {
+		WithSecurityContext context = AnnotatedElementUtils
+			.findMergedAnnotation(SetupExplicit.class,
+				WithSecurityContext.class);
+
+		assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_METHOD);
+	}
+
+	@WithMockUser(setupBefore = TestExecutionEvent.TEST_METHOD)
+	private class SetupExplicit {
+	}
+
+	@Test
+	public void findMergedAnnotationWhenSetupOverriddenThenOverridden() {
+		WithSecurityContext context = AnnotatedElementUtils.findMergedAnnotation(SetupOverridden.class,
+			WithSecurityContext.class);
+
+		assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_EXECUTION);
+	}
+
+	@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
+	private class SetupOverridden {
+	}
+}

+ 144 - 0
test/src/test/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListenerTests.java

@@ -0,0 +1,144 @@
+/*
+ * Copyright 2002-2018 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.security.test.context.support;
+
+import org.junit.After;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.test.context.TestSecurityContextHolder;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.junit4.rules.SpringClassRule;
+import org.springframework.test.context.junit4.rules.SpringMethodRule;
+
+import java.lang.reflect.Method;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+@RunWith(MockitoJUnitRunner.class)
+@ContextConfiguration(classes = WithSecurityContextTestExecutionListenerTests.NoOpConfiguration.class)
+public class WithSecurityContextTestExecutionListenerTests {
+	@ClassRule
+	public static final SpringClassRule spring = new SpringClassRule();
+	@Rule
+	public final SpringMethodRule springMethod = new SpringMethodRule();
+
+	@Autowired
+	private ApplicationContext applicationContext;
+
+	@Mock
+	private TestContext testContext;
+
+	private WithSecurityContextTestExecutionListener listener = new WithSecurityContextTestExecutionListener();
+
+	@After
+	public void cleanup() {
+		TestSecurityContextHolder.clearContext();
+	}
+
+	@Test
+	public void beforeTestMethodWhenWithMockUserTestExecutionDefaultThenSecurityContextSet() throws Exception {
+		Method testMethod = TheTest.class.getMethod("withMockUserDefault");
+		when(this.testContext.getApplicationContext()).thenReturn(this.applicationContext);
+		when(this.testContext.getTestMethod()).thenReturn(testMethod);
+
+		this.listener.beforeTestMethod(this.testContext);
+
+		assertThat(TestSecurityContextHolder.getContext().getAuthentication()).isNotNull();
+		verify(this.testContext, never()).setAttribute(eq(WithSecurityContextTestExecutionListener.SECURITY_CONTEXT_ATTR_NAME), any(SecurityContext.class));
+	}
+
+	@Test
+	public void beforeTestMethodWhenWithMockUserTestMethodThenSecurityContextSet() throws Exception {
+		Method testMethod = TheTest.class.getMethod("withMockUserTestMethod");
+		when(this.testContext.getApplicationContext()).thenReturn(this.applicationContext);
+		when(this.testContext.getTestMethod()).thenReturn(testMethod);
+
+		this.listener.beforeTestMethod(this.testContext);
+
+		assertThat(TestSecurityContextHolder.getContext().getAuthentication()).isNotNull();
+		verify(this.testContext, never()).setAttribute(eq(WithSecurityContextTestExecutionListener.SECURITY_CONTEXT_ATTR_NAME), any(SecurityContext.class));
+	}
+
+	@Test
+	public void beforeTestMethodWhenWithMockUserTestExecutionThenTestContextSet() throws Exception {
+		Method testMethod = TheTest.class.getMethod("withMockUserTestExecution");
+		when(this.testContext.getApplicationContext()).thenReturn(this.applicationContext);
+		when(this.testContext.getTestMethod()).thenReturn(testMethod);
+
+		this.listener.beforeTestMethod(this.testContext);
+
+		assertThat(TestSecurityContextHolder.getContext().getAuthentication()).isNull();
+		verify(this.testContext).setAttribute(eq(WithSecurityContextTestExecutionListener.SECURITY_CONTEXT_ATTR_NAME), any(SecurityContext.class));
+	}
+
+	@Test
+	public void beforeTestExecutionWhenTestContextNullThenSecurityContextNotSet() throws Exception {
+		this.listener.beforeTestExecution(this.testContext);
+
+		assertThat(TestSecurityContextHolder.getContext().getAuthentication()).isNull();
+	}
+
+	@Test
+	public void beforeTestExecutionWhenTestContextNotNullThenSecurityContextSet() throws Exception {
+		SecurityContextImpl securityContext = new SecurityContextImpl();
+		securityContext.setAuthentication(new TestingAuthenticationToken("user", "passsword", "ROLE_USER"));
+		when(this.testContext.removeAttribute(WithSecurityContextTestExecutionListener.SECURITY_CONTEXT_ATTR_NAME)).thenReturn(securityContext);
+
+		this.listener.beforeTestExecution(this.testContext);
+
+		assertThat(TestSecurityContextHolder.getContext().getAuthentication()).isEqualTo(securityContext.getAuthentication());
+	}
+
+	@Configuration
+	static class NoOpConfiguration {}
+
+	static class TheTest {
+		@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
+		public void withMockUserTestExecution() {
+		}
+
+		@WithMockUser(setupBefore = TestExecutionEvent.TEST_METHOD)
+		public void withMockUserTestMethod() {
+		}
+
+		@WithMockUser
+		public void withMockUserDefault() {
+		}
+	}
+
+
+}

+ 34 - 1
test/src/test/java/org/springframework/security/test/context/support/WithUserDetailsTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2018 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.
@@ -18,6 +18,7 @@ package org.springframework.security.test.context.support;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import org.junit.Test;
+import org.springframework.core.annotation.AnnotatedElementUtils;
 import org.springframework.core.annotation.AnnotationUtils;
 
 public class WithUserDetailsTests {
@@ -27,9 +28,41 @@ public class WithUserDetailsTests {
 		WithUserDetails userDetails = AnnotationUtils.findAnnotation(Annotated.class,
 				WithUserDetails.class);
 		assertThat(userDetails.value()).isEqualTo("user");
+
+		WithSecurityContext context = AnnotatedElementUtils
+			.findMergedAnnotation(Annotated.class,
+				WithSecurityContext.class);
+
+		assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_METHOD);
 	}
 
 	@WithUserDetails
 	private static class Annotated {
 	}
+
+	@Test
+	public void findMergedAnnotationWhenSetupExplicitThenOverridden() {
+		WithSecurityContext context = AnnotatedElementUtils
+			.findMergedAnnotation(SetupExplicit.class,
+				WithSecurityContext.class);
+
+		assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_METHOD);
+	}
+
+	@WithUserDetails(setupBefore = TestExecutionEvent.TEST_METHOD)
+	private class SetupExplicit {
+	}
+
+	@Test
+	public void findMergedAnnotationWhenSetupOverriddenThenOverridden() {
+		WithSecurityContext context = AnnotatedElementUtils
+			.findMergedAnnotation(SetupOverridden.class,
+				WithSecurityContext.class);
+
+		assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_EXECUTION);
+	}
+
+	@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)
+	private class SetupOverridden {
+	}
 }