From 7bc98db23c12cb17673f4f6d195d4ef76cda4fd2 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 19 May 2017 14:40:40 -0500 Subject: [PATCH] Add WebTestClient test support SecurityExchangeMutators Fixes gh-4343 --- ...ity-samples-javaconfig-hellowebflux.gradle | 1 + .../sample/HelloWebfluxApplicationTests.java | 238 ++++++++++++++++++ ...y-samples-javaconfig-hellowebfluxfn.gradle | 1 + ...a => HelloWebfluxFnApplicationITests.java} | 2 +- .../sample/HelloWebfluxFnApplication.java | 20 +- .../HelloWebfluxFnApplicationTests.java | 219 ++++++++++++++++ test/spring-security-test.gradle | 1 + .../server/SecurityExchangeMutators.java | 192 ++++++++++++++ .../server/SecurityExchangeMutatorsTests.java | 108 ++++++++ 9 files changed, 772 insertions(+), 10 deletions(-) create mode 100644 samples/javaconfig/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java rename samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/{HelloWebfluxFnApplicationTests.java => HelloWebfluxFnApplicationITests.java} (99%) create mode 100644 samples/javaconfig/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java create mode 100644 test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityExchangeMutators.java create mode 100644 test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityExchangeMutatorsTests.java diff --git a/samples/javaconfig/hellowebflux/spring-security-samples-javaconfig-hellowebflux.gradle b/samples/javaconfig/hellowebflux/spring-security-samples-javaconfig-hellowebflux.gradle index 2b6dd88356..1141fc0861 100644 --- a/samples/javaconfig/hellowebflux/spring-security-samples-javaconfig-hellowebflux.gradle +++ b/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' diff --git a/samples/javaconfig/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java b/samples/javaconfig/hellowebflux/src/test/java/sample/HelloWebfluxApplicationTests.java new file mode 100644 index 0000000000..9b718905db --- /dev/null +++ b/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())); + } +} diff --git a/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle b/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle index 2b6dd88356..1141fc0861 100644 --- a/samples/javaconfig/hellowebfluxfn/spring-security-samples-javaconfig-hellowebfluxfn.gradle +++ b/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' diff --git a/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationTests.java b/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java similarity index 99% rename from samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationTests.java rename to samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationITests.java index 6052487023..b1a2bea584 100644 --- a/samples/javaconfig/hellowebfluxfn/src/integration-test/java/sample/HelloWebfluxFnApplicationTests.java +++ b/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; diff --git a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java b/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java index d7d29d284e..28d3f604ff 100644 --- a/samples/javaconfig/hellowebfluxfn/src/main/java/sample/HelloWebfluxFnApplication.java +++ b/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 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 diff --git a/samples/javaconfig/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java b/samples/javaconfig/hellowebfluxfn/src/test/java/sample/HelloWebfluxFnApplicationTests.java new file mode 100644 index 0000000000..77575d8540 --- /dev/null +++ b/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())); + } +} diff --git a/test/spring-security-test.gradle b/test/spring-security-test.gradle index 031fe0cd22..fd982b2c6f 100644 --- a/test/spring-security-test.gradle +++ b/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' diff --git a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityExchangeMutators.java b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityExchangeMutators.java new file mode 100644 index 0000000000..7f40022aea --- /dev/null +++ b/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}} to provide to + * {@link org.springframework.test.web.reactive.server.WebTestClient#exchangeMutator(UnaryOperator)} + */ + public static UnaryOperator 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}} to provide to + * {@link org.springframework.test.web.reactive.server.WebTestClient#exchangeMutator(UnaryOperator)} + */ + public static UnaryOperator 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}} to provide to + * {@link org.springframework.test.web.reactive.server.WebTestClient#exchangeMutator(UnaryOperator)} + */ + public static UnaryOperator 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}} 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}} 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 { + 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 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); + } + } +} diff --git a/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityExchangeMutatorsTests.java b/test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityExchangeMutatorsTests.java new file mode 100644 index 0000000000..1f789950c1 --- /dev/null +++ b/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()); + } +}