diff --git a/test/spring-security-test.gradle b/test/spring-security-test.gradle index 02fac497a9..f5fae54ef0 100644 --- a/test/spring-security-test.gradle +++ b/test/spring-security-test.gradle @@ -13,6 +13,7 @@ dependencies { provided 'javax.servlet:javax.servlet-api' testCompile 'com.fasterxml.jackson.core:jackson-databind' + testCompile 'io.projectreactor:reactor-test' testCompile 'org.skyscreamer:jsonassert' testCompile 'org.springframework:spring-webmvc' testCompile 'org.springframework:spring-tx' diff --git a/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java b/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java index a53678918c..d8cab45f09 100644 --- a/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java +++ b/test/src/main/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListeners.java @@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.context.ApplicationContext; +import org.springframework.security.test.context.support.ReactorContextTestExecutionListener; import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; import org.springframework.test.context.TestExecutionListeners; @@ -40,6 +41,7 @@ import org.springframework.test.context.TestExecutionListeners; @Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -@TestExecutionListeners(inheritListeners = false, listeners = WithSecurityContextTestExecutionListener.class) +@TestExecutionListeners(inheritListeners = false, listeners = {WithSecurityContextTestExecutionListener.class, + ReactorContextTestExecutionListener.class}) public @interface SecurityTestExecutionListeners { } diff --git a/test/src/main/java/org/springframework/security/test/context/support/DelegatingTestExecutionListener.java b/test/src/main/java/org/springframework/security/test/context/support/DelegatingTestExecutionListener.java new file mode 100644 index 0000000000..da2de00cf3 --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/context/support/DelegatingTestExecutionListener.java @@ -0,0 +1,74 @@ +/* + * + * * Copyright 2002-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.security.test.context.support; + +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.util.Assert; + +/** + * @author Rob Winch + * @since 5.0 + */ +class DelegatingTestExecutionListener + extends AbstractTestExecutionListener { + + private final TestExecutionListener delegate; + + public DelegatingTestExecutionListener(TestExecutionListener delegate) { + Assert.notNull(delegate, "delegate cannot be null"); + this.delegate = delegate; + } + + @Override + public void beforeTestClass(TestContext testContext) throws Exception { + delegate.beforeTestClass(testContext); + } + + @Override + public void prepareTestInstance(TestContext testContext) throws Exception { + delegate.prepareTestInstance(testContext); + } + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + delegate.beforeTestMethod(testContext); + } + + @Override + public void beforeTestExecution(TestContext testContext) throws Exception { + delegate.beforeTestExecution(testContext); + } + + @Override + public void afterTestExecution(TestContext testContext) throws Exception { + delegate.afterTestExecution(testContext); + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + delegate.afterTestMethod(testContext); + } + + @Override + public void afterTestClass(TestContext testContext) throws Exception { + delegate.afterTestClass(testContext); + } +} diff --git a/test/src/main/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListener.java b/test/src/main/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListener.java new file mode 100644 index 0000000000..389e2083a2 --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListener.java @@ -0,0 +1,113 @@ +/* + * + * * Copyright 2002-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.security.test.context.support; + +import org.reactivestreams.Subscription; +import org.springframework.security.core.Authentication; +import org.springframework.security.test.context.TestSecurityContextHolder; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.util.ClassUtils; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.util.context.Context; + +/** + * Sets up the Reactor Context with the Authentication from the TestSecurityContextHolder + * and then clears the Reactor Context at the end of the tests. + * + * @author Rob Winch + * @since 5.0 + */ +public class ReactorContextTestExecutionListener + extends DelegatingTestExecutionListener { + + private static final String HOOKS_CLASS_NAME = "reactor.core.publisher.Hooks"; + + public ReactorContextTestExecutionListener() { + super(createDelegate()); + } + + private static TestExecutionListener createDelegate() { + return ClassUtils.isPresent(HOOKS_CLASS_NAME, ReactorContextTestExecutionListener.class.getClassLoader()) ? + new DelegateTestExecutionListener() : + new AbstractTestExecutionListener() {}; + } + + private static class DelegateTestExecutionListener extends AbstractTestExecutionListener { + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + Hooks.onLastOperator(Operators.lift((s, sub) -> new SecuritySubContext<>(sub))); + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + Hooks.resetOnLastOperator(); + } + + private static class SecuritySubContext implements CoreSubscriber { + private final CoreSubscriber delegate; + + SecuritySubContext(CoreSubscriber delegate) { + this.delegate = delegate; + } + + @Override + public Context currentContext() { + Context context = delegate.currentContext(); + Authentication authentication = TestSecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + return context; + } + return context.put(Authentication.class, Mono.just(authentication)); + } + + @Override + public void onSubscribe(Subscription s) { + delegate.onSubscribe(s); + } + + @Override + public void onNext(T t) { + delegate.onNext(t); + } + + @Override + public void onError(Throwable t) { + delegate.onError(t); + } + + @Override + public void onComplete() { + delegate.onComplete(); + } + } + } + + /** + * Returns {@code 11000}. + */ + @Override + public int getOrder() { + return 11000; + } +} diff --git a/test/src/main/resources/META-INF/spring.factories b/test/src/main/resources/META-INF/spring.factories index 4298016895..48be9c5476 100644 --- a/test/src/main/resources/META-INF/spring.factories +++ b/test/src/main/resources/META-INF/spring.factories @@ -1 +1,3 @@ -org.springframework.test.context.TestExecutionListener = org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener \ No newline at end of file +org.springframework.test.context.TestExecutionListener = \ + org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener,\ + org.springframework.security.test.context.support.ReactorContextTestExecutionListener diff --git a/test/src/test/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListenerTests.java b/test/src/test/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListenerTests.java index fd956fa698..2164ce396d 100644 --- a/test/src/test/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListenerTests.java +++ b/test/src/test/java/org/springframework/security/test/context/annotation/SecurityTestExecutionListenerTests.java @@ -19,9 +19,14 @@ import static org.assertj.core.api.Assertions.assertThat; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.security.Principal; @RunWith(SpringJUnit4ClassRunner.class) @SecurityTestExecutionListeners @@ -29,7 +34,20 @@ public class SecurityTestExecutionListenerTests { @WithMockUser @Test - public void registered() { + public void withSecurityContextTestExecutionListenerIsRegistered() { assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("user"); } -} \ No newline at end of file + + + @WithMockUser + @Test + public void reactorContextTestSecurityContextHolderExecutionListenerTestIsRegistered() { + Mono name = Mono.currentContext() + .flatMap( context -> context.>get(Authentication.class)) + .map(Principal::getName); + + StepVerifier.create(name) + .expectNext("user") + .verifyComplete(); + } +} diff --git a/test/src/test/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListenerTests.java b/test/src/test/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListenerTests.java new file mode 100644 index 0000000000..83f5a595c0 --- /dev/null +++ b/test/src/test/java/org/springframework/security/test/context/support/ReactorContextTestExecutionListenerTests.java @@ -0,0 +1,119 @@ +/* + * + * * Copyright 2002-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.security.test.context.support; + +/** + * @author Rob Winch + * @since 5.0 + */ + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.OrderComparator; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.test.context.TestSecurityContextHolder; +import org.springframework.test.context.TestContext; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class ReactorContextTestExecutionListenerTests { + + @Mock + private TestContext testContext; + + private ReactorContextTestExecutionListener listener = + new ReactorContextTestExecutionListener(); + + @After + public void cleanup() { + TestSecurityContextHolder.clearContext(); + Hooks.resetOnLastOperator(); + } + + @Test + public void beforeTestMethodWhenSecurityContextEmptyThenReactorContextNull() throws Exception { + listener.beforeTestMethod(testContext); + + assertThat(Mono.currentContext().block()).isNull(); + } + + @Test + public void beforeTestMethodWhenNullAuthenticationThenReactorContextNull() throws Exception { + TestSecurityContextHolder.setContext(new SecurityContextImpl()); + + listener.beforeTestMethod(testContext); + + assertThat(Mono.currentContext().block()).isNull(); + } + + + @Test + public void beforeTestMethodWhenAuthenticationThenReactorContextHasAuthentication() throws Exception { + TestingAuthenticationToken expectedAuthentication = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + SecurityContextImpl context = new SecurityContextImpl(); + context.setAuthentication(expectedAuthentication); + TestSecurityContextHolder.setContext(context); + + listener.beforeTestMethod(testContext); + + assertAuthentication(expectedAuthentication); + } + + @Test + public void afterTestMethodWhenSecurityContextEmptyThenNoError() throws Exception { + listener.beforeTestMethod(testContext); + + listener.afterTestMethod(testContext); + } + + @Test + public void afterTestMethodWhenSetupThenReactorContextNull() throws Exception { + beforeTestMethodWhenAuthenticationThenReactorContextHasAuthentication(); + + listener.afterTestMethod(testContext); + + assertThat(Mono.currentContext().block()).isNull(); + } + + @Test + public void orderWhenComparedToWithSecurityContextTestExecutionListenerIsAfter() { + OrderComparator comparator = new OrderComparator(); + WithSecurityContextTestExecutionListener withSecurity = new WithSecurityContextTestExecutionListener(); + ReactorContextTestExecutionListener reactorContext = new ReactorContextTestExecutionListener(); + assertThat(comparator.compare(withSecurity, reactorContext)).isLessThan(0); + } + + public void assertAuthentication(Authentication expected) { + Mono authentication = Mono.currentContext() + .flatMap( context -> context.>get(Authentication.class)); + + StepVerifier.create(authentication) + .expectNext(expected) + .verifyComplete(); + } +}