From 6a84f9693036d7fa034e37e76500b1cca1fd7799 Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Wed, 3 Sep 2025 12:59:46 -0500 Subject: [PATCH] Enable Null checking in spring-security-test via JSpecify Closes gh-17840 --- test/spring-security-test.gradle | 4 ++ .../aot/hint/WebTestUtilsRuntimeHints.java | 4 +- .../security/test/aot/hint/package-info.java | 23 +++++++++ .../test/context/annotation/package-info.java | 23 +++++++++ .../security/test/context/package-info.java | 24 +++++++++ ...hSecurityContextTestExecutionListener.java | 19 +++++-- ...WithUserDetailsSecurityContextFactory.java | 5 +- .../test/context/support/package-info.java | 23 +++++++++ .../server/SecurityMockServerConfigurers.java | 38 +++++++++++--- .../web/reactive/server/package-info.java | 23 +++++++++ .../SecurityMockMvcRequestBuilders.java | 9 ++-- .../SecurityMockMvcRequestPostProcessors.java | 49 +++++++++++++------ .../web/servlet/request/package-info.java | 24 +++++++++ .../SecurityMockMvcResultMatchers.java | 16 +++--- .../web/servlet/response/package-info.java | 23 +++++++++ .../setup/SecurityMockMvcConfigurer.java | 3 ++ .../test/web/servlet/setup/package-info.java | 23 +++++++++ .../test/web/support/WebTestUtils.java | 11 +++-- .../test/web/support/package-info.java | 24 +++++++++ 19 files changed, 324 insertions(+), 44 deletions(-) create mode 100644 test/src/main/java/org/springframework/security/test/aot/hint/package-info.java create mode 100644 test/src/main/java/org/springframework/security/test/context/annotation/package-info.java create mode 100644 test/src/main/java/org/springframework/security/test/context/package-info.java create mode 100644 test/src/main/java/org/springframework/security/test/context/support/package-info.java create mode 100644 test/src/main/java/org/springframework/security/test/web/reactive/server/package-info.java create mode 100644 test/src/main/java/org/springframework/security/test/web/servlet/request/package-info.java create mode 100644 test/src/main/java/org/springframework/security/test/web/servlet/response/package-info.java create mode 100644 test/src/main/java/org/springframework/security/test/web/servlet/setup/package-info.java create mode 100644 test/src/main/java/org/springframework/security/test/web/support/package-info.java diff --git a/test/spring-security-test.gradle b/test/spring-security-test.gradle index 066e454269..afc54e6986 100644 --- a/test/spring-security-test.gradle +++ b/test/spring-security-test.gradle @@ -1,3 +1,7 @@ +plugins { + id 'security-nullability' +} + apply plugin: 'io.spring.convention.spring-module' dependencies { diff --git a/test/src/main/java/org/springframework/security/test/aot/hint/WebTestUtilsRuntimeHints.java b/test/src/main/java/org/springframework/security/test/aot/hint/WebTestUtilsRuntimeHints.java index 066bb498af..20f50b7327 100644 --- a/test/src/main/java/org/springframework/security/test/aot/hint/WebTestUtilsRuntimeHints.java +++ b/test/src/main/java/org/springframework/security/test/aot/hint/WebTestUtilsRuntimeHints.java @@ -16,6 +16,8 @@ package org.springframework.security.test.aot.hint; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -36,7 +38,7 @@ import org.springframework.util.ClassUtils; class WebTestUtilsRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { if (!ClassUtils.isPresent("jakarta.servlet.Filter", classLoader)) { return; } diff --git a/test/src/main/java/org/springframework/security/test/aot/hint/package-info.java b/test/src/main/java/org/springframework/security/test/aot/hint/package-info.java new file mode 100644 index 0000000000..c64fc71fea --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/aot/hint/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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 + * + * https://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. + */ + +/** + * AOT Support for Spring Security test. + */ +@NullMarked +package org.springframework.security.test.aot.hint; + +import org.jspecify.annotations.NullMarked; diff --git a/test/src/main/java/org/springframework/security/test/context/annotation/package-info.java b/test/src/main/java/org/springframework/security/test/context/annotation/package-info.java new file mode 100644 index 0000000000..7bc18eacb5 --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/context/annotation/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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 + * + * https://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. + */ + +/** + * Support for Framework's Test annotations. + */ +@NullMarked +package org.springframework.security.test.context.annotation; + +import org.jspecify.annotations.NullMarked; diff --git a/test/src/main/java/org/springframework/security/test/context/package-info.java b/test/src/main/java/org/springframework/security/test/context/package-info.java new file mode 100644 index 0000000000..c5806d45f9 --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/context/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2004-present 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 + * + * https://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. + */ + +/** + * Spring Security support managing the + * {@link org.springframework.security.core.context.SecurityContext}. + */ +@NullMarked +package org.springframework.security.test.context; + +import org.jspecify.annotations.NullMarked; diff --git a/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java b/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java index b0fb2ae28e..9b013794e6 100644 --- a/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java +++ b/test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java @@ -20,6 +20,9 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.util.function.Supplier; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.context.ApplicationContext; import org.springframework.core.GenericTypeResolver; @@ -76,6 +79,7 @@ public class WithSecurityContextTestExecutionListener extends AbstractTestExecut * {@link WithSecurityContext} on it. If that is not found, the class is inspected. If * still not found, then no {@link SecurityContext} is populated. */ + @NullUnmarked @Override public void beforeTestMethod(TestContext testContext) { TestSecurityContext testSecurityContext = createTestSecurityContext(testContext.getTestMethod(), testContext); @@ -98,6 +102,7 @@ public class WithSecurityContextTestExecutionListener extends AbstractTestExecut * If configured before test execution sets the SecurityContext * @since 5.1 */ + @NullUnmarked @Override public void beforeTestExecution(TestContext testContext) { Supplier supplier = (Supplier) testContext @@ -107,13 +112,13 @@ public class WithSecurityContextTestExecutionListener extends AbstractTestExecut } } - private TestSecurityContext createTestSecurityContext(AnnotatedElement annotated, TestContext context) { + private @Nullable TestSecurityContext createTestSecurityContext(AnnotatedElement annotated, TestContext context) { WithSecurityContext withSecurityContext = AnnotatedElementUtils.findMergedAnnotation(annotated, WithSecurityContext.class); return createTestSecurityContext(annotated, withSecurityContext, context); } - private TestSecurityContext createTestSecurityContext(Class annotated, TestContext context) { + private @Nullable TestSecurityContext createTestSecurityContext(Class annotated, TestContext context) { TestContextAnnotationUtils.AnnotationDescriptor withSecurityContextDescriptor = TestContextAnnotationUtils .findAnnotationDescriptor(annotated, WithSecurityContext.class); if (withSecurityContextDescriptor == null) { @@ -124,9 +129,10 @@ public class WithSecurityContextTestExecutionListener extends AbstractTestExecut return createTestSecurityContext(rootDeclaringClass, withSecurityContext, context); } + @NullUnmarked @SuppressWarnings({ "rawtypes", "unchecked" }) - private TestSecurityContext createTestSecurityContext(AnnotatedElement annotated, - WithSecurityContext withSecurityContext, TestContext context) { + private @Nullable TestSecurityContext createTestSecurityContext(AnnotatedElement annotated, + @Nullable WithSecurityContext withSecurityContext, TestContext context) { if (withSecurityContext == null) { return null; } @@ -147,7 +153,9 @@ public class WithSecurityContextTestExecutionListener extends AbstractTestExecut return new TestSecurityContext(supplier, initialize); } - private Annotation findAnnotation(AnnotatedElement annotated, Class type) { + @NullUnmarked + private @Nullable Annotation findAnnotation(AnnotatedElement annotated, + @Nullable Class type) { Annotation findAnnotation = AnnotatedElementUtils.findMergedAnnotation(annotated, type); if (findAnnotation != null) { return findAnnotation; @@ -181,6 +189,7 @@ public class WithSecurityContextTestExecutionListener extends AbstractTestExecut * Clears out the {@link TestSecurityContextHolder} and the * {@link SecurityContextHolder} after each test method. */ + @NullUnmarked @Override public void afterTestMethod(TestContext testContext) { this.securityContextHolderStrategyConverter.convert(testContext).clearContext(); diff --git a/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java b/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java index 71df1de46f..dcd07dc0a0 100644 --- a/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java +++ b/test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java @@ -16,6 +16,8 @@ package org.springframework.security.test.context.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanNotOfRequiredTypeException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -89,7 +91,7 @@ final class WithUserDetailsSecurityContextFactory implements WithSecurityContext : this.beans.getBean(UserDetailsService.class); } - UserDetailsService findAndAdaptReactiveUserDetailsService(String beanName) { + @Nullable UserDetailsService findAndAdaptReactiveUserDetailsService(String beanName) { try { ReactiveUserDetailsService reactiveUserDetailsService = StringUtils.hasLength(beanName) ? this.beans.getBean(beanName, ReactiveUserDetailsService.class) @@ -110,6 +112,7 @@ final class WithUserDetailsSecurityContextFactory implements WithSecurityContext } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return this.userDetailsService.findByUsername(username).block(); } diff --git a/test/src/main/java/org/springframework/security/test/context/support/package-info.java b/test/src/main/java/org/springframework/security/test/context/support/package-info.java new file mode 100644 index 0000000000..0aa42976af --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/context/support/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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 + * + * https://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. + */ + +/** + * Spring Security support classes for the Spring TestContext Framework. + */ +@NullMarked +package org.springframework.security.test.context.support; + +import org.jspecify.annotations.NullMarked; diff --git a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java index 02339b6fab..da424588a9 100644 --- a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java +++ b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java @@ -29,6 +29,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; +import org.jspecify.annotations.NullUnmarked; import reactor.core.publisher.Mono; import org.springframework.context.ApplicationContext; @@ -190,6 +191,7 @@ public final class SecurityMockServerConfigurers { * @return the {@link OAuth2LoginMutator} to further configure or use * @since 5.3 */ + @NullUnmarked public static OAuth2LoginMutator mockOAuth2Login() { OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", null, null, Collections.singleton("read")); @@ -203,6 +205,7 @@ public final class SecurityMockServerConfigurers { * @return the {@link OidcLoginMutator} to further configure or use * @since 5.3 */ + @NullUnmarked public static OidcLoginMutator mockOidcLogin() { OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", null, null, Collections.singleton("read")); @@ -252,6 +255,7 @@ public final class SecurityMockServerConfigurers { private CsrfMutator() { } + @NullUnmarked @Override public void afterConfigurerAdded(WebTestClient.Builder builder, @Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) { @@ -394,6 +398,7 @@ public final class SecurityMockServerConfigurers { builder.filters(addSetupMutatorFilter()); } + @NullUnmarked @Override public void afterConfigurerAdded(WebTestClient.Builder builder, @Nullable WebHttpHandlerBuilder webHttpHandlerBuilder, @@ -537,6 +542,7 @@ public final class SecurityMockServerConfigurers { configurer().afterConfigureAdded(serverSpec); } + @NullUnmarked @Override public void afterConfigurerAdded(WebTestClient.Builder builder, @Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) { @@ -547,6 +553,7 @@ public final class SecurityMockServerConfigurers { configurer().afterConfigurerAdded(builder, httpHandlerBuilder, connector); } + @NullUnmarked private T configurer() { return mockAuthentication( new JwtAuthenticationToken(this.jwt, this.authoritiesConverter.convert(this.jwt))); @@ -631,6 +638,7 @@ public final class SecurityMockServerConfigurers { configurer().afterConfigureAdded(serverSpec); } + @NullUnmarked @Override public void afterConfigurerAdded(WebTestClient.Builder builder, @Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) { @@ -688,6 +696,7 @@ public final class SecurityMockServerConfigurers { return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt); } + @NullUnmarked private Instant getInstant(Map attributes, String name) { Object value = attributes.get(name); if (value == null) { @@ -865,12 +874,15 @@ public final class SecurityMockServerConfigurers { private OAuth2AccessToken accessToken; + @Nullable private OidcIdToken idToken; + @SuppressWarnings("NullAway.Init") private OidcUserInfo userInfo; private Supplier oidcUser = this::defaultPrincipal; + @Nullable private Collection authorities; private OidcLoginMutator(OAuth2AccessToken accessToken) { @@ -1015,6 +1027,7 @@ public final class SecurityMockServerConfigurers { return authorities; } + @NullUnmarked private OidcIdToken getOidcIdToken() { if (this.idToken != null) { return this.idToken; @@ -1036,10 +1049,12 @@ public final class SecurityMockServerConfigurers { * @author Josh Cummings * @since 5.3 */ + @NullUnmarked public static final class OAuth2ClientMutator implements WebTestClientConfigurer, MockServerConfigurer { private String registrationId = "test"; + @Nullable private ClientRegistration clientRegistration; private String principalName = "user"; @@ -1115,12 +1130,14 @@ public final class SecurityMockServerConfigurers { public void afterConfigureAdded(WebTestClient.MockServerSpec serverSpec) { } + @NullUnmarked @Override public void afterConfigurerAdded(WebTestClient.Builder builder, @Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) { httpHandlerBuilder.filters(addAuthorizedClientFilter()); } + @NullUnmarked private Consumer> addAuthorizedClientFilter() { OAuth2AuthorizedClient client = getClient(); return (filters) -> filters.add(0, (exchange, chain) -> { @@ -1136,6 +1153,7 @@ public final class SecurityMockServerConfigurers { }); } + @NullUnmarked private OAuth2AuthorizedClient getClient() { Assert.notNull(this.clientRegistration, "Please specify a ClientRegistration via one of the clientRegistration methods"); @@ -1163,12 +1181,14 @@ public final class SecurityMockServerConfigurers { private final ReactiveOAuth2AuthorizedClientManager delegate; + @Nullable private ServerOAuth2AuthorizedClientRepository authorizedClientRepository; TestOAuth2AuthorizedClientManager(ReactiveOAuth2AuthorizedClientManager delegate) { this.delegate = delegate; } + @NullUnmarked @Override public Mono authorize(OAuth2AuthorizeRequest authorizeRequest) { ServerWebExchange exchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName()); @@ -1183,7 +1203,8 @@ public final class SecurityMockServerConfigurers { exchange.getAttributes().put(ENABLED_ATTR_NAME, Boolean.TRUE); } - boolean isEnabled(ServerWebExchange exchange) { + @NullUnmarked + boolean isEnabled(@Nullable ServerWebExchange exchange) { return Boolean.TRUE.equals(exchange.getAttribute(ENABLED_ATTR_NAME)); } @@ -1202,7 +1223,8 @@ public final class SecurityMockServerConfigurers { private final ServerOAuth2AuthorizedClientRepository delegate; - TestOAuth2AuthorizedClientRepository(ServerOAuth2AuthorizedClientRepository delegate) { + @NullUnmarked + TestOAuth2AuthorizedClientRepository(@Nullable ServerOAuth2AuthorizedClientRepository delegate) { this.delegate = delegate; } @@ -1261,7 +1283,8 @@ public final class SecurityMockServerConfigurers { * @return the {@link ReactiveOAuth2AuthorizedClientManager} for the specified * {@link ServerWebExchange} */ - static ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository(ServerWebExchange exchange) { + static @Nullable ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository( + ServerWebExchange exchange) { ReactiveOAuth2AuthorizedClientManager manager = getOAuth2AuthorizedClientManager(exchange); if (manager == null) { return DEFAULT_CLIENT_REPO; @@ -1294,7 +1317,8 @@ public final class SecurityMockServerConfigurers { ((TestOAuth2AuthorizedClientManager) manager).authorizedClientRepository = repository; } - static ReactiveOAuth2AuthorizedClientManager getOAuth2AuthorizedClientManager(ServerWebExchange exchange) { + static @Nullable ReactiveOAuth2AuthorizedClientManager getOAuth2AuthorizedClientManager( + ServerWebExchange exchange) { OAuth2AuthorizedClientArgumentResolver resolver = findResolver(exchange, OAuth2AuthorizedClientArgumentResolver.class); if (resolver == null) { @@ -1323,7 +1347,7 @@ public final class SecurityMockServerConfigurers { } @SuppressWarnings("unchecked") - static T findResolver(ServerWebExchange exchange, + static @Nullable T findResolver(ServerWebExchange exchange, Class resolverClass) { if (!ClassUtils.isPresent( "org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter", @@ -1335,7 +1359,7 @@ public final class SecurityMockServerConfigurers { private static class WebFluxClasspathGuard { - static T findResolver(ServerWebExchange exchange, + static @Nullable T findResolver(ServerWebExchange exchange, Class resolverClass) { RequestMappingHandlerAdapter handlerAdapter = getRequestMappingHandlerAdapter(exchange); if (handlerAdapter == null) { @@ -1358,7 +1382,7 @@ public final class SecurityMockServerConfigurers { return null; } - private static RequestMappingHandlerAdapter getRequestMappingHandlerAdapter( + private static @Nullable RequestMappingHandlerAdapter getRequestMappingHandlerAdapter( ServerWebExchange exchange) { ApplicationContext context = exchange.getApplicationContext(); if (context != null) { diff --git a/test/src/main/java/org/springframework/security/test/web/reactive/server/package-info.java b/test/src/main/java/org/springframework/security/test/web/reactive/server/package-info.java new file mode 100644 index 0000000000..eb606d20c8 --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/web/reactive/server/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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 + * + * https://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. + */ + +/** + * Spring Security upport for testing Spring WebFlux server endpoints via WebTestClient. + */ +@NullMarked +package org.springframework.security.test.web.reactive.server; + +import org.jspecify.annotations.NullMarked; diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java index d60b854510..cc506881b0 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java @@ -17,6 +17,7 @@ package org.springframework.security.test.web.servlet.request; import jakarta.servlet.ServletContext; +import org.jspecify.annotations.Nullable; import org.springframework.beans.Mergeable; import org.springframework.http.MediaType; @@ -91,7 +92,7 @@ public final class SecurityMockMvcRequestBuilders { private RequestPostProcessor postProcessor = csrf(); - private Mergeable parent; + private @Nullable Mergeable parent; private LogoutRequestBuilder() { } @@ -135,7 +136,7 @@ public final class SecurityMockMvcRequestBuilders { } @Override - public Object merge(Object parent) { + public Object merge(@Nullable Object parent) { if (parent == null) { return this; } @@ -168,7 +169,7 @@ public final class SecurityMockMvcRequestBuilders { private MediaType acceptMediaType = MediaType.APPLICATION_FORM_URLENCODED; - private Mergeable parent; + private @Nullable Mergeable parent; private RequestPostProcessor postProcessor = csrf(); @@ -297,7 +298,7 @@ public final class SecurityMockMvcRequestBuilders { } @Override - public Object merge(Object parent) { + public Object merge(@Nullable Object parent) { if (parent == null) { return this; } diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java index 9e96333f5e..d6ac8d9f0b 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java @@ -40,6 +40,8 @@ import java.util.stream.Collectors; import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.core.io.DefaultResourceLoader; @@ -397,6 +399,7 @@ public final class SecurityMockMvcRequestPostProcessors { * @return the {@link OidcLoginRequestPostProcessor} for additional customization * @since 5.3 */ + @NullUnmarked public static OAuth2LoginRequestPostProcessor oauth2Login() { OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", null, null, Collections.singleton("read")); @@ -425,6 +428,7 @@ public final class SecurityMockMvcRequestPostProcessors { * @return the {@link OidcLoginRequestPostProcessor} for additional customization * @since 5.3 */ + @NullUnmarked public static OidcLoginRequestPostProcessor oidcLogin() { OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", null, null, Collections.singleton("read")); @@ -513,6 +517,7 @@ public final class SecurityMockMvcRequestPostProcessors { private CsrfRequestPostProcessor() { } + @NullUnmarked @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { CsrfTokenRepository repository = WebTestUtils.getCsrfTokenRepository(request); @@ -577,7 +582,7 @@ public final class SecurityMockMvcRequestPostProcessors { } @Override - public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { + public void saveToken(@Nullable CsrfToken token, HttpServletRequest request, HttpServletResponse response) { if (isEnabled(request)) { request.setAttribute(TOKEN_ATTR_NAME, token); } @@ -587,7 +592,7 @@ public final class SecurityMockMvcRequestPostProcessors { } @Override - public CsrfToken loadToken(HttpServletRequest request) { + public @Nullable CsrfToken loadToken(HttpServletRequest request) { if (isEnabled(request)) { return (CsrfToken) request.getAttribute(TOKEN_ATTR_NAME); } @@ -697,8 +702,9 @@ public final class SecurityMockMvcRequestPostProcessors { * @return the MD5 of the digest authentication response, encoded in hex * @throws IllegalArgumentException if the supplied qop value is unsupported. */ - private static String generateDigest(String username, String realm, String password, String httpMethod, - String uri, String qop, String nonce, String nc, String cnonce) throws IllegalArgumentException { + private static String generateDigest(String username, String realm, String password, + @Nullable String httpMethod, @Nullable String uri, String qop, String nonce, String nc, String cnonce) + throws IllegalArgumentException { String a1Md5 = encodePasswordInA1Format(username, realm, password); String a2 = httpMethod + ":" + uri; String a2Md5 = md5Hex(a2); @@ -1129,6 +1135,7 @@ public final class SecurityMockMvcRequestPostProcessors { return this; } + @NullUnmarked @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { CsrfFilter.skipRequest(request); @@ -1255,6 +1262,7 @@ public final class SecurityMockMvcRequestPostProcessors { return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt); } + @NullUnmarked private Instant getInstant(Map attributes, String name) { Object value = attributes.get(name); if (value == null) { @@ -1407,13 +1415,14 @@ public final class SecurityMockMvcRequestPostProcessors { private OAuth2AccessToken accessToken; - private OidcIdToken idToken; + private @Nullable OidcIdToken idToken; + @SuppressWarnings("NullAway.Init") private OidcUserInfo userInfo; private Supplier oidcUser = this::defaultPrincipal; - private Collection authorities; + private @Nullable Collection authorities; private OidcLoginRequestPostProcessor(OAuth2AccessToken accessToken) { this.accessToken = accessToken; @@ -1525,6 +1534,7 @@ public final class SecurityMockMvcRequestPostProcessors { return authorities; } + @NullUnmarked private OidcIdToken getOidcIdToken() { if (this.idToken != null) { return this.idToken; @@ -1546,11 +1556,12 @@ public final class SecurityMockMvcRequestPostProcessors { * @author Josh Cummings * @since 5.3 */ + @NullUnmarked public static final class OAuth2ClientRequestPostProcessor implements RequestPostProcessor { private String registrationId = "test"; - private ClientRegistration clientRegistration; + private @Nullable ClientRegistration clientRegistration; private String principalName = "user"; @@ -1610,6 +1621,7 @@ public final class SecurityMockMvcRequestPostProcessors { return this; } + @NullUnmarked @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { if (this.clientRegistration == null) { @@ -1650,14 +1662,15 @@ public final class SecurityMockMvcRequestPostProcessors { private final OAuth2AuthorizedClientManager delegate; - private OAuth2AuthorizedClientRepository authorizedClientRepository; + private @Nullable OAuth2AuthorizedClientRepository authorizedClientRepository; TestOAuth2AuthorizedClientManager(OAuth2AuthorizedClientManager delegate) { this.delegate = delegate; } + @NullUnmarked @Override - public OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest) { + public @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest) { HttpServletRequest request = authorizeRequest.getAttribute(HttpServletRequest.class.getName()); if (isEnabled(request)) { return this.authorizedClientRepository.loadAuthorizedClient( @@ -1670,7 +1683,8 @@ public final class SecurityMockMvcRequestPostProcessors { request.setAttribute(ENABLED_ATTR_NAME, Boolean.TRUE); } - boolean isEnabled(HttpServletRequest request) { + @NullUnmarked + boolean isEnabled(@Nullable HttpServletRequest request) { return Boolean.TRUE.equals(request.getAttribute(ENABLED_ATTR_NAME)); } @@ -1689,7 +1703,8 @@ public final class SecurityMockMvcRequestPostProcessors { private final OAuth2AuthorizedClientRepository delegate; - TestOAuth2AuthorizedClientRepository(OAuth2AuthorizedClientRepository delegate) { + @NullUnmarked + TestOAuth2AuthorizedClientRepository(@Nullable OAuth2AuthorizedClientRepository delegate) { this.delegate = delegate; } @@ -1748,7 +1763,8 @@ public final class SecurityMockMvcRequestPostProcessors { * @return the {@link OAuth2AuthorizedClientManager} for the specified * {@link HttpServletRequest} */ - static OAuth2AuthorizedClientRepository getAuthorizedClientRepository(HttpServletRequest request) { + static @Nullable OAuth2AuthorizedClientRepository getAuthorizedClientRepository( + HttpServletRequest request) { OAuth2AuthorizedClientManager manager = getOAuth2AuthorizedClientManager(request); if (manager == null) { return DEFAULT_CLIENT_REPO; @@ -1781,7 +1797,8 @@ public final class SecurityMockMvcRequestPostProcessors { ((TestOAuth2AuthorizedClientManager) manager).authorizedClientRepository = repository; } - static OAuth2AuthorizedClientManager getOAuth2AuthorizedClientManager(HttpServletRequest request) { + static @Nullable OAuth2AuthorizedClientManager getOAuth2AuthorizedClientManager( + HttpServletRequest request) { OAuth2AuthorizedClientArgumentResolver resolver = findResolver(request, OAuth2AuthorizedClientArgumentResolver.class); if (resolver == null) { @@ -1809,7 +1826,7 @@ public final class SecurityMockMvcRequestPostProcessors { } @SuppressWarnings("unchecked") - static T findResolver(HttpServletRequest request, + static @Nullable T findResolver(HttpServletRequest request, Class resolverClass) { if (!ClassUtils.isPresent( "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter", null)) { @@ -1820,7 +1837,7 @@ public final class SecurityMockMvcRequestPostProcessors { private static class WebMvcClasspathGuard { - static T findResolver(HttpServletRequest request, + static @Nullable T findResolver(HttpServletRequest request, Class resolverClass) { ServletContext servletContext = request.getServletContext(); RequestMappingHandlerAdapter mapping = getRequestMappingHandlerAdapter(servletContext); @@ -1839,7 +1856,7 @@ public final class SecurityMockMvcRequestPostProcessors { return null; } - private static RequestMappingHandlerAdapter getRequestMappingHandlerAdapter( + private static @Nullable RequestMappingHandlerAdapter getRequestMappingHandlerAdapter( ServletContext servletContext) { WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(servletContext); if (context != null) { diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/package-info.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/package-info.java new file mode 100644 index 0000000000..35729ffc76 --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2004-present 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 + * + * https://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. + */ + +/** + * Spring Security built-in org.springframework.test.web.servlet.RequestBuilder + * implementations. + */ +@NullMarked +package org.springframework.security.test.web.servlet.request; + +import org.jspecify.annotations.NullMarked; diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultMatchers.java b/test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultMatchers.java index 465e9ae53c..99e67893a9 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultMatchers.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultMatchers.java @@ -20,6 +20,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.function.Consumer; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.Authentication; @@ -81,21 +84,22 @@ public final class SecurityMockMvcResultMatchers { */ public static final class AuthenticatedMatcher extends AuthenticationMatcher { - private SecurityContext expectedContext; + private @Nullable SecurityContext expectedContext; - private Authentication expectedAuthentication; + private @Nullable Authentication expectedAuthentication; - private Object expectedAuthenticationPrincipal; + private @Nullable Object expectedAuthenticationPrincipal; - private String expectedAuthenticationName; + private @Nullable String expectedAuthenticationName; - private Collection expectedGrantedAuthorities; + private @Nullable Collection expectedGrantedAuthorities; - private Consumer assertAuthentication; + private @Nullable Consumer assertAuthentication; AuthenticatedMatcher() { } + @NullUnmarked @Override public void match(MvcResult result) { SecurityContext context = load(result); diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/response/package-info.java b/test/src/main/java/org/springframework/security/test/web/servlet/response/package-info.java new file mode 100644 index 0000000000..112cfd19e5 --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/web/servlet/response/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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 + * + * https://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. + */ + +/** + * Spring Security server-side support for testing Spring MVC applications. + */ +@NullMarked +package org.springframework.security.test.web.servlet.response; + +import org.jspecify.annotations.NullMarked; diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/setup/SecurityMockMvcConfigurer.java b/test/src/main/java/org/springframework/security/test/web/servlet/setup/SecurityMockMvcConfigurer.java index 6d1a466a26..83bd0bd330 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/setup/SecurityMockMvcConfigurer.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/setup/SecurityMockMvcConfigurer.java @@ -24,6 +24,7 @@ import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import org.jspecify.annotations.NullUnmarked; import org.springframework.security.config.BeanIds; import org.springframework.test.web.servlet.request.RequestPostProcessor; @@ -66,6 +67,7 @@ final class SecurityMockMvcConfigurer extends MockMvcConfigurerAdapter { builder.addFilters(this.delegateFilter); } + @NullUnmarked @Override public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder builder, WebApplicationContext context) { @@ -100,6 +102,7 @@ final class SecurityMockMvcConfigurer extends MockMvcConfigurerAdapter { */ static class DelegateFilter implements Filter { + @SuppressWarnings("NullAway.Init") private Filter delegate; DelegateFilter() { diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/setup/package-info.java b/test/src/main/java/org/springframework/security/test/web/servlet/setup/package-info.java new file mode 100644 index 0000000000..80e4fec051 --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/web/servlet/setup/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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 + * + * https://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. + */ + +/** + * Spring Security built-in MockMvcBuilder implementations. + */ +@NullMarked +package org.springframework.security.test.web.servlet.setup; + +import org.jspecify.annotations.NullMarked; diff --git a/test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java b/test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java index a4bab9127b..77a64eb246 100644 --- a/test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java +++ b/test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java @@ -21,6 +21,8 @@ import java.util.List; import jakarta.servlet.Filter; import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.security.config.BeanIds; @@ -64,6 +66,7 @@ public abstract class WebTestUtils { * @return the {@link SecurityContextRepository} for the specified * {@link HttpServletRequest} */ + @NullUnmarked public static SecurityContextRepository getSecurityContextRepository(HttpServletRequest request) { SecurityContextPersistenceFilter filter = findFilter(request, SecurityContextPersistenceFilter.class); if (filter != null) { @@ -103,7 +106,7 @@ public abstract class WebTestUtils { * @return the {@link CsrfTokenRepository} for the specified * {@link HttpServletRequest} */ - public static CsrfTokenRepository getCsrfTokenRepository(HttpServletRequest request) { + public static @Nullable CsrfTokenRepository getCsrfTokenRepository(HttpServletRequest request) { CsrfFilter filter = findFilter(request, CsrfFilter.class); if (filter == null) { return DEFAULT_TOKEN_REPO; @@ -120,7 +123,7 @@ public abstract class WebTestUtils { * @return the {@link CsrfTokenRequestHandler} for the specified * {@link HttpServletRequest} */ - public static CsrfTokenRequestHandler getCsrfTokenRequestHandler(HttpServletRequest request) { + public static @Nullable CsrfTokenRequestHandler getCsrfTokenRequestHandler(HttpServletRequest request) { CsrfFilter filter = findFilter(request, CsrfFilter.class); if (filter == null) { return DEFAULT_CSRF_HANDLER; @@ -142,7 +145,7 @@ public abstract class WebTestUtils { } @SuppressWarnings("unchecked") - static T findFilter(HttpServletRequest request, Class filterClass) { + static @Nullable T findFilter(HttpServletRequest request, Class filterClass) { ServletContext servletContext = request.getServletContext(); Filter springSecurityFilterChain = getSpringSecurityFilterChain(servletContext); if (springSecurityFilterChain == null) { @@ -160,7 +163,7 @@ public abstract class WebTestUtils { return null; } - private static Filter getSpringSecurityFilterChain(ServletContext servletContext) { + private static @Nullable Filter getSpringSecurityFilterChain(ServletContext servletContext) { Filter result = (Filter) servletContext.getAttribute(BeanIds.SPRING_SECURITY_FILTER_CHAIN); if (result != null) { return result; diff --git a/test/src/main/java/org/springframework/security/test/web/support/package-info.java b/test/src/main/java/org/springframework/security/test/web/support/package-info.java new file mode 100644 index 0000000000..46dbf3cc7c --- /dev/null +++ b/test/src/main/java/org/springframework/security/test/web/support/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2004-present 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 + * + * https://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. + */ + +/** + * Spring Security supporting the org.springframework.web.context package, such as + * WebApplicationContext implementations and various utility classes. + */ +@NullMarked +package org.springframework.security.test.web.support; + +import org.jspecify.annotations.NullMarked;