From bed6ad3c436d24f809d33a7ecc267ddb5c9db365 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 28 Mar 2025 15:58:27 +0000 Subject: [PATCH] Polish UserDetailsServiceAutoConfigurationTests Closes gh-44939 --- ...rDetailsServiceAutoConfigurationTests.java | 195 +++++++++++------- 1 file changed, 119 insertions(+), 76 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java index 6de9275db26..4cef5fc3b00 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -18,17 +18,27 @@ package org.springframework.boot.autoconfigure.security.servlet; import java.util.Collections; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport; +import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -66,7 +76,8 @@ class UserDetailsServiceAutoConfigurationTests { @Test void testDefaultUsernamePassword(CapturedOutput output) { - this.contextRunner.with(noOtherFormsOfAuthenticationOnTheClasspath()).run((context) -> { + this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent()).run((context) -> { + assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue(); UserDetailsService manager = context.getBean(UserDetailsService.class); assertThat(output).contains("Using generated security password:"); assertThat(manager.loadUserByUsername("user")).isNotNull(); @@ -75,60 +86,68 @@ class UserDetailsServiceAutoConfigurationTests { @Test void defaultUserNotCreatedIfAuthenticationManagerBeanPresent(CapturedOutput output) { - this.contextRunner.withUserConfiguration(TestAuthenticationManagerConfiguration.class).run((context) -> { - AuthenticationManager manager = context.getBean(AuthenticationManager.class); - assertThat(manager) - .isEqualTo(context.getBean(TestAuthenticationManagerConfiguration.class).authenticationManager); - assertThat(output).doesNotContain("Using generated security password: "); - TestingAuthenticationToken token = new TestingAuthenticationToken("foo", "bar"); - assertThat(manager.authenticate(token)).isNotNull(); - }); + this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent()) + .withUserConfiguration(TestAuthenticationManagerConfiguration.class) + .run((context) -> { + assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue(); + AuthenticationManager manager = context.getBean(AuthenticationManager.class); + assertThat(manager) + .isEqualTo(context.getBean(TestAuthenticationManagerConfiguration.class).authenticationManager); + assertThat(output).doesNotContain("Using generated security password: "); + TestingAuthenticationToken token = new TestingAuthenticationToken("foo", "bar"); + assertThat(manager.authenticate(token)).isNotNull(); + }); } @Test void defaultUserNotCreatedIfAuthenticationManagerResolverBeanPresent(CapturedOutput output) { - this.contextRunner.withUserConfiguration(TestAuthenticationManagerResolverConfiguration.class) - .run((context) -> assertThat(output).doesNotContain("Using generated security password: ")); + this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent()) + .withUserConfiguration(TestAuthenticationManagerResolverConfiguration.class) + .run((context) -> { + assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue(); + assertThat(output).doesNotContain("Using generated security password: "); + }); } @Test void defaultUserNotCreatedIfUserDetailsServiceBeanPresent(CapturedOutput output) { - this.contextRunner.withUserConfiguration(TestUserDetailsServiceConfiguration.class).run((context) -> { - UserDetailsService userDetailsService = context.getBean(UserDetailsService.class); - assertThat(output).doesNotContain("Using generated security password: "); - assertThat(userDetailsService.loadUserByUsername("foo")).isNotNull(); - }); + this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent()) + .withUserConfiguration(TestUserDetailsServiceConfiguration.class) + .run((context) -> { + assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue(); + UserDetailsService userDetailsService = context.getBean(UserDetailsService.class); + assertThat(output).doesNotContain("Using generated security password: "); + assertThat(userDetailsService.loadUserByUsername("foo")).isNotNull(); + }); } @Test void defaultUserNotCreatedIfAuthenticationProviderBeanPresent(CapturedOutput output) { - this.contextRunner.withUserConfiguration(TestAuthenticationProviderConfiguration.class).run((context) -> { - AuthenticationProvider provider = context.getBean(AuthenticationProvider.class); - assertThat(output).doesNotContain("Using generated security password: "); - TestingAuthenticationToken token = new TestingAuthenticationToken("foo", "bar"); - assertThat(provider.authenticate(token)).isNotNull(); - }); - } - - @Test - void defaultUserNotCreatedIfResourceServerWithOpaqueIsUsed() { - this.contextRunner.withUserConfiguration(TestConfigWithIntrospectionClient.class).run((context) -> { - assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class); - assertThat(context).doesNotHaveBean(UserDetailsService.class); - }); + this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent()) + .withUserConfiguration(TestAuthenticationProviderConfiguration.class) + .run((context) -> { + assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue(); + AuthenticationProvider provider = context.getBean(AuthenticationProvider.class); + assertThat(output).doesNotContain("Using generated security password: "); + TestingAuthenticationToken token = new TestingAuthenticationToken("foo", "bar"); + assertThat(provider.authenticate(token)).isNotNull(); + }); } @Test - void defaultUserNotCreatedIfResourceServerWithJWTIsUsed() { - this.contextRunner.withUserConfiguration(TestConfigWithJwtDecoder.class).run((context) -> { - assertThat(context).hasSingleBean(JwtDecoder.class); - assertThat(context).doesNotHaveBean(UserDetailsService.class); - }); + void defaultUserNotCreatedIfJwtDecoderBeanPresent() { + this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent()) + .withUserConfiguration(TestConfigWithJwtDecoder.class) + .run((context) -> { + assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue(); + assertThat(context).hasSingleBean(JwtDecoder.class); + assertThat(context).doesNotHaveBean(UserDetailsService.class); + }); } @Test void userDetailsServiceWhenPasswordEncoderAbsentAndDefaultPassword() { - this.contextRunner.with(noOtherFormsOfAuthenticationOnTheClasspath()) + this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent()) .withUserConfiguration(TestSecurityConfiguration.class) .run(((context) -> { InMemoryUserDetailsManager userDetailsService = context.getBean(InMemoryUserDetailsManager.class); @@ -153,56 +172,33 @@ class UserDetailsServiceAutoConfigurationTests { testPasswordEncoding(TestConfigWithPasswordEncoder.class, "secret", "secret"); } - @Test - void userDetailsServiceWhenClientRegistrationRepositoryPresent() { - this.contextRunner - .withClassLoader( - new FilteredClassLoader(OpaqueTokenIntrospector.class, RelyingPartyRegistrationRepository.class)) - .run(((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class))); + @ParameterizedTest + @EnumSource + void whenClassOfAlternativeIsPresentUserDetailsServiceBacksOff(AlternativeFormOfAuthentication alternative) { + this.contextRunner.with(alternative.present()) + .run((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class)); } - @Test - void userDetailsServiceWhenOpaqueTokenIntrospectorPresent() { - this.contextRunner - .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, - RelyingPartyRegistrationRepository.class)) - .run(((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class))); - } - - @Test - void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresent() { - this.contextRunner - .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class)) - .run(((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class))); - } - - @Test - void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresentAndUsernameConfigured() { - this.contextRunner - .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class)) + @ParameterizedTest + @EnumSource + void whenAlternativeIsPresentAndUsernameIsConfiguredThenUserDetailsServiceIsAutoConfigured( + AlternativeFormOfAuthentication alternative) { + this.contextRunner.with(alternative.present()) .withPropertyValues("spring.security.user.name=alice") .run(((context) -> assertThat(context).hasSingleBean(InMemoryUserDetailsManager.class))); } - @Test - void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresentAndPasswordConfigured() { - this.contextRunner - .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class)) + @ParameterizedTest + @EnumSource + void whenAlternativeIsPresentAndPasswordIsConfiguredThenUserDetailsServiceIsAutoConfigured( + AlternativeFormOfAuthentication alternative) { + this.contextRunner.with(alternative.present()) .withPropertyValues("spring.security.user.password=secret") .run(((context) -> assertThat(context).hasSingleBean(InMemoryUserDetailsManager.class))); } - private Function noOtherFormsOfAuthenticationOnTheClasspath() { - return (contextRunner) -> contextRunner - .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class, - RelyingPartyRegistrationRepository.class)); - } - private void testPasswordEncoding(Class configClass, String providedPassword, String expectedPassword) { - this.contextRunner.with(noOtherFormsOfAuthenticationOnTheClasspath()) - .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class, - RelyingPartyRegistrationRepository.class)) - .withUserConfiguration(configClass) + this.contextRunner.withUserConfiguration(configClass) .withPropertyValues("spring.security.user.password=" + providedPassword) .run(((context) -> { InMemoryUserDetailsManager userDetailsService = context.getBean(InMemoryUserDetailsManager.class); @@ -211,6 +207,18 @@ class UserDetailsServiceAutoConfigurationTests { })); } + private ConditionOutcome outcomeOfMissingAlternativeCondition(ConfigurableApplicationContext context) { + ConditionAndOutcomes conditionAndOutcomes = ConditionEvaluationReport.get(context.getBeanFactory()) + .getConditionAndOutcomesBySource() + .get(UserDetailsServiceAutoConfiguration.class.getName()); + for (ConditionAndOutcome conditionAndOutcome : conditionAndOutcomes) { + if (conditionAndOutcome.getCondition() instanceof MissingAlternativeOrUserPropertiesConfigured) { + return conditionAndOutcome.getOutcome(); + } + } + return null; + } + @Configuration(proxyBeanMethods = false) static class TestAuthenticationManagerConfiguration { @@ -306,4 +314,39 @@ class UserDetailsServiceAutoConfigurationTests { } + private enum AlternativeFormOfAuthentication { + + CLIENT_REGISTRATION_REPOSITORY(ClientRegistrationRepository.class), + + OPAQUE_TOKEN_INTROSPECTOR(OpaqueTokenIntrospector.class), + + RELYING_PARTY_REGISTRATION_REPOSITORY(RelyingPartyRegistrationRepository.class); + + private final Class type; + + AlternativeFormOfAuthentication(Class type) { + this.type = type; + } + + private Class getType() { + return this.type; + } + + private Function present() { + return (contextRunner) -> contextRunner + .withClassLoader(new FilteredClassLoader(Stream.of(AlternativeFormOfAuthentication.values()) + .filter(Predicate.not(this::equals)) + .map(AlternativeFormOfAuthentication::getType) + .toArray(Class[]::new))); + } + + private static Function nonPresent() { + return (contextRunner) -> contextRunner + .withClassLoader(new FilteredClassLoader(Stream.of(AlternativeFormOfAuthentication.values()) + .map(AlternativeFormOfAuthentication::getType) + .toArray(Class[]::new))); + } + + } + }