Browse Source

Enable Null checking in spring-security-test via JSpecify

Closes gh-17840
Rob Winch 1 month ago
parent
commit
6a84f96930
19 changed files with 324 additions and 44 deletions
  1. 4 0
      test/spring-security-test.gradle
  2. 3 1
      test/src/main/java/org/springframework/security/test/aot/hint/WebTestUtilsRuntimeHints.java
  3. 23 0
      test/src/main/java/org/springframework/security/test/aot/hint/package-info.java
  4. 23 0
      test/src/main/java/org/springframework/security/test/context/annotation/package-info.java
  5. 24 0
      test/src/main/java/org/springframework/security/test/context/package-info.java
  6. 14 5
      test/src/main/java/org/springframework/security/test/context/support/WithSecurityContextTestExecutionListener.java
  7. 4 1
      test/src/main/java/org/springframework/security/test/context/support/WithUserDetailsSecurityContextFactory.java
  8. 23 0
      test/src/main/java/org/springframework/security/test/context/support/package-info.java
  9. 31 7
      test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java
  10. 23 0
      test/src/main/java/org/springframework/security/test/web/reactive/server/package-info.java
  11. 5 4
      test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestBuilders.java
  12. 33 16
      test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java
  13. 24 0
      test/src/main/java/org/springframework/security/test/web/servlet/request/package-info.java
  14. 10 6
      test/src/main/java/org/springframework/security/test/web/servlet/response/SecurityMockMvcResultMatchers.java
  15. 23 0
      test/src/main/java/org/springframework/security/test/web/servlet/response/package-info.java
  16. 3 0
      test/src/main/java/org/springframework/security/test/web/servlet/setup/SecurityMockMvcConfigurer.java
  17. 23 0
      test/src/main/java/org/springframework/security/test/web/servlet/setup/package-info.java
  18. 7 4
      test/src/main/java/org/springframework/security/test/web/support/WebTestUtils.java
  19. 24 0
      test/src/main/java/org/springframework/security/test/web/support/package-info.java

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

@@ -1,3 +1,7 @@
+plugins {
+	id 'security-nullability'
+}
+
 apply plugin: 'io.spring.convention.spring-module'
 
 dependencies {

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

+ 23 - 0
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;

+ 23 - 0
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;

+ 24 - 0
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;

+ 14 - 5
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<SecurityContext> supplier = (Supplier<SecurityContext>) 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<WithSecurityContext> 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<? extends Annotation> type) {
+	@NullUnmarked
+	private @Nullable Annotation findAnnotation(AnnotatedElement annotated,
+			@Nullable Class<? extends Annotation> 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();

+ 4 - 1
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();
 		}

+ 23 - 0
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;

+ 31 - 7
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 extends WebTestClientConfigurer & MockServerConfigurer> 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<String, Object> 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> oidcUser = this::defaultPrincipal;
 
+		@Nullable
 		private Collection<GrantedAuthority> 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<List<WebFilter>> 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<OAuth2AuthorizedClient> 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 extends HandlerMethodArgumentResolver> T findResolver(ServerWebExchange exchange,
+			static <T extends HandlerMethodArgumentResolver> @Nullable T findResolver(ServerWebExchange exchange,
 					Class<T> 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 extends HandlerMethodArgumentResolver> T findResolver(ServerWebExchange exchange,
+				static <T extends HandlerMethodArgumentResolver> @Nullable T findResolver(ServerWebExchange exchange,
 						Class<T> 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) {

+ 23 - 0
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;

+ 5 - 4
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;
 			}

+ 33 - 16
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<String, Object> 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> oidcUser = this::defaultPrincipal;
 
-		private Collection<GrantedAuthority> authorities;
+		private @Nullable Collection<GrantedAuthority> 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 extends HandlerMethodArgumentResolver> T findResolver(HttpServletRequest request,
+			static <T extends HandlerMethodArgumentResolver> @Nullable T findResolver(HttpServletRequest request,
 					Class<T> 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 extends HandlerMethodArgumentResolver> T findResolver(HttpServletRequest request,
+				static <T extends HandlerMethodArgumentResolver> @Nullable T findResolver(HttpServletRequest request,
 						Class<T> 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) {

+ 24 - 0
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;

+ 10 - 6
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<AuthenticatedMatcher> {
 
-		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<? extends GrantedAuthority> expectedGrantedAuthorities;
+		private @Nullable Collection<? extends GrantedAuthority> expectedGrantedAuthorities;
 
-		private Consumer<Authentication> assertAuthentication;
+		private @Nullable Consumer<Authentication> assertAuthentication;
 
 		AuthenticatedMatcher() {
 		}
 
+		@NullUnmarked
 		@Override
 		public void match(MvcResult result) {
 			SecurityContext context = load(result);

+ 23 - 0
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;

+ 3 - 0
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() {

+ 23 - 0
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;

+ 7 - 4
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 extends Filter> T findFilter(HttpServletRequest request, Class<T> filterClass) {
+	static <T extends Filter> @Nullable T findFilter(HttpServletRequest request, Class<T> 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;

+ 24 - 0
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;