I would like to be able to use WebTestClient to test applications that use SslInfo without the need to use an HTTP connection (e.g. use WebTestClient.bindToApplicationContext(ApplicationContext)).
Right now the only way I can do this is to provide a custom WebTestClientConfigurer that mutates the ServerHttpRequest. I thought about providing this in Spring Security, but since there are no Spring Security dependencies, I think that it makes more sense in Spring Framework.
Comment From: sbrannen
Are you envisioning something like the following, where mockSslInfo() would be a static import for a method that returns a WebTestClientConfigurer?
client = WebTestClient
.bindToApplicationContext(context)
.build()
.mutateWith(mockSslInfo());
Comment From: rwinch
Thanks for the response @sbrannen
Yes. I was imagining Spring Framework would provide a WebTestClientConfigurer implementation that allowed replacing the SslInfo. However, I didn't want to be prescriptive on how Spring Framework solved this.
Comment From: sbrannen
After discussions with @rstoyanchev, we decided instead to introduce a new sslInfo(SslInfo) method in WebTestClient.MockServerSpec.
That can be used like this:
SslInfo sslInfo = mock();
when(sslInfo.getSessionId()).thenReturn("mock");
WebTestClient client = WebTestClient.bindToApplicationContext(context)
.sslInfo(sslInfo)
// ...
.build();
@rwinch, does that suit your needs?
Comment From: rwinch
Thanks! This does work.
Given it's a pojo I wonder if you could expose an implementation of SslInfo so that a mock is not needed?
Comment From: sbrannen
Thanks! This does work.
Thanks for the feedback. I'll proceed with that.
Given it's a pojo I wonder if you could expose an implementation of SslInfo so that a mock is not needed?
I just introduced MockSslInfo in https://github.com/spring-projects/spring-framework/commit/9ca7b623aa8822869653888e95be2e07ac4998fa.
Comment From: sbrannen
@rwinch This is now available on main, so feel free to try it out in 7.0 snapshots.
Or take a look at ApplicationContextTests.
https://github.com/spring-projects/spring-framework/blob/1fb04cb83a605717cc9fd237ea62de11875f4e68/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java#L65-L76
Comment From: sbrannen
FYI: we decided to remove the recently introduced MockSslInfo in favor of introducing static factory methods directly in the SslInfo interface. See commit f478f5cdc8c73ed302d7c315c72ed18d018b4989 for details.
Thus, the example now looks like this:
https://github.com/spring-projects/spring-framework/blob/f478f5cdc8c73ed302d7c315c72ed18d018b4989/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java#L67-L70
Comment From: rwinch
@sbrannen @rstoyanchev Thank you for the work here.
I'd like to use SslInfo testing support to test different SslInfo scenarios, but I'm running into some difficulty because the SslInfo must be specified on MockServerSpec rather than WebTestClient.
First, in my tests I typically use a shared WebTestClient instance. This typically works well, but because I cannot vary the SslInfo I cannot do this anymore. Instead, I'd need a shared MockServerSpec.
Since I cannot reuse WebTestClient, Spring Boot's WebTestClient support cannot be used if the test would like to provide SslInfo.
Additionally, test that are using MockServerSpec need extra boilerplate of .configureClient().build() for every test.
Is there a way that the support can be updated to allow adding SslInfo to a WebTestClient so that Spring Boot's support can be used?
Comment From: rstoyanchev
There is a good reason for this to be in WebTestClient.MockServerSpec as it applies only to mock servers, and is only relevant for bindTo* setups with a mock server. WebTestClient.Builder also covers live server tests with bindToServer. We could reject such input with a live server, but the API shouldn't allow illegal input in the first place.
Could there be a way to update the tests still to keep them as they are now? Not knowing the exact details, I can't be too specific, but something like make MockSslInfo available one way or another (explicitly pass or set somewhere) to the code that creates the shared WebTestClient so it can apply it conditionally. Or allow use of Consumer<MockServerSpec> for more advanced customizations, which seems like a more general topic, i.e. how one customizes the MockServerSpec when necessary.
Comment From: rwinch
Thanks for your work on this. I'm eager to simplify the testing within Spring Security as well as our users testing of X509 with WebTestClient.
Not knowing the exact details, I can't be too specific, but something like make MockSslInfo available one way or another (explicitly pass or set somewhere) to the code that creates the shared WebTestClient so it can apply it conditionally.
Sorry for not providing more details sooner. I've put together a small sample to help illustrate what I'm looking for. You can find it at https://github.com/rwinch/spring-sample/tree/mocksslinfo
Right now the sample only provides tests for no x509 and a single x509, but you can imagine that in a real scenario users would need to test all permutations of SslInfo to cover all their code branches. For example, they would likely have different x509 certs for each testing persona (e.g. an admin, a regular user, certs with invalid formatted subject dn, etc).
There is a good reason for this to be in WebTestClient.MockServerSpec as it applies only to mock servers, and is only relevant for bindTo* setups with a mock server. WebTestClient.Builder also covers live server tests with bindToServer. We could reject such input with a live server, but the API shouldn't allow illegal input in the first place.
I wonder if this is a good reason to leverage WebTestClient.mutateWith(WebTestClientConfigurer) for providing a mock SslInfo since, as the sample demonstrates, it works with bindToServer and bindToApplicationContext?
Another reason that I think that mutateWith works well is that it works well with the existing Spring Boot Autoconfiguration. If Framework continues to use MockServerSpec, I'd hope that the Framework team could coordinate with the Spring Boot team to support an Autoconfiguration arrangement that allows testing with different SslInfo values.
I worked around not having Autoconfiguration, but we still see that the user is forced to invoke .configureClient().build() for every test (even when they don't need to modify the SslInfo) which I don't view as ideal (it could be more concise). It's also worth noting the need to for marking the MockServerSpec as a prototype Bean (or using @DirtiesContext) since MockServerSpec.sslInfo modifies the MockServerSpec in the ApplicationContext.
Could there be a way to update the tests still to keep them as they are now? Not knowing the exact details, I can't be too specific, but something like make MockSslInfo available one way or another (explicitly pass or set somewhere) to the code that creates the shared WebTestClient so it can apply it conditionally. Or allow use of Consumer
for more advanced customizations, which seems like a more general topic, i.e. how one customizes the MockServerSpec when necessary.
Perhaps you can experiment with the sample that I've provided to see what you can come up with. Ideally the result would be as concise as possible, work with Spring Boot Autoconfiguration (that might require updates from them), and work with bindToServer as well.
Thanks again!
Comment From: rstoyanchev
Re-opening since there is more here to consider. Thanks for the sample, I will experiment with it.
Comment From: rstoyanchev
as the sample demonstrates, it works with bindToServer and bindToApplicationContext?
They're both using bindToApplicationContext actually. As you will see for bindToServer it's only the SpringBootTest with a random port. You need to comment out @AutoConfigureWebTestClient whose Javadoc says clearly it does not rely on a running server.
Note also the WebHttpHandlerBuilder (the thing that builds the mock server chain) that's passed into WebTestClientConfigurer is @Nullable, and the Javadoc for that says it is null in case of a running server.
Generally, it is the nature of having to configure a mock server and a test client in one sequence. In that sense this isn't about SslInfo only. When you need to change anything at the mock server level, you need MockServerSpec. There is no way around that.
I do think it's possible to simplify tests with helper methods as shown below. Note also that when not using SslInfo, it passes null, which resets the SslInfo in the MockServerSpec instance, and there is no need for the prototype bean or the ordering of tests:
void sslInfoThenOkAndBodyIsSubjectDn() {
getClient("/", SslInfo.from("id", TestCertificates.rod()))
.get().uri("/").exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("CN=rod,OU=Spring Security,O=Spring Framework");
}
@Test
void sslInfoNullThenUnauthorized() {
getClient("/").get().uri("/").exchange().expectStatus().isUnauthorized();
}
private WebTestClient getClient(String path) {
return getClient(path, null);
}
private WebTestClient getClient(String path, @Nullable SslInfo sslInfo) {
return this.server.sslInfo(sslInfo).configureClient().build();
}
We could possibly provide an additional way to hook in the MockSslInfo through a WebTestClientConfigurer. That would be geared at setting the user identity as you have done. Then you could do it via MockServerSpec or with configurer. What do you think?
Comment From: rwinch
They're both using bindToApplicationContext actually. As you will see for bindToServer it's only the SpringBootTest with a random port. You need to comment out @AutoConfigureWebTestClient whose Javadoc says clearly it does not rely on a running server.
Thank you. You are right about this.
I do think it's possible to simplify tests with helper methods as shown below.
Sure, but then every user needs to add a helper method.
We could possibly provide an additional way to hook in the MockSslInfo through a WebTestClientConfigurer. That would be geared at setting the user identity as you have done. Then you could do it via MockServerSpec or with configurer. What do you think?
Thank you. I think this would improve the user experience and likely allow users to leverage Spring Boot's Autoconfiguration without changes for the Boot team or our users.
Comment From: rstoyanchev
I do think it's possible to simplify tests with helper methods as shown below.
Sure, but then every user needs to add a helper method.
The first statement there was the key, i.e. that there is no way around the fact there are two things to initialize. The thought about simplifying tests was just a thought that something like that is possible.
It occurs to me also that we actually do have a shortcut on MockServerSpec to build the client directly, i.e. bypassing .configureClient() so this works fine:
@Test
void sslInfoThenOkAndBodyIsSubjectDn() {
this.server.sslInfo(SslInfo.from("id", TestCertificates.rod())).build()
.get()
.uri("/")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("CN=rod,OU=Spring Security,O=Spring Framework");
}
@Test
void sslInfoNullThenUnauthorized() {
this.server.sslInfo(null).build().get().uri("/")
.exchange()
.expectStatus().isUnauthorized();
}
I should make the sslInfo method parameter Nullable though. Currently it isn't.
Comment From: rwinch
Skipping configureClient makes it a little more concise, but it still requires users to write their tests differently (use MockServerSpec) if they need to use sslInfo vs if they don't (use WebTestClient). I think that we should avoid forcing our users to change the way they write tests if we can help it.