Преглед на файлове

Add WebTestClient test support

SecurityExchangeMutators

Fixes gh-4343
Rob Winch преди 8 години
родител
ревизия
7bc98db23c

+ 1 - 0
samples/javaconfig/hellowebflux/spring-security-samples-javaconfig-hellowebflux.gradle

@@ -10,6 +10,7 @@ dependencies {
 	compile 'org.springframework:spring-context'
 	compile 'org.springframework:spring-webflux'
 
+	testCompile project(':spring-security-test')
 	testCompile 'io.projectreactor.addons:reactor-test'
 	testCompile 'org.skyscreamer:jsonassert'
 	testCompile 'org.springframework:spring-test'

+ 238 - 0
samples/javaconfig/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java

@@ -0,0 +1,238 @@
+/*
+ *
+ *  * 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 sample;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseCookie;
+import org.springframework.security.web.server.header.ContentTypeOptionsHttpHeadersWriter;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.reactive.server.ExchangeResult;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+
+import java.nio.charset.Charset;
+import java.util.Base64;
+
+import static org.springframework.security.test.web.reactive.server.SecurityExchangeMutators.withUser;
+import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = HelloWebfluxApplication.class)
+@ActiveProfiles("test")
+public class HelloWebfluxApplicationTests {
+	@Autowired
+	ApplicationContext context;
+
+	WebTestClient rest;
+
+	@Before
+	public void setup() {
+		this.rest = WebTestClient.bindToApplicationContext(context).build();
+	}
+
+	@Test
+	public void basicRequired() throws Exception {
+		this.rest
+			.get()
+			.uri("/users")
+			.exchange()
+			.expectStatus().isUnauthorized();
+	}
+
+	@Test
+	public void basicWorks() throws Exception {
+		this.rest
+			.filter(robsCredentials())
+			.get()
+			.uri("/users")
+			.exchange()
+			.expectStatus().isOk()
+			.expectBody().json("[{\"id\":null,\"username\":\"rob\",\"password\":\"rob\",\"firstname\":\"Rob\",\"lastname\":\"Winch\"},{\"id\":null,\"username\":\"admin\",\"password\":\"admin\",\"firstname\":\"Admin\",\"lastname\":\"User\"}]");
+	}
+
+	@Test
+	public void basicWhenPasswordInvalid401() throws Exception {
+		this.rest
+			.filter(invalidPassword())
+			.get()
+			.uri("/users")
+			.exchange()
+			.expectStatus().isUnauthorized()
+			.expectBody().isEmpty();
+	}
+
+	@Test
+	public void authorizationAdmin403() throws Exception {
+		this.rest
+			.filter(robsCredentials())
+			.get()
+			.uri("/admin")
+			.exchange()
+			.expectStatus().isEqualTo(HttpStatus.FORBIDDEN)
+			.expectBody().isEmpty();
+	}
+
+	@Test
+	public void authorizationAdmin200() throws Exception {
+		this.rest
+			.filter(adminCredentials())
+			.get()
+			.uri("/admin")
+			.exchange()
+			.expectStatus().isOk();
+	}
+
+	@Test
+	public void basicMissingUser401() throws Exception {
+		this.rest
+			.filter(basicAuthentication("missing-user", "password"))
+			.get()
+			.uri("/admin")
+			.exchange()
+			.expectStatus().isUnauthorized();
+	}
+
+	@Test
+	public void basicInvalidPassword401() throws Exception {
+		this.rest
+			.filter(invalidPassword())
+			.get()
+			.uri("/admin")
+			.exchange()
+			.expectStatus().isUnauthorized();
+	}
+
+	@Test
+	public void basicInvalidParts401() throws Exception {
+		this.rest
+			.get()
+			.uri("/admin")
+			.header("Authorization", "Basic " + base64Encode("no colon"))
+			.exchange()
+			.expectStatus().isUnauthorized();
+	}
+
+	@Test
+	public void sessionWorks() throws Exception {
+		ExchangeResult result = this.rest
+				.filter(robsCredentials())
+				.get()
+				.uri("/users")
+				.exchange()
+				.returnResult(String.class);
+
+		ResponseCookie session = result.getResponseCookies().getFirst("SESSION");
+
+		this.rest
+			.get()
+			.uri("/users")
+			.cookie(session.getName(), session.getValue())
+			.exchange()
+			.expectStatus().isOk();
+	}
+
+	@Test
+	public void mockSupport() throws Exception {
+		this.rest
+			.exchangeMutator( withUser() )
+			.get()
+			.uri("/users")
+			.exchange()
+			.expectStatus().isOk();
+
+		this.rest
+			.get()
+			.uri("/users")
+			.exchange()
+			.expectStatus().isUnauthorized();
+	}
+
+	@Test
+	public void me() throws Exception {
+		this.rest
+			.filter(robsCredentials())
+			.get()
+			.uri("/me")
+			.exchange()
+			.expectStatus().isOk()
+			.expectBody().json("{\"username\" : \"rob\"}");
+	}
+
+	@Test
+	public void monoMe() throws Exception {
+		this.rest
+			.filter(robsCredentials())
+			.get()
+			.uri("/mono/me")
+			.exchange()
+			.expectStatus().isOk()
+			.expectBody().json("{\"username\" : \"rob\"}");
+	}
+
+	@Test
+	public void principal() throws Exception {
+		this.rest
+			.filter(robsCredentials())
+			.get()
+			.uri("/principal")
+			.exchange()
+			.expectStatus().isOk()
+			.expectBody().json("{\"username\" : \"rob\"}");
+	}
+
+	@Test
+	public void headers() throws Exception {
+		this.rest
+				.filter(robsCredentials())
+				.get()
+				.uri("/principal")
+				.exchange()
+				.expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
+				.expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
+				.expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
+				.expectHeader().valueEquals(ContentTypeOptionsHttpHeadersWriter.X_CONTENT_OPTIONS, ContentTypeOptionsHttpHeadersWriter.NOSNIFF);
+	}
+
+	private ExchangeFilterFunction robsCredentials() {
+		return basicAuthentication("rob","rob");
+	}
+
+	private ExchangeFilterFunction invalidPassword() {
+		return basicAuthentication("rob","INVALID");
+	}
+
+	private ExchangeFilterFunction adminCredentials() {
+		return basicAuthentication("admin","admin");
+	}
+
+	private String base64Encode(String value) {
+		return Base64.getEncoder().encodeToString(value.getBytes(Charset.defaultCharset()));
+	}
+}

+ 1 - 0
samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle

@@ -10,6 +10,7 @@ dependencies {
 	compile 'org.springframework:spring-context'
 	compile 'org.springframework:spring-webflux'
 
+	testCompile project(':spring-security-test')
 	testCompile 'io.projectreactor.addons:reactor-test'
 	testCompile 'org.skyscreamer:jsonassert'
 	testCompile 'org.springframework:spring-test'

+ 1 - 1
samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationTests.java → samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java

@@ -42,7 +42,7 @@ import static org.springframework.web.reactive.function.client.ExchangeFilterFun
 @RunWith(SpringRunner.class)
 @ContextConfiguration(classes = HelloWebfluxFnApplication.class)
 @TestPropertySource(properties = "server.port=0")
-public class HelloWebfluxFnApplicationTests {
+public class HelloWebfluxFnApplicationITests {
 	@Value("#{@nettyContext.address().getPort()}")
 	int port;
 

+ 11 - 9
samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java

@@ -19,10 +19,7 @@ package sample;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.ApplicationContext;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.*;
 import org.springframework.core.ReactiveAdapterRegistry;
 import org.springframework.http.server.reactive.HttpHandler;
 import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
@@ -72,8 +69,16 @@ public class HelloWebfluxFnApplication {
 		}
 	}
 
+	@Profile("default")
 	@Bean
-	public NettyContext nettyContext(UserController userController, WebFilter springSecurityFilterChain) {
+	public NettyContext nettyContext(HttpHandler handler) {
+		ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
+		HttpServer httpServer = HttpServer.create("localhost", port);
+		return httpServer.newHandler(adapter).block();
+	}
+
+	@Bean
+	public HttpHandler httpHandler(UserController userController, WebFilter springSecurityFilterChain) {
 		RouterFunction<ServerResponse> route = route(
 				GET("/principal"), userController::principal).andRoute(
 				GET("/users"), userController::users).andRoute(
@@ -82,10 +87,7 @@ public class HelloWebfluxFnApplication {
 		HandlerStrategies handlerStrategies = HandlerStrategies.builder()
 			.webFilter(springSecurityFilterChain).build();
 
-		HttpHandler handler = RouterFunctions.toHttpHandler(route, handlerStrategies);
-		ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
-		HttpServer httpServer = HttpServer.create("localhost", port);
-		return httpServer.newHandler(adapter).block();
+		return RouterFunctions.toHttpHandler(route, handlerStrategies);
 	}
 
 	@Bean

+ 219 - 0
samples/javaconfig/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java

@@ -0,0 +1,219 @@
+/*
+ *
+ *  * 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 sample;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseCookie;
+import org.springframework.http.server.reactive.HttpHandler;
+import org.springframework.security.web.server.header.ContentTypeOptionsHttpHeadersWriter;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.reactive.server.ExchangeResult;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+
+import java.nio.charset.Charset;
+import java.util.Base64;
+
+import static org.springframework.security.test.web.reactive.server.SecurityExchangeMutators.withUser;
+import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = HelloWebfluxFnApplication.class)
+@ActiveProfiles("test")
+public class HelloWebfluxFnApplicationTests {
+	@Autowired
+	HttpHandler handler;
+
+	WebTestClient rest;
+
+	@Before
+	public void setup() {
+		this.rest = WebTestClient.bindToHttpHandler(handler).build();
+	}
+
+	@Test
+	public void basicRequired() throws Exception {
+		this.rest
+			.get()
+			.uri("/users")
+			.exchange()
+			.expectStatus().isUnauthorized();
+	}
+
+	@Test
+	public void basicWorks() throws Exception {
+		this.rest
+			.filter(robsCredentials())
+			.get()
+			.uri("/users")
+			.exchange()
+			.expectStatus().isOk()
+			.expectBody().json("[{\"id\":null,\"username\":\"rob\",\"password\":\"rob\",\"firstname\":\"Rob\",\"lastname\":\"Winch\"},{\"id\":null,\"username\":\"admin\",\"password\":\"admin\",\"firstname\":\"Admin\",\"lastname\":\"User\"}]");
+	}
+
+	@Test
+	public void basicWhenPasswordInvalid401() throws Exception {
+		this.rest
+			.filter(invalidPassword())
+			.get()
+			.uri("/users")
+			.exchange()
+			.expectStatus().isUnauthorized()
+			.expectBody().isEmpty();
+	}
+
+	@Test
+	public void authorizationAdmin403() throws Exception {
+		this.rest
+			.filter(robsCredentials())
+			.get()
+			.uri("/admin")
+			.exchange()
+			.expectStatus().isEqualTo(HttpStatus.FORBIDDEN)
+			.expectBody().isEmpty();
+	}
+
+	@Test
+	public void authorizationAdmin200() throws Exception {
+		this.rest
+			.filter(adminCredentials())
+			.get()
+			.uri("/admin")
+			.exchange()
+			.expectStatus().isOk();
+	}
+
+	@Test
+	public void basicMissingUser401() throws Exception {
+		this.rest
+			.filter(basicAuthentication("missing-user", "password"))
+			.get()
+			.uri("/admin")
+			.exchange()
+			.expectStatus().isUnauthorized();
+	}
+
+	@Test
+	public void basicInvalidPassword401() throws Exception {
+		this.rest
+			.filter(invalidPassword())
+			.get()
+			.uri("/admin")
+			.exchange()
+			.expectStatus().isUnauthorized();
+	}
+
+	@Test
+	public void basicInvalidParts401() throws Exception {
+		this.rest
+			.get()
+			.uri("/admin")
+			.header("Authorization", "Basic " + base64Encode("no colon"))
+			.exchange()
+			.expectStatus().isUnauthorized();
+	}
+
+	@Test
+	public void sessionWorks() throws Exception {
+		ExchangeResult result = this.rest
+			.filter(robsCredentials())
+			.get()
+			.uri("/users")
+			.exchange()
+			.returnResult(String.class);
+
+		ResponseCookie session = result.getResponseCookies().getFirst("SESSION");
+
+		this.rest
+			.get()
+			.uri("/users")
+			.cookie(session.getName(), session.getValue())
+			.exchange()
+			.expectStatus().isOk();
+	}
+
+	@Ignore
+	@Test
+	public void mockSupport() throws Exception {
+		this.rest
+			.exchangeMutator( withUser() )
+			.get()
+			.uri("/users")
+			.exchange()
+			.expectStatus().isOk();
+
+		this.rest
+			.get()
+			.uri("/users")
+			.exchange()
+			.expectStatus().isUnauthorized();
+	}
+
+	@Test
+	public void principal() throws Exception {
+		this.rest
+			.filter(robsCredentials())
+			.get()
+			.uri("/principal")
+			.exchange()
+			.expectStatus().isOk()
+			.expectBody().json("{\"username\" : \"rob\"}");
+	}
+
+	@Test
+	public void headers() throws Exception {
+		this.rest
+			.filter(robsCredentials())
+			.get()
+			.uri("/principal")
+			.exchange()
+			.expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")
+			.expectHeader().valueEquals(HttpHeaders.EXPIRES, "0")
+			.expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache")
+			.expectHeader().valueEquals(ContentTypeOptionsHttpHeadersWriter.X_CONTENT_OPTIONS, ContentTypeOptionsHttpHeadersWriter.NOSNIFF);
+	}
+
+	private ExchangeFilterFunction robsCredentials() {
+		return basicAuthentication("rob","rob");
+	}
+
+	private ExchangeFilterFunction invalidPassword() {
+		return basicAuthentication("rob","INVALID");
+	}
+
+	private ExchangeFilterFunction adminCredentials() {
+		return basicAuthentication("admin","admin");
+	}
+
+	private String base64Encode(String value) {
+		return Base64.getEncoder().encodeToString(value.getBytes(Charset.defaultCharset()));
+	}
+}

+ 1 - 0
test/spring-security-test.gradle

@@ -7,6 +7,7 @@ dependencies {
 	compile 'org.springframework:spring-test'
 
 	optional project(':spring-security-config')
+	optional 'io.projectreactor:reactor-core'
 
 	provided 'javax.servlet:javax.servlet-api'
 

+ 192 - 0
test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityExchangeMutators.java

@@ -0,0 +1,192 @@
+/*
+ *
+ *  * 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.web.reactive.server;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.security.Principal;
+import java.util.Collection;
+import java.util.function.UnaryOperator;
+
+/**
+ * Test utilities for working with Spring Security and
+ * {{@link org.springframework.test.web.reactive.server.WebTestClient#exchangeMutator(UnaryOperator)}}.
+ *
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class SecurityExchangeMutators {
+	/**
+	 * Updates the ServerWebExchange to use the provided Principal
+	 *
+	 * @param principal the principal to use.
+	 * @return the {@link UnaryOperator<ServerWebExchange>}} to provide to
+	 * {@link org.springframework.test.web.reactive.server.WebTestClient#exchangeMutator(UnaryOperator)}
+	 */
+	public static UnaryOperator<ServerWebExchange> withPrincipal(Principal principal) {
+		return m -> m.mutate().principal(Mono.just(principal)).build();
+	}
+
+	/**
+	 * Updates the ServerWebExchange to use the provided Authentication as the Principal
+	 *
+	 * @param authentication the Authentication to use.
+	 * @return the {@link UnaryOperator<ServerWebExchange>}} to provide to
+	 * {@link org.springframework.test.web.reactive.server.WebTestClient#exchangeMutator(UnaryOperator)}
+	 */
+	public static UnaryOperator<ServerWebExchange> withAuthentication(Authentication authentication) {
+		return withPrincipal(authentication);
+	}
+
+	/**
+	 * Updates the ServerWebExchange to use the provided UserDetails to create a UsernamePasswordAuthenticationToken as
+	 * the Principal
+	 *
+	 * @param userDetails the UserDetails to use.
+	 * @return the {@link UnaryOperator<ServerWebExchange>}} to provide to
+	 * {@link org.springframework.test.web.reactive.server.WebTestClient#exchangeMutator(UnaryOperator)}
+	 */
+	public static UnaryOperator<ServerWebExchange> withUser(UserDetails userDetails) {
+		return withAuthentication(new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities()));
+	}
+
+	/**
+	 * Updates the ServerWebExchange to use a UserDetails to create a UsernamePasswordAuthenticationToken as
+	 * the Principal. This uses a default username of "user", password of "password", and granted authorities of
+	 * "ROLE_USER".
+	 *
+	 * @return the {@link UnaryOperator<ServerWebExchange>}} to provide to
+	 * {@link org.springframework.test.web.reactive.server.WebTestClient#exchangeMutator(UnaryOperator)}
+	 */
+	public static UserExchangeMutator withUser() {
+		return withUser("user");
+	}
+
+
+	/**
+	 * Updates the ServerWebExchange to use a UserDetails to create a UsernamePasswordAuthenticationToken as
+	 * the Principal. This uses a default password of "password" and granted authorities of
+	 * "ROLE_USER".
+	 *
+	 * @return the {@link UnaryOperator<ServerWebExchange>}} to provide to
+	 * {@link org.springframework.test.web.reactive.server.WebTestClient#exchangeMutator(UnaryOperator)}
+	 */
+	public static UserExchangeMutator withUser(String username) {
+		return new UserExchangeMutator(username);
+	}
+
+	/**
+	 * Updates the WebServerExchange using {@code SecurityExchangeMutators#withUser(UserDetails)}. Defaults to use a
+	 * password of "password" and granted authorities of "ROLE_USER".
+	 */
+	public static class UserExchangeMutator implements UnaryOperator<ServerWebExchange> {
+		private final User.UserBuilder userBuilder;
+
+		private UserExchangeMutator(String username) {
+			userBuilder = User.withUsername(username);
+			password("password");
+			roles("USER");
+		}
+
+		/**
+		 * Specifies the password to use. Default is "password".
+		 * @param password the password to use
+		 * @return the UserExchangeMutator
+		 */
+		public UserExchangeMutator password(String password) {
+			userBuilder.password(password);
+			return this;
+		}
+
+		/**
+		 * Specifies the roles to use. Default is "USER". This is similar to authorities except each role is
+		 * automatically prefixed with "ROLE_USER".
+		 *
+		 * @param roles the roles to use.
+		 * @return the UserExchangeMutator
+		 */
+		public UserExchangeMutator roles(String... roles) {
+			userBuilder.roles(roles);
+			return this;
+		}
+
+		/**
+		 * Specifies the {@code GrantedAuthority}s to use. Default is "ROLE_USER".
+		 *
+		 * @param authorities the authorities to use.
+		 * @return the UserExchangeMutator
+		 */
+		public UserExchangeMutator authorities(GrantedAuthority... authorities) {
+			userBuilder.authorities(authorities);
+			return this;
+		}
+
+		/**
+		 * Specifies the {@code GrantedAuthority}s to use. Default is "ROLE_USER".
+		 *
+		 * @param authorities the authorities to use.
+		 * @return the UserExchangeMutator
+		 */
+		public UserExchangeMutator authorities(Collection<? extends GrantedAuthority> authorities) {
+			userBuilder.authorities(authorities);
+			return this;
+		}
+
+		/**
+		 * Specifies the {@code GrantedAuthority}s to use. Default is "ROLE_USER".
+		 * @param authorities the authorities to use.
+		 * @return the UserExchangeMutator
+		 */
+		public UserExchangeMutator authorities(String... authorities) {
+			userBuilder.authorities(authorities);
+			return this;
+		}
+
+		public UserExchangeMutator accountExpired(boolean accountExpired) {
+			userBuilder.accountExpired(accountExpired);
+			return this;
+		}
+
+		public UserExchangeMutator accountLocked(boolean accountLocked) {
+			userBuilder.accountLocked(accountLocked);
+			return this;
+		}
+
+		public UserExchangeMutator credentialsExpired(boolean credentialsExpired) {
+			userBuilder.credentialsExpired(credentialsExpired);
+			return this;
+		}
+
+		public UserExchangeMutator disabled(boolean disabled) {
+			userBuilder.disabled(disabled);
+			return this;
+		}
+
+		@Override
+		public ServerWebExchange apply(ServerWebExchange serverWebExchange) {
+			return withUser(userBuilder.build()).apply(serverWebExchange);
+		}
+	}
+}

+ 108 - 0
test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityExchangeMutatorsTests.java

@@ -0,0 +1,108 @@
+/*
+ *
+ *  * 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.web.reactive.server;
+
+import org.assertj.core.api.AssertionsForInterfaceTypes;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.web.server.ServerWebExchange;
+
+import java.security.Principal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.security.test.web.reactive.server.SecurityExchangeMutators.withAuthentication;
+import static org.springframework.security.test.web.reactive.server.SecurityExchangeMutators.withPrincipal;
+import static org.springframework.security.test.web.reactive.server.SecurityExchangeMutators.withUser;
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class SecurityExchangeMutatorsTests {
+	@Mock
+	Principal principal;
+	@Mock
+	Authentication authentication;
+
+	ServerWebExchange exchange = MockServerHttpRequest.get("/").toExchange();
+
+	User.UserBuilder userBuilder = User.withUsername("user").password("password").roles("USER");
+
+	@Test
+	public void withPrincipalWhenHappyPathThenSuccess() {
+		assertThat(withPrincipal(principal).apply(exchange).getPrincipal().block()).isEqualTo(principal);
+	}
+
+	@Test
+	public void withAuthenticationWhenHappyPathThenSuccess() {
+		assertThat(withAuthentication(authentication).apply(exchange).getPrincipal().block()).isEqualTo(authentication);
+	}
+
+	@Test
+	public void withUserWhenDefaultsThenSuccess() {
+		Principal principal = withUser().apply(exchange).getPrincipal().block();
+
+		assertPrincipalCreatedFromUserDetails(principal, userBuilder.build());
+	}
+
+	@Test
+	public void withUserStringWhenHappyPathThenSuccess() {
+		Principal principal = withUser(userBuilder.build().getUsername() ).apply(exchange).getPrincipal().block();
+
+		assertPrincipalCreatedFromUserDetails(principal, userBuilder.build());
+	}
+
+	@Test
+	public void withUserStringWhenCustomThenSuccess() {
+		SecurityExchangeMutators.UserExchangeMutator withUser = withUser("admin").password("secret").roles("USER", "ADMIN");
+		userBuilder = User.withUsername("admin").password("secret").roles("USER", "ADMIN");
+
+		Principal principal = withUser.apply(exchange).getPrincipal().block();
+
+		assertPrincipalCreatedFromUserDetails(principal, userBuilder.build() );
+	}
+
+	@Test
+	public void withUserUserDetailsWhenHappyPathThenSuccess() {
+		Principal principal = withUser(userBuilder.build()).apply(exchange).getPrincipal().block();
+
+		assertPrincipalCreatedFromUserDetails(principal, userBuilder.build());
+	}
+
+	private void assertPrincipalCreatedFromUserDetails(Principal principal, UserDetails originalUserDetails) {
+		assertThat(principal).isInstanceOf(UsernamePasswordAuthenticationToken.class);
+
+		UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) principal;
+		assertThat(authentication.getCredentials()).isEqualTo(originalUserDetails.getPassword());
+		assertThat(authentication.getAuthorities()).containsOnlyElementsOf(originalUserDetails.getAuthorities());
+
+		UserDetails userDetails = (UserDetails) authentication.getPrincipal();
+		assertThat(userDetails.getPassword()).isEqualTo(authentication.getCredentials());
+		assertThat(authentication.getAuthorities()).containsOnlyElementsOf(userDetails.getAuthorities());
+	}
+}