소스 검색

Add ReactorContextTestExecutionListener

Fixes gh-4502
Rob Winch 8 년 전
부모
커밋
7ae4506a88

+ 1 - 0
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'

+ 3 - 1
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 {
 }

+ 74 - 0
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);
+	}
+}

+ 113 - 0
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<T> implements CoreSubscriber<T> {
+			private final CoreSubscriber<T> delegate;
+
+			SecuritySubContext(CoreSubscriber<T> 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;
+	}
+}

+ 3 - 1
test/src/main/resources/META-INF/spring.factories

@@ -1 +1,3 @@
-org.springframework.test.context.TestExecutionListener = org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener
+org.springframework.test.context.TestExecutionListener = \
+	org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener,\
+	org.springframework.security.test.context.support.ReactorContextTestExecutionListener

+ 20 - 2
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");
 	}
-}
+
+
+	@WithMockUser
+	@Test
+	public void reactorContextTestSecurityContextHolderExecutionListenerTestIsRegistered() {
+		Mono<String> name = Mono.currentContext()
+			.flatMap( context -> context.<Mono<Authentication>>get(Authentication.class))
+			.map(Principal::getName);
+
+		StepVerifier.create(name)
+			.expectNext("user")
+			.verifyComplete();
+	}
+}

+ 119 - 0
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> authentication = Mono.currentContext()
+			.flatMap( context -> context.<Mono<Authentication>>get(Authentication.class));
+
+		StepVerifier.create(authentication)
+			.expectNext(expected)
+			.verifyComplete();
+	}
+}